1use std::path::PathBuf;
4
5use sva_core::{Asked, CliError};
6
7mod reading;
8mod solve;
9
10use reading::{analyze_args, render_args};
11pub use solve::SolveArgs;
12use solve::solve_args;
13
14#[derive(Debug, PartialEq)]
15pub struct RenderArgs {
16 pub target: String,
18 pub until: Option<String>,
19 pub rate: Option<u32>,
20 pub bits: Option<i32>,
22 pub threads: Option<std::num::NonZeroUsize>,
24 pub asked: Vec<Asked>,
25 pub confirm: bool,
27 pub cache: CacheAt,
28 pub sets: Vec<(String, String)>,
30}
31
32#[derive(Clone, Debug, Default, PartialEq, Eq)]
34pub enum CacheAt {
35 #[default]
36 Platform,
37 Path(PathBuf),
38 Off,
39}
40
41#[derive(Debug, PartialEq)]
42pub struct AnalyzeArgs {
43 pub path: PathBuf,
44 pub asked: Vec<Asked>,
45 pub confirm: bool,
47}
48
49#[derive(Debug, PartialEq)]
50pub enum Command {
51 Version,
52 Help,
53 Render(Box<RenderArgs>),
54 Analyze(Box<AnalyzeArgs>),
55 Solve(Box<SolveArgs>),
56 Lint {
58 target: Option<String>,
59 format: Format,
60 body_chars: usize,
61 },
62 Trace {
63 target: String,
64 },
65 Builtins,
66 Outline {
68 text: String,
69 },
70 New {
71 name: String,
72 idempotency_key: Option<String>,
75 },
76}
77
78#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
80pub enum Format {
81 #[default]
82 Json,
83 Text,
84}
85
86const STANDARD_VERBS: [(&str, &str); 4] = [
87 ("validate", "lint"),
88 ("list", "builtins"),
89 ("create", "new"),
90 ("show", "trace"),
91];
92
93fn canonical(subcommand: &str) -> &str {
94 STANDARD_VERBS
95 .iter()
96 .find(|(alias, _)| *alias == subcommand)
97 .map_or(subcommand, |(_, name)| *name)
98}
99
100pub(crate) fn value<'a>(
103 it: &mut impl Iterator<Item = &'a String>,
104 name: &str,
105) -> Result<String, CliError> {
106 it.next()
107 .cloned()
108 .ok_or_else(|| CliError::usage("cli.missing_value", format!("{name} needs a value")))
109}
110
111pub fn parse_args(argv: &[String]) -> Result<Command, CliError> {
113 if argv.iter().any(|a| a == "--version" || a == "-V") {
114 return Ok(Command::Version);
115 }
116 if argv.iter().any(|a| a == "--help") {
117 return Ok(Command::Help);
118 }
119
120 let mut it = argv.iter();
121 let subcommand = canonical(
122 it.next()
123 .ok_or_else(|| CliError::usage("cli.missing_subcommand", "no subcommand given"))?,
124 );
125 let rest = it.as_slice();
126 match subcommand {
127 "render" => render_args(rest),
128 "analyze" => analyze_args(rest),
129 "solve" => solve_args(rest),
130 "lint" => lint_args(rest),
131 "trace" => trace_args(rest),
132 "builtins" => builtins_args(rest),
133 "outline" => outline_args(rest),
134 "new" => new_args(rest),
135 other => Err(CliError::usage(
136 "cli.unknown_subcommand",
137 format!("unknown subcommand `{other}`"),
138 )),
139 }
140}
141
142fn builtins_args(rest: &[String]) -> Result<Command, CliError> {
143 match rest.first() {
144 None => Ok(Command::Builtins),
145 Some(extra) => Err(CliError::usage(
146 "cli.unknown_argument",
147 format!("builtins takes no arguments, not `{extra}`"),
148 )),
149 }
150}
151
152fn outline_args(rest: &[String]) -> Result<Command, CliError> {
153 match rest {
154 [text] => Ok(Command::Outline { text: text.clone() }),
155 _ => Err(CliError::usage(
156 "cli.missing_argument",
157 "outline takes one <expression>, quoted as one argument",
158 )),
159 }
160}
161
162fn new_args(rest: &[String]) -> Result<Command, CliError> {
163 match rest {
164 [name] if !name.starts_with("--") => Ok(Command::New {
165 name: name.clone(),
166 idempotency_key: None,
167 }),
168 [name, flag, key] if !name.starts_with("--") && flag == "--idempotency-key" => {
169 Ok(Command::New {
170 name: name.clone(),
171 idempotency_key: Some(key.clone()),
172 })
173 }
174 [] => Err(CliError::usage("cli.missing_argument", "missing <name>")),
175 _ => Err(CliError::usage(
176 "cli.unknown_argument",
177 "new takes one <name> and an optional `--idempotency-key <key>`",
178 )),
179 }
180}
181
182fn lint_args(rest: &[String]) -> Result<Command, CliError> {
183 let mut it = rest.iter().peekable();
184 let target = match it.peek() {
185 Some(a) if !a.starts_with("--") => it.next().cloned(),
186 _ => None,
187 };
188 let mut format = Format::default();
189 let mut body_chars = crate::lint::BODY_CHARS;
190 while let Some(flag) = it.next() {
191 match flag.as_str() {
192 "--body-chars" => {
193 let text = it.next().map(String::as_str).unwrap_or("nothing");
194 body_chars = text.parse().map_err(|_| {
195 CliError::usage(
196 "cli.bad_value",
197 format!("`--body-chars` takes a whole number of characters, not `{text}`"),
198 )
199 })?;
200 }
201 "--format" => {
202 format = match it.next().map(String::as_str) {
203 Some("json") => Format::Json,
204 Some("text") => Format::Text,
205 other => {
206 return Err(CliError::usage(
207 "cli.bad_value",
208 format!(
209 "`--format` takes json or text, not `{}`",
210 other.unwrap_or("nothing")
211 ),
212 ));
213 }
214 };
215 }
216 extra => {
217 return Err(CliError::usage(
218 "cli.unknown_argument",
219 format!(
220 "lint takes only ['<expression>'], `--format <json|text>` and \
221 `--body-chars <n>`, not `{extra}`"
222 ),
223 ));
224 }
225 }
226 }
227 Ok(Command::Lint {
228 target,
229 format,
230 body_chars,
231 })
232}
233
234fn trace_args(rest: &[String]) -> Result<Command, CliError> {
236 match rest {
237 [target] if !target.starts_with("--") => Ok(Command::Trace {
238 target: target.clone(),
239 }),
240 [] => Err(CliError::usage(
241 "cli.missing_argument",
242 "missing <node|expression>",
243 )),
244 _ => Err(CliError::usage(
245 "cli.unknown_argument",
246 "trace takes one <node|expression> and nothing else",
247 )),
248 }
249}
250
251#[cfg(test)]
252mod tests {
253 use super::*;
254 use std::path::Path;
255
256 fn argv(parts: &[&str]) -> Vec<String> {
257 parts.iter().map(|s| (*s).to_string()).collect()
258 }
259
260 fn rendered(parts: &[&str]) -> RenderArgs {
261 match parse_args(&argv(parts)) {
262 Ok(Command::Render(args)) => *args,
263 other => panic!("expected a render, got {other:?}"),
264 }
265 }
266
267 fn refused(parts: &[&str]) -> String {
268 match parse_args(&argv(parts)) {
269 Err(CliError::Usage { message, .. }) => message,
270 other => panic!("expected a usage refusal, got {other:?}"),
271 }
272 }
273
274 #[test]
277 fn each_argument_mistake_answers_its_own_code_naming_the_text() {
278 let render = |tail: &[&'static str]| [&["render", "@a"][..], tail].concat();
279 let cases: [(Vec<&str>, &str, &str); 14] = [
280 (
281 render(&["--representation", "spectre"]),
282 "query.unknown_representation",
283 "`spectre`",
284 ),
285 (
286 render(&["--representation", "lines(depth=2)"]),
287 "query.unknown_argument",
288 "`depth`",
289 ),
290 (
291 render(&["--representation", "ledger(depth=many)"]),
292 "query.bad_value",
293 "`many`",
294 ),
295 (
296 render(&["--representation", "lines,lines"]),
297 "query.duplicate",
298 "`lines`",
299 ),
300 (
301 render(&["--representation", "bindings"]),
302 "query.missing_argument",
303 "`bindings`",
304 ),
305 (
306 render(&["--representation", "spectrum(peaks=8"]),
307 "query.malformed",
308 "`spectrum(peaks=8`",
309 ),
310 (
311 render(&["--representation", "exact-envelope"]),
312 "query.retired",
313 "`exact-envelope`",
314 ),
315 (
316 render(&["--representation", "a=x,b=x"]),
317 "query.unknown_representation",
318 "`a`",
319 ),
320 (
321 render(&["--representation", "lines=x.json,atoms=./x.json"]),
322 "query.shared_destination",
323 "x.json",
324 ),
325 (
326 render(&["--representation", "lines", "--bogus"]),
327 "cli.unknown_argument",
328 "`--bogus`",
329 ),
330 (
331 render(&["--representation"]),
332 "cli.missing_value",
333 "--representation",
334 ),
335 (
336 render(&["--representation", "lines", "--rate", "0"]),
337 "cli.bad_value",
338 "`0`",
339 ),
340 (vec!["bench"], "cli.unknown_subcommand", "`bench`"),
341 (
342 vec!["analyze", "/tmp/a.wav", "--representation", "ledger"],
343 "query.not_analyzable",
344 "`ledger`",
345 ),
346 ];
347 for (parts, code, text) in cases {
348 let Err(CliError::Usage {
349 code: held,
350 message,
351 }) = parse_args(&argv(&parts))
352 else {
353 panic!("{parts:?} refuses as an argument mistake");
354 };
355 assert_eq!(held, code, "{parts:?}: {message}");
356 assert!(message.contains(text), "{parts:?}: {message}");
357 assert!(!message.contains("usage:"), "{parts:?}: {message}");
358 }
359 }
360
361 #[test]
362 fn version_and_help_are_recognized_anywhere_in_argv() {
363 assert_eq!(parse_args(&argv(&["--version"])).unwrap(), Command::Version);
364 assert_eq!(parse_args(&argv(&["-V"])).unwrap(), Command::Version);
365 assert_eq!(
366 parse_args(&argv(&["lint", "--help"])).unwrap(),
367 Command::Help
368 );
369 }
370
371 #[test]
372 fn each_standard_verb_alias_reaches_the_subcommand_it_names() {
373 assert_eq!(
374 parse_args(&argv(&["validate"])).unwrap(),
375 Command::Lint {
376 target: None,
377 format: Format::Json,
378 body_chars: crate::lint::BODY_CHARS,
379 }
380 );
381 assert_eq!(parse_args(&argv(&["list"])).unwrap(), Command::Builtins);
382 assert_eq!(
383 parse_args(&argv(&["show", "kick"])).unwrap(),
384 Command::Trace {
385 target: "kick".to_string(),
386 }
387 );
388 assert_eq!(
389 parse_args(&argv(&["create", "song1"])).unwrap(),
390 Command::New {
391 name: "song1".to_string(),
392 idempotency_key: None,
393 }
394 );
395 }
396
397 #[test]
400 fn a_render_takes_a_target_readings_with_their_arguments_and_flags() {
401 let args = rendered(&[
402 "render",
403 "@piano([0, 2b], f0=C4)",
404 "--representation",
405 "samples=/tmp/out.wav, spectrum(peaks=8, frame=50ms)",
406 "--representation",
407 "ledger(depth=2, brief=1)=/tmp/ledger.json,bindings(node=@voice)",
408 "--rate",
409 "48000",
410 "--bits",
411 "16",
412 "--until",
413 "t > 1s",
414 "--threads",
415 "3",
416 ]);
417 assert_eq!(args.target, "@piano([0, 2b], f0=C4)");
418 assert_eq!(args.until.as_deref(), Some("t > 1s"));
419 assert_eq!(args.rate, Some(48_000));
420 assert_eq!(args.bits, Some(16));
421 assert_eq!(args.threads.map(std::num::NonZeroUsize::get), Some(3));
422 let names: Vec<&str> = args.asked.iter().map(|a| a.name.as_str()).collect();
423 assert_eq!(names, ["samples", "spectrum", "ledger", "bindings"]);
424 assert_eq!(args.asked[0].path(), Some(Path::new("/tmp/out.wav")));
425 assert_eq!(
426 args.asked[1]
427 .representation(sva_engine::DEFAULT_SAMPLE_RATE, None)
428 .unwrap(),
429 sva_engine::Representation::Spectrum {
430 max_peaks: 8,
431 frame_secs: Some(0.05)
432 }
433 );
434 assert_eq!(
435 args.asked[2]
436 .representation(sva_engine::DEFAULT_SAMPLE_RATE, None)
437 .unwrap(),
438 sva_engine::Representation::Ledger { depth: 2 }
439 );
440 assert!(args.asked[2].brief);
441 assert_eq!(args.asked[2].path(), Some(Path::new("/tmp/ledger.json")));
442 assert_eq!(args.asked[3].node.as_deref(), Some("voice"));
443 }
444
445 #[test]
446 fn a_render_on_no_threads_is_refused() {
447 let message = refused(&[
448 "render",
449 "1",
450 "--representation",
451 "envelope",
452 "--threads",
453 "0",
454 ]);
455 assert!(message.contains("--threads"), "{message}");
456 }
457
458 #[test]
459 fn a_target_may_open_with_a_minus() {
460 let args = rendered(&["render", "-1*sin(2*pi*220*t)", "--representation", "lines"]);
461 assert_eq!(args.target, "-1*sin(2*pi*220*t)");
462 }
463
464 #[test]
465 fn a_render_with_no_target_or_no_reading_refuses() {
466 refused(&["render"]);
467 refused(&["render", "--representation", "lines"]);
468 refused(&["render", "@a"]);
469 }
470
471 #[test]
473 fn a_retired_flag_refuses_by_name() {
474 for gone in [
475 "-c",
476 "--in",
477 "--from",
478 "--to",
479 "--max",
480 "--sample-rate",
481 "--as",
482 "--node",
483 "--frame",
484 "--depth",
485 "--peaks",
486 "--oversample",
487 "--brief",
488 "--skim",
489 "--pcm16",
490 ] {
491 let message = refused(&["render", "@a", "--representation", "lines", gone, "x"]);
492 assert!(message.contains(gone), "{gone}: {message}");
493 }
494 }
495
496 #[test]
497 fn an_argument_its_reading_does_not_take_or_a_malformed_one_refuses() {
498 for reading in [
499 "lines(depth=2)",
500 "ledger(colour=red)",
501 "ledger(depth)",
502 "ledger(depth=many)",
503 "ledger(brief=yes)",
504 "samples(bits=16)",
505 "spectrum(peaks=8",
506 "bindings(node=voice)",
507 ] {
508 refused(&["render", "@a", "--representation", reading]);
509 }
510 }
511
512 #[test]
514 fn a_reading_asked_twice_refuses_by_name() {
515 for written in [
516 &["--representation", "spectrum(peaks=4),spectrum(frame=20ms)"][..],
517 &["--representation", "lines", "--representation", "lines"][..],
518 &["--representation", "samples=/tmp/a.wav,samples"][..],
519 ] {
520 let message = refused(&[&["render", "@a"][..], written].concat());
521 assert!(message.contains("asked more than once"), "{message}");
522 }
523 let message = refused(&[
524 "analyze",
525 "/tmp/a.wav",
526 "--representation",
527 "pitch,pitch(peaks=2)",
528 ]);
529 assert!(message.contains("`pitch`"), "{message}");
530 }
531
532 #[test]
533 fn a_representation_that_left_the_language_names_what_replaced_it() {
534 for (gone, write) in sva_core::RETIRED {
535 let message = refused(&["render", "@a", "--representation", gone]);
536 assert!(message.contains(write), "{message}");
537 }
538 }
539
540 #[test]
541 fn a_wav_destination_takes_samples_and_nothing_else() {
542 refused(&["render", "@a", "--representation", "ledger=/tmp/out.wav"]);
543 }
544
545 #[test]
548 fn a_device_destination_takes_samples_and_a_buffer_beside_every_other_flag() {
549 use sva_core::Destination;
550 let row = sva_engine::Representation::Samples.reading();
551 assert_eq!(row.takes, [sva_engine::Arg::Buffer]);
552 assert_eq!(sva_engine::Arg::Buffer.key(), "buffer");
553 let args = rendered(&["render", "@a", "--representation", "samples=alsa:default"]);
554 assert_eq!(
555 args.asked[0].dest,
556 Some(Destination::Alsa {
557 device: "default".to_string(),
558 buffer_secs: sva_engine::DEFAULT_BUFFER_SECS,
559 })
560 );
561 assert_eq!(args.asked[0].path(), None);
562 let args = rendered(&[
563 "render",
564 "@a",
565 "--representation",
566 "samples(buffer=20ms)=alsa:hw:1,0,loudness",
567 "--until",
568 "t > 1s",
569 "--rate",
570 "44100",
571 ]);
572 let names: Vec<&str> = args.asked.iter().map(|a| a.name.as_str()).collect();
573 assert_eq!(names, ["samples", "loudness"]);
574 assert_eq!(
575 args.asked[0].dest,
576 Some(Destination::Alsa {
577 device: "hw:1,0".to_string(),
578 buffer_secs: 0.02,
579 }),
580 "a comma inside a device name stays in it where no reading follows"
581 );
582 assert_eq!(args.asked[1].dest, None);
583 assert_eq!(args.until.as_deref(), Some("t > 1s"));
584 assert_eq!(args.rate, Some(44_100));
585 let args = rendered(&[
586 "render",
587 "@a",
588 "--representation",
589 "lines,samples=alsa:plughw:CARD=PCH,DEV=0",
590 ]);
591 assert_eq!(
592 args.asked[1].dest,
593 Some(Destination::Alsa {
594 device: "plughw:CARD=PCH,DEV=0".to_string(),
595 buffer_secs: sva_engine::DEFAULT_BUFFER_SECS,
596 })
597 );
598 }
599
600 #[test]
601 fn a_device_refuses_what_it_cannot_play_naming_the_text() {
602 let render = |r: &'static str| vec!["render", "@a", "--representation", r];
603 let cases: [(Vec<&str>, &str, &str); 9] = [
604 (
605 render("samples=alsa:hw:0,samples=alsa:hw:1"),
606 "query.duplicate",
607 "`samples`",
608 ),
609 (
610 render("samples=alsa:"),
611 "destination.no_device",
612 "`samples=alsa:`",
613 ),
614 (
615 render("loudness=alsa:default"),
616 "destination.not_audio",
617 "`loudness`",
618 ),
619 (
620 render("samples(buffer=50ms)"),
621 "query.buffer_without_device",
622 "`samples`",
623 ),
624 (
625 render("samples(buffer=50ms)=/tmp/a.wav"),
626 "query.buffer_without_device",
627 "`buffer`",
628 ),
629 (
630 render("samples(buffer=0)=alsa:default"),
631 "query.bad_value",
632 "`0`",
633 ),
634 (
635 render("samples(buffer=5000s)=alsa:default"),
636 "query.bad_value",
637 "`5000s`",
638 ),
639 (
640 render("lines(buffer=1)"),
641 "query.unknown_argument",
642 "`buffer`",
643 ),
644 (
645 vec![
646 "analyze",
647 "/tmp/a.wav",
648 "--representation",
649 "samples=alsa:hw:1",
650 ],
651 "destination.needs_render",
652 "`alsa:hw:1`",
653 ),
654 ];
655 for (parts, code, text) in cases {
656 let Err(CliError::Usage {
657 code: held,
658 message,
659 }) = parse_args(&argv(&parts))
660 else {
661 panic!("{parts:?} refuses as an argument mistake");
662 };
663 assert_eq!(held, code, "{parts:?}: {message}");
664 assert!(message.contains(text), "{parts:?}: {message}");
665 }
666 }
667
668 #[cfg(not(target_os = "linux"))]
669 #[test]
670 fn off_linux_a_device_refuses_at_parse_as_an_unsupported_platform() {
671 let parts = argv(&["render", "@a", "--representation", "samples=alsa:default"]);
672 let Err(CliError::Usage { code, .. }) = parse_args(&parts) else {
673 panic!("refused");
674 };
675 assert_eq!(code, "destination.unsupported_platform");
676 }
677
678 #[test]
679 fn a_harmonic_reading_takes_its_floor_in_decibels_and_its_norm_by_name() {
680 let args = rendered(&[
681 "render",
682 "@a",
683 "--representation",
684 "voice_leading(frame=10ms, floor=-60db, norm=l2),dissonance",
685 ]);
686 assert_eq!(
687 args.asked[0]
688 .representation(sva_engine::DEFAULT_SAMPLE_RATE, None)
689 .unwrap(),
690 sva_engine::Representation::Harmony {
691 reading: sva_engine::Harmonic::VoiceLeading {
692 norm: sva_engine::harmony::voice_leading::Norm::L2
693 },
694 frame_secs: 0.01,
695 floor_db: -60.0,
696 }
697 );
698 assert_eq!(
699 args.asked[1]
700 .representation(sva_engine::DEFAULT_SAMPLE_RATE, None)
701 .unwrap(),
702 sva_engine::Representation::Harmony {
703 reading: sva_engine::Harmonic::Dissonance {
704 max_pairs: sva_engine::DEFAULT_MAX_PAIRS
705 },
706 frame_secs: sva_engine::DEFAULT_FRAME_SECS,
707 floor_db: sva_engine::DEFAULT_FLOOR_DB,
708 }
709 );
710 let render = |r: &'static str| vec!["render", "@a", "--representation", r];
711 for (parts, text) in [
712 (render("dissonance(floor=-60)"), "`-60`"),
713 (render("voice_leading(norm=l3)"), "`l3`"),
714 (render("dissonance(norm=l1)"), "`norm`"),
715 ] {
716 let Err(CliError::Usage { message, .. }) = parse_args(&argv(&parts)) else {
717 panic!("{parts:?} refuses as an argument mistake");
718 };
719 assert!(message.contains(text), "{parts:?}: {message}");
720 }
721 }
722
723 #[test]
724 fn inharmonicity_takes_the_harmonic_frame_and_floor() {
725 let args = rendered(&[
726 "render",
727 "@a",
728 "--representation",
729 "inharmonicity(frame=10ms, floor=-60db)",
730 ]);
731 assert_eq!(
732 args.asked[0]
733 .representation(sva_engine::DEFAULT_SAMPLE_RATE, None)
734 .unwrap(),
735 sva_engine::Representation::Harmony {
736 reading: sva_engine::Harmonic::Inharmonicity,
737 frame_secs: 0.01,
738 floor_db: -60.0,
739 }
740 );
741 }
742
743 #[test]
744 fn periodicity_takes_its_step_window_and_curve() {
745 let args = rendered(&[
746 "render",
747 "@a",
748 "--representation",
749 "periodicity(frame=0.5s, window=8s, curve=1)",
750 ]);
751 assert_eq!(
752 args.asked[0]
753 .representation(sva_engine::DEFAULT_SAMPLE_RATE, None)
754 .unwrap(),
755 sva_engine::Representation::Periodicity {
756 frame_secs: 0.5,
757 window_secs: 8.0,
758 curve: true,
759 }
760 );
761 }
762
763 #[test]
764 fn modulation_takes_its_step_window_and_bpm() {
765 let args = rendered(&[
766 "render",
767 "@a",
768 "--representation",
769 "modulation(frame=0.25s, window=6s, bpm=140)",
770 ]);
771 assert_eq!(
772 args.asked[0]
773 .representation(sva_engine::DEFAULT_SAMPLE_RATE, None)
774 .unwrap(),
775 sva_engine::Representation::Modulation {
776 frame_secs: 0.25,
777 window_secs: 6.0,
778 bpm: Some(140.0),
779 }
780 );
781 refused(&["render", "@a", "--representation", "modulation(bpm=0)"]);
782 refused(&["render", "@a", "--representation", "modulation(bpm=2s)"]);
783 }
784
785 #[test]
786 fn self_similarity_takes_its_cell_smooth_window_and_listing() {
787 let args = rendered(&[
788 "render",
789 "@a",
790 "--representation",
791 "self_similarity(frame=0.1s, cell=1s, smooth=3s, window=12s, floor=-50db, \
792 peaks=4, curve=1)",
793 ]);
794 assert_eq!(
795 args.asked[0]
796 .representation(sva_engine::DEFAULT_SAMPLE_RATE, None)
797 .unwrap(),
798 sva_engine::Representation::SelfSimilarity {
799 frame_secs: 0.1,
800 cell_secs: 1.0,
801 smooth_secs: 3.0,
802 window_secs: 12.0,
803 floor_db: -50.0,
804 listed: 4,
805 curve: true,
806 }
807 );
808 refused(&[
809 "render",
810 "@a",
811 "--representation",
812 "self_similarity(cell=0)",
813 ]);
814 refused(&[
815 "render",
816 "@a",
817 "--representation",
818 "self_similarity(smooth=x)",
819 ]);
820 }
821
822 #[test]
823 fn bindings_needs_a_node() {
824 refused(&["render", "@a", "--representation", "bindings"]);
825 }
826
827 #[test]
828 fn lint_takes_an_optional_target_and_nothing_else() {
829 assert_eq!(
830 parse_args(&argv(&["lint", "@drums/kick"])).unwrap(),
831 Command::Lint {
832 target: Some("@drums/kick".to_string()),
833 format: Format::Json,
834 body_chars: crate::lint::BODY_CHARS,
835 }
836 );
837 refused(&["lint", "a", "b"]);
838 refused(&["lint", "--in", "x"]);
839 }
840
841 #[test]
842 fn new_takes_a_name_and_an_optional_idempotency_key() {
843 assert_eq!(
844 parse_args(&argv(&["new", "song1", "--idempotency-key", "k"])).unwrap(),
845 Command::New {
846 name: "song1".to_string(),
847 idempotency_key: Some("k".to_string()),
848 }
849 );
850 refused(&["new"]);
851 }
852
853 #[test]
854 fn builtins_takes_no_arguments() {
855 assert_eq!(parse_args(&argv(&["builtins"])).unwrap(), Command::Builtins);
856 refused(&["builtins", "x"]);
857 }
858
859 #[test]
860 fn a_subcommand_this_cli_does_not_answer_refuses_by_name() {
861 for gone in ["bench", "nonlinear-solve"] {
862 assert!(refused(&[gone]).contains(gone));
863 }
864 }
865}