Fix/post grep rtk cleanups - #5
Conversation
… blocks + failed summaries, drops passing-test noise, handles ctest -V prefixes, and filters combined stdout/stderr.
There was a problem hiding this comment.
Code Review
This pull request introduces a new ctest command and output filter to display compact GoogleTest failure summaries, updates git argument parsing to treat filenames with extensions as paths if they exist, and fixes log truncation to use character counts instead of byte lengths. The review feedback highlights a logic bug in the ctest summary line parser that causes dead code, an O(N^2) performance bottleneck in the line compaction helper, and opportunities to optimize string length checks from O(N) to O(1) in the log analyzer.
| fn is_gtest_failed_summary_line(line: &str) -> bool { | ||
| // Summary/listing: | ||
| // "[ FAILED ] 1 test, listed below:" | ||
| // "[ FAILED ] Foo.Fail" | ||
| // "[ PASSED ] 2 tests." | ||
| if line.starts_with("[ FAILED ]") { | ||
| return true; | ||
| } | ||
| if line.starts_with("[ PASSED ]") && line.contains("tests") { | ||
| return true; | ||
| } | ||
| false | ||
| } |
There was a problem hiding this comment.
The is_gtest_failed_summary_line function returns true for any line starting with [ FAILED ]. However, individual test failure lines (e.g., [ FAILED ] Foo.Fail (0 ms)) also start with this prefix. Because is_gtest_failed_summary_line is checked before emitting_failure_block in the main loop, individual test failure lines will match the summary check first. This makes the is_gtest_failed_test_line check dead code and prevents current_test from being reset to None.
To fix this, ensure that is_gtest_failed_summary_line excludes lines containing ( (which indicate individual test failure lines).
| fn is_gtest_failed_summary_line(line: &str) -> bool { | |
| // Summary/listing: | |
| // "[ FAILED ] 1 test, listed below:" | |
| // "[ FAILED ] Foo.Fail" | |
| // "[ PASSED ] 2 tests." | |
| if line.starts_with("[ FAILED ]") { | |
| return true; | |
| } | |
| if line.starts_with("[ PASSED ]") && line.contains("tests") { | |
| return true; | |
| } | |
| false | |
| } | |
| fn is_gtest_failed_summary_line(line: &str) -> bool { | |
| // Summary/listing: | |
| // "[ FAILED ] 1 test, listed below:" | |
| // "[ FAILED ] Foo.Fail" | |
| // "[ PASSED ] 2 tests." | |
| if line.starts_with("[ FAILED ]") && !line.contains('(') { | |
| return true; | |
| } | |
| if line.starts_with("[ PASSED ]") && line.contains("tests") { | |
| return true; | |
| } | |
| false | |
| } |
| fn compact_lines(mut lines: Vec<String>) -> Vec<String> { | ||
| // Trim leading/trailing empties, collapse multiple blank lines. | ||
| while lines.first().is_some_and(|l| l.trim().is_empty()) { | ||
| lines.remove(0); | ||
| } | ||
| while lines.last().is_some_and(|l| l.trim().is_empty()) { | ||
| lines.pop(); | ||
| } |
There was a problem hiding this comment.
Using lines.remove(0) in a loop is highly inefficient because it shifts all remaining elements in the vector on each iteration, resulting in
We can optimize this to position and rposition, and then draining the range directly.
fn compact_lines(mut lines: Vec<String>) -> Vec<String> {
// Trim leading/trailing empties, collapse multiple blank lines.
let first_non_empty = lines.iter().position(|l| !l.trim().is_empty()).unwrap_or(lines.len());
let last_non_empty = lines.iter().rposition(|l| !l.trim().is_empty()).map(|idx| idx + 1).unwrap_or(0);
if first_non_empty >= last_non_empty {
return Vec::new();
}
let mut out: Vec<String> = Vec::with_capacity(last_non_empty - first_non_empty);
let mut last_blank = false;
for l in lines.drain(first_non_empty..last_non_empty) {
let blank = l.trim().is_empty();
if blank {
if last_blank {
continue;
}
last_blank = true;
out.push(String::new());
} else {
last_blank = false;
out.push(l);
}
}
out
}| let truncated = if original.chars().count() > 100 { | ||
| let t: String = original.chars().take(97).collect(); | ||
| format!("{}...", t) | ||
| } else { |
There was a problem hiding this comment.
Calling original.chars().count() traverses the entire string, which is an
Since we only need to check if the character count exceeds 100, we can optimize this to .take(101).count().
| let truncated = if original.chars().count() > 100 { | |
| let t: String = original.chars().take(97).collect(); | |
| format!("{}...", t) | |
| } else { | |
| let truncated = if original.chars().take(101).count() > 100 { | |
| let t: String = original.chars().take(97).collect(); | |
| format!("{}...", t) | |
| } else { | |
| let truncated = if original.chars().count() > 100 { | ||
| let t: String = original.chars().take(97).collect(); | ||
| format!("{}...", t) | ||
| } else { |
There was a problem hiding this comment.
Calling original.chars().count() traverses the entire string, which is an
Since we only need to check if the character count exceeds 100, we can optimize this to .take(101).count().
| let truncated = if original.chars().count() > 100 { | |
| let t: String = original.chars().take(97).collect(); | |
| format!("{}...", t) | |
| } else { | |
| let truncated = if original.chars().take(101).count() > 100 { | |
| let t: String = original.chars().take(97).collect(); | |
| format!("{}...", t) | |
| } else { | |
The comment was previously added in response to review point #5. Removed per follow-up feedback — the arm itself is self-explanatory in context alongside the other Host variants.
Summary
Test plan
cargo fmt --all && cargo clippy --all-targets && cargo testrtk <command>output inspected