coding_tools/testrun.rs
1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 Jonathan Shook
3
4//! Pure helpers behind `ct-test`'s output handling — currently the `--focus`
5//! distiller, which reduces a captured stream to the lines that matter.
6
7/// Distil `text` to the lines matching `re`, each with `ctx` lines of context,
8/// merging overlapping windows; non-contiguous windows are separated by a `--`
9/// line and every kept line is prefixed with its 1-based number. Returns `None`
10/// when nothing matches.
11///
12/// # Examples
13///
14/// ```
15/// use coding_tools::testrun::focus_block;
16/// use coding_tools::pattern::compile;
17///
18/// let re = compile("ERROR").unwrap();
19/// let log = "ok\nERROR: a\nok\nok\nok\nERROR: b\ntail\n";
20/// // ctx 0 keeps only the matching lines; the two windows are separated by `--`.
21/// assert_eq!(focus_block(log, &re, 0).unwrap(), "2: ERROR: a\n--\n6: ERROR: b");
22///
23/// assert!(focus_block("all clean here", &re, 0).is_none());
24/// ```
25pub fn focus_block(text: &str, re: ®ex::Regex, ctx: usize) -> Option<String> {
26 let lines: Vec<&str> = text.lines().collect();
27 let mut keep = vec![false; lines.len()];
28 let mut any = false;
29 for (i, l) in lines.iter().enumerate() {
30 if re.is_match(l) {
31 any = true;
32 let lo = i.saturating_sub(ctx);
33 let hi = (i + ctx).min(lines.len().saturating_sub(1));
34 keep[lo..=hi].iter_mut().for_each(|k| *k = true);
35 }
36 }
37 if !any {
38 return None;
39 }
40 let mut out = String::new();
41 let mut prev: Option<usize> = None;
42 for (i, &k) in keep.iter().enumerate() {
43 if !k {
44 continue;
45 }
46 if let Some(p) = prev
47 && i > p + 1
48 {
49 out.push_str("--\n");
50 }
51 out.push_str(&format!("{}: {}\n", i + 1, lines[i]));
52 prev = Some(i);
53 }
54 Some(out.trim_end().to_string())
55}
56
57#[cfg(test)]
58mod tests {
59 use super::*;
60 use crate::pattern::compile;
61
62 #[test]
63 fn focus_keeps_matching_windows_with_context() {
64 let re = compile("ERROR").unwrap();
65 let log = "a\nb\nERROR x\nd\ne\nf\nERROR y\nh\n";
66 // ctx 1 -> windows [2..4] and [6..8] (1-based), non-contiguous.
67 let block = focus_block(log, &re, 1).unwrap();
68 assert_eq!(block, "2: b\n3: ERROR x\n4: d\n--\n6: f\n7: ERROR y\n8: h");
69 }
70
71 #[test]
72 fn focus_none_when_no_match() {
73 let re = compile("ERROR").unwrap();
74 assert!(focus_block("nothing relevant\n", &re, 2).is_none());
75 }
76}