Skip to main content

aft/compress/
pytest.rs

1use crate::compress::Compressor;
2
3pub struct PytestCompressor;
4
5impl Compressor for PytestCompressor {
6    fn matches(&self, command: &str) -> bool {
7        let tokens: Vec<&str> = command.split_whitespace().collect();
8        tokens.first().is_some_and(|head| *head == "pytest")
9            || tokens
10                .windows(3)
11                .any(|window| matches!(window, ["python" | "python3", "-m", "pytest"]))
12    }
13
14    fn compress(&self, _command: &str, output: &str) -> String {
15        compress_pytest(output)
16    }
17}
18
19fn compress_pytest(output: &str) -> String {
20    let lines: Vec<&str> = output.lines().collect();
21    let mut result = Vec::new();
22    let mut index = 0usize;
23
24    while index < lines.len() {
25        let line = lines[index];
26        let trimmed = line.trim();
27
28        if is_header_line(trimmed) || is_failure_or_error_test_line(trimmed) {
29            result.push(line.to_string());
30            index += 1;
31            continue;
32        }
33
34        if is_section_header(trimmed, "FAILURES") || is_section_header(trimmed, "ERRORS") {
35            while index < lines.len() {
36                let current = lines[index];
37                let current_trimmed = current.trim();
38                if index != 0
39                    && index != lines.len() - 1
40                    && current_trimmed.starts_with('=')
41                    && !is_section_header(current_trimmed, "FAILURES")
42                    && !is_section_header(current_trimmed, "ERRORS")
43                {
44                    break;
45                }
46                result.push(current.to_string());
47                index += 1;
48            }
49            continue;
50        }
51
52        if is_section_header(trimmed, "warnings summary") {
53            let (warnings, next_index) = compress_warnings(&lines, index);
54            result.extend(warnings);
55            index = next_index;
56            continue;
57        }
58
59        if is_section_header(trimmed, "short test summary info") || is_final_summary(trimmed) {
60            result.push(line.to_string());
61            index += 1;
62            continue;
63        }
64
65        if is_pass_status_line(trimmed) {
66            index += 1;
67            continue;
68        }
69
70        index += 1;
71    }
72
73    trim_trailing_lines(&result.join("\n"))
74}
75
76fn is_header_line(trimmed: &str) -> bool {
77    trimmed.starts_with("platform ")
78        || trimmed.starts_with("rootdir:")
79        || trimmed.starts_with("collected ")
80}
81
82fn is_failure_or_error_test_line(trimmed: &str) -> bool {
83    trimmed.contains(" FAILED")
84        || trimmed.ends_with(" FAILED")
85        || trimmed.contains(" ERROR")
86        || trimmed.ends_with(" ERROR")
87}
88
89fn is_section_header(trimmed: &str, name: &str) -> bool {
90    trimmed.starts_with('=') && trimmed.contains(name) && trimmed.ends_with('=')
91}
92
93fn is_pass_status_line(trimmed: &str) -> bool {
94    !trimmed.is_empty()
95        && (trimmed
96            .chars()
97            .all(|char| matches!(char, '.' | 's' | 'x' | 'X'))
98            || trimmed.ends_with(" PASSED")
99            || trimmed.contains(" PASSED "))
100}
101
102fn is_final_summary(trimmed: &str) -> bool {
103    trimmed.starts_with('=')
104        && (trimmed.contains(" passed")
105            || trimmed.contains(" failed")
106            || trimmed.contains(" error")
107            || trimmed.contains(" skipped")
108            || trimmed.contains(" xfailed"))
109        && trimmed.ends_with('=')
110}
111
112fn compress_warnings(lines: &[&str], start: usize) -> (Vec<String>, usize) {
113    let mut result = vec![lines[start].to_string()];
114    let mut index = start + 1;
115    let mut warnings_seen = 0usize;
116    let mut omitted = 0usize;
117
118    while index < lines.len() {
119        let line = lines[index];
120        let trimmed = line.trim();
121        if trimmed.starts_with('=') && trimmed.ends_with('=') {
122            break;
123        }
124        if is_warning_entry(trimmed) {
125            warnings_seen += 1;
126            if warnings_seen <= 5 {
127                result.push(line.to_string());
128            } else {
129                omitted += 1;
130            }
131        } else if warnings_seen <= 5 {
132            result.push(line.to_string());
133        }
134        index += 1;
135    }
136
137    if omitted > 0 {
138        result.push(format!("... and {omitted} more warnings"));
139    }
140
141    (result, index)
142}
143
144fn is_warning_entry(trimmed: &str) -> bool {
145    trimmed.contains("Warning:") || trimmed.contains("warning:") || trimmed.starts_with("tests/")
146}
147
148fn trim_trailing_lines(input: &str) -> String {
149    input
150        .lines()
151        .map(str::trim_end)
152        .collect::<Vec<_>>()
153        .join("\n")
154}