use forge::signal::compactor;
use once_cell::sync::Lazy;
use regex::Regex;
static DOWNLOAD_RE: Lazy<Regex> = Lazy::new(|| {
Regex::new(r"(?m)^go: (downloading|extracting|finding module|found) [^\n]+\n?").unwrap()
});
static MODULE_CACHE_RE: Lazy<Regex> =
Lazy::new(|| Regex::new(r"(?m)^go: (added|upgraded|downgraded) [^\n]+\n?").unwrap());
static TEST_PASS_RE: Lazy<Regex> = Lazy::new(|| Regex::new(r"(?m)^--- PASS: [^\n]+\n?").unwrap());
static TEST_SKIP_RE: Lazy<Regex> = Lazy::new(|| Regex::new(r"(?m)^--- SKIP: [^\n]+\n?").unwrap());
static LINK_VERBOSE_RE: Lazy<Regex> =
Lazy::new(|| Regex::new(r"(?m)^[#/][^\n]+(\.a|\.o|\.so)[^\n]*\n?").unwrap());
static VET_PKG_RE: Lazy<Regex> = Lazy::new(|| Regex::new(r"(?m)^# [a-zA-Z0-9/_\-\.]+\n?").unwrap());
static MOD_DOWNLOAD_RE: Lazy<Regex> = Lazy::new(|| {
Regex::new(r"(?m)^go: (downloading|extracting|finding|fetching) [^\n]+\n?").unwrap()
});
static GENERATE_ECHO_RE: Lazy<Regex> =
Lazy::new(|| Regex::new(r"(?m)^//go:generate [^\n]+\n?").unwrap());
static HASH_LINE_RE: Lazy<Regex> =
Lazy::new(|| Regex::new(r"(?m)^[a-f0-9]{64}\s[^\n]+\n?").unwrap());
pub fn compress_build(raw: &str, exit_code: i32) -> String {
let cleaned = compactor::normalise(raw);
let s = DOWNLOAD_RE.replace_all(&cleaned, "");
if exit_code == 0 {
let s = LINK_VERBOSE_RE.replace_all(&s, "");
return compactor::collapse_blanks(&s);
}
let errors: Vec<&str> = s.lines().filter(|l| !l.is_empty()).collect();
errors.join("\n")
}
pub fn compress_test(raw: &str, exit_code: i32) -> String {
let cleaned = compactor::normalise(raw);
let s = DOWNLOAD_RE.replace_all(&cleaned, "");
if exit_code == 0 {
let summary: Vec<&str> = s
.lines()
.filter(|l| {
l.starts_with("ok ")
|| l.starts_with("FAIL")
|| l.starts_with("?")
|| l.contains("coverage:")
})
.collect();
return summary.join("\n");
}
let s = TEST_PASS_RE.replace_all(&s, "");
let s = TEST_SKIP_RE.replace_all(&s, "");
compactor::collapse_blanks(&s)
}
pub fn compress_mod(raw: &str) -> String {
let cleaned = compactor::normalise(raw);
let s = MOD_DOWNLOAD_RE.replace_all(&cleaned, "");
let kept: Vec<&str> = s
.lines()
.filter(|l| {
let t = l.trim();
t.is_empty()
|| t.starts_with("go: added")
|| t.starts_with("go: removed")
|| t.starts_with("go: upgraded")
|| t.starts_with("go: downgraded")
|| t.starts_with("go: requires")
|| t.contains("error")
|| t.contains("all modules verified")
|| t.contains("inconsistent")
})
.collect();
compactor::collapse_blanks(&kept.join("\n"))
}
pub fn compress_mod_verify(raw: &str) -> String {
let cleaned = compactor::normalise(raw);
let s = HASH_LINE_RE.replace_all(&cleaned, "");
compactor::collapse_blanks(&s)
}
pub fn compress_vet(raw: &str) -> String {
let cleaned = compactor::normalise(raw);
let s = VET_PKG_RE.replace_all(&cleaned, "");
let mut msg_counts: std::collections::HashMap<String, usize> = std::collections::HashMap::new();
let mut unique_lines: Vec<String> = Vec::new();
for line in s.lines() {
let t = line.trim();
if t.is_empty() {
continue;
}
if let Some(colon_pos) = t.find(':') {
let after = &t[colon_pos + 1..];
let msg = after
.splitn(3, ':')
.nth(2)
.unwrap_or(after)
.trim()
.to_string();
if !msg.is_empty() {
let count = msg_counts.entry(msg.clone()).or_insert(0);
*count += 1;
if *count == 1 {
unique_lines.push(line.to_string());
} else if *count == 2 {
if let Some(pos) = unique_lines.iter().position(|l| l.contains(msg.as_str())) {
unique_lines[pos] = format!("… {msg} (×{count})");
}
} else {
if let Some(pos) = unique_lines
.iter()
.position(|l| l.contains(msg.as_str()) && l.contains('×'))
{
unique_lines[pos] = format!("… {msg} (×{count})");
}
}
continue;
}
}
unique_lines.push(line.to_string());
}
compactor::collapse_blanks(&unique_lines.join("\n"))
}
pub fn compress_generate(raw: &str) -> String {
let cleaned = compactor::normalise(raw);
let s = GENERATE_ECHO_RE.replace_all(&cleaned, "");
let lines: Vec<&str> = s.lines().filter(|l| !l.trim().is_empty()).collect();
if lines.is_empty() {
return "(go generate: completed)".to_string();
}
lines.join("\n")
}
pub fn compress_work(raw: &str) -> String {
let cleaned = compactor::normalise(raw);
let s = MOD_DOWNLOAD_RE.replace_all(&cleaned, "");
compactor::collapse_blanks(&s)
}
pub fn compress_go(subcmd: &str, raw: &str, exit_code: i32) -> String {
let sub = subcmd.trim();
match sub {
"build" | "run" | "install" => compress_build(raw, exit_code),
"test" => compress_test(raw, exit_code),
"vet" => compress_vet(raw),
"generate" => compress_generate(raw),
"work" => compress_work(raw),
"mod" => {
if sub.contains("verify") {
compress_mod_verify(raw)
} else {
compress_mod(raw)
}
}
_ if sub.starts_with("mod") => {
if sub.contains("verify") {
compress_mod_verify(raw)
} else {
compress_mod(raw)
}
}
_ => {
let cleaned = compactor::normalise(raw);
let s = DOWNLOAD_RE.replace_all(&cleaned, "");
let s = MODULE_CACHE_RE.replace_all(&s, "");
compactor::collapse_blanks(&s)
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn build_strips_download_lines() {
let raw =
"go: downloading github.com/foo/bar v1.2.3\ngo: extracting github.com/foo/bar v1.2.3\n";
let out = compress_build(raw, 0);
assert!(!out.contains("downloading"), "{out}");
}
#[test]
fn test_success_keeps_ok_lines() {
let raw =
"--- PASS: TestFoo (0.01s)\n--- PASS: TestBar (0.02s)\nok github.com/foo/pkg 0.034s\n";
let out = compress_test(raw, 0);
assert!(out.contains("ok"), "{out}");
assert!(!out.contains("PASS: TestFoo"), "{out}");
}
#[test]
fn test_failure_keeps_fail_lines() {
let raw = "--- PASS: TestFoo (0.01s)\n--- FAIL: TestBar (0.02s)\n\tmain_test.go:42: expected 1, got 2\nFAIL\n";
let out = compress_test(raw, 1);
assert!(out.contains("FAIL"), "{out}");
assert!(!out.contains("PASS: TestFoo"), "{out}");
}
#[test]
fn mod_strips_downloading() {
let raw =
"go: downloading golang.org/x/sys v0.0.0-20230101\ngo: added golang.org/x/sys v0.0.0\n";
let out = compress_mod(raw);
assert!(!out.contains("downloading"), "{out}");
assert!(out.contains("added"), "{out}");
}
#[test]
fn mod_keeps_removed_lines() {
let raw = "go: downloading old/pkg v1.0.0\ngo: removed old/pkg v1.0.0\ngo: added new/pkg v2.0.0\n";
let out = compress_mod(raw);
assert!(out.contains("removed"), "{out}");
assert!(out.contains("added"), "{out}");
assert!(!out.contains("downloading"), "{out}");
}
#[test]
fn vet_strips_package_headers() {
let raw = "# github.com/foo/bar\n./bar.go:10:5: suspicious argument in Printf call\n# github.com/foo/baz\n";
let out = compress_vet(raw);
assert!(out.contains("Printf"), "{out}");
assert!(!out.contains("# github.com/foo/bar"), "{out}");
}
#[test]
fn generate_strips_invocation_lines() {
let raw = "//go:generate stringer -type=Color\n//go:generate mockgen -source=foo.go\nfoo.go:10: undefined: Color\n";
let out = compress_generate(raw);
assert!(!out.contains("go:generate"), "{out}");
assert!(out.contains("undefined"), "{out}");
}
#[test]
fn generate_no_output_shows_completed() {
let raw = "//go:generate stringer -type=Foo\n";
let out = compress_generate(raw);
assert!(out.contains("completed"), "{out}");
}
#[test]
fn dispatcher_routes_test() {
let raw = "ok github.com/example/pkg 0.01s\n";
let out = compress_go("test", raw, 0);
assert!(out.contains("ok"));
}
}