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::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#[derive(Clone, Copy, Debug, PartialEq, Eq)]
145pub enum StoreVersionIncompatibility {
146 MajorMismatch {
148 archive_major: StoreFormatMajorVersion,
150 supported_major: StoreFormatMajorVersion,
152 },
153 MinorTooNew {
155 archive_minor: StoreFormatMinorVersion,
157 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
189pub(super) const RUNS_JSON_FORMAT_VERSION: RunsJsonFormatVersion = RunsJsonFormatVersion::new(2);
195
196pub const STORE_FORMAT_VERSION: StoreFormatVersion = StoreFormatVersion::new(
202 StoreFormatMajorVersion::new(1),
203 StoreFormatMinorVersion::new(0),
204);
205
206#[derive(Debug, Clone, Copy, PartialEq, Eq)]
208pub enum RunsJsonWritePermission {
209 Allowed,
211 Denied {
213 file_version: RunsJsonFormatVersion,
215 max_supported_version: RunsJsonFormatVersion,
217 },
218}
219
220#[derive(Debug, Deserialize, Serialize)]
222#[serde(rename_all = "kebab-case")]
223pub(super) struct RecordedRunList {
224 pub(super) format_version: RunsJsonFormatVersion,
226
227 #[serde(default, skip_serializing_if = "Option::is_none")]
232 pub(super) last_pruned_at: Option<DateTime<Utc>>,
233
234 #[serde(default)]
236 pub(super) runs: Vec<RecordedRun>,
237}
238
239pub(super) struct RunListData {
241 pub(super) runs: Vec<RecordedRunInfo>,
242 pub(super) last_pruned_at: Option<DateTime<Utc>>,
243}
244
245impl RecordedRunList {
246 #[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 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 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 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#[derive(Clone, Debug, Deserialize, Serialize)]
297#[serde(rename_all = "kebab-case")]
298pub(super) struct RecordedRun {
299 pub(super) run_id: ReportUuid,
301 pub(super) store_format_version: StoreFormatMajorVersion,
306 #[serde(default)]
311 pub(super) store_format_minor_version: StoreFormatMinorVersion,
312 pub(super) nextest_version: Version,
314 pub(super) started_at: DateTime<FixedOffset>,
316 pub(super) last_written_at: DateTime<FixedOffset>,
322 #[serde(default, skip_serializing_if = "Option::is_none")]
324 pub(super) duration_secs: Option<f64>,
325 #[serde(default)]
327 pub(super) cli_args: Vec<String>,
328 #[serde(default)]
333 pub(super) build_scope_args: Vec<String>,
334 #[serde(default)]
338 pub(super) env_vars: BTreeMap<String, String>,
339 #[serde(default)]
341 pub(super) parent_run_id: Option<ReportUuid>,
342 pub(super) sizes: RecordedSizesFormat,
346 pub(super) status: RecordedRunStatusFormat,
348}
349
350#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Deserialize, Serialize)]
352#[serde(rename_all = "kebab-case")]
353pub(super) struct RecordedSizesFormat {
354 pub(super) log: ComponentSizesFormat,
356 pub(super) store: ComponentSizesFormat,
358}
359
360#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Deserialize, Serialize)]
362#[serde(rename_all = "kebab-case")]
363pub(super) struct ComponentSizesFormat {
364 pub(super) compressed: u64,
366 pub(super) uncompressed: u64,
368 #[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#[derive(Clone, Debug, Deserialize, Serialize)]
409#[serde(tag = "status", rename_all = "kebab-case")]
410pub(super) enum RecordedRunStatusFormat {
411 Incomplete,
413 #[serde(rename_all = "kebab-case")]
415 Completed {
416 initial_run_count: usize,
418 passed: usize,
420 failed: usize,
422 exit_code: i32,
424 },
425 #[serde(rename_all = "kebab-case")]
427 Cancelled {
428 initial_run_count: usize,
430 passed: usize,
432 failed: usize,
434 exit_code: i32,
436 },
437 #[serde(rename_all = "kebab-case")]
439 StressCompleted {
440 initial_iteration_count: Option<NonZero<u32>>,
442 success_count: u32,
444 failed_count: u32,
446 exit_code: i32,
448 },
449 #[serde(rename_all = "kebab-case")]
451 StressCancelled {
452 initial_iteration_count: Option<NonZero<u32>>,
454 success_count: u32,
456 failed_count: u32,
458 exit_code: i32,
460 },
461 #[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#[derive(Clone, Debug, Deserialize, Serialize)]
605#[serde(rename_all = "kebab-case")]
606pub struct RerunInfo {
607 pub parent_run_id: ReportUuid,
609
610 pub root_info: RerunRootInfo,
612
613 pub test_suites: IdOrdMap<RerunTestSuiteInfo>,
615}
616
617#[derive(Clone, Debug, Deserialize, Serialize)]
619#[serde(rename_all = "kebab-case")]
620pub struct RerunRootInfo {
621 pub run_id: ReportUuid,
623
624 pub build_scope_args: Vec<String>,
626}
627
628impl RerunRootInfo {
629 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#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
644pub struct RerunTestSuiteInfo {
645 pub binary_id: RustBinaryId,
647
648 pub passing: BTreeSet<TestCaseName>,
650
651 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
673pub static STORE_ZIP_FILE_NAME: &str = "store.zip";
679
680pub static RUN_LOG_FILE_NAME: &str = "run.log.zst";
682
683pub fn has_zip_extension(path: &Utf8Path) -> bool {
685 path.extension()
686 .is_some_and(|ext| ext.eq_ignore_ascii_case("zip"))
687}
688
689pub static CARGO_METADATA_JSON_PATH: &str = "meta/cargo-metadata.json";
692pub static TEST_LIST_JSON_PATH: &str = "meta/test-list.json";
694pub static RECORD_OPTS_JSON_PATH: &str = "meta/record-opts.json";
696pub static RERUN_INFO_JSON_PATH: &str = "meta/rerun-info.json";
698pub static STDOUT_DICT_PATH: &str = "meta/stdout.dict";
700pub static STDERR_DICT_PATH: &str = "meta/stderr.dict";
702
703define_format_version! {
708 pub struct PortableRecordingFormatMajorVersion;
710}
711
712define_format_version! {
713 @default
714 pub struct PortableRecordingFormatMinorVersion;
716}
717
718#[derive(Clone, Copy, Debug, PartialEq, Eq, Deserialize, Serialize)]
720pub struct PortableRecordingFormatVersion {
721 pub major: PortableRecordingFormatMajorVersion,
723 pub minor: PortableRecordingFormatMinorVersion,
725}
726
727impl PortableRecordingFormatVersion {
728 pub const fn new(
730 major: PortableRecordingFormatMajorVersion,
731 minor: PortableRecordingFormatMinorVersion,
732 ) -> Self {
733 Self { major, minor }
734 }
735
736 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#[derive(Clone, Copy, Debug, PartialEq, Eq)]
767pub enum PortableRecordingVersionIncompatibility {
768 MajorMismatch {
770 archive_major: PortableRecordingFormatMajorVersion,
772 supported_major: PortableRecordingFormatMajorVersion,
774 },
775 MinorTooNew {
777 archive_minor: PortableRecordingFormatMinorVersion,
779 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
811pub const PORTABLE_RECORDING_FORMAT_VERSION: PortableRecordingFormatVersion =
813 PortableRecordingFormatVersion::new(
814 PortableRecordingFormatMajorVersion::new(1),
815 PortableRecordingFormatMinorVersion::new(0),
816 );
817
818pub static PORTABLE_MANIFEST_FILE_NAME: &str = "manifest.json";
820
821#[derive(Debug, Deserialize, Serialize)]
826#[serde(rename_all = "kebab-case")]
827pub(crate) struct PortableManifest {
828 pub(crate) format_version: PortableRecordingFormatVersion,
830 pub(super) run: RecordedRun,
832}
833
834impl PortableManifest {
835 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 pub(crate) fn run_info(&self) -> RecordedRunInfo {
845 RecordedRunInfo::from(self.run.clone())
846 }
847
848 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#[derive(Clone, Copy, Debug, PartialEq, Eq)]
859pub enum OutputDict {
860 Stdout,
862 Stderr,
864 None,
866}
867
868impl OutputDict {
869 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 if first_component != "out" {
883 return Self::None;
884 }
885
886 Self::for_output_file_name(iter.as_path().as_str())
887 }
888
889 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 Self::None
901 }
902 }
903
904 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
916pub(super) fn stored_file_options() -> FileOptions {
923 let mut options = FileOptions::default();
924 options.compression_method = CompressionMethod::STORE;
925 options
926}
927
928pub(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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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}