mod common;
use common::*;
const RFC4180_MULTILINE: &str = "name,note\n\"alice\",\"hello\nworld\"\n\"bob\",\"ok\"\n";
#[test]
fn csv_quoted_embedded_newline_is_one_record_sequential() {
let (stdout, _stderr, exit_code) =
run_kelora_with_input(&["-f", "csv", "-F", "json"], RFC4180_MULTILINE);
assert_eq!(exit_code, 0, "stdout: {stdout}");
let lines: Vec<&str> = stdout.lines().collect();
assert_eq!(lines.len(), 2, "expected 2 records, got: {stdout}");
assert_eq!(lines[0], r#"{"name":"alice","note":"hello\nworld"}"#);
assert_eq!(lines[1], r#"{"name":"bob","note":"ok"}"#);
}
#[test]
fn csv_multiline_round_trips_through_csv_output() {
let input = "{\"a\":\"x\",\"b\":\"line1\\nline2\"}\n";
let (csv, _stderr, code) = run_kelora_with_input(&["-j", "-F", "csv", "-k", "a,b"], input);
assert_eq!(code, 0);
let (json, _stderr, code) = run_kelora_with_input(&["-f", "csv", "-F", "json"], &csv);
assert_eq!(code, 0);
assert_eq!(
json.lines().count(),
1,
"round-trip changed record count: {json}"
);
assert_eq!(json.trim(), r#"{"a":"x","b":"line1\nline2"}"#);
}
#[test]
fn csv_multiline_strict_does_not_misreport_ragged_rows() {
let (stdout, stderr, exit_code) =
run_kelora_with_input(&["-f", "csv", "-F", "json", "--strict"], RFC4180_MULTILINE);
assert_eq!(exit_code, 0, "stderr: {stderr}");
assert_eq!(stdout.lines().count(), 2);
assert!(
!stderr.contains("expected 2"),
"strict should not raise a ragged-row error here: {stderr}"
);
}
#[test]
fn csv_multiline_parallel_matches_sequential() {
let (stdout, _stderr, exit_code) =
run_kelora_with_input(&["-f", "csv", "-F", "json", "-P"], RFC4180_MULTILINE);
assert_eq!(exit_code, 0, "stdout: {stdout}");
let lines: Vec<&str> = stdout.lines().collect();
assert_eq!(lines.len(), 2, "expected 2 records, got: {stdout}");
assert_eq!(lines[0], r#"{"name":"alice","note":"hello\nworld"}"#);
assert_eq!(lines[1], r#"{"name":"bob","note":"ok"}"#);
}
#[test]
fn csv_multiline_parallel_strict_does_not_corrupt() {
let (stdout, stderr, exit_code) = run_kelora_with_input(
&["-f", "csv", "-F", "json", "-P", "--strict"],
RFC4180_MULTILINE,
);
assert_eq!(exit_code, 0, "parallel+strict stderr: {stderr}");
assert_eq!(stdout.lines().count(), 2, "stdout: {stdout}");
assert!(
!stderr.contains("Unterminated quoted field"),
"well-formed record must not be reported as unterminated: {stderr}"
);
}
#[test]
fn csv_multiline_parallel_field_spanning_several_lines() {
let input = "a,b\n\"x\",\"one\ntwo\nthree\"\np,q\n";
let (stdout, _stderr, exit_code) =
run_kelora_with_input(&["-f", "csv", "-F", "json", "-P"], input);
assert_eq!(exit_code, 0, "stdout: {stdout}");
let lines: Vec<&str> = stdout.lines().collect();
assert_eq!(lines.len(), 2, "stdout: {stdout}");
assert_eq!(lines[0], r#"{"a":"x","b":"one\ntwo\nthree"}"#);
assert_eq!(lines[1], r#"{"a":"p","b":"q"}"#);
}
#[test]
fn csv_multiline_parallel_holds_record_across_tiny_batches() {
let (stdout, _stderr, exit_code) = run_kelora_with_input(
&["-f", "csv", "-F", "json", "-P", "--batch-size", "1"],
RFC4180_MULTILINE,
);
assert_eq!(exit_code, 0, "stdout: {stdout}");
let lines: Vec<&str> = stdout.lines().collect();
assert_eq!(lines.len(), 2, "stdout: {stdout}");
assert_eq!(lines[0], r#"{"name":"alice","note":"hello\nworld"}"#);
assert_eq!(lines[1], r#"{"name":"bob","note":"ok"}"#);
}
#[test]
fn csv_unterminated_quote_at_eof_errors_in_both_modes() {
let input = "name,note\n\"alice\",\"hello\nworld\n";
for args in [
&["-f", "csv", "-F", "json", "--strict"][..],
&["-f", "csv", "-F", "json", "-P", "--strict"][..],
] {
let (_stdout, stderr, exit_code) = run_kelora_with_input(args, input);
assert_ne!(exit_code, 0, "{args:?} should fail, stderr: {stderr}");
assert!(
stderr.contains("Unterminated quoted field"),
"{args:?} expected a clear cause, got: {stderr}"
);
}
}
#[test]
fn csv_blank_line_inside_quoted_field_is_preserved() {
let input = "a,b\n\"x\",\"line1\n\nline3\"\np,q\n";
for args in [
&["-f", "csv", "-F", "json"][..],
&["-f", "csv", "-F", "json", "-P"][..],
] {
let (stdout, _stderr, exit_code) = run_kelora_with_input(args, input);
assert_eq!(exit_code, 0, "{args:?} stdout: {stdout}");
let lines: Vec<&str> = stdout.lines().collect();
assert_eq!(lines.len(), 2, "{args:?} stdout: {stdout}");
assert_eq!(lines[0], r#"{"a":"x","b":"line1\n\nline3"}"#, "{args:?}");
assert_eq!(lines[1], r#"{"a":"p","b":"q"}"#, "{args:?}");
}
}
#[test]
fn csv_ignore_lines_does_not_eat_record_continuation() {
let input = "a,b\n\"x\",\"keep\nDEBUG stuff\nmore\"\np,q\n";
for args in [
&["-f", "csv", "-F", "json", "--ignore-lines", "DEBUG"][..],
&["-f", "csv", "-F", "json", "-P", "--ignore-lines", "DEBUG"][..],
] {
let (stdout, _stderr, exit_code) = run_kelora_with_input(args, input);
assert_eq!(exit_code, 0, "{args:?} stdout: {stdout}");
let lines: Vec<&str> = stdout.lines().collect();
assert_eq!(lines.len(), 2, "{args:?} stdout: {stdout}");
assert_eq!(
lines[0], r#"{"a":"x","b":"keep\nDEBUG stuff\nmore"}"#,
"{args:?}"
);
assert_eq!(lines[1], r#"{"a":"p","b":"q"}"#, "{args:?}");
}
}
#[test]
fn keys_hint_not_raised_for_exec_created_field() {
let input = "{\"level\":\"INFO\"}\n{\"level\":\"WARN\"}\n";
let (stdout, stderr, exit_code) = run_kelora_with_input(
&["-j", "--exec", "e.tag = e.level + \"!\"", "-k", "tag"],
input,
);
assert_eq!(exit_code, 0);
assert!(stdout.contains("tag='INFO!'"), "stdout: {stdout}");
assert!(
!stderr.contains("never present"),
"field created in --exec must not be flagged as missing: {stderr}"
);
}
#[test]
fn keys_hint_still_raised_for_genuine_typo() {
let input = "{\"level\":\"INFO\"}\n";
let (_stdout, stderr, _exit_code) = run_kelora_with_input(&["-j", "-k", "nope"], input);
assert!(
stderr.contains("never present"),
"real typo should still be flagged: {stderr}"
);
}
#[test]
fn level_stats_do_not_list_non_level_field_values() {
let input = "{\"level\":\"WARN\",\"severity\":\"low\"}\n\
{\"level\":\"WARN\",\"severity\":\"high\"}\n";
let (stdout, _stderr, exit_code) = run_kelora_with_input(&["-j", "-s"], input);
assert_eq!(exit_code, 0);
let levels_line = stdout
.lines()
.find(|l| l.contains("Levels seen:"))
.unwrap_or_default();
assert!(
levels_line.contains("WARN"),
"expected WARN in: {levels_line}"
);
assert!(
!levels_line.contains("high") && !levels_line.contains("low"),
"severity values must not appear as levels: {levels_line}"
);
}