Files
rtk-ai--rtk/src/cmds/rust/cargo_cmd.rs
T
2026-07-13 12:01:57 +08:00

2808 lines
95 KiB
Rust

//! Filters cargo output — build errors, test results, clippy warnings.
use crate::core::args_utils;
use crate::core::runner;
use crate::core::stream::{BlockHandler, BlockStreamFilter, StreamFilter};
use crate::core::truncate::{CAP_ERRORS, CAP_LIST, CAP_WARNINGS};
use crate::core::utils::{join_with_overflow, resolved_command, truncate};
use anyhow::Result;
use serde::Deserialize;
use std::cmp::Ordering;
use std::collections::HashMap;
use std::ffi::OsString;
use std::sync::OnceLock;
#[derive(Debug, Clone)]
pub enum CargoCommand {
Build,
Test,
Clippy,
Check,
Install,
Nextest,
}
pub fn run(cmd: CargoCommand, args: &[String], verbose: u8) -> Result<i32> {
match cmd {
CargoCommand::Build => run_build(args, verbose),
CargoCommand::Test => run_test(args, verbose),
CargoCommand::Clippy => run_clippy(args, verbose),
CargoCommand::Check => run_check(args, verbose),
CargoCommand::Install => run_install(args, verbose),
CargoCommand::Nextest => run_nextest(args, verbose),
}
}
// --- Stream handlers ---
struct CargoBuildHandler {
compiled: usize,
warnings: usize,
error_count: usize,
finished_line: Option<String>,
label: &'static str,
}
impl CargoBuildHandler {
fn with_label(label: &'static str) -> Self {
Self {
compiled: 0,
warnings: 0,
error_count: 0,
finished_line: None,
label,
}
}
}
impl BlockHandler for CargoBuildHandler {
fn should_skip(&mut self, line: &str) -> bool {
let trimmed = line.trim_start();
if trimmed.starts_with("Compiling") || trimmed.starts_with("Checking") {
self.compiled += 1;
return true;
}
if trimmed.starts_with("Downloading") || trimmed.starts_with("Downloaded") {
return true;
}
if trimmed.starts_with("Finished") {
self.finished_line = Some(trimmed.to_string());
return true;
}
if line.starts_with("warning:") && line.contains("generated") && line.contains("warning") {
return true;
}
if (line.starts_with("error:") || line.starts_with("error["))
&& (line.contains("aborting due to") || line.contains("could not compile"))
{
return true;
}
false
}
fn is_block_start(&mut self, line: &str) -> bool {
if line.starts_with("error[") || line.starts_with("error:") {
self.error_count += 1;
return true;
}
if line.starts_with("warning:") || line.starts_with("warning[") {
self.warnings += 1;
return true;
}
false
}
fn is_block_continuation(&mut self, line: &str, block: &[String]) -> bool {
!(line.trim().is_empty() && block.len() > 3)
}
fn format_summary(&self, exit_code: i32, _raw: &str) -> Option<String> {
if self.error_count == 0 && self.warnings == 0 && exit_code == 0 {
return Some(cargo_build_success_line(
self.compiled,
self.finished_line.as_deref(),
self.label,
));
}
// The streamed path only runs for non-json build/check; error blocks are
// emitted live, so the summary carries no rendered diagnostics.
let empty = JsonDiagnostics {
errors: Vec::new(),
warnings: Vec::new(),
};
Some(cargo_build_failure_summary(
self.compiled,
self.error_count,
self.warnings,
&empty,
self.label,
exit_code,
))
}
}
struct CargoTestHandler {
in_failure_section: bool,
in_failure_names: bool,
summary_lines: Vec<String>,
has_compile_errors: bool,
}
impl CargoTestHandler {
fn new() -> Self {
Self {
in_failure_section: false,
in_failure_names: false,
summary_lines: Vec::new(),
has_compile_errors: false,
}
}
}
impl BlockHandler for CargoTestHandler {
fn should_skip(&mut self, line: &str) -> bool {
let trimmed = line.trim_start();
if trimmed.starts_with("Compiling")
|| trimmed.starts_with("Downloading")
|| trimmed.starts_with("Downloaded")
|| trimmed.starts_with("Finished")
{
return true;
}
if line.starts_with("running ") {
return true;
}
if line.starts_with("test ") && line.ends_with("... ok") {
return true;
}
// Track compile errors for fallback
if trimmed.starts_with("error[") || trimmed.starts_with("error:") {
self.has_compile_errors = true;
}
// "failures:" toggles section state
if line == "failures:" {
if self.in_failure_section {
// Second "failures:" = list of failure names — skip them
self.in_failure_names = true;
}
self.in_failure_section = true;
return true;
}
// Skip the failure name listing section
if self.in_failure_names {
if line.starts_with("test result:") {
self.in_failure_names = false;
self.in_failure_section = false;
self.summary_lines.push(line.to_string());
return true;
}
return true;
}
if line.starts_with("test result:") {
self.summary_lines.push(line.to_string());
self.in_failure_section = false;
return true;
}
false
}
fn is_block_start(&mut self, line: &str) -> bool {
self.in_failure_section && line.starts_with("---- ")
}
fn is_block_continuation(&mut self, line: &str, _block: &[String]) -> bool {
self.in_failure_section && !line.starts_with("---- ")
}
fn format_summary(&self, _exit_code: i32, raw: &str) -> Option<String> {
if self.summary_lines.is_empty() {
let json = extract_json_diagnostics(raw);
if self.has_compile_errors || !json.errors.is_empty() {
// Content-based (exit 0): a real compile error yields "cargo test: N
// errors"; a bare "could not compile" leaves the raw tail fallback.
let build_filtered = filter_cargo_build_labeled(raw, "test", 0);
if build_filtered.contains("cargo test:") {
return Some(format!("{}\n", build_filtered));
}
// Fallback: last 5 meaningful lines
let meaningful: Vec<&str> = raw
.lines()
.filter(|l| !l.trim().is_empty() && !l.trim_start().starts_with("Compiling"))
.collect();
let last5: Vec<&str> = meaningful.iter().rev().take(5).rev().copied().collect();
return Some(format!("{}\n", last5.join("\n")));
}
}
// No failures emitted — aggregate pass results
let mut aggregated: Option<AggregatedTestResult> = None;
let mut all_parsed = true;
for line in &self.summary_lines {
if let Some(parsed) = AggregatedTestResult::parse_line(line) {
if let Some(ref mut agg) = aggregated {
agg.merge(&parsed);
} else {
aggregated = Some(parsed);
}
} else {
all_parsed = false;
break;
}
}
if all_parsed {
if let Some(agg) = aggregated {
if agg.suites > 0 {
return Some(format!("{}\n", agg.format_compact()));
}
}
}
// Fallback: show raw summary lines
if !self.summary_lines.is_empty() {
let mut s = String::new();
for line in &self.summary_lines {
s.push_str(line);
s.push('\n');
}
return Some(s);
}
None
}
}
/// Generic cargo command runner with filtering.
/// Builds the Command with restored `--` separator, then delegates to shared runner.
fn run_cargo_filtered<F>(
subcommand: &str,
args: &[String],
verbose: u8,
filter_fn: F,
) -> Result<i32>
where
F: Fn(&str) -> String,
{
let mut cmd = resolved_command("cargo");
cmd.arg(subcommand);
let restored_args = args_utils::restore_double_dash(args);
for arg in &restored_args {
cmd.arg(arg);
}
if verbose > 0 {
eprintln!("Running: cargo {} {}", subcommand, restored_args.join(" "));
}
runner::run_filtered(
cmd,
&format!("cargo {}", subcommand),
&restored_args.join(" "),
filter_fn,
runner::RunOptions::with_tee(&format!("cargo_{}", subcommand)),
)
}
/// Same as `run_cargo_filtered` but the filter also receives the child exit code,
/// so it can tell a genuine failure from a clean run when no diagnostics parse.
fn run_cargo_filtered_with_exit<F>(
subcommand: &str,
args: &[String],
verbose: u8,
filter_fn: F,
) -> Result<i32>
where
F: Fn(&str, i32) -> String,
{
let mut cmd = resolved_command("cargo");
cmd.arg(subcommand);
let restored_args = args_utils::restore_double_dash(args);
for arg in &restored_args {
cmd.arg(arg);
}
if verbose > 0 {
eprintln!("Running: cargo {} {}", subcommand, restored_args.join(" "));
}
runner::run_filtered_with_exit(
cmd,
&format!("cargo {}", subcommand),
&restored_args.join(" "),
filter_fn,
runner::RunOptions::with_tee(&format!("cargo_{}", subcommand)),
)
}
fn run_cargo_streamed(
subcommand: &str,
args: &[String],
verbose: u8,
filter: Box<dyn StreamFilter>,
) -> Result<i32> {
let mut cmd = resolved_command("cargo");
cmd.arg(subcommand);
let restored_args = args_utils::restore_double_dash(args);
for arg in &restored_args {
cmd.arg(arg);
}
if verbose > 0 {
eprintln!("Running: cargo {} {}", subcommand, restored_args.join(" "));
}
runner::run_streamed(
cmd,
&format!("cargo {}", subcommand),
&restored_args.join(" "),
filter,
runner::RunOptions::with_tee(&format!("cargo_{}", subcommand)),
)
}
fn has_json_message_format(args: &[String]) -> bool {
let mut json = false;
let mut iter = args.iter();
while let Some(arg) = iter.next() {
if let Some(val) = arg.strip_prefix("--message-format=") {
json = val.contains("json");
} else if arg == "--message-format" {
if let Some(val) = iter.next() {
json = val.contains("json");
}
}
}
json
}
fn run_build(args: &[String], verbose: u8) -> Result<i32> {
if has_json_message_format(args) {
return run_cargo_filtered_with_exit("build", args, verbose, |o, exit| {
filter_cargo_build_labeled(o, "build", exit)
});
}
run_cargo_streamed(
"build",
args,
verbose,
Box::new(BlockStreamFilter::new(CargoBuildHandler::with_label("build"))),
)
}
fn run_test(args: &[String], verbose: u8) -> Result<i32> {
// No json branch here on purpose: --message-format=json only reformats the
// build phase, the test harness output stays human-readable. CargoTestHandler
// reads both — it aggregates the `test result:` lines and, on a compile error,
// parses the json diagnostics in format_summary.
run_cargo_streamed(
"test",
args,
verbose,
Box::new(BlockStreamFilter::new(CargoTestHandler::new())),
)
}
fn run_clippy(args: &[String], verbose: u8) -> Result<i32> {
if has_json_message_format(args) {
return run_cargo_filtered_with_exit("clippy", args, verbose, filter_cargo_clippy_json);
}
run_cargo_filtered("clippy", args, verbose, filter_cargo_clippy)
}
fn run_check(args: &[String], verbose: u8) -> Result<i32> {
if has_json_message_format(args) {
return run_cargo_filtered_with_exit("check", args, verbose, |o, exit| {
filter_cargo_build_labeled(o, "check", exit)
});
}
run_cargo_streamed(
"check",
args,
verbose,
Box::new(BlockStreamFilter::new(CargoBuildHandler::with_label("check"))),
)
}
fn run_install(args: &[String], verbose: u8) -> Result<i32> {
run_cargo_filtered("install", args, verbose, filter_cargo_install)
}
fn run_nextest(args: &[String], verbose: u8) -> Result<i32> {
run_cargo_filtered("nextest", args, verbose, filter_cargo_nextest)
}
/// Format crate name + version into a display string
fn format_crate_info(name: &str, version: &str, fallback: &str) -> String {
if name.is_empty() {
fallback.to_string()
} else if version.is_empty() {
name.to_string()
} else {
format!("{} {}", name, version)
}
}
/// Filter cargo install output - strip dep compilation, keep installed/replaced/errors
fn filter_cargo_install(output: &str) -> String {
let mut errors: Vec<String> = Vec::new();
let mut error_count = 0;
let mut compiled = 0;
let mut in_error = false;
let mut current_error = Vec::new();
let mut installed_crate = String::new();
let mut installed_version = String::new();
let mut replaced_lines: Vec<String> = Vec::new();
let mut already_installed = false;
let mut ignored_line = String::new();
for line in output.lines() {
let trimmed = line.trim_start();
// Strip noise: dep compilation, downloading, locking, etc.
if trimmed.starts_with("Compiling") {
compiled += 1;
continue;
}
if trimmed.starts_with("Downloading")
|| trimmed.starts_with("Downloaded")
|| trimmed.starts_with("Locking")
|| trimmed.starts_with("Updating")
|| trimmed.starts_with("Adding")
|| trimmed.starts_with("Finished")
|| trimmed.starts_with("Blocking waiting for file lock")
{
continue;
}
// Keep: Installing line (extract crate name + version)
if trimmed.starts_with("Installing") {
let rest = trimmed.strip_prefix("Installing").unwrap_or("").trim();
if !rest.is_empty() && !rest.starts_with('/') {
if let Some((name, version)) = rest.split_once(' ') {
installed_crate = name.to_string();
installed_version = version.to_string();
} else {
installed_crate = rest.to_string();
}
}
continue;
}
// Keep: Installed line (extract crate + version if not already set)
if trimmed.starts_with("Installed") {
let rest = trimmed.strip_prefix("Installed").unwrap_or("").trim();
if !rest.is_empty() && installed_crate.is_empty() {
let mut parts = rest.split_whitespace();
if let (Some(name), Some(version)) = (parts.next(), parts.next()) {
installed_crate = name.to_string();
installed_version = version.to_string();
}
}
continue;
}
// Keep: Replacing/Replaced lines
if trimmed.starts_with("Replacing") || trimmed.starts_with("Replaced") {
replaced_lines.push(trimmed.to_string());
continue;
}
// Keep: "Ignored package" (already up to date)
if trimmed.starts_with("Ignored package") {
already_installed = true;
ignored_line = trimmed.to_string();
continue;
}
// Keep: actionable warnings (e.g., "be sure to add `/path` to your PATH")
// Skip summary lines like "warning: `crate` generated N warnings"
if line.starts_with("warning:") {
if !(line.contains("generated") && line.contains("warning")) {
replaced_lines.push(line.to_string());
}
continue;
}
// Detect error blocks
if line.starts_with("error[") || line.starts_with("error:") {
if line.contains("aborting due to") || line.contains("could not compile") {
continue;
}
if in_error && !current_error.is_empty() {
errors.push(current_error.join("\n"));
current_error.clear();
}
error_count += 1;
in_error = true;
current_error.push(line.to_string());
} else if in_error {
if line.trim().is_empty() && current_error.len() > 3 {
errors.push(current_error.join("\n"));
current_error.clear();
in_error = false;
} else {
current_error.push(line.to_string());
}
}
}
if !current_error.is_empty() {
errors.push(current_error.join("\n"));
}
// Already installed / up to date
if already_installed {
let info = ignored_line.split('`').nth(1).unwrap_or(&ignored_line);
return format!("cargo install: {} already installed", info);
}
// Errors
if error_count > 0 {
let crate_info = format_crate_info(&installed_crate, &installed_version, "");
let deps_info = if compiled > 0 {
format!(", {} deps compiled", compiled)
} else {
String::new()
};
let mut result = String::new();
if crate_info.is_empty() {
result.push_str(&format!(
"cargo install: {} error{}{}\n",
error_count,
if error_count > 1 { "s" } else { "" },
deps_info
));
} else {
result.push_str(&format!(
"cargo install: {} error{} ({}{})\n",
error_count,
if error_count > 1 { "s" } else { "" },
crate_info,
deps_info
));
}
const MAX_INSTALL_ERRORS: usize = CAP_ERRORS;
for (i, err) in errors.iter().enumerate().take(MAX_INSTALL_ERRORS) {
result.push_str(err);
result.push('\n');
if i < errors.len() - 1 {
result.push('\n');
}
}
if errors.len() > MAX_INSTALL_ERRORS {
result.push_str(&format!(
"\n… +{} more issues\n",
errors.len() - MAX_INSTALL_ERRORS
));
let all_errors = errors.join("\n\n");
if let Some(hint) = crate::core::tee::force_tee_hint(&all_errors, "cargo-build-errors")
{
result.push_str(&format!(" {}\n", hint));
}
}
return result.trim().to_string();
}
// Success
let crate_info = format_crate_info(&installed_crate, &installed_version, "package");
let mut result = format!("cargo install ({}, {} deps compiled)", crate_info, compiled);
for line in &replaced_lines {
result.push_str(&format!("\n {}", line));
}
result
}
/// Push a completed failure block (header + body) into the failures list, then clear the buffers.
fn flush_failure_block(header: &mut String, body: &mut Vec<String>, failures: &mut Vec<String>) {
if header.is_empty() {
return;
}
let mut block = header.clone();
if !body.is_empty() {
block.push('\n');
block.push_str(&body.join("\n"));
}
failures.push(block);
header.clear();
body.clear();
}
/// Filter cargo nextest output - show failures + compact summary
fn filter_cargo_nextest(output: &str) -> String {
static SUMMARY_RE: OnceLock<regex::Regex> = OnceLock::new();
let summary_re = SUMMARY_RE.get_or_init(|| {
regex::Regex::new(
r"Summary \[\s*([\d.]+)s\]\s+(\d+) tests? run:\s+(\d+) passed(?:,\s+(\d+) failed)?(?:,\s+(\d+) skipped)?"
).expect("invalid nextest summary regex")
});
static STARTING_RE: OnceLock<regex::Regex> = OnceLock::new();
let starting_re = STARTING_RE.get_or_init(|| {
regex::Regex::new(r"Starting \d+ tests? across (\d+) binar(?:y|ies)")
.expect("invalid nextest starting regex")
});
let mut failures: Vec<String> = Vec::new();
let mut in_failure_block = false;
let mut past_summary = false;
let mut current_failure_header = String::new();
let mut current_failure_body = Vec::new();
let mut summary_line = String::new();
let mut binaries: u32 = 0;
let mut has_cancel_line = false;
for line in output.lines() {
let trimmed = line.trim();
// Strip compilation noise
if trimmed.starts_with("Compiling")
|| trimmed.starts_with("Downloading")
|| trimmed.starts_with("Downloaded")
|| trimmed.starts_with("Finished")
|| trimmed.starts_with("Locking")
|| trimmed.starts_with("Updating")
{
continue;
}
// Strip separator lines (────)
if trimmed.starts_with("────") {
continue;
}
// Skip post-summary recap lines (FAIL duplicates + "error: test run failed")
if past_summary {
continue;
}
// Parse binary count from Starting line
if trimmed.starts_with("Starting") {
if let Some(caps) = starting_re.captures(trimmed) {
if let Some(m) = caps.get(1) {
binaries = m.as_str().parse().unwrap_or(0);
}
}
continue;
}
// Strip PASS lines
if trimmed.starts_with("PASS") {
if in_failure_block {
flush_failure_block(
&mut current_failure_header,
&mut current_failure_body,
&mut failures,
);
in_failure_block = false;
}
continue;
}
// Detect FAIL lines
if trimmed.starts_with("FAIL") {
// Close previous failure block if any
if in_failure_block {
flush_failure_block(
&mut current_failure_header,
&mut current_failure_body,
&mut failures,
);
}
current_failure_header = trimmed.to_string();
in_failure_block = true;
continue;
}
// Cancellation notice
if trimmed.starts_with("Cancelling") || trimmed.starts_with("Canceling") {
has_cancel_line = true;
continue;
}
// Nextest run ID line
if trimmed.starts_with("Nextest run ID") {
continue;
}
// Parse summary
if trimmed.starts_with("Summary") {
summary_line = trimmed.to_string();
if in_failure_block {
flush_failure_block(
&mut current_failure_header,
&mut current_failure_body,
&mut failures,
);
in_failure_block = false;
}
past_summary = true;
continue;
}
// Collect failure body lines (stdout/stderr sections)
if in_failure_block {
current_failure_body.push(line.to_string());
}
}
// Close last failure block
if in_failure_block {
flush_failure_block(
&mut current_failure_header,
&mut current_failure_body,
&mut failures,
);
}
// Parse summary with regex
if let Some(caps) = summary_re.captures(&summary_line) {
let duration = caps.get(1).map_or("?", |m| m.as_str());
let passed: u32 = caps
.get(3)
.and_then(|m| m.as_str().parse().ok())
.unwrap_or(0);
let failed: u32 = caps
.get(4)
.and_then(|m| m.as_str().parse().ok())
.unwrap_or(0);
let skipped: u32 = caps
.get(5)
.and_then(|m| m.as_str().parse().ok())
.unwrap_or(0);
let binary_text = match binaries.cmp(&1) {
Ordering::Greater => format!("{} binaries", binaries),
Ordering::Equal => "1 binary".to_string(),
Ordering::Less => String::new(),
};
if failed == 0 {
// All pass - compact single line
let mut parts = vec![format!("{} passed", passed)];
if skipped > 0 {
parts.push(format!("{} skipped", skipped));
}
let meta = if binary_text.is_empty() {
format!("{}s", duration)
} else {
format!("{}, {}s", binary_text, duration)
};
return format!("cargo nextest: {} ({})", parts.join(", "), meta);
}
// With failures - show failure details then summary
let mut result = String::new();
for failure in &failures {
result.push_str(failure);
result.push('\n');
}
if has_cancel_line {
result.push_str("Cancelling due to test failure\n");
}
let mut summary_parts = vec![format!("{} passed", passed)];
if failed > 0 {
summary_parts.push(format!("{} failed", failed));
}
if skipped > 0 {
summary_parts.push(format!("{} skipped", skipped));
}
let meta = if binary_text.is_empty() {
format!("{}s", duration)
} else {
format!("{}, {}s", binary_text, duration)
};
result.push_str(&format!(
"cargo nextest: {} ({})",
summary_parts.join(", "),
meta
));
return result.trim().to_string();
}
// Fallback: if summary regex didn't match, show what we have
if !failures.is_empty() {
let mut result = String::new();
for failure in &failures {
result.push_str(failure);
result.push('\n');
}
if !summary_line.is_empty() {
result.push_str(&summary_line);
}
return result.trim().to_string();
}
if !summary_line.is_empty() {
return summary_line;
}
// Empty or unrecognized
String::new()
}
struct JsonDiagnostics {
errors: Vec<String>,
warnings: Vec<String>,
}
#[derive(Deserialize)]
struct CargoJsonLine {
reason: String,
message: Option<CargoDiagnostic>,
}
#[derive(Deserialize)]
struct CargoDiagnostic {
level: String,
#[serde(default)]
message: String,
rendered: Option<String>,
}
fn extract_json_diagnostics(raw: &str) -> JsonDiagnostics {
let mut errors = Vec::new();
let mut warnings = Vec::new();
for line in raw.lines() {
let line = line.trim_start();
if !line.starts_with('{') {
continue;
}
let Ok(entry) = serde_json::from_str::<CargoJsonLine>(line) else {
continue;
};
if entry.reason != "compiler-message" {
continue;
}
let Some(msg) = entry.message else {
continue;
};
let bucket = match msg.level.as_str() {
"error" | "error: internal compiler error" => &mut errors,
"warning" => &mut warnings,
_ => continue,
};
if msg.message.starts_with("aborting due to")
|| msg.message.starts_with("could not compile")
|| (msg.message.contains("warning") && msg.message.contains("generated"))
{
continue;
}
if let Some(rendered) = msg.rendered {
bucket.push(crate::core::utils::strip_ansi(&rendered).trim_end().to_string());
} else if !msg.message.is_empty() {
bucket.push(msg.message);
}
}
JsonDiagnostics { errors, warnings }
}
fn merge_diag_counts(error_count: usize, warnings: usize, json: &JsonDiagnostics) -> (usize, usize) {
(
error_count.max(json.errors.len()),
warnings.max(json.warnings.len()),
)
}
fn cargo_build_success_line(compiled: usize, finished: Option<&str>, label: &str) -> String {
match finished {
Some(f) => format!("cargo {} ({} crates compiled)\n{}\n", label, compiled, f),
None => format!("cargo {} ({} crates compiled)\n", label, compiled),
}
}
fn cargo_build_failure_summary(
compiled: usize,
errors: usize,
warnings: usize,
json: &JsonDiagnostics,
label: &str,
exit_code: i32,
) -> String {
let mut out = if errors == 0 && warnings == 0 {
format!("cargo {}: failed (exit {})\n", label, exit_code)
} else {
format!(
"cargo {}: {} errors, {} warnings ({} crates)\n",
label, errors, warnings, compiled
)
};
if !json.errors.is_empty() {
let shown: Vec<String> = json.errors.iter().take(CAP_ERRORS).cloned().collect();
out.push_str(&join_with_overflow(
&shown,
json.errors.len(),
CAP_ERRORS,
"errors",
));
out.push('\n');
}
if !json.warnings.is_empty() {
let shown: Vec<String> = json.warnings.iter().take(CAP_WARNINGS).cloned().collect();
out.push_str(&join_with_overflow(
&shown,
json.warnings.len(),
CAP_WARNINGS,
"warnings",
));
out.push('\n');
}
if json.errors.len() > CAP_ERRORS || json.warnings.len() > CAP_WARNINGS {
let full = json
.errors
.iter()
.chain(json.warnings.iter())
.map(String::as_str)
.collect::<Vec<_>>()
.join("\n\n");
if let Some(hint) = crate::core::tee::force_tee_hint(&full, "cargo-json-issues") {
out.push_str(&format!(" {}\n", hint));
}
}
out
}
#[cfg(test)]
fn filter_cargo_build(output: &str) -> String {
filter_cargo_build_labeled(output, "build", 0)
}
fn filter_cargo_build_labeled(output: &str, label: &'static str, exit_code: i32) -> String {
let mut handler = CargoBuildHandler::with_label(label);
let mut blocks: Vec<Vec<String>> = Vec::new();
let mut current_block: Vec<String> = Vec::new();
let mut in_block = false;
for line in output.lines() {
if handler.should_skip(line) {
continue;
}
if handler.is_block_start(line) {
if in_block && !current_block.is_empty() {
blocks.push(std::mem::take(&mut current_block));
}
in_block = true;
current_block.push(line.to_string());
} else if in_block {
if handler.is_block_continuation(line, &current_block) {
current_block.push(line.to_string());
} else {
blocks.push(std::mem::take(&mut current_block));
in_block = false;
}
}
}
if !current_block.is_empty() {
blocks.push(current_block);
}
let json = extract_json_diagnostics(output);
let (errors, warnings) = merge_diag_counts(handler.error_count, handler.warnings, &json);
if errors == 0 && warnings == 0 && exit_code == 0 {
return cargo_build_success_line(handler.compiled, handler.finished_line.as_deref(), label);
}
let mut result =
cargo_build_failure_summary(handler.compiled, errors, warnings, &json, label, exit_code);
const MAX_CHECK_BLOCKS: usize = CAP_ERRORS;
for (i, blk) in blocks.iter().enumerate().take(MAX_CHECK_BLOCKS) {
result.push_str(&blk.join("\n"));
result.push('\n');
if i < blocks.len() - 1 {
result.push('\n');
}
}
if blocks.len() > MAX_CHECK_BLOCKS {
result.push_str(&format!(
"\n… +{} more issues\n",
blocks.len() - MAX_CHECK_BLOCKS
));
let all_blocks: String = blocks
.iter()
.map(|b| b.join("\n"))
.collect::<Vec<_>>()
.join("\n\n");
if let Some(hint) = crate::core::tee::force_tee_hint(&all_blocks, "cargo-check-issues") {
result.push_str(&format!(" {}\n", hint));
}
}
result.trim().to_string()
}
/// Aggregated test results for compact display
#[derive(Debug, Default, Clone)]
struct AggregatedTestResult {
passed: usize,
failed: usize,
ignored: usize,
measured: usize,
filtered_out: usize,
suites: usize,
duration_secs: f64,
has_duration: bool,
}
impl AggregatedTestResult {
/// Parse a test result summary line
/// Format: "test result: ok. 15 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.01s"
fn parse_line(line: &str) -> Option<Self> {
static RE: OnceLock<regex::Regex> = OnceLock::new();
let re = RE.get_or_init(|| {
regex::Regex::new(
r"test result: (\w+)\.\s+(\d+) passed;\s+(\d+) failed;\s+(\d+) ignored;\s+(\d+) measured;\s+(\d+) filtered out(?:;\s+finished in ([\d.]+)s)?"
).unwrap()
});
let caps = re.captures(line)?;
let status = caps.get(1)?.as_str();
// Only aggregate if status is "ok" (all tests passed)
if status != "ok" {
return None;
}
let passed = caps.get(2)?.as_str().parse().ok()?;
let failed = caps.get(3)?.as_str().parse().ok()?;
let ignored = caps.get(4)?.as_str().parse().ok()?;
let measured = caps.get(5)?.as_str().parse().ok()?;
let filtered_out = caps.get(6)?.as_str().parse().ok()?;
let (duration_secs, has_duration) = if let Some(duration_match) = caps.get(7) {
(duration_match.as_str().parse().unwrap_or(0.0), true)
} else {
(0.0, false)
};
Some(Self {
passed,
failed,
ignored,
measured,
filtered_out,
suites: 1,
duration_secs,
has_duration,
})
}
/// Merge another test result into this one
fn merge(&mut self, other: &Self) {
self.passed += other.passed;
self.failed += other.failed;
self.ignored += other.ignored;
self.measured += other.measured;
self.filtered_out += other.filtered_out;
self.suites += other.suites;
self.duration_secs += other.duration_secs;
self.has_duration = self.has_duration && other.has_duration;
}
/// Format as compact single line
fn format_compact(&self) -> String {
let mut parts = vec![format!("{} passed", self.passed)];
if self.ignored > 0 {
parts.push(format!("{} ignored", self.ignored));
}
if self.filtered_out > 0 {
parts.push(format!("{} filtered out", self.filtered_out));
}
let counts = parts.join(", ");
let suite_text = if self.suites == 1 {
"1 suite".to_string()
} else {
format!("{} suites", self.suites)
};
if self.has_duration {
format!(
"cargo test: {} ({}, {:.2}s)",
counts, suite_text, self.duration_secs
)
} else {
format!("cargo test: {} ({})", counts, suite_text)
}
}
}
pub(crate) fn filter_cargo_test(output: &str) -> String {
let mut failures: Vec<String> = Vec::new();
let mut summary_lines: Vec<String> = Vec::new();
let mut in_failure_section = false;
let mut current_failure = Vec::new();
for line in output.lines() {
// Skip compilation lines
if line.trim_start().starts_with("Compiling")
|| line.trim_start().starts_with("Downloading")
|| line.trim_start().starts_with("Downloaded")
|| line.trim_start().starts_with("Finished")
{
continue;
}
// Skip "running N tests" and individual "test ... ok" lines
if line.starts_with("running ") || (line.starts_with("test ") && line.ends_with("... ok")) {
continue;
}
// Detect failures section
if line == "failures:" {
in_failure_section = true;
continue;
}
if in_failure_section {
if line.starts_with("test result:") {
in_failure_section = false;
summary_lines.push(line.to_string());
} else if line.starts_with(" ") || line.starts_with("---- ") {
current_failure.push(line.to_string());
} else if line.trim().is_empty() && !current_failure.is_empty() {
failures.push(current_failure.join("\n"));
current_failure.clear();
} else if !line.trim().is_empty() {
current_failure.push(line.to_string());
}
}
// Capture test result summary
if !in_failure_section && line.starts_with("test result:") {
summary_lines.push(line.to_string());
}
}
if !current_failure.is_empty() {
failures.push(current_failure.join("\n"));
}
let mut result = String::new();
if failures.is_empty() && !summary_lines.is_empty() {
// All passed - try to aggregate
let mut aggregated: Option<AggregatedTestResult> = None;
let mut all_parsed = true;
for line in &summary_lines {
if let Some(parsed) = AggregatedTestResult::parse_line(line) {
if let Some(ref mut agg) = aggregated {
agg.merge(&parsed);
} else {
aggregated = Some(parsed);
}
} else {
all_parsed = false;
break;
}
}
// If all lines parsed successfully and we have at least one suite, return compact format
if all_parsed {
if let Some(agg) = aggregated {
if agg.suites > 0 {
return agg.format_compact();
}
}
}
// Fallback: use original behavior if regex failed
for line in &summary_lines {
result.push_str(&format!("{}\n", line));
}
return result.trim().to_string();
}
if !failures.is_empty() {
result.push_str(&format!("FAILURES ({}):\n", failures.len()));
const MAX_FAILURES: usize = CAP_WARNINGS;
for (i, failure) in failures.iter().enumerate().take(MAX_FAILURES) {
result.push_str(&format!("{}. {}\n", i + 1, truncate(failure, 200)));
}
if failures.len() > MAX_FAILURES {
result.push_str(&format!("\n… +{} more failures\n", failures.len() - MAX_FAILURES));
let all_failures = failures.join("\n\n");
if let Some(hint) =
crate::core::tee::force_tee_hint(&all_failures, "cargo-test-failures")
{
result.push_str(&format!(" {}\n", hint));
}
}
result.push('\n');
}
for line in &summary_lines {
result.push_str(&format!("{}\n", line));
}
if result.trim().is_empty() {
let json = extract_json_diagnostics(output);
let has_compile_errors = !json.errors.is_empty()
|| output.lines().any(|line| {
let trimmed = line.trim_start();
trimmed.starts_with("error[") || trimmed.starts_with("error:")
});
if has_compile_errors {
let build_filtered = filter_cargo_build_labeled(output, "test", 0);
if build_filtered.contains("cargo test:") {
return build_filtered;
}
}
// Fallback: show last meaningful lines
let meaningful: Vec<&str> = output
.lines()
.filter(|l| !l.trim().is_empty() && !l.trim_start().starts_with("Compiling"))
.collect();
for line in meaningful.iter().rev().take(5).rev() {
result.push_str(&format!("{}\n", line));
}
}
result.trim().to_string()
}
/// Filter cargo clippy output - show full error blocks, group warnings by lint rule
fn filter_cargo_clippy(output: &str) -> String {
let mut by_rule: HashMap<String, Vec<String>> = HashMap::new();
let mut error_count = 0;
let mut warning_count = 0;
// Each entry is a full multi-line error block (headline + location + code context)
let mut error_blocks: Vec<Vec<String>> = Vec::new();
let mut current_rule = String::new();
let mut in_error = false;
let mut current_block: Vec<String> = Vec::new();
for line in output.lines() {
// Skip compilation progress lines
if line.trim_start().starts_with("Compiling")
|| line.trim_start().starts_with("Checking")
|| line.trim_start().starts_with("Downloading")
|| line.trim_start().starts_with("Downloaded")
|| line.trim_start().starts_with("Finished")
{
if in_error && !current_block.is_empty() {
error_blocks.push(current_block.clone());
current_block.clear();
in_error = false;
}
continue;
}
// Skip noise: summary counts and abort lines
if (line.contains("generated") && line.contains("warning"))
|| line.contains("aborting due to")
|| line.contains("could not compile")
{
continue;
}
let is_error_line = line.starts_with("error:") || line.starts_with("error[");
let is_warning_line = line.starts_with("warning:") || line.starts_with("warning[");
if is_error_line || is_warning_line {
// Flush any in-progress error block before starting a new diagnostic
if in_error && !current_block.is_empty() {
error_blocks.push(current_block.clone());
current_block.clear();
}
in_error = false;
if is_error_line {
error_count += 1;
in_error = true;
current_block.push(line.to_string());
} else {
warning_count += 1;
}
// Extract rule/error-code from brackets for warning grouping
current_rule = if let Some(bracket_start) = line.rfind('[') {
if let Some(bracket_end) = line.rfind(']') {
line[bracket_start + 1..bracket_end].to_string()
} else {
line.to_string()
}
} else {
let prefix = if is_error_line {
"error: "
} else {
"warning: "
};
line.strip_prefix(prefix).unwrap_or(line).to_string()
};
} else if line.trim_start().starts_with("--> ") {
let location = line.trim_start().trim_start_matches("--> ").to_string();
if !current_rule.is_empty() {
by_rule
.entry(current_rule.clone())
.or_default()
.push(location);
}
if in_error {
current_block.push(line.to_string());
}
} else if in_error {
if line.trim().is_empty() {
// Blank line terminates the error block
if !current_block.is_empty() {
error_blocks.push(current_block.clone());
current_block.clear();
}
in_error = false;
} else if current_block.len() < 15 {
// Collect code-context lines (|, ^, = note:, help:, etc.)
current_block.push(line.to_string());
}
}
}
// Flush final error block
if in_error && !current_block.is_empty() {
error_blocks.push(current_block);
}
if error_count == 0 && warning_count == 0 {
return "cargo clippy: No issues found".to_string();
}
let mut result = String::new();
result.push_str(&format!(
"cargo clippy: {} errors, {} warnings\n",
error_count, warning_count
));
// Show full error blocks so developers can see what needs fixing
if !error_blocks.is_empty() {
const MAX_CLIPPY_ERRORS: usize = CAP_WARNINGS;
result.push_str("\nErrors:\n");
for block in error_blocks.iter().take(MAX_CLIPPY_ERRORS) {
for block_line in block {
result.push_str(&format!(" {}\n", truncate(block_line, 160)));
}
result.push('\n');
}
if error_blocks.len() > MAX_CLIPPY_ERRORS {
result.push_str(&format!(
" … +{} more errors\n",
error_blocks.len() - MAX_CLIPPY_ERRORS
));
let all_blocks: String = error_blocks
.iter()
.map(|b| b.join("\n"))
.collect::<Vec<_>>()
.join("\n\n");
if let Some(hint) =
crate::core::tee::force_tee_hint(&all_blocks, "cargo-clippy-errors")
{
result.push_str(&format!(" {}\n", hint));
}
}
}
// Sort warning rules by frequency
let mut rule_counts: Vec<_> = by_rule.iter().collect();
rule_counts.sort_by_key(|b| std::cmp::Reverse(b.1.len()));
const MAX_RULES: usize = CAP_LIST;
for (rule, locations) in rule_counts.iter().take(MAX_RULES) {
result.push_str(&format!(" {} ({}x)\n", rule, locations.len()));
for loc in locations.iter().take(3) {
result.push_str(&format!(" {}\n", loc));
}
if locations.len() > 3 {
result.push_str(&format!(" … +{} more\n", locations.len() - 3));
}
}
if by_rule.len() > MAX_RULES {
result.push_str(&format!("\n… +{} more rules\n", by_rule.len() - MAX_RULES));
let all_rules = rule_counts
.iter()
.map(|(rule, locs)| format!("{} ({}x)", rule, locs.len()))
.collect::<Vec<_>>()
.join("\n");
if let Some(hint) =
crate::core::tee::force_tee_tail_hint(&all_rules, "cargo-clippy-rules", MAX_RULES + 1)
{
result.push_str(&format!(" {}\n", hint));
}
}
result.trim().to_string()
}
fn filter_cargo_clippy_json(output: &str, exit_code: i32) -> String {
let json = extract_json_diagnostics(output);
if json.errors.is_empty() && json.warnings.is_empty() && exit_code == 0 {
return "cargo clippy: No issues found".to_string();
}
filter_cargo_build_labeled(output, "clippy", exit_code)
}
pub fn run_passthrough(args: &[OsString], verbose: u8) -> Result<i32> {
crate::core::runner::run_passthrough("cargo", args, verbose)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::args_utils::restore_double_dash_with_raw;
#[test]
fn test_restore_double_dash_with_separator() {
// rtk cargo test -- --nocapture → clap gives ["--nocapture"]
let args: Vec<String> = vec!["--nocapture".into()];
let raw = vec![
"rtk".into(),
"cargo".into(),
"test".into(),
"--".into(),
"--nocapture".into(),
];
let result = restore_double_dash_with_raw(&args, &raw);
assert_eq!(result, vec!["--", "--nocapture"]);
}
#[test]
fn test_restore_double_dash_with_test_name() {
// rtk cargo test my_test -- --nocapture → clap gives ["my_test", "--nocapture"]
let args: Vec<String> = vec!["my_test".into(), "--nocapture".into()];
let raw = vec![
"rtk".into(),
"cargo".into(),
"test".into(),
"my_test".into(),
"--".into(),
"--nocapture".into(),
];
let result = restore_double_dash_with_raw(&args, &raw);
assert_eq!(result, vec!["my_test", "--", "--nocapture"]);
}
#[test]
fn test_restore_double_dash_without_separator() {
// rtk cargo test my_test → no --, args unchanged
let args: Vec<String> = vec!["my_test".into()];
let raw = vec![
"rtk".into(),
"cargo".into(),
"test".into(),
"my_test".into(),
];
let result = restore_double_dash_with_raw(&args, &raw);
assert_eq!(result, vec!["my_test"]);
}
#[test]
fn test_restore_double_dash_empty_args() {
let args: Vec<String> = vec![];
let raw = vec!["rtk".into(), "cargo".into(), "test".into()];
let result = restore_double_dash_with_raw(&args, &raw);
assert!(result.is_empty());
}
#[test]
fn test_restore_double_dash_clippy() {
// rtk cargo clippy -- -D warnings → clap gives ["-D", "warnings"]
let args: Vec<String> = vec!["-D".into(), "warnings".into()];
let raw = vec![
"rtk".into(),
"cargo".into(),
"clippy".into(),
"--".into(),
"-D".into(),
"warnings".into(),
];
let result = restore_double_dash_with_raw(&args, &raw);
assert_eq!(result, vec!["--", "-D", "warnings"]);
}
#[test]
fn test_restore_double_dash_clippy_with_package_flags() {
// rtk cargo clippy -p my-service -p my-crate -- -D warnings
// Clap with trailing_var_arg preserves "--" when args precede it
// → clap gives ["-p", "my-service", "-p", "my-crate", "--", "-D", "warnings"]
let args: Vec<String> = vec![
"-p".into(),
"my-service".into(),
"-p".into(),
"my-crate".into(),
"--".into(),
"-D".into(),
"warnings".into(),
];
let raw = vec![
"rtk".into(),
"cargo".into(),
"clippy".into(),
"-p".into(),
"my-service".into(),
"-p".into(),
"my-crate".into(),
"--".into(),
"-D".into(),
"warnings".into(),
];
let result = restore_double_dash_with_raw(&args, &raw);
// Should NOT double the "--"
assert_eq!(
result,
vec!["-p", "my-service", "-p", "my-crate", "--", "-D", "warnings"]
);
// Verify only one "--" exists
assert_eq!(result.iter().filter(|a| *a == "--").count(), 1);
}
#[test]
fn test_filter_cargo_build_success() {
let output = r#" Compiling libc v0.2.153
Compiling cfg-if v1.0.0
Compiling rtk v0.5.0
Finished dev [unoptimized + debuginfo] target(s) in 15.23s
"#;
let result = filter_cargo_build(output);
assert!(result.contains("cargo build"));
assert!(result.contains("3 crates compiled"));
}
#[test]
fn test_filter_cargo_build_errors() {
let output = r#" Compiling rtk v0.5.0
error[E0308]: mismatched types
--> src/main.rs:10:5
|
10| "hello"
| ^^^^^^^ expected `i32`, found `&str`
error: aborting due to 1 previous error
"#;
let result = filter_cargo_build(output);
assert!(result.contains("1 errors"));
assert!(result.contains("E0308"));
assert!(result.contains("mismatched types"));
}
#[test]
fn test_filter_cargo_test_all_pass() {
let output = r#" Compiling rtk v0.5.0
Finished test [unoptimized + debuginfo] target(s) in 2.53s
Running target/debug/deps/rtk-abc123
running 15 tests
test utils::tests::test_truncate_short_string ... ok
test utils::tests::test_truncate_long_string ... ok
test utils::tests::test_strip_ansi_simple ... ok
test result: ok. 15 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.01s
"#;
let result = filter_cargo_test(output);
assert!(
result.contains("cargo test: 15 passed (1 suite, 0.01s)"),
"Expected compact format, got: {}",
result
);
assert!(!result.contains("Compiling"));
assert!(!result.contains("test utils"));
}
#[test]
fn test_filter_cargo_test_failures() {
let output = r#"running 5 tests
test foo::test_a ... ok
test foo::test_b ... FAILED
test foo::test_c ... ok
failures:
---- foo::test_b stdout ----
thread 'foo::test_b' panicked at 'assert_eq!(1, 2)'
failures:
foo::test_b
test result: FAILED. 4 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out
"#;
let result = filter_cargo_test(output);
assert!(result.contains("FAILURES"));
assert!(result.contains("test_b"));
assert!(result.contains("test result:"));
}
#[test]
fn test_filter_cargo_test_multi_suite_all_pass() {
let output = r#" Compiling rtk v0.5.0
Finished test [unoptimized + debuginfo] target(s) in 2.53s
Running unittests src/lib.rs (target/debug/deps/rtk-abc123)
running 50 tests
test result: ok. 50 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.45s
Running unittests src/main.rs (target/debug/deps/rtk-def456)
running 30 tests
test result: ok. 30 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.30s
Running tests/integration.rs (target/debug/deps/integration-ghi789)
running 25 tests
test result: ok. 25 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.25s
Doc-tests rtk
running 32 tests
test result: ok. 32 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.45s
"#;
let result = filter_cargo_test(output);
assert!(
result.contains("cargo test: 137 passed (4 suites, 1.45s)"),
"Expected aggregated format, got: {}",
result
);
assert!(!result.contains("running"));
}
#[test]
fn test_filter_cargo_test_multi_suite_with_failures() {
let output = r#" Running unittests src/lib.rs
running 20 tests
test result: ok. 20 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.10s
Running unittests src/main.rs
running 15 tests
test foo::test_bad ... FAILED
failures:
---- foo::test_bad stdout ----
thread panicked at 'assertion failed'
test result: FAILED. 14 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.05s
Running tests/integration.rs
running 10 tests
test result: ok. 10 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.02s
"#;
let result = filter_cargo_test(output);
// Should NOT aggregate when there are failures
assert!(result.contains("FAILURES"), "got: {}", result);
assert!(result.contains("test_bad"), "got: {}", result);
assert!(result.contains("test result:"), "got: {}", result);
// Should show individual summaries
assert!(result.contains("20 passed"), "got: {}", result);
assert!(result.contains("14 passed"), "got: {}", result);
assert!(result.contains("10 passed"), "got: {}", result);
}
#[test]
fn test_filter_cargo_test_all_suites_zero_tests() {
let output = r#" Running unittests src/empty1.rs
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Running unittests src/empty2.rs
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Running tests/empty3.rs
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
"#;
let result = filter_cargo_test(output);
assert!(
result.contains("cargo test: 0 passed (3 suites, 0.00s)"),
"Expected compact format for zero tests, got: {}",
result
);
}
#[test]
fn test_filter_cargo_test_with_ignored_and_filtered() {
let output = r#" Running unittests src/lib.rs
running 50 tests
test result: ok. 45 passed; 0 failed; 3 ignored; 0 measured; 2 filtered out; finished in 0.50s
Running tests/integration.rs
running 20 tests
test result: ok. 18 passed; 0 failed; 2 ignored; 0 measured; 0 filtered out; finished in 0.20s
"#;
let result = filter_cargo_test(output);
assert!(
result.contains("cargo test: 63 passed, 5 ignored, 2 filtered out (2 suites, 0.70s)"),
"Expected compact format with ignored and filtered, got: {}",
result
);
}
#[test]
fn test_filter_cargo_test_single_suite_compact() {
let output = r#" Running unittests src/main.rs
running 15 tests
test result: ok. 15 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.01s
"#;
let result = filter_cargo_test(output);
assert!(
result.contains("cargo test: 15 passed (1 suite, 0.01s)"),
"Expected singular 'suite', got: {}",
result
);
}
#[test]
fn test_filter_cargo_test_regex_fallback() {
let output = r#" Running unittests src/main.rs
running 15 tests
test result: MALFORMED LINE WITHOUT PROPER FORMAT
"#;
let result = filter_cargo_test(output);
// Should fallback to original behavior (show line without checkmark)
assert!(
result.contains("test result: MALFORMED"),
"Expected fallback format, got: {}",
result
);
}
#[test]
fn test_filter_cargo_test_compile_error_preserves_error_header() {
let output = r#" Compiling rtk v0.31.0 (/workspace/projects/rtk)
error[E0425]: cannot find value `missing_symbol` in this scope
--> tests/repro_compile_fail.rs:3:13
|
3 | let _ = missing_symbol;
| ^^^^^^^^^^^^^^ not found in this scope
For more information about this error, try `rustc --explain E0425`.
error: could not compile `rtk` (test "repro_compile_fail") due to 1 previous error
"#;
let result = filter_cargo_test(output);
assert!(result.contains("cargo test: 1 errors, 0 warnings (1 crates)"));
assert!(result.contains("error[E0425]"), "got: {}", result);
assert!(
result.contains("--> tests/repro_compile_fail.rs:3:13"),
"got: {}",
result
);
assert!(!result.starts_with('|'), "got: {}", result);
}
#[test]
fn test_filter_cargo_clippy_clean() {
let output = r#" Checking rtk v0.5.0
Finished dev [unoptimized + debuginfo] target(s) in 1.53s
"#;
let result = filter_cargo_clippy(output);
assert!(result.contains("cargo clippy: No issues found"));
}
#[test]
fn test_filter_cargo_clippy_warnings() {
let output = r#" Checking rtk v0.5.0
warning: unused variable: `x` [unused_variables]
--> src/main.rs:10:9
|
10| let x = 5;
| ^ help: if this is intentional, prefix it with an underscore: `_x`
warning: this function has too many arguments [clippy::too_many_arguments]
--> src/git.rs:16:1
|
16| pub fn run(a: i32, b: i32, c: i32, d: i32, e: i32, f: i32, g: i32, h: i32) {}
|
warning: `rtk` (bin) generated 2 warnings
Finished dev [unoptimized + debuginfo] target(s) in 1.53s
"#;
let result = filter_cargo_clippy(output);
assert!(result.contains("0 errors, 2 warnings"));
assert!(result.contains("unused_variables"));
assert!(result.contains("clippy::too_many_arguments"));
}
#[test]
fn test_filter_cargo_clippy_includes_error_details() {
let output = r#" Checking rtk v0.5.0
error: struct literals are not allowed here
warning: unused variable: `x` [unused_variables]
Finished dev [unoptimized + debuginfo] target(s) in 1.53s
"#;
let result = filter_cargo_clippy(output);
assert!(result.contains("cargo clippy: 1 errors, 1 warnings"));
assert!(result.contains("Errors:"));
assert!(result.contains("struct literals are not allowed here"));
}
#[test]
fn test_filter_cargo_clippy_shows_full_error_block() {
// Full multi-line error block must be shown so the developer can debug
let output = r#" Checking rtk v0.5.0
error[E0308]: mismatched types
--> src/main.rs:10:5
|
9 | fn foo() -> i32 {
| --- expected `i32` because of return type
10| "hello"
| ^^^^^^^ expected `i32`, found `&str`
error: aborting due to 1 previous error
"#;
let result = filter_cargo_clippy(output);
assert!(
result.contains("cargo clippy: 1 errors, 0 warnings"),
"got: {}",
result
);
assert!(
result.contains("error[E0308]: mismatched types"),
"got: {}",
result
);
assert!(result.contains("src/main.rs:10:5"), "got: {}", result);
assert!(
result.contains("expected `i32`, found `&str`"),
"got: {}",
result
);
}
#[test]
fn test_filter_cargo_clippy_multiple_errors_show_all_blocks() {
let output = r#"error[E0308]: mismatched types
--> src/foo.rs:5:3
error[E0425]: cannot find value `x`
--> src/bar.rs:12:9
error: aborting due to 2 previous errors
"#;
let result = filter_cargo_clippy(output);
assert!(result.contains("2 errors"), "got: {}", result);
assert!(result.contains("src/foo.rs:5:3"), "got: {}", result);
assert!(result.contains("src/bar.rs:12:9"), "got: {}", result);
}
#[test]
fn test_filter_cargo_install_success() {
let output = r#" Installing rtk v0.11.0
Downloading crates ...
Downloaded anyhow v1.0.80
Downloaded clap v4.5.0
Compiling libc v0.2.153
Compiling cfg-if v1.0.0
Compiling anyhow v1.0.80
Compiling clap v4.5.0
Compiling rtk v0.11.0
Finished `release` profile [optimized] target(s) in 45.23s
Replacing /Users/user/.cargo/bin/rtk
Replaced package `rtk v0.9.4` with `rtk v0.11.0` (/Users/user/.cargo/bin/rtk)
"#;
let result = filter_cargo_install(output);
assert!(result.contains("cargo install"), "got: {}", result);
assert!(result.contains("rtk v0.11.0"), "got: {}", result);
assert!(result.contains("5 deps compiled"), "got: {}", result);
assert!(result.contains("Replaced"), "got: {}", result);
assert!(!result.contains("Compiling"), "got: {}", result);
assert!(!result.contains("Downloading"), "got: {}", result);
}
#[test]
fn test_filter_cargo_install_replace() {
let output = r#" Installing rtk v0.11.0
Compiling rtk v0.11.0
Finished `release` profile [optimized] target(s) in 10.0s
Replacing /Users/user/.cargo/bin/rtk
Replaced package `rtk v0.9.4` with `rtk v0.11.0` (/Users/user/.cargo/bin/rtk)
"#;
let result = filter_cargo_install(output);
assert!(result.contains("cargo install"), "got: {}", result);
assert!(result.contains("Replacing"), "got: {}", result);
assert!(result.contains("Replaced"), "got: {}", result);
}
#[test]
fn test_filter_cargo_install_error() {
let output = r#" Installing rtk v0.11.0
Compiling rtk v0.11.0
error[E0308]: mismatched types
--> src/main.rs:10:5
|
10| "hello"
| ^^^^^^^ expected `i32`, found `&str`
error: aborting due to 1 previous error
"#;
let result = filter_cargo_install(output);
assert!(result.contains("cargo install: 1 error"), "got: {}", result);
assert!(result.contains("E0308"), "got: {}", result);
assert!(result.contains("mismatched types"), "got: {}", result);
assert!(!result.contains("aborting"), "got: {}", result);
}
#[test]
fn test_filter_cargo_install_already_installed() {
let output = r#" Ignored package `rtk v0.11.0`, is already installed
"#;
let result = filter_cargo_install(output);
assert!(result.contains("already installed"), "got: {}", result);
assert!(result.contains("rtk v0.11.0"), "got: {}", result);
}
#[test]
fn test_filter_cargo_install_up_to_date() {
let output = r#" Ignored package `cargo-deb v2.1.0 (/Users/user/cargo-deb)`, is already installed
"#;
let result = filter_cargo_install(output);
assert!(result.contains("already installed"), "got: {}", result);
assert!(result.contains("cargo-deb v2.1.0"), "got: {}", result);
}
#[test]
fn test_filter_cargo_install_empty_output() {
let result = filter_cargo_install("");
assert!(result.contains("cargo install"), "got: {}", result);
assert!(result.contains("0 deps compiled"), "got: {}", result);
}
#[test]
fn test_filter_cargo_install_path_warning() {
let output = r#" Installing rtk v0.11.0
Compiling rtk v0.11.0
Finished `release` profile [optimized] target(s) in 10.0s
Replacing /Users/user/.cargo/bin/rtk
Replaced package `rtk v0.9.4` with `rtk v0.11.0` (/Users/user/.cargo/bin/rtk)
warning: be sure to add `/Users/user/.cargo/bin` to your PATH
"#;
let result = filter_cargo_install(output);
assert!(result.contains("cargo install"), "got: {}", result);
assert!(
result.contains("be sure to add"),
"PATH warning should be kept: {}",
result
);
assert!(result.contains("Replaced"), "got: {}", result);
}
#[test]
fn test_filter_cargo_install_multiple_errors() {
let output = r#" Installing rtk v0.11.0
Compiling rtk v0.11.0
error[E0308]: mismatched types
--> src/main.rs:10:5
|
10| "hello"
| ^^^^^^^ expected `i32`, found `&str`
error[E0425]: cannot find value `foo`
--> src/lib.rs:20:9
|
20| foo
| ^^^ not found in this scope
error: aborting due to 2 previous errors
"#;
let result = filter_cargo_install(output);
assert!(
result.contains("2 errors"),
"should show 2 errors: {}",
result
);
assert!(result.contains("E0308"), "got: {}", result);
assert!(result.contains("E0425"), "got: {}", result);
assert!(!result.contains("aborting"), "got: {}", result);
}
#[test]
fn test_filter_cargo_install_locking_and_blocking() {
let output = r#" Locking 45 packages to latest compatible versions
Blocking waiting for file lock on package cache
Downloading crates ...
Downloaded serde v1.0.200
Compiling serde v1.0.200
Compiling rtk v0.11.0
Finished `release` profile [optimized] target(s) in 30.0s
Installing rtk v0.11.0
"#;
let result = filter_cargo_install(output);
assert!(result.contains("cargo install"), "got: {}", result);
assert!(!result.contains("Locking"), "got: {}", result);
assert!(!result.contains("Blocking"), "got: {}", result);
assert!(!result.contains("Downloading"), "got: {}", result);
}
#[test]
fn test_filter_cargo_install_from_path() {
let output = r#" Installing /Users/user/projects/rtk
Compiling rtk v0.11.0
Finished `release` profile [optimized] target(s) in 10.0s
"#;
let result = filter_cargo_install(output);
// Path-based install: crate info not extracted from path
assert!(result.contains("cargo install"), "got: {}", result);
assert!(result.contains("1 deps compiled"), "got: {}", result);
}
#[test]
fn test_format_crate_info() {
assert_eq!(format_crate_info("rtk", "v0.11.0", ""), "rtk v0.11.0");
assert_eq!(format_crate_info("rtk", "", ""), "rtk");
assert_eq!(format_crate_info("", "", "package"), "package");
assert_eq!(format_crate_info("", "v0.1.0", "fallback"), "fallback");
}
#[test]
fn test_filter_cargo_nextest_all_pass() {
let output = r#" Compiling rtk v0.15.2
Finished `test` profile [unoptimized + debuginfo] target(s) in 0.04s
────────────────────────────
Starting 301 tests across 1 binary
PASS [ 0.009s] (1/301) rtk::bin/rtk cargo_cmd::tests::test_one
PASS [ 0.008s] (2/301) rtk::bin/rtk cargo_cmd::tests::test_two
PASS [ 0.007s] (301/301) rtk::bin/rtk cargo_cmd::tests::test_last
────────────────────────────
Summary [ 0.192s] 301 tests run: 301 passed, 0 skipped
"#;
let result = filter_cargo_nextest(output);
assert_eq!(
result, "cargo nextest: 301 passed (1 binary, 0.192s)",
"got: {}",
result
);
}
#[test]
fn test_filter_cargo_nextest_with_failures() {
let output = r#" Starting 4 tests across 1 binary (1 test skipped)
PASS [ 0.006s] (1/4) test-proj tests::passing_test
FAIL [ 0.006s] (2/4) test-proj tests::failing_test
stderr ───
thread 'tests::failing_test' panicked at src/lib.rs:15:9:
assertion `left == right` failed
left: 1
right: 2
Cancelling due to test failure: 2 tests still running
PASS [ 0.007s] (3/4) test-proj tests::another_passing
FAIL [ 0.006s] (4/4) test-proj tests::another_failing
stderr ───
thread 'tests::another_failing' panicked at src/lib.rs:20:9:
something went wrong
────────────────────────────
Summary [ 0.007s] 4 tests run: 2 passed, 2 failed, 1 skipped
FAIL [ 0.006s] (2/4) test-proj tests::failing_test
FAIL [ 0.006s] (4/4) test-proj tests::another_failing
error: test run failed
"#;
let result = filter_cargo_nextest(output);
assert!(
result.contains("tests::failing_test"),
"should contain first failure: {}",
result
);
assert!(
result.contains("tests::another_failing"),
"should contain second failure: {}",
result
);
assert!(
result.contains("panicked"),
"should contain stderr detail: {}",
result
);
assert!(
result.contains("2 passed, 2 failed, 1 skipped"),
"should contain summary: {}",
result
);
assert!(
!result.contains("PASS"),
"should not contain PASS lines: {}",
result
);
// Post-summary FAIL recaps must not create duplicate FAIL header entries
// (test names may appear in both header and stderr body naturally)
assert_eq!(
result.matches("FAIL [").count(),
2,
"should have exactly 2 FAIL headers (no post-summary duplicates): {}",
result
);
assert!(
!result.contains("error: test run failed"),
"should not contain post-summary error line: {}",
result
);
}
#[test]
fn test_filter_cargo_nextest_with_skipped() {
let output = r#" Starting 50 tests across 2 binaries (3 tests skipped)
PASS [ 0.010s] (1/50) rtk::bin/rtk test_one
PASS [ 0.010s] (50/50) rtk::bin/rtk test_last
────────────────────────────
Summary [ 0.500s] 50 tests run: 50 passed, 3 skipped
"#;
let result = filter_cargo_nextest(output);
assert_eq!(
result, "cargo nextest: 50 passed, 3 skipped (2 binaries, 0.500s)",
"got: {}",
result
);
}
#[test]
fn test_filter_cargo_nextest_single_failure_detail() {
let output = r#" Starting 2 tests across 1 binary
PASS [ 0.005s] (1/2) proj tests::good
FAIL [ 0.005s] (2/2) proj tests::bad
stderr ───
thread 'tests::bad' panicked at src/lib.rs:5:9:
assertion failed: false
────────────────────────────
Summary [ 0.010s] 2 tests run: 1 passed, 1 failed
FAIL [ 0.005s] (2/2) proj tests::bad
error: test run failed
"#;
let result = filter_cargo_nextest(output);
assert!(
result.contains("assertion failed: false"),
"should show panic message: {}",
result
);
assert!(
result.contains("1 passed, 1 failed"),
"should show summary: {}",
result
);
// Post-summary recap must not duplicate FAIL headers
assert_eq!(
result.matches("FAIL [").count(),
1,
"should have exactly 1 FAIL header (no post-summary duplicate): {}",
result
);
}
#[test]
fn test_filter_cargo_nextest_multiple_binaries() {
let output = r#" Starting 100 tests across 5 binaries
PASS [ 0.010s] (100/100) test_last
────────────────────────────
Summary [ 1.234s] 100 tests run: 100 passed, 0 skipped
"#;
let result = filter_cargo_nextest(output);
assert_eq!(
result, "cargo nextest: 100 passed (5 binaries, 1.234s)",
"got: {}",
result
);
}
#[test]
fn test_filter_cargo_nextest_compilation_stripped() {
let output = r#" Compiling serde v1.0.200
Compiling rtk v0.15.2
Downloading crates ...
Finished `test` profile [unoptimized + debuginfo] target(s) in 5.00s
────────────────────────────
Starting 10 tests across 1 binary
PASS [ 0.010s] (10/10) test_last
────────────────────────────
Summary [ 0.050s] 10 tests run: 10 passed, 0 skipped
"#;
let result = filter_cargo_nextest(output);
assert!(
!result.contains("Compiling"),
"should strip Compiling: {}",
result
);
assert!(
!result.contains("Downloading"),
"should strip Downloading: {}",
result
);
assert!(
!result.contains("Finished"),
"should strip Finished: {}",
result
);
assert!(
result.contains("cargo nextest: 10 passed"),
"got: {}",
result
);
}
#[test]
fn test_filter_cargo_nextest_empty() {
let result = filter_cargo_nextest("");
assert!(result.is_empty(), "got: {}", result);
}
#[test]
fn test_filter_cargo_nextest_cancellation_notice() {
let output = r#" Starting 3 tests across 1 binary
FAIL [ 0.005s] (1/3) proj tests::bad
stderr ───
thread panicked at 'oops'
Cancelling due to test failure: 2 tests still running
────────────────────────────
Summary [ 0.010s] 3 tests run: 2 passed, 1 failed
FAIL [ 0.005s] (1/3) proj tests::bad
error: test run failed
"#;
let result = filter_cargo_nextest(output);
assert!(
result.contains("Cancelling due to test failure"),
"should include cancel notice: {}",
result
);
assert!(
result.contains("1 failed"),
"should show failure count: {}",
result
);
// Post-summary recap must not duplicate FAIL headers
assert_eq!(
result.matches("FAIL [").count(),
1,
"should have exactly 1 FAIL header (no post-summary duplicate): {}",
result
);
}
#[test]
fn test_filter_cargo_nextest_summary_regex_fallback() {
let output = r#" Starting 5 tests across 1 binary
PASS [ 0.005s] (5/5) test_last
────────────────────────────
Summary MALFORMED LINE
"#;
let result = filter_cargo_nextest(output);
assert!(
result.contains("Summary MALFORMED"),
"should fall back to raw summary: {}",
result
);
}
// --- Streaming handler tests ---
use crate::core::stream::tests::run_block_filter;
#[test]
fn test_cargo_build_stream_success() {
let input = " Compiling libc v0.2.153\n Compiling cfg-if v1.0.0\n Compiling rtk v0.5.0\n Finished dev [unoptimized + debuginfo] target(s) in 15.23s\n";
let mut f = BlockStreamFilter::new(CargoBuildHandler::with_label("build"));
let result = run_block_filter(&mut f, input, 0);
assert!(result.contains("3 crates compiled"), "got: {}", result);
assert!(result.contains("Finished"), "got: {}", result);
assert!(!result.contains("Compiling"), "got: {}", result);
}
#[test]
fn test_cargo_build_stream_json_success() {
let input = concat!(
" Compiling demo v0.1.0 (/tmp/demo)\n",
r#"{"reason":"compiler-artifact","package_id":"demo 0.1.0","target":{"name":"demo"},"executable":"/tmp/demo/target/debug/demo"}"#,
"\n",
" Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.39s\n",
r#"{"reason":"build-finished","success":true}"#,
"\n",
);
let mut f = BlockStreamFilter::new(CargoBuildHandler::with_label("build"));
let result = run_block_filter(&mut f, input, 0);
assert!(result.contains("1 crates compiled"), "got: {}", result);
assert!(result.contains("Finished"), "got: {}", result);
assert!(!result.contains("error"), "got: {}", result);
assert!(!result.contains("Compiling"), "got: {}", result);
}
#[test]
fn test_cargo_build_stream_errors() {
let input = r#" Compiling rtk v0.5.0
error[E0308]: mismatched types
--> src/main.rs:10:5
|
10| "hello"
| ^^^^^^^ expected `i32`, found `&str`
error: aborting due to 1 previous error
"#;
let mut f = BlockStreamFilter::new(CargoBuildHandler::with_label("build"));
let result = run_block_filter(&mut f, input, 1);
assert!(result.contains("E0308"), "got: {}", result);
assert!(result.contains("mismatched types"), "got: {}", result);
assert!(result.contains("1 errors"), "got: {}", result);
assert!(!result.contains("aborting"), "got: {}", result);
}
#[test]
fn test_filter_cargo_build_json_warning_rendered() {
let input = concat!(
" Compiling v_cargo v0.1.0 (/tmp/v_cargo)\n",
r#"{"reason":"compiler-message","package_id":"v_cargo 0.1.0","message":{"level":"warning","message":"unused variable: `x`","rendered":"warning: unused variable: `x`\n --> src/main.rs:2:9"}}"#,
"\n",
r#"{"reason":"build-finished","success":true}"#,
"\n",
);
let result = filter_cargo_build_labeled(input, "build", 0);
assert!(result.contains("unused variable"), "got: {}", result);
assert!(result.contains("1 warnings"), "got: {}", result);
assert!(!result.contains("crates compiled"), "got: {}", result);
}
#[test]
fn test_cargo_check_stream_success_label() {
let input = " Checking demo v0.1.0 (/tmp/demo)\n Finished dev [unoptimized + debuginfo] target(s) in 0.42s\n";
let mut f = BlockStreamFilter::new(CargoBuildHandler::with_label("check"));
let result = run_block_filter(&mut f, input, 0);
assert!(result.contains("cargo check"), "got: {}", result);
assert!(!result.contains("cargo build"), "got: {}", result);
}
#[test]
fn test_filter_cargo_build_labeled_clippy_json_not_clean() {
let input = concat!(
" Checking demo v0.1.0 (/tmp/demo)\n",
r#"{"reason":"compiler-message","message":{"code":{"code":"E0308"},"level":"error","message":"mismatched types","rendered":"error[E0308]: mismatched types\n --> src/main.rs:2:18"}}"#,
"\n",
r#"{"reason":"build-finished","success":false}"#,
"\n",
);
let result = filter_cargo_build_labeled(input, "clippy", 1);
assert!(result.contains("cargo clippy:"), "got: {}", result);
assert!(result.contains("1 errors"), "got: {}", result);
assert!(
!result.contains("No issues found"),
"json clippy errors must not be swallowed: {}",
result
);
}
#[test]
fn test_filter_cargo_clippy_json_clean() {
let input = concat!(
" Checking demo v0.1.0 (/tmp/demo)\n",
r#"{"reason":"compiler-artifact","package_id":"demo 0.1.0","target":{"name":"demo"}}"#,
"\n",
r#"{"reason":"build-finished","success":true}"#,
"\n",
);
assert_eq!(
filter_cargo_clippy_json(input, 0),
"cargo clippy: No issues found"
);
}
#[test]
fn test_filter_cargo_clippy_json_warnings() {
let input = concat!(
" Checking demo v0.1.0 (/tmp/demo)\n",
r#"{"reason":"compiler-message","message":{"level":"warning","message":"unused variable: `x`","rendered":"warning: unused variable: `x`\n --> src/main.rs:2:9"}}"#,
"\n",
r#"{"reason":"build-finished","success":true}"#,
"\n",
);
let result = filter_cargo_clippy_json(input, 0);
assert!(result.contains("unused variable"), "got: {}", result);
assert!(result.contains("cargo clippy: 0 errors, 1 warnings"), "got: {}", result);
}
#[test]
fn test_batch_filter_nonzero_exit_without_diagnostics_is_not_compiled() {
// build-script failure: exit 101, no compiler-message diagnostics parsed.
let input = concat!(
" Compiling demo v0.1.0 (/tmp/demo)\n",
r#"{"reason":"build-finished","success":false}"#,
"\n",
);
let result = filter_cargo_build_labeled(input, "build", 101);
assert!(
!result.contains("crates compiled"),
"a failed build must not be reported as compiled: {}",
result
);
assert!(result.contains("cargo build: failed (exit 101)"), "got: {}", result);
}
#[test]
fn test_failure_summary_line_is_first() {
let json = JsonDiagnostics {
errors: vec!["error[E0308]: mismatched types".to_string()],
warnings: Vec::new(),
};
let out = cargo_build_failure_summary(1, 1, 0, &json, "build", 101);
assert!(
out.starts_with("cargo build: 1 errors"),
"summary line must come first: {}",
out
);
}
#[test]
fn test_filter_cargo_build_labeled_check() {
let input = concat!(
r#"{"reason":"compiler-message","message":{"level":"error","message":"mismatched types","rendered":"error[E0308]: mismatched types\n --> src/main.rs:2:18"}}"#,
"\n",
r#"{"reason":"build-finished","success":false}"#,
"\n",
);
let result = filter_cargo_build_labeled(input, "check", 1);
assert!(result.contains("cargo check:"), "got: {}", result);
assert!(!result.contains("cargo build:"), "got: {}", result);
}
#[test]
fn test_extract_json_diagnostics_strips_ansi() {
let output = concat!(
r#"{"reason":"compiler-message","message":{"level":"error","message":"mismatched types","rendered":"\u001b[1m\u001b[31merror[E0308]\u001b[0m: mismatched types"}}"#,
"\n",
r#"{"reason":"build-finished","success":false}"#,
"\n",
);
let json = extract_json_diagnostics(output);
assert_eq!(json.errors.len(), 1);
assert!(
!json.errors[0].contains('\u{1b}'),
"ansi escapes must be stripped: {:?}",
json.errors[0]
);
assert!(
json.errors[0].contains("error[E0308]"),
"got: {:?}",
json.errors[0]
);
}
#[test]
fn test_extract_json_diagnostics_skips_generated_summary() {
let output = concat!(
r#"{"reason":"compiler-message","message":{"level":"warning","message":"unused variable: `x`","rendered":"warning: unused variable: `x`"}}"#,
"\n",
r#"{"reason":"compiler-message","message":{"level":"warning","message":"`demo` (bin \"demo\") generated 1 warning","rendered":"warning: `demo` (bin \"demo\") generated 1 warning"}}"#,
"\n",
r#"{"reason":"build-finished","success":true}"#,
"\n",
);
let json = extract_json_diagnostics(output);
assert_eq!(json.warnings.len(), 1, "generated summary must not inflate the count");
assert!(
!json.warnings.iter().any(|w| w.contains("generated")),
"the generated summary line must not appear in the output"
);
}
#[test]
fn test_extract_json_diagnostics_counts_ice_as_error() {
let output = concat!(
r#"{"reason":"compiler-message","message":{"level":"error: internal compiler error","message":"unexpected panic","rendered":"error: internal compiler error: unexpected panic"}}"#,
"\n",
r#"{"reason":"build-finished","success":false}"#,
"\n",
);
let json = extract_json_diagnostics(output);
assert_eq!(json.errors.len(), 1, "an ICE must count as an error");
assert!(json.errors[0].contains("internal compiler error"), "got: {:?}", json.errors[0]);
}
#[test]
fn test_json_format_inline_eq() {
assert!(has_json_message_format(&["--message-format=json".into()]));
}
#[test]
fn test_json_format_space_separated() {
assert!(has_json_message_format(&[
"--message-format".into(),
"json".into(),
]));
}
#[test]
fn test_json_format_rendered_ansi() {
assert!(has_json_message_format(&[
"--message-format=json-diagnostic-rendered-ansi".into()
]));
}
#[test]
fn test_json_format_render_diagnostics() {
assert!(has_json_message_format(&[
"--message-format=json-render-diagnostics".into()
]));
}
#[test]
fn test_json_format_space_rendered() {
assert!(has_json_message_format(&[
"--message-format".into(),
"json-diagnostic-rendered-ansi".into(),
]));
}
#[test]
fn test_non_json_format_not_detected() {
assert!(!has_json_message_format(&["--message-format=human".into()]));
}
#[test]
fn test_no_format_flag() {
assert!(!has_json_message_format(&[
"--release".into(),
"-p".into(),
"my-crate".into(),
]));
}
#[test]
fn test_json_format_among_other_args() {
assert!(has_json_message_format(&[
"--release".into(),
"-p".into(),
"my-crate".into(),
"--message-format=json".into(),
]));
}
#[test]
fn test_json_format_trailing_message_format_no_value() {
assert!(!has_json_message_format(&["--message-format".into()]));
}
#[test]
fn test_json_format_last_occurrence_wins() {
assert!(!has_json_message_format(&[
"--message-format=json".into(),
"--message-format=human".into(),
]));
assert!(has_json_message_format(&[
"--message-format=human".into(),
"--message-format=json".into(),
]));
}
#[test]
fn test_filter_cargo_build_json_failure_not_compiled() {
let output = concat!(
r#"{"reason":"compiler-message","message":{"rendered":"error[E0277]: the trait bound is not satisfied","level":"error","message":"trait bound"}}"#,
"\n",
r#"{"reason":"build-finished","success":false}"#,
"\n",
);
let result = filter_cargo_build(output);
assert!(result.contains("E0277"), "got: {}", result);
assert!(result.contains("1 errors"), "got: {}", result);
assert!(!result.contains("crates compiled"), "got: {}", result);
}
#[test]
fn test_filter_cargo_build_json_errors_capped() {
let total = CAP_ERRORS + 5;
let mut output = String::new();
for i in 0..total {
output.push_str(&format!(
r#"{{"reason":"compiler-message","message":{{"code":{{"code":"E0308"}},"level":"error","message":"mismatched types","rendered":"error[E0308]: mismatched types ({})"}}}}"#,
i
));
output.push('\n');
}
output.push_str(r#"{"reason":"build-finished","success":false}"#);
output.push('\n');
let result = filter_cargo_build(&output);
let rendered = result.matches("error[E0308]").count();
assert_eq!(rendered, CAP_ERRORS, "json errors must be capped: {}", result);
assert!(
result.contains(&format!("… +{} more errors", total - CAP_ERRORS)),
"expected overflow hint: {}",
result
);
assert!(
result.contains(&format!("{} errors", total)),
"summary must report the real total: {}",
result
);
}
#[test]
fn test_filter_cargo_build_json_savings() {
// Real --message-format=json lines carry a verbose envelope (spans,
// children, code, message) around the human `rendered` text. The filter
// keeps only `rendered` and caps the list, so savings come from both
// dropping the envelope and capping a large failing build.
let template = r#"{"reason":"compiler-message","package_id":"demo 0.1.0 (path+file:///tmp/demo)","manifest_path":"/tmp/demo/Cargo.toml","target":{"kind":["bin"],"crate_types":["bin"],"name":"demo","src_path":"/tmp/demo/src/main.rs","edition":"2021","doc":true,"doctest":false,"test":true},"message":{"rendered":"error[E0308]: mismatched types\n --> src/main.rs:IDX:18\n |\nIDX | let _xIDX: i32 = \"errIDX\";\n | --- ^^^^^^^ expected `i32`, found `&str`\n | |\n | expected due to this\n\n","$message_type":"diagnostic","children":[{"children":[],"code":null,"level":"note","message":"expected due to the type annotation here","rendered":null,"spans":[]}],"code":{"code":"E0308","explanation":null},"level":"error","message":"mismatched types","spans":[{"byte_end":40,"byte_start":33,"column_end":25,"column_start":18,"expansion":null,"file_name":"src/main.rs","is_primary":true,"label":"expected `i32`, found `&str`","line_end":IDX,"line_start":IDX,"suggested_replacement":null,"suggestion_applicability":null,"text":[{"highlight_end":25,"highlight_start":18,"text":" let _xIDX: i32 = \"errIDX\";"}]}]}}"#;
let total = CAP_ERRORS + 30;
let mut input = String::new();
for i in 0..total {
input.push_str(&template.replace("IDX", &i.to_string()));
input.push('\n');
}
input.push_str(r#"{"reason":"build-finished","success":false}"#);
input.push('\n');
let result = filter_cargo_build(&input);
// The cap is what bounds the output, so assert it holds here too.
let rendered = result.matches("error[E0308]").count();
assert_eq!(rendered, CAP_ERRORS, "json errors must be capped: {}", result);
assert!(
result.contains(&format!("… +{} more errors", total - CAP_ERRORS)),
"expected overflow hint: {}",
result
);
let raw = input.split_whitespace().count();
let out = result.split_whitespace().count();
let savings = 100.0 - (out as f64 / raw as f64) * 100.0;
assert!(
savings >= 60.0,
"token savings dropped below 60%: {savings:.1}%"
);
}
#[test]
fn test_cargo_test_stream_all_pass() {
let input = r#" Compiling rtk v0.5.0
Finished test [unoptimized + debuginfo] target(s) in 2.53s
Running target/debug/deps/rtk-abc123
running 15 tests
test utils::tests::test_truncate_short_string ... ok
test utils::tests::test_truncate_long_string ... ok
test utils::tests::test_strip_ansi_simple ... ok
test result: ok. 15 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.01s
"#;
let mut f = BlockStreamFilter::new(CargoTestHandler::new());
let result = run_block_filter(&mut f, input, 0);
assert!(
result.contains("cargo test: 15 passed (1 suite, 0.01s)"),
"got: {}",
result
);
assert!(!result.contains("Compiling"), "got: {}", result);
}
#[test]
fn test_cargo_test_stream_failures() {
let input = r#"running 5 tests
test foo::test_a ... ok
test foo::test_b ... FAILED
test foo::test_c ... ok
failures:
---- foo::test_b stdout ----
thread 'foo::test_b' panicked at 'assert_eq!(1, 2)'
failures:
foo::test_b
test result: FAILED. 4 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out
"#;
let mut f = BlockStreamFilter::new(CargoTestHandler::new());
let result = run_block_filter(&mut f, input, 1);
assert!(result.contains("test_b"), "got: {}", result);
assert!(result.contains("panicked"), "got: {}", result);
}
#[test]
fn test_cargo_test_stream_multi_suite() {
let input = r#" Running unittests src/lib.rs (target/debug/deps/rtk-abc123)
running 50 tests
test result: ok. 50 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.45s
Running unittests src/main.rs (target/debug/deps/rtk-def456)
running 30 tests
test result: ok. 30 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.30s
"#;
let mut f = BlockStreamFilter::new(CargoTestHandler::new());
let result = run_block_filter(&mut f, input, 0);
assert!(
result.contains("cargo test: 80 passed (2 suites, 0.75s)"),
"got: {}",
result
);
}
#[test]
fn test_cargo_test_stream_compile_error() {
let input = r#" Compiling rtk v0.31.0 (/workspace/projects/rtk)
error[E0425]: cannot find value `missing_symbol` in this scope
--> tests/repro_compile_fail.rs:3:13
|
3 | let _ = missing_symbol;
| ^^^^^^^^^^^^^^ not found in this scope
For more information about this error, try `rustc --explain E0425`.
error: could not compile `rtk` (test "repro_compile_fail") due to 1 previous error
"#;
let mut f = BlockStreamFilter::new(CargoTestHandler::new());
let result = run_block_filter(&mut f, input, 1);
assert!(result.contains("cargo test:"), "got: {}", result);
assert!(result.contains("1 errors"), "got: {}", result);
}
#[test]
fn test_cargo_test_stream_json_compile_error() {
let input = concat!(
" Compiling v_cargo v0.1.0 (/tmp/v_cargo)\n",
r#"{"reason":"compiler-message","package_id":"v_cargo 0.1.0","message":{"code":{"code":"E0425"},"level":"error","message":"cannot find value `missing_symbol` in this scope","rendered":"error[E0425]: cannot find value `missing_symbol` in this scope\n --> tests/repro.rs:3:13"}}"#,
"\n",
r#"{"reason":"build-finished","success":false}"#,
"\n",
);
let mut f = BlockStreamFilter::new(CargoTestHandler::new());
let result = run_block_filter(&mut f, input, 101);
assert!(!result.trim().is_empty(), "json compile error must not be silent");
assert!(result.contains("cargo test:"), "got: {}", result);
assert!(result.contains("1 errors"), "got: {}", result);
assert!(result.contains("E0425"), "diagnostic must be surfaced: {}", result);
}
#[test]
fn test_cargo_test_stream_no_diagnostic_falls_back_to_raw() {
// "could not compile" sets has_compile_errors but is skipped by the build
// re-scan, so the reused filter yields a success-shaped line. We must not
// trust it — fall back to the raw tail instead of a bogus "compiled".
let input = concat!(
" Compiling foo v0.1.0 (/tmp/foo)\n",
"error: could not compile `foo` (test \"bar\") due to previous error\n",
);
let mut f = BlockStreamFilter::new(CargoTestHandler::new());
let result = run_block_filter(&mut f, input, 101);
assert!(
!result.contains("crates compiled"),
"must not report a failed test run as compiled: {}",
result
);
assert!(result.contains("could not compile"), "got: {}", result);
}
}