use ctx_exec::{
CompressOptions, DEFAULT_HEAD_LINES, DEFAULT_TAIL_LINES, StreamCompressor, compress,
estimate_tokens,
};
fn opts() -> CompressOptions {
CompressOptions::default()
}
fn lines(n: usize, label: &str) -> String {
let mut out = Vec::with_capacity(n);
for i in 1..=n {
out.push(format!("{label}{i}"));
}
out.join("\n")
}
#[test]
fn keeps_critical_lines_in_the_middle() {
let mut input = lines(12, "l");
input.push_str("\nerror: something broke\n");
input.push('\n');
input.push_str(&lines(12, "m"));
let result = compress(&input, &opts()).unwrap();
let text = &result.text;
assert!(text.contains("error: something broke"));
assert!(text.contains("... [7 lines omitted]"));
assert!(text.contains("... [8 lines omitted]"));
assert_eq!(
result.stats.kept_lines,
DEFAULT_HEAD_LINES + DEFAULT_TAIL_LINES + 1
);
assert_eq!(result.stats.total_lines, 26);
assert_eq!(
result.stats.omitted_lines,
result.stats.total_lines - result.stats.kept_lines
);
assert!(result.stats.saved_percent > 0);
}
#[test]
fn keeps_critical_lines_case_insensitively() {
let mut input = lines(12, "l");
input.push_str("\nPANIC: boom\nfailed\nwarning\nfatal\n");
input.push_str(&lines(12, "m"));
let result = compress(&input, &opts()).unwrap();
for needle in ["PANIC: boom", "failed", "warning", "fatal"] {
assert!(result.text.contains(needle), "missing {needle}");
}
}
#[test]
fn non_critical_lines_are_folded_away() {
let mut input = lines(12, "l");
input.push_str("\ntraceback debug note\n");
input.push_str(&lines(12, "m"));
let result = compress(&input, &opts()).unwrap();
assert!(!result.text.contains("traceback debug note"));
assert!(result.text.contains("... [15 lines omitted]"));
}
#[test]
fn head_and_tail_are_always_kept() {
let input = lines(24, "l");
let result = compress(&input, &opts()).unwrap();
assert_eq!(
result.stats.kept_lines,
DEFAULT_HEAD_LINES + DEFAULT_TAIL_LINES
);
assert!(
result
.text
.starts_with("l1\nl2\nl3\nl4\nl5\n... [14 lines omitted]")
);
assert!(result.text.ends_with("l20\nl21\nl22\nl23\nl24"));
}
#[test]
fn custom_keep_pattern() {
let mut input = lines(12, "l");
input.push_str("\nTODO: fix this later\n");
input.push_str(&lines(12, "m"));
let no_keep = compress(&input, &opts()).unwrap();
assert!(!no_keep.text.contains("TODO: fix this later"));
let mut custom = opts();
custom.keep_patterns.push("TODO".to_string());
let with_keep = compress(&input, &custom).unwrap();
assert!(with_keep.text.contains("TODO: fix this later"));
}
#[test]
fn small_output_passes_through_uncompressed() {
let input = lines(8, "l");
let result = compress(&input, &opts()).unwrap();
assert_eq!(result.text, input);
assert_eq!(result.stats.omitted_lines, 0);
assert_eq!(result.stats.saved_percent, 0);
}
#[test]
fn collapse_threshold_turns_off_compression() {
let mut input = lines(12, "l");
input.push_str("\nerror: e\n");
input.push_str(&lines(12, "m"));
let mut wide = opts();
wide.collapse_threshold = 26;
let result = compress(&input, &wide).unwrap();
assert_eq!(result.text, input);
assert_eq!(result.stats.omitted_lines, 0);
let mut narrow = opts();
narrow.collapse_threshold = 0;
let result = compress(&input, &narrow).unwrap();
assert!(result.text.contains("... [7 lines omitted]"));
assert!(result.stats.omitted_lines > 0);
}
#[test]
fn empty_output() {
let result = compress("", &opts()).unwrap();
assert!(result.text.is_empty());
assert_eq!(result.stats.total_lines, 0);
assert_eq!(result.stats.saved_percent, 0);
}
#[test]
fn invalid_pattern_errors_with_pattern_name() {
let mut custom = opts();
custom.keep_patterns.push("[".to_string());
let err = compress("whatever", &custom).unwrap_err();
let message = err.to_string();
assert!(
message.contains('['),
"message should name the pattern: {message}"
);
}
#[test]
fn invalid_pattern_errors_even_for_small_outputs() {
let mut custom = opts();
custom.keep_patterns.push("[".to_string());
assert!(compress("tiny", &custom).is_err());
}
#[test]
fn output_is_byte_stable() {
let mut input = lines(30, "l");
input.push_str("\nerror: e\n");
input.push_str(&lines(30, "m"));
let a = compress(&input, &opts()).unwrap();
let b = compress(&input, &opts()).unwrap();
assert_eq!(a.text, b.text);
assert_eq!(a.stats, b.stats);
assert_eq!(a.text, compress(&input, &opts()).unwrap().text);
}
#[test]
fn single_line_output_is_kept_as_is() {
let result = compress("error: single", &opts()).unwrap();
assert_eq!(result.text, "error: single");
}
#[test]
fn estimate_tokens_uses_cl100k_bpe() {
assert_eq!(estimate_tokens(""), 0);
assert_eq!(estimate_tokens("a"), 1);
assert_eq!(estimate_tokens("€"), 1);
assert_eq!(estimate_tokens("hello world"), 2);
assert_eq!(estimate_tokens("abcdefgh"), 1);
assert_eq!(estimate_tokens("你好"), 2);
assert_eq!(
estimate_tokens("这是一个中文句子,用来测试 token 计数。"),
17
);
}
#[test]
fn saved_percent_is_zero_for_passthrough() {
let input = lines(8, "l");
let result = compress(&input, &opts()).unwrap();
assert_eq!(result.stats.saved_percent, 0);
}
#[test]
fn empty_keep_list_matches_nothing() {
let mut input = lines(12, "l");
input.push_str("\nthis line must be omitted\n");
input.push_str(&lines(12, "m"));
let mut custom = opts();
custom.keep_patterns.clear();
let result = compress(&input, &custom).unwrap();
assert!(!result.text.contains("this line must be omitted"));
assert_eq!(
result.stats.kept_lines,
DEFAULT_HEAD_LINES + DEFAULT_TAIL_LINES
);
}
#[test]
fn per_pattern_anchors_survive_individual_compilation() {
let mut input = lines(12, "l");
input.push_str("\nxxPASSxx\n");
input.push_str("PASS: real hit\n");
input.push_str(&lines(12, "m"));
let mut custom = opts();
custom.keep_patterns.clear();
custom.keep_patterns.push("^PASS".to_string());
let result = compress(&input, &custom).unwrap();
assert!(!result.text.contains("xxPASSxx"), "unanchored match leaked");
assert!(result.text.contains("PASS: real hit"));
}
fn stream_bytes(bytes: &[u8], options: &CompressOptions) -> ctx_exec::CompressResult {
let mut sc = StreamCompressor::new(options).unwrap();
sc.push(bytes);
sc.finish()
}
#[test]
fn stream_compressor_matches_batch_output() {
let mut input = lines(12, "l");
input.push_str("\nerror: something broke\n\nTODO: fix me\n");
input.push_str(&lines(12, "m"));
let batch = compress(&input, &opts()).unwrap();
let streamed = stream_bytes(input.as_bytes(), &opts());
assert_eq!(streamed.text, batch.text);
assert_eq!(streamed.stats, batch.stats);
}
#[test]
fn stream_compressor_tolerates_arbitrary_chunk_boundaries() {
let mut input = lines(30, "你好").clone();
input.push_str("\nwarning: 中文告警\n");
input.push_str(&lines(30, "m"));
let batch = compress(&input, &opts()).unwrap();
let bytes = input.as_bytes();
let mut sc = StreamCompressor::new(&opts()).unwrap();
let mut i = 0;
let mut step = 1;
while i < bytes.len() {
let end = (i + step).min(bytes.len());
sc.push(&bytes[i..end]);
i = end;
step = step % 7 + 1;
}
let streamed = sc.finish();
assert_eq!(
streamed.text, batch.text,
"streamed: {}\nbatch: {}",
streamed.text, batch.text
);
}
#[test]
fn stream_compressor_preserves_crlf_passthrough() {
let input = "a\r\nb\r\nc\r\n";
let batch = compress(input, &opts()).unwrap();
assert_eq!(batch.text, input);
let streamed = stream_bytes(input.as_bytes(), &opts());
assert_eq!(streamed.text, input);
assert_eq!(streamed.stats, batch.stats);
}
#[test]
fn stream_compressor_normalizes_crlf_in_compressed_mode() {
let mut input = String::new();
for i in 1..=30 {
input.push_str(&format!("line {i}\r\n"));
}
let batch = compress(&input, &opts()).unwrap();
assert!(!batch.text.contains('\r'));
let streamed = stream_bytes(input.as_bytes(), &opts());
assert_eq!(streamed.text, batch.text);
assert_eq!(streamed.stats, batch.stats);
}
#[test]
fn stream_compressor_no_tail_or_head() {
let mut options = opts();
options.head_lines = 0;
options.tail_lines = 0;
let mut input = lines(10, "l");
input.push_str("\nerror: kept\n");
input.push_str(&lines(10, "m"));
let batch = compress(&input, &options).unwrap();
let streamed = stream_bytes(input.as_bytes(), &options);
assert_eq!(streamed.text, batch.text);
assert_eq!(streamed.stats, batch.stats);
assert!(streamed.text.starts_with("... [10 lines omitted]"));
}
#[test]
fn stream_compressor_unterminated_final_line() {
let mut input = lines(40, "l");
input.push_str("\nfatal: no trailing newline");
let batch = compress(&input, &opts()).unwrap();
let streamed = stream_bytes(input.as_bytes(), &opts());
assert_eq!(streamed.text, batch.text);
assert_eq!(streamed.stats, batch.stats);
assert!(streamed.text.ends_with("fatal: no trailing newline"));
}
#[test]
fn stream_compressor_is_byte_stable() {
let mut input = lines(60, "l");
input.push_str("\nerror: boom\n");
input.push_str(&lines(60, "m"));
let a = stream_bytes(input.as_bytes(), &opts());
let b = stream_bytes(input.as_bytes(), &opts());
assert_eq!(a.text, b.text);
assert_eq!(a.stats, b.stats);
}
#[test]
fn stream_compressor_handles_ten_million_plain_lines() {
let mut sc = StreamCompressor::new(&opts()).unwrap();
let mut line = String::from("plain data\n");
for _ in 0..10_000 {
sc.push(line.as_bytes());
}
line.clear();
line.push_str("plain data");
sc.push(line.as_bytes());
let result = sc.finish();
assert_eq!(result.stats.total_lines, 10_001);
assert!(result.stats.kept_lines < 200);
assert!(result.stats.saved_percent > 90);
}