1use super::{
7 CompletedRunStats, ComponentSizes, RecordedRunInfo, RecordedRunStatus, RecordedSizes,
8 StressCompletedRunStats,
9};
10use crate::build_scope::BuildScope;
11use camino::Utf8Path;
12use chrono::{DateTime, FixedOffset, Utc};
13use eazip::{CompressionMethod, write::FileOptions};
14use iddqd::{IdOrdItem, IdOrdMap, id_upcast};
15use nextest_metadata::{RustBinaryId, TestCaseName};
16use quick_junit::ReportUuid;
17use semver::Version;
18use serde::{Deserialize, Serialize};
19use std::{
20 collections::{BTreeMap, BTreeSet},
21 fmt,
22 num::NonZero,
23};
24
25macro_rules! define_format_version {
33 (
34 $(#[$attr:meta])*
35 $vis:vis struct $name:ident;
36 ) => {
37 $(#[$attr])*
38 #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Deserialize, Serialize)]
39 #[serde(transparent)]
40 $vis struct $name(u32);
41
42 impl $name {
43 #[doc = concat!("Creates a new `", stringify!($name), "`.")]
44 pub const fn new(version: u32) -> Self {
45 Self(version)
46 }
47 }
48
49 impl fmt::Display for $name {
50 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
51 write!(f, "{}", self.0)
52 }
53 }
54 };
55
56 (
57 @default
58 $(#[$attr:meta])*
59 $vis:vis struct $name:ident;
60 ) => {
61 $(#[$attr])*
62 #[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Deserialize, Serialize)]
63 #[serde(transparent)]
64 $vis struct $name(u32);
65
66 impl $name {
67 #[doc = concat!("Creates a new `", stringify!($name), "`.")]
68 pub const fn new(version: u32) -> Self {
69 Self(version)
70 }
71 }
72
73 impl fmt::Display for $name {
74 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
75 write!(f, "{}", self.0)
76 }
77 }
78 };
79}
80
81define_format_version! {
82 pub struct RunsJsonFormatVersion;
89}
90
91define_format_version! {
92 pub struct StoreFormatMajorVersion;
95}
96
97define_format_version! {
98 @default
99 pub struct StoreFormatMinorVersion;
101}
102
103#[derive(Clone, Copy, Debug, PartialEq, Eq)]
105pub struct StoreFormatVersion {
106 pub major: StoreFormatMajorVersion,
108 pub minor: StoreFormatMinorVersion,
110}
111
112impl StoreFormatVersion {
113 pub const fn new(major: StoreFormatMajorVersion, minor: StoreFormatMinorVersion) -> Self {
115 Self { major, minor }
116 }
117
118 pub fn check_readable_by(self, supported: Self) -> Result<(), StoreVersionIncompatibility> {
121 if self.major < supported.major {
122 return Err(StoreVersionIncompatibility::RecordingTooOld {
123 recording_major: self.major,
124 supported_major: supported.major,
125 guidance: self.major.too_old_guidance(),
126 });
127 }
128 if self.major > supported.major {
129 return Err(StoreVersionIncompatibility::RecordingTooNew {
130 recording_major: self.major,
131 supported_major: supported.major,
132 });
133 }
134 if self.minor > supported.minor {
135 return Err(StoreVersionIncompatibility::MinorTooNew {
136 recording_minor: self.minor,
137 supported_minor: supported.minor,
138 });
139 }
140 Ok(())
141 }
142}
143
144impl fmt::Display for StoreFormatVersion {
145 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
146 write!(f, "{}.{}", self.major, self.minor)
147 }
148}
149
150impl StoreFormatMajorVersion {
151 pub fn too_old_guidance(self) -> TooOldRecordingGuidance {
154 match self.0 {
155 1 => TooOldRecordingGuidance::Discard {
157 last_nextest_version: "0.9.130",
158 },
159 2 => TooOldRecordingGuidance::Discard {
160 last_nextest_version: "0.9.149",
161 },
162 _ => TooOldRecordingGuidance::Unknown,
163 }
164 }
165}
166
167#[derive(Clone, Copy, Debug, PartialEq, Eq)]
170pub enum TooOldRecordingGuidance {
171 UseOlderNextest {
176 last_nextest_version: &'static str,
178 },
179 Discard {
181 last_nextest_version: &'static str,
183 },
184 Unknown,
186}
187
188#[derive(Clone, Debug, PartialEq, Eq)]
191pub enum StoreVersionIncompatibility {
192 RecordingTooOld {
194 recording_major: StoreFormatMajorVersion,
196 supported_major: StoreFormatMajorVersion,
198 guidance: TooOldRecordingGuidance,
200 },
201 RecordingTooNew {
203 recording_major: StoreFormatMajorVersion,
205 supported_major: StoreFormatMajorVersion,
207 },
208 MinorTooNew {
210 recording_minor: StoreFormatMinorVersion,
212 supported_minor: StoreFormatMinorVersion,
214 },
215}
216
217impl fmt::Display for StoreVersionIncompatibility {
218 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
219 match self {
220 Self::RecordingTooOld {
221 recording_major,
222 supported_major,
223 guidance,
224 } => {
225 write!(
226 f,
227 "recording has major version {recording_major}, \
228 but this nextest requires version {supported_major}"
229 )?;
230 match guidance {
231 TooOldRecordingGuidance::UseOlderNextest {
232 last_nextest_version,
233 } => write!(
234 f,
235 "\n(use nextest <= {last_nextest_version} to replay this recording)"
236 ),
237 TooOldRecordingGuidance::Discard {
238 last_nextest_version,
239 } => write!(
240 f,
241 "\n(recordings from nextest {last_nextest_version} and earlier \
242 are no longer supported; discard this recording and record a new run)"
243 ),
244 TooOldRecordingGuidance::Unknown => Ok(()),
245 }
246 }
247 Self::RecordingTooNew {
248 recording_major,
249 supported_major,
250 } => {
251 write!(
252 f,
253 "recording has major version {recording_major}, \
254 but this nextest only supports version {supported_major} \
255 (upgrade nextest to replay this recording)"
256 )
257 }
258 Self::MinorTooNew {
259 recording_minor,
260 supported_minor,
261 } => {
262 write!(
263 f,
264 "minor version {} is newer than supported version {}",
265 recording_minor, supported_minor
266 )
267 }
268 }
269 }
270}
271
272pub(super) const RUNS_JSON_FORMAT_VERSION: RunsJsonFormatVersion = RunsJsonFormatVersion::new(3);
284
285pub const STORE_FORMAT_VERSION: StoreFormatVersion = StoreFormatVersion::new(
305 StoreFormatMajorVersion::new(3),
306 StoreFormatMinorVersion::new(0),
307);
308
309pub(super) const FORCE_STORE_FORMAT_VERSION_ENV: &str = "__NEXTEST_FORCE_STORE_FORMAT_VERSION";
319
320pub(super) fn store_format_version_for_new_run() -> StoreFormatVersion {
322 let Some(raw) = std::env::var_os(FORCE_STORE_FORMAT_VERSION_ENV) else {
323 return STORE_FORMAT_VERSION;
324 };
325 let raw = raw.to_str().unwrap_or_else(|| {
326 panic!("{FORCE_STORE_FORMAT_VERSION_ENV} contains non-UTF-8 bytes");
327 });
328 let (major, minor) = raw.split_once('.').unwrap_or_else(|| {
329 panic!(
330 "{FORCE_STORE_FORMAT_VERSION_ENV}={raw:?} is malformed \
331 (expected MAJOR.MINOR, e.g. 9999.0)"
332 )
333 });
334 let major: u32 = major.parse().unwrap_or_else(|err| {
335 panic!("{FORCE_STORE_FORMAT_VERSION_ENV}={raw:?} has invalid major version: {err}")
336 });
337 let minor: u32 = minor.parse().unwrap_or_else(|err| {
338 panic!("{FORCE_STORE_FORMAT_VERSION_ENV}={raw:?} has invalid minor version: {err}")
339 });
340 StoreFormatVersion::new(
341 StoreFormatMajorVersion::new(major),
342 StoreFormatMinorVersion::new(minor),
343 )
344}
345
346#[derive(Debug, Clone, Copy, PartialEq, Eq)]
348pub enum RunsJsonWritePermission {
349 Allowed,
351 Denied {
353 file_version: RunsJsonFormatVersion,
355 max_supported_version: RunsJsonFormatVersion,
357 },
358}
359
360#[derive(Debug, Deserialize, Serialize)]
362#[serde(rename_all = "kebab-case")]
363pub(super) struct RecordedRunList {
364 pub(super) format_version: RunsJsonFormatVersion,
366
367 #[serde(default, skip_serializing_if = "Option::is_none")]
372 pub(super) last_pruned_at: Option<DateTime<Utc>>,
373
374 #[serde(default)]
376 pub(super) runs: Vec<RecordedRun>,
377}
378
379pub(super) struct RunListData {
381 pub(super) runs: Vec<RecordedRunInfo>,
382 pub(super) last_pruned_at: Option<DateTime<Utc>>,
383}
384
385impl RecordedRunList {
386 #[cfg(test)]
388 fn new() -> Self {
389 Self {
390 format_version: RUNS_JSON_FORMAT_VERSION,
391 last_pruned_at: None,
392 runs: Vec::new(),
393 }
394 }
395
396 pub(super) fn into_data(self) -> RunListData {
398 RunListData {
399 runs: self.runs.into_iter().map(RecordedRunInfo::from).collect(),
400 last_pruned_at: self.last_pruned_at,
401 }
402 }
403
404 pub(super) fn from_data(
409 runs: &[RecordedRunInfo],
410 last_pruned_at: Option<DateTime<Utc>>,
411 ) -> Self {
412 Self {
413 format_version: RUNS_JSON_FORMAT_VERSION,
414 last_pruned_at,
415 runs: runs.iter().map(RecordedRun::from).collect(),
416 }
417 }
418
419 pub(super) fn write_permission(&self) -> RunsJsonWritePermission {
424 if self.format_version > RUNS_JSON_FORMAT_VERSION {
425 RunsJsonWritePermission::Denied {
426 file_version: self.format_version,
427 max_supported_version: RUNS_JSON_FORMAT_VERSION,
428 }
429 } else {
430 RunsJsonWritePermission::Allowed
431 }
432 }
433}
434
435#[derive(Clone, Debug, Deserialize, Serialize)]
437#[serde(rename_all = "kebab-case")]
438pub(super) struct RecordedRun {
439 pub(super) run_id: ReportUuid,
441 pub(super) store_format_version: StoreFormatMajorVersion,
446 #[serde(default)]
451 pub(super) store_format_minor_version: StoreFormatMinorVersion,
452 pub(super) nextest_version: Version,
454 pub(super) started_at: DateTime<FixedOffset>,
456 pub(super) last_written_at: DateTime<FixedOffset>,
462 #[serde(default, skip_serializing_if = "Option::is_none")]
464 pub(super) duration_secs: Option<f64>,
465 #[serde(default)]
467 pub(super) cli_args: Vec<String>,
468 #[serde(default)]
472 pub(super) env_vars: BTreeMap<String, String>,
473 #[serde(default)]
475 pub(super) parent_run_id: Option<ReportUuid>,
476 pub(super) sizes: RecordedSizesFormat,
480 pub(super) status: RecordedRunStatusFormat,
482}
483
484#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Deserialize, Serialize)]
486#[serde(rename_all = "kebab-case")]
487pub(super) struct RecordedSizesFormat {
488 pub(super) log: ComponentSizesFormat,
490 pub(super) store: ComponentSizesFormat,
492}
493
494#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Deserialize, Serialize)]
496#[serde(rename_all = "kebab-case")]
497pub(super) struct ComponentSizesFormat {
498 pub(super) compressed: u64,
500 pub(super) uncompressed: u64,
502 #[serde(default)]
504 pub(super) entries: u64,
505}
506
507impl From<RecordedSizes> for RecordedSizesFormat {
508 fn from(sizes: RecordedSizes) -> Self {
509 Self {
510 log: ComponentSizesFormat {
511 compressed: sizes.log.compressed,
512 uncompressed: sizes.log.uncompressed,
513 entries: sizes.log.entries,
514 },
515 store: ComponentSizesFormat {
516 compressed: sizes.store.compressed,
517 uncompressed: sizes.store.uncompressed,
518 entries: sizes.store.entries,
519 },
520 }
521 }
522}
523
524impl From<RecordedSizesFormat> for RecordedSizes {
525 fn from(sizes: RecordedSizesFormat) -> Self {
526 Self {
527 log: ComponentSizes {
528 compressed: sizes.log.compressed,
529 uncompressed: sizes.log.uncompressed,
530 entries: sizes.log.entries,
531 },
532 store: ComponentSizes {
533 compressed: sizes.store.compressed,
534 uncompressed: sizes.store.uncompressed,
535 entries: sizes.store.entries,
536 },
537 }
538 }
539}
540
541#[derive(Clone, Debug, Deserialize, Serialize)]
543#[serde(tag = "status", rename_all = "kebab-case")]
544pub(super) enum RecordedRunStatusFormat {
545 Incomplete,
547 #[serde(rename_all = "kebab-case")]
549 Completed {
550 initial_run_count: usize,
552 passed: usize,
554 failed: usize,
556 exit_code: i32,
558 },
559 #[serde(rename_all = "kebab-case")]
561 Cancelled {
562 initial_run_count: usize,
564 passed: usize,
566 failed: usize,
568 exit_code: i32,
570 },
571 #[serde(rename_all = "kebab-case")]
573 StressCompleted {
574 initial_iteration_count: Option<NonZero<u32>>,
576 success_count: u32,
578 failed_count: u32,
580 exit_code: i32,
582 },
583 #[serde(rename_all = "kebab-case")]
585 StressCancelled {
586 initial_iteration_count: Option<NonZero<u32>>,
588 success_count: u32,
590 failed_count: u32,
592 exit_code: i32,
594 },
595 #[serde(other)]
600 Unknown,
601}
602
603impl From<RecordedRun> for RecordedRunInfo {
604 fn from(run: RecordedRun) -> Self {
605 Self {
606 run_id: run.run_id,
607 store_format_version: StoreFormatVersion::new(
608 run.store_format_version,
609 run.store_format_minor_version,
610 ),
611 nextest_version: run.nextest_version,
612 started_at: run.started_at,
613 last_written_at: run.last_written_at,
614 duration_secs: run.duration_secs,
615 cli_args: run.cli_args,
616 env_vars: run.env_vars,
617 parent_run_id: run.parent_run_id,
618 sizes: run.sizes.into(),
619 status: run.status.into(),
620 }
621 }
622}
623
624impl From<&RecordedRunInfo> for RecordedRun {
625 fn from(run: &RecordedRunInfo) -> Self {
626 Self {
627 run_id: run.run_id,
628 store_format_version: run.store_format_version.major,
629 store_format_minor_version: run.store_format_version.minor,
630 nextest_version: run.nextest_version.clone(),
631 started_at: run.started_at,
632 last_written_at: run.last_written_at,
633 duration_secs: run.duration_secs,
634 cli_args: run.cli_args.clone(),
635 env_vars: run.env_vars.clone(),
636 parent_run_id: run.parent_run_id,
637 sizes: run.sizes.into(),
638 status: (&run.status).into(),
639 }
640 }
641}
642
643impl From<RecordedRunStatusFormat> for RecordedRunStatus {
644 fn from(status: RecordedRunStatusFormat) -> Self {
645 match status {
646 RecordedRunStatusFormat::Incomplete => Self::Incomplete,
647 RecordedRunStatusFormat::Unknown => Self::Unknown,
648 RecordedRunStatusFormat::Completed {
649 initial_run_count,
650 passed,
651 failed,
652 exit_code,
653 } => Self::Completed(CompletedRunStats {
654 initial_run_count,
655 passed,
656 failed,
657 exit_code,
658 }),
659 RecordedRunStatusFormat::Cancelled {
660 initial_run_count,
661 passed,
662 failed,
663 exit_code,
664 } => Self::Cancelled(CompletedRunStats {
665 initial_run_count,
666 passed,
667 failed,
668 exit_code,
669 }),
670 RecordedRunStatusFormat::StressCompleted {
671 initial_iteration_count,
672 success_count,
673 failed_count,
674 exit_code,
675 } => Self::StressCompleted(StressCompletedRunStats {
676 initial_iteration_count,
677 success_count,
678 failed_count,
679 exit_code,
680 }),
681 RecordedRunStatusFormat::StressCancelled {
682 initial_iteration_count,
683 success_count,
684 failed_count,
685 exit_code,
686 } => Self::StressCancelled(StressCompletedRunStats {
687 initial_iteration_count,
688 success_count,
689 failed_count,
690 exit_code,
691 }),
692 }
693 }
694}
695
696impl From<&RecordedRunStatus> for RecordedRunStatusFormat {
697 fn from(status: &RecordedRunStatus) -> Self {
698 match status {
699 RecordedRunStatus::Incomplete => Self::Incomplete,
700 RecordedRunStatus::Unknown => Self::Unknown,
701 RecordedRunStatus::Completed(stats) => Self::Completed {
702 initial_run_count: stats.initial_run_count,
703 passed: stats.passed,
704 failed: stats.failed,
705 exit_code: stats.exit_code,
706 },
707 RecordedRunStatus::Cancelled(stats) => Self::Cancelled {
708 initial_run_count: stats.initial_run_count,
709 passed: stats.passed,
710 failed: stats.failed,
711 exit_code: stats.exit_code,
712 },
713 RecordedRunStatus::StressCompleted(stats) => Self::StressCompleted {
714 initial_iteration_count: stats.initial_iteration_count,
715 success_count: stats.success_count,
716 failed_count: stats.failed_count,
717 exit_code: stats.exit_code,
718 },
719 RecordedRunStatus::StressCancelled(stats) => Self::StressCancelled {
720 initial_iteration_count: stats.initial_iteration_count,
721 success_count: stats.success_count,
722 failed_count: stats.failed_count,
723 exit_code: stats.exit_code,
724 },
725 }
726 }
727}
728
729#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
732#[serde(rename_all = "kebab-case")]
733pub struct RecordedBuildScope {
734 pub build_kind: BuildKind,
736 pub scope: BuildScope,
741}
742
743impl RecordedBuildScope {
744 pub fn new(build_kind: BuildKind, scope: BuildScope) -> Self {
747 Self { build_kind, scope }
748 }
749}
750
751#[derive(Clone, Copy, Debug, PartialEq, Eq, Deserialize, Serialize)]
756#[serde(rename_all = "kebab-case")]
757pub enum BuildKind {
758 Cargo,
761 Reused,
763}
764
765#[derive(Clone, Debug, Deserialize, Serialize)]
773#[serde(rename_all = "kebab-case")]
774pub struct RerunInfo {
775 pub parent_run_id: ReportUuid,
777
778 pub root_info: RerunRootInfo,
780
781 pub test_suites: IdOrdMap<RerunTestSuiteInfo>,
783}
784
785#[derive(Clone, Debug, Deserialize, Serialize)]
787#[serde(rename_all = "kebab-case")]
788pub struct RerunRootInfo {
789 pub run_id: ReportUuid,
791
792 pub build_scope: RecordedBuildScope,
794}
795
796impl RerunRootInfo {
797 pub fn new(run_id: ReportUuid, build_scope: RecordedBuildScope) -> Self {
802 Self {
803 run_id,
804 build_scope,
805 }
806 }
807}
808
809#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
811pub struct RerunTestSuiteInfo {
812 pub binary_id: RustBinaryId,
814
815 pub passing: BTreeSet<TestCaseName>,
817
818 pub outstanding: BTreeSet<TestCaseName>,
820}
821
822impl RerunTestSuiteInfo {
823 pub(super) fn new(binary_id: RustBinaryId) -> Self {
824 Self {
825 binary_id,
826 passing: BTreeSet::new(),
827 outstanding: BTreeSet::new(),
828 }
829 }
830}
831
832impl IdOrdItem for RerunTestSuiteInfo {
833 type Key<'a> = &'a RustBinaryId;
834 fn key(&self) -> Self::Key<'_> {
835 &self.binary_id
836 }
837 id_upcast!();
838}
839
840pub static STORE_ZIP_FILE_NAME: &str = "store.zip";
846
847pub static RUN_LOG_FILE_NAME: &str = "run.log.zst";
849
850pub fn has_zip_extension(path: &Utf8Path) -> bool {
852 path.extension()
853 .is_some_and(|ext| ext.eq_ignore_ascii_case("zip"))
854}
855
856pub static CARGO_METADATA_JSON_PATH: &str = "meta/cargo-metadata.json";
859pub static TEST_LIST_JSON_PATH: &str = "meta/test-list.json";
861pub static RECORD_OPTS_JSON_PATH: &str = "meta/record-opts.json";
863pub static BUILD_SCOPE_JSON_PATH: &str = "meta/build-scope.json";
868pub static RERUN_INFO_JSON_PATH: &str = "meta/rerun-info.json";
870pub static STDOUT_DICT_PATH: &str = "meta/stdout.dict";
872pub static STDERR_DICT_PATH: &str = "meta/stderr.dict";
874
875define_format_version! {
880 pub struct PortableRecordingFormatMajorVersion;
882}
883
884define_format_version! {
885 @default
886 pub struct PortableRecordingFormatMinorVersion;
888}
889
890#[derive(Clone, Copy, Debug, PartialEq, Eq, Deserialize, Serialize)]
892pub struct PortableRecordingFormatVersion {
893 pub major: PortableRecordingFormatMajorVersion,
895 pub minor: PortableRecordingFormatMinorVersion,
897}
898
899impl PortableRecordingFormatVersion {
900 pub const fn new(
902 major: PortableRecordingFormatMajorVersion,
903 minor: PortableRecordingFormatMinorVersion,
904 ) -> Self {
905 Self { major, minor }
906 }
907
908 pub fn check_readable_by(
911 self,
912 supported: Self,
913 ) -> Result<(), PortableRecordingVersionIncompatibility> {
914 if self.major != supported.major {
915 return Err(PortableRecordingVersionIncompatibility::MajorMismatch {
916 recording_major: self.major,
917 supported_major: supported.major,
918 });
919 }
920 if self.minor > supported.minor {
921 return Err(PortableRecordingVersionIncompatibility::MinorTooNew {
922 recording_minor: self.minor,
923 supported_minor: supported.minor,
924 });
925 }
926 Ok(())
927 }
928}
929
930impl fmt::Display for PortableRecordingFormatVersion {
931 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
932 write!(f, "{}.{}", self.major, self.minor)
933 }
934}
935
936#[derive(Clone, Copy, Debug, PartialEq, Eq)]
939pub enum PortableRecordingVersionIncompatibility {
940 MajorMismatch {
942 recording_major: PortableRecordingFormatMajorVersion,
944 supported_major: PortableRecordingFormatMajorVersion,
946 },
947 MinorTooNew {
949 recording_minor: PortableRecordingFormatMinorVersion,
951 supported_minor: PortableRecordingFormatMinorVersion,
953 },
954}
955
956impl fmt::Display for PortableRecordingVersionIncompatibility {
957 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
958 match self {
959 Self::MajorMismatch {
960 recording_major,
961 supported_major,
962 } => {
963 write!(
964 f,
965 "major version {} differs from supported version {}",
966 recording_major, supported_major
967 )
968 }
969 Self::MinorTooNew {
970 recording_minor,
971 supported_minor,
972 } => {
973 write!(
974 f,
975 "minor version {} is newer than supported version {}",
976 recording_minor, supported_minor
977 )
978 }
979 }
980 }
981}
982
983pub const PORTABLE_RECORDING_FORMAT_VERSION: PortableRecordingFormatVersion =
990 PortableRecordingFormatVersion::new(
991 PortableRecordingFormatMajorVersion::new(1),
992 PortableRecordingFormatMinorVersion::new(1),
993 );
994
995pub static PORTABLE_MANIFEST_FILE_NAME: &str = "manifest.json";
997
998#[derive(Debug, Deserialize, Serialize)]
1003#[serde(rename_all = "kebab-case")]
1004pub(crate) struct PortableManifest {
1005 pub(crate) format_version: PortableRecordingFormatVersion,
1007 pub(super) run: RecordedRun,
1009}
1010
1011impl PortableManifest {
1012 pub(crate) fn new(run: &RecordedRunInfo) -> Self {
1014 Self {
1015 format_version: PORTABLE_RECORDING_FORMAT_VERSION,
1016 run: RecordedRun::from(run),
1017 }
1018 }
1019
1020 pub(crate) fn run_info(&self) -> RecordedRunInfo {
1022 RecordedRunInfo::from(self.run.clone())
1023 }
1024
1025 pub(crate) fn store_format_version(&self) -> StoreFormatVersion {
1027 StoreFormatVersion::new(
1028 self.run.store_format_version,
1029 self.run.store_format_minor_version,
1030 )
1031 }
1032}
1033
1034#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1036pub enum OutputDict {
1037 Stdout,
1039 Stderr,
1041 None,
1043}
1044
1045impl OutputDict {
1046 pub fn for_path(path: &Utf8Path) -> Self {
1054 let mut iter = path.iter();
1055 let Some(first_component) = iter.next() else {
1056 return Self::None;
1057 };
1058 if first_component != "out" {
1060 return Self::None;
1061 }
1062
1063 Self::for_output_file_name(iter.as_path().as_str())
1064 }
1065
1066 pub fn for_output_file_name(file_name: &str) -> Self {
1071 if file_name.ends_with("-stdout") || file_name.ends_with("-combined") {
1072 Self::Stdout
1073 } else if file_name.ends_with("-stderr") {
1074 Self::Stderr
1075 } else {
1076 Self::None
1078 }
1079 }
1080
1081 pub fn dict_bytes(self) -> Option<&'static [u8]> {
1085 match self {
1086 Self::Stdout => Some(super::dicts::STDOUT),
1087 Self::Stderr => Some(super::dicts::STDERR),
1088 Self::None => None,
1089 }
1090 }
1091}
1092
1093pub(super) fn stored_file_options() -> FileOptions {
1100 let mut options = FileOptions::default();
1101 options.compression_method = CompressionMethod::STORE;
1102 options
1103}
1104
1105pub(super) fn zstd_file_options() -> FileOptions {
1107 let mut options = FileOptions::default();
1108 options.compression_method = CompressionMethod::ZSTD;
1109 options.level = Some(3);
1110 options
1111}
1112
1113#[cfg(test)]
1114mod tests {
1115 use super::*;
1116 use crate::build_scope::{
1117 FeatureSelection, NamedTargets, PackageSelection, PackageSpec, TargetSelection,
1118 };
1119
1120 #[test]
1121 fn test_output_dict_for_path() {
1122 assert_eq!(
1124 OutputDict::for_path("meta/cargo-metadata.json".as_ref()),
1125 OutputDict::None
1126 );
1127 assert_eq!(
1128 OutputDict::for_path("meta/test-list.json".as_ref()),
1129 OutputDict::None
1130 );
1131
1132 assert_eq!(
1134 OutputDict::for_path("out/0123456789abcdef-stdout".as_ref()),
1135 OutputDict::Stdout
1136 );
1137 assert_eq!(
1138 OutputDict::for_path("out/0123456789abcdef-stderr".as_ref()),
1139 OutputDict::Stderr
1140 );
1141 assert_eq!(
1142 OutputDict::for_path("out/0123456789abcdef-combined".as_ref()),
1143 OutputDict::Stdout
1144 );
1145 }
1146
1147 #[test]
1148 fn test_output_dict_for_output_file_name() {
1149 assert_eq!(
1151 OutputDict::for_output_file_name("0123456789abcdef-stdout"),
1152 OutputDict::Stdout
1153 );
1154 assert_eq!(
1155 OutputDict::for_output_file_name("0123456789abcdef-stderr"),
1156 OutputDict::Stderr
1157 );
1158 assert_eq!(
1159 OutputDict::for_output_file_name("0123456789abcdef-combined"),
1160 OutputDict::Stdout
1161 );
1162 assert_eq!(
1163 OutputDict::for_output_file_name("0123456789abcdef-unknown"),
1164 OutputDict::None
1165 );
1166 }
1167
1168 #[test]
1169 fn test_dict_bytes() {
1170 assert!(OutputDict::Stdout.dict_bytes().is_some());
1171 assert!(OutputDict::Stderr.dict_bytes().is_some());
1172 assert!(OutputDict::None.dict_bytes().is_none());
1173 }
1174
1175 #[test]
1176 fn test_runs_json_missing_version() {
1177 let json = r#"{"runs": []}"#;
1179 let result: Result<RecordedRunList, _> = serde_json::from_str(json);
1180 assert!(result.is_err(), "expected error for missing format-version");
1181 }
1182
1183 #[test]
1184 fn test_runs_json_current_version() {
1185 let json = format!(
1187 r#"{{"format-version": {}, "runs": []}}"#,
1188 RUNS_JSON_FORMAT_VERSION
1189 );
1190 let list: RecordedRunList = serde_json::from_str(&json).expect("should deserialize");
1191 assert_eq!(list.write_permission(), RunsJsonWritePermission::Allowed);
1192 }
1193
1194 #[test]
1195 fn test_runs_json_older_version() {
1196 let json = r#"{"format-version": 1, "runs": []}"#;
1200 let list: RecordedRunList = serde_json::from_str(json).expect("should deserialize");
1201 assert_eq!(list.write_permission(), RunsJsonWritePermission::Allowed);
1202 }
1203
1204 #[test]
1205 fn test_runs_json_newer_version() {
1206 let json = r#"{"format-version": 99, "runs": []}"#;
1208 let list: RecordedRunList = serde_json::from_str(json).expect("should deserialize");
1209 assert_eq!(
1210 list.write_permission(),
1211 RunsJsonWritePermission::Denied {
1212 file_version: RunsJsonFormatVersion::new(99),
1213 max_supported_version: RUNS_JSON_FORMAT_VERSION,
1214 }
1215 );
1216 }
1217
1218 #[test]
1219 fn test_runs_json_serialization_includes_version() {
1220 let list = RecordedRunList::from_data(&[], None);
1222 let json = serde_json::to_string(&list).expect("should serialize");
1223 assert!(
1224 json.contains("format-version"),
1225 "serialized runs.json.zst should include format-version"
1226 );
1227
1228 let parsed: serde_json::Value = serde_json::from_str(&json).expect("should parse");
1230 let version: RunsJsonFormatVersion =
1231 serde_json::from_value(parsed["format-version"].clone()).expect("valid version");
1232 assert_eq!(
1233 version, RUNS_JSON_FORMAT_VERSION,
1234 "format-version should be current version"
1235 );
1236 }
1237
1238 #[test]
1239 fn test_runs_json_new() {
1240 let list = RecordedRunList::new();
1242 assert_eq!(list.format_version, RUNS_JSON_FORMAT_VERSION);
1243 assert!(list.runs.is_empty());
1244 assert_eq!(list.write_permission(), RunsJsonWritePermission::Allowed);
1245 }
1246
1247 fn make_test_build_scope() -> BuildScope {
1250 BuildScope::new(
1251 PackageSelection {
1252 specs: Vec::new(),
1253 workspace: true,
1254 exclude: vec![PackageSpec::new("nextest-*").expect("spec is valid")],
1255 },
1256 TargetSelection {
1257 lib: false,
1258 bins: NamedTargets::empty(),
1259 examples: NamedTargets::empty(),
1260 tests: NamedTargets {
1261 names: vec!["integration".to_owned()],
1262 all: false,
1263 },
1264 benches: NamedTargets::empty(),
1265 all_targets: false,
1266 },
1267 FeatureSelection {
1268 features: vec!["feat-a".to_owned()],
1269 all_features: false,
1270 no_default_features: false,
1271 },
1272 )
1273 }
1274
1275 fn make_test_run(status: RecordedRunStatusFormat) -> RecordedRun {
1276 RecordedRun {
1277 run_id: ReportUuid::from_u128(0x550e8400_e29b_41d4_a716_446655440000),
1278 store_format_version: STORE_FORMAT_VERSION.major,
1279 store_format_minor_version: STORE_FORMAT_VERSION.minor,
1280 nextest_version: Version::new(0, 9, 111),
1281 started_at: DateTime::parse_from_rfc3339("2024-12-19T14:22:33-08:00")
1282 .expect("valid timestamp"),
1283 last_written_at: DateTime::parse_from_rfc3339("2024-12-19T22:22:33Z")
1284 .expect("valid timestamp"),
1285 duration_secs: Some(12.345),
1286 cli_args: vec![
1287 "cargo".to_owned(),
1288 "nextest".to_owned(),
1289 "run".to_owned(),
1290 "--workspace".to_owned(),
1291 ],
1292 env_vars: BTreeMap::from([
1293 ("CARGO_TERM_COLOR".to_owned(), "always".to_owned()),
1294 ("NEXTEST_PROFILE".to_owned(), "ci".to_owned()),
1295 ]),
1296 parent_run_id: Some(ReportUuid::from_u128(
1297 0x550e7400_e29b_41d4_a716_446655440000,
1298 )),
1299 sizes: RecordedSizesFormat {
1300 log: ComponentSizesFormat {
1301 compressed: 2345,
1302 uncompressed: 5678,
1303 entries: 42,
1304 },
1305 store: ComponentSizesFormat {
1306 compressed: 10000,
1307 uncompressed: 40000,
1308 entries: 15,
1309 },
1310 },
1311 status,
1312 }
1313 }
1314
1315 #[test]
1316 fn test_recorded_run_serialize_incomplete() {
1317 let run = make_test_run(RecordedRunStatusFormat::Incomplete);
1318 let json = serde_json::to_string_pretty(&run).expect("serialization should succeed");
1319 insta::assert_snapshot!(json);
1320 }
1321
1322 #[test]
1323 fn test_recorded_run_serialize_completed() {
1324 let run = make_test_run(RecordedRunStatusFormat::Completed {
1325 initial_run_count: 100,
1326 passed: 95,
1327 failed: 5,
1328 exit_code: 0,
1329 });
1330 let json = serde_json::to_string_pretty(&run).expect("serialization should succeed");
1331 insta::assert_snapshot!(json);
1332 }
1333
1334 #[test]
1335 fn test_recorded_run_serialize_cancelled() {
1336 let run = make_test_run(RecordedRunStatusFormat::Cancelled {
1337 initial_run_count: 100,
1338 passed: 45,
1339 failed: 5,
1340 exit_code: 100,
1341 });
1342 let json = serde_json::to_string_pretty(&run).expect("serialization should succeed");
1343 insta::assert_snapshot!(json);
1344 }
1345
1346 #[test]
1347 fn test_recorded_run_serialize_stress_completed() {
1348 let run = make_test_run(RecordedRunStatusFormat::StressCompleted {
1349 initial_iteration_count: NonZero::new(100),
1350 success_count: 98,
1351 failed_count: 2,
1352 exit_code: 0,
1353 });
1354 let json = serde_json::to_string_pretty(&run).expect("serialization should succeed");
1355 insta::assert_snapshot!(json);
1356 }
1357
1358 #[test]
1359 fn test_recorded_run_serialize_stress_cancelled() {
1360 let run = make_test_run(RecordedRunStatusFormat::StressCancelled {
1361 initial_iteration_count: NonZero::new(100),
1362 success_count: 45,
1363 failed_count: 5,
1364 exit_code: 100,
1365 });
1366 let json = serde_json::to_string_pretty(&run).expect("serialization should succeed");
1367 insta::assert_snapshot!(json);
1368 }
1369
1370 #[test]
1371 fn test_recorded_run_deserialize_unknown_status() {
1372 let json = r#"{
1375 "run-id": "550e8400-e29b-41d4-a716-446655440000",
1376 "store-format-version": 999,
1377 "nextest-version": "0.9.999",
1378 "started-at": "2024-12-19T14:22:33-08:00",
1379 "last-written-at": "2024-12-19T22:22:33Z",
1380 "cli-args": ["cargo", "nextest", "run"],
1381 "env-vars": {},
1382 "sizes": {
1383 "log": { "compressed": 2345, "uncompressed": 5678 },
1384 "store": { "compressed": 10000, "uncompressed": 40000 }
1385 },
1386 "status": {
1387 "status": "super-new-status",
1388 "some-future-field": 42
1389 }
1390 }"#;
1391 let run: RecordedRun = serde_json::from_str(json).expect("should deserialize");
1392 assert!(
1393 matches!(run.status, RecordedRunStatusFormat::Unknown),
1394 "unknown status should deserialize to Unknown variant"
1395 );
1396
1397 let info: RecordedRunInfo = run.into();
1399 assert!(
1400 matches!(info.status, RecordedRunStatus::Unknown),
1401 "Unknown format should convert to Unknown domain type"
1402 );
1403 }
1404
1405 #[test]
1406 fn test_recorded_run_roundtrip() {
1407 let original = make_test_run(RecordedRunStatusFormat::Completed {
1408 initial_run_count: 100,
1409 passed: 95,
1410 failed: 5,
1411 exit_code: 0,
1412 });
1413 let json = serde_json::to_string(&original).expect("serialization should succeed");
1414 let roundtripped: RecordedRun =
1415 serde_json::from_str(&json).expect("deserialization should succeed");
1416
1417 assert_eq!(roundtripped.run_id, original.run_id);
1418 assert_eq!(roundtripped.nextest_version, original.nextest_version);
1419 assert_eq!(roundtripped.started_at, original.started_at);
1420 assert_eq!(roundtripped.sizes, original.sizes);
1421
1422 let info: RecordedRunInfo = roundtripped.into();
1424 match info.status {
1425 RecordedRunStatus::Completed(stats) => {
1426 assert_eq!(stats.initial_run_count, 100);
1427 assert_eq!(stats.passed, 95);
1428 assert_eq!(stats.failed, 5);
1429 }
1430 _ => panic!("expected Completed variant"),
1431 }
1432 }
1433
1434 fn version(major: u32, minor: u32) -> StoreFormatVersion {
1438 StoreFormatVersion::new(
1439 StoreFormatMajorVersion::new(major),
1440 StoreFormatMinorVersion::new(minor),
1441 )
1442 }
1443
1444 #[test]
1445 fn test_store_version_compatibility() {
1446 assert!(
1447 version(1, 0).check_readable_by(version(1, 0)).is_ok(),
1448 "same version should be compatible"
1449 );
1450
1451 assert!(
1452 version(1, 0).check_readable_by(version(1, 2)).is_ok(),
1453 "older minor version should be compatible"
1454 );
1455
1456 let error = version(1, 3).check_readable_by(version(1, 2)).unwrap_err();
1457 assert_eq!(
1458 error,
1459 StoreVersionIncompatibility::MinorTooNew {
1460 recording_minor: StoreFormatMinorVersion::new(3),
1461 supported_minor: StoreFormatMinorVersion::new(2),
1462 },
1463 "newer minor version should be incompatible"
1464 );
1465 insta::assert_snapshot!(error.to_string(), @"minor version 3 is newer than supported version 2");
1466
1467 let error = version(2, 0).check_readable_by(version(1, 5)).unwrap_err();
1469 assert_eq!(
1470 error,
1471 StoreVersionIncompatibility::RecordingTooNew {
1472 recording_major: StoreFormatMajorVersion::new(2),
1473 supported_major: StoreFormatMajorVersion::new(1),
1474 },
1475 );
1476 insta::assert_snapshot!(
1477 error.to_string(),
1478 @"recording has major version 2, but this nextest only supports version 1 (upgrade nextest to replay this recording)"
1479 );
1480
1481 let error = version(1, 0).check_readable_by(version(2, 0)).unwrap_err();
1483 assert_eq!(
1484 error,
1485 StoreVersionIncompatibility::RecordingTooOld {
1486 recording_major: StoreFormatMajorVersion::new(1),
1487 supported_major: StoreFormatMajorVersion::new(2),
1488 guidance: TooOldRecordingGuidance::Discard {
1489 last_nextest_version: "0.9.130",
1490 },
1491 },
1492 );
1493 insta::assert_snapshot!(
1494 error.to_string(),
1495 @"
1496 recording has major version 1, but this nextest requires version 2
1497 (recordings from nextest 0.9.130 and earlier are no longer supported; discard this recording and record a new run)
1498 "
1499 );
1500
1501 let error = version(3, 0).check_readable_by(version(5, 0)).unwrap_err();
1503 assert_eq!(
1504 error,
1505 StoreVersionIncompatibility::RecordingTooOld {
1506 recording_major: StoreFormatMajorVersion::new(3),
1507 supported_major: StoreFormatMajorVersion::new(5),
1508 guidance: TooOldRecordingGuidance::Unknown,
1509 },
1510 );
1511 insta::assert_snapshot!(
1512 error.to_string(),
1513 @"recording has major version 3, but this nextest requires version 5"
1514 );
1515
1516 let error = StoreVersionIncompatibility::RecordingTooOld {
1519 recording_major: StoreFormatMajorVersion::new(3),
1520 supported_major: StoreFormatMajorVersion::new(4),
1521 guidance: TooOldRecordingGuidance::UseOlderNextest {
1522 last_nextest_version: "0.9.200",
1523 },
1524 };
1525 insta::assert_snapshot!(error.to_string(), @"
1526 recording has major version 3, but this nextest requires version 4
1527 (use nextest <= 0.9.200 to replay this recording)
1528 ");
1529
1530 insta::assert_snapshot!(version(1, 2).to_string(), @"1.2");
1531 }
1532
1533 #[test]
1535 fn test_too_old_guidance_known_for_all_older_majors() {
1536 for major in 1..STORE_FORMAT_VERSION.major.0 {
1537 let major = StoreFormatMajorVersion::new(major);
1538 match major.too_old_guidance() {
1539 TooOldRecordingGuidance::UseOlderNextest { .. }
1540 | TooOldRecordingGuidance::Discard { .. } => {}
1541 TooOldRecordingGuidance::Unknown => {
1542 panic!(
1543 "store format major version {major} has no too_old_guidance entry \
1544 (add one when bumping STORE_FORMAT_VERSION's major version)"
1545 )
1546 }
1547 }
1548 }
1549 }
1550
1551 #[test]
1552 fn test_runs_json_with_store_format_2_entry() {
1553 let json = r#"{
1556 "format-version": 2,
1557 "runs": [{
1558 "run-id": "550e8400-e29b-41d4-a716-446655440000",
1559 "store-format-version": 2,
1560 "store-format-minor-version": 1,
1561 "nextest-version": "0.9.149",
1562 "started-at": "2024-12-19T14:22:33-08:00",
1563 "last-written-at": "2024-12-19T22:22:33Z",
1564 "cli-args": ["cargo", "nextest", "run", "-p", "foo", "--features", "a b"],
1565 "build-scope-args": ["--package=foo", "--features=a b"],
1566 "env-vars": {},
1567 "sizes": {
1568 "log": { "compressed": 0, "uncompressed": 0 },
1569 "store": { "compressed": 0, "uncompressed": 0 }
1570 },
1571 "status": { "status": "incomplete" }
1572 }]
1573 }"#;
1574 let list: RecordedRunList = serde_json::from_str(json).expect("2.x entry deserializes");
1575 let data = list.into_data();
1576 assert_eq!(data.runs.len(), 1);
1577 assert_eq!(data.runs[0].store_format_version, version(2, 1));
1578
1579 let rewritten = RecordedRunList::from_data(&data.runs, data.last_pruned_at);
1582 let rewritten = serde_json::to_value(&rewritten).expect("rewritten run list serializes");
1583 let rewritten_run = &rewritten["runs"][0];
1584 assert_eq!(rewritten_run.get("build-scope-args"), None);
1585 }
1586
1587 #[test]
1588 fn test_rerun_root_info_serialize() {
1589 let root_info = RerunRootInfo::new(
1590 ReportUuid::from_u128(0x550e8400_e29b_41d4_a716_446655440000),
1591 RecordedBuildScope::new(BuildKind::Cargo, make_test_build_scope()),
1592 );
1593 let json = serde_json::to_string_pretty(&root_info).expect("serialization should succeed");
1594 insta::assert_snapshot!(json);
1595
1596 let roundtripped: RerunRootInfo =
1597 serde_json::from_str(&json).expect("deserialization should succeed");
1598 assert_eq!(roundtripped.run_id, root_info.run_id);
1599 assert_eq!(roundtripped.build_scope, root_info.build_scope);
1600 }
1601
1602 #[test]
1603 fn test_rerun_root_info_deserialize_errors() {
1604 let cases = [
1605 (
1607 "store_format_2",
1608 r#"{"run-id": "550e8400-e29b-41d4-a716-446655440000", "build-scope-args": ["--workspace"]}"#,
1609 ),
1610 (
1612 "missing_build_kind",
1613 r#"{"run-id": "550e8400-e29b-41d4-a716-446655440000", "build-scope": {"scope": {}}}"#,
1614 ),
1615 (
1617 "missing_scope",
1618 r#"{"run-id": "550e8400-e29b-41d4-a716-446655440000", "build-scope": {"build-kind": "cargo"}}"#,
1619 ),
1620 (
1622 "unknown_build_kind",
1623 r#"{"run-id": "550e8400-e29b-41d4-a716-446655440000", "build-scope": {"build-kind": "bazel", "scope": {}}}"#,
1624 ),
1625 ];
1626 let errors: Vec<_> = cases
1627 .iter()
1628 .map(|(label, json)| {
1629 let error = serde_json::from_str::<RerunRootInfo>(json)
1630 .expect_err("rerun root info should fail to parse");
1631 format!("{label}: {error}")
1632 })
1633 .collect();
1634 insta::assert_snapshot!(errors.join("\n"), @"
1635 store_format_2: missing field `build-scope` at line 1 column 87
1636 missing_build_kind: missing field `build-kind` at line 1 column 79
1637 missing_scope: missing field `scope` at line 1 column 89
1638 unknown_build_kind: unknown variant `bazel`, expected `cargo` or `reused` at line 1 column 88
1639 ");
1640 }
1641
1642 #[test]
1643 fn test_recorded_run_deserialize_without_minor_version() {
1644 let json = r#"{
1646 "run-id": "550e8400-e29b-41d4-a716-446655440000",
1647 "store-format-version": 1,
1648 "nextest-version": "0.9.111",
1649 "started-at": "2024-12-19T14:22:33-08:00",
1650 "last-written-at": "2024-12-19T22:22:33Z",
1651 "cli-args": [],
1652 "env-vars": {},
1653 "sizes": {
1654 "log": { "compressed": 0, "uncompressed": 0 },
1655 "store": { "compressed": 0, "uncompressed": 0 }
1656 },
1657 "status": { "status": "incomplete" }
1658 }"#;
1659 let run: RecordedRun = serde_json::from_str(json).expect("should deserialize");
1660 assert_eq!(run.store_format_version, StoreFormatMajorVersion::new(1));
1661 assert_eq!(
1662 run.store_format_minor_version,
1663 StoreFormatMinorVersion::new(0)
1664 );
1665
1666 let info: RecordedRunInfo = run.into();
1668 assert_eq!(info.store_format_version, version(1, 0));
1669 }
1670
1671 #[test]
1672 fn test_recorded_run_serialize_includes_minor_version() {
1673 let run = make_test_run(RecordedRunStatusFormat::Incomplete);
1675 let json = serde_json::to_string_pretty(&run).expect("serialization should succeed");
1676 assert!(
1677 json.contains("store-format-minor-version"),
1678 "serialized run should include store-format-minor-version"
1679 );
1680 }
1681
1682 fn portable_version(major: u32, minor: u32) -> PortableRecordingFormatVersion {
1686 PortableRecordingFormatVersion::new(
1687 PortableRecordingFormatMajorVersion::new(major),
1688 PortableRecordingFormatMinorVersion::new(minor),
1689 )
1690 }
1691
1692 #[test]
1693 fn test_portable_version_compatibility() {
1694 assert!(
1695 portable_version(1, 0)
1696 .check_readable_by(portable_version(1, 0))
1697 .is_ok(),
1698 "same version should be compatible"
1699 );
1700
1701 assert!(
1702 portable_version(1, 0)
1703 .check_readable_by(portable_version(1, 2))
1704 .is_ok(),
1705 "older minor version should be compatible"
1706 );
1707
1708 let error = portable_version(1, 3)
1709 .check_readable_by(portable_version(1, 2))
1710 .unwrap_err();
1711 assert_eq!(
1712 error,
1713 PortableRecordingVersionIncompatibility::MinorTooNew {
1714 recording_minor: PortableRecordingFormatMinorVersion::new(3),
1715 supported_minor: PortableRecordingFormatMinorVersion::new(2),
1716 },
1717 "newer minor version should be incompatible"
1718 );
1719 insta::assert_snapshot!(error.to_string(), @"minor version 3 is newer than supported version 2");
1720
1721 let error = portable_version(2, 0)
1722 .check_readable_by(portable_version(1, 5))
1723 .unwrap_err();
1724 assert_eq!(
1725 error,
1726 PortableRecordingVersionIncompatibility::MajorMismatch {
1727 recording_major: PortableRecordingFormatMajorVersion::new(2),
1728 supported_major: PortableRecordingFormatMajorVersion::new(1),
1729 },
1730 "different major version should be incompatible"
1731 );
1732 insta::assert_snapshot!(error.to_string(), @"major version 2 differs from supported version 1");
1733
1734 insta::assert_snapshot!(portable_version(1, 2).to_string(), @"1.2");
1735 }
1736
1737 #[test]
1738 fn test_portable_version_serialization() {
1739 let version = portable_version(1, 0);
1741 let json = serde_json::to_string(&version).expect("serialization should succeed");
1742 insta::assert_snapshot!(json, @r#"{"major":1,"minor":0}"#);
1743
1744 let roundtripped: PortableRecordingFormatVersion =
1746 serde_json::from_str(&json).expect("deserialization should succeed");
1747 assert_eq!(roundtripped, version);
1748 }
1749
1750 #[test]
1751 fn test_portable_manifest_format_version() {
1752 assert_eq!(
1754 PORTABLE_RECORDING_FORMAT_VERSION,
1755 portable_version(1, 1),
1756 "current portable recording format version should be 1.1"
1757 );
1758 }
1759}