1mod args;
2mod backup;
3mod build;
4mod endpoints;
5mod fleets;
6mod install;
7mod list;
8mod manifest;
9mod medic;
10mod output;
11mod replica;
12mod restore;
13mod scaffold;
14mod snapshot;
15mod status;
16#[cfg(test)]
17mod test_support;
18
19use crate::args::{
20 INTERNAL_ICP_OPTION, INTERNAL_NETWORK_OPTION, first_arg_is_help, icp_arg, network_arg,
21 parse_matches,
22};
23use clap::{Arg, ArgAction, Command};
24use std::ffi::OsString;
25use thiserror::Error as ThisError;
26
27const VERSION_TEXT: &str = concat!("canic ", env!("CARGO_PKG_VERSION"));
28const TOP_LEVEL_HELP_TEMPLATE: &str = "{name} {version}\n{about-with-newline}\n{usage-heading} {usage}\n\n{before-help}Options:\n{options}{after-help}\n";
29const COLOR_RESET: &str = "\x1b[0m";
30const COLOR_HEADING: &str = "\x1b[1m";
31const COLOR_GROUP: &str = "\x1b[38;5;245m";
32const COLOR_COMMAND: &str = "\x1b[38;5;109m";
33const COLOR_TIP: &str = "\x1b[38;5;245m";
34const DISPATCH_ARGS: &str = "args";
35
36#[derive(Clone, Copy, Debug, Eq, PartialEq)]
41enum CommandScope {
42 Global,
43 FleetContext,
44 BackupRestore,
45 WorkspaceFiles,
46}
47
48impl CommandScope {
49 const fn heading(self) -> &'static str {
50 match self {
51 Self::Global => "Global commands",
52 Self::FleetContext => "Fleet commands",
53 Self::BackupRestore => "Backup and restore commands",
54 Self::WorkspaceFiles => "Workspace and file commands",
55 }
56 }
57}
58
59#[derive(Clone, Copy, Debug, Eq, PartialEq)]
64struct CommandSpec {
65 name: &'static str,
66 about: &'static str,
67 scope: CommandScope,
68}
69
70const COMMAND_SPECS: &[CommandSpec] = &[
71 CommandSpec {
72 name: "status",
73 about: "Show quick Canic project status",
74 scope: CommandScope::Global,
75 },
76 CommandSpec {
77 name: "fleet",
78 about: "Manage Canic fleets",
79 scope: CommandScope::Global,
80 },
81 CommandSpec {
82 name: "replica",
83 about: "Manage the local ICP replica",
84 scope: CommandScope::Global,
85 },
86 CommandSpec {
87 name: "install",
88 about: "Install and bootstrap a Canic fleet",
89 scope: CommandScope::FleetContext,
90 },
91 CommandSpec {
92 name: "config",
93 about: "Inspect selected fleet config",
94 scope: CommandScope::FleetContext,
95 },
96 CommandSpec {
97 name: "list",
98 about: "List deployed fleet canisters",
99 scope: CommandScope::FleetContext,
100 },
101 CommandSpec {
102 name: "endpoints",
103 about: "List canister Candid endpoints",
104 scope: CommandScope::FleetContext,
105 },
106 CommandSpec {
107 name: "medic",
108 about: "Diagnose local Canic fleet setup",
109 scope: CommandScope::FleetContext,
110 },
111 CommandSpec {
112 name: "snapshot",
113 about: "Capture and download canister snapshots",
114 scope: CommandScope::BackupRestore,
115 },
116 CommandSpec {
117 name: "backup",
118 about: "Verify backup directories and journal status",
119 scope: CommandScope::BackupRestore,
120 },
121 CommandSpec {
122 name: "manifest",
123 about: "Validate fleet backup manifests",
124 scope: CommandScope::BackupRestore,
125 },
126 CommandSpec {
127 name: "restore",
128 about: "Plan or run snapshot restores",
129 scope: CommandScope::BackupRestore,
130 },
131 CommandSpec {
132 name: "build",
133 about: "Build one Canic canister artifact",
134 scope: CommandScope::WorkspaceFiles,
135 },
136];
137
138#[derive(Debug, ThisError)]
143pub enum CliError {
144 #[error("{0}")]
145 Usage(String),
146
147 #[error("backup: {0}")]
148 Backup(String),
149
150 #[error("build: {0}")]
151 Build(String),
152
153 #[error("config: {0}")]
154 Config(String),
155
156 #[error("endpoints: {0}")]
157 Endpoints(String),
158
159 #[error("install: {0}")]
160 Install(String),
161
162 #[error("fleet: {0}")]
163 Fleets(String),
164
165 #[error("list: {0}")]
166 List(String),
167
168 #[error("manifest: {0}")]
169 Manifest(String),
170
171 #[error("medic: {0}")]
172 Medic(String),
173
174 #[error("snapshot: {0}")]
175 Snapshot(String),
176
177 #[error("restore: {0}")]
178 Restore(String),
179
180 #[error("replica: {0}")]
181 Replica(String),
182
183 #[error("status: {0}")]
184 Status(String),
185}
186
187impl From<backup::BackupCommandError> for CliError {
188 fn from(err: backup::BackupCommandError) -> Self {
189 Self::Backup(err.to_string())
190 }
191}
192
193impl From<build::BuildCommandError> for CliError {
194 fn from(err: build::BuildCommandError) -> Self {
195 Self::Build(err.to_string())
196 }
197}
198
199impl From<endpoints::EndpointsCommandError> for CliError {
200 fn from(err: endpoints::EndpointsCommandError) -> Self {
201 Self::Endpoints(err.to_string())
202 }
203}
204
205impl From<install::InstallCommandError> for CliError {
206 fn from(err: install::InstallCommandError) -> Self {
207 Self::Install(err.to_string())
208 }
209}
210
211impl From<fleets::FleetCommandError> for CliError {
212 fn from(err: fleets::FleetCommandError) -> Self {
213 Self::Fleets(err.to_string())
214 }
215}
216
217impl From<list::ListCommandError> for CliError {
218 fn from(err: list::ListCommandError) -> Self {
219 Self::List(err.to_string())
220 }
221}
222
223impl From<manifest::ManifestCommandError> for CliError {
224 fn from(err: manifest::ManifestCommandError) -> Self {
225 Self::Manifest(err.to_string())
226 }
227}
228
229impl From<medic::MedicCommandError> for CliError {
230 fn from(err: medic::MedicCommandError) -> Self {
231 Self::Medic(err.to_string())
232 }
233}
234
235impl From<snapshot::SnapshotCommandError> for CliError {
236 fn from(err: snapshot::SnapshotCommandError) -> Self {
237 Self::Snapshot(err.to_string())
238 }
239}
240
241impl From<restore::RestoreCommandError> for CliError {
242 fn from(err: restore::RestoreCommandError) -> Self {
243 Self::Restore(err.to_string())
244 }
245}
246
247impl From<replica::ReplicaCommandError> for CliError {
248 fn from(err: replica::ReplicaCommandError) -> Self {
249 Self::Replica(err.to_string())
250 }
251}
252
253impl From<status::StatusCommandError> for CliError {
254 fn from(err: status::StatusCommandError) -> Self {
255 Self::Status(err.to_string())
256 }
257}
258
259pub fn run_from_env() -> Result<(), CliError> {
261 run(std::env::args_os().skip(1))
262}
263
264pub fn run<I>(args: I) -> Result<(), CliError>
266where
267 I: IntoIterator<Item = OsString>,
268{
269 let args = args.into_iter().collect::<Vec<_>>();
270 if first_arg_is_help(&args) {
271 println!("{}", usage());
272 return Ok(());
273 }
274 if let Some(option) = command_local_global_option(&args) {
275 return Err(CliError::Usage(format!(
276 "{option} is a top-level option; put it before the command\n\n{}",
277 usage()
278 )));
279 }
280
281 let matches =
282 parse_matches(top_level_dispatch_command(), args).map_err(|_| CliError::Usage(usage()))?;
283 if matches.get_flag("version") {
284 println!("{}", version_text());
285 return Ok(());
286 }
287 let global_icp = matches.get_one::<String>("icp").cloned();
288 let global_network = matches.get_one::<String>("network").cloned();
289
290 let Some((command, subcommand_matches)) = matches.subcommand() else {
291 return Err(CliError::Usage(usage()));
292 };
293 let mut tail = subcommand_matches
294 .get_many::<OsString>(DISPATCH_ARGS)
295 .map(|values| values.cloned().collect::<Vec<_>>())
296 .unwrap_or_default();
297 apply_global_icp(command, &mut tail, global_icp);
298 apply_global_network(command, &mut tail, global_network);
299 let tail = tail.into_iter();
300
301 match command {
302 "backup" => backup::run(tail).map_err(CliError::from),
303 "build" => build::run(tail).map_err(CliError::from),
304 "config" => list::run_config(tail).map_err(|err| CliError::Config(err.to_string())),
305 "endpoints" => endpoints::run(tail).map_err(CliError::from),
306 "fleet" => fleets::run(tail).map_err(CliError::from),
307 "install" => install::run(tail).map_err(CliError::from),
308 "list" => list::run(tail).map_err(CliError::from),
309 "manifest" => manifest::run(tail).map_err(CliError::from),
310 "medic" => medic::run(tail).map_err(CliError::from),
311 "replica" => replica::run(tail).map_err(CliError::from),
312 "snapshot" => snapshot::run(tail).map_err(CliError::from),
313 "status" => status::run(tail).map_err(CliError::from),
314 "restore" => restore::run(tail).map_err(CliError::from),
315 _ => unreachable!("top-level dispatch command only defines known commands"),
316 }
317}
318
319#[must_use]
320pub fn top_level_command() -> Command {
321 let command = Command::new("canic")
322 .version(env!("CARGO_PKG_VERSION"))
323 .about("Operator CLI for Canic install, backup, and restore workflows")
324 .disable_version_flag(true)
325 .arg(
326 Arg::new("version")
327 .short('V')
328 .long("version")
329 .action(ArgAction::SetTrue)
330 .help("Print version"),
331 )
332 .arg(icp_arg().global(true))
333 .arg(network_arg().global(true))
334 .subcommand_help_heading("Commands")
335 .help_template(TOP_LEVEL_HELP_TEMPLATE)
336 .before_help(grouped_command_section(COMMAND_SPECS).join("\n"))
337 .after_help("Run `canic <command> help` for command-specific help.");
338
339 COMMAND_SPECS.iter().fold(command, |command, spec| {
340 command.subcommand(Command::new(spec.name).about(spec.about))
341 })
342}
343
344fn top_level_dispatch_command() -> Command {
345 let command = Command::new("canic")
346 .disable_help_flag(true)
347 .disable_version_flag(true)
348 .arg(
349 Arg::new("version")
350 .short('V')
351 .long("version")
352 .action(ArgAction::SetTrue),
353 );
354 let command = command
355 .arg(icp_arg().global(true))
356 .arg(network_arg().global(true));
357
358 COMMAND_SPECS.iter().fold(command, |command, spec| {
359 command.subcommand(
360 Command::new(spec.name).arg(
361 Arg::new(DISPATCH_ARGS)
362 .num_args(0..)
363 .allow_hyphen_values(true)
364 .trailing_var_arg(true)
365 .value_parser(clap::value_parser!(OsString)),
366 ),
367 )
368 })
369}
370
371fn command_local_global_option(args: &[OsString]) -> Option<&'static str> {
372 let mut index = 0;
373 while index < args.len() {
374 let arg = args[index].to_str()?;
375 if COMMAND_SPECS.iter().any(|spec| spec.name == arg) {
376 return args[index + 1..]
377 .iter()
378 .filter_map(|arg| arg.to_str())
379 .find_map(global_option_name);
380 }
381 index += if matches!(arg, "--icp" | "--network") {
382 2
383 } else {
384 1
385 };
386 }
387 None
388}
389
390fn global_option_name(arg: &str) -> Option<&'static str> {
391 match arg {
392 "--icp" => Some("--icp"),
393 "--network" => Some("--network"),
394 _ if arg.starts_with("--icp=") => Some("--icp"),
395 _ if arg.starts_with("--network=") => Some("--network"),
396 _ => None,
397 }
398}
399
400fn apply_global_icp(command: &str, tail: &mut Vec<OsString>, global_icp: Option<String>) {
401 let Some(global_icp) = global_icp else {
402 return;
403 };
404 if tail_has_option(tail, INTERNAL_ICP_OPTION) {
405 return;
406 }
407 if !command_accepts_global_icp(command, tail) {
408 return;
409 }
410
411 tail.push(OsString::from(INTERNAL_ICP_OPTION));
412 tail.push(OsString::from(global_icp));
413}
414
415fn apply_global_network(command: &str, tail: &mut Vec<OsString>, global_network: Option<String>) {
416 let Some(global_network) = global_network else {
417 return;
418 };
419 if tail_has_option(tail, INTERNAL_NETWORK_OPTION) {
420 return;
421 }
422 if !command_accepts_global_network(command, tail) {
423 return;
424 }
425
426 tail.push(OsString::from(INTERNAL_NETWORK_OPTION));
427 tail.push(OsString::from(global_network));
428}
429
430fn command_accepts_global_icp(command: &str, tail: &[OsString]) -> bool {
431 match command {
432 "endpoints" | "list" | "medic" | "status" => true,
433 "replica" => matches!(
434 tail.first().and_then(|arg| arg.to_str()),
435 Some("start" | "status" | "stop")
436 ),
437 "snapshot" => tail.first().and_then(|arg| arg.to_str()) == Some("download"),
438 "backup" => tail.first().and_then(|arg| arg.to_str()) == Some("create"),
439 "restore" => tail.first().and_then(|arg| arg.to_str()) == Some("run"),
440 _ => false,
441 }
442}
443
444fn command_accepts_global_network(command: &str, tail: &[OsString]) -> bool {
445 match command {
446 "endpoints" | "install" | "list" | "medic" | "status" => true,
447 "fleet" => tail.first().and_then(|arg| arg.to_str()) == Some("list"),
448 "snapshot" => tail.first().and_then(|arg| arg.to_str()) == Some("download"),
449 "backup" => tail.first().and_then(|arg| arg.to_str()) == Some("create"),
450 "restore" => tail.first().and_then(|arg| arg.to_str()) == Some("run"),
451 _ => false,
452 }
453}
454
455fn tail_has_option(tail: &[OsString], name: &str) -> bool {
456 tail.iter().any(|arg| arg.to_str() == Some(name))
457}
458
459#[must_use]
460pub const fn version_text() -> &'static str {
461 VERSION_TEXT
462}
463
464fn usage() -> String {
465 let mut lines = vec![
466 color(
467 COLOR_HEADING,
468 &format!("Canic Operator CLI v{}", env!("CARGO_PKG_VERSION")),
469 ),
470 String::new(),
471 "Usage: canic [OPTIONS] <COMMAND>".to_string(),
472 String::new(),
473 color(COLOR_HEADING, "Commands:"),
474 ];
475 lines.extend(grouped_command_section(COMMAND_SPECS));
476 lines.extend([
477 String::new(),
478 color(COLOR_HEADING, "Options:"),
479 " --icp <path> Path to the icp executable for ICP-backed commands".to_string(),
480 " --network <name> ICP CLI network for networked commands".to_string(),
481 " -V, --version Print version".to_string(),
482 " -h, --help Print help".to_string(),
483 String::new(),
484 format!(
485 "{}Tip:{} Run {} for command-specific help.",
486 COLOR_TIP,
487 COLOR_RESET,
488 color(COLOR_COMMAND, "`canic <command> help`")
489 ),
490 ]);
491 lines.join("\n")
492}
493
494fn grouped_command_section(specs: &[CommandSpec]) -> Vec<String> {
495 let mut lines = Vec::new();
496 let scopes = [
497 CommandScope::Global,
498 CommandScope::FleetContext,
499 CommandScope::BackupRestore,
500 CommandScope::WorkspaceFiles,
501 ];
502 for (index, scope) in scopes.into_iter().enumerate() {
503 lines.push(format!(" {}", color(COLOR_GROUP, scope.heading())));
504 for spec in specs.iter().filter(|spec| spec.scope == scope) {
505 let command = format!("{:<12}", spec.name);
506 lines.push(format!(
507 " {} {}",
508 color(COLOR_COMMAND, &command),
509 spec.about
510 ));
511 }
512 if index + 1 < scopes.len() {
513 lines.push(String::new());
514 }
515 }
516 lines
517}
518
519fn color(code: &str, text: &str) -> String {
520 format!("{code}{text}{COLOR_RESET}")
521}
522
523#[cfg(test)]
524mod tests {
525 use super::*;
526
527 #[test]
529 fn usage_lists_command_families() {
530 let text = usage();
531 let plain = strip_ansi(&text);
532
533 assert!(plain.contains(&format!(
534 "Canic Operator CLI v{}",
535 env!("CARGO_PKG_VERSION")
536 )));
537 assert!(plain.contains("Usage: canic [OPTIONS] <COMMAND>"));
538 assert!(plain.contains("\nCommands:\n"));
539 assert!(plain.contains("Global commands"));
540 assert!(plain.contains("Fleet commands"));
541 assert!(plain.contains("Backup and restore commands"));
542 assert!(plain.contains("Workspace and file commands"));
543 assert!(plain.find(" status") < plain.find(" fleet"));
544 assert!(plain.find(" fleet") < plain.find(" replica"));
545 assert!(plain.find(" replica") < plain.find(" install"));
546 assert!(plain.find(" install") < plain.find(" config"));
547 assert!(plain.find(" config") < plain.find(" list"));
548 assert!(plain.find(" list") < plain.find(" endpoints"));
549 assert!(plain.find(" endpoints") < plain.find(" snapshot"));
550 assert!(plain.find(" snapshot") < plain.find(" backup"));
551 assert!(plain.find(" backup") < plain.find(" manifest"));
552 assert!(plain.find(" manifest") < plain.find(" restore"));
553 assert!(plain.find(" restore") < plain.find(" build"));
554 assert!(plain.contains("Options:"));
555 assert!(plain.contains("--icp <path>"));
556 assert!(plain.contains("--network <name>"));
557 assert!(!plain.contains(" scaffold"));
558 assert!(plain.contains("config"));
559 assert!(plain.contains("list"));
560 assert!(plain.contains("endpoints"));
561 assert!(plain.contains("build"));
562 assert!(!plain.contains(" network"));
563 assert!(!plain.contains(" defaults"));
564 assert!(plain.contains(" status"));
565 assert!(plain.contains("fleet"));
566 assert!(plain.contains("replica"));
567 assert!(plain.contains("install"));
568 assert!(plain.contains("snapshot"));
569 assert!(plain.contains("backup"));
570 assert!(plain.contains("manifest"));
571 assert!(plain.contains("medic"));
572 assert!(plain.contains("restore"));
573 assert!(plain.contains("Tip: Run `canic <command> help`"));
574 assert!(text.contains(COLOR_HEADING));
575 assert!(text.contains(COLOR_GROUP));
576 assert!(text.contains(COLOR_COMMAND));
577 }
578
579 #[test]
581 fn command_family_help_returns_ok() {
582 for args in [
583 &["backup", "help"][..],
584 &["backup", "create", "help"],
585 &["backup", "inspect", "help"],
586 &["backup", "list", "help"],
587 &["backup", "status", "help"],
588 &["backup", "verify", "help"],
589 &["build", "help"],
590 &["config", "help"],
591 &["endpoints", "help"],
592 &["install", "help"],
593 &["fleet"],
594 &["fleet", "help"],
595 &["fleet", "create", "help"],
596 &["fleet", "list", "help"],
597 &["fleet", "delete", "help"],
598 &["replica"],
599 &["replica", "help"],
600 &["replica", "start", "help"],
601 &["replica", "status", "help"],
602 &["replica", "stop", "help"],
603 &["list", "help"],
604 &["restore", "help"],
605 &["restore", "plan", "help"],
606 &["restore", "apply", "help"],
607 &["restore", "run", "help"],
608 &["manifest", "help"],
609 &["manifest", "validate", "help"],
610 &["medic", "help"],
611 &["snapshot", "help"],
612 &["snapshot", "download", "help"],
613 &["status", "help"],
614 ] {
615 assert_run_ok(args);
616 }
617 }
618
619 #[test]
621 fn version_flags_return_ok() {
622 assert_eq!(version_text(), concat!("canic ", env!("CARGO_PKG_VERSION")));
623 assert!(run([OsString::from("--version")]).is_ok());
624 assert!(
625 run([
626 OsString::from("backup"),
627 OsString::from("list"),
628 OsString::from("--dir"),
629 OsString::from("version")
630 ])
631 .is_ok()
632 );
633 assert!(run([OsString::from("backup"), OsString::from("--version")]).is_ok());
634 assert!(
635 run([
636 OsString::from("backup"),
637 OsString::from("list"),
638 OsString::from("--version")
639 ])
640 .is_ok()
641 );
642 assert!(run([OsString::from("build"), OsString::from("--version")]).is_ok());
643 assert!(run([OsString::from("config"), OsString::from("--version")]).is_ok());
644 assert!(run([OsString::from("endpoints"), OsString::from("--version")]).is_ok());
645 assert!(run([OsString::from("install"), OsString::from("--version")]).is_ok());
646 assert!(run([OsString::from("fleet"), OsString::from("--version")]).is_ok());
647 assert!(run([OsString::from("replica"), OsString::from("--version")]).is_ok());
648 assert!(run([OsString::from("status"), OsString::from("--version")]).is_ok());
649 assert!(
650 run([
651 OsString::from("fleet"),
652 OsString::from("create"),
653 OsString::from("--version")
654 ])
655 .is_ok()
656 );
657 assert!(
658 run([
659 OsString::from("replica"),
660 OsString::from("start"),
661 OsString::from("--version")
662 ])
663 .is_ok()
664 );
665 assert!(run([OsString::from("list"), OsString::from("--version")]).is_ok());
666 assert!(run([OsString::from("restore"), OsString::from("--version")]).is_ok());
667 assert!(run([OsString::from("manifest"), OsString::from("--version")]).is_ok());
668 assert!(run([OsString::from("medic"), OsString::from("--version")]).is_ok());
669 assert!(run([OsString::from("snapshot"), OsString::from("--version")]).is_ok());
670 assert!(
671 run([
672 OsString::from("snapshot"),
673 OsString::from("download"),
674 OsString::from("--version")
675 ])
676 .is_ok()
677 );
678 }
679
680 #[test]
681 fn global_icp_is_forwarded_to_commands_that_use_icp() {
682 let mut tail = vec![OsString::from("test")];
683
684 apply_global_icp("medic", &mut tail, Some("/tmp/icp".to_string()));
685
686 assert_eq!(
687 tail,
688 vec![
689 OsString::from("test"),
690 OsString::from(INTERNAL_ICP_OPTION),
691 OsString::from("/tmp/icp")
692 ]
693 );
694 }
695
696 #[test]
697 fn global_icp_does_not_override_internal_forwarded_icp() {
698 let mut tail = vec![
699 OsString::from("test"),
700 OsString::from(INTERNAL_ICP_OPTION),
701 OsString::from("/bin/icp"),
702 ];
703
704 apply_global_icp("medic", &mut tail, Some("/tmp/icp".to_string()));
705
706 assert_eq!(
707 tail,
708 vec![
709 OsString::from("test"),
710 OsString::from(INTERNAL_ICP_OPTION),
711 OsString::from("/bin/icp")
712 ]
713 );
714 }
715
716 #[test]
717 fn global_icp_is_forwarded_only_to_restore_run() {
718 let mut plan_tail = vec![OsString::from("plan")];
719 let mut run_tail = vec![OsString::from("run")];
720
721 apply_global_icp("restore", &mut plan_tail, Some("/tmp/icp".to_string()));
722 apply_global_icp("restore", &mut run_tail, Some("/tmp/icp".to_string()));
723
724 assert_eq!(plan_tail, vec![OsString::from("plan")]);
725 assert_eq!(
726 run_tail,
727 vec![
728 OsString::from("run"),
729 OsString::from(INTERNAL_ICP_OPTION),
730 OsString::from("/tmp/icp")
731 ]
732 );
733 }
734
735 #[test]
736 fn global_icp_is_forwarded_only_to_replica_leaf_commands() {
737 let mut family_tail = Vec::new();
738 let mut start_tail = vec![OsString::from("start")];
739
740 apply_global_icp("replica", &mut family_tail, Some("/tmp/icp".to_string()));
741 apply_global_icp("replica", &mut start_tail, Some("/tmp/icp".to_string()));
742
743 assert!(family_tail.is_empty());
744 assert_eq!(
745 start_tail,
746 vec![
747 OsString::from("start"),
748 OsString::from(INTERNAL_ICP_OPTION),
749 OsString::from("/tmp/icp")
750 ]
751 );
752 }
753
754 #[test]
755 fn global_network_is_forwarded_to_commands_that_use_network() {
756 let mut tail = vec![OsString::from("test")];
757
758 apply_global_network("install", &mut tail, Some("ic".to_string()));
759
760 assert_eq!(
761 tail,
762 vec![
763 OsString::from("test"),
764 OsString::from(INTERNAL_NETWORK_OPTION),
765 OsString::from("ic")
766 ]
767 );
768 }
769
770 #[test]
771 fn global_network_does_not_override_internal_forwarded_network() {
772 let mut tail = vec![
773 OsString::from("test"),
774 OsString::from(INTERNAL_NETWORK_OPTION),
775 OsString::from("local"),
776 ];
777
778 apply_global_network("install", &mut tail, Some("ic".to_string()));
779
780 assert_eq!(
781 tail,
782 vec![
783 OsString::from("test"),
784 OsString::from(INTERNAL_NETWORK_OPTION),
785 OsString::from("local")
786 ]
787 );
788 }
789
790 #[test]
791 fn global_network_is_forwarded_only_to_restore_run() {
792 let mut plan_tail = vec![OsString::from("plan")];
793 let mut run_tail = vec![OsString::from("run")];
794
795 apply_global_network("restore", &mut plan_tail, Some("ic".to_string()));
796 apply_global_network("restore", &mut run_tail, Some("ic".to_string()));
797
798 assert_eq!(plan_tail, vec![OsString::from("plan")]);
799 assert_eq!(
800 run_tail,
801 vec![
802 OsString::from("run"),
803 OsString::from(INTERNAL_NETWORK_OPTION),
804 OsString::from("ic")
805 ]
806 );
807 }
808
809 #[test]
810 fn global_network_is_forwarded_only_to_fleet_list() {
811 let mut create_tail = vec![OsString::from("create")];
812 let mut list_tail = vec![OsString::from("list")];
813
814 apply_global_network("fleet", &mut create_tail, Some("local".to_string()));
815 apply_global_network("fleet", &mut list_tail, Some("local".to_string()));
816
817 assert_eq!(create_tail, vec![OsString::from("create")]);
818 assert_eq!(
819 list_tail,
820 vec![
821 OsString::from("list"),
822 OsString::from(INTERNAL_NETWORK_OPTION),
823 OsString::from("local")
824 ]
825 );
826 }
827
828 #[test]
829 fn command_local_global_options_are_hard_rejected() {
830 assert!(matches!(
831 run([
832 OsString::from("status"),
833 OsString::from("--network"),
834 OsString::from("local")
835 ]),
836 Err(CliError::Usage(_))
837 ));
838 assert!(matches!(
839 run([
840 OsString::from("medic"),
841 OsString::from("test"),
842 OsString::from("--icp"),
843 OsString::from("icp")
844 ]),
845 Err(CliError::Usage(_))
846 ));
847 }
848
849 fn strip_ansi(text: &str) -> String {
851 let mut plain = String::new();
852 let mut chars = text.chars().peekable();
853 while let Some(ch) = chars.next() {
854 if ch == '\x1b' && chars.peek() == Some(&'[') {
855 chars.next();
856 for ch in chars.by_ref() {
857 if ch == 'm' {
858 break;
859 }
860 }
861 continue;
862 }
863 plain.push(ch);
864 }
865 plain
866 }
867
868 fn assert_run_ok(raw_args: &[&str]) {
870 let args = raw_args.iter().map(OsString::from).collect::<Vec<_>>();
871 assert!(
872 run(args).is_ok(),
873 "expected successful run for {raw_args:?}"
874 );
875 }
876}