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