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); 15] = [
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 vec!["analyze", "/tmp/a.flac", "--representation", "bands"],
333 "analyze.not_wav",
334 "`/tmp/a.flac`",
335 ),
336 ];
337 for (parts, code, text) in cases {
338 let Err(CliError::Usage {
339 code: held,
340 message,
341 }) = parse_args(&argv(&parts))
342 else {
343 panic!("{parts:?} refuses as an argument mistake");
344 };
345 assert_eq!(held, code, "{parts:?}: {message}");
346 assert!(message.contains(text), "{parts:?}: {message}");
347 assert!(!message.contains("usage:"), "{parts:?}: {message}");
348 }
349 }
350
351 #[test]
352 fn version_and_help_are_recognized_anywhere_in_argv() {
353 assert_eq!(parse_args(&argv(&["--version"])).unwrap(), Command::Version);
354 assert_eq!(parse_args(&argv(&["-V"])).unwrap(), Command::Version);
355 assert_eq!(
356 parse_args(&argv(&["lint", "--help"])).unwrap(),
357 Command::Help
358 );
359 }
360
361 #[test]
362 fn each_standard_verb_alias_reaches_the_subcommand_it_names() {
363 assert_eq!(
364 parse_args(&argv(&["validate"])).unwrap(),
365 Command::Lint {
366 target: None,
367 format: Format::Json,
368 }
369 );
370 assert_eq!(parse_args(&argv(&["list"])).unwrap(), Command::Builtins);
371 assert_eq!(
372 parse_args(&argv(&["show", "kick"])).unwrap(),
373 Command::Trace {
374 target: "kick".to_string(),
375 }
376 );
377 assert_eq!(
378 parse_args(&argv(&["create", "song1"])).unwrap(),
379 Command::New {
380 name: "song1".to_string(),
381 idempotency_key: None,
382 }
383 );
384 }
385
386 #[test]
389 fn a_render_takes_a_target_readings_with_their_arguments_and_flags() {
390 let args = rendered(&[
391 "render",
392 "@piano([0, 2b], f0=C4)",
393 "--representation",
394 "samples=/tmp/out.wav, spectrum(peaks=8, frame=50ms)",
395 "--representation",
396 "ledger(depth=2, brief=1)=/tmp/ledger.json,bindings(node=@voice)",
397 "--rate",
398 "48000",
399 "--bits",
400 "16",
401 "--until",
402 "t > 1s",
403 "--threads",
404 "3",
405 ]);
406 assert_eq!(args.target, "@piano([0, 2b], f0=C4)");
407 assert_eq!(args.until.as_deref(), Some("t > 1s"));
408 assert_eq!(args.rate, Some(48_000));
409 assert_eq!(args.bits, Some(16));
410 assert_eq!(args.threads.map(std::num::NonZeroUsize::get), Some(3));
411 let names: Vec<&str> = args.asked.iter().map(|a| a.name.as_str()).collect();
412 assert_eq!(names, ["samples", "spectrum", "ledger", "bindings"]);
413 assert_eq!(args.asked[0].path(), Some(Path::new("/tmp/out.wav")));
414 assert_eq!(
415 args.asked[1]
416 .representation(sva_engine::DEFAULT_SAMPLE_RATE, None)
417 .unwrap(),
418 sva_engine::Representation::Spectrum {
419 max_peaks: 8,
420 frame_secs: Some(0.05)
421 }
422 );
423 assert_eq!(
424 args.asked[2]
425 .representation(sva_engine::DEFAULT_SAMPLE_RATE, None)
426 .unwrap(),
427 sva_engine::Representation::Ledger { depth: 2 }
428 );
429 assert!(args.asked[2].brief);
430 assert_eq!(args.asked[2].path(), Some(Path::new("/tmp/ledger.json")));
431 assert_eq!(args.asked[3].node.as_deref(), Some("voice"));
432 }
433
434 #[test]
435 fn a_render_on_no_threads_is_refused() {
436 let message = refused(&[
437 "render",
438 "1",
439 "--representation",
440 "envelope",
441 "--threads",
442 "0",
443 ]);
444 assert!(message.contains("--threads"), "{message}");
445 }
446
447 #[test]
448 fn a_target_may_open_with_a_minus() {
449 let args = rendered(&["render", "-1*sin(2*pi*220*t)", "--representation", "lines"]);
450 assert_eq!(args.target, "-1*sin(2*pi*220*t)");
451 }
452
453 #[test]
454 fn a_render_with_no_target_or_no_reading_refuses() {
455 refused(&["render"]);
456 refused(&["render", "--representation", "lines"]);
457 refused(&["render", "@a"]);
458 }
459
460 #[test]
462 fn a_retired_flag_refuses_by_name() {
463 for gone in [
464 "-c",
465 "--in",
466 "--from",
467 "--to",
468 "--max",
469 "--sample-rate",
470 "--as",
471 "--node",
472 "--frame",
473 "--depth",
474 "--peaks",
475 "--oversample",
476 "--brief",
477 "--skim",
478 "--pcm16",
479 ] {
480 let message = refused(&["render", "@a", "--representation", "lines", gone, "x"]);
481 assert!(message.contains(gone), "{gone}: {message}");
482 }
483 }
484
485 #[test]
486 fn an_argument_its_reading_does_not_take_or_a_malformed_one_refuses() {
487 for reading in [
488 "lines(depth=2)",
489 "ledger(colour=red)",
490 "ledger(depth)",
491 "ledger(depth=many)",
492 "ledger(brief=yes)",
493 "samples(bits=16)",
494 "spectrum(peaks=8",
495 "bindings(node=voice)",
496 ] {
497 refused(&["render", "@a", "--representation", reading]);
498 }
499 }
500
501 #[test]
503 fn a_reading_asked_twice_refuses_by_name() {
504 for written in [
505 &["--representation", "spectrum(peaks=4),spectrum(frame=20ms)"][..],
506 &["--representation", "lines", "--representation", "lines"][..],
507 &["--representation", "samples=/tmp/a.wav,samples"][..],
508 ] {
509 let message = refused(&[&["render", "@a"][..], written].concat());
510 assert!(message.contains("asked more than once"), "{message}");
511 }
512 let message = refused(&[
513 "analyze",
514 "/tmp/a.wav",
515 "--representation",
516 "pitch,pitch(peaks=2)",
517 ]);
518 assert!(message.contains("`pitch`"), "{message}");
519 }
520
521 #[test]
522 fn a_representation_that_left_the_language_names_what_replaced_it() {
523 for (gone, write) in sva_core::RETIRED {
524 let message = refused(&["render", "@a", "--representation", gone]);
525 assert!(message.contains(write), "{message}");
526 }
527 }
528
529 #[test]
530 fn a_wav_destination_takes_samples_and_nothing_else() {
531 refused(&["render", "@a", "--representation", "ledger=/tmp/out.wav"]);
532 }
533
534 #[test]
537 fn a_device_destination_takes_samples_and_a_buffer_beside_every_other_flag() {
538 use sva_core::Destination;
539 let row = sva_engine::Representation::Samples.reading();
540 assert_eq!(row.takes, [sva_engine::Arg::Buffer]);
541 assert_eq!(sva_engine::Arg::Buffer.key(), "buffer");
542 let args = rendered(&["render", "@a", "--representation", "samples=alsa:default"]);
543 assert_eq!(
544 args.asked[0].dest,
545 Some(Destination::Alsa {
546 device: "default".to_string(),
547 buffer_secs: sva_engine::DEFAULT_BUFFER_SECS,
548 })
549 );
550 assert_eq!(args.asked[0].path(), None);
551 let args = rendered(&[
552 "render",
553 "@a",
554 "--representation",
555 "samples(buffer=20ms)=alsa:hw:1,0,loudness",
556 "--until",
557 "t > 1s",
558 "--rate",
559 "44100",
560 ]);
561 let names: Vec<&str> = args.asked.iter().map(|a| a.name.as_str()).collect();
562 assert_eq!(names, ["samples", "loudness"]);
563 assert_eq!(
564 args.asked[0].dest,
565 Some(Destination::Alsa {
566 device: "hw:1,0".to_string(),
567 buffer_secs: 0.02,
568 }),
569 "a comma inside a device name stays in it where no reading follows"
570 );
571 assert_eq!(args.asked[1].dest, None);
572 assert_eq!(args.until.as_deref(), Some("t > 1s"));
573 assert_eq!(args.rate, Some(44_100));
574 let args = rendered(&[
575 "render",
576 "@a",
577 "--representation",
578 "lines,samples=alsa:plughw:CARD=PCH,DEV=0",
579 ]);
580 assert_eq!(
581 args.asked[1].dest,
582 Some(Destination::Alsa {
583 device: "plughw:CARD=PCH,DEV=0".to_string(),
584 buffer_secs: sva_engine::DEFAULT_BUFFER_SECS,
585 })
586 );
587 }
588
589 #[test]
590 fn a_device_refuses_what_it_cannot_play_naming_the_text() {
591 let render = |r: &'static str| vec!["render", "@a", "--representation", r];
592 let cases: [(Vec<&str>, &str, &str); 9] = [
593 (
594 render("samples=alsa:hw:0,samples=alsa:hw:1"),
595 "query.duplicate",
596 "`samples`",
597 ),
598 (
599 render("samples=alsa:"),
600 "destination.no_device",
601 "`samples=alsa:`",
602 ),
603 (
604 render("loudness=alsa:default"),
605 "destination.not_audio",
606 "`loudness`",
607 ),
608 (
609 render("samples(buffer=50ms)"),
610 "query.buffer_without_device",
611 "`samples`",
612 ),
613 (
614 render("samples(buffer=50ms)=/tmp/a.wav"),
615 "query.buffer_without_device",
616 "`buffer`",
617 ),
618 (
619 render("samples(buffer=0)=alsa:default"),
620 "query.bad_value",
621 "`0`",
622 ),
623 (
624 render("samples(buffer=5000s)=alsa:default"),
625 "query.bad_value",
626 "`5000s`",
627 ),
628 (
629 render("lines(buffer=1)"),
630 "query.unknown_argument",
631 "`buffer`",
632 ),
633 (
634 vec![
635 "analyze",
636 "/tmp/a.wav",
637 "--representation",
638 "samples=alsa:hw:1",
639 ],
640 "destination.needs_render",
641 "`alsa:hw:1`",
642 ),
643 ];
644 for (parts, code, text) in cases {
645 let Err(CliError::Usage {
646 code: held,
647 message,
648 }) = parse_args(&argv(&parts))
649 else {
650 panic!("{parts:?} refuses as an argument mistake");
651 };
652 assert_eq!(held, code, "{parts:?}: {message}");
653 assert!(message.contains(text), "{parts:?}: {message}");
654 }
655 }
656
657 #[cfg(not(target_os = "linux"))]
658 #[test]
659 fn off_linux_a_device_refuses_at_parse_as_an_unsupported_platform() {
660 let parts = argv(&["render", "@a", "--representation", "samples=alsa:default"]);
661 let Err(CliError::Usage { code, .. }) = parse_args(&parts) else {
662 panic!("refused");
663 };
664 assert_eq!(code, "destination.unsupported_platform");
665 }
666
667 #[test]
668 fn a_harmonic_reading_takes_its_floor_in_decibels_and_its_norm_by_name() {
669 let args = rendered(&[
670 "render",
671 "@a",
672 "--representation",
673 "voice_leading(frame=10ms, floor=-60db, norm=l2),dissonance",
674 ]);
675 assert_eq!(
676 args.asked[0]
677 .representation(sva_engine::DEFAULT_SAMPLE_RATE, None)
678 .unwrap(),
679 sva_engine::Representation::Harmony {
680 reading: sva_engine::Harmonic::VoiceLeading {
681 norm: sva_engine::harmony::voice_leading::Norm::L2
682 },
683 frame_secs: 0.01,
684 floor_db: -60.0,
685 }
686 );
687 assert_eq!(
688 args.asked[1]
689 .representation(sva_engine::DEFAULT_SAMPLE_RATE, None)
690 .unwrap(),
691 sva_engine::Representation::Harmony {
692 reading: sva_engine::Harmonic::Dissonance {
693 max_pairs: sva_engine::DEFAULT_MAX_PAIRS
694 },
695 frame_secs: sva_engine::DEFAULT_FRAME_SECS,
696 floor_db: sva_engine::DEFAULT_FLOOR_DB,
697 }
698 );
699 let render = |r: &'static str| vec!["render", "@a", "--representation", r];
700 for (parts, text) in [
701 (render("dissonance(floor=-60)"), "`-60`"),
702 (render("voice_leading(norm=l3)"), "`l3`"),
703 (render("dissonance(norm=l1)"), "`norm`"),
704 ] {
705 let Err(CliError::Usage { message, .. }) = parse_args(&argv(&parts)) else {
706 panic!("{parts:?} refuses as an argument mistake");
707 };
708 assert!(message.contains(text), "{parts:?}: {message}");
709 }
710 }
711
712 #[test]
713 fn inharmonicity_takes_the_harmonic_frame_and_floor() {
714 let args = rendered(&[
715 "render",
716 "@a",
717 "--representation",
718 "inharmonicity(frame=10ms, floor=-60db)",
719 ]);
720 assert_eq!(
721 args.asked[0]
722 .representation(sva_engine::DEFAULT_SAMPLE_RATE, None)
723 .unwrap(),
724 sva_engine::Representation::Harmony {
725 reading: sva_engine::Harmonic::Inharmonicity,
726 frame_secs: 0.01,
727 floor_db: -60.0,
728 }
729 );
730 }
731
732 #[test]
733 fn periodicity_takes_its_step_window_and_curve() {
734 let args = rendered(&[
735 "render",
736 "@a",
737 "--representation",
738 "periodicity(frame=0.5s, window=8s, curve=1)",
739 ]);
740 assert_eq!(
741 args.asked[0]
742 .representation(sva_engine::DEFAULT_SAMPLE_RATE, None)
743 .unwrap(),
744 sva_engine::Representation::Periodicity {
745 frame_secs: 0.5,
746 window_secs: 8.0,
747 curve: true,
748 }
749 );
750 }
751
752 #[test]
753 fn modulation_takes_its_step_window_and_bpm() {
754 let args = rendered(&[
755 "render",
756 "@a",
757 "--representation",
758 "modulation(frame=0.25s, window=6s, bpm=140)",
759 ]);
760 assert_eq!(
761 args.asked[0]
762 .representation(sva_engine::DEFAULT_SAMPLE_RATE, None)
763 .unwrap(),
764 sva_engine::Representation::Modulation {
765 frame_secs: 0.25,
766 window_secs: 6.0,
767 bpm: Some(140.0),
768 }
769 );
770 refused(&["render", "@a", "--representation", "modulation(bpm=0)"]);
771 refused(&["render", "@a", "--representation", "modulation(bpm=2s)"]);
772 }
773
774 #[test]
775 fn self_similarity_takes_its_cell_smooth_window_and_listing() {
776 let args = rendered(&[
777 "render",
778 "@a",
779 "--representation",
780 "self_similarity(frame=0.1s, cell=1s, smooth=3s, window=12s, floor=-50db, \
781 peaks=4, curve=1)",
782 ]);
783 assert_eq!(
784 args.asked[0]
785 .representation(sva_engine::DEFAULT_SAMPLE_RATE, None)
786 .unwrap(),
787 sva_engine::Representation::SelfSimilarity {
788 frame_secs: 0.1,
789 cell_secs: 1.0,
790 smooth_secs: 3.0,
791 window_secs: 12.0,
792 floor_db: -50.0,
793 listed: 4,
794 curve: true,
795 }
796 );
797 refused(&[
798 "render",
799 "@a",
800 "--representation",
801 "self_similarity(cell=0)",
802 ]);
803 refused(&[
804 "render",
805 "@a",
806 "--representation",
807 "self_similarity(smooth=x)",
808 ]);
809 }
810
811 #[test]
812 fn bands_prunes_at_one_db_or_the_level_written_and_cuts_only_a_pruned_track() {
813 let bands = |call: &str| {
814 rendered(&["render", "@a", "--representation", call]).asked[0]
815 .representation(sva_engine::DEFAULT_SAMPLE_RATE, None)
816 .unwrap()
817 };
818 let pruned = |eps_db, floor_db| sva_engine::Representation::Bands {
819 prune: Some(sva_engine::Prune { eps_db, floor_db }),
820 };
821 let none = sva_engine::Representation::Bands { prune: None };
822 assert_eq!(bands("bands"), none);
823 assert_eq!(bands("bands(prune=0)"), none);
824 assert_eq!(bands("bands(prune=1)"), pruned(1.0, -80.0));
825 assert_eq!(
826 bands("bands(prune=0.25db, floor=-60db)"),
827 pruned(0.25, -60.0)
828 );
829 for call in [
830 "bands(floor=-60db)",
831 "bands(prune=1, floor=0db)",
832 "bands(prune=0.1)",
833 "bands(prune=-1db)",
834 "bands(prune=0.0000001db)",
835 ] {
836 refused(&["render", "@a", "--representation", call]);
837 }
838 }
839
840 #[test]
841 fn bindings_needs_a_node() {
842 refused(&["render", "@a", "--representation", "bindings"]);
843 }
844
845 #[test]
846 fn lint_takes_an_optional_target_and_nothing_else() {
847 assert_eq!(
848 parse_args(&argv(&["lint", "@drums/kick"])).unwrap(),
849 Command::Lint {
850 target: Some("@drums/kick".to_string()),
851 format: Format::Json,
852 }
853 );
854 refused(&["lint", "a", "b"]);
855 refused(&["lint", "--in", "x"]);
856 }
857
858 #[test]
859 fn new_takes_a_name_and_an_optional_idempotency_key() {
860 assert_eq!(
861 parse_args(&argv(&["new", "song1", "--idempotency-key", "k"])).unwrap(),
862 Command::New {
863 name: "song1".to_string(),
864 idempotency_key: Some("k".to_string()),
865 }
866 );
867 refused(&["new"]);
868 }
869
870 #[test]
871 fn builtins_takes_no_arguments() {
872 assert_eq!(parse_args(&argv(&["builtins"])).unwrap(), Command::Builtins);
873 refused(&["builtins", "x"]);
874 }
875
876 #[test]
877 fn a_subcommand_this_cli_does_not_answer_refuses_by_name() {
878 for gone in ["bench", "nonlinear-solve"] {
879 assert!(refused(&[gone]).contains(gone));
880 }
881 }
882}