1use crate::trace::canonicalize::{TraceEventKey, canonicalize, trace_event_key, trace_fingerprint};
49use crate::trace::event::TraceEvent;
50use crate::trace::replay::ReplayEvent;
51use crate::trace::scoring::EvidenceEntry;
52use crate::types::{CancelKind, RegionId, TaskId, Time};
53use serde::{Deserialize, Serialize};
54use std::fmt;
55
56pub const CRASHPACK_SCHEMA_VERSION: u32 = 1;
64
65#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
75pub struct CrashPackConfig {
76 pub seed: u64,
78
79 pub config_hash: u64,
83
84 pub worker_count: usize,
86
87 #[serde(default, skip_serializing_if = "Option::is_none")]
89 pub max_steps: Option<u64>,
90
91 #[serde(default, skip_serializing_if = "Option::is_none")]
95 pub commit_hash: Option<String>,
96}
97
98impl Default for CrashPackConfig {
99 fn default() -> Self {
100 Self {
101 seed: 0,
102 config_hash: 0,
103 worker_count: 1,
104 max_steps: None,
105 commit_hash: None,
106 }
107 }
108}
109
110#[derive(Debug, Clone, Serialize)]
118pub struct FailureInfo {
119 pub task: TaskId,
121
122 pub region: RegionId,
124
125 pub outcome: FailureOutcome,
127
128 pub virtual_time: Time,
130}
131
132impl PartialEq for FailureInfo {
133 fn eq(&self, other: &Self) -> bool {
134 self.task == other.task
135 && self.region == other.region
136 && self.outcome == other.outcome
137 && self.virtual_time == other.virtual_time
138 }
139}
140
141impl Eq for FailureInfo {}
142
143#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
148pub enum FailureOutcome {
149 Err,
151 Cancelled {
153 cancel_kind: CancelKind,
155 },
156 Panicked {
158 message: String,
160 },
161}
162
163#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
165pub struct EvidenceEntrySnapshot {
166 pub birth: usize,
168 pub death: usize,
170 pub is_novel: bool,
172 pub persistence: Option<u64>,
174}
175
176impl From<EvidenceEntry> for EvidenceEntrySnapshot {
177 fn from(e: EvidenceEntry) -> Self {
178 Self {
179 birth: e.class.birth,
180 death: e.class.death,
181 is_novel: e.is_novel,
182 persistence: e.persistence,
183 }
184 }
185}
186
187#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
196pub struct SupervisionSnapshot {
197 pub virtual_time: Time,
199
200 pub task: TaskId,
202
203 pub region: RegionId,
205
206 pub decision: String,
208
209 pub context: Option<String>,
211}
212
213pub const MINIMUM_SUPPORTED_SCHEMA_VERSION: u32 = 1;
222
223#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
228#[serde(tag = "kind")]
229pub enum AttachmentKind {
230 CanonicalPrefix,
232 DivergentPrefix,
234 EvidenceLedger,
236 SupervisionLog,
238 OracleViolations,
240 Custom {
242 tag: String,
244 },
245}
246
247#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
252pub struct ManifestAttachment {
253 #[serde(flatten)]
255 pub kind: AttachmentKind,
256
257 pub item_count: u64,
259
260 #[serde(default, skip_serializing_if = "is_zero")]
262 pub size_hint_bytes: u64,
263}
264
265#[allow(clippy::trivially_copy_pass_by_ref)] fn is_zero(v: &u64) -> bool {
268 *v == 0
269}
270
271#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
286pub struct CrashPackManifest {
287 pub schema_version: u32,
289
290 pub config: CrashPackConfig,
292
293 pub fingerprint: u64,
298
299 pub event_count: u64,
301
302 pub created_at: u64,
304
305 #[serde(default, skip_serializing_if = "Vec::is_empty")]
310 pub attachments: Vec<ManifestAttachment>,
311}
312
313#[derive(Debug, Clone, PartialEq, Eq)]
315pub enum ManifestValidationError {
316 VersionTooNew {
318 manifest_version: u32,
320 supported_version: u32,
322 },
323 VersionTooOld {
325 manifest_version: u32,
327 minimum_version: u32,
329 },
330}
331
332impl std::fmt::Display for ManifestValidationError {
333 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
334 match self {
335 Self::VersionTooNew {
336 manifest_version,
337 supported_version,
338 } => write!(
339 f,
340 "crash pack schema v{manifest_version} is newer than supported v{supported_version}"
341 ),
342 Self::VersionTooOld {
343 manifest_version,
344 minimum_version,
345 } => write!(
346 f,
347 "crash pack schema v{manifest_version} is older than minimum v{minimum_version}"
348 ),
349 }
350 }
351}
352
353impl std::error::Error for ManifestValidationError {}
354
355impl CrashPackManifest {
356 #[must_use]
358 pub fn new(config: CrashPackConfig, fingerprint: u64, event_count: u64) -> Self {
359 Self {
360 schema_version: CRASHPACK_SCHEMA_VERSION,
361 config,
362 fingerprint,
363 event_count,
364 created_at: wall_clock_nanos(),
365 attachments: Vec::new(),
366 }
367 }
368
369 pub fn validate(&self) -> Result<(), ManifestValidationError> {
374 if self.schema_version > CRASHPACK_SCHEMA_VERSION {
375 return Err(ManifestValidationError::VersionTooNew {
376 manifest_version: self.schema_version,
377 supported_version: CRASHPACK_SCHEMA_VERSION,
378 });
379 }
380 if self.schema_version < MINIMUM_SUPPORTED_SCHEMA_VERSION {
381 return Err(ManifestValidationError::VersionTooOld {
382 manifest_version: self.schema_version,
383 minimum_version: MINIMUM_SUPPORTED_SCHEMA_VERSION,
384 });
385 }
386 Ok(())
387 }
388
389 #[must_use]
391 pub fn is_compatible(&self) -> bool {
392 self.validate().is_ok()
393 }
394
395 #[must_use]
397 pub fn attachment(&self, kind: &AttachmentKind) -> Option<&ManifestAttachment> {
398 self.attachments.iter().find(|a| &a.kind == kind)
399 }
400
401 #[must_use]
403 pub fn has_attachment(&self, kind: &AttachmentKind) -> bool {
404 self.attachment(kind).is_some()
405 }
406}
407
408#[derive(Debug, Clone, Serialize)]
432pub struct CrashPack {
433 pub manifest: CrashPackManifest,
435
436 pub failure: FailureInfo,
438
439 pub canonical_prefix: Vec<Vec<TraceEventKey>>,
444
445 pub divergent_prefix: Vec<ReplayEvent>,
451
452 pub evidence: Vec<EvidenceEntrySnapshot>,
457
458 pub supervision_log: Vec<SupervisionSnapshot>,
463
464 pub oracle_violations: Vec<String>,
468
469 #[serde(default, skip_serializing_if = "Option::is_none")]
474 pub replay: Option<ReplayCommand>,
475}
476
477impl PartialEq for CrashPack {
478 fn eq(&self, other: &Self) -> bool {
479 self.manifest.schema_version == other.manifest.schema_version
481 && self.manifest.config == other.manifest.config
482 && self.manifest.fingerprint == other.manifest.fingerprint
483 && self.manifest.event_count == other.manifest.event_count
484 && self.manifest.attachments == other.manifest.attachments
485 && self.failure == other.failure
486 && self.canonical_prefix == other.canonical_prefix
487 && self.divergent_prefix == other.divergent_prefix
488 && self.evidence == other.evidence
489 && self.supervision_log == other.supervision_log
490 && self.oracle_violations == other.oracle_violations
491 && self.replay == other.replay
492 }
493}
494
495impl Eq for CrashPack {}
496
497impl CrashPack {
498 #[must_use]
500 pub fn builder(config: CrashPackConfig) -> CrashPackBuilder {
501 CrashPackBuilder {
502 config,
503 failure: None,
504 fingerprint: 0,
505 event_count: 0,
506 canonical_prefix: Vec::new(),
507 divergent_prefix: Vec::new(),
508 evidence: Vec::new(),
509 supervision_log: Vec::new(),
510 oracle_violations: Vec::new(),
511 replay: None,
512 }
513 }
514
515 #[must_use]
520 pub fn replay_command(&self, artifact_path: Option<&str>) -> ReplayCommand {
521 ReplayCommand::from_config(&self.manifest.config, artifact_path)
522 }
523
524 #[must_use]
526 pub fn has_violations(&self) -> bool {
527 !self.oracle_violations.is_empty()
528 }
529
530 #[must_use]
532 pub fn has_divergent_prefix(&self) -> bool {
533 !self.divergent_prefix.is_empty()
534 }
535
536 #[must_use]
538 pub fn seed(&self) -> u64 {
539 self.manifest.config.seed
540 }
541
542 #[must_use]
544 pub fn fingerprint(&self) -> u64 {
545 self.manifest.fingerprint
546 }
547}
548
549#[derive(Debug)]
558pub struct CrashPackBuilder {
559 config: CrashPackConfig,
560 failure: Option<FailureInfo>,
561 fingerprint: u64,
562 event_count: u64,
563 canonical_prefix: Vec<Vec<TraceEventKey>>,
564 divergent_prefix: Vec<ReplayEvent>,
565 evidence: Vec<EvidenceEntrySnapshot>,
566 supervision_log: Vec<SupervisionSnapshot>,
567 oracle_violations: Vec<String>,
568 replay: Option<ReplayCommand>,
569}
570
571#[derive(Debug, Clone, Copy, PartialEq, Eq)]
573pub enum CrashPackBuildError {
574 MissingFailure,
576}
577
578impl fmt::Display for CrashPackBuildError {
579 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
580 match self {
581 Self::MissingFailure => f.write_str("crash pack builder requires failure metadata"),
582 }
583 }
584}
585
586impl std::error::Error for CrashPackBuildError {}
587
588impl CrashPackBuilder {
589 #[must_use]
591 pub fn failure(mut self, failure: FailureInfo) -> Self {
592 self.failure = Some(failure);
593 self
594 }
595
596 #[must_use]
598 pub fn fingerprint(mut self, fingerprint: u64) -> Self {
599 self.fingerprint = fingerprint;
600 self
601 }
602
603 #[must_use]
605 pub fn event_count(mut self, count: u64) -> Self {
606 self.event_count = count;
607 self
608 }
609
610 #[must_use]
621 pub fn from_trace(mut self, events: &[TraceEvent]) -> Self {
622 let foata = canonicalize(events);
623 self.canonical_prefix = foata
624 .layers()
625 .iter()
626 .map(|layer| layer.iter().map(trace_event_key).collect())
627 .collect();
628 self.fingerprint = trace_fingerprint(events);
629 self.event_count = events.len() as u64;
630 self
631 }
632
633 #[must_use]
635 pub fn canonical_prefix(mut self, prefix: Vec<Vec<TraceEventKey>>) -> Self {
636 self.canonical_prefix = prefix;
637 self
638 }
639
640 #[must_use]
642 pub fn divergent_prefix(mut self, prefix: Vec<ReplayEvent>) -> Self {
643 self.divergent_prefix = prefix;
644 self
645 }
646
647 #[must_use]
649 pub fn evidence(mut self, entries: Vec<EvidenceEntry>) -> Self {
650 self.evidence = entries
651 .into_iter()
652 .map(EvidenceEntrySnapshot::from)
653 .collect();
654 self
655 }
656
657 #[must_use]
659 pub fn supervision_snapshot(mut self, snapshot: SupervisionSnapshot) -> Self {
660 self.supervision_log.push(snapshot);
661 self
662 }
663
664 #[must_use]
666 pub fn oracle_violations(mut self, violations: Vec<String>) -> Self {
667 let mut v = violations;
668 v.sort();
669 v.dedup();
670 self.oracle_violations = v;
671 self
672 }
673
674 #[must_use]
676 pub fn replay(mut self, command: ReplayCommand) -> Self {
677 self.replay = Some(command);
678 self
679 }
680
681 pub fn build(self) -> Result<CrashPack, CrashPackBuildError> {
688 let failure = self.failure.ok_or(CrashPackBuildError::MissingFailure)?;
689
690 let mut supervision_log = self.supervision_log;
694 supervision_log.sort_by(|a, b| {
695 a.virtual_time
696 .cmp(&b.virtual_time)
697 .then_with(|| a.task.cmp(&b.task))
698 .then_with(|| a.region.cmp(&b.region))
699 .then_with(|| a.decision.cmp(&b.decision))
700 .then_with(|| a.context.cmp(&b.context))
701 });
702
703 let mut attachments = Vec::new();
705 if !self.canonical_prefix.is_empty() {
706 let item_count: u64 = self
707 .canonical_prefix
708 .iter()
709 .map(|layer| layer.len() as u64)
710 .sum();
711 attachments.push(ManifestAttachment {
712 kind: AttachmentKind::CanonicalPrefix,
713 item_count,
714 size_hint_bytes: 0,
715 });
716 }
717 if !self.divergent_prefix.is_empty() {
718 attachments.push(ManifestAttachment {
719 kind: AttachmentKind::DivergentPrefix,
720 item_count: self.divergent_prefix.len() as u64,
721 size_hint_bytes: 0,
722 });
723 }
724 if !self.evidence.is_empty() {
725 attachments.push(ManifestAttachment {
726 kind: AttachmentKind::EvidenceLedger,
727 item_count: self.evidence.len() as u64,
728 size_hint_bytes: 0,
729 });
730 }
731 if !supervision_log.is_empty() {
732 attachments.push(ManifestAttachment {
733 kind: AttachmentKind::SupervisionLog,
734 item_count: supervision_log.len() as u64,
735 size_hint_bytes: 0,
736 });
737 }
738 if !self.oracle_violations.is_empty() {
739 attachments.push(ManifestAttachment {
740 kind: AttachmentKind::OracleViolations,
741 item_count: self.oracle_violations.len() as u64,
742 size_hint_bytes: 0,
743 });
744 }
745
746 let mut manifest = CrashPackManifest::new(self.config, self.fingerprint, self.event_count);
747 manifest.attachments = attachments;
748
749 Ok(CrashPack {
750 manifest,
751 failure,
752 canonical_prefix: self.canonical_prefix,
753 divergent_prefix: self.divergent_prefix,
754 evidence: self.evidence,
755 supervision_log,
756 oracle_violations: self.oracle_violations,
757 replay: self.replay,
758 })
759 }
760}
761
762#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
768pub struct ReplayEnvVar {
769 pub key: String,
771 pub value: String,
773}
774
775#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
791pub struct ReplayCommand {
792 pub program: String,
794
795 pub args: Vec<String>,
797
798 #[serde(default, skip_serializing_if = "Vec::is_empty")]
800 pub env: Vec<ReplayEnvVar>,
801
802 pub command_line: String,
806}
807
808impl ReplayCommand {
809 #[must_use]
814 pub fn from_config(config: &CrashPackConfig, artifact_path: Option<&str>) -> Self {
815 let mut args = vec![
816 "test".to_string(),
817 "--lib".to_string(),
818 "--".to_string(),
819 "--seed".to_string(),
820 config.seed.to_string(),
821 ];
822
823 let mut env = Vec::new();
824
825 env.push(ReplayEnvVar {
826 key: "ASUPERSYNC_WORKERS".to_string(),
827 value: config.worker_count.to_string(),
828 });
829
830 if let Some(max_steps) = config.max_steps {
831 env.push(ReplayEnvVar {
832 key: "ASUPERSYNC_MAX_STEPS".to_string(),
833 value: max_steps.to_string(),
834 });
835 }
836
837 if let Some(path) = artifact_path {
838 args.push("--crashpack".to_string());
839 args.push(path.to_string());
840 }
841
842 let command_line = build_command_line("cargo", &args, &env);
843
844 Self {
845 program: "cargo".to_string(),
846 args,
847 env,
848 command_line,
849 }
850 }
851
852 #[must_use]
854 pub fn from_config_cli(config: &CrashPackConfig, artifact_path: &str) -> Self {
855 let mut args = vec![
856 "trace".to_string(),
857 "replay".to_string(),
858 "--seed".to_string(),
859 config.seed.to_string(),
860 "--workers".to_string(),
861 config.worker_count.to_string(),
862 ];
863
864 if let Some(max_steps) = config.max_steps {
865 args.push("--max-steps".to_string());
866 args.push(max_steps.to_string());
867 }
868
869 args.push(artifact_path.to_string());
870
871 let command_line = build_command_line("asupersync", &args, &[]);
872
873 Self {
874 program: "asupersync".to_string(),
875 args,
876 env: Vec::new(),
877 command_line,
878 }
879 }
880}
881
882impl std::fmt::Display for ReplayCommand {
883 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
884 write!(f, "{}", self.command_line)
885 }
886}
887
888fn build_command_line(program: &str, args: &[String], env: &[ReplayEnvVar]) -> String {
890 let mut parts = Vec::new();
891 for var in env {
892 parts.push(format!(
893 "{}={}",
894 shell_escape(&var.key),
895 shell_escape(&var.value)
896 ));
897 }
898 parts.push(program.to_string());
899 for arg in args {
900 parts.push(shell_escape(arg));
901 }
902 parts.join(" ")
903}
904
905fn shell_escape(s: &str) -> String {
910 if s.is_empty() {
911 return "''".to_string();
912 }
913 if s.chars()
914 .all(|c| c.is_alphanumeric() || matches!(c, '-' | '_' | '.' | '/' | ':' | '=' | ','))
915 {
916 s.to_string()
917 } else {
918 format!("'{}'", s.replace('\'', "'\\''"))
919 }
920}
921
922#[derive(Debug, Clone, PartialEq, Eq)]
932pub struct ArtifactId {
933 path: String,
935}
936
937impl ArtifactId {
938 #[must_use]
940 pub fn path(&self) -> &str {
941 &self.path
942 }
943}
944
945impl std::fmt::Display for ArtifactId {
946 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
947 write!(f, "{}", self.path)
948 }
949}
950
951#[derive(Debug)]
953pub enum CrashPackWriteError {
954 Serialize(String),
956 Io(std::io::Error),
958}
959
960impl std::fmt::Display for CrashPackWriteError {
961 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
962 match self {
963 Self::Serialize(msg) => write!(f, "crash pack serialization failed: {msg}"),
964 Self::Io(e) => write!(f, "crash pack I/O error: {e}"),
965 }
966 }
967}
968
969impl std::error::Error for CrashPackWriteError {
970 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
971 match self {
972 Self::Io(e) => Some(e),
973 Self::Serialize(_) => None,
974 }
975 }
976}
977
978pub trait CrashPackWriter: Send + Sync + std::fmt::Debug {
990 fn write(&self, pack: &CrashPack) -> Result<ArtifactId, CrashPackWriteError>;
992
993 fn is_persistent(&self) -> bool;
995
996 fn name(&self) -> &'static str;
998}
999
1000#[must_use]
1004pub fn artifact_filename(pack: &CrashPack) -> String {
1005 format!(
1006 "crashpack-{:016x}-{:016x}-{:016x}-v{}.json",
1007 pack.seed(),
1008 pack.manifest.config.config_hash,
1009 pack.fingerprint(),
1010 pack.manifest.schema_version,
1011 )
1012}
1013
1014#[derive(Debug)]
1020pub struct FileCrashPackWriter {
1021 base_dir: std::path::PathBuf,
1022}
1023
1024impl FileCrashPackWriter {
1025 #[must_use]
1029 pub fn new(base_dir: std::path::PathBuf) -> Self {
1030 Self { base_dir }
1031 }
1032
1033 #[must_use]
1035 pub fn base_dir(&self) -> &std::path::Path {
1036 &self.base_dir
1037 }
1038}
1039
1040impl CrashPackWriter for FileCrashPackWriter {
1041 fn write(&self, pack: &CrashPack) -> Result<ArtifactId, CrashPackWriteError> {
1042 let filename = artifact_filename(pack);
1043 let path = self.base_dir.join(&filename);
1044
1045 let json = serde_json::to_string_pretty(pack)
1046 .map_err(|e| CrashPackWriteError::Serialize(e.to_string()))?;
1047
1048 std::fs::write(&path, json.as_bytes()).map_err(CrashPackWriteError::Io)?;
1049
1050 Ok(ArtifactId {
1051 path: path.to_string_lossy().into_owned(),
1052 })
1053 }
1054
1055 fn is_persistent(&self) -> bool {
1056 true
1057 }
1058
1059 fn name(&self) -> &'static str {
1060 "file"
1061 }
1062}
1063
1064#[derive(Debug, Default)]
1068pub struct MemoryCrashPackWriter {
1069 packs: parking_lot::Mutex<Vec<(ArtifactId, String)>>,
1070}
1071
1072impl MemoryCrashPackWriter {
1073 #[must_use]
1075 pub fn new() -> Self {
1076 Self::default()
1077 }
1078
1079 pub fn written(&self) -> Vec<(ArtifactId, String)> {
1081 self.packs.lock().clone()
1082 }
1083
1084 #[must_use]
1086 pub fn count(&self) -> usize {
1087 self.packs.lock().len()
1088 }
1089}
1090
1091impl CrashPackWriter for MemoryCrashPackWriter {
1092 fn write(&self, pack: &CrashPack) -> Result<ArtifactId, CrashPackWriteError> {
1093 let filename = artifact_filename(pack);
1094 let json = serde_json::to_string_pretty(pack)
1095 .map_err(|e| CrashPackWriteError::Serialize(e.to_string()))?;
1096
1097 let artifact_id = ArtifactId { path: filename };
1098 self.packs.lock().push((artifact_id.clone(), json));
1099
1100 Ok(artifact_id)
1101 }
1102
1103 fn is_persistent(&self) -> bool {
1104 false
1105 }
1106
1107 fn name(&self) -> &'static str {
1108 "memory"
1109 }
1110}
1111
1112fn wall_clock_nanos() -> u64 {
1118 std::time::SystemTime::now()
1119 .duration_since(std::time::UNIX_EPOCH)
1120 .map_or(0, |d| d.as_nanos().min(u128::from(u64::MAX)) as u64)
1121}
1122
1123#[cfg(test)]
1128mod tests {
1129 use super::*;
1130 use crate::util::ArenaIndex;
1131
1132 fn init_test(name: &str) {
1133 crate::test_utils::init_test_logging();
1134 crate::test_phase!(name);
1135 }
1136
1137 fn tid(n: u32) -> TaskId {
1138 TaskId::from_arena(ArenaIndex::new(n, 0))
1139 }
1140
1141 fn rid(n: u32) -> RegionId {
1142 RegionId::from_arena(ArenaIndex::new(n, 0))
1143 }
1144
1145 fn sample_failure() -> FailureInfo {
1146 FailureInfo {
1147 task: tid(1),
1148 region: rid(0),
1149 outcome: FailureOutcome::Panicked {
1150 message: "test panic".to_string(),
1151 },
1152 virtual_time: Time::from_secs(5),
1153 }
1154 }
1155
1156 fn sample_config() -> CrashPackConfig {
1157 CrashPackConfig {
1158 seed: 42,
1159 config_hash: 0xDEAD,
1160 worker_count: 4,
1161 max_steps: Some(1000),
1162 commit_hash: Some("abc123".to_string()),
1163 }
1164 }
1165
1166 #[test]
1167 fn builder_missing_failure_returns_error() {
1168 init_test("builder_missing_failure_returns_error");
1169
1170 let err = CrashPack::builder(sample_config())
1171 .build()
1172 .expect_err("builder should fail closed without failure metadata");
1173
1174 assert_eq!(err, CrashPackBuildError::MissingFailure);
1175 assert_eq!(
1176 err.to_string(),
1177 "crash pack builder requires failure metadata"
1178 );
1179
1180 crate::test_complete!("builder_missing_failure_returns_error");
1181 }
1182
1183 #[test]
1184 fn schema_version_is_set() {
1185 init_test("schema_version_is_set");
1186
1187 let pack = CrashPack::builder(sample_config())
1188 .failure(sample_failure())
1189 .build()
1190 .expect("crash pack builder should have failure metadata");
1191
1192 assert_eq!(pack.manifest.schema_version, CRASHPACK_SCHEMA_VERSION);
1193 assert_eq!(pack.manifest.schema_version, 1);
1194
1195 crate::test_complete!("schema_version_is_set");
1196 }
1197
1198 #[test]
1199 fn builder_sets_all_fields() {
1200 init_test("builder_sets_all_fields");
1201
1202 let pack = CrashPack::builder(sample_config())
1203 .failure(sample_failure())
1204 .fingerprint(0xCAFE_BABE)
1205 .event_count(500)
1206 .oracle_violations(vec!["inv-1".into(), "inv-2".into()])
1207 .build()
1208 .expect("crash pack builder should have failure metadata");
1209
1210 assert_eq!(pack.manifest.config.seed, 42);
1211 assert_eq!(pack.manifest.config.config_hash, 0xDEAD);
1212 assert_eq!(pack.manifest.config.worker_count, 4);
1213 assert_eq!(pack.manifest.config.max_steps, Some(1000));
1214 assert_eq!(pack.manifest.config.commit_hash.as_deref(), Some("abc123"));
1215 assert_eq!(pack.manifest.fingerprint, 0xCAFE_BABE);
1216 assert_eq!(pack.manifest.event_count, 500);
1217 assert_eq!(pack.failure.task, tid(1));
1218 assert_eq!(pack.failure.region, rid(0));
1219 assert_eq!(pack.failure.virtual_time, Time::from_secs(5));
1220 assert!(pack.has_violations());
1221 assert_eq!(pack.oracle_violations, vec!["inv-1", "inv-2"]);
1222 assert!(!pack.has_divergent_prefix());
1223
1224 crate::test_complete!("builder_sets_all_fields");
1225 }
1226
1227 #[test]
1228 fn default_config() {
1229 init_test("default_config");
1230
1231 let config = CrashPackConfig::default();
1232 assert_eq!(config.seed, 0);
1233 assert_eq!(config.config_hash, 0);
1234 assert_eq!(config.worker_count, 1);
1235 assert_eq!(config.max_steps, None);
1236 assert_eq!(config.commit_hash, None);
1237
1238 crate::test_complete!("default_config");
1239 }
1240
1241 #[test]
1242 fn seed_and_fingerprint_accessors() {
1243 init_test("seed_and_fingerprint_accessors");
1244
1245 let pack = CrashPack::builder(CrashPackConfig {
1246 seed: 999,
1247 ..Default::default()
1248 })
1249 .failure(sample_failure())
1250 .fingerprint(0x1234)
1251 .build()
1252 .expect("crash pack builder should have failure metadata");
1253
1254 assert_eq!(pack.seed(), 999);
1255 assert_eq!(pack.fingerprint(), 0x1234);
1256
1257 crate::test_complete!("seed_and_fingerprint_accessors");
1258 }
1259
1260 #[test]
1261 fn oracle_violations_sorted_and_deduped() {
1262 init_test("oracle_violations_sorted_and_deduped");
1263
1264 let pack = CrashPack::builder(CrashPackConfig::default())
1265 .failure(sample_failure())
1266 .oracle_violations(vec![
1267 "z-violation".into(),
1268 "a-violation".into(),
1269 "z-violation".into(), "m-violation".into(),
1271 ])
1272 .build()
1273 .expect("crash pack builder should have failure metadata");
1274
1275 assert_eq!(
1276 pack.oracle_violations,
1277 vec!["a-violation", "m-violation", "z-violation"]
1278 );
1279
1280 crate::test_complete!("oracle_violations_sorted_and_deduped");
1281 }
1282
1283 #[test]
1284 fn supervision_log_sorted_by_vt() {
1285 init_test("supervision_log_sorted_by_vt");
1286
1287 let pack = CrashPack::builder(CrashPackConfig::default())
1288 .failure(sample_failure())
1289 .supervision_snapshot(SupervisionSnapshot {
1290 virtual_time: Time::from_secs(10),
1291 task: tid(1),
1292 region: rid(0),
1293 decision: "restart".into(),
1294 context: Some("attempt 2 of 3".into()),
1295 })
1296 .supervision_snapshot(SupervisionSnapshot {
1297 virtual_time: Time::from_secs(5),
1298 task: tid(1),
1299 region: rid(0),
1300 decision: "restart".into(),
1301 context: Some("attempt 1 of 3".into()),
1302 })
1303 .supervision_snapshot(SupervisionSnapshot {
1304 virtual_time: Time::from_secs(15),
1305 task: tid(1),
1306 region: rid(0),
1307 decision: "stop".into(),
1308 context: Some("budget exhausted".into()),
1309 })
1310 .build()
1311 .expect("crash pack builder should have failure metadata");
1312
1313 assert_eq!(pack.supervision_log.len(), 3);
1314 assert_eq!(pack.supervision_log[0].virtual_time, Time::from_secs(5));
1316 assert_eq!(pack.supervision_log[1].virtual_time, Time::from_secs(10));
1317 assert_eq!(pack.supervision_log[2].virtual_time, Time::from_secs(15));
1318
1319 crate::test_complete!("supervision_log_sorted_by_vt");
1320 }
1321
1322 #[test]
1323 fn supervision_log_equal_vt_has_deterministic_total_order() {
1324 init_test("supervision_log_equal_vt_has_deterministic_total_order");
1325
1326 let s1 = SupervisionSnapshot {
1327 virtual_time: Time::from_secs(5),
1328 task: tid(2),
1329 region: rid(0),
1330 decision: "restart".into(),
1331 context: Some("ctx-b".into()),
1332 };
1333 let s2 = SupervisionSnapshot {
1334 virtual_time: Time::from_secs(5),
1335 task: tid(1),
1336 region: rid(0),
1337 decision: "restart".into(),
1338 context: Some("ctx-a".into()),
1339 };
1340 let s3 = SupervisionSnapshot {
1341 virtual_time: Time::from_secs(5),
1342 task: tid(1),
1343 region: rid(0),
1344 decision: "escalate".into(),
1345 context: Some("ctx-a".into()),
1346 };
1347
1348 let pack_a = CrashPack::builder(CrashPackConfig::default())
1349 .failure(sample_failure())
1350 .supervision_snapshot(s1.clone())
1351 .supervision_snapshot(s2.clone())
1352 .supervision_snapshot(s3.clone())
1353 .build()
1354 .expect("crash pack builder should have failure metadata");
1355
1356 let pack_b = CrashPack::builder(CrashPackConfig::default())
1357 .failure(sample_failure())
1358 .supervision_snapshot(s3.clone())
1359 .supervision_snapshot(s1.clone())
1360 .supervision_snapshot(s2.clone())
1361 .build()
1362 .expect("crash pack builder should have failure metadata");
1363
1364 assert_eq!(pack_a.supervision_log, pack_b.supervision_log);
1366 assert_eq!(pack_a.supervision_log, vec![s3, s2, s1]);
1367
1368 crate::test_complete!("supervision_log_equal_vt_has_deterministic_total_order");
1369 }
1370
1371 #[test]
1372 fn crash_pack_equality_ignores_created_at() {
1373 init_test("crash_pack_equality_ignores_created_at");
1374
1375 let pack1 = CrashPack::builder(sample_config())
1376 .failure(sample_failure())
1377 .fingerprint(0xABCD)
1378 .build()
1379 .expect("crash pack builder should have failure metadata");
1380
1381 let pack2 = CrashPack::builder(sample_config())
1383 .failure(sample_failure())
1384 .fingerprint(0xABCD)
1385 .build()
1386 .expect("crash pack builder should have failure metadata");
1387
1388 assert_eq!(pack1, pack2);
1390
1391 crate::test_complete!("crash_pack_equality_ignores_created_at");
1392 }
1393
1394 #[test]
1395 fn crash_pack_inequality_on_different_fingerprint() {
1396 init_test("crash_pack_inequality_on_different_fingerprint");
1397
1398 let pack1 = CrashPack::builder(sample_config())
1399 .failure(sample_failure())
1400 .fingerprint(0x1111)
1401 .build()
1402 .expect("crash pack builder should have failure metadata");
1403
1404 let pack2 = CrashPack::builder(sample_config())
1405 .failure(sample_failure())
1406 .fingerprint(0x2222)
1407 .build()
1408 .expect("crash pack builder should have failure metadata");
1409
1410 assert_ne!(pack1, pack2);
1411
1412 crate::test_complete!("crash_pack_inequality_on_different_fingerprint");
1413 }
1414
1415 #[test]
1416 fn crash_pack_inequality_on_different_divergent_prefix() {
1417 init_test("crash_pack_inequality_on_different_divergent_prefix");
1418
1419 let pack1 = CrashPack::builder(sample_config())
1420 .failure(sample_failure())
1421 .fingerprint(0xABCD)
1422 .divergent_prefix(vec![ReplayEvent::RngSeed { seed: 1 }])
1423 .build()
1424 .expect("crash pack builder should have failure metadata");
1425
1426 let pack2 = CrashPack::builder(sample_config())
1427 .failure(sample_failure())
1428 .fingerprint(0xABCD)
1429 .divergent_prefix(vec![ReplayEvent::RngSeed { seed: 2 }])
1430 .build()
1431 .expect("crash pack builder should have failure metadata");
1432
1433 assert_ne!(pack1, pack2);
1434
1435 crate::test_complete!("crash_pack_inequality_on_different_divergent_prefix");
1436 }
1437
1438 #[test]
1439 fn empty_pack_defaults() {
1440 init_test("empty_pack_defaults");
1441
1442 let pack = CrashPack::builder(CrashPackConfig::default())
1443 .failure(sample_failure())
1444 .build()
1445 .expect("crash pack builder should have failure metadata");
1446
1447 assert!(pack.canonical_prefix.is_empty());
1448 assert!(pack.divergent_prefix.is_empty());
1449 assert!(pack.evidence.is_empty());
1450 assert!(pack.supervision_log.is_empty());
1451 assert!(pack.oracle_violations.is_empty());
1452 assert!(!pack.has_violations());
1453 assert!(!pack.has_divergent_prefix());
1454
1455 crate::test_complete!("empty_pack_defaults");
1456 }
1457
1458 #[test]
1459 fn failure_info_equality() {
1460 init_test("failure_info_equality");
1461
1462 let f1 = FailureInfo {
1463 task: tid(1),
1464 region: rid(0),
1465 outcome: FailureOutcome::Panicked {
1466 message: "a".to_string(),
1467 },
1468 virtual_time: Time::from_secs(5),
1469 };
1470 let f2 = FailureInfo {
1471 task: tid(1),
1472 region: rid(0),
1473 outcome: FailureOutcome::Err, virtual_time: Time::from_secs(5),
1475 };
1476 assert_ne!(f1, f2);
1478
1479 let f3 = FailureInfo {
1480 task: tid(2), region: rid(0),
1482 outcome: FailureOutcome::Panicked {
1483 message: "a".to_string(),
1484 },
1485 virtual_time: Time::from_secs(5),
1486 };
1487 assert_ne!(f1, f3);
1488
1489 crate::test_complete!("failure_info_equality");
1490 }
1491
1492 #[test]
1493 fn manifest_new_sets_version() {
1494 init_test("manifest_new_sets_version");
1495
1496 let manifest = CrashPackManifest::new(CrashPackConfig::default(), 0xBEEF, 100);
1497
1498 assert_eq!(manifest.schema_version, CRASHPACK_SCHEMA_VERSION);
1499 assert_eq!(manifest.fingerprint, 0xBEEF);
1500 assert_eq!(manifest.event_count, 100);
1501 assert!(manifest.created_at > 0);
1502
1503 crate::test_complete!("manifest_new_sets_version");
1504 }
1505
1506 #[test]
1507 fn with_divergent_prefix() {
1508 init_test("with_divergent_prefix");
1509
1510 let prefix = vec![
1511 ReplayEvent::RngSeed { seed: 42 },
1512 ReplayEvent::TaskScheduled {
1513 task: crate::trace::replay::CompactTaskId(1),
1514 at_tick: 0,
1515 },
1516 ];
1517
1518 let pack = CrashPack::builder(CrashPackConfig::default())
1519 .failure(sample_failure())
1520 .divergent_prefix(prefix)
1521 .build()
1522 .expect("crash pack builder should have failure metadata");
1523
1524 assert!(pack.has_divergent_prefix());
1525 assert_eq!(pack.divergent_prefix.len(), 2);
1526
1527 crate::test_complete!("with_divergent_prefix");
1528 }
1529
1530 #[test]
1531 fn with_canonical_prefix() {
1532 init_test("with_canonical_prefix");
1533
1534 let layer = vec![TraceEventKey {
1535 kind: 1,
1536 primary: 0,
1537 secondary: 0,
1538 tertiary: 0,
1539 }];
1540
1541 let pack = CrashPack::builder(CrashPackConfig::default())
1542 .failure(sample_failure())
1543 .canonical_prefix(vec![layer])
1544 .build()
1545 .expect("crash pack builder should have failure metadata");
1546
1547 assert_eq!(pack.canonical_prefix.len(), 1);
1548
1549 crate::test_complete!("with_canonical_prefix");
1550 }
1551
1552 #[test]
1553 fn supervision_snapshot_with_context() {
1554 init_test("supervision_snapshot_with_context");
1555
1556 let snap = SupervisionSnapshot {
1557 virtual_time: Time::from_secs(10),
1558 task: tid(3),
1559 region: rid(1),
1560 decision: "escalate".into(),
1561 context: Some("parent region R0".into()),
1562 };
1563
1564 assert_eq!(snap.decision, "escalate");
1565 assert_eq!(snap.context.as_deref(), Some("parent region R0"));
1566
1567 crate::test_complete!("supervision_snapshot_with_context");
1568 }
1569
1570 #[test]
1575 fn from_trace_populates_fields() {
1576 init_test("from_trace_populates_fields");
1577
1578 let events = [
1579 TraceEvent::spawn(1, Time::ZERO, tid(1), rid(1)),
1580 TraceEvent::spawn(2, Time::ZERO, tid(2), rid(2)),
1581 TraceEvent::complete(3, Time::ZERO, tid(1), rid(1)),
1582 ];
1583
1584 let pack = CrashPack::builder(sample_config())
1585 .failure(sample_failure())
1586 .from_trace(&events)
1587 .build()
1588 .expect("crash pack builder should have failure metadata");
1589
1590 assert_eq!(pack.manifest.event_count, 3);
1591 assert_ne!(pack.manifest.fingerprint, 0);
1592 assert!(!pack.canonical_prefix.is_empty());
1593
1594 crate::test_complete!("from_trace_populates_fields");
1595 }
1596
1597 #[test]
1598 fn from_trace_equivalent_traces_same_fingerprint() {
1599 init_test("from_trace_equivalent_traces_same_fingerprint");
1600
1601 let trace_a = [
1605 TraceEvent::spawn(1, Time::ZERO, tid(1), rid(1)),
1606 TraceEvent::spawn(2, Time::ZERO, tid(2), rid(2)),
1607 ];
1608 let trace_b = [
1609 TraceEvent::spawn(1, Time::ZERO, tid(2), rid(2)),
1610 TraceEvent::spawn(2, Time::ZERO, tid(1), rid(1)),
1611 ];
1612
1613 let pack_a = CrashPack::builder(sample_config())
1614 .failure(sample_failure())
1615 .from_trace(&trace_a)
1616 .build()
1617 .expect("crash pack builder should have failure metadata");
1618 let pack_b = CrashPack::builder(sample_config())
1619 .failure(sample_failure())
1620 .from_trace(&trace_b)
1621 .build()
1622 .expect("crash pack builder should have failure metadata");
1623
1624 assert_eq!(pack_a.fingerprint(), pack_b.fingerprint());
1625 assert_eq!(pack_a.canonical_prefix, pack_b.canonical_prefix);
1626 assert_eq!(pack_a, pack_b);
1627
1628 crate::test_complete!("from_trace_equivalent_traces_same_fingerprint");
1629 }
1630
1631 #[test]
1632 fn from_trace_different_dependent_traces_different_fingerprint() {
1633 init_test("from_trace_different_dependent_traces_different_fingerprint");
1634
1635 let trace_a = [
1638 TraceEvent::spawn(1, Time::ZERO, tid(1), rid(1)),
1639 TraceEvent::complete(2, Time::ZERO, tid(1), rid(1)),
1640 ];
1641 let trace_b = [
1642 TraceEvent::complete(1, Time::ZERO, tid(1), rid(1)),
1643 TraceEvent::spawn(2, Time::ZERO, tid(1), rid(1)),
1644 ];
1645
1646 let pack_a = CrashPack::builder(sample_config())
1647 .failure(sample_failure())
1648 .from_trace(&trace_a)
1649 .build()
1650 .expect("crash pack builder should have failure metadata");
1651 let pack_b = CrashPack::builder(sample_config())
1652 .failure(sample_failure())
1653 .from_trace(&trace_b)
1654 .build()
1655 .expect("crash pack builder should have failure metadata");
1656
1657 assert_ne!(pack_a.fingerprint(), pack_b.fingerprint());
1658 assert_ne!(pack_a, pack_b);
1659
1660 crate::test_complete!("from_trace_different_dependent_traces_different_fingerprint");
1661 }
1662
1663 #[test]
1664 fn from_trace_canonical_prefix_matches_foata_layers() {
1665 init_test("from_trace_canonical_prefix_matches_foata_layers");
1666
1667 let events = [
1668 TraceEvent::spawn(1, Time::ZERO, tid(1), rid(1)),
1669 TraceEvent::spawn(2, Time::ZERO, tid(2), rid(2)),
1670 TraceEvent::complete(3, Time::ZERO, tid(1), rid(1)),
1671 TraceEvent::complete(4, Time::ZERO, tid(2), rid(2)),
1672 ];
1673
1674 let pack = CrashPack::builder(CrashPackConfig::default())
1675 .failure(sample_failure())
1676 .from_trace(&events)
1677 .build()
1678 .expect("crash pack builder should have failure metadata");
1679
1680 let foata = canonicalize(&events);
1682 let expected_prefix: Vec<Vec<TraceEventKey>> = foata
1683 .layers()
1684 .iter()
1685 .map(|layer| layer.iter().map(trace_event_key).collect())
1686 .collect();
1687
1688 assert_eq!(pack.canonical_prefix, expected_prefix);
1689
1690 crate::test_complete!("from_trace_canonical_prefix_matches_foata_layers");
1691 }
1692
1693 #[test]
1694 fn from_trace_empty_trace() {
1695 init_test("from_trace_empty_trace");
1696
1697 let pack = CrashPack::builder(CrashPackConfig::default())
1698 .failure(sample_failure())
1699 .from_trace(&[])
1700 .build()
1701 .expect("crash pack builder should have failure metadata");
1702
1703 assert!(pack.canonical_prefix.is_empty());
1704 assert_eq!(pack.manifest.event_count, 0);
1705
1706 crate::test_complete!("from_trace_empty_trace");
1707 }
1708
1709 #[test]
1710 fn from_trace_three_independent_all_permutations() {
1711 init_test("from_trace_three_independent_all_permutations");
1712
1713 let e1 = TraceEvent::spawn(1, Time::ZERO, tid(1), rid(1));
1716 let e2 = TraceEvent::spawn(2, Time::ZERO, tid(2), rid(2));
1717 let e3 = TraceEvent::spawn(3, Time::ZERO, tid(3), rid(3));
1718
1719 let perms: Vec<Vec<TraceEvent>> = vec![
1720 vec![e1.clone(), e2.clone(), e3.clone()],
1721 vec![e1.clone(), e3.clone(), e2.clone()],
1722 vec![e2.clone(), e1.clone(), e3.clone()],
1723 vec![e2.clone(), e3.clone(), e1.clone()],
1724 vec![e3.clone(), e1.clone(), e2.clone()],
1725 vec![e3, e2, e1],
1726 ];
1727
1728 let reference = CrashPack::builder(CrashPackConfig::default())
1729 .failure(sample_failure())
1730 .from_trace(&perms[0])
1731 .build()
1732 .expect("crash pack builder should have failure metadata");
1733
1734 for (i, perm) in perms.iter().enumerate().skip(1) {
1735 let pack = CrashPack::builder(CrashPackConfig::default())
1736 .failure(sample_failure())
1737 .from_trace(perm)
1738 .build()
1739 .expect("crash pack builder should have failure metadata");
1740 assert_eq!(
1741 pack.fingerprint(),
1742 reference.fingerprint(),
1743 "permutation {i} has different fingerprint"
1744 );
1745 assert_eq!(
1746 pack.canonical_prefix, reference.canonical_prefix,
1747 "permutation {i} has different canonical prefix"
1748 );
1749 }
1750
1751 crate::test_complete!("from_trace_three_independent_all_permutations");
1752 }
1753
1754 #[test]
1755 fn from_trace_diamond_dependency() {
1756 init_test("from_trace_diamond_dependency");
1757
1758 let trace_a = [
1761 TraceEvent::region_created(1, Time::ZERO, rid(1), None),
1762 TraceEvent::spawn(2, Time::ZERO, tid(1), rid(1)),
1763 TraceEvent::spawn(3, Time::ZERO, tid(2), rid(1)),
1764 TraceEvent::complete(4, Time::ZERO, tid(1), rid(1)),
1765 TraceEvent::complete(5, Time::ZERO, tid(2), rid(1)),
1766 ];
1767 let trace_b = [
1768 TraceEvent::region_created(1, Time::ZERO, rid(1), None),
1769 TraceEvent::spawn(2, Time::ZERO, tid(2), rid(1)),
1770 TraceEvent::spawn(3, Time::ZERO, tid(1), rid(1)),
1771 TraceEvent::complete(4, Time::ZERO, tid(2), rid(1)),
1772 TraceEvent::complete(5, Time::ZERO, tid(1), rid(1)),
1773 ];
1774
1775 let pack_a = CrashPack::builder(sample_config())
1776 .failure(sample_failure())
1777 .from_trace(&trace_a)
1778 .build()
1779 .expect("crash pack builder should have failure metadata");
1780 let pack_b = CrashPack::builder(sample_config())
1781 .failure(sample_failure())
1782 .from_trace(&trace_b)
1783 .build()
1784 .expect("crash pack builder should have failure metadata");
1785
1786 assert_eq!(pack_a.fingerprint(), pack_b.fingerprint());
1787 assert_eq!(pack_a.canonical_prefix, pack_b.canonical_prefix);
1788 assert_eq!(pack_a.canonical_prefix.len(), 3);
1790
1791 crate::test_complete!("from_trace_diamond_dependency");
1792 }
1793
1794 #[test]
1799 fn artifact_filename_is_deterministic() {
1800 init_test("artifact_filename_is_deterministic");
1801
1802 let pack = CrashPack::builder(CrashPackConfig {
1803 seed: 42,
1804 ..Default::default()
1805 })
1806 .failure(sample_failure())
1807 .fingerprint(0xCAFE_BABE)
1808 .build()
1809 .expect("crash pack builder should have failure metadata");
1810
1811 let name1 = artifact_filename(&pack);
1812 let name2 = artifact_filename(&pack);
1813 assert_eq!(name1, name2);
1814 assert_eq!(
1815 name1,
1816 "crashpack-000000000000002a-0000000000000000-00000000cafebabe-v1.json"
1817 );
1818
1819 crate::test_complete!("artifact_filename_is_deterministic");
1820 }
1821
1822 #[test]
1823 fn artifact_filename_varies_by_seed_and_fingerprint() {
1824 init_test("artifact_filename_varies_by_seed_and_fingerprint");
1825
1826 let pack_a = CrashPack::builder(CrashPackConfig {
1827 seed: 1,
1828 ..Default::default()
1829 })
1830 .failure(sample_failure())
1831 .fingerprint(0xAAAA)
1832 .build()
1833 .expect("crash pack builder should have failure metadata");
1834
1835 let pack_b = CrashPack::builder(CrashPackConfig {
1836 seed: 2,
1837 ..Default::default()
1838 })
1839 .failure(sample_failure())
1840 .fingerprint(0xBBBB)
1841 .build()
1842 .expect("crash pack builder should have failure metadata");
1843
1844 assert_ne!(artifact_filename(&pack_a), artifact_filename(&pack_b));
1845
1846 crate::test_complete!("artifact_filename_varies_by_seed_and_fingerprint");
1847 }
1848
1849 #[test]
1850 fn artifact_filename_varies_by_config_hash() {
1851 init_test("artifact_filename_varies_by_config_hash");
1852
1853 let pack_a = CrashPack::builder(CrashPackConfig {
1854 seed: 42,
1855 config_hash: 0xAAAA,
1856 ..Default::default()
1857 })
1858 .failure(sample_failure())
1859 .fingerprint(0x1234)
1860 .build()
1861 .expect("crash pack builder should have failure metadata");
1862
1863 let pack_b = CrashPack::builder(CrashPackConfig {
1864 seed: 42,
1865 config_hash: 0xBBBB,
1866 ..Default::default()
1867 })
1868 .failure(sample_failure())
1869 .fingerprint(0x1234)
1870 .build()
1871 .expect("crash pack builder should have failure metadata");
1872
1873 assert_ne!(artifact_filename(&pack_a), artifact_filename(&pack_b));
1874
1875 crate::test_complete!("artifact_filename_varies_by_config_hash");
1876 }
1877
1878 #[test]
1879 fn memory_writer_collects_packs() {
1880 init_test("memory_writer_collects_packs");
1881
1882 let writer = MemoryCrashPackWriter::new();
1883 assert_eq!(writer.count(), 0);
1884 assert!(!writer.is_persistent());
1885 assert_eq!(writer.name(), "memory");
1886
1887 let pack = CrashPack::builder(sample_config())
1888 .failure(sample_failure())
1889 .fingerprint(0x1234)
1890 .build()
1891 .expect("crash pack builder should have failure metadata");
1892
1893 let artifact = writer.write(&pack).unwrap();
1894 assert_eq!(writer.count(), 1);
1895 assert!(artifact.path().contains("crashpack-"));
1896 assert!(artifact.path().contains("1234"));
1897
1898 let pack2 = CrashPack::builder(CrashPackConfig {
1900 seed: 99,
1901 ..Default::default()
1902 })
1903 .failure(sample_failure())
1904 .fingerprint(0x5678)
1905 .build()
1906 .expect("crash pack builder should have failure metadata");
1907
1908 let artifact2 = writer.write(&pack2).unwrap();
1909 assert_eq!(writer.count(), 2);
1910 assert_ne!(artifact.path(), artifact2.path());
1911
1912 crate::test_complete!("memory_writer_collects_packs");
1913 }
1914
1915 #[test]
1916 fn memory_writer_produces_valid_json() {
1917 init_test("memory_writer_produces_valid_json");
1918
1919 let writer = MemoryCrashPackWriter::new();
1920 let pack = CrashPack::builder(sample_config())
1921 .failure(sample_failure())
1922 .fingerprint(0xDEAD)
1923 .event_count(42)
1924 .oracle_violations(vec!["inv-1".into()])
1925 .build()
1926 .expect("crash pack builder should have failure metadata");
1927
1928 writer.write(&pack).unwrap();
1929 let written = writer.written();
1930 assert_eq!(written.len(), 1);
1931
1932 let json = &written[0].1;
1933 let parsed: serde_json::Value = serde_json::from_str(json).unwrap();
1935 assert_eq!(parsed["manifest"]["config"]["seed"], 42);
1936 assert_eq!(parsed["manifest"]["fingerprint"], 0xDEAD_u64);
1937 assert_eq!(parsed["manifest"]["event_count"], 42);
1938 assert_eq!(parsed["oracle_violations"][0], "inv-1");
1939
1940 crate::test_complete!("memory_writer_produces_valid_json");
1941 }
1942
1943 #[test]
1944 fn file_writer_writes_to_disk() {
1945 init_test("file_writer_writes_to_disk");
1946
1947 let dir = std::env::temp_dir().join("asupersync_test_crashpack");
1948 let _ = std::fs::create_dir_all(&dir);
1949
1950 let writer = FileCrashPackWriter::new(dir.clone());
1951 assert!(writer.is_persistent());
1952 assert_eq!(writer.name(), "file");
1953 assert_eq!(writer.base_dir(), dir.as_path());
1954
1955 let pack = CrashPack::builder(CrashPackConfig {
1956 seed: 7,
1957 ..Default::default()
1958 })
1959 .failure(sample_failure())
1960 .fingerprint(0xBEEF)
1961 .build()
1962 .expect("crash pack builder should have failure metadata");
1963
1964 let artifact = writer.write(&pack).unwrap();
1965 let expected_name = artifact_filename(&pack);
1966
1967 assert!(artifact.path().contains(&expected_name));
1969
1970 let contents = std::fs::read_to_string(artifact.path()).unwrap();
1972 let parsed: serde_json::Value = serde_json::from_str(&contents).unwrap();
1973 assert_eq!(parsed["manifest"]["config"]["seed"], 7);
1974
1975 let _ = std::fs::remove_file(artifact.path());
1977 let _ = std::fs::remove_dir(&dir);
1978
1979 crate::test_complete!("file_writer_writes_to_disk");
1980 }
1981
1982 #[test]
1983 fn file_writer_fails_on_missing_dir() {
1984 init_test("file_writer_fails_on_missing_dir");
1985
1986 let writer =
1987 FileCrashPackWriter::new(std::path::PathBuf::from("/nonexistent/crashpack/dir"));
1988
1989 let pack = CrashPack::builder(CrashPackConfig::default())
1990 .failure(sample_failure())
1991 .build()
1992 .expect("crash pack builder should have failure metadata");
1993
1994 let result = writer.write(&pack);
1995 assert!(result.is_err());
1996
1997 crate::test_complete!("file_writer_fails_on_missing_dir");
1998 }
1999
2000 #[test]
2001 fn artifact_id_display() {
2002 init_test("artifact_id_display");
2003
2004 let id = ArtifactId {
2005 path: "some/path.json".to_string(),
2006 };
2007 assert_eq!(format!("{id}"), "some/path.json");
2008 assert_eq!(id.path(), "some/path.json");
2009
2010 crate::test_complete!("artifact_id_display");
2011 }
2012
2013 #[test]
2014 fn conformance_no_ambient_writes() {
2015 init_test("conformance_no_ambient_writes");
2016
2017 let pack = CrashPack::builder(sample_config())
2020 .failure(sample_failure())
2021 .build()
2022 .expect("crash pack builder should have failure metadata");
2023
2024 assert_eq!(pack.seed(), 42);
2026
2027 let writer = MemoryCrashPackWriter::new();
2029 assert_eq!(writer.count(), 0);
2030 writer.write(&pack).unwrap();
2031 assert_eq!(writer.count(), 1);
2032
2033 crate::test_complete!("conformance_no_ambient_writes");
2034 }
2035
2036 #[test]
2037 fn conformance_same_pack_same_artifact_path() {
2038 init_test("conformance_same_pack_same_artifact_path");
2039
2040 let writer = MemoryCrashPackWriter::new();
2041
2042 let pack = CrashPack::builder(CrashPackConfig {
2043 seed: 100,
2044 ..Default::default()
2045 })
2046 .failure(sample_failure())
2047 .fingerprint(0xFACE)
2048 .build()
2049 .expect("crash pack builder should have failure metadata");
2050
2051 let id1 = writer.write(&pack).unwrap();
2052 let id2 = writer.write(&pack).unwrap();
2053
2054 assert_eq!(id1.path(), id2.path());
2056
2057 crate::test_complete!("conformance_same_pack_same_artifact_path");
2058 }
2059
2060 #[test]
2065 fn manifest_validate_current_version() {
2066 init_test("manifest_validate_current_version");
2067
2068 let manifest = CrashPackManifest::new(CrashPackConfig::default(), 0, 0);
2069 assert!(manifest.validate().is_ok());
2070 assert!(manifest.is_compatible());
2071 assert_eq!(manifest.schema_version, CRASHPACK_SCHEMA_VERSION);
2072
2073 crate::test_complete!("manifest_validate_current_version");
2074 }
2075
2076 #[test]
2077 fn manifest_validate_rejects_future_version() {
2078 init_test("manifest_validate_rejects_future_version");
2079
2080 let mut manifest = CrashPackManifest::new(CrashPackConfig::default(), 0, 0);
2081 manifest.schema_version = CRASHPACK_SCHEMA_VERSION + 1;
2082
2083 let err = manifest.validate().unwrap_err();
2084 assert!(!manifest.is_compatible());
2085 assert!(matches!(err, ManifestValidationError::VersionTooNew { .. }));
2086 assert!(err.to_string().contains("newer than supported"));
2088
2089 crate::test_complete!("manifest_validate_rejects_future_version");
2090 }
2091
2092 #[test]
2093 fn manifest_validate_rejects_old_version() {
2094 init_test("manifest_validate_rejects_old_version");
2095
2096 let mut manifest = CrashPackManifest::new(CrashPackConfig::default(), 0, 0);
2097 manifest.schema_version = 0; let err = manifest.validate().unwrap_err();
2100 assert!(!manifest.is_compatible());
2101 assert!(matches!(err, ManifestValidationError::VersionTooOld { .. }));
2102 assert!(err.to_string().contains("older than minimum"));
2103
2104 crate::test_complete!("manifest_validate_rejects_old_version");
2105 }
2106
2107 #[test]
2108 fn manifest_attachments_auto_populated() {
2109 init_test("manifest_attachments_auto_populated");
2110
2111 let events = [
2114 TraceEvent::spawn(1, Time::ZERO, tid(1), rid(1)),
2115 TraceEvent::complete(2, Time::ZERO, tid(1), rid(1)),
2116 ];
2117
2118 let pack = CrashPack::builder(sample_config())
2119 .failure(sample_failure())
2120 .from_trace(&events)
2121 .divergent_prefix(vec![ReplayEvent::RngSeed { seed: 42 }])
2122 .oracle_violations(vec!["inv-1".into()])
2123 .build()
2124 .expect("crash pack builder should have failure metadata");
2125
2126 assert_eq!(pack.manifest.attachments.len(), 3);
2127 assert!(
2128 pack.manifest
2129 .has_attachment(&AttachmentKind::CanonicalPrefix)
2130 );
2131 assert!(
2132 pack.manifest
2133 .has_attachment(&AttachmentKind::DivergentPrefix)
2134 );
2135 assert!(
2136 pack.manifest
2137 .has_attachment(&AttachmentKind::OracleViolations)
2138 );
2139 assert!(
2140 !pack
2141 .manifest
2142 .has_attachment(&AttachmentKind::EvidenceLedger)
2143 );
2144 assert!(
2145 !pack
2146 .manifest
2147 .has_attachment(&AttachmentKind::SupervisionLog)
2148 );
2149
2150 crate::test_complete!("manifest_attachments_auto_populated");
2151 }
2152
2153 #[test]
2154 fn manifest_empty_pack_no_attachments() {
2155 init_test("manifest_empty_pack_no_attachments");
2156
2157 let pack = CrashPack::builder(CrashPackConfig::default())
2158 .failure(sample_failure())
2159 .build()
2160 .expect("crash pack builder should have failure metadata");
2161
2162 assert!(pack.manifest.attachments.is_empty());
2163
2164 crate::test_complete!("manifest_empty_pack_no_attachments");
2165 }
2166
2167 #[test]
2168 fn manifest_attachment_item_counts() {
2169 init_test("manifest_attachment_item_counts");
2170
2171 let pack = CrashPack::builder(sample_config())
2172 .failure(sample_failure())
2173 .canonical_prefix(vec![
2174 vec![TraceEventKey {
2175 kind: 1,
2176 primary: 0,
2177 secondary: 0,
2178 tertiary: 0,
2179 }],
2180 vec![
2181 TraceEventKey {
2182 kind: 2,
2183 primary: 1,
2184 secondary: 0,
2185 tertiary: 0,
2186 },
2187 TraceEventKey {
2188 kind: 2,
2189 primary: 2,
2190 secondary: 0,
2191 tertiary: 0,
2192 },
2193 ],
2194 ])
2195 .supervision_snapshot(SupervisionSnapshot {
2196 virtual_time: Time::from_secs(1),
2197 task: tid(1),
2198 region: rid(0),
2199 decision: "restart".into(),
2200 context: None,
2201 })
2202 .build()
2203 .expect("crash pack builder should have failure metadata");
2204
2205 let cp = pack
2207 .manifest
2208 .attachment(&AttachmentKind::CanonicalPrefix)
2209 .unwrap();
2210 assert_eq!(cp.item_count, 3);
2211
2212 let sl = pack
2214 .manifest
2215 .attachment(&AttachmentKind::SupervisionLog)
2216 .unwrap();
2217 assert_eq!(sl.item_count, 1);
2218
2219 crate::test_complete!("manifest_attachment_item_counts");
2220 }
2221
2222 #[test]
2223 fn manifest_attachment_kind_serde_round_trip() {
2224 init_test("manifest_attachment_kind_serde_round_trip");
2225
2226 let kinds = vec![
2227 AttachmentKind::CanonicalPrefix,
2228 AttachmentKind::DivergentPrefix,
2229 AttachmentKind::EvidenceLedger,
2230 AttachmentKind::SupervisionLog,
2231 AttachmentKind::OracleViolations,
2232 AttachmentKind::Custom {
2233 tag: "heap-dump".into(),
2234 },
2235 ];
2236
2237 for kind in &kinds {
2238 let json = serde_json::to_string(kind).unwrap();
2239 let parsed: AttachmentKind = serde_json::from_str(&json).unwrap();
2240 assert_eq!(&parsed, kind, "round trip failed for {json}");
2241 }
2242
2243 crate::test_complete!("manifest_attachment_kind_serde_round_trip");
2244 }
2245
2246 #[test]
2247 fn manifest_serde_round_trip_with_attachments() {
2248 init_test("manifest_serde_round_trip_with_attachments");
2249
2250 let mut manifest = CrashPackManifest::new(sample_config(), 0xBEEF, 100);
2251 manifest.attachments = vec![
2252 ManifestAttachment {
2253 kind: AttachmentKind::CanonicalPrefix,
2254 item_count: 10,
2255 size_hint_bytes: 2048,
2256 },
2257 ManifestAttachment {
2258 kind: AttachmentKind::Custom {
2259 tag: "user-data".into(),
2260 },
2261 item_count: 1,
2262 size_hint_bytes: 0,
2263 },
2264 ];
2265
2266 let json = serde_json::to_string_pretty(&manifest).unwrap();
2267 let parsed: CrashPackManifest = serde_json::from_str(&json).unwrap();
2268
2269 assert_eq!(parsed.schema_version, CRASHPACK_SCHEMA_VERSION);
2270 assert_eq!(parsed.config.seed, 42);
2271 assert_eq!(parsed.fingerprint, 0xBEEF);
2272 assert_eq!(parsed.attachments.len(), 2);
2273 assert_eq!(parsed.attachments[0].kind, AttachmentKind::CanonicalPrefix);
2274 assert_eq!(parsed.attachments[0].item_count, 10);
2275 assert_eq!(parsed.attachments[0].size_hint_bytes, 2048);
2276 assert_eq!(
2277 parsed.attachments[1].kind,
2278 AttachmentKind::Custom {
2279 tag: "user-data".into()
2280 }
2281 );
2282
2283 crate::test_complete!("manifest_serde_round_trip_with_attachments");
2284 }
2285
2286 #[test]
2287 fn manifest_deserialize_without_attachments() {
2288 init_test("manifest_deserialize_without_attachments");
2289
2290 let json = r#"{
2293 "schema_version": 1,
2294 "config": { "seed": 1, "config_hash": 0, "worker_count": 1 },
2295 "fingerprint": 999,
2296 "event_count": 50,
2297 "created_at": 0
2298 }"#;
2299
2300 let manifest: CrashPackManifest = serde_json::from_str(json).unwrap();
2301 assert_eq!(manifest.schema_version, 1);
2302 assert_eq!(manifest.fingerprint, 999);
2303 assert!(manifest.attachments.is_empty());
2304 assert!(manifest.is_compatible());
2305
2306 crate::test_complete!("manifest_deserialize_without_attachments");
2307 }
2308
2309 #[test]
2310 fn manifest_json_skips_empty_attachments() {
2311 init_test("manifest_json_skips_empty_attachments");
2312
2313 let manifest = CrashPackManifest::new(CrashPackConfig::default(), 0, 0);
2314 let json = serde_json::to_string(&manifest).unwrap();
2315
2316 assert!(!json.contains("attachments"));
2318
2319 crate::test_complete!("manifest_json_skips_empty_attachments");
2320 }
2321
2322 #[test]
2323 fn manifest_json_skips_zero_size_hint() {
2324 init_test("manifest_json_skips_zero_size_hint");
2325
2326 let attachment = ManifestAttachment {
2327 kind: AttachmentKind::CanonicalPrefix,
2328 item_count: 5,
2329 size_hint_bytes: 0,
2330 };
2331 let json = serde_json::to_string(&attachment).unwrap();
2332 assert!(!json.contains("size_hint_bytes"));
2333
2334 let non_zero = ManifestAttachment {
2335 kind: AttachmentKind::CanonicalPrefix,
2336 item_count: 5,
2337 size_hint_bytes: 1024,
2338 };
2339 let json2 = serde_json::to_string(&non_zero).unwrap();
2340 assert!(json2.contains("size_hint_bytes"));
2341
2342 crate::test_complete!("manifest_json_skips_zero_size_hint");
2343 }
2344
2345 #[test]
2346 fn conformance_attachments_in_crash_pack_json() {
2347 init_test("conformance_attachments_in_crash_pack_json");
2348
2349 let events = [
2351 TraceEvent::spawn(1, Time::ZERO, tid(1), rid(1)),
2352 TraceEvent::complete(2, Time::ZERO, tid(1), rid(1)),
2353 ];
2354
2355 let pack = CrashPack::builder(sample_config())
2356 .failure(sample_failure())
2357 .from_trace(&events)
2358 .divergent_prefix(vec![ReplayEvent::RngSeed { seed: 42 }])
2359 .oracle_violations(vec!["v1".into()])
2360 .supervision_snapshot(SupervisionSnapshot {
2361 virtual_time: Time::from_secs(1),
2362 task: tid(1),
2363 region: rid(0),
2364 decision: "restart".into(),
2365 context: None,
2366 })
2367 .build()
2368 .expect("crash pack builder should have failure metadata");
2369
2370 let writer = MemoryCrashPackWriter::new();
2371 writer.write(&pack).unwrap();
2372 let json_str = &writer.written()[0].1;
2373 let parsed: serde_json::Value = serde_json::from_str(json_str).unwrap();
2374
2375 let atts = parsed["manifest"]["attachments"].as_array().unwrap();
2376 assert_eq!(atts.len(), 4);
2377
2378 let kinds: Vec<&str> = atts.iter().map(|a| a["kind"].as_str().unwrap()).collect();
2380 assert!(kinds.contains(&"CanonicalPrefix"));
2381 assert!(kinds.contains(&"DivergentPrefix"));
2382 assert!(kinds.contains(&"SupervisionLog"));
2383 assert!(kinds.contains(&"OracleViolations"));
2384
2385 crate::test_complete!("conformance_attachments_in_crash_pack_json");
2386 }
2387
2388 #[test]
2389 fn conformance_validation_error_is_std_error() {
2390 init_test("conformance_validation_error_is_std_error");
2391
2392 let err = ManifestValidationError::VersionTooNew {
2393 manifest_version: 99,
2394 supported_version: 1,
2395 };
2396
2397 let _: &dyn std::error::Error = &err;
2399 assert!(err.to_string().contains("99"));
2400
2401 crate::test_complete!("conformance_validation_error_is_std_error");
2402 }
2403
2404 #[test]
2409 fn replay_command_from_config_basic() {
2410 init_test("replay_command_from_config_basic");
2411
2412 let config = CrashPackConfig {
2413 seed: 42,
2414 config_hash: 0xDEAD,
2415 worker_count: 4,
2416 max_steps: Some(1000),
2417 commit_hash: Some("abc123".to_string()),
2418 };
2419
2420 let cmd = ReplayCommand::from_config(&config, None);
2421 assert_eq!(cmd.program, "cargo");
2422 assert!(cmd.args.contains(&"--seed".to_string()));
2423 assert!(cmd.args.contains(&"42".to_string()));
2424 assert!(!cmd.env.is_empty());
2425 assert!(cmd.command_line.contains("cargo"));
2426 assert!(cmd.command_line.contains("--seed"));
2427 assert!(cmd.command_line.contains("42"));
2428 assert!(cmd.command_line.contains("ASUPERSYNC_WORKERS=4"));
2429
2430 crate::test_complete!("replay_command_from_config_basic");
2431 }
2432
2433 #[test]
2434 fn replay_command_from_config_with_artifact() {
2435 init_test("replay_command_from_config_with_artifact");
2436
2437 let config = CrashPackConfig {
2438 seed: 99,
2439 worker_count: 2,
2440 ..Default::default()
2441 };
2442
2443 let cmd = ReplayCommand::from_config(&config, Some("crashes/pack.json"));
2444 assert!(cmd.args.contains(&"--crashpack".to_string()));
2445 assert!(cmd.args.contains(&"crashes/pack.json".to_string()));
2446 assert!(cmd.command_line.contains("--crashpack"));
2447 assert!(cmd.command_line.contains("crashes/pack.json"));
2448
2449 crate::test_complete!("replay_command_from_config_with_artifact");
2450 }
2451
2452 #[test]
2453 fn replay_command_cli_mode() {
2454 init_test("replay_command_cli_mode");
2455
2456 let config = CrashPackConfig {
2457 seed: 7,
2458 worker_count: 8,
2459 max_steps: Some(500),
2460 ..Default::default()
2461 };
2462
2463 let cmd = ReplayCommand::from_config_cli(&config, "crashpack.json");
2464 assert_eq!(cmd.program, "asupersync");
2465 assert!(cmd.args.contains(&"trace".to_string()));
2466 assert!(cmd.args.contains(&"replay".to_string()));
2467 assert!(cmd.args.contains(&"--seed".to_string()));
2468 assert!(cmd.args.contains(&"7".to_string()));
2469 assert!(cmd.args.contains(&"--workers".to_string()));
2470 assert!(cmd.args.contains(&"8".to_string()));
2471 assert!(cmd.args.contains(&"--max-steps".to_string()));
2472 assert!(cmd.args.contains(&"500".to_string()));
2473 assert!(cmd.args.contains(&"crashpack.json".to_string()));
2474 assert!(cmd.env.is_empty());
2475 assert_eq!(
2476 cmd.command_line,
2477 "asupersync trace replay --seed 7 --workers 8 --max-steps 500 crashpack.json"
2478 );
2479
2480 crate::test_complete!("replay_command_cli_mode");
2481 }
2482
2483 #[test]
2484 fn replay_command_display() {
2485 init_test("replay_command_display");
2486
2487 let cmd = ReplayCommand::from_config_cli(
2488 &CrashPackConfig {
2489 seed: 1,
2490 worker_count: 1,
2491 ..Default::default()
2492 },
2493 "test.json",
2494 );
2495
2496 let displayed = format!("{cmd}");
2497 assert_eq!(displayed, cmd.command_line);
2498
2499 crate::test_complete!("replay_command_display");
2500 }
2501
2502 #[test]
2503 fn replay_command_serde_round_trip() {
2504 init_test("replay_command_serde_round_trip");
2505
2506 let cmd = ReplayCommand::from_config(
2507 &CrashPackConfig {
2508 seed: 42,
2509 worker_count: 4,
2510 max_steps: Some(1000),
2511 ..Default::default()
2512 },
2513 Some("pack.json"),
2514 );
2515
2516 let json = serde_json::to_string_pretty(&cmd).unwrap();
2517 let parsed: ReplayCommand = serde_json::from_str(&json).unwrap();
2518 assert_eq!(parsed, cmd);
2519
2520 crate::test_complete!("replay_command_serde_round_trip");
2521 }
2522
2523 #[test]
2524 fn replay_command_in_crash_pack() {
2525 init_test("replay_command_in_crash_pack");
2526
2527 let config = sample_config();
2528 let replay_cmd = ReplayCommand::from_config(&config, Some("crashes/test.json"));
2529
2530 let pack = CrashPack::builder(config)
2531 .failure(sample_failure())
2532 .fingerprint(0xCAFE)
2533 .replay(replay_cmd.clone())
2534 .build()
2535 .expect("crash pack builder should have failure metadata");
2536
2537 assert_eq!(pack.replay.as_ref(), Some(&replay_cmd));
2538
2539 let writer = MemoryCrashPackWriter::new();
2541 writer.write(&pack).unwrap();
2542 let json_str = &writer.written()[0].1;
2543 let parsed: serde_json::Value = serde_json::from_str(json_str).unwrap();
2544 assert!(parsed["replay"]["program"].as_str().is_some());
2545 assert!(
2546 parsed["replay"]["command_line"]
2547 .as_str()
2548 .unwrap()
2549 .contains("--seed")
2550 );
2551
2552 crate::test_complete!("replay_command_in_crash_pack");
2553 }
2554
2555 #[test]
2556 fn replay_command_absent_by_default() {
2557 init_test("replay_command_absent_by_default");
2558
2559 let pack = CrashPack::builder(CrashPackConfig::default())
2560 .failure(sample_failure())
2561 .build()
2562 .expect("crash pack builder should have failure metadata");
2563
2564 assert!(pack.replay.is_none());
2565
2566 let writer = MemoryCrashPackWriter::new();
2568 writer.write(&pack).unwrap();
2569 let json_str = &writer.written()[0].1;
2570 assert!(!json_str.contains("\"replay\""));
2571
2572 crate::test_complete!("replay_command_absent_by_default");
2573 }
2574
2575 #[test]
2576 fn replay_command_convenience_method() {
2577 init_test("replay_command_convenience_method");
2578
2579 let pack = CrashPack::builder(CrashPackConfig {
2580 seed: 77,
2581 worker_count: 2,
2582 ..Default::default()
2583 })
2584 .failure(sample_failure())
2585 .build()
2586 .expect("crash pack builder should have failure metadata");
2587
2588 let cmd = pack.replay_command(Some("output.json"));
2589 assert!(cmd.command_line.contains("--seed"));
2590 assert!(cmd.command_line.contains("77"));
2591 assert!(cmd.command_line.contains("output.json"));
2592
2593 crate::test_complete!("replay_command_convenience_method");
2594 }
2595
2596 #[test]
2597 fn replay_command_max_steps_included_when_set() {
2598 init_test("replay_command_max_steps_included_when_set");
2599
2600 let with_steps = ReplayCommand::from_config(
2601 &CrashPackConfig {
2602 seed: 1,
2603 max_steps: Some(999),
2604 ..Default::default()
2605 },
2606 None,
2607 );
2608 assert!(with_steps.command_line.contains("ASUPERSYNC_MAX_STEPS=999"));
2609
2610 let without_steps = ReplayCommand::from_config(
2611 &CrashPackConfig {
2612 seed: 1,
2613 max_steps: None,
2614 ..Default::default()
2615 },
2616 None,
2617 );
2618 assert!(!without_steps.command_line.contains("ASUPERSYNC_MAX_STEPS"));
2619
2620 crate::test_complete!("replay_command_max_steps_included_when_set");
2621 }
2622
2623 #[test]
2624 fn shell_escape_handles_special_chars() {
2625 init_test("shell_escape_handles_special_chars");
2626
2627 assert_eq!(shell_escape("hello"), "hello");
2629 assert_eq!(shell_escape("path/to/file.json"), "path/to/file.json");
2630 assert_eq!(shell_escape("42"), "42");
2631
2632 assert_eq!(shell_escape("hello world"), "'hello world'");
2634
2635 assert_eq!(shell_escape(""), "''");
2637
2638 crate::test_complete!("shell_escape_handles_special_chars");
2639 }
2640
2641 fn golden_failure_events() -> Vec<TraceEvent> {
2647 vec![
2648 TraceEvent::region_created(1, Time::ZERO, rid(1), None),
2649 TraceEvent::spawn(2, Time::ZERO, tid(1), rid(1)),
2650 TraceEvent::spawn(3, Time::ZERO, tid(2), rid(1)),
2651 TraceEvent::poll(4, Time::from_nanos(100), tid(1), rid(1)),
2652 TraceEvent::poll(5, Time::from_nanos(100), tid(2), rid(1)),
2653 TraceEvent::complete(6, Time::from_nanos(200), tid(1), rid(1)),
2654 ]
2655 }
2656
2657 fn golden_config() -> CrashPackConfig {
2658 CrashPackConfig {
2659 seed: 42,
2660 config_hash: 0xDEAD,
2661 worker_count: 4,
2662 max_steps: Some(1000),
2663 commit_hash: Some("abc123def".to_string()),
2664 }
2665 }
2666
2667 fn golden_failure_info() -> FailureInfo {
2668 FailureInfo {
2669 task: tid(2),
2670 region: rid(1),
2671 outcome: FailureOutcome::Panicked {
2672 message: "worker panic in golden scenario".to_string(),
2673 },
2674 virtual_time: Time::from_nanos(200),
2675 }
2676 }
2677
2678 #[test]
2679 fn golden_deterministic_emission() {
2680 init_test("golden_deterministic_emission");
2681
2682 let events = golden_failure_events();
2683
2684 let pack1 = CrashPack::builder(golden_config())
2686 .failure(golden_failure_info())
2687 .from_trace(&events)
2688 .build()
2689 .expect("crash pack builder should have failure metadata");
2690
2691 let pack2 = CrashPack::builder(golden_config())
2692 .failure(golden_failure_info())
2693 .from_trace(&events)
2694 .build()
2695 .expect("crash pack builder should have failure metadata");
2696
2697 assert_eq!(pack1, pack2);
2699 assert_eq!(pack1.fingerprint(), pack2.fingerprint());
2700 assert_eq!(pack1.canonical_prefix, pack2.canonical_prefix);
2701 assert_eq!(pack1.manifest.event_count, pack2.manifest.event_count);
2702
2703 crate::test_complete!("golden_deterministic_emission");
2704 }
2705
2706 #[test]
2707 fn golden_fingerprint_stability() {
2708 init_test("golden_fingerprint_stability");
2709
2710 let events = golden_failure_events();
2711 let pack = CrashPack::builder(golden_config())
2712 .failure(golden_failure_info())
2713 .from_trace(&events)
2714 .build()
2715 .expect("crash pack builder should have failure metadata");
2716
2717 let fp = pack.fingerprint();
2719 assert_ne!(fp, 0);
2720
2721 let fp2 = CrashPack::builder(golden_config())
2723 .failure(golden_failure_info())
2724 .from_trace(&events)
2725 .build()
2726 .expect("crash pack builder should have failure metadata")
2727 .fingerprint();
2728 assert_eq!(fp, fp2);
2729
2730 assert_eq!(fp, crate::trace::canonicalize::trace_fingerprint(&events));
2732
2733 crate::test_complete!("golden_fingerprint_stability");
2734 }
2735
2736 #[test]
2737 fn golden_canonical_prefix_structure() {
2738 init_test("golden_canonical_prefix_structure");
2739
2740 let events = golden_failure_events();
2741 let pack = CrashPack::builder(golden_config())
2742 .failure(golden_failure_info())
2743 .from_trace(&events)
2744 .build()
2745 .expect("crash pack builder should have failure metadata");
2746
2747 assert_eq!(
2753 pack.canonical_prefix.len(),
2754 4,
2755 "expected 4 Foata layers, got {}",
2756 pack.canonical_prefix.len()
2757 );
2758 assert_eq!(pack.canonical_prefix[0].len(), 1); assert_eq!(pack.canonical_prefix[1].len(), 2); assert_eq!(pack.canonical_prefix[2].len(), 2); assert_eq!(pack.canonical_prefix[3].len(), 1); assert_eq!(pack.manifest.event_count, 6);
2765
2766 crate::test_complete!("golden_canonical_prefix_structure");
2767 }
2768
2769 #[test]
2770 fn golden_equivalent_schedule_same_pack() {
2771 init_test("golden_equivalent_schedule_same_pack");
2772
2773 let events_a = golden_failure_events();
2776 let events_b = vec![
2777 TraceEvent::region_created(1, Time::ZERO, rid(1), None),
2778 TraceEvent::spawn(2, Time::ZERO, tid(2), rid(1)), TraceEvent::spawn(3, Time::ZERO, tid(1), rid(1)), TraceEvent::poll(4, Time::from_nanos(100), tid(2), rid(1)),
2781 TraceEvent::poll(5, Time::from_nanos(100), tid(1), rid(1)),
2782 TraceEvent::complete(6, Time::from_nanos(200), tid(1), rid(1)),
2783 ];
2784
2785 let pack_a = CrashPack::builder(golden_config())
2786 .failure(golden_failure_info())
2787 .from_trace(&events_a)
2788 .build()
2789 .expect("crash pack builder should have failure metadata");
2790 let pack_b = CrashPack::builder(golden_config())
2791 .failure(golden_failure_info())
2792 .from_trace(&events_b)
2793 .build()
2794 .expect("crash pack builder should have failure metadata");
2795
2796 assert_eq!(pack_a.fingerprint(), pack_b.fingerprint());
2798 assert_eq!(pack_a.canonical_prefix, pack_b.canonical_prefix);
2799 assert_eq!(pack_a, pack_b);
2800
2801 crate::test_complete!("golden_equivalent_schedule_same_pack");
2802 }
2803
2804 #[test]
2805 fn golden_replay_prefix_round_trip() {
2806 use crate::trace::replay::{
2807 CompactRegionId, CompactTaskId, ReplayEvent, ReplayTrace, TraceMetadata,
2808 };
2809 use crate::trace::replayer::TraceReplayer;
2810
2811 init_test("golden_replay_prefix_round_trip");
2812
2813 let replay_events = vec![
2815 ReplayEvent::RngSeed { seed: 42 },
2816 ReplayEvent::RegionCreated {
2817 region: CompactRegionId(1),
2818 parent: None,
2819 at_tick: 0,
2820 },
2821 ReplayEvent::TaskSpawned {
2822 task: CompactTaskId(1),
2823 region: CompactRegionId(1),
2824 at_tick: 0,
2825 },
2826 ReplayEvent::TaskSpawned {
2827 task: CompactTaskId(2),
2828 region: CompactRegionId(1),
2829 at_tick: 0,
2830 },
2831 ReplayEvent::TaskScheduled {
2832 task: CompactTaskId(1),
2833 at_tick: 100,
2834 },
2835 ReplayEvent::TaskScheduled {
2836 task: CompactTaskId(2),
2837 at_tick: 100,
2838 },
2839 ReplayEvent::TaskCompleted {
2840 task: CompactTaskId(1),
2841 outcome: 0, },
2843 ];
2844
2845 let trace = ReplayTrace {
2846 metadata: TraceMetadata::new(42),
2847 events: replay_events.clone(),
2848 cursor: 0,
2849 };
2850
2851 let pack = CrashPack::builder(golden_config())
2853 .failure(golden_failure_info())
2854 .from_trace(&golden_failure_events())
2855 .divergent_prefix(replay_events.clone())
2856 .build()
2857 .expect("crash pack builder should have failure metadata");
2858
2859 assert!(pack.has_divergent_prefix());
2860 assert_eq!(pack.divergent_prefix.len(), 7);
2861
2862 let mut replayer = TraceReplayer::new(trace);
2865 for expected_event in &replay_events {
2866 let actual = replayer.next().expect("replayer should have more events");
2867 assert_eq!(actual, expected_event);
2868 }
2869 assert!(replayer.is_completed());
2870
2871 crate::test_complete!("golden_replay_prefix_round_trip");
2872 }
2873
2874 #[test]
2875 fn golden_replay_serialization_round_trip() {
2876 use crate::trace::replay::{
2877 CompactRegionId, CompactTaskId, ReplayEvent, ReplayTrace, TraceMetadata,
2878 };
2879
2880 init_test("golden_replay_serialization_round_trip");
2881
2882 let replay_events = vec![
2883 ReplayEvent::RngSeed { seed: 42 },
2884 ReplayEvent::TaskSpawned {
2885 task: CompactTaskId(1),
2886 region: CompactRegionId(1),
2887 at_tick: 0,
2888 },
2889 ReplayEvent::TaskCompleted {
2890 task: CompactTaskId(1),
2891 outcome: 3, },
2893 ];
2894
2895 let mut trace = ReplayTrace::new(TraceMetadata::new(42));
2896 for ev in &replay_events {
2897 trace.push(ev.clone());
2898 }
2899
2900 let bytes = trace.to_bytes().expect("serialize");
2902 let loaded = ReplayTrace::from_bytes(&bytes).expect("deserialize");
2903
2904 assert_eq!(loaded.metadata.seed, 42);
2905 assert_eq!(loaded.events.len(), 3);
2906 assert_eq!(loaded.events, replay_events);
2907
2908 crate::test_complete!("golden_replay_serialization_round_trip");
2909 }
2910
2911 #[test]
2912 fn golden_crash_pack_json_round_trip() {
2913 init_test("golden_crash_pack_json_round_trip");
2914
2915 let events = golden_failure_events();
2916 let pack = CrashPack::builder(golden_config())
2917 .failure(golden_failure_info())
2918 .from_trace(&events)
2919 .oracle_violations(vec!["invariant-x".into()])
2920 .build()
2921 .expect("crash pack builder should have failure metadata");
2922
2923 let writer = MemoryCrashPackWriter::new();
2924 writer.write(&pack).unwrap();
2925 let written = writer.written();
2926 let json = &written[0].1;
2927
2928 let parsed: serde_json::Value = serde_json::from_str(json).unwrap();
2930 assert_eq!(parsed["manifest"]["config"]["seed"], 42);
2931 assert_eq!(parsed["manifest"]["config"]["config_hash"], 0xDEAD_u64);
2932 assert_eq!(parsed["manifest"]["event_count"], 6);
2933 assert_ne!(parsed["manifest"]["fingerprint"], 0);
2934 assert_eq!(parsed["oracle_violations"][0], "invariant-x");
2935
2936 let prefix = &parsed["canonical_prefix"];
2938 assert!(prefix.is_array());
2939 assert_eq!(prefix.as_array().unwrap().len(), 4); crate::test_complete!("golden_crash_pack_json_round_trip");
2942 }
2943
2944 #[test]
2945 fn golden_minimization_integration() {
2946 use crate::trace::divergence::{MinimizationConfig, minimize_divergent_prefix};
2947 use crate::trace::replay::{ReplayEvent, ReplayTrace, TraceMetadata};
2948
2949 init_test("golden_minimization_integration");
2950
2951 let replay_events: Vec<_> = (0..20)
2953 .map(|i| ReplayEvent::RngValue { value: i })
2954 .collect();
2955
2956 let trace = ReplayTrace {
2957 metadata: TraceMetadata::new(42),
2958 events: replay_events,
2959 cursor: 0,
2960 };
2961
2962 let threshold = 12;
2964 let result = minimize_divergent_prefix(&trace, &MinimizationConfig::default(), |prefix| {
2965 prefix.len() >= threshold
2966 });
2967
2968 assert_eq!(result.minimized_len, threshold);
2969 assert_eq!(result.original_len, 20);
2970 assert!(!result.truncated);
2971
2972 let pack = CrashPack::builder(golden_config())
2974 .failure(golden_failure_info())
2975 .from_trace(&golden_failure_events())
2976 .divergent_prefix(result.prefix.events)
2977 .build()
2978 .expect("crash pack builder should have failure metadata");
2979
2980 assert!(pack.has_divergent_prefix());
2981 assert_eq!(pack.divergent_prefix.len(), threshold);
2982
2983 crate::test_complete!("golden_minimization_integration");
2984 }
2985
2986 #[test]
3005 fn walkthrough_01_forced_failure_and_emission() {
3006 init_test("walkthrough_01_forced_failure_and_emission");
3007
3008 let events = vec![
3013 TraceEvent::region_created(1, Time::ZERO, rid(1), None),
3014 TraceEvent::spawn(2, Time::ZERO, tid(1), rid(1)),
3015 TraceEvent::spawn(3, Time::ZERO, tid(2), rid(1)),
3016 TraceEvent::poll(4, Time::from_nanos(100), tid(1), rid(1)),
3017 TraceEvent::poll(5, Time::from_nanos(100), tid(2), rid(1)),
3018 TraceEvent::complete(6, Time::from_nanos(200), tid(1), rid(1)),
3020 ];
3021
3022 let failure = FailureInfo {
3024 task: tid(2),
3025 region: rid(1),
3026 outcome: FailureOutcome::Panicked {
3027 message: "assertion failed: balance >= 0".to_string(),
3028 },
3029 virtual_time: Time::from_nanos(200),
3030 };
3031
3032 let config = CrashPackConfig {
3037 seed: 42,
3038 config_hash: 0xCAFE,
3039 worker_count: 2,
3040 max_steps: Some(500),
3041 commit_hash: Some("a1b2c3d".to_string()),
3042 };
3043
3044 let pack = CrashPack::builder(config)
3045 .failure(failure)
3046 .from_trace(&events)
3047 .oracle_violations(vec!["balance-invariant".to_string()])
3048 .build()
3049 .expect("crash pack builder should have failure metadata");
3050
3051 assert_eq!(pack.seed(), 42);
3053 assert_eq!(pack.manifest.schema_version, CRASHPACK_SCHEMA_VERSION);
3054 assert_eq!(pack.manifest.event_count, 6);
3055 assert!(
3056 pack.manifest.fingerprint != 0,
3057 "fingerprint should be non-zero"
3058 );
3059 assert!(pack.has_violations());
3060 assert_eq!(pack.oracle_violations, vec!["balance-invariant"]);
3061
3062 assert!(
3064 !pack.canonical_prefix.is_empty(),
3065 "canonical prefix should have Foata layers"
3066 );
3067
3068 assert!(
3070 pack.manifest
3071 .has_attachment(&AttachmentKind::CanonicalPrefix)
3072 );
3073 assert!(
3074 pack.manifest
3075 .has_attachment(&AttachmentKind::OracleViolations)
3076 );
3077
3078 crate::test_complete!("walkthrough_01_forced_failure_and_emission");
3079 }
3080
3081 #[test]
3086 fn walkthrough_02_write_and_read_artifact() {
3087 init_test("walkthrough_02_write_and_read_artifact");
3088
3089 let pack = walkthrough_pack();
3090
3091 let writer = MemoryCrashPackWriter::new();
3093 let artifact = writer.write(&pack).expect("write should succeed");
3094
3095 assert!(
3097 artifact.path().starts_with("crashpack-000000000000002a-"),
3098 "path should encode seed 42 (0x2a): {}",
3099 artifact.path()
3100 );
3101 assert!(
3102 artifact.path().ends_with("-v1.json"),
3103 "path should end with schema version: {}",
3104 artifact.path()
3105 );
3106
3107 let written = writer.written();
3109 assert_eq!(written.len(), 1);
3110 let json = &written[0].1;
3111 let parsed: serde_json::Value = serde_json::from_str(json).expect("valid JSON");
3112
3113 assert_eq!(parsed["manifest"]["config"]["seed"], 42);
3115 assert_eq!(parsed["manifest"]["schema_version"], 1);
3116
3117 assert!(
3119 parsed["failure"]["outcome"]["Panicked"]["message"]
3120 .as_str()
3121 .unwrap()
3122 .contains("balance >= 0"),
3123 "failure message should be preserved"
3124 );
3125
3126 crate::test_complete!("walkthrough_02_write_and_read_artifact");
3127 }
3128
3129 #[test]
3134 fn walkthrough_03_fingerprint_interpretation() {
3135 use crate::trace::canonicalize::trace_fingerprint;
3136
3137 init_test("walkthrough_03_fingerprint_interpretation");
3138
3139 let schedule_a = vec![
3141 TraceEvent::region_created(1, Time::ZERO, rid(1), None),
3142 TraceEvent::spawn(2, Time::ZERO, tid(1), rid(1)),
3143 TraceEvent::spawn(3, Time::ZERO, tid(2), rid(1)),
3144 TraceEvent::poll(4, Time::from_nanos(100), tid(1), rid(1)),
3145 TraceEvent::poll(5, Time::from_nanos(100), tid(2), rid(1)),
3146 TraceEvent::complete(6, Time::from_nanos(200), tid(1), rid(1)),
3147 ];
3148
3149 let schedule_b = vec![
3151 TraceEvent::region_created(1, Time::ZERO, rid(1), None),
3152 TraceEvent::spawn(2, Time::ZERO, tid(1), rid(1)),
3153 TraceEvent::spawn(3, Time::ZERO, tid(2), rid(1)),
3154 TraceEvent::poll(4, Time::from_nanos(100), tid(2), rid(1)), TraceEvent::poll(5, Time::from_nanos(100), tid(1), rid(1)), TraceEvent::complete(6, Time::from_nanos(200), tid(1), rid(1)),
3157 ];
3158
3159 let fp_a = trace_fingerprint(&schedule_a);
3160 let fp_b = trace_fingerprint(&schedule_b);
3161
3162 assert_eq!(
3166 fp_a, fp_b,
3167 "equivalent schedules should have the same canonical fingerprint"
3168 );
3169
3170 crate::test_complete!("walkthrough_03_fingerprint_interpretation");
3171 }
3172
3173 #[test]
3178 fn walkthrough_04_replay_command() {
3179 init_test("walkthrough_04_replay_command");
3180
3181 let pack = walkthrough_pack();
3182
3183 let replay = pack.replay_command(None);
3185 assert_eq!(replay.program, "cargo");
3186 assert!(replay.args.contains(&"--seed".to_string()));
3187 assert!(replay.args.contains(&"42".to_string()));
3188
3189 assert!(
3191 replay.command_line.contains("cargo test"),
3192 "command line should contain cargo test: {}",
3193 replay.command_line
3194 );
3195 assert!(
3196 replay.command_line.contains("--seed 42"),
3197 "command line should contain seed: {}",
3198 replay.command_line
3199 );
3200
3201 let replay_with_path = pack.replay_command(Some("/tmp/crashpacks/my_pack.json"));
3203 assert!(
3204 replay_with_path
3205 .command_line
3206 .contains("/tmp/crashpacks/my_pack.json"),
3207 "command line should reference artifact: {}",
3208 replay_with_path.command_line
3209 );
3210
3211 let cli_replay =
3213 ReplayCommand::from_config_cli(&pack.manifest.config, "/tmp/crashpack.json");
3214 assert_eq!(cli_replay.program, "asupersync");
3215 assert!(
3216 cli_replay.command_line.contains("trace replay"),
3217 "CLI mode should use 'trace replay' subcommand: {}",
3218 cli_replay.command_line
3219 );
3220
3221 let display = format!("{replay}");
3223 assert_eq!(display, replay.command_line);
3224
3225 crate::test_complete!("walkthrough_04_replay_command");
3226 }
3227
3228 #[test]
3233 fn walkthrough_05_minimization() {
3234 use crate::trace::divergence::{MinimizationConfig, minimize_divergent_prefix};
3235 use crate::trace::replay::{ReplayEvent, ReplayTrace, TraceMetadata};
3236
3237 init_test("walkthrough_05_minimization");
3238
3239 let replay_events: Vec<_> = (0..50)
3241 .map(|i| ReplayEvent::RngValue { value: i })
3242 .collect();
3243
3244 let trace = ReplayTrace {
3245 metadata: TraceMetadata::new(42),
3246 events: replay_events,
3247 cursor: 0,
3248 };
3249
3250 let failure_threshold = 15;
3252 let result = minimize_divergent_prefix(&trace, &MinimizationConfig::default(), |prefix| {
3253 prefix.len() >= failure_threshold
3254 });
3255
3256 assert_eq!(result.minimized_len, failure_threshold);
3257 assert_eq!(result.original_len, 50);
3258
3259 let config = CrashPackConfig {
3261 seed: 42,
3262 config_hash: 0xCAFE,
3263 worker_count: 2,
3264 max_steps: Some(500),
3265 commit_hash: Some("a1b2c3d".to_string()),
3266 };
3267
3268 let failure = FailureInfo {
3269 task: tid(2),
3270 region: rid(1),
3271 outcome: FailureOutcome::Panicked {
3272 message: "assertion failed: balance >= 0".to_string(),
3273 },
3274 virtual_time: Time::from_nanos(200),
3275 };
3276
3277 let pack = CrashPack::builder(config)
3278 .failure(failure)
3279 .divergent_prefix(result.prefix.events)
3280 .fingerprint(0xABCD)
3281 .build()
3282 .expect("crash pack builder should have failure metadata");
3283
3284 assert!(pack.has_divergent_prefix());
3285 assert_eq!(
3286 pack.divergent_prefix.len(),
3287 failure_threshold,
3288 "minimized prefix should be {failure_threshold} events, not {}",
3289 pack.divergent_prefix.len()
3290 );
3291
3292 assert!(
3294 pack.manifest
3295 .has_attachment(&AttachmentKind::DivergentPrefix)
3296 );
3297 let att = pack
3298 .manifest
3299 .attachment(&AttachmentKind::DivergentPrefix)
3300 .unwrap();
3301 assert_eq!(att.item_count, failure_threshold as u64);
3302
3303 crate::test_complete!("walkthrough_05_minimization");
3304 }
3305
3306 fn walkthrough_pack() -> CrashPack {
3308 let events = vec![
3309 TraceEvent::region_created(1, Time::ZERO, rid(1), None),
3310 TraceEvent::spawn(2, Time::ZERO, tid(1), rid(1)),
3311 TraceEvent::spawn(3, Time::ZERO, tid(2), rid(1)),
3312 TraceEvent::poll(4, Time::from_nanos(100), tid(1), rid(1)),
3313 TraceEvent::poll(5, Time::from_nanos(100), tid(2), rid(1)),
3314 TraceEvent::complete(6, Time::from_nanos(200), tid(1), rid(1)),
3315 ];
3316
3317 let config = CrashPackConfig {
3318 seed: 42,
3319 config_hash: 0xCAFE,
3320 worker_count: 2,
3321 max_steps: Some(500),
3322 commit_hash: Some("a1b2c3d".to_string()),
3323 };
3324
3325 let failure = FailureInfo {
3326 task: tid(2),
3327 region: rid(1),
3328 outcome: FailureOutcome::Panicked {
3329 message: "assertion failed: balance >= 0".to_string(),
3330 },
3331 virtual_time: Time::from_nanos(200),
3332 };
3333
3334 CrashPack::builder(config)
3335 .failure(failure)
3336 .from_trace(&events)
3337 .oracle_violations(vec!["balance-invariant".to_string()])
3338 .build()
3339 .expect("crash pack builder should have failure metadata")
3340 }
3341
3342 #[test]
3345 fn crash_pack_config_debug_clone_eq_default() {
3346 let c = CrashPackConfig::default();
3347 assert_eq!(c.seed, 0);
3348 assert_eq!(c.config_hash, 0);
3349 assert_eq!(c.worker_count, 1);
3350 assert_eq!(c.max_steps, None);
3351 assert_eq!(c.commit_hash, None);
3352 let c2 = c.clone();
3353 assert_eq!(c, c2);
3354 let dbg = format!("{c:?}");
3355 assert!(dbg.contains("CrashPackConfig"));
3356 }
3357
3358 #[test]
3359 fn failure_outcome_debug_clone_eq() {
3360 let e = FailureOutcome::Err;
3361 let e2 = e.clone();
3362 assert_eq!(e, e2);
3363 assert_ne!(
3364 e,
3365 FailureOutcome::Panicked {
3366 message: "boom".into()
3367 }
3368 );
3369 let c = FailureOutcome::Cancelled {
3370 cancel_kind: CancelKind::User,
3371 };
3372 let c2 = c.clone();
3373 assert_eq!(c, c2);
3374 let dbg = format!("{e:?}");
3375 assert!(dbg.contains("Err"));
3376 }
3377
3378 #[test]
3379 fn attachment_kind_debug_clone_eq() {
3380 let a = AttachmentKind::CanonicalPrefix;
3381 let a2 = a.clone();
3382 assert_eq!(a, a2);
3383 assert_ne!(a, AttachmentKind::DivergentPrefix);
3384 assert_ne!(a, AttachmentKind::EvidenceLedger);
3385 assert_ne!(a, AttachmentKind::SupervisionLog);
3386 assert_ne!(a, AttachmentKind::OracleViolations);
3387 let custom = AttachmentKind::Custom {
3388 tag: "my_data".into(),
3389 };
3390 let custom2 = custom.clone();
3391 assert_eq!(custom, custom2);
3392 let dbg = format!("{a:?}");
3393 assert!(dbg.contains("CanonicalPrefix"));
3394 }
3395
3396 #[test]
3397 fn manifest_validation_error_debug_clone_eq() {
3398 let e = ManifestValidationError::VersionTooNew {
3399 manifest_version: 5,
3400 supported_version: 1,
3401 };
3402 let e2 = e.clone();
3403 assert_eq!(e, e2);
3404 assert_ne!(
3405 e,
3406 ManifestValidationError::VersionTooOld {
3407 manifest_version: 0,
3408 minimum_version: 1,
3409 }
3410 );
3411 let dbg = format!("{e:?}");
3412 assert!(dbg.contains("VersionTooNew"));
3413 }
3414
3415 #[test]
3416 fn evidence_entry_snapshot_debug_clone_eq() {
3417 let s = EvidenceEntrySnapshot {
3418 birth: 0,
3419 death: 5,
3420 is_novel: true,
3421 persistence: Some(5),
3422 };
3423 let s2 = s.clone();
3424 assert_eq!(s, s2);
3425 let dbg = format!("{s:?}");
3426 assert!(dbg.contains("EvidenceEntrySnapshot"));
3427 }
3428
3429 #[test]
3430 fn supervision_snapshot_debug_clone_eq() {
3431 let s = SupervisionSnapshot {
3432 virtual_time: Time::from_secs(1),
3433 task: tid(1),
3434 region: rid(0),
3435 decision: "restart".into(),
3436 context: Some("attempt 2".into()),
3437 };
3438 let s2 = s.clone();
3439 assert_eq!(s, s2);
3440 let dbg = format!("{s:?}");
3441 assert!(dbg.contains("SupervisionSnapshot"));
3442 }
3443
3444 #[test]
3445 fn manifest_attachment_debug_clone_eq() {
3446 let a = ManifestAttachment {
3447 kind: AttachmentKind::EvidenceLedger,
3448 item_count: 10,
3449 size_hint_bytes: 256,
3450 };
3451 let a2 = a.clone();
3452 assert_eq!(a, a2);
3453 let dbg = format!("{a:?}");
3454 assert!(dbg.contains("ManifestAttachment"));
3455 }
3456
3457 #[test]
3458 fn crash_pack_manifest_debug_clone_eq() {
3459 let m = CrashPackManifest {
3460 schema_version: CRASHPACK_SCHEMA_VERSION,
3461 config: CrashPackConfig::default(),
3462 fingerprint: 0xABCD,
3463 event_count: 100,
3464 created_at: 0,
3465 attachments: vec![],
3466 };
3467 let m2 = m.clone();
3468 assert_eq!(m, m2);
3469 let dbg = format!("{m:?}");
3470 assert!(dbg.contains("CrashPackManifest"));
3471 }
3472}