use forge::signal::compactor;
use once_cell::sync::Lazy;
use regex::Regex;
use std::collections::HashMap;
static COMPILING_RE: Lazy<Regex> =
Lazy::new(|| Regex::new(r"(?m)^ Compiling [^\n]+\n?").unwrap());
static DOWNLOADING_RE: Lazy<Regex> =
Lazy::new(|| Regex::new(r"(?m)^ Downloading [^\n]+\n?").unwrap());
static LOCKING_RE: Lazy<Regex> = Lazy::new(|| Regex::new(r"(?m)^ Locking [^\n]+\n?").unwrap());
static FRESH_RE: Lazy<Regex> = Lazy::new(|| Regex::new(r"(?m)^ Fresh [^\n]+\n?").unwrap());
static CHECKING_RE: Lazy<Regex> = Lazy::new(|| Regex::new(r"(?m)^ Checking [^\n]+\n?").unwrap());
#[allow(dead_code)]
static TIMING_RE: Lazy<Regex> =
Lazy::new(|| Regex::new(r"(?m)^\s+Finished .+ in \d+\.\d+s\n?").unwrap());
static TEST_PASS_RE: Lazy<Regex> =
Lazy::new(|| Regex::new(r"(?m)^test [^\n]+ \.\.\. ok\n?").unwrap());
static TREE_DEDUP_RE: Lazy<Regex> =
Lazy::new(|| Regex::new(r"(?m)^([│├└─ ]*\w[^\n]*) \(\*\)\n?").unwrap());
pub fn compress_build(raw: &str, _exit_code: i32) -> String {
let cleaned = compactor::normalise(raw);
let s = COMPILING_RE.replace_all(&cleaned, "");
let s = CHECKING_RE.replace_all(&s, "");
let s = FRESH_RE.replace_all(&s, "");
let s = DOWNLOADING_RE.replace_all(&s, "");
compactor::collapse_blanks(&s)
}
static CLIPPY_WARN_RULE_RE: Lazy<Regex> =
Lazy::new(|| Regex::new(r"^warning: ([a-zA-Z_:]+)").unwrap());
pub fn compress_clippy(raw: &str) -> String {
let cleaned = compactor::normalise(raw);
let s = COMPILING_RE.replace_all(&cleaned, "");
let s = CHECKING_RE.replace_all(&s, "");
let mut rule_counts: HashMap<String, usize> = HashMap::new();
let mut errors_and_summary: Vec<String> = Vec::new();
let mut in_warning_block = false;
let mut current_rule: Option<String> = None;
for line in s.lines() {
let trimmed = line.trim();
if trimmed.is_empty() {
in_warning_block = false;
continue;
}
if line.starts_with("error") {
in_warning_block = false;
current_rule = None;
errors_and_summary.push(line.to_string());
continue;
}
if (line.starts_with("warning:") || line.starts_with("error:"))
&& (line.contains("warning") && line.ends_with("emitted")
|| line.contains("error") && line.contains("aborting"))
{
errors_and_summary.push(line.to_string());
continue;
}
if let Some(caps) = CLIPPY_WARN_RULE_RE.captures(line) {
let rule = caps[1].to_string();
*rule_counts.entry(rule.clone()).or_insert(0) += 1;
current_rule = Some(rule);
in_warning_block = true;
continue;
}
if in_warning_block && line.trim_start().starts_with("-->") {
if let Some(ref rule) = current_rule {
if rule_counts.get(rule).copied().unwrap_or(0) == 1 {
errors_and_summary.push(line.to_string());
}
}
continue;
}
if in_warning_block
&& (trimmed.starts_with('|')
|| trimmed.starts_with('=')
|| trimmed.starts_with("note:")
|| trimmed.starts_with("help:"))
{
continue;
}
if !in_warning_block {
errors_and_summary.push(line.to_string());
}
}
let mut grouped: Vec<String> = Vec::new();
for (rule, count) in &rule_counts {
if *count > 1 {
grouped.push(format!("warning: {rule} (×{count})"));
} else {
grouped.push(format!("warning: {rule}"));
}
}
grouped.sort();
let mut result = grouped;
result.extend(errors_and_summary);
compactor::collapse_blanks(&result.join("\n"))
}
pub fn compress_test(raw: &str, exit_code: i32) -> String {
if raw.contains(" PASS ") || raw.contains(" FAIL ") || raw.contains(" RETRY ") {
return compress_nextest(raw, exit_code);
}
let cleaned = compactor::normalise(raw);
let s = COMPILING_RE.replace_all(&cleaned, "");
if exit_code == 0 {
let summary: Vec<&str> = s
.lines()
.filter(|l| l.starts_with("test result") || l.starts_with("running "))
.collect();
return summary.join("\n");
}
let s = TEST_PASS_RE.replace_all(&s, "");
compactor::collapse_blanks(&s)
}
pub fn compress_nextest(raw: &str, exit_code: i32) -> String {
let cleaned = compactor::normalise(raw);
let s = COMPILING_RE.replace_all(&cleaned, "");
let timing_re = once_cell::sync::Lazy::force(&NEXTEST_TIMING_RE);
let s = timing_re.replace_all(&s, "");
if s.lines()
.any(|l| l.contains(": test") || l.trim().ends_with("::"))
{
let tests: Vec<&str> = s
.lines()
.filter(|l| l.contains("::") && !l.trim().is_empty())
.collect();
if !tests.is_empty() {
return format!("{} tests listed:\n{}", tests.len(), tests.join("\n"));
}
}
let pass_count = s
.lines()
.filter(|l| l.trim_start().starts_with("PASS "))
.count();
if exit_code == 0 {
let summary: Vec<&str> = s
.lines()
.filter(|l| {
let t = l.trim_start();
t.starts_with("Summary") || t.contains("passed") || t.contains("test run")
})
.collect();
let summary_str = if summary.is_empty() {
s.lines()
.rfind(|l| !l.trim().is_empty())
.unwrap_or("")
.to_string()
} else {
summary.join("\n")
};
if pass_count > 0 {
return format!("{pass_count} tests passed\n{summary_str}");
}
return summary_str;
}
let mut out: Vec<&str> = Vec::new();
let mut in_fail_block = false;
for line in s.lines() {
let t = line.trim_start();
if t.starts_with("FAIL ") {
in_fail_block = true;
out.push(line);
continue;
}
if t.starts_with("PASS ") {
in_fail_block = false;
continue;
}
if t.starts_with("Summary") || t.starts_with("error") {
in_fail_block = false;
out.push(line);
continue;
}
if t.starts_with("RETRY ") {
continue;
}
if in_fail_block {
out.push(line);
}
}
if pass_count > 0 {
out.insert(0, "");
}
let result = out.join("\n");
if pass_count > 0 {
return format!("{pass_count} passed (not shown)\n{result}");
}
compactor::collapse_blanks(&result)
}
static NEXTEST_TIMING_RE: Lazy<Regex> =
Lazy::new(|| Regex::new(r"(?m)^\s+\[\s*\d+\.\d+s\][^\n]*\n?").unwrap());
pub fn compress_add(raw: &str) -> String {
let cleaned = compactor::normalise(raw);
let s = DOWNLOADING_RE.replace_all(&cleaned, "");
let s = LOCKING_RE.replace_all(&s, "");
compactor::collapse_blanks(&s)
}
pub fn compress_tree(raw: &str) -> String {
let cleaned = compactor::normalise(raw);
let lines: Vec<&str> = cleaned.lines().collect();
if lines.len() <= 60 {
return cleaned;
}
let s = TREE_DEDUP_RE.replace_all(&cleaned, "");
let remaining: Vec<&str> = s.lines().filter(|l| !l.trim().is_empty()).collect();
if remaining.len() <= 80 {
return remaining.join("\n");
}
format!(
"{}\n... [{} more entries omitted] ...",
remaining[..80].join("\n"),
remaining.len() - 80
)
}
pub fn compress_update(raw: &str) -> String {
let cleaned = compactor::normalise(raw);
let s = LOCKING_RE.replace_all(&cleaned, "");
compactor::collapse_blanks(&s)
}
pub fn compress_fmt(raw: &str) -> String {
let cleaned = compactor::normalise(raw);
if cleaned.trim().is_empty() {
return "(fmt: no changes)".to_string();
}
let out: Vec<&str> = cleaned
.lines()
.filter(|l| l.starts_with("Diff in") || l.starts_with('+') || l.starts_with('-'))
.collect();
if out.is_empty() {
cleaned
} else {
out.join("\n")
}
}
pub fn compress_bench(raw: &str) -> String {
let cleaned = compactor::normalise(raw);
let s = COMPILING_RE.replace_all(&cleaned, "");
let s = CHECKING_RE.replace_all(&s, "");
let out: Vec<&str> = s
.lines()
.filter(|l| l.contains("ns/iter") || l.contains("error") || l.starts_with("running"))
.collect();
if out.is_empty() {
compactor::collapse_blanks(&s)
} else {
out.join("\n")
}
}
pub fn compress_doc(raw: &str) -> String {
let cleaned = compactor::normalise(raw);
let s = COMPILING_RE.replace_all(&cleaned, "");
let s = CHECKING_RE.replace_all(&s, "");
let doc_lines: Vec<&str> = s
.lines()
.filter(|l| l.starts_with(" Documenting"))
.collect();
let other: Vec<&str> = s
.lines()
.filter(|l| !l.starts_with(" Documenting"))
.collect();
let doc_summary = if doc_lines.len() > 2 {
format!(" Documenting [+{} crates] ...", doc_lines.len())
} else {
doc_lines.join("\n")
};
let rest = compactor::collapse_blanks(&other.join("\n"));
if doc_summary.is_empty() {
rest
} else {
format!("{doc_summary}\n{rest}")
}
}
pub fn compress_publish(raw: &str) -> String {
let cleaned = compactor::normalise(raw);
let s = COMPILING_RE.replace_all(&cleaned, "");
compactor::collapse_blanks(&s)
}
pub fn compress_run(raw: &str, exit_code: i32) -> String {
let cleaned = compactor::normalise(raw);
let s = COMPILING_RE.replace_all(&cleaned, "");
let s = CHECKING_RE.replace_all(&s, "");
let out = compactor::collapse_blanks(&s);
if exit_code != 0 && out.trim().is_empty() {
cleaned
} else {
out
}
}
pub fn compress_cargo(subcmd: &str, raw: &str, exit_code: i32) -> String {
match subcmd {
"build" | "b" => compress_build(raw, exit_code),
"check" | "c" => compress_build(raw, exit_code),
"clippy" => compress_clippy(raw),
"test" | "t" => compress_test(raw, exit_code),
"nextest" => compress_nextest(raw, exit_code),
"add" => compress_add(raw),
"remove" | "rm" => compress_add(raw),
"tree" => compress_tree(raw),
"update" => compress_update(raw),
"fmt" => compress_fmt(raw),
"bench" => compress_bench(raw),
"doc" => compress_doc(raw),
"publish" => compress_publish(raw),
"run" | "r" => compress_run(raw, exit_code),
_ => {
let cleaned = compactor::normalise(raw);
let s = COMPILING_RE.replace_all(&cleaned, "");
compactor::collapse_blanks(&s)
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn build_strips_compiling_lines() {
let raw = " Compiling foo v0.1.0\n Compiling bar v1.2.3\nerror[E0412]: not found\n";
let out = compress_build(raw, 1);
assert!(!out.contains("Compiling foo"), "{out}");
assert!(out.contains("error"));
}
#[test]
fn build_success_keeps_finished_line() {
let raw = " Compiling foo v0.1.0\n Finished dev [unoptimized] target(s) in 2.31s\n";
let out = compress_build(raw, 0);
assert!(!out.contains("Compiling"), "{out}");
assert!(
out.contains("Finished"),
"Finished summary must survive: {out}"
);
}
#[test]
fn test_success_keeps_summary_only() {
let raw = "running 3 tests\ntest foo ... ok\ntest bar ... ok\ntest baz ... ok\ntest result: ok. 3 passed\n";
let out = compress_test(raw, 0);
assert!(!out.contains("test foo ... ok"), "{out}");
assert!(out.contains("test result"));
}
#[test]
fn test_failure_strips_passing_keeps_fails() {
let raw = "test foo ... ok\ntest bar ... FAILED\ntest result: FAILED. 1 failed\n";
let out = compress_test(raw, 1);
assert!(!out.contains("test foo ... ok"), "{out}");
assert!(out.contains("FAILED"));
}
#[test]
fn tree_truncates_large_output() {
let raw = (0..100)
.map(|i| format!("│ dep{i} v0.1.{i}"))
.collect::<Vec<_>>()
.join("\n");
let out = compress_tree(&raw);
assert!(out.contains("omitted"), "{out}");
}
#[test]
fn update_strips_locking() {
let raw =
" Locking 42 packages\n Updating crates.io index\n Updating foo v0.1 -> v0.2\n";
let out = compress_update(raw);
assert!(!out.contains("Locking"), "{out}");
assert!(out.contains("Updating foo"));
}
#[test]
fn fmt_no_changes_returns_message() {
assert!(compress_fmt("").contains("no changes"));
}
#[test]
fn clippy_groups_repeated_warnings() {
let block = |rule: &str| {
format!("warning: {rule}\n --> src/foo.rs:1:1\n |\n1 | let x = 1;\n |\n = note: see docs\n")
};
let raw = format!(
"{}{}{}",
block("clippy::unwrap_used"),
block("clippy::unwrap_used"),
block("clippy::expect_used")
);
let out = compress_clippy(&raw);
assert!(
out.contains("×2") || out.contains("(×2)"),
"should group repeated warning: {out}"
);
assert!(
!out.contains("let x = 1"),
"code snippet should be stripped: {out}"
);
}
#[test]
fn clippy_always_keeps_error_lines() {
let raw = " Checking foo v0.1.0\nerror[E0425]: cannot find value `x`\n --> src/lib.rs:5:9\nwarning: clippy::unwrap_used\n --> src/foo.rs:1:1\nwarning: 1 warning emitted\n";
let out = compress_clippy(&raw);
assert!(out.contains("E0425"), "{out}");
assert!(!out.contains("Checking foo"), "{out}");
}
#[test]
fn nextest_all_pass_collapses_to_summary() {
let raw = " Compiling foo v0.1.0\n PASS foo::test_a 0.001s\n PASS foo::test_b 0.002s\n PASS foo::test_c 0.003s\nSummary [0.123s] 3 tests run: 3 passed, 0 failed\n";
let out = compress_nextest(raw, 0);
assert!(!out.contains("PASS foo::test_a"), "{out}");
assert!(
out.contains("3") && (out.contains("passed") || out.contains("Summary")),
"{out}"
);
}
#[test]
fn nextest_failure_keeps_fail_lines() {
let raw = " PASS foo::test_a 0.001s\n FAIL foo::test_b 0.100s\nthread 'main' panicked at 'assertion failed'\nSummary [0.200s] 2 tests run: 1 passed, 1 failed\n";
let out = compress_nextest(raw, 1);
assert!(out.contains("FAIL foo::test_b"), "{out}");
assert!(!out.contains("PASS foo::test_a"), "{out}");
assert!(out.contains("Summary") || out.contains("panicked"), "{out}");
}
#[test]
fn nextest_strips_retry_lines() {
let raw = " RETRY foo::test_flaky attempt 1\n RETRY foo::test_flaky attempt 2\n FAIL foo::test_flaky 0.500s\nSummary [1.000s] 1 tests run: 0 passed, 1 failed\n";
let out = compress_nextest(raw, 1);
assert!(!out.contains("RETRY"), "{out}");
assert!(out.contains("FAIL"), "{out}");
}
#[test]
fn nextest_list_shows_test_names() {
let raw =
"foo::unit::test_a: test\nfoo::unit::test_b: test\nfoo::integration::test_c: test\n";
let out = compress_nextest(raw, 0);
assert!(out.contains("listed") || out.contains("test_a"), "{out}");
}
#[test]
fn nextest_strips_timing_prefix_lines() {
let raw = " [ 0.001s] PASS foo::test_a\n [ 0.002s] PASS foo::test_b\nSummary 2 passed\n";
let out = compress_nextest(raw, 0);
assert!(!out.contains("0.001s"), "{out}");
}
}