From db359d031b4e14b7969ef43ab3ce53ebfe10b710 Mon Sep 17 00:00:00 2001 From: Jakob Ankarhem Date: Fri, 24 Apr 2026 16:29:49 +0200 Subject: [PATCH 1/2] test: add CLI unit tests, integration tests, and criterion benchmarks --- Cargo.lock | 267 ++++++++++++++++++++++++++ Cargo.toml | 11 ++ benches/json_normalization.rs | 41 ++++ benches/timerange_parsing.rs | 27 +++ src/presentation/cli.rs | 348 ++++++++++++++++++++++++++++++++++ tests/cli_integration.rs | 39 ++++ 6 files changed, 733 insertions(+) create mode 100644 benches/json_normalization.rs create mode 100644 benches/timerange_parsing.rs create mode 100644 tests/cli_integration.rs diff --git a/Cargo.lock b/Cargo.lock index 0bc53ca..39a2c1e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,21 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "anes" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" + [[package]] name = "anstream" version = "1.0.0" @@ -69,6 +84,12 @@ 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.16.3" @@ -115,6 +136,12 @@ version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" +[[package]] +name = "cast" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" + [[package]] name = "cc" version = "1.2.60" @@ -145,6 +172,33 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" +[[package]] +name = "ciborium" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42e69ffd6f0917f5c029256a24d0161db17cea3997d185db0d35926308770f0e" +dependencies = [ + "ciborium-io", + "ciborium-ll", + "serde", +] + +[[package]] +name = "ciborium-io" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05afea1e0a06c9be33d539b876f1ce3692f4afea2cb41f740e7743225ed1c757" + +[[package]] +name = "ciborium-ll" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57663b653d948a338bfb3eeba9bb2fd5fcfaecb9e199e87e1eda4d9e8b240fd9" +dependencies = [ + "ciborium-io", + "half", +] + [[package]] name = "clap" version = "4.6.1" @@ -226,6 +280,73 @@ version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" +[[package]] +name = "criterion" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2b12d017a929603d80db1831cd3a24082f8137ce19c69e6447f54f5fc8d692f" +dependencies = [ + "anes", + "cast", + "ciborium", + "clap", + "criterion-plot", + "is-terminal", + "itertools", + "num-traits", + "once_cell", + "oorandom", + "plotters", + "rayon", + "regex", + "serde", + "serde_derive", + "serde_json", + "tinytemplate", + "walkdir", +] + +[[package]] +name = "criterion-plot" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b50826342786a51a89e2da3a28f1c32b06e387201bc2d19791f622c673706b1" +dependencies = [ + "cast", + "itertools", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "crunchy" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" + [[package]] name = "deranged" version = "0.5.8" @@ -273,6 +394,12 @@ version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + [[package]] name = "equivalent" version = "1.0.2" @@ -388,6 +515,7 @@ version = "0.0.3-alpha" dependencies = [ "async-trait", "clap", + "criterion", "dirs", "exn", "humantime", @@ -405,6 +533,17 @@ dependencies = [ "url", ] +[[package]] +name = "half" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" +dependencies = [ + "cfg-if", + "crunchy", + "zerocopy", +] + [[package]] name = "hashbrown" version = "0.17.0" @@ -417,6 +556,12 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +[[package]] +name = "hermit-abi" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" + [[package]] name = "http" version = "1.4.0" @@ -649,12 +794,32 @@ dependencies = [ "serde", ] +[[package]] +name = "is-terminal" +version = "0.4.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46" +dependencies = [ + "hermit-abi", + "libc", + "windows-sys 0.61.2", +] + [[package]] name = "is_terminal_polyfill" version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" +[[package]] +name = "itertools" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.18" @@ -789,6 +954,15 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c6673768db2d862beb9b39a78fdcb1a69439615d5794a1be50caa9bc92c81967" +[[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.4" @@ -801,6 +975,12 @@ version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" +[[package]] +name = "oorandom" +version = "11.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e" + [[package]] name = "openssl-probe" version = "0.2.1" @@ -825,6 +1005,34 @@ version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" +[[package]] +name = "plotters" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5aeb6f403d7a4911efb1e33402027fc44f29b5bf6def3effcc22d7bb75f2b747" +dependencies = [ + "num-traits", + "plotters-backend", + "plotters-svg", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "plotters-backend" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df42e13c12958a16b3f7f4386b9ab1f3e7933914ecea48da7139435263a4172a" + +[[package]] +name = "plotters-svg" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51bae2ac328883f7acdfea3d66a7c35751187f870bc81f94563733a154d7a670" +dependencies = [ + "plotters-backend", +] + [[package]] name = "potential_utf" version = "0.1.5" @@ -958,6 +1166,26 @@ dependencies = [ "getrandom 0.3.4", ] +[[package]] +name = "rayon" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + [[package]] name = "redox_users" version = "0.5.2" @@ -969,6 +1197,35 @@ dependencies = [ "thiserror 2.0.18", ] +[[package]] +name = "regex" +version = "1.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" + [[package]] name = "reqwest" version = "0.13.2" @@ -1411,6 +1668,16 @@ dependencies = [ "zerovec", ] +[[package]] +name = "tinytemplate" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be4d6b5f19ff7664e8c98d03e2139cb510db9b0a60b55f8e8709b689d939b6bc" +dependencies = [ + "serde", + "serde_json", +] + [[package]] name = "tinyvec" version = "1.11.0" diff --git a/Cargo.toml b/Cargo.toml index 35b1fc0..ab840b9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,6 +23,17 @@ toml = "1" time = { version = "0.3", features = ["parsing", "formatting"] } url = { version = "2", features = ["serde"] } +[dev-dependencies] +criterion = "0.5" + +[[bench]] +name = "timerange_parsing" +harness = false + +[[bench]] +name = "json_normalization" +harness = false + [profile.dev] debug = 0 diff --git a/benches/json_normalization.rs b/benches/json_normalization.rs new file mode 100644 index 0000000..e15ca6a --- /dev/null +++ b/benches/json_normalization.rs @@ -0,0 +1,41 @@ +use criterion::{black_box, criterion_group, criterion_main, Criterion}; +use serde_json::json; + +fn bench_json_serialize(c: &mut Criterion) { + let messages: Vec> = (0..100) + .map(|i| { + let mut map = serde_json::Map::new(); + map.insert("message".to_string(), json!(format!("test message {i}"))); + map.insert("level".to_string(), json!("ERROR")); + map.insert("timestamp".to_string(), json!("2026-01-15T14:30:00Z")); + map + }) + .collect(); + + c.bench_function("serialize_100_messages", |b| { + b.iter(|| serde_json::to_string(black_box(&messages)).unwrap()) + }); +} + +fn bench_json_deserialize(c: &mut Criterion) { + let messages: Vec> = (0..100) + .map(|i| { + let mut map = serde_json::Map::new(); + map.insert("message".to_string(), json!(format!("test message {i}"))); + map.insert("level".to_string(), json!("ERROR")); + map.insert("timestamp".to_string(), json!("2026-01-15T14:30:00Z")); + map + }) + .collect(); + let serialized = serde_json::to_string(&messages).unwrap(); + + c.bench_function("deserialize_100_messages", |b| { + b.iter(|| { + let _: Vec> = + serde_json::from_str(black_box(&serialized)).unwrap(); + }) + }); +} + +criterion_group!(benches, bench_json_serialize, bench_json_deserialize); +criterion_main!(benches); diff --git a/benches/timerange_parsing.rs b/benches/timerange_parsing.rs new file mode 100644 index 0000000..4cbc703 --- /dev/null +++ b/benches/timerange_parsing.rs @@ -0,0 +1,27 @@ +use criterion::{black_box, criterion_group, criterion_main, Criterion}; +use graylog_cli::domain::timerange::{CommandTimerange, TimerangeInput}; + +fn bench_relative_timerange(c: &mut Criterion) { + let input = TimerangeInput { + relative: Some("15m".to_string()), + from: None, + to: None, + }; + c.bench_function("relative_timerange_15m", |b| { + b.iter(|| CommandTimerange::from_input(black_box(input.clone())).unwrap()) + }); +} + +fn bench_absolute_timerange(c: &mut Criterion) { + let input = TimerangeInput { + relative: None, + from: Some("2026-01-01T00:00:00Z".to_string()), + to: Some("2026-01-02T00:00:00Z".to_string()), + }; + c.bench_function("absolute_timerange", |b| { + b.iter(|| CommandTimerange::from_input(black_box(input.clone())).unwrap()) + }); +} + +criterion_group!(benches, bench_relative_timerange, bench_absolute_timerange); +criterion_main!(benches); diff --git a/src/presentation/cli.rs b/src/presentation/cli.rs index 81b70b6..5175fb5 100644 --- a/src/presentation/cli.rs +++ b/src/presentation/cli.rs @@ -354,3 +354,351 @@ impl From for AggregationType { } } } + +#[cfg(test)] +mod tests { + use super::*; + use clap::Parser; + + fn parse(args: &[&str]) -> Result { + Cli::try_parse_from(args) + } + + // --- Auth tests --- + + #[test] + fn auth_requires_url_and_token() { + let result = parse(&["graylog-cli", "auth"]); + assert!(result.is_err(), "auth without --url and --token should fail"); + } + + #[test] + fn auth_succeeds_with_url_and_token() { + let cli = parse(&[ + "graylog-cli", + "auth", + "--url", + "http://localhost:9000", + "--token", + "secret", + ]) + .expect("auth with --url and --token should parse"); + + match cli.command { + Commands::Auth(args) => { + assert_eq!(args.url, "http://localhost:9000"); + assert_eq!(args.token, "secret"); + } + _ => panic!("expected Auth command"), + } + } + + // --- Search tests --- + + #[test] + fn search_requires_query() { + let result = parse(&["graylog-cli", "search"]); + assert!(result.is_err(), "search without query should fail"); + } + + #[test] + fn search_succeeds_with_query() { + let cli = + parse(&["graylog-cli", "search", "level:ERROR"]).expect("search with query should parse"); + + match cli.command { + Commands::Search(args) => { + assert_eq!(args.query, "level:ERROR"); + } + _ => panic!("expected Search command"), + } + } + + #[test] + fn search_with_all_options() { + let cli = parse(&[ + "graylog-cli", + "search", + "level:ERROR", + "--field", + "message", + "--limit", + "10", + "--offset", + "5", + "--sort", + "source", + "--sort-direction", + "asc", + "--group-by", + "level", + "--all-pages", + "--all-fields", + "--stream-id", + "abc123", + ]) + .expect("search with all options should parse"); + + match cli.command { + Commands::Search(args) => { + assert_eq!(args.query, "level:ERROR"); + assert_eq!(args.field, vec!["message"]); + assert_eq!(args.limit, Some(10)); + assert_eq!(args.offset, Some(5)); + assert_eq!(args.sort.as_deref(), Some("source")); + assert_eq!(args.sort_direction, Some(SortDirectionArg::Asc)); + assert_eq!(args.group_by.as_deref(), Some("level")); + assert!(args.all_pages); + assert!(args.all_fields); + assert_eq!(args.stream_id, vec!["abc123"]); + } + _ => panic!("expected Search command"), + } + } + + // --- Aggregate tests --- + + #[test] + fn aggregate_requires_query_aggregation_type_and_field() { + assert!( + parse(&["graylog-cli", "aggregate"]).is_err(), + "aggregate with no args should fail" + ); + assert!( + parse(&["graylog-cli", "aggregate", "level:ERROR"]).is_err(), + "aggregate without --aggregation-type and --field should fail" + ); + assert!( + parse(&[ + "graylog-cli", + "aggregate", + "level:ERROR", + "--aggregation-type", + "terms" + ]) + .is_err(), + "aggregate without --field should fail" + ); + } + + #[test] + fn aggregate_succeeds_with_required_args() { + let cli = parse(&[ + "graylog-cli", + "aggregate", + "level:ERROR", + "--aggregation-type", + "terms", + "--field", + "level", + ]) + .expect("aggregate with required args should parse"); + + match cli.command { + Commands::Aggregate(args) => { + assert_eq!(args.query, "level:ERROR"); + assert_eq!(args.aggregation_type, AggregationTypeArg::Terms); + assert_eq!(args.field, "level"); + } + _ => panic!("expected Aggregate command"), + } + } + + #[test] + fn aggregate_date_histogram_requires_interval_via_validate() { + let cli = parse(&[ + "graylog-cli", + "aggregate", + "level:ERROR", + "--aggregation-type", + "date_histogram", + "--field", + "timestamp", + ]) + .expect("clap parsing should succeed for date_histogram without --interval"); + + let err = cli + .validate() + .expect_err("validate should reject date_histogram without --interval"); + + let msg = err.to_string(); + assert!( + msg.contains("interval"), + "error should mention 'interval': {msg}" + ); + } + + // --- Count-by-level tests --- + + #[test] + fn count_by_level_needs_no_positional_args() { + let cli = parse(&["graylog-cli", "count-by-level"]) + .expect("count-by-level with no args should parse"); + + assert!( + matches!(cli.command, Commands::CountByLevel(_)), + "expected CountByLevel command" + ); + } + + // --- Streams subcommand tests --- + + #[test] + fn streams_list_parses() { + let cli = + parse(&["graylog-cli", "streams", "list"]).expect("streams list should parse"); + + match cli.command { + Commands::Streams { command } => { + assert!(matches!(command, StreamsCommands::List)); + } + _ => panic!("expected Streams command"), + } + } + + #[test] + fn streams_show_parses() { + let cli = + parse(&["graylog-cli", "streams", "show", "abc"]).expect("streams show should parse"); + + match cli.command { + Commands::Streams { command } => match command { + StreamsCommands::Show(args) => { + assert_eq!(args.stream_id, "abc"); + } + _ => panic!("expected Show subcommand"), + }, + _ => panic!("expected Streams command"), + } + } + + #[test] + fn streams_find_parses() { + let cli = + parse(&["graylog-cli", "streams", "find", "test"]).expect("streams find should parse"); + + match cli.command { + Commands::Streams { command } => match command { + StreamsCommands::Find(args) => { + assert_eq!(args.name, "test"); + } + _ => panic!("expected Find subcommand"), + }, + _ => panic!("expected Streams command"), + } + } + + // --- Ping tests --- + + #[test] + fn ping_needs_no_args() { + let cli = parse(&["graylog-cli", "ping"]).expect("ping should parse"); + assert!(matches!(cli.command, Commands::Ping)); + } + + // --- Fields tests --- + + #[test] + fn fields_needs_no_args() { + let cli = parse(&["graylog-cli", "fields"]).expect("fields should parse"); + assert!(matches!(cli.command, Commands::Fields)); + } + + // --- Limit validation tests --- + + #[test] + fn search_rejects_limit_zero() { + let result = parse(&["graylog-cli", "search", "test", "--limit", "0"]); + assert!( + result.is_err(), + "search --limit 0 should be rejected by clap value parser" + ); + } + + #[test] + fn search_rejects_limit_above_1000() { + let result = parse(&["graylog-cli", "search", "test", "--limit", "1001"]); + assert!( + result.is_err(), + "search --limit 1001 should be rejected by clap value parser" + ); + } + + #[test] + fn streams_search_rejects_limit_zero() { + let result = parse(&["graylog-cli", "streams", "search", "abc", "test", "--limit", "0"]); + assert!( + result.is_err(), + "streams search --limit 0 should be rejected by clap value parser" + ); + } + + #[test] + fn streams_search_rejects_limit_above_100() { + let result = parse(&[ + "graylog-cli", + "streams", + "search", + "abc", + "test", + "--limit", + "101", + ]); + assert!( + result.is_err(), + "streams search --limit 101 should be rejected by clap value parser" + ); + } + + // --- Sort-direction validation tests --- + + #[test] + fn sort_direction_accepts_asc_and_desc() { + let cli_asc = parse(&[ + "graylog-cli", + "search", + "test", + "--sort-direction", + "asc", + ]) + .expect("asc should be accepted"); + + match cli_asc.command { + Commands::Search(args) => { + assert_eq!(args.sort_direction, Some(SortDirectionArg::Asc)); + } + _ => panic!("expected Search command"), + } + + let cli_desc = parse(&[ + "graylog-cli", + "search", + "test", + "--sort-direction", + "desc", + ]) + .expect("desc should be accepted"); + + match cli_desc.command { + Commands::Search(args) => { + assert_eq!(args.sort_direction, Some(SortDirectionArg::Desc)); + } + _ => panic!("expected Search command"), + } + } + + #[test] + fn sort_direction_rejects_invalid_value() { + let result = parse(&[ + "graylog-cli", + "search", + "test", + "--sort-direction", + "invalid", + ]); + assert!( + result.is_err(), + "invalid sort-direction should be rejected" + ); + } +} diff --git a/tests/cli_integration.rs b/tests/cli_integration.rs new file mode 100644 index 0000000..2fa9935 --- /dev/null +++ b/tests/cli_integration.rs @@ -0,0 +1,39 @@ +use std::process::Command; + +fn graylog_cli() -> Command { + Command::new("cargo") +} + +fn run(args: &[&str]) -> std::process::Output { + let mut cmd = graylog_cli(); + cmd.args(["run", "--"]).args(args); + cmd.output().expect("failed to run graylog-cli") +} + +#[test] +fn help_flag_exits_zero() { + let output = run(&["--help"]); + assert!(output.status.success()); + let stdout = String::from_utf8_lossy(&output.stdout); + assert!(stdout.contains("graylog-cli")); + assert!(stdout.contains("search")); + assert!(stdout.contains("auth")); +} + +#[test] +fn version_flag_exits_zero() { + let output = run(&["--version"]); + assert!(output.status.success()); +} + +#[test] +fn no_args_shows_help() { + let output = run(&[]); + assert!(!output.status.success()); +} + +#[test] +fn unknown_command_fails() { + let output = run(&["nonexistent"]); + assert!(!output.status.success()); +} From ebafc5e9ac7dccc9e3a802bac875d19031835d0e Mon Sep 17 00:00:00 2001 From: Jakob Ankarhem Date: Mon, 27 Apr 2026 08:01:18 +0200 Subject: [PATCH 2/2] fmt --- benches/json_normalization.rs | 2 +- benches/timerange_parsing.rs | 2 +- src/presentation/cli.rs | 47 ++++++++++++++++------------------- 3 files changed, 23 insertions(+), 28 deletions(-) diff --git a/benches/json_normalization.rs b/benches/json_normalization.rs index e15ca6a..c1ae8ee 100644 --- a/benches/json_normalization.rs +++ b/benches/json_normalization.rs @@ -1,4 +1,4 @@ -use criterion::{black_box, criterion_group, criterion_main, Criterion}; +use criterion::{Criterion, black_box, criterion_group, criterion_main}; use serde_json::json; fn bench_json_serialize(c: &mut Criterion) { diff --git a/benches/timerange_parsing.rs b/benches/timerange_parsing.rs index 4cbc703..85004fc 100644 --- a/benches/timerange_parsing.rs +++ b/benches/timerange_parsing.rs @@ -1,4 +1,4 @@ -use criterion::{black_box, criterion_group, criterion_main, Criterion}; +use criterion::{Criterion, black_box, criterion_group, criterion_main}; use graylog_cli::domain::timerange::{CommandTimerange, TimerangeInput}; fn bench_relative_timerange(c: &mut Criterion) { diff --git a/src/presentation/cli.rs b/src/presentation/cli.rs index 5175fb5..8ae0900 100644 --- a/src/presentation/cli.rs +++ b/src/presentation/cli.rs @@ -369,7 +369,10 @@ mod tests { #[test] fn auth_requires_url_and_token() { let result = parse(&["graylog-cli", "auth"]); - assert!(result.is_err(), "auth without --url and --token should fail"); + assert!( + result.is_err(), + "auth without --url and --token should fail" + ); } #[test] @@ -403,8 +406,8 @@ mod tests { #[test] fn search_succeeds_with_query() { - let cli = - parse(&["graylog-cli", "search", "level:ERROR"]).expect("search with query should parse"); + let cli = parse(&["graylog-cli", "search", "level:ERROR"]) + .expect("search with query should parse"); match cli.command { Commands::Search(args) => { @@ -545,8 +548,7 @@ mod tests { #[test] fn streams_list_parses() { - let cli = - parse(&["graylog-cli", "streams", "list"]).expect("streams list should parse"); + let cli = parse(&["graylog-cli", "streams", "list"]).expect("streams list should parse"); match cli.command { Commands::Streams { command } => { @@ -626,7 +628,15 @@ mod tests { #[test] fn streams_search_rejects_limit_zero() { - let result = parse(&["graylog-cli", "streams", "search", "abc", "test", "--limit", "0"]); + let result = parse(&[ + "graylog-cli", + "streams", + "search", + "abc", + "test", + "--limit", + "0", + ]); assert!( result.is_err(), "streams search --limit 0 should be rejected by clap value parser" @@ -654,14 +664,8 @@ mod tests { #[test] fn sort_direction_accepts_asc_and_desc() { - let cli_asc = parse(&[ - "graylog-cli", - "search", - "test", - "--sort-direction", - "asc", - ]) - .expect("asc should be accepted"); + let cli_asc = parse(&["graylog-cli", "search", "test", "--sort-direction", "asc"]) + .expect("asc should be accepted"); match cli_asc.command { Commands::Search(args) => { @@ -670,14 +674,8 @@ mod tests { _ => panic!("expected Search command"), } - let cli_desc = parse(&[ - "graylog-cli", - "search", - "test", - "--sort-direction", - "desc", - ]) - .expect("desc should be accepted"); + let cli_desc = parse(&["graylog-cli", "search", "test", "--sort-direction", "desc"]) + .expect("desc should be accepted"); match cli_desc.command { Commands::Search(args) => { @@ -696,9 +694,6 @@ mod tests { "--sort-direction", "invalid", ]); - assert!( - result.is_err(), - "invalid sort-direction should be rejected" - ); + assert!(result.is_err(), "invalid sort-direction should be rejected"); } }