257 lines
6.9 KiB
Rust
257 lines
6.9 KiB
Rust
//! 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"));
|
|
}
|