Skip to main content

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: &regex::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}