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