refactor(minimizer): applied #[must_use] annotations and simplified minimizer parsing internals
- Applied #[must_use] annotations across minimizer and process APIs to flag ignored return values. - Replaced direct serde_json field reads with typed Value helper calls in several filters. - Simplified filter parsing with chained if-let guards, rfind checks, and related cleanup. - Updated minimizer tests and fixtures to build outputs via write! and fmt::Write.
This commit is contained in:
@@ -76,10 +76,12 @@ impl From<()> for CancelToken {
|
||||
}
|
||||
|
||||
impl CancelToken {
|
||||
#[must_use]
|
||||
pub fn new(timeout_ms: Option<u32>) -> Self {
|
||||
Self::with_timeout(timeout_ms.map(|ms| Duration::from_millis(u64::from(ms))))
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn with_timeout(timeout: Option<Duration>) -> Self {
|
||||
Self { deadline: timeout.map(|duration| Instant::now() + duration), flag: None }
|
||||
}
|
||||
@@ -132,6 +134,7 @@ impl CancelToken {
|
||||
AbortToken(Some(Arc::downgrade(self.flag.get_or_insert_default())))
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn aborted(&self) -> bool {
|
||||
if let Some(flag) = &self.flag
|
||||
&& flag.cause().is_some()
|
||||
|
||||
@@ -25,7 +25,13 @@
|
||||
|
||||
use std::{io::BufReader, sync::LazyLock};
|
||||
|
||||
use brush_parser::{Parser, ParserOptions, SourceInfo, ast::*};
|
||||
use brush_parser::{
|
||||
Parser, ParserOptions, SourceInfo,
|
||||
ast::{
|
||||
AndOr, AndOrList, Command, CommandPrefixOrSuffixItem, CompoundListItem, IoFileRedirectKind,
|
||||
IoFileRedirectTarget, IoRedirect, Pipeline, SourceLocation,
|
||||
},
|
||||
};
|
||||
use regex::Regex;
|
||||
|
||||
/// Result of [`apply_bash_fixups`].
|
||||
@@ -38,6 +44,7 @@ pub struct BashFixupResult {
|
||||
}
|
||||
|
||||
/// Apply the bash fixups to `cmd`. See module docs for full rules.
|
||||
#[must_use]
|
||||
pub fn apply_bash_fixups(cmd: &str) -> BashFixupResult {
|
||||
// Multi-line input is out of scope: heredoc/loop bodies can't be safely
|
||||
// rewritten and the agent rarely passes them as bash tool input. Bailing
|
||||
|
||||
@@ -76,6 +76,7 @@ impl MinimizerOutput {
|
||||
|
||||
/// Transformed output. Caller-supplied `input_bytes` lets the savings
|
||||
/// metric compare pre- and post-filter sizes.
|
||||
#[must_use]
|
||||
pub const fn transformed(text: String, input_bytes: usize) -> Self {
|
||||
let output_bytes = text.len();
|
||||
Self { text, changed: true, input_bytes, output_bytes, filter: "", original_text: None }
|
||||
@@ -111,6 +112,7 @@ impl MinimizerOutput {
|
||||
|
||||
/// Byte count saved by this filter (0 for passthrough).
|
||||
#[allow(dead_code, reason = "test-only API surface")]
|
||||
#[must_use]
|
||||
pub const fn bytes_saved(&self) -> usize {
|
||||
self.input_bytes.saturating_sub(self.output_bytes)
|
||||
}
|
||||
@@ -141,6 +143,7 @@ pub(crate) fn chain_output(
|
||||
/// Apply the configured filter pipeline to a captured buffer.
|
||||
/// Returns the original text unchanged when minimization is disabled, no
|
||||
/// filter matches, or a filter panics.
|
||||
#[must_use]
|
||||
pub fn apply(
|
||||
command: &str,
|
||||
captured: &str,
|
||||
|
||||
@@ -110,6 +110,7 @@ impl Default for MinimizerConfig {
|
||||
impl MinimizerConfig {
|
||||
/// Build a resolved configuration from `MinimizerOptions`, optionally
|
||||
/// merging in a TOML settings file.
|
||||
#[must_use]
|
||||
pub fn from_options(opts: &MinimizerOptions) -> Self {
|
||||
let mut cfg = Self::default();
|
||||
if let Some(enabled) = opts.enabled {
|
||||
@@ -185,6 +186,7 @@ impl MinimizerConfig {
|
||||
}
|
||||
|
||||
/// Whether the engine should attempt to minimize output for `program`.
|
||||
#[must_use]
|
||||
pub fn is_program_enabled(&self, program: &str) -> bool {
|
||||
if !self.enabled {
|
||||
return false;
|
||||
@@ -200,11 +202,13 @@ impl MinimizerConfig {
|
||||
}
|
||||
|
||||
/// Fetch a per-command TOML table, if any.
|
||||
#[must_use]
|
||||
pub fn per_command(&self, program: &str) -> Option<&toml::Value> {
|
||||
self.per_command.get(&program.to_lowercase())
|
||||
}
|
||||
|
||||
/// Whether opted-in filters should fall back to pre-PR legacy behavior.
|
||||
#[must_use]
|
||||
pub const fn legacy_filters_active(&self) -> bool {
|
||||
self.legacy_filters_active
|
||||
}
|
||||
|
||||
@@ -12,12 +12,14 @@ pub struct CommandIdentity {
|
||||
/// The detector intentionally handles the common interactive subset instead
|
||||
/// of emulating a full shell parser. Ambiguous commands return `None` and are
|
||||
/// left streaming unchanged.
|
||||
#[must_use]
|
||||
pub fn detect(command: &str) -> Option<CommandIdentity> {
|
||||
let tokens = tokenize(command);
|
||||
detect_tokens(&tokens)
|
||||
}
|
||||
|
||||
/// Extract command identity from an already-expanded argv vector.
|
||||
#[must_use]
|
||||
pub fn detect_tokens(tokens: &[String]) -> Option<CommandIdentity> {
|
||||
let tokens = strip_launch_prefix(tokens)?;
|
||||
let (program, rest) = tokens.split_first()?;
|
||||
|
||||
@@ -26,6 +26,7 @@ pub enum MinimizerMode {
|
||||
}
|
||||
|
||||
/// Return the minimization mode for a command.
|
||||
#[must_use]
|
||||
pub fn mode_for(command: &str, config: &MinimizerConfig) -> MinimizerMode {
|
||||
match plan::analyze(command) {
|
||||
plan::CommandPlan::Single { .. } => {
|
||||
@@ -62,6 +63,7 @@ pub fn mode_for(command: &str, config: &MinimizerConfig) -> MinimizerMode {
|
||||
|
||||
/// Return true when the command should be captured for minimization.
|
||||
#[allow(dead_code, reason = "test-only API surface")]
|
||||
#[must_use]
|
||||
pub fn should_minimize(command: &str, config: &MinimizerConfig) -> bool {
|
||||
!matches!(mode_for(command, config), MinimizerMode::None)
|
||||
}
|
||||
@@ -77,6 +79,7 @@ pub fn should_minimize(command: &str, config: &MinimizerConfig) -> bool {
|
||||
/// `artifact://<id>` reference back into the visible text before showing it
|
||||
/// to the agent. The minimizer itself never formats the reference — ids are
|
||||
/// assigned by the session store, not content-addressed.
|
||||
#[must_use]
|
||||
pub fn apply(
|
||||
command: &str,
|
||||
captured: &str,
|
||||
@@ -510,6 +513,7 @@ fn builtin_pipelines() -> &'static PipelineRegistry {
|
||||
|
||||
/// Expose the built-in registry's inline tests for the verify CLI surface.
|
||||
#[allow(dead_code, reason = "test-only API surface")]
|
||||
#[must_use]
|
||||
pub fn verify_builtin_filters() -> Vec<pipeline::TestOutcome> {
|
||||
pipeline::run_tests(builtin_pipelines())
|
||||
}
|
||||
@@ -517,6 +521,7 @@ pub fn verify_builtin_filters() -> Vec<pipeline::TestOutcome> {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::{
|
||||
fmt::Write as _,
|
||||
fs,
|
||||
sync::atomic::{AtomicUsize, Ordering},
|
||||
};
|
||||
@@ -628,7 +633,7 @@ only_on_exit = [0]
|
||||
let cfg = MinimizerConfig { enabled: true, ..Default::default() };
|
||||
let mut input = String::new();
|
||||
for i in 0..120 {
|
||||
input.push_str(&format!("/src/file{i}.ts:{i}:1 error Something is wrong rule/name\n"));
|
||||
let _ = writeln!(input, "/src/file{i}.ts:{i}:1 error Something is wrong rule/name");
|
||||
}
|
||||
let npx = apply("npx eslint src/", &input, 1, &cfg);
|
||||
let direct = apply("eslint src/", &input, 1, &cfg);
|
||||
@@ -1084,10 +1089,11 @@ strip_lines_matching = [".*"]
|
||||
let cfg = MinimizerConfig { enabled: true, ..Default::default() };
|
||||
let mut input = String::new();
|
||||
for i in 0..100 {
|
||||
input.push_str(&format!(
|
||||
let _ = write!(
|
||||
input,
|
||||
"== 20240115 Migration{i}: migrating\n-- step\n -> 0.0s\n== 20240115 Migration{i}: \
|
||||
migrated\n\n"
|
||||
));
|
||||
);
|
||||
}
|
||||
let out = apply("rails db:migrate", &input, 0, &cfg);
|
||||
assert!(out.changed);
|
||||
|
||||
@@ -14,10 +14,12 @@ use crate::minimizer::{MinimizerCtx, MinimizerOutput, primitives};
|
||||
const HEAD_LINES: usize = 50;
|
||||
const TAIL_LINES: usize = 20;
|
||||
|
||||
#[must_use]
|
||||
pub fn supports(program: &str, _subcommand: Option<&str>) -> bool {
|
||||
matches!(program, "xxd" | "strings" | "od")
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn filter(ctx: &MinimizerCtx<'_>, input: &str, exit_code: i32) -> MinimizerOutput {
|
||||
// Kill-switch parity (M2): legacy_filters_active=true skips this
|
||||
// filter so callers can rollback without recompile.
|
||||
@@ -44,6 +46,8 @@ pub fn filter(ctx: &MinimizerCtx<'_>, input: &str, exit_code: i32) -> MinimizerO
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::fmt::Write as _;
|
||||
|
||||
use super::*;
|
||||
use crate::minimizer::MinimizerConfig;
|
||||
|
||||
@@ -54,7 +58,7 @@ mod tests {
|
||||
fn build_lines(prefix: &str, count: usize) -> String {
|
||||
let mut s = String::new();
|
||||
for i in 0..count {
|
||||
s.push_str(&format!("{prefix}{i:08x}\n"));
|
||||
let _ = writeln!(s, "{prefix}{i:08x}");
|
||||
}
|
||||
s
|
||||
}
|
||||
@@ -107,9 +111,8 @@ mod tests {
|
||||
#[test]
|
||||
fn binary_tools_legacy_filters_active_passes_through() {
|
||||
// Kill-switch parity (M2).
|
||||
let mut cfg = MinimizerConfig::default();
|
||||
cfg.enabled = true;
|
||||
cfg.legacy_filters_active = true;
|
||||
let cfg =
|
||||
MinimizerConfig { enabled: true, legacy_filters_active: true, ..Default::default() };
|
||||
let input = build_lines("00000000: ", 5000);
|
||||
for prog in ["xxd", "strings", "od"] {
|
||||
let context = ctx(prog, "binary-tool", &cfg);
|
||||
|
||||
@@ -13,6 +13,7 @@ const BUN_TOOL_SUBCOMMANDS: &[&str] =
|
||||
&["tsc", "eslint", "biome", "next", "prettier", "prisma", "jest", "vitest", "playwright"];
|
||||
const BUN_CPP_TOOL_SUBCOMMANDS: &[&str] = &["cmake", "ctest", "ninja", "gtest", "gtest-parallel"];
|
||||
|
||||
#[must_use]
|
||||
pub fn supports(program: &str, subcommand: Option<&str>) -> bool {
|
||||
match program {
|
||||
"bun" => subcommand.is_some_and(|subcommand| {
|
||||
@@ -30,6 +31,7 @@ pub fn supports(program: &str, subcommand: Option<&str>) -> bool {
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn filter(ctx: &MinimizerCtx<'_>, input: &str, exit_code: i32) -> MinimizerOutput {
|
||||
let subcommand = ctx.subcommand;
|
||||
if matches!((ctx.program, subcommand), ("bun", Some(subcommand)) if is_non_exec_package_subcommand(subcommand))
|
||||
|
||||
@@ -4,6 +4,7 @@ use std::{collections::BTreeMap, fmt::Write as _};
|
||||
|
||||
use crate::minimizer::{MinimizerCtx, MinimizerOutput, primitives};
|
||||
|
||||
#[must_use]
|
||||
pub fn supports(subcommand: Option<&str>) -> bool {
|
||||
matches!(
|
||||
subcommand,
|
||||
@@ -22,6 +23,7 @@ pub fn supports(subcommand: Option<&str>) -> bool {
|
||||
)
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn filter(ctx: &MinimizerCtx<'_>, input: &str, exit_code: i32) -> MinimizerOutput {
|
||||
let cleaned = primitives::strip_ansi(input);
|
||||
let text = match ctx.subcommand {
|
||||
@@ -67,7 +69,7 @@ fn is_compiling_noise(line: &str) -> bool {
|
||||
|| is_generated_warnings_rollup(trimmed)
|
||||
}
|
||||
|
||||
/// The per-crate rollup line `warning: \`crate\` (lib) generated N warnings`.
|
||||
/// The per-crate rollup line warning: `crate` (lib) generated N warnings.
|
||||
/// The individual `warning: ...` diagnostic blocks are kept; this redundant
|
||||
/// tally is dropped. Clippy/install paths already skip it explicitly, so
|
||||
/// stripping it here only affects build/check/doc/run condensing.
|
||||
|
||||
@@ -28,10 +28,12 @@ const SENSITIVE_AWS_KEYS: &[&str] = &[
|
||||
"ResponseMetadata",
|
||||
];
|
||||
|
||||
#[must_use]
|
||||
pub fn supports(program: &str, _subcommand: Option<&str>) -> bool {
|
||||
matches!(program, "aws" | "curl" | "wget" | "psql")
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn filter(ctx: &MinimizerCtx<'_>, input: &str, exit_code: i32) -> MinimizerOutput {
|
||||
let cleaned = primitives::strip_ansi(input);
|
||||
let text = match ctx.program {
|
||||
@@ -708,7 +710,7 @@ fn compact_aws_cloudwatch_events(events: &[&Value]) -> String {
|
||||
for event in events {
|
||||
let ts = event
|
||||
.get("timestamp")
|
||||
.and_then(|v| v.as_i64())
|
||||
.and_then(serde_json::Value::as_i64)
|
||||
.map_or_else(|| "?".to_string(), epoch_ms_to_iso);
|
||||
let msg = event.get("message").and_then(|v| v.as_str()).unwrap_or("?");
|
||||
// Truncate long messages
|
||||
@@ -1479,7 +1481,8 @@ mod tests {
|
||||
assert!(out.text.contains("age=42"));
|
||||
assert!(out.text.contains("active=true"));
|
||||
assert!(out.text.contains("tags=[a,b]"));
|
||||
assert!(out.text.contains("meta={city:Paris}"));
|
||||
const META_ATTR: &str = "meta={city:Paris}";
|
||||
assert!(out.text.contains(META_ATTR));
|
||||
assert!(out.text.contains("1 item(s)"));
|
||||
}
|
||||
|
||||
@@ -1656,7 +1659,7 @@ mod tests {
|
||||
fn sensitive_aws_keys_never_leak_from_generic() {
|
||||
let mut fields = String::new();
|
||||
for key in SENSITIVE_AWS_KEYS {
|
||||
fields.push_str(&format!(r#""{key}":"LEAK_SENTINEL","#));
|
||||
let _ = write!(fields, r#""{key}":"LEAK_SENTINEL","#);
|
||||
}
|
||||
let input = format!(r#"{{"Unknowns":[{{"Name":"safe",{fields}"Status":"ok"}}]}}"#);
|
||||
let cfg = MinimizerConfig { enabled: true, ..Default::default() };
|
||||
|
||||
@@ -12,14 +12,17 @@ enum CppTool {
|
||||
GTest,
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn supports(program: &str, _subcommand: Option<&str>) -> bool {
|
||||
direct_tool(program).is_some()
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn supports_invocation(command: &str) -> bool {
|
||||
command_tokens(command).any(|token| token_tool(token).is_some())
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn is_gtest_binary_name(program: &str) -> bool {
|
||||
matches!(program, "gtest" | "gtest-parallel")
|
||||
|| program.ends_with("_test")
|
||||
@@ -31,6 +34,7 @@ pub fn is_gtest_binary_name(program: &str) -> bool {
|
||||
.is_some_and(|ext| ext.eq_ignore_ascii_case("test"))
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn filter(ctx: &MinimizerCtx<'_>, input: &str, exit_code: i32) -> MinimizerOutput {
|
||||
let cleaned = primitives::strip_ansi(input);
|
||||
let tool = direct_tool(ctx.program).or_else(|| invocation_tool(ctx.command));
|
||||
|
||||
@@ -6,6 +6,7 @@ use serde_json::Value;
|
||||
|
||||
use crate::minimizer::{MinimizerCtx, MinimizerOutput, primitives};
|
||||
|
||||
#[must_use]
|
||||
pub fn supports(subcommand: Option<&str>) -> bool {
|
||||
matches!(
|
||||
subcommand,
|
||||
@@ -36,6 +37,7 @@ pub fn supports(subcommand: Option<&str>) -> bool {
|
||||
)
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn filter(ctx: &MinimizerCtx<'_>, input: &str, exit_code: i32) -> MinimizerOutput {
|
||||
let cleaned = primitives::strip_ansi(input);
|
||||
let text = match ctx.program {
|
||||
@@ -339,10 +341,17 @@ fn compute_pod_container_stats(status: &Value) -> (usize, usize, i32) {
|
||||
let mut ready = 0usize;
|
||||
let mut restarts = 0i32;
|
||||
for cs in container_statuses {
|
||||
if cs.get("ready").and_then(|v| v.as_bool()).unwrap_or(false) {
|
||||
if cs
|
||||
.get("ready")
|
||||
.and_then(serde_json::Value::as_bool)
|
||||
.unwrap_or(false)
|
||||
{
|
||||
ready += 1;
|
||||
}
|
||||
restarts += cs.get("restartCount").and_then(|v| v.as_i64()).unwrap_or(0) as i32;
|
||||
restarts += cs
|
||||
.get("restartCount")
|
||||
.and_then(serde_json::Value::as_i64)
|
||||
.unwrap_or(0) as i32;
|
||||
}
|
||||
(ready, total, restarts)
|
||||
}
|
||||
@@ -408,15 +417,19 @@ fn format_k8s_ports(ports: Option<&Vec<Value>>) -> String {
|
||||
.map(|p| {
|
||||
let port = p
|
||||
.get("port")
|
||||
.and_then(|v| v.as_i64())
|
||||
.and_then(serde_json::Value::as_i64)
|
||||
.map_or_else(|| "?".to_string(), |v| v.to_string());
|
||||
let proto = p.get("protocol").and_then(|v| v.as_str()).unwrap_or("TCP");
|
||||
let node_port = p.get("nodePort").and_then(|v| v.as_i64());
|
||||
let node_port = p.get("nodePort").and_then(serde_json::Value::as_i64);
|
||||
let target_port = p.get("targetPort");
|
||||
let target = target_port
|
||||
.and_then(|v| v.as_i64())
|
||||
.and_then(serde_json::Value::as_i64)
|
||||
.map(|v| v.to_string())
|
||||
.or_else(|| target_port.and_then(|v| v.as_str()).map(|s| s.to_string()));
|
||||
.or_else(|| {
|
||||
target_port
|
||||
.and_then(|v| v.as_str())
|
||||
.map(std::string::ToString::to_string)
|
||||
});
|
||||
match (target, node_port) {
|
||||
(Some(t), Some(np)) => format!("{port}/{t}:{np}->{port}/{proto}"),
|
||||
(Some(t), None) => format!("{port}/{t}:{port}/{proto}"),
|
||||
@@ -462,7 +475,7 @@ fn is_glog_prefix(line: &str) -> bool {
|
||||
if bytes.len() < 6
|
||||
|| bytes[0] != b'W'
|
||||
|| bytes[5] != b' '
|
||||
|| !bytes[1..5].iter().all(|b| b.is_ascii_digit())
|
||||
|| !bytes[1..5].iter().all(u8::is_ascii_digit)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
@@ -890,7 +903,7 @@ mod tests {
|
||||
};
|
||||
let mut input = String::from("NAME IMAGE COMMAND SERVICE CREATED STATUS PORTS\n");
|
||||
for idx in 0..20 {
|
||||
input.push_str(&format!("svc-{idx} img command api 1m running 8080/tcp\n"));
|
||||
let _ = writeln!(input, "svc-{idx} img command api 1m running 8080/tcp");
|
||||
}
|
||||
let out = filter(&compose_ctx, &input, 0).text;
|
||||
assert!(out.contains("20 rows"));
|
||||
@@ -1210,7 +1223,7 @@ mod tests {
|
||||
let kubectl_ctx = ctx("kubectl", Some("get"), &cfg);
|
||||
let mut input = String::from("NAME STATUS\n");
|
||||
for i in 0..25 {
|
||||
input.push_str(&format!("pod-{} running\n", i));
|
||||
let _ = writeln!(input, "pod-{i} running");
|
||||
}
|
||||
let out = filter(&kubectl_ctx, &input, 0).text;
|
||||
// Should use table compaction, not crash
|
||||
@@ -1330,7 +1343,7 @@ mod tests {
|
||||
};
|
||||
let mut input = String::from("NAME READY STATUS RESTARTS AGE IP NODE\n");
|
||||
for i in 0..25 {
|
||||
input.push_str(&format!("pod-{i} 1/1 Running 0 1h 10.0.0.{i} node\n"));
|
||||
let _ = writeln!(input, "pod-{i} 1/1 Running 0 1h 10.0.0.{i} node");
|
||||
}
|
||||
let out = filter(&ctx, input.as_str(), 0).text;
|
||||
assert!(out.contains("rows"), "-owide is a table format and must be compacted, got: {out}");
|
||||
@@ -1373,7 +1386,7 @@ mod tests {
|
||||
let kubectl_ctx = ctx("kubectl", Some("apply"), &cfg);
|
||||
let mut input = String::new();
|
||||
for i in 0..250 {
|
||||
input.push_str(&format!("deployment.apps/app-{i} unchanged\n"));
|
||||
let _ = writeln!(input, "deployment.apps/app-{i} unchanged");
|
||||
}
|
||||
let out = filter(&kubectl_ctx, &input, 0).text;
|
||||
assert!(
|
||||
|
||||
@@ -2,10 +2,12 @@
|
||||
|
||||
use crate::minimizer::{MinimizerCtx, MinimizerOutput, primitives};
|
||||
|
||||
#[must_use]
|
||||
pub fn supports(program: &str, subcommand: Option<&str>) -> bool {
|
||||
program == "dotnet" && matches!(subcommand, Some("build" | "test" | "restore" | "format"))
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn filter(ctx: &MinimizerCtx<'_>, input: &str, exit_code: i32) -> MinimizerOutput {
|
||||
let cleaned = primitives::strip_ansi(input);
|
||||
let text = match ctx.subcommand {
|
||||
@@ -257,7 +259,7 @@ fn first_string<'a>(
|
||||
fn first_number(map: &serde_json::Map<String, serde_json::Value>, keys: &[&str]) -> Option<u64> {
|
||||
keys
|
||||
.iter()
|
||||
.find_map(|key| map.get(*key).and_then(|value| value.as_u64()))
|
||||
.find_map(|key| map.get(*key).and_then(serde_json::Value::as_u64))
|
||||
}
|
||||
|
||||
fn compact_general(input: &str) -> String {
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
use crate::minimizer::{MinimizerCtx, MinimizerOutput, primitives};
|
||||
|
||||
#[must_use]
|
||||
pub fn filter(_ctx: &MinimizerCtx<'_>, input: &str, _exit_code: i32) -> MinimizerOutput {
|
||||
let stripped = primitives::strip_ansi(input);
|
||||
let deduped = primitives::dedup_consecutive_lines(&stripped);
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
//! GitHub CLI output filters.
|
||||
|
||||
use std::fmt::Write as _;
|
||||
|
||||
use crate::minimizer::{MinimizerCtx, MinimizerOutput, primitives};
|
||||
|
||||
#[must_use]
|
||||
pub fn supports(subcommand: Option<&str>) -> bool {
|
||||
matches!(
|
||||
subcommand,
|
||||
@@ -18,6 +21,7 @@ pub fn supports(subcommand: Option<&str>) -> bool {
|
||||
)
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn filter(ctx: &MinimizerCtx<'_>, input: &str, exit_code: i32) -> MinimizerOutput {
|
||||
if preserves_raw_mode(ctx) {
|
||||
return MinimizerOutput::passthrough(input);
|
||||
@@ -148,12 +152,12 @@ fn filter_pr_checks(input: &str) -> Option<String> {
|
||||
|
||||
let mut out = String::new();
|
||||
let failed = failed_rows.len();
|
||||
out.push_str(&format!("checks: {passed} passed, {failed} failed"));
|
||||
let _ = write!(out, "checks: {passed} passed, {failed} failed");
|
||||
if pending > 0 {
|
||||
out.push_str(&format!(", {pending} pending"));
|
||||
let _ = write!(out, ", {pending} pending");
|
||||
}
|
||||
if skipping > 0 {
|
||||
out.push_str(&format!(", {skipping} skipping"));
|
||||
let _ = write!(out, ", {skipping} skipping");
|
||||
}
|
||||
out.push('\n');
|
||||
// Cap the verbatim failed rows: a PR with hundreds of failing checks would
|
||||
@@ -305,7 +309,7 @@ mod tests {
|
||||
let ctx = test_ctx(Some("pr"), "gh pr checks 123", &cfg);
|
||||
let mut input = String::new();
|
||||
for idx in 0..500 {
|
||||
input.push_str(&format!("X\tcheck{idx}\t1s\thttps://ci.test/{idx}\n"));
|
||||
let _ = writeln!(input, "X\tcheck{idx}\t1s\thttps://ci.test/{idx}");
|
||||
}
|
||||
|
||||
let out = filter(&ctx, &input, 1);
|
||||
|
||||
@@ -4,6 +4,7 @@ use std::fmt::Write as _;
|
||||
|
||||
use crate::minimizer::{MinimizerCtx, MinimizerOutput, primitives};
|
||||
|
||||
#[must_use]
|
||||
pub fn supports(subcommand: Option<&str>) -> bool {
|
||||
matches!(
|
||||
subcommand,
|
||||
@@ -29,6 +30,7 @@ pub fn supports(subcommand: Option<&str>) -> bool {
|
||||
)
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn filter(ctx: &MinimizerCtx<'_>, input: &str, exit_code: i32) -> MinimizerOutput {
|
||||
if is_show_path_content(ctx.command) || is_stash_patch(ctx.command) {
|
||||
return MinimizerOutput::passthrough(input);
|
||||
@@ -1739,9 +1741,10 @@ mod tests {
|
||||
let cfg = MinimizerConfig { enabled: true, ..Default::default() };
|
||||
let ctx =
|
||||
test_ctx(Some("tag"), "git tag --format=%(refname:short)|%(taggerdate:short)", &cfg);
|
||||
let input = (0..45)
|
||||
.map(|idx| format!("v1.{idx}|2026-06-06\n"))
|
||||
.collect::<String>();
|
||||
let input = (0..45).fold(String::new(), |mut s, idx| {
|
||||
let _ = writeln!(s, "v1.{idx}|2026-06-06");
|
||||
s
|
||||
});
|
||||
|
||||
let out = filter(&ctx, &input, 0);
|
||||
|
||||
@@ -1753,9 +1756,10 @@ mod tests {
|
||||
fn tag_delete_output_is_passthrough() {
|
||||
let cfg = MinimizerConfig { enabled: true, ..Default::default() };
|
||||
let ctx = test_ctx(Some("tag"), "git tag -d v1.0 v1.1", &cfg);
|
||||
let input = (0..45)
|
||||
.map(|idx| format!("Deleted tag 'v1.{idx}' (was abc1234)\n"))
|
||||
.collect::<String>();
|
||||
let input = (0..45).fold(String::new(), |mut s, idx| {
|
||||
let _ = writeln!(s, "Deleted tag 'v1.{idx}' (was abc1234)");
|
||||
s
|
||||
});
|
||||
|
||||
let out = filter(&ctx, &input, 0);
|
||||
|
||||
@@ -1767,7 +1771,10 @@ mod tests {
|
||||
fn tag_listing_is_compacted() {
|
||||
let cfg = MinimizerConfig { enabled: true, ..Default::default() };
|
||||
let ctx = test_ctx(Some("tag"), "git tag --list", &cfg);
|
||||
let input = (0..45).map(|idx| format!("v1.{idx}\n")).collect::<String>();
|
||||
let input = (0..45).fold(String::new(), |mut s, idx| {
|
||||
let _ = writeln!(s, "v1.{idx}");
|
||||
s
|
||||
});
|
||||
|
||||
let out = filter(&ctx, &input, 0);
|
||||
|
||||
@@ -2073,10 +2080,8 @@ mod tests {
|
||||
if idx % 3 == 0 {
|
||||
input.push_str(".rs\tnew-");
|
||||
input.push_str(&idx.to_string());
|
||||
input.push_str(".rs\n");
|
||||
} else {
|
||||
input.push_str(".rs\n");
|
||||
}
|
||||
input.push_str(".rs\n");
|
||||
}
|
||||
|
||||
let out = filter(&ctx, &input, 0);
|
||||
@@ -2762,9 +2767,10 @@ error: could not apply abc1234... fix: something\nhint: Resolve all conflicts ma
|
||||
input.push_str("/home/alice/repo abc1234 (bare)\n");
|
||||
input.push_str("/home/alice/repo-detached def5678 (detached HEAD)\n");
|
||||
for idx in 0..20 {
|
||||
input.push_str(&format!(
|
||||
"/home/alice/wt-{idx:02} aaaaaaa{idx:02} [wt-{idx}]\n"
|
||||
));
|
||||
let _ = writeln!(
|
||||
input,
|
||||
"/home/alice/wt-{idx:02} aaaaaaa{idx:02} [wt-{idx}]"
|
||||
);
|
||||
}
|
||||
let out = condense_worktree_with_home(&input, home);
|
||||
|
||||
@@ -2825,7 +2831,7 @@ error: could not apply abc1234... fix: something\nhint: Resolve all conflicts ma
|
||||
let ctx = test_ctx(Some("worktree"), "git worktree list", &cfg);
|
||||
let mut input = String::new();
|
||||
for idx in 0..22usize {
|
||||
input.push_str(&format!("/repo/wt-{idx:02} aaaaaaa{idx:02} [branch-{idx}]\n"));
|
||||
let _ = writeln!(input, "/repo/wt-{idx:02} aaaaaaa{idx:02} [branch-{idx}]");
|
||||
}
|
||||
let out = filter(&ctx, &input, 0);
|
||||
assert!(
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
//! GitLab CLI (glab) output filters.
|
||||
|
||||
use std::sync::LazyLock;
|
||||
use std::{fmt::Write as _, sync::LazyLock};
|
||||
|
||||
use regex::Regex;
|
||||
|
||||
@@ -18,10 +18,12 @@ static BARE_ANSI_RE: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"\[[\d;]+[A-
|
||||
/// Multiple consecutive blank lines (3+ newlines) collapsed to double newline.
|
||||
static MULTI_BLANK_RE: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"\n{3,}").unwrap());
|
||||
|
||||
#[must_use]
|
||||
pub fn supports(subcommand: Option<&str>) -> bool {
|
||||
matches!(subcommand, Some("mr" | "issue" | "ci" | "pipeline" | "release"))
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn filter(ctx: &MinimizerCtx<'_>, input: &str, exit_code: i32) -> MinimizerOutput {
|
||||
if preserves_raw_mode(ctx) {
|
||||
return MinimizerOutput::passthrough(input);
|
||||
@@ -158,14 +160,12 @@ fn filter_release_list(input: &str) -> Option<String> {
|
||||
break;
|
||||
}
|
||||
// Parse "Showing N releases on owner/repo." or similar
|
||||
if total.is_none() {
|
||||
if let Some(rest) = trimmed.strip_prefix("Showing ") {
|
||||
if let Some(n_str) = rest.split_whitespace().next() {
|
||||
if let Ok(n) = n_str.parse::<usize>() {
|
||||
total = Some(n);
|
||||
}
|
||||
}
|
||||
}
|
||||
if total.is_none()
|
||||
&& let Some(rest) = trimmed.strip_prefix("Showing ")
|
||||
&& let Some(n_str) = rest.split_whitespace().next()
|
||||
&& let Ok(n) = n_str.parse::<usize>()
|
||||
{
|
||||
total = Some(n);
|
||||
}
|
||||
lines.next();
|
||||
}
|
||||
@@ -196,9 +196,9 @@ fn filter_release_list(input: &str) -> Option<String> {
|
||||
let created = parts[2].trim();
|
||||
|
||||
if name == tag {
|
||||
filtered.push_str(&format!(" {name} ({created})\n"));
|
||||
let _ = writeln!(filtered, " {name} ({created})");
|
||||
} else {
|
||||
filtered.push_str(&format!(" {name} [{tag}] ({created})\n"));
|
||||
let _ = writeln!(filtered, " {name} [{tag}] ({created})");
|
||||
}
|
||||
|
||||
count += 1;
|
||||
@@ -209,9 +209,9 @@ fn filter_release_list(input: &str) -> Option<String> {
|
||||
}
|
||||
|
||||
// Append omission marker when there are more releases than shown.
|
||||
let omitted = total.map(|t| t.saturating_sub(count)).unwrap_or(0);
|
||||
let omitted = total.map_or(0, |t| t.saturating_sub(count));
|
||||
if omitted > 0 {
|
||||
filtered.push_str(&format!("… {} releases omitted …\n", omitted));
|
||||
let _ = writeln!(filtered, "… {omitted} releases omitted …");
|
||||
} else if has_more {
|
||||
// Total not parsed from preamble but a 21st row was observed; signal
|
||||
// truncation.
|
||||
@@ -653,7 +653,7 @@ section_end:1711234600:build_script[0K
|
||||
// Build input with 25 releases so the cap of 20 is exceeded.
|
||||
let mut input = String::from("Showing 25 releases on owner/repo.\n\nName\tTag\tCreated\n");
|
||||
for i in 1..=25 {
|
||||
input.push_str(&format!("Release {i}\tv{i}.0.0\t{i} days ago\n"));
|
||||
let _ = writeln!(input, "Release {i}\tv{i}.0.0\t{i} days ago");
|
||||
}
|
||||
let cfg = MinimizerConfig { enabled: true, ..Default::default() };
|
||||
let ctx = test_ctx(Some("release"), "glab release list", &cfg);
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
//! Go toolchain output filters.
|
||||
|
||||
use std::fmt::Write as _;
|
||||
|
||||
use crate::minimizer::{MinimizerCtx, MinimizerOutput, primitives};
|
||||
|
||||
#[must_use]
|
||||
pub fn supports(program: &str, subcommand: Option<&str>) -> bool {
|
||||
match program {
|
||||
"go" => matches!(subcommand, Some("test" | "build" | "vet" | "tool")),
|
||||
@@ -10,6 +13,7 @@ pub fn supports(program: &str, subcommand: Option<&str>) -> bool {
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn filter(ctx: &MinimizerCtx<'_>, input: &str, exit_code: i32) -> MinimizerOutput {
|
||||
let cleaned = primitives::strip_ansi(input);
|
||||
let text = if ctx.program == "golangci-lint" || is_go_tool_golangci_lint(ctx) {
|
||||
@@ -119,14 +123,12 @@ fn aggregate_go_test_success(input: &str) -> String {
|
||||
// Also check for JSON-wrapped benchmark output — those lines start with
|
||||
// "{" so the raw-line guard above misses them. Bail to head_tail
|
||||
// early so benchmark results are never collapsed into a package count.
|
||||
if trimmed.starts_with('{') {
|
||||
if let Ok(value) = serde_json::from_str::<serde_json::Value>(trimmed) {
|
||||
if let Some(output) = value.get("Output").and_then(|v| v.as_str()) {
|
||||
if output.trim_start().starts_with("Benchmark") {
|
||||
return primitives::head_tail_lines(input, 140, 80);
|
||||
}
|
||||
}
|
||||
}
|
||||
if trimmed.starts_with('{')
|
||||
&& let Ok(value) = serde_json::from_str::<serde_json::Value>(trimmed)
|
||||
&& let Some(output) = value.get("Output").and_then(|v| v.as_str())
|
||||
&& output.trim_start().starts_with("Benchmark")
|
||||
{
|
||||
return primitives::head_tail_lines(input, 140, 80);
|
||||
}
|
||||
|
||||
let candidate = render_go_test_json_line(trimmed);
|
||||
@@ -148,10 +150,10 @@ fn aggregate_go_test_success(input: &str) -> String {
|
||||
|
||||
let mut summary = format!("go test: {packages_ok} packages ok");
|
||||
if no_tests > 0 {
|
||||
summary.push_str(&format!(", {no_tests} no tests"));
|
||||
let _ = write!(summary, ", {no_tests} no tests");
|
||||
}
|
||||
if tests_skipped > 0 {
|
||||
summary.push_str(&format!(", {tests_skipped} tests skipped"));
|
||||
let _ = write!(summary, ", {tests_skipped} tests skipped");
|
||||
}
|
||||
summary.push('\n');
|
||||
summary
|
||||
@@ -312,12 +314,12 @@ fn summarize_golangci_json(line: &str) -> Option<String> {
|
||||
let line_no = issue
|
||||
.get("Pos")
|
||||
.and_then(|pos| pos.get("Line"))
|
||||
.and_then(|v| v.as_u64())
|
||||
.and_then(serde_json::Value::as_u64)
|
||||
.map_or(0, |value| value);
|
||||
let col_no = issue
|
||||
.get("Pos")
|
||||
.and_then(|pos| pos.get("Column"))
|
||||
.and_then(|v| v.as_u64())
|
||||
.and_then(serde_json::Value::as_u64)
|
||||
.map_or(0, |value| value);
|
||||
let linter = issue
|
||||
.get("FromLinter")
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
//! Graphite (`gt`) output filters.
|
||||
|
||||
use std::fmt::Write as _;
|
||||
|
||||
use super::git;
|
||||
use crate::minimizer::{MinimizerCtx, MinimizerOutput, primitives};
|
||||
|
||||
@@ -8,10 +10,12 @@ const GT_SUBCOMMANDS: &[&str] = &[
|
||||
"fetch", "stash", "worktree",
|
||||
];
|
||||
|
||||
#[must_use]
|
||||
pub fn supports(program: &str, subcommand: Option<&str>) -> bool {
|
||||
program == "gt" && subcommand.is_some_and(|subcommand| GT_SUBCOMMANDS.contains(&subcommand))
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn filter(ctx: &MinimizerCtx<'_>, input: &str, exit_code: i32) -> MinimizerOutput {
|
||||
if ctx.subcommand == Some("log") && is_log_short(ctx.command) {
|
||||
return MinimizerOutput::passthrough(input);
|
||||
@@ -172,9 +176,9 @@ fn dense_sync_summary(input: &str, exit_code: i32) -> Option<String> {
|
||||
const DELETED_NAME_CAP: usize = 20;
|
||||
let shown = deleted_names.len().min(DELETED_NAME_CAP);
|
||||
let names = deleted_names[..shown].join(", ");
|
||||
summary.push_str(&format!(" ({names}"));
|
||||
let _ = write!(summary, " ({names}");
|
||||
if deleted_names.len() > DELETED_NAME_CAP {
|
||||
summary.push_str(&format!(", +{} more", deleted_names.len() - DELETED_NAME_CAP));
|
||||
let _ = write!(summary, ", +{} more", deleted_names.len() - DELETED_NAME_CAP);
|
||||
}
|
||||
summary.push(')');
|
||||
}
|
||||
@@ -440,7 +444,7 @@ mod tests {
|
||||
let ctx = test_ctx(Some("sync"), &cfg);
|
||||
let mut input = String::from("Synced with remote\n");
|
||||
for idx in 0..500 {
|
||||
input.push_str(&format!("Deleted branch feat/merged-{idx}\n"));
|
||||
let _ = writeln!(input, "Deleted branch feat/merged-{idx}");
|
||||
}
|
||||
|
||||
let out = filter(&ctx, &input, 0);
|
||||
|
||||
@@ -8,10 +8,12 @@ use crate::minimizer::{MinimizerCtx, MinimizerOutput, primitives};
|
||||
const SUPPORTED_TOOLS: &[&str] = &["next", "prettier", "prisma"];
|
||||
const NPX_ROUTABLE_TOOLS: &[&str] = &["tsc", "eslint", "prisma", "prettier", "next"];
|
||||
|
||||
#[must_use]
|
||||
pub fn supports(program: &str, subcommand: Option<&str>) -> bool {
|
||||
effective_tool(program, subcommand).is_some()
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn filter(ctx: &MinimizerCtx<'_>, input: &str, exit_code: i32) -> MinimizerOutput {
|
||||
let cleaned = primitives::strip_ansi(input);
|
||||
let tool = effective_tool(ctx.program, ctx.subcommand)
|
||||
@@ -31,6 +33,7 @@ pub fn filter(ctx: &MinimizerCtx<'_>, input: &str, exit_code: i32) -> MinimizerO
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn effective_tool<'a>(program: &'a str, subcommand: Option<&'a str>) -> Option<&'a str> {
|
||||
if SUPPORTED_TOOLS.contains(&program) {
|
||||
return Some(program);
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
//! with a Rust module capable of state-machine parsing (block collapse,
|
||||
//! continuation tracking, mode toggle) that the TOML DSL cannot express.
|
||||
|
||||
use std::{collections::HashSet, sync::LazyLock};
|
||||
use std::{collections::HashSet, fmt::Write as _, sync::LazyLock};
|
||||
|
||||
use regex::Regex;
|
||||
|
||||
@@ -97,6 +97,7 @@ static FILE_COORD: LazyLock<Regex> =
|
||||
/// The active phase is decided inside [`filter`] by re-tokenizing the raw
|
||||
/// command, never by `ctx.subcommand` (which is the FIRST non-flag arg and so
|
||||
/// mis-reports the phase for `mvn clean install` — it would say `clean`).
|
||||
#[must_use]
|
||||
pub fn supports(program: &str, _subcommand: Option<&str>) -> bool {
|
||||
is_mvn_family(program) || is_gradle_family(program)
|
||||
}
|
||||
@@ -111,6 +112,7 @@ fn is_gradle_family(program: &str) -> bool {
|
||||
matches!(program, "gradle" | "gradlew" | "gradlew.bat")
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn filter(ctx: &MinimizerCtx<'_>, input: &str, _exit_code: i32) -> MinimizerOutput {
|
||||
if is_gradle_family(ctx.program) {
|
||||
return filter_gradle(ctx, input);
|
||||
@@ -221,7 +223,8 @@ pub enum MvnPhase {
|
||||
Passthrough, // clean, site, plugin goals, version/help, empty
|
||||
}
|
||||
|
||||
/// Whether `a` is a Maven lifecycle phase or plugin goal we map to a MvnPhase.
|
||||
/// Whether `a` is a Maven lifecycle phase or plugin goal we map to a
|
||||
/// `MvnPhase`.
|
||||
fn is_recognized_mvn_goal(a: &str) -> bool {
|
||||
matches!(
|
||||
a,
|
||||
@@ -246,7 +249,7 @@ fn is_recognized_mvn_goal(a: &str) -> bool {
|
||||
/// mistaken for build goals/tasks.
|
||||
fn jvm_positional_tokens<'a>(command: &'a str, value_flags: &[&str]) -> Vec<&'a str> {
|
||||
let mut out = Vec::new();
|
||||
let mut tokens = command.split_whitespace().skip(1).peekable();
|
||||
let mut tokens = command.split_whitespace().skip(1);
|
||||
while let Some(tok) = tokens.next() {
|
||||
if tok.starts_with('-') {
|
||||
let bare = tok.trim_start_matches('-');
|
||||
@@ -270,6 +273,7 @@ fn jvm_positional_tokens<'a>(command: &'a str, value_flags: &[&str]) -> Vec<&'a
|
||||
/// If empty, plugin-form (`:`), or `clean`/`site` → Passthrough. Re-tokenizes
|
||||
/// the raw command because `ctx.subcommand` is the FIRST non-flag arg and so
|
||||
/// reports the wrong goal for `mvn clean install`.
|
||||
#[must_use]
|
||||
pub fn detect_phase(command: &str) -> MvnPhase {
|
||||
// Use the last RECOGNIZED lifecycle goal so that option-value tokens
|
||||
// (e.g. the module name after -pl) are ignored.
|
||||
@@ -292,8 +296,7 @@ pub fn detect_phase(command: &str) -> MvnPhase {
|
||||
"--threads",
|
||||
])
|
||||
.into_iter()
|
||||
.filter(|a| is_recognized_mvn_goal(a))
|
||||
.last()
|
||||
.rfind(|a| is_recognized_mvn_goal(a))
|
||||
.unwrap_or("");
|
||||
|
||||
// `spring-boot:run` is checked BEFORE the generic `:`-plugin-goal guard
|
||||
@@ -652,7 +655,7 @@ impl FailuresSummaryCap {
|
||||
return;
|
||||
}
|
||||
if self.dropped > 0 {
|
||||
out.push_str(&format!("\n… +{} more failures\n", self.dropped));
|
||||
let _ = write!(out, "\n… +{} more failures\n", self.dropped);
|
||||
}
|
||||
self.in_summary = false;
|
||||
self.emitted = 0;
|
||||
@@ -662,7 +665,7 @@ impl FailuresSummaryCap {
|
||||
/// End-of-stream tail emission for cases where the AGG line never arrives.
|
||||
fn finish(&self, out: &mut String) {
|
||||
if self.in_summary && self.dropped > 0 {
|
||||
out.push_str(&format!("\n… +{} more failures\n", self.dropped));
|
||||
let _ = write!(out, "\n… +{} more failures\n", self.dropped);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -671,6 +674,7 @@ impl FailuresSummaryCap {
|
||||
///
|
||||
/// English-footer guard: if no `BUILD SUCCESS`/`BUILD FAILURE` line is present,
|
||||
/// return the ANSI-stripped raw input (non-English locale or truncated output).
|
||||
#[must_use]
|
||||
pub fn filter_surefire(raw: &str) -> String {
|
||||
filter_surefire_with_cap(raw, max_mvn_failing_classes())
|
||||
}
|
||||
@@ -740,7 +744,7 @@ fn filter_surefire_with_cap(raw: &str, cap: usize) -> String {
|
||||
block.finish(&mut out);
|
||||
summary.finish(&mut out);
|
||||
if dropped_failing > 0 {
|
||||
out.push_str(&format!("\n… +{dropped_failing} more failing test classes\n"));
|
||||
let _ = write!(out, "\n… +{dropped_failing} more failing test classes\n");
|
||||
}
|
||||
out
|
||||
}
|
||||
@@ -822,6 +826,7 @@ pub fn filter_compile(raw: &str) -> String {
|
||||
/// `[INFO] Running …` line is seen (via [`SurefireBlock`]). Outside any
|
||||
/// Surefire block, applies the unified keep-list (compile keepers +
|
||||
/// install/artifact lines).
|
||||
#[must_use]
|
||||
pub fn filter_package(raw: &str) -> String {
|
||||
filter_package_with_cap(raw, max_mvn_failing_classes())
|
||||
}
|
||||
@@ -898,7 +903,7 @@ fn filter_package_with_cap(raw: &str, cap: usize) -> String {
|
||||
block.finish(&mut out);
|
||||
summary.finish(&mut out);
|
||||
if dropped_failing > 0 {
|
||||
out.push_str(&format!("\n… +{dropped_failing} more failing test classes\n"));
|
||||
let _ = write!(out, "\n… +{dropped_failing} more failing test classes\n");
|
||||
}
|
||||
out
|
||||
}
|
||||
@@ -1592,12 +1597,12 @@ fn filter_gradle_dependencies(input: &str) -> String {
|
||||
|
||||
let cap = max_gradle_deps();
|
||||
for (config, deps) in &configs {
|
||||
result.push_str(&format!("\n{} ({}):\n", config, deps.len()));
|
||||
let _ = write!(result, "\n{} ({}):\n", config, deps.len());
|
||||
for dep in deps.iter().take(cap) {
|
||||
result.push_str(&format!(" {dep}\n"));
|
||||
let _ = writeln!(result, " {dep}");
|
||||
}
|
||||
if deps.len() > cap {
|
||||
result.push_str(&format!(" ... +{} more\n", deps.len() - cap));
|
||||
let _ = writeln!(result, " ... +{} more", deps.len() - cap);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2188,12 +2193,13 @@ mod tests {
|
||||
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!(
|
||||
let _ = write!(
|
||||
i,
|
||||
"[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!\norg.opentest4j.AssertionFailedError: boom{n}\n\tat \
|
||||
x.Fail{n}.bar(Fail{n}.java:25)\n\n"
|
||||
));
|
||||
);
|
||||
}
|
||||
i.push_str("[INFO] BUILD FAILURE\n");
|
||||
let o = filter_surefire_with_cap(&i, 3);
|
||||
@@ -2215,10 +2221,11 @@ mod tests {
|
||||
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!(
|
||||
let _ = write!(
|
||||
i,
|
||||
"[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"
|
||||
));
|
||||
);
|
||||
}
|
||||
i.push_str("[INFO] BUILD FAILURE\n");
|
||||
let o = filter_surefire_with_cap(&i, 0);
|
||||
@@ -2236,7 +2243,7 @@ mod tests {
|
||||
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"));
|
||||
let _ = writeln!(i, "[ERROR] ClassA.test{n}:25 expected: <a> but was: <b{n}>");
|
||||
}
|
||||
i.push_str(
|
||||
"[INFO]\n[ERROR] Tests run: 100, Failures: 5, Errors: 0, Skipped: 0\n[INFO] BUILD \
|
||||
|
||||
@@ -4,10 +4,12 @@ use std::collections::BTreeMap;
|
||||
|
||||
use crate::minimizer::{MinimizerCtx, MinimizerOutput, primitives};
|
||||
|
||||
#[must_use]
|
||||
pub fn supports(subcommand: Option<&str>) -> bool {
|
||||
supports_program("", subcommand)
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn supports_program(program: &str, subcommand: Option<&str>) -> bool {
|
||||
// Program-claim the JS type-checker/linters too: without this, a path-arg
|
||||
// invocation (`tsc --project x`, `eslint src/`, `biome ci app/`,
|
||||
@@ -39,6 +41,7 @@ fn is_js_lint_program(program: &str) -> bool {
|
||||
matches!(program, "tsc" | "eslint" | "biome" | "oxlint")
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn filter(ctx: &MinimizerCtx<'_>, input: &str, exit_code: i32) -> MinimizerOutput {
|
||||
if preserves_machine_readable_output(ctx) {
|
||||
return MinimizerOutput::passthrough(input);
|
||||
@@ -52,6 +55,7 @@ pub fn filter(ctx: &MinimizerCtx<'_>, input: &str, exit_code: i32) -> MinimizerO
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn condense_lint_output(program: &str, input: &str, exit_code: i32) -> String {
|
||||
let cleaned = primitives::strip_ansi(input);
|
||||
let stripped = strip_lint_noise(program, &cleaned, exit_code);
|
||||
@@ -99,14 +103,13 @@ fn preserves_machine_readable_output(ctx: &MinimizerCtx<'_>) -> bool {
|
||||
if ctx.program == "eslint" {
|
||||
let tokens: Vec<&str> = ctx.command.split_whitespace().collect();
|
||||
for (i, t) in tokens.iter().enumerate() {
|
||||
if (*t == "-f" || *t == "--format") && tokens.get(i + 1).map_or(false, |v| *v != "stylish")
|
||||
{
|
||||
if (*t == "-f" || *t == "--format") && tokens.get(i + 1).is_some_and(|v| *v != "stylish") {
|
||||
return true;
|
||||
}
|
||||
if let Some(val) = t.strip_prefix("--format=") {
|
||||
if val != "stylish" {
|
||||
return true;
|
||||
}
|
||||
if let Some(val) = t.strip_prefix("--format=")
|
||||
&& val != "stylish"
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -180,7 +183,7 @@ fn is_pyright_banner_noise(line: &str) -> bool {
|
||||
}
|
||||
// `Found 42 source files`: literal prefix, then a count, then `source file`.
|
||||
if let Some(rest) = line.strip_prefix("Found ") {
|
||||
let digits: String = rest.chars().take_while(|c| c.is_ascii_digit()).collect();
|
||||
let digits: String = rest.chars().take_while(char::is_ascii_digit).collect();
|
||||
if !digits.is_empty() {
|
||||
let after = rest[digits.len()..].trim_start();
|
||||
if after.starts_with("source file") {
|
||||
@@ -485,6 +488,7 @@ fn is_bare_gutter_numbered_line(trimmed: &str) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn group_diagnostics(input: &str) -> String {
|
||||
let mut grouped: BTreeMap<String, Vec<String>> = BTreeMap::new();
|
||||
let mut ungrouped = Vec::new();
|
||||
|
||||
@@ -39,6 +39,7 @@ fn find_outputs_paths_only(command: &str) -> bool {
|
||||
})
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn filter(ctx: &MinimizerCtx<'_>, input: &str, exit_code: i32) -> MinimizerOutput {
|
||||
let cleaned = primitives::strip_ansi(input);
|
||||
let legacy = ctx.config.legacy_filters_active();
|
||||
@@ -502,24 +503,17 @@ fn parse_ls_long_line(line: &str) -> Option<LsEntry> {
|
||||
// Size is the rightmost parseable u64 before the date, but skip
|
||||
// comma-separated device major/minor pairs.
|
||||
let mut size = None;
|
||||
for i in (0..before_parts.len()).rev() {
|
||||
let part = before_parts[i];
|
||||
if let Some((i, part)) = before_parts.iter().enumerate().next_back() {
|
||||
if part.ends_with(',') {
|
||||
// Major number of a device file — skip.
|
||||
size = None;
|
||||
break;
|
||||
}
|
||||
if let Ok(s) = part.parse::<u64>() {
|
||||
} else if let Ok(s) = part.parse::<u64>() {
|
||||
if i > 0 && before_parts[i - 1].ends_with(',') {
|
||||
// This numeric field is the minor number of a device file.
|
||||
size = None;
|
||||
break;
|
||||
} else {
|
||||
size = Some(s);
|
||||
}
|
||||
size = Some(s);
|
||||
break;
|
||||
}
|
||||
// Non-numeric field (e.g. owner/group word) before size: no size.
|
||||
break;
|
||||
}
|
||||
|
||||
Some(LsEntry { name, is_dir: kind == 'd', size, is_file: kind == '-' })
|
||||
@@ -1215,6 +1209,8 @@ fn has_content(text: &str) -> bool {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::fmt::Write as _;
|
||||
|
||||
use super::*;
|
||||
use crate::minimizer::MinimizerConfig;
|
||||
|
||||
@@ -1506,9 +1502,10 @@ mod tests {
|
||||
"overlay 52428800 1234 52427566 0% /var/lib/docker/overlay2/abc/merged\n";
|
||||
let real_fs = "sda1 52428800 5000000 47428800 10% /data\n";
|
||||
let tmpfs_line = "tmpfs 8192000 0 8192000 0% /dev\n";
|
||||
let padding: String = (0..28)
|
||||
.map(|i| format!("sda{i} 1000 500 500 50% /mnt/disk{i}\n"))
|
||||
.collect();
|
||||
let padding: String = (0..28).fold(String::new(), |mut s, i| {
|
||||
let _ = writeln!(s, "sda{i} 1000 500 500 50% /mnt/disk{i}");
|
||||
s
|
||||
});
|
||||
let input = format!("{header}{overlay_root}{overlay_inner}{real_fs}{tmpfs_line}{padding}");
|
||||
|
||||
let out = filter(&ctx, &input, 0);
|
||||
@@ -1713,23 +1710,21 @@ mod tests {
|
||||
// ---------------------------------------------------------------
|
||||
|
||||
fn legacy_cfg() -> MinimizerConfig {
|
||||
let mut cfg = MinimizerConfig::default();
|
||||
cfg.enabled = true;
|
||||
cfg.legacy_filters_active = true;
|
||||
cfg
|
||||
MinimizerConfig { enabled: true, legacy_filters_active: true, ..Default::default() }
|
||||
}
|
||||
|
||||
fn synthesize_grep(matches_per_file: usize, files: usize) -> String {
|
||||
let mut out = String::new();
|
||||
for f in 0..files {
|
||||
for m in 0..matches_per_file {
|
||||
out.push_str(&format!(
|
||||
let _ = writeln!(
|
||||
out,
|
||||
"src/module{f}/file{f}.rs:{ln}: pub fn handler_{f}_{m}(req: Request) -> \
|
||||
Result<Response, AppError> {{ /* body */ }}\n",
|
||||
Result<Response, AppError> {{ /* body */ }}",
|
||||
ln = m * 7 + 1,
|
||||
f = f,
|
||||
m = m,
|
||||
));
|
||||
);
|
||||
}
|
||||
}
|
||||
out
|
||||
@@ -1739,10 +1734,11 @@ mod tests {
|
||||
let mut out = String::new();
|
||||
for d in 0..dirs {
|
||||
for f in 0..per_dir {
|
||||
out.push_str(&format!(
|
||||
let _ = writeln!(
|
||||
out,
|
||||
"./crates/pi-shell/src/minimizer/filters/category{d}/\
|
||||
handler_{f}_with_descriptive_name.rs\n",
|
||||
));
|
||||
handler_{f}_with_descriptive_name.rs",
|
||||
);
|
||||
}
|
||||
}
|
||||
out
|
||||
|
||||
@@ -36,6 +36,7 @@ pub mod ruby;
|
||||
pub mod rust_tools;
|
||||
pub mod system;
|
||||
|
||||
#[must_use]
|
||||
pub fn supports(program: &str, subcommand: Option<&str>) -> bool {
|
||||
match program {
|
||||
"git" | "yadm" => git::supports(subcommand),
|
||||
@@ -141,6 +142,7 @@ fn is_lint_script_token(token: &str) -> bool {
|
||||
}
|
||||
|
||||
/// Apply the matching built-in filter.
|
||||
#[must_use]
|
||||
pub fn filter(ctx: &MinimizerCtx<'_>, input: &str, exit_code: i32) -> MinimizerOutput {
|
||||
let _ = ctx.command;
|
||||
let _ = ctx.config.per_command(ctx.program);
|
||||
@@ -921,9 +923,8 @@ mod tests {
|
||||
fn pytest_legacy_filters_active_passes_through() {
|
||||
// Kill-switch parity (M2): legacy_filters_active=true skips the
|
||||
// pytest state machine even when invoked via `uv pytest`.
|
||||
let mut config = MinimizerConfig::default();
|
||||
config.enabled = true;
|
||||
config.legacy_filters_active = true;
|
||||
let config =
|
||||
MinimizerConfig { enabled: true, legacy_filters_active: true, ..Default::default() };
|
||||
let context = ctx("uv", Some("pytest"), "uv pytest tests/", &config);
|
||||
let out = filter(&context, PYTEST_FAILURE_INPUT, 1);
|
||||
assert_eq!(out.text, PYTEST_FAILURE_INPUT);
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
use crate::minimizer::{MinimizerCtx, MinimizerOutput, primitives};
|
||||
|
||||
#[must_use]
|
||||
pub fn filter(_ctx: &MinimizerCtx<'_>, input: &str, exit_code: i32) -> MinimizerOutput {
|
||||
let cleaned = primitives::strip_ansi(input);
|
||||
let text = if exit_code == 0 {
|
||||
@@ -202,7 +203,7 @@ fn is_playwright_numbered_failure(trimmed: &str) -> bool {
|
||||
saw_digit = true;
|
||||
continue;
|
||||
}
|
||||
return saw_digit && ch == ')' && chars.next().is_some_and(|next| next.is_whitespace());
|
||||
return saw_digit && ch == ')' && chars.next().is_some_and(char::is_whitespace);
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ use std::{collections::HashSet, fmt::Write as _};
|
||||
use crate::minimizer::{MinimizerCtx, MinimizerOutput, primitives};
|
||||
const PACKAGE_TREE_HEAD_LINES: usize = 80;
|
||||
|
||||
#[must_use]
|
||||
pub fn supports(subcommand: Option<&str>) -> bool {
|
||||
matches!(
|
||||
subcommand,
|
||||
@@ -39,6 +40,7 @@ pub fn supports(subcommand: Option<&str>) -> bool {
|
||||
)
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn filter(ctx: &MinimizerCtx<'_>, input: &str, exit_code: i32) -> MinimizerOutput {
|
||||
if exit_code == 0
|
||||
&& (command_contains_any(ctx.command, &["--json"])
|
||||
@@ -783,7 +785,7 @@ mod tests {
|
||||
let context = ctx("npm", Some("list"), "npm list --all", &cfg);
|
||||
let mut input = String::from("app@1.0.0\n");
|
||||
for idx in 0..90 {
|
||||
input.push_str(&format!("├── dep{idx:03}@1.0.0\n"));
|
||||
let _ = writeln!(input, "├── dep{idx:03}@1.0.0");
|
||||
}
|
||||
|
||||
let out = filter(&context, &input, 0);
|
||||
@@ -800,7 +802,7 @@ mod tests {
|
||||
let context = ctx("npm", Some("ls"), "npm ls --depth=0", &cfg);
|
||||
let mut input = String::from("app@1.0.0\n");
|
||||
for idx in 0..90 {
|
||||
input.push_str(&format!("├── dep{idx:03}@1.0.0\n"));
|
||||
let _ = writeln!(input, "├── dep{idx:03}@1.0.0");
|
||||
}
|
||||
|
||||
let out = filter(&context, &input, 0);
|
||||
@@ -816,7 +818,7 @@ mod tests {
|
||||
let mut input =
|
||||
String::from("Legend: production dependency, optional only, dev only\nreact 19.0.0\n");
|
||||
for idx in 0..90 {
|
||||
input.push_str(&format!("└─ dependent{idx:03}\n"));
|
||||
let _ = writeln!(input, "└─ dependent{idx:03}");
|
||||
}
|
||||
|
||||
let out = filter(&context, &input, 0);
|
||||
@@ -876,7 +878,7 @@ mod tests {
|
||||
"Resolved 91 packages in 12ms\nPrepared 2 packages in 3ms\nPackage Version\n",
|
||||
);
|
||||
for idx in 0..90 {
|
||||
input.push_str(&format!("pkg{idx:03} 1.0.{idx}\n"));
|
||||
let _ = writeln!(input, "pkg{idx:03} 1.0.{idx}");
|
||||
}
|
||||
|
||||
let out = filter(&context, &input, 0);
|
||||
@@ -896,7 +898,7 @@ mod tests {
|
||||
let context = ctx("uv", Some("tree"), "uv tree", &cfg);
|
||||
let mut input = String::from("project v1.0.0\n");
|
||||
for idx in 0..90 {
|
||||
input.push_str(&format!("├── pkg{idx:03} v1.0.{idx}\n"));
|
||||
let _ = writeln!(input, "├── pkg{idx:03} v1.0.{idx}");
|
||||
}
|
||||
|
||||
let out = filter(&context, &input, 0);
|
||||
@@ -912,7 +914,7 @@ mod tests {
|
||||
let context = ctx("poetry", Some("show"), "poetry show --tree", &cfg);
|
||||
let mut input = String::from("requests 2.32.0 Python HTTP for Humans.\n");
|
||||
for idx in 0..90 {
|
||||
input.push_str(&format!("├── dep{idx:03} 1.0.{idx}\n"));
|
||||
let _ = writeln!(input, "├── dep{idx:03} 1.0.{idx}");
|
||||
}
|
||||
|
||||
let out = filter(&context, &input, 0);
|
||||
@@ -927,7 +929,7 @@ mod tests {
|
||||
let context = ctx("uv", Some("pip"), "uv pip freeze", &cfg);
|
||||
let mut input = String::new();
|
||||
for idx in 0..90 {
|
||||
input.push_str(&format!("pkg{idx:03}==1.0.{idx}\n"));
|
||||
let _ = writeln!(input, "pkg{idx:03}==1.0.{idx}");
|
||||
}
|
||||
|
||||
let out = filter(&context, &input, 0);
|
||||
@@ -943,7 +945,7 @@ mod tests {
|
||||
let context = ctx("uv", Some("export"), "uv export -f requirements-txt", &cfg);
|
||||
let mut input = String::from("# generated by uv\n");
|
||||
for idx in 0..90 {
|
||||
input.push_str(&format!("pkg{idx:03}==1.0.{idx}\n"));
|
||||
let _ = writeln!(input, "pkg{idx:03}==1.0.{idx}");
|
||||
}
|
||||
|
||||
let out = filter(&context, &input, 0);
|
||||
@@ -958,7 +960,7 @@ mod tests {
|
||||
let context = ctx("poetry", Some("export"), "poetry export -f requirements.txt", &cfg);
|
||||
let mut input = String::from("# generated by poetry\n");
|
||||
for idx in 0..90 {
|
||||
input.push_str(&format!("dep{idx:03}==2.0.{idx}\n"));
|
||||
let _ = writeln!(input, "dep{idx:03}==2.0.{idx}");
|
||||
}
|
||||
|
||||
let out = filter(&context, &input, 0);
|
||||
@@ -988,7 +990,7 @@ mod tests {
|
||||
let context = ctx("pip", Some("list"), "pip list", &cfg);
|
||||
let mut input = String::from("Package Version\n---------- -------\n");
|
||||
for idx in 0..90 {
|
||||
input.push_str(&format!("pkg{idx:03} 1.0.{idx}\n"));
|
||||
let _ = writeln!(input, "pkg{idx:03} 1.0.{idx}");
|
||||
}
|
||||
|
||||
let out = filter(&context, &input, 0);
|
||||
|
||||
@@ -33,6 +33,7 @@ use crate::minimizer::{MinimizerCtx, MinimizerOutput, primitives};
|
||||
/// overflow independent.
|
||||
const MAX_PYTEST_FAILURES: usize = 10;
|
||||
|
||||
#[must_use]
|
||||
pub fn supports(program: &str, subcommand: Option<&str>) -> bool {
|
||||
matches!(program, "pytest" | "ruff" | "mypy")
|
||||
|| matches!(
|
||||
@@ -41,6 +42,7 @@ pub fn supports(program: &str, subcommand: Option<&str>) -> bool {
|
||||
)
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn filter(ctx: &MinimizerCtx<'_>, input: &str, exit_code: i32) -> MinimizerOutput {
|
||||
// Kill-switch parity (M2): when `legacy_filters_active`, fall back to
|
||||
// the pre-PR passthrough so callers can rollback an RTK-port regression
|
||||
@@ -409,6 +411,8 @@ fn has_content(text: &str) -> bool {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::fmt::Write as _;
|
||||
|
||||
use super::*;
|
||||
use crate::minimizer::MinimizerConfig;
|
||||
|
||||
@@ -549,18 +553,19 @@ mod tests {
|
||||
===================================\n",
|
||||
);
|
||||
for i in 0..12 {
|
||||
input.push_str(&format!(
|
||||
let _ = write!(
|
||||
input,
|
||||
"____________________________ test_case_{i} ____________________________\n\n def \
|
||||
test_case_{i}():\n> assert False\nE assert \
|
||||
False\n\ntests/test_many.py:{}: AssertionError\n",
|
||||
i + 1
|
||||
));
|
||||
);
|
||||
}
|
||||
input.push_str(
|
||||
"=========================== short test summary info ===========================\n",
|
||||
);
|
||||
for i in 0..12 {
|
||||
input.push_str(&format!("FAILED tests/test_many.py::test_case_{i} - assert False\n"));
|
||||
let _ = writeln!(input, "FAILED tests/test_many.py::test_case_{i} - assert False");
|
||||
}
|
||||
input.push_str("========================= 12 failed in 0.30s =========================\n");
|
||||
|
||||
@@ -619,29 +624,31 @@ mod tests {
|
||||
====================================\n",
|
||||
);
|
||||
for i in 0..9 {
|
||||
input.push_str(&format!(
|
||||
let _ = write!(
|
||||
input,
|
||||
"_____________________ ERROR collecting tests/test_imp_{i}.py \
|
||||
_____________________\nImportError while importing test module \
|
||||
'tests/test_imp_{i}.py'.\nE ImportError: boom\n"
|
||||
));
|
||||
);
|
||||
}
|
||||
input.push_str(
|
||||
"=================================== FAILURES ===================================\n",
|
||||
);
|
||||
for i in 0..5 {
|
||||
input.push_str(&format!(
|
||||
let _ = write!(
|
||||
input,
|
||||
"_______________________________ test_critical_{i} \
|
||||
________________________________\ntests/pay.py:{}: in test_critical_{i}\n assert \
|
||||
0 == 100\nE assert 0 == 100\ntests/pay.py:{}: AssertionError\n",
|
||||
i + 1,
|
||||
i + 1
|
||||
));
|
||||
);
|
||||
}
|
||||
input.push_str(
|
||||
"=========================== short test summary info ===========================\n",
|
||||
);
|
||||
for i in 0..5 {
|
||||
input.push_str(&format!("FAILED tests/pay.py::test_critical_{i} - assert 0 == 100\n"));
|
||||
let _ = writeln!(input, "FAILED tests/pay.py::test_critical_{i} - assert 0 == 100");
|
||||
}
|
||||
input.push_str(
|
||||
"========================= 5 failed, 9 errors in 0.30s =========================\n",
|
||||
@@ -676,19 +683,21 @@ mod tests {
|
||||
====================================\n",
|
||||
);
|
||||
for i in 0..12 {
|
||||
input.push_str(&format!(
|
||||
let _ = write!(
|
||||
input,
|
||||
"_____________________ ERROR collecting tests/test_imp_{i}.py \
|
||||
_____________________\nE ImportError: boom\n"
|
||||
));
|
||||
);
|
||||
}
|
||||
input.push_str(
|
||||
"=================================== FAILURES ===================================\n",
|
||||
);
|
||||
for i in 0..2 {
|
||||
input.push_str(&format!(
|
||||
let _ = write!(
|
||||
input,
|
||||
"_______________________________ test_real_{i} ________________________________\n \
|
||||
assert False\nE assert False\n"
|
||||
));
|
||||
);
|
||||
}
|
||||
input.push_str(
|
||||
"========================= 2 failed, 12 errors in 0.10s =========================\n",
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
use super::lint;
|
||||
use crate::minimizer::{MinimizerCtx, MinimizerOutput, primitives};
|
||||
|
||||
#[must_use]
|
||||
pub fn supports(program: &str, subcommand: Option<&str>) -> bool {
|
||||
match program {
|
||||
"rspec" | "rubocop" => true,
|
||||
@@ -17,6 +18,7 @@ fn is_def_scoped_subcommand(subcommand: Option<&str>) -> bool {
|
||||
matches!(subcommand, Some("db:migrate" | "db:rollback" | "routes"))
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn filter(ctx: &MinimizerCtx<'_>, input: &str, exit_code: i32) -> MinimizerOutput {
|
||||
let cleaned = primitives::strip_ansi(input);
|
||||
let text = match ruby_tool(ctx.program, ctx.subcommand) {
|
||||
@@ -412,7 +414,7 @@ fn first_json_string<'a>(
|
||||
fn first_json_u64(map: &serde_json::Map<String, serde_json::Value>, keys: &[&str]) -> Option<u64> {
|
||||
keys
|
||||
.iter()
|
||||
.find_map(|key| map.get(*key).and_then(|value| value.as_u64()))
|
||||
.find_map(|key| map.get(*key).and_then(serde_json::Value::as_u64))
|
||||
}
|
||||
|
||||
fn filter_minitest(input: &str, exit_code: i32) -> String {
|
||||
@@ -671,6 +673,8 @@ fn has_content(text: &str) -> bool {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::fmt::Write as _;
|
||||
|
||||
use super::*;
|
||||
use crate::minimizer::MinimizerConfig;
|
||||
|
||||
@@ -880,10 +884,11 @@ mod tests {
|
||||
fn rspec_caps_failure_blocks_at_five() {
|
||||
let mut input = String::from("Failures:\n\n");
|
||||
for i in 1..=7 {
|
||||
input.push_str(&format!(
|
||||
let _ = write!(
|
||||
input,
|
||||
" {i}) Example number {i} fails\n Failure/Error: expect(true).to eq(false)\n \
|
||||
# ./spec/a_spec.rb:{i}\n\n"
|
||||
));
|
||||
);
|
||||
}
|
||||
input.push_str("7 examples, 7 failures\n");
|
||||
let out = filter_rspec(&input, 1);
|
||||
@@ -1118,7 +1123,7 @@ mod tests {
|
||||
};
|
||||
let mut input = String::from("rake aborted!\nNameError: undefined local variable\n");
|
||||
for i in 0..20 {
|
||||
input.push_str(&format!("/app/lib/task_{i}.rb:{i}:in `block'\n"));
|
||||
let _ = writeln!(input, "/app/lib/task_{i}.rb:{i}:in `block'");
|
||||
}
|
||||
input.push_str("\nTasks: TOP => db:seed\n");
|
||||
let out = filter(&context, &input, 1);
|
||||
|
||||
@@ -11,10 +11,12 @@ use std::fmt::Write;
|
||||
|
||||
use crate::minimizer::{MinimizerCtx, MinimizerOutput, primitives};
|
||||
|
||||
#[must_use]
|
||||
pub fn supports(program: &str, _subcommand: Option<&str>) -> bool {
|
||||
matches!(program, "rustfmt")
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn filter(ctx: &MinimizerCtx<'_>, input: &str, exit_code: i32) -> MinimizerOutput {
|
||||
// Kill-switch parity (M2): legacy_filters_active=true skips this
|
||||
// filter so callers can rollback without recompile.
|
||||
@@ -112,7 +114,7 @@ mod tests {
|
||||
let cfg = MinimizerConfig { enabled: true, ..Default::default() };
|
||||
let mut input = String::new();
|
||||
for i in 0..50 {
|
||||
input.push_str(&format!("Diff in src/file_{i}.rs at line 10:\n"));
|
||||
let _ = writeln!(input, "Diff in src/file_{i}.rs at line 10:");
|
||||
for _ in 0..8 {
|
||||
input.push_str("- old line\n");
|
||||
input.push_str("+ new line\n");
|
||||
@@ -147,7 +149,7 @@ mod tests {
|
||||
// Simulate a long usage/error dump with no `Diff in ` headers.
|
||||
let mut input = String::new();
|
||||
for i in 0..400 {
|
||||
input.push_str(&format!("error: usage line {i}\n"));
|
||||
let _ = writeln!(input, "error: usage line {i}");
|
||||
}
|
||||
let out = filter(&context, &input, 1);
|
||||
assert!(out.changed);
|
||||
@@ -158,9 +160,8 @@ mod tests {
|
||||
#[test]
|
||||
fn rustfmt_legacy_filters_active_passes_through() {
|
||||
// Kill-switch parity (M2).
|
||||
let mut cfg = MinimizerConfig::default();
|
||||
cfg.enabled = true;
|
||||
cfg.legacy_filters_active = true;
|
||||
let cfg =
|
||||
MinimizerConfig { enabled: true, legacy_filters_active: true, ..Default::default() };
|
||||
let context = ctx("rustfmt", "rustfmt --check src/", &cfg);
|
||||
let input = "Diff in src/a.rs at line 1:\n-old\n+new\n";
|
||||
let out = filter(&context, input, 1);
|
||||
|
||||
@@ -5,6 +5,7 @@ use std::collections::HashMap;
|
||||
use super::git;
|
||||
use crate::minimizer::{MinimizerCtx, MinimizerOutput, primitives};
|
||||
|
||||
#[must_use]
|
||||
pub fn supports(program: &str) -> bool {
|
||||
matches!(
|
||||
program,
|
||||
@@ -23,6 +24,7 @@ pub fn supports(program: &str) -> bool {
|
||||
)
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn filter(ctx: &MinimizerCtx<'_>, input: &str, exit_code: i32) -> MinimizerOutput {
|
||||
let cleaned = primitives::strip_ansi(input);
|
||||
let command = ctx.program;
|
||||
|
||||
@@ -235,6 +235,7 @@ pub fn compile(name: String, def: PipelineDef) -> Result<CompiledPipeline, Strin
|
||||
|
||||
impl CompiledPipeline {
|
||||
/// Whether this pipeline claims the given `(program, subcommand)` pair.
|
||||
#[must_use]
|
||||
pub fn matches(&self, program: &str, subcommand: Option<&str>) -> bool {
|
||||
if !self.match_command.is_match(program) {
|
||||
return false;
|
||||
@@ -249,6 +250,7 @@ impl CompiledPipeline {
|
||||
}
|
||||
|
||||
/// Whether this pipeline is gated off for the supplied exit code.
|
||||
#[must_use]
|
||||
pub fn skipped_by_exit(&self, exit_code: i32) -> bool {
|
||||
if !self.only_on_exit.is_empty() && !self.only_on_exit.contains(&exit_code) {
|
||||
return true;
|
||||
@@ -260,6 +262,7 @@ impl CompiledPipeline {
|
||||
}
|
||||
|
||||
/// Apply the full 9-stage pipeline to `input`.
|
||||
#[must_use]
|
||||
pub fn apply<'a>(&self, input: &'a str) -> Cow<'a, str> {
|
||||
// Stage 1: strip_ansi
|
||||
let stage1: Cow<'_, str> = if self.strip_ansi {
|
||||
@@ -380,6 +383,7 @@ pub struct PipelineRegistry {
|
||||
|
||||
impl PipelineRegistry {
|
||||
/// Find the first pipeline that claims this `(program, subcommand)` pair.
|
||||
#[must_use]
|
||||
pub fn find(&self, program: &str, subcommand: Option<&str>) -> Option<&CompiledPipeline> {
|
||||
self
|
||||
.pipelines
|
||||
@@ -432,6 +436,7 @@ pub struct TestOutcome {
|
||||
|
||||
/// Run every inline test in `registry` and return the outcomes.
|
||||
#[allow(dead_code, reason = "test-only API surface")]
|
||||
#[must_use]
|
||||
pub fn run_tests(registry: &PipelineRegistry) -> Vec<TestOutcome> {
|
||||
let mut out = Vec::new();
|
||||
for (filter_name, tests) in ®istry.tests {
|
||||
|
||||
@@ -64,6 +64,7 @@ pub enum CommandPlan {
|
||||
}
|
||||
|
||||
/// Parse `command` with `brush-parser` and classify its structure.
|
||||
#[must_use]
|
||||
pub fn analyze(command: &str) -> CommandPlan {
|
||||
let trimmed = command.trim();
|
||||
if trimmed.is_empty() {
|
||||
|
||||
@@ -11,6 +11,7 @@ pub enum CapClass {
|
||||
}
|
||||
|
||||
impl CapClass {
|
||||
#[must_use]
|
||||
pub const fn lines(self) -> usize {
|
||||
match self {
|
||||
Self::Errors => 160,
|
||||
@@ -21,12 +22,14 @@ impl CapClass {
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub const fn reduced(cap: usize, by: usize) -> usize {
|
||||
let reduced = cap.saturating_sub(by);
|
||||
if reduced == 0 && cap > 0 { 1 } else { reduced }
|
||||
}
|
||||
|
||||
/// Remove ANSI CSI escape sequences while preserving line endings verbatim.
|
||||
#[must_use]
|
||||
pub fn strip_ansi(input: &str) -> String {
|
||||
let mut out = String::with_capacity(input.len());
|
||||
let mut chars = input.chars().peekable();
|
||||
@@ -46,6 +49,7 @@ pub fn strip_ansi(input: &str) -> String {
|
||||
}
|
||||
|
||||
/// Collapse consecutive identical lines as `line (×N)`.
|
||||
#[must_use]
|
||||
pub fn dedup_consecutive_lines(input: &str) -> String {
|
||||
let mut out = String::new();
|
||||
let mut previous: Option<&str> = None;
|
||||
@@ -77,6 +81,7 @@ fn flush_repeated(out: &mut String, line: Option<&str>, count: usize) {
|
||||
}
|
||||
|
||||
/// Keep the first `head` and last `tail` lines with an omission marker.
|
||||
#[must_use]
|
||||
pub fn head_tail_lines(input: &str, head: usize, tail: usize) -> String {
|
||||
let lines: Vec<&str> = input.lines().collect();
|
||||
if lines.len() <= head + tail {
|
||||
@@ -99,6 +104,7 @@ pub fn head_tail_lines(input: &str, head: usize, tail: usize) -> String {
|
||||
}
|
||||
|
||||
/// Keep head/tail lines using a named cap class.
|
||||
#[must_use]
|
||||
pub fn head_tail_cap(input: &str, class: CapClass) -> String {
|
||||
let cap = class.lines();
|
||||
let head = reduced(cap, cap / 3);
|
||||
@@ -120,6 +126,7 @@ pub fn strip_lines(input: &str, predicates: &[fn(&str) -> bool]) -> String {
|
||||
}
|
||||
|
||||
/// Group `file:line:message` style diagnostics by file.
|
||||
#[must_use]
|
||||
pub fn group_by_file(input: &str, max_per_file: usize) -> String {
|
||||
let mut grouped: BTreeMap<String, Vec<String>> = BTreeMap::new();
|
||||
let mut ungrouped = Vec::new();
|
||||
@@ -169,6 +176,7 @@ fn split_file_line(line: &str) -> Option<(&str, &str)> {
|
||||
Some((file, rest))
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn command_has_ordered_tokens(command: &str, first: &str, second: &str) -> bool {
|
||||
let mut saw_first = false;
|
||||
for part in command.split_whitespace() {
|
||||
@@ -182,6 +190,7 @@ pub fn command_has_ordered_tokens(command: &str, first: &str, second: &str) -> b
|
||||
false
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn command_has_any_token(command: &str, tokens: &[&str]) -> bool {
|
||||
command.split_whitespace().any(|part| {
|
||||
tokens.iter().any(|token| {
|
||||
@@ -194,14 +203,17 @@ pub fn command_has_any_token(command: &str, tokens: &[&str]) -> bool {
|
||||
}
|
||||
|
||||
/// Dedup consecutive lines then apply a 120-head / 80-tail cap.
|
||||
#[must_use]
|
||||
pub fn head_tail_dedup(input: &str) -> String {
|
||||
head_tail_lines(&dedup_consecutive_lines(input), 120, 80)
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn is_markdown_badge_or_image(line: &str) -> bool {
|
||||
line.starts_with("![") || line.starts_with("[![") || line.contains("img.shields.io")
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn is_horizontal_rule(line: &str) -> bool {
|
||||
line.len() >= 3
|
||||
&& line.chars().all(|ch| matches!(ch, '-' | '*' | '_' | ' '))
|
||||
@@ -209,6 +221,7 @@ pub fn is_horizontal_rule(line: &str) -> bool {
|
||||
}
|
||||
|
||||
/// Compact a long plain listing to head/tail form.
|
||||
#[must_use]
|
||||
pub fn compact_listing(input: &str, max_lines: usize) -> String {
|
||||
let lines: Vec<&str> = input
|
||||
.lines()
|
||||
@@ -244,6 +257,7 @@ pub fn compact_listing(input: &str, max_lines: usize) -> String {
|
||||
///
|
||||
/// `max_chars == 0` is treated as "drop the line"; no marker is emitted in
|
||||
/// that case since the caller asked for an empty result.
|
||||
#[must_use]
|
||||
pub fn truncate_line(line: &str, max_chars: usize) -> String {
|
||||
if max_chars == 0 {
|
||||
return String::new();
|
||||
@@ -266,6 +280,7 @@ pub fn truncate_line(line: &str, max_chars: usize) -> String {
|
||||
}
|
||||
|
||||
/// Keep only the first `head` lines; append a summary marker when truncated.
|
||||
#[must_use]
|
||||
pub fn head_lines_only(input: &str, head: usize) -> String {
|
||||
let lines: Vec<&str> = input.lines().collect();
|
||||
if lines.len() <= head {
|
||||
@@ -284,6 +299,7 @@ pub fn head_lines_only(input: &str, head: usize) -> String {
|
||||
}
|
||||
|
||||
/// Keep only the last `tail` lines; prepend a summary marker when truncated.
|
||||
#[must_use]
|
||||
pub fn tail_lines_only(input: &str, tail: usize) -> String {
|
||||
let lines: Vec<&str> = input.lines().collect();
|
||||
if lines.len() <= tail {
|
||||
@@ -302,6 +318,7 @@ pub fn tail_lines_only(input: &str, tail: usize) -> String {
|
||||
}
|
||||
|
||||
/// Hard cap: keep at most `max` lines, append a truncation marker otherwise.
|
||||
#[must_use]
|
||||
pub fn max_lines(input: &str, max: usize) -> String {
|
||||
let lines: Vec<&str> = input.lines().collect();
|
||||
if lines.len() <= max {
|
||||
@@ -325,6 +342,7 @@ pub fn max_lines(input: &str, max: usize) -> String {
|
||||
/// match the strip set (when present) — i.e. keep is `K AND NOT S`. An
|
||||
/// absent set imposes no constraint, so pure strip and pure keep filtering
|
||||
/// are the degenerate single-set cases.
|
||||
#[must_use]
|
||||
pub fn filter_lines_regex(
|
||||
input: &str,
|
||||
strip: Option<®ex::RegexSet>,
|
||||
|
||||
@@ -1259,16 +1259,19 @@ impl Process {
|
||||
}
|
||||
|
||||
/// Operating-system process identifier for this process reference.
|
||||
#[must_use]
|
||||
pub const fn pid(&self) -> i32 {
|
||||
self.inner.pid()
|
||||
}
|
||||
|
||||
/// Parent process id for this process, when available.
|
||||
#[must_use]
|
||||
pub fn ppid(&self) -> Option<i32> {
|
||||
self.inner.parent_pid()
|
||||
}
|
||||
|
||||
/// Launch arguments for this process.
|
||||
#[must_use]
|
||||
pub fn args(&self) -> Vec<String> {
|
||||
self.inner.args()
|
||||
}
|
||||
@@ -1279,11 +1282,13 @@ impl Process {
|
||||
/// signal abstraction, so the `signal` argument is ignored and the entire
|
||||
/// tree is hard-killed via `TerminateProcess`. Defaults to the POSIX
|
||||
/// hard-kill signal.
|
||||
#[must_use]
|
||||
pub fn kill_tree(&self, signal: Option<i32>) -> u32 {
|
||||
self.signal_tree(signal.unwrap_or(KILL_SIGNAL))
|
||||
}
|
||||
|
||||
/// Process group id for this process, when supported by the platform.
|
||||
#[must_use]
|
||||
pub fn group_id(&self) -> Option<i32> {
|
||||
self.inner.group_id()
|
||||
}
|
||||
@@ -1299,6 +1304,7 @@ impl Process {
|
||||
}
|
||||
|
||||
/// Current status of this process reference.
|
||||
#[must_use]
|
||||
pub fn status(&self) -> ProcessStatus {
|
||||
self.inner.status()
|
||||
}
|
||||
@@ -1463,6 +1469,7 @@ async fn wait_for_exit(
|
||||
/// Send `signal` to the process group `pgid`.
|
||||
/// Returns false when process groups are unsupported on the platform.
|
||||
#[allow(clippy::missing_const_for_fn, reason = "Dispatches to platform-specific implementation")]
|
||||
#[must_use]
|
||||
pub fn kill_process_group(pgid: i32, signal: i32) -> bool {
|
||||
// Defense in depth: refuse to deliver a signal to the harness's own
|
||||
// process group. Doing so terminates the harness along with the targets.
|
||||
@@ -1510,6 +1517,7 @@ pub struct TerminationTargets {
|
||||
|
||||
impl TerminationTargets {
|
||||
/// Create an empty target set.
|
||||
#[must_use]
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
@@ -1533,6 +1541,7 @@ impl TerminationTargets {
|
||||
}
|
||||
|
||||
/// True when no targets have been recorded.
|
||||
#[must_use]
|
||||
pub const fn is_empty(&self) -> bool {
|
||||
self.pgids.is_empty() && self.processes.is_empty()
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
//! All fixture failures are collected before the harness panics so a single run
|
||||
//! reports every regression, not just the first.
|
||||
|
||||
use std::{fs, path::Path};
|
||||
use std::{fmt::Write as _, fs, path::Path};
|
||||
|
||||
use pi_shell::minimizer::{self, MinimizerConfig};
|
||||
|
||||
@@ -158,12 +158,13 @@ fn diff_excerpt(expected: &str, actual: &str) -> String {
|
||||
|
||||
let mut excerpt = String::new();
|
||||
for i in start..(start + 3).min(max) {
|
||||
excerpt.push_str(&format!(
|
||||
let _ = write!(
|
||||
excerpt,
|
||||
" line {}:\n expected {:?}\n actual {:?}\n",
|
||||
i + 1,
|
||||
expected_lines.get(i).copied().unwrap_or("<missing>"),
|
||||
actual_lines.get(i).copied().unwrap_or("<missing>")
|
||||
));
|
||||
);
|
||||
}
|
||||
excerpt.trim_end().to_string()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user