1use super::{
7 CompletedRunStats, ComponentSizes, RecordedRunInfo, RecordedRunStatus, RecordedSizes,
8 StressCompletedRunStats,
9};
10use camino::Utf8Path;
11use chrono::{DateTime, FixedOffset, Utc};
12use eazip::{CompressionMethod, write::FileOptions};
13use iddqd::{IdOrdItem, IdOrdMap, id_upcast};
14use nextest_metadata::{RustBinaryId, TestCaseName};
15use quick_junit::ReportUuid;
16use semver::Version;
17use serde::{Deserialize, Serialize};
18use std::{
19 collections::{BTreeMap, BTreeSet},
20 fmt,
21 num::NonZero,
22};
23
24macro_rules! define_format_version {
32 (
33 $(#[$attr:meta])*
34 $vis:vis struct $name:ident;
35 ) => {
36 $(#[$attr])*
37 #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Deserialize, Serialize)]
38 #[serde(transparent)]
39 $vis struct $name(u32);
40
41 impl $name {
42 #[doc = concat!("Creates a new `", stringify!($name), "`.")]
43 pub const fn new(version: u32) -> Self {
44 Self(version)
45 }
46 }
47
48 impl fmt::Display for $name {
49 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
50 write!(f, "{}", self.0)
51 }
52 }
53 };
54
55 (
56 @default
57 $(#[$attr:meta])*
58 $vis:vis struct $name:ident;
59 ) => {
60 $(#[$attr])*
61 #[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Deserialize, Serialize)]
62 #[serde(transparent)]
63 $vis struct $name(u32);
64
65 impl $name {
66 #[doc = concat!("Creates a new `", stringify!($name), "`.")]
67 pub const fn new(version: u32) -> Self {
68 Self(version)
69 }
70 }
71
72 impl fmt::Display for $name {
73 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
74 write!(f, "{}", self.0)
75 }
76 }
77 };
78}
79
80define_format_version! {
81 pub struct RunsJsonFormatVersion;
88}
89
90define_format_version! {
91 pub struct StoreFormatMajorVersion;
94}
95
96define_format_version! {
97 @default
98 pub struct StoreFormatMinorVersion;
100}
101
102#[derive(Clone, Copy, Debug, PartialEq, Eq)]
104pub struct StoreFormatVersion {
105 pub major: StoreFormatMajorVersion,
107 pub minor: StoreFormatMinorVersion,
109}
110
111impl StoreFormatVersion {
112 pub const fn new(major: StoreFormatMajorVersion, minor: StoreFormatMinorVersion) -> Self {
114 Self { major, minor }
115 }
116
117 pub fn check_readable_by(self, supported: Self) -> Result<(), StoreVersionIncompatibility> {
120 if self.major < supported.major {
121 return Err(StoreVersionIncompatibility::RecordingTooOld {
122 recording_major: self.major,
123 supported_major: supported.major,
124 last_nextest_version: self.major.last_nextest_version(),
125 });
126 }
127 if self.major > supported.major {
128 return Err(StoreVersionIncompatibility::RecordingTooNew {
129 recording_major: self.major,
130 supported_major: supported.major,
131 });
132 }
133 if self.minor > supported.minor {
134 return Err(StoreVersionIncompatibility::MinorTooNew {
135 recording_minor: self.minor,
136 supported_minor: supported.minor,
137 });
138 }
139 Ok(())
140 }
141}
142
143impl fmt::Display for StoreFormatVersion {
144 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
145 write!(f, "{}.{}", self.major, self.minor)
146 }
147}
148
149impl StoreFormatMajorVersion {
150 pub fn last_nextest_version(self) -> Option<&'static str> {
156 match self.0 {
157 1 => Some("0.9.130"),
158 _ => None,
159 }
160 }
161}
162
163#[derive(Clone, Debug, PartialEq, Eq)]
166pub enum StoreVersionIncompatibility {
167 RecordingTooOld {
169 recording_major: StoreFormatMajorVersion,
171 supported_major: StoreFormatMajorVersion,
173 last_nextest_version: Option<&'static str>,
176 },
177 RecordingTooNew {
179 recording_major: StoreFormatMajorVersion,
181 supported_major: StoreFormatMajorVersion,
183 },
184 MinorTooNew {
186 recording_minor: StoreFormatMinorVersion,
188 supported_minor: StoreFormatMinorVersion,
190 },
191}
192
193impl fmt::Display for StoreVersionIncompatibility {
194 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
195 match self {
196 Self::RecordingTooOld {
197 recording_major,
198 supported_major,
199 last_nextest_version,
200 } => {
201 write!(
202 f,
203 "recording has major version {recording_major}, \
204 but this nextest requires version {supported_major}"
205 )?;
206 if let Some(version) = last_nextest_version {
207 write!(f, " (use nextest <= {version} to replay this recording)")?;
208 }
209 Ok(())
210 }
211 Self::RecordingTooNew {
212 recording_major,
213 supported_major,
214 } => {
215 write!(
216 f,
217 "recording has major version {recording_major}, \
218 but this nextest only supports version {supported_major} \
219 (upgrade nextest to replay this recording)"
220 )
221 }
222 Self::MinorTooNew {
223 recording_minor,
224 supported_minor,
225 } => {
226 write!(
227 f,
228 "minor version {} is newer than supported version {}",
229 recording_minor, supported_minor
230 )
231 }
232 }
233 }
234}
235
236pub(super) const RUNS_JSON_FORMAT_VERSION: RunsJsonFormatVersion = RunsJsonFormatVersion::new(2);
242
243pub const STORE_FORMAT_VERSION: StoreFormatVersion = StoreFormatVersion::new(
258 StoreFormatMajorVersion::new(2),
259 StoreFormatMinorVersion::new(1),
260);
261
262pub(super) const FORCE_STORE_FORMAT_VERSION_ENV: &str = "__NEXTEST_FORCE_STORE_FORMAT_VERSION";
272
273pub(super) fn store_format_version_for_new_run() -> StoreFormatVersion {
275 let Some(raw) = std::env::var_os(FORCE_STORE_FORMAT_VERSION_ENV) else {
276 return STORE_FORMAT_VERSION;
277 };
278 let raw = raw.to_str().unwrap_or_else(|| {
279 panic!("{FORCE_STORE_FORMAT_VERSION_ENV} contains non-UTF-8 bytes");
280 });
281 let (major, minor) = raw.split_once('.').unwrap_or_else(|| {
282 panic!(
283 "{FORCE_STORE_FORMAT_VERSION_ENV}={raw:?} is malformed \
284 (expected MAJOR.MINOR, e.g. 9999.0)"
285 )
286 });
287 let major: u32 = major.parse().unwrap_or_else(|err| {
288 panic!("{FORCE_STORE_FORMAT_VERSION_ENV}={raw:?} has invalid major version: {err}")
289 });
290 let minor: u32 = minor.parse().unwrap_or_else(|err| {
291 panic!("{FORCE_STORE_FORMAT_VERSION_ENV}={raw:?} has invalid minor version: {err}")
292 });
293 StoreFormatVersion::new(
294 StoreFormatMajorVersion::new(major),
295 StoreFormatMinorVersion::new(minor),
296 )
297}
298
299#[derive(Debug, Clone, Copy, PartialEq, Eq)]
301pub enum RunsJsonWritePermission {
302 Allowed,
304 Denied {
306 file_version: RunsJsonFormatVersion,
308 max_supported_version: RunsJsonFormatVersion,
310 },
311}
312
313#[derive(Debug, Deserialize, Serialize)]
315#[serde(rename_all = "kebab-case")]
316pub(super) struct RecordedRunList {
317 pub(super) format_version: RunsJsonFormatVersion,
319
320 #[serde(default, skip_serializing_if = "Option::is_none")]
325 pub(super) last_pruned_at: Option<DateTime<Utc>>,
326
327 #[serde(default)]
329 pub(super) runs: Vec<RecordedRun>,
330}
331
332pub(super) struct RunListData {
334 pub(super) runs: Vec<RecordedRunInfo>,
335 pub(super) last_pruned_at: Option<DateTime<Utc>>,
336}
337
338impl RecordedRunList {
339 #[cfg(test)]
341 fn new() -> Self {
342 Self {
343 format_version: RUNS_JSON_FORMAT_VERSION,
344 last_pruned_at: None,
345 runs: Vec::new(),
346 }
347 }
348
349 pub(super) fn into_data(self) -> RunListData {
351 RunListData {
352 runs: self.runs.into_iter().map(RecordedRunInfo::from).collect(),
353 last_pruned_at: self.last_pruned_at,
354 }
355 }
356
357 pub(super) fn from_data(
362 runs: &[RecordedRunInfo],
363 last_pruned_at: Option<DateTime<Utc>>,
364 ) -> Self {
365 Self {
366 format_version: RUNS_JSON_FORMAT_VERSION,
367 last_pruned_at,
368 runs: runs.iter().map(RecordedRun::from).collect(),
369 }
370 }
371
372 pub(super) fn write_permission(&self) -> RunsJsonWritePermission {
377 if self.format_version > RUNS_JSON_FORMAT_VERSION {
378 RunsJsonWritePermission::Denied {
379 file_version: self.format_version,
380 max_supported_version: RUNS_JSON_FORMAT_VERSION,
381 }
382 } else {
383 RunsJsonWritePermission::Allowed
384 }
385 }
386}
387
388#[derive(Clone, Debug, Deserialize, Serialize)]
390#[serde(rename_all = "kebab-case")]
391pub(super) struct RecordedRun {
392 pub(super) run_id: ReportUuid,
394 pub(super) store_format_version: StoreFormatMajorVersion,
399 #[serde(default)]
404 pub(super) store_format_minor_version: StoreFormatMinorVersion,
405 pub(super) nextest_version: Version,
407 pub(super) started_at: DateTime<FixedOffset>,
409 pub(super) last_written_at: DateTime<FixedOffset>,
415 #[serde(default, skip_serializing_if = "Option::is_none")]
417 pub(super) duration_secs: Option<f64>,
418 #[serde(default)]
420 pub(super) cli_args: Vec<String>,
421 #[serde(default)]
426 pub(super) build_scope_args: Vec<String>,
427 #[serde(default)]
431 pub(super) env_vars: BTreeMap<String, String>,
432 #[serde(default)]
434 pub(super) parent_run_id: Option<ReportUuid>,
435 pub(super) sizes: RecordedSizesFormat,
439 pub(super) status: RecordedRunStatusFormat,
441}
442
443#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Deserialize, Serialize)]
445#[serde(rename_all = "kebab-case")]
446pub(super) struct RecordedSizesFormat {
447 pub(super) log: ComponentSizesFormat,
449 pub(super) store: ComponentSizesFormat,
451}
452
453#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Deserialize, Serialize)]
455#[serde(rename_all = "kebab-case")]
456pub(super) struct ComponentSizesFormat {
457 pub(super) compressed: u64,
459 pub(super) uncompressed: u64,
461 #[serde(default)]
463 pub(super) entries: u64,
464}
465
466impl From<RecordedSizes> for RecordedSizesFormat {
467 fn from(sizes: RecordedSizes) -> Self {
468 Self {
469 log: ComponentSizesFormat {
470 compressed: sizes.log.compressed,
471 uncompressed: sizes.log.uncompressed,
472 entries: sizes.log.entries,
473 },
474 store: ComponentSizesFormat {
475 compressed: sizes.store.compressed,
476 uncompressed: sizes.store.uncompressed,
477 entries: sizes.store.entries,
478 },
479 }
480 }
481}
482
483impl From<RecordedSizesFormat> for RecordedSizes {
484 fn from(sizes: RecordedSizesFormat) -> Self {
485 Self {
486 log: ComponentSizes {
487 compressed: sizes.log.compressed,
488 uncompressed: sizes.log.uncompressed,
489 entries: sizes.log.entries,
490 },
491 store: ComponentSizes {
492 compressed: sizes.store.compressed,
493 uncompressed: sizes.store.uncompressed,
494 entries: sizes.store.entries,
495 },
496 }
497 }
498}
499
500#[derive(Clone, Debug, Deserialize, Serialize)]
502#[serde(tag = "status", rename_all = "kebab-case")]
503pub(super) enum RecordedRunStatusFormat {
504 Incomplete,
506 #[serde(rename_all = "kebab-case")]
508 Completed {
509 initial_run_count: usize,
511 passed: usize,
513 failed: usize,
515 exit_code: i32,
517 },
518 #[serde(rename_all = "kebab-case")]
520 Cancelled {
521 initial_run_count: usize,
523 passed: usize,
525 failed: usize,
527 exit_code: i32,
529 },
530 #[serde(rename_all = "kebab-case")]
532 StressCompleted {
533 initial_iteration_count: Option<NonZero<u32>>,
535 success_count: u32,
537 failed_count: u32,
539 exit_code: i32,
541 },
542 #[serde(rename_all = "kebab-case")]
544 StressCancelled {
545 initial_iteration_count: Option<NonZero<u32>>,
547 success_count: u32,
549 failed_count: u32,
551 exit_code: i32,
553 },
554 #[serde(other)]
559 Unknown,
560}
561
562impl From<RecordedRun> for RecordedRunInfo {
563 fn from(run: RecordedRun) -> Self {
564 Self {
565 run_id: run.run_id,
566 store_format_version: StoreFormatVersion::new(
567 run.store_format_version,
568 run.store_format_minor_version,
569 ),
570 nextest_version: run.nextest_version,
571 started_at: run.started_at,
572 last_written_at: run.last_written_at,
573 duration_secs: run.duration_secs,
574 cli_args: run.cli_args,
575 build_scope_args: run.build_scope_args,
576 env_vars: run.env_vars,
577 parent_run_id: run.parent_run_id,
578 sizes: run.sizes.into(),
579 status: run.status.into(),
580 }
581 }
582}
583
584impl From<&RecordedRunInfo> for RecordedRun {
585 fn from(run: &RecordedRunInfo) -> Self {
586 Self {
587 run_id: run.run_id,
588 store_format_version: run.store_format_version.major,
589 store_format_minor_version: run.store_format_version.minor,
590 nextest_version: run.nextest_version.clone(),
591 started_at: run.started_at,
592 last_written_at: run.last_written_at,
593 duration_secs: run.duration_secs,
594 cli_args: run.cli_args.clone(),
595 build_scope_args: run.build_scope_args.clone(),
596 env_vars: run.env_vars.clone(),
597 parent_run_id: run.parent_run_id,
598 sizes: run.sizes.into(),
599 status: (&run.status).into(),
600 }
601 }
602}
603
604impl From<RecordedRunStatusFormat> for RecordedRunStatus {
605 fn from(status: RecordedRunStatusFormat) -> Self {
606 match status {
607 RecordedRunStatusFormat::Incomplete => Self::Incomplete,
608 RecordedRunStatusFormat::Unknown => Self::Unknown,
609 RecordedRunStatusFormat::Completed {
610 initial_run_count,
611 passed,
612 failed,
613 exit_code,
614 } => Self::Completed(CompletedRunStats {
615 initial_run_count,
616 passed,
617 failed,
618 exit_code,
619 }),
620 RecordedRunStatusFormat::Cancelled {
621 initial_run_count,
622 passed,
623 failed,
624 exit_code,
625 } => Self::Cancelled(CompletedRunStats {
626 initial_run_count,
627 passed,
628 failed,
629 exit_code,
630 }),
631 RecordedRunStatusFormat::StressCompleted {
632 initial_iteration_count,
633 success_count,
634 failed_count,
635 exit_code,
636 } => Self::StressCompleted(StressCompletedRunStats {
637 initial_iteration_count,
638 success_count,
639 failed_count,
640 exit_code,
641 }),
642 RecordedRunStatusFormat::StressCancelled {
643 initial_iteration_count,
644 success_count,
645 failed_count,
646 exit_code,
647 } => Self::StressCancelled(StressCompletedRunStats {
648 initial_iteration_count,
649 success_count,
650 failed_count,
651 exit_code,
652 }),
653 }
654 }
655}
656
657impl From<&RecordedRunStatus> for RecordedRunStatusFormat {
658 fn from(status: &RecordedRunStatus) -> Self {
659 match status {
660 RecordedRunStatus::Incomplete => Self::Incomplete,
661 RecordedRunStatus::Unknown => Self::Unknown,
662 RecordedRunStatus::Completed(stats) => Self::Completed {
663 initial_run_count: stats.initial_run_count,
664 passed: stats.passed,
665 failed: stats.failed,
666 exit_code: stats.exit_code,
667 },
668 RecordedRunStatus::Cancelled(stats) => Self::Cancelled {
669 initial_run_count: stats.initial_run_count,
670 passed: stats.passed,
671 failed: stats.failed,
672 exit_code: stats.exit_code,
673 },
674 RecordedRunStatus::StressCompleted(stats) => Self::StressCompleted {
675 initial_iteration_count: stats.initial_iteration_count,
676 success_count: stats.success_count,
677 failed_count: stats.failed_count,
678 exit_code: stats.exit_code,
679 },
680 RecordedRunStatus::StressCancelled(stats) => Self::StressCancelled {
681 initial_iteration_count: stats.initial_iteration_count,
682 success_count: stats.success_count,
683 failed_count: stats.failed_count,
684 exit_code: stats.exit_code,
685 },
686 }
687 }
688}
689
690#[derive(Clone, Debug, Deserialize, Serialize)]
698#[serde(rename_all = "kebab-case")]
699pub struct RerunInfo {
700 pub parent_run_id: ReportUuid,
702
703 pub root_info: RerunRootInfo,
705
706 pub test_suites: IdOrdMap<RerunTestSuiteInfo>,
708}
709
710#[derive(Clone, Debug, Deserialize, Serialize)]
712#[serde(rename_all = "kebab-case")]
713pub struct RerunRootInfo {
714 pub run_id: ReportUuid,
716
717 pub build_scope_args: Vec<String>,
719}
720
721impl RerunRootInfo {
722 pub fn new(run_id: ReportUuid, build_scope_args: Vec<String>) -> Self {
728 Self {
729 run_id,
730 build_scope_args,
731 }
732 }
733}
734
735#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
737pub struct RerunTestSuiteInfo {
738 pub binary_id: RustBinaryId,
740
741 pub passing: BTreeSet<TestCaseName>,
743
744 pub outstanding: BTreeSet<TestCaseName>,
746}
747
748impl RerunTestSuiteInfo {
749 pub(super) fn new(binary_id: RustBinaryId) -> Self {
750 Self {
751 binary_id,
752 passing: BTreeSet::new(),
753 outstanding: BTreeSet::new(),
754 }
755 }
756}
757
758impl IdOrdItem for RerunTestSuiteInfo {
759 type Key<'a> = &'a RustBinaryId;
760 fn key(&self) -> Self::Key<'_> {
761 &self.binary_id
762 }
763 id_upcast!();
764}
765
766pub static STORE_ZIP_FILE_NAME: &str = "store.zip";
772
773pub static RUN_LOG_FILE_NAME: &str = "run.log.zst";
775
776pub fn has_zip_extension(path: &Utf8Path) -> bool {
778 path.extension()
779 .is_some_and(|ext| ext.eq_ignore_ascii_case("zip"))
780}
781
782pub static CARGO_METADATA_JSON_PATH: &str = "meta/cargo-metadata.json";
785pub static TEST_LIST_JSON_PATH: &str = "meta/test-list.json";
787pub static RECORD_OPTS_JSON_PATH: &str = "meta/record-opts.json";
789pub static RERUN_INFO_JSON_PATH: &str = "meta/rerun-info.json";
791pub static STDOUT_DICT_PATH: &str = "meta/stdout.dict";
793pub static STDERR_DICT_PATH: &str = "meta/stderr.dict";
795
796define_format_version! {
801 pub struct PortableRecordingFormatMajorVersion;
803}
804
805define_format_version! {
806 @default
807 pub struct PortableRecordingFormatMinorVersion;
809}
810
811#[derive(Clone, Copy, Debug, PartialEq, Eq, Deserialize, Serialize)]
813pub struct PortableRecordingFormatVersion {
814 pub major: PortableRecordingFormatMajorVersion,
816 pub minor: PortableRecordingFormatMinorVersion,
818}
819
820impl PortableRecordingFormatVersion {
821 pub const fn new(
823 major: PortableRecordingFormatMajorVersion,
824 minor: PortableRecordingFormatMinorVersion,
825 ) -> Self {
826 Self { major, minor }
827 }
828
829 pub fn check_readable_by(
832 self,
833 supported: Self,
834 ) -> Result<(), PortableRecordingVersionIncompatibility> {
835 if self.major != supported.major {
836 return Err(PortableRecordingVersionIncompatibility::MajorMismatch {
837 recording_major: self.major,
838 supported_major: supported.major,
839 });
840 }
841 if self.minor > supported.minor {
842 return Err(PortableRecordingVersionIncompatibility::MinorTooNew {
843 recording_minor: self.minor,
844 supported_minor: supported.minor,
845 });
846 }
847 Ok(())
848 }
849}
850
851impl fmt::Display for PortableRecordingFormatVersion {
852 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
853 write!(f, "{}.{}", self.major, self.minor)
854 }
855}
856
857#[derive(Clone, Copy, Debug, PartialEq, Eq)]
860pub enum PortableRecordingVersionIncompatibility {
861 MajorMismatch {
863 recording_major: PortableRecordingFormatMajorVersion,
865 supported_major: PortableRecordingFormatMajorVersion,
867 },
868 MinorTooNew {
870 recording_minor: PortableRecordingFormatMinorVersion,
872 supported_minor: PortableRecordingFormatMinorVersion,
874 },
875}
876
877impl fmt::Display for PortableRecordingVersionIncompatibility {
878 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
879 match self {
880 Self::MajorMismatch {
881 recording_major,
882 supported_major,
883 } => {
884 write!(
885 f,
886 "major version {} differs from supported version {}",
887 recording_major, supported_major
888 )
889 }
890 Self::MinorTooNew {
891 recording_minor,
892 supported_minor,
893 } => {
894 write!(
895 f,
896 "minor version {} is newer than supported version {}",
897 recording_minor, supported_minor
898 )
899 }
900 }
901 }
902}
903
904pub const PORTABLE_RECORDING_FORMAT_VERSION: PortableRecordingFormatVersion =
906 PortableRecordingFormatVersion::new(
907 PortableRecordingFormatMajorVersion::new(1),
908 PortableRecordingFormatMinorVersion::new(0),
909 );
910
911pub static PORTABLE_MANIFEST_FILE_NAME: &str = "manifest.json";
913
914#[derive(Debug, Deserialize, Serialize)]
919#[serde(rename_all = "kebab-case")]
920pub(crate) struct PortableManifest {
921 pub(crate) format_version: PortableRecordingFormatVersion,
923 pub(super) run: RecordedRun,
925}
926
927impl PortableManifest {
928 pub(crate) fn new(run: &RecordedRunInfo) -> Self {
930 Self {
931 format_version: PORTABLE_RECORDING_FORMAT_VERSION,
932 run: RecordedRun::from(run),
933 }
934 }
935
936 pub(crate) fn run_info(&self) -> RecordedRunInfo {
938 RecordedRunInfo::from(self.run.clone())
939 }
940
941 pub(crate) fn store_format_version(&self) -> StoreFormatVersion {
943 StoreFormatVersion::new(
944 self.run.store_format_version,
945 self.run.store_format_minor_version,
946 )
947 }
948}
949
950#[derive(Clone, Copy, Debug, PartialEq, Eq)]
952pub enum OutputDict {
953 Stdout,
955 Stderr,
957 None,
959}
960
961impl OutputDict {
962 pub fn for_path(path: &Utf8Path) -> Self {
970 let mut iter = path.iter();
971 let Some(first_component) = iter.next() else {
972 return Self::None;
973 };
974 if first_component != "out" {
976 return Self::None;
977 }
978
979 Self::for_output_file_name(iter.as_path().as_str())
980 }
981
982 pub fn for_output_file_name(file_name: &str) -> Self {
987 if file_name.ends_with("-stdout") || file_name.ends_with("-combined") {
988 Self::Stdout
989 } else if file_name.ends_with("-stderr") {
990 Self::Stderr
991 } else {
992 Self::None
994 }
995 }
996
997 pub fn dict_bytes(self) -> Option<&'static [u8]> {
1001 match self {
1002 Self::Stdout => Some(super::dicts::STDOUT),
1003 Self::Stderr => Some(super::dicts::STDERR),
1004 Self::None => None,
1005 }
1006 }
1007}
1008
1009pub(super) fn stored_file_options() -> FileOptions {
1016 let mut options = FileOptions::default();
1017 options.compression_method = CompressionMethod::STORE;
1018 options
1019}
1020
1021pub(super) fn zstd_file_options() -> FileOptions {
1023 let mut options = FileOptions::default();
1024 options.compression_method = CompressionMethod::ZSTD;
1025 options.level = Some(3);
1026 options
1027}
1028
1029#[cfg(test)]
1030mod tests {
1031 use super::*;
1032
1033 #[test]
1034 fn test_output_dict_for_path() {
1035 assert_eq!(
1037 OutputDict::for_path("meta/cargo-metadata.json".as_ref()),
1038 OutputDict::None
1039 );
1040 assert_eq!(
1041 OutputDict::for_path("meta/test-list.json".as_ref()),
1042 OutputDict::None
1043 );
1044
1045 assert_eq!(
1047 OutputDict::for_path("out/0123456789abcdef-stdout".as_ref()),
1048 OutputDict::Stdout
1049 );
1050 assert_eq!(
1051 OutputDict::for_path("out/0123456789abcdef-stderr".as_ref()),
1052 OutputDict::Stderr
1053 );
1054 assert_eq!(
1055 OutputDict::for_path("out/0123456789abcdef-combined".as_ref()),
1056 OutputDict::Stdout
1057 );
1058 }
1059
1060 #[test]
1061 fn test_output_dict_for_output_file_name() {
1062 assert_eq!(
1064 OutputDict::for_output_file_name("0123456789abcdef-stdout"),
1065 OutputDict::Stdout
1066 );
1067 assert_eq!(
1068 OutputDict::for_output_file_name("0123456789abcdef-stderr"),
1069 OutputDict::Stderr
1070 );
1071 assert_eq!(
1072 OutputDict::for_output_file_name("0123456789abcdef-combined"),
1073 OutputDict::Stdout
1074 );
1075 assert_eq!(
1076 OutputDict::for_output_file_name("0123456789abcdef-unknown"),
1077 OutputDict::None
1078 );
1079 }
1080
1081 #[test]
1082 fn test_dict_bytes() {
1083 assert!(OutputDict::Stdout.dict_bytes().is_some());
1084 assert!(OutputDict::Stderr.dict_bytes().is_some());
1085 assert!(OutputDict::None.dict_bytes().is_none());
1086 }
1087
1088 #[test]
1089 fn test_runs_json_missing_version() {
1090 let json = r#"{"runs": []}"#;
1092 let result: Result<RecordedRunList, _> = serde_json::from_str(json);
1093 assert!(result.is_err(), "expected error for missing format-version");
1094 }
1095
1096 #[test]
1097 fn test_runs_json_current_version() {
1098 let json = format!(
1100 r#"{{"format-version": {}, "runs": []}}"#,
1101 RUNS_JSON_FORMAT_VERSION
1102 );
1103 let list: RecordedRunList = serde_json::from_str(&json).expect("should deserialize");
1104 assert_eq!(list.write_permission(), RunsJsonWritePermission::Allowed);
1105 }
1106
1107 #[test]
1108 fn test_runs_json_older_version() {
1109 let json = r#"{"format-version": 1, "runs": []}"#;
1113 let list: RecordedRunList = serde_json::from_str(json).expect("should deserialize");
1114 assert_eq!(list.write_permission(), RunsJsonWritePermission::Allowed);
1115 }
1116
1117 #[test]
1118 fn test_runs_json_newer_version() {
1119 let json = r#"{"format-version": 99, "runs": []}"#;
1121 let list: RecordedRunList = serde_json::from_str(json).expect("should deserialize");
1122 assert_eq!(
1123 list.write_permission(),
1124 RunsJsonWritePermission::Denied {
1125 file_version: RunsJsonFormatVersion::new(99),
1126 max_supported_version: RUNS_JSON_FORMAT_VERSION,
1127 }
1128 );
1129 }
1130
1131 #[test]
1132 fn test_runs_json_serialization_includes_version() {
1133 let list = RecordedRunList::from_data(&[], None);
1135 let json = serde_json::to_string(&list).expect("should serialize");
1136 assert!(
1137 json.contains("format-version"),
1138 "serialized runs.json.zst should include format-version"
1139 );
1140
1141 let parsed: serde_json::Value = serde_json::from_str(&json).expect("should parse");
1143 let version: RunsJsonFormatVersion =
1144 serde_json::from_value(parsed["format-version"].clone()).expect("valid version");
1145 assert_eq!(
1146 version, RUNS_JSON_FORMAT_VERSION,
1147 "format-version should be current version"
1148 );
1149 }
1150
1151 #[test]
1152 fn test_runs_json_new() {
1153 let list = RecordedRunList::new();
1155 assert_eq!(list.format_version, RUNS_JSON_FORMAT_VERSION);
1156 assert!(list.runs.is_empty());
1157 assert_eq!(list.write_permission(), RunsJsonWritePermission::Allowed);
1158 }
1159
1160 fn make_test_run(status: RecordedRunStatusFormat) -> RecordedRun {
1163 RecordedRun {
1164 run_id: ReportUuid::from_u128(0x550e8400_e29b_41d4_a716_446655440000),
1165 store_format_version: STORE_FORMAT_VERSION.major,
1166 store_format_minor_version: STORE_FORMAT_VERSION.minor,
1167 nextest_version: Version::new(0, 9, 111),
1168 started_at: DateTime::parse_from_rfc3339("2024-12-19T14:22:33-08:00")
1169 .expect("valid timestamp"),
1170 last_written_at: DateTime::parse_from_rfc3339("2024-12-19T22:22:33Z")
1171 .expect("valid timestamp"),
1172 duration_secs: Some(12.345),
1173 cli_args: vec![
1174 "cargo".to_owned(),
1175 "nextest".to_owned(),
1176 "run".to_owned(),
1177 "--workspace".to_owned(),
1178 ],
1179 build_scope_args: vec!["--workspace".to_owned()],
1180 env_vars: BTreeMap::from([
1181 ("CARGO_TERM_COLOR".to_owned(), "always".to_owned()),
1182 ("NEXTEST_PROFILE".to_owned(), "ci".to_owned()),
1183 ]),
1184 parent_run_id: Some(ReportUuid::from_u128(
1185 0x550e7400_e29b_41d4_a716_446655440000,
1186 )),
1187 sizes: RecordedSizesFormat {
1188 log: ComponentSizesFormat {
1189 compressed: 2345,
1190 uncompressed: 5678,
1191 entries: 42,
1192 },
1193 store: ComponentSizesFormat {
1194 compressed: 10000,
1195 uncompressed: 40000,
1196 entries: 15,
1197 },
1198 },
1199 status,
1200 }
1201 }
1202
1203 #[test]
1204 fn test_recorded_run_serialize_incomplete() {
1205 let run = make_test_run(RecordedRunStatusFormat::Incomplete);
1206 let json = serde_json::to_string_pretty(&run).expect("serialization should succeed");
1207 insta::assert_snapshot!(json);
1208 }
1209
1210 #[test]
1211 fn test_recorded_run_serialize_completed() {
1212 let run = make_test_run(RecordedRunStatusFormat::Completed {
1213 initial_run_count: 100,
1214 passed: 95,
1215 failed: 5,
1216 exit_code: 0,
1217 });
1218 let json = serde_json::to_string_pretty(&run).expect("serialization should succeed");
1219 insta::assert_snapshot!(json);
1220 }
1221
1222 #[test]
1223 fn test_recorded_run_serialize_cancelled() {
1224 let run = make_test_run(RecordedRunStatusFormat::Cancelled {
1225 initial_run_count: 100,
1226 passed: 45,
1227 failed: 5,
1228 exit_code: 100,
1229 });
1230 let json = serde_json::to_string_pretty(&run).expect("serialization should succeed");
1231 insta::assert_snapshot!(json);
1232 }
1233
1234 #[test]
1235 fn test_recorded_run_serialize_stress_completed() {
1236 let run = make_test_run(RecordedRunStatusFormat::StressCompleted {
1237 initial_iteration_count: NonZero::new(100),
1238 success_count: 98,
1239 failed_count: 2,
1240 exit_code: 0,
1241 });
1242 let json = serde_json::to_string_pretty(&run).expect("serialization should succeed");
1243 insta::assert_snapshot!(json);
1244 }
1245
1246 #[test]
1247 fn test_recorded_run_serialize_stress_cancelled() {
1248 let run = make_test_run(RecordedRunStatusFormat::StressCancelled {
1249 initial_iteration_count: NonZero::new(100),
1250 success_count: 45,
1251 failed_count: 5,
1252 exit_code: 100,
1253 });
1254 let json = serde_json::to_string_pretty(&run).expect("serialization should succeed");
1255 insta::assert_snapshot!(json);
1256 }
1257
1258 #[test]
1259 fn test_recorded_run_deserialize_unknown_status() {
1260 let json = r#"{
1263 "run-id": "550e8400-e29b-41d4-a716-446655440000",
1264 "store-format-version": 999,
1265 "nextest-version": "0.9.999",
1266 "started-at": "2024-12-19T14:22:33-08:00",
1267 "last-written-at": "2024-12-19T22:22:33Z",
1268 "cli-args": ["cargo", "nextest", "run"],
1269 "env-vars": {},
1270 "sizes": {
1271 "log": { "compressed": 2345, "uncompressed": 5678 },
1272 "store": { "compressed": 10000, "uncompressed": 40000 }
1273 },
1274 "status": {
1275 "status": "super-new-status",
1276 "some-future-field": 42
1277 }
1278 }"#;
1279 let run: RecordedRun = serde_json::from_str(json).expect("should deserialize");
1280 assert!(
1281 matches!(run.status, RecordedRunStatusFormat::Unknown),
1282 "unknown status should deserialize to Unknown variant"
1283 );
1284
1285 let info: RecordedRunInfo = run.into();
1287 assert!(
1288 matches!(info.status, RecordedRunStatus::Unknown),
1289 "Unknown format should convert to Unknown domain type"
1290 );
1291 }
1292
1293 #[test]
1294 fn test_recorded_run_roundtrip() {
1295 let original = make_test_run(RecordedRunStatusFormat::Completed {
1296 initial_run_count: 100,
1297 passed: 95,
1298 failed: 5,
1299 exit_code: 0,
1300 });
1301 let json = serde_json::to_string(&original).expect("serialization should succeed");
1302 let roundtripped: RecordedRun =
1303 serde_json::from_str(&json).expect("deserialization should succeed");
1304
1305 assert_eq!(roundtripped.run_id, original.run_id);
1306 assert_eq!(roundtripped.nextest_version, original.nextest_version);
1307 assert_eq!(roundtripped.started_at, original.started_at);
1308 assert_eq!(roundtripped.sizes, original.sizes);
1309
1310 let info: RecordedRunInfo = roundtripped.into();
1312 match info.status {
1313 RecordedRunStatus::Completed(stats) => {
1314 assert_eq!(stats.initial_run_count, 100);
1315 assert_eq!(stats.passed, 95);
1316 assert_eq!(stats.failed, 5);
1317 }
1318 _ => panic!("expected Completed variant"),
1319 }
1320 }
1321
1322 fn version(major: u32, minor: u32) -> StoreFormatVersion {
1326 StoreFormatVersion::new(
1327 StoreFormatMajorVersion::new(major),
1328 StoreFormatMinorVersion::new(minor),
1329 )
1330 }
1331
1332 #[test]
1333 fn test_store_version_compatibility() {
1334 assert!(
1335 version(1, 0).check_readable_by(version(1, 0)).is_ok(),
1336 "same version should be compatible"
1337 );
1338
1339 assert!(
1340 version(1, 0).check_readable_by(version(1, 2)).is_ok(),
1341 "older minor version should be compatible"
1342 );
1343
1344 let error = version(1, 3).check_readable_by(version(1, 2)).unwrap_err();
1345 assert_eq!(
1346 error,
1347 StoreVersionIncompatibility::MinorTooNew {
1348 recording_minor: StoreFormatMinorVersion::new(3),
1349 supported_minor: StoreFormatMinorVersion::new(2),
1350 },
1351 "newer minor version should be incompatible"
1352 );
1353 insta::assert_snapshot!(error.to_string(), @"minor version 3 is newer than supported version 2");
1354
1355 let error = version(2, 0).check_readable_by(version(1, 5)).unwrap_err();
1357 assert_eq!(
1358 error,
1359 StoreVersionIncompatibility::RecordingTooNew {
1360 recording_major: StoreFormatMajorVersion::new(2),
1361 supported_major: StoreFormatMajorVersion::new(1),
1362 },
1363 );
1364 insta::assert_snapshot!(
1365 error.to_string(),
1366 @"recording has major version 2, but this nextest only supports version 1 (upgrade nextest to replay this recording)"
1367 );
1368
1369 let error = version(1, 0).check_readable_by(version(2, 0)).unwrap_err();
1371 assert_eq!(
1372 error,
1373 StoreVersionIncompatibility::RecordingTooOld {
1374 recording_major: StoreFormatMajorVersion::new(1),
1375 supported_major: StoreFormatMajorVersion::new(2),
1376 last_nextest_version: Some("0.9.130"),
1377 },
1378 );
1379 insta::assert_snapshot!(
1380 error.to_string(),
1381 @"recording has major version 1, but this nextest requires version 2 (use nextest <= 0.9.130 to replay this recording)"
1382 );
1383
1384 let error = version(3, 0).check_readable_by(version(5, 0)).unwrap_err();
1386 assert_eq!(
1387 error,
1388 StoreVersionIncompatibility::RecordingTooOld {
1389 recording_major: StoreFormatMajorVersion::new(3),
1390 supported_major: StoreFormatMajorVersion::new(5),
1391 last_nextest_version: None,
1392 },
1393 );
1394 insta::assert_snapshot!(
1395 error.to_string(),
1396 @"recording has major version 3, but this nextest requires version 5"
1397 );
1398
1399 insta::assert_snapshot!(version(1, 2).to_string(), @"1.2");
1400 }
1401
1402 #[test]
1403 fn test_recorded_run_deserialize_without_minor_version() {
1404 let json = r#"{
1406 "run-id": "550e8400-e29b-41d4-a716-446655440000",
1407 "store-format-version": 1,
1408 "nextest-version": "0.9.111",
1409 "started-at": "2024-12-19T14:22:33-08:00",
1410 "last-written-at": "2024-12-19T22:22:33Z",
1411 "cli-args": [],
1412 "env-vars": {},
1413 "sizes": {
1414 "log": { "compressed": 0, "uncompressed": 0 },
1415 "store": { "compressed": 0, "uncompressed": 0 }
1416 },
1417 "status": { "status": "incomplete" }
1418 }"#;
1419 let run: RecordedRun = serde_json::from_str(json).expect("should deserialize");
1420 assert_eq!(run.store_format_version, StoreFormatMajorVersion::new(1));
1421 assert_eq!(
1422 run.store_format_minor_version,
1423 StoreFormatMinorVersion::new(0)
1424 );
1425
1426 let info: RecordedRunInfo = run.into();
1428 assert_eq!(info.store_format_version, version(1, 0));
1429 }
1430
1431 #[test]
1432 fn test_recorded_run_serialize_includes_minor_version() {
1433 let run = make_test_run(RecordedRunStatusFormat::Incomplete);
1435 let json = serde_json::to_string_pretty(&run).expect("serialization should succeed");
1436 assert!(
1437 json.contains("store-format-minor-version"),
1438 "serialized run should include store-format-minor-version"
1439 );
1440 }
1441
1442 fn portable_version(major: u32, minor: u32) -> PortableRecordingFormatVersion {
1446 PortableRecordingFormatVersion::new(
1447 PortableRecordingFormatMajorVersion::new(major),
1448 PortableRecordingFormatMinorVersion::new(minor),
1449 )
1450 }
1451
1452 #[test]
1453 fn test_portable_version_compatibility() {
1454 assert!(
1455 portable_version(1, 0)
1456 .check_readable_by(portable_version(1, 0))
1457 .is_ok(),
1458 "same version should be compatible"
1459 );
1460
1461 assert!(
1462 portable_version(1, 0)
1463 .check_readable_by(portable_version(1, 2))
1464 .is_ok(),
1465 "older minor version should be compatible"
1466 );
1467
1468 let error = portable_version(1, 3)
1469 .check_readable_by(portable_version(1, 2))
1470 .unwrap_err();
1471 assert_eq!(
1472 error,
1473 PortableRecordingVersionIncompatibility::MinorTooNew {
1474 recording_minor: PortableRecordingFormatMinorVersion::new(3),
1475 supported_minor: PortableRecordingFormatMinorVersion::new(2),
1476 },
1477 "newer minor version should be incompatible"
1478 );
1479 insta::assert_snapshot!(error.to_string(), @"minor version 3 is newer than supported version 2");
1480
1481 let error = portable_version(2, 0)
1482 .check_readable_by(portable_version(1, 5))
1483 .unwrap_err();
1484 assert_eq!(
1485 error,
1486 PortableRecordingVersionIncompatibility::MajorMismatch {
1487 recording_major: PortableRecordingFormatMajorVersion::new(2),
1488 supported_major: PortableRecordingFormatMajorVersion::new(1),
1489 },
1490 "different major version should be incompatible"
1491 );
1492 insta::assert_snapshot!(error.to_string(), @"major version 2 differs from supported version 1");
1493
1494 insta::assert_snapshot!(portable_version(1, 2).to_string(), @"1.2");
1495 }
1496
1497 #[test]
1498 fn test_portable_version_serialization() {
1499 let version = portable_version(1, 0);
1501 let json = serde_json::to_string(&version).expect("serialization should succeed");
1502 insta::assert_snapshot!(json, @r#"{"major":1,"minor":0}"#);
1503
1504 let roundtripped: PortableRecordingFormatVersion =
1506 serde_json::from_str(&json).expect("deserialization should succeed");
1507 assert_eq!(roundtripped, version);
1508 }
1509
1510 #[test]
1511 fn test_portable_manifest_format_version() {
1512 assert_eq!(
1514 PORTABLE_RECORDING_FORMAT_VERSION,
1515 portable_version(1, 0),
1516 "current portable recording format version should be 1.0"
1517 );
1518 }
1519}