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