use core::time::Duration;
use std::borrow::Cow;
use std::io::{self, BufReader, Read};
use std::process::{ChildStderr, ChildStdout, Command, ExitStatus, Stdio};
use std::sync::mpsc::{Receiver, RecvTimeoutError};
use std::sync::{Arc, Mutex, mpsc};
use std::thread;
use std::time::Instant;
use camino::Utf8Path;
use cargo_gamma_process::{MemoryRequest, MemoryUsage, PreparedCommand, ProcessTree, SpawnedCommand, prepare};
mod hubs;
#[cfg(test)]
use cargo_gamma_process::faults::{self as process_faults, Fault as ProcessFault};
use hubs::Pulse;
#[doc(inline)]
pub use hubs::READERS;
#[cfg(test)]
#[cfg(not(loom))]
use hubs::Readers;
#[cfg(loom)]
pub(crate) use hubs::run_loom_models;
#[cfg(test)]
use super::faults::{self, Fault};
use super::harness_filters::HarnessFilters;
use super::loader::{Launch, STACK_VAR, UNDER_GAMMA_VAR, configure_loader};
use super::nextest;
use super::progress::{Progress, Watch};
use super::stall::Stall;
use super::test_binary::{TEST_THREADS_VAR, TestBinary};
use super::workspace::Workspace;
const INSTA_UPDATE_VAR: &str = "INSTA_UPDATE";
const INSTA_FORCE_PASS_VAR: &str = "INSTA_FORCE_PASS";
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub(super) enum Only<'name> {
#[default]
All,
One(&'name str),
These(&'name [&'name str]),
}
impl<'name> Only<'name> {
fn names(self) -> Cow<'name, [&'name str]> {
match self {
Self::All => Cow::Owned(Vec::new()),
Self::One(name) => Cow::Owned(vec![name]),
Self::These(names) => Cow::Borrowed(names),
}
}
}
#[derive(Debug, Clone, Copy)]
pub(super) struct Attempt<'name> {
pub(super) active: Option<u32>,
pub(super) timeout: Option<Duration>,
pub(super) stall: Stall,
pub(super) request: MemoryRequest,
pub(super) only: Only<'name>,
pub(super) census: Option<&'name Utf8Path>,
}
impl Attempt<'_> {
const fn exonerating(self) -> Self {
Self { active: None, ..self }
}
const fn lengthened(self) -> Self {
Self {
timeout: match self.timeout {
Some(timeout) => Some(timeout.saturating_mul(CONFIRM_FACTOR)),
None => None,
},
..self
}
}
fn patient(self) -> Self {
Self {
stall: self.stall.scaled(CONFIRM_FACTOR),
..self
}
}
}
#[derive(Debug, PartialEq, Eq)]
pub(super) enum Verdict {
Passed,
Failed(Option<String>),
TestEnumerationFailed(String),
TimedOut,
Stalled(Option<String>),
MemoryLimit {
peak: Option<u64>,
limit: u64,
},
Flaky(Option<String>),
Unmetered(String),
Unjudged(String),
}
pub(super) fn run_binary(work: &Workspace, binary: &TestBinary, attempt: Attempt<'_>, confirm: bool) -> Verdict {
let confirmed = settle_suspicions(attempt, |attempt| observe(work, binary, attempt).verdict);
match confirmed {
Verdict::Failed(test) if confirm => confirm_kill(work, binary, attempt, test),
Verdict::TestEnumerationFailed(output) => confirm_enumeration(work, binary, attempt, output),
other => other,
}
}
const SPAWN_ATTEMPTS: usize = 4;
const SPAWN_BACKOFF: Duration = Duration::from_millis(50);
enum StartError {
Containment(String),
Spawn(io::Error),
}
fn spawn_patiently(command: Command, request: MemoryRequest) -> Result<SpawnedCommand, StartError> {
let mut waited = SPAWN_BACKOFF;
let mut prepared = prepare(command, request).map_err(|cause| StartError::Containment(cause.to_string()))?;
for _attempt in 1..SPAWN_ATTEMPTS {
match spawn_once(prepared) {
Ok(spawned) => return Ok(spawned),
Err(FailedSpawn { cause, prepared: returned }) if transient(&cause) => {
prepared = (*returned)
.backoff(waited)
.map_err(|cause| StartError::Containment(cause.to_string()))?;
}
Err(FailedSpawn { cause, .. }) => return Err(StartError::Spawn(cause)),
}
waited = waited.saturating_mul(2);
}
spawn_once(prepared).map_err(|failure| StartError::Spawn(failure.cause))
}
struct FailedSpawn {
cause: io::Error,
prepared: Box<PreparedCommand>,
}
fn spawn_once(prepared: PreparedCommand) -> Result<SpawnedCommand, FailedSpawn> {
#[cfg(test)]
if faults::fired(Fault::Spawn) {
return Err(FailedSpawn {
cause: io::Error::new(io::ErrorKind::WouldBlock, "the process table is full"),
prepared: Box::new(prepared),
});
}
prepared.spawn().map_err(|failure| {
let (cause, prepared) = failure.into_parts();
FailedSpawn {
cause,
prepared: Box::new(prepared),
}
})
}
fn transient(cause: &io::Error) -> bool {
if cause.kind() == io::ErrorKind::WouldBlock || cause.kind() == io::ErrorKind::Interrupted {
return true;
}
#[cfg(unix)]
{
matches!(
cause.raw_os_error(),
Some(libc::EAGAIN | libc::EMFILE | libc::ENFILE | libc::ENOMEM)
)
}
#[cfg(not(unix))]
{
false
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Suspicion {
Timeout,
Stall,
Memory,
}
impl Suspicion {
const fn of(verdict: &Verdict) -> Option<Self> {
match verdict {
Verdict::TimedOut => Some(Self::Timeout),
Verdict::Stalled(_) => Some(Self::Stall),
Verdict::MemoryLimit { .. } => Some(Self::Memory),
_evidence => None,
}
}
fn checking(self, attempt: Attempt<'_>) -> Attempt<'_> {
match self {
Self::Timeout => attempt.lengthened(),
Self::Stall => attempt.patient(),
Self::Memory => attempt,
}
}
}
const CONFIRM_ROUNDS: usize = 2;
fn settle_suspicions(attempt: Attempt<'_>, mut run: impl FnMut(Attempt<'_>) -> Verdict) -> Verdict {
let mut verdict = run(attempt);
let mut checked = None;
for _round in 0..CONFIRM_ROUNDS {
let Some(suspicion) = Suspicion::of(&verdict) else {
break;
};
if checked == Some(suspicion) {
break;
}
checked = Some(suspicion);
verdict = run(suspicion.checking(attempt));
}
verdict
}
fn confirm_kill(work: &Workspace, binary: &TestBinary, attempt: Attempt<'_>, test: Option<String>) -> Verdict {
if attempt.active.is_none() {
return Verdict::Failed(test);
}
let exonerating_attempt = test.as_deref().map_or_else(
|| attempt.exonerating(),
|name| Attempt {
active: None,
only: Only::One(name),
..attempt
},
);
match observe(work, binary, exonerating_attempt).verdict {
Verdict::Passed => Verdict::Failed(test),
Verdict::Failed(also) => Verdict::Flaky(test.or(also)),
Verdict::Unmetered(reason) => Verdict::Unmetered(reason),
Verdict::Unjudged(reason) => Verdict::Unjudged(reason),
_unestablished => Verdict::Flaky(test),
}
}
fn confirm_enumeration(work: &Workspace, binary: &TestBinary, attempt: Attempt<'_>, output: String) -> Verdict {
match observe(work, binary, attempt.exonerating()).verdict {
Verdict::Passed => Verdict::TestEnumerationFailed(output),
Verdict::Failed(test) => Verdict::Flaky(test),
Verdict::Unmetered(reason) => Verdict::Unmetered(reason),
Verdict::Unjudged(reason) => Verdict::Unjudged(reason),
_unestablished => Verdict::Flaky(None),
}
}
fn announce_partial(binary: &TestBinary) {
announce(&format!(
"the output of `{}` could not be read to the end, so the failing test may be named in text \
this run never saw. The exit status still decided the verdict.",
binary.path
));
}
fn announce(message: &str) {
crate::notes::note(message.to_owned());
}
pub(crate) const CONFIRM_FACTOR: u32 = 3;
fn launcher(work: &Workspace, binary: &TestBinary, only: Only<'_>) -> Result<Command, String> {
let Some(harness) = work.runner() else {
let mut command = Command::new(binary.path.as_std_path());
let names = only.names();
if names.is_empty() {
let _ = command.args(work.test_arguments());
} else {
let user = HarnessFilters::parse(work.test_arguments());
let allowed: Vec<&str> = names.iter().copied().filter(|name| user.admits(name)).collect();
if allowed.is_empty() {
return Err(format!(
"the test selection this run made for `{}` names nothing the harness filters allow, \
so it was refused rather than run as a wider selection",
binary.path
));
}
let _ = command.args(user.flags());
let _ = command.args(allowed).arg("--exact");
}
let _ = command.current_dir(working_directory(work, binary).as_std_path());
return Ok(command);
};
harness.command(work, binary, &only.names()).map_err(|cause| cause.to_string())
}
fn settle(under_nextest: bool, active: Option<u32>, code: Option<i32>, text: &[u8], usage: MemoryUsage) -> (Verdict, MemoryUsage) {
if text
.windows(gamma_rt::ENVIRONMENT_ERROR_MARKER.len())
.any(|window| window == gamma_rt::ENVIRONMENT_ERROR_MARKER)
{
return (
Verdict::Unmetered("the guard runtime could not acquire the process startup environment".to_owned()),
usage,
);
}
let output = String::from_utf8_lossy(text);
if !under_nextest {
return (Verdict::Failed(first_failure(&output).map(str::to_owned)), usage);
}
match code {
Some(nextest::TEST_RUN_FAILED) => (Verdict::Failed(nextest::first_failure(&output).map(str::to_owned)), usage),
Some(nextest::NO_TESTS_RUN) => (
Verdict::Unmetered("`cargo nextest` matched no tests for a binary this run built".to_owned()),
usage,
),
Some(nextest::TEST_LIST_CREATION_FAILED) if active.is_some() => (Verdict::TestEnumerationFailed(output.trim().to_owned()), usage),
other => (Verdict::Unmetered(nextest_runner_failure(other, &output)), usage),
}
}
fn nextest_runner_failure(code: Option<i32>, output: &str) -> String {
let mut reason = format!(
"`cargo nextest` exited with {}, which does not describe a test run",
code.map_or_else(|| "a signal".to_owned(), |code| format!("code {code}"))
);
let useful = output.trim();
if !useful.is_empty() {
reason.push_str(":\n");
reason.push_str(&tail(useful, 20));
}
reason
}
fn configure(
command: &mut Command,
binary: &TestBinary,
launch: &Launch,
threads: Option<&str>,
active: Option<u32>,
census: Option<&Utf8Path>,
) {
let _ = command
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.env("RUST_BACKTRACE", "0")
.env(STACK_VAR, launch.stack.as_str())
.env(UNDER_GAMMA_VAR, "1")
.env(INSTA_UPDATE_VAR, "no")
.env(INSTA_FORCE_PASS_VAR, "0");
if let Some(threads) = threads {
let _ = command.env(TEST_THREADS_VAR, threads);
}
if !binary.manifest_dir.as_str().is_empty() {
let _ = command.env("CARGO_MANIFEST_DIR", binary.manifest_dir.as_std_path());
}
match active {
Some(ordinal) => {
let _ = command.env(gamma_rt::ACTIVE_VAR, ordinal.to_string());
}
None => {
let _ = command.env_remove(gamma_rt::ACTIVE_VAR);
}
}
match census {
Some(path) => {
let _ = command.env(gamma_rt::CENSUS_VAR, path.as_std_path());
}
None => {
let _ = command.env_remove(gamma_rt::CENSUS_VAR);
}
}
}
fn run_with(work: &Workspace, binary: &TestBinary, attempt: Attempt<'_>, progress: &Arc<Mutex<Progress>>) -> (Verdict, MemoryUsage) {
let (active, timeout, stall, request) = (attempt.active, attempt.timeout, attempt.stall, attempt.request);
let mut command = match launcher(work, binary, attempt.only) {
Ok(command) => command,
Err(reason) => return (Verdict::Unmetered(reason), MemoryUsage::default()),
};
let launch = work.launch();
configure_loader(&mut command, launch);
let under_nextest = work.runner().is_some();
configure(&mut command, binary, launch, work.harness_threads(), active, attempt.census);
let spawned = match spawn_patiently(command, request) {
Ok(started) => started,
Err(StartError::Containment(reason)) => return (Verdict::Unmetered(reason), MemoryUsage::default()),
Err(StartError::Spawn(cause)) => {
let reason = if request.wanted() {
format!(
"`{}` could not be started inside its memory accounting boundary: {cause}",
binary.path
)
} else {
format!("`{}` could not be started: {cause}", binary.path)
};
return (Verdict::Unjudged(reason), MemoryUsage::default());
}
};
let mut subtree = match ProcessTree::adopt(spawned) {
Ok(subtree) => subtree,
Err(reason) => {
return (Verdict::Unjudged(reason.to_string()), MemoryUsage::default());
}
};
let pulse = Arc::new(Pulse::default());
let drained = match readers(&mut subtree, progress, &pulse, under_nextest) {
Ok(drained) => drained,
Err(cause) => {
let (usage, _ceiling) = cut_short(&mut subtree, request);
return (
Verdict::Unjudged(format!("`{}` output could not be supervised: {cause}", binary.path)),
usage,
);
}
};
let deadline = timeout.map(deadline_after);
loop {
let seen = pulse.seen();
match ask_after(&mut subtree) {
Ok(Some(status)) => {
debug_assert!(subtree.released(), "the containment is released before the output is drained");
let (text, whole) = collected(&drained, DRAIN_GRACE);
let usage = subtree.usage();
let ceiling = request.limit.filter(|_limit| exhausted(&usage, status.success()));
if let Some(verdict) = environment_verdict(progress) {
return (verdict, usage);
}
if status.success() {
return (Verdict::Passed, usage);
}
if !whole {
announce_partial(binary);
}
let (verdict, usage) = settle(under_nextest, active, status.code(), &text, usage);
return (prefer_named(verdict, usage.peak, ceiling), usage);
}
Ok(None) => {
if let Some(verdict) = environment_verdict(progress) {
let (usage, _ceiling) = cut_short(&mut subtree, request);
return (verdict, usage);
}
if let Some(name) = failure_to_cut_short(under_nextest, progress) {
let (usage, ceiling) = cut_short(&mut subtree, request);
return (cut_by_named_failure(name, usage.peak, ceiling), usage);
}
let stalled = stall.exceeded(progress);
if stalled || deadline.is_some_and(|deadline| Instant::now() >= deadline) {
let (usage, ceiling) = cut_short(&mut subtree, request);
if let Some(verdict) = unfinished_nextest_failure(under_nextest, progress) {
return (verdict, usage);
}
if let Some(limit) = ceiling {
return (Verdict::MemoryLimit { peak: usage.peak, limit }, usage);
}
return if stalled {
(Verdict::Stalled(last_test(progress)), usage)
} else {
(Verdict::TimedOut, usage)
};
}
pulse.wait(seen, WAIT_CAP.min(deadline.map_or(WAIT_CAP, remaining)).min(stall.slack(progress)));
}
Err(cause) => {
let (usage, _ceiling) = cut_short(&mut subtree, request);
debug_assert!(subtree.released(), "the containment is released before the output is drained");
let (_text, _whole) = collected(&drained, DRAIN_GRACE);
return (
Verdict::Unjudged(format!("`{}` could not be asked whether it had finished: {cause}", binary.path)),
usage,
);
}
}
}
}
fn deadline_after(timeout: Duration) -> Instant {
let now = Instant::now();
let timeout = timeout.min(Duration::from_secs(crate::bounds::MOST_SECONDS));
now.checked_add(timeout).unwrap_or(now)
}
fn collected(drained: &Receiver<(Vec<u8>, bool)>, grace: Duration) -> (Vec<u8>, bool) {
let mut text = Vec::new();
let mut whole = true;
loop {
match drained.recv_timeout(grace) {
Ok((chunk, complete)) => {
text.extend_from_slice(&chunk);
whole &= complete;
}
Err(RecvTimeoutError::Disconnected) => return (text, whole),
Err(RecvTimeoutError::Timeout) => return (text, false),
}
}
}
fn ask_after(subtree: &mut ProcessTree) -> io::Result<Option<ExitStatus>> {
#[cfg(test)]
if faults::fired(Fault::Wait) {
return Err(io::Error::other("the wait a test asked to fail"));
}
subtree.observe()
}
fn prefer_named(verdict: Verdict, peak: Option<u64>, ceiling: Option<u64>) -> Verdict {
let Some(limit) = ceiling else {
return verdict;
};
match verdict {
Verdict::Failed(Some(name)) => Verdict::Failed(Some(name)),
Verdict::Failed(None) => Verdict::MemoryLimit { peak, limit },
other => other,
}
}
fn cut_by_named_failure(name: String, peak: Option<u64>, ceiling: Option<u64>) -> Verdict {
prefer_named(Verdict::Failed(Some(name)), peak, ceiling)
}
fn cut_short(subtree: &mut ProcessTree, request: MemoryRequest) -> (MemoryUsage, Option<u64>) {
#[cfg(unix)]
debug_assert!(
!subtree.released(),
"the subtree is signalled while it still holds its leader and its watch slot"
);
let _reaped = subtree.terminate();
let usage = subtree.usage();
let ceiling = request.limit.filter(|_limit| exhausted(&usage, false));
(usage, ceiling)
}
const fn exhausted(usage: &MemoryUsage, succeeded: bool) -> bool {
!succeeded && usage.exhausted
}
const DRAIN_GRACE: Duration = Duration::from_secs(5);
#[derive(Debug)]
pub(super) struct Observation {
pub(super) verdict: Verdict,
pub(super) quiet: Duration,
pub(super) tests: Option<usize>,
pub(super) peak: Option<u64>,
}
fn working_directory<'work>(work: &'work Workspace, binary: &'work TestBinary) -> &'work Utf8Path {
if binary.manifest_dir.as_str().is_empty() {
&work.root
} else {
&binary.manifest_dir
}
}
pub(super) fn observe(work: &Workspace, binary: &TestBinary, attempt: Attempt<'_>) -> Observation {
let progress = Arc::new(Mutex::new(Progress::new(watch(work))));
let (verdict, usage) = run_with(work, binary, attempt, &progress);
let quiet = quiet_of(&progress);
#[expect(clippy::unwrap_used, reason = "the reader only panics if the whole process is unwinding")]
let tests = progress.lock().unwrap().tests;
Observation {
verdict,
quiet,
tests,
peak: usage.peak,
}
}
fn watch(work: &Workspace) -> Watch {
if work.runner().is_some() {
return Watch::Nextest;
}
let interleaved = work
.test_arguments()
.iter()
.any(|argument| argument == "--nocapture" || argument == "--show-output");
if interleaved || std::env::var_os("RUST_TEST_NOCAPTURE").is_some() {
return Watch::Off;
}
Watch::Libtest
}
fn remaining(deadline: Instant) -> Duration {
deadline.saturating_duration_since(Instant::now())
}
const WAIT_CAP: Duration = Duration::from_millis(50);
fn failure_to_cut_short(under_nextest: bool, progress: &Mutex<Progress>) -> Option<String> {
(!under_nextest).then(|| announced_failure(progress)).flatten()
}
fn announced_failure(progress: &Mutex<Progress>) -> Option<String> {
#[expect(clippy::unwrap_used, reason = "the reader only panics if the whole process is unwinding")]
progress.lock().unwrap().failed.clone()
}
fn environment_failure(progress: &Mutex<Progress>) -> bool {
#[expect(clippy::unwrap_used, reason = "the reader only panics if the whole process is unwinding")]
progress.lock().unwrap().environment_error
}
fn environment_verdict(progress: &Mutex<Progress>) -> Option<Verdict> {
environment_failure(progress)
.then(|| Verdict::Unmetered("the guard runtime could not acquire the process startup environment".to_owned()))
}
fn unfinished_nextest_failure(under_nextest: bool, progress: &Mutex<Progress>) -> Option<Verdict> {
(under_nextest && announced_failure(progress).is_some()).then(|| {
Verdict::Unmetered(
"`cargo nextest` announced a failure but did not finish before the run budget, \
so its captured startup output could not be classified"
.to_owned(),
)
})
}
fn last_test(progress: &Mutex<Progress>) -> Option<String> {
#[expect(clippy::unwrap_used, reason = "the reader only panics if the whole process is unwinding")]
progress.lock().unwrap().test.clone()
}
fn quiet_of(progress: &Mutex<Progress>) -> Duration {
#[expect(clippy::unwrap_used, reason = "the reader only panics if the whole process is unwinding")]
let progress = progress.lock().unwrap();
progress.quiet.max(Instant::now().saturating_duration_since(progress.heard))
}
const OUTPUT_CAP: usize = 4 * 1024 * 1024;
fn readers(
subtree: &mut ProcessTree,
progress: &Arc<Mutex<Progress>>,
pulse: &Arc<Pulse>,
under_nextest: bool,
) -> io::Result<Receiver<(Vec<u8>, bool)>> {
let (sink, drained) = mpsc::channel::<(Vec<u8>, bool)>();
let pipes = [
subtree.take_stdout().map(|pipe| (Either::Out(pipe), !under_nextest)),
subtree.take_stderr().map(|pipe| (Either::Err(pipe), under_nextest)),
];
for (pipe, authoritative) in pipes.into_iter().flatten() {
let published = Arc::clone(progress);
let pulse = Arc::clone(pulse);
let sink = sink.clone();
#[cfg(test)]
let refused = faults::fired(Fault::Thread);
#[cfg(not(test))]
let refused = false;
let spawned = if refused {
Err(io::Error::other("the reader thread a test asked to fail"))
} else {
thread::Builder::new().name("cargo-gamma-output".to_owned()).spawn(move || {
READERS.started();
let collected = match pipe {
Either::Out(pipe) => drain(pipe, &published, &pulse, authoritative),
Either::Err(pipe) => drain(pipe, &published, &pulse, authoritative),
};
let _sent = sink.send(collected);
pulse.signal();
READERS.finished();
})
};
let _handle = spawned?;
}
drop(sink);
Ok(drained)
}
enum Either {
Out(ChildStdout),
Err(ChildStderr),
}
fn drain<R: Read>(pipe: R, progress: &Mutex<Progress>, pulse: &Pulse, authoritative: bool) -> (Vec<u8>, bool) {
use std::io::BufRead as _;
let mut reader = BufReader::new(pipe);
let mut kept = Vec::new();
let mut line = Vec::new();
let mut whole = true;
loop {
line.clear();
match reader.read_until(b'\n', &mut line) {
Ok(0) => return (kept, whole),
Err(_truncated) => return (kept, false),
Ok(_read) => {
#[expect(clippy::unwrap_used, reason = "the watcher only panics if the process is unwinding")]
let decisive = {
let mut progress = progress.lock().unwrap();
let before = progress.failed.is_some();
let environment_error = progress.environment_error;
let line = String::from_utf8_lossy(&line);
if authoritative {
progress.heard(&line);
} else {
progress.heard_diagnostic(&line);
}
(!before && progress.failed.is_some()) || (!environment_error && progress.environment_error)
};
if decisive {
pulse.signal();
}
let room = OUTPUT_CAP.saturating_sub(kept.len());
let kept_now = line.len().min(room);
kept.extend_from_slice(&line[..kept_now]);
if kept_now < line.len() {
whole = false;
}
}
}
}
}
fn first_failure(output: &str) -> Option<&str> {
output.lines().find_map(|line| {
let trimmed = line.trim();
let rest = trimmed.strip_prefix("test ")?;
let name = rest.strip_suffix(" ... FAILED")?;
Some(name.trim())
})
}
pub(super) fn tail(text: &str, count: usize) -> String {
let lines: Vec<&str> = text.lines().collect();
let start = lines.len().saturating_sub(count);
lines.get(start..).unwrap_or_default().join("\n")
}
#[cfg(all(test, not(loom), not(miri)))]
mod fuzz {
use super::super::progress::{Progress, Watch};
use super::first_failure;
use crate::testing::{spliced, token};
#[test]
fn a_libtest_failure_is_never_lost_among_arbitrary_output() {
bolero::check!()
.with_type::<(Vec<String>, String, usize)>()
.for_each(|(noise, name, at)| {
let output = spliced(noise, &format!("test {} ... FAILED", token(name)), *at);
assert!(first_failure(&output).is_some(), "the failure was lost in {output:?}");
});
}
#[test]
fn arbitrary_output_is_read_without_panicking() {
bolero::check!().with_type::<String>().for_each(|output| {
if let Some(name) = first_failure(output) {
assert!(!name.contains('\n'), "a test name spans lines: {name:?}");
}
});
}
#[test]
fn the_streaming_reader_sees_the_failure_the_batch_reader_sees() {
bolero::check!()
.with_type::<(Vec<String>, String, usize)>()
.for_each(|(noise, name, at)| {
let line = format!("test {} ... FAILED", token(name));
let output = spliced(noise, &line, *at);
let mut progress = Progress::new(Watch::Libtest);
progress.heard("running 1 test");
for line in output.lines() {
progress.heard(line);
}
assert!(progress.failed.is_some(), "the streaming reader lost the failure in {output:?}");
});
}
}
#[cfg(all(test, not(loom), not(miri)))]
mod tests {
use super::*;
#[test]
fn a_pipe_that_fails_part_way_is_not_read_as_the_end_of_the_stream() {
struct Faltering(bool);
impl Read for Faltering {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
if self.0 {
return Err(io::Error::other("the read a test asked to fail"));
}
self.0 = true;
let line = b"running 1 test\n";
buf[..line.len()].copy_from_slice(line);
Ok(line.len())
}
}
let progress = Mutex::new(Progress::new(Watch::Libtest));
let pulse = Pulse::default();
let (kept, whole) = drain(Faltering(false), &progress, &pulse, true);
assert!(!whole, "a severed pipe was reported as the end of the stream");
assert_eq!(String::from_utf8_lossy(&kept), "running 1 test\n");
}
#[test]
fn output_discarded_past_the_cap_marks_the_stream_incomplete() {
let mut output = vec![b'x'; OUTPUT_CAP];
output.extend_from_slice(b"\ntest meaningful::killer ... FAILED\n");
output.extend_from_slice(gamma_rt::ENVIRONMENT_ERROR_MARKER);
let progress = Mutex::new(Progress::new(Watch::Libtest));
let pulse = Pulse::default();
let (kept, whole) = drain(io::Cursor::new(output), &progress, &pulse, true);
assert_eq!(kept.len(), OUTPUT_CAP);
assert!(!whole, "discarding bytes past the cap was reported as a whole stream");
assert!(environment_failure(&progress), "the marker remains visible outside the output cap");
}
#[test]
fn diagnostic_output_cannot_forge_a_libtest_failure() {
let output = b"running 1 test\ntest forged::failure ... FAILED\n";
let progress = Mutex::new(Progress::new(Watch::Libtest));
let pulse = Pulse::default();
let (_kept, whole) = drain(io::Cursor::new(output), &progress, &pulse, false);
assert!(whole);
assert_eq!(announced_failure(&progress), None);
assert_eq!(last_test(&progress), None);
}
#[test]
fn a_confirmation_that_crosses_into_another_detection_class_is_confirmed_in_turn() {
let mut issued = 0_usize;
let verdict = settle_suspicions(attempt_for_confirmation(), |_attempt| {
issued += 1;
match issued {
1 => Verdict::TimedOut,
2 => Verdict::MemoryLimit { peak: Some(9), limit: 8 },
_settled => Verdict::Passed,
}
});
assert_eq!(issued, 3, "the memory verdict the confirmation produced was believed unchecked");
assert_eq!(verdict, Verdict::Passed);
}
#[test]
fn a_confirmation_that_agrees_ends_the_ladder() {
let mut issued = 0_usize;
let verdict = settle_suspicions(attempt_for_confirmation(), |_attempt| {
issued += 1;
Verdict::TimedOut
});
assert_eq!(issued, 2, "a reproduced timeout was asked about again");
assert_eq!(verdict, Verdict::TimedOut);
}
#[test]
fn a_verdict_that_never_settles_stops_at_the_bound() {
let mut issued = 0_usize;
let _verdict = settle_suspicions(attempt_for_confirmation(), |_attempt| {
issued += 1;
if issued.is_multiple_of(2) {
Verdict::MemoryLimit { peak: Some(9), limit: 8 }
} else {
Verdict::TimedOut
}
});
assert_eq!(issued, 1 + CONFIRM_ROUNDS, "the ladder is not bounded");
}
#[test]
fn a_settled_verdict_is_not_re_run() {
let mut issued = 0_usize;
let verdict = settle_suspicions(attempt_for_confirmation(), |_attempt| {
issued += 1;
Verdict::Failed(Some("a::b".to_owned()))
});
assert_eq!(issued, 1);
assert_eq!(verdict, Verdict::Failed(Some("a::b".to_owned())));
}
fn attempt_for_confirmation() -> Attempt<'static> {
Attempt {
active: Some(1),
timeout: Some(Duration::from_secs(30)),
stall: Stall::NONE,
request: MemoryRequest::default(),
only: Only::All,
census: None,
}
}
#[test]
fn a_reader_that_never_returns_leaves_the_collection_partial() {
let (sink, drained) = mpsc::channel::<(Vec<u8>, bool)>();
let _still_reading = sink.clone();
let _sent = sink.send((b"running 1 test\n".to_vec(), true));
drop(sink);
let (text, whole) = collected(&drained, Duration::from_millis(50));
assert!(!whole, "an absent reader was read as the end of the output");
assert_eq!(String::from_utf8_lossy(&text), "running 1 test\n");
}
#[test]
fn readers_that_all_returned_leave_the_collection_whole() {
let (sink, drained) = mpsc::channel::<(Vec<u8>, bool)>();
let _sent = sink.send((b"running 1 test\n".to_vec(), true));
let _also = sink.send((b"test a::b ... FAILED\n".to_vec(), true));
drop(sink);
let (text, whole) = collected(&drained, Duration::from_secs(30));
assert!(whole, "a complete collection was reported as partial");
assert!(
String::from_utf8_lossy(&text).contains("FAILED"),
"{:?}",
String::from_utf8_lossy(&text)
);
}
#[test]
fn a_read_interrupted_by_a_signal_is_retried_rather_than_cut_short() {
struct Interrupted(u8);
impl Read for Interrupted {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
self.0 = self.0.saturating_add(1);
let line: &[u8] = match self.0 {
1 => b"running 1 test\n",
2 => return Err(io::Error::from(io::ErrorKind::Interrupted)),
3 => b"test a::b ... FAILED\n",
_ => return Ok(0),
};
buf[..line.len()].copy_from_slice(line);
Ok(line.len())
}
}
let progress = Mutex::new(Progress::new(Watch::Libtest));
let pulse = Pulse::default();
let (kept, whole) = drain(Interrupted(0), &progress, &pulse, true);
assert!(whole, "an interrupted read was reported as truncation");
assert!(
String::from_utf8_lossy(&kept).contains("test a::b ... FAILED"),
"the text after the interruption was lost: {:?}",
String::from_utf8_lossy(&kept)
);
}
#[test]
fn a_verdict_reached_on_a_truncated_stream_says_the_text_was_partial() {
crate::notes::alone(|| {
let binary = crate::testing::helper();
announce_partial(&binary);
let raised = crate::notes::drain();
assert_eq!(raised.len(), 1, "{raised:?}");
assert!(raised[0].contains(binary.path.as_str()), "the binary is not named: {raised:?}");
});
}
fn scripted(script: &[&str]) -> (tempfile::TempDir, Workspace) {
crate::testing::helper_workspace("verdict", script)
}
#[cfg(unix)]
fn shell(body: &str) -> (tempfile::TempDir, Workspace) {
crate::testing::shell_workspace("verdict", body)
}
#[test]
#[cfg(unix)]
fn a_binary_that_exits_normally_still_has_its_survivors_killed() {
let alive = crate::testing::within(crate::testing::WATCHDOG, "sweeping a leaked grandchild", || {
let marker = std::env::temp_dir().join(format!("gamma-verdict-survivor.{}", std::process::id()));
let _removed = std::fs::remove_file(&marker);
let (_directory, work) = shell(&format!("sleep 300 & echo $! > {}; exit 0", marker.display()));
let leaky = crate::testing::test_binary("/bin/sh");
let verdict = run_with(
&work,
&leaky,
Attempt {
active: None,
timeout: Some(Duration::from_secs(30)),
stall: Stall::NONE,
request: MemoryRequest::default(),
only: Only::All,
census: None,
},
&Arc::new(Mutex::new(Progress::new(Watch::Libtest))),
)
.0;
assert!(matches!(verdict, Verdict::Passed), "{verdict:?}");
let recorded = std::fs::read_to_string(&marker).expect("the fixture records its background child");
let pid: i32 = recorded.trim().parse().expect("the fixture writes a pid");
let _removed = std::fs::remove_file(&marker);
let deadline = Instant::now() + Duration::from_secs(10);
loop {
if !cargo_gamma_unsafe::group::exists(pid) {
break false;
}
if Instant::now() >= deadline {
break true;
}
thread::sleep(Duration::from_millis(20));
}
});
assert!(!alive, "the grandchild outlived the test binary that spawned it");
}
#[test]
fn a_binary_that_cannot_be_spawned_is_unjudged_rather_than_a_kill() {
let (_directory, work) = scripted(&["exit:0"]);
let missing = crate::testing::test_binary(work.root.join("no-such-binary").as_str());
let verdict = run_binary(
&work,
&missing,
Attempt {
active: None,
timeout: Some(Duration::from_secs(1)),
stall: Stall::NONE,
request: MemoryRequest { meter: false, limit: None },
only: Only::All,
census: None,
},
true,
);
match verdict {
Verdict::Unjudged(reason) => assert!(reason.contains("no-such-binary"), "{reason}"),
other => panic!("expected an unjudged run, got {other:?}"),
}
}
#[test]
fn a_binary_that_cannot_be_spawned_inside_its_memory_boundary_is_unjudged() {
if crate::testing::without_memory_support("a run reporting the memory a mutant used") {
return;
}
let (_directory, work) = scripted(&["exit:0"]);
let missing = crate::testing::test_binary(work.root.join("no-such-binary").as_str());
let verdict = run_binary(
&work,
&missing,
Attempt {
active: None,
timeout: Some(Duration::from_secs(1)),
stall: Stall::NONE,
request: MemoryRequest { meter: true, limit: None },
only: Only::All,
census: None,
},
true,
);
assert!(matches!(verdict, Verdict::Unjudged(_)), "{verdict:?}");
}
#[test]
fn a_spawn_the_machine_refuses_is_tried_again() {
let (_directory, work) = scripted(&["exit:0"]);
let binary = crate::testing::helper();
let _refused = faults::arm(Fault::Spawn);
let verdict = run_binary(&work, &binary, plain_attempt(), true);
assert_eq!(verdict, Verdict::Passed, "the retry must let the run go ahead");
}
#[test]
fn a_spawn_refused_every_time_leaves_the_mutant_unjudged() {
let (_directory, work) = scripted(&["exit:0"]);
let binary = crate::testing::helper();
let _refusals: Vec<_> = (0..SPAWN_ATTEMPTS).map(|_round| faults::arm(Fault::Spawn)).collect();
let verdict = run_binary(&work, &binary, plain_attempt(), true);
match verdict {
Verdict::Unjudged(reason) => assert!(reason.contains("could not be started"), "{reason}"),
other => panic!("expected an unjudged run, got {other:?}"),
}
}
#[cfg(unix)]
#[test]
fn spawn_backoff_closes_the_interrupt_window_before_sleeping() {
let (_directory, work) = scripted(&["exit:0"]);
let binary = crate::testing::helper();
let _refused = faults::arm(Fault::Spawn);
let _interrupted = process_faults::arm_late(ProcessFault::Window, Duration::from_millis(10));
let started = Instant::now();
let verdict = run_binary(&work, &binary, plain_attempt(), true);
assert!(matches!(verdict, Verdict::Unmetered(_)), "{verdict:?}");
assert!(started.elapsed() < SPAWN_BACKOFF.saturating_mul(2), "{:?}", started.elapsed());
}
#[test]
fn reader_thread_creation_failure_leaves_the_attempt_unjudged() {
let (_directory, work) = scripted(&["flood:5000000", "exit:1"]);
let binary = crate::testing::helper();
let _refused = faults::arm(Fault::Thread);
let verdict = run_binary(&work, &binary, plain_attempt(), true);
match verdict {
Verdict::Unjudged(reason) => assert!(reason.contains("output could not be supervised"), "{reason}"),
other => panic!("expected an unjudged attempt, got {other:?}"),
}
}
#[test]
fn only_a_transient_refusal_is_waited_out() {
assert!(transient(&io::Error::from(io::ErrorKind::WouldBlock)));
assert!(transient(&io::Error::from(io::ErrorKind::Interrupted)));
assert!(!transient(&io::Error::from(io::ErrorKind::NotFound)));
assert!(!transient(&io::Error::from(io::ErrorKind::PermissionDenied)));
#[cfg(unix)]
{
assert!(transient(&io::Error::from_raw_os_error(libc::EMFILE)));
assert!(transient(&io::Error::from_raw_os_error(libc::ENFILE)));
assert!(transient(&io::Error::from_raw_os_error(libc::ENOMEM)));
assert!(!transient(&io::Error::from_raw_os_error(libc::ENOEXEC)));
}
}
fn plain_attempt() -> Attempt<'static> {
Attempt {
active: None,
timeout: Some(Duration::from_secs(30)),
stall: Stall::NONE,
request: MemoryRequest { meter: false, limit: None },
only: Only::All,
census: None,
}
}
#[test]
fn an_undelegated_host_says_what_is_missing_before_anything_is_spawned() {
if super::super::memory::support().is_ok() {
return;
}
let (_directory, work) = scripted(&["exit:0"]);
let ok = crate::testing::helper();
let verdict = run_binary(
&work,
&ok,
Attempt {
active: None,
timeout: Some(Duration::from_secs(1)),
stall: Stall::NONE,
request: MemoryRequest { meter: true, limit: None },
only: Only::All,
census: None,
},
true,
);
match verdict {
Verdict::Unmetered(reason) => assert!(!reason.is_empty(), "a refusal has to say why"),
other => panic!("expected an unmetered refusal, got {other:?}"),
}
}
#[test]
fn containment_that_cannot_be_installed_is_reported_rather_than_run_without() {
let (_directory, work) = scripted(&["exit:0"]);
let ok = crate::testing::helper();
let _armed = process_faults::arm(ProcessFault::Prepare);
let observed = observe(
&work,
&ok,
Attempt {
active: None,
timeout: Some(Duration::from_secs(10)),
stall: Stall::NONE,
request: MemoryRequest::default(),
only: Only::All,
census: None,
},
);
match observed.verdict {
Verdict::Unmetered(reason) => assert!(!reason.is_empty(), "a refusal has to say why"),
other => panic!("expected an unmetered refusal, got {other:?}"),
}
}
#[test]
fn a_child_that_cannot_be_adopted_is_ended_rather_than_left_outside_the_boundary() {
crate::testing::within(crate::testing::WATCHDOG, "ending an unadoptable child", || {
let started = crate::testing::workdir("verdict-adopt");
let root = camino::Utf8PathBuf::from_path_buf(started.path().to_path_buf()).expect("a UTF-8 scratch path");
let marker = root.join("ran");
let (_directory, work) =
crate::testing::helper_workspace("verdict-adopt", &["sleep:250", &format!("touch:{marker}"), "exit:0"]);
let ok = crate::testing::helper();
let _armed = process_faults::arm(ProcessFault::Adopt);
let observed = observe(
&work,
&ok,
Attempt {
active: None,
timeout: Some(Duration::from_secs(10)),
stall: Stall::NONE,
request: MemoryRequest::default(),
only: Only::All,
census: None,
},
);
match observed.verdict {
Verdict::Unjudged(reason) => assert!(!reason.is_empty(), "a refusal has to say why"),
other => panic!("expected an unjudged refusal, got {other:?}"),
}
thread::sleep(Duration::from_millis(1500));
assert!(
!marker.as_std_path().exists(),
"a child that could not be adopted has to be ended, not left running outside the boundary"
);
});
}
#[test]
fn a_child_that_cannot_be_asked_after_is_unjudged_rather_than_a_failure() {
crate::testing::within(crate::testing::WATCHDOG, "a wait that fails", || {
let (_directory, work) = scripted(&["sleep:200", "exit:0"]);
let ok = crate::testing::helper();
let _armed = faults::arm(Fault::Wait);
let observed = observe(
&work,
&ok,
Attempt {
active: None,
timeout: Some(Duration::from_secs(10)),
stall: Stall::NONE,
request: MemoryRequest::default(),
only: Only::All,
census: None,
},
);
match observed.verdict {
Verdict::Unjudged(reason) => assert!(reason.contains("finished"), "{reason}"),
other => panic!("expected an unjudged run, got {other:?}"),
}
});
}
#[test]
#[cfg(unix)]
fn a_wait_that_fails_still_takes_the_subtree_with_it() {
crate::testing::within(crate::testing::WATCHDOG, "a wait that fails mid-run", || {
let started = crate::testing::workdir("verdict-wait");
let root = camino::Utf8PathBuf::from_path_buf(started.path().to_path_buf()).expect("a UTF-8 scratch path");
let (running, survived) = (root.join("running"), root.join("survived"));
let (_directory, work) = scripted(&[&format!("spawn:touch:{running}|sleep:2000|touch:{survived}"), "sleep:30000"]);
let ok = crate::testing::helper();
let _armed = faults::arm_late(Fault::Wait, Duration::from_millis(750));
let observed = observe(
&work,
&ok,
Attempt {
active: None,
timeout: Some(Duration::from_secs(30)),
stall: Stall::NONE,
request: MemoryRequest::default(),
only: Only::All,
census: None,
},
);
match observed.verdict {
Verdict::Unjudged(reason) => assert!(!reason.is_empty(), "an unjudged run has to say why"),
other => panic!("expected an unjudged run, got {other:?}"),
}
assert!(
running.as_std_path().exists(),
"the descendant never started, so this test proves nothing about killing it"
);
thread::sleep(Duration::from_millis(2500));
assert!(
!survived.as_std_path().exists(),
"a wait that failed has to take the whole subtree with it, not just the child"
);
});
}
#[test]
fn an_unrepresentable_deadline_is_clamped_without_panicking() {
let before = Instant::now();
let deadline = deadline_after(Duration::MAX);
let latest = Instant::now()
.checked_add(Duration::from_secs(crate::bounds::MOST_SECONDS))
.expect("one year is a representable deadline");
assert!(deadline >= before);
assert!(deadline <= latest);
}
#[test]
fn a_binary_that_outlives_its_budget_times_out() {
let (_directory, work) = scripted(&["sleep:30000"]);
let sleeper = crate::testing::helper();
assert_eq!(
run_binary(
&work,
&sleeper,
Attempt {
active: None,
timeout: Some(Duration::from_millis(50)),
stall: Stall::NONE,
request: MemoryRequest::default(),
only: Only::All,
census: None,
},
true
),
Verdict::TimedOut
);
}
#[test]
fn a_binary_that_goes_quiet_is_stalled_at_the_last_test_it_named() {
let (_directory, work) = scripted(&["print:test slow::case ... ", "sleep:30000"]);
let hanger = crate::testing::helper();
let verdict = run_binary(
&work,
&hanger,
Attempt {
active: None,
timeout: Some(Duration::from_mins(1)),
stall: Stall {
budget: Some(Duration::from_millis(50)),
},
request: MemoryRequest::default(),
only: Only::All,
census: None,
},
true,
);
assert!(matches!(verdict, Verdict::Stalled(_)), "{verdict:?}");
}
#[test]
fn startup_silence_measured_by_the_baseline_does_not_stall_the_same_binary() {
let (_directory, work) =
crate::testing::helper_workspace("verdict-startup-silence-", &["sleep:100", "print:running 0 tests", "exit:0"]);
let binary = crate::testing::helper();
let baseline = observe(
&work,
&binary,
Attempt {
active: None,
timeout: Some(Duration::from_secs(5)),
stall: Stall::NONE,
request: MemoryRequest::default(),
only: Only::All,
census: None,
},
);
let measured = baseline.quiet;
let observed = observe(
&work,
&binary,
Attempt {
active: Some(1),
timeout: Some(Duration::from_secs(5)),
stall: Stall {
budget: Some(measured.saturating_mul(4)),
},
request: MemoryRequest::default(),
only: Only::All,
census: None,
},
);
assert!(measured >= Duration::from_millis(80), "{measured:?}");
assert_eq!(observed.verdict, Verdict::Passed);
}
#[test]
fn a_binary_runs_from_its_package_root() {
let (_directory, work) = scripted(&["require-file:marker.txt"]);
let package = work.root.join("crates").join("subject");
std::fs::create_dir_all(package.as_std_path()).expect("the package directory is created");
std::fs::write(package.join("marker.txt").as_std_path(), "here").expect("the marker is written");
let binary = TestBinary {
manifest_dir: package,
..crate::testing::helper()
};
assert_eq!(
run_binary(
&work,
&binary,
Attempt {
active: None,
timeout: Some(Duration::from_secs(30)),
stall: Stall::NONE,
request: MemoryRequest::default(),
only: Only::All,
census: None,
},
true
),
Verdict::Passed
);
}
#[test]
fn a_binary_that_exits_cleanly_passes() {
let (_directory, work) = scripted(&["print:test a::b ... ok", "exit:0"]);
let ok = crate::testing::helper();
assert_eq!(
run_binary(
&work,
&ok,
Attempt {
active: None,
timeout: Some(Duration::from_secs(30)),
stall: Stall::NONE,
request: MemoryRequest::default(),
only: Only::All,
census: None,
},
true
),
Verdict::Passed
);
}
#[test]
fn a_failure_convicts_a_mutant_without_waiting_for_the_rest_of_the_binary() {
let (_directory, work) = scripted(&["print:running 2 tests", "print:test a::b ... FAILED", "sleep:30000", "exit:101"]);
let binary = crate::testing::helper();
let started = Instant::now();
let verdict = run_binary(
&work,
&binary,
Attempt {
active: None,
timeout: Some(Duration::from_mins(2)),
stall: Stall::NONE,
request: MemoryRequest::default(),
only: Only::All,
census: None,
},
true,
);
let took = started.elapsed();
assert_eq!(verdict, Verdict::Failed(Some("a::b".to_owned())));
assert!(took < Duration::from_secs(15), "the run waited for the whole binary: {took:?}");
}
#[test]
fn the_test_named_is_the_first_that_failed_and_not_a_later_one() {
let (_directory, work) = scripted(&[
"print:running 3 tests",
"print:test a::first ... FAILED",
"print:test a::second ... FAILED",
"exit:101",
]);
let binary = crate::testing::helper();
assert_eq!(
run_binary(
&work,
&binary,
Attempt {
active: None,
timeout: Some(Duration::from_secs(30)),
stall: Stall::NONE,
request: MemoryRequest::default(),
only: Only::All,
census: None,
},
true
),
Verdict::Failed(Some("a::first".to_owned()))
);
}
#[test]
fn a_harness_that_announced_no_suite_is_judged_only_by_how_it_exits() {
let (_directory, work) = scripted(&["print:test a::b ... FAILED", "exit:0"]);
let binary = crate::testing::helper();
assert_eq!(
run_binary(
&work,
&binary,
Attempt {
active: None,
timeout: Some(Duration::from_secs(30)),
stall: Stall::NONE,
request: MemoryRequest::default(),
only: Only::All,
census: None,
},
true
),
Verdict::Passed
);
}
#[test]
fn a_run_with_capture_turned_off_takes_no_verdict_from_the_output() {
let (_directory, work) = scripted(&["--nocapture", "print:running 1 test", "print:test a::b ... FAILED", "exit:0"]);
let binary = crate::testing::helper();
assert_eq!(
run_binary(
&work,
&binary,
Attempt {
active: None,
timeout: Some(Duration::from_secs(30)),
stall: Stall::NONE,
request: MemoryRequest::default(),
only: Only::All,
census: None,
},
true
),
Verdict::Passed
);
}
#[test]
fn insta_is_told_not_to_accept_what_a_mutant_produced() {
let (_directory, work) = scripted(&["write-env:seen.txt|INSTA_UPDATE|INSTA_FORCE_PASS", "exit:0"]);
let binary = crate::testing::helper();
assert_eq!(
run_binary(
&work,
&binary,
Attempt {
active: None,
timeout: Some(Duration::from_secs(30)),
stall: Stall::NONE,
request: MemoryRequest::default(),
only: Only::All,
census: None,
},
true
),
Verdict::Passed
);
let seen = std::fs::read_to_string(work.root.join("seen.txt").as_std_path()).expect("the child recorded what it saw");
assert_eq!(seen, "no 0");
}
#[test]
fn a_binary_that_prints_past_the_pipe_cap_is_still_drained_to_completion() {
let (_directory, work) = scripted(&["flood:5000000", "print:test a::b ... ok", "exit:0"]);
let chatty = crate::testing::helper();
assert_eq!(
run_binary(
&work,
&chatty,
Attempt {
active: None,
timeout: Some(Duration::from_secs(30)),
stall: Stall::NONE,
request: MemoryRequest::default(),
only: Only::All,
census: None,
},
true
),
Verdict::Passed
);
}
#[test]
fn a_failing_binary_is_named_by_its_first_failing_test() {
let (_directory, work) = scripted(&[
"when-env:GAMMA_ACTIVE|print:test a::b ... FAILED",
"when-env:GAMMA_ACTIVE|exit:101",
"print:test a::b ... ok",
"exit:0",
]);
let bad = crate::testing::helper();
assert_eq!(
run_binary(
&work,
&bad,
Attempt {
active: Some(7),
timeout: Some(Duration::from_secs(30)),
stall: Stall::NONE,
request: MemoryRequest::default(),
only: Only::All,
census: None,
},
true
),
Verdict::Failed(Some("a::b".to_owned()))
);
}
fn assert_memory_verdict_reports_its_figures(verdict: &Verdict, expected: u64) {
let Verdict::MemoryLimit { peak, limit } = verdict else {
panic!("expected a memory verdict, got {verdict:?}");
};
assert_eq!(*limit, expected, "the verdict must carry the ceiling that fired: {verdict:?}");
let peak = peak.expect("the platform reports a peak when it enforces a ceiling");
assert!(
peak >= expected / 2,
"a peak of {peak} against a ceiling of {expected} is not the figure that crossed it",
);
}
#[test]
fn a_binary_that_passes_its_ceiling_is_a_memory_verdict() {
if crate::testing::without_memory_support("a mutant stopped for passing its ceiling") {
return;
}
let (_directory, work) = crate::testing::helper_workspace("verdict-memory", &["eat:512", "exit:0"]);
let greedy = crate::testing::helper();
let limit = 32 * 1024 * 1024;
let verdict = run_binary(
&work,
&greedy,
Attempt {
active: Some(1),
timeout: Some(Duration::from_mins(1)),
stall: Stall::NONE,
request: MemoryRequest {
meter: true,
limit: Some(limit),
},
only: Only::All,
census: None,
},
true,
);
assert_memory_verdict_reports_its_figures(&verdict, limit);
}
#[test]
fn a_binary_that_stalls_while_exhausting_its_ceiling_is_convicted_of_the_memory() {
if crate::testing::without_memory_support("a stalling mutant convicted of its memory") {
return;
}
let (_directory, work) = crate::testing::helper_workspace("verdict-memory-stall", &["eat:1024", "sleep:30000"]);
let greedy = crate::testing::helper();
let limit = 32 * 1024 * 1024;
let verdict = run_with(
&work,
&greedy,
Attempt {
active: Some(1),
timeout: Some(Duration::from_mins(1)),
stall: Stall {
budget: Some(Duration::from_secs(2)),
},
request: MemoryRequest {
meter: true,
limit: Some(limit),
},
only: Only::All,
census: None,
},
&Arc::new(Mutex::new(Progress::new(Watch::Libtest))),
)
.0;
assert_memory_verdict_reports_its_figures(&verdict, limit);
}
#[test]
fn a_binary_that_stays_under_its_ceiling_is_judged_normally() {
if crate::testing::without_memory_support("a ceiling reported with the figure that crossed it") {
return;
}
let (_directory, work) = scripted(&["print:test a::b ... ok", "exit:0"]);
let modest = crate::testing::helper();
assert_eq!(
run_binary(
&work,
&modest,
Attempt {
active: None,
timeout: Some(Duration::from_secs(30)),
stall: Stall::NONE,
request: MemoryRequest {
meter: true,
limit: Some(512 * 1024 * 1024)
},
only: Only::All,
census: None,
},
true
),
Verdict::Passed
);
}
#[test]
fn a_successful_run_is_never_convicted_by_its_peak() {
let usage = MemoryUsage {
peak: Some(1024),
exhausted: false,
};
assert!(!exhausted(&usage, true));
}
#[test]
fn a_workload_that_reclaimed_and_then_failed_is_not_a_memory_verdict() {
let reclaimed = MemoryUsage {
peak: Some(1024),
exhausted: false,
};
assert!(!exhausted(&reclaimed, false));
}
#[test]
fn a_kernel_reported_kill_is_believed_whatever_the_peak_says() {
let usage = MemoryUsage {
peak: Some(1),
exhausted: true,
};
assert!(exhausted(&usage, false));
assert!(!exhausted(&usage, true));
}
#[test]
fn a_named_failure_outranks_the_ceiling_the_same_run_crossed() {
let named = prefer_named(
Verdict::Failed(Some("module::case".to_owned())),
Some(300 * 1024 * 1024),
Some(256 * 1024 * 1024),
);
assert_eq!(named, Verdict::Failed(Some("module::case".to_owned())));
let anonymous = prefer_named(Verdict::Failed(None), Some(300), Some(256));
assert_eq!(
anonymous,
Verdict::MemoryLimit {
peak: Some(300),
limit: 256
}
);
let unmetered = prefer_named(Verdict::Unmetered("no cgroup".to_owned()), None, Some(256));
assert!(matches!(unmetered, Verdict::Unmetered(_)), "{unmetered:?}");
let plain = prefer_named(Verdict::Failed(Some("module::case".to_owned())), Some(1), None);
assert_eq!(plain, Verdict::Failed(Some("module::case".to_owned())));
}
#[test]
fn a_run_cut_short_by_a_named_failure_keeps_the_name_beside_the_ceiling() {
let named = cut_by_named_failure("module::case".to_owned(), Some(300), Some(256));
assert_eq!(named, Verdict::Failed(Some("module::case".to_owned())));
let plain = cut_by_named_failure("module::case".to_owned(), None, None);
assert_eq!(plain, Verdict::Failed(Some("module::case".to_owned())));
}
#[test]
#[cfg(windows)]
fn a_named_failure_survives_crossing_a_memory_ceiling_end_to_end() {
if crate::testing::without_memory_support("a named failure surviving a memory ceiling") {
return;
}
let (_directory, work) = crate::testing::helper_workspace(
"verdict-named-memory",
&[
"print:running 1 test",
"print:test failing::case ... FAILED",
"eat:512",
"sleep:30000",
"exit:101",
],
);
let binary = crate::testing::helper();
let limit = 32 * 1024 * 1024;
let verdict = run_binary(
&work,
&binary,
Attempt {
active: None,
timeout: Some(Duration::from_mins(1)),
stall: Stall::NONE,
request: MemoryRequest {
meter: true,
limit: Some(limit),
},
only: Only::All,
census: None,
},
false,
);
assert_eq!(verdict, Verdict::Failed(Some("failing::case".to_owned())));
}
#[test]
fn an_exoneration_runs_only_the_failing_test_avoiding_suite_tail_timeouts() {
let (_directory, work) = scripted(&[
"when-env:GAMMA_ACTIVE|print:test first ... FAILED",
"when-env:GAMMA_ACTIVE|exit:101",
"when-arg:first|exit:0",
"sleep:30000",
]);
let binary = crate::testing::helper();
let verdict = run_binary(
&work,
&binary,
Attempt {
active: Some(7),
timeout: Some(Duration::from_millis(500)),
stall: Stall::NONE,
request: MemoryRequest::default(),
only: Only::All,
census: None,
},
true,
);
assert_eq!(verdict, Verdict::Failed(Some("first".to_owned())));
}
#[test]
fn an_exoneration_that_ran_out_of_time_does_not_confirm_a_kill() {
let (_directory, work) = scripted(&["when-env:GAMMA_ACTIVE|exit:101", "sleep:30000"]);
let binary = crate::testing::helper();
let verdict = confirm_kill(
&work,
&binary,
Attempt {
active: Some(7),
timeout: Some(Duration::from_millis(200)),
stall: Stall::NONE,
request: MemoryRequest::default(),
only: Only::All,
census: None,
},
Some("a::b".to_owned()),
);
assert_eq!(verdict, Verdict::Flaky(Some("a::b".to_owned())), "{verdict:?}");
}
#[test]
fn an_exonerations_own_overrun_is_not_adopted_as_the_mutants_verdict() {
let (_directory, work) = scripted(&["when-env:GAMMA_ACTIVE|exit:101", "sleep:30000"]);
let binary = crate::testing::helper();
let verdict = confirm_enumeration(
&work,
&binary,
Attempt {
active: Some(7),
timeout: Some(Duration::from_millis(200)),
stall: Stall::NONE,
request: MemoryRequest::default(),
only: Only::All,
census: None,
},
"nextest could not list the tests".to_owned(),
);
assert_eq!(verdict, Verdict::Flaky(None), "{verdict:?}");
}
#[test]
fn a_test_that_fails_without_the_mutant_too_is_not_a_kill() {
let (_directory, work) = scripted(&["print:running 1 test", "print:test a::b ... FAILED", "exit:101"]);
let flaky = crate::testing::helper();
assert_eq!(
run_binary(
&work,
&flaky,
Attempt {
active: Some(7),
timeout: Some(Duration::from_secs(30)),
stall: Stall::NONE,
request: MemoryRequest::default(),
only: Only::All,
census: None,
},
true
),
Verdict::Flaky(Some("a::b".to_owned()))
);
}
#[test]
fn a_run_that_declines_to_confirm_believes_the_failing_test() {
let (_directory, work) = scripted(&["print:running 1 test", "print:test a::b ... FAILED", "exit:101"]);
let flaky = crate::testing::helper();
assert_eq!(
run_binary(
&work,
&flaky,
Attempt {
active: Some(7),
timeout: Some(Duration::from_secs(30)),
stall: Stall::NONE,
request: MemoryRequest::default(),
only: Only::All,
census: None,
},
false
),
Verdict::Failed(Some("a::b".to_owned()))
);
}
#[test]
fn a_failure_with_no_mutant_active_is_reported_as_it_stands() {
let (_directory, work) = scripted(&["print:test a::b ... FAILED", "exit:101"]);
let failing = crate::testing::helper();
assert_eq!(
run_binary(
&work,
&failing,
Attempt {
active: None,
timeout: Some(Duration::from_secs(30)),
stall: Stall::NONE,
request: MemoryRequest::default(),
only: Only::All,
census: None,
},
true
),
Verdict::Failed(Some("a::b".to_owned()))
);
}
#[test]
fn the_tail_keeps_the_last_lines() {
assert_eq!(tail("a\nb\nc\nd", 2), "c\nd");
assert_eq!(tail("a\nb", 10), "a\nb");
assert_eq!(tail("", 3), "");
}
#[test]
fn the_first_libtest_failure_name_is_extracted() {
let output = "running 2 tests\n\
test passing ... ok\n\
test module::case ... FAILED\n\
test later ... FAILED\n";
assert_eq!(first_failure(output), Some("module::case"));
assert_eq!(first_failure("custom harness output"), None);
}
#[test]
fn nextest_convicts_a_mutant_only_on_a_real_test_failure() {
let output = b" FAIL [ 0.024s] (1/2) nxspike tests::fails_when_asked\n";
let (verdict, _usage) = settle(true, Some(7), Some(nextest::TEST_RUN_FAILED), output, MemoryUsage::default());
assert_eq!(verdict, Verdict::Failed(Some("tests::fails_when_asked".to_owned())));
}
#[test]
fn nextest_test_enumeration_failure_is_suspect_only_with_a_mutant_active() {
let output = b"error: test binary exited while listing tests";
let (suspect, _usage) = settle(
true,
Some(7),
Some(nextest::TEST_LIST_CREATION_FAILED),
output,
MemoryUsage::default(),
);
let (baseline, _usage) = settle(true, None, Some(nextest::TEST_LIST_CREATION_FAILED), output, MemoryUsage::default());
assert_eq!(
suspect,
Verdict::TestEnumerationFailed("error: test binary exited while listing tests".to_owned())
);
assert!(
matches!(baseline, Verdict::Unmetered(ref reason) if reason.contains("code 104") && reason.contains("exited while listing")),
"{baseline:?}"
);
}
#[test]
fn nextest_matching_no_tests_abandons_the_run_rather_than_scoring_it() {
let (verdict, _usage) = settle(true, Some(7), Some(nextest::NO_TESTS_RUN), b"", MemoryUsage::default());
assert!(matches!(verdict, Verdict::Unmetered(_)), "{verdict:?}");
}
#[test]
fn a_code_nextest_uses_for_its_own_failures_is_not_a_verdict() {
for code in [Some(94), Some(101), None] {
let (verdict, _usage) = settle(true, Some(7), code, b"", MemoryUsage::default());
assert!(matches!(verdict, Verdict::Unmetered(_)), "{code:?} gave {verdict:?}");
}
}
#[test]
fn a_directly_run_binary_is_judged_by_its_output_and_not_its_code() {
let output = b"test module::case ... FAILED\n";
for code in [Some(nextest::NO_TESTS_RUN), Some(101), None] {
let (verdict, _usage) = settle(false, Some(7), code, output, MemoryUsage::default());
assert_eq!(verdict, Verdict::Failed(Some("module::case".to_owned())), "{code:?}");
}
}
#[test]
fn an_environment_acquisition_failure_never_convicts_a_mutant() {
let mut output = b"test module::case ... FAILED\n".to_vec();
output.extend_from_slice(gamma_rt::ENVIRONMENT_ERROR_MARKER);
for under_nextest in [false, true] {
let (verdict, _usage) = settle(
under_nextest,
Some(7),
Some(nextest::TEST_RUN_FAILED),
&output,
MemoryUsage::default(),
);
assert!(
matches!(verdict, Verdict::Unmetered(ref reason) if reason.contains("startup environment")),
"{verdict:?}"
);
}
}
#[test]
fn a_nextest_failure_waits_for_a_later_environment_error_marker() {
let progress = Mutex::new(Progress::new(Watch::Nextest));
progress
.lock()
.expect("the progress lock is not poisoned")
.heard(" FAIL [ 0.024s] (1/1) nxspike tests::case\n");
assert_eq!(announced_failure(&progress).as_deref(), Some("tests::case"));
assert_eq!(failure_to_cut_short(true, &progress), None);
assert!(matches!(unfinished_nextest_failure(true, &progress), Some(Verdict::Unmetered(_))));
let mut output = b" FAIL [ 0.024s] (1/1) nxspike tests::case\n".to_vec();
output.extend_from_slice(gamma_rt::ENVIRONMENT_ERROR_MARKER);
progress
.lock()
.expect("the progress lock is not poisoned")
.heard(core::str::from_utf8(gamma_rt::ENVIRONMENT_ERROR_MARKER).expect("the marker is ASCII"));
assert!(environment_failure(&progress));
assert_eq!(failure_to_cut_short(true, &progress), None);
let direct = Mutex::new(Progress::new(Watch::Libtest));
{
let mut state = direct.lock().expect("the progress lock is not poisoned");
state.heard("running 1 test\n");
state.heard("test tests::case ... FAILED\n");
}
assert_eq!(failure_to_cut_short(false, &direct).as_deref(), Some("tests::case"));
let (verdict, _usage) = settle(true, Some(7), Some(nextest::TEST_RUN_FAILED), &output, MemoryUsage::default());
assert!(
matches!(verdict, Verdict::Unmetered(ref reason) if reason.contains("startup environment")),
"{verdict:?}"
);
}
#[test]
fn a_run_through_nextest_is_watched_in_nextest_format() {
let (_scratch, mut work) = crate::testing::helper_workspace("watch-nextest", &["exit:0"]);
work.set_test_args(vec!["--nocapture".to_owned()]);
work.set_runner(nextest::Harness::fake(&[("/t/deps/nxspike-abc", "nxspike")]));
assert_eq!(watch(&work), Watch::Nextest);
}
#[test]
fn a_plain_libtest_run_is_watched_in_libtest_format() {
let (_scratch, mut work) = crate::testing::helper_workspace("watch-libtest", &["exit:0"]);
work.set_test_args(Vec::new());
assert_eq!(watch(&work), Watch::Libtest);
}
#[test]
fn interleaved_output_gives_up_watching_entirely() {
for argument in ["--nocapture", "--show-output"] {
let (_scratch, mut work) = crate::testing::helper_workspace("watch-off", &["exit:0"]);
work.set_test_args(vec![argument.to_owned()]);
assert_eq!(watch(&work), Watch::Off, "{argument}");
}
}
#[test]
fn a_binary_the_runner_does_not_know_yields_no_command() {
let (_scratch, mut work) = crate::testing::helper_workspace("launch-stranger", &["exit:0"]);
work.set_runner(nextest::Harness::fake(&[("/t/deps/nxspike-abc", "nxspike")]));
let reason = launcher(&work, &crate::testing::test_binary("/t/deps/stranger-def"), Only::All)
.expect_err("an unknown binary cannot be launched");
assert!(reason.contains("/t/deps/stranger-def"), "{reason}");
}
#[test]
fn a_run_that_cannot_be_launched_is_unmetered_rather_than_a_kill() {
let (_directory, mut work) = scripted(&["exit:0"]);
work.set_runner(nextest::Harness::fake(&[("/t/deps/nxspike-abc", "nxspike")]));
let verdict = run_with(
&work,
&crate::testing::test_binary("/t/deps/stranger-def"),
Attempt {
active: None,
timeout: Some(Duration::from_mins(1)),
stall: Stall { budget: None },
request: MemoryRequest { meter: false, limit: None },
only: Only::All,
census: None,
},
&Arc::new(Mutex::new(Progress::new(Watch::Nextest))),
)
.0;
assert!(
matches!(verdict, Verdict::Unmetered(ref reason) if reason.contains("stranger-def")),
"{verdict:?}"
);
}
#[test]
fn a_run_without_a_runner_invokes_the_binary_itself() {
let (_scratch, work) = crate::testing::helper_workspace("launch-direct", &["exit:0"]);
let binary = crate::testing::helper();
let command = launcher(&work, &binary, Only::All).expect("a direct launch needs nothing from a runner");
let args: Vec<_> = command.get_args().map(|arg| arg.to_string_lossy().into_owned()).collect();
assert_eq!(command.get_program(), binary.path.as_str());
assert_eq!(args, vec!["--gamma-step=exit:0"]);
}
#[test]
fn a_run_narrowed_to_one_test_asks_for_it_by_name_and_exactly() {
let (_scratch, work) = crate::testing::helper_workspace("launch-filtered", &["exit:0"]);
let binary = crate::testing::helper();
let command = launcher(&work, &binary, Only::One("tests::parses")).expect("a direct launch needs nothing from a runner");
let args: Vec<_> = command.get_args().map(|arg| arg.to_string_lossy().into_owned()).collect();
assert_eq!(args, vec!["--gamma-step=exit:0", "tests::parses", "--exact"]);
}
#[test]
fn a_run_narrowed_by_a_census_asks_for_all_of_its_tests_at_once() {
let (_scratch, work) = crate::testing::helper_workspace("launch-censused", &["exit:0"]);
let binary = crate::testing::helper();
let names = ["tests::parses", "tests::rejects"];
let command = launcher(&work, &binary, Only::These(&names)).expect("a direct launch needs nothing from a runner");
let args: Vec<_> = command.get_args().map(|arg| arg.to_string_lossy().into_owned()).collect();
assert_eq!(args, vec!["--gamma-step=exit:0", "tests::parses", "tests::rejects", "--exact"]);
}
#[test]
fn a_census_selection_with_no_names_runs_the_binary_whole() {
let (_scratch, work) = crate::testing::helper_workspace("launch-censused-empty", &["exit:0"]);
let binary = crate::testing::helper();
let command = launcher(&work, &binary, Only::These(&[])).expect("a direct launch needs nothing from a runner");
let args: Vec<_> = command.get_args().map(|arg| arg.to_string_lossy().into_owned()).collect();
assert_eq!(args, vec!["--gamma-step=exit:0"]);
}
#[test]
fn a_chosen_test_the_users_filter_excludes_is_refused_rather_than_run() {
let (_scratch, mut work) = crate::testing::helper_workspace("launch-excluded", &["exit:0"]);
let binary = crate::testing::helper();
work.set_test_args(vec!["allowed".to_owned()]);
let refusal = launcher(&work, &binary, Only::One("tests::excluded")).expect_err("the selection is empty");
assert!(refusal.contains("harness filters allow"), "{refusal}");
}
#[test]
fn a_chosen_test_the_users_filter_admits_is_run_alone_under_their_flags() {
let (_scratch, mut work) = crate::testing::helper_workspace("launch-admitted", &["exit:0"]);
let binary = crate::testing::helper();
work.set_test_args(vec!["--skip".to_owned(), "slow".to_owned(), "allowed".to_owned()]);
let command = launcher(&work, &binary, Only::One("tests::allowed_case")).expect("the selection is not empty");
let args: Vec<_> = command.get_args().map(|arg| arg.to_string_lossy().into_owned()).collect();
assert_eq!(args, vec!["--skip", "slow", "tests::allowed_case", "--exact"]);
}
#[test]
fn a_census_selection_keeps_only_the_tests_the_users_filter_admits() {
let (_scratch, mut work) = crate::testing::helper_workspace("launch-intersected", &["exit:0"]);
let binary = crate::testing::helper();
work.set_test_args(vec!["allowed".to_owned()]);
let names = ["tests::allowed_one", "tests::excluded", "tests::allowed_two"];
let command = launcher(&work, &binary, Only::These(&names)).expect("the selection is not empty");
let args: Vec<_> = command.get_args().map(|arg| arg.to_string_lossy().into_owned()).collect();
assert_eq!(args, vec!["tests::allowed_one", "tests::allowed_two", "--exact"]);
}
#[test]
fn a_wakeup_raised_before_the_wait_is_not_slept_through() {
let pulse = Pulse::default();
let seen = pulse.seen();
pulse.signal();
let started = Instant::now();
pulse.wait(seen, Duration::from_secs(30));
assert!(
started.elapsed() < Duration::from_secs(5),
"the wait slept through a signal it had already been sent"
);
}
#[test]
fn a_wakeup_raised_during_a_wait_ends_it() {
let pulse = Arc::new(Pulse::default());
let seen = pulse.seen();
let started = Instant::now();
thread::scope(|scope| {
let _signaller = scope.spawn(|| {
thread::sleep(Duration::from_millis(20));
pulse.signal();
});
pulse.wait(seen, Duration::from_secs(30));
});
assert!(
started.elapsed() < Duration::from_secs(5),
"the wait ran to its timeout despite being signalled"
);
}
#[cfg_attr(
any(coverage_nightly, windows),
ignore = "coverage instrumentation or Windows process startup makes this wall-clock assertion meaningless"
)]
#[test]
fn a_binary_that_exits_at_once_is_not_waited_on() {
let (_directory, work) = scripted(&["exit:0"]);
let quick = crate::testing::helper();
let launches = 20;
let started = Instant::now();
for _ in 0..launches {
let verdict = run_binary(
&work,
&quick,
Attempt {
active: None,
timeout: Some(Duration::from_mins(1)),
stall: Stall::NONE,
request: MemoryRequest::default(),
only: Only::All,
census: None,
},
false,
);
assert!(matches!(verdict, Verdict::Passed), "{verdict:?}");
}
assert!(
started.elapsed() < WAIT_CAP * launches,
"{launches} instant launches took {:?}, which is at least a full wait each",
started.elapsed()
);
}
#[test]
fn a_readers_gauge_counts_a_reader_for_as_long_as_it_runs() {
let gauge = Readers::new();
assert_eq!(gauge.live(), 0);
assert_eq!(gauge.peak(), 0);
gauge.started();
gauge.started();
assert_eq!(gauge.live(), 2);
assert_eq!(gauge.peak(), 2);
gauge.finished();
assert_eq!(gauge.live(), 1, "a finished reader is still counted as running");
assert_eq!(gauge.peak(), 2, "the peak fell back to the live count");
gauge.started();
gauge.finished();
gauge.finished();
assert_eq!(gauge.live(), 0);
assert_eq!(gauge.peak(), 2, "the peak forgot a reader that has since finished");
}
#[test]
fn an_ordinary_binary_leaves_no_reader_behind() {
let before = READERS.live();
let (_directory, work) = scripted(&["print:done", "exit:0"]);
let quick = crate::testing::helper();
let verdict = run_binary(
&work,
&quick,
Attempt {
active: None,
timeout: Some(Duration::from_mins(1)),
stall: Stall::NONE,
request: MemoryRequest::default(),
only: Only::All,
census: None,
},
false,
);
assert!(matches!(verdict, Verdict::Passed), "{verdict:?}");
assert!(READERS.peak() > 0, "the run started no readers at all");
for _attempt in 0..200 {
if READERS.live() <= before {
return;
}
thread::sleep(Duration::from_millis(10));
}
assert!(READERS.live() <= before, "a reader outlived the binary it was reading");
}
}