use super::{
CompletedRunStats, ComponentSizes, RecordedRunInfo, RecordedRunStatus, RecordedSizes,
StressCompletedRunStats,
};
use camino::Utf8Path;
use chrono::{DateTime, FixedOffset, Utc};
use eazip::{CompressionMethod, write::FileOptions};
use iddqd::{IdOrdItem, IdOrdMap, id_upcast};
use nextest_metadata::{RustBinaryId, TestCaseName};
use quick_junit::ReportUuid;
use semver::Version;
use serde::{Deserialize, Serialize};
use std::{
collections::{BTreeMap, BTreeSet},
fmt,
num::NonZero,
};
macro_rules! define_format_version {
(
$(#[$attr:meta])*
$vis:vis struct $name:ident;
) => {
$(#[$attr])*
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Deserialize, Serialize)]
#[serde(transparent)]
$vis struct $name(u32);
impl $name {
#[doc = concat!("Creates a new `", stringify!($name), "`.")]
pub const fn new(version: u32) -> Self {
Self(version)
}
}
impl fmt::Display for $name {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
}
}
};
(
@default
$(#[$attr:meta])*
$vis:vis struct $name:ident;
) => {
$(#[$attr])*
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Deserialize, Serialize)]
#[serde(transparent)]
$vis struct $name(u32);
impl $name {
#[doc = concat!("Creates a new `", stringify!($name), "`.")]
pub const fn new(version: u32) -> Self {
Self(version)
}
}
impl fmt::Display for $name {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
}
}
};
}
define_format_version! {
pub struct RunsJsonFormatVersion;
}
define_format_version! {
pub struct StoreFormatMajorVersion;
}
define_format_version! {
@default
pub struct StoreFormatMinorVersion;
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct StoreFormatVersion {
pub major: StoreFormatMajorVersion,
pub minor: StoreFormatMinorVersion,
}
impl StoreFormatVersion {
pub const fn new(major: StoreFormatMajorVersion, minor: StoreFormatMinorVersion) -> Self {
Self { major, minor }
}
pub fn check_readable_by(self, supported: Self) -> Result<(), StoreVersionIncompatibility> {
if self.major < supported.major {
return Err(StoreVersionIncompatibility::RecordingTooOld {
recording_major: self.major,
supported_major: supported.major,
last_nextest_version: self.major.last_nextest_version(),
});
}
if self.major > supported.major {
return Err(StoreVersionIncompatibility::RecordingTooNew {
recording_major: self.major,
supported_major: supported.major,
});
}
if self.minor > supported.minor {
return Err(StoreVersionIncompatibility::MinorTooNew {
recording_minor: self.minor,
supported_minor: supported.minor,
});
}
Ok(())
}
}
impl fmt::Display for StoreFormatVersion {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}.{}", self.major, self.minor)
}
}
impl StoreFormatMajorVersion {
pub fn last_nextest_version(self) -> Option<&'static str> {
match self.0 {
1 => Some("0.9.130"),
_ => None,
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum StoreVersionIncompatibility {
RecordingTooOld {
recording_major: StoreFormatMajorVersion,
supported_major: StoreFormatMajorVersion,
last_nextest_version: Option<&'static str>,
},
RecordingTooNew {
recording_major: StoreFormatMajorVersion,
supported_major: StoreFormatMajorVersion,
},
MinorTooNew {
recording_minor: StoreFormatMinorVersion,
supported_minor: StoreFormatMinorVersion,
},
}
impl fmt::Display for StoreVersionIncompatibility {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::RecordingTooOld {
recording_major,
supported_major,
last_nextest_version,
} => {
write!(
f,
"recording has major version {recording_major}, \
but this nextest requires version {supported_major}"
)?;
if let Some(version) = last_nextest_version {
write!(f, " (use nextest <= {version} to replay this recording)")?;
}
Ok(())
}
Self::RecordingTooNew {
recording_major,
supported_major,
} => {
write!(
f,
"recording has major version {recording_major}, \
but this nextest only supports version {supported_major} \
(upgrade nextest to replay this recording)"
)
}
Self::MinorTooNew {
recording_minor,
supported_minor,
} => {
write!(
f,
"minor version {} is newer than supported version {}",
recording_minor, supported_minor
)
}
}
}
}
pub(super) const RUNS_JSON_FORMAT_VERSION: RunsJsonFormatVersion = RunsJsonFormatVersion::new(2);
pub const STORE_FORMAT_VERSION: StoreFormatVersion = StoreFormatVersion::new(
StoreFormatMajorVersion::new(2),
StoreFormatMinorVersion::new(0),
);
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RunsJsonWritePermission {
Allowed,
Denied {
file_version: RunsJsonFormatVersion,
max_supported_version: RunsJsonFormatVersion,
},
}
#[derive(Debug, Deserialize, Serialize)]
#[serde(rename_all = "kebab-case")]
pub(super) struct RecordedRunList {
pub(super) format_version: RunsJsonFormatVersion,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub(super) last_pruned_at: Option<DateTime<Utc>>,
#[serde(default)]
pub(super) runs: Vec<RecordedRun>,
}
pub(super) struct RunListData {
pub(super) runs: Vec<RecordedRunInfo>,
pub(super) last_pruned_at: Option<DateTime<Utc>>,
}
impl RecordedRunList {
#[cfg(test)]
fn new() -> Self {
Self {
format_version: RUNS_JSON_FORMAT_VERSION,
last_pruned_at: None,
runs: Vec::new(),
}
}
pub(super) fn into_data(self) -> RunListData {
RunListData {
runs: self.runs.into_iter().map(RecordedRunInfo::from).collect(),
last_pruned_at: self.last_pruned_at,
}
}
pub(super) fn from_data(
runs: &[RecordedRunInfo],
last_pruned_at: Option<DateTime<Utc>>,
) -> Self {
Self {
format_version: RUNS_JSON_FORMAT_VERSION,
last_pruned_at,
runs: runs.iter().map(RecordedRun::from).collect(),
}
}
pub(super) fn write_permission(&self) -> RunsJsonWritePermission {
if self.format_version > RUNS_JSON_FORMAT_VERSION {
RunsJsonWritePermission::Denied {
file_version: self.format_version,
max_supported_version: RUNS_JSON_FORMAT_VERSION,
}
} else {
RunsJsonWritePermission::Allowed
}
}
}
#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(rename_all = "kebab-case")]
pub(super) struct RecordedRun {
pub(super) run_id: ReportUuid,
pub(super) store_format_version: StoreFormatMajorVersion,
#[serde(default)]
pub(super) store_format_minor_version: StoreFormatMinorVersion,
pub(super) nextest_version: Version,
pub(super) started_at: DateTime<FixedOffset>,
pub(super) last_written_at: DateTime<FixedOffset>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub(super) duration_secs: Option<f64>,
#[serde(default)]
pub(super) cli_args: Vec<String>,
#[serde(default)]
pub(super) build_scope_args: Vec<String>,
#[serde(default)]
pub(super) env_vars: BTreeMap<String, String>,
#[serde(default)]
pub(super) parent_run_id: Option<ReportUuid>,
pub(super) sizes: RecordedSizesFormat,
pub(super) status: RecordedRunStatusFormat,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Deserialize, Serialize)]
#[serde(rename_all = "kebab-case")]
pub(super) struct RecordedSizesFormat {
pub(super) log: ComponentSizesFormat,
pub(super) store: ComponentSizesFormat,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Deserialize, Serialize)]
#[serde(rename_all = "kebab-case")]
pub(super) struct ComponentSizesFormat {
pub(super) compressed: u64,
pub(super) uncompressed: u64,
#[serde(default)]
pub(super) entries: u64,
}
impl From<RecordedSizes> for RecordedSizesFormat {
fn from(sizes: RecordedSizes) -> Self {
Self {
log: ComponentSizesFormat {
compressed: sizes.log.compressed,
uncompressed: sizes.log.uncompressed,
entries: sizes.log.entries,
},
store: ComponentSizesFormat {
compressed: sizes.store.compressed,
uncompressed: sizes.store.uncompressed,
entries: sizes.store.entries,
},
}
}
}
impl From<RecordedSizesFormat> for RecordedSizes {
fn from(sizes: RecordedSizesFormat) -> Self {
Self {
log: ComponentSizes {
compressed: sizes.log.compressed,
uncompressed: sizes.log.uncompressed,
entries: sizes.log.entries,
},
store: ComponentSizes {
compressed: sizes.store.compressed,
uncompressed: sizes.store.uncompressed,
entries: sizes.store.entries,
},
}
}
}
#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(tag = "status", rename_all = "kebab-case")]
pub(super) enum RecordedRunStatusFormat {
Incomplete,
#[serde(rename_all = "kebab-case")]
Completed {
initial_run_count: usize,
passed: usize,
failed: usize,
exit_code: i32,
},
#[serde(rename_all = "kebab-case")]
Cancelled {
initial_run_count: usize,
passed: usize,
failed: usize,
exit_code: i32,
},
#[serde(rename_all = "kebab-case")]
StressCompleted {
initial_iteration_count: Option<NonZero<u32>>,
success_count: u32,
failed_count: u32,
exit_code: i32,
},
#[serde(rename_all = "kebab-case")]
StressCancelled {
initial_iteration_count: Option<NonZero<u32>>,
success_count: u32,
failed_count: u32,
exit_code: i32,
},
#[serde(other)]
Unknown,
}
impl From<RecordedRun> for RecordedRunInfo {
fn from(run: RecordedRun) -> Self {
Self {
run_id: run.run_id,
store_format_version: StoreFormatVersion::new(
run.store_format_version,
run.store_format_minor_version,
),
nextest_version: run.nextest_version,
started_at: run.started_at,
last_written_at: run.last_written_at,
duration_secs: run.duration_secs,
cli_args: run.cli_args,
build_scope_args: run.build_scope_args,
env_vars: run.env_vars,
parent_run_id: run.parent_run_id,
sizes: run.sizes.into(),
status: run.status.into(),
}
}
}
impl From<&RecordedRunInfo> for RecordedRun {
fn from(run: &RecordedRunInfo) -> Self {
Self {
run_id: run.run_id,
store_format_version: run.store_format_version.major,
store_format_minor_version: run.store_format_version.minor,
nextest_version: run.nextest_version.clone(),
started_at: run.started_at,
last_written_at: run.last_written_at,
duration_secs: run.duration_secs,
cli_args: run.cli_args.clone(),
build_scope_args: run.build_scope_args.clone(),
env_vars: run.env_vars.clone(),
parent_run_id: run.parent_run_id,
sizes: run.sizes.into(),
status: (&run.status).into(),
}
}
}
impl From<RecordedRunStatusFormat> for RecordedRunStatus {
fn from(status: RecordedRunStatusFormat) -> Self {
match status {
RecordedRunStatusFormat::Incomplete => Self::Incomplete,
RecordedRunStatusFormat::Unknown => Self::Unknown,
RecordedRunStatusFormat::Completed {
initial_run_count,
passed,
failed,
exit_code,
} => Self::Completed(CompletedRunStats {
initial_run_count,
passed,
failed,
exit_code,
}),
RecordedRunStatusFormat::Cancelled {
initial_run_count,
passed,
failed,
exit_code,
} => Self::Cancelled(CompletedRunStats {
initial_run_count,
passed,
failed,
exit_code,
}),
RecordedRunStatusFormat::StressCompleted {
initial_iteration_count,
success_count,
failed_count,
exit_code,
} => Self::StressCompleted(StressCompletedRunStats {
initial_iteration_count,
success_count,
failed_count,
exit_code,
}),
RecordedRunStatusFormat::StressCancelled {
initial_iteration_count,
success_count,
failed_count,
exit_code,
} => Self::StressCancelled(StressCompletedRunStats {
initial_iteration_count,
success_count,
failed_count,
exit_code,
}),
}
}
}
impl From<&RecordedRunStatus> for RecordedRunStatusFormat {
fn from(status: &RecordedRunStatus) -> Self {
match status {
RecordedRunStatus::Incomplete => Self::Incomplete,
RecordedRunStatus::Unknown => Self::Unknown,
RecordedRunStatus::Completed(stats) => Self::Completed {
initial_run_count: stats.initial_run_count,
passed: stats.passed,
failed: stats.failed,
exit_code: stats.exit_code,
},
RecordedRunStatus::Cancelled(stats) => Self::Cancelled {
initial_run_count: stats.initial_run_count,
passed: stats.passed,
failed: stats.failed,
exit_code: stats.exit_code,
},
RecordedRunStatus::StressCompleted(stats) => Self::StressCompleted {
initial_iteration_count: stats.initial_iteration_count,
success_count: stats.success_count,
failed_count: stats.failed_count,
exit_code: stats.exit_code,
},
RecordedRunStatus::StressCancelled(stats) => Self::StressCancelled {
initial_iteration_count: stats.initial_iteration_count,
success_count: stats.success_count,
failed_count: stats.failed_count,
exit_code: stats.exit_code,
},
}
}
}
#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(rename_all = "kebab-case")]
pub struct RerunInfo {
pub parent_run_id: ReportUuid,
pub root_info: RerunRootInfo,
pub test_suites: IdOrdMap<RerunTestSuiteInfo>,
}
#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(rename_all = "kebab-case")]
pub struct RerunRootInfo {
pub run_id: ReportUuid,
pub build_scope_args: Vec<String>,
}
impl RerunRootInfo {
pub fn new(run_id: ReportUuid, build_scope_args: Vec<String>) -> Self {
Self {
run_id,
build_scope_args,
}
}
}
#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
pub struct RerunTestSuiteInfo {
pub binary_id: RustBinaryId,
pub passing: BTreeSet<TestCaseName>,
pub outstanding: BTreeSet<TestCaseName>,
}
impl RerunTestSuiteInfo {
pub(super) fn new(binary_id: RustBinaryId) -> Self {
Self {
binary_id,
passing: BTreeSet::new(),
outstanding: BTreeSet::new(),
}
}
}
impl IdOrdItem for RerunTestSuiteInfo {
type Key<'a> = &'a RustBinaryId;
fn key(&self) -> Self::Key<'_> {
&self.binary_id
}
id_upcast!();
}
pub static STORE_ZIP_FILE_NAME: &str = "store.zip";
pub static RUN_LOG_FILE_NAME: &str = "run.log.zst";
pub fn has_zip_extension(path: &Utf8Path) -> bool {
path.extension()
.is_some_and(|ext| ext.eq_ignore_ascii_case("zip"))
}
pub static CARGO_METADATA_JSON_PATH: &str = "meta/cargo-metadata.json";
pub static TEST_LIST_JSON_PATH: &str = "meta/test-list.json";
pub static RECORD_OPTS_JSON_PATH: &str = "meta/record-opts.json";
pub static RERUN_INFO_JSON_PATH: &str = "meta/rerun-info.json";
pub static STDOUT_DICT_PATH: &str = "meta/stdout.dict";
pub static STDERR_DICT_PATH: &str = "meta/stderr.dict";
define_format_version! {
pub struct PortableRecordingFormatMajorVersion;
}
define_format_version! {
@default
pub struct PortableRecordingFormatMinorVersion;
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Deserialize, Serialize)]
pub struct PortableRecordingFormatVersion {
pub major: PortableRecordingFormatMajorVersion,
pub minor: PortableRecordingFormatMinorVersion,
}
impl PortableRecordingFormatVersion {
pub const fn new(
major: PortableRecordingFormatMajorVersion,
minor: PortableRecordingFormatMinorVersion,
) -> Self {
Self { major, minor }
}
pub fn check_readable_by(
self,
supported: Self,
) -> Result<(), PortableRecordingVersionIncompatibility> {
if self.major != supported.major {
return Err(PortableRecordingVersionIncompatibility::MajorMismatch {
recording_major: self.major,
supported_major: supported.major,
});
}
if self.minor > supported.minor {
return Err(PortableRecordingVersionIncompatibility::MinorTooNew {
recording_minor: self.minor,
supported_minor: supported.minor,
});
}
Ok(())
}
}
impl fmt::Display for PortableRecordingFormatVersion {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}.{}", self.major, self.minor)
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum PortableRecordingVersionIncompatibility {
MajorMismatch {
recording_major: PortableRecordingFormatMajorVersion,
supported_major: PortableRecordingFormatMajorVersion,
},
MinorTooNew {
recording_minor: PortableRecordingFormatMinorVersion,
supported_minor: PortableRecordingFormatMinorVersion,
},
}
impl fmt::Display for PortableRecordingVersionIncompatibility {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::MajorMismatch {
recording_major,
supported_major,
} => {
write!(
f,
"major version {} differs from supported version {}",
recording_major, supported_major
)
}
Self::MinorTooNew {
recording_minor,
supported_minor,
} => {
write!(
f,
"minor version {} is newer than supported version {}",
recording_minor, supported_minor
)
}
}
}
}
pub const PORTABLE_RECORDING_FORMAT_VERSION: PortableRecordingFormatVersion =
PortableRecordingFormatVersion::new(
PortableRecordingFormatMajorVersion::new(1),
PortableRecordingFormatMinorVersion::new(0),
);
pub static PORTABLE_MANIFEST_FILE_NAME: &str = "manifest.json";
#[derive(Debug, Deserialize, Serialize)]
#[serde(rename_all = "kebab-case")]
pub(crate) struct PortableManifest {
pub(crate) format_version: PortableRecordingFormatVersion,
pub(super) run: RecordedRun,
}
impl PortableManifest {
pub(crate) fn new(run: &RecordedRunInfo) -> Self {
Self {
format_version: PORTABLE_RECORDING_FORMAT_VERSION,
run: RecordedRun::from(run),
}
}
pub(crate) fn run_info(&self) -> RecordedRunInfo {
RecordedRunInfo::from(self.run.clone())
}
pub(crate) fn store_format_version(&self) -> StoreFormatVersion {
StoreFormatVersion::new(
self.run.store_format_version,
self.run.store_format_minor_version,
)
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum OutputDict {
Stdout,
Stderr,
None,
}
impl OutputDict {
pub fn for_path(path: &Utf8Path) -> Self {
let mut iter = path.iter();
let Some(first_component) = iter.next() else {
return Self::None;
};
if first_component != "out" {
return Self::None;
}
Self::for_output_file_name(iter.as_path().as_str())
}
pub fn for_output_file_name(file_name: &str) -> Self {
if file_name.ends_with("-stdout") || file_name.ends_with("-combined") {
Self::Stdout
} else if file_name.ends_with("-stderr") {
Self::Stderr
} else {
Self::None
}
}
pub fn dict_bytes(self) -> Option<&'static [u8]> {
match self {
Self::Stdout => Some(super::dicts::STDOUT),
Self::Stderr => Some(super::dicts::STDERR),
Self::None => None,
}
}
}
pub(super) fn stored_file_options() -> FileOptions {
let mut options = FileOptions::default();
options.compression_method = CompressionMethod::STORE;
options
}
pub(super) fn zstd_file_options() -> FileOptions {
let mut options = FileOptions::default();
options.compression_method = CompressionMethod::ZSTD;
options.level = Some(3);
options
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_output_dict_for_path() {
assert_eq!(
OutputDict::for_path("meta/cargo-metadata.json".as_ref()),
OutputDict::None
);
assert_eq!(
OutputDict::for_path("meta/test-list.json".as_ref()),
OutputDict::None
);
assert_eq!(
OutputDict::for_path("out/0123456789abcdef-stdout".as_ref()),
OutputDict::Stdout
);
assert_eq!(
OutputDict::for_path("out/0123456789abcdef-stderr".as_ref()),
OutputDict::Stderr
);
assert_eq!(
OutputDict::for_path("out/0123456789abcdef-combined".as_ref()),
OutputDict::Stdout
);
}
#[test]
fn test_output_dict_for_output_file_name() {
assert_eq!(
OutputDict::for_output_file_name("0123456789abcdef-stdout"),
OutputDict::Stdout
);
assert_eq!(
OutputDict::for_output_file_name("0123456789abcdef-stderr"),
OutputDict::Stderr
);
assert_eq!(
OutputDict::for_output_file_name("0123456789abcdef-combined"),
OutputDict::Stdout
);
assert_eq!(
OutputDict::for_output_file_name("0123456789abcdef-unknown"),
OutputDict::None
);
}
#[test]
fn test_dict_bytes() {
assert!(OutputDict::Stdout.dict_bytes().is_some());
assert!(OutputDict::Stderr.dict_bytes().is_some());
assert!(OutputDict::None.dict_bytes().is_none());
}
#[test]
fn test_runs_json_missing_version() {
let json = r#"{"runs": []}"#;
let result: Result<RecordedRunList, _> = serde_json::from_str(json);
assert!(result.is_err(), "expected error for missing format-version");
}
#[test]
fn test_runs_json_current_version() {
let json = format!(
r#"{{"format-version": {}, "runs": []}}"#,
RUNS_JSON_FORMAT_VERSION
);
let list: RecordedRunList = serde_json::from_str(&json).expect("should deserialize");
assert_eq!(list.write_permission(), RunsJsonWritePermission::Allowed);
}
#[test]
fn test_runs_json_older_version() {
let json = r#"{"format-version": 1, "runs": []}"#;
let list: RecordedRunList = serde_json::from_str(json).expect("should deserialize");
assert_eq!(list.write_permission(), RunsJsonWritePermission::Allowed);
}
#[test]
fn test_runs_json_newer_version() {
let json = r#"{"format-version": 99, "runs": []}"#;
let list: RecordedRunList = serde_json::from_str(json).expect("should deserialize");
assert_eq!(
list.write_permission(),
RunsJsonWritePermission::Denied {
file_version: RunsJsonFormatVersion::new(99),
max_supported_version: RUNS_JSON_FORMAT_VERSION,
}
);
}
#[test]
fn test_runs_json_serialization_includes_version() {
let list = RecordedRunList::from_data(&[], None);
let json = serde_json::to_string(&list).expect("should serialize");
assert!(
json.contains("format-version"),
"serialized runs.json.zst should include format-version"
);
let parsed: serde_json::Value = serde_json::from_str(&json).expect("should parse");
let version: RunsJsonFormatVersion =
serde_json::from_value(parsed["format-version"].clone()).expect("valid version");
assert_eq!(
version, RUNS_JSON_FORMAT_VERSION,
"format-version should be current version"
);
}
#[test]
fn test_runs_json_new() {
let list = RecordedRunList::new();
assert_eq!(list.format_version, RUNS_JSON_FORMAT_VERSION);
assert!(list.runs.is_empty());
assert_eq!(list.write_permission(), RunsJsonWritePermission::Allowed);
}
fn make_test_run(status: RecordedRunStatusFormat) -> RecordedRun {
RecordedRun {
run_id: ReportUuid::from_u128(0x550e8400_e29b_41d4_a716_446655440000),
store_format_version: STORE_FORMAT_VERSION.major,
store_format_minor_version: STORE_FORMAT_VERSION.minor,
nextest_version: Version::new(0, 9, 111),
started_at: DateTime::parse_from_rfc3339("2024-12-19T14:22:33-08:00")
.expect("valid timestamp"),
last_written_at: DateTime::parse_from_rfc3339("2024-12-19T22:22:33Z")
.expect("valid timestamp"),
duration_secs: Some(12.345),
cli_args: vec![
"cargo".to_owned(),
"nextest".to_owned(),
"run".to_owned(),
"--workspace".to_owned(),
],
build_scope_args: vec!["--workspace".to_owned()],
env_vars: BTreeMap::from([
("CARGO_TERM_COLOR".to_owned(), "always".to_owned()),
("NEXTEST_PROFILE".to_owned(), "ci".to_owned()),
]),
parent_run_id: Some(ReportUuid::from_u128(
0x550e7400_e29b_41d4_a716_446655440000,
)),
sizes: RecordedSizesFormat {
log: ComponentSizesFormat {
compressed: 2345,
uncompressed: 5678,
entries: 42,
},
store: ComponentSizesFormat {
compressed: 10000,
uncompressed: 40000,
entries: 15,
},
},
status,
}
}
#[test]
fn test_recorded_run_serialize_incomplete() {
let run = make_test_run(RecordedRunStatusFormat::Incomplete);
let json = serde_json::to_string_pretty(&run).expect("serialization should succeed");
insta::assert_snapshot!(json);
}
#[test]
fn test_recorded_run_serialize_completed() {
let run = make_test_run(RecordedRunStatusFormat::Completed {
initial_run_count: 100,
passed: 95,
failed: 5,
exit_code: 0,
});
let json = serde_json::to_string_pretty(&run).expect("serialization should succeed");
insta::assert_snapshot!(json);
}
#[test]
fn test_recorded_run_serialize_cancelled() {
let run = make_test_run(RecordedRunStatusFormat::Cancelled {
initial_run_count: 100,
passed: 45,
failed: 5,
exit_code: 100,
});
let json = serde_json::to_string_pretty(&run).expect("serialization should succeed");
insta::assert_snapshot!(json);
}
#[test]
fn test_recorded_run_serialize_stress_completed() {
let run = make_test_run(RecordedRunStatusFormat::StressCompleted {
initial_iteration_count: NonZero::new(100),
success_count: 98,
failed_count: 2,
exit_code: 0,
});
let json = serde_json::to_string_pretty(&run).expect("serialization should succeed");
insta::assert_snapshot!(json);
}
#[test]
fn test_recorded_run_serialize_stress_cancelled() {
let run = make_test_run(RecordedRunStatusFormat::StressCancelled {
initial_iteration_count: NonZero::new(100),
success_count: 45,
failed_count: 5,
exit_code: 100,
});
let json = serde_json::to_string_pretty(&run).expect("serialization should succeed");
insta::assert_snapshot!(json);
}
#[test]
fn test_recorded_run_deserialize_unknown_status() {
let json = r#"{
"run-id": "550e8400-e29b-41d4-a716-446655440000",
"store-format-version": 999,
"nextest-version": "0.9.999",
"started-at": "2024-12-19T14:22:33-08:00",
"last-written-at": "2024-12-19T22:22:33Z",
"cli-args": ["cargo", "nextest", "run"],
"env-vars": {},
"sizes": {
"log": { "compressed": 2345, "uncompressed": 5678 },
"store": { "compressed": 10000, "uncompressed": 40000 }
},
"status": {
"status": "super-new-status",
"some-future-field": 42
}
}"#;
let run: RecordedRun = serde_json::from_str(json).expect("should deserialize");
assert!(
matches!(run.status, RecordedRunStatusFormat::Unknown),
"unknown status should deserialize to Unknown variant"
);
let info: RecordedRunInfo = run.into();
assert!(
matches!(info.status, RecordedRunStatus::Unknown),
"Unknown format should convert to Unknown domain type"
);
}
#[test]
fn test_recorded_run_roundtrip() {
let original = make_test_run(RecordedRunStatusFormat::Completed {
initial_run_count: 100,
passed: 95,
failed: 5,
exit_code: 0,
});
let json = serde_json::to_string(&original).expect("serialization should succeed");
let roundtripped: RecordedRun =
serde_json::from_str(&json).expect("deserialization should succeed");
assert_eq!(roundtripped.run_id, original.run_id);
assert_eq!(roundtripped.nextest_version, original.nextest_version);
assert_eq!(roundtripped.started_at, original.started_at);
assert_eq!(roundtripped.sizes, original.sizes);
let info: RecordedRunInfo = roundtripped.into();
match info.status {
RecordedRunStatus::Completed(stats) => {
assert_eq!(stats.initial_run_count, 100);
assert_eq!(stats.passed, 95);
assert_eq!(stats.failed, 5);
}
_ => panic!("expected Completed variant"),
}
}
fn version(major: u32, minor: u32) -> StoreFormatVersion {
StoreFormatVersion::new(
StoreFormatMajorVersion::new(major),
StoreFormatMinorVersion::new(minor),
)
}
#[test]
fn test_store_version_compatibility() {
assert!(
version(1, 0).check_readable_by(version(1, 0)).is_ok(),
"same version should be compatible"
);
assert!(
version(1, 0).check_readable_by(version(1, 2)).is_ok(),
"older minor version should be compatible"
);
let error = version(1, 3).check_readable_by(version(1, 2)).unwrap_err();
assert_eq!(
error,
StoreVersionIncompatibility::MinorTooNew {
recording_minor: StoreFormatMinorVersion::new(3),
supported_minor: StoreFormatMinorVersion::new(2),
},
"newer minor version should be incompatible"
);
insta::assert_snapshot!(error.to_string(), @"minor version 3 is newer than supported version 2");
let error = version(2, 0).check_readable_by(version(1, 5)).unwrap_err();
assert_eq!(
error,
StoreVersionIncompatibility::RecordingTooNew {
recording_major: StoreFormatMajorVersion::new(2),
supported_major: StoreFormatMajorVersion::new(1),
},
);
insta::assert_snapshot!(
error.to_string(),
@"recording has major version 2, but this nextest only supports version 1 (upgrade nextest to replay this recording)"
);
let error = version(1, 0).check_readable_by(version(2, 0)).unwrap_err();
assert_eq!(
error,
StoreVersionIncompatibility::RecordingTooOld {
recording_major: StoreFormatMajorVersion::new(1),
supported_major: StoreFormatMajorVersion::new(2),
last_nextest_version: Some("0.9.130"),
},
);
insta::assert_snapshot!(
error.to_string(),
@"recording has major version 1, but this nextest requires version 2 (use nextest <= 0.9.130 to replay this recording)"
);
let error = version(3, 0).check_readable_by(version(5, 0)).unwrap_err();
assert_eq!(
error,
StoreVersionIncompatibility::RecordingTooOld {
recording_major: StoreFormatMajorVersion::new(3),
supported_major: StoreFormatMajorVersion::new(5),
last_nextest_version: None,
},
);
insta::assert_snapshot!(
error.to_string(),
@"recording has major version 3, but this nextest requires version 5"
);
insta::assert_snapshot!(version(1, 2).to_string(), @"1.2");
}
#[test]
fn test_recorded_run_deserialize_without_minor_version() {
let json = r#"{
"run-id": "550e8400-e29b-41d4-a716-446655440000",
"store-format-version": 1,
"nextest-version": "0.9.111",
"started-at": "2024-12-19T14:22:33-08:00",
"last-written-at": "2024-12-19T22:22:33Z",
"cli-args": [],
"env-vars": {},
"sizes": {
"log": { "compressed": 0, "uncompressed": 0 },
"store": { "compressed": 0, "uncompressed": 0 }
},
"status": { "status": "incomplete" }
}"#;
let run: RecordedRun = serde_json::from_str(json).expect("should deserialize");
assert_eq!(run.store_format_version, StoreFormatMajorVersion::new(1));
assert_eq!(
run.store_format_minor_version,
StoreFormatMinorVersion::new(0)
);
let info: RecordedRunInfo = run.into();
assert_eq!(info.store_format_version, version(1, 0));
}
#[test]
fn test_recorded_run_serialize_includes_minor_version() {
let run = make_test_run(RecordedRunStatusFormat::Incomplete);
let json = serde_json::to_string_pretty(&run).expect("serialization should succeed");
assert!(
json.contains("store-format-minor-version"),
"serialized run should include store-format-minor-version"
);
}
fn portable_version(major: u32, minor: u32) -> PortableRecordingFormatVersion {
PortableRecordingFormatVersion::new(
PortableRecordingFormatMajorVersion::new(major),
PortableRecordingFormatMinorVersion::new(minor),
)
}
#[test]
fn test_portable_version_compatibility() {
assert!(
portable_version(1, 0)
.check_readable_by(portable_version(1, 0))
.is_ok(),
"same version should be compatible"
);
assert!(
portable_version(1, 0)
.check_readable_by(portable_version(1, 2))
.is_ok(),
"older minor version should be compatible"
);
let error = portable_version(1, 3)
.check_readable_by(portable_version(1, 2))
.unwrap_err();
assert_eq!(
error,
PortableRecordingVersionIncompatibility::MinorTooNew {
recording_minor: PortableRecordingFormatMinorVersion::new(3),
supported_minor: PortableRecordingFormatMinorVersion::new(2),
},
"newer minor version should be incompatible"
);
insta::assert_snapshot!(error.to_string(), @"minor version 3 is newer than supported version 2");
let error = portable_version(2, 0)
.check_readable_by(portable_version(1, 5))
.unwrap_err();
assert_eq!(
error,
PortableRecordingVersionIncompatibility::MajorMismatch {
recording_major: PortableRecordingFormatMajorVersion::new(2),
supported_major: PortableRecordingFormatMajorVersion::new(1),
},
"different major version should be incompatible"
);
insta::assert_snapshot!(error.to_string(), @"major version 2 differs from supported version 1");
insta::assert_snapshot!(portable_version(1, 2).to_string(), @"1.2");
}
#[test]
fn test_portable_version_serialization() {
let version = portable_version(1, 0);
let json = serde_json::to_string(&version).expect("serialization should succeed");
insta::assert_snapshot!(json, @r#"{"major":1,"minor":0}"#);
let roundtripped: PortableRecordingFormatVersion =
serde_json::from_str(&json).expect("deserialization should succeed");
assert_eq!(roundtripped, version);
}
#[test]
fn test_portable_manifest_format_version() {
assert_eq!(
PORTABLE_RECORDING_FORMAT_VERSION,
portable_version(1, 0),
"current portable recording format version should be 1.0"
);
}
}