From b7ff8b4b76236d7ceb1abe87d5c1c4ab2cad61bf Mon Sep 17 00:00:00 2001 From: diogo464 Date: Thu, 19 Jun 2025 09:06:49 +0100 Subject: Add comprehensive CLI tests using assert_cmd MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add assert_cmd, predicates, and tempfile dev dependencies - Create 18 comprehensive integration tests covering all CLI commands - Test success scenarios, error conditions, and edge cases - Use temporary directories for isolated test environments - Cover file creation, process management, and cleanup functionality All tests pass and provide good coverage of the CLI interface. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- Cargo.lock | 267 ++++++++++++++++++++++++++++++++++++++++++++- Cargo.toml | 5 + tests/cli.rs | 346 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 617 insertions(+), 1 deletion(-) create mode 100644 tests/cli.rs diff --git a/Cargo.lock b/Cargo.lock index ab2a0bd..25252ed 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,15 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + [[package]] name = "anstream" version = "0.6.19" @@ -58,6 +67,28 @@ version = "1.0.98" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487" +[[package]] +name = "assert_cmd" +version = "2.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bd389a4b2970a01282ee455294913c0a43724daedcd1a24c3eb0ec1c1320b66" +dependencies = [ + "anstyle", + "bstr", + "doc-comment", + "libc", + "predicates", + "predicates-core", + "predicates-tree", + "wait-timeout", +] + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + [[package]] name = "bitflags" version = "1.3.2" @@ -70,6 +101,17 @@ version = "2.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" +[[package]] +name = "bstr" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "234113d19d0d7d613b40e86fb654acf958910802bcceab913a4f9e7cda03b1a4" +dependencies = [ + "memchr", + "regex-automata", + "serde", +] + [[package]] name = "cfg-if" version = "1.0.1" @@ -143,14 +185,45 @@ name = "demon" version = "0.1.0" dependencies = [ "anyhow", + "assert_cmd", "clap", "ctrlc", "glob", "notify", + "predicates", + "tempfile", "tracing", "tracing-subscriber", ] +[[package]] +name = "difflib" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8" + +[[package]] +name = "doc-comment" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10" + +[[package]] +name = "errno" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cea14ef9355e3beab063703aa9dab15afd25f0667c341310c1e5274bb1d0da18" +dependencies = [ + "libc", + "windows-sys", +] + +[[package]] +name = "fastrand" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + [[package]] name = "filetime" version = "0.2.25" @@ -163,6 +236,15 @@ dependencies = [ "windows-sys", ] +[[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 = "fsevent-sys" version = "4.1.0" @@ -172,6 +254,18 @@ dependencies = [ "libc", ] +[[package]] +name = "getrandom" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasi 0.14.2+wasi-0.2.4", +] + [[package]] name = "glob" version = "0.3.2" @@ -253,12 +347,24 @@ dependencies = [ "redox_syscall", ] +[[package]] +name = "linux-raw-sys" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" + [[package]] name = "log" version = "0.4.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" +[[package]] +name = "memchr" +version = "2.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" + [[package]] name = "mio" version = "1.0.4" @@ -267,7 +373,7 @@ checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c" dependencies = [ "libc", "log", - "wasi", + "wasi 0.11.1+wasi-snapshot-preview1", "windows-sys", ] @@ -283,6 +389,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 = "notify" version = "8.0.0" @@ -318,6 +430,15 @@ dependencies = [ "winapi", ] +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + [[package]] name = "once_cell" version = "1.21.3" @@ -342,6 +463,36 @@ version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" +[[package]] +name = "predicates" +version = "3.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5d19ee57562043d37e82899fade9a22ebab7be9cef5026b07fda9cdd4293573" +dependencies = [ + "anstyle", + "difflib", + "float-cmp", + "normalize-line-endings", + "predicates-core", + "regex", +] + +[[package]] +name = "predicates-core" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "727e462b119fe9c93fd0eb1429a5f7647394014cf3c04ab2c0350eeb09095ffa" + +[[package]] +name = "predicates-tree" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72dd2d6d381dfb73a193c7fca536518d7caee39fc8503f74e7dc0be0531b425c" +dependencies = [ + "predicates-core", + "termtree", +] + [[package]] name = "proc-macro2" version = "1.0.95" @@ -360,6 +511,12 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + [[package]] name = "redox_syscall" version = "0.5.13" @@ -369,6 +526,48 @@ dependencies = [ "bitflags 2.9.1", ] +[[package]] +name = "regex" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" + +[[package]] +name = "rustix" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c71e83d6afe7ff64890ec6b71d6a69bb8a610ab78ce364b3352876bb4c801266" +dependencies = [ + "bitflags 2.9.1", + "errno", + "libc", + "linux-raw-sys", + "windows-sys", +] + [[package]] name = "same-file" version = "1.0.6" @@ -378,6 +577,26 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "serde" +version = "1.0.219" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.219" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "sharded-slab" version = "0.1.7" @@ -410,6 +629,25 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "tempfile" +version = "3.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8a64e3985349f2441a1a9ef0b853f869006c3855f2cda6862a94d26ebb9d6a1" +dependencies = [ + "fastrand", + "getrandom", + "once_cell", + "rustix", + "windows-sys", +] + +[[package]] +name = "termtree" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f50febec83f5ee1df3015341d8bd429f2d1cc62bcba7ea2076759d315084683" + [[package]] name = "thread_local" version = "1.1.9" @@ -494,6 +732,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" @@ -510,6 +757,15 @@ version = "0.11.1+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" +[[package]] +name = "wasi" +version = "0.14.2+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" +dependencies = [ + "wit-bindgen-rt", +] + [[package]] name = "winapi" version = "0.3.9" @@ -613,3 +869,12 @@ name = "windows_x86_64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "wit-bindgen-rt" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" +dependencies = [ + "bitflags 2.9.1", +] diff --git a/Cargo.toml b/Cargo.toml index f22a42a..ee3e226 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,3 +11,8 @@ glob = "0.3.2" notify = "8.0.0" tracing = "0.1.41" tracing-subscriber = "0.3.19" + +[dev-dependencies] +assert_cmd = "2.0.17" +predicates = "3.1.3" +tempfile = "3.20.0" diff --git a/tests/cli.rs b/tests/cli.rs new file mode 100644 index 0000000..7cb68b7 --- /dev/null +++ b/tests/cli.rs @@ -0,0 +1,346 @@ +use assert_cmd::Command; +use predicates::prelude::*; +use std::fs; +use std::time::Duration; +use tempfile::TempDir; + +#[test] +fn test_help_output() { + let mut cmd = Command::cargo_bin("demon").unwrap(); + cmd.args(&["--help"]) + .assert() + .success() + .stdout(predicate::str::contains("daemon process management")) + .stdout(predicate::str::contains("run")) + .stdout(predicate::str::contains("stop")) + .stdout(predicate::str::contains("tail")) + .stdout(predicate::str::contains("cat")) + .stdout(predicate::str::contains("list")) + .stdout(predicate::str::contains("status")) + .stdout(predicate::str::contains("clean")); +} + +#[test] +fn test_version_output() { + let mut cmd = Command::cargo_bin("demon").unwrap(); + cmd.args(&["--version"]) + .assert() + .success() + .stdout(predicate::str::contains("demon 0.1.0")); +} + +#[test] +fn test_run_missing_command() { + let temp_dir = TempDir::new().unwrap(); + + let mut cmd = Command::cargo_bin("demon").unwrap(); + cmd.current_dir(temp_dir.path()) + .args(&["run", "--id", "test"]) + .assert() + .failure() + .stderr(predicate::str::contains("Command cannot be empty")); +} + +#[test] +fn test_run_creates_files() { + let temp_dir = TempDir::new().unwrap(); + + let mut cmd = Command::cargo_bin("demon").unwrap(); + cmd.current_dir(temp_dir.path()) + .args(&["run", "--id", "test", "echo", "hello"]) + .assert() + .success() + .stdout(predicate::str::contains("Started daemon 'test'")); + + // Verify files were created + assert!(temp_dir.path().join("test.pid").exists()); + assert!(temp_dir.path().join("test.stdout").exists()); + assert!(temp_dir.path().join("test.stderr").exists()); + + // Check that stdout contains our output + let stdout_content = fs::read_to_string(temp_dir.path().join("test.stdout")).unwrap(); + assert_eq!(stdout_content.trim(), "hello"); +} + +#[test] +fn test_run_duplicate_process() { + let temp_dir = TempDir::new().unwrap(); + + // Start a long-running process + let mut cmd = Command::cargo_bin("demon").unwrap(); + cmd.current_dir(temp_dir.path()) + .args(&["run", "--id", "long", "sleep", "30"]) + .assert() + .success(); + + // Try to start another with the same ID + let mut cmd = Command::cargo_bin("demon").unwrap(); + cmd.current_dir(temp_dir.path()) + .args(&["run", "--id", "long", "sleep", "5"]) + .assert() + .failure() + .stderr(predicate::str::contains("already running")); + + // Clean up the running process + let mut cmd = Command::cargo_bin("demon").unwrap(); + cmd.current_dir(temp_dir.path()) + .args(&["stop", "--id", "long"]) + .assert() + .success(); +} + +#[test] +fn test_list_empty() { + let temp_dir = TempDir::new().unwrap(); + + let mut cmd = Command::cargo_bin("demon").unwrap(); + cmd.current_dir(temp_dir.path()) + .args(&["list"]) + .assert() + .success() + .stdout(predicate::str::contains("ID")) + .stdout(predicate::str::contains("PID")) + .stdout(predicate::str::contains("STATUS")) + .stdout(predicate::str::contains("No daemon processes found")); +} + +#[test] +fn test_list_with_processes() { + let temp_dir = TempDir::new().unwrap(); + + // Start a process + let mut cmd = Command::cargo_bin("demon").unwrap(); + cmd.current_dir(temp_dir.path()) + .args(&["run", "--id", "test", "echo", "done"]) + .assert() + .success(); + + // List processes + let mut cmd = Command::cargo_bin("demon").unwrap(); + cmd.current_dir(temp_dir.path()) + .args(&["list"]) + .assert() + .success() + .stdout(predicate::str::contains("test")) + .stdout(predicate::str::contains("DEAD")); // Process should be finished by now +} + +#[test] +fn test_cat_output() { + let temp_dir = TempDir::new().unwrap(); + + // Create a process with output + let mut cmd = Command::cargo_bin("demon").unwrap(); + cmd.current_dir(temp_dir.path()) + .args(&[ + "run", "--id", "test", "--", + "sh", "-c", "echo 'stdout line'; echo 'stderr line' >&2" + ]) + .assert() + .success(); + + // Cat the output + let mut cmd = Command::cargo_bin("demon").unwrap(); + cmd.current_dir(temp_dir.path()) + .args(&["cat", "--id", "test"]) + .assert() + .success() + .stdout(predicate::str::contains("stdout line")) + .stdout(predicate::str::contains("stderr line")); +} + +#[test] +fn test_cat_stdout_only() { + let temp_dir = TempDir::new().unwrap(); + + // Create a process with output + let mut cmd = Command::cargo_bin("demon").unwrap(); + cmd.current_dir(temp_dir.path()) + .args(&[ + "run", "--id", "test", "--", + "sh", "-c", "echo 'stdout line'; echo 'stderr line' >&2" + ]) + .assert() + .success(); + + // Cat only stdout + let mut cmd = Command::cargo_bin("demon").unwrap(); + cmd.current_dir(temp_dir.path()) + .args(&["cat", "--id", "test", "--stdout"]) + .assert() + .success() + .stdout(predicate::str::contains("stdout line")) + .stdout(predicate::str::contains("stderr line").not()); +} + +#[test] +fn test_status_nonexistent() { + let temp_dir = TempDir::new().unwrap(); + + let mut cmd = Command::cargo_bin("demon").unwrap(); + cmd.current_dir(temp_dir.path()) + .args(&["status", "--id", "nonexistent"]) + .assert() + .success() + .stdout(predicate::str::contains("NOT FOUND")); +} + +#[test] +fn test_status_dead_process() { + let temp_dir = TempDir::new().unwrap(); + + // Create a short-lived process + let mut cmd = Command::cargo_bin("demon").unwrap(); + cmd.current_dir(temp_dir.path()) + .args(&["run", "--id", "dead", "echo", "hello"]) + .assert() + .success(); + + // Check its status (should be dead) + let mut cmd = Command::cargo_bin("demon").unwrap(); + cmd.current_dir(temp_dir.path()) + .args(&["status", "--id", "dead"]) + .assert() + .success() + .stdout(predicate::str::contains("DEAD")); +} + +#[test] +fn test_stop_nonexistent() { + let temp_dir = TempDir::new().unwrap(); + + let mut cmd = Command::cargo_bin("demon").unwrap(); + cmd.current_dir(temp_dir.path()) + .args(&["stop", "--id", "nonexistent"]) + .assert() + .success() + .stdout(predicate::str::contains("not running")); +} + +#[test] +fn test_stop_process() { + let temp_dir = TempDir::new().unwrap(); + + // Start a long-running process + let mut cmd = Command::cargo_bin("demon").unwrap(); + cmd.current_dir(temp_dir.path()) + .args(&["run", "--id", "long", "sleep", "10"]) + .assert() + .success(); + + // Stop it + let mut cmd = Command::cargo_bin("demon").unwrap(); + cmd.current_dir(temp_dir.path()) + .args(&["stop", "--id", "long"]) + .assert() + .success() + .stdout(predicate::str::contains("terminated gracefully")); + + // Verify PID file is gone + assert!(!temp_dir.path().join("long.pid").exists()); +} + +#[test] +fn test_clean_no_orphans() { + let temp_dir = TempDir::new().unwrap(); + + let mut cmd = Command::cargo_bin("demon").unwrap(); + cmd.current_dir(temp_dir.path()) + .args(&["clean"]) + .assert() + .success() + .stdout(predicate::str::contains("No orphaned files found")); +} + +#[test] +fn test_clean_with_orphans() { + let temp_dir = TempDir::new().unwrap(); + + // Create a dead process + let mut cmd = Command::cargo_bin("demon").unwrap(); + cmd.current_dir(temp_dir.path()) + .args(&["run", "--id", "dead", "echo", "hello"]) + .assert() + .success(); + + // Clean up orphaned files + let mut cmd = Command::cargo_bin("demon").unwrap(); + cmd.current_dir(temp_dir.path()) + .args(&["clean"]) + .assert() + .success() + .stdout(predicate::str::contains("Cleaned up")) + .stdout(predicate::str::contains("orphaned")); + + // Verify files are gone + assert!(!temp_dir.path().join("dead.pid").exists()); + assert!(!temp_dir.path().join("dead.stdout").exists()); + assert!(!temp_dir.path().join("dead.stderr").exists()); +} + +#[test] +fn test_run_with_complex_command() { + let temp_dir = TempDir::new().unwrap(); + + let mut cmd = Command::cargo_bin("demon").unwrap(); + cmd.current_dir(temp_dir.path()) + .args(&[ + "run", "--id", "complex", "--", + "sh", "-c", "for i in 1 2 3; do echo \"line $i\"; done" + ]) + .assert() + .success(); + + // Give the process a moment to complete + std::thread::sleep(Duration::from_millis(100)); + + // Check the output contains all lines + let stdout_content = fs::read_to_string(temp_dir.path().join("complex.stdout")).unwrap(); + assert!(stdout_content.contains("line 1")); + assert!(stdout_content.contains("line 2")); + assert!(stdout_content.contains("line 3")); +} + +#[test] +fn test_timeout_configuration() { + let temp_dir = TempDir::new().unwrap(); + + // Start a process + let mut cmd = Command::cargo_bin("demon").unwrap(); + cmd.current_dir(temp_dir.path()) + .args(&["run", "--id", "timeout-test", "sleep", "5"]) + .assert() + .success(); + + // Stop with custom timeout (should work normally since sleep responds to SIGTERM) + let mut cmd = Command::cargo_bin("demon").unwrap(); + cmd.current_dir(temp_dir.path()) + .args(&["stop", "--id", "timeout-test", "--timeout", "2"]) + .assert() + .success() + .stdout(predicate::str::contains("terminated gracefully")); +} + +#[test] +fn test_invalid_process_id() { + let temp_dir = TempDir::new().unwrap(); + + // Create an invalid PID file + fs::write(temp_dir.path().join("invalid.pid"), "not-a-number").unwrap(); + + // Status should handle it gracefully + let mut cmd = Command::cargo_bin("demon").unwrap(); + cmd.current_dir(temp_dir.path()) + .args(&["status", "--id", "invalid"]) + .assert() + .success() + .stdout(predicate::str::contains("ERROR")); + + // Clean should remove it + let mut cmd = Command::cargo_bin("demon").unwrap(); + cmd.current_dir(temp_dir.path()) + .args(&["clean"]) + .assert() + .success() + .stdout(predicate::str::contains("invalid PID file")); +} \ No newline at end of file -- cgit