1use super::{
7 CompletedRunStats, ComponentSizes, RecordedRunInfo, RecordedRunStatus, RecordedSizes,
8 StressCompletedRunStats,
9};
10use camino::Utf8Path;
11use chrono::{DateTime, FixedOffset, Utc};
12use eazip::{CompressionMethod, write::FileOptions};
13use iddqd::{IdOrdItem, IdOrdMap, id_upcast};
14use nextest_metadata::{RustBinaryId, TestCaseName};
15use quick_junit::ReportUuid;
16use semver::Version;
17use serde::{Deserialize, Serialize};
18use std::{
19 collections::{BTreeMap, BTreeSet},
20 fmt,
21 num::NonZero,
22};
23
24macro_rules! define_format_version {
32 (
33 $(#[$attr:meta])*
34 $vis:vis struct $name:ident;
35 ) => {
36 $(#[$attr])*
37 #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Deserialize, Serialize)]
38 #[serde(transparent)]
39 $vis struct $name(u32);
40
41 impl $name {
42 #[doc = concat!("Creates a new `", stringify!($name), "`.")]
43 pub const fn new(version: u32) -> Self {
44 Self(version)
45 }
46 }
47
48 impl fmt::Display for $name {
49 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
50 write!(f, "{}", self.0)
51 }
52 }
53 };
54
55 (
56 @default
57 $(#[$attr:meta])*
58 $vis:vis struct $name:ident;
59 ) => {
60 $(#[$attr])*
61 #[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Deserialize, Serialize)]
62 #[serde(transparent)]
63 $vis struct $name(u32);
64
65 impl $name {
66 #[doc = concat!("Creates a new `", stringify!($name), "`.")]
67 pub const fn new(version: u32) -> Self {
68 Self(version)
69 }
70 }
71
72 impl fmt::Display for $name {
73 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
74 write!(f, "{}", self.0)
75 }
76 }
77 };
78}
79
80define_format_version! {
81 pub struct RunsJsonFormatVersion;
88}
89
90define_format_version! {
91 pub struct StoreFormatMajorVersion;
94}
95
96define_format_version! {
97 @default
98 pub struct StoreFormatMinorVersion;
100}
101
102#[derive(Clone, Copy, Debug, PartialEq, Eq)]
104pub struct StoreFormatVersion {
105 pub major: StoreFormatMajorVersion,
107 pub minor: StoreFormatMinorVersion,
109}
110
111impl StoreFormatVersion {
112 pub const fn new(major: StoreFormatMajorVersion, minor: StoreFormatMinorVersion) -> Self {
114 Self { major, minor }
115 }
116
117 pub fn check_readable_by(self, supported: Self) -> Result<(), StoreVersionIncompatibility> {
120 if self.major < supported.major {
121 return Err(StoreVersionIncompatibility::RecordingTooOld {
122 recording_major: self.major,
123 supported_major: supported.major,
124 last_nextest_version: self.major.last_nextest_version(),
125 });
126 }
127 if self.major > supported.major {
128 return Err(StoreVersionIncompatibility::RecordingTooNew {
129 recording_major: self.major,
130 supported_major: supported.major,
131 });
132 }
133 if self.minor > supported.minor {
134 return Err(StoreVersionIncompatibility::MinorTooNew {
135 recording_minor: self.minor,
136 supported_minor: supported.minor,
137 });
138 }
139 Ok(())
140 }
141}
142
143impl fmt::Display for StoreFormatVersion {
144 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
145 write!(f, "{}.{}", self.major, self.minor)
146 }
147}
148
149impl StoreFormatMajorVersion {
150 pub fn last_nextest_version(self) -> Option<&'static str> {
156 match self.0 {
157 1 => Some("0.9.130"),
158 _ => None,
159 }
160 }
161}
162
163#[derive(Clone, Debug, PartialEq, Eq)]
166pub enum StoreVersionIncompatibility {
167 RecordingTooOld {
169 recording_major: StoreFormatMajorVersion,
171 supported_major: StoreFormatMajorVersion,
173 last_nextest_version: Option<&'static str>,
176 },
177 RecordingTooNew {
179 recording_major: StoreFormatMajorVersion,
181 supported_major: StoreFormatMajorVersion,
183 },
184 MinorTooNew {
186 recording_minor: StoreFormatMinorVersion,
188 supported_minor: StoreFormatMinorVersion,
190 },
191}
192
193impl fmt::Display for StoreVersionIncompatibility {
194 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
195 match self {
196 Self::RecordingTooOld {
197 recording_major,
198 supported_major,
199 last_nextest_version,
200 } => {
201 write!(
202 f,
203 "recording has major version {recording_major}, \
204 but this nextest requires version {supported_major}"
205 )?;
206 if let Some(version) = last_nextest_version {
207 write!(f, " (use nextest <= {version} to replay this recording)")?;
208 }
209 Ok(())
210 }
211 Self::RecordingTooNew {
212 recording_major,
213 supported_major,
214 } => {
215 write!(
216 f,
217 "recording has major version {recording_major}, \
218 but this nextest only supports version {supported_major} \
219 (upgrade nextest to replay this recording)"
220 )
221 }
222 Self::MinorTooNew {
223 recording_minor,
224 supported_minor,
225 } => {
226 write!(
227 f,
228 "minor version {} is newer than supported version {}",
229 recording_minor, supported_minor
230 )
231 }
232 }
233 }
234}
235
236pub(super) const RUNS_JSON_FORMAT_VERSION: RunsJsonFormatVersion = RunsJsonFormatVersion::new(2);
242
243pub const STORE_FORMAT_VERSION: StoreFormatVersion = StoreFormatVersion::new(
255 StoreFormatMajorVersion::new(2),
256 StoreFormatMinorVersion::new(0),
257);
258
259#[derive(Debug, Clone, Copy, PartialEq, Eq)]
261pub enum RunsJsonWritePermission {
262 Allowed,
264 Denied {
266 file_version: RunsJsonFormatVersion,
268 max_supported_version: RunsJsonFormatVersion,
270 },
271}
272
273#[derive(Debug, Deserialize, Serialize)]
275#[serde(rename_all = "kebab-case")]
276pub(super) struct RecordedRunList {
277 pub(super) format_version: RunsJsonFormatVersion,
279
280 #[serde(default, skip_serializing_if = "Option::is_none")]
285 pub(super) last_pruned_at: Option<DateTime<Utc>>,
286
287 #[serde(default)]
289 pub(super) runs: Vec<RecordedRun>,
290}
291
292pub(super) struct RunListData {
294 pub(super) runs: Vec<RecordedRunInfo>,
295 pub(super) last_pruned_at: Option<DateTime<Utc>>,
296}
297
298impl RecordedRunList {
299 #[cfg(test)]
301 fn new() -> Self {
302 Self {
303 format_version: RUNS_JSON_FORMAT_VERSION,
304 last_pruned_at: None,
305 runs: Vec::new(),
306 }
307 }
308
309 pub(super) fn into_data(self) -> RunListData {
311 RunListData {
312 runs: self.runs.into_iter().map(RecordedRunInfo::from).collect(),
313 last_pruned_at: self.last_pruned_at,
314 }
315 }
316
317 pub(super) fn from_data(
322 runs: &[RecordedRunInfo],
323 last_pruned_at: Option<DateTime<Utc>>,
324 ) -> Self {
325 Self {
326 format_version: RUNS_JSON_FORMAT_VERSION,
327 last_pruned_at,
328 runs: runs.iter().map(RecordedRun::from).collect(),
329 }
330 }
331
332 pub(super) fn write_permission(&self) -> RunsJsonWritePermission {
337 if self.format_version > RUNS_JSON_FORMAT_VERSION {
338 RunsJsonWritePermission::Denied {
339 file_version: self.format_version,
340 max_supported_version: RUNS_JSON_FORMAT_VERSION,
341 }
342 } else {
343 RunsJsonWritePermission::Allowed
344 }
345 }
346}
347
348#[derive(Clone, Debug, Deserialize, Serialize)]
350#[serde(rename_all = "kebab-case")]
351pub(super) struct RecordedRun {
352 pub(super) run_id: ReportUuid,
354 pub(super) store_format_version: StoreFormatMajorVersion,
359 #[serde(default)]
364 pub(super) store_format_minor_version: StoreFormatMinorVersion,
365 pub(super) nextest_version: Version,
367 pub(super) started_at: DateTime<FixedOffset>,
369 pub(super) last_written_at: DateTime<FixedOffset>,
375 #[serde(default, skip_serializing_if = "Option::is_none")]
377 pub(super) duration_secs: Option<f64>,
378 #[serde(default)]
380 pub(super) cli_args: Vec<String>,
381 #[serde(default)]
386 pub(super) build_scope_args: Vec<String>,
387 #[serde(default)]
391 pub(super) env_vars: BTreeMap<String, String>,
392 #[serde(default)]
394 pub(super) parent_run_id: Option<ReportUuid>,
395 pub(super) sizes: RecordedSizesFormat,
399 pub(super) status: RecordedRunStatusFormat,
401}
402
403#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Deserialize, Serialize)]
405#[serde(rename_all = "kebab-case")]
406pub(super) struct RecordedSizesFormat {
407 pub(super) log: ComponentSizesFormat,
409 pub(super) store: ComponentSizesFormat,
411}
412
413#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Deserialize, Serialize)]
415#[serde(rename_all = "kebab-case")]
416pub(super) struct ComponentSizesFormat {
417 pub(super) compressed: u64,
419 pub(super) uncompressed: u64,
421 #[serde(default)]
423 pub(super) entries: u64,
424}
425
426impl From<RecordedSizes> for RecordedSizesFormat {
427 fn from(sizes: RecordedSizes) -> Self {
428 Self {
429 log: ComponentSizesFormat {
430 compressed: sizes.log.compressed,
431 uncompressed: sizes.log.uncompressed,
432 entries: sizes.log.entries,
433 },
434 store: ComponentSizesFormat {
435 compressed: sizes.store.compressed,
436 uncompressed: sizes.store.uncompressed,
437 entries: sizes.store.entries,
438 },
439 }
440 }
441}
442
443impl From<RecordedSizesFormat> for RecordedSizes {
444 fn from(sizes: RecordedSizesFormat) -> Self {
445 Self {
446 log: ComponentSizes {
447 compressed: sizes.log.compressed,
448 uncompressed: sizes.log.uncompressed,
449 entries: sizes.log.entries,
450 },
451 store: ComponentSizes {
452 compressed: sizes.store.compressed,
453 uncompressed: sizes.store.uncompressed,
454 entries: sizes.store.entries,
455 },
456 }
457 }
458}
459
460#[derive(Clone, Debug, Deserialize, Serialize)]
462#[serde(tag = "status", rename_all = "kebab-case")]
463pub(super) enum RecordedRunStatusFormat {
464 Incomplete,
466 #[serde(rename_all = "kebab-case")]
468 Completed {
469 initial_run_count: usize,
471 passed: usize,
473 failed: usize,
475 exit_code: i32,
477 },
478 #[serde(rename_all = "kebab-case")]
480 Cancelled {
481 initial_run_count: usize,
483 passed: usize,
485 failed: usize,
487 exit_code: i32,
489 },
490 #[serde(rename_all = "kebab-case")]
492 StressCompleted {
493 initial_iteration_count: Option<NonZero<u32>>,
495 success_count: u32,
497 failed_count: u32,
499 exit_code: i32,
501 },
502 #[serde(rename_all = "kebab-case")]
504 StressCancelled {
505 initial_iteration_count: Option<NonZero<u32>>,
507 success_count: u32,
509 failed_count: u32,
511 exit_code: i32,
513 },
514 #[serde(other)]
519 Unknown,
520}
521
522impl From<RecordedRun> for RecordedRunInfo {
523 fn from(run: RecordedRun) -> Self {
524 Self {
525 run_id: run.run_id,
526 store_format_version: StoreFormatVersion::new(
527 run.store_format_version,
528 run.store_format_minor_version,
529 ),
530 nextest_version: run.nextest_version,
531 started_at: run.started_at,
532 last_written_at: run.last_written_at,
533 duration_secs: run.duration_secs,
534 cli_args: run.cli_args,
535 build_scope_args: run.build_scope_args,
536 env_vars: run.env_vars,
537 parent_run_id: run.parent_run_id,
538 sizes: run.sizes.into(),
539 status: run.status.into(),
540 }
541 }
542}
543
544impl From<&RecordedRunInfo> for RecordedRun {
545 fn from(run: &RecordedRunInfo) -> Self {
546 Self {
547 run_id: run.run_id,
548 store_format_version: run.store_format_version.major,
549 store_format_minor_version: run.store_format_version.minor,
550 nextest_version: run.nextest_version.clone(),
551 started_at: run.started_at,
552 last_written_at: run.last_written_at,
553 duration_secs: run.duration_secs,
554 cli_args: run.cli_args.clone(),
555 build_scope_args: run.build_scope_args.clone(),
556 env_vars: run.env_vars.clone(),
557 parent_run_id: run.parent_run_id,
558 sizes: run.sizes.into(),
559 status: (&run.status).into(),
560 }
561 }
562}
563
564impl From<RecordedRunStatusFormat> for RecordedRunStatus {
565 fn from(status: RecordedRunStatusFormat) -> Self {
566 match status {
567 RecordedRunStatusFormat::Incomplete => Self::Incomplete,
568 RecordedRunStatusFormat::Unknown => Self::Unknown,
569 RecordedRunStatusFormat::Completed {
570 initial_run_count,
571 passed,
572 failed,
573 exit_code,
574 } => Self::Completed(CompletedRunStats {
575 initial_run_count,
576 passed,
577 failed,
578 exit_code,
579 }),
580 RecordedRunStatusFormat::Cancelled {
581 initial_run_count,
582 passed,
583 failed,
584 exit_code,
585 } => Self::Cancelled(CompletedRunStats {
586 initial_run_count,
587 passed,
588 failed,
589 exit_code,
590 }),
591 RecordedRunStatusFormat::StressCompleted {
592 initial_iteration_count,
593 success_count,
594 failed_count,
595 exit_code,
596 } => Self::StressCompleted(StressCompletedRunStats {
597 initial_iteration_count,
598 success_count,
599 failed_count,
600 exit_code,
601 }),
602 RecordedRunStatusFormat::StressCancelled {
603 initial_iteration_count,
604 success_count,
605 failed_count,
606 exit_code,
607 } => Self::StressCancelled(StressCompletedRunStats {
608 initial_iteration_count,
609 success_count,
610 failed_count,
611 exit_code,
612 }),
613 }
614 }
615}
616
617impl From<&RecordedRunStatus> for RecordedRunStatusFormat {
618 fn from(status: &RecordedRunStatus) -> Self {
619 match status {
620 RecordedRunStatus::Incomplete => Self::Incomplete,
621 RecordedRunStatus::Unknown => Self::Unknown,
622 RecordedRunStatus::Completed(stats) => Self::Completed {
623 initial_run_count: stats.initial_run_count,
624 passed: stats.passed,
625 failed: stats.failed,
626 exit_code: stats.exit_code,
627 },
628 RecordedRunStatus::Cancelled(stats) => Self::Cancelled {
629 initial_run_count: stats.initial_run_count,
630 passed: stats.passed,
631 failed: stats.failed,
632 exit_code: stats.exit_code,
633 },
634 RecordedRunStatus::StressCompleted(stats) => Self::StressCompleted {
635 initial_iteration_count: stats.initial_iteration_count,
636 success_count: stats.success_count,
637 failed_count: stats.failed_count,
638 exit_code: stats.exit_code,
639 },
640 RecordedRunStatus::StressCancelled(stats) => Self::StressCancelled {
641 initial_iteration_count: stats.initial_iteration_count,
642 success_count: stats.success_count,
643 failed_count: stats.failed_count,
644 exit_code: stats.exit_code,
645 },
646 }
647 }
648}
649
650#[derive(Clone, Debug, Deserialize, Serialize)]
658#[serde(rename_all = "kebab-case")]
659pub struct RerunInfo {
660 pub parent_run_id: ReportUuid,
662
663 pub root_info: RerunRootInfo,
665
666 pub test_suites: IdOrdMap<RerunTestSuiteInfo>,
668}
669
670#[derive(Clone, Debug, Deserialize, Serialize)]
672#[serde(rename_all = "kebab-case")]
673pub struct RerunRootInfo {
674 pub run_id: ReportUuid,
676
677 pub build_scope_args: Vec<String>,
679}
680
681impl RerunRootInfo {
682 pub fn new(run_id: ReportUuid, build_scope_args: Vec<String>) -> Self {
688 Self {
689 run_id,
690 build_scope_args,
691 }
692 }
693}
694
695#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
697pub struct RerunTestSuiteInfo {
698 pub binary_id: RustBinaryId,
700
701 pub passing: BTreeSet<TestCaseName>,
703
704 pub outstanding: BTreeSet<TestCaseName>,
706}
707
708impl RerunTestSuiteInfo {
709 pub(super) fn new(binary_id: RustBinaryId) -> Self {
710 Self {
711 binary_id,
712 passing: BTreeSet::new(),
713 outstanding: BTreeSet::new(),
714 }
715 }
716}
717
718impl IdOrdItem for RerunTestSuiteInfo {
719 type Key<'a> = &'a RustBinaryId;
720 fn key(&self) -> Self::Key<'_> {
721 &self.binary_id
722 }
723 id_upcast!();
724}
725
726pub static STORE_ZIP_FILE_NAME: &str = "store.zip";
732
733pub static RUN_LOG_FILE_NAME: &str = "run.log.zst";
735
736pub fn has_zip_extension(path: &Utf8Path) -> bool {
738 path.extension()
739 .is_some_and(|ext| ext.eq_ignore_ascii_case("zip"))
740}
741
742pub static CARGO_METADATA_JSON_PATH: &str = "meta/cargo-metadata.json";
745pub static TEST_LIST_JSON_PATH: &str = "meta/test-list.json";
747pub static RECORD_OPTS_JSON_PATH: &str = "meta/record-opts.json";
749pub static RERUN_INFO_JSON_PATH: &str = "meta/rerun-info.json";
751pub static STDOUT_DICT_PATH: &str = "meta/stdout.dict";
753pub static STDERR_DICT_PATH: &str = "meta/stderr.dict";
755
756define_format_version! {
761 pub struct PortableRecordingFormatMajorVersion;
763}
764
765define_format_version! {
766 @default
767 pub struct PortableRecordingFormatMinorVersion;
769}
770
771#[derive(Clone, Copy, Debug, PartialEq, Eq, Deserialize, Serialize)]
773pub struct PortableRecordingFormatVersion {
774 pub major: PortableRecordingFormatMajorVersion,
776 pub minor: PortableRecordingFormatMinorVersion,
778}
779
780impl PortableRecordingFormatVersion {
781 pub const fn new(
783 major: PortableRecordingFormatMajorVersion,
784 minor: PortableRecordingFormatMinorVersion,
785 ) -> Self {
786 Self { major, minor }
787 }
788
789 pub fn check_readable_by(
792 self,
793 supported: Self,
794 ) -> Result<(), PortableRecordingVersionIncompatibility> {
795 if self.major != supported.major {
796 return Err(PortableRecordingVersionIncompatibility::MajorMismatch {
797 recording_major: self.major,
798 supported_major: supported.major,
799 });
800 }
801 if self.minor > supported.minor {
802 return Err(PortableRecordingVersionIncompatibility::MinorTooNew {
803 recording_minor: self.minor,
804 supported_minor: supported.minor,
805 });
806 }
807 Ok(())
808 }
809}
810
811impl fmt::Display for PortableRecordingFormatVersion {
812 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
813 write!(f, "{}.{}", self.major, self.minor)
814 }
815}
816
817#[derive(Clone, Copy, Debug, PartialEq, Eq)]
820pub enum PortableRecordingVersionIncompatibility {
821 MajorMismatch {
823 recording_major: PortableRecordingFormatMajorVersion,
825 supported_major: PortableRecordingFormatMajorVersion,
827 },
828 MinorTooNew {
830 recording_minor: PortableRecordingFormatMinorVersion,
832 supported_minor: PortableRecordingFormatMinorVersion,
834 },
835}
836
837impl fmt::Display for PortableRecordingVersionIncompatibility {
838 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
839 match self {
840 Self::MajorMismatch {
841 recording_major,
842 supported_major,
843 } => {
844 write!(
845 f,
846 "major version {} differs from supported version {}",
847 recording_major, supported_major
848 )
849 }
850 Self::MinorTooNew {
851 recording_minor,
852 supported_minor,
853 } => {
854 write!(
855 f,
856 "minor version {} is newer than supported version {}",
857 recording_minor, supported_minor
858 )
859 }
860 }
861 }
862}
863
864pub const PORTABLE_RECORDING_FORMAT_VERSION: PortableRecordingFormatVersion =
866 PortableRecordingFormatVersion::new(
867 PortableRecordingFormatMajorVersion::new(1),
868 PortableRecordingFormatMinorVersion::new(0),
869 );
870
871pub static PORTABLE_MANIFEST_FILE_NAME: &str = "manifest.json";
873
874#[derive(Debug, Deserialize, Serialize)]
879#[serde(rename_all = "kebab-case")]
880pub(crate) struct PortableManifest {
881 pub(crate) format_version: PortableRecordingFormatVersion,
883 pub(super) run: RecordedRun,
885}
886
887impl PortableManifest {
888 pub(crate) fn new(run: &RecordedRunInfo) -> Self {
890 Self {
891 format_version: PORTABLE_RECORDING_FORMAT_VERSION,
892 run: RecordedRun::from(run),
893 }
894 }
895
896 pub(crate) fn run_info(&self) -> RecordedRunInfo {
898 RecordedRunInfo::from(self.run.clone())
899 }
900
901 pub(crate) fn store_format_version(&self) -> StoreFormatVersion {
903 StoreFormatVersion::new(
904 self.run.store_format_version,
905 self.run.store_format_minor_version,
906 )
907 }
908}
909
910#[derive(Clone, Copy, Debug, PartialEq, Eq)]
912pub enum OutputDict {
913 Stdout,
915 Stderr,
917 None,
919}
920
921impl OutputDict {
922 pub fn for_path(path: &Utf8Path) -> Self {
930 let mut iter = path.iter();
931 let Some(first_component) = iter.next() else {
932 return Self::None;
933 };
934 if first_component != "out" {
936 return Self::None;
937 }
938
939 Self::for_output_file_name(iter.as_path().as_str())
940 }
941
942 pub fn for_output_file_name(file_name: &str) -> Self {
947 if file_name.ends_with("-stdout") || file_name.ends_with("-combined") {
948 Self::Stdout
949 } else if file_name.ends_with("-stderr") {
950 Self::Stderr
951 } else {
952 Self::None
954 }
955 }
956
957 pub fn dict_bytes(self) -> Option<&'static [u8]> {
961 match self {
962 Self::Stdout => Some(super::dicts::STDOUT),
963 Self::Stderr => Some(super::dicts::STDERR),
964 Self::None => None,
965 }
966 }
967}
968
969pub(super) fn stored_file_options() -> FileOptions {
976 let mut options = FileOptions::default();
977 options.compression_method = CompressionMethod::STORE;
978 options
979}
980
981pub(super) fn zstd_file_options() -> FileOptions {
983 let mut options = FileOptions::default();
984 options.compression_method = CompressionMethod::ZSTD;
985 options.level = Some(3);
986 options
987}
988
989#[cfg(test)]
990mod tests {
991 use super::*;
992
993 #[test]
994 fn test_output_dict_for_path() {
995 assert_eq!(
997 OutputDict::for_path("meta/cargo-metadata.json".as_ref()),
998 OutputDict::None
999 );
1000 assert_eq!(
1001 OutputDict::for_path("meta/test-list.json".as_ref()),
1002 OutputDict::None
1003 );
1004
1005 assert_eq!(
1007 OutputDict::for_path("out/0123456789abcdef-stdout".as_ref()),
1008 OutputDict::Stdout
1009 );
1010 assert_eq!(
1011 OutputDict::for_path("out/0123456789abcdef-stderr".as_ref()),
1012 OutputDict::Stderr
1013 );
1014 assert_eq!(
1015 OutputDict::for_path("out/0123456789abcdef-combined".as_ref()),
1016 OutputDict::Stdout
1017 );
1018 }
1019
1020 #[test]
1021 fn test_output_dict_for_output_file_name() {
1022 assert_eq!(
1024 OutputDict::for_output_file_name("0123456789abcdef-stdout"),
1025 OutputDict::Stdout
1026 );
1027 assert_eq!(
1028 OutputDict::for_output_file_name("0123456789abcdef-stderr"),
1029 OutputDict::Stderr
1030 );
1031 assert_eq!(
1032 OutputDict::for_output_file_name("0123456789abcdef-combined"),
1033 OutputDict::Stdout
1034 );
1035 assert_eq!(
1036 OutputDict::for_output_file_name("0123456789abcdef-unknown"),
1037 OutputDict::None
1038 );
1039 }
1040
1041 #[test]
1042 fn test_dict_bytes() {
1043 assert!(OutputDict::Stdout.dict_bytes().is_some());
1044 assert!(OutputDict::Stderr.dict_bytes().is_some());
1045 assert!(OutputDict::None.dict_bytes().is_none());
1046 }
1047
1048 #[test]
1049 fn test_runs_json_missing_version() {
1050 let json = r#"{"runs": []}"#;
1052 let result: Result<RecordedRunList, _> = serde_json::from_str(json);
1053 assert!(result.is_err(), "expected error for missing format-version");
1054 }
1055
1056 #[test]
1057 fn test_runs_json_current_version() {
1058 let json = format!(
1060 r#"{{"format-version": {}, "runs": []}}"#,
1061 RUNS_JSON_FORMAT_VERSION
1062 );
1063 let list: RecordedRunList = serde_json::from_str(&json).expect("should deserialize");
1064 assert_eq!(list.write_permission(), RunsJsonWritePermission::Allowed);
1065 }
1066
1067 #[test]
1068 fn test_runs_json_older_version() {
1069 let json = r#"{"format-version": 1, "runs": []}"#;
1073 let list: RecordedRunList = serde_json::from_str(json).expect("should deserialize");
1074 assert_eq!(list.write_permission(), RunsJsonWritePermission::Allowed);
1075 }
1076
1077 #[test]
1078 fn test_runs_json_newer_version() {
1079 let json = r#"{"format-version": 99, "runs": []}"#;
1081 let list: RecordedRunList = serde_json::from_str(json).expect("should deserialize");
1082 assert_eq!(
1083 list.write_permission(),
1084 RunsJsonWritePermission::Denied {
1085 file_version: RunsJsonFormatVersion::new(99),
1086 max_supported_version: RUNS_JSON_FORMAT_VERSION,
1087 }
1088 );
1089 }
1090
1091 #[test]
1092 fn test_runs_json_serialization_includes_version() {
1093 let list = RecordedRunList::from_data(&[], None);
1095 let json = serde_json::to_string(&list).expect("should serialize");
1096 assert!(
1097 json.contains("format-version"),
1098 "serialized runs.json.zst should include format-version"
1099 );
1100
1101 let parsed: serde_json::Value = serde_json::from_str(&json).expect("should parse");
1103 let version: RunsJsonFormatVersion =
1104 serde_json::from_value(parsed["format-version"].clone()).expect("valid version");
1105 assert_eq!(
1106 version, RUNS_JSON_FORMAT_VERSION,
1107 "format-version should be current version"
1108 );
1109 }
1110
1111 #[test]
1112 fn test_runs_json_new() {
1113 let list = RecordedRunList::new();
1115 assert_eq!(list.format_version, RUNS_JSON_FORMAT_VERSION);
1116 assert!(list.runs.is_empty());
1117 assert_eq!(list.write_permission(), RunsJsonWritePermission::Allowed);
1118 }
1119
1120 fn make_test_run(status: RecordedRunStatusFormat) -> RecordedRun {
1123 RecordedRun {
1124 run_id: ReportUuid::from_u128(0x550e8400_e29b_41d4_a716_446655440000),
1125 store_format_version: STORE_FORMAT_VERSION.major,
1126 store_format_minor_version: STORE_FORMAT_VERSION.minor,
1127 nextest_version: Version::new(0, 9, 111),
1128 started_at: DateTime::parse_from_rfc3339("2024-12-19T14:22:33-08:00")
1129 .expect("valid timestamp"),
1130 last_written_at: DateTime::parse_from_rfc3339("2024-12-19T22:22:33Z")
1131 .expect("valid timestamp"),
1132 duration_secs: Some(12.345),
1133 cli_args: vec![
1134 "cargo".to_owned(),
1135 "nextest".to_owned(),
1136 "run".to_owned(),
1137 "--workspace".to_owned(),
1138 ],
1139 build_scope_args: vec!["--workspace".to_owned()],
1140 env_vars: BTreeMap::from([
1141 ("CARGO_TERM_COLOR".to_owned(), "always".to_owned()),
1142 ("NEXTEST_PROFILE".to_owned(), "ci".to_owned()),
1143 ]),
1144 parent_run_id: Some(ReportUuid::from_u128(
1145 0x550e7400_e29b_41d4_a716_446655440000,
1146 )),
1147 sizes: RecordedSizesFormat {
1148 log: ComponentSizesFormat {
1149 compressed: 2345,
1150 uncompressed: 5678,
1151 entries: 42,
1152 },
1153 store: ComponentSizesFormat {
1154 compressed: 10000,
1155 uncompressed: 40000,
1156 entries: 15,
1157 },
1158 },
1159 status,
1160 }
1161 }
1162
1163 #[test]
1164 fn test_recorded_run_serialize_incomplete() {
1165 let run = make_test_run(RecordedRunStatusFormat::Incomplete);
1166 let json = serde_json::to_string_pretty(&run).expect("serialization should succeed");
1167 insta::assert_snapshot!(json);
1168 }
1169
1170 #[test]
1171 fn test_recorded_run_serialize_completed() {
1172 let run = make_test_run(RecordedRunStatusFormat::Completed {
1173 initial_run_count: 100,
1174 passed: 95,
1175 failed: 5,
1176 exit_code: 0,
1177 });
1178 let json = serde_json::to_string_pretty(&run).expect("serialization should succeed");
1179 insta::assert_snapshot!(json);
1180 }
1181
1182 #[test]
1183 fn test_recorded_run_serialize_cancelled() {
1184 let run = make_test_run(RecordedRunStatusFormat::Cancelled {
1185 initial_run_count: 100,
1186 passed: 45,
1187 failed: 5,
1188 exit_code: 100,
1189 });
1190 let json = serde_json::to_string_pretty(&run).expect("serialization should succeed");
1191 insta::assert_snapshot!(json);
1192 }
1193
1194 #[test]
1195 fn test_recorded_run_serialize_stress_completed() {
1196 let run = make_test_run(RecordedRunStatusFormat::StressCompleted {
1197 initial_iteration_count: NonZero::new(100),
1198 success_count: 98,
1199 failed_count: 2,
1200 exit_code: 0,
1201 });
1202 let json = serde_json::to_string_pretty(&run).expect("serialization should succeed");
1203 insta::assert_snapshot!(json);
1204 }
1205
1206 #[test]
1207 fn test_recorded_run_serialize_stress_cancelled() {
1208 let run = make_test_run(RecordedRunStatusFormat::StressCancelled {
1209 initial_iteration_count: NonZero::new(100),
1210 success_count: 45,
1211 failed_count: 5,
1212 exit_code: 100,
1213 });
1214 let json = serde_json::to_string_pretty(&run).expect("serialization should succeed");
1215 insta::assert_snapshot!(json);
1216 }
1217
1218 #[test]
1219 fn test_recorded_run_deserialize_unknown_status() {
1220 let json = r#"{
1223 "run-id": "550e8400-e29b-41d4-a716-446655440000",
1224 "store-format-version": 999,
1225 "nextest-version": "0.9.999",
1226 "started-at": "2024-12-19T14:22:33-08:00",
1227 "last-written-at": "2024-12-19T22:22:33Z",
1228 "cli-args": ["cargo", "nextest", "run"],
1229 "env-vars": {},
1230 "sizes": {
1231 "log": { "compressed": 2345, "uncompressed": 5678 },
1232 "store": { "compressed": 10000, "uncompressed": 40000 }
1233 },
1234 "status": {
1235 "status": "super-new-status",
1236 "some-future-field": 42
1237 }
1238 }"#;
1239 let run: RecordedRun = serde_json::from_str(json).expect("should deserialize");
1240 assert!(
1241 matches!(run.status, RecordedRunStatusFormat::Unknown),
1242 "unknown status should deserialize to Unknown variant"
1243 );
1244
1245 let info: RecordedRunInfo = run.into();
1247 assert!(
1248 matches!(info.status, RecordedRunStatus::Unknown),
1249 "Unknown format should convert to Unknown domain type"
1250 );
1251 }
1252
1253 #[test]
1254 fn test_recorded_run_roundtrip() {
1255 let original = make_test_run(RecordedRunStatusFormat::Completed {
1256 initial_run_count: 100,
1257 passed: 95,
1258 failed: 5,
1259 exit_code: 0,
1260 });
1261 let json = serde_json::to_string(&original).expect("serialization should succeed");
1262 let roundtripped: RecordedRun =
1263 serde_json::from_str(&json).expect("deserialization should succeed");
1264
1265 assert_eq!(roundtripped.run_id, original.run_id);
1266 assert_eq!(roundtripped.nextest_version, original.nextest_version);
1267 assert_eq!(roundtripped.started_at, original.started_at);
1268 assert_eq!(roundtripped.sizes, original.sizes);
1269
1270 let info: RecordedRunInfo = roundtripped.into();
1272 match info.status {
1273 RecordedRunStatus::Completed(stats) => {
1274 assert_eq!(stats.initial_run_count, 100);
1275 assert_eq!(stats.passed, 95);
1276 assert_eq!(stats.failed, 5);
1277 }
1278 _ => panic!("expected Completed variant"),
1279 }
1280 }
1281
1282 fn version(major: u32, minor: u32) -> StoreFormatVersion {
1286 StoreFormatVersion::new(
1287 StoreFormatMajorVersion::new(major),
1288 StoreFormatMinorVersion::new(minor),
1289 )
1290 }
1291
1292 #[test]
1293 fn test_store_version_compatibility() {
1294 assert!(
1295 version(1, 0).check_readable_by(version(1, 0)).is_ok(),
1296 "same version should be compatible"
1297 );
1298
1299 assert!(
1300 version(1, 0).check_readable_by(version(1, 2)).is_ok(),
1301 "older minor version should be compatible"
1302 );
1303
1304 let error = version(1, 3).check_readable_by(version(1, 2)).unwrap_err();
1305 assert_eq!(
1306 error,
1307 StoreVersionIncompatibility::MinorTooNew {
1308 recording_minor: StoreFormatMinorVersion::new(3),
1309 supported_minor: StoreFormatMinorVersion::new(2),
1310 },
1311 "newer minor version should be incompatible"
1312 );
1313 insta::assert_snapshot!(error.to_string(), @"minor version 3 is newer than supported version 2");
1314
1315 let error = version(2, 0).check_readable_by(version(1, 5)).unwrap_err();
1317 assert_eq!(
1318 error,
1319 StoreVersionIncompatibility::RecordingTooNew {
1320 recording_major: StoreFormatMajorVersion::new(2),
1321 supported_major: StoreFormatMajorVersion::new(1),
1322 },
1323 );
1324 insta::assert_snapshot!(
1325 error.to_string(),
1326 @"recording has major version 2, but this nextest only supports version 1 (upgrade nextest to replay this recording)"
1327 );
1328
1329 let error = version(1, 0).check_readable_by(version(2, 0)).unwrap_err();
1331 assert_eq!(
1332 error,
1333 StoreVersionIncompatibility::RecordingTooOld {
1334 recording_major: StoreFormatMajorVersion::new(1),
1335 supported_major: StoreFormatMajorVersion::new(2),
1336 last_nextest_version: Some("0.9.130"),
1337 },
1338 );
1339 insta::assert_snapshot!(
1340 error.to_string(),
1341 @"recording has major version 1, but this nextest requires version 2 (use nextest <= 0.9.130 to replay this recording)"
1342 );
1343
1344 let error = version(3, 0).check_readable_by(version(5, 0)).unwrap_err();
1346 assert_eq!(
1347 error,
1348 StoreVersionIncompatibility::RecordingTooOld {
1349 recording_major: StoreFormatMajorVersion::new(3),
1350 supported_major: StoreFormatMajorVersion::new(5),
1351 last_nextest_version: None,
1352 },
1353 );
1354 insta::assert_snapshot!(
1355 error.to_string(),
1356 @"recording has major version 3, but this nextest requires version 5"
1357 );
1358
1359 insta::assert_snapshot!(version(1, 2).to_string(), @"1.2");
1360 }
1361
1362 #[test]
1363 fn test_recorded_run_deserialize_without_minor_version() {
1364 let json = r#"{
1366 "run-id": "550e8400-e29b-41d4-a716-446655440000",
1367 "store-format-version": 1,
1368 "nextest-version": "0.9.111",
1369 "started-at": "2024-12-19T14:22:33-08:00",
1370 "last-written-at": "2024-12-19T22:22:33Z",
1371 "cli-args": [],
1372 "env-vars": {},
1373 "sizes": {
1374 "log": { "compressed": 0, "uncompressed": 0 },
1375 "store": { "compressed": 0, "uncompressed": 0 }
1376 },
1377 "status": { "status": "incomplete" }
1378 }"#;
1379 let run: RecordedRun = serde_json::from_str(json).expect("should deserialize");
1380 assert_eq!(run.store_format_version, StoreFormatMajorVersion::new(1));
1381 assert_eq!(
1382 run.store_format_minor_version,
1383 StoreFormatMinorVersion::new(0)
1384 );
1385
1386 let info: RecordedRunInfo = run.into();
1388 assert_eq!(info.store_format_version, version(1, 0));
1389 }
1390
1391 #[test]
1392 fn test_recorded_run_serialize_includes_minor_version() {
1393 let run = make_test_run(RecordedRunStatusFormat::Incomplete);
1395 let json = serde_json::to_string_pretty(&run).expect("serialization should succeed");
1396 assert!(
1397 json.contains("store-format-minor-version"),
1398 "serialized run should include store-format-minor-version"
1399 );
1400 }
1401
1402 fn portable_version(major: u32, minor: u32) -> PortableRecordingFormatVersion {
1406 PortableRecordingFormatVersion::new(
1407 PortableRecordingFormatMajorVersion::new(major),
1408 PortableRecordingFormatMinorVersion::new(minor),
1409 )
1410 }
1411
1412 #[test]
1413 fn test_portable_version_compatibility() {
1414 assert!(
1415 portable_version(1, 0)
1416 .check_readable_by(portable_version(1, 0))
1417 .is_ok(),
1418 "same version should be compatible"
1419 );
1420
1421 assert!(
1422 portable_version(1, 0)
1423 .check_readable_by(portable_version(1, 2))
1424 .is_ok(),
1425 "older minor version should be compatible"
1426 );
1427
1428 let error = portable_version(1, 3)
1429 .check_readable_by(portable_version(1, 2))
1430 .unwrap_err();
1431 assert_eq!(
1432 error,
1433 PortableRecordingVersionIncompatibility::MinorTooNew {
1434 recording_minor: PortableRecordingFormatMinorVersion::new(3),
1435 supported_minor: PortableRecordingFormatMinorVersion::new(2),
1436 },
1437 "newer minor version should be incompatible"
1438 );
1439 insta::assert_snapshot!(error.to_string(), @"minor version 3 is newer than supported version 2");
1440
1441 let error = portable_version(2, 0)
1442 .check_readable_by(portable_version(1, 5))
1443 .unwrap_err();
1444 assert_eq!(
1445 error,
1446 PortableRecordingVersionIncompatibility::MajorMismatch {
1447 recording_major: PortableRecordingFormatMajorVersion::new(2),
1448 supported_major: PortableRecordingFormatMajorVersion::new(1),
1449 },
1450 "different major version should be incompatible"
1451 );
1452 insta::assert_snapshot!(error.to_string(), @"major version 2 differs from supported version 1");
1453
1454 insta::assert_snapshot!(portable_version(1, 2).to_string(), @"1.2");
1455 }
1456
1457 #[test]
1458 fn test_portable_version_serialization() {
1459 let version = portable_version(1, 0);
1461 let json = serde_json::to_string(&version).expect("serialization should succeed");
1462 insta::assert_snapshot!(json, @r#"{"major":1,"minor":0}"#);
1463
1464 let roundtripped: PortableRecordingFormatVersion =
1466 serde_json::from_str(&json).expect("deserialization should succeed");
1467 assert_eq!(roundtripped, version);
1468 }
1469
1470 #[test]
1471 fn test_portable_manifest_format_version() {
1472 assert_eq!(
1474 PORTABLE_RECORDING_FORMAT_VERSION,
1475 portable_version(1, 0),
1476 "current portable recording format version should be 1.0"
1477 );
1478 }
1479}