1use super::{
7 CompletedRunStats, ComponentSizes, RecordedRunInfo, RecordedRunStatus, RecordedSizes,
8 StressCompletedRunStats,
9};
10use camino::Utf8Path;
11use chrono::{DateTime, FixedOffset, Utc};
12use iddqd::{IdOrdItem, IdOrdMap, id_upcast};
13use nextest_metadata::{RustBinaryId, TestCaseName};
14use quick_junit::ReportUuid;
15use semver::Version;
16use serde::{Deserialize, Serialize};
17use std::{
18 collections::{BTreeMap, BTreeSet},
19 fmt,
20 num::NonZero,
21};
22
23macro_rules! define_format_version {
31 (
32 $(#[$attr:meta])*
33 $vis:vis struct $name:ident;
34 ) => {
35 $(#[$attr])*
36 #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Deserialize, Serialize)]
37 #[serde(transparent)]
38 $vis struct $name(u32);
39
40 impl $name {
41 #[doc = concat!("Creates a new `", stringify!($name), "`.")]
42 pub const fn new(version: u32) -> Self {
43 Self(version)
44 }
45 }
46
47 impl fmt::Display for $name {
48 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
49 write!(f, "{}", self.0)
50 }
51 }
52 };
53
54 (
55 @default
56 $(#[$attr:meta])*
57 $vis:vis struct $name:ident;
58 ) => {
59 $(#[$attr])*
60 #[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Deserialize, Serialize)]
61 #[serde(transparent)]
62 $vis struct $name(u32);
63
64 impl $name {
65 #[doc = concat!("Creates a new `", stringify!($name), "`.")]
66 pub const fn new(version: u32) -> Self {
67 Self(version)
68 }
69 }
70
71 impl fmt::Display for $name {
72 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
73 write!(f, "{}", self.0)
74 }
75 }
76 };
77}
78
79define_format_version! {
80 pub struct RunsJsonFormatVersion;
87}
88
89define_format_version! {
90 pub struct StoreFormatMajorVersion;
93}
94
95define_format_version! {
96 @default
97 pub struct StoreFormatMinorVersion;
99}
100
101#[derive(Clone, Copy, Debug, PartialEq, Eq)]
103pub struct StoreFormatVersion {
104 pub major: StoreFormatMajorVersion,
106 pub minor: StoreFormatMinorVersion,
108}
109
110impl StoreFormatVersion {
111 pub const fn new(major: StoreFormatMajorVersion, minor: StoreFormatMinorVersion) -> Self {
113 Self { major, minor }
114 }
115
116 pub fn check_readable_by(self, supported: Self) -> Result<(), StoreVersionIncompatibility> {
119 if self.major != supported.major {
120 return Err(StoreVersionIncompatibility::MajorMismatch {
121 archive_major: self.major,
122 supported_major: supported.major,
123 });
124 }
125 if self.minor > supported.minor {
126 return Err(StoreVersionIncompatibility::MinorTooNew {
127 archive_minor: self.minor,
128 supported_minor: supported.minor,
129 });
130 }
131 Ok(())
132 }
133}
134
135impl fmt::Display for StoreFormatVersion {
136 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
137 write!(f, "{}.{}", self.major, self.minor)
138 }
139}
140
141#[derive(Clone, Copy, Debug, PartialEq, Eq)]
144pub enum StoreVersionIncompatibility {
145 MajorMismatch {
147 archive_major: StoreFormatMajorVersion,
149 supported_major: StoreFormatMajorVersion,
151 },
152 MinorTooNew {
154 archive_minor: StoreFormatMinorVersion,
156 supported_minor: StoreFormatMinorVersion,
158 },
159}
160
161impl fmt::Display for StoreVersionIncompatibility {
162 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
163 match self {
164 Self::MajorMismatch {
165 archive_major,
166 supported_major,
167 } => {
168 write!(
169 f,
170 "major version {} differs from supported version {}",
171 archive_major, supported_major
172 )
173 }
174 Self::MinorTooNew {
175 archive_minor,
176 supported_minor,
177 } => {
178 write!(
179 f,
180 "minor version {} is newer than supported version {}",
181 archive_minor, supported_minor
182 )
183 }
184 }
185 }
186}
187
188pub(super) const RUNS_JSON_FORMAT_VERSION: RunsJsonFormatVersion = RunsJsonFormatVersion::new(2);
194
195pub const STORE_FORMAT_VERSION: StoreFormatVersion = StoreFormatVersion::new(
201 StoreFormatMajorVersion::new(1),
202 StoreFormatMinorVersion::new(0),
203);
204
205#[derive(Debug, Clone, Copy, PartialEq, Eq)]
207pub enum RunsJsonWritePermission {
208 Allowed,
210 Denied {
212 file_version: RunsJsonFormatVersion,
214 max_supported_version: RunsJsonFormatVersion,
216 },
217}
218
219#[derive(Debug, Deserialize, Serialize)]
221#[serde(rename_all = "kebab-case")]
222pub(super) struct RecordedRunList {
223 pub(super) format_version: RunsJsonFormatVersion,
225
226 #[serde(default, skip_serializing_if = "Option::is_none")]
231 pub(super) last_pruned_at: Option<DateTime<Utc>>,
232
233 #[serde(default)]
235 pub(super) runs: Vec<RecordedRun>,
236}
237
238pub(super) struct RunListData {
240 pub(super) runs: Vec<RecordedRunInfo>,
241 pub(super) last_pruned_at: Option<DateTime<Utc>>,
242}
243
244impl RecordedRunList {
245 #[cfg(test)]
247 fn new() -> Self {
248 Self {
249 format_version: RUNS_JSON_FORMAT_VERSION,
250 last_pruned_at: None,
251 runs: Vec::new(),
252 }
253 }
254
255 pub(super) fn into_data(self) -> RunListData {
257 RunListData {
258 runs: self.runs.into_iter().map(RecordedRunInfo::from).collect(),
259 last_pruned_at: self.last_pruned_at,
260 }
261 }
262
263 pub(super) fn from_data(
268 runs: &[RecordedRunInfo],
269 last_pruned_at: Option<DateTime<Utc>>,
270 ) -> Self {
271 Self {
272 format_version: RUNS_JSON_FORMAT_VERSION,
273 last_pruned_at,
274 runs: runs.iter().map(RecordedRun::from).collect(),
275 }
276 }
277
278 pub(super) fn write_permission(&self) -> RunsJsonWritePermission {
283 if self.format_version > RUNS_JSON_FORMAT_VERSION {
284 RunsJsonWritePermission::Denied {
285 file_version: self.format_version,
286 max_supported_version: RUNS_JSON_FORMAT_VERSION,
287 }
288 } else {
289 RunsJsonWritePermission::Allowed
290 }
291 }
292}
293
294#[derive(Clone, Debug, Deserialize, Serialize)]
296#[serde(rename_all = "kebab-case")]
297pub(super) struct RecordedRun {
298 pub(super) run_id: ReportUuid,
300 pub(super) store_format_version: StoreFormatMajorVersion,
305 #[serde(default)]
310 pub(super) store_format_minor_version: StoreFormatMinorVersion,
311 pub(super) nextest_version: Version,
313 pub(super) started_at: DateTime<FixedOffset>,
315 pub(super) last_written_at: DateTime<FixedOffset>,
321 #[serde(default, skip_serializing_if = "Option::is_none")]
323 pub(super) duration_secs: Option<f64>,
324 #[serde(default)]
326 pub(super) cli_args: Vec<String>,
327 #[serde(default)]
332 pub(super) build_scope_args: Vec<String>,
333 #[serde(default)]
337 pub(super) env_vars: BTreeMap<String, String>,
338 #[serde(default)]
340 pub(super) parent_run_id: Option<ReportUuid>,
341 pub(super) sizes: RecordedSizesFormat,
345 pub(super) status: RecordedRunStatusFormat,
347}
348
349#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Deserialize, Serialize)]
351#[serde(rename_all = "kebab-case")]
352pub(super) struct RecordedSizesFormat {
353 pub(super) log: ComponentSizesFormat,
355 pub(super) store: ComponentSizesFormat,
357}
358
359#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Deserialize, Serialize)]
361#[serde(rename_all = "kebab-case")]
362pub(super) struct ComponentSizesFormat {
363 pub(super) compressed: u64,
365 pub(super) uncompressed: u64,
367 #[serde(default)]
369 pub(super) entries: u64,
370}
371
372impl From<RecordedSizes> for RecordedSizesFormat {
373 fn from(sizes: RecordedSizes) -> Self {
374 Self {
375 log: ComponentSizesFormat {
376 compressed: sizes.log.compressed,
377 uncompressed: sizes.log.uncompressed,
378 entries: sizes.log.entries,
379 },
380 store: ComponentSizesFormat {
381 compressed: sizes.store.compressed,
382 uncompressed: sizes.store.uncompressed,
383 entries: sizes.store.entries,
384 },
385 }
386 }
387}
388
389impl From<RecordedSizesFormat> for RecordedSizes {
390 fn from(sizes: RecordedSizesFormat) -> Self {
391 Self {
392 log: ComponentSizes {
393 compressed: sizes.log.compressed,
394 uncompressed: sizes.log.uncompressed,
395 entries: sizes.log.entries,
396 },
397 store: ComponentSizes {
398 compressed: sizes.store.compressed,
399 uncompressed: sizes.store.uncompressed,
400 entries: sizes.store.entries,
401 },
402 }
403 }
404}
405
406#[derive(Clone, Debug, Deserialize, Serialize)]
408#[serde(tag = "status", rename_all = "kebab-case")]
409pub(super) enum RecordedRunStatusFormat {
410 Incomplete,
412 #[serde(rename_all = "kebab-case")]
414 Completed {
415 initial_run_count: usize,
417 passed: usize,
419 failed: usize,
421 exit_code: i32,
423 },
424 #[serde(rename_all = "kebab-case")]
426 Cancelled {
427 initial_run_count: usize,
429 passed: usize,
431 failed: usize,
433 exit_code: i32,
435 },
436 #[serde(rename_all = "kebab-case")]
438 StressCompleted {
439 initial_iteration_count: Option<NonZero<u32>>,
441 success_count: u32,
443 failed_count: u32,
445 exit_code: i32,
447 },
448 #[serde(rename_all = "kebab-case")]
450 StressCancelled {
451 initial_iteration_count: Option<NonZero<u32>>,
453 success_count: u32,
455 failed_count: u32,
457 exit_code: i32,
459 },
460 #[serde(other)]
465 Unknown,
466}
467
468impl From<RecordedRun> for RecordedRunInfo {
469 fn from(run: RecordedRun) -> Self {
470 Self {
471 run_id: run.run_id,
472 store_format_version: StoreFormatVersion::new(
473 run.store_format_version,
474 run.store_format_minor_version,
475 ),
476 nextest_version: run.nextest_version,
477 started_at: run.started_at,
478 last_written_at: run.last_written_at,
479 duration_secs: run.duration_secs,
480 cli_args: run.cli_args,
481 build_scope_args: run.build_scope_args,
482 env_vars: run.env_vars,
483 parent_run_id: run.parent_run_id,
484 sizes: run.sizes.into(),
485 status: run.status.into(),
486 }
487 }
488}
489
490impl From<&RecordedRunInfo> for RecordedRun {
491 fn from(run: &RecordedRunInfo) -> Self {
492 Self {
493 run_id: run.run_id,
494 store_format_version: run.store_format_version.major,
495 store_format_minor_version: run.store_format_version.minor,
496 nextest_version: run.nextest_version.clone(),
497 started_at: run.started_at,
498 last_written_at: run.last_written_at,
499 duration_secs: run.duration_secs,
500 cli_args: run.cli_args.clone(),
501 build_scope_args: run.build_scope_args.clone(),
502 env_vars: run.env_vars.clone(),
503 parent_run_id: run.parent_run_id,
504 sizes: run.sizes.into(),
505 status: (&run.status).into(),
506 }
507 }
508}
509
510impl From<RecordedRunStatusFormat> for RecordedRunStatus {
511 fn from(status: RecordedRunStatusFormat) -> Self {
512 match status {
513 RecordedRunStatusFormat::Incomplete => Self::Incomplete,
514 RecordedRunStatusFormat::Unknown => Self::Unknown,
515 RecordedRunStatusFormat::Completed {
516 initial_run_count,
517 passed,
518 failed,
519 exit_code,
520 } => Self::Completed(CompletedRunStats {
521 initial_run_count,
522 passed,
523 failed,
524 exit_code,
525 }),
526 RecordedRunStatusFormat::Cancelled {
527 initial_run_count,
528 passed,
529 failed,
530 exit_code,
531 } => Self::Cancelled(CompletedRunStats {
532 initial_run_count,
533 passed,
534 failed,
535 exit_code,
536 }),
537 RecordedRunStatusFormat::StressCompleted {
538 initial_iteration_count,
539 success_count,
540 failed_count,
541 exit_code,
542 } => Self::StressCompleted(StressCompletedRunStats {
543 initial_iteration_count,
544 success_count,
545 failed_count,
546 exit_code,
547 }),
548 RecordedRunStatusFormat::StressCancelled {
549 initial_iteration_count,
550 success_count,
551 failed_count,
552 exit_code,
553 } => Self::StressCancelled(StressCompletedRunStats {
554 initial_iteration_count,
555 success_count,
556 failed_count,
557 exit_code,
558 }),
559 }
560 }
561}
562
563impl From<&RecordedRunStatus> for RecordedRunStatusFormat {
564 fn from(status: &RecordedRunStatus) -> Self {
565 match status {
566 RecordedRunStatus::Incomplete => Self::Incomplete,
567 RecordedRunStatus::Unknown => Self::Unknown,
568 RecordedRunStatus::Completed(stats) => Self::Completed {
569 initial_run_count: stats.initial_run_count,
570 passed: stats.passed,
571 failed: stats.failed,
572 exit_code: stats.exit_code,
573 },
574 RecordedRunStatus::Cancelled(stats) => Self::Cancelled {
575 initial_run_count: stats.initial_run_count,
576 passed: stats.passed,
577 failed: stats.failed,
578 exit_code: stats.exit_code,
579 },
580 RecordedRunStatus::StressCompleted(stats) => Self::StressCompleted {
581 initial_iteration_count: stats.initial_iteration_count,
582 success_count: stats.success_count,
583 failed_count: stats.failed_count,
584 exit_code: stats.exit_code,
585 },
586 RecordedRunStatus::StressCancelled(stats) => Self::StressCancelled {
587 initial_iteration_count: stats.initial_iteration_count,
588 success_count: stats.success_count,
589 failed_count: stats.failed_count,
590 exit_code: stats.exit_code,
591 },
592 }
593 }
594}
595
596#[derive(Clone, Debug, Deserialize, Serialize)]
604#[serde(rename_all = "kebab-case")]
605pub struct RerunInfo {
606 pub parent_run_id: ReportUuid,
608
609 pub root_info: RerunRootInfo,
611
612 pub test_suites: IdOrdMap<RerunTestSuiteInfo>,
614}
615
616#[derive(Clone, Debug, Deserialize, Serialize)]
618#[serde(rename_all = "kebab-case")]
619pub struct RerunRootInfo {
620 pub run_id: ReportUuid,
622
623 pub build_scope_args: Vec<String>,
625}
626
627impl RerunRootInfo {
628 pub fn new(run_id: ReportUuid, build_scope_args: Vec<String>) -> Self {
634 Self {
635 run_id,
636 build_scope_args,
637 }
638 }
639}
640
641#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
643pub struct RerunTestSuiteInfo {
644 pub binary_id: RustBinaryId,
646
647 pub passing: BTreeSet<TestCaseName>,
649
650 pub outstanding: BTreeSet<TestCaseName>,
652}
653
654impl RerunTestSuiteInfo {
655 pub(super) fn new(binary_id: RustBinaryId) -> Self {
656 Self {
657 binary_id,
658 passing: BTreeSet::new(),
659 outstanding: BTreeSet::new(),
660 }
661 }
662}
663
664impl IdOrdItem for RerunTestSuiteInfo {
665 type Key<'a> = &'a RustBinaryId;
666 fn key(&self) -> Self::Key<'_> {
667 &self.binary_id
668 }
669 id_upcast!();
670}
671
672pub static STORE_ZIP_FILE_NAME: &str = "store.zip";
678
679pub static RUN_LOG_FILE_NAME: &str = "run.log.zst";
681
682pub fn has_zip_extension(path: &Utf8Path) -> bool {
684 path.extension()
685 .is_some_and(|ext| ext.eq_ignore_ascii_case("zip"))
686}
687
688pub static CARGO_METADATA_JSON_PATH: &str = "meta/cargo-metadata.json";
691pub static TEST_LIST_JSON_PATH: &str = "meta/test-list.json";
693pub static RECORD_OPTS_JSON_PATH: &str = "meta/record-opts.json";
695pub static RERUN_INFO_JSON_PATH: &str = "meta/rerun-info.json";
697pub static STDOUT_DICT_PATH: &str = "meta/stdout.dict";
699pub static STDERR_DICT_PATH: &str = "meta/stderr.dict";
701
702define_format_version! {
707 pub struct PortableRecordingFormatMajorVersion;
709}
710
711define_format_version! {
712 @default
713 pub struct PortableRecordingFormatMinorVersion;
715}
716
717#[derive(Clone, Copy, Debug, PartialEq, Eq, Deserialize, Serialize)]
719pub struct PortableRecordingFormatVersion {
720 pub major: PortableRecordingFormatMajorVersion,
722 pub minor: PortableRecordingFormatMinorVersion,
724}
725
726impl PortableRecordingFormatVersion {
727 pub const fn new(
729 major: PortableRecordingFormatMajorVersion,
730 minor: PortableRecordingFormatMinorVersion,
731 ) -> Self {
732 Self { major, minor }
733 }
734
735 pub fn check_readable_by(
738 self,
739 supported: Self,
740 ) -> Result<(), PortableRecordingVersionIncompatibility> {
741 if self.major != supported.major {
742 return Err(PortableRecordingVersionIncompatibility::MajorMismatch {
743 archive_major: self.major,
744 supported_major: supported.major,
745 });
746 }
747 if self.minor > supported.minor {
748 return Err(PortableRecordingVersionIncompatibility::MinorTooNew {
749 archive_minor: self.minor,
750 supported_minor: supported.minor,
751 });
752 }
753 Ok(())
754 }
755}
756
757impl fmt::Display for PortableRecordingFormatVersion {
758 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
759 write!(f, "{}.{}", self.major, self.minor)
760 }
761}
762
763#[derive(Clone, Copy, Debug, PartialEq, Eq)]
766pub enum PortableRecordingVersionIncompatibility {
767 MajorMismatch {
769 archive_major: PortableRecordingFormatMajorVersion,
771 supported_major: PortableRecordingFormatMajorVersion,
773 },
774 MinorTooNew {
776 archive_minor: PortableRecordingFormatMinorVersion,
778 supported_minor: PortableRecordingFormatMinorVersion,
780 },
781}
782
783impl fmt::Display for PortableRecordingVersionIncompatibility {
784 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
785 match self {
786 Self::MajorMismatch {
787 archive_major,
788 supported_major,
789 } => {
790 write!(
791 f,
792 "major version {} differs from supported version {}",
793 archive_major, supported_major
794 )
795 }
796 Self::MinorTooNew {
797 archive_minor,
798 supported_minor,
799 } => {
800 write!(
801 f,
802 "minor version {} is newer than supported version {}",
803 archive_minor, supported_minor
804 )
805 }
806 }
807 }
808}
809
810pub const PORTABLE_RECORDING_FORMAT_VERSION: PortableRecordingFormatVersion =
812 PortableRecordingFormatVersion::new(
813 PortableRecordingFormatMajorVersion::new(1),
814 PortableRecordingFormatMinorVersion::new(0),
815 );
816
817pub static PORTABLE_MANIFEST_FILE_NAME: &str = "manifest.json";
819
820#[derive(Debug, Deserialize, Serialize)]
825#[serde(rename_all = "kebab-case")]
826pub(crate) struct PortableManifest {
827 pub(crate) format_version: PortableRecordingFormatVersion,
829 pub(super) run: RecordedRun,
831}
832
833impl PortableManifest {
834 pub(crate) fn new(run: &RecordedRunInfo) -> Self {
836 Self {
837 format_version: PORTABLE_RECORDING_FORMAT_VERSION,
838 run: RecordedRun::from(run),
839 }
840 }
841
842 pub(crate) fn run_info(&self) -> RecordedRunInfo {
844 RecordedRunInfo::from(self.run.clone())
845 }
846
847 pub(crate) fn store_format_version(&self) -> StoreFormatVersion {
849 StoreFormatVersion::new(
850 self.run.store_format_version,
851 self.run.store_format_minor_version,
852 )
853 }
854}
855
856#[derive(Clone, Copy, Debug, PartialEq, Eq)]
858pub enum OutputDict {
859 Stdout,
861 Stderr,
863 None,
865}
866
867impl OutputDict {
868 pub fn for_path(path: &Utf8Path) -> Self {
876 let mut iter = path.iter();
877 let Some(first_component) = iter.next() else {
878 return Self::None;
879 };
880 if first_component != "out" {
882 return Self::None;
883 }
884
885 Self::for_output_file_name(iter.as_path().as_str())
886 }
887
888 pub fn for_output_file_name(file_name: &str) -> Self {
893 if file_name.ends_with("-stdout") || file_name.ends_with("-combined") {
894 Self::Stdout
895 } else if file_name.ends_with("-stderr") {
896 Self::Stderr
897 } else {
898 Self::None
900 }
901 }
902
903 pub fn dict_bytes(self) -> Option<&'static [u8]> {
907 match self {
908 Self::Stdout => Some(super::dicts::STDOUT),
909 Self::Stderr => Some(super::dicts::STDERR),
910 Self::None => None,
911 }
912 }
913}
914
915#[cfg(test)]
916mod tests {
917 use super::*;
918
919 #[test]
920 fn test_output_dict_for_path() {
921 assert_eq!(
923 OutputDict::for_path("meta/cargo-metadata.json".as_ref()),
924 OutputDict::None
925 );
926 assert_eq!(
927 OutputDict::for_path("meta/test-list.json".as_ref()),
928 OutputDict::None
929 );
930
931 assert_eq!(
933 OutputDict::for_path("out/0123456789abcdef-stdout".as_ref()),
934 OutputDict::Stdout
935 );
936 assert_eq!(
937 OutputDict::for_path("out/0123456789abcdef-stderr".as_ref()),
938 OutputDict::Stderr
939 );
940 assert_eq!(
941 OutputDict::for_path("out/0123456789abcdef-combined".as_ref()),
942 OutputDict::Stdout
943 );
944 }
945
946 #[test]
947 fn test_output_dict_for_output_file_name() {
948 assert_eq!(
950 OutputDict::for_output_file_name("0123456789abcdef-stdout"),
951 OutputDict::Stdout
952 );
953 assert_eq!(
954 OutputDict::for_output_file_name("0123456789abcdef-stderr"),
955 OutputDict::Stderr
956 );
957 assert_eq!(
958 OutputDict::for_output_file_name("0123456789abcdef-combined"),
959 OutputDict::Stdout
960 );
961 assert_eq!(
962 OutputDict::for_output_file_name("0123456789abcdef-unknown"),
963 OutputDict::None
964 );
965 }
966
967 #[test]
968 fn test_dict_bytes() {
969 assert!(OutputDict::Stdout.dict_bytes().is_some());
970 assert!(OutputDict::Stderr.dict_bytes().is_some());
971 assert!(OutputDict::None.dict_bytes().is_none());
972 }
973
974 #[test]
975 fn test_runs_json_missing_version() {
976 let json = r#"{"runs": []}"#;
978 let result: Result<RecordedRunList, _> = serde_json::from_str(json);
979 assert!(result.is_err(), "expected error for missing format-version");
980 }
981
982 #[test]
983 fn test_runs_json_current_version() {
984 let json = format!(
986 r#"{{"format-version": {}, "runs": []}}"#,
987 RUNS_JSON_FORMAT_VERSION
988 );
989 let list: RecordedRunList = serde_json::from_str(&json).expect("should deserialize");
990 assert_eq!(list.write_permission(), RunsJsonWritePermission::Allowed);
991 }
992
993 #[test]
994 fn test_runs_json_older_version() {
995 let json = r#"{"format-version": 1, "runs": []}"#;
999 let list: RecordedRunList = serde_json::from_str(json).expect("should deserialize");
1000 assert_eq!(list.write_permission(), RunsJsonWritePermission::Allowed);
1001 }
1002
1003 #[test]
1004 fn test_runs_json_newer_version() {
1005 let json = r#"{"format-version": 99, "runs": []}"#;
1007 let list: RecordedRunList = serde_json::from_str(json).expect("should deserialize");
1008 assert_eq!(
1009 list.write_permission(),
1010 RunsJsonWritePermission::Denied {
1011 file_version: RunsJsonFormatVersion::new(99),
1012 max_supported_version: RUNS_JSON_FORMAT_VERSION,
1013 }
1014 );
1015 }
1016
1017 #[test]
1018 fn test_runs_json_serialization_includes_version() {
1019 let list = RecordedRunList::from_data(&[], None);
1021 let json = serde_json::to_string(&list).expect("should serialize");
1022 assert!(
1023 json.contains("format-version"),
1024 "serialized runs.json.zst should include format-version"
1025 );
1026
1027 let parsed: serde_json::Value = serde_json::from_str(&json).expect("should parse");
1029 let version: RunsJsonFormatVersion =
1030 serde_json::from_value(parsed["format-version"].clone()).expect("valid version");
1031 assert_eq!(
1032 version, RUNS_JSON_FORMAT_VERSION,
1033 "format-version should be current version"
1034 );
1035 }
1036
1037 #[test]
1038 fn test_runs_json_new() {
1039 let list = RecordedRunList::new();
1041 assert_eq!(list.format_version, RUNS_JSON_FORMAT_VERSION);
1042 assert!(list.runs.is_empty());
1043 assert_eq!(list.write_permission(), RunsJsonWritePermission::Allowed);
1044 }
1045
1046 fn make_test_run(status: RecordedRunStatusFormat) -> RecordedRun {
1049 RecordedRun {
1050 run_id: ReportUuid::from_u128(0x550e8400_e29b_41d4_a716_446655440000),
1051 store_format_version: STORE_FORMAT_VERSION.major,
1052 store_format_minor_version: STORE_FORMAT_VERSION.minor,
1053 nextest_version: Version::new(0, 9, 111),
1054 started_at: DateTime::parse_from_rfc3339("2024-12-19T14:22:33-08:00")
1055 .expect("valid timestamp"),
1056 last_written_at: DateTime::parse_from_rfc3339("2024-12-19T22:22:33Z")
1057 .expect("valid timestamp"),
1058 duration_secs: Some(12.345),
1059 cli_args: vec![
1060 "cargo".to_owned(),
1061 "nextest".to_owned(),
1062 "run".to_owned(),
1063 "--workspace".to_owned(),
1064 ],
1065 build_scope_args: vec!["--workspace".to_owned()],
1066 env_vars: BTreeMap::from([
1067 ("CARGO_TERM_COLOR".to_owned(), "always".to_owned()),
1068 ("NEXTEST_PROFILE".to_owned(), "ci".to_owned()),
1069 ]),
1070 parent_run_id: Some(ReportUuid::from_u128(
1071 0x550e7400_e29b_41d4_a716_446655440000,
1072 )),
1073 sizes: RecordedSizesFormat {
1074 log: ComponentSizesFormat {
1075 compressed: 2345,
1076 uncompressed: 5678,
1077 entries: 42,
1078 },
1079 store: ComponentSizesFormat {
1080 compressed: 10000,
1081 uncompressed: 40000,
1082 entries: 15,
1083 },
1084 },
1085 status,
1086 }
1087 }
1088
1089 #[test]
1090 fn test_recorded_run_serialize_incomplete() {
1091 let run = make_test_run(RecordedRunStatusFormat::Incomplete);
1092 let json = serde_json::to_string_pretty(&run).expect("serialization should succeed");
1093 insta::assert_snapshot!(json);
1094 }
1095
1096 #[test]
1097 fn test_recorded_run_serialize_completed() {
1098 let run = make_test_run(RecordedRunStatusFormat::Completed {
1099 initial_run_count: 100,
1100 passed: 95,
1101 failed: 5,
1102 exit_code: 0,
1103 });
1104 let json = serde_json::to_string_pretty(&run).expect("serialization should succeed");
1105 insta::assert_snapshot!(json);
1106 }
1107
1108 #[test]
1109 fn test_recorded_run_serialize_cancelled() {
1110 let run = make_test_run(RecordedRunStatusFormat::Cancelled {
1111 initial_run_count: 100,
1112 passed: 45,
1113 failed: 5,
1114 exit_code: 100,
1115 });
1116 let json = serde_json::to_string_pretty(&run).expect("serialization should succeed");
1117 insta::assert_snapshot!(json);
1118 }
1119
1120 #[test]
1121 fn test_recorded_run_serialize_stress_completed() {
1122 let run = make_test_run(RecordedRunStatusFormat::StressCompleted {
1123 initial_iteration_count: NonZero::new(100),
1124 success_count: 98,
1125 failed_count: 2,
1126 exit_code: 0,
1127 });
1128 let json = serde_json::to_string_pretty(&run).expect("serialization should succeed");
1129 insta::assert_snapshot!(json);
1130 }
1131
1132 #[test]
1133 fn test_recorded_run_serialize_stress_cancelled() {
1134 let run = make_test_run(RecordedRunStatusFormat::StressCancelled {
1135 initial_iteration_count: NonZero::new(100),
1136 success_count: 45,
1137 failed_count: 5,
1138 exit_code: 100,
1139 });
1140 let json = serde_json::to_string_pretty(&run).expect("serialization should succeed");
1141 insta::assert_snapshot!(json);
1142 }
1143
1144 #[test]
1145 fn test_recorded_run_deserialize_unknown_status() {
1146 let json = r#"{
1149 "run-id": "550e8400-e29b-41d4-a716-446655440000",
1150 "store-format-version": 999,
1151 "nextest-version": "0.9.999",
1152 "started-at": "2024-12-19T14:22:33-08:00",
1153 "last-written-at": "2024-12-19T22:22:33Z",
1154 "cli-args": ["cargo", "nextest", "run"],
1155 "env-vars": {},
1156 "sizes": {
1157 "log": { "compressed": 2345, "uncompressed": 5678 },
1158 "store": { "compressed": 10000, "uncompressed": 40000 }
1159 },
1160 "status": {
1161 "status": "super-new-status",
1162 "some-future-field": 42
1163 }
1164 }"#;
1165 let run: RecordedRun = serde_json::from_str(json).expect("should deserialize");
1166 assert!(
1167 matches!(run.status, RecordedRunStatusFormat::Unknown),
1168 "unknown status should deserialize to Unknown variant"
1169 );
1170
1171 let info: RecordedRunInfo = run.into();
1173 assert!(
1174 matches!(info.status, RecordedRunStatus::Unknown),
1175 "Unknown format should convert to Unknown domain type"
1176 );
1177 }
1178
1179 #[test]
1180 fn test_recorded_run_roundtrip() {
1181 let original = make_test_run(RecordedRunStatusFormat::Completed {
1182 initial_run_count: 100,
1183 passed: 95,
1184 failed: 5,
1185 exit_code: 0,
1186 });
1187 let json = serde_json::to_string(&original).expect("serialization should succeed");
1188 let roundtripped: RecordedRun =
1189 serde_json::from_str(&json).expect("deserialization should succeed");
1190
1191 assert_eq!(roundtripped.run_id, original.run_id);
1192 assert_eq!(roundtripped.nextest_version, original.nextest_version);
1193 assert_eq!(roundtripped.started_at, original.started_at);
1194 assert_eq!(roundtripped.sizes, original.sizes);
1195
1196 let info: RecordedRunInfo = roundtripped.into();
1198 match info.status {
1199 RecordedRunStatus::Completed(stats) => {
1200 assert_eq!(stats.initial_run_count, 100);
1201 assert_eq!(stats.passed, 95);
1202 assert_eq!(stats.failed, 5);
1203 }
1204 _ => panic!("expected Completed variant"),
1205 }
1206 }
1207
1208 fn version(major: u32, minor: u32) -> StoreFormatVersion {
1212 StoreFormatVersion::new(
1213 StoreFormatMajorVersion::new(major),
1214 StoreFormatMinorVersion::new(minor),
1215 )
1216 }
1217
1218 #[test]
1219 fn test_store_version_compatibility() {
1220 assert!(
1221 version(1, 0).check_readable_by(version(1, 0)).is_ok(),
1222 "same version should be compatible"
1223 );
1224
1225 assert!(
1226 version(1, 0).check_readable_by(version(1, 2)).is_ok(),
1227 "older minor version should be compatible"
1228 );
1229
1230 let error = version(1, 3).check_readable_by(version(1, 2)).unwrap_err();
1231 assert_eq!(
1232 error,
1233 StoreVersionIncompatibility::MinorTooNew {
1234 archive_minor: StoreFormatMinorVersion::new(3),
1235 supported_minor: StoreFormatMinorVersion::new(2),
1236 },
1237 "newer minor version should be incompatible"
1238 );
1239 insta::assert_snapshot!(error.to_string(), @"minor version 3 is newer than supported version 2");
1240
1241 let error = version(2, 0).check_readable_by(version(1, 5)).unwrap_err();
1242 assert_eq!(
1243 error,
1244 StoreVersionIncompatibility::MajorMismatch {
1245 archive_major: StoreFormatMajorVersion::new(2),
1246 supported_major: StoreFormatMajorVersion::new(1),
1247 },
1248 "different major version should be incompatible"
1249 );
1250 insta::assert_snapshot!(error.to_string(), @"major version 2 differs from supported version 1");
1251
1252 insta::assert_snapshot!(version(1, 2).to_string(), @"1.2");
1253 }
1254
1255 #[test]
1256 fn test_recorded_run_deserialize_without_minor_version() {
1257 let json = r#"{
1259 "run-id": "550e8400-e29b-41d4-a716-446655440000",
1260 "store-format-version": 1,
1261 "nextest-version": "0.9.111",
1262 "started-at": "2024-12-19T14:22:33-08:00",
1263 "last-written-at": "2024-12-19T22:22:33Z",
1264 "cli-args": [],
1265 "env-vars": {},
1266 "sizes": {
1267 "log": { "compressed": 0, "uncompressed": 0 },
1268 "store": { "compressed": 0, "uncompressed": 0 }
1269 },
1270 "status": { "status": "incomplete" }
1271 }"#;
1272 let run: RecordedRun = serde_json::from_str(json).expect("should deserialize");
1273 assert_eq!(run.store_format_version, StoreFormatMajorVersion::new(1));
1274 assert_eq!(
1275 run.store_format_minor_version,
1276 StoreFormatMinorVersion::new(0)
1277 );
1278
1279 let info: RecordedRunInfo = run.into();
1281 assert_eq!(info.store_format_version, version(1, 0));
1282 }
1283
1284 #[test]
1285 fn test_recorded_run_serialize_includes_minor_version() {
1286 let run = make_test_run(RecordedRunStatusFormat::Incomplete);
1288 let json = serde_json::to_string_pretty(&run).expect("serialization should succeed");
1289 assert!(
1290 json.contains("store-format-minor-version"),
1291 "serialized run should include store-format-minor-version"
1292 );
1293 }
1294
1295 fn portable_version(major: u32, minor: u32) -> PortableRecordingFormatVersion {
1299 PortableRecordingFormatVersion::new(
1300 PortableRecordingFormatMajorVersion::new(major),
1301 PortableRecordingFormatMinorVersion::new(minor),
1302 )
1303 }
1304
1305 #[test]
1306 fn test_portable_version_compatibility() {
1307 assert!(
1308 portable_version(1, 0)
1309 .check_readable_by(portable_version(1, 0))
1310 .is_ok(),
1311 "same version should be compatible"
1312 );
1313
1314 assert!(
1315 portable_version(1, 0)
1316 .check_readable_by(portable_version(1, 2))
1317 .is_ok(),
1318 "older minor version should be compatible"
1319 );
1320
1321 let error = portable_version(1, 3)
1322 .check_readable_by(portable_version(1, 2))
1323 .unwrap_err();
1324 assert_eq!(
1325 error,
1326 PortableRecordingVersionIncompatibility::MinorTooNew {
1327 archive_minor: PortableRecordingFormatMinorVersion::new(3),
1328 supported_minor: PortableRecordingFormatMinorVersion::new(2),
1329 },
1330 "newer minor version should be incompatible"
1331 );
1332 insta::assert_snapshot!(error.to_string(), @"minor version 3 is newer than supported version 2");
1333
1334 let error = portable_version(2, 0)
1335 .check_readable_by(portable_version(1, 5))
1336 .unwrap_err();
1337 assert_eq!(
1338 error,
1339 PortableRecordingVersionIncompatibility::MajorMismatch {
1340 archive_major: PortableRecordingFormatMajorVersion::new(2),
1341 supported_major: PortableRecordingFormatMajorVersion::new(1),
1342 },
1343 "different major version should be incompatible"
1344 );
1345 insta::assert_snapshot!(error.to_string(), @"major version 2 differs from supported version 1");
1346
1347 insta::assert_snapshot!(portable_version(1, 2).to_string(), @"1.2");
1348 }
1349
1350 #[test]
1351 fn test_portable_version_serialization() {
1352 let version = portable_version(1, 0);
1354 let json = serde_json::to_string(&version).expect("serialization should succeed");
1355 insta::assert_snapshot!(json, @r#"{"major":1,"minor":0}"#);
1356
1357 let roundtripped: PortableRecordingFormatVersion =
1359 serde_json::from_str(&json).expect("deserialization should succeed");
1360 assert_eq!(roundtripped, version);
1361 }
1362
1363 #[test]
1364 fn test_portable_manifest_format_version() {
1365 assert_eq!(
1367 PORTABLE_RECORDING_FORMAT_VERSION,
1368 portable_version(1, 0),
1369 "current portable recording format version should be 1.0"
1370 );
1371 }
1372}