use crate::internal::{FlakinessControl, RegisteredTest, TestResult};
use crate::output::progress::StderrProgress;
use crate::output::{write_failure_summary_to_stderr, LogFile, StdoutOrLogFile, TestRunnerOutput};
use ctrf_rs::test::{Status, Test};
use serde_json::json;
use std::collections::{HashMap, HashSet};
use std::io::Write;
use std::path::PathBuf;
use std::sync::Mutex;
use std::time::{Duration, SystemTime, UNIX_EPOCH};
pub(crate) struct Ctrf {
show_output: bool,
state: Mutex<CtrfState>,
progress: StderrProgress,
}
impl Ctrf {
pub fn new(show_output: bool, logfile_path: Option<PathBuf>) -> Self {
let target = match logfile_path {
Some(path) => StdoutOrLogFile::LogFile(LogFile::new(path, false)),
None => StdoutOrLogFile::stdout(),
};
Self {
show_output,
state: Mutex::new(CtrfState::new(target)),
progress: StderrProgress::new(),
}
}
}
impl TestRunnerOutput for Ctrf {
fn start_suite(&self, tests: &[RegisteredTest]) {
let mut state = self.state.lock().unwrap();
state.start.replace(SystemTime::now());
state.tests.clear();
state.summary = SummaryCounts::default();
state.suites.clear();
state.pending_tests.clear();
state.pending_stops.clear();
drop(state);
self.progress.start_suite(tests.len());
}
fn start_running_test(&self, registered_test: &RegisteredTest, idx: usize, count: usize) {
let mut state = self.state.lock().unwrap();
let mut test = Test::new(
registered_test.fully_qualified_name(),
Status::Pending,
Duration::ZERO,
);
test.start = Some(
SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_millis() as u64,
);
state
.pending_tests
.insert(registered_test.fully_qualified_name(), test);
drop(state);
self.progress
.start_running_test(registered_test, idx, count);
}
fn repeat_running_test(
&self,
registered_test: &RegisteredTest,
_idx: usize,
_count: usize,
_attempt: usize,
_max_attempts: usize,
_reason: &str,
) {
let mut state = self.state.lock().unwrap();
let test = state
.pending_tests
.get_mut(®istered_test.fully_qualified_name())
.expect("repeat_running_test called with a test that has not been started yet");
test.retries = Some(test.retries.unwrap_or_default() + 1);
test.flaky = match registered_test.props.flakiness_control {
FlakinessControl::None => None,
FlakinessControl::ProveNonFlaky(_) => Some(false),
FlakinessControl::RetryKnownFlaky(_) => Some(true),
};
}
fn finished_running_test(
&self,
registered_test: &RegisteredTest,
idx: usize,
count: usize,
result: &TestResult,
) {
self.progress
.finished_running_test(registered_test, idx, count, result);
let mut state = self.state.lock().unwrap();
let stop = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_millis() as u64;
state
.pending_stops
.insert(registered_test.fully_qualified_name(), stop);
let (status, suite, value) = {
let pending_test = state
.pending_tests
.get(®istered_test.fully_qualified_name())
.expect("finished_running_test called on a test that has not been started before");
build_test_value(
registered_test,
result,
self.show_output,
pending_test.flaky,
pending_test.retries,
pending_test.start,
Some(stop),
)
};
state.tests.push(value);
match status {
Status::Passed => state.summary.passed += 1,
Status::Failed => state.summary.failed += 1,
Status::Pending => state.summary.pending += 1,
Status::Skipped => state.summary.skipped += 1,
Status::Other => state.summary.other += 1,
}
if let Some(s) = suite {
state.suites.insert(s);
}
write_ctrf_snapshot(&mut state, false);
}
fn finished_suite(
&self,
_registered_tests: &[RegisteredTest],
results: &[(RegisteredTest, TestResult)],
exec_time: Duration,
) {
let mut state = self.state.lock().unwrap();
state.tests.clear();
state.summary = SummaryCounts::default();
state.suites.clear();
for (registered_test, result) in results {
let qname = registered_test.fully_qualified_name();
let pending_test = state.pending_tests.get(&qname);
let flaky = pending_test.and_then(|t| t.flaky);
let retries = pending_test.and_then(|t| t.retries);
let start = pending_test.and_then(|t| t.start);
let stop = state.pending_stops.get(&qname).copied().unwrap_or_else(|| {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_millis() as u64
});
let (status, suite, value) = build_test_value(
registered_test,
result,
self.show_output,
flaky,
retries,
start,
Some(stop),
);
state.tests.push(value);
match status {
Status::Passed => state.summary.passed += 1,
Status::Failed => state.summary.failed += 1,
Status::Pending => state.summary.pending += 1,
Status::Skipped => state.summary.skipped += 1,
Status::Other => state.summary.other += 1,
}
if let Some(s) = suite {
state.suites.insert(s);
}
}
write_ctrf_snapshot(&mut state, true);
state.start = None;
write_failure_summary_to_stderr(results, exec_time);
}
fn test_list(&self, _registered_tests: &[RegisteredTest]) {}
}
fn build_test_value(
registered_test: &RegisteredTest,
result: &TestResult,
show_output: bool,
flaky: Option<bool>,
retries: Option<usize>,
start: Option<u64>,
stop: Option<u64>,
) -> (Status, Option<String>, serde_json::Value) {
let mut test = Test::new(
registered_test.fully_qualified_name(),
match result {
TestResult::Passed { .. } => Status::Passed,
TestResult::Benchmarked { .. } => Status::Passed,
TestResult::Failed { .. } => Status::Failed,
TestResult::Ignored { .. } => Status::Skipped,
},
match result {
TestResult::Passed { exec_time, .. } => *exec_time,
TestResult::Failed { exec_time, .. } => *exec_time,
TestResult::Benchmarked { exec_time, .. } => *exec_time,
TestResult::Ignored { .. } => Duration::ZERO,
},
);
let mut stdout_lines = vec![];
let mut stderr_lines = vec![];
for capture in result.captured_output() {
match capture {
crate::internal::CapturedOutput::Stdout { line, .. } => stdout_lines.push(line.clone()),
crate::internal::CapturedOutput::Stderr { line, .. } => stderr_lines.push(line.clone()),
crate::internal::CapturedOutput::Host { line, .. } => {
stdout_lines.push(format!("[host] {line}"))
}
}
}
if result.is_failed() || show_output {
test.stdout = stdout_lines;
test.stderr = stderr_lines;
}
test.message = result.failure_message();
test.suite = Some(registered_test.crate_and_module());
test.flaky = flaky;
test.retries = retries;
test.start = start;
test.stop = stop;
let status = test.status();
let suite = test.suite.clone();
let value = serde_json::to_value(&test).expect("Failed to serialize CTRF test");
(status, suite, value)
}
fn write_ctrf_snapshot(state: &mut CtrfState, is_final: bool) {
let started = match state.start {
Some(s) => s,
None => return,
};
let reset = state
.target
.reset_log_file()
.expect("Failed to reset CTRF log file");
if !reset && !is_final {
return;
}
let now = SystemTime::now();
let start_ms = started.duration_since(UNIX_EPOCH).unwrap().as_millis() as u64;
let stop_ms = now.duration_since(UNIX_EPOCH).unwrap().as_millis() as u64;
let total = state.summary.passed
+ state.summary.failed
+ state.summary.pending
+ state.summary.skipped
+ state.summary.other;
let mut summary = json!({
"tests": total,
"passed": state.summary.passed,
"failed": state.summary.failed,
"pending": state.summary.pending,
"skipped": state.summary.skipped,
"other": state.summary.other,
"start": start_ms,
"stop": stop_ms,
});
if !state.suites.is_empty() {
summary
.as_object_mut()
.unwrap()
.insert("suites".to_string(), json!(state.suites.len()));
}
let report = json!({
"reportFormat": ctrf_rs::report::REPORT_FORMAT,
"specVersion": ctrf_rs::report::SPEC_VERSION.to_string(),
"timestamp": format!("{now:?}"),
"generatedBy": "test-r",
"results": {
"tool": { "name": "ctrf-rs" },
"summary": summary,
"tests": state.tests,
},
});
let raw = serde_json::to_string(&report).expect("Failed to serialize CTRF document");
let out = &mut state.target;
writeln!(out, "{}", raw).expect("Failed to write to output");
out.flush().expect("Failed to flush CTRF output");
}
#[derive(Default)]
struct SummaryCounts {
passed: usize,
failed: usize,
pending: usize,
skipped: usize,
other: usize,
}
struct CtrfState {
pub target: StdoutOrLogFile,
pub tests: Vec<serde_json::Value>,
pub summary: SummaryCounts,
pub suites: HashSet<String>,
pub pending_tests: HashMap<String, Test>,
pub pending_stops: HashMap<String, u64>,
pub start: Option<SystemTime>,
}
impl CtrfState {
pub fn new(target: StdoutOrLogFile) -> Self {
Self {
target,
tests: Vec::new(),
summary: SummaryCounts::default(),
suites: HashSet::new(),
pending_tests: HashMap::new(),
pending_stops: HashMap::new(),
start: None,
}
}
}