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::{resolved_command, truncate};
use anyhow::Result;
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),
}
}
fn restore_double_dash(args: &[String]) -> Vec<String> {
let raw_args: Vec<String> = std::env::args().collect();
restore_double_dash_with_raw(args, &raw_args)
}
fn restore_double_dash_with_raw(args: &[String], raw_args: &[String]) -> Vec<String> {
if args.is_empty() {
return args.to_vec();
}
if args.iter().any(|a| a == "--") {
return args.to_vec();
}
let sep_pos = match raw_args.iter().position(|a| a == "--") {
Some(pos) => pos,
None => return args.to_vec(),
};
let args_before_sep = raw_args[..sep_pos]
.iter()
.filter(|a| args.contains(a))
.count();
let mut result = Vec::with_capacity(args.len() + 1);
result.extend_from_slice(&args[..args_before_sep]);
result.push("--".to_string());
result.extend_from_slice(&args[args_before_sep..]);
result
}
struct CargoBuildHandler {
compiled: usize,
warnings: usize,
error_count: usize,
finished_line: Option<String>,
}
impl CargoBuildHandler {
fn new() -> Self {
Self {
compiled: 0,
warnings: 0,
error_count: 0,
finished_line: None,
}
}
}
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 {
let mut s = format!("cargo build ({} crates compiled)", self.compiled);
if let Some(ref finished) = self.finished_line {
s = format!("{}\n{}", s, finished);
}
Some(format!("{}\n", s))
} else {
Some(format!(
"═══════════════════════════════════════\ncargo build: {} errors, {} warnings ({} crates)\n",
self.error_count, self.warnings, self.compiled
))
}
}
}
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;
}
if trimmed.starts_with("error[") || trimmed.starts_with("error:") {
self.has_compile_errors = true;
}
if line == "failures:" {
if self.in_failure_section {
self.in_failure_names = true;
}
self.in_failure_section = true;
return true;
}
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() && self.has_compile_errors {
let build_filtered = filter_cargo_build(raw);
if build_filtered.starts_with("cargo build:") {
return Some(format!(
"{}\n",
build_filtered.replacen("cargo build:", "cargo test:", 1)
));
}
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")));
}
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()));
}
}
}
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
}
}
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 = 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)),
)
}
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 = 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 run_build(args: &[String], verbose: u8) -> Result<i32> {
run_cargo_streamed(
"build",
args,
verbose,
Box::new(BlockStreamFilter::new(CargoBuildHandler::new())),
)
}
fn run_test(args: &[String], verbose: u8) -> Result<i32> {
run_cargo_streamed(
"test",
args,
verbose,
Box::new(BlockStreamFilter::new(CargoTestHandler::new())),
)
}
fn run_clippy(args: &[String], verbose: u8) -> Result<i32> {
run_cargo_filtered("clippy", args, verbose, filter_cargo_clippy)
}
fn run_check(args: &[String], verbose: u8) -> Result<i32> {
run_cargo_streamed(
"check",
args,
verbose,
Box::new(BlockStreamFilter::new(CargoBuildHandler::new())),
)
}
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)
}
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)
}
}
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();
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;
}
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;
}
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;
}
if trimmed.starts_with("Replacing") || trimmed.starts_with("Replaced") {
replaced_lines.push(trimmed.to_string());
continue;
}
if trimmed.starts_with("Ignored package") {
already_installed = true;
ignored_line = trimmed.to_string();
continue;
}
if line.starts_with("warning:") {
if !(line.contains("generated") && line.contains("warning")) {
replaced_lines.push(line.to_string());
}
continue;
}
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"));
}
if already_installed {
let info = ignored_line.split('`').nth(1).unwrap_or(&ignored_line);
return format!("cargo install: {} already installed", info);
}
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
));
}
result.push_str("═══════════════════════════════════════\n");
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();
}
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
}
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();
}
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();
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;
}
if trimmed.starts_with("────") {
continue;
}
if past_summary {
continue;
}
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;
}
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;
}
if trimmed.starts_with("FAIL") {
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;
}
if trimmed.starts_with("Cancelling") || trimmed.starts_with("Canceling") {
has_cancel_line = true;
continue;
}
if trimmed.starts_with("Nextest run ID") {
continue;
}
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;
}
if in_failure_block {
current_failure_body.push(line.to_string());
}
}
if in_failure_block {
flush_failure_block(
&mut current_failure_header,
&mut current_failure_body,
&mut failures,
);
}
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 {
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);
}
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();
}
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;
}
String::new()
}
fn filter_cargo_build(output: &str) -> String {
let mut handler = CargoBuildHandler::new();
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, ¤t_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);
}
if handler.error_count == 0 && handler.warnings == 0 {
let mut s = format!("cargo build ({} crates compiled)", handler.compiled);
if let Some(ref finished) = handler.finished_line {
s = format!("{}\n{}", s, finished);
}
return s;
}
let mut result = format!(
"cargo build: {} errors, {} warnings ({} crates)\n═══════════════════════════════════════\n",
handler.error_count, handler.warnings, handler.compiled
);
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()
}
#[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 {
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();
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,
})
}
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;
}
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() {
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;
}
if line.starts_with("running ") || (line.starts_with("test ") && line.ends_with("... ok")) {
continue;
}
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());
}
}
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() {
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_parsed {
if let Some(agg) = aggregated {
if agg.suites > 0 {
return agg.format_compact();
}
}
}
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()));
result.push_str("═══════════════════════════════════════\n");
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 has_compile_errors = 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(output);
if build_filtered.starts_with("cargo build:") {
return build_filtered.replacen("cargo build:", "cargo test:", 1);
}
}
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()
}
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;
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() {
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;
}
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 {
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;
}
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() {
if !current_block.is_empty() {
error_blocks.push(current_block.clone());
current_block.clear();
}
in_error = false;
} else if current_block.len() < 15 {
current_block.push(line.to_string());
}
}
}
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
));
result.push_str("═══════════════════════════════════════\n");
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));
}
}
}
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()
}
pub fn run_passthrough(args: &[OsString], verbose: u8) -> Result<i32> {
crate::core::runner::run_passthrough("cargo", args, verbose)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_restore_double_dash_with_separator() {
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() {
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() {
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() {
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() {
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);
assert_eq!(
result,
vec!["-p", "my-service", "-p", "my-crate", "--", "-D", "warnings"]
);
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);
assert!(result.contains("FAILURES"), "got: {}", result);
assert!(result.contains("test_bad"), "got: {}", result);
assert!(result.contains("test result:"), "got: {}", result);
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);
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() {
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);
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
);
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
);
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
);
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
);
}
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::new());
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_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::new());
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_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);
}
}