use crate::core::runner::{self, RunOptions};
use crate::core::truncate::CAP_WARNINGS;
use crate::core::utils::{resolved_command, strip_ansi};
use anyhow::Result;
use lazy_static::lazy_static;
use regex::Regex;
use std::collections::HashSet;
use std::ffi::OsString;
use std::path::Path;
use std::process::Command;
const MAX_MVN_FAILING_CLASSES: usize = CAP_WARNINGS;
lazy_static! {
static ref RUNNING: Regex = Regex::new(r"^\[INFO\] Running ").unwrap();
static ref CLOSE: Regex = Regex::new(
r"^\[(?:INFO|ERROR|WARNING)\] Tests run: \d+, Failures: (\d+), Errors: (\d+), Skipped: \d+, Time elapsed: [^ ]+ s(?:\s+<<<\s*(?:FAILURE|ERROR)!)?\s+--?\s+in (.+)$"
).unwrap();
static ref BUILD_FOOT: Regex = Regex::new(r"^\[(?:INFO|ERROR)\] BUILD (?:SUCCESS|FAILURE)$").unwrap();
static ref RESULTS: Regex = Regex::new(r"^\[INFO\] Results:\s*$").unwrap();
static ref AGG: Regex = Regex::new(
r"^\[(?:INFO|ERROR)\] Tests run: \d+, Failures: \d+, Errors: \d+, Skipped: \d+\s*$"
).unwrap();
static ref PLUGIN_BANNER: Regex = Regex::new(r"^\[INFO\] --- .* @ .* ---$").unwrap();
static ref MODULE_BANNER: Regex = Regex::new(r"^\[INFO\] -+< .+ >-+$").unwrap();
static ref REACTOR_SUMMARY: Regex = Regex::new(r"^\[INFO\] Reactor Summary for ").unwrap();
static ref FILE_COORD: Regex = Regex::new(r"/[^:]+\.java:\[\d+,\d+\]").unwrap();
}
fn is_quiet(args: &[String]) -> bool {
args.iter().any(|a| a == "-q" || a == "--quiet")
}
#[derive(Debug, PartialEq, Clone, Copy)]
pub enum MvnPhase {
Test, Compile, Package, Passthrough, }
pub fn detect_phase(args: &[String]) -> MvnPhase {
let last = args
.iter()
.filter(|a| !a.starts_with('-'))
.map(|s| s.as_str())
.next_back()
.unwrap_or("");
if last.is_empty() || last.contains(':') {
return MvnPhase::Passthrough;
}
match last {
"clean" | "site" | "site-deploy" => MvnPhase::Passthrough,
"test" | "integration-test" => MvnPhase::Test,
"compile" | "test-compile" => MvnPhase::Compile,
"package" | "install" | "verify" | "deploy" => MvnPhase::Package,
_ => MvnPhase::Passthrough,
}
}
const FRAMEWORK_FRAME_PREFIXES: &[&str] = &[
"at org.junit.",
"at junit.",
"at org.apache.maven.surefire.",
"at sun.reflect.",
"at jdk.internal.reflect.",
"at jdk.proxy",
"at java.base/",
"at java.lang.reflect.",
"at java.util.",
];
fn is_framework_frame(trimmed: &str) -> bool {
FRAMEWORK_FRAME_PREFIXES
.iter()
.any(|p| trimmed.starts_with(p))
}
const BOILER_PREFIXES: &[&str] = &[
"[ERROR] See ",
"[ERROR] -> [Help",
"[ERROR] To see the full stack trace",
"[ERROR] Re-run Maven",
"[ERROR] For more information",
"[ERROR] [Help",
];
fn is_boilerplate(line: &str) -> bool {
BOILER_PREFIXES.iter().any(|p| line.starts_with(p)) || line.trim_end() == "[ERROR]"
}
fn is_per_test_subline(line: &str) -> bool {
line.starts_with("[ERROR] ")
&& (line.contains("<<< FAILURE!") || line.contains("<<< ERROR!"))
}
fn has_english_footer(stripped: &str) -> bool {
stripped.lines().any(|l| {
let t = l.trim();
t.ends_with(" BUILD SUCCESS") || t.ends_with(" BUILD FAILURE")
})
}
fn reactor_summary_keep(line: &str, in_reactor_summary: &mut bool) -> bool {
if REACTOR_SUMMARY.is_match(line) {
*in_reactor_summary = true;
return true;
}
if BUILD_FOOT.is_match(line) {
*in_reactor_summary = false;
return false;
}
*in_reactor_summary
}
fn keep_outside_block(line: &str) -> bool {
if is_boilerplate(line) {
return false;
}
RESULTS.is_match(line)
|| AGG.is_match(line)
|| BUILD_FOOT.is_match(line)
|| MODULE_BANNER.is_match(line)
|| line.starts_with("[INFO] Total time:")
|| line.starts_with("[INFO] Finished at:")
|| line.starts_with("[INFO] Building ")
|| line.starts_with("[INFO] Scanning ")
|| line.starts_with("[INFO] Installing ")
|| line.starts_with("[ERROR] Failures:")
|| line.starts_with("[ERROR] Errors:")
|| (line.starts_with("[ERROR]") && !line.starts_with("[ERROR] Tests run:"))
|| line.starts_with("[INFO] Building war:")
|| line.starts_with("[INFO] Building jar:")
|| line.starts_with("[INFO] Building ear:")
}
struct SurefireBlock<'a> {
block_lines: Vec<&'a str>,
block_running: Option<&'a str>,
in_block: bool,
failure_trail: bool,
drop_trail: bool,
trail_rearm: Option<bool>,
}
enum SurefireStep<'a> {
Consumed,
FailingClose {
running: Option<&'a str>,
lines: Vec<&'a str>,
close: &'a str,
},
Passthrough,
}
impl<'a> SurefireBlock<'a> {
fn new() -> Self {
Self {
block_lines: Vec::new(),
block_running: None,
in_block: false,
failure_trail: false,
drop_trail: false,
trail_rearm: None,
}
}
fn step(&mut self, line: &'a str, out: &mut String) -> SurefireStep<'a> {
if PLUGIN_BANNER.is_match(line) {
return SurefireStep::Consumed;
}
if RUNNING.is_match(line) {
if self.in_block {
self.flush_open_block_as_keep(out);
}
self.block_lines.clear();
self.block_running = Some(line);
self.in_block = true;
self.failure_trail = false;
self.trail_rearm = None;
return SurefireStep::Consumed;
}
if self.in_block {
if let Some(caps) = CLOSE.captures(line) {
let fail = caps.get(1).map(|m| m.as_str() != "0").unwrap_or(false);
let err = caps.get(2).map(|m| m.as_str() != "0").unwrap_or(false);
if fail || err {
let lines = std::mem::take(&mut self.block_lines);
let running = self.block_running.take();
self.in_block = false;
return SurefireStep::FailingClose {
running,
lines,
close: line,
};
}
self.block_lines.clear();
self.block_running = None;
self.in_block = false;
return SurefireStep::Consumed;
}
self.block_lines.push(line);
return SurefireStep::Consumed;
}
if self.failure_trail {
if line.is_empty() {
if !self.drop_trail {
out.push('\n');
}
self.trail_rearm = Some(self.drop_trail);
self.failure_trail = false;
self.drop_trail = false;
return SurefireStep::Consumed;
}
let t = line.trim_start();
if t.starts_with("at ") && is_framework_frame(t) {
return SurefireStep::Consumed;
}
if self.drop_trail {
return SurefireStep::Consumed;
}
out.push_str(line);
out.push('\n');
return SurefireStep::Consumed;
}
if let Some(dropped) = self.trail_rearm {
if line.is_empty() {
return SurefireStep::Passthrough;
}
self.trail_rearm = None; if is_per_test_subline(line) {
self.failure_trail = true;
self.drop_trail = dropped;
if !dropped {
out.push_str(line);
out.push('\n');
}
return SurefireStep::Consumed;
}
}
SurefireStep::Passthrough
}
fn drop_failing(&mut self) {
self.failure_trail = true;
self.drop_trail = true;
self.trail_rearm = None;
}
fn commit_failing(
&mut self,
out: &mut String,
running: Option<&str>,
lines: &[&str],
close: &str,
) {
if let Some(r) = running {
out.push_str(r);
out.push('\n');
}
for l in lines {
let t = l.trim_start();
if t.starts_with("at ") && is_framework_frame(t) {
continue;
}
out.push_str(l);
out.push('\n');
}
out.push_str(close);
out.push('\n');
self.failure_trail = true;
self.trail_rearm = None;
}
fn finish(&mut self, out: &mut String) {
if self.in_block {
self.flush_open_block_as_keep(out);
}
}
fn flush_open_block_as_keep(&mut self, out: &mut String) {
if let Some(r) = self.block_running.take() {
out.push_str(r);
out.push('\n');
}
for l in self.block_lines.drain(..) {
out.push_str(l);
out.push('\n');
}
self.in_block = false;
}
}
struct FailuresSummaryCap {
cap: usize,
in_summary: bool,
emitted: usize,
dropped: usize,
}
impl FailuresSummaryCap {
fn new(cap: usize) -> Self {
Self {
cap,
in_summary: false,
emitted: 0,
dropped: 0,
}
}
fn handle_entry(&mut self, line: &str, out: &mut String) -> bool {
if !self.in_summary || !line.starts_with("[ERROR] ") {
return false;
}
if self.emitted < self.cap {
out.push_str(line);
out.push('\n');
self.emitted += 1;
} else {
self.dropped += 1;
}
true
}
fn handle_header(&mut self, line: &str) {
if line.starts_with("[ERROR] Failures:") {
self.in_summary = true;
self.emitted = 0;
self.dropped = 0;
}
}
fn handle_aggregate(&mut self, line: &str, out: &mut String) {
if !self.in_summary || !AGG.is_match(line) {
return;
}
if self.dropped > 0 {
out.push_str(&format!("\n… +{} more failures\n", self.dropped));
}
self.in_summary = false;
self.emitted = 0;
self.dropped = 0;
}
fn finish(&mut self, out: &mut String) {
if self.in_summary && self.dropped > 0 {
out.push_str(&format!("\n… +{} more failures\n", self.dropped));
}
}
}
pub fn filter_surefire(raw: &str) -> String {
filter_surefire_with_cap(raw, MAX_MVN_FAILING_CLASSES)
}
fn filter_surefire_with_cap(raw: &str, cap: usize) -> String {
let stripped = strip_ansi(raw);
if !has_english_footer(&stripped) {
return stripped;
}
let mut out = String::new();
let mut block = SurefireBlock::new();
let mut keep_continuation = false;
let mut in_reactor_summary = false;
let mut emitted_failing: usize = 0;
let mut dropped_failing: usize = 0;
let mut summary = FailuresSummaryCap::new(cap);
for line in stripped.lines() {
match block.step(line, &mut out) {
SurefireStep::Consumed => continue,
SurefireStep::FailingClose {
running,
lines,
close,
} => {
if emitted_failing < cap {
block.commit_failing(&mut out, running, &lines, close);
emitted_failing += 1;
} else {
block.drop_failing();
dropped_failing += 1;
}
keep_continuation = false;
continue;
}
SurefireStep::Passthrough => {}
}
if keep_continuation && (line.starts_with(' ') || line.starts_with('\t')) {
out.push_str(line);
out.push('\n');
continue;
}
if summary.handle_entry(line, &mut out) {
continue;
}
let reactor_keep = reactor_summary_keep(line, &mut in_reactor_summary);
if reactor_keep || keep_outside_block(line) {
summary.handle_aggregate(line, &mut out);
summary.handle_header(line);
out.push_str(line);
out.push('\n');
keep_continuation = line.starts_with("[ERROR]")
&& !line.starts_with("[ERROR] Tests run:")
&& !line.starts_with("[ERROR] Failures:")
&& !line.starts_with("[ERROR] Errors:");
continue;
}
keep_continuation = false;
}
block.finish(&mut out);
summary.finish(&mut out);
if dropped_failing > 0 {
out.push_str(&format!(
"\n… +{} more failing test classes\n",
dropped_failing
));
}
out
}
pub fn filter_compile(raw: &str) -> String {
let stripped = strip_ansi(raw);
if !has_english_footer(&stripped) {
return stripped;
}
let mut out = String::new();
let mut keep_continuation = false;
let mut seen_warnings: HashSet<String> = HashSet::new();
for line in stripped.lines() {
if MODULE_BANNER.is_match(line) {
out.push_str(line);
out.push('\n');
keep_continuation = false;
continue;
}
if BUILD_FOOT.is_match(line)
|| line.starts_with("[INFO] Building ")
|| line.starts_with("[INFO] Total time:")
|| line.starts_with("[INFO] Finished at:")
|| line.starts_with("[INFO] Scanning ")
{
out.push_str(line);
out.push('\n');
keep_continuation = false;
continue;
}
if is_boilerplate(line) {
keep_continuation = false;
continue;
}
if line.starts_with("[ERROR]") {
out.push_str(line);
out.push('\n');
keep_continuation = true;
continue;
}
if keep_continuation && (line.starts_with(' ') || line.starts_with('\t')) {
out.push_str(line);
out.push('\n');
continue;
}
if line.starts_with("[WARNING]") {
let payload = line.strip_prefix("[WARNING] ").unwrap_or(line);
let norm = FILE_COORD.replace_all(payload, "").to_string();
if seen_warnings.insert(norm) {
out.push_str(line);
out.push('\n');
}
keep_continuation = false;
continue;
}
keep_continuation = false;
}
out
}
pub fn filter_package(raw: &str) -> String {
filter_package_with_cap(raw, MAX_MVN_FAILING_CLASSES)
}
fn filter_package_with_cap(raw: &str, cap: usize) -> String {
let stripped = strip_ansi(raw);
if !has_english_footer(&stripped) {
return stripped;
}
let mut out = String::new();
let mut block = SurefireBlock::new();
let mut keep_continuation = false;
let mut in_reactor_summary = false;
let mut seen_warnings: HashSet<String> = HashSet::new();
let mut emitted_failing: usize = 0;
let mut dropped_failing: usize = 0;
let mut summary = FailuresSummaryCap::new(cap);
for line in stripped.lines() {
match block.step(line, &mut out) {
SurefireStep::Consumed => continue,
SurefireStep::FailingClose {
running,
lines,
close,
} => {
if emitted_failing < cap {
block.commit_failing(&mut out, running, &lines, close);
emitted_failing += 1;
} else {
block.drop_failing();
dropped_failing += 1;
}
keep_continuation = false;
continue;
}
SurefireStep::Passthrough => {}
}
if summary.handle_entry(line, &mut out) {
continue;
}
let reactor_keep = reactor_summary_keep(line, &mut in_reactor_summary);
if reactor_keep || MODULE_BANNER.is_match(line) || keep_outside_block(line) {
summary.handle_aggregate(line, &mut out);
summary.handle_header(line);
out.push_str(line);
out.push('\n');
keep_continuation = line.starts_with("[ERROR]")
&& !line.starts_with("[ERROR] Tests run:")
&& !line.starts_with("[ERROR] Failures:")
&& !line.starts_with("[ERROR] Errors:");
continue;
}
if keep_continuation && (line.starts_with(' ') || line.starts_with('\t')) {
out.push_str(line);
out.push('\n');
continue;
}
if line.starts_with("[WARNING]") {
let payload = line.strip_prefix("[WARNING] ").unwrap_or(line);
let norm = FILE_COORD.replace_all(payload, "").to_string();
if seen_warnings.insert(norm) {
out.push_str(line);
out.push('\n');
}
keep_continuation = false;
continue;
}
keep_continuation = false;
}
block.finish(&mut out);
summary.finish(&mut out);
if dropped_failing > 0 {
out.push_str(&format!(
"\n… +{} more failing test classes\n",
dropped_failing
));
}
out
}
pub fn filter_quiet(raw: &str) -> String {
let stripped = strip_ansi(raw);
if stripped.trim().is_empty() {
return String::new();
}
let mut out = String::new();
let mut failure_trail = false;
for line in stripped.lines() {
if CLOSE.is_match(line) {
out.push_str(line);
out.push('\n');
failure_trail =
line.contains("<<< FAILURE!") || line.contains("<<< ERROR!");
continue;
}
if is_per_test_subline(line) {
out.push_str(line);
out.push('\n');
failure_trail = true;
continue;
}
if failure_trail {
if line.trim().is_empty() {
out.push('\n');
failure_trail = false;
continue;
}
let t = line.trim_start();
if t.starts_with("at ") && is_framework_frame(t) {
continue;
}
out.push_str(line);
out.push('\n');
continue;
}
if line.starts_with("[ERROR] Tests run:")
|| line.starts_with("[ERROR] Failures:")
|| line.starts_with("[ERROR] Errors:")
|| line.starts_with("[ERROR] ")
|| line.starts_with("[ERROR] Failed to execute goal")
{
out.push_str(line);
out.push('\n');
continue;
}
if is_boilerplate(line) {
continue;
}
out.push_str(line);
out.push('\n');
}
out
}
fn mvn_binary() -> &'static str {
if cfg!(windows) {
if Path::new(".\\mvnw.cmd").exists() {
".\\mvnw.cmd"
} else {
"mvn"
}
} else if Path::new("./mvnw").exists() {
"./mvnw"
} else {
"mvn"
}
}
fn new_mvn_command(args: &[String]) -> Command {
let mut cmd = if cfg!(windows) {
if Path::new(".\\mvnw.cmd").exists() {
Command::new(".\\mvnw.cmd")
} else {
resolved_command("mvn")
}
} else if Path::new("./mvnw").exists() {
Command::new("./mvnw")
} else {
resolved_command("mvn")
};
cmd.args(args);
cmd
}
pub fn run(args: &[String], verbose: u8) -> Result<i32> {
if args
.iter()
.any(|a| matches!(a.as_str(), "-X" | "--debug" | "-e" | "--errors"))
{
let osargs: Vec<OsString> = args.iter().map(OsString::from).collect();
return runner::run_passthrough(mvn_binary(), &osargs, verbose);
}
let tool = mvn_binary();
let args_display = args.join(" ");
if is_quiet(args) {
let phase = detect_phase(args);
if matches!(phase, MvnPhase::Passthrough) {
let osargs: Vec<OsString> = args.iter().map(OsString::from).collect();
return runner::run_passthrough(tool, &osargs, verbose);
}
return runner::run_filtered(
new_mvn_command(args),
tool,
&args_display,
filter_quiet,
RunOptions::with_tee("mvn_quiet"),
);
}
let phase = detect_phase(args);
match phase {
MvnPhase::Test => runner::run_filtered(
new_mvn_command(args),
tool,
&args_display,
filter_surefire,
RunOptions::with_tee("mvn_test"),
),
MvnPhase::Compile => runner::run_filtered(
new_mvn_command(args),
tool,
&args_display,
filter_compile,
RunOptions::with_tee("mvn_compile"),
),
MvnPhase::Package => runner::run_filtered(
new_mvn_command(args),
tool,
&args_display,
filter_package,
RunOptions::with_tee("mvn_package"),
),
MvnPhase::Passthrough => {
let osargs: Vec<OsString> = args.iter().map(OsString::from).collect();
runner::run_passthrough(tool, &osargs, verbose)
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use flate2::read::GzDecoder;
use std::io::Read;
fn count_tokens(s: &str) -> usize {
s.split_whitespace().count()
}
fn gunzip(bytes: &[u8]) -> String {
let mut s = String::new();
GzDecoder::new(bytes)
.read_to_string(&mut s)
.expect("gunzip");
s
}
fn s<S: Into<String>>(it: impl IntoIterator<Item = S>) -> Vec<String> {
it.into_iter().map(Into::into).collect()
}
#[test]
fn phase_test() {
assert_eq!(detect_phase(&s(["test"])), MvnPhase::Test);
}
#[test]
fn phase_integration_test() {
assert_eq!(detect_phase(&s(["integration-test"])), MvnPhase::Test);
}
#[test]
fn phase_compile() {
assert_eq!(detect_phase(&s(["compile"])), MvnPhase::Compile);
}
#[test]
fn phase_test_compile() {
assert_eq!(detect_phase(&s(["test-compile"])), MvnPhase::Compile);
}
#[test]
fn phase_install() {
assert_eq!(detect_phase(&s(["install"])), MvnPhase::Package);
}
#[test]
fn phase_package() {
assert_eq!(detect_phase(&s(["package"])), MvnPhase::Package);
}
#[test]
fn phase_verify() {
assert_eq!(detect_phase(&s(["verify"])), MvnPhase::Package);
}
#[test]
fn phase_deploy() {
assert_eq!(detect_phase(&s(["deploy"])), MvnPhase::Package);
}
#[test]
fn phase_clean_install_is_pkg() {
assert_eq!(detect_phase(&s(["clean", "install"])), MvnPhase::Package);
}
#[test]
fn phase_flags_before_goal() {
assert_eq!(
detect_phase(&s(["-B", "-DskipTests", "test"])),
MvnPhase::Test
);
}
#[test]
fn phase_clean_only_passthrough() {
assert_eq!(detect_phase(&s(["clean"])), MvnPhase::Passthrough);
}
#[test]
fn phase_site_passthrough() {
assert_eq!(detect_phase(&s(["site"])), MvnPhase::Passthrough);
}
#[test]
fn phase_plugin_goal_passthrough() {
assert_eq!(
detect_phase(&s(["dependency:tree"])),
MvnPhase::Passthrough
);
}
#[test]
fn phase_empty_passthrough() {
let v: Vec<String> = Vec::new();
assert_eq!(detect_phase(&v), MvnPhase::Passthrough);
}
#[test]
fn phase_version_long() {
assert_eq!(detect_phase(&s(["--version"])), MvnPhase::Passthrough);
}
#[test]
fn phase_version_short() {
assert_eq!(detect_phase(&s(["-v"])), MvnPhase::Passthrough);
}
#[test]
fn phase_version_java_style() {
assert_eq!(detect_phase(&s(["-version"])), MvnPhase::Passthrough);
}
#[test]
fn phase_help() {
assert_eq!(detect_phase(&s(["--help"])), MvnPhase::Passthrough);
}
#[test]
fn filter_surefire_pass_output_compact() {
let i = include_str!("../../../tests/fixtures/mvn_test_pass_slice_raw.txt");
let o = filter_surefire(i);
assert!(!o.contains("Running org.apache.commons.cli.help.UtilTest"));
assert!(!o.contains("Time elapsed: 1.023 s -- in"));
let savings = 100.0 - (count_tokens(&o) as f64 / count_tokens(i) as f64 * 100.0);
assert!(
savings >= 50.0,
"pass-fixture savings >=50%, got {:.1}%",
savings
);
}
#[test]
fn filter_surefire_fail_keeps_signal() {
let i = include_str!("../../../tests/fixtures/mvn_test_fail_slice_raw.txt");
let o = filter_surefire(i);
assert!(o.contains("BUILD FAILURE"));
assert!(o.contains("Failures: 1"));
}
#[test]
fn surefire_drops_passing_block() {
let i = include_str!("../../../tests/fixtures/mvn_test_pass_slice_raw.txt");
let o = filter_surefire(i);
assert!(
!o.contains("at org.junit."),
"framework frames stripped; got:\n{}",
o
);
assert!(
!o.contains("Running org.apache.commons.cli.ConverterTests"),
"passing-test Running line dropped; got:\n{}",
o
);
assert!(
o.contains("BUILD SUCCESS"),
"footer preserved; got:\n{}",
o
);
assert!(
o.contains("Tests run: 977, Failures: 0"),
"aggregate preserved; got:\n{}",
o
);
}
#[test]
fn surefire_preserves_failing_signal() {
let i = include_str!("../../../tests/fixtures/mvn_test_fail_slice_raw.txt");
let o = filter_surefire(i);
assert!(
o.contains("Failures: 1"),
"failing aggregate preserved; got:\n{}",
o
);
assert!(
o.contains("AssertionFailedError"),
"exception class preserved; got:\n{}",
o
);
assert!(
o.contains("at org.apache.commons.cli.RtkInducedFailTest.rtkInducedFailure"),
"user-code frame preserved; got:\n{}",
o
);
assert!(
!o.contains("at org.junit."),
"framework frames stripped in failing block; got:\n{}",
o
);
}
#[test]
fn surefire_matches_legacy_2x_close_line() {
let i = "[INFO] -----< x >-----\n[INFO] Running x.Foo\n[INFO] Tests run: 3, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.123 s - in x.Foo\n[INFO] BUILD SUCCESS\n";
let o = filter_surefire(i);
assert!(
!o.contains("Running x.Foo"),
"2.x ` - in ` close-line matched; passing block dropped; got:\n{}",
o
);
assert!(
o.contains("BUILD SUCCESS"),
"footer preserved; got:\n{}",
o
);
}
#[test]
fn surefire_matches_warning_skipped_close_line() {
let i = "[INFO] -----< x >-----\n[INFO] Running x.Skip\n[WARNING] Tests run: 5, Failures: 0, Errors: 0, Skipped: 5, Time elapsed: 0.010 s -- in x.Skip\n[INFO] BUILD SUCCESS\n";
let o = filter_surefire(i);
assert!(
!o.contains("Running x.Skip"),
"[WARNING] close-line matched; block dropped; got:\n{}",
o
);
}
#[test]
fn surefire_preserves_3x_failure_trail() {
let i = "[INFO] -----< x >-----\n\
[INFO] Running x.Foo\n\
[ERROR] Tests run: 1, Failures: 1, Errors: 0, Skipped: 0, Time elapsed: 0.033 s <<< FAILURE! -- in x.Foo\n\
[ERROR] x.Foo.bar -- Time elapsed: 0.025 s <<< FAILURE!\n\
org.opentest4j.AssertionFailedError: expected: <a> but was: <b>\n\
\tat x.Foo.bar(Foo.java:25)\n\
\tat org.junit.jupiter.api.Assertions.assertEquals(Assertions.java:1)\n\
\n\
[INFO] BUILD FAILURE\n";
let o = filter_surefire(i);
assert!(o.contains("AssertionFailedError"), "exception preserved; got:\n{}", o);
assert!(o.contains("at x.Foo.bar"), "user frame preserved; got:\n{}", o);
assert!(
!o.contains("at org.junit."),
"framework frame stripped in trail; got:\n{}",
o
);
}
#[test]
fn surefire_keeps_all_failures_in_multi_failure_class() {
let i = include_str!("../../../tests/fixtures/mvn_test_multifail_slice_raw.txt");
let o = filter_surefire(i);
assert!(
o.contains("AssertionFailedError: failOne: addition should equal five"),
"first failure message preserved; got:\n{}",
o
);
assert!(
o.contains("IllegalStateException: failTwo: induced error"),
"second failure (ERROR! subline) message preserved; got:\n{}",
o
);
assert!(
o.contains("at com.example.rtk.CalcTest.failOne(CalcTest.java:12)"),
"first user frame preserved; got:\n{}",
o
);
assert!(
o.contains("at com.example.rtk.CalcTest.failTwo(CalcTest.java:17)"),
"second user frame preserved; got:\n{}",
o
);
assert!(
!o.contains("at org.junit."),
"junit frames stripped; got:\n{}",
o
);
assert!(
!o.contains("at java.base/"),
"jdk frames stripped; got:\n{}",
o
);
}
#[test]
fn package_keeps_all_failures_in_multi_failure_class() {
let i = include_str!("../../../tests/fixtures/mvn_test_multifail_slice_raw.txt");
let o = filter_package(i);
assert!(
o.contains("AssertionFailedError: failOne: addition should equal five"),
"first failure message preserved; got:\n{}",
o
);
assert!(
o.contains("IllegalStateException: failTwo: induced error"),
"second failure message preserved; got:\n{}",
o
);
assert!(
!o.contains("at org.junit."),
"junit frames stripped; got:\n{}",
o
);
assert!(
!o.contains("at java.base/"),
"jdk frames stripped; got:\n{}",
o
);
}
#[test]
fn surefire_drop_failing_drops_all_sublines_of_capped_class() {
let i = "[INFO] Scanning for projects...\n\
[INFO] -----< x >-----\n\
[INFO] Running x.FailA\n\
[ERROR] Tests run: 1, Failures: 1, Errors: 0, Skipped: 0, Time elapsed: 0.011 s <<< FAILURE! -- in x.FailA\n\
[ERROR] x.FailA.one -- Time elapsed: 0.010 s <<< FAILURE!\n\
org.opentest4j.AssertionFailedError: boomA\n\
\tat x.FailA.one(FailA.java:10)\n\
\n\
[INFO] Running x.MultiFail\n\
[ERROR] Tests run: 2, Failures: 1, Errors: 1, Skipped: 0, Time elapsed: 0.051 s <<< FAILURE! -- in x.MultiFail\n\
[ERROR] x.MultiFail.first -- Time elapsed: 0.020 s <<< FAILURE!\n\
org.opentest4j.AssertionFailedError: boomFirst\n\
\tat x.MultiFail.first(MultiFail.java:20)\n\
\n\
[ERROR] x.MultiFail.second -- Time elapsed: 0.030 s <<< ERROR!\n\
java.lang.IllegalStateException: boomSecond\n\
\tat x.MultiFail.second(MultiFail.java:30)\n\
\n\
[INFO] BUILD FAILURE\n";
let o = filter_surefire_with_cap(i, 1);
assert!(o.contains("boomA"), "first class kept; got:\n{}", o);
assert!(
!o.contains("Running x.MultiFail") && !o.contains("boomFirst"),
"capped class first block dropped; got:\n{}",
o
);
assert!(
!o.contains("x.MultiFail.second") && !o.contains("boomSecond"),
"capped class second per-test block dropped (re-arm inherits drop); got:\n{}",
o
);
assert!(
o.contains("… +1 more failing test classes"),
"tail counts one class, not one per failure; got:\n{}",
o
);
}
#[test]
fn surefire_rearm_disarms_at_results_boundary() {
let i = "[INFO] -----< x >-----\n\
[INFO] Running x.MultiFail\n\
[ERROR] Tests run: 2, Failures: 2, Errors: 0, Skipped: 0, Time elapsed: 0.051 s <<< FAILURE! -- in x.MultiFail\n\
[ERROR] x.MultiFail.first -- Time elapsed: 0.020 s <<< FAILURE!\n\
org.opentest4j.AssertionFailedError: boomFirst\n\
\n\
[ERROR] x.MultiFail.second -- Time elapsed: 0.030 s <<< FAILURE!\n\
org.opentest4j.AssertionFailedError: boomSecond\n\
\n\
[INFO] Results:\n\
[ERROR] Tests run: 2, Failures: 2, Errors: 0, Skipped: 0\n\
[INFO] BUILD FAILURE\n";
let o = filter_surefire(i);
assert!(o.contains("boomSecond"), "second block kept; got:\n{}", o);
assert!(
o.contains("[INFO] Results:"),
"Results boundary disarms re-arm and is kept; got:\n{}",
o
);
assert!(
o.contains("[ERROR] Tests run: 2, Failures: 2"),
"aggregate kept; got:\n{}",
o
);
}
#[test]
fn surefire_tolerates_double_blank_between_failure_blocks() {
let i = "[INFO] -----< x >-----\n\
[INFO] Running x.MultiFail\n\
[ERROR] Tests run: 2, Failures: 2, Errors: 0, Skipped: 0, Time elapsed: 0.051 s <<< FAILURE! -- in x.MultiFail\n\
[ERROR] x.MultiFail.first -- Time elapsed: 0.020 s <<< FAILURE!\n\
org.opentest4j.AssertionFailedError: boomFirst\n\
\n\
\n\
[ERROR] x.MultiFail.second -- Time elapsed: 0.030 s <<< FAILURE!\n\
org.opentest4j.AssertionFailedError: boomSecond\n\
\n\
[INFO] BUILD FAILURE\n";
let o = filter_surefire(i);
assert!(o.contains("boomFirst"), "first block kept; got:\n{}", o);
assert!(
o.contains("boomSecond"),
"second block re-enters trail across double blank; got:\n{}",
o
);
assert!(
!o.contains("\n\n\n"),
"no spurious blank lines leak; got:\n{:?}",
o
);
}
#[test]
fn surefire_single_failure_output_unchanged() {
let i = include_str!("../../../tests/fixtures/mvn_test_fail_slice_raw.txt");
let o = filter_surefire(i);
let expected = "[INFO] Scanning for projects...\n\
[INFO] ----------------------< commons-cli:commons-cli >-----------------------\n\
[INFO] Building Apache Commons CLI 1.11.1-SNAPSHOT\n\
[INFO] Running org.apache.commons.cli.RtkInducedFailTest\n\
[ERROR] Tests run: 1, Failures: 1, Errors: 0, Skipped: 0, Time elapsed: 0.033 s <<< FAILURE! -- in org.apache.commons.cli.RtkInducedFailTest\n\
[ERROR] org.apache.commons.cli.RtkInducedFailTest.rtkInducedFailure -- Time elapsed: 0.025 s <<< FAILURE!\n\
org.opentest4j.AssertionFailedError: expected: <expected> but was: <actual>\n\
\tat org.apache.commons.cli.RtkInducedFailTest.rtkInducedFailure(RtkInducedFailTest.java:25)\n\
\n\
[INFO] Results:\n\
[ERROR] Failures:\n\
[ERROR] RtkInducedFailTest.rtkInducedFailure:25 expected: <expected> but was: <actual>\n\
[ERROR] Tests run: 978, Failures: 1, Errors: 0, Skipped: 61\n\
[INFO] BUILD FAILURE\n\
[INFO] Total time: 01:05 min\n\
[INFO] Finished at: 2026-05-21T14:57:09Z\n\
[ERROR] Failed to execute goal org.apache.maven.plugins:maven-surefire-plugin:3.5.5:test (default-test) on project commons-cli: There are test failures.\n";
assert_eq!(o, expected, "single-failure output must be byte-identical");
}
#[test]
fn savings_mvn_test_multifail_slice() {
let i = include_str!("../../../tests/fixtures/mvn_test_multifail_slice_raw.txt");
let o = filter_surefire(i);
let savings = 100.0 - (count_tokens(&o) as f64 / count_tokens(i) as f64 * 100.0);
assert!(
savings >= 30.0,
"multifail slice ≥30% savings (dense failure-signal fixture), got {:.1}%",
savings
);
}
#[test]
fn surefire_drops_help_boilerplate_in_nonquiet_mode() {
let i = include_str!("../../../tests/fixtures/mvn_test_multifail_slice_raw.txt");
let o = filter_surefire(i);
assert!(
o.contains("[ERROR] Failed to execute goal"),
"goal terminator kept; got:\n{}",
o
);
assert!(!o.contains("[Help 1]"), "help link stripped; got:\n{}", o);
assert!(
!o.contains("Re-run Maven"),
"re-run hint stripped; got:\n{}",
o
);
assert!(
!o.contains("To see the full stack trace"),
"stack-trace hint stripped; got:\n{}",
o
);
assert!(
!o.contains("See dump files"),
"dump-file pointer stripped; got:\n{}",
o
);
assert!(
!o.lines().any(|l| l.trim_end() == "[ERROR]"),
"bare [ERROR] dividers stripped; got:\n{}",
o
);
}
#[test]
fn close_line_matches_error_marker() {
let line = "[ERROR] Tests run: 1, Failures: 0, Errors: 1, Skipped: 0, Time elapsed: 0.006 s <<< ERROR! -- in com.example.rtk.BoomTest";
let caps = CLOSE
.captures(line)
.expect("CLOSE must match an ERROR!-marked close line");
assert_eq!(caps.get(1).expect("failures group").as_str(), "0");
assert_eq!(caps.get(2).expect("errors group").as_str(), "1");
}
#[test]
fn surefire_keeps_compile_continuation_on_test_phase() {
let i = include_str!("../../../tests/fixtures/mvn_test_compile_fail_slice_raw.txt");
let o = filter_surefire(i);
assert!(o.contains("cannot find symbol"), "ERROR line preserved; got:\n{}", o);
assert!(
o.contains("symbol: variable bar"),
"indented `symbol:` continuation preserved; got:\n{}",
o
);
assert!(
o.contains("location: class org.apache.commons.cli.CompileBreaker"),
"indented `location:` continuation preserved; got:\n{}",
o
);
assert!(o.contains("BUILD FAILURE"), "footer preserved; got:\n{}", o);
}
#[test]
fn package_still_keeps_compile_error_continuation_after_refactor() {
let i = include_str!("../../../tests/fixtures/mvn_compile_error_slice_raw.txt");
let o = filter_package(i);
assert!(o.contains("cannot find symbol"), "ERROR line preserved; got:\n{}", o);
assert!(
o.contains("symbol: variable bar"),
"indented `symbol:` continuation preserved; got:\n{}",
o
);
assert!(
o.contains("location: class org.apache.commons.cli.CompileBreaker"),
"indented `location:` continuation preserved; got:\n{}",
o
);
}
#[test]
fn surefire_keeps_module_banner() {
let i = "[INFO] Scanning for projects...\n[INFO] -----< com.example:myapp >-----\n[INFO] BUILD SUCCESS\n";
let o = filter_surefire(i);
assert!(o.contains("-----< com.example:myapp >-----"));
}
#[test]
fn surefire_preserves_real_durations() {
let i = "[INFO] -----< x >-----\n[INFO] Running x.Foo\n[ERROR] Tests run: 1, Failures: 1, Errors: 0, Skipped: 0, Time elapsed: 2.341 s <<< FAILURE! - in x.Foo\n[INFO] BUILD FAILURE\n[INFO] Total time: 4.567 s\n";
let o = filter_surefire(i);
assert!(
o.contains("2.341 s"),
"raw close-line duration preserved; got:\n{}",
o
);
assert!(
o.contains("Total time: 4.567 s"),
"raw total time preserved; got:\n{}",
o
);
assert!(
!o.contains("Time elapsed: T s"),
"no normalisation in production; got:\n{}",
o
);
}
#[test]
fn footer_guard_french_passthrough() {
let i = include_str!("../../../tests/fixtures/mvn_locale_fr_raw.txt");
let o = filter_surefire(i);
assert!(
o.contains("BUILD ÉCHEC"),
"footer-guard must pass through non-English output; got:\n{}",
o
);
assert_eq!(
o.lines().count(),
i.lines().count(),
"footer-guard returns raw input"
);
}
#[test]
fn footer_guard_no_pom_passthrough() {
let i = include_str!("../../../tests/fixtures/mvn_no_pom_raw.txt");
let o = filter_surefire(i);
assert!(
o.contains("there is no POM"),
"no-pom error preserved; got:\n{}",
o
);
}
#[test]
fn surefire_handles_crlf_line_endings() {
let i_lf = include_str!("../../../tests/fixtures/mvn_test_pass_slice_raw.txt")
.replace("\r\n", "\n");
let o_lf = filter_surefire(&i_lf);
let i_crlf = i_lf.replace('\n', "\r\n");
let o_crlf = filter_surefire(&i_crlf);
assert_eq!(
o_lf,
o_crlf.replace("\r\n", "\n"),
"CRLF filtered output must match LF (modulo line endings)"
);
}
#[test]
fn package_handles_crlf_line_endings() {
let i_lf = include_str!("../../../tests/fixtures/mvn_install_slice_raw.txt")
.replace("\r\n", "\n");
let o_lf = filter_package(&i_lf);
let i_crlf = i_lf.replace('\n', "\r\n");
let o_crlf = filter_package(&i_crlf);
assert_eq!(
o_lf,
o_crlf.replace("\r\n", "\n"),
"CRLF filtered output must match LF (modulo line endings)"
);
}
#[test]
fn surefire_caps_failing_blocks_emits_tail() {
let mut i = String::from(
"[INFO] Scanning for projects...\n\
[INFO] -----< x >-----\n",
);
for n in 1..=5 {
i.push_str(&format!(
"[INFO] Running x.Fail{n}\n\
[ERROR] Tests run: 1, Failures: 1, Errors: 0, Skipped: 0, Time elapsed: 0.0{n}1 s <<< FAILURE! -- in x.Fail{n}\n\
[ERROR] x.Fail{n}.bar -- Time elapsed: 0.0{n}0 s <<< FAILURE!\n\
org.opentest4j.AssertionFailedError: boom{n}\n\
\tat x.Fail{n}.bar(Fail{n}.java:25)\n\
\n",
n = n
));
}
i.push_str("[INFO] BUILD FAILURE\n");
let o = filter_surefire_with_cap(&i, 3);
for n in 1..=3 {
assert!(
o.contains(&format!("Running x.Fail{}", n)),
"Fail{n} kept; got:\n{}",
o,
n = n
);
assert!(
o.contains(&format!("in x.Fail{}", n)),
"Fail{n} close line kept; got:\n{}",
o,
n = n
);
}
for n in 4..=5 {
assert!(
!o.contains(&format!("Running x.Fail{}", n)),
"Fail{n} dropped; got:\n{}",
o,
n = n
);
assert!(
!o.contains(&format!("AssertionFailedError: boom{}", n)),
"Fail{n} exception dropped; got:\n{}",
o,
n = n
);
}
assert!(
o.contains("… +2 more failing test classes"),
"tail emitted; got:\n{}",
o
);
}
#[test]
fn surefire_cap_zero_emits_summary_only() {
let mut i = String::from(
"[INFO] Scanning for projects...\n\
[INFO] -----< x >-----\n",
);
for n in 1..=5 {
i.push_str(&format!(
"[INFO] Running x.Fail{n}\n\
[ERROR] Tests run: 1, Failures: 1, Errors: 0, Skipped: 0, Time elapsed: 0.0{n}1 s <<< FAILURE! -- in x.Fail{n}\n\
\n",
n = n
));
}
i.push_str("[INFO] BUILD FAILURE\n");
let o = filter_surefire_with_cap(&i, 0);
for n in 1..=5 {
assert!(
!o.contains(&format!("Running x.Fail{}", n)),
"Fail{n} dropped under cap=0; got:\n{}",
o,
n = n
);
}
assert!(
o.contains("+5 more failing test classes"),
"tail counts all 5 under cap=0; got:\n{}",
o
);
}
#[test]
fn failures_summary_block_is_capped() {
let mut i = String::from(
"[INFO] -----< x >-----\n\
[INFO] Results:\n\
[INFO]\n\
[ERROR] Failures:\n",
);
for n in 1..=5 {
i.push_str(&format!(
"[ERROR] ClassA.test{n}:25 expected: <a> but was: <b{n}>\n",
n = n
));
}
i.push_str(
"[INFO]\n\
[ERROR] Tests run: 100, Failures: 5, Errors: 0, Skipped: 0\n\
[INFO] BUILD FAILURE\n",
);
let o = filter_surefire_with_cap(&i, 3);
for n in 1..=3 {
assert!(
o.contains(&format!("ClassA.test{}:25", n)),
"entry {n} kept; got:\n{}",
o,
n = n
);
}
for n in 4..=5 {
assert!(
!o.contains(&format!("ClassA.test{}:25", n)),
"entry {n} dropped; got:\n{}",
o,
n = n
);
}
let tail_idx = o
.find("… +2 more failures")
.unwrap_or_else(|| panic!("tail must appear; got:\n{}", o));
let agg_idx = o
.find("[ERROR] Tests run: 100")
.unwrap_or_else(|| panic!("aggregate must appear; got:\n{}", o));
assert!(
tail_idx < agg_idx,
"tail must precede aggregate; tail@{} agg@{}; got:\n{}",
tail_idx,
agg_idx,
o
);
}
#[test]
fn reactor_summary_kept_on_multi_module_pass() {
let i = include_str!("../../../tests/fixtures/mvn_reactor_pass_slice_raw.txt");
let o = filter_package(i);
assert!(
o.contains("Reactor Summary for multi-module-skeleton"),
"reactor summary header preserved; got:\n{}",
o
);
assert!(
o.contains("[INFO] child-a ............................................ SUCCESS"),
"per-module SUCCESS row preserved; got:\n{}",
o
);
assert!(
o.contains("[INFO] child-b ............................................ SUCCESS"),
"second per-module SUCCESS row preserved; got:\n{}",
o
);
assert!(
o.contains("BUILD SUCCESS"),
"footer preserved; got:\n{}",
o
);
}
#[test]
fn reactor_summary_kept_on_multi_module_fail() {
let i = include_str!("../../../tests/fixtures/mvn_reactor_fail_slice_raw.txt");
let o = filter_package(i);
assert!(
o.contains("Reactor Summary for multi-module-skeleton"),
"reactor summary header preserved; got:\n{}",
o
);
assert!(
o.contains("child-a ............................................ SUCCESS"),
"successful module row preserved; got:\n{}",
o
);
assert!(
o.contains("child-b ............................................ FAILURE"),
"failing module row preserved; got:\n{}",
o
);
assert!(o.contains("BUILD FAILURE"), "footer preserved; got:\n{}", o);
assert!(
o.contains("[ERROR] Failed to execute goal"),
"goal terminator preserved; got:\n{}",
o
);
assert!(
o.contains("mvn <args> -rf :child-b"),
"resume hint preserved (actionable signal); got:\n{}",
o
);
assert!(!o.contains("[Help 1]"), "help boilerplate stripped; got:\n{}", o);
assert!(
!o.contains("Re-run Maven"),
"re-run hint stripped; got:\n{}",
o
);
let savings = 100.0 - (count_tokens(&o) as f64 / count_tokens(i) as f64 * 100.0);
assert!(
savings >= 30.0,
"reactor-fail slice savings >=30% (short fixture); got {:.1}%",
savings
);
}
#[test]
fn filter_compile_error_compact() {
let i = include_str!("../../../tests/fixtures/mvn_compile_error_slice_raw.txt");
let o = filter_compile(i);
let savings = 100.0 - (count_tokens(&o) as f64 / count_tokens(i) as f64 * 100.0);
assert!(
savings >= 30.0,
"compile-error fixture is small; >=30% savings, got {:.1}%",
savings
);
}
#[test]
fn compile_preserves_error_continuation() {
let i = include_str!("../../../tests/fixtures/mvn_compile_error_slice_raw.txt");
let o = filter_compile(i);
assert!(o.contains("cannot find symbol"), "ERROR line preserved");
assert!(
o.contains("symbol: variable bar"),
"indented continuation preserved"
);
assert!(o.contains("BUILD FAILURE"), "footer preserved");
assert!(
!o.contains("[Help 1]"),
"help boilerplate stripped in compile path; got:\n{}",
o
);
}
#[test]
fn compile_dedupes_warnings() {
let i = "[INFO] -----< x >-----\n\
[WARNING] /a.java:[1,2] uses deprecated API\n\
[WARNING] /b.java:[3,4] uses deprecated API\n\
[WARNING] /a.java:[5,6] unchecked cast\n\
[INFO] BUILD SUCCESS\n";
let o = filter_compile(i);
let warns = o.matches("[WARNING]").count();
assert_eq!(warns, 2, "dedup by normalised message; got:\n{}", o);
}
#[test]
fn filter_package_install_compact() {
let i = include_str!("../../../tests/fixtures/mvn_install_slice_raw.txt");
let o = filter_package(i);
let savings = 100.0 - (count_tokens(&o) as f64 / count_tokens(i) as f64 * 100.0);
assert!(
savings >= 50.0,
"install-slice savings >=50%, got {:.1}%",
savings
);
}
#[test]
fn package_keeps_install_lines() {
let i = include_str!("../../../tests/fixtures/mvn_install_slice_raw.txt");
let o = filter_package(i);
assert!(
o.contains("Installing"),
"install line preserved; got:\n{}",
o
);
assert!(
o.contains("Building jar:"),
"jar line preserved; got:\n{}",
o
);
assert!(
!o.contains("at org.junit."),
"framework frames stripped; got:\n{}",
o
);
}
#[test]
#[ignore]
fn print_savings_summary() {
let pf = gunzip(include_bytes!("../../../tests/fixtures/mvn_test_pass_full_raw.txt.gz"));
let pf_out = filter_surefire(&pf);
let pf_in_tok = count_tokens(&pf);
let pf_out_tok = count_tokens(&pf_out);
let pf_s = 100.0 - (pf_out_tok as f64 / pf_in_tok as f64 * 100.0);
println!(
"mvn_test_pass_full: {} -> {} tokens ({:.1}% savings)",
pf_in_tok, pf_out_tok, pf_s
);
let inst = gunzip(include_bytes!("../../../tests/fixtures/mvn_install_full_raw.txt.gz"));
let inst_out = filter_package(&inst);
let inst_in_tok = count_tokens(&inst);
let inst_out_tok = count_tokens(&inst_out);
let inst_s = 100.0 - (inst_out_tok as f64 / inst_in_tok as f64 * 100.0);
println!(
"mvn_install_full: {} -> {} tokens ({:.1}% savings)",
inst_in_tok, inst_out_tok, inst_s
);
}
#[test]
fn savings_mvn_test_pass_full() {
let bytes = include_bytes!("../../../tests/fixtures/mvn_test_pass_full_raw.txt.gz");
let i = gunzip(bytes);
let o = filter_surefire(&i);
let savings = 100.0 - (count_tokens(&o) as f64 / count_tokens(&i) as f64 * 100.0);
assert!(
savings >= 90.0,
"mvn test ≥90% savings on full fixture, got {:.1}% (raw={} tok, filtered={} tok)",
savings,
count_tokens(&i),
count_tokens(&o)
);
}
#[test]
fn savings_mvn_install_full() {
let bytes = include_bytes!("../../../tests/fixtures/mvn_install_full_raw.txt.gz");
let i = gunzip(bytes);
let o = filter_package(&i);
let savings = 100.0 - (count_tokens(&o) as f64 / count_tokens(&i) as f64 * 100.0);
assert!(
savings >= 85.0,
"mvn install ≥85% savings on full fixture, got {:.1}% (raw={} tok, filtered={} tok)",
savings,
count_tokens(&i),
count_tokens(&o)
);
}
#[test]
fn quiet_detects_short_flag() {
assert!(is_quiet(&s(["-q", "test"])));
assert!(is_quiet(&s(["test", "-q"])));
assert!(is_quiet(&s(["-B", "-q", "-DskipFoo", "install"])));
}
#[test]
fn quiet_detects_long_flag() {
assert!(is_quiet(&s(["--quiet", "test"])));
}
#[test]
fn quiet_does_not_match_unrelated_flags() {
assert!(!is_quiet(&s(["-Q", "test"])));
assert!(!is_quiet(&s(["-quiet", "test"])));
assert!(!is_quiet(&s(["-B", "test"])));
}
#[test]
fn quiet_green_run_is_empty() {
assert_eq!(filter_quiet(""), "");
assert_eq!(filter_quiet(" \n\n \n"), "");
}
#[test]
fn quiet_fail_strips_framework_and_boilerplate() {
let i = include_str!("../../../tests/fixtures/mvn_quiet_fail_raw.txt");
let o = filter_quiet(i);
assert!(
o.contains("Tests run: 1, Failures: 1, Errors: 0, Skipped: 0"),
"close-line preserved; got:\n{}",
o
);
assert!(
o.contains("AssertionFailedError"),
"exception class preserved; got:\n{}",
o
);
assert!(
o.contains("at x.FailTest.this_will_fail"),
"user-code frame preserved; got:\n{}",
o
);
assert!(
o.contains("[ERROR] Failures:"),
"failure summary header preserved; got:\n{}",
o
);
assert!(
o.contains("[ERROR] Tests run: 6, Failures: 1, Errors: 0, Skipped: 0"),
"aggregate preserved; got:\n{}",
o
);
assert!(
o.contains("[ERROR] Failed to execute goal"),
"goal terminator preserved; got:\n{}",
o
);
assert!(
!o.contains("at org.junit."),
"junit frame stripped; got:\n{}",
o
);
assert!(
!o.contains("at java.base/"),
"java.base frame stripped; got:\n{}",
o
);
assert!(
!o.contains("To see the full stack trace"),
"help boilerplate stripped; got:\n{}",
o
);
assert!(
!o.contains("[Help 1] http"),
"help link stripped; got:\n{}",
o
);
assert!(
!o.contains("See /tmp/") && !o.contains("See dump files"),
"log-pointer lines stripped; got:\n{}",
o
);
}
#[test]
fn savings_mvn_quiet_fail() {
let i = include_str!("../../../tests/fixtures/mvn_quiet_fail_raw.txt");
let o = filter_quiet(i);
let savings = 100.0 - (count_tokens(&o) as f64 / count_tokens(i) as f64 * 100.0);
assert!(
savings >= 50.0,
"mvn -q fail ≥50% savings, got {:.1}% (raw={} tok, filtered={} tok)",
savings,
count_tokens(i),
count_tokens(&o)
);
}
#[test]
fn quiet_unknown_error_line_kept_as_safety_net() {
let i = "[ERROR] Some unexpected error output we don't classify\n";
let o = filter_quiet(i);
assert!(
o.contains("Some unexpected error output"),
"unclassified ERROR line preserved; got:\n{}",
o
);
}
}