use super::{FinalStatusLevel, StatusLevel, TestOutputDisplay};
#[cfg(test)]
use crate::output_spec::ArbitraryOutputSpec;
use crate::{
config::{
elements::{
FlakyResult, JunitFlakyFailStatus, LeakTimeoutResult, SlowTimeoutResult, TestGroup,
},
scripts::ScriptId,
},
errors::{ChildError, ChildFdError, ChildStartError, ErrorList},
list::{OwnedTestInstanceId, TestInstanceId, TestList},
output_spec::{LiveSpec, OutputSpec, SerializableOutputSpec},
runner::{StressCondition, StressCount},
test_output::{ChildExecutionOutput, ChildOutput, ChildSingleOutput},
};
use chrono::{DateTime, FixedOffset};
use nextest_metadata::MismatchReason;
use quick_junit::ReportUuid;
use serde::{Deserialize, Serialize};
use smol_str::SmolStr;
use std::{
collections::BTreeMap, ffi::c_int, fmt, num::NonZero, process::ExitStatus, time::Duration,
};
pub const SIGTERM: c_int = 15;
#[derive(Clone, Debug)]
pub enum ReporterEvent<'a> {
Tick,
Test(Box<TestEvent<'a>>),
}
#[derive(Clone, Debug)]
pub struct TestEvent<'a> {
pub timestamp: DateTime<FixedOffset>,
pub elapsed: Duration,
pub kind: TestEventKind<'a>,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub struct TestSlotAssignment {
pub global_slot: u64,
pub group_slot: Option<u64>,
pub test_group: TestGroup,
}
#[derive(Clone, Debug)]
pub enum TestEventKind<'a> {
RunStarted {
test_list: &'a TestList<'a>,
run_id: ReportUuid,
profile_name: String,
cli_args: Vec<String>,
stress_condition: Option<StressCondition>,
},
StressSubRunStarted {
progress: StressProgress,
},
SetupScriptStarted {
stress_index: Option<StressIndex>,
index: usize,
total: usize,
script_id: ScriptId,
program: String,
args: Vec<String>,
no_capture: bool,
},
SetupScriptSlow {
stress_index: Option<StressIndex>,
script_id: ScriptId,
program: String,
args: Vec<String>,
elapsed: Duration,
will_terminate: bool,
},
SetupScriptFinished {
stress_index: Option<StressIndex>,
index: usize,
total: usize,
script_id: ScriptId,
program: String,
args: Vec<String>,
junit_store_success_output: bool,
junit_store_failure_output: bool,
no_capture: bool,
run_status: SetupScriptExecuteStatus<LiveSpec>,
},
TestStarted {
stress_index: Option<StressIndex>,
test_instance: TestInstanceId<'a>,
slot_assignment: TestSlotAssignment,
current_stats: RunStats,
running: usize,
command_line: Vec<String>,
},
TestSlow {
stress_index: Option<StressIndex>,
test_instance: TestInstanceId<'a>,
retry_data: RetryData,
elapsed: Duration,
will_terminate: bool,
},
TestAttemptFailedWillRetry {
stress_index: Option<StressIndex>,
test_instance: TestInstanceId<'a>,
run_status: ExecuteStatus<LiveSpec>,
delay_before_next_attempt: Duration,
failure_output: TestOutputDisplay,
running: usize,
},
TestRetryStarted {
stress_index: Option<StressIndex>,
test_instance: TestInstanceId<'a>,
slot_assignment: TestSlotAssignment,
retry_data: RetryData,
running: usize,
command_line: Vec<String>,
},
TestFinished {
stress_index: Option<StressIndex>,
test_instance: TestInstanceId<'a>,
success_output: TestOutputDisplay,
failure_output: TestOutputDisplay,
junit_store_success_output: bool,
junit_store_failure_output: bool,
junit_flaky_fail_status: JunitFlakyFailStatus,
run_statuses: ExecutionStatuses<LiveSpec>,
current_stats: RunStats,
running: usize,
},
TestSkipped {
stress_index: Option<StressIndex>,
test_instance: TestInstanceId<'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,
},
RunBeginCancel {
setup_scripts_running: usize,
current_stats: RunStats,
running: usize,
},
RunBeginKill {
setup_scripts_running: usize,
current_stats: RunStats,
running: usize,
},
RunPaused {
setup_scripts_running: usize,
running: usize,
},
RunContinued {
setup_scripts_running: usize,
running: usize,
},
StressSubRunFinished {
progress: StressProgress,
sub_elapsed: Duration,
sub_stats: RunStats,
},
RunFinished {
run_id: ReportUuid,
start_time: DateTime<FixedOffset>,
elapsed: Duration,
run_stats: RunFinishedStats,
outstanding_not_seen: Option<TestsNotSeen>,
},
}
#[derive(Clone, Debug)]
pub struct TestsNotSeen {
pub not_seen: Vec<OwnedTestInstanceId>,
pub total_not_seen: usize,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "progress-type", rename_all = "kebab-case")]
#[cfg_attr(test, derive(test_strategy::Arbitrary))]
pub enum StressProgress {
Count {
total: StressCount,
elapsed: Duration,
completed: u32,
},
Time {
total: Duration,
elapsed: Duration,
completed: u32,
},
}
impl StressProgress {
pub fn remaining(&self) -> Option<StressRemaining> {
match self {
Self::Count {
total: StressCount::Count { count },
elapsed: _,
completed,
} => count
.get()
.checked_sub(*completed)
.and_then(|remaining| NonZero::try_from(remaining).ok())
.map(StressRemaining::Count),
Self::Count {
total: StressCount::Infinite,
..
} => Some(StressRemaining::Infinite),
Self::Time {
total,
elapsed,
completed: _,
} => total.checked_sub(*elapsed).map(StressRemaining::Time),
}
}
pub fn unique_id(&self, run_id: ReportUuid) -> String {
let stress_current = match self {
Self::Count { completed, .. } | Self::Time { completed, .. } => *completed,
};
format!("{}:@stress-{}", run_id, stress_current)
}
}
#[derive(Clone, Debug)]
pub enum StressRemaining {
Count(NonZero<u32>),
Infinite,
Time(Duration),
}
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
#[cfg_attr(test, derive(test_strategy::Arbitrary))]
pub struct StressIndex {
pub current: u32,
pub total: Option<NonZero<u32>>,
}
impl StressIndex {
pub fn total_get(&self) -> Option<u32> {
self.total.map(|t| t.get())
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "kebab-case")]
#[cfg_attr(test, derive(test_strategy::Arbitrary))]
pub enum RunFinishedStats {
Single(RunStats),
Stress(StressRunStats),
}
impl RunFinishedStats {
pub fn final_stats(&self) -> FinalRunStats {
match self {
Self::Single(stats) => stats.summarize_final(),
Self::Stress(stats) => stats.last_final_stats,
}
}
}
#[derive(Copy, Clone, Default, Debug, Eq, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
#[cfg_attr(test, derive(test_strategy::Arbitrary))]
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 passed_timed_out: usize,
pub flaky: usize,
pub failed: usize,
pub failed_slow: usize,
pub failed_timed_out: usize,
pub leaky: usize,
pub leaky_failed: usize,
pub exec_failed: usize,
pub skipped: usize,
pub cancel_reason: Option<CancelReason>,
}
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.failed_timed_out
}
pub fn summarize_final(&self) -> FinalRunStats {
if self.failed_setup_script_count() > 0 {
if self.cancel_reason > Some(CancelReason::TestFailure) {
FinalRunStats::Cancelled {
reason: self.cancel_reason,
kind: RunStatsFailureKind::SetupScript,
}
} else {
FinalRunStats::Failed {
kind: RunStatsFailureKind::SetupScript,
}
}
} else if self.setup_scripts_initial_count > self.setup_scripts_finished_count {
FinalRunStats::Cancelled {
reason: self.cancel_reason,
kind: RunStatsFailureKind::SetupScript,
}
} else if self.failed_count() > 0 {
let kind = RunStatsFailureKind::Test {
initial_run_count: self.initial_run_count,
not_run: self.initial_run_count.saturating_sub(self.finished_count),
};
if self.cancel_reason > Some(CancelReason::TestFailure) {
FinalRunStats::Cancelled {
reason: self.cancel_reason,
kind,
}
} else {
FinalRunStats::Failed { kind }
}
} else if self.initial_run_count > self.finished_count {
FinalRunStats::Cancelled {
reason: self.cancel_reason,
kind: 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<LiveSpec>) {
self.setup_scripts_finished_count += 1;
match status.result {
ExecutionResultDescription::Pass
| ExecutionResultDescription::Leak {
result: LeakTimeoutResult::Pass,
} => {
self.setup_scripts_passed += 1;
}
ExecutionResultDescription::Fail { .. }
| ExecutionResultDescription::Leak {
result: LeakTimeoutResult::Fail,
} => {
self.setup_scripts_failed += 1;
}
ExecutionResultDescription::ExecFail => {
self.setup_scripts_exec_failed += 1;
}
ExecutionResultDescription::Timeout { .. } => {
self.setup_scripts_timed_out += 1;
}
}
}
pub(crate) fn on_test_finished(&mut self, run_statuses: &ExecutionStatuses<LiveSpec>) {
self.finished_count += 1;
let last_status = run_statuses.last_status();
match last_status.result {
ExecutionResultDescription::Pass => {
let is_flaky = run_statuses.len() > 1;
if is_flaky {
match run_statuses.flaky_result() {
FlakyResult::Fail => {
self.failed += 1;
if last_status.is_slow {
self.failed_slow += 1;
}
}
FlakyResult::Pass => {
self.passed += 1;
if last_status.is_slow {
self.passed_slow += 1;
}
self.flaky += 1;
}
}
} else {
self.passed += 1;
if last_status.is_slow {
self.passed_slow += 1;
}
}
}
ExecutionResultDescription::Leak {
result: LeakTimeoutResult::Pass,
} => {
let is_flaky = run_statuses.len() > 1;
if is_flaky {
match run_statuses.flaky_result() {
FlakyResult::Fail => {
self.failed += 1;
if last_status.is_slow {
self.failed_slow += 1;
}
self.leaky += 1;
}
FlakyResult::Pass => {
self.passed += 1;
self.leaky += 1;
if last_status.is_slow {
self.passed_slow += 1;
}
self.flaky += 1;
}
}
} else {
self.passed += 1;
self.leaky += 1;
if last_status.is_slow {
self.passed_slow += 1;
}
}
}
ExecutionResultDescription::Leak {
result: LeakTimeoutResult::Fail,
} => {
self.failed += 1;
self.leaky_failed += 1;
if last_status.is_slow {
self.failed_slow += 1;
}
}
ExecutionResultDescription::Fail { .. } => {
self.failed += 1;
if last_status.is_slow {
self.failed_slow += 1;
}
}
ExecutionResultDescription::Timeout {
result: SlowTimeoutResult::Pass,
} => {
let is_flaky = run_statuses.len() > 1;
if is_flaky {
match run_statuses.flaky_result() {
FlakyResult::Fail => {
self.failed += 1;
if last_status.is_slow {
self.failed_slow += 1;
}
}
FlakyResult::Pass => {
self.passed += 1;
self.passed_timed_out += 1;
self.flaky += 1;
}
}
} else {
self.passed += 1;
self.passed_timed_out += 1;
}
}
ExecutionResultDescription::Timeout {
result: SlowTimeoutResult::Fail,
} => {
self.failed_timed_out += 1;
}
ExecutionResultDescription::ExecFail => self.exec_failed += 1,
}
}
}
#[derive(Copy, Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
#[serde(tag = "outcome", rename_all = "kebab-case")]
#[cfg_attr(test, derive(test_strategy::Arbitrary))]
pub enum FinalRunStats {
Success,
NoTestsRun,
Cancelled {
reason: Option<CancelReason>,
kind: RunStatsFailureKind,
},
Failed {
kind: RunStatsFailureKind,
},
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
#[cfg_attr(test, derive(test_strategy::Arbitrary))]
pub struct StressRunStats {
pub completed: StressIndex,
pub success_count: u32,
pub failed_count: u32,
pub last_final_stats: FinalRunStats,
}
impl StressRunStats {
pub fn summarize_final(&self) -> StressFinalRunStats {
if self.failed_count > 0 {
StressFinalRunStats::Failed
} else if matches!(self.last_final_stats, FinalRunStats::Cancelled { .. }) {
StressFinalRunStats::Cancelled
} else if matches!(self.last_final_stats, FinalRunStats::NoTestsRun) {
StressFinalRunStats::NoTestsRun
} else {
StressFinalRunStats::Success
}
}
}
pub enum StressFinalRunStats {
Success,
NoTestsRun,
Cancelled,
Failed,
}
#[derive(Copy, Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
#[serde(tag = "step", rename_all = "kebab-case")]
#[cfg_attr(test, derive(test_strategy::Arbitrary))]
pub enum RunStatsFailureKind {
SetupScript,
Test {
initial_run_count: usize,
not_run: usize,
},
}
#[derive_where::derive_where(Clone, Debug, PartialEq, Eq; S::ChildOutputDesc)]
#[derive(Serialize)]
#[serde(
rename_all = "kebab-case",
bound(serialize = "S: SerializableOutputSpec")
)]
#[cfg_attr(
test,
derive(test_strategy::Arbitrary),
arbitrary(bound(S: ArbitraryOutputSpec))
)]
pub struct ExecutionStatuses<S: OutputSpec> {
#[cfg_attr(test, strategy(proptest::collection::vec(proptest::arbitrary::any::<ExecuteStatus<S>>(), 1..=3)))]
statuses: Vec<ExecuteStatus<S>>,
#[serde(default)]
flaky_result: FlakyResult,
}
impl<'de, S: SerializableOutputSpec> Deserialize<'de> for ExecutionStatuses<S> {
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
#[derive(Deserialize)]
#[serde(
rename_all = "kebab-case",
bound(deserialize = "S: SerializableOutputSpec")
)]
struct Helper<S: OutputSpec> {
statuses: Vec<ExecuteStatus<S>>,
#[serde(default)]
flaky_result: FlakyResult,
}
let helper = Helper::<S>::deserialize(deserializer)?;
if helper.statuses.is_empty() {
return Err(serde::de::Error::custom("expected non-empty statuses"));
}
Ok(Self {
statuses: helper.statuses,
flaky_result: helper.flaky_result,
})
}
}
#[expect(clippy::len_without_is_empty)] impl<S: OutputSpec> ExecutionStatuses<S> {
pub(crate) fn new(statuses: Vec<ExecuteStatus<S>>, flaky_result: FlakyResult) -> Self {
debug_assert!(!statuses.is_empty(), "ExecutionStatuses must be non-empty");
Self {
statuses,
flaky_result,
}
}
pub fn flaky_result(&self) -> FlakyResult {
self.flaky_result
}
pub fn last_status(&self) -> &ExecuteStatus<S> {
self.statuses
.last()
.expect("execution statuses is non-empty")
}
pub fn iter(&self) -> impl DoubleEndedIterator<Item = &'_ ExecuteStatus<S>> + '_ {
self.statuses.iter()
}
pub fn len(&self) -> usize {
self.statuses.len()
}
pub fn describe(&self) -> ExecutionDescription<'_, S> {
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],
result: self.flaky_result,
}
} 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,
}
}
}
}
impl<S: OutputSpec> IntoIterator for ExecutionStatuses<S> {
type Item = ExecuteStatus<S>;
type IntoIter = std::vec::IntoIter<ExecuteStatus<S>>;
fn into_iter(self) -> Self::IntoIter {
self.statuses.into_iter()
}
}
#[derive_where::derive_where(Debug; S::ChildOutputDesc)]
pub enum ExecutionDescription<'a, S: OutputSpec> {
Success {
single_status: &'a ExecuteStatus<S>,
},
Flaky {
last_status: &'a ExecuteStatus<S>,
prior_statuses: &'a [ExecuteStatus<S>],
result: FlakyResult,
},
Failure {
first_status: &'a ExecuteStatus<S>,
last_status: &'a ExecuteStatus<S>,
retries: &'a [ExecuteStatus<S>],
},
}
impl<S: OutputSpec> Clone for ExecutionDescription<'_, S> {
fn clone(&self) -> Self {
*self
}
}
impl<S: OutputSpec> Copy for ExecutionDescription<'_, S> {}
impl<'a, S: OutputSpec> ExecutionDescription<'a, S> {
pub fn status_level(&self) -> StatusLevel {
match self {
ExecutionDescription::Success { single_status } => match single_status.result {
ExecutionResultDescription::Leak {
result: LeakTimeoutResult::Pass,
} => StatusLevel::Leak,
ExecutionResultDescription::Pass => StatusLevel::Pass,
ExecutionResultDescription::Timeout {
result: SlowTimeoutResult::Pass,
} => StatusLevel::Slow,
ref other => unreachable!(
"Success only permits Pass, Leak Pass, or Timeout Pass, found {other:?}"
),
},
ExecutionDescription::Flaky {
result: FlakyResult::Pass,
..
} => StatusLevel::Retry,
ExecutionDescription::Flaky {
result: FlakyResult::Fail,
..
} => StatusLevel::Fail,
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 {
ExecutionResultDescription::Pass => FinalStatusLevel::Pass,
ExecutionResultDescription::Leak {
result: LeakTimeoutResult::Pass,
} => FinalStatusLevel::Leak,
ExecutionResultDescription::Timeout {
result: SlowTimeoutResult::Pass,
} => FinalStatusLevel::Slow,
ref other => unreachable!(
"Success only permits Pass, Leak Pass, or Timeout Pass, found {other:?}"
),
}
}
}
ExecutionDescription::Flaky {
result: FlakyResult::Pass,
..
} => FinalStatusLevel::Flaky,
ExecutionDescription::Flaky {
result: FlakyResult::Fail,
..
} => FinalStatusLevel::Fail,
ExecutionDescription::Failure { .. } => FinalStatusLevel::Fail,
}
}
pub fn is_success_for_output(&self) -> bool {
match self {
ExecutionDescription::Success { .. } => true,
ExecutionDescription::Flaky { .. } => true,
ExecutionDescription::Failure { .. } => false,
}
}
pub fn last_status(&self) -> &'a ExecuteStatus<S> {
match self {
ExecutionDescription::Success {
single_status: last_status,
}
| ExecutionDescription::Flaky { last_status, .. }
| ExecutionDescription::Failure { last_status, .. } => last_status,
}
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
#[cfg_attr(test, derive(test_strategy::Arbitrary))]
pub struct ErrorSummary {
pub short_message: String,
pub description: String,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
#[cfg_attr(test, derive(test_strategy::Arbitrary))]
pub struct OutputErrorSlice {
pub slice: String,
pub start: usize,
}
#[derive_where::derive_where(Clone, Debug, PartialEq, Eq; S::ChildOutputDesc)]
#[derive(Serialize, Deserialize)]
#[serde(
rename_all = "kebab-case",
bound(
serialize = "S: SerializableOutputSpec",
deserialize = "S: SerializableOutputSpec"
)
)]
#[cfg_attr(
test,
derive(test_strategy::Arbitrary),
arbitrary(bound(S: ArbitraryOutputSpec))
)]
pub struct ExecuteStatus<S: OutputSpec> {
pub retry_data: RetryData,
pub output: ChildExecutionOutputDescription<S>,
pub result: ExecutionResultDescription,
#[cfg_attr(
test,
strategy(crate::reporter::test_helpers::arb_datetime_fixed_offset())
)]
pub start_time: DateTime<FixedOffset>,
#[cfg_attr(test, strategy(crate::reporter::test_helpers::arb_duration()))]
pub time_taken: Duration,
pub is_slow: bool,
#[cfg_attr(test, strategy(crate::reporter::test_helpers::arb_duration()))]
pub delay_before_start: Duration,
pub error_summary: Option<ErrorSummary>,
pub output_error_slice: Option<OutputErrorSlice>,
}
#[derive_where::derive_where(Clone, Debug, PartialEq, Eq; S::ChildOutputDesc)]
#[derive(Serialize, Deserialize)]
#[serde(
rename_all = "kebab-case",
bound(
serialize = "S: SerializableOutputSpec",
deserialize = "S: SerializableOutputSpec"
)
)]
#[cfg_attr(
test,
derive(test_strategy::Arbitrary),
arbitrary(bound(S: ArbitraryOutputSpec))
)]
pub struct SetupScriptExecuteStatus<S: OutputSpec> {
pub output: ChildExecutionOutputDescription<S>,
pub result: ExecutionResultDescription,
#[cfg_attr(
test,
strategy(crate::reporter::test_helpers::arb_datetime_fixed_offset())
)]
pub start_time: DateTime<FixedOffset>,
#[cfg_attr(test, strategy(crate::reporter::test_helpers::arb_duration()))]
pub time_taken: Duration,
pub is_slow: bool,
pub env_map: Option<SetupScriptEnvMap>,
pub error_summary: Option<ErrorSummary>,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
#[cfg_attr(test, derive(test_strategy::Arbitrary))]
pub struct SetupScriptEnvMap {
pub env_map: BTreeMap<String, String>,
}
#[derive_where::derive_where(Clone, Debug, PartialEq, Eq; S::ChildOutputDesc)]
#[derive(Serialize, Deserialize)]
#[serde(
tag = "type",
rename_all = "kebab-case",
bound(
serialize = "S: SerializableOutputSpec",
deserialize = "S: SerializableOutputSpec"
)
)]
#[cfg_attr(
test,
derive(test_strategy::Arbitrary),
arbitrary(bound(S: ArbitraryOutputSpec))
)]
pub enum ChildExecutionOutputDescription<S: OutputSpec> {
Output {
result: Option<ExecutionResultDescription>,
output: S::ChildOutputDesc,
errors: Option<ErrorList<ChildErrorDescription>>,
},
StartError(ChildStartErrorDescription),
}
impl<S: OutputSpec> ChildExecutionOutputDescription<S> {
pub fn has_errors(&self) -> bool {
match self {
Self::Output { errors, result, .. } => {
if errors.is_some() {
return true;
}
if let Some(result) = result {
return !result.is_success();
}
false
}
Self::StartError(_) => true,
}
}
}
#[derive(Clone, Debug)]
pub enum ChildOutputDescription {
Split {
stdout: Option<ChildSingleOutput>,
stderr: Option<ChildSingleOutput>,
},
Combined {
output: ChildSingleOutput,
},
NotLoaded,
}
impl ChildOutputDescription {
pub fn stdout_stderr_len(&self) -> (Option<u64>, Option<u64>) {
match self {
Self::Split { stdout, stderr } => (
stdout.as_ref().map(|s| s.buf().len() as u64),
stderr.as_ref().map(|s| s.buf().len() as u64),
),
Self::Combined { output } => (Some(output.buf().len() as u64), None),
Self::NotLoaded => {
unreachable!(
"attempted to get output lengths from output that was not loaded \
(this method is only called from the live runner, where NotLoaded \
is never produced)"
);
}
}
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "kebab-case")]
#[cfg_attr(test, derive(test_strategy::Arbitrary))]
pub enum ChildStartErrorDescription {
TempPath {
source: SerializableError,
},
Spawn {
source: SerializableError,
},
}
impl fmt::Display for ChildStartErrorDescription {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::TempPath { .. } => {
write!(f, "error creating temporary path for setup script")
}
Self::Spawn { .. } => write!(f, "error spawning child process"),
}
}
}
impl std::error::Error for ChildStartErrorDescription {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::TempPath { source } | Self::Spawn { source } => Some(source),
}
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "kebab-case")]
#[cfg_attr(test, derive(test_strategy::Arbitrary))]
pub enum ChildErrorDescription {
ReadStdout {
source: SerializableError,
},
ReadStderr {
source: SerializableError,
},
ReadCombined {
source: SerializableError,
},
Wait {
source: SerializableError,
},
SetupScriptOutput {
source: SerializableError,
},
}
impl fmt::Display for ChildErrorDescription {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::ReadStdout { .. } => write!(f, "error reading standard output"),
Self::ReadStderr { .. } => write!(f, "error reading standard error"),
Self::ReadCombined { .. } => {
write!(f, "error reading combined stream")
}
Self::Wait { .. } => {
write!(f, "error waiting for child process to exit")
}
Self::SetupScriptOutput { .. } => {
write!(f, "error reading setup script output")
}
}
}
}
impl std::error::Error for ChildErrorDescription {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::ReadStdout { source }
| Self::ReadStderr { source }
| Self::ReadCombined { source }
| Self::Wait { source }
| Self::SetupScriptOutput { source } => Some(source),
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct SerializableError {
message: String,
source: Option<Box<SerializableError>>,
}
impl SerializableError {
pub fn new(error: &dyn std::error::Error) -> Self {
let message = error.to_string();
let mut causes = Vec::new();
let mut source = error.source();
while let Some(err) = source {
causes.push(err.to_string());
source = err.source();
}
Self::from_message_and_causes(message, causes)
}
pub fn from_message_and_causes(message: String, causes: Vec<String>) -> Self {
let mut next = None;
for cause in causes.into_iter().rev() {
let error = Self {
message: cause,
source: next.map(Box::new),
};
next = Some(error);
}
Self {
message,
source: next.map(Box::new),
}
}
pub fn message(&self) -> &str {
&self.message
}
pub fn sources(&self) -> SerializableErrorSources<'_> {
SerializableErrorSources {
current: self.source.as_deref(),
}
}
}
impl fmt::Display for SerializableError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.message)
}
}
impl std::error::Error for SerializableError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
self.source
.as_deref()
.map(|s| s as &(dyn std::error::Error + 'static))
}
}
#[derive(Debug)]
pub struct SerializableErrorSources<'a> {
current: Option<&'a SerializableError>,
}
impl<'a> Iterator for SerializableErrorSources<'a> {
type Item = &'a SerializableError;
fn next(&mut self) -> Option<Self::Item> {
let current = self.current?;
self.current = current.source.as_deref();
Some(current)
}
}
mod serializable_error_serde {
use super::*;
#[derive(Serialize, Deserialize)]
struct Ser {
message: String,
#[serde(default)]
causes: Vec<String>,
}
impl Serialize for SerializableError {
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
let mut causes = Vec::new();
let mut cause = self.source.as_ref();
while let Some(c) = cause {
causes.push(c.message.clone());
cause = c.source.as_ref();
}
let ser = Ser {
message: self.message.clone(),
causes,
};
ser.serialize(serializer)
}
}
impl<'de> Deserialize<'de> for SerializableError {
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
let ser = Ser::deserialize(deserializer)?;
Ok(SerializableError::from_message_and_causes(
ser.message,
ser.causes,
))
}
}
}
#[cfg(test)]
mod serializable_error_arbitrary {
use super::*;
use proptest::prelude::*;
impl Arbitrary for SerializableError {
type Parameters = ();
type Strategy = BoxedStrategy<Self>;
fn arbitrary_with(_: Self::Parameters) -> Self::Strategy {
(
any::<String>(),
proptest::collection::vec(any::<String>(), 0..3),
)
.prop_map(|(message, causes)| {
SerializableError::from_message_and_causes(message, causes)
})
.boxed()
}
}
}
impl From<ChildExecutionOutput> for ChildExecutionOutputDescription<LiveSpec> {
fn from(output: ChildExecutionOutput) -> Self {
match output {
ChildExecutionOutput::Output {
result,
output,
errors,
} => Self::Output {
result: result.map(ExecutionResultDescription::from),
output: ChildOutputDescription::from(output),
errors: errors.map(|e| e.map(ChildErrorDescription::from)),
},
ChildExecutionOutput::StartError(error) => {
Self::StartError(ChildStartErrorDescription::from(error))
}
}
}
}
impl From<ChildOutput> for ChildOutputDescription {
fn from(output: ChildOutput) -> Self {
match output {
ChildOutput::Split(split) => Self::Split {
stdout: split.stdout,
stderr: split.stderr,
},
ChildOutput::Combined { output } => Self::Combined { output },
}
}
}
impl From<ChildStartError> for ChildStartErrorDescription {
fn from(error: ChildStartError) -> Self {
match error {
ChildStartError::TempPath(e) => Self::TempPath {
source: SerializableError::new(&*e),
},
ChildStartError::Spawn(e) => Self::Spawn {
source: SerializableError::new(&*e),
},
}
}
}
impl From<ChildError> for ChildErrorDescription {
fn from(error: ChildError) -> Self {
match error {
ChildError::Fd(ChildFdError::ReadStdout(e)) => Self::ReadStdout {
source: SerializableError::new(&*e),
},
ChildError::Fd(ChildFdError::ReadStderr(e)) => Self::ReadStderr {
source: SerializableError::new(&*e),
},
ChildError::Fd(ChildFdError::ReadCombined(e)) => Self::ReadCombined {
source: SerializableError::new(&*e),
},
ChildError::Fd(ChildFdError::Wait(e)) => Self::Wait {
source: SerializableError::new(&*e),
},
ChildError::SetupScriptOutput(e) => Self::SetupScriptOutput {
source: SerializableError::new(&e),
},
}
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
#[cfg_attr(test, derive(test_strategy::Arbitrary))]
pub struct RetryData {
pub attempt: u32,
pub total_attempts: u32,
}
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 {
result: SlowTimeoutResult,
},
}
impl ExecutionResult {
pub fn is_success(self) -> bool {
match self {
ExecutionResult::Pass
| ExecutionResult::Timeout {
result: SlowTimeoutResult::Pass,
}
| ExecutionResult::Leak {
result: LeakTimeoutResult::Pass,
} => true,
ExecutionResult::Leak {
result: LeakTimeoutResult::Fail,
}
| ExecutionResult::Fail { .. }
| ExecutionResult::ExecFail
| ExecutionResult::Timeout {
result: SlowTimeoutResult::Fail,
} => false,
}
}
pub fn as_static_str(&self) -> &'static str {
match self {
ExecutionResult::Pass => "pass",
ExecutionResult::Leak { .. } => "leak",
ExecutionResult::Fail { .. } => "fail",
ExecutionResult::ExecFail => "exec-fail",
ExecutionResult::Timeout { .. } => "timeout",
}
}
}
#[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(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "kebab-case")]
#[cfg_attr(test, derive(test_strategy::Arbitrary))]
#[non_exhaustive]
pub enum AbortDescription {
UnixSignal {
signal: i32,
#[cfg_attr(
test,
strategy(proptest::option::of(crate::reporter::test_helpers::arb_smol_str()))
)]
name: Option<SmolStr>,
},
WindowsNtStatus {
code: i32,
#[cfg_attr(
test,
strategy(proptest::option::of(crate::reporter::test_helpers::arb_smol_str()))
)]
message: Option<SmolStr>,
},
WindowsJobObject,
}
impl fmt::Display for AbortDescription {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::UnixSignal { signal, name } => {
write!(f, "aborted with signal {signal}")?;
if let Some(name) = name {
write!(f, " (SIG{name})")?;
}
Ok(())
}
Self::WindowsNtStatus { code, message } => {
write!(f, "aborted with code {code:#010x}")?;
if let Some(message) = message {
write!(f, ": {message}")?;
}
Ok(())
}
Self::WindowsJobObject => {
write!(f, "terminated via job object")
}
}
}
}
impl From<AbortStatus> for AbortDescription {
fn from(status: AbortStatus) -> Self {
cfg_if::cfg_if! {
if #[cfg(unix)] {
match status {
AbortStatus::UnixSignal(signal) => Self::UnixSignal {
signal,
name: crate::helpers::signal_str(signal).map(SmolStr::new_static),
},
}
} else if #[cfg(windows)] {
match status {
AbortStatus::WindowsNtStatus(code) => Self::WindowsNtStatus {
code,
message: crate::helpers::windows_nt_status_message(code),
},
AbortStatus::JobObject => Self::WindowsJobObject,
}
} else {
match status {}
}
}
}
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "kebab-case")]
#[cfg_attr(test, derive(test_strategy::Arbitrary))]
#[non_exhaustive]
pub enum FailureDescription {
ExitCode {
code: i32,
},
Abort {
abort: AbortDescription,
},
}
impl From<FailureStatus> for FailureDescription {
fn from(status: FailureStatus) -> Self {
match status {
FailureStatus::ExitCode(code) => Self::ExitCode { code },
FailureStatus::Abort(abort) => Self::Abort {
abort: AbortDescription::from(abort),
},
}
}
}
impl fmt::Display for FailureDescription {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::ExitCode { code } => write!(f, "exited with code {code}"),
Self::Abort { abort } => write!(f, "{abort}"),
}
}
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
#[serde(tag = "status", rename_all = "kebab-case")]
#[cfg_attr(test, derive(test_strategy::Arbitrary))]
#[non_exhaustive]
pub enum ExecutionResultDescription {
Pass,
Leak {
result: LeakTimeoutResult,
},
Fail {
failure: FailureDescription,
leaked: bool,
},
ExecFail,
Timeout {
result: SlowTimeoutResult,
},
}
impl ExecutionResultDescription {
pub fn is_success(&self) -> bool {
match self {
Self::Pass
| Self::Timeout {
result: SlowTimeoutResult::Pass,
}
| Self::Leak {
result: LeakTimeoutResult::Pass,
} => true,
Self::Leak {
result: LeakTimeoutResult::Fail,
}
| Self::Fail { .. }
| Self::ExecFail
| Self::Timeout {
result: SlowTimeoutResult::Fail,
} => false,
}
}
pub fn as_static_str(&self) -> &'static str {
match self {
Self::Pass => "pass",
Self::Leak { .. } => "leak",
Self::Fail { .. } => "fail",
Self::ExecFail => "exec-fail",
Self::Timeout { .. } => "timeout",
}
}
pub fn is_termination_failure(&self) -> bool {
matches!(
self,
Self::Fail {
failure: FailureDescription::Abort {
abort: AbortDescription::UnixSignal {
signal: SIGTERM,
..
},
},
..
} | Self::Fail {
failure: FailureDescription::Abort {
abort: AbortDescription::WindowsJobObject,
},
..
}
)
}
}
impl From<ExecutionResult> for ExecutionResultDescription {
fn from(result: ExecutionResult) -> Self {
match result {
ExecutionResult::Pass => Self::Pass,
ExecutionResult::Leak { result } => Self::Leak { result },
ExecutionResult::Fail {
failure_status,
leaked,
} => Self::Fail {
failure: FailureDescription::from(failure_status),
leaked,
},
ExecutionResult::ExecFail => Self::ExecFail,
ExecutionResult::Timeout { result } => Self::Timeout { result },
}
}
}
#[derive(Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Deserialize, Serialize)]
#[serde(rename_all = "kebab-case")]
#[cfg_attr(test, derive(test_strategy::Arbitrary))]
pub enum CancelReason {
SetupScriptFailure,
TestFailure,
ReportError,
GlobalTimeout,
TestFailureImmediate,
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::TestFailureImmediate => "test failure",
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,
}
}
}
#[derive(Clone, Debug)]
pub enum InfoResponse<'a> {
SetupScript(SetupScriptInfoResponse),
Test(TestInfoResponse<'a>),
}
#[derive(Clone, Debug)]
pub struct SetupScriptInfoResponse {
pub stress_index: Option<StressIndex>,
pub script_id: ScriptId,
pub program: String,
pub args: Vec<String>,
pub state: UnitState,
pub output: ChildExecutionOutputDescription<LiveSpec>,
}
#[derive(Clone, Debug)]
pub struct TestInfoResponse<'a> {
pub stress_index: Option<StressIndex>,
pub test_instance: TestInstanceId<'a>,
pub retry_data: RetryData,
pub state: UnitState,
pub output: ChildExecutionOutputDescription<LiveSpec>,
}
#[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<ExecutionResultDescription>,
waiting_duration: Duration,
remaining: Duration,
},
Terminating(UnitTerminatingState),
Exited {
result: ExecutionResultDescription,
time_taken: Duration,
slow_after: Option<Duration>,
},
DelayBeforeNextAttempt {
previous_result: ExecutionResultDescription,
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 {
reason: None,
kind: 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 {
kind: 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 {
kind: RunStatsFailureKind::Test {
initial_run_count: 42,
not_run: 0,
},
},
"exec failed => failure"
);
assert_eq!(
RunStats {
initial_run_count: 42,
finished_count: 42,
failed_timed_out: 1,
..RunStats::default()
}
.summarize_final(),
FinalRunStats::Failed {
kind: RunStatsFailureKind::Test {
initial_run_count: 42,
not_run: 0,
},
},
"timed out => failure {:?} {:?}",
RunStats {
initial_run_count: 42,
finished_count: 42,
failed_timed_out: 1,
..RunStats::default()
}
.summarize_final(),
FinalRunStats::Failed {
kind: RunStatsFailureKind::Test {
initial_run_count: 42,
not_run: 0,
},
},
);
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 {
reason: None,
kind: 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 {
kind: 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 {
kind: 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 {
kind: 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"
);
}
fn make_execute_status(
result: ExecutionResultDescription,
attempt: u32,
total_attempts: u32,
) -> ExecuteStatus<LiveSpec> {
make_execute_status_slow(result, attempt, total_attempts, false)
}
fn make_execute_status_slow(
result: ExecutionResultDescription,
attempt: u32,
total_attempts: u32,
is_slow: bool,
) -> ExecuteStatus<LiveSpec> {
ExecuteStatus {
retry_data: RetryData {
attempt,
total_attempts,
},
output: ChildExecutionOutputDescription::Output {
result: Some(result.clone()),
output: ChildOutputDescription::Split {
stdout: None,
stderr: None,
},
errors: None,
},
result,
start_time: chrono::Utc::now().into(),
time_taken: Duration::from_millis(100),
is_slow,
delay_before_start: Duration::ZERO,
error_summary: None,
output_error_slice: None,
}
}
#[test]
fn is_success_for_output_by_variant() {
let pass_status = make_execute_status(ExecutionResultDescription::Pass, 1, 1);
let success_statuses = ExecutionStatuses::new(vec![pass_status], FlakyResult::Pass);
let describe = success_statuses.describe();
assert!(
matches!(describe, ExecutionDescription::Success { .. }),
"single pass is Success"
);
assert!(
describe.is_success_for_output(),
"Success: output is success output"
);
let fail_status = make_execute_status(
ExecutionResultDescription::Fail {
failure: FailureDescription::ExitCode { code: 1 },
leaked: false,
},
1,
2,
);
let pass_status = make_execute_status(ExecutionResultDescription::Pass, 2, 2);
let flaky_pass_statuses =
ExecutionStatuses::new(vec![fail_status, pass_status], FlakyResult::Pass);
let describe = flaky_pass_statuses.describe();
assert!(
matches!(
describe,
ExecutionDescription::Flaky {
result: FlakyResult::Pass,
..
}
),
"fail then pass with FlakyResult::Pass is Flaky Pass"
);
assert!(
describe.is_success_for_output(),
"Flaky pass: output is success output"
);
let fail_status = make_execute_status(
ExecutionResultDescription::Fail {
failure: FailureDescription::ExitCode { code: 1 },
leaked: false,
},
1,
2,
);
let pass_status = make_execute_status(ExecutionResultDescription::Pass, 2, 2);
let flaky_fail_statuses =
ExecutionStatuses::new(vec![fail_status, pass_status], FlakyResult::Fail);
let describe = flaky_fail_statuses.describe();
assert!(
matches!(
describe,
ExecutionDescription::Flaky {
result: FlakyResult::Fail,
..
}
),
"fail then pass with FlakyResult::Fail is Flaky Fail"
);
assert!(
describe.is_success_for_output(),
"Flaky fail: output is still success output (last attempt passed)"
);
let fail_status = make_execute_status(
ExecutionResultDescription::Fail {
failure: FailureDescription::ExitCode { code: 1 },
leaked: false,
},
1,
1,
);
let failure_statuses = ExecutionStatuses::new(vec![fail_status], FlakyResult::Pass);
let describe = failure_statuses.describe();
assert!(
matches!(describe, ExecutionDescription::Failure { .. }),
"single fail is Failure"
);
assert!(
!describe.is_success_for_output(),
"Failure: output is not success output"
);
let fail1 = make_execute_status(
ExecutionResultDescription::Fail {
failure: FailureDescription::ExitCode { code: 1 },
leaked: false,
},
1,
2,
);
let fail2 = make_execute_status(
ExecutionResultDescription::Fail {
failure: FailureDescription::ExitCode { code: 1 },
leaked: false,
},
2,
2,
);
let failure_retry_statuses = ExecutionStatuses::new(vec![fail1, fail2], FlakyResult::Pass);
let describe = failure_retry_statuses.describe();
assert!(
matches!(describe, ExecutionDescription::Failure { .. }),
"all-fail with retries is Failure"
);
assert!(
!describe.is_success_for_output(),
"Failure with retries: output is not success output"
);
}
#[test]
fn abort_description_serialization() {
let unix_with_name = AbortDescription::UnixSignal {
signal: 15,
name: Some("TERM".into()),
};
let json = serde_json::to_string_pretty(&unix_with_name).unwrap();
insta::assert_snapshot!("abort_unix_signal_with_name", json);
let roundtrip: AbortDescription = serde_json::from_str(&json).unwrap();
assert_eq!(unix_with_name, roundtrip);
let unix_no_name = AbortDescription::UnixSignal {
signal: 42,
name: None,
};
let json = serde_json::to_string_pretty(&unix_no_name).unwrap();
insta::assert_snapshot!("abort_unix_signal_no_name", json);
let roundtrip: AbortDescription = serde_json::from_str(&json).unwrap();
assert_eq!(unix_no_name, roundtrip);
let windows_nt = AbortDescription::WindowsNtStatus {
code: -1073741510_i32,
message: Some("The application terminated as a result of a CTRL+C.".into()),
};
let json = serde_json::to_string_pretty(&windows_nt).unwrap();
insta::assert_snapshot!("abort_windows_nt_status", json);
let roundtrip: AbortDescription = serde_json::from_str(&json).unwrap();
assert_eq!(windows_nt, roundtrip);
let windows_nt_no_msg = AbortDescription::WindowsNtStatus {
code: -1073741819_i32,
message: None,
};
let json = serde_json::to_string_pretty(&windows_nt_no_msg).unwrap();
insta::assert_snapshot!("abort_windows_nt_status_no_message", json);
let roundtrip: AbortDescription = serde_json::from_str(&json).unwrap();
assert_eq!(windows_nt_no_msg, roundtrip);
let job = AbortDescription::WindowsJobObject;
let json = serde_json::to_string_pretty(&job).unwrap();
insta::assert_snapshot!("abort_windows_job_object", json);
let roundtrip: AbortDescription = serde_json::from_str(&json).unwrap();
assert_eq!(job, roundtrip);
}
#[test]
fn abort_description_cross_platform_deserialization() {
let unix_json = r#"{"kind":"unix-signal","signal":11,"name":"SEGV"}"#;
let unix_desc: AbortDescription = serde_json::from_str(unix_json).unwrap();
assert_eq!(
unix_desc,
AbortDescription::UnixSignal {
signal: 11,
name: Some("SEGV".into()),
}
);
let windows_json = r#"{"kind":"windows-nt-status","code":-1073741510,"message":"CTRL+C"}"#;
let windows_desc: AbortDescription = serde_json::from_str(windows_json).unwrap();
assert_eq!(
windows_desc,
AbortDescription::WindowsNtStatus {
code: -1073741510,
message: Some("CTRL+C".into()),
}
);
let job_json = r#"{"kind":"windows-job-object"}"#;
let job_desc: AbortDescription = serde_json::from_str(job_json).unwrap();
assert_eq!(job_desc, AbortDescription::WindowsJobObject);
}
#[test]
fn abort_description_display() {
let unix = AbortDescription::UnixSignal {
signal: 15,
name: Some("TERM".into()),
};
assert_eq!(unix.to_string(), "aborted with signal 15 (SIGTERM)");
let unix_no_name = AbortDescription::UnixSignal {
signal: 42,
name: None,
};
assert_eq!(unix_no_name.to_string(), "aborted with signal 42");
let windows = AbortDescription::WindowsNtStatus {
code: -1073741510,
message: Some("CTRL+C exit".into()),
};
assert_eq!(
windows.to_string(),
"aborted with code 0xc000013a: CTRL+C exit"
);
let windows_no_msg = AbortDescription::WindowsNtStatus {
code: -1073741510,
message: None,
};
assert_eq!(windows_no_msg.to_string(), "aborted with code 0xc000013a");
let job = AbortDescription::WindowsJobObject;
assert_eq!(job.to_string(), "terminated via job object");
}
#[cfg(unix)]
#[test]
fn abort_description_from_abort_status() {
let status = AbortStatus::UnixSignal(15);
let description = AbortDescription::from(status);
assert_eq!(
description,
AbortDescription::UnixSignal {
signal: 15,
name: Some("TERM".into()),
}
);
let unknown_status = AbortStatus::UnixSignal(42);
let unknown_description = AbortDescription::from(unknown_status);
assert_eq!(
unknown_description,
AbortDescription::UnixSignal {
signal: 42,
name: None,
}
);
}
#[test]
fn execution_result_description_serialization() {
let pass = ExecutionResultDescription::Pass;
let json = serde_json::to_string_pretty(&pass).unwrap();
insta::assert_snapshot!("pass", json);
let roundtrip: ExecutionResultDescription = serde_json::from_str(&json).unwrap();
assert_eq!(pass, roundtrip);
let leak_pass = ExecutionResultDescription::Leak {
result: LeakTimeoutResult::Pass,
};
let json = serde_json::to_string_pretty(&leak_pass).unwrap();
insta::assert_snapshot!("leak_pass", json);
let roundtrip: ExecutionResultDescription = serde_json::from_str(&json).unwrap();
assert_eq!(leak_pass, roundtrip);
let leak_fail = ExecutionResultDescription::Leak {
result: LeakTimeoutResult::Fail,
};
let json = serde_json::to_string_pretty(&leak_fail).unwrap();
insta::assert_snapshot!("leak_fail", json);
let roundtrip: ExecutionResultDescription = serde_json::from_str(&json).unwrap();
assert_eq!(leak_fail, roundtrip);
let fail_exit_code = ExecutionResultDescription::Fail {
failure: FailureDescription::ExitCode { code: 101 },
leaked: false,
};
let json = serde_json::to_string_pretty(&fail_exit_code).unwrap();
insta::assert_snapshot!("fail_exit_code", json);
let roundtrip: ExecutionResultDescription = serde_json::from_str(&json).unwrap();
assert_eq!(fail_exit_code, roundtrip);
let fail_exit_code_leaked = ExecutionResultDescription::Fail {
failure: FailureDescription::ExitCode { code: 1 },
leaked: true,
};
let json = serde_json::to_string_pretty(&fail_exit_code_leaked).unwrap();
insta::assert_snapshot!("fail_exit_code_leaked", json);
let roundtrip: ExecutionResultDescription = serde_json::from_str(&json).unwrap();
assert_eq!(fail_exit_code_leaked, roundtrip);
let fail_unix_signal = ExecutionResultDescription::Fail {
failure: FailureDescription::Abort {
abort: AbortDescription::UnixSignal {
signal: 11,
name: Some("SEGV".into()),
},
},
leaked: false,
};
let json = serde_json::to_string_pretty(&fail_unix_signal).unwrap();
insta::assert_snapshot!("fail_unix_signal", json);
let roundtrip: ExecutionResultDescription = serde_json::from_str(&json).unwrap();
assert_eq!(fail_unix_signal, roundtrip);
let fail_unix_signal_unknown = ExecutionResultDescription::Fail {
failure: FailureDescription::Abort {
abort: AbortDescription::UnixSignal {
signal: 42,
name: None,
},
},
leaked: true,
};
let json = serde_json::to_string_pretty(&fail_unix_signal_unknown).unwrap();
insta::assert_snapshot!("fail_unix_signal_unknown_leaked", json);
let roundtrip: ExecutionResultDescription = serde_json::from_str(&json).unwrap();
assert_eq!(fail_unix_signal_unknown, roundtrip);
let fail_windows_nt = ExecutionResultDescription::Fail {
failure: FailureDescription::Abort {
abort: AbortDescription::WindowsNtStatus {
code: -1073741510,
message: Some("The application terminated as a result of a CTRL+C.".into()),
},
},
leaked: false,
};
let json = serde_json::to_string_pretty(&fail_windows_nt).unwrap();
insta::assert_snapshot!("fail_windows_nt_status", json);
let roundtrip: ExecutionResultDescription = serde_json::from_str(&json).unwrap();
assert_eq!(fail_windows_nt, roundtrip);
let fail_windows_nt_no_msg = ExecutionResultDescription::Fail {
failure: FailureDescription::Abort {
abort: AbortDescription::WindowsNtStatus {
code: -1073741819,
message: None,
},
},
leaked: false,
};
let json = serde_json::to_string_pretty(&fail_windows_nt_no_msg).unwrap();
insta::assert_snapshot!("fail_windows_nt_status_no_message", json);
let roundtrip: ExecutionResultDescription = serde_json::from_str(&json).unwrap();
assert_eq!(fail_windows_nt_no_msg, roundtrip);
let fail_job_object = ExecutionResultDescription::Fail {
failure: FailureDescription::Abort {
abort: AbortDescription::WindowsJobObject,
},
leaked: false,
};
let json = serde_json::to_string_pretty(&fail_job_object).unwrap();
insta::assert_snapshot!("fail_windows_job_object", json);
let roundtrip: ExecutionResultDescription = serde_json::from_str(&json).unwrap();
assert_eq!(fail_job_object, roundtrip);
let exec_fail = ExecutionResultDescription::ExecFail;
let json = serde_json::to_string_pretty(&exec_fail).unwrap();
insta::assert_snapshot!("exec_fail", json);
let roundtrip: ExecutionResultDescription = serde_json::from_str(&json).unwrap();
assert_eq!(exec_fail, roundtrip);
let timeout_pass = ExecutionResultDescription::Timeout {
result: SlowTimeoutResult::Pass,
};
let json = serde_json::to_string_pretty(&timeout_pass).unwrap();
insta::assert_snapshot!("timeout_pass", json);
let roundtrip: ExecutionResultDescription = serde_json::from_str(&json).unwrap();
assert_eq!(timeout_pass, roundtrip);
let timeout_fail = ExecutionResultDescription::Timeout {
result: SlowTimeoutResult::Fail,
};
let json = serde_json::to_string_pretty(&timeout_fail).unwrap();
insta::assert_snapshot!("timeout_fail", json);
let roundtrip: ExecutionResultDescription = serde_json::from_str(&json).unwrap();
assert_eq!(timeout_fail, roundtrip);
}
fn make_flaky_statuses(
pass_result: ExecutionResultDescription,
flaky_result: FlakyResult,
is_slow: bool,
) -> ExecutionStatuses<LiveSpec> {
let fail = make_execute_status(
ExecutionResultDescription::Fail {
failure: FailureDescription::ExitCode { code: 1 },
leaked: false,
},
1,
2,
);
let pass = make_execute_status_slow(pass_result, 2, 2, is_slow);
ExecutionStatuses::new(vec![fail, pass], flaky_result)
}
fn run_on_test_finished(statuses: &ExecutionStatuses<LiveSpec>) -> RunStats {
let mut stats = RunStats {
initial_run_count: 1,
..RunStats::default()
};
stats.on_test_finished(statuses);
stats
}
#[test]
fn on_test_finished_pass_flaky() {
let stats = run_on_test_finished(&make_flaky_statuses(
ExecutionResultDescription::Pass,
FlakyResult::Fail,
false,
));
assert_eq!(stats.finished_count, 1);
assert_eq!(stats.failed, 1);
assert_eq!(stats.failed_slow, 0, "not slow");
assert_eq!(stats.passed, 0);
assert_eq!(stats.flaky, 0);
let stats = run_on_test_finished(&make_flaky_statuses(
ExecutionResultDescription::Pass,
FlakyResult::Fail,
true,
));
assert_eq!(stats.failed, 1);
assert_eq!(stats.failed_slow, 1);
assert_eq!(stats.passed, 0);
assert_eq!(stats.flaky, 0);
let stats = run_on_test_finished(&make_flaky_statuses(
ExecutionResultDescription::Pass,
FlakyResult::Pass,
true,
));
assert_eq!(stats.passed, 1);
assert_eq!(stats.passed_slow, 1);
assert_eq!(stats.flaky, 1);
assert_eq!(stats.failed, 0);
}
#[test]
fn on_test_finished_leak_pass_flaky() {
let stats = run_on_test_finished(&make_flaky_statuses(
ExecutionResultDescription::Leak {
result: LeakTimeoutResult::Pass,
},
FlakyResult::Fail,
false,
));
assert_eq!(stats.failed, 1);
assert_eq!(stats.failed_slow, 0, "not slow");
assert_eq!(stats.leaky, 1, "leak still tracked");
assert_eq!(stats.passed, 0);
assert_eq!(stats.flaky, 0);
let stats = run_on_test_finished(&make_flaky_statuses(
ExecutionResultDescription::Leak {
result: LeakTimeoutResult::Pass,
},
FlakyResult::Fail,
true,
));
assert_eq!(stats.failed, 1);
assert_eq!(stats.failed_slow, 1);
assert_eq!(stats.leaky, 1);
assert_eq!(stats.passed, 0);
assert_eq!(stats.flaky, 0);
let stats = run_on_test_finished(&make_flaky_statuses(
ExecutionResultDescription::Leak {
result: LeakTimeoutResult::Pass,
},
FlakyResult::Pass,
true,
));
assert_eq!(stats.passed, 1);
assert_eq!(stats.passed_slow, 1);
assert_eq!(stats.leaky, 1);
assert_eq!(stats.flaky, 1);
assert_eq!(stats.failed, 0);
}
#[test]
fn on_test_finished_timeout_pass_flaky() {
let stats = run_on_test_finished(&make_flaky_statuses(
ExecutionResultDescription::Timeout {
result: SlowTimeoutResult::Pass,
},
FlakyResult::Fail,
true,
));
assert_eq!(stats.failed, 1);
assert_eq!(stats.failed_slow, 1);
assert_eq!(stats.passed, 0);
assert_eq!(stats.passed_timed_out, 0);
assert_eq!(stats.flaky, 0);
let stats = run_on_test_finished(&make_flaky_statuses(
ExecutionResultDescription::Timeout {
result: SlowTimeoutResult::Pass,
},
FlakyResult::Pass,
false,
));
assert_eq!(stats.passed, 1);
assert_eq!(stats.passed_timed_out, 1);
assert_eq!(stats.flaky, 1);
assert_eq!(stats.failed, 0);
}
#[test]
fn on_test_finished_non_flaky() {
let pass = make_execute_status_slow(ExecutionResultDescription::Pass, 1, 1, true);
let stats = run_on_test_finished(&ExecutionStatuses::new(vec![pass], FlakyResult::Pass));
assert_eq!(stats.passed, 1);
assert_eq!(stats.passed_slow, 1);
assert_eq!(stats.flaky, 0);
assert_eq!(stats.failed, 0);
let fail = make_execute_status_slow(
ExecutionResultDescription::Fail {
failure: FailureDescription::ExitCode { code: 1 },
leaked: false,
},
1,
1,
true,
);
let stats = run_on_test_finished(&ExecutionStatuses::new(vec![fail], FlakyResult::Pass));
assert_eq!(stats.failed, 1);
assert_eq!(stats.failed_slow, 1);
assert_eq!(stats.passed, 0);
assert_eq!(stats.flaky, 0);
}
}