//! Integration tests for the `jsonlgrep` command. use std::path::PathBuf; use assert_cmd::Command; use jsonlgrep::{Query, parse_query}; use predicates::prelude::*; fn cargo_command() -> Command { Command::cargo_bin("jsonlgrep").expect("binary") } fn cargo_binary() -> PathBuf { assert_cmd::cargo::cargo_bin("jsonlgrep") } fn powershell_command(script: String) -> Command { let mut command = Command::new("pwsh"); command.args(["-NoProfile", "-Command"]).arg(script); command } fn ps_quote(value: impl std::fmt::Display) -> String { format!("'{}'", value.to_string().replace('\'', "''")) } fn fixture(path: &str) -> PathBuf { let fixture = PathBuf::from(env!("CARGO_MANIFEST_DIR")) .join("..") .join("..") .join("fixtures") .join(path); assert!( fixture.exists(), "missing fixture `{path}` at {}", fixture.display() ); fixture } #[test] fn parses_field_queries() { assert_eq!( parse_query("level=error").expect("exact query"), Query::FieldEquals { field: "level".into(), value: "error".into(), } ); assert!(matches!( parse_query("msg~=fail").expect("regex query"), Query::FieldRegex { field, .. } if field == "msg" )); assert_eq!( parse_query("panic").expect("text query"), Query::Contains("panic".into()) ); assert!(matches!( parse_query("level!=info").expect("negative exact query"), Query::FieldNotEquals { field, value } if field == "level" && value == "info" )); assert!(matches!( parse_query("msg!~=login").expect("negative regex query"), Query::FieldNotRegex { field, .. } if field == "msg" )); } #[test] fn filters_and_projects_jsonl_records() { cargo_command() .arg("level=error") .arg(fixture("jsonl/events.jsonl")) .arg("--pick") .arg("ts,msg") .assert() .success() .stdout(predicate::str::contains( "ts=2026-04-20T12:01:00Z msg=failed login", )); } #[test] fn text_and_json_projections_survive_conditional_raw_retention() { let temp = tempfile::tempdir().expect("tempdir"); let path = temp.path().join("spaced.jsonl"); std::fs::write( &path, "{ \"level\" : \"error\" , \"msg\" : \"failed login\" , \"ts\" : \"2026-04-20T12:01:00Z\" }\n", ) .expect("fixture"); let mut text = cargo_command(); text.arg("level=error") .arg(&path) .arg("--pick") .arg("ts,msg") .assert() .success() .stdout("ts=2026-04-20T12:01:00Z msg=failed login\n"); let mut json = cargo_command(); json.arg("level=error") .arg(&path) .arg("--pick") .arg("ts,msg") .arg("--json") .assert() .success() .stdout(predicate::str::contains("\"ts\":\"2026-04-20T12:01:00Z\"")) .stdout(predicate::str::contains("\"msg\":\"failed login\"")); } #[test] fn counts_matches_as_json() { cargo_command() .arg("msg~=login") .arg(fixture("jsonl/events.jsonl")) .arg("--count") .arg("--json") .assert() .success() .stdout("{\"count\":2}\n"); } #[test] fn supports_powershell_pipeline() { let binary = cargo_binary(); let input = fixture("jsonl/events.jsonl"); let script = format!( "[System.IO.File]::ReadLines({}) | & {} 'level=warn'", ps_quote(input.display()), ps_quote(binary.display()) ); powershell_command(script) .assert() .success() .stdout(predicate::str::contains("cache warmup")); } #[test] fn filters_nested_fields_and_negative_predicates() { let temp = tempfile::tempdir().expect("tempdir"); let path = temp.path().join("nested.jsonl"); std::fs::write( &path, concat!( "{\"event\":{\"user\":{\"name\":\"alice\"}},\"status\":500,\"ok\":false}\n", "{\"event\":{\"user\":{\"name\":\"bob\"}},\"status\":200,\"ok\":true}\n", "{\"event\":{\"user\":{\"name\":\"carol\"}},\"status\":404,\"ok\":false}\n" ), ) .expect("fixture"); let mut nested = cargo_command(); nested .arg("event.user.name=alice") .arg(&path) .arg("--pick") .arg("event.user.name,status") .assert() .success() .stdout(predicate::str::contains("event.user.name=alice status=500")); let mut negative_exact = cargo_command(); negative_exact .arg("status!=200") .arg(&path) .arg("--pick") .arg("event.user.name,status") .assert() .success() .stdout(predicate::str::contains("alice")) .stdout(predicate::str::contains("carol")) .stdout(predicate::str::contains("bob").not()); let mut negative_regex = cargo_command(); negative_regex .arg("event.user.name!~=^(alice|bob)$") .arg(&path) .arg("--pick") .arg("event.user.name") .assert() .success() .stdout("event.user.name=carol\n"); } #[test] fn help_includes_examples_for_nested_and_powershell_usage() { cargo_command() .arg("--help") .assert() .success() .stdout(predicate::str::contains("event.user.name=alice")) .stdout(predicate::str::contains("field!=value")) .stdout(predicate::str::contains("ConvertFrom-Json")) .stdout(predicate::str::contains("--toon")); } #[test] fn mistyped_regex_operator_reports_the_fix() { let error = parse_query("level~warn|error").expect_err("missing equals should fail"); assert!( error .to_string() .contains("did you mean 'level~=warn|error'"), "unexpected query error: {error}" ); cargo_command() .arg("level~warn|error") .arg(fixture("jsonl/events.jsonl")) .assert() .failure() .stderr(predicate::str::contains("did you mean 'level~=warn|error'")); } #[test] fn stops_after_limit_before_parsing_invalid_trailing_jsonl() { let temp = tempfile::tempdir().expect("tempdir"); let path = temp.path().join("limited.jsonl"); std::fs::write( &path, concat!( "{\"level\":\"error\",\"msg\":\"first\"}\n", "{not valid json}\n", "{\"level\":\"error\",\"msg\":\"third\"}\n" ), ) .expect("fixture"); cargo_command() .arg("level=error") .arg(&path) .arg("--input-format") .arg("jsonl") .arg("--count") .arg("--limit") .arg("1") .assert() .success() .stdout("1\n"); } #[test] fn rejects_zero_limit() { cargo_command() .arg("level=error") .arg(fixture("jsonl/events.jsonl")) .arg("--limit") .arg("0") .assert() .failure() .code(2) .stderr(predicate::str::contains("--limit must be greater than 0")); }