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