use std::io::{self, IsTerminal, Read, Write};
use std::path::{Path, PathBuf};
use std::process::{Command, Stdio};
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use std::time::{Duration, Instant, SystemTime};
use clap::Parser;
use notify::{Config, Event, RecommendedWatcher, RecursiveMode, Watcher};
use rvtest::core::{CoverageFormat, ReportFormat, TestRun};
#[cfg(test)]
use rvtest::core::{TestCase, TestKind, TestStatus, TestSuite};
use rvtest::coverage::{CoverageCollector, CoverageConfig};
use rvtest::report::{self, TestReporter};
use rvtest::runner::parse_cargo_test_output;
#[derive(Parser)]
#[command(
name = "cargo-rvtest",
about = "A Next Level Testing Library for Rust",
version,
long_about = "rvtest is A Next Level Testing Library for Rust.\n\n\
rvtest extends Rust's built-in testing with BDD specs, \
property-based testing, parametrized tests, and rich reporting. \
Use `cargo rvtest` to run tests or `cargo rvtest --coverage` \
for code coverage analysis."
)]
struct Cli {
#[arg(short = 'f', long = "filter")]
filter: Option<String>,
#[arg(short = 't', long = "tag")]
include_tags: Vec<String>,
#[arg(short = 'E', long = "exclude-tag")]
exclude_tags: Vec<String>,
#[arg(short = 'r', long = "retries", default_value = "0")]
retries: u32,
#[arg(long = "timeout")]
timeout_secs: Option<f64>,
#[arg(long = "no-parallel")]
no_parallel: bool,
#[arg(long = "max-threads", default_value = "0")]
max_threads: usize,
#[arg(short = 'F', long = "format", default_value = "pretty")]
format: String,
#[arg(long = "fail-fast")]
fail_fast: bool,
#[arg(long = "seed")]
seed: Option<u64>,
#[arg(short = 'v', long = "verbose")]
verbose: bool,
#[arg(long = "show-output")]
show_output: bool,
#[arg(long = "watch")]
watch: bool,
#[arg(long = "daemon")]
daemon: bool,
#[arg(long = "detect-flaky", default_missing_value = "10", num_args = 0..=1, require_equals = false, default_value = "0")]
detect_flaky: u32,
#[arg(long = "fast")]
fast: bool,
#[arg(long = "cranelift")]
cranelift: bool,
#[arg(long = "parallel-frontend")]
parallel_frontend: Option<usize>,
#[arg(long = "profile-slow", default_missing_value = "5", num_args = 0..=1, require_equals = false)]
profile_slow: Option<u32>,
#[arg(long = "update-all")]
update_all: bool,
#[arg(long = "review")]
review: bool,
#[arg(long = "coverage")]
coverage: bool,
#[arg(long = "coverage-format", default_value = "summary")]
coverage_format: String,
#[arg(long = "coverage-dir", default_value = "target/coverage")]
coverage_dir: PathBuf,
#[arg(long = "coverage-min")]
coverage_min: Option<f64>,
#[arg(long = "coverage-open")]
coverage_open: bool,
}
fn main() {
let args: Vec<String> = std::env::args().collect();
let raw_args: Vec<String> = if args.len() > 1 && args[1] == "rvtest" {
let mut a = vec![args[0].clone()];
a.extend_from_slice(&args[2..]);
a
} else {
args
};
let args = Cli::parse_from(raw_args);
if args.coverage || args.coverage_open {
let cov_format: CoverageFormat = args.coverage_format.parse().unwrap_or_else(|e| {
eprintln!("{e}, falling back to 'summary'");
CoverageFormat::Summary
});
let cov_config = CoverageConfig {
enabled: true,
format: cov_format,
output_dir: args.coverage_dir.clone(),
min_threshold: args.coverage_min,
open_report: args.coverage_open,
..Default::default()
};
let collector = CoverageCollector::new(cov_config);
match collector.collect() {
Ok(report) => {
println!(
"Coverage: {:.1}% lines, {:.1}% functions, {:.1}% regions",
report.line_coverage,
report.function_coverage,
report.region_coverage,
);
if let Some(path) = &report.report_path {
println!("Report: {}", path.display());
}
std::process::exit(0);
}
Err(e) => {
eprintln!("Coverage collection failed:\n{e}");
std::process::exit(1);
}
}
}
if args.update_all {
rvtest::snapshot::set_update_all(true);
}
if args.review {
rvtest::snapshot::set_review_mode(true);
}
if args.daemon {
let filter = args.filter.clone();
let format: ReportFormat = args.format.parse().unwrap_or(ReportFormat::Pretty);
let daemon = rvtest::daemon::CompileDaemon::new(filter, format);
daemon.run();
return;
}
if args.watch {
let filter = args.filter.clone();
let format = args.format.clone();
let fast = args.fast;
let slow_count = args.profile_slow.unwrap_or(0) as usize;
let cranelift = args.cranelift;
let parallel_frontend = args.parallel_frontend;
watch_loop(filter, format, fast, slow_count, cranelift, parallel_frontend);
return;
}
if args.detect_flaky > 0 {
let filter = args.filter.clone();
let n = args.detect_flaky;
let verbose = args.verbose;
let fast = args.fast;
let cranelift = args.cranelift;
let parallel_frontend = args.parallel_frontend;
detect_flaky(filter, n, verbose, fast, cranelift, parallel_frontend);
return;
}
let format: ReportFormat = args.format.parse().unwrap_or_else(|e| {
eprintln!("{e}, falling back to 'pretty'");
ReportFormat::Pretty
});
if args.cranelift || args.parallel_frontend.is_some() {
if !is_nightly() {
eprintln!("warning: --cranelift and --parallel-frontend require nightly Rust.\n\
Switch with: `rustup default nightly` or use `cargo +nightly rvtest`.");
}
if args.cranelift && !has_cranelift_component() {
eprintln!("warning: Cranelift codegen backend not found.\n\
Install: `rustup component add rustc-codegen-cranelift-preview --toolchain nightly`");
}
}
let run = run_cargo_test(args.filter.as_deref(), args.fast, args.cranelift, args.parallel_frontend);
let report = render(&format, &run, args.profile_slow.unwrap_or(0) as usize);
println!("{report}");
std::process::exit(if run.success() { 0 } else { 1 });
}
fn run_cargo_test(filter: Option<&str>, fast: bool, cranelift: bool, parallel_frontend: Option<usize>) -> TestRun {
let start = SystemTime::now();
let wall_start = Instant::now();
let mut cmd = Command::new("cargo");
cmd.arg("test").arg("--color=never");
let mut extra_rustflags: Vec<String> = Vec::new();
if fast {
cmd.env("CARGO_PROFILE_DEV_DEBUG", "0");
if let Some(linker) = detect_fast_linker() {
extra_rustflags.push(format!("-C link-arg=-fuse-ld={linker}"));
}
}
if cranelift {
extra_rustflags.push("-Zcodegen-backend=cranelift".to_owned());
}
if let Some(n) = parallel_frontend {
extra_rustflags.push(format!("-Zthreads={n}"));
}
if !extra_rustflags.is_empty() {
let extra = extra_rustflags.join(" ");
let existing = std::env::var_os("RUSTFLAGS");
let merged = match existing {
Some(ref val) if !val.is_empty() => format!("{} {}", val.to_str().unwrap_or(""), extra),
None | Some(_) => extra,
};
cmd.env("RUSTFLAGS", merged);
}
if let Some(f) = filter {
cmd.arg("--").arg(f);
}
let is_tty = io::stdout().is_terminal();
let running = Arc::new(AtomicBool::new(true));
let r = running.clone();
let spinner_handle = std::thread::spawn(move || {
if !is_tty {
r.store(false, Ordering::SeqCst);
return;
}
let frames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
let mut i = 0;
while r.load(Ordering::SeqCst) {
print!("\r {} {} {} running...", frames[i], dim("cargo test"), dim("tests"));
io::stdout().flush().ok();
i = (i + 1) % frames.len();
std::thread::sleep(Duration::from_millis(80));
}
});
let output = match cmd.output() {
Ok(o) => {
running.store(false, Ordering::SeqCst);
let _ = spinner_handle.join();
if is_tty {
print!("\r");
io::stdout().flush().ok();
}
o
}
Err(e) => {
running.store(false, Ordering::SeqCst);
let _ = spinner_handle.join();
if is_tty {
print!("\r");
io::stdout().flush().ok();
}
eprintln!("Error: failed to run `cargo test`: {e}");
std::process::exit(1);
}
};
let duration = wall_start.elapsed();
let stdout = String::from_utf8_lossy(&output.stdout);
let stderr = String::from_utf8_lossy(&output.stderr);
let suites = parse_cargo_test_output(&stderr, &stdout);
TestRun {
suites,
start_time: start,
end_time: SystemTime::now(),
duration,
}
}
fn dim(s: &str) -> String {
format!("\x1b[2m{s}\x1b[0m")
}
fn is_nightly() -> bool {
let output = Command::new("rustc").arg("--version").output().ok();
match output {
Some(o) if o.status.success() => {
let s = String::from_utf8_lossy(&o.stdout);
s.contains("nightly")
}
_ => false,
}
}
fn has_cranelift_component() -> bool {
let mut cmd = Command::new("rustc");
cmd.args(["-Zcodegen-backend=cranelift", "--version"]);
cmd.stdout(Stdio::null()).stderr(Stdio::null());
cmd.status().map(|s| s.success()).unwrap_or(false)
}
fn detect_fast_linker() -> Option<&'static str> {
if Command::new("mold").arg("--version").stdout(Stdio::null()).stderr(Stdio::null()).status().is_ok() {
return Some("mold");
}
if Command::new("ld.lld").arg("--version").stdout(Stdio::null()).stderr(Stdio::null()).status().is_ok() {
return Some("lld");
}
None
}
fn watch_loop(mut filter: Option<String>, format_str: String, fast: bool, slow_count: usize, cranelift: bool, parallel_frontend: Option<usize>) {
let done = Arc::new(AtomicBool::new(false));
let format: ReportFormat = format_str.parse().unwrap_or(ReportFormat::Pretty);
let (tx, rx) = std::sync::mpsc::channel::<Result<Event, notify::Error>>();
let mut watcher = match RecommendedWatcher::new(tx, Config::default()) {
Ok(w) => w,
Err(e) => {
eprintln!("Error: cannot start file watcher: {e}");
std::process::exit(1);
}
};
for dir in &["src", "tests"] {
if Path::new(dir).exists() {
let _ = watcher.watch(Path::new(dir), RecursiveMode::Recursive);
}
}
run_and_print(&filter, &format, fast, slow_count, cranelift, parallel_frontend);
eprint!(" Watching src/, tests/ for changes... [q] quit [r] re-run [f] filter\n\n");
#[cfg(unix)]
{
unsafe {
libc::signal(libc::SIGINT, sigint_handler as *const () as libc::sighandler_t);
}
}
let debounce = Duration::from_millis(300);
let mut pending = false;
loop {
if done.load(Ordering::SeqCst) {
break;
}
let deadline = Instant::now() + debounce;
while Instant::now() < deadline && !done.load(Ordering::SeqCst) {
match rx.recv_timeout(Duration::from_millis(50)) {
Ok(Ok(_)) => pending = true,
Ok(Err(_)) => {}
Err(std::sync::mpsc::RecvTimeoutError::Timeout) => {}
Err(std::sync::mpsc::RecvTimeoutError::Disconnected) => break,
}
}
#[cfg(unix)]
if !done.load(Ordering::SeqCst) {
match check_watch_key() {
WatchKey::Quit => {
eprintln!("Quitting.");
break;
}
WatchKey::Rerun => {
eprintln!(" Re-running tests...\n");
run_and_print(&filter, &format, fast, slow_count, cranelift, parallel_frontend);
eprint!("\n Watching... [q] quit [r] re-run [f] filter\n\n");
continue;
}
WatchKey::Filter => {
eprint!(" Enter filter: ");
let _ = std::io::stdout().flush();
let mut input = String::new();
if std::io::stdin().read_line(&mut input).is_ok() {
let trimmed = input.trim().to_owned();
if trimmed.is_empty() {
filter = None;
eprintln!(" Filter cleared.");
} else {
filter = Some(trimmed);
eprintln!(" Filter set to: {}", filter.as_deref().unwrap_or(""));
}
}
eprintln!(" Re-running tests...\n");
run_and_print(&filter, &format, fast, slow_count, cranelift, parallel_frontend);
eprint!("\n Watching... [q] quit [r] re-run [f] filter\n\n");
continue;
}
WatchKey::None => {}
}
}
if !pending && !done.load(Ordering::SeqCst) {
std::thread::sleep(Duration::from_millis(100));
continue;
}
pending = false;
if done.load(Ordering::SeqCst) {
break;
}
eprintln!(" Change detected — re-running tests...\n");
run_and_print(&filter, &format, fast, slow_count, cranelift, parallel_frontend);
eprint!("\n Watching... [q] quit [r] re-run [f] filter\n\n");
}
}
#[cfg(unix)]
unsafe extern "C" fn sigint_handler(_: libc::c_int) {
}
enum WatchKey { Quit, Rerun, Filter, None }
#[cfg(unix)]
fn check_watch_key() -> WatchKey {
use std::os::fd::AsRawFd;
let fd = io::stdin().as_raw_fd();
let mut fds: libc::fd_set = unsafe { std::mem::zeroed() };
unsafe { libc::FD_SET(fd, &mut fds) };
let mut tv = libc::timeval { tv_sec: 0, tv_usec: 0 };
let ret = unsafe { libc::select(fd + 1, &mut fds, std::ptr::null_mut(), std::ptr::null_mut(), &mut tv) };
if ret > 0 {
let mut buf = [0u8; 1];
if io::stdin().read_exact(&mut buf).is_ok() {
match buf[0] {
b'q' | b'Q' => return WatchKey::Quit,
b'r' | b'R' => return WatchKey::Rerun,
b'f' | b'F' => return WatchKey::Filter,
_ => {}
}
}
}
WatchKey::None
}
#[cfg(not(unix))]
fn check_watch_key() -> WatchKey {
WatchKey::None
}
fn detect_flaky(filter: Option<String>, num_runs: u32, verbose: bool, fast: bool, cranelift: bool, parallel_frontend: Option<usize>) {
use std::collections::HashMap;
eprintln!("\n 🔍 Running test suite {num_runs} times to detect flaky tests...\n");
let mut results: HashMap<String, (u32, u32)> = HashMap::new();
for run in 1..=num_runs {
if verbose {
eprint!(" Run {run}/{num_runs}... ");
}
let test_run = run_cargo_test(filter.as_deref(), fast, cranelift, parallel_frontend);
for suite in &test_run.suites {
for test in &suite.tests {
if test.status.is_skipped() {
continue;
}
let entry = results.entry(test.name.clone()).or_insert((0, 0));
entry.1 += 1; if test.status.is_passed() {
entry.0 += 1; }
}
}
if verbose {
let passed = test_run.total_passed();
let failed = test_run.total_failed();
eprintln!("{passed} passed, {failed} failed");
}
}
eprintln!();
let mut flaky_found = false;
let mut sorted: Vec<_> = results.into_iter().collect();
sorted.sort_by(|a, b| a.0.cmp(&b.0));
for (name, (passes, total)) in &sorted {
let rate = *passes as f64 / *total as f64 * 100.0;
if rate < 100.0 {
flaky_found = true;
eprintln!(
" ⚠ {name:<60} {passes}/{total} passes ({rate:.0}%)"
);
}
}
if !flaky_found {
eprintln!(" ✅ No flaky tests detected — every test passed on all {num_runs} runs.");
}
eprintln!();
}
fn run_and_print(filter: &Option<String>, format: &ReportFormat, fast: bool, slow_count: usize, cranelift: bool, parallel_frontend: Option<usize>) {
let run = run_cargo_test(filter.as_deref(), fast, cranelift, parallel_frontend);
let report = render(format, &run, slow_count);
println!("{report}");
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn dim_wraps_in_ansi() {
let s = dim("hello");
assert_eq!(s, "\x1b[2mhello\x1b[0m");
}
#[test]
fn dim_empty_string() {
let s = dim("");
assert_eq!(s, "\x1b[2m\x1b[0m");
}
#[test]
fn detect_fast_linker_returns_some_or_none() {
let linker = detect_fast_linker();
match linker {
Some("mold") | Some("lld") | None => {}
_ => panic!("unexpected linker: {linker:?}"),
}
}
#[test]
fn parse_cargo_test_output_empty() {
let suites = parse_cargo_test_output("", "");
assert!(suites.is_empty() || suites.len() == 1);
}
#[test]
fn parse_cargo_test_output_with_one_pass() {
let stderr = "Running unittests src/lib.rs (target/debug/deps/lib-abc123)\n";
let stdout = "test my_test ... ok\ntest result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out\n";
let suites = parse_cargo_test_output(stderr, stdout);
assert_eq!(suites.len(), 1);
assert_eq!(suites[0].tests.len(), 1);
assert!(suites[0].tests[0].status.is_passed());
}
#[test]
fn parse_cargo_test_output_with_failure() {
let stderr = "Running unittests src/lib.rs (target/debug/deps/lib-abc123)\n";
let stdout = "test failing_test ... FAILED\n\ntest result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out\n\nfailures:\n\nfailures:\n failing_test\n";
let suites = parse_cargo_test_output(stderr, stdout);
assert_eq!(suites.len(), 1);
assert!(!suites[0].success());
}
#[test]
fn parse_cargo_test_output_with_ignored() {
let stderr = "Running unittests src/lib.rs (target/debug/deps/lib-abc123)\n";
let stdout = "test skipped_test ... ignored\ntest result: ok. 0 passed; 0 failed; 1 ignored; 0 measured; 0 filtered out\n";
let suites = parse_cargo_test_output(stderr, stdout);
assert_eq!(suites.len(), 1);
assert!(suites[0].tests[0].status.is_skipped());
}
#[test]
fn parse_cargo_test_output_multiple_sections() {
let stderr = "Running unittests src/lib.rs (target/debug/deps/lib-abc123)\nRunning tests/integration.rs (target/debug/deps/integration-def456)\n";
let stdout = "test unit_test ... ok\ntest result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out\ntest integration_test ... ok\ntest result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out\n";
let suites = parse_cargo_test_output(stderr, stdout);
assert_eq!(suites.len(), 2);
}
#[test]
fn parse_cargo_test_output_doc_tests() {
let stderr = "Doc-tests rvtest\n";
let stdout = "test test_foo ... ok\ntest result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out\n";
let suites = parse_cargo_test_output(stderr, stdout);
assert_eq!(suites.len(), 1);
assert_eq!(suites[0].kind, TestKind::Doc);
}
#[test]
fn parse_cargo_test_output_fallback_section() {
let suites = parse_cargo_test_output("", "test foo ... ok\ntest result: ok. 1 passed; 0 failed; 0 ignored\n");
assert!(!suites.is_empty());
}
#[test]
fn parse_cargo_test_output_failure_details() {
let stderr = "Running unittests src/lib.rs (target/debug/deps/lib-abc123)\n";
let stdout = "\
---- my_test stdout ----
some detail line
another detail
test my_test ... FAILED
test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out
failures:
my_test
";
let suites = parse_cargo_test_output(stderr, stdout);
assert!(!suites.is_empty());
assert!(!suites[0].success());
}
#[test]
fn parse_cargo_test_output_doc_test_name_formatting() {
let stderr = "Doc-tests rvtest\n";
let stdout = "test test_foo ... ok\ntest result: ok. 1 passed; 0 failed\n";
let suites = parse_cargo_test_output(stderr, stdout);
assert_eq!(suites.len(), 1);
assert_eq!(suites[0].kind, TestKind::Doc);
assert_eq!(suites[0].source_path, "rvtest");
assert_eq!(suites[0].tests[0].name, "rvtest - test_foo");
}
#[test]
fn parse_cargo_test_output_failure_with_location_suite_name() {
let stderr = "Running tests/integration.rs (target/debug/deps/integration-abc)\n";
let stdout = "test my_test ... FAILED\ntest result: FAILED. 0 passed; 1 failed\n";
let suites = parse_cargo_test_output(stderr, stdout);
assert_eq!(suites[0].kind, TestKind::Integration);
assert_eq!(suites[0].source_path, "tests/integration.rs");
}
#[test]
fn render_with_slow_tests() {
let mut suite = TestSuite::new("test");
suite.tests.push(TestCase {
name: "test :: slow".into(), suite: Some("test".into()), tags: vec![],
status: TestStatus::Passed, duration: Duration::from_secs(2),
assertions: 0, location: None, parameters: vec![], captured_output: None,
});
let run = TestRun {
suites: vec![suite],
start_time: SystemTime::now(),
end_time: SystemTime::now(),
duration: Duration::from_secs(2),
};
let result = render(&ReportFormat::Compact, &run, 5);
assert!(result.contains("Slowest"));
assert!(result.contains("2.00s"));
}
#[test]
fn render_without_slow() {
let run = TestRun::new();
let result = render(&ReportFormat::Compact, &run, 0);
assert!(result.contains("0/0"));
}
#[test]
fn render_all_formats() {
let run = TestRun::new();
for fmt in [ReportFormat::Pretty, ReportFormat::Tap, ReportFormat::Junit, ReportFormat::Json, ReportFormat::Compact, ReportFormat::Github] {
let result = render(&fmt, &run, 0);
assert!(!result.is_empty(), "render output should not be empty for {fmt:?}");
}
}
#[test]
fn render_slow_zero_no_slow_section() {
let mut suite = TestSuite::new("test");
suite.tests.push(TestCase {
name: "test :: fast".into(), suite: Some("test".into()), tags: vec![],
status: TestStatus::Passed, duration: Duration::from_millis(1),
assertions: 0, location: None, parameters: vec![], captured_output: None,
});
let run = TestRun {
suites: vec![suite],
start_time: SystemTime::now(),
end_time: SystemTime::now(),
duration: Duration::from_millis(1),
};
let result = render(&ReportFormat::Pretty, &run, 0);
assert!(!result.contains("Slowest"), "should not show slowest section when count is 0");
}
#[test]
fn render_slow_nonzero_no_tests() {
let run = TestRun::new();
let result = render(&ReportFormat::Pretty, &run, 5);
assert!(!result.contains("Slowest"));
}
#[test]
fn render_pretty_with_multiple_suites() {
let mut s1 = TestSuite::new("A");
s1.tests.push(TestCase::new("A :: t1"));
let mut s2 = TestSuite::new("B");
s2.tests.push(TestCase::new("B :: t2"));
let run = TestRun {
suites: vec![s1, s2],
start_time: SystemTime::now(),
end_time: SystemTime::now(),
duration: Duration::from_millis(10),
};
let result = render(&ReportFormat::Pretty, &run, 0);
assert!(result.contains("A"));
assert!(result.contains("B"));
}
#[test]
fn format_duration_exact_second() {
let s = crate::report::format_duration(Duration::from_secs(1));
assert_eq!(s, "1.00s");
}
#[test]
fn format_duration_just_below_second() {
let s = crate::report::format_duration(Duration::from_millis(999));
assert_eq!(s, "999.0ms");
}
#[test]
fn parse_cargo_test_output_no_parentheses() {
let stderr = "Running unittests src/lib.rs\n";
let stdout = "test foo ... ok\ntest result: ok. 1 passed; 0 failed\n";
let suites = parse_cargo_test_output(stderr, stdout);
assert!(!suites.is_empty());
}
#[test]
fn parse_cargo_test_output_malformed_test_line() {
let stderr = "Running unittests src/lib.rs\n";
let stdout = "test malformed_no_separator\ntest result: ok. 0 passed; 0 failed\n";
let suites = parse_cargo_test_output(stderr, stdout);
assert_eq!(suites.len(), 1);
assert_eq!(suites[0].tests.len(), 0);
}
#[test]
fn parse_cargo_test_output_extra_lines_after_last_section() {
let stderr = "Running unittests src/lib.rs\n";
let stdout = "test t1 ... ok\ntest result: ok. 1 passed; 0 failed\ntest extra_after_result ... ok\n";
let suites = parse_cargo_test_output(stderr, stdout);
assert_eq!(suites.len(), 1);
assert_eq!(suites[0].tests.len(), 1);
}
#[test]
fn parse_cargo_test_output_multiple_failures_with_details() {
let stderr = "Running unittests src/lib.rs\n";
let stdout = "\
---- test_a stdout ----
detail for a
---- test_b stdout ----
detail for b
test test_a ... FAILED
test test_b ... FAILED
test result: FAILED. 0 passed; 2 failed; 0 ignored; 0 measured; 0 filtered out
failures:
test_a
test_b
";
let suites = parse_cargo_test_output(stderr, stdout);
assert!(!suites.is_empty());
assert!(!suites[0].success());
assert_eq!(suites[0].tests.len(), 2);
}
#[test]
fn parse_cargo_test_output_empty_failure_line() {
let stderr = "Running unittests src/lib.rs\n";
let stdout = "\
---- my_test stdout ----
test my_test ... FAILED
test result: FAILED. 0 passed; 1 failed; 0 ignored
failures:
my_test
";
let suites = parse_cargo_test_output(stderr, stdout);
assert!(!suites[0].success());
}
#[test]
fn parse_cargo_test_output_unknown_status_skipped() {
let stderr = "Running unittests src/lib.rs\n";
let stdout = "\
test my_test ... ???unknown???
test result: ok. 0 passed; 0 failed; 0 ignored
";
let suites = parse_cargo_test_output(stderr, stdout);
assert_eq!(suites[0].tests.len(), 0);
}
#[test]
fn is_nightly_returns_bool() {
let _ = is_nightly();
}
#[test]
fn has_cranelift_component_returns_bool() {
let _ = has_cranelift_component();
}
#[test]
fn cli_defaults() {
let args = Cli::parse_from(["cargo-rvtest"]);
assert!(!args.fast);
assert!(!args.cranelift);
assert!(args.parallel_frontend.is_none());
}
#[test]
fn cli_cranelift_flag() {
let args = Cli::parse_from(["cargo-rvtest", "--cranelift"]);
assert!(args.cranelift);
}
#[test]
fn cli_parallel_frontend() {
let args = Cli::parse_from(["cargo-rvtest", "--parallel-frontend", "4"]);
assert_eq!(args.parallel_frontend, Some(4));
}
#[test]
fn cli_cranelift_with_fast() {
let args = Cli::parse_from(["cargo-rvtest", "--fast", "--cranelift"]);
assert!(args.fast);
assert!(args.cranelift);
}
#[test]
fn cli_review_flag() {
let args = Cli::parse_from(["cargo-rvtest", "--review"]);
assert!(args.review);
}
#[test]
fn cli_daemon_flag() {
let args = Cli::parse_from(["cargo-rvtest", "--daemon"]);
assert!(args.daemon);
}
#[test]
fn cli_all_fast_flags() {
let args = Cli::parse_from([
"cargo-rvtest",
"--fast",
"--cranelift",
"--parallel-frontend",
"8",
]);
assert!(args.fast);
assert!(args.cranelift);
assert_eq!(args.parallel_frontend, Some(8));
}
}
fn render(format: &ReportFormat, run: &TestRun, slow_count: usize) -> String {
let reporter: Box<dyn TestReporter> = match format {
ReportFormat::Pretty => Box::new(report::PrettyReporter::new()),
ReportFormat::Tap => Box::new(report::TapReporter),
ReportFormat::Junit => Box::new(report::JunitReporter::new()),
ReportFormat::Json => Box::new(report::JsonReporter),
ReportFormat::Compact => Box::new(report::CompactReporter),
ReportFormat::Github => Box::new(report::GithubReporter),
};
let mut out = reporter.report(run);
if slow_count > 0 {
let slow = run.slowest(slow_count);
if !slow.is_empty() {
use std::fmt::Write;
let _ = writeln!(out);
let _ = writeln!(out, " {} Slowest tests", dim("⏱"));
for (i, test) in slow.iter().enumerate() {
let dur = report::format_duration(test.duration);
let name = test.name.replace(" :: ", " > ");
let _ = writeln!(out, " {}. {:>8} {}", i + 1, dur, name);
}
}
}
out
}