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(
255 StoreFormatMajorVersion::new(2),
256 StoreFormatMinorVersion::new(0),
257);
258
259pub(super) const FORCE_STORE_FORMAT_VERSION_ENV: &str = "__NEXTEST_FORCE_STORE_FORMAT_VERSION";
269
270pub(super) fn store_format_version_for_new_run() -> StoreFormatVersion {
272 let Some(raw) = std::env::var_os(FORCE_STORE_FORMAT_VERSION_ENV) else {
273 return STORE_FORMAT_VERSION;
274 };
275 let raw = raw.to_str().unwrap_or_else(|| {
276 panic!("{FORCE_STORE_FORMAT_VERSION_ENV} contains non-UTF-8 bytes");
277 });
278 let (major, minor) = raw.split_once('.').unwrap_or_else(|| {
279 panic!(
280 "{FORCE_STORE_FORMAT_VERSION_ENV}={raw:?} is malformed \
281 (expected MAJOR.MINOR, e.g. 9999.0)"
282 )
283 });
284 let major: u32 = major.parse().unwrap_or_else(|err| {
285 panic!("{FORCE_STORE_FORMAT_VERSION_ENV}={raw:?} has invalid major version: {err}")
286 });
287 let minor: u32 = minor.parse().unwrap_or_else(|err| {
288 panic!("{FORCE_STORE_FORMAT_VERSION_ENV}={raw:?} has invalid minor version: {err}")
289 });
290 StoreFormatVersion::new(
291 StoreFormatMajorVersion::new(major),
292 StoreFormatMinorVersion::new(minor),
293 )
294}
295
296#[derive(Debug, Clone, Copy, PartialEq, Eq)]
298pub enum RunsJsonWritePermission {
299 Allowed,
301 Denied {
303 file_version: RunsJsonFormatVersion,
305 max_supported_version: RunsJsonFormatVersion,
307 },
308}
309
310#[derive(Debug, Deserialize, Serialize)]
312#[serde(rename_all = "kebab-case")]
313pub(super) struct RecordedRunList {
314 pub(super) format_version: RunsJsonFormatVersion,
316
317 #[serde(default, skip_serializing_if = "Option::is_none")]
322 pub(super) last_pruned_at: Option<DateTime<Utc>>,
323
324 #[serde(default)]
326 pub(super) runs: Vec<RecordedRun>,
327}
328
329pub(super) struct RunListData {
331 pub(super) runs: Vec<RecordedRunInfo>,
332 pub(super) last_pruned_at: Option<DateTime<Utc>>,
333}
334
335impl RecordedRunList {
336 #[cfg(test)]
338 fn new() -> Self {
339 Self {
340 format_version: RUNS_JSON_FORMAT_VERSION,
341 last_pruned_at: None,
342 runs: Vec::new(),
343 }
344 }
345
346 pub(super) fn into_data(self) -> RunListData {
348 RunListData {
349 runs: self.runs.into_iter().map(RecordedRunInfo::from).collect(),
350 last_pruned_at: self.last_pruned_at,
351 }
352 }
353
354 pub(super) fn from_data(
359 runs: &[RecordedRunInfo],
360 last_pruned_at: Option<DateTime<Utc>>,
361 ) -> Self {
362 Self {
363 format_version: RUNS_JSON_FORMAT_VERSION,
364 last_pruned_at,
365 runs: runs.iter().map(RecordedRun::from).collect(),
366 }
367 }
368
369 pub(super) fn write_permission(&self) -> RunsJsonWritePermission {
374 if self.format_version > RUNS_JSON_FORMAT_VERSION {
375 RunsJsonWritePermission::Denied {
376 file_version: self.format_version,
377 max_supported_version: RUNS_JSON_FORMAT_VERSION,
378 }
379 } else {
380 RunsJsonWritePermission::Allowed
381 }
382 }
383}
384
385#[derive(Clone, Debug, Deserialize, Serialize)]
387#[serde(rename_all = "kebab-case")]
388pub(super) struct RecordedRun {
389 pub(super) run_id: ReportUuid,
391 pub(super) store_format_version: StoreFormatMajorVersion,
396 #[serde(default)]
401 pub(super) store_format_minor_version: StoreFormatMinorVersion,
402 pub(super) nextest_version: Version,
404 pub(super) started_at: DateTime<FixedOffset>,
406 pub(super) last_written_at: DateTime<FixedOffset>,
412 #[serde(default, skip_serializing_if = "Option::is_none")]
414 pub(super) duration_secs: Option<f64>,
415 #[serde(default)]
417 pub(super) cli_args: Vec<String>,
418 #[serde(default)]
423 pub(super) build_scope_args: Vec<String>,
424 #[serde(default)]
428 pub(super) env_vars: BTreeMap<String, String>,
429 #[serde(default)]
431 pub(super) parent_run_id: Option<ReportUuid>,
432 pub(super) sizes: RecordedSizesFormat,
436 pub(super) status: RecordedRunStatusFormat,
438}
439
440#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Deserialize, Serialize)]
442#[serde(rename_all = "kebab-case")]
443pub(super) struct RecordedSizesFormat {
444 pub(super) log: ComponentSizesFormat,
446 pub(super) store: ComponentSizesFormat,
448}
449
450#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Deserialize, Serialize)]
452#[serde(rename_all = "kebab-case")]
453pub(super) struct ComponentSizesFormat {
454 pub(super) compressed: u64,
456 pub(super) uncompressed: u64,
458 #[serde(default)]
460 pub(super) entries: u64,
461}
462
463impl From<RecordedSizes> for RecordedSizesFormat {
464 fn from(sizes: RecordedSizes) -> Self {
465 Self {
466 log: ComponentSizesFormat {
467 compressed: sizes.log.compressed,
468 uncompressed: sizes.log.uncompressed,
469 entries: sizes.log.entries,
470 },
471 store: ComponentSizesFormat {
472 compressed: sizes.store.compressed,
473 uncompressed: sizes.store.uncompressed,
474 entries: sizes.store.entries,
475 },
476 }
477 }
478}
479
480impl From<RecordedSizesFormat> for RecordedSizes {
481 fn from(sizes: RecordedSizesFormat) -> Self {
482 Self {
483 log: ComponentSizes {
484 compressed: sizes.log.compressed,
485 uncompressed: sizes.log.uncompressed,
486 entries: sizes.log.entries,
487 },
488 store: ComponentSizes {
489 compressed: sizes.store.compressed,
490 uncompressed: sizes.store.uncompressed,
491 entries: sizes.store.entries,
492 },
493 }
494 }
495}
496
497#[derive(Clone, Debug, Deserialize, Serialize)]
499#[serde(tag = "status", rename_all = "kebab-case")]
500pub(super) enum RecordedRunStatusFormat {
501 Incomplete,
503 #[serde(rename_all = "kebab-case")]
505 Completed {
506 initial_run_count: usize,
508 passed: usize,
510 failed: usize,
512 exit_code: i32,
514 },
515 #[serde(rename_all = "kebab-case")]
517 Cancelled {
518 initial_run_count: usize,
520 passed: usize,
522 failed: usize,
524 exit_code: i32,
526 },
527 #[serde(rename_all = "kebab-case")]
529 StressCompleted {
530 initial_iteration_count: Option<NonZero<u32>>,
532 success_count: u32,
534 failed_count: u32,
536 exit_code: i32,
538 },
539 #[serde(rename_all = "kebab-case")]
541 StressCancelled {
542 initial_iteration_count: Option<NonZero<u32>>,
544 success_count: u32,
546 failed_count: u32,
548 exit_code: i32,
550 },
551 #[serde(other)]
556 Unknown,
557}
558
559impl From<RecordedRun> for RecordedRunInfo {
560 fn from(run: RecordedRun) -> Self {
561 Self {
562 run_id: run.run_id,
563 store_format_version: StoreFormatVersion::new(
564 run.store_format_version,
565 run.store_format_minor_version,
566 ),
567 nextest_version: run.nextest_version,
568 started_at: run.started_at,
569 last_written_at: run.last_written_at,
570 duration_secs: run.duration_secs,
571 cli_args: run.cli_args,
572 build_scope_args: run.build_scope_args,
573 env_vars: run.env_vars,
574 parent_run_id: run.parent_run_id,
575 sizes: run.sizes.into(),
576 status: run.status.into(),
577 }
578 }
579}
580
581impl From<&RecordedRunInfo> for RecordedRun {
582 fn from(run: &RecordedRunInfo) -> Self {
583 Self {
584 run_id: run.run_id,
585 store_format_version: run.store_format_version.major,
586 store_format_minor_version: run.store_format_version.minor,
587 nextest_version: run.nextest_version.clone(),
588 started_at: run.started_at,
589 last_written_at: run.last_written_at,
590 duration_secs: run.duration_secs,
591 cli_args: run.cli_args.clone(),
592 build_scope_args: run.build_scope_args.clone(),
593 env_vars: run.env_vars.clone(),
594 parent_run_id: run.parent_run_id,
595 sizes: run.sizes.into(),
596 status: (&run.status).into(),
597 }
598 }
599}
600
601impl From<RecordedRunStatusFormat> for RecordedRunStatus {
602 fn from(status: RecordedRunStatusFormat) -> Self {
603 match status {
604 RecordedRunStatusFormat::Incomplete => Self::Incomplete,
605 RecordedRunStatusFormat::Unknown => Self::Unknown,
606 RecordedRunStatusFormat::Completed {
607 initial_run_count,
608 passed,
609 failed,
610 exit_code,
611 } => Self::Completed(CompletedRunStats {
612 initial_run_count,
613 passed,
614 failed,
615 exit_code,
616 }),
617 RecordedRunStatusFormat::Cancelled {
618 initial_run_count,
619 passed,
620 failed,
621 exit_code,
622 } => Self::Cancelled(CompletedRunStats {
623 initial_run_count,
624 passed,
625 failed,
626 exit_code,
627 }),
628 RecordedRunStatusFormat::StressCompleted {
629 initial_iteration_count,
630 success_count,
631 failed_count,
632 exit_code,
633 } => Self::StressCompleted(StressCompletedRunStats {
634 initial_iteration_count,
635 success_count,
636 failed_count,
637 exit_code,
638 }),
639 RecordedRunStatusFormat::StressCancelled {
640 initial_iteration_count,
641 success_count,
642 failed_count,
643 exit_code,
644 } => Self::StressCancelled(StressCompletedRunStats {
645 initial_iteration_count,
646 success_count,
647 failed_count,
648 exit_code,
649 }),
650 }
651 }
652}
653
654impl From<&RecordedRunStatus> for RecordedRunStatusFormat {
655 fn from(status: &RecordedRunStatus) -> Self {
656 match status {
657 RecordedRunStatus::Incomplete => Self::Incomplete,
658 RecordedRunStatus::Unknown => Self::Unknown,
659 RecordedRunStatus::Completed(stats) => Self::Completed {
660 initial_run_count: stats.initial_run_count,
661 passed: stats.passed,
662 failed: stats.failed,
663 exit_code: stats.exit_code,
664 },
665 RecordedRunStatus::Cancelled(stats) => Self::Cancelled {
666 initial_run_count: stats.initial_run_count,
667 passed: stats.passed,
668 failed: stats.failed,
669 exit_code: stats.exit_code,
670 },
671 RecordedRunStatus::StressCompleted(stats) => Self::StressCompleted {
672 initial_iteration_count: stats.initial_iteration_count,
673 success_count: stats.success_count,
674 failed_count: stats.failed_count,
675 exit_code: stats.exit_code,
676 },
677 RecordedRunStatus::StressCancelled(stats) => Self::StressCancelled {
678 initial_iteration_count: stats.initial_iteration_count,
679 success_count: stats.success_count,
680 failed_count: stats.failed_count,
681 exit_code: stats.exit_code,
682 },
683 }
684 }
685}
686
687#[derive(Clone, Debug, Deserialize, Serialize)]
695#[serde(rename_all = "kebab-case")]
696pub struct RerunInfo {
697 pub parent_run_id: ReportUuid,
699
700 pub root_info: RerunRootInfo,
702
703 pub test_suites: IdOrdMap<RerunTestSuiteInfo>,
705}
706
707#[derive(Clone, Debug, Deserialize, Serialize)]
709#[serde(rename_all = "kebab-case")]
710pub struct RerunRootInfo {
711 pub run_id: ReportUuid,
713
714 pub build_scope_args: Vec<String>,
716}
717
718impl RerunRootInfo {
719 pub fn new(run_id: ReportUuid, build_scope_args: Vec<String>) -> Self {
725 Self {
726 run_id,
727 build_scope_args,
728 }
729 }
730}
731
732#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
734pub struct RerunTestSuiteInfo {
735 pub binary_id: RustBinaryId,
737
738 pub passing: BTreeSet<TestCaseName>,
740
741 pub outstanding: BTreeSet<TestCaseName>,
743}
744
745impl RerunTestSuiteInfo {
746 pub(super) fn new(binary_id: RustBinaryId) -> Self {
747 Self {
748 binary_id,
749 passing: BTreeSet::new(),
750 outstanding: BTreeSet::new(),
751 }
752 }
753}
754
755impl IdOrdItem for RerunTestSuiteInfo {
756 type Key<'a> = &'a RustBinaryId;
757 fn key(&self) -> Self::Key<'_> {
758 &self.binary_id
759 }
760 id_upcast!();
761}
762
763pub static STORE_ZIP_FILE_NAME: &str = "store.zip";
769
770pub static RUN_LOG_FILE_NAME: &str = "run.log.zst";
772
773pub fn has_zip_extension(path: &Utf8Path) -> bool {
775 path.extension()
776 .is_some_and(|ext| ext.eq_ignore_ascii_case("zip"))
777}
778
779pub static CARGO_METADATA_JSON_PATH: &str = "meta/cargo-metadata.json";
782pub static TEST_LIST_JSON_PATH: &str = "meta/test-list.json";
784pub static RECORD_OPTS_JSON_PATH: &str = "meta/record-opts.json";
786pub static RERUN_INFO_JSON_PATH: &str = "meta/rerun-info.json";
788pub static STDOUT_DICT_PATH: &str = "meta/stdout.dict";
790pub static STDERR_DICT_PATH: &str = "meta/stderr.dict";
792
793define_format_version! {
798 pub struct PortableRecordingFormatMajorVersion;
800}
801
802define_format_version! {
803 @default
804 pub struct PortableRecordingFormatMinorVersion;
806}
807
808#[derive(Clone, Copy, Debug, PartialEq, Eq, Deserialize, Serialize)]
810pub struct PortableRecordingFormatVersion {
811 pub major: PortableRecordingFormatMajorVersion,
813 pub minor: PortableRecordingFormatMinorVersion,
815}
816
817impl PortableRecordingFormatVersion {
818 pub const fn new(
820 major: PortableRecordingFormatMajorVersion,
821 minor: PortableRecordingFormatMinorVersion,
822 ) -> Self {
823 Self { major, minor }
824 }
825
826 pub fn check_readable_by(
829 self,
830 supported: Self,
831 ) -> Result<(), PortableRecordingVersionIncompatibility> {
832 if self.major != supported.major {
833 return Err(PortableRecordingVersionIncompatibility::MajorMismatch {
834 recording_major: self.major,
835 supported_major: supported.major,
836 });
837 }
838 if self.minor > supported.minor {
839 return Err(PortableRecordingVersionIncompatibility::MinorTooNew {
840 recording_minor: self.minor,
841 supported_minor: supported.minor,
842 });
843 }
844 Ok(())
845 }
846}
847
848impl fmt::Display for PortableRecordingFormatVersion {
849 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
850 write!(f, "{}.{}", self.major, self.minor)
851 }
852}
853
854#[derive(Clone, Copy, Debug, PartialEq, Eq)]
857pub enum PortableRecordingVersionIncompatibility {
858 MajorMismatch {
860 recording_major: PortableRecordingFormatMajorVersion,
862 supported_major: PortableRecordingFormatMajorVersion,
864 },
865 MinorTooNew {
867 recording_minor: PortableRecordingFormatMinorVersion,
869 supported_minor: PortableRecordingFormatMinorVersion,
871 },
872}
873
874impl fmt::Display for PortableRecordingVersionIncompatibility {
875 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
876 match self {
877 Self::MajorMismatch {
878 recording_major,
879 supported_major,
880 } => {
881 write!(
882 f,
883 "major version {} differs from supported version {}",
884 recording_major, supported_major
885 )
886 }
887 Self::MinorTooNew {
888 recording_minor,
889 supported_minor,
890 } => {
891 write!(
892 f,
893 "minor version {} is newer than supported version {}",
894 recording_minor, supported_minor
895 )
896 }
897 }
898 }
899}
900
901pub const PORTABLE_RECORDING_FORMAT_VERSION: PortableRecordingFormatVersion =
903 PortableRecordingFormatVersion::new(
904 PortableRecordingFormatMajorVersion::new(1),
905 PortableRecordingFormatMinorVersion::new(0),
906 );
907
908pub static PORTABLE_MANIFEST_FILE_NAME: &str = "manifest.json";
910
911#[derive(Debug, Deserialize, Serialize)]
916#[serde(rename_all = "kebab-case")]
917pub(crate) struct PortableManifest {
918 pub(crate) format_version: PortableRecordingFormatVersion,
920 pub(super) run: RecordedRun,
922}
923
924impl PortableManifest {
925 pub(crate) fn new(run: &RecordedRunInfo) -> Self {
927 Self {
928 format_version: PORTABLE_RECORDING_FORMAT_VERSION,
929 run: RecordedRun::from(run),
930 }
931 }
932
933 pub(crate) fn run_info(&self) -> RecordedRunInfo {
935 RecordedRunInfo::from(self.run.clone())
936 }
937
938 pub(crate) fn store_format_version(&self) -> StoreFormatVersion {
940 StoreFormatVersion::new(
941 self.run.store_format_version,
942 self.run.store_format_minor_version,
943 )
944 }
945}
946
947#[derive(Clone, Copy, Debug, PartialEq, Eq)]
949pub enum OutputDict {
950 Stdout,
952 Stderr,
954 None,
956}
957
958impl OutputDict {
959 pub fn for_path(path: &Utf8Path) -> Self {
967 let mut iter = path.iter();
968 let Some(first_component) = iter.next() else {
969 return Self::None;
970 };
971 if first_component != "out" {
973 return Self::None;
974 }
975
976 Self::for_output_file_name(iter.as_path().as_str())
977 }
978
979 pub fn for_output_file_name(file_name: &str) -> Self {
984 if file_name.ends_with("-stdout") || file_name.ends_with("-combined") {
985 Self::Stdout
986 } else if file_name.ends_with("-stderr") {
987 Self::Stderr
988 } else {
989 Self::None
991 }
992 }
993
994 pub fn dict_bytes(self) -> Option<&'static [u8]> {
998 match self {
999 Self::Stdout => Some(super::dicts::STDOUT),
1000 Self::Stderr => Some(super::dicts::STDERR),
1001 Self::None => None,
1002 }
1003 }
1004}
1005
1006pub(super) fn stored_file_options() -> FileOptions {
1013 let mut options = FileOptions::default();
1014 options.compression_method = CompressionMethod::STORE;
1015 options
1016}
1017
1018pub(super) fn zstd_file_options() -> FileOptions {
1020 let mut options = FileOptions::default();
1021 options.compression_method = CompressionMethod::ZSTD;
1022 options.level = Some(3);
1023 options
1024}
1025
1026#[cfg(test)]
1027mod tests {
1028 use super::*;
1029
1030 #[test]
1031 fn test_output_dict_for_path() {
1032 assert_eq!(
1034 OutputDict::for_path("meta/cargo-metadata.json".as_ref()),
1035 OutputDict::None
1036 );
1037 assert_eq!(
1038 OutputDict::for_path("meta/test-list.json".as_ref()),
1039 OutputDict::None
1040 );
1041
1042 assert_eq!(
1044 OutputDict::for_path("out/0123456789abcdef-stdout".as_ref()),
1045 OutputDict::Stdout
1046 );
1047 assert_eq!(
1048 OutputDict::for_path("out/0123456789abcdef-stderr".as_ref()),
1049 OutputDict::Stderr
1050 );
1051 assert_eq!(
1052 OutputDict::for_path("out/0123456789abcdef-combined".as_ref()),
1053 OutputDict::Stdout
1054 );
1055 }
1056
1057 #[test]
1058 fn test_output_dict_for_output_file_name() {
1059 assert_eq!(
1061 OutputDict::for_output_file_name("0123456789abcdef-stdout"),
1062 OutputDict::Stdout
1063 );
1064 assert_eq!(
1065 OutputDict::for_output_file_name("0123456789abcdef-stderr"),
1066 OutputDict::Stderr
1067 );
1068 assert_eq!(
1069 OutputDict::for_output_file_name("0123456789abcdef-combined"),
1070 OutputDict::Stdout
1071 );
1072 assert_eq!(
1073 OutputDict::for_output_file_name("0123456789abcdef-unknown"),
1074 OutputDict::None
1075 );
1076 }
1077
1078 #[test]
1079 fn test_dict_bytes() {
1080 assert!(OutputDict::Stdout.dict_bytes().is_some());
1081 assert!(OutputDict::Stderr.dict_bytes().is_some());
1082 assert!(OutputDict::None.dict_bytes().is_none());
1083 }
1084
1085 #[test]
1086 fn test_runs_json_missing_version() {
1087 let json = r#"{"runs": []}"#;
1089 let result: Result<RecordedRunList, _> = serde_json::from_str(json);
1090 assert!(result.is_err(), "expected error for missing format-version");
1091 }
1092
1093 #[test]
1094 fn test_runs_json_current_version() {
1095 let json = format!(
1097 r#"{{"format-version": {}, "runs": []}}"#,
1098 RUNS_JSON_FORMAT_VERSION
1099 );
1100 let list: RecordedRunList = serde_json::from_str(&json).expect("should deserialize");
1101 assert_eq!(list.write_permission(), RunsJsonWritePermission::Allowed);
1102 }
1103
1104 #[test]
1105 fn test_runs_json_older_version() {
1106 let json = r#"{"format-version": 1, "runs": []}"#;
1110 let list: RecordedRunList = serde_json::from_str(json).expect("should deserialize");
1111 assert_eq!(list.write_permission(), RunsJsonWritePermission::Allowed);
1112 }
1113
1114 #[test]
1115 fn test_runs_json_newer_version() {
1116 let json = r#"{"format-version": 99, "runs": []}"#;
1118 let list: RecordedRunList = serde_json::from_str(json).expect("should deserialize");
1119 assert_eq!(
1120 list.write_permission(),
1121 RunsJsonWritePermission::Denied {
1122 file_version: RunsJsonFormatVersion::new(99),
1123 max_supported_version: RUNS_JSON_FORMAT_VERSION,
1124 }
1125 );
1126 }
1127
1128 #[test]
1129 fn test_runs_json_serialization_includes_version() {
1130 let list = RecordedRunList::from_data(&[], None);
1132 let json = serde_json::to_string(&list).expect("should serialize");
1133 assert!(
1134 json.contains("format-version"),
1135 "serialized runs.json.zst should include format-version"
1136 );
1137
1138 let parsed: serde_json::Value = serde_json::from_str(&json).expect("should parse");
1140 let version: RunsJsonFormatVersion =
1141 serde_json::from_value(parsed["format-version"].clone()).expect("valid version");
1142 assert_eq!(
1143 version, RUNS_JSON_FORMAT_VERSION,
1144 "format-version should be current version"
1145 );
1146 }
1147
1148 #[test]
1149 fn test_runs_json_new() {
1150 let list = RecordedRunList::new();
1152 assert_eq!(list.format_version, RUNS_JSON_FORMAT_VERSION);
1153 assert!(list.runs.is_empty());
1154 assert_eq!(list.write_permission(), RunsJsonWritePermission::Allowed);
1155 }
1156
1157 fn make_test_run(status: RecordedRunStatusFormat) -> RecordedRun {
1160 RecordedRun {
1161 run_id: ReportUuid::from_u128(0x550e8400_e29b_41d4_a716_446655440000),
1162 store_format_version: STORE_FORMAT_VERSION.major,
1163 store_format_minor_version: STORE_FORMAT_VERSION.minor,
1164 nextest_version: Version::new(0, 9, 111),
1165 started_at: DateTime::parse_from_rfc3339("2024-12-19T14:22:33-08:00")
1166 .expect("valid timestamp"),
1167 last_written_at: DateTime::parse_from_rfc3339("2024-12-19T22:22:33Z")
1168 .expect("valid timestamp"),
1169 duration_secs: Some(12.345),
1170 cli_args: vec![
1171 "cargo".to_owned(),
1172 "nextest".to_owned(),
1173 "run".to_owned(),
1174 "--workspace".to_owned(),
1175 ],
1176 build_scope_args: vec!["--workspace".to_owned()],
1177 env_vars: BTreeMap::from([
1178 ("CARGO_TERM_COLOR".to_owned(), "always".to_owned()),
1179 ("NEXTEST_PROFILE".to_owned(), "ci".to_owned()),
1180 ]),
1181 parent_run_id: Some(ReportUuid::from_u128(
1182 0x550e7400_e29b_41d4_a716_446655440000,
1183 )),
1184 sizes: RecordedSizesFormat {
1185 log: ComponentSizesFormat {
1186 compressed: 2345,
1187 uncompressed: 5678,
1188 entries: 42,
1189 },
1190 store: ComponentSizesFormat {
1191 compressed: 10000,
1192 uncompressed: 40000,
1193 entries: 15,
1194 },
1195 },
1196 status,
1197 }
1198 }
1199
1200 #[test]
1201 fn test_recorded_run_serialize_incomplete() {
1202 let run = make_test_run(RecordedRunStatusFormat::Incomplete);
1203 let json = serde_json::to_string_pretty(&run).expect("serialization should succeed");
1204 insta::assert_snapshot!(json);
1205 }
1206
1207 #[test]
1208 fn test_recorded_run_serialize_completed() {
1209 let run = make_test_run(RecordedRunStatusFormat::Completed {
1210 initial_run_count: 100,
1211 passed: 95,
1212 failed: 5,
1213 exit_code: 0,
1214 });
1215 let json = serde_json::to_string_pretty(&run).expect("serialization should succeed");
1216 insta::assert_snapshot!(json);
1217 }
1218
1219 #[test]
1220 fn test_recorded_run_serialize_cancelled() {
1221 let run = make_test_run(RecordedRunStatusFormat::Cancelled {
1222 initial_run_count: 100,
1223 passed: 45,
1224 failed: 5,
1225 exit_code: 100,
1226 });
1227 let json = serde_json::to_string_pretty(&run).expect("serialization should succeed");
1228 insta::assert_snapshot!(json);
1229 }
1230
1231 #[test]
1232 fn test_recorded_run_serialize_stress_completed() {
1233 let run = make_test_run(RecordedRunStatusFormat::StressCompleted {
1234 initial_iteration_count: NonZero::new(100),
1235 success_count: 98,
1236 failed_count: 2,
1237 exit_code: 0,
1238 });
1239 let json = serde_json::to_string_pretty(&run).expect("serialization should succeed");
1240 insta::assert_snapshot!(json);
1241 }
1242
1243 #[test]
1244 fn test_recorded_run_serialize_stress_cancelled() {
1245 let run = make_test_run(RecordedRunStatusFormat::StressCancelled {
1246 initial_iteration_count: NonZero::new(100),
1247 success_count: 45,
1248 failed_count: 5,
1249 exit_code: 100,
1250 });
1251 let json = serde_json::to_string_pretty(&run).expect("serialization should succeed");
1252 insta::assert_snapshot!(json);
1253 }
1254
1255 #[test]
1256 fn test_recorded_run_deserialize_unknown_status() {
1257 let json = r#"{
1260 "run-id": "550e8400-e29b-41d4-a716-446655440000",
1261 "store-format-version": 999,
1262 "nextest-version": "0.9.999",
1263 "started-at": "2024-12-19T14:22:33-08:00",
1264 "last-written-at": "2024-12-19T22:22:33Z",
1265 "cli-args": ["cargo", "nextest", "run"],
1266 "env-vars": {},
1267 "sizes": {
1268 "log": { "compressed": 2345, "uncompressed": 5678 },
1269 "store": { "compressed": 10000, "uncompressed": 40000 }
1270 },
1271 "status": {
1272 "status": "super-new-status",
1273 "some-future-field": 42
1274 }
1275 }"#;
1276 let run: RecordedRun = serde_json::from_str(json).expect("should deserialize");
1277 assert!(
1278 matches!(run.status, RecordedRunStatusFormat::Unknown),
1279 "unknown status should deserialize to Unknown variant"
1280 );
1281
1282 let info: RecordedRunInfo = run.into();
1284 assert!(
1285 matches!(info.status, RecordedRunStatus::Unknown),
1286 "Unknown format should convert to Unknown domain type"
1287 );
1288 }
1289
1290 #[test]
1291 fn test_recorded_run_roundtrip() {
1292 let original = make_test_run(RecordedRunStatusFormat::Completed {
1293 initial_run_count: 100,
1294 passed: 95,
1295 failed: 5,
1296 exit_code: 0,
1297 });
1298 let json = serde_json::to_string(&original).expect("serialization should succeed");
1299 let roundtripped: RecordedRun =
1300 serde_json::from_str(&json).expect("deserialization should succeed");
1301
1302 assert_eq!(roundtripped.run_id, original.run_id);
1303 assert_eq!(roundtripped.nextest_version, original.nextest_version);
1304 assert_eq!(roundtripped.started_at, original.started_at);
1305 assert_eq!(roundtripped.sizes, original.sizes);
1306
1307 let info: RecordedRunInfo = roundtripped.into();
1309 match info.status {
1310 RecordedRunStatus::Completed(stats) => {
1311 assert_eq!(stats.initial_run_count, 100);
1312 assert_eq!(stats.passed, 95);
1313 assert_eq!(stats.failed, 5);
1314 }
1315 _ => panic!("expected Completed variant"),
1316 }
1317 }
1318
1319 fn version(major: u32, minor: u32) -> StoreFormatVersion {
1323 StoreFormatVersion::new(
1324 StoreFormatMajorVersion::new(major),
1325 StoreFormatMinorVersion::new(minor),
1326 )
1327 }
1328
1329 #[test]
1330 fn test_store_version_compatibility() {
1331 assert!(
1332 version(1, 0).check_readable_by(version(1, 0)).is_ok(),
1333 "same version should be compatible"
1334 );
1335
1336 assert!(
1337 version(1, 0).check_readable_by(version(1, 2)).is_ok(),
1338 "older minor version should be compatible"
1339 );
1340
1341 let error = version(1, 3).check_readable_by(version(1, 2)).unwrap_err();
1342 assert_eq!(
1343 error,
1344 StoreVersionIncompatibility::MinorTooNew {
1345 recording_minor: StoreFormatMinorVersion::new(3),
1346 supported_minor: StoreFormatMinorVersion::new(2),
1347 },
1348 "newer minor version should be incompatible"
1349 );
1350 insta::assert_snapshot!(error.to_string(), @"minor version 3 is newer than supported version 2");
1351
1352 let error = version(2, 0).check_readable_by(version(1, 5)).unwrap_err();
1354 assert_eq!(
1355 error,
1356 StoreVersionIncompatibility::RecordingTooNew {
1357 recording_major: StoreFormatMajorVersion::new(2),
1358 supported_major: StoreFormatMajorVersion::new(1),
1359 },
1360 );
1361 insta::assert_snapshot!(
1362 error.to_string(),
1363 @"recording has major version 2, but this nextest only supports version 1 (upgrade nextest to replay this recording)"
1364 );
1365
1366 let error = version(1, 0).check_readable_by(version(2, 0)).unwrap_err();
1368 assert_eq!(
1369 error,
1370 StoreVersionIncompatibility::RecordingTooOld {
1371 recording_major: StoreFormatMajorVersion::new(1),
1372 supported_major: StoreFormatMajorVersion::new(2),
1373 last_nextest_version: Some("0.9.130"),
1374 },
1375 );
1376 insta::assert_snapshot!(
1377 error.to_string(),
1378 @"recording has major version 1, but this nextest requires version 2 (use nextest <= 0.9.130 to replay this recording)"
1379 );
1380
1381 let error = version(3, 0).check_readable_by(version(5, 0)).unwrap_err();
1383 assert_eq!(
1384 error,
1385 StoreVersionIncompatibility::RecordingTooOld {
1386 recording_major: StoreFormatMajorVersion::new(3),
1387 supported_major: StoreFormatMajorVersion::new(5),
1388 last_nextest_version: None,
1389 },
1390 );
1391 insta::assert_snapshot!(
1392 error.to_string(),
1393 @"recording has major version 3, but this nextest requires version 5"
1394 );
1395
1396 insta::assert_snapshot!(version(1, 2).to_string(), @"1.2");
1397 }
1398
1399 #[test]
1400 fn test_recorded_run_deserialize_without_minor_version() {
1401 let json = r#"{
1403 "run-id": "550e8400-e29b-41d4-a716-446655440000",
1404 "store-format-version": 1,
1405 "nextest-version": "0.9.111",
1406 "started-at": "2024-12-19T14:22:33-08:00",
1407 "last-written-at": "2024-12-19T22:22:33Z",
1408 "cli-args": [],
1409 "env-vars": {},
1410 "sizes": {
1411 "log": { "compressed": 0, "uncompressed": 0 },
1412 "store": { "compressed": 0, "uncompressed": 0 }
1413 },
1414 "status": { "status": "incomplete" }
1415 }"#;
1416 let run: RecordedRun = serde_json::from_str(json).expect("should deserialize");
1417 assert_eq!(run.store_format_version, StoreFormatMajorVersion::new(1));
1418 assert_eq!(
1419 run.store_format_minor_version,
1420 StoreFormatMinorVersion::new(0)
1421 );
1422
1423 let info: RecordedRunInfo = run.into();
1425 assert_eq!(info.store_format_version, version(1, 0));
1426 }
1427
1428 #[test]
1429 fn test_recorded_run_serialize_includes_minor_version() {
1430 let run = make_test_run(RecordedRunStatusFormat::Incomplete);
1432 let json = serde_json::to_string_pretty(&run).expect("serialization should succeed");
1433 assert!(
1434 json.contains("store-format-minor-version"),
1435 "serialized run should include store-format-minor-version"
1436 );
1437 }
1438
1439 fn portable_version(major: u32, minor: u32) -> PortableRecordingFormatVersion {
1443 PortableRecordingFormatVersion::new(
1444 PortableRecordingFormatMajorVersion::new(major),
1445 PortableRecordingFormatMinorVersion::new(minor),
1446 )
1447 }
1448
1449 #[test]
1450 fn test_portable_version_compatibility() {
1451 assert!(
1452 portable_version(1, 0)
1453 .check_readable_by(portable_version(1, 0))
1454 .is_ok(),
1455 "same version should be compatible"
1456 );
1457
1458 assert!(
1459 portable_version(1, 0)
1460 .check_readable_by(portable_version(1, 2))
1461 .is_ok(),
1462 "older minor version should be compatible"
1463 );
1464
1465 let error = portable_version(1, 3)
1466 .check_readable_by(portable_version(1, 2))
1467 .unwrap_err();
1468 assert_eq!(
1469 error,
1470 PortableRecordingVersionIncompatibility::MinorTooNew {
1471 recording_minor: PortableRecordingFormatMinorVersion::new(3),
1472 supported_minor: PortableRecordingFormatMinorVersion::new(2),
1473 },
1474 "newer minor version should be incompatible"
1475 );
1476 insta::assert_snapshot!(error.to_string(), @"minor version 3 is newer than supported version 2");
1477
1478 let error = portable_version(2, 0)
1479 .check_readable_by(portable_version(1, 5))
1480 .unwrap_err();
1481 assert_eq!(
1482 error,
1483 PortableRecordingVersionIncompatibility::MajorMismatch {
1484 recording_major: PortableRecordingFormatMajorVersion::new(2),
1485 supported_major: PortableRecordingFormatMajorVersion::new(1),
1486 },
1487 "different major version should be incompatible"
1488 );
1489 insta::assert_snapshot!(error.to_string(), @"major version 2 differs from supported version 1");
1490
1491 insta::assert_snapshot!(portable_version(1, 2).to_string(), @"1.2");
1492 }
1493
1494 #[test]
1495 fn test_portable_version_serialization() {
1496 let version = portable_version(1, 0);
1498 let json = serde_json::to_string(&version).expect("serialization should succeed");
1499 insta::assert_snapshot!(json, @r#"{"major":1,"minor":0}"#);
1500
1501 let roundtripped: PortableRecordingFormatVersion =
1503 serde_json::from_str(&json).expect("deserialization should succeed");
1504 assert_eq!(roundtripped, version);
1505 }
1506
1507 #[test]
1508 fn test_portable_manifest_format_version() {
1509 assert_eq!(
1511 PORTABLE_RECORDING_FORMAT_VERSION,
1512 portable_version(1, 0),
1513 "current portable recording format version should be 1.0"
1514 );
1515 }
1516}