use super::{FinalStatusLevel, StatusLevel, TestOutputDisplay};
use crate::{
config::{elements::LeakTimeoutResult, scripts::ScriptId},
list::{TestInstance, TestInstanceId, TestList},
test_output::ChildExecutionOutput,
};
use chrono::{DateTime, FixedOffset};
use nextest_metadata::MismatchReason;
use quick_junit::ReportUuid;
use std::{collections::BTreeMap, fmt, process::ExitStatus, time::Duration};
#[derive(Clone, Debug)]
pub struct TestEvent<'a> {
pub timestamp: DateTime<FixedOffset>,
pub elapsed: Duration,
pub kind: TestEventKind<'a>,
}
#[derive(Clone, Debug)]
pub enum TestEventKind<'a> {
RunStarted {
test_list: &'a TestList<'a>,
run_id: ReportUuid,
profile_name: String,
cli_args: Vec<String>,
},
SetupScriptStarted {
index: usize,
total: usize,
script_id: ScriptId,
program: String,
args: &'a [String],
no_capture: bool,
},
SetupScriptSlow {
script_id: ScriptId,
program: String,
args: &'a [String],
elapsed: Duration,
will_terminate: bool,
},
SetupScriptFinished {
index: usize,
total: usize,
script_id: ScriptId,
program: String,
args: &'a [String],
junit_store_success_output: bool,
junit_store_failure_output: bool,
no_capture: bool,
run_status: SetupScriptExecuteStatus,
},
TestStarted {
test_instance: TestInstance<'a>,
current_stats: RunStats,
running: usize,
cancel_state: Option<CancelReason>,
},
TestSlow {
test_instance: TestInstance<'a>,
retry_data: RetryData,
elapsed: Duration,
will_terminate: bool,
},
TestAttemptFailedWillRetry {
test_instance: TestInstance<'a>,
run_status: ExecuteStatus,
delay_before_next_attempt: Duration,
failure_output: TestOutputDisplay,
},
TestRetryStarted {
test_instance: TestInstance<'a>,
retry_data: RetryData,
},
TestFinished {
test_instance: TestInstance<'a>,
success_output: TestOutputDisplay,
failure_output: TestOutputDisplay,
junit_store_success_output: bool,
junit_store_failure_output: bool,
run_statuses: ExecutionStatuses,
current_stats: RunStats,
running: usize,
cancel_state: Option<CancelReason>,
},
TestSkipped {
test_instance: TestInstance<'a>,
reason: MismatchReason,
},
InfoStarted {
total: usize,
run_stats: RunStats,
},
InfoResponse {
index: usize,
total: usize,
response: InfoResponse<'a>,
},
InfoFinished {
missing: usize,
},
InputEnter {
current_stats: RunStats,
running: usize,
cancel_reason: Option<CancelReason>,
},
RunBeginCancel {
setup_scripts_running: usize,
current_stats: RunStats,
running: usize,
reason: CancelReason,
},
RunBeginKill {
setup_scripts_running: usize,
current_stats: RunStats,
running: usize,
reason: CancelReason,
},
RunPaused {
setup_scripts_running: usize,
running: usize,
},
RunContinued {
setup_scripts_running: usize,
running: usize,
},
RunFinished {
run_id: ReportUuid,
start_time: DateTime<FixedOffset>,
elapsed: Duration,
run_stats: RunStats,
},
}
#[derive(Copy, Clone, Default, Debug, Eq, PartialEq)]
pub struct RunStats {
pub initial_run_count: usize,
pub finished_count: usize,
pub setup_scripts_initial_count: usize,
pub setup_scripts_finished_count: usize,
pub setup_scripts_passed: usize,
pub setup_scripts_failed: usize,
pub setup_scripts_exec_failed: usize,
pub setup_scripts_timed_out: usize,
pub passed: usize,
pub passed_slow: usize,
pub flaky: usize,
pub failed: usize,
pub failed_slow: usize,
pub timed_out: usize,
pub leaky: usize,
pub leaky_failed: usize,
pub exec_failed: usize,
pub skipped: usize,
}
impl RunStats {
pub fn has_failures(&self) -> bool {
self.failed_setup_script_count() > 0 || self.failed_count() > 0
}
pub fn failed_setup_script_count(&self) -> usize {
self.setup_scripts_failed + self.setup_scripts_exec_failed + self.setup_scripts_timed_out
}
pub fn failed_count(&self) -> usize {
self.failed + self.exec_failed + self.timed_out
}
pub fn summarize_final(&self) -> FinalRunStats {
if self.failed_setup_script_count() > 0 {
FinalRunStats::Failed(RunStatsFailureKind::SetupScript)
} else if self.setup_scripts_initial_count > self.setup_scripts_finished_count {
FinalRunStats::Cancelled(RunStatsFailureKind::SetupScript)
} else if self.failed_count() > 0 {
FinalRunStats::Failed(RunStatsFailureKind::Test {
initial_run_count: self.initial_run_count,
not_run: self.initial_run_count.saturating_sub(self.finished_count),
})
} else if self.initial_run_count > self.finished_count {
FinalRunStats::Cancelled(RunStatsFailureKind::Test {
initial_run_count: self.initial_run_count,
not_run: self.initial_run_count.saturating_sub(self.finished_count),
})
} else if self.finished_count == 0 {
FinalRunStats::NoTestsRun
} else {
FinalRunStats::Success
}
}
pub(crate) fn on_setup_script_finished(&mut self, status: &SetupScriptExecuteStatus) {
self.setup_scripts_finished_count += 1;
match status.result {
ExecutionResult::Pass
| ExecutionResult::Leak {
result: LeakTimeoutResult::Pass,
} => {
self.setup_scripts_passed += 1;
}
ExecutionResult::Fail { .. }
| ExecutionResult::Leak {
result: LeakTimeoutResult::Fail,
} => {
self.setup_scripts_failed += 1;
}
ExecutionResult::ExecFail => {
self.setup_scripts_exec_failed += 1;
}
ExecutionResult::Timeout => {
self.setup_scripts_timed_out += 1;
}
}
}
pub(crate) fn on_test_finished(&mut self, run_statuses: &ExecutionStatuses) {
self.finished_count += 1;
let last_status = run_statuses.last_status();
match last_status.result {
ExecutionResult::Pass => {
self.passed += 1;
if last_status.is_slow {
self.passed_slow += 1;
}
if run_statuses.len() > 1 {
self.flaky += 1;
}
}
ExecutionResult::Leak {
result: LeakTimeoutResult::Pass,
} => {
self.passed += 1;
self.leaky += 1;
if last_status.is_slow {
self.passed_slow += 1;
}
if run_statuses.len() > 1 {
self.flaky += 1;
}
}
ExecutionResult::Leak {
result: LeakTimeoutResult::Fail,
} => {
self.failed += 1;
self.leaky_failed += 1;
if last_status.is_slow {
self.failed_slow += 1;
}
}
ExecutionResult::Fail { .. } => {
self.failed += 1;
if last_status.is_slow {
self.failed_slow += 1;
}
}
ExecutionResult::Timeout => self.timed_out += 1,
ExecutionResult::ExecFail => self.exec_failed += 1,
}
}
}
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub enum FinalRunStats {
Success,
NoTestsRun,
Cancelled(RunStatsFailureKind),
Failed(RunStatsFailureKind),
}
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub enum RunStatsFailureKind {
SetupScript,
Test {
initial_run_count: usize,
not_run: usize,
},
}
#[derive(Clone, Debug)]
pub struct ExecutionStatuses {
statuses: Vec<ExecuteStatus>,
}
#[expect(clippy::len_without_is_empty)] impl ExecutionStatuses {
pub(crate) fn new(statuses: Vec<ExecuteStatus>) -> Self {
Self { statuses }
}
pub fn last_status(&self) -> &ExecuteStatus {
self.statuses
.last()
.expect("execution statuses is non-empty")
}
pub fn iter(&self) -> impl DoubleEndedIterator<Item = &'_ ExecuteStatus> + '_ {
self.statuses.iter()
}
pub fn len(&self) -> usize {
self.statuses.len()
}
pub fn describe(&self) -> ExecutionDescription<'_> {
let last_status = self.last_status();
if last_status.result.is_success() {
if self.statuses.len() > 1 {
ExecutionDescription::Flaky {
last_status,
prior_statuses: &self.statuses[..self.statuses.len() - 1],
}
} else {
ExecutionDescription::Success {
single_status: last_status,
}
}
} else {
let first_status = self
.statuses
.first()
.expect("execution statuses is non-empty");
let retries = &self.statuses[1..];
ExecutionDescription::Failure {
first_status,
last_status,
retries,
}
}
}
}
#[derive(Copy, Clone, Debug)]
pub enum ExecutionDescription<'a> {
Success {
single_status: &'a ExecuteStatus,
},
Flaky {
last_status: &'a ExecuteStatus,
prior_statuses: &'a [ExecuteStatus],
},
Failure {
first_status: &'a ExecuteStatus,
last_status: &'a ExecuteStatus,
retries: &'a [ExecuteStatus],
},
}
impl<'a> ExecutionDescription<'a> {
pub fn status_level(&self) -> StatusLevel {
match self {
ExecutionDescription::Success { single_status } => match single_status.result {
ExecutionResult::Leak {
result: LeakTimeoutResult::Pass,
} => StatusLevel::Leak,
ExecutionResult::Pass => StatusLevel::Pass,
other => unreachable!("Success only permits Pass or Leak Pass, found {other:?}"),
},
ExecutionDescription::Flaky { .. } => StatusLevel::Retry,
ExecutionDescription::Failure { .. } => StatusLevel::Fail,
}
}
pub fn final_status_level(&self) -> FinalStatusLevel {
match self {
ExecutionDescription::Success { single_status, .. } => {
if single_status.is_slow {
FinalStatusLevel::Slow
} else {
match single_status.result {
ExecutionResult::Pass => FinalStatusLevel::Pass,
ExecutionResult::Leak {
result: LeakTimeoutResult::Pass,
} => FinalStatusLevel::Leak,
other => {
unreachable!("Success only permits Pass or Leak Pass, found {other:?}")
}
}
}
}
ExecutionDescription::Flaky { .. } => FinalStatusLevel::Flaky,
ExecutionDescription::Failure { .. } => FinalStatusLevel::Fail,
}
}
pub fn last_status(&self) -> &'a ExecuteStatus {
match self {
ExecutionDescription::Success {
single_status: last_status,
}
| ExecutionDescription::Flaky { last_status, .. }
| ExecutionDescription::Failure { last_status, .. } => last_status,
}
}
}
#[derive(Clone, Debug)]
pub struct ExecuteStatus {
pub retry_data: RetryData,
pub output: ChildExecutionOutput,
pub result: ExecutionResult,
pub start_time: DateTime<FixedOffset>,
pub time_taken: Duration,
pub is_slow: bool,
pub delay_before_start: Duration,
}
#[derive(Clone, Debug)]
pub struct SetupScriptExecuteStatus {
pub output: ChildExecutionOutput,
pub result: ExecutionResult,
pub start_time: DateTime<FixedOffset>,
pub time_taken: Duration,
pub is_slow: bool,
pub env_map: Option<SetupScriptEnvMap>,
}
#[derive(Clone, Debug)]
pub struct SetupScriptEnvMap {
pub env_map: BTreeMap<String, String>,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, PartialOrd, Ord)]
pub struct RetryData {
pub attempt: usize,
pub total_attempts: usize,
}
impl RetryData {
pub fn is_last_attempt(&self) -> bool {
self.attempt >= self.total_attempts
}
}
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub enum ExecutionResult {
Pass,
Leak {
result: LeakTimeoutResult,
},
Fail {
failure_status: FailureStatus,
leaked: bool,
},
ExecFail,
Timeout,
}
impl ExecutionResult {
pub fn is_success(self) -> bool {
match self {
ExecutionResult::Pass
| ExecutionResult::Leak {
result: LeakTimeoutResult::Pass,
} => true,
ExecutionResult::Leak {
result: LeakTimeoutResult::Fail,
}
| ExecutionResult::Fail { .. }
| ExecutionResult::ExecFail
| ExecutionResult::Timeout => false,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum FailureStatus {
ExitCode(i32),
Abort(AbortStatus),
}
impl FailureStatus {
pub fn extract(exit_status: ExitStatus) -> Self {
if let Some(abort_status) = AbortStatus::extract(exit_status) {
FailureStatus::Abort(abort_status)
} else {
FailureStatus::ExitCode(
exit_status
.code()
.expect("if abort_status is None, then code must be present"),
)
}
}
}
#[derive(Copy, Clone, Eq, PartialEq)]
pub enum AbortStatus {
#[cfg(unix)]
UnixSignal(i32),
#[cfg(windows)]
WindowsNtStatus(windows_sys::Win32::Foundation::NTSTATUS),
#[cfg(windows)]
JobObject,
}
impl AbortStatus {
pub fn extract(exit_status: ExitStatus) -> Option<Self> {
cfg_if::cfg_if! {
if #[cfg(unix)] {
use std::os::unix::process::ExitStatusExt;
exit_status.signal().map(AbortStatus::UnixSignal)
} else if #[cfg(windows)] {
exit_status.code().and_then(|code| {
(code < 0).then_some(AbortStatus::WindowsNtStatus(code))
})
} else {
None
}
}
}
}
impl fmt::Debug for AbortStatus {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
#[cfg(unix)]
AbortStatus::UnixSignal(signal) => write!(f, "UnixSignal({signal})"),
#[cfg(windows)]
AbortStatus::WindowsNtStatus(status) => write!(f, "WindowsNtStatus({status:x})"),
#[cfg(windows)]
AbortStatus::JobObject => write!(f, "JobObject"),
}
}
}
#[derive(Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd)]
#[cfg_attr(test, derive(test_strategy::Arbitrary))]
pub enum CancelReason {
SetupScriptFailure,
TestFailure,
ReportError,
GlobalTimeout,
Signal,
Interrupt,
SecondSignal,
}
impl CancelReason {
pub(crate) fn to_static_str(self) -> &'static str {
match self {
CancelReason::SetupScriptFailure => "setup script failure",
CancelReason::TestFailure => "test failure",
CancelReason::ReportError => "reporting error",
CancelReason::GlobalTimeout => "global timeout",
CancelReason::Signal => "signal",
CancelReason::Interrupt => "interrupt",
CancelReason::SecondSignal => "second signal",
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum UnitKind {
Test,
Script,
}
impl UnitKind {
pub(crate) const WAITING_ON_TEST_MESSAGE: &str = "waiting on test process";
pub(crate) const WAITING_ON_SCRIPT_MESSAGE: &str = "waiting on script process";
pub(crate) const EXECUTING_TEST_MESSAGE: &str = "executing test";
pub(crate) const EXECUTING_SCRIPT_MESSAGE: &str = "executing script";
pub(crate) fn waiting_on_message(&self) -> &'static str {
match self {
UnitKind::Test => Self::WAITING_ON_TEST_MESSAGE,
UnitKind::Script => Self::WAITING_ON_SCRIPT_MESSAGE,
}
}
pub(crate) fn executing_message(&self) -> &'static str {
match self {
UnitKind::Test => Self::EXECUTING_TEST_MESSAGE,
UnitKind::Script => Self::EXECUTING_SCRIPT_MESSAGE,
}
}
}
impl fmt::Display for UnitKind {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
UnitKind::Script => write!(f, "script"),
UnitKind::Test => write!(f, "test"),
}
}
}
#[derive(Clone, Debug)]
pub enum InfoResponse<'a> {
SetupScript(SetupScriptInfoResponse<'a>),
Test(TestInfoResponse<'a>),
}
#[derive(Clone, Debug)]
pub struct SetupScriptInfoResponse<'a> {
pub script_id: ScriptId,
pub program: String,
pub args: &'a [String],
pub state: UnitState,
pub output: ChildExecutionOutput,
}
#[derive(Clone, Debug)]
pub struct TestInfoResponse<'a> {
pub test_instance: TestInstanceId<'a>,
pub retry_data: RetryData,
pub state: UnitState,
pub output: ChildExecutionOutput,
}
#[derive(Clone, Debug)]
pub enum UnitState {
Running {
pid: u32,
time_taken: Duration,
slow_after: Option<Duration>,
},
Exiting {
pid: u32,
time_taken: Duration,
slow_after: Option<Duration>,
tentative_result: Option<ExecutionResult>,
waiting_duration: Duration,
remaining: Duration,
},
Terminating(UnitTerminatingState),
Exited {
result: ExecutionResult,
time_taken: Duration,
slow_after: Option<Duration>,
},
DelayBeforeNextAttempt {
previous_result: ExecutionResult,
previous_slow: bool,
waiting_duration: Duration,
remaining: Duration,
},
}
impl UnitState {
pub fn has_valid_output(&self) -> bool {
match self {
UnitState::Running { .. }
| UnitState::Exiting { .. }
| UnitState::Terminating(_)
| UnitState::Exited { .. } => true,
UnitState::DelayBeforeNextAttempt { .. } => false,
}
}
}
#[derive(Clone, Debug)]
pub struct UnitTerminatingState {
pub pid: u32,
pub time_taken: Duration,
pub reason: UnitTerminateReason,
pub method: UnitTerminateMethod,
pub waiting_duration: Duration,
pub remaining: Duration,
}
#[derive(Clone, Copy, Debug)]
pub enum UnitTerminateReason {
Timeout,
Signal,
Interrupt,
}
impl fmt::Display for UnitTerminateReason {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
UnitTerminateReason::Timeout => write!(f, "timeout"),
UnitTerminateReason::Signal => write!(f, "signal"),
UnitTerminateReason::Interrupt => write!(f, "interrupt"),
}
}
}
#[derive(Clone, Copy, Debug)]
pub enum UnitTerminateMethod {
#[cfg(unix)]
Signal(UnitTerminateSignal),
#[cfg(windows)]
JobObject,
#[cfg(windows)]
Wait,
#[cfg(test)]
Fake,
}
#[cfg(unix)]
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum UnitTerminateSignal {
Interrupt,
Term,
Hangup,
Quit,
Kill,
}
#[cfg(unix)]
impl fmt::Display for UnitTerminateSignal {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
UnitTerminateSignal::Interrupt => write!(f, "SIGINT"),
UnitTerminateSignal::Term => write!(f, "SIGTERM"),
UnitTerminateSignal::Hangup => write!(f, "SIGHUP"),
UnitTerminateSignal::Quit => write!(f, "SIGQUIT"),
UnitTerminateSignal::Kill => write!(f, "SIGKILL"),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_is_success() {
assert_eq!(
RunStats::default().summarize_final(),
FinalRunStats::NoTestsRun,
"empty run => no tests run"
);
assert_eq!(
RunStats {
initial_run_count: 42,
finished_count: 42,
..RunStats::default()
}
.summarize_final(),
FinalRunStats::Success,
"initial run count = final run count => success"
);
assert_eq!(
RunStats {
initial_run_count: 42,
finished_count: 41,
..RunStats::default()
}
.summarize_final(),
FinalRunStats::Cancelled(RunStatsFailureKind::Test {
initial_run_count: 42,
not_run: 1
}),
"initial run count > final run count => cancelled"
);
assert_eq!(
RunStats {
initial_run_count: 42,
finished_count: 42,
failed: 1,
..RunStats::default()
}
.summarize_final(),
FinalRunStats::Failed(RunStatsFailureKind::Test {
initial_run_count: 42,
not_run: 0
}),
"failed => failure"
);
assert_eq!(
RunStats {
initial_run_count: 42,
finished_count: 42,
exec_failed: 1,
..RunStats::default()
}
.summarize_final(),
FinalRunStats::Failed(RunStatsFailureKind::Test {
initial_run_count: 42,
not_run: 0
}),
"exec failed => failure"
);
assert_eq!(
RunStats {
initial_run_count: 42,
finished_count: 42,
timed_out: 1,
..RunStats::default()
}
.summarize_final(),
FinalRunStats::Failed(RunStatsFailureKind::Test {
initial_run_count: 42,
not_run: 0
}),
"timed out => failure"
);
assert_eq!(
RunStats {
initial_run_count: 42,
finished_count: 42,
skipped: 1,
..RunStats::default()
}
.summarize_final(),
FinalRunStats::Success,
"skipped => not considered a failure"
);
assert_eq!(
RunStats {
setup_scripts_initial_count: 2,
setup_scripts_finished_count: 1,
..RunStats::default()
}
.summarize_final(),
FinalRunStats::Cancelled(RunStatsFailureKind::SetupScript),
"setup script failed => failure"
);
assert_eq!(
RunStats {
setup_scripts_initial_count: 2,
setup_scripts_finished_count: 2,
setup_scripts_failed: 1,
..RunStats::default()
}
.summarize_final(),
FinalRunStats::Failed(RunStatsFailureKind::SetupScript),
"setup script failed => failure"
);
assert_eq!(
RunStats {
setup_scripts_initial_count: 2,
setup_scripts_finished_count: 2,
setup_scripts_exec_failed: 1,
..RunStats::default()
}
.summarize_final(),
FinalRunStats::Failed(RunStatsFailureKind::SetupScript),
"setup script exec failed => failure"
);
assert_eq!(
RunStats {
setup_scripts_initial_count: 2,
setup_scripts_finished_count: 2,
setup_scripts_timed_out: 1,
..RunStats::default()
}
.summarize_final(),
FinalRunStats::Failed(RunStatsFailureKind::SetupScript),
"setup script timed out => failure"
);
assert_eq!(
RunStats {
setup_scripts_initial_count: 2,
setup_scripts_finished_count: 2,
setup_scripts_passed: 2,
..RunStats::default()
}
.summarize_final(),
FinalRunStats::NoTestsRun,
"setup scripts passed => success, but no tests run"
);
}
}