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 },
61 Trace {
62 target: String,
63 },
64 Builtins,
65 Outline {
67 text: String,
68 },
69 New {
70 name: String,
71 idempotency_key: Option<String>,
74 },
75}
76
77#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
79pub enum Format {
80 #[default]
81 Json,
82 Text,
83}
84
85const STANDARD_VERBS: [(&str, &str); 4] = [
86 ("validate", "lint"),
87 ("list", "builtins"),
88 ("create", "new"),
89 ("show", "trace"),
90];
91
92fn canonical(subcommand: &str) -> &str {
93 STANDARD_VERBS
94 .iter()
95 .find(|(alias, _)| *alias == subcommand)
96 .map_or(subcommand, |(_, name)| *name)
97}
98
99pub(crate) fn value<'a>(
102 it: &mut impl Iterator<Item = &'a String>,
103 name: &str,
104) -> Result<String, CliError> {
105 it.next()
106 .cloned()
107 .ok_or_else(|| CliError::usage("cli.missing_value", format!("{name} needs a value")))
108}
109
110pub fn parse_args(argv: &[String]) -> Result<Command, CliError> {
112 if argv.iter().any(|a| a == "--version" || a == "-V") {
113 return Ok(Command::Version);
114 }
115 if argv.iter().any(|a| a == "--help") {
116 return Ok(Command::Help);
117 }
118
119 let mut it = argv.iter();
120 let subcommand = canonical(
121 it.next()
122 .ok_or_else(|| CliError::usage("cli.missing_subcommand", "no subcommand given"))?,
123 );
124 let rest = it.as_slice();
125 match subcommand {
126 "render" => render_args(rest),
127 "analyze" => analyze_args(rest),
128 "solve" => solve_args(rest),
129 "lint" => lint_args(rest),
130 "trace" => trace_args(rest),
131 "builtins" => builtins_args(rest),
132 "outline" => outline_args(rest),
133 "new" => new_args(rest),
134 other => Err(CliError::usage(
135 "cli.unknown_subcommand",
136 format!("unknown subcommand `{other}`"),
137 )),
138 }
139}
140
141fn builtins_args(rest: &[String]) -> Result<Command, CliError> {
142 match rest.first() {
143 None => Ok(Command::Builtins),
144 Some(extra) => Err(CliError::usage(
145 "cli.unknown_argument",
146 format!("builtins takes no arguments, not `{extra}`"),
147 )),
148 }
149}
150
151fn outline_args(rest: &[String]) -> Result<Command, CliError> {
152 match rest {
153 [text] => Ok(Command::Outline { text: text.clone() }),
154 _ => Err(CliError::usage(
155 "cli.missing_argument",
156 "outline takes one <expression>, quoted as one argument",
157 )),
158 }
159}
160
161fn new_args(rest: &[String]) -> Result<Command, CliError> {
162 match rest {
163 [name] if !name.starts_with("--") => Ok(Command::New {
164 name: name.clone(),
165 idempotency_key: None,
166 }),
167 [name, flag, key] if !name.starts_with("--") && flag == "--idempotency-key" => {
168 Ok(Command::New {
169 name: name.clone(),
170 idempotency_key: Some(key.clone()),
171 })
172 }
173 [] => Err(CliError::usage("cli.missing_argument", "missing <name>")),
174 _ => Err(CliError::usage(
175 "cli.unknown_argument",
176 "new takes one <name> and an optional `--idempotency-key <key>`",
177 )),
178 }
179}
180
181fn lint_args(rest: &[String]) -> Result<Command, CliError> {
182 let mut it = rest.iter().peekable();
183 let target = match it.peek() {
184 Some(a) if !a.starts_with("--") => it.next().cloned(),
185 _ => None,
186 };
187 let mut format = Format::default();
188 while let Some(flag) = it.next() {
189 match flag.as_str() {
190 "--format" => {
191 format = match it.next().map(String::as_str) {
192 Some("json") => Format::Json,
193 Some("text") => Format::Text,
194 other => {
195 return Err(CliError::usage(
196 "cli.bad_value",
197 format!(
198 "`--format` takes json or text, not `{}`",
199 other.unwrap_or("nothing")
200 ),
201 ));
202 }
203 };
204 }
205 extra => {
206 return Err(CliError::usage(
207 "cli.unknown_argument",
208 format!(
209 "lint takes only ['<expression>'] and `--format <json|text>`, not \
210 `{extra}`"
211 ),
212 ));
213 }
214 }
215 }
216 Ok(Command::Lint { target, format })
217}
218
219fn trace_args(rest: &[String]) -> Result<Command, CliError> {
221 match rest {
222 [target] if !target.starts_with("--") => Ok(Command::Trace {
223 target: target.clone(),
224 }),
225 [] => Err(CliError::usage(
226 "cli.missing_argument",
227 "missing <node|expression>",
228 )),
229 _ => Err(CliError::usage(
230 "cli.unknown_argument",
231 "trace takes one <node|expression> and nothing else",
232 )),
233 }
234}
235
236#[cfg(test)]
237mod tests {
238 use super::*;
239 use std::path::Path;
240
241 fn argv(parts: &[&str]) -> Vec<String> {
242 parts.iter().map(|s| (*s).to_string()).collect()
243 }
244
245 fn rendered(parts: &[&str]) -> RenderArgs {
246 match parse_args(&argv(parts)) {
247 Ok(Command::Render(args)) => *args,
248 other => panic!("expected a render, got {other:?}"),
249 }
250 }
251
252 fn refused(parts: &[&str]) -> String {
253 match parse_args(&argv(parts)) {
254 Err(CliError::Usage { message, .. }) => message,
255 other => panic!("expected a usage refusal, got {other:?}"),
256 }
257 }
258
259 #[test]
262 fn each_argument_mistake_answers_its_own_code_naming_the_text() {
263 let render = |tail: &[&'static str]| [&["render", "@a"][..], tail].concat();
264 let cases: [(Vec<&str>, &str, &str); 14] = [
265 (
266 render(&["--representation", "spectre"]),
267 "query.unknown_representation",
268 "`spectre`",
269 ),
270 (
271 render(&["--representation", "lines(depth=2)"]),
272 "query.unknown_argument",
273 "`depth`",
274 ),
275 (
276 render(&["--representation", "ledger(depth=many)"]),
277 "query.bad_value",
278 "`many`",
279 ),
280 (
281 render(&["--representation", "lines,lines"]),
282 "query.duplicate",
283 "`lines`",
284 ),
285 (
286 render(&["--representation", "bindings"]),
287 "query.missing_argument",
288 "`bindings`",
289 ),
290 (
291 render(&["--representation", "spectrum(peaks=8"]),
292 "query.malformed",
293 "`spectrum(peaks=8`",
294 ),
295 (
296 render(&["--representation", "exact-envelope"]),
297 "query.retired",
298 "`exact-envelope`",
299 ),
300 (
301 render(&["--representation", "a=x,b=x"]),
302 "query.unknown_representation",
303 "`a`",
304 ),
305 (
306 render(&["--representation", "lines=x.json,atoms=./x.json"]),
307 "query.shared_destination",
308 "x.json",
309 ),
310 (
311 render(&["--representation", "lines", "--bogus"]),
312 "cli.unknown_argument",
313 "`--bogus`",
314 ),
315 (
316 render(&["--representation"]),
317 "cli.missing_value",
318 "--representation",
319 ),
320 (
321 render(&["--representation", "lines", "--rate", "0"]),
322 "cli.bad_value",
323 "`0`",
324 ),
325 (vec!["bench"], "cli.unknown_subcommand", "`bench`"),
326 (
327 vec!["analyze", "/tmp/a.wav", "--representation", "ledger"],
328 "query.not_analyzable",
329 "`ledger`",
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}