1mod cli;
2mod ic;
3mod icrc;
4mod nns;
5mod output;
6mod progress;
7mod sns;
8mod storage;
9
10#[cfg(test)]
11mod test_support;
12
13use crate::cli::clap::{
14 parse_matches_or_usage, passthrough_args, passthrough_subcommand, string_option,
15};
16use clap::{Arg, ArgAction, Command};
17use ic_query::subnet_catalog::MAINNET_NETWORK;
18use std::ffi::OsString;
19use thiserror::Error as ThisError;
20
21const TOP_LEVEL_HELP_TEMPLATE: &str = "{name} {version}\n{about-with-newline}\n{usage-heading} {usage}\n\nCommands:\n{subcommands}\n\nOptions:\n{options}{after-help}\n";
22const VERSION_TEXT: &str = concat!("icq ", env!("CARGO_PKG_VERSION"));
23const INTERNAL_NETWORK_OPTION: &str = "--__icq-network";
24
25const fn version_text() -> &'static str {
26 VERSION_TEXT
27}
28
29#[derive(Debug, ThisError)]
36pub enum IcqCliError {
37 #[error("{0}")]
38 Usage(String),
39
40 #[error("nns: {0}")]
41 Nns(#[from] nns::NnsCommandError),
42
43 #[error("icrc: {0}")]
44 Icrc(#[from] icrc::IcrcCommandError),
45
46 #[error("ic: {0}")]
47 Ic(#[from] ic::IcCommandError),
48
49 #[error("sns: {0}")]
50 Sns(#[from] sns::SnsCommandError),
51}
52
53impl IcqCliError {
54 #[must_use]
56 pub fn is_broken_pipe(&self) -> bool {
57 match self {
58 Self::Ic(error) if error.is_broken_pipe() => true,
59 Self::Nns(nns::NnsCommandError::Io(err))
60 | Self::Icrc(icrc::IcrcCommandError::Io(err))
61 | Self::Sns(sns::SnsCommandError::Io(err)) => {
62 err.kind() == std::io::ErrorKind::BrokenPipe
63 }
64 Self::Usage(_) | Self::Ic(_) | Self::Nns(_) | Self::Icrc(_) | Self::Sns(_) => false,
65 }
66 }
67
68 #[must_use]
70 pub const fn exit_code(&self) -> i32 {
71 match self {
72 Self::Usage(_)
73 | Self::Ic(ic::IcCommandError::Usage(_))
74 | Self::Nns(nns::NnsCommandError::Usage(_))
75 | Self::Icrc(icrc::IcrcCommandError::Usage(_))
76 | Self::Sns(sns::SnsCommandError::Usage(_)) => 2,
77 Self::Ic(error) => error.exit_code(),
78 Self::Nns(_) | Self::Icrc(_) | Self::Sns(_) => 1,
79 }
80 }
81}
82
83pub fn run_from_env() -> Result<(), IcqCliError> {
85 run(std::env::args_os().skip(1))
86}
87
88pub fn run<I>(args: I) -> Result<(), IcqCliError>
90where
91 I: IntoIterator<Item = OsString>,
92{
93 let Some(args) = collect_args_or_print_help(args, usage) else {
94 return Ok(());
95 };
96 if let Some((command, option)) = command_local_global_option(&args) {
97 if matches!(command, "ic" | "icrc") {
98 return Err(unsupported_global_network_error(command));
99 }
100 return Err(IcqCliError::Usage(format!(
101 "{option} is a top-level option; put it before the command\n\n{}",
102 usage()
103 )));
104 }
105
106 let matches = parse_matches_or_usage(top_level_dispatch_command(), args, usage)
107 .map_err(IcqCliError::Usage)?;
108 if matches.get_flag("version") {
109 println!("{VERSION_TEXT}");
110 return Ok(());
111 }
112 let global_network = string_option(&matches, "network");
113
114 let Some((command, subcommand_matches)) = matches.subcommand() else {
115 return Err(IcqCliError::Usage(usage()));
116 };
117 let mut tail = passthrough_args(subcommand_matches);
118 apply_global_network(command, &mut tail, global_network)?;
119 let tail = tail.into_iter();
120
121 match command {
122 "ic" => Ok(ic::run(tail)?),
123 "icrc" => Ok(icrc::run(tail)?),
124 "nns" => Ok(nns::run(tail)?),
125 "sns" => Ok(sns::run(tail)?),
126 _ => unreachable!("top-level dispatch command only defines known commands"),
127 }
128}
129
130fn collect_args_or_print_help<I>(args: I, usage: impl FnOnce() -> String) -> Option<Vec<OsString>>
131where
132 I: IntoIterator<Item = OsString>,
133{
134 let args = args.into_iter().collect::<Vec<_>>();
135 if top_level_help_requested(&args) {
136 println!("{}", usage());
137 return None;
138 }
139 Some(args)
140}
141
142fn top_level_help_requested(args: &[OsString]) -> bool {
143 let mut index = 0;
144 while index < args.len() {
145 let Some(arg) = args[index].to_str() else {
146 return false;
147 };
148 if command_family(arg).is_some() {
149 return false;
150 }
151 if matches!(arg, "help" | "--help" | "-h") {
152 return true;
153 }
154 index += if arg == "--network" { 2 } else { 1 };
155 }
156 false
157}
158
159fn network_arg() -> Arg {
160 Arg::new("network")
161 .num_args(1)
162 .long("network")
163 .value_name("name")
164 .help("Network identity for NNS and SNS commands; currently only ic")
165}
166
167fn top_level_command() -> Command {
168 Command::new("icq")
169 .version(env!("CARGO_PKG_VERSION"))
170 .about("Internet Computer metadata query CLI")
171 .disable_help_subcommand(true)
172 .disable_version_flag(true)
173 .arg(
174 Arg::new("version")
175 .short('V')
176 .long("version")
177 .action(ArgAction::SetTrue)
178 .help("Print version"),
179 )
180 .arg(network_arg().global(true))
181 .subcommand_help_heading("Commands")
182 .help_template(TOP_LEVEL_HELP_TEMPLATE)
183 .after_help("Run `icq <command> help` for command-specific help.")
184 .subcommands(
185 COMMAND_FAMILIES
186 .iter()
187 .map(|family| Command::new(family.name).about(family.about)),
188 )
189}
190
191fn top_level_dispatch_command() -> Command {
192 let command = Command::new("icq")
193 .disable_help_flag(true)
194 .disable_help_subcommand(true)
195 .disable_version_flag(true)
196 .arg(
197 Arg::new("version")
198 .short('V')
199 .long("version")
200 .action(ArgAction::SetTrue),
201 )
202 .arg(network_arg().global(true));
203
204 COMMAND_FAMILIES.iter().fold(command, |command, family| {
205 command.subcommand(passthrough_subcommand(
206 Command::new(family.name).about(family.about),
207 ))
208 })
209}
210
211fn usage() -> String {
212 let mut command = top_level_command();
213 command.render_help().to_string()
214}
215
216fn command_local_global_option(args: &[OsString]) -> Option<(&'static str, &'static str)> {
217 let mut index = 0;
218 while index < args.len() {
219 let arg = args[index].to_str()?;
220 if let Some(family) = command_family(arg) {
221 return args[index + 1..]
222 .iter()
223 .filter_map(|arg| arg.to_str())
224 .find_map(global_option_name)
225 .map(|option| (family.name, option));
226 }
227 index += if arg == "--network" { 2 } else { 1 };
228 }
229 None
230}
231
232fn global_option_name(arg: &str) -> Option<&'static str> {
233 match arg {
234 "--network" => Some("--network"),
235 _ if arg.starts_with("--network=") => Some("--network"),
236 _ => None,
237 }
238}
239
240fn apply_global_network(
241 command: &str,
242 tail: &mut Vec<OsString>,
243 global_network: Option<String>,
244) -> Result<(), IcqCliError> {
245 let Some(global_network) = global_network else {
246 return Ok(());
247 };
248 if tail_requests_help_or_version(tail) {
249 return Ok(());
250 }
251 if !command_accepts_global_network(command, tail) {
252 return Err(unsupported_global_network_error(command));
253 }
254 if global_network != MAINNET_NETWORK {
255 return Err(unsupported_mainnet_network_error(command, &global_network));
256 }
257 if tail_has_option(tail, INTERNAL_NETWORK_OPTION) {
258 return Ok(());
259 }
260
261 tail.push(OsString::from(INTERNAL_NETWORK_OPTION));
262 tail.push(OsString::from(global_network));
263 Ok(())
264}
265
266fn unsupported_global_network_error(command: &str) -> IcqCliError {
267 let guidance = if matches!(command, "ic" | "icrc") {
268 " use the command's --source-endpoint option to select its API endpoint"
269 } else {
270 ""
271 };
272 IcqCliError::Usage(format!(
273 "--network is not supported by `icq {command}`;{guidance}\n\n{}",
274 usage()
275 ))
276}
277
278fn unsupported_mainnet_network_error(command: &str, network: &str) -> IcqCliError {
279 IcqCliError::Usage(format!(
280 "`icq {command}` currently supports only the mainnet `{MAINNET_NETWORK}` network; received `{network}`\n\n{}",
281 usage()
282 ))
283}
284
285fn command_accepts_global_network(command: &str, tail: &[OsString]) -> bool {
286 command_family(command).is_some_and(|family| (family.accepts_global_network)(tail))
287}
288
289fn tail_has_option(tail: &[OsString], name: &str) -> bool {
290 tail.iter().any(|arg| arg.to_str() == Some(name))
291}
292
293fn tail_requests_help_or_version(tail: &[OsString]) -> bool {
294 tail.iter()
295 .filter_map(|arg| arg.to_str())
296 .any(|arg| matches!(arg, "help" | "--help" | "-h" | "--version" | "-V"))
297}
298
299#[derive(Clone, Copy, Debug)]
300struct CommandFamily {
301 name: &'static str,
302 about: &'static str,
303 accepts_global_network: fn(&[OsString]) -> bool,
304}
305
306const COMMAND_FAMILIES: &[CommandFamily] = &[
307 CommandFamily {
308 name: "ic",
309 about: "Inspect official IC Dashboard metadata",
310 accepts_global_network: ic_accepts_global_network,
311 },
312 CommandFamily {
313 name: "icrc",
314 about: "Inspect generic ICRC ledger and account metadata",
315 accepts_global_network: icrc_accepts_global_network,
316 },
317 CommandFamily {
318 name: "nns",
319 about: "Inspect NNS metadata",
320 accepts_global_network: nns_accepts_global_network,
321 },
322 CommandFamily {
323 name: "sns",
324 about: "Inspect SNS metadata",
325 accepts_global_network: sns_accepts_global_network,
326 },
327];
328
329fn command_family(name: &str) -> Option<&'static CommandFamily> {
330 COMMAND_FAMILIES.iter().find(|family| family.name == name)
331}
332
333const fn nns_accepts_global_network(_tail: &[OsString]) -> bool {
334 true
335}
336
337const fn ic_accepts_global_network(_tail: &[OsString]) -> bool {
338 false
339}
340
341const fn icrc_accepts_global_network(_tail: &[OsString]) -> bool {
342 false
343}
344
345const fn sns_accepts_global_network(_tail: &[OsString]) -> bool {
346 true
347}
348
349#[cfg(test)]
350mod tests {
351 use super::*;
352
353 #[test]
354 fn usage_lists_query_families() {
355 let text = usage();
356
357 assert!(text.contains("Usage: icq [OPTIONS] [COMMAND]"));
358 assert!(text.contains("ic"));
359 assert!(text.contains("Inspect official IC Dashboard metadata"));
360 assert!(text.contains("icrc"));
361 assert!(text.contains("Inspect generic ICRC ledger and account metadata"));
362 assert!(text.contains("nns"));
363 assert!(text.contains("Inspect NNS metadata"));
364 assert!(text.contains("sns"));
365 assert!(text.contains("Inspect SNS metadata"));
366 assert!(text.contains("Run `icq <command> help`"));
367 }
368
369 #[test]
370 fn top_level_usage_snapshot() {
371 let expected = format!(
372 "\
373icq {}
374Internet Computer metadata query CLI
375
376Usage: icq [OPTIONS] [COMMAND]
377
378Commands:
379 ic Inspect official IC Dashboard metadata
380 icrc Inspect generic ICRC ledger and account metadata
381 nns Inspect NNS metadata
382 sns Inspect SNS metadata
383
384Options:
385 -V, --version Print version
386 --network <name> Network identity for NNS and SNS commands; currently only ic
387 -h, --help Print help
388
389Run `icq <command> help` for command-specific help.
390",
391 env!("CARGO_PKG_VERSION")
392 );
393
394 assert_eq!(usage(), expected);
395 }
396
397 #[test]
398 fn command_family_help_returns_ok() {
399 for args in [
400 &["ic", "help"][..],
401 &["ic", "canister", "help"],
402 &["ic", "canister", "info", "help"],
403 &["icrc", "help"],
404 &["icrc", "ledger", "help"],
405 &["icrc", "ledger", "token", "help"],
406 &["icrc", "account", "help"],
407 &["icrc", "account", "balance", "help"],
408 &["icrc", "account", "allowance", "help"],
409 &["icrc", "account", "transaction", "help"],
410 &["icrc", "account", "transaction", "page", "help"],
411 &["icrc", "account", "transaction", "list", "help"],
412 &["icrc", "account", "transaction", "refresh", "help"],
413 &["icrc", "account", "transaction", "cache", "help"],
414 &["icrc", "account", "transaction", "cache", "status", "help"],
415 &["icrc", "ledger", "index", "help"],
416 &["nns", "help"][..],
417 &["nns", "data-center", "help"],
418 &["nns", "data-center", "list", "help"],
419 &["nns", "data-center", "info", "help"],
420 &["nns", "data-center", "refresh", "help"],
421 &["nns", "node", "help"],
422 &["nns", "node", "list", "help"],
423 &["nns", "node", "info", "help"],
424 &["nns", "node", "refresh", "help"],
425 &["nns", "node-provider", "help"],
426 &["nns", "node-provider", "list", "help"],
427 &["nns", "node-provider", "info", "help"],
428 &["nns", "node-provider", "refresh", "help"],
429 &["nns", "node-operator", "help"],
430 &["nns", "node-operator", "list", "help"],
431 &["nns", "node-operator", "info", "help"],
432 &["nns", "node-operator", "refresh", "help"],
433 &["nns", "proposal", "help"],
434 &["nns", "proposal", "list", "help"],
435 &["nns", "proposal", "info", "help"],
436 &["nns", "registry", "help"],
437 &["nns", "registry", "version", "help"],
438 &["nns", "subnet", "help"],
439 &["nns", "subnet", "list", "help"],
440 &["nns", "subnet", "info", "help"],
441 &["nns", "subnet", "refresh", "help"],
442 &["nns", "topology", "help"],
443 &["nns", "topology", "summary", "help"],
444 &["nns", "topology", "coverage", "help"],
445 &["nns", "topology", "versions", "help"],
446 &["nns", "topology", "health", "help"],
447 &["nns", "topology", "gaps", "help"],
448 &["nns", "topology", "capacity", "help"],
449 &["nns", "topology", "regions", "help"],
450 &["nns", "topology", "providers", "help"],
451 &["nns", "topology", "refresh", "help"],
452 &["sns", "help"],
453 &["sns", "list", "help"],
454 &["sns", "info", "help"],
455 &["sns", "token", "help"],
456 &["sns", "params", "help"],
457 &["sns", "proposal", "help"],
458 &["sns", "proposal", "list", "help"],
459 &["sns", "proposal", "info", "help"],
460 &["sns", "proposal", "cache", "help"],
461 &["sns", "proposal", "cache", "list", "help"],
462 &["sns", "proposal", "cache", "status", "help"],
463 &["sns", "proposal", "refresh", "help"],
464 &["sns", "neuron", "help"],
465 &["sns", "neuron", "list", "help"],
466 &["sns", "neuron", "cache", "help"],
467 &["sns", "neuron", "cache", "list", "help"],
468 &["sns", "neuron", "cache", "status", "help"],
469 &["sns", "neuron", "refresh", "help"],
470 ] {
471 assert_run_ok(args);
472 }
473 }
474
475 #[test]
476 fn version_flags_return_ok() {
477 assert_eq!(VERSION_TEXT, concat!("icq ", env!("CARGO_PKG_VERSION")));
478 assert!(run([OsString::from("--version")]).is_ok());
479 assert!(run([OsString::from("ic"), OsString::from("--version")]).is_ok());
480 assert!(run([OsString::from("icrc"), OsString::from("--version")]).is_ok());
481 assert!(run([OsString::from("nns"), OsString::from("--version")]).is_ok());
482 assert!(run([OsString::from("sns"), OsString::from("--version")]).is_ok());
483 assert!(
484 run([
485 OsString::from("nns"),
486 OsString::from("subnet"),
487 OsString::from("list"),
488 OsString::from("--version")
489 ])
490 .is_ok()
491 );
492
493 let mut sns_info_tail = vec![OsString::from("info"), OsString::from("1")];
494
495 apply_global_network("sns", &mut sns_info_tail, Some("ic".to_string()))
496 .expect("SNS supports global network");
497
498 assert_eq!(
499 sns_info_tail,
500 vec![
501 OsString::from("info"),
502 OsString::from("1"),
503 OsString::from(INTERNAL_NETWORK_OPTION),
504 OsString::from("ic")
505 ]
506 );
507 }
508
509 #[test]
510 fn typed_cli_errors_preserve_exit_and_broken_pipe_semantics() {
511 let usage = IcqCliError::Icrc(icrc::IcrcCommandError::Usage("bad input".to_string()));
512 assert_eq!(usage.exit_code(), 2);
513 assert!(!usage.is_broken_pipe());
514
515 let broken_pipe = IcqCliError::Icrc(icrc::IcrcCommandError::Io(std::io::Error::from(
516 std::io::ErrorKind::BrokenPipe,
517 )));
518 assert_eq!(broken_pipe.exit_code(), 1);
519 assert!(broken_pipe.is_broken_pipe());
520 }
521
522 #[test]
523 fn global_network_is_forwarded_to_networked_leaf_commands() {
524 for (command, leaf) in [
525 ("nns", "data-center"),
526 ("nns", "governance"),
527 ("nns", "neuron"),
528 ("nns", "node"),
529 ("nns", "node-operator"),
530 ("nns", "node-provider"),
531 ("nns", "proposal"),
532 ("nns", "registry"),
533 ("nns", "subnet"),
534 ("nns", "topology"),
535 ("sns", "canister"),
536 ("sns", "info"),
537 ("sns", "list"),
538 ("sns", "neuron"),
539 ("sns", "params"),
540 ("sns", "proposal"),
541 ("sns", "token"),
542 ] {
543 let mut tail = vec![OsString::from(leaf), OsString::from("list")];
544
545 apply_global_network(command, &mut tail, Some("ic".to_string()))
546 .expect("NNS and SNS families support the global network");
547
548 assert_eq!(
549 tail,
550 vec![
551 OsString::from(leaf),
552 OsString::from("list"),
553 OsString::from(INTERNAL_NETWORK_OPTION),
554 OsString::from("ic")
555 ]
556 );
557 }
558 }
559
560 #[test]
561 fn non_mainnet_network_is_rejected_before_nns_or_sns_dispatch() {
562 for command in ["nns", "sns"] {
563 let mut tail = if command == "nns" {
564 vec![OsString::from("proposal"), OsString::from("list")]
565 } else {
566 vec![OsString::from("list")]
567 };
568
569 let error = apply_global_network(command, &mut tail, Some("local".to_string()))
570 .expect_err("current NNS and SNS adapters are mainnet-only");
571
572 assert_eq!(error.exit_code(), 2);
573 assert!(error.to_string().contains("supports only the mainnet `ic`"));
574 assert!(error.to_string().contains("received `local`"));
575 assert!(!tail_has_option(&tail, INTERNAL_NETWORK_OPTION));
576 }
577
578 let mut preforwarded_tail = vec![
579 OsString::from("proposal"),
580 OsString::from("list"),
581 OsString::from(INTERNAL_NETWORK_OPTION),
582 OsString::from(MAINNET_NETWORK),
583 ];
584 let error = apply_global_network("nns", &mut preforwarded_tail, Some("local".to_string()))
585 .expect_err("an internal forwarded value must not bypass global validation");
586 assert!(error.to_string().contains("received `local`"));
587
588 for args in [
589 vec![
590 OsString::from("--network"),
591 OsString::from("local"),
592 OsString::from("nns"),
593 OsString::from("proposal"),
594 OsString::from("list"),
595 ],
596 vec![
597 OsString::from("--network"),
598 OsString::from("local"),
599 OsString::from("nns"),
600 OsString::from("governance"),
601 OsString::from("economics"),
602 ],
603 vec![
604 OsString::from("--network"),
605 OsString::from("local"),
606 OsString::from("nns"),
607 OsString::from("neuron"),
608 OsString::from("list"),
609 ],
610 vec![
611 OsString::from("--network"),
612 OsString::from("local"),
613 OsString::from("sns"),
614 OsString::from("list"),
615 ],
616 vec![
617 OsString::from("--network"),
618 OsString::from("local"),
619 OsString::from("sns"),
620 OsString::from("canister"),
621 OsString::from("list"),
622 OsString::from("1"),
623 ],
624 ] {
625 let command = args[2].to_string_lossy().into_owned();
626 let error = run(args).expect_err("non-mainnet network must fail before dispatch");
627
628 assert_eq!(error.exit_code(), 2);
629 assert!(
630 error
631 .to_string()
632 .starts_with(&format!("`icq {command}` currently"))
633 );
634 }
635 }
636
637 #[test]
638 fn global_network_is_rejected_when_the_family_uses_endpoint_identity() {
639 for (command, mut tail) in [
640 (
641 "ic",
642 vec![OsString::from("canister"), OsString::from("info")],
643 ),
644 (
645 "icrc",
646 vec![OsString::from("ledger"), OsString::from("token")],
647 ),
648 ] {
649 let original = tail.clone();
650 let error = apply_global_network(command, &mut tail, Some("ic".to_string()))
651 .expect_err("endpoint-identified family must reject global network");
652
653 assert_eq!(error.exit_code(), 2);
654 assert!(error.to_string().contains("--network is not supported"));
655 assert!(error.to_string().contains(&format!("icq {command}")));
656 assert!(error.to_string().contains("--source-endpoint"));
657 assert_eq!(tail, original);
658 }
659
660 let error = run([
661 OsString::from("--network"),
662 OsString::from("ic"),
663 OsString::from("icrc"),
664 OsString::from("ledger"),
665 OsString::from("token"),
666 OsString::from("ryjl3-tyaaa-aaaaa-aaaba-cai"),
667 ])
668 .expect_err("ICRC global network must fail before dispatch");
669
670 assert_eq!(error.exit_code(), 2);
671 assert!(error.to_string().contains("--source-endpoint"));
672
673 let error = run([
674 OsString::from("icrc"),
675 OsString::from("ledger"),
676 OsString::from("token"),
677 OsString::from("ryjl3-tyaaa-aaaaa-aaaba-cai"),
678 OsString::from("--network"),
679 OsString::from("ic"),
680 ])
681 .expect_err("command-local ICRC network must use the same rejection");
682
683 assert_eq!(error.exit_code(), 2);
684 assert!(error.to_string().contains("--network is not supported"));
685 assert!(!error.to_string().contains("put it before the command"));
686
687 assert!(
688 run([
689 OsString::from("--network"),
690 OsString::from("ic"),
691 OsString::from("icrc"),
692 OsString::from("ledger"),
693 OsString::from("token"),
694 OsString::from("help"),
695 ])
696 .is_ok(),
697 "help must remain available without dispatching a query"
698 );
699
700 let error = run([
701 OsString::from("--network"),
702 OsString::from("ic"),
703 OsString::from("ic"),
704 OsString::from("canister"),
705 OsString::from("info"),
706 OsString::from("ryjl3-tyaaa-aaaaa-aaaba-cai"),
707 ])
708 .expect_err("Dashboard family global network must fail before dispatch");
709
710 assert_eq!(error.exit_code(), 2);
711 assert!(error.to_string().contains("icq ic"));
712 assert!(error.to_string().contains("--source-endpoint"));
713
714 let error = run([
715 OsString::from("ic"),
716 OsString::from("canister"),
717 OsString::from("info"),
718 OsString::from("ryjl3-tyaaa-aaaaa-aaaba-cai"),
719 OsString::from("--network"),
720 OsString::from("ic"),
721 ])
722 .expect_err("command-local Dashboard network must use the same rejection");
723
724 assert_eq!(error.exit_code(), 2);
725 assert!(error.to_string().contains("--network is not supported"));
726 assert!(!error.to_string().contains("put it before the command"));
727 }
728
729 #[test]
730 fn malformed_source_endpoint_returns_typed_error_without_network_io() {
731 let error = run([
732 OsString::from("icrc"),
733 OsString::from("ledger"),
734 OsString::from("token"),
735 OsString::from("ryjl3-tyaaa-aaaaa-aaaba-cai"),
736 OsString::from("--source-endpoint"),
737 OsString::from(":::"),
738 ])
739 .expect_err("malformed endpoint must return an error");
740
741 assert_eq!(error.exit_code(), 1);
742 assert!(error.to_string().contains("failed to build IC agent"));
743 assert!(error.to_string().contains(":::"));
744 }
745
746 #[test]
747 fn sns_nested_commands_dispatch_through_clap_subcommands() {
748 assert!(
749 run([
750 OsString::from("sns"),
751 OsString::from("neuron"),
752 OsString::from("refresh"),
753 OsString::from("--help")
754 ])
755 .is_ok()
756 );
757 assert!(
758 run([
759 OsString::from("sns"),
760 OsString::from("proposal"),
761 OsString::from("cache"),
762 OsString::from("status"),
763 OsString::from("--help")
764 ])
765 .is_ok()
766 );
767 }
768
769 fn assert_run_ok(args: &[&str]) {
770 let args = args.iter().copied().map(OsString::from).collect::<Vec<_>>();
771 if let Err(err) = run(args.clone()) {
772 panic!("expected {args:?} to succeed, got {err}");
773 }
774 }
775}