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 sha2::{Digest, Sha256};
55use std::fmt;
56
57pub const CRASHPACK_SCHEMA_VERSION: u32 = 1;
65
66#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
76pub struct CrashPackConfig {
77 pub seed: u64,
79
80 pub config_hash: u64,
84
85 pub worker_count: usize,
87
88 #[serde(default, skip_serializing_if = "Option::is_none")]
90 pub max_steps: Option<u64>,
91
92 #[serde(default, skip_serializing_if = "Option::is_none")]
96 pub commit_hash: Option<String>,
97}
98
99impl Default for CrashPackConfig {
100 fn default() -> Self {
101 Self {
102 seed: 0,
103 config_hash: 0,
104 worker_count: 1,
105 max_steps: None,
106 commit_hash: None,
107 }
108 }
109}
110
111#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
119pub struct FailureInfo {
120 pub task: TaskId,
122
123 pub region: RegionId,
125
126 pub outcome: FailureOutcome,
128
129 pub virtual_time: Time,
131}
132
133#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
138pub enum FailureOutcome {
139 Err,
141 Cancelled {
143 cancel_kind: CancelKind,
145 },
146 Panicked {
148 message: String,
150 },
151}
152
153#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
155pub struct EvidenceEntrySnapshot {
156 pub birth: usize,
158 pub death: usize,
160 pub is_novel: bool,
162 pub persistence: Option<u64>,
164}
165
166impl From<EvidenceEntry> for EvidenceEntrySnapshot {
167 fn from(e: EvidenceEntry) -> Self {
168 Self {
169 birth: e.class.birth,
170 death: e.class.death,
171 is_novel: e.is_novel,
172 persistence: e.persistence,
173 }
174 }
175}
176
177#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
186pub struct SupervisionSnapshot {
187 pub virtual_time: Time,
189
190 pub task: TaskId,
192
193 pub region: RegionId,
195
196 pub decision: String,
198
199 pub context: Option<String>,
201}
202
203pub const MINIMUM_SUPPORTED_SCHEMA_VERSION: u32 = 1;
212
213#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
218#[serde(tag = "kind")]
219pub enum AttachmentKind {
220 CanonicalPrefix,
222 DivergentPrefix,
224 EvidenceLedger,
226 SupervisionLog,
228 OracleViolations,
230 Custom {
232 tag: String,
234 },
235}
236
237#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
242pub struct ManifestAttachment {
243 #[serde(flatten)]
245 pub kind: AttachmentKind,
246
247 pub item_count: u64,
249
250 #[serde(default, skip_serializing_if = "is_zero")]
252 pub size_hint_bytes: u64,
253}
254
255#[allow(clippy::trivially_copy_pass_by_ref)] fn is_zero(v: &u64) -> bool {
258 *v == 0
259}
260
261#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
276pub struct CrashPackManifest {
277 pub schema_version: u32,
279
280 pub config: CrashPackConfig,
282
283 pub fingerprint: u64,
288
289 pub event_count: u64,
291
292 pub created_at: u64,
294
295 #[serde(default, skip_serializing_if = "Vec::is_empty")]
300 pub attachments: Vec<ManifestAttachment>,
301
302 #[serde(default, skip_serializing_if = "Option::is_none")]
311 pub content_checksum: Option<String>,
312}
313
314#[derive(Debug, Clone, PartialEq, Eq)]
316pub enum ManifestValidationError {
317 VersionTooNew {
319 manifest_version: u32,
321 supported_version: u32,
323 },
324 VersionTooOld {
326 manifest_version: u32,
328 minimum_version: u32,
330 },
331}
332
333impl std::fmt::Display for ManifestValidationError {
334 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
335 match self {
336 Self::VersionTooNew {
337 manifest_version,
338 supported_version,
339 } => write!(
340 f,
341 "crash pack schema v{manifest_version} is newer than supported v{supported_version}"
342 ),
343 Self::VersionTooOld {
344 manifest_version,
345 minimum_version,
346 } => write!(
347 f,
348 "crash pack schema v{manifest_version} is older than minimum v{minimum_version}"
349 ),
350 }
351 }
352}
353
354impl std::error::Error for ManifestValidationError {}
355
356impl CrashPackManifest {
357 #[must_use]
363 pub fn new(config: CrashPackConfig, fingerprint: u64, event_count: u64) -> Self {
364 Self::new_with_created_at(config, fingerprint, event_count, wall_clock_nanos())
365 }
366
367 #[must_use]
374 pub fn new_with_created_at(
375 config: CrashPackConfig,
376 fingerprint: u64,
377 event_count: u64,
378 created_at: u64,
379 ) -> Self {
380 Self {
381 schema_version: CRASHPACK_SCHEMA_VERSION,
382 config,
383 fingerprint,
384 event_count,
385 created_at,
386 attachments: Vec::new(),
387 content_checksum: None,
388 }
389 }
390
391 pub fn validate(&self) -> Result<(), ManifestValidationError> {
396 if self.schema_version > CRASHPACK_SCHEMA_VERSION {
397 return Err(ManifestValidationError::VersionTooNew {
398 manifest_version: self.schema_version,
399 supported_version: CRASHPACK_SCHEMA_VERSION,
400 });
401 }
402 if self.schema_version < MINIMUM_SUPPORTED_SCHEMA_VERSION {
403 return Err(ManifestValidationError::VersionTooOld {
404 manifest_version: self.schema_version,
405 minimum_version: MINIMUM_SUPPORTED_SCHEMA_VERSION,
406 });
407 }
408 Ok(())
409 }
410
411 #[must_use]
413 pub fn is_compatible(&self) -> bool {
414 self.validate().is_ok()
415 }
416
417 #[must_use]
419 pub fn attachment(&self, kind: &AttachmentKind) -> Option<&ManifestAttachment> {
420 self.attachments.iter().find(|a| &a.kind == kind)
421 }
422
423 #[must_use]
425 pub fn has_attachment(&self, kind: &AttachmentKind) -> bool {
426 self.attachment(kind).is_some()
427 }
428}
429
430#[derive(Debug, Clone, Serialize)]
454pub struct CrashPack {
455 pub manifest: CrashPackManifest,
457
458 pub failure: FailureInfo,
460
461 pub canonical_prefix: Vec<Vec<TraceEventKey>>,
466
467 pub divergent_prefix: Vec<ReplayEvent>,
473
474 pub evidence: Vec<EvidenceEntrySnapshot>,
479
480 pub supervision_log: Vec<SupervisionSnapshot>,
485
486 pub oracle_violations: Vec<String>,
490
491 #[serde(default, skip_serializing_if = "Option::is_none")]
496 pub replay: Option<ReplayCommand>,
497}
498
499impl PartialEq for CrashPack {
500 fn eq(&self, other: &Self) -> bool {
501 self.manifest.schema_version == other.manifest.schema_version
503 && self.manifest.config == other.manifest.config
504 && self.manifest.fingerprint == other.manifest.fingerprint
505 && self.manifest.event_count == other.manifest.event_count
506 && self.manifest.attachments == other.manifest.attachments
507 && self.failure == other.failure
508 && self.canonical_prefix == other.canonical_prefix
509 && self.divergent_prefix == other.divergent_prefix
510 && self.evidence == other.evidence
511 && self.supervision_log == other.supervision_log
512 && self.oracle_violations == other.oracle_violations
513 && self.replay == other.replay
514 }
515}
516
517impl Eq for CrashPack {}
518
519impl CrashPack {
520 #[must_use]
522 pub fn builder(config: CrashPackConfig) -> CrashPackBuilder {
523 CrashPackBuilder {
524 config,
525 failure: None,
526 fingerprint: 0,
527 event_count: 0,
528 created_at: None,
529 canonical_prefix: Vec::new(),
530 divergent_prefix: Vec::new(),
531 evidence: Vec::new(),
532 supervision_log: Vec::new(),
533 oracle_violations: Vec::new(),
534 replay: None,
535 }
536 }
537
538 #[must_use]
543 pub fn replay_command(&self, artifact_path: Option<&str>) -> ReplayCommand {
544 ReplayCommand::from_config(&self.manifest.config, artifact_path)
545 }
546
547 #[must_use]
549 pub fn has_violations(&self) -> bool {
550 !self.oracle_violations.is_empty()
551 }
552
553 #[must_use]
555 pub fn has_divergent_prefix(&self) -> bool {
556 !self.divergent_prefix.is_empty()
557 }
558
559 #[must_use]
561 pub fn seed(&self) -> u64 {
562 self.manifest.config.seed
563 }
564
565 #[must_use]
567 pub fn fingerprint(&self) -> u64 {
568 self.manifest.fingerprint
569 }
570
571 #[must_use]
580 pub fn verify_integrity(&self) -> bool {
581 let Some(stored) = self.manifest.content_checksum.as_deref() else {
582 return false;
583 };
584 let mut probe = self.clone();
585 probe.manifest.content_checksum = None;
586 compute_content_checksum(&probe).as_str() == stored
587 }
588}
589
590fn compute_content_checksum(pack: &CrashPack) -> String {
598 let bytes = serde_json::to_vec(pack).unwrap_or_default();
599 hex::encode(Sha256::digest(&bytes))
600}
601
602#[derive(Debug)]
611pub struct CrashPackBuilder {
612 config: CrashPackConfig,
613 failure: Option<FailureInfo>,
614 fingerprint: u64,
615 event_count: u64,
616 created_at: Option<u64>,
617 canonical_prefix: Vec<Vec<TraceEventKey>>,
618 divergent_prefix: Vec<ReplayEvent>,
619 evidence: Vec<EvidenceEntrySnapshot>,
620 supervision_log: Vec<SupervisionSnapshot>,
621 oracle_violations: Vec<String>,
622 replay: Option<ReplayCommand>,
623}
624
625#[derive(Debug, Clone, Copy, PartialEq, Eq)]
627pub enum CrashPackBuildError {
628 MissingFailure,
630}
631
632impl fmt::Display for CrashPackBuildError {
633 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
634 match self {
635 Self::MissingFailure => f.write_str("crash pack builder requires failure metadata"),
636 }
637 }
638}
639
640impl std::error::Error for CrashPackBuildError {}
641
642impl CrashPackBuilder {
643 #[must_use]
645 pub fn failure(mut self, failure: FailureInfo) -> Self {
646 self.failure = Some(failure);
647 self
648 }
649
650 #[must_use]
652 pub fn fingerprint(mut self, fingerprint: u64) -> Self {
653 self.fingerprint = fingerprint;
654 self
655 }
656
657 #[must_use]
659 pub fn event_count(mut self, count: u64) -> Self {
660 self.event_count = count;
661 self
662 }
663
664 #[must_use]
669 pub fn created_at(mut self, created_at: u64) -> Self {
670 self.created_at = Some(created_at);
671 self
672 }
673
674 #[must_use]
685 pub fn from_trace(mut self, events: &[TraceEvent]) -> Self {
686 let foata = canonicalize(events);
687 self.canonical_prefix = foata
688 .layers()
689 .iter()
690 .map(|layer| layer.iter().map(trace_event_key).collect())
691 .collect();
692 self.fingerprint = trace_fingerprint(events);
693 self.event_count = events.len() as u64;
694 self
695 }
696
697 #[must_use]
699 pub fn canonical_prefix(mut self, prefix: Vec<Vec<TraceEventKey>>) -> Self {
700 self.canonical_prefix = prefix;
701 self
702 }
703
704 #[must_use]
706 pub fn divergent_prefix(mut self, prefix: Vec<ReplayEvent>) -> Self {
707 self.divergent_prefix = prefix;
708 self
709 }
710
711 #[must_use]
713 pub fn evidence(mut self, entries: Vec<EvidenceEntry>) -> Self {
714 self.evidence = entries
715 .into_iter()
716 .map(EvidenceEntrySnapshot::from)
717 .collect();
718 self
719 }
720
721 #[must_use]
723 pub fn supervision_snapshot(mut self, snapshot: SupervisionSnapshot) -> Self {
724 self.supervision_log.push(snapshot);
725 self
726 }
727
728 #[must_use]
730 pub fn oracle_violations(mut self, violations: Vec<String>) -> Self {
731 let mut v = violations;
732 v.sort();
733 v.dedup();
734 self.oracle_violations = v;
735 self
736 }
737
738 #[must_use]
740 pub fn replay(mut self, command: ReplayCommand) -> Self {
741 self.replay = Some(command);
742 self
743 }
744
745 pub fn build(self) -> Result<CrashPack, CrashPackBuildError> {
752 let failure = self.failure.ok_or(CrashPackBuildError::MissingFailure)?;
753
754 let mut supervision_log = self.supervision_log;
758 supervision_log.sort_by(|a, b| {
759 a.virtual_time
760 .cmp(&b.virtual_time)
761 .then_with(|| a.task.cmp(&b.task))
762 .then_with(|| a.region.cmp(&b.region))
763 .then_with(|| a.decision.cmp(&b.decision))
764 .then_with(|| a.context.cmp(&b.context))
765 });
766
767 let mut attachments = Vec::new();
769 if !self.canonical_prefix.is_empty() {
770 let item_count: u64 = self
771 .canonical_prefix
772 .iter()
773 .map(|layer| layer.len() as u64)
774 .sum();
775 attachments.push(ManifestAttachment {
776 kind: AttachmentKind::CanonicalPrefix,
777 item_count,
778 size_hint_bytes: 0,
779 });
780 }
781 if !self.divergent_prefix.is_empty() {
782 attachments.push(ManifestAttachment {
783 kind: AttachmentKind::DivergentPrefix,
784 item_count: self.divergent_prefix.len() as u64,
785 size_hint_bytes: 0,
786 });
787 }
788 if !self.evidence.is_empty() {
789 attachments.push(ManifestAttachment {
790 kind: AttachmentKind::EvidenceLedger,
791 item_count: self.evidence.len() as u64,
792 size_hint_bytes: 0,
793 });
794 }
795 if !supervision_log.is_empty() {
796 attachments.push(ManifestAttachment {
797 kind: AttachmentKind::SupervisionLog,
798 item_count: supervision_log.len() as u64,
799 size_hint_bytes: 0,
800 });
801 }
802 if !self.oracle_violations.is_empty() {
803 attachments.push(ManifestAttachment {
804 kind: AttachmentKind::OracleViolations,
805 item_count: self.oracle_violations.len() as u64,
806 size_hint_bytes: 0,
807 });
808 }
809
810 let mut manifest = if let Some(created_at) = self.created_at {
811 CrashPackManifest::new_with_created_at(
812 self.config,
813 self.fingerprint,
814 self.event_count,
815 created_at,
816 )
817 } else {
818 CrashPackManifest::new(self.config, self.fingerprint, self.event_count)
819 };
820 manifest.attachments = attachments;
821
822 let mut pack = CrashPack {
823 manifest,
824 failure,
825 canonical_prefix: self.canonical_prefix,
826 divergent_prefix: self.divergent_prefix,
827 evidence: self.evidence,
828 supervision_log,
829 oracle_violations: self.oracle_violations,
830 replay: self.replay,
831 };
832 let checksum = compute_content_checksum(&pack);
836 pack.manifest.content_checksum = Some(checksum);
837 Ok(pack)
838 }
839}
840
841#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
847pub struct ReplayEnvVar {
848 pub key: String,
850 pub value: String,
852}
853
854#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
870pub struct ReplayCommand {
871 pub program: String,
873
874 pub args: Vec<String>,
876
877 #[serde(default, skip_serializing_if = "Vec::is_empty")]
879 pub env: Vec<ReplayEnvVar>,
880
881 pub command_line: String,
885}
886
887impl ReplayCommand {
888 #[must_use]
893 pub fn from_config(config: &CrashPackConfig, artifact_path: Option<&str>) -> Self {
894 let mut args = vec![
895 "test".to_string(),
896 "--lib".to_string(),
897 "--".to_string(),
898 "--seed".to_string(),
899 config.seed.to_string(),
900 ];
901
902 let mut env = Vec::new();
903
904 env.push(ReplayEnvVar {
905 key: "ASUPERSYNC_WORKERS".to_string(),
906 value: config.worker_count.to_string(),
907 });
908
909 if let Some(max_steps) = config.max_steps {
910 env.push(ReplayEnvVar {
911 key: "ASUPERSYNC_MAX_STEPS".to_string(),
912 value: max_steps.to_string(),
913 });
914 }
915
916 if let Some(path) = artifact_path {
917 args.push("--crashpack".to_string());
918 args.push(path.to_string());
919 }
920
921 let command_line = build_command_line("cargo", &args, &env);
922
923 Self {
924 program: "cargo".to_string(),
925 args,
926 env,
927 command_line,
928 }
929 }
930
931 #[must_use]
933 pub fn from_config_cli(config: &CrashPackConfig, artifact_path: &str) -> Self {
934 let mut args = vec![
935 "trace".to_string(),
936 "replay".to_string(),
937 "--seed".to_string(),
938 config.seed.to_string(),
939 "--workers".to_string(),
940 config.worker_count.to_string(),
941 ];
942
943 if let Some(max_steps) = config.max_steps {
944 args.push("--max-steps".to_string());
945 args.push(max_steps.to_string());
946 }
947
948 args.push(artifact_path.to_string());
949
950 let command_line = build_command_line("asupersync", &args, &[]);
951
952 Self {
953 program: "asupersync".to_string(),
954 args,
955 env: Vec::new(),
956 command_line,
957 }
958 }
959}
960
961impl std::fmt::Display for ReplayCommand {
962 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
963 write!(f, "{}", self.command_line)
964 }
965}
966
967fn build_command_line(program: &str, args: &[String], env: &[ReplayEnvVar]) -> String {
969 let mut parts = Vec::new();
970 for var in env {
971 parts.push(format!(
972 "{}={}",
973 shell_escape(&var.key),
974 shell_escape(&var.value)
975 ));
976 }
977 parts.push(program.to_string());
978 for arg in args {
979 parts.push(shell_escape(arg));
980 }
981 parts.join(" ")
982}
983
984fn shell_escape(s: &str) -> String {
989 if s.is_empty() {
990 return "''".to_string();
991 }
992 if s.chars()
993 .all(|c| c.is_alphanumeric() || matches!(c, '-' | '_' | '.' | '/' | ':' | '=' | ','))
994 {
995 s.to_string()
996 } else {
997 format!("'{}'", s.replace('\'', "'\\''"))
998 }
999}
1000
1001#[derive(Debug, Clone, PartialEq, Eq)]
1011pub struct ArtifactId {
1012 path: String,
1014}
1015
1016impl ArtifactId {
1017 #[must_use]
1019 pub fn path(&self) -> &str {
1020 &self.path
1021 }
1022}
1023
1024impl std::fmt::Display for ArtifactId {
1025 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1026 write!(f, "{}", self.path)
1027 }
1028}
1029
1030#[derive(Debug)]
1032pub enum CrashPackWriteError {
1033 Serialize(String),
1035 Io(std::io::Error),
1037}
1038
1039impl std::fmt::Display for CrashPackWriteError {
1040 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1041 match self {
1042 Self::Serialize(msg) => write!(f, "crash pack serialization failed: {msg}"),
1043 Self::Io(e) => write!(f, "crash pack I/O error: {e}"),
1044 }
1045 }
1046}
1047
1048impl std::error::Error for CrashPackWriteError {
1049 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
1050 match self {
1051 Self::Io(e) => Some(e),
1052 Self::Serialize(_) => None,
1053 }
1054 }
1055}
1056
1057pub trait CrashPackWriter: Send + Sync + std::fmt::Debug {
1069 fn write(&self, pack: &CrashPack) -> Result<ArtifactId, CrashPackWriteError>;
1071
1072 fn is_persistent(&self) -> bool;
1074
1075 fn name(&self) -> &'static str;
1077}
1078
1079#[must_use]
1083pub fn artifact_filename(pack: &CrashPack) -> String {
1084 format!(
1085 "crashpack-{:016x}-{:016x}-{:016x}-v{}.json",
1086 pack.seed(),
1087 pack.manifest.config.config_hash,
1088 pack.fingerprint(),
1089 pack.manifest.schema_version,
1090 )
1091}
1092
1093#[derive(Debug)]
1099pub struct FileCrashPackWriter {
1100 base_dir: std::path::PathBuf,
1101}
1102
1103impl FileCrashPackWriter {
1104 #[must_use]
1108 pub fn new(base_dir: std::path::PathBuf) -> Self {
1109 Self { base_dir }
1110 }
1111
1112 #[must_use]
1114 pub fn base_dir(&self) -> &std::path::Path {
1115 &self.base_dir
1116 }
1117}
1118
1119impl CrashPackWriter for FileCrashPackWriter {
1120 fn write(&self, pack: &CrashPack) -> Result<ArtifactId, CrashPackWriteError> {
1121 let filename = artifact_filename(pack);
1122 let path = self.base_dir.join(&filename); let json = serde_json::to_string_pretty(pack)
1125 .map_err(|e| CrashPackWriteError::Serialize(e.to_string()))?;
1126
1127 let tmp_path = self
1134 .base_dir
1135 .join(format!(".{filename}.{}.tmp", std::process::id())); std::fs::write(&tmp_path, json.as_bytes()).map_err(CrashPackWriteError::Io)?;
1138 if let Err(e) = std::fs::rename(&tmp_path, &path) {
1139 let _ = std::fs::remove_file(&tmp_path);
1141 return Err(CrashPackWriteError::Io(e));
1142 }
1143
1144 Ok(ArtifactId {
1145 path: path.to_string_lossy().into_owned(),
1146 })
1147 }
1148
1149 fn is_persistent(&self) -> bool {
1150 true
1151 }
1152
1153 fn name(&self) -> &'static str {
1154 "file"
1155 }
1156}
1157
1158#[derive(Debug, Default)]
1162pub struct MemoryCrashPackWriter {
1163 packs: parking_lot::Mutex<Vec<(ArtifactId, String)>>,
1164}
1165
1166impl MemoryCrashPackWriter {
1167 #[must_use]
1169 pub fn new() -> Self {
1170 Self::default()
1171 }
1172
1173 pub fn written(&self) -> Vec<(ArtifactId, String)> {
1175 self.packs.lock().clone()
1176 }
1177
1178 #[must_use]
1180 pub fn count(&self) -> usize {
1181 self.packs.lock().len()
1182 }
1183}
1184
1185impl CrashPackWriter for MemoryCrashPackWriter {
1186 fn write(&self, pack: &CrashPack) -> Result<ArtifactId, CrashPackWriteError> {
1187 let filename = artifact_filename(pack);
1188 let json = serde_json::to_string_pretty(pack)
1189 .map_err(|e| CrashPackWriteError::Serialize(e.to_string()))?;
1190
1191 let artifact_id = ArtifactId { path: filename };
1192 self.packs.lock().push((artifact_id.clone(), json));
1193
1194 Ok(artifact_id)
1195 }
1196
1197 fn is_persistent(&self) -> bool {
1198 false
1199 }
1200
1201 fn name(&self) -> &'static str {
1202 "memory"
1203 }
1204}
1205
1206fn wall_clock_nanos() -> u64 {
1212 std::time::SystemTime::now()
1213 .duration_since(std::time::UNIX_EPOCH)
1214 .map_or(0, |d| d.as_nanos().min(u128::from(u64::MAX)) as u64)
1215}
1216
1217#[cfg(test)]
1222mod tests {
1223 #![allow(
1224 clippy::pedantic,
1225 clippy::nursery,
1226 clippy::expect_fun_call,
1227 clippy::map_unwrap_or,
1228 clippy::cast_possible_wrap,
1229 clippy::future_not_send
1230 )]
1231 use super::*;
1232 use crate::util::ArenaIndex;
1233
1234 fn init_test(name: &str) {
1235 crate::test_utils::init_test_logging();
1236 crate::test_phase!(name);
1237 }
1238
1239 fn tid(n: u32) -> TaskId {
1240 TaskId::from_arena(ArenaIndex::new(n, 0))
1241 }
1242
1243 fn rid(n: u32) -> RegionId {
1244 RegionId::from_arena(ArenaIndex::new(n, 0))
1245 }
1246
1247 fn sample_failure() -> FailureInfo {
1248 FailureInfo {
1249 task: tid(1),
1250 region: rid(0),
1251 outcome: FailureOutcome::Panicked {
1252 message: "test panic".to_string(),
1253 },
1254 virtual_time: Time::from_secs(5),
1255 }
1256 }
1257
1258 fn sample_config() -> CrashPackConfig {
1259 CrashPackConfig {
1260 seed: 42,
1261 config_hash: 0xDEAD,
1262 worker_count: 4,
1263 max_steps: Some(1000),
1264 commit_hash: Some("abc123".to_string()),
1265 }
1266 }
1267
1268 #[test]
1269 fn builder_missing_failure_returns_error() {
1270 init_test("builder_missing_failure_returns_error");
1271
1272 let err = CrashPack::builder(sample_config())
1273 .build()
1274 .expect_err("builder should fail closed without failure metadata");
1275
1276 assert_eq!(err, CrashPackBuildError::MissingFailure);
1277 assert_eq!(
1278 err.to_string(),
1279 "crash pack builder requires failure metadata"
1280 );
1281
1282 crate::test_complete!("builder_missing_failure_returns_error");
1283 }
1284
1285 #[test]
1291 fn manifest_new_with_created_at_uses_supplied_timestamp() {
1292 init_test("manifest_new_with_created_at_uses_supplied_timestamp");
1293 let manifest = CrashPackManifest::new_with_created_at(
1294 CrashPackConfig::default(),
1295 0xCAFE_BABE,
1296 42,
1297 1_700_000_000_000_000_000,
1298 );
1299 assert_eq!(manifest.created_at, 1_700_000_000_000_000_000);
1300 assert_eq!(manifest.fingerprint, 0xCAFE_BABE);
1301 assert_eq!(manifest.event_count, 42);
1302
1303 let other = CrashPackManifest::new_with_created_at(
1306 CrashPackConfig::default(),
1307 0xCAFE_BABE,
1308 42,
1309 1_700_000_000_000_000_000,
1310 );
1311 assert_eq!(manifest.created_at, other.created_at);
1312 crate::test_complete!("manifest_new_with_created_at_uses_supplied_timestamp");
1313 }
1314
1315 #[test]
1316 fn schema_version_is_set() {
1317 init_test("schema_version_is_set");
1318
1319 let pack = CrashPack::builder(sample_config())
1320 .failure(sample_failure())
1321 .build()
1322 .expect("crash pack builder should have failure metadata");
1323
1324 assert_eq!(pack.manifest.schema_version, CRASHPACK_SCHEMA_VERSION);
1325 assert_eq!(pack.manifest.schema_version, 1);
1326
1327 crate::test_complete!("schema_version_is_set");
1328 }
1329
1330 #[test]
1331 fn built_pack_carries_valid_content_checksum() {
1332 init_test("built_pack_carries_valid_content_checksum");
1333
1334 let pack = CrashPack::builder(sample_config())
1335 .failure(sample_failure())
1336 .fingerprint(0xCAFE_BABE)
1337 .oracle_violations(vec!["inv-1".into()])
1338 .build()
1339 .expect("crash pack builder should have failure metadata");
1340
1341 assert!(pack.manifest.content_checksum.is_some());
1343 assert_eq!(pack.manifest.content_checksum.as_deref().unwrap().len(), 64);
1345 assert!(pack.verify_integrity());
1347
1348 crate::test_complete!("built_pack_carries_valid_content_checksum");
1349 }
1350
1351 #[test]
1352 fn content_checksum_detects_body_tampering() {
1353 init_test("content_checksum_detects_body_tampering");
1354
1355 let pack = CrashPack::builder(sample_config())
1356 .failure(sample_failure())
1357 .fingerprint(0xCAFE_BABE)
1358 .build()
1359 .expect("crash pack builder should have failure metadata");
1360 assert!(pack.verify_integrity());
1361
1362 let mut tampered = pack.clone();
1364 tampered.oracle_violations.push("smuggled-violation".into());
1365 assert!(
1366 !tampered.verify_integrity(),
1367 "checksum must catch body tampering"
1368 );
1369
1370 let mut tampered_meta = pack;
1372 tampered_meta.manifest.fingerprint ^= 1;
1373 assert!(!tampered_meta.verify_integrity());
1374
1375 crate::test_complete!("content_checksum_detects_body_tampering");
1376 }
1377
1378 #[test]
1379 fn verify_integrity_false_without_checksum() {
1380 init_test("verify_integrity_false_without_checksum");
1381
1382 let mut pack = CrashPack::builder(sample_config())
1383 .failure(sample_failure())
1384 .build()
1385 .expect("crash pack builder should have failure metadata");
1386 pack.manifest.content_checksum = None;
1388 assert!(!pack.verify_integrity());
1389
1390 crate::test_complete!("verify_integrity_false_without_checksum");
1391 }
1392
1393 #[test]
1394 fn builder_sets_all_fields() {
1395 init_test("builder_sets_all_fields");
1396
1397 let pack = CrashPack::builder(sample_config())
1398 .failure(sample_failure())
1399 .fingerprint(0xCAFE_BABE)
1400 .event_count(500)
1401 .oracle_violations(vec!["inv-1".into(), "inv-2".into()])
1402 .build()
1403 .expect("crash pack builder should have failure metadata");
1404
1405 assert_eq!(pack.manifest.config.seed, 42);
1406 assert_eq!(pack.manifest.config.config_hash, 0xDEAD);
1407 assert_eq!(pack.manifest.config.worker_count, 4);
1408 assert_eq!(pack.manifest.config.max_steps, Some(1000));
1409 assert_eq!(pack.manifest.config.commit_hash.as_deref(), Some("abc123"));
1410 assert_eq!(pack.manifest.fingerprint, 0xCAFE_BABE);
1411 assert_eq!(pack.manifest.event_count, 500);
1412 assert_eq!(pack.failure.task, tid(1));
1413 assert_eq!(pack.failure.region, rid(0));
1414 assert_eq!(pack.failure.virtual_time, Time::from_secs(5));
1415 assert!(pack.has_violations());
1416 assert_eq!(pack.oracle_violations, vec!["inv-1", "inv-2"]);
1417 assert!(!pack.has_divergent_prefix());
1418
1419 crate::test_complete!("builder_sets_all_fields");
1420 }
1421
1422 #[test]
1423 fn default_config() {
1424 init_test("default_config");
1425
1426 let config = CrashPackConfig::default();
1427 assert_eq!(config.seed, 0);
1428 assert_eq!(config.config_hash, 0);
1429 assert_eq!(config.worker_count, 1);
1430 assert_eq!(config.max_steps, None);
1431 assert_eq!(config.commit_hash, None);
1432
1433 crate::test_complete!("default_config");
1434 }
1435
1436 #[test]
1437 fn seed_and_fingerprint_accessors() {
1438 init_test("seed_and_fingerprint_accessors");
1439
1440 let pack = CrashPack::builder(CrashPackConfig {
1441 seed: 999,
1442 ..Default::default()
1443 })
1444 .failure(sample_failure())
1445 .fingerprint(0x1234)
1446 .build()
1447 .expect("crash pack builder should have failure metadata");
1448
1449 assert_eq!(pack.seed(), 999);
1450 assert_eq!(pack.fingerprint(), 0x1234);
1451
1452 crate::test_complete!("seed_and_fingerprint_accessors");
1453 }
1454
1455 #[test]
1456 fn oracle_violations_sorted_and_deduped() {
1457 init_test("oracle_violations_sorted_and_deduped");
1458
1459 let pack = CrashPack::builder(CrashPackConfig::default())
1460 .failure(sample_failure())
1461 .oracle_violations(vec![
1462 "z-violation".into(),
1463 "a-violation".into(),
1464 "z-violation".into(), "m-violation".into(),
1466 ])
1467 .build()
1468 .expect("crash pack builder should have failure metadata");
1469
1470 assert_eq!(
1471 pack.oracle_violations,
1472 vec!["a-violation", "m-violation", "z-violation"]
1473 );
1474
1475 crate::test_complete!("oracle_violations_sorted_and_deduped");
1476 }
1477
1478 #[test]
1479 fn supervision_log_sorted_by_vt() {
1480 init_test("supervision_log_sorted_by_vt");
1481
1482 let pack = CrashPack::builder(CrashPackConfig::default())
1483 .failure(sample_failure())
1484 .supervision_snapshot(SupervisionSnapshot {
1485 virtual_time: Time::from_secs(10),
1486 task: tid(1),
1487 region: rid(0),
1488 decision: "restart".into(),
1489 context: Some("attempt 2 of 3".into()),
1490 })
1491 .supervision_snapshot(SupervisionSnapshot {
1492 virtual_time: Time::from_secs(5),
1493 task: tid(1),
1494 region: rid(0),
1495 decision: "restart".into(),
1496 context: Some("attempt 1 of 3".into()),
1497 })
1498 .supervision_snapshot(SupervisionSnapshot {
1499 virtual_time: Time::from_secs(15),
1500 task: tid(1),
1501 region: rid(0),
1502 decision: "stop".into(),
1503 context: Some("budget exhausted".into()),
1504 })
1505 .build()
1506 .expect("crash pack builder should have failure metadata");
1507
1508 assert_eq!(pack.supervision_log.len(), 3);
1509 assert_eq!(pack.supervision_log[0].virtual_time, Time::from_secs(5));
1511 assert_eq!(pack.supervision_log[1].virtual_time, Time::from_secs(10));
1512 assert_eq!(pack.supervision_log[2].virtual_time, Time::from_secs(15));
1513
1514 crate::test_complete!("supervision_log_sorted_by_vt");
1515 }
1516
1517 #[test]
1518 fn supervision_log_equal_vt_has_deterministic_total_order() {
1519 init_test("supervision_log_equal_vt_has_deterministic_total_order");
1520
1521 let s1 = SupervisionSnapshot {
1522 virtual_time: Time::from_secs(5),
1523 task: tid(2),
1524 region: rid(0),
1525 decision: "restart".into(),
1526 context: Some("ctx-b".into()),
1527 };
1528 let s2 = SupervisionSnapshot {
1529 virtual_time: Time::from_secs(5),
1530 task: tid(1),
1531 region: rid(0),
1532 decision: "restart".into(),
1533 context: Some("ctx-a".into()),
1534 };
1535 let s3 = SupervisionSnapshot {
1536 virtual_time: Time::from_secs(5),
1537 task: tid(1),
1538 region: rid(0),
1539 decision: "escalate".into(),
1540 context: Some("ctx-a".into()),
1541 };
1542
1543 let pack_a = CrashPack::builder(CrashPackConfig::default())
1544 .failure(sample_failure())
1545 .supervision_snapshot(s1.clone())
1546 .supervision_snapshot(s2.clone())
1547 .supervision_snapshot(s3.clone())
1548 .build()
1549 .expect("crash pack builder should have failure metadata");
1550
1551 let pack_b = CrashPack::builder(CrashPackConfig::default())
1552 .failure(sample_failure())
1553 .supervision_snapshot(s3.clone())
1554 .supervision_snapshot(s1.clone())
1555 .supervision_snapshot(s2.clone())
1556 .build()
1557 .expect("crash pack builder should have failure metadata");
1558
1559 assert_eq!(pack_a.supervision_log, pack_b.supervision_log);
1561 assert_eq!(pack_a.supervision_log, vec![s3, s2, s1]);
1562
1563 crate::test_complete!("supervision_log_equal_vt_has_deterministic_total_order");
1564 }
1565
1566 #[test]
1567 fn crash_pack_equality_ignores_created_at() {
1568 init_test("crash_pack_equality_ignores_created_at");
1569
1570 let pack1 = CrashPack::builder(sample_config())
1571 .failure(sample_failure())
1572 .fingerprint(0xABCD)
1573 .build()
1574 .expect("crash pack builder should have failure metadata");
1575
1576 let pack2 = CrashPack::builder(sample_config())
1578 .failure(sample_failure())
1579 .fingerprint(0xABCD)
1580 .build()
1581 .expect("crash pack builder should have failure metadata");
1582
1583 assert_eq!(pack1, pack2);
1585
1586 crate::test_complete!("crash_pack_equality_ignores_created_at");
1587 }
1588
1589 #[test]
1590 fn crash_pack_inequality_on_different_fingerprint() {
1591 init_test("crash_pack_inequality_on_different_fingerprint");
1592
1593 let pack1 = CrashPack::builder(sample_config())
1594 .failure(sample_failure())
1595 .fingerprint(0x1111)
1596 .build()
1597 .expect("crash pack builder should have failure metadata");
1598
1599 let pack2 = CrashPack::builder(sample_config())
1600 .failure(sample_failure())
1601 .fingerprint(0x2222)
1602 .build()
1603 .expect("crash pack builder should have failure metadata");
1604
1605 assert_ne!(pack1, pack2);
1606
1607 crate::test_complete!("crash_pack_inequality_on_different_fingerprint");
1608 }
1609
1610 #[test]
1611 fn crash_pack_inequality_on_different_divergent_prefix() {
1612 init_test("crash_pack_inequality_on_different_divergent_prefix");
1613
1614 let pack1 = CrashPack::builder(sample_config())
1615 .failure(sample_failure())
1616 .fingerprint(0xABCD)
1617 .divergent_prefix(vec![ReplayEvent::RngSeed { seed: 1 }])
1618 .build()
1619 .expect("crash pack builder should have failure metadata");
1620
1621 let pack2 = CrashPack::builder(sample_config())
1622 .failure(sample_failure())
1623 .fingerprint(0xABCD)
1624 .divergent_prefix(vec![ReplayEvent::RngSeed { seed: 2 }])
1625 .build()
1626 .expect("crash pack builder should have failure metadata");
1627
1628 assert_ne!(pack1, pack2);
1629
1630 crate::test_complete!("crash_pack_inequality_on_different_divergent_prefix");
1631 }
1632
1633 #[test]
1634 fn empty_pack_defaults() {
1635 init_test("empty_pack_defaults");
1636
1637 let pack = CrashPack::builder(CrashPackConfig::default())
1638 .failure(sample_failure())
1639 .build()
1640 .expect("crash pack builder should have failure metadata");
1641
1642 assert!(pack.canonical_prefix.is_empty());
1643 assert!(pack.divergent_prefix.is_empty());
1644 assert!(pack.evidence.is_empty());
1645 assert!(pack.supervision_log.is_empty());
1646 assert!(pack.oracle_violations.is_empty());
1647 assert!(!pack.has_violations());
1648 assert!(!pack.has_divergent_prefix());
1649
1650 crate::test_complete!("empty_pack_defaults");
1651 }
1652
1653 #[test]
1654 fn failure_info_equality() {
1655 init_test("failure_info_equality");
1656
1657 let f1 = FailureInfo {
1658 task: tid(1),
1659 region: rid(0),
1660 outcome: FailureOutcome::Panicked {
1661 message: "a".to_string(),
1662 },
1663 virtual_time: Time::from_secs(5),
1664 };
1665 let f2 = FailureInfo {
1666 task: tid(1),
1667 region: rid(0),
1668 outcome: FailureOutcome::Err, virtual_time: Time::from_secs(5),
1670 };
1671 assert_ne!(f1, f2);
1673
1674 let f3 = FailureInfo {
1675 task: tid(2), region: rid(0),
1677 outcome: FailureOutcome::Panicked {
1678 message: "a".to_string(),
1679 },
1680 virtual_time: Time::from_secs(5),
1681 };
1682 assert_ne!(f1, f3);
1683
1684 crate::test_complete!("failure_info_equality");
1685 }
1686
1687 #[test]
1688 fn manifest_new_sets_version() {
1689 init_test("manifest_new_sets_version");
1690
1691 let manifest = CrashPackManifest::new(CrashPackConfig::default(), 0xBEEF, 100);
1692
1693 assert_eq!(manifest.schema_version, CRASHPACK_SCHEMA_VERSION);
1694 assert_eq!(manifest.fingerprint, 0xBEEF);
1695 assert_eq!(manifest.event_count, 100);
1696 assert!(manifest.created_at > 0);
1697
1698 crate::test_complete!("manifest_new_sets_version");
1699 }
1700
1701 #[test]
1702 fn with_divergent_prefix() {
1703 init_test("with_divergent_prefix");
1704
1705 let prefix = vec![
1706 ReplayEvent::RngSeed { seed: 42 },
1707 ReplayEvent::TaskScheduled {
1708 task: crate::trace::replay::CompactTaskId(1),
1709 at_tick: 0,
1710 },
1711 ];
1712
1713 let pack = CrashPack::builder(CrashPackConfig::default())
1714 .failure(sample_failure())
1715 .divergent_prefix(prefix)
1716 .build()
1717 .expect("crash pack builder should have failure metadata");
1718
1719 assert!(pack.has_divergent_prefix());
1720 assert_eq!(pack.divergent_prefix.len(), 2);
1721
1722 crate::test_complete!("with_divergent_prefix");
1723 }
1724
1725 #[test]
1726 fn with_canonical_prefix() {
1727 init_test("with_canonical_prefix");
1728
1729 let layer = vec![TraceEventKey {
1730 kind: 1,
1731 primary: 0,
1732 secondary: 0,
1733 tertiary: 0,
1734 }];
1735
1736 let pack = CrashPack::builder(CrashPackConfig::default())
1737 .failure(sample_failure())
1738 .canonical_prefix(vec![layer])
1739 .build()
1740 .expect("crash pack builder should have failure metadata");
1741
1742 assert_eq!(pack.canonical_prefix.len(), 1);
1743
1744 crate::test_complete!("with_canonical_prefix");
1745 }
1746
1747 #[test]
1748 fn supervision_snapshot_with_context() {
1749 init_test("supervision_snapshot_with_context");
1750
1751 let snap = SupervisionSnapshot {
1752 virtual_time: Time::from_secs(10),
1753 task: tid(3),
1754 region: rid(1),
1755 decision: "escalate".into(),
1756 context: Some("parent region R0".into()),
1757 };
1758
1759 assert_eq!(snap.decision, "escalate");
1760 assert_eq!(snap.context.as_deref(), Some("parent region R0"));
1761
1762 crate::test_complete!("supervision_snapshot_with_context");
1763 }
1764
1765 #[test]
1770 fn from_trace_populates_fields() {
1771 init_test("from_trace_populates_fields");
1772
1773 let events = [
1774 TraceEvent::spawn(1, Time::ZERO, tid(1), rid(1)),
1775 TraceEvent::spawn(2, Time::ZERO, tid(2), rid(2)),
1776 TraceEvent::complete(3, Time::ZERO, tid(1), rid(1)),
1777 ];
1778
1779 let pack = CrashPack::builder(sample_config())
1780 .failure(sample_failure())
1781 .from_trace(&events)
1782 .build()
1783 .expect("crash pack builder should have failure metadata");
1784
1785 assert_eq!(pack.manifest.event_count, 3);
1786 assert_ne!(pack.manifest.fingerprint, 0);
1787 assert!(!pack.canonical_prefix.is_empty());
1788
1789 crate::test_complete!("from_trace_populates_fields");
1790 }
1791
1792 #[test]
1793 fn from_trace_equivalent_traces_same_fingerprint() {
1794 init_test("from_trace_equivalent_traces_same_fingerprint");
1795
1796 let trace_a = [
1800 TraceEvent::spawn(1, Time::ZERO, tid(1), rid(1)),
1801 TraceEvent::spawn(2, Time::ZERO, tid(2), rid(2)),
1802 ];
1803 let trace_b = [
1804 TraceEvent::spawn(1, Time::ZERO, tid(2), rid(2)),
1805 TraceEvent::spawn(2, Time::ZERO, tid(1), rid(1)),
1806 ];
1807
1808 let pack_a = CrashPack::builder(sample_config())
1809 .failure(sample_failure())
1810 .from_trace(&trace_a)
1811 .build()
1812 .expect("crash pack builder should have failure metadata");
1813 let pack_b = CrashPack::builder(sample_config())
1814 .failure(sample_failure())
1815 .from_trace(&trace_b)
1816 .build()
1817 .expect("crash pack builder should have failure metadata");
1818
1819 assert_eq!(pack_a.fingerprint(), pack_b.fingerprint());
1820 assert_eq!(pack_a.canonical_prefix, pack_b.canonical_prefix);
1821 assert_eq!(pack_a, pack_b);
1822
1823 crate::test_complete!("from_trace_equivalent_traces_same_fingerprint");
1824 }
1825
1826 #[test]
1827 fn from_trace_different_dependent_traces_different_fingerprint() {
1828 init_test("from_trace_different_dependent_traces_different_fingerprint");
1829
1830 let trace_a = [
1833 TraceEvent::spawn(1, Time::ZERO, tid(1), rid(1)),
1834 TraceEvent::complete(2, Time::ZERO, tid(1), rid(1)),
1835 ];
1836 let trace_b = [
1837 TraceEvent::complete(1, Time::ZERO, tid(1), rid(1)),
1838 TraceEvent::spawn(2, Time::ZERO, tid(1), rid(1)),
1839 ];
1840
1841 let pack_a = CrashPack::builder(sample_config())
1842 .failure(sample_failure())
1843 .from_trace(&trace_a)
1844 .build()
1845 .expect("crash pack builder should have failure metadata");
1846 let pack_b = CrashPack::builder(sample_config())
1847 .failure(sample_failure())
1848 .from_trace(&trace_b)
1849 .build()
1850 .expect("crash pack builder should have failure metadata");
1851
1852 assert_ne!(pack_a.fingerprint(), pack_b.fingerprint());
1853 assert_ne!(pack_a, pack_b);
1854
1855 crate::test_complete!("from_trace_different_dependent_traces_different_fingerprint");
1856 }
1857
1858 #[test]
1859 fn from_trace_canonical_prefix_matches_foata_layers() {
1860 init_test("from_trace_canonical_prefix_matches_foata_layers");
1861
1862 let events = [
1863 TraceEvent::spawn(1, Time::ZERO, tid(1), rid(1)),
1864 TraceEvent::spawn(2, Time::ZERO, tid(2), rid(2)),
1865 TraceEvent::complete(3, Time::ZERO, tid(1), rid(1)),
1866 TraceEvent::complete(4, Time::ZERO, tid(2), rid(2)),
1867 ];
1868
1869 let pack = CrashPack::builder(CrashPackConfig::default())
1870 .failure(sample_failure())
1871 .from_trace(&events)
1872 .build()
1873 .expect("crash pack builder should have failure metadata");
1874
1875 let foata = canonicalize(&events);
1877 let expected_prefix: Vec<Vec<TraceEventKey>> = foata
1878 .layers()
1879 .iter()
1880 .map(|layer| layer.iter().map(trace_event_key).collect())
1881 .collect();
1882
1883 assert_eq!(pack.canonical_prefix, expected_prefix);
1884
1885 crate::test_complete!("from_trace_canonical_prefix_matches_foata_layers");
1886 }
1887
1888 #[test]
1889 fn from_trace_empty_trace() {
1890 init_test("from_trace_empty_trace");
1891
1892 let pack = CrashPack::builder(CrashPackConfig::default())
1893 .failure(sample_failure())
1894 .from_trace(&[])
1895 .build()
1896 .expect("crash pack builder should have failure metadata");
1897
1898 assert!(pack.canonical_prefix.is_empty());
1899 assert_eq!(pack.manifest.event_count, 0);
1900
1901 crate::test_complete!("from_trace_empty_trace");
1902 }
1903
1904 #[test]
1905 fn from_trace_three_independent_all_permutations() {
1906 init_test("from_trace_three_independent_all_permutations");
1907
1908 let e1 = TraceEvent::spawn(1, Time::ZERO, tid(1), rid(1));
1911 let e2 = TraceEvent::spawn(2, Time::ZERO, tid(2), rid(2));
1912 let e3 = TraceEvent::spawn(3, Time::ZERO, tid(3), rid(3));
1913
1914 let perms: Vec<Vec<TraceEvent>> = vec![
1915 vec![e1.clone(), e2.clone(), e3.clone()],
1916 vec![e1.clone(), e3.clone(), e2.clone()],
1917 vec![e2.clone(), e1.clone(), e3.clone()],
1918 vec![e2.clone(), e3.clone(), e1.clone()],
1919 vec![e3.clone(), e1.clone(), e2.clone()],
1920 vec![e3, e2, e1],
1921 ];
1922
1923 let reference = CrashPack::builder(CrashPackConfig::default())
1924 .failure(sample_failure())
1925 .from_trace(&perms[0])
1926 .build()
1927 .expect("crash pack builder should have failure metadata");
1928
1929 for (i, perm) in perms.iter().enumerate().skip(1) {
1930 let pack = CrashPack::builder(CrashPackConfig::default())
1931 .failure(sample_failure())
1932 .from_trace(perm)
1933 .build()
1934 .expect("crash pack builder should have failure metadata");
1935 assert_eq!(
1936 pack.fingerprint(),
1937 reference.fingerprint(),
1938 "permutation {i} has different fingerprint"
1939 );
1940 assert_eq!(
1941 pack.canonical_prefix, reference.canonical_prefix,
1942 "permutation {i} has different canonical prefix"
1943 );
1944 }
1945
1946 crate::test_complete!("from_trace_three_independent_all_permutations");
1947 }
1948
1949 #[test]
1950 fn from_trace_diamond_dependency() {
1951 init_test("from_trace_diamond_dependency");
1952
1953 let trace_a = [
1956 TraceEvent::region_created(1, Time::ZERO, rid(1), None),
1957 TraceEvent::spawn(2, Time::ZERO, tid(1), rid(1)),
1958 TraceEvent::spawn(3, Time::ZERO, tid(2), rid(1)),
1959 TraceEvent::complete(4, Time::ZERO, tid(1), rid(1)),
1960 TraceEvent::complete(5, Time::ZERO, tid(2), rid(1)),
1961 ];
1962 let trace_b = [
1963 TraceEvent::region_created(1, Time::ZERO, rid(1), None),
1964 TraceEvent::spawn(2, Time::ZERO, tid(2), rid(1)),
1965 TraceEvent::spawn(3, Time::ZERO, tid(1), rid(1)),
1966 TraceEvent::complete(4, Time::ZERO, tid(2), rid(1)),
1967 TraceEvent::complete(5, Time::ZERO, tid(1), rid(1)),
1968 ];
1969
1970 let pack_a = CrashPack::builder(sample_config())
1971 .failure(sample_failure())
1972 .from_trace(&trace_a)
1973 .build()
1974 .expect("crash pack builder should have failure metadata");
1975 let pack_b = CrashPack::builder(sample_config())
1976 .failure(sample_failure())
1977 .from_trace(&trace_b)
1978 .build()
1979 .expect("crash pack builder should have failure metadata");
1980
1981 assert_eq!(pack_a.fingerprint(), pack_b.fingerprint());
1982 assert_eq!(pack_a.canonical_prefix, pack_b.canonical_prefix);
1983 assert_eq!(pack_a.canonical_prefix.len(), 3);
1985
1986 crate::test_complete!("from_trace_diamond_dependency");
1987 }
1988
1989 #[test]
1994 fn artifact_filename_is_deterministic() {
1995 init_test("artifact_filename_is_deterministic");
1996
1997 let pack = CrashPack::builder(CrashPackConfig {
1998 seed: 42,
1999 ..Default::default()
2000 })
2001 .failure(sample_failure())
2002 .fingerprint(0xCAFE_BABE)
2003 .build()
2004 .expect("crash pack builder should have failure metadata");
2005
2006 let name1 = artifact_filename(&pack);
2007 let name2 = artifact_filename(&pack);
2008 assert_eq!(name1, name2);
2009 assert_eq!(
2010 name1,
2011 "crashpack-000000000000002a-0000000000000000-00000000cafebabe-v1.json"
2012 );
2013
2014 crate::test_complete!("artifact_filename_is_deterministic");
2015 }
2016
2017 #[test]
2018 fn artifact_filename_varies_by_seed_and_fingerprint() {
2019 init_test("artifact_filename_varies_by_seed_and_fingerprint");
2020
2021 let pack_a = CrashPack::builder(CrashPackConfig {
2022 seed: 1,
2023 ..Default::default()
2024 })
2025 .failure(sample_failure())
2026 .fingerprint(0xAAAA)
2027 .build()
2028 .expect("crash pack builder should have failure metadata");
2029
2030 let pack_b = CrashPack::builder(CrashPackConfig {
2031 seed: 2,
2032 ..Default::default()
2033 })
2034 .failure(sample_failure())
2035 .fingerprint(0xBBBB)
2036 .build()
2037 .expect("crash pack builder should have failure metadata");
2038
2039 assert_ne!(artifact_filename(&pack_a), artifact_filename(&pack_b));
2040
2041 crate::test_complete!("artifact_filename_varies_by_seed_and_fingerprint");
2042 }
2043
2044 #[test]
2045 fn artifact_filename_varies_by_config_hash() {
2046 init_test("artifact_filename_varies_by_config_hash");
2047
2048 let pack_a = CrashPack::builder(CrashPackConfig {
2049 seed: 42,
2050 config_hash: 0xAAAA,
2051 ..Default::default()
2052 })
2053 .failure(sample_failure())
2054 .fingerprint(0x1234)
2055 .build()
2056 .expect("crash pack builder should have failure metadata");
2057
2058 let pack_b = CrashPack::builder(CrashPackConfig {
2059 seed: 42,
2060 config_hash: 0xBBBB,
2061 ..Default::default()
2062 })
2063 .failure(sample_failure())
2064 .fingerprint(0x1234)
2065 .build()
2066 .expect("crash pack builder should have failure metadata");
2067
2068 assert_ne!(artifact_filename(&pack_a), artifact_filename(&pack_b));
2069
2070 crate::test_complete!("artifact_filename_varies_by_config_hash");
2071 }
2072
2073 #[test]
2074 fn memory_writer_collects_packs() {
2075 init_test("memory_writer_collects_packs");
2076
2077 let writer = MemoryCrashPackWriter::new();
2078 assert_eq!(writer.count(), 0);
2079 assert!(!writer.is_persistent());
2080 assert_eq!(writer.name(), "memory");
2081
2082 let pack = CrashPack::builder(sample_config())
2083 .failure(sample_failure())
2084 .fingerprint(0x1234)
2085 .build()
2086 .expect("crash pack builder should have failure metadata");
2087
2088 let artifact = writer.write(&pack).unwrap();
2089 assert_eq!(writer.count(), 1);
2090 assert!(artifact.path().contains("crashpack-"));
2091 assert!(artifact.path().contains("1234"));
2092
2093 let pack2 = CrashPack::builder(CrashPackConfig {
2095 seed: 99,
2096 ..Default::default()
2097 })
2098 .failure(sample_failure())
2099 .fingerprint(0x5678)
2100 .build()
2101 .expect("crash pack builder should have failure metadata");
2102
2103 let artifact2 = writer.write(&pack2).unwrap();
2104 assert_eq!(writer.count(), 2);
2105 assert_ne!(artifact.path(), artifact2.path());
2106
2107 crate::test_complete!("memory_writer_collects_packs");
2108 }
2109
2110 #[test]
2111 fn memory_writer_produces_valid_json() {
2112 init_test("memory_writer_produces_valid_json");
2113
2114 let writer = MemoryCrashPackWriter::new();
2115 let pack = CrashPack::builder(sample_config())
2116 .failure(sample_failure())
2117 .fingerprint(0xDEAD)
2118 .event_count(42)
2119 .oracle_violations(vec!["inv-1".into()])
2120 .build()
2121 .expect("crash pack builder should have failure metadata");
2122
2123 writer.write(&pack).unwrap();
2124 let written = writer.written();
2125 assert_eq!(written.len(), 1);
2126
2127 let json = &written[0].1;
2128 let parsed: serde_json::Value = serde_json::from_str(json).unwrap();
2130 assert_eq!(parsed["manifest"]["config"]["seed"], 42);
2131 assert_eq!(parsed["manifest"]["fingerprint"], 0xDEAD_u64);
2132 assert_eq!(parsed["manifest"]["event_count"], 42);
2133 assert_eq!(parsed["oracle_violations"][0], "inv-1");
2134
2135 crate::test_complete!("memory_writer_produces_valid_json");
2136 }
2137
2138 #[test]
2139 fn file_writer_writes_to_disk() {
2140 init_test("file_writer_writes_to_disk");
2141
2142 let dir = std::env::temp_dir().join("asupersync_test_crashpack");
2143 let _ = std::fs::create_dir_all(&dir);
2144
2145 let writer = FileCrashPackWriter::new(dir.clone());
2146 assert!(writer.is_persistent());
2147 assert_eq!(writer.name(), "file");
2148 assert_eq!(writer.base_dir(), dir.as_path());
2149
2150 let pack = CrashPack::builder(CrashPackConfig {
2151 seed: 7,
2152 ..Default::default()
2153 })
2154 .failure(sample_failure())
2155 .fingerprint(0xBEEF)
2156 .build()
2157 .expect("crash pack builder should have failure metadata");
2158
2159 let artifact = writer.write(&pack).unwrap();
2160 let expected_name = artifact_filename(&pack);
2161
2162 assert!(artifact.path().contains(&expected_name));
2164
2165 let contents = std::fs::read_to_string(artifact.path()).unwrap();
2167 let parsed: serde_json::Value = serde_json::from_str(&contents).unwrap();
2168 assert_eq!(parsed["manifest"]["config"]["seed"], 7);
2169
2170 let _ = std::fs::remove_file(artifact.path());
2172 let _ = std::fs::remove_dir(&dir);
2173
2174 crate::test_complete!("file_writer_writes_to_disk");
2175 }
2176
2177 #[test]
2178 fn file_writer_fails_on_missing_dir() {
2179 init_test("file_writer_fails_on_missing_dir");
2180
2181 let writer =
2182 FileCrashPackWriter::new(std::path::PathBuf::from("/nonexistent/crashpack/dir"));
2183
2184 let pack = CrashPack::builder(CrashPackConfig::default())
2185 .failure(sample_failure())
2186 .build()
2187 .expect("crash pack builder should have failure metadata");
2188
2189 let result = writer.write(&pack);
2190 assert!(result.is_err());
2191
2192 crate::test_complete!("file_writer_fails_on_missing_dir");
2193 }
2194
2195 #[test]
2196 fn artifact_id_display() {
2197 init_test("artifact_id_display");
2198
2199 let id = ArtifactId {
2200 path: "some/path.json".to_string(),
2201 };
2202 assert_eq!(format!("{id}"), "some/path.json");
2203 assert_eq!(id.path(), "some/path.json");
2204
2205 crate::test_complete!("artifact_id_display");
2206 }
2207
2208 #[test]
2209 fn conformance_no_ambient_writes() {
2210 init_test("conformance_no_ambient_writes");
2211
2212 let pack = CrashPack::builder(sample_config())
2215 .failure(sample_failure())
2216 .build()
2217 .expect("crash pack builder should have failure metadata");
2218
2219 assert_eq!(pack.seed(), 42);
2221
2222 let writer = MemoryCrashPackWriter::new();
2224 assert_eq!(writer.count(), 0);
2225 writer.write(&pack).unwrap();
2226 assert_eq!(writer.count(), 1);
2227
2228 crate::test_complete!("conformance_no_ambient_writes");
2229 }
2230
2231 #[test]
2232 fn conformance_same_pack_same_artifact_path() {
2233 init_test("conformance_same_pack_same_artifact_path");
2234
2235 let writer = MemoryCrashPackWriter::new();
2236
2237 let pack = CrashPack::builder(CrashPackConfig {
2238 seed: 100,
2239 ..Default::default()
2240 })
2241 .failure(sample_failure())
2242 .fingerprint(0xFACE)
2243 .build()
2244 .expect("crash pack builder should have failure metadata");
2245
2246 let id1 = writer.write(&pack).unwrap();
2247 let id2 = writer.write(&pack).unwrap();
2248
2249 assert_eq!(id1.path(), id2.path());
2251
2252 crate::test_complete!("conformance_same_pack_same_artifact_path");
2253 }
2254
2255 #[test]
2260 fn manifest_validate_current_version() {
2261 init_test("manifest_validate_current_version");
2262
2263 let manifest = CrashPackManifest::new(CrashPackConfig::default(), 0, 0);
2264 assert!(manifest.validate().is_ok());
2265 assert!(manifest.is_compatible());
2266 assert_eq!(manifest.schema_version, CRASHPACK_SCHEMA_VERSION);
2267
2268 crate::test_complete!("manifest_validate_current_version");
2269 }
2270
2271 #[test]
2272 fn manifest_validate_rejects_future_version() {
2273 init_test("manifest_validate_rejects_future_version");
2274
2275 let mut manifest = CrashPackManifest::new(CrashPackConfig::default(), 0, 0);
2276 manifest.schema_version = CRASHPACK_SCHEMA_VERSION + 1;
2277
2278 let err = manifest.validate().unwrap_err();
2279 assert!(!manifest.is_compatible());
2280 assert!(matches!(err, ManifestValidationError::VersionTooNew { .. }));
2281 assert!(err.to_string().contains("newer than supported"));
2283
2284 crate::test_complete!("manifest_validate_rejects_future_version");
2285 }
2286
2287 #[test]
2288 fn manifest_validate_rejects_old_version() {
2289 init_test("manifest_validate_rejects_old_version");
2290
2291 let mut manifest = CrashPackManifest::new(CrashPackConfig::default(), 0, 0);
2292 manifest.schema_version = 0; let err = manifest.validate().unwrap_err();
2295 assert!(!manifest.is_compatible());
2296 assert!(matches!(err, ManifestValidationError::VersionTooOld { .. }));
2297 assert!(err.to_string().contains("older than minimum"));
2298
2299 crate::test_complete!("manifest_validate_rejects_old_version");
2300 }
2301
2302 #[test]
2303 fn manifest_attachments_auto_populated() {
2304 init_test("manifest_attachments_auto_populated");
2305
2306 let events = [
2309 TraceEvent::spawn(1, Time::ZERO, tid(1), rid(1)),
2310 TraceEvent::complete(2, Time::ZERO, tid(1), rid(1)),
2311 ];
2312
2313 let pack = CrashPack::builder(sample_config())
2314 .failure(sample_failure())
2315 .from_trace(&events)
2316 .divergent_prefix(vec![ReplayEvent::RngSeed { seed: 42 }])
2317 .oracle_violations(vec!["inv-1".into()])
2318 .build()
2319 .expect("crash pack builder should have failure metadata");
2320
2321 assert_eq!(pack.manifest.attachments.len(), 3);
2322 assert!(
2323 pack.manifest
2324 .has_attachment(&AttachmentKind::CanonicalPrefix)
2325 );
2326 assert!(
2327 pack.manifest
2328 .has_attachment(&AttachmentKind::DivergentPrefix)
2329 );
2330 assert!(
2331 pack.manifest
2332 .has_attachment(&AttachmentKind::OracleViolations)
2333 );
2334 assert!(
2335 !pack
2336 .manifest
2337 .has_attachment(&AttachmentKind::EvidenceLedger)
2338 );
2339 assert!(
2340 !pack
2341 .manifest
2342 .has_attachment(&AttachmentKind::SupervisionLog)
2343 );
2344
2345 crate::test_complete!("manifest_attachments_auto_populated");
2346 }
2347
2348 #[test]
2349 fn manifest_empty_pack_no_attachments() {
2350 init_test("manifest_empty_pack_no_attachments");
2351
2352 let pack = CrashPack::builder(CrashPackConfig::default())
2353 .failure(sample_failure())
2354 .build()
2355 .expect("crash pack builder should have failure metadata");
2356
2357 assert!(pack.manifest.attachments.is_empty());
2358
2359 crate::test_complete!("manifest_empty_pack_no_attachments");
2360 }
2361
2362 #[test]
2363 fn manifest_attachment_item_counts() {
2364 init_test("manifest_attachment_item_counts");
2365
2366 let pack = CrashPack::builder(sample_config())
2367 .failure(sample_failure())
2368 .canonical_prefix(vec![
2369 vec![TraceEventKey {
2370 kind: 1,
2371 primary: 0,
2372 secondary: 0,
2373 tertiary: 0,
2374 }],
2375 vec![
2376 TraceEventKey {
2377 kind: 2,
2378 primary: 1,
2379 secondary: 0,
2380 tertiary: 0,
2381 },
2382 TraceEventKey {
2383 kind: 2,
2384 primary: 2,
2385 secondary: 0,
2386 tertiary: 0,
2387 },
2388 ],
2389 ])
2390 .supervision_snapshot(SupervisionSnapshot {
2391 virtual_time: Time::from_secs(1),
2392 task: tid(1),
2393 region: rid(0),
2394 decision: "restart".into(),
2395 context: None,
2396 })
2397 .build()
2398 .expect("crash pack builder should have failure metadata");
2399
2400 let cp = pack
2402 .manifest
2403 .attachment(&AttachmentKind::CanonicalPrefix)
2404 .unwrap();
2405 assert_eq!(cp.item_count, 3);
2406
2407 let sl = pack
2409 .manifest
2410 .attachment(&AttachmentKind::SupervisionLog)
2411 .unwrap();
2412 assert_eq!(sl.item_count, 1);
2413
2414 crate::test_complete!("manifest_attachment_item_counts");
2415 }
2416
2417 #[test]
2418 fn manifest_attachment_kind_serde_round_trip() {
2419 init_test("manifest_attachment_kind_serde_round_trip");
2420
2421 let kinds = vec![
2422 AttachmentKind::CanonicalPrefix,
2423 AttachmentKind::DivergentPrefix,
2424 AttachmentKind::EvidenceLedger,
2425 AttachmentKind::SupervisionLog,
2426 AttachmentKind::OracleViolations,
2427 AttachmentKind::Custom {
2428 tag: "heap-dump".into(),
2429 },
2430 ];
2431
2432 for kind in &kinds {
2433 let json = serde_json::to_string(kind).unwrap();
2434 let parsed: AttachmentKind = serde_json::from_str(&json).unwrap();
2435 assert_eq!(&parsed, kind, "round trip failed for {json}");
2436 }
2437
2438 crate::test_complete!("manifest_attachment_kind_serde_round_trip");
2439 }
2440
2441 #[test]
2442 fn manifest_serde_round_trip_with_attachments() {
2443 init_test("manifest_serde_round_trip_with_attachments");
2444
2445 let mut manifest = CrashPackManifest::new(sample_config(), 0xBEEF, 100);
2446 manifest.attachments = vec![
2447 ManifestAttachment {
2448 kind: AttachmentKind::CanonicalPrefix,
2449 item_count: 10,
2450 size_hint_bytes: 2048,
2451 },
2452 ManifestAttachment {
2453 kind: AttachmentKind::Custom {
2454 tag: "user-data".into(),
2455 },
2456 item_count: 1,
2457 size_hint_bytes: 0,
2458 },
2459 ];
2460
2461 let json = serde_json::to_string_pretty(&manifest).unwrap();
2462 let parsed: CrashPackManifest = serde_json::from_str(&json).unwrap();
2463
2464 assert_eq!(parsed.schema_version, CRASHPACK_SCHEMA_VERSION);
2465 assert_eq!(parsed.config.seed, 42);
2466 assert_eq!(parsed.fingerprint, 0xBEEF);
2467 assert_eq!(parsed.attachments.len(), 2);
2468 assert_eq!(parsed.attachments[0].kind, AttachmentKind::CanonicalPrefix);
2469 assert_eq!(parsed.attachments[0].item_count, 10);
2470 assert_eq!(parsed.attachments[0].size_hint_bytes, 2048);
2471 assert_eq!(
2472 parsed.attachments[1].kind,
2473 AttachmentKind::Custom {
2474 tag: "user-data".into()
2475 }
2476 );
2477
2478 crate::test_complete!("manifest_serde_round_trip_with_attachments");
2479 }
2480
2481 #[test]
2482 fn manifest_deserialize_without_attachments() {
2483 init_test("manifest_deserialize_without_attachments");
2484
2485 let json = r#"{
2488 "schema_version": 1,
2489 "config": { "seed": 1, "config_hash": 0, "worker_count": 1 },
2490 "fingerprint": 999,
2491 "event_count": 50,
2492 "created_at": 0
2493 }"#;
2494
2495 let manifest: CrashPackManifest = serde_json::from_str(json).unwrap();
2496 assert_eq!(manifest.schema_version, 1);
2497 assert_eq!(manifest.fingerprint, 999);
2498 assert!(manifest.attachments.is_empty());
2499 assert!(manifest.is_compatible());
2500
2501 crate::test_complete!("manifest_deserialize_without_attachments");
2502 }
2503
2504 #[test]
2505 fn manifest_json_skips_empty_attachments() {
2506 init_test("manifest_json_skips_empty_attachments");
2507
2508 let manifest = CrashPackManifest::new(CrashPackConfig::default(), 0, 0);
2509 let json = serde_json::to_string(&manifest).unwrap();
2510
2511 assert!(!json.contains("attachments"));
2513
2514 crate::test_complete!("manifest_json_skips_empty_attachments");
2515 }
2516
2517 #[test]
2518 fn manifest_json_skips_zero_size_hint() {
2519 init_test("manifest_json_skips_zero_size_hint");
2520
2521 let attachment = ManifestAttachment {
2522 kind: AttachmentKind::CanonicalPrefix,
2523 item_count: 5,
2524 size_hint_bytes: 0,
2525 };
2526 let json = serde_json::to_string(&attachment).unwrap();
2527 assert!(!json.contains("size_hint_bytes"));
2528
2529 let non_zero = ManifestAttachment {
2530 kind: AttachmentKind::CanonicalPrefix,
2531 item_count: 5,
2532 size_hint_bytes: 1024,
2533 };
2534 let json2 = serde_json::to_string(&non_zero).unwrap();
2535 assert!(json2.contains("size_hint_bytes"));
2536
2537 crate::test_complete!("manifest_json_skips_zero_size_hint");
2538 }
2539
2540 #[test]
2541 fn conformance_attachments_in_crash_pack_json() {
2542 init_test("conformance_attachments_in_crash_pack_json");
2543
2544 let events = [
2546 TraceEvent::spawn(1, Time::ZERO, tid(1), rid(1)),
2547 TraceEvent::complete(2, Time::ZERO, tid(1), rid(1)),
2548 ];
2549
2550 let pack = CrashPack::builder(sample_config())
2551 .failure(sample_failure())
2552 .from_trace(&events)
2553 .divergent_prefix(vec![ReplayEvent::RngSeed { seed: 42 }])
2554 .oracle_violations(vec!["v1".into()])
2555 .supervision_snapshot(SupervisionSnapshot {
2556 virtual_time: Time::from_secs(1),
2557 task: tid(1),
2558 region: rid(0),
2559 decision: "restart".into(),
2560 context: None,
2561 })
2562 .build()
2563 .expect("crash pack builder should have failure metadata");
2564
2565 let writer = MemoryCrashPackWriter::new();
2566 writer.write(&pack).unwrap();
2567 let json_str = &writer.written()[0].1;
2568 let parsed: serde_json::Value = serde_json::from_str(json_str).unwrap();
2569
2570 let atts = parsed["manifest"]["attachments"].as_array().unwrap();
2571 assert_eq!(atts.len(), 4);
2572
2573 let kinds: Vec<&str> = atts.iter().map(|a| a["kind"].as_str().unwrap()).collect();
2575 assert!(kinds.contains(&"CanonicalPrefix"));
2576 assert!(kinds.contains(&"DivergentPrefix"));
2577 assert!(kinds.contains(&"SupervisionLog"));
2578 assert!(kinds.contains(&"OracleViolations"));
2579
2580 crate::test_complete!("conformance_attachments_in_crash_pack_json");
2581 }
2582
2583 #[test]
2584 fn conformance_validation_error_is_std_error() {
2585 init_test("conformance_validation_error_is_std_error");
2586
2587 let err = ManifestValidationError::VersionTooNew {
2588 manifest_version: 99,
2589 supported_version: 1,
2590 };
2591
2592 let _: &dyn std::error::Error = &err;
2594 assert!(err.to_string().contains("99"));
2595
2596 crate::test_complete!("conformance_validation_error_is_std_error");
2597 }
2598
2599 #[test]
2604 fn replay_command_from_config_basic() {
2605 init_test("replay_command_from_config_basic");
2606
2607 let config = CrashPackConfig {
2608 seed: 42,
2609 config_hash: 0xDEAD,
2610 worker_count: 4,
2611 max_steps: Some(1000),
2612 commit_hash: Some("abc123".to_string()),
2613 };
2614
2615 let cmd = ReplayCommand::from_config(&config, None);
2616 assert_eq!(cmd.program, "cargo");
2617 assert!(cmd.args.contains(&"--seed".to_string()));
2618 assert!(cmd.args.contains(&"42".to_string()));
2619 assert!(!cmd.env.is_empty());
2620 assert!(cmd.command_line.contains("cargo"));
2621 assert!(cmd.command_line.contains("--seed"));
2622 assert!(cmd.command_line.contains("42"));
2623 assert!(cmd.command_line.contains("ASUPERSYNC_WORKERS=4"));
2624
2625 crate::test_complete!("replay_command_from_config_basic");
2626 }
2627
2628 #[test]
2629 fn replay_command_from_config_with_artifact() {
2630 init_test("replay_command_from_config_with_artifact");
2631
2632 let config = CrashPackConfig {
2633 seed: 99,
2634 worker_count: 2,
2635 ..Default::default()
2636 };
2637
2638 let cmd = ReplayCommand::from_config(&config, Some("crashes/pack.json"));
2639 assert!(cmd.args.contains(&"--crashpack".to_string()));
2640 assert!(cmd.args.contains(&"crashes/pack.json".to_string()));
2641 assert!(cmd.command_line.contains("--crashpack"));
2642 assert!(cmd.command_line.contains("crashes/pack.json"));
2643
2644 crate::test_complete!("replay_command_from_config_with_artifact");
2645 }
2646
2647 #[test]
2648 fn replay_command_cli_mode() {
2649 init_test("replay_command_cli_mode");
2650
2651 let config = CrashPackConfig {
2652 seed: 7,
2653 worker_count: 8,
2654 max_steps: Some(500),
2655 ..Default::default()
2656 };
2657
2658 let cmd = ReplayCommand::from_config_cli(&config, "crashpack.json");
2659 assert_eq!(cmd.program, "asupersync");
2660 assert!(cmd.args.contains(&"trace".to_string()));
2661 assert!(cmd.args.contains(&"replay".to_string()));
2662 assert!(cmd.args.contains(&"--seed".to_string()));
2663 assert!(cmd.args.contains(&"7".to_string()));
2664 assert!(cmd.args.contains(&"--workers".to_string()));
2665 assert!(cmd.args.contains(&"8".to_string()));
2666 assert!(cmd.args.contains(&"--max-steps".to_string()));
2667 assert!(cmd.args.contains(&"500".to_string()));
2668 assert!(cmd.args.contains(&"crashpack.json".to_string()));
2669 assert!(cmd.env.is_empty());
2670 assert_eq!(
2671 cmd.command_line,
2672 "asupersync trace replay --seed 7 --workers 8 --max-steps 500 crashpack.json"
2673 );
2674
2675 crate::test_complete!("replay_command_cli_mode");
2676 }
2677
2678 #[test]
2679 fn replay_command_display() {
2680 init_test("replay_command_display");
2681
2682 let cmd = ReplayCommand::from_config_cli(
2683 &CrashPackConfig {
2684 seed: 1,
2685 worker_count: 1,
2686 ..Default::default()
2687 },
2688 "test.json",
2689 );
2690
2691 let displayed = format!("{cmd}");
2692 assert_eq!(displayed, cmd.command_line);
2693
2694 crate::test_complete!("replay_command_display");
2695 }
2696
2697 #[test]
2698 fn replay_command_serde_round_trip() {
2699 init_test("replay_command_serde_round_trip");
2700
2701 let cmd = ReplayCommand::from_config(
2702 &CrashPackConfig {
2703 seed: 42,
2704 worker_count: 4,
2705 max_steps: Some(1000),
2706 ..Default::default()
2707 },
2708 Some("pack.json"),
2709 );
2710
2711 let json = serde_json::to_string_pretty(&cmd).unwrap();
2712 let parsed: ReplayCommand = serde_json::from_str(&json).unwrap();
2713 assert_eq!(parsed, cmd);
2714
2715 crate::test_complete!("replay_command_serde_round_trip");
2716 }
2717
2718 #[test]
2719 fn replay_command_in_crash_pack() {
2720 init_test("replay_command_in_crash_pack");
2721
2722 let config = sample_config();
2723 let replay_cmd = ReplayCommand::from_config(&config, Some("crashes/test.json"));
2724
2725 let pack = CrashPack::builder(config)
2726 .failure(sample_failure())
2727 .fingerprint(0xCAFE)
2728 .replay(replay_cmd.clone())
2729 .build()
2730 .expect("crash pack builder should have failure metadata");
2731
2732 assert_eq!(pack.replay.as_ref(), Some(&replay_cmd));
2733
2734 let writer = MemoryCrashPackWriter::new();
2736 writer.write(&pack).unwrap();
2737 let json_str = &writer.written()[0].1;
2738 let parsed: serde_json::Value = serde_json::from_str(json_str).unwrap();
2739 assert!(parsed["replay"]["program"].as_str().is_some());
2740 assert!(
2741 parsed["replay"]["command_line"]
2742 .as_str()
2743 .unwrap()
2744 .contains("--seed")
2745 );
2746
2747 crate::test_complete!("replay_command_in_crash_pack");
2748 }
2749
2750 #[test]
2751 fn replay_command_absent_by_default() {
2752 init_test("replay_command_absent_by_default");
2753
2754 let pack = CrashPack::builder(CrashPackConfig::default())
2755 .failure(sample_failure())
2756 .build()
2757 .expect("crash pack builder should have failure metadata");
2758
2759 assert!(pack.replay.is_none());
2760
2761 let writer = MemoryCrashPackWriter::new();
2763 writer.write(&pack).unwrap();
2764 let json_str = &writer.written()[0].1;
2765 assert!(!json_str.contains("\"replay\""));
2766
2767 crate::test_complete!("replay_command_absent_by_default");
2768 }
2769
2770 #[test]
2771 fn replay_command_convenience_method() {
2772 init_test("replay_command_convenience_method");
2773
2774 let pack = CrashPack::builder(CrashPackConfig {
2775 seed: 77,
2776 worker_count: 2,
2777 ..Default::default()
2778 })
2779 .failure(sample_failure())
2780 .build()
2781 .expect("crash pack builder should have failure metadata");
2782
2783 let cmd = pack.replay_command(Some("output.json"));
2784 assert!(cmd.command_line.contains("--seed"));
2785 assert!(cmd.command_line.contains("77"));
2786 assert!(cmd.command_line.contains("output.json"));
2787
2788 crate::test_complete!("replay_command_convenience_method");
2789 }
2790
2791 #[test]
2792 fn replay_command_max_steps_included_when_set() {
2793 init_test("replay_command_max_steps_included_when_set");
2794
2795 let with_steps = ReplayCommand::from_config(
2796 &CrashPackConfig {
2797 seed: 1,
2798 max_steps: Some(999),
2799 ..Default::default()
2800 },
2801 None,
2802 );
2803 assert!(with_steps.command_line.contains("ASUPERSYNC_MAX_STEPS=999"));
2804
2805 let without_steps = ReplayCommand::from_config(
2806 &CrashPackConfig {
2807 seed: 1,
2808 max_steps: None,
2809 ..Default::default()
2810 },
2811 None,
2812 );
2813 assert!(!without_steps.command_line.contains("ASUPERSYNC_MAX_STEPS"));
2814
2815 crate::test_complete!("replay_command_max_steps_included_when_set");
2816 }
2817
2818 #[test]
2819 fn shell_escape_handles_special_chars() {
2820 init_test("shell_escape_handles_special_chars");
2821
2822 assert_eq!(shell_escape("hello"), "hello");
2824 assert_eq!(shell_escape("path/to/file.json"), "path/to/file.json");
2825 assert_eq!(shell_escape("42"), "42");
2826
2827 assert_eq!(shell_escape("hello world"), "'hello world'");
2829
2830 assert_eq!(shell_escape(""), "''");
2832
2833 crate::test_complete!("shell_escape_handles_special_chars");
2834 }
2835
2836 fn golden_failure_events() -> Vec<TraceEvent> {
2842 vec![
2843 TraceEvent::region_created(1, Time::ZERO, rid(1), None),
2844 TraceEvent::spawn(2, Time::ZERO, tid(1), rid(1)),
2845 TraceEvent::spawn(3, Time::ZERO, tid(2), rid(1)),
2846 TraceEvent::poll(4, Time::from_nanos(100), tid(1), rid(1)),
2847 TraceEvent::poll(5, Time::from_nanos(100), tid(2), rid(1)),
2848 TraceEvent::complete(6, Time::from_nanos(200), tid(1), rid(1)),
2849 ]
2850 }
2851
2852 fn golden_config() -> CrashPackConfig {
2853 CrashPackConfig {
2854 seed: 42,
2855 config_hash: 0xDEAD,
2856 worker_count: 4,
2857 max_steps: Some(1000),
2858 commit_hash: Some("abc123def".to_string()),
2859 }
2860 }
2861
2862 fn golden_failure_info() -> FailureInfo {
2863 FailureInfo {
2864 task: tid(2),
2865 region: rid(1),
2866 outcome: FailureOutcome::Panicked {
2867 message: "worker panic in golden scenario".to_string(),
2868 },
2869 virtual_time: Time::from_nanos(200),
2870 }
2871 }
2872
2873 #[test]
2874 fn golden_deterministic_emission() {
2875 init_test("golden_deterministic_emission");
2876
2877 let events = golden_failure_events();
2878
2879 let pack1 = CrashPack::builder(golden_config())
2881 .failure(golden_failure_info())
2882 .from_trace(&events)
2883 .build()
2884 .expect("crash pack builder should have failure metadata");
2885
2886 let pack2 = CrashPack::builder(golden_config())
2887 .failure(golden_failure_info())
2888 .from_trace(&events)
2889 .build()
2890 .expect("crash pack builder should have failure metadata");
2891
2892 assert_eq!(pack1, pack2);
2894 assert_eq!(pack1.fingerprint(), pack2.fingerprint());
2895 assert_eq!(pack1.canonical_prefix, pack2.canonical_prefix);
2896 assert_eq!(pack1.manifest.event_count, pack2.manifest.event_count);
2897
2898 crate::test_complete!("golden_deterministic_emission");
2899 }
2900
2901 #[test]
2902 fn golden_fingerprint_stability() {
2903 init_test("golden_fingerprint_stability");
2904
2905 let events = golden_failure_events();
2906 let pack = CrashPack::builder(golden_config())
2907 .failure(golden_failure_info())
2908 .from_trace(&events)
2909 .build()
2910 .expect("crash pack builder should have failure metadata");
2911
2912 let fp = pack.fingerprint();
2914 assert_ne!(fp, 0);
2915
2916 let fp2 = CrashPack::builder(golden_config())
2918 .failure(golden_failure_info())
2919 .from_trace(&events)
2920 .build()
2921 .expect("crash pack builder should have failure metadata")
2922 .fingerprint();
2923 assert_eq!(fp, fp2);
2924
2925 assert_eq!(fp, crate::trace::canonicalize::trace_fingerprint(&events));
2927
2928 crate::test_complete!("golden_fingerprint_stability");
2929 }
2930
2931 #[test]
2932 fn golden_canonical_prefix_structure() {
2933 init_test("golden_canonical_prefix_structure");
2934
2935 let events = golden_failure_events();
2936 let pack = CrashPack::builder(golden_config())
2937 .failure(golden_failure_info())
2938 .from_trace(&events)
2939 .build()
2940 .expect("crash pack builder should have failure metadata");
2941
2942 assert_eq!(
2948 pack.canonical_prefix.len(),
2949 4,
2950 "expected 4 Foata layers, got {}",
2951 pack.canonical_prefix.len()
2952 );
2953 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);
2960
2961 crate::test_complete!("golden_canonical_prefix_structure");
2962 }
2963
2964 #[test]
2965 fn golden_equivalent_schedule_same_pack() {
2966 init_test("golden_equivalent_schedule_same_pack");
2967
2968 let events_a = golden_failure_events();
2971 let events_b = vec![
2972 TraceEvent::region_created(1, Time::ZERO, rid(1), None),
2973 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)),
2976 TraceEvent::poll(5, Time::from_nanos(100), tid(1), rid(1)),
2977 TraceEvent::complete(6, Time::from_nanos(200), tid(1), rid(1)),
2978 ];
2979
2980 let pack_a = CrashPack::builder(golden_config())
2981 .failure(golden_failure_info())
2982 .from_trace(&events_a)
2983 .build()
2984 .expect("crash pack builder should have failure metadata");
2985 let pack_b = CrashPack::builder(golden_config())
2986 .failure(golden_failure_info())
2987 .from_trace(&events_b)
2988 .build()
2989 .expect("crash pack builder should have failure metadata");
2990
2991 assert_eq!(pack_a.fingerprint(), pack_b.fingerprint());
2993 assert_eq!(pack_a.canonical_prefix, pack_b.canonical_prefix);
2994 assert_eq!(pack_a, pack_b);
2995
2996 crate::test_complete!("golden_equivalent_schedule_same_pack");
2997 }
2998
2999 #[test]
3000 fn golden_replay_prefix_round_trip() {
3001 use crate::trace::replay::{
3002 CompactRegionId, CompactTaskId, ReplayEvent, ReplayTrace, TraceMetadata,
3003 };
3004 use crate::trace::replayer::TraceReplayer;
3005
3006 init_test("golden_replay_prefix_round_trip");
3007
3008 let replay_events = vec![
3010 ReplayEvent::RngSeed { seed: 42 },
3011 ReplayEvent::RegionCreated {
3012 region: CompactRegionId(1),
3013 parent: None,
3014 at_tick: 0,
3015 },
3016 ReplayEvent::TaskSpawned {
3017 task: CompactTaskId(1),
3018 region: CompactRegionId(1),
3019 at_tick: 0,
3020 },
3021 ReplayEvent::TaskSpawned {
3022 task: CompactTaskId(2),
3023 region: CompactRegionId(1),
3024 at_tick: 0,
3025 },
3026 ReplayEvent::TaskScheduled {
3027 task: CompactTaskId(1),
3028 at_tick: 100,
3029 },
3030 ReplayEvent::TaskScheduled {
3031 task: CompactTaskId(2),
3032 at_tick: 100,
3033 },
3034 ReplayEvent::TaskCompleted {
3035 task: CompactTaskId(1),
3036 outcome: 0, },
3038 ];
3039
3040 let trace = ReplayTrace {
3041 metadata: TraceMetadata::new(42),
3042 events: replay_events.clone(),
3043 cursor: 0,
3044 };
3045
3046 let pack = CrashPack::builder(golden_config())
3048 .failure(golden_failure_info())
3049 .from_trace(&golden_failure_events())
3050 .divergent_prefix(replay_events.clone())
3051 .build()
3052 .expect("crash pack builder should have failure metadata");
3053
3054 assert!(pack.has_divergent_prefix());
3055 assert_eq!(pack.divergent_prefix.len(), 7);
3056
3057 let mut replayer = TraceReplayer::new(trace);
3060 for expected_event in &replay_events {
3061 let actual = replayer.next().expect("replayer should have more events");
3062 assert_eq!(actual, expected_event);
3063 }
3064 assert!(replayer.is_completed());
3065
3066 crate::test_complete!("golden_replay_prefix_round_trip");
3067 }
3068
3069 #[test]
3070 fn golden_replay_serialization_round_trip() {
3071 use crate::trace::replay::{
3072 CompactRegionId, CompactTaskId, ReplayEvent, ReplayTrace, TraceMetadata,
3073 };
3074
3075 init_test("golden_replay_serialization_round_trip");
3076
3077 let replay_events = vec![
3078 ReplayEvent::RngSeed { seed: 42 },
3079 ReplayEvent::TaskSpawned {
3080 task: CompactTaskId(1),
3081 region: CompactRegionId(1),
3082 at_tick: 0,
3083 },
3084 ReplayEvent::TaskCompleted {
3085 task: CompactTaskId(1),
3086 outcome: 3, },
3088 ];
3089
3090 let mut trace = ReplayTrace::new(TraceMetadata::new(42));
3091 for ev in &replay_events {
3092 trace.push(ev.clone());
3093 }
3094
3095 let bytes = trace.to_bytes().expect("serialize");
3097 let loaded = ReplayTrace::from_bytes(&bytes).expect("deserialize");
3098
3099 assert_eq!(loaded.metadata.seed, 42);
3100 assert_eq!(loaded.events.len(), 3);
3101 assert_eq!(loaded.events, replay_events);
3102
3103 crate::test_complete!("golden_replay_serialization_round_trip");
3104 }
3105
3106 #[test]
3107 fn golden_crash_pack_json_round_trip() {
3108 init_test("golden_crash_pack_json_round_trip");
3109
3110 let events = golden_failure_events();
3111 let pack = CrashPack::builder(golden_config())
3112 .failure(golden_failure_info())
3113 .from_trace(&events)
3114 .oracle_violations(vec!["invariant-x".into()])
3115 .build()
3116 .expect("crash pack builder should have failure metadata");
3117
3118 let writer = MemoryCrashPackWriter::new();
3119 writer.write(&pack).unwrap();
3120 let written = writer.written();
3121 let json = &written[0].1;
3122
3123 let parsed: serde_json::Value = serde_json::from_str(json).unwrap();
3125 assert_eq!(parsed["manifest"]["config"]["seed"], 42);
3126 assert_eq!(parsed["manifest"]["config"]["config_hash"], 0xDEAD_u64);
3127 assert_eq!(parsed["manifest"]["event_count"], 6);
3128 assert_ne!(parsed["manifest"]["fingerprint"], 0);
3129 assert_eq!(parsed["oracle_violations"][0], "invariant-x");
3130
3131 let prefix = &parsed["canonical_prefix"];
3133 assert!(prefix.is_array());
3134 assert_eq!(prefix.as_array().unwrap().len(), 4); crate::test_complete!("golden_crash_pack_json_round_trip");
3137 }
3138
3139 #[test]
3140 fn golden_minimization_integration() {
3141 use crate::trace::divergence::{MinimizationConfig, minimize_divergent_prefix};
3142 use crate::trace::replay::{ReplayEvent, ReplayTrace, TraceMetadata};
3143
3144 init_test("golden_minimization_integration");
3145
3146 let replay_events: Vec<_> = (0..20)
3148 .map(|i| ReplayEvent::RngValue { value: i })
3149 .collect();
3150
3151 let trace = ReplayTrace {
3152 metadata: TraceMetadata::new(42),
3153 events: replay_events,
3154 cursor: 0,
3155 };
3156
3157 let threshold = 12;
3159 let result = minimize_divergent_prefix(&trace, &MinimizationConfig::default(), |prefix| {
3160 prefix.len() >= threshold
3161 });
3162
3163 assert_eq!(result.minimized_len, threshold);
3164 assert_eq!(result.original_len, 20);
3165 assert!(!result.truncated);
3166
3167 let pack = CrashPack::builder(golden_config())
3169 .failure(golden_failure_info())
3170 .from_trace(&golden_failure_events())
3171 .divergent_prefix(result.prefix.events)
3172 .build()
3173 .expect("crash pack builder should have failure metadata");
3174
3175 assert!(pack.has_divergent_prefix());
3176 assert_eq!(pack.divergent_prefix.len(), threshold);
3177
3178 crate::test_complete!("golden_minimization_integration");
3179 }
3180
3181 #[test]
3200 fn walkthrough_01_forced_failure_and_emission() {
3201 init_test("walkthrough_01_forced_failure_and_emission");
3202
3203 let events = vec![
3208 TraceEvent::region_created(1, Time::ZERO, rid(1), None),
3209 TraceEvent::spawn(2, Time::ZERO, tid(1), rid(1)),
3210 TraceEvent::spawn(3, Time::ZERO, tid(2), rid(1)),
3211 TraceEvent::poll(4, Time::from_nanos(100), tid(1), rid(1)),
3212 TraceEvent::poll(5, Time::from_nanos(100), tid(2), rid(1)),
3213 TraceEvent::complete(6, Time::from_nanos(200), tid(1), rid(1)),
3215 ];
3216
3217 let failure = FailureInfo {
3219 task: tid(2),
3220 region: rid(1),
3221 outcome: FailureOutcome::Panicked {
3222 message: "assertion failed: balance >= 0".to_string(),
3223 },
3224 virtual_time: Time::from_nanos(200),
3225 };
3226
3227 let config = CrashPackConfig {
3232 seed: 42,
3233 config_hash: 0xCAFE,
3234 worker_count: 2,
3235 max_steps: Some(500),
3236 commit_hash: Some("a1b2c3d".to_string()),
3237 };
3238
3239 let pack = CrashPack::builder(config)
3240 .failure(failure)
3241 .from_trace(&events)
3242 .oracle_violations(vec!["balance-invariant".to_string()])
3243 .build()
3244 .expect("crash pack builder should have failure metadata");
3245
3246 assert_eq!(pack.seed(), 42);
3248 assert_eq!(pack.manifest.schema_version, CRASHPACK_SCHEMA_VERSION);
3249 assert_eq!(pack.manifest.event_count, 6);
3250 assert!(
3251 pack.manifest.fingerprint != 0,
3252 "fingerprint should be non-zero"
3253 );
3254 assert!(pack.has_violations());
3255 assert_eq!(pack.oracle_violations, vec!["balance-invariant"]);
3256
3257 assert!(
3259 !pack.canonical_prefix.is_empty(),
3260 "canonical prefix should have Foata layers"
3261 );
3262
3263 assert!(
3265 pack.manifest
3266 .has_attachment(&AttachmentKind::CanonicalPrefix)
3267 );
3268 assert!(
3269 pack.manifest
3270 .has_attachment(&AttachmentKind::OracleViolations)
3271 );
3272
3273 crate::test_complete!("walkthrough_01_forced_failure_and_emission");
3274 }
3275
3276 #[test]
3281 fn walkthrough_02_write_and_read_artifact() {
3282 init_test("walkthrough_02_write_and_read_artifact");
3283
3284 let pack = walkthrough_pack();
3285
3286 let writer = MemoryCrashPackWriter::new();
3288 let artifact = writer.write(&pack).expect("write should succeed");
3289
3290 assert!(
3292 artifact.path().starts_with("crashpack-000000000000002a-"),
3293 "path should encode seed 42 (0x2a): {}",
3294 artifact.path()
3295 );
3296 assert!(
3297 artifact.path().ends_with("-v1.json"),
3298 "path should end with schema version: {}",
3299 artifact.path()
3300 );
3301
3302 let written = writer.written();
3304 assert_eq!(written.len(), 1);
3305 let json = &written[0].1;
3306 let parsed: serde_json::Value = serde_json::from_str(json).expect("valid JSON");
3307
3308 assert_eq!(parsed["manifest"]["config"]["seed"], 42);
3310 assert_eq!(parsed["manifest"]["schema_version"], 1);
3311
3312 assert!(
3314 parsed["failure"]["outcome"]["Panicked"]["message"]
3315 .as_str()
3316 .unwrap()
3317 .contains("balance >= 0"),
3318 "failure message should be preserved"
3319 );
3320
3321 crate::test_complete!("walkthrough_02_write_and_read_artifact");
3322 }
3323
3324 #[test]
3329 fn walkthrough_03_fingerprint_interpretation() {
3330 use crate::trace::canonicalize::trace_fingerprint;
3331
3332 init_test("walkthrough_03_fingerprint_interpretation");
3333
3334 let schedule_a = vec![
3336 TraceEvent::region_created(1, Time::ZERO, rid(1), None),
3337 TraceEvent::spawn(2, Time::ZERO, tid(1), rid(1)),
3338 TraceEvent::spawn(3, Time::ZERO, tid(2), rid(1)),
3339 TraceEvent::poll(4, Time::from_nanos(100), tid(1), rid(1)),
3340 TraceEvent::poll(5, Time::from_nanos(100), tid(2), rid(1)),
3341 TraceEvent::complete(6, Time::from_nanos(200), tid(1), rid(1)),
3342 ];
3343
3344 let schedule_b = vec![
3346 TraceEvent::region_created(1, Time::ZERO, rid(1), None),
3347 TraceEvent::spawn(2, Time::ZERO, tid(1), rid(1)),
3348 TraceEvent::spawn(3, Time::ZERO, tid(2), rid(1)),
3349 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)),
3352 ];
3353
3354 let fp_a = trace_fingerprint(&schedule_a);
3355 let fp_b = trace_fingerprint(&schedule_b);
3356
3357 assert_eq!(
3361 fp_a, fp_b,
3362 "equivalent schedules should have the same canonical fingerprint"
3363 );
3364
3365 crate::test_complete!("walkthrough_03_fingerprint_interpretation");
3366 }
3367
3368 #[test]
3373 fn walkthrough_04_replay_command() {
3374 init_test("walkthrough_04_replay_command");
3375
3376 let pack = walkthrough_pack();
3377
3378 let replay = pack.replay_command(None);
3380 assert_eq!(replay.program, "cargo");
3381 assert!(replay.args.contains(&"--seed".to_string()));
3382 assert!(replay.args.contains(&"42".to_string()));
3383
3384 assert!(
3386 replay.command_line.contains("cargo test"),
3387 "command line should contain cargo test: {}",
3388 replay.command_line
3389 );
3390 assert!(
3391 replay.command_line.contains("--seed 42"),
3392 "command line should contain seed: {}",
3393 replay.command_line
3394 );
3395
3396 let replay_with_path = pack.replay_command(Some("/tmp/crashpacks/my_pack.json"));
3398 assert!(
3399 replay_with_path
3400 .command_line
3401 .contains("/tmp/crashpacks/my_pack.json"),
3402 "command line should reference artifact: {}",
3403 replay_with_path.command_line
3404 );
3405
3406 let cli_replay =
3408 ReplayCommand::from_config_cli(&pack.manifest.config, "/tmp/crashpack.json");
3409 assert_eq!(cli_replay.program, "asupersync");
3410 assert!(
3411 cli_replay.command_line.contains("trace replay"),
3412 "CLI mode should use 'trace replay' subcommand: {}",
3413 cli_replay.command_line
3414 );
3415
3416 let display = format!("{replay}");
3418 assert_eq!(display, replay.command_line);
3419
3420 crate::test_complete!("walkthrough_04_replay_command");
3421 }
3422
3423 #[test]
3428 fn walkthrough_05_minimization() {
3429 use crate::trace::divergence::{MinimizationConfig, minimize_divergent_prefix};
3430 use crate::trace::replay::{ReplayEvent, ReplayTrace, TraceMetadata};
3431
3432 init_test("walkthrough_05_minimization");
3433
3434 let replay_events: Vec<_> = (0..50)
3436 .map(|i| ReplayEvent::RngValue { value: i })
3437 .collect();
3438
3439 let trace = ReplayTrace {
3440 metadata: TraceMetadata::new(42),
3441 events: replay_events,
3442 cursor: 0,
3443 };
3444
3445 let failure_threshold = 15;
3447 let result = minimize_divergent_prefix(&trace, &MinimizationConfig::default(), |prefix| {
3448 prefix.len() >= failure_threshold
3449 });
3450
3451 assert_eq!(result.minimized_len, failure_threshold);
3452 assert_eq!(result.original_len, 50);
3453
3454 let config = CrashPackConfig {
3456 seed: 42,
3457 config_hash: 0xCAFE,
3458 worker_count: 2,
3459 max_steps: Some(500),
3460 commit_hash: Some("a1b2c3d".to_string()),
3461 };
3462
3463 let failure = FailureInfo {
3464 task: tid(2),
3465 region: rid(1),
3466 outcome: FailureOutcome::Panicked {
3467 message: "assertion failed: balance >= 0".to_string(),
3468 },
3469 virtual_time: Time::from_nanos(200),
3470 };
3471
3472 let pack = CrashPack::builder(config)
3473 .failure(failure)
3474 .divergent_prefix(result.prefix.events)
3475 .fingerprint(0xABCD)
3476 .build()
3477 .expect("crash pack builder should have failure metadata");
3478
3479 assert!(pack.has_divergent_prefix());
3480 assert_eq!(
3481 pack.divergent_prefix.len(),
3482 failure_threshold,
3483 "minimized prefix should be {failure_threshold} events, not {}",
3484 pack.divergent_prefix.len()
3485 );
3486
3487 assert!(
3489 pack.manifest
3490 .has_attachment(&AttachmentKind::DivergentPrefix)
3491 );
3492 let att = pack
3493 .manifest
3494 .attachment(&AttachmentKind::DivergentPrefix)
3495 .unwrap();
3496 assert_eq!(att.item_count, failure_threshold as u64);
3497
3498 crate::test_complete!("walkthrough_05_minimization");
3499 }
3500
3501 fn walkthrough_pack() -> CrashPack {
3503 let events = vec![
3504 TraceEvent::region_created(1, Time::ZERO, rid(1), None),
3505 TraceEvent::spawn(2, Time::ZERO, tid(1), rid(1)),
3506 TraceEvent::spawn(3, Time::ZERO, tid(2), rid(1)),
3507 TraceEvent::poll(4, Time::from_nanos(100), tid(1), rid(1)),
3508 TraceEvent::poll(5, Time::from_nanos(100), tid(2), rid(1)),
3509 TraceEvent::complete(6, Time::from_nanos(200), tid(1), rid(1)),
3510 ];
3511
3512 let config = CrashPackConfig {
3513 seed: 42,
3514 config_hash: 0xCAFE,
3515 worker_count: 2,
3516 max_steps: Some(500),
3517 commit_hash: Some("a1b2c3d".to_string()),
3518 };
3519
3520 let failure = FailureInfo {
3521 task: tid(2),
3522 region: rid(1),
3523 outcome: FailureOutcome::Panicked {
3524 message: "assertion failed: balance >= 0".to_string(),
3525 },
3526 virtual_time: Time::from_nanos(200),
3527 };
3528
3529 CrashPack::builder(config)
3530 .failure(failure)
3531 .from_trace(&events)
3532 .oracle_violations(vec!["balance-invariant".to_string()])
3533 .build()
3534 .expect("crash pack builder should have failure metadata")
3535 }
3536
3537 #[test]
3540 fn crash_pack_config_debug_clone_eq_default() {
3541 let c = CrashPackConfig::default();
3542 assert_eq!(c.seed, 0);
3543 assert_eq!(c.config_hash, 0);
3544 assert_eq!(c.worker_count, 1);
3545 assert_eq!(c.max_steps, None);
3546 assert_eq!(c.commit_hash, None);
3547 let c2 = c.clone();
3548 assert_eq!(c, c2);
3549 let dbg = format!("{c:?}");
3550 assert!(dbg.contains("CrashPackConfig"));
3551 }
3552
3553 #[test]
3554 fn failure_outcome_debug_clone_eq() {
3555 let e = FailureOutcome::Err;
3556 let e2 = e.clone();
3557 assert_eq!(e, e2);
3558 assert_ne!(
3559 e,
3560 FailureOutcome::Panicked {
3561 message: "boom".into()
3562 }
3563 );
3564 let c = FailureOutcome::Cancelled {
3565 cancel_kind: CancelKind::User,
3566 };
3567 let c2 = c.clone();
3568 assert_eq!(c, c2);
3569 let dbg = format!("{e:?}");
3570 assert!(dbg.contains("Err"));
3571 }
3572
3573 #[test]
3574 fn attachment_kind_debug_clone_eq() {
3575 let a = AttachmentKind::CanonicalPrefix;
3576 let a2 = a.clone();
3577 assert_eq!(a, a2);
3578 assert_ne!(a, AttachmentKind::DivergentPrefix);
3579 assert_ne!(a, AttachmentKind::EvidenceLedger);
3580 assert_ne!(a, AttachmentKind::SupervisionLog);
3581 assert_ne!(a, AttachmentKind::OracleViolations);
3582 let custom = AttachmentKind::Custom {
3583 tag: "my_data".into(),
3584 };
3585 let custom2 = custom.clone();
3586 assert_eq!(custom, custom2);
3587 let dbg = format!("{a:?}");
3588 assert!(dbg.contains("CanonicalPrefix"));
3589 }
3590
3591 #[test]
3592 fn manifest_validation_error_debug_clone_eq() {
3593 let e = ManifestValidationError::VersionTooNew {
3594 manifest_version: 5,
3595 supported_version: 1,
3596 };
3597 let e2 = e.clone();
3598 assert_eq!(e, e2);
3599 assert_ne!(
3600 e,
3601 ManifestValidationError::VersionTooOld {
3602 manifest_version: 0,
3603 minimum_version: 1,
3604 }
3605 );
3606 let dbg = format!("{e:?}");
3607 assert!(dbg.contains("VersionTooNew"));
3608 }
3609
3610 #[test]
3611 fn evidence_entry_snapshot_debug_clone_eq() {
3612 let s = EvidenceEntrySnapshot {
3613 birth: 0,
3614 death: 5,
3615 is_novel: true,
3616 persistence: Some(5),
3617 };
3618 let s2 = s.clone();
3619 assert_eq!(s, s2);
3620 let dbg = format!("{s:?}");
3621 assert!(dbg.contains("EvidenceEntrySnapshot"));
3622 }
3623
3624 #[test]
3625 fn supervision_snapshot_debug_clone_eq() {
3626 let s = SupervisionSnapshot {
3627 virtual_time: Time::from_secs(1),
3628 task: tid(1),
3629 region: rid(0),
3630 decision: "restart".into(),
3631 context: Some("attempt 2".into()),
3632 };
3633 let s2 = s.clone();
3634 assert_eq!(s, s2);
3635 let dbg = format!("{s:?}");
3636 assert!(dbg.contains("SupervisionSnapshot"));
3637 }
3638
3639 #[test]
3640 fn manifest_attachment_debug_clone_eq() {
3641 let a = ManifestAttachment {
3642 kind: AttachmentKind::EvidenceLedger,
3643 item_count: 10,
3644 size_hint_bytes: 256,
3645 };
3646 let a2 = a.clone();
3647 assert_eq!(a, a2);
3648 let dbg = format!("{a:?}");
3649 assert!(dbg.contains("ManifestAttachment"));
3650 }
3651
3652 #[test]
3653 fn crash_pack_manifest_debug_clone_eq() {
3654 let m = CrashPackManifest {
3655 schema_version: CRASHPACK_SCHEMA_VERSION,
3656 config: CrashPackConfig::default(),
3657 fingerprint: 0xABCD,
3658 event_count: 100,
3659 created_at: 0,
3660 attachments: vec![],
3661 content_checksum: None,
3662 };
3663 let m2 = m.clone();
3664 assert_eq!(m, m2);
3665 let dbg = format!("{m:?}");
3666 assert!(dbg.contains("CrashPackManifest"));
3667 }
3668}