Skip to main content

nextest_runner/record/
format.rs

1// Copyright (c) The nextest Contributors
2// SPDX-License-Identifier: MIT OR Apache-2.0
3
4//! Recording format metadata shared between recorder and reader.
5
6use 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
24// ---
25// Format version newtypes
26// ---
27
28/// Defines a newtype wrapper around `u32` for format versions.
29///
30/// Use `@default` variant to also derive `Default` (defaults to 0).
31macro_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    /// Version of the `runs.json.zst` outer format.
82    ///
83    /// Increment this when adding new semantically important fields to `runs.json.zst`.
84    /// Readers can read newer versions (assuming append-only evolution with serde
85    /// defaults), but writers must refuse to write if the file version is higher
86    /// than this.
87    pub struct RunsJsonFormatVersion;
88}
89
90define_format_version! {
91    /// Major version of the `store.zip` archive format for breaking changes to the
92    /// archive structure.
93    pub struct StoreFormatMajorVersion;
94}
95
96define_format_version! {
97    @default
98    /// Minor version of the `store.zip` archive format for additive changes.
99    pub struct StoreFormatMinorVersion;
100}
101
102/// Combined major and minor version of the `store.zip` archive format.
103#[derive(Clone, Copy, Debug, PartialEq, Eq)]
104pub struct StoreFormatVersion {
105    /// The major version (breaking changes).
106    pub major: StoreFormatMajorVersion,
107    /// The minor version (additive changes).
108    pub minor: StoreFormatMinorVersion,
109}
110
111impl StoreFormatVersion {
112    /// Creates a new `StoreFormatVersion`.
113    pub const fn new(major: StoreFormatMajorVersion, minor: StoreFormatMinorVersion) -> Self {
114        Self { major, minor }
115    }
116
117    /// Checks if an archive with version `self` can be read by a reader that
118    /// supports `supported`.
119    pub fn check_readable_by(self, supported: Self) -> Result<(), StoreVersionIncompatibility> {
120        if self.major != supported.major {
121            return Err(StoreVersionIncompatibility::MajorMismatch {
122                archive_major: self.major,
123                supported_major: supported.major,
124            });
125        }
126        if self.minor > supported.minor {
127            return Err(StoreVersionIncompatibility::MinorTooNew {
128                archive_minor: self.minor,
129                supported_minor: supported.minor,
130            });
131        }
132        Ok(())
133    }
134}
135
136impl fmt::Display for StoreFormatVersion {
137    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
138        write!(f, "{}.{}", self.major, self.minor)
139    }
140}
141
142/// An incompatibility between an archive's store format version and what the
143/// reader supports.
144#[derive(Clone, Copy, Debug, PartialEq, Eq)]
145pub enum StoreVersionIncompatibility {
146    /// The archive's major version differs from the supported major version.
147    MajorMismatch {
148        /// The major version in the archive.
149        archive_major: StoreFormatMajorVersion,
150        /// The major version this nextest supports.
151        supported_major: StoreFormatMajorVersion,
152    },
153    /// The archive's minor version is newer than the supported minor version.
154    MinorTooNew {
155        /// The minor version in the archive.
156        archive_minor: StoreFormatMinorVersion,
157        /// The maximum minor version this nextest supports.
158        supported_minor: StoreFormatMinorVersion,
159    },
160}
161
162impl fmt::Display for StoreVersionIncompatibility {
163    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
164        match self {
165            Self::MajorMismatch {
166                archive_major,
167                supported_major,
168            } => {
169                write!(
170                    f,
171                    "major version {} differs from supported version {}",
172                    archive_major, supported_major
173                )
174            }
175            Self::MinorTooNew {
176                archive_minor,
177                supported_minor,
178            } => {
179                write!(
180                    f,
181                    "minor version {} is newer than supported version {}",
182                    archive_minor, supported_minor
183                )
184            }
185        }
186    }
187}
188
189// ---
190// runs.json.zst format types
191// ---
192
193/// The current format version for runs.json.zst.
194pub(super) const RUNS_JSON_FORMAT_VERSION: RunsJsonFormatVersion = RunsJsonFormatVersion::new(2);
195
196/// The current format version for recorded test runs (store.zip and run.log).
197///
198/// This combines a major version (for breaking changes) and a minor version
199/// (for additive changes). Readers check compatibility via
200/// [`StoreFormatVersion::check_readable_by`].
201pub const STORE_FORMAT_VERSION: StoreFormatVersion = StoreFormatVersion::new(
202    StoreFormatMajorVersion::new(1),
203    StoreFormatMinorVersion::new(0),
204);
205
206/// Whether a runs.json.zst file can be written to.
207#[derive(Debug, Clone, Copy, PartialEq, Eq)]
208pub enum RunsJsonWritePermission {
209    /// Writing is allowed.
210    Allowed,
211    /// Writing is not allowed because the file has a newer format version.
212    Denied {
213        /// The format version in the file.
214        file_version: RunsJsonFormatVersion,
215        /// The maximum version this nextest can write.
216        max_supported_version: RunsJsonFormatVersion,
217    },
218}
219
220/// The list of recorded runs (serialization format for runs.json.zst).
221#[derive(Debug, Deserialize, Serialize)]
222#[serde(rename_all = "kebab-case")]
223pub(super) struct RecordedRunList {
224    /// The format version of this file.
225    pub(super) format_version: RunsJsonFormatVersion,
226
227    /// When the store was last pruned.
228    ///
229    /// Used to implement once-daily implicit pruning. Explicit pruning via CLI
230    /// always runs regardless of this value.
231    #[serde(default, skip_serializing_if = "Option::is_none")]
232    pub(super) last_pruned_at: Option<DateTime<Utc>>,
233
234    /// The list of runs.
235    #[serde(default)]
236    pub(super) runs: Vec<RecordedRun>,
237}
238
239/// Data extracted from a `RecordedRunList`.
240pub(super) struct RunListData {
241    pub(super) runs: Vec<RecordedRunInfo>,
242    pub(super) last_pruned_at: Option<DateTime<Utc>>,
243}
244
245impl RecordedRunList {
246    /// Creates a new, empty run list with the current format version.
247    #[cfg(test)]
248    fn new() -> Self {
249        Self {
250            format_version: RUNS_JSON_FORMAT_VERSION,
251            last_pruned_at: None,
252            runs: Vec::new(),
253        }
254    }
255
256    /// Converts the serialization format to internal representation.
257    pub(super) fn into_data(self) -> RunListData {
258        RunListData {
259            runs: self.runs.into_iter().map(RecordedRunInfo::from).collect(),
260            last_pruned_at: self.last_pruned_at,
261        }
262    }
263
264    /// Creates a serialization format from internal representation.
265    ///
266    /// Always uses the current format version. If the file had an older version,
267    /// this effectively upgrades it when written back.
268    pub(super) fn from_data(
269        runs: &[RecordedRunInfo],
270        last_pruned_at: Option<DateTime<Utc>>,
271    ) -> Self {
272        Self {
273            format_version: RUNS_JSON_FORMAT_VERSION,
274            last_pruned_at,
275            runs: runs.iter().map(RecordedRun::from).collect(),
276        }
277    }
278
279    /// Returns whether this runs.json.zst can be written to by this nextest version.
280    ///
281    /// If the file has a newer format version than we support, writing is denied
282    /// to avoid data loss.
283    pub(super) fn write_permission(&self) -> RunsJsonWritePermission {
284        if self.format_version > RUNS_JSON_FORMAT_VERSION {
285            RunsJsonWritePermission::Denied {
286                file_version: self.format_version,
287                max_supported_version: RUNS_JSON_FORMAT_VERSION,
288            }
289        } else {
290            RunsJsonWritePermission::Allowed
291        }
292    }
293}
294
295/// Metadata about a recorded run (serialization format for runs.json.zst and portable recordings).
296#[derive(Clone, Debug, Deserialize, Serialize)]
297#[serde(rename_all = "kebab-case")]
298pub(super) struct RecordedRun {
299    /// The unique identifier for this run.
300    pub(super) run_id: ReportUuid,
301    /// The major format version of this run's store.zip and run.log.
302    ///
303    /// Runs with a different major version cannot be replayed by this nextest
304    /// version.
305    pub(super) store_format_version: StoreFormatMajorVersion,
306    /// The minor format version of this run's store.zip and run.log.
307    ///
308    /// Runs with a newer minor version (same major) cannot be replayed by this
309    /// nextest version. Older minor versions are compatible.
310    #[serde(default)]
311    pub(super) store_format_minor_version: StoreFormatMinorVersion,
312    /// The version of nextest that created this run.
313    pub(super) nextest_version: Version,
314    /// When the run started.
315    pub(super) started_at: DateTime<FixedOffset>,
316    /// When this run was last written to.
317    ///
318    /// Used for LRU eviction. Updated when the run is created, when the run
319    /// completes, and in the future when operations like `rerun` reference
320    /// this run.
321    pub(super) last_written_at: DateTime<FixedOffset>,
322    /// Duration of the run in seconds.
323    #[serde(default, skip_serializing_if = "Option::is_none")]
324    pub(super) duration_secs: Option<f64>,
325    /// The command-line arguments used to invoke nextest.
326    #[serde(default)]
327    pub(super) cli_args: Vec<String>,
328    /// Build scope arguments (package and target selection).
329    ///
330    /// These determine which packages and targets are built. In a rerun chain,
331    /// these are inherited from the original run unless explicitly overridden.
332    #[serde(default)]
333    pub(super) build_scope_args: Vec<String>,
334    /// Environment variables that affect nextest behavior (NEXTEST_* and CARGO_*).
335    ///
336    /// This has a default for deserializing old runs.json.zst files that don't have this field.
337    #[serde(default)]
338    pub(super) env_vars: BTreeMap<String, String>,
339    /// The parent run ID.
340    #[serde(default)]
341    pub(super) parent_run_id: Option<ReportUuid>,
342    /// Sizes broken down by component (log and store).
343    ///
344    /// This is all zeros until the run completes successfully.
345    pub(super) sizes: RecordedSizesFormat,
346    /// Status and statistics for the run.
347    pub(super) status: RecordedRunStatusFormat,
348}
349
350/// Sizes broken down by component (serialization format for runs.json.zst).
351#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Deserialize, Serialize)]
352#[serde(rename_all = "kebab-case")]
353pub(super) struct RecordedSizesFormat {
354    /// Sizes for the run log (run.log.zst).
355    pub(super) log: ComponentSizesFormat,
356    /// Sizes for the store archive (store.zip).
357    pub(super) store: ComponentSizesFormat,
358}
359
360/// Compressed and uncompressed sizes for a single component (serialization format).
361#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Deserialize, Serialize)]
362#[serde(rename_all = "kebab-case")]
363pub(super) struct ComponentSizesFormat {
364    /// Compressed size in bytes.
365    pub(super) compressed: u64,
366    /// Uncompressed size in bytes.
367    pub(super) uncompressed: u64,
368    /// Number of entries (records for log, files for store).
369    #[serde(default)]
370    pub(super) entries: u64,
371}
372
373impl From<RecordedSizes> for RecordedSizesFormat {
374    fn from(sizes: RecordedSizes) -> Self {
375        Self {
376            log: ComponentSizesFormat {
377                compressed: sizes.log.compressed,
378                uncompressed: sizes.log.uncompressed,
379                entries: sizes.log.entries,
380            },
381            store: ComponentSizesFormat {
382                compressed: sizes.store.compressed,
383                uncompressed: sizes.store.uncompressed,
384                entries: sizes.store.entries,
385            },
386        }
387    }
388}
389
390impl From<RecordedSizesFormat> for RecordedSizes {
391    fn from(sizes: RecordedSizesFormat) -> Self {
392        Self {
393            log: ComponentSizes {
394                compressed: sizes.log.compressed,
395                uncompressed: sizes.log.uncompressed,
396                entries: sizes.log.entries,
397            },
398            store: ComponentSizes {
399                compressed: sizes.store.compressed,
400                uncompressed: sizes.store.uncompressed,
401                entries: sizes.store.entries,
402            },
403        }
404    }
405}
406
407/// Status of a recorded run (serialization format).
408#[derive(Clone, Debug, Deserialize, Serialize)]
409#[serde(tag = "status", rename_all = "kebab-case")]
410pub(super) enum RecordedRunStatusFormat {
411    /// The run was interrupted before completion.
412    Incomplete,
413    /// A normal test run completed.
414    #[serde(rename_all = "kebab-case")]
415    Completed {
416        /// The number of tests that were expected to run.
417        initial_run_count: usize,
418        /// The number of tests that passed.
419        passed: usize,
420        /// The number of tests that failed.
421        failed: usize,
422        /// The exit code from the run.
423        exit_code: i32,
424    },
425    /// A normal test run was cancelled.
426    #[serde(rename_all = "kebab-case")]
427    Cancelled {
428        /// The number of tests that were expected to run.
429        initial_run_count: usize,
430        /// The number of tests that passed.
431        passed: usize,
432        /// The number of tests that failed.
433        failed: usize,
434        /// The exit code from the run.
435        exit_code: i32,
436    },
437    /// A stress test run completed.
438    #[serde(rename_all = "kebab-case")]
439    StressCompleted {
440        /// The number of stress iterations that were expected to run, if known.
441        initial_iteration_count: Option<NonZero<u32>>,
442        /// The number of stress iterations that succeeded.
443        success_count: u32,
444        /// The number of stress iterations that failed.
445        failed_count: u32,
446        /// The exit code from the run.
447        exit_code: i32,
448    },
449    /// A stress test run was cancelled.
450    #[serde(rename_all = "kebab-case")]
451    StressCancelled {
452        /// The number of stress iterations that were expected to run, if known.
453        initial_iteration_count: Option<NonZero<u32>>,
454        /// The number of stress iterations that succeeded.
455        success_count: u32,
456        /// The number of stress iterations that failed.
457        failed_count: u32,
458        /// The exit code from the run.
459        exit_code: i32,
460    },
461    /// An unknown status from a newer version of nextest.
462    ///
463    /// This variant is used for forward compatibility when reading runs.json.zst
464    /// files created by newer nextest versions that may have new status types.
465    #[serde(other)]
466    Unknown,
467}
468
469impl From<RecordedRun> for RecordedRunInfo {
470    fn from(run: RecordedRun) -> Self {
471        Self {
472            run_id: run.run_id,
473            store_format_version: StoreFormatVersion::new(
474                run.store_format_version,
475                run.store_format_minor_version,
476            ),
477            nextest_version: run.nextest_version,
478            started_at: run.started_at,
479            last_written_at: run.last_written_at,
480            duration_secs: run.duration_secs,
481            cli_args: run.cli_args,
482            build_scope_args: run.build_scope_args,
483            env_vars: run.env_vars,
484            parent_run_id: run.parent_run_id,
485            sizes: run.sizes.into(),
486            status: run.status.into(),
487        }
488    }
489}
490
491impl From<&RecordedRunInfo> for RecordedRun {
492    fn from(run: &RecordedRunInfo) -> Self {
493        Self {
494            run_id: run.run_id,
495            store_format_version: run.store_format_version.major,
496            store_format_minor_version: run.store_format_version.minor,
497            nextest_version: run.nextest_version.clone(),
498            started_at: run.started_at,
499            last_written_at: run.last_written_at,
500            duration_secs: run.duration_secs,
501            cli_args: run.cli_args.clone(),
502            build_scope_args: run.build_scope_args.clone(),
503            env_vars: run.env_vars.clone(),
504            parent_run_id: run.parent_run_id,
505            sizes: run.sizes.into(),
506            status: (&run.status).into(),
507        }
508    }
509}
510
511impl From<RecordedRunStatusFormat> for RecordedRunStatus {
512    fn from(status: RecordedRunStatusFormat) -> Self {
513        match status {
514            RecordedRunStatusFormat::Incomplete => Self::Incomplete,
515            RecordedRunStatusFormat::Unknown => Self::Unknown,
516            RecordedRunStatusFormat::Completed {
517                initial_run_count,
518                passed,
519                failed,
520                exit_code,
521            } => Self::Completed(CompletedRunStats {
522                initial_run_count,
523                passed,
524                failed,
525                exit_code,
526            }),
527            RecordedRunStatusFormat::Cancelled {
528                initial_run_count,
529                passed,
530                failed,
531                exit_code,
532            } => Self::Cancelled(CompletedRunStats {
533                initial_run_count,
534                passed,
535                failed,
536                exit_code,
537            }),
538            RecordedRunStatusFormat::StressCompleted {
539                initial_iteration_count,
540                success_count,
541                failed_count,
542                exit_code,
543            } => Self::StressCompleted(StressCompletedRunStats {
544                initial_iteration_count,
545                success_count,
546                failed_count,
547                exit_code,
548            }),
549            RecordedRunStatusFormat::StressCancelled {
550                initial_iteration_count,
551                success_count,
552                failed_count,
553                exit_code,
554            } => Self::StressCancelled(StressCompletedRunStats {
555                initial_iteration_count,
556                success_count,
557                failed_count,
558                exit_code,
559            }),
560        }
561    }
562}
563
564impl From<&RecordedRunStatus> for RecordedRunStatusFormat {
565    fn from(status: &RecordedRunStatus) -> Self {
566        match status {
567            RecordedRunStatus::Incomplete => Self::Incomplete,
568            RecordedRunStatus::Unknown => Self::Unknown,
569            RecordedRunStatus::Completed(stats) => Self::Completed {
570                initial_run_count: stats.initial_run_count,
571                passed: stats.passed,
572                failed: stats.failed,
573                exit_code: stats.exit_code,
574            },
575            RecordedRunStatus::Cancelled(stats) => Self::Cancelled {
576                initial_run_count: stats.initial_run_count,
577                passed: stats.passed,
578                failed: stats.failed,
579                exit_code: stats.exit_code,
580            },
581            RecordedRunStatus::StressCompleted(stats) => Self::StressCompleted {
582                initial_iteration_count: stats.initial_iteration_count,
583                success_count: stats.success_count,
584                failed_count: stats.failed_count,
585                exit_code: stats.exit_code,
586            },
587            RecordedRunStatus::StressCancelled(stats) => Self::StressCancelled {
588                initial_iteration_count: stats.initial_iteration_count,
589                success_count: stats.success_count,
590                failed_count: stats.failed_count,
591                exit_code: stats.exit_code,
592            },
593        }
594    }
595}
596
597// ---
598// Rerun types
599// ---
600
601/// Rerun-specific metadata stored in `meta/rerun-info.json`.
602///
603/// This is only present for reruns (runs with a parent run).
604#[derive(Clone, Debug, Deserialize, Serialize)]
605#[serde(rename_all = "kebab-case")]
606pub struct RerunInfo {
607    /// The immediate parent run ID.
608    pub parent_run_id: ReportUuid,
609
610    /// Root information from the original run.
611    pub root_info: RerunRootInfo,
612
613    /// The set of outstanding and passing test cases.
614    pub test_suites: IdOrdMap<RerunTestSuiteInfo>,
615}
616
617/// For a rerun, information obtained from the root of the rerun chain.
618#[derive(Clone, Debug, Deserialize, Serialize)]
619#[serde(rename_all = "kebab-case")]
620pub struct RerunRootInfo {
621    /// The run ID.
622    pub run_id: ReportUuid,
623
624    /// Build scope args from the original run.
625    pub build_scope_args: Vec<String>,
626}
627
628impl RerunRootInfo {
629    /// Creates a new `RerunRootInfo` for a root of a rerun chain.
630    ///
631    /// `build_scope_args` should be the build scope arguments extracted from
632    /// the original run's CLI args. Use `extract_build_scope_args` from
633    /// `cargo-nextest` to extract these.
634    pub fn new(run_id: ReportUuid, build_scope_args: Vec<String>) -> Self {
635        Self {
636            run_id,
637            build_scope_args,
638        }
639    }
640}
641
642/// A test suite's outstanding and passing test cases.
643#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
644pub struct RerunTestSuiteInfo {
645    /// The binary ID.
646    pub binary_id: RustBinaryId,
647
648    /// The set of passing test cases.
649    pub passing: BTreeSet<TestCaseName>,
650
651    /// The set of outstanding test cases.
652    pub outstanding: BTreeSet<TestCaseName>,
653}
654
655impl RerunTestSuiteInfo {
656    pub(super) fn new(binary_id: RustBinaryId) -> Self {
657        Self {
658            binary_id,
659            passing: BTreeSet::new(),
660            outstanding: BTreeSet::new(),
661        }
662    }
663}
664
665impl IdOrdItem for RerunTestSuiteInfo {
666    type Key<'a> = &'a RustBinaryId;
667    fn key(&self) -> Self::Key<'_> {
668        &self.binary_id
669    }
670    id_upcast!();
671}
672
673// ---
674// Recording format types
675// ---
676
677/// File name for the store archive.
678pub static STORE_ZIP_FILE_NAME: &str = "store.zip";
679
680/// File name for the run log.
681pub static RUN_LOG_FILE_NAME: &str = "run.log.zst";
682
683/// Returns true if the path has a `.zip` extension (case-insensitive).
684pub fn has_zip_extension(path: &Utf8Path) -> bool {
685    path.extension()
686        .is_some_and(|ext| ext.eq_ignore_ascii_case("zip"))
687}
688
689// Paths within the zip archive.
690/// Path to cargo metadata within the store archive.
691pub static CARGO_METADATA_JSON_PATH: &str = "meta/cargo-metadata.json";
692/// Path to the test list within the store archive.
693pub static TEST_LIST_JSON_PATH: &str = "meta/test-list.json";
694/// Path to record options within the store archive.
695pub static RECORD_OPTS_JSON_PATH: &str = "meta/record-opts.json";
696/// Path to rerun info within the store archive (only present for reruns).
697pub static RERUN_INFO_JSON_PATH: &str = "meta/rerun-info.json";
698/// Path to the stdout dictionary within the store archive.
699pub static STDOUT_DICT_PATH: &str = "meta/stdout.dict";
700/// Path to the stderr dictionary within the store archive.
701pub static STDERR_DICT_PATH: &str = "meta/stderr.dict";
702
703// ---
704// Portable recording format types
705// ---
706
707define_format_version! {
708    /// Major version of the portable recording format for breaking changes.
709    pub struct PortableRecordingFormatMajorVersion;
710}
711
712define_format_version! {
713    @default
714    /// Minor version of the portable recording format for additive changes.
715    pub struct PortableRecordingFormatMinorVersion;
716}
717
718/// Combined major and minor version of the portable recording format.
719#[derive(Clone, Copy, Debug, PartialEq, Eq, Deserialize, Serialize)]
720pub struct PortableRecordingFormatVersion {
721    /// The major version (breaking changes).
722    pub major: PortableRecordingFormatMajorVersion,
723    /// The minor version (additive changes).
724    pub minor: PortableRecordingFormatMinorVersion,
725}
726
727impl PortableRecordingFormatVersion {
728    /// Creates a new `PortableRecordingFormatVersion`.
729    pub const fn new(
730        major: PortableRecordingFormatMajorVersion,
731        minor: PortableRecordingFormatMinorVersion,
732    ) -> Self {
733        Self { major, minor }
734    }
735
736    /// Checks if an archive with version `self` can be read by a reader that
737    /// supports `supported`.
738    pub fn check_readable_by(
739        self,
740        supported: Self,
741    ) -> Result<(), PortableRecordingVersionIncompatibility> {
742        if self.major != supported.major {
743            return Err(PortableRecordingVersionIncompatibility::MajorMismatch {
744                archive_major: self.major,
745                supported_major: supported.major,
746            });
747        }
748        if self.minor > supported.minor {
749            return Err(PortableRecordingVersionIncompatibility::MinorTooNew {
750                archive_minor: self.minor,
751                supported_minor: supported.minor,
752            });
753        }
754        Ok(())
755    }
756}
757
758impl fmt::Display for PortableRecordingFormatVersion {
759    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
760        write!(f, "{}.{}", self.major, self.minor)
761    }
762}
763
764/// An incompatibility between an archive's portable format version and what the
765/// reader supports.
766#[derive(Clone, Copy, Debug, PartialEq, Eq)]
767pub enum PortableRecordingVersionIncompatibility {
768    /// The archive's major version differs from the supported major version.
769    MajorMismatch {
770        /// The major version in the archive.
771        archive_major: PortableRecordingFormatMajorVersion,
772        /// The major version this nextest supports.
773        supported_major: PortableRecordingFormatMajorVersion,
774    },
775    /// The archive's minor version is newer than the supported minor version.
776    MinorTooNew {
777        /// The minor version in the archive.
778        archive_minor: PortableRecordingFormatMinorVersion,
779        /// The maximum minor version this nextest supports.
780        supported_minor: PortableRecordingFormatMinorVersion,
781    },
782}
783
784impl fmt::Display for PortableRecordingVersionIncompatibility {
785    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
786        match self {
787            Self::MajorMismatch {
788                archive_major,
789                supported_major,
790            } => {
791                write!(
792                    f,
793                    "major version {} differs from supported version {}",
794                    archive_major, supported_major
795                )
796            }
797            Self::MinorTooNew {
798                archive_minor,
799                supported_minor,
800            } => {
801                write!(
802                    f,
803                    "minor version {} is newer than supported version {}",
804                    archive_minor, supported_minor
805                )
806            }
807        }
808    }
809}
810
811/// The current format version for portable recordings.
812pub const PORTABLE_RECORDING_FORMAT_VERSION: PortableRecordingFormatVersion =
813    PortableRecordingFormatVersion::new(
814        PortableRecordingFormatMajorVersion::new(1),
815        PortableRecordingFormatMinorVersion::new(0),
816    );
817
818/// File name for the manifest within a portable recording.
819pub static PORTABLE_MANIFEST_FILE_NAME: &str = "manifest.json";
820
821/// The manifest for a portable recording.
822///
823/// A portable recording packages a single recorded run into a self-contained
824/// zip file for sharing and import.
825#[derive(Debug, Deserialize, Serialize)]
826#[serde(rename_all = "kebab-case")]
827pub(crate) struct PortableManifest {
828    /// The format version of this portable recording.
829    pub(crate) format_version: PortableRecordingFormatVersion,
830    /// The run metadata.
831    pub(super) run: RecordedRun,
832}
833
834impl PortableManifest {
835    /// Creates a new manifest for the given run.
836    pub(crate) fn new(run: &RecordedRunInfo) -> Self {
837        Self {
838            format_version: PORTABLE_RECORDING_FORMAT_VERSION,
839            run: RecordedRun::from(run),
840        }
841    }
842
843    /// Returns the run info extracted from this manifest.
844    pub(crate) fn run_info(&self) -> RecordedRunInfo {
845        RecordedRunInfo::from(self.run.clone())
846    }
847
848    /// Returns the store format version from the run metadata.
849    pub(crate) fn store_format_version(&self) -> StoreFormatVersion {
850        StoreFormatVersion::new(
851            self.run.store_format_version,
852            self.run.store_format_minor_version,
853        )
854    }
855}
856
857/// Which dictionary to use for compressing/decompressing a file.
858#[derive(Clone, Copy, Debug, PartialEq, Eq)]
859pub enum OutputDict {
860    /// Use the stdout dictionary (for stdout and combined output).
861    Stdout,
862    /// Use the stderr dictionary.
863    Stderr,
864    /// Use standard zstd compression (for metadata files).
865    None,
866}
867
868impl OutputDict {
869    /// Determines which dictionary to use based on the file path.
870    ///
871    /// Output files in `out/` use dictionaries based on their suffix:
872    /// - `-stdout` and `-combined` use the stdout dictionary.
873    /// - `-stderr` uses the stderr dictionary.
874    ///
875    /// All other files (metadata in `meta/`) use standard zstd.
876    pub fn for_path(path: &Utf8Path) -> Self {
877        let mut iter = path.iter();
878        let Some(first_component) = iter.next() else {
879            return Self::None;
880        };
881        // Output files are always in the out/ directory.
882        if first_component != "out" {
883            return Self::None;
884        }
885
886        Self::for_output_file_name(iter.as_path().as_str())
887    }
888
889    /// Determines which dictionary to use based on the output file name.
890    ///
891    /// The file name should be the basename without the `out/` prefix,
892    /// e.g., `test-abc123-1-stdout`.
893    pub fn for_output_file_name(file_name: &str) -> Self {
894        if file_name.ends_with("-stdout") || file_name.ends_with("-combined") {
895            Self::Stdout
896        } else if file_name.ends_with("-stderr") {
897            Self::Stderr
898        } else {
899            // Unknown output type, use standard compression.
900            Self::None
901        }
902    }
903
904    /// Returns the dictionary bytes for this output type (for writing new archives).
905    ///
906    /// Returns `None` for `OutputDict::None`.
907    pub fn dict_bytes(self) -> Option<&'static [u8]> {
908        match self {
909            Self::Stdout => Some(super::dicts::STDOUT),
910            Self::Stderr => Some(super::dicts::STDERR),
911            Self::None => None,
912        }
913    }
914}
915
916// ---
917// Zip file options helpers
918// ---
919
920/// Returns file options for storing pre-compressed data (no additional
921/// compression).
922pub(super) fn stored_file_options() -> FileOptions {
923    let mut options = FileOptions::default();
924    options.compression_method = CompressionMethod::STORE;
925    options
926}
927
928/// Returns file options for zstd-compressed data.
929pub(super) fn zstd_file_options() -> FileOptions {
930    let mut options = FileOptions::default();
931    options.compression_method = CompressionMethod::ZSTD;
932    options.level = Some(3);
933    options
934}
935
936#[cfg(test)]
937mod tests {
938    use super::*;
939
940    #[test]
941    fn test_output_dict_for_path() {
942        // Metadata files should not use dictionaries.
943        assert_eq!(
944            OutputDict::for_path("meta/cargo-metadata.json".as_ref()),
945            OutputDict::None
946        );
947        assert_eq!(
948            OutputDict::for_path("meta/test-list.json".as_ref()),
949            OutputDict::None
950        );
951
952        // Content-addressed output files should use appropriate dictionaries.
953        assert_eq!(
954            OutputDict::for_path("out/0123456789abcdef-stdout".as_ref()),
955            OutputDict::Stdout
956        );
957        assert_eq!(
958            OutputDict::for_path("out/0123456789abcdef-stderr".as_ref()),
959            OutputDict::Stderr
960        );
961        assert_eq!(
962            OutputDict::for_path("out/0123456789abcdef-combined".as_ref()),
963            OutputDict::Stdout
964        );
965    }
966
967    #[test]
968    fn test_output_dict_for_output_file_name() {
969        // Content-addressed file names.
970        assert_eq!(
971            OutputDict::for_output_file_name("0123456789abcdef-stdout"),
972            OutputDict::Stdout
973        );
974        assert_eq!(
975            OutputDict::for_output_file_name("0123456789abcdef-stderr"),
976            OutputDict::Stderr
977        );
978        assert_eq!(
979            OutputDict::for_output_file_name("0123456789abcdef-combined"),
980            OutputDict::Stdout
981        );
982        assert_eq!(
983            OutputDict::for_output_file_name("0123456789abcdef-unknown"),
984            OutputDict::None
985        );
986    }
987
988    #[test]
989    fn test_dict_bytes() {
990        assert!(OutputDict::Stdout.dict_bytes().is_some());
991        assert!(OutputDict::Stderr.dict_bytes().is_some());
992        assert!(OutputDict::None.dict_bytes().is_none());
993    }
994
995    #[test]
996    fn test_runs_json_missing_version() {
997        // runs.json.zst without format-version should fail to deserialize.
998        let json = r#"{"runs": []}"#;
999        let result: Result<RecordedRunList, _> = serde_json::from_str(json);
1000        assert!(result.is_err(), "expected error for missing format-version");
1001    }
1002
1003    #[test]
1004    fn test_runs_json_current_version() {
1005        // runs.json.zst with current version should deserialize and allow writes.
1006        let json = format!(
1007            r#"{{"format-version": {}, "runs": []}}"#,
1008            RUNS_JSON_FORMAT_VERSION
1009        );
1010        let list: RecordedRunList = serde_json::from_str(&json).expect("should deserialize");
1011        assert_eq!(list.write_permission(), RunsJsonWritePermission::Allowed);
1012    }
1013
1014    #[test]
1015    fn test_runs_json_older_version() {
1016        // runs.json.zst with older version (if any existed) should allow writes.
1017        // Since we only have version 1, test version 0 if we supported it.
1018        // For now, this test just ensures version 1 allows writes.
1019        let json = r#"{"format-version": 1, "runs": []}"#;
1020        let list: RecordedRunList = serde_json::from_str(json).expect("should deserialize");
1021        assert_eq!(list.write_permission(), RunsJsonWritePermission::Allowed);
1022    }
1023
1024    #[test]
1025    fn test_runs_json_newer_version() {
1026        // runs.json.zst with newer version should deserialize but deny writes.
1027        let json = r#"{"format-version": 99, "runs": []}"#;
1028        let list: RecordedRunList = serde_json::from_str(json).expect("should deserialize");
1029        assert_eq!(
1030            list.write_permission(),
1031            RunsJsonWritePermission::Denied {
1032                file_version: RunsJsonFormatVersion::new(99),
1033                max_supported_version: RUNS_JSON_FORMAT_VERSION,
1034            }
1035        );
1036    }
1037
1038    #[test]
1039    fn test_runs_json_serialization_includes_version() {
1040        // Serialized runs.json.zst should always include format-version.
1041        let list = RecordedRunList::from_data(&[], None);
1042        let json = serde_json::to_string(&list).expect("should serialize");
1043        assert!(
1044            json.contains("format-version"),
1045            "serialized runs.json.zst should include format-version"
1046        );
1047
1048        // Verify it's the current version.
1049        let parsed: serde_json::Value = serde_json::from_str(&json).expect("should parse");
1050        let version: RunsJsonFormatVersion =
1051            serde_json::from_value(parsed["format-version"].clone()).expect("valid version");
1052        assert_eq!(
1053            version, RUNS_JSON_FORMAT_VERSION,
1054            "format-version should be current version"
1055        );
1056    }
1057
1058    #[test]
1059    fn test_runs_json_new() {
1060        // RecordedRunList::new() should create with current version.
1061        let list = RecordedRunList::new();
1062        assert_eq!(list.format_version, RUNS_JSON_FORMAT_VERSION);
1063        assert!(list.runs.is_empty());
1064        assert_eq!(list.write_permission(), RunsJsonWritePermission::Allowed);
1065    }
1066
1067    // --- RecordedRun serialization snapshot tests ---
1068
1069    fn make_test_run(status: RecordedRunStatusFormat) -> RecordedRun {
1070        RecordedRun {
1071            run_id: ReportUuid::from_u128(0x550e8400_e29b_41d4_a716_446655440000),
1072            store_format_version: STORE_FORMAT_VERSION.major,
1073            store_format_minor_version: STORE_FORMAT_VERSION.minor,
1074            nextest_version: Version::new(0, 9, 111),
1075            started_at: DateTime::parse_from_rfc3339("2024-12-19T14:22:33-08:00")
1076                .expect("valid timestamp"),
1077            last_written_at: DateTime::parse_from_rfc3339("2024-12-19T22:22:33Z")
1078                .expect("valid timestamp"),
1079            duration_secs: Some(12.345),
1080            cli_args: vec![
1081                "cargo".to_owned(),
1082                "nextest".to_owned(),
1083                "run".to_owned(),
1084                "--workspace".to_owned(),
1085            ],
1086            build_scope_args: vec!["--workspace".to_owned()],
1087            env_vars: BTreeMap::from([
1088                ("CARGO_TERM_COLOR".to_owned(), "always".to_owned()),
1089                ("NEXTEST_PROFILE".to_owned(), "ci".to_owned()),
1090            ]),
1091            parent_run_id: Some(ReportUuid::from_u128(
1092                0x550e7400_e29b_41d4_a716_446655440000,
1093            )),
1094            sizes: RecordedSizesFormat {
1095                log: ComponentSizesFormat {
1096                    compressed: 2345,
1097                    uncompressed: 5678,
1098                    entries: 42,
1099                },
1100                store: ComponentSizesFormat {
1101                    compressed: 10000,
1102                    uncompressed: 40000,
1103                    entries: 15,
1104                },
1105            },
1106            status,
1107        }
1108    }
1109
1110    #[test]
1111    fn test_recorded_run_serialize_incomplete() {
1112        let run = make_test_run(RecordedRunStatusFormat::Incomplete);
1113        let json = serde_json::to_string_pretty(&run).expect("serialization should succeed");
1114        insta::assert_snapshot!(json);
1115    }
1116
1117    #[test]
1118    fn test_recorded_run_serialize_completed() {
1119        let run = make_test_run(RecordedRunStatusFormat::Completed {
1120            initial_run_count: 100,
1121            passed: 95,
1122            failed: 5,
1123            exit_code: 0,
1124        });
1125        let json = serde_json::to_string_pretty(&run).expect("serialization should succeed");
1126        insta::assert_snapshot!(json);
1127    }
1128
1129    #[test]
1130    fn test_recorded_run_serialize_cancelled() {
1131        let run = make_test_run(RecordedRunStatusFormat::Cancelled {
1132            initial_run_count: 100,
1133            passed: 45,
1134            failed: 5,
1135            exit_code: 100,
1136        });
1137        let json = serde_json::to_string_pretty(&run).expect("serialization should succeed");
1138        insta::assert_snapshot!(json);
1139    }
1140
1141    #[test]
1142    fn test_recorded_run_serialize_stress_completed() {
1143        let run = make_test_run(RecordedRunStatusFormat::StressCompleted {
1144            initial_iteration_count: NonZero::new(100),
1145            success_count: 98,
1146            failed_count: 2,
1147            exit_code: 0,
1148        });
1149        let json = serde_json::to_string_pretty(&run).expect("serialization should succeed");
1150        insta::assert_snapshot!(json);
1151    }
1152
1153    #[test]
1154    fn test_recorded_run_serialize_stress_cancelled() {
1155        let run = make_test_run(RecordedRunStatusFormat::StressCancelled {
1156            initial_iteration_count: NonZero::new(100),
1157            success_count: 45,
1158            failed_count: 5,
1159            exit_code: 100,
1160        });
1161        let json = serde_json::to_string_pretty(&run).expect("serialization should succeed");
1162        insta::assert_snapshot!(json);
1163    }
1164
1165    #[test]
1166    fn test_recorded_run_deserialize_unknown_status() {
1167        // Simulate a run from a future nextest version with an unknown status.
1168        // The store-format-version is set to 999 to indicate a future version.
1169        let json = r#"{
1170            "run-id": "550e8400-e29b-41d4-a716-446655440000",
1171            "store-format-version": 999,
1172            "nextest-version": "0.9.999",
1173            "started-at": "2024-12-19T14:22:33-08:00",
1174            "last-written-at": "2024-12-19T22:22:33Z",
1175            "cli-args": ["cargo", "nextest", "run"],
1176            "env-vars": {},
1177            "sizes": {
1178                "log": { "compressed": 2345, "uncompressed": 5678 },
1179                "store": { "compressed": 10000, "uncompressed": 40000 }
1180            },
1181            "status": {
1182                "status": "super-new-status",
1183                "some-future-field": 42
1184            }
1185        }"#;
1186        let run: RecordedRun = serde_json::from_str(json).expect("should deserialize");
1187        assert!(
1188            matches!(run.status, RecordedRunStatusFormat::Unknown),
1189            "unknown status should deserialize to Unknown variant"
1190        );
1191
1192        // Verify domain conversion preserves Unknown.
1193        let info: RecordedRunInfo = run.into();
1194        assert!(
1195            matches!(info.status, RecordedRunStatus::Unknown),
1196            "Unknown format should convert to Unknown domain type"
1197        );
1198    }
1199
1200    #[test]
1201    fn test_recorded_run_roundtrip() {
1202        let original = make_test_run(RecordedRunStatusFormat::Completed {
1203            initial_run_count: 100,
1204            passed: 95,
1205            failed: 5,
1206            exit_code: 0,
1207        });
1208        let json = serde_json::to_string(&original).expect("serialization should succeed");
1209        let roundtripped: RecordedRun =
1210            serde_json::from_str(&json).expect("deserialization should succeed");
1211
1212        assert_eq!(roundtripped.run_id, original.run_id);
1213        assert_eq!(roundtripped.nextest_version, original.nextest_version);
1214        assert_eq!(roundtripped.started_at, original.started_at);
1215        assert_eq!(roundtripped.sizes, original.sizes);
1216
1217        // Verify status fields via domain conversion.
1218        let info: RecordedRunInfo = roundtripped.into();
1219        match info.status {
1220            RecordedRunStatus::Completed(stats) => {
1221                assert_eq!(stats.initial_run_count, 100);
1222                assert_eq!(stats.passed, 95);
1223                assert_eq!(stats.failed, 5);
1224            }
1225            _ => panic!("expected Completed variant"),
1226        }
1227    }
1228
1229    // --- Store format version tests ---
1230
1231    /// Helper to create a StoreFormatVersion.
1232    fn version(major: u32, minor: u32) -> StoreFormatVersion {
1233        StoreFormatVersion::new(
1234            StoreFormatMajorVersion::new(major),
1235            StoreFormatMinorVersion::new(minor),
1236        )
1237    }
1238
1239    #[test]
1240    fn test_store_version_compatibility() {
1241        assert!(
1242            version(1, 0).check_readable_by(version(1, 0)).is_ok(),
1243            "same version should be compatible"
1244        );
1245
1246        assert!(
1247            version(1, 0).check_readable_by(version(1, 2)).is_ok(),
1248            "older minor version should be compatible"
1249        );
1250
1251        let error = version(1, 3).check_readable_by(version(1, 2)).unwrap_err();
1252        assert_eq!(
1253            error,
1254            StoreVersionIncompatibility::MinorTooNew {
1255                archive_minor: StoreFormatMinorVersion::new(3),
1256                supported_minor: StoreFormatMinorVersion::new(2),
1257            },
1258            "newer minor version should be incompatible"
1259        );
1260        insta::assert_snapshot!(error.to_string(), @"minor version 3 is newer than supported version 2");
1261
1262        let error = version(2, 0).check_readable_by(version(1, 5)).unwrap_err();
1263        assert_eq!(
1264            error,
1265            StoreVersionIncompatibility::MajorMismatch {
1266                archive_major: StoreFormatMajorVersion::new(2),
1267                supported_major: StoreFormatMajorVersion::new(1),
1268            },
1269            "different major version should be incompatible"
1270        );
1271        insta::assert_snapshot!(error.to_string(), @"major version 2 differs from supported version 1");
1272
1273        insta::assert_snapshot!(version(1, 2).to_string(), @"1.2");
1274    }
1275
1276    #[test]
1277    fn test_recorded_run_deserialize_without_minor_version() {
1278        // Old archives without store-format-minor-version should default to 0.
1279        let json = r#"{
1280            "run-id": "550e8400-e29b-41d4-a716-446655440000",
1281            "store-format-version": 1,
1282            "nextest-version": "0.9.111",
1283            "started-at": "2024-12-19T14:22:33-08:00",
1284            "last-written-at": "2024-12-19T22:22:33Z",
1285            "cli-args": [],
1286            "env-vars": {},
1287            "sizes": {
1288                "log": { "compressed": 0, "uncompressed": 0 },
1289                "store": { "compressed": 0, "uncompressed": 0 }
1290            },
1291            "status": { "status": "incomplete" }
1292        }"#;
1293        let run: RecordedRun = serde_json::from_str(json).expect("should deserialize");
1294        assert_eq!(run.store_format_version, StoreFormatMajorVersion::new(1));
1295        assert_eq!(
1296            run.store_format_minor_version,
1297            StoreFormatMinorVersion::new(0)
1298        );
1299
1300        // Domain conversion should produce a StoreFormatVersion with minor 0.
1301        let info: RecordedRunInfo = run.into();
1302        assert_eq!(info.store_format_version, version(1, 0));
1303    }
1304
1305    #[test]
1306    fn test_recorded_run_serialize_includes_minor_version() {
1307        // New archives should include store-format-minor-version in serialization.
1308        let run = make_test_run(RecordedRunStatusFormat::Incomplete);
1309        let json = serde_json::to_string_pretty(&run).expect("serialization should succeed");
1310        assert!(
1311            json.contains("store-format-minor-version"),
1312            "serialized run should include store-format-minor-version"
1313        );
1314    }
1315
1316    // --- Portable archive format version tests ---
1317
1318    /// Helper to create a PortableRecordingFormatVersion.
1319    fn portable_version(major: u32, minor: u32) -> PortableRecordingFormatVersion {
1320        PortableRecordingFormatVersion::new(
1321            PortableRecordingFormatMajorVersion::new(major),
1322            PortableRecordingFormatMinorVersion::new(minor),
1323        )
1324    }
1325
1326    #[test]
1327    fn test_portable_version_compatibility() {
1328        assert!(
1329            portable_version(1, 0)
1330                .check_readable_by(portable_version(1, 0))
1331                .is_ok(),
1332            "same version should be compatible"
1333        );
1334
1335        assert!(
1336            portable_version(1, 0)
1337                .check_readable_by(portable_version(1, 2))
1338                .is_ok(),
1339            "older minor version should be compatible"
1340        );
1341
1342        let error = portable_version(1, 3)
1343            .check_readable_by(portable_version(1, 2))
1344            .unwrap_err();
1345        assert_eq!(
1346            error,
1347            PortableRecordingVersionIncompatibility::MinorTooNew {
1348                archive_minor: PortableRecordingFormatMinorVersion::new(3),
1349                supported_minor: PortableRecordingFormatMinorVersion::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 = portable_version(2, 0)
1356            .check_readable_by(portable_version(1, 5))
1357            .unwrap_err();
1358        assert_eq!(
1359            error,
1360            PortableRecordingVersionIncompatibility::MajorMismatch {
1361                archive_major: PortableRecordingFormatMajorVersion::new(2),
1362                supported_major: PortableRecordingFormatMajorVersion::new(1),
1363            },
1364            "different major version should be incompatible"
1365        );
1366        insta::assert_snapshot!(error.to_string(), @"major version 2 differs from supported version 1");
1367
1368        insta::assert_snapshot!(portable_version(1, 2).to_string(), @"1.2");
1369    }
1370
1371    #[test]
1372    fn test_portable_version_serialization() {
1373        // Test that PortableRecordingFormatVersion serializes to {major: ..., minor: ...}.
1374        let version = portable_version(1, 0);
1375        let json = serde_json::to_string(&version).expect("serialization should succeed");
1376        insta::assert_snapshot!(json, @r#"{"major":1,"minor":0}"#);
1377
1378        // Test roundtrip.
1379        let roundtripped: PortableRecordingFormatVersion =
1380            serde_json::from_str(&json).expect("deserialization should succeed");
1381        assert_eq!(roundtripped, version);
1382    }
1383
1384    #[test]
1385    fn test_portable_manifest_format_version() {
1386        // Verify the current PORTABLE_RECORDING_FORMAT_VERSION constant.
1387        assert_eq!(
1388            PORTABLE_RECORDING_FORMAT_VERSION,
1389            portable_version(1, 0),
1390            "current portable recording format version should be 1.0"
1391        );
1392    }
1393}