From 525178e191f54281c8002187c462c3945a791feb Mon Sep 17 00:00:00 2001 From: ADD-SP Date: Sun, 15 Feb 2026 07:07:25 +0000 Subject: [PATCH] tests(cli): use `assert_cmd` and `predicates` for CLI testing --- Cargo.lock | 109 +++++++++++++++++++++++++++ Cargo.toml | 2 + tests/cli_tests.rs | 179 ++++++++++++++++++++++++--------------------- 3 files changed, 206 insertions(+), 84 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 0e2cd84..945a0a5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -71,12 +71,33 @@ dependencies = [ "serde_json", ] +[[package]] +name = "assert_cmd" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c5bcfa8749ac45dd12cb11055aeeb6b27a3895560d60d71e3c23bf979e60514" +dependencies = [ + "anstyle", + "bstr", + "libc", + "predicates", + "predicates-core", + "predicates-tree", + "wait-timeout", +] + [[package]] name = "atomic-waker" version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + [[package]] name = "aws-lc-rs" version = "1.15.4" @@ -178,6 +199,17 @@ version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" +[[package]] +name = "bstr" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63044e1ae8e69f3b5a92c736ca6269b8d12fa7efe39bf34ddb06d102cf0e2cab" +dependencies = [ + "memchr", + "regex-automata", + "serde", +] + [[package]] name = "bumpalo" version = "3.19.1" @@ -270,6 +302,7 @@ checksum = "3a822ea5bc7590f9d40f1ba12c0dc3c2760f3482c6984db1573ad11031420831" name = "clawshell" version = "0.0.1" dependencies = [ + "assert_cmd", "axum", "bytes", "clap", @@ -281,6 +314,7 @@ dependencies = [ "inquire", "insta", "nix", + "predicates", "regex", "reqwest", "serde", @@ -448,6 +482,12 @@ dependencies = [ "syn", ] +[[package]] +name = "difflib" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8" + [[package]] name = "displaydoc" version = "0.2.5" @@ -531,6 +571,15 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" +[[package]] +name = "float-cmp" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b09cf3155332e944990140d967ff5eceb70df778b34f77d8075db46e4704e6d8" +dependencies = [ + "num-traits", +] + [[package]] name = "fnv" version = "1.0.7" @@ -1144,6 +1193,12 @@ dependencies = [ "libc", ] +[[package]] +name = "normalize-line-endings" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61807f77802ff30975e01f4f071c8ba10c022052f98b3294119f3e615d13e5be" + [[package]] name = "nu-ansi-term" version = "0.50.3" @@ -1153,6 +1208,15 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + [[package]] name = "num_cpus" version = "1.17.0" @@ -1240,6 +1304,36 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "predicates" +version = "3.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ada8f2932f28a27ee7b70dd6c1c39ea0675c55a36879ab92f3a715eaa1e63cfe" +dependencies = [ + "anstyle", + "difflib", + "float-cmp", + "normalize-line-endings", + "predicates-core", + "regex", +] + +[[package]] +name = "predicates-core" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cad38746f3166b4031b1a0d39ad9f954dd291e7854fcc0eed52ee41a0b50d144" + +[[package]] +name = "predicates-tree" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0de1b847b39c8131db0467e9df1ff60e6d0562ab8e9a16e568ad0fdb372e2f2" +dependencies = [ + "predicates-core", + "termtree", +] + [[package]] name = "proc-macro2" version = "1.0.106" @@ -1818,6 +1912,12 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "termtree" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f50febec83f5ee1df3015341d8bd429f2d1cc62bcba7ea2076759d315084683" + [[package]] name = "thiserror" version = "1.0.69" @@ -2164,6 +2264,15 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" +[[package]] +name = "wait-timeout" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ac3b126d3914f9849036f826e054cbabdc8519970b8998ddaf3b5bd3c65f11" +dependencies = [ + "libc", +] + [[package]] name = "walkdir" version = "2.5.0" diff --git a/Cargo.toml b/Cargo.toml index f48d89f..eae24d6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -34,6 +34,8 @@ tower = { version = "0.5.3", features = ["util"] } wiremock = "0.6" datatest-stable = "0.3" insta = { version = "1", features = ["yaml"] } +assert_cmd = "2" +predicates = "3" [[test]] name = "config_fixtures" diff --git a/tests/cli_tests.rs b/tests/cli_tests.rs index 21fd0fb..7253814 100644 --- a/tests/cli_tests.rs +++ b/tests/cli_tests.rs @@ -1,9 +1,10 @@ -use std::process::Command; +use assert_cmd::Command; +use assert_cmd::cargo::cargo_bin_cmd; +use predicates::prelude::*; +use predicates::str::contains; -fn cargo_bin() -> Command { - let mut cmd = Command::new(env!("CARGO")); - cmd.args(["run", "--quiet", "--"]); - cmd +fn cmd() -> Command { + cargo_bin_cmd!("clawshell") } fn pid_file_path() -> std::path::PathBuf { @@ -33,83 +34,85 @@ fn ensure_log_dir() -> bool { #[test] fn test_help_output() { - let output = cargo_bin().arg("help").output().unwrap(); - let stdout = String::from_utf8_lossy(&output.stdout); - assert!(stdout.contains("ClawShell")); - assert!(stdout.contains("start")); - assert!(stdout.contains("stop")); - assert!(stdout.contains("status")); - assert!(stdout.contains("restart")); - assert!(stdout.contains("logs")); - assert!(stdout.contains("config")); - assert!(stdout.contains("onboard")); - assert!(stdout.contains("version")); + cmd() + .arg("help") + .assert() + .success() + .stdout(contains("ClawShell")) + .stdout(contains("start")) + .stdout(contains("stop")) + .stdout(contains("status")) + .stdout(contains("restart")) + .stdout(contains("logs")) + .stdout(contains("config")) + .stdout(contains("onboard")) + .stdout(contains("version")); } #[test] fn test_version_output() { - let output = cargo_bin().arg("version").output().unwrap(); - let stdout = String::from_utf8_lossy(&output.stdout); - let lower = stdout.to_lowercase(); - assert!(lower.contains("clawshell")); - assert!(stdout.contains("v0.0.1")); - assert!(lower.contains("openclaw")); + cmd() + .arg("version") + .assert() + .success() + .stdout(contains("clawshell").or(contains("ClawShell").or(contains("Clawshell")))) + .stdout(contains("v0.0.1")) + .stdout(contains("openclaw").or(contains("OpenClaw"))); } #[test] fn test_status_when_not_running() { let _ = std::fs::remove_file(pid_file_path()); - let output = cargo_bin().arg("status").output().unwrap(); - let stdout = String::from_utf8_lossy(&output.stdout); - assert!(stdout.contains("not running")); + cmd() + .arg("status") + .assert() + .success() + .stdout(contains("not running")); } #[test] fn test_stop_when_not_running() { let _ = std::fs::remove_file(pid_file_path()); - let output = cargo_bin().arg("stop").output().unwrap(); - let stdout = String::from_utf8_lossy(&output.stdout); - assert!(stdout.contains("not running")); + cmd() + .arg("stop") + .assert() + .success() + .stdout(contains("not running")); } #[test] fn test_start_with_invalid_config() { - let output = cargo_bin() + cmd() .args([ "start", "--config", "/nonexistent/config.toml", "--foreground", ]) - .output() - .unwrap(); - let stderr = String::from_utf8_lossy(&output.stderr); - assert!(!output.status.success() || stderr.contains("Failed to load configuration")); + .assert() + .failure(); } #[test] fn test_config_display_missing_file() { - let output = cargo_bin() + cmd() .args(["config", "--file", "/nonexistent/config.toml"]) - .output() - .unwrap(); - let stderr = String::from_utf8_lossy(&output.stderr); - assert!(stderr.contains("not found") || !output.status.success()); + .assert() + .failure(); } #[test] fn test_config_display_example_file() { - let output = cargo_bin() + cmd() .args(["config", "--file", "clawshell.example.toml"]) - .output() - .unwrap(); - let stdout = String::from_utf8_lossy(&output.stdout); - assert!(stdout.contains("ClawShell")); - assert!(stdout.contains("Configuration")); - assert!(stdout.contains("Listen:")); - assert!(stdout.contains("configured")); + .assert() + .success() + .stdout(contains("ClawShell")) + .stdout(contains("Configuration")) + .stdout(contains("Listen:")) + .stdout(contains("configured")); } /// Combined log tests to avoid race conditions on the shared log file. @@ -128,9 +131,11 @@ fn test_logs_commands() { // Test 1: No log file let _ = std::fs::remove_file(&log_path); - let output = cargo_bin().arg("logs").output().unwrap(); - let stdout = String::from_utf8_lossy(&output.stdout); - assert!(stdout.contains("No logs available")); + cmd() + .arg("logs") + .assert() + .success() + .stdout(contains("No logs available")); // Test 2: Level filter std::fs::write( @@ -139,14 +144,13 @@ fn test_logs_commands() { ) .unwrap(); - let output = cargo_bin() + cmd() .args(["logs", "--level", "error"]) - .output() - .unwrap(); - let stdout = String::from_utf8_lossy(&output.stdout); - assert!(stdout.contains("ERROR"), "stdout was: {}", stdout); - assert!(!stdout.contains("INFO Starting")); - assert!(!stdout.contains("DEBUG")); + .assert() + .success() + .stdout(contains("ERROR")) + .stdout(contains("INFO Starting").not()) + .stdout(contains("DEBUG").not()); // Test 3: Keyword filter std::fs::write( @@ -155,13 +159,12 @@ fn test_logs_commands() { ) .unwrap(); - let output = cargo_bin() + cmd() .args(["logs", "--filter", "timeout"]) - .output() - .unwrap(); - let stdout = String::from_utf8_lossy(&output.stdout); - assert!(stdout.contains("timeout")); - assert!(!stdout.contains("Starting")); + .assert() + .success() + .stdout(contains("timeout")) + .stdout(contains("Starting").not()); // Test 4: Num limit let lines: String = (1..=20) @@ -169,10 +172,12 @@ fn test_logs_commands() { .collect(); std::fs::write(&log_path, &lines).unwrap(); - let output = cargo_bin().args(["logs", "--num", "5"]).output().unwrap(); - let stdout = String::from_utf8_lossy(&output.stdout); - assert!(stdout.contains("Line 16"), "stdout was: {}", stdout); - assert!(stdout.contains("Line 20")); + cmd() + .args(["logs", "--num", "5"]) + .assert() + .success() + .stdout(contains("Line 16")) + .stdout(contains("Line 20")); // Clean up let _ = std::fs::remove_file(&log_path); @@ -180,38 +185,44 @@ fn test_logs_commands() { #[test] fn test_help_subcommand_examples() { - let output = cargo_bin().arg("--help").output().unwrap(); - let stdout = String::from_utf8_lossy(&output.stdout); - assert!(stdout.contains("EXAMPLES")); - assert!(stdout.contains("clawshell start")); - assert!(stdout.contains("clawshell stop")); + cmd() + .arg("--help") + .assert() + .success() + .stdout(contains("EXAMPLES")) + .stdout(contains("clawshell start")) + .stdout(contains("clawshell stop")); } #[test] fn test_onboard_requires_root() { if !nix::unistd::getuid().is_root() { - let output = cargo_bin().arg("onboard").output().unwrap(); - let stdout = String::from_utf8_lossy(&output.stdout); - let lower = stdout.to_lowercase(); - assert!(lower.contains("administrative privileges")); - assert!(stdout.contains("sudo clawshell onboard")); + cmd() + .arg("onboard") + .assert() + .failure() + .stdout(contains("Administrative Privileges Required")) + .stdout(contains("sudo clawshell onboard")); } } #[test] fn test_uninstall_requires_root() { if !nix::unistd::getuid().is_root() { - let output = cargo_bin().args(["uninstall", "--yes"]).output().unwrap(); - let stdout = String::from_utf8_lossy(&output.stdout); - let lower = stdout.to_lowercase(); - assert!(lower.contains("administrative privileges")); - assert!(stdout.contains("sudo clawshell uninstall")); + cmd() + .args(["uninstall", "--yes"]) + .assert() + .failure() + .stdout(contains("Administrative Privileges Required")) + .stdout(contains("sudo clawshell uninstall")); } } #[test] fn test_help_shows_uninstall() { - let output = cargo_bin().arg("help").output().unwrap(); - let stdout = String::from_utf8_lossy(&output.stdout); - assert!(stdout.contains("uninstall")); + cmd() + .arg("help") + .assert() + .success() + .stdout(contains("uninstall")); }