use crate::{
config::elements::LeakTimeoutResult,
errors::{DisplayErrorChain, FormatVersionError, FormatVersionErrorInner, WriteEventError},
list::RustTestSuite,
reporter::events::{ExecutionResult, TestEvent, TestEventKind},
test_output::{ChildExecutionOutput, ChildOutput, ChildSingleOutput},
};
use bstr::ByteSlice;
use nextest_metadata::MismatchReason;
use std::{collections::BTreeMap, fmt::Write as _};
#[derive(Copy, Clone)]
#[repr(u8)]
enum FormatMinorVersion {
First = 1,
#[doc(hidden)]
_Max,
}
#[derive(Copy, Clone)]
#[repr(u8)]
enum FormatMajorVersion {
Unstable = 0,
#[doc(hidden)]
_Max,
}
struct LibtestSuite<'cfg> {
failed: usize,
succeeded: usize,
ignored: usize,
filtered: usize,
running: usize,
meta: &'cfg RustTestSuite<'cfg>,
total: std::time::Duration,
ignore_block: Option<bytes::BytesMut>,
output_block: bytes::BytesMut,
}
#[derive(Copy, Clone, Debug)]
pub enum EmitNextestObject {
Yes,
No,
}
const KIND_TEST: &str = "test";
const KIND_SUITE: &str = "suite";
const EVENT_STARTED: &str = "started";
const EVENT_IGNORED: &str = "ignored";
const EVENT_OK: &str = "ok";
const EVENT_FAILED: &str = "failed";
#[inline]
fn fmt_err(err: std::fmt::Error) -> WriteEventError {
WriteEventError::Io(std::io::Error::new(std::io::ErrorKind::OutOfMemory, err))
}
pub struct LibtestReporter<'cfg> {
_minor: FormatMinorVersion,
_major: FormatMajorVersion,
test_suites: BTreeMap<&'cfg str, LibtestSuite<'cfg>>,
emit_nextest_obj: bool,
}
impl<'cfg> LibtestReporter<'cfg> {
pub fn new(
version: Option<&str>,
emit_nextest_obj: EmitNextestObject,
) -> Result<Self, FormatVersionError> {
let emit_nextest_obj = matches!(emit_nextest_obj, EmitNextestObject::Yes);
let Some(version) = version else {
return Ok(Self {
_minor: FormatMinorVersion::First,
_major: FormatMajorVersion::Unstable,
test_suites: BTreeMap::new(),
emit_nextest_obj,
});
};
let Some((major, minor)) = version.split_once('.') else {
return Err(FormatVersionError {
input: version.into(),
error: FormatVersionErrorInner::InvalidFormat {
expected: "<major>.<minor>",
},
});
};
let major: u8 = major.parse().map_err(|err| FormatVersionError {
input: version.into(),
error: FormatVersionErrorInner::InvalidInteger {
which: "major",
err,
},
})?;
let minor: u8 = minor.parse().map_err(|err| FormatVersionError {
input: version.into(),
error: FormatVersionErrorInner::InvalidInteger {
which: "minor",
err,
},
})?;
let major = match major {
0 => FormatMajorVersion::Unstable,
o => {
return Err(FormatVersionError {
input: version.into(),
error: FormatVersionErrorInner::InvalidValue {
which: "major",
value: o,
range: (FormatMajorVersion::Unstable as u8)
..(FormatMajorVersion::_Max as u8),
},
});
}
};
let minor = match minor {
1 => FormatMinorVersion::First,
o => {
return Err(FormatVersionError {
input: version.into(),
error: FormatVersionErrorInner::InvalidValue {
which: "minor",
value: o,
range: (FormatMinorVersion::First as u8)..(FormatMinorVersion::_Max as u8),
},
});
}
};
Ok(Self {
_major: major,
_minor: minor,
test_suites: BTreeMap::new(),
emit_nextest_obj,
})
}
pub(crate) fn write_event(&mut self, event: &TestEvent<'cfg>) -> Result<(), WriteEventError> {
let mut retries = None;
let (kind, eve, test_instance) = match &event.kind {
TestEventKind::TestStarted { test_instance, .. } => {
(KIND_TEST, EVENT_STARTED, test_instance)
}
TestEventKind::TestSkipped {
test_instance,
reason: MismatchReason::Ignored,
} => {
(KIND_TEST, EVENT_STARTED, test_instance)
}
TestEventKind::TestFinished {
test_instance,
run_statuses,
..
} => {
if run_statuses.len() > 1 {
retries = Some(run_statuses.len());
}
(
KIND_TEST,
match run_statuses.last_status().result {
ExecutionResult::Pass
| ExecutionResult::Leak {
result: LeakTimeoutResult::Pass,
} => EVENT_OK,
ExecutionResult::Leak {
result: LeakTimeoutResult::Fail,
}
| ExecutionResult::Fail { .. }
| ExecutionResult::ExecFail
| ExecutionResult::Timeout => EVENT_FAILED,
},
test_instance,
)
}
TestEventKind::RunFinished { .. } => {
for test_suite in std::mem::take(&mut self.test_suites).into_values() {
self.finalize(test_suite)?;
}
return Ok(());
}
_ => return Ok(()),
};
let suite_info = test_instance.suite_info;
let crate_name = suite_info.package.name();
let binary_name = &suite_info.binary_name;
let test_suite = match self.test_suites.entry(suite_info.binary_id.as_str()) {
std::collections::btree_map::Entry::Vacant(e) => {
let (running, ignored, filtered) =
suite_info.status.test_cases().fold((0, 0, 0), |acc, tc| {
if tc.1.ignored {
(acc.0, acc.1 + 1, acc.2)
} else if tc.1.filter_match.is_match() {
(acc.0 + 1, acc.1, acc.2)
} else {
(acc.0, acc.1, acc.2 + 1)
}
});
let mut out = bytes::BytesMut::with_capacity(1024);
write!(
&mut out,
r#"{{"type":"{KIND_SUITE}","event":"{EVENT_STARTED}","test_count":{}"#,
running + ignored,
)
.map_err(fmt_err)?;
if self.emit_nextest_obj {
write!(
&mut out,
r#","nextest":{{"crate":"{crate_name}","test_binary":"{binary_name}","kind":"{}"}}"#,
suite_info.kind,
)
.map_err(fmt_err)?;
}
out.extend_from_slice(b"}\n");
e.insert(LibtestSuite {
running,
failed: 0,
succeeded: 0,
ignored,
filtered,
meta: test_instance.suite_info,
total: std::time::Duration::new(0, 0),
ignore_block: None,
output_block: out,
})
}
std::collections::btree_map::Entry::Occupied(e) => e.into_mut(),
};
let out = &mut test_suite.output_block;
if matches!(event.kind, TestEventKind::TestFinished { .. }) {
if let Some(ib) = test_suite.ignore_block.take() {
out.extend_from_slice(&ib);
}
}
write!(
out,
r#"{{"type":"{kind}","event":"{eve}","name":"{}::{}${}"#,
suite_info.package.name(),
suite_info.binary_name,
test_instance.name,
)
.map_err(fmt_err)?;
if let Some(retry_count) = retries {
write!(out, "#{retry_count}\"").map_err(fmt_err)?;
} else {
out.extend_from_slice(b"\"");
}
match &event.kind {
TestEventKind::TestFinished { run_statuses, .. } => {
let last_status = run_statuses.last_status();
test_suite.total += last_status.time_taken;
test_suite.running -= 1;
write!(
out,
r#","exec_time":{}"#,
last_status.time_taken.as_secs_f64()
)
.map_err(fmt_err)?;
match last_status.result {
ExecutionResult::Fail { .. } | ExecutionResult::ExecFail => {
test_suite.failed += 1;
write!(out, r#","stdout":""#).map_err(fmt_err)?;
strip_human_output_from_failed_test(
&last_status.output,
out,
test_instance.name,
)?;
out.extend_from_slice(b"\"");
}
ExecutionResult::Timeout => {
test_suite.failed += 1;
out.extend_from_slice(br#","reason":"time limit exceeded""#);
}
_ => {
test_suite.succeeded += 1;
}
}
}
TestEventKind::TestSkipped { .. } => {
test_suite.running -= 1;
if test_suite.ignore_block.is_none() {
test_suite.ignore_block = Some(bytes::BytesMut::with_capacity(1024));
}
let ib = test_suite
.ignore_block
.get_or_insert_with(|| bytes::BytesMut::with_capacity(1024));
writeln!(
ib,
r#"{{"type":"{kind}","event":"{EVENT_IGNORED}","name":"{}::{}${}"}}"#,
suite_info.package.name(),
suite_info.binary_name,
test_instance.name,
)
.map_err(fmt_err)?;
}
_ => {}
};
out.extend_from_slice(b"}\n");
if test_suite.running > 0 {
return Ok(());
}
if let Some(test_suite) = self.test_suites.remove(suite_info.binary_id.as_str()) {
self.finalize(test_suite)?;
}
Ok(())
}
fn finalize(&self, mut test_suite: LibtestSuite) -> Result<(), WriteEventError> {
let event = if test_suite.failed > 0 {
EVENT_FAILED
} else {
EVENT_OK
};
let out = &mut test_suite.output_block;
let suite_info = test_suite.meta;
if test_suite.running > 0 {
test_suite.filtered += test_suite.running;
}
write!(
out,
r#"{{"type":"{KIND_SUITE}","event":"{event}","passed":{},"failed":{},"ignored":{},"measured":0,"filtered_out":{},"exec_time":{}"#,
test_suite.succeeded,
test_suite.failed,
test_suite.ignored,
test_suite.filtered,
test_suite.total.as_secs_f64(),
)
.map_err(fmt_err)?;
if self.emit_nextest_obj {
let crate_name = suite_info.package.name();
let binary_name = &suite_info.binary_name;
write!(
out,
r#","nextest":{{"crate":"{crate_name}","test_binary":"{binary_name}","kind":"{}"}}"#,
suite_info.kind,
)
.map_err(fmt_err)?;
}
out.extend_from_slice(b"}\n");
{
use std::io::Write as _;
let mut stdout = std::io::stdout().lock();
stdout.write_all(out).map_err(WriteEventError::Io)?;
stdout.flush().map_err(WriteEventError::Io)?;
}
Ok(())
}
}
fn strip_human_output_from_failed_test(
output: &ChildExecutionOutput,
out: &mut bytes::BytesMut,
test_name: &str,
) -> Result<(), WriteEventError> {
match output {
ChildExecutionOutput::Output {
result: _,
output,
errors,
} => {
match output {
ChildOutput::Combined { output } => {
strip_human_stdout_or_combined(output, out, test_name)?;
}
ChildOutput::Split(split) => {
#[cfg(not(test))]
{
debug_assert!(false, "libtest output requires CaptureStrategy::Combined");
}
if let Some(stdout) = &split.stdout {
if !stdout.is_empty() {
write!(out, "--- STDOUT ---\\n").map_err(fmt_err)?;
strip_human_stdout_or_combined(stdout, out, test_name)?;
}
} else {
write!(out, "(stdout not captured)").map_err(fmt_err)?;
}
if let Some(stderr) = &split.stderr {
if !stderr.is_empty() {
write!(out, "\\n--- STDERR ---\\n").map_err(fmt_err)?;
write!(out, "{}", EscapedString(stderr.as_str_lossy()))
.map_err(fmt_err)?;
}
} else {
writeln!(out, "\\n(stderr not captured)").map_err(fmt_err)?;
}
}
}
if let Some(errors) = errors {
write!(out, "\\n--- EXECUTION ERRORS ---\\n").map_err(fmt_err)?;
write!(
out,
"{}",
EscapedString(&DisplayErrorChain::new(errors).to_string())
)
.map_err(fmt_err)?;
}
}
ChildExecutionOutput::StartError(error) => {
write!(out, "--- EXECUTION ERROR ---\\n").map_err(fmt_err)?;
write!(
out,
"{}",
EscapedString(&DisplayErrorChain::new(error).to_string())
)
.map_err(fmt_err)?;
}
}
Ok(())
}
fn strip_human_stdout_or_combined(
output: &ChildSingleOutput,
out: &mut bytes::BytesMut,
test_name: &str,
) -> Result<(), WriteEventError> {
if output.buf.contains_str("running 1 test\n") {
let lines = output
.lines()
.skip_while(|line| line != b"running 1 test")
.skip(1)
.take_while(|line| {
if let Some(name) = line
.strip_prefix(b"test ")
.and_then(|np| np.strip_suffix(b" ... FAILED"))
{
if test_name.as_bytes() == name {
return false;
}
}
true
})
.map(|line| line.to_str_lossy());
for line in lines {
write!(out, "{}\\n", EscapedString(&line)).map_err(fmt_err)?;
}
} else {
write!(out, "{}", EscapedString(output.as_str_lossy())).map_err(fmt_err)?;
}
Ok(())
}
struct EscapedString<'s>(&'s str);
impl std::fmt::Display for EscapedString<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> ::std::fmt::Result {
let mut start = 0;
let s = self.0;
for (i, byte) in s.bytes().enumerate() {
let escaped = match byte {
b'"' => "\\\"",
b'\\' => "\\\\",
b'\x00' => "\\u0000",
b'\x01' => "\\u0001",
b'\x02' => "\\u0002",
b'\x03' => "\\u0003",
b'\x04' => "\\u0004",
b'\x05' => "\\u0005",
b'\x06' => "\\u0006",
b'\x07' => "\\u0007",
b'\x08' => "\\b",
b'\t' => "\\t",
b'\n' => "\\n",
b'\x0b' => "\\u000b",
b'\x0c' => "\\f",
b'\r' => "\\r",
b'\x0e' => "\\u000e",
b'\x0f' => "\\u000f",
b'\x10' => "\\u0010",
b'\x11' => "\\u0011",
b'\x12' => "\\u0012",
b'\x13' => "\\u0013",
b'\x14' => "\\u0014",
b'\x15' => "\\u0015",
b'\x16' => "\\u0016",
b'\x17' => "\\u0017",
b'\x18' => "\\u0018",
b'\x19' => "\\u0019",
b'\x1a' => "\\u001a",
b'\x1b' => "\\u001b",
b'\x1c' => "\\u001c",
b'\x1d' => "\\u001d",
b'\x1e' => "\\u001e",
b'\x1f' => "\\u001f",
b'\x7f' => "\\u007f",
_ => {
continue;
}
};
if start < i {
f.write_str(&s[start..i])?;
}
f.write_str(escaped)?;
start = i + 1;
}
if start != self.0.len() {
f.write_str(&s[start..])?;
}
Ok(())
}
}
#[cfg(test)]
mod test {
use crate::{
errors::ChildStartError,
reporter::structured::libtest::strip_human_output_from_failed_test,
test_output::{ChildExecutionOutput, ChildOutput, ChildSplitOutput},
};
use bytes::BytesMut;
use color_eyre::eyre::eyre;
use std::{io, sync::Arc};
#[test]
fn strips_human_output() {
const TEST_OUTPUT: &[&str] = &[
"\n",
"running 1 test\n",
"[src/index.rs:185] \"boop\" = \"boop\"\n",
"this is stdout\n",
"this i stderr\nok?\n",
"thread 'index::test::download_url_crates_io'",
r" panicked at src/index.rs:206:9:
oh no
stack backtrace:
0: rust_begin_unwind
at /rustc/a28077b28a02b92985b3a3faecf92813155f1ea1/library/std/src/panicking.rs:597:5
1: core::panicking::panic_fmt
at /rustc/a28077b28a02b92985b3a3faecf92813155f1ea1/library/core/src/panicking.rs:72:14
2: tame_index::index::test::download_url_crates_io
at ./src/index.rs:206:9
3: tame_index::index::test::download_url_crates_io::{{closure}}
at ./src/index.rs:179:33
4: core::ops::function::FnOnce::call_once
at /rustc/a28077b28a02b92985b3a3faecf92813155f1ea1/library/core/src/ops/function.rs:250:5
5: core::ops::function::FnOnce::call_once
at /rustc/a28077b28a02b92985b3a3faecf92813155f1ea1/library/core/src/ops/function.rs:250:5
note: Some details are omitted, run with `RUST_BACKTRACE=full` for a verbose backtrace.
",
"test index::test::download_url_crates_io ... FAILED\n",
"\n\nfailures:\n\nfailures:\n index::test::download_url_crates_io\n\ntest result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 13 filtered out; finished in 0.01s\n",
];
let output = {
let mut acc = BytesMut::new();
for line in TEST_OUTPUT {
acc.extend_from_slice(line.as_bytes());
}
ChildOutput::Combined {
output: acc.freeze().into(),
}
};
let mut actual = bytes::BytesMut::new();
strip_human_output_from_failed_test(
&ChildExecutionOutput::Output {
result: None,
output,
errors: None,
},
&mut actual,
"index::test::download_url_crates_io",
)
.unwrap();
insta::assert_snapshot!(std::str::from_utf8(&actual).unwrap());
}
#[test]
fn strips_human_output_custom_test_harness() {
const TEST_OUTPUT: &[&str] = &["\n", "this is a custom test harness!!!\n", "1 test passed"];
let output = {
let mut acc = BytesMut::new();
for line in TEST_OUTPUT {
acc.extend_from_slice(line.as_bytes());
}
ChildOutput::Combined {
output: acc.freeze().into(),
}
};
let mut actual = bytes::BytesMut::new();
strip_human_output_from_failed_test(
&ChildExecutionOutput::Output {
result: None,
output,
errors: None,
},
&mut actual,
"non-existent",
)
.unwrap();
insta::assert_snapshot!(std::str::from_utf8(&actual).unwrap());
}
#[test]
fn strips_human_output_start_error() {
let inner_error = eyre!("inner error");
let error = io::Error::other(inner_error);
let output = ChildExecutionOutput::StartError(ChildStartError::Spawn(Arc::new(error)));
let mut actual = bytes::BytesMut::new();
strip_human_output_from_failed_test(&output, &mut actual, "non-existent").unwrap();
insta::assert_snapshot!(std::str::from_utf8(&actual).unwrap());
}
#[test]
fn strips_human_output_none() {
let mut actual = bytes::BytesMut::new();
strip_human_output_from_failed_test(
&ChildExecutionOutput::Output {
result: None,
output: ChildOutput::Split(ChildSplitOutput {
stdout: None,
stderr: None,
}),
errors: None,
},
&mut actual,
"non-existent",
)
.unwrap();
insta::assert_snapshot!(std::str::from_utf8(&actual).unwrap());
}
}