1use crate::models::{Error, Result};
19use std::path::{Path, PathBuf};
20use std::time::{SystemTime, UNIX_EPOCH};
21
22#[derive(Debug, Clone, Copy, PartialEq, Eq)]
24pub enum RunStatus {
25 Running,
26 Completed,
27 Failed,
28 Aborted,
29}
30
31impl RunStatus {
32 pub fn as_str(&self) -> &'static str {
33 match self {
34 Self::Running => "running",
35 Self::Completed => "completed",
36 Self::Failed => "failed",
37 Self::Aborted => "aborted",
38 }
39 }
40
41 pub fn parse(s: &str) -> Option<Self> {
43 match s {
44 "running" => Some(Self::Running),
45 "completed" => Some(Self::Completed),
46 "failed" => Some(Self::Failed),
47 "aborted" => Some(Self::Aborted),
48 _ => None,
49 }
50 }
51}
52
53#[derive(Debug, Clone, Copy, PartialEq, Eq)]
55pub enum StepStatus {
56 Pending,
57 Running,
58 Completed,
59 Failed,
60 Skipped,
61}
62
63impl StepStatus {
64 pub fn as_str(&self) -> &'static str {
65 match self {
66 Self::Pending => "pending",
67 Self::Running => "running",
68 Self::Completed => "completed",
69 Self::Failed => "failed",
70 Self::Skipped => "skipped",
71 }
72 }
73
74 pub fn parse(s: &str) -> Option<Self> {
76 match s {
77 "pending" => Some(Self::Pending),
78 "running" => Some(Self::Running),
79 "completed" => Some(Self::Completed),
80 "failed" => Some(Self::Failed),
81 "skipped" => Some(Self::Skipped),
82 _ => None,
83 }
84 }
85}
86
87#[derive(Debug, Clone)]
89pub struct InstallerRun {
90 pub run_id: String,
92 pub installer_name: String,
94 pub installer_version: String,
96 pub started_at: u64,
98 pub completed_at: Option<u64>,
100 pub status: RunStatus,
102 pub hermetic_mode: bool,
104 pub lockfile_hash: Option<String>,
106}
107
108impl InstallerRun {
109 pub fn new(installer_name: &str, installer_version: &str) -> Self {
111 let run_id = generate_run_id();
112 let started_at = current_timestamp();
113
114 Self {
115 run_id,
116 installer_name: installer_name.to_string(),
117 installer_version: installer_version.to_string(),
118 started_at,
119 completed_at: None,
120 status: RunStatus::Running,
121 hermetic_mode: false,
122 lockfile_hash: None,
123 }
124 }
125
126 pub fn new_hermetic(
128 installer_name: &str,
129 installer_version: &str,
130 lockfile_hash: &str,
131 ) -> Self {
132 let mut run = Self::new(installer_name, installer_version);
133 run.hermetic_mode = true;
134 run.lockfile_hash = Some(lockfile_hash.to_string());
135 run
136 }
137
138 pub fn complete(&mut self) {
140 self.status = RunStatus::Completed;
141 self.completed_at = Some(current_timestamp());
142 }
143
144 pub fn fail(&mut self) {
146 self.status = RunStatus::Failed;
147 self.completed_at = Some(current_timestamp());
148 }
149}
150
151#[derive(Debug, Clone)]
153pub struct StepCheckpoint {
154 pub run_id: String,
156 pub step_id: String,
158 pub status: StepStatus,
160 pub started_at: Option<u64>,
162 pub completed_at: Option<u64>,
164 pub duration_ms: Option<u64>,
166 pub state_snapshot: Option<String>,
168 pub output_log: Option<String>,
170 pub error_message: Option<String>,
172}
173
174impl StepCheckpoint {
175 pub fn new(run_id: &str, step_id: &str) -> Self {
177 Self {
178 run_id: run_id.to_string(),
179 step_id: step_id.to_string(),
180 status: StepStatus::Pending,
181 started_at: None,
182 completed_at: None,
183 duration_ms: None,
184 state_snapshot: None,
185 output_log: None,
186 error_message: None,
187 }
188 }
189
190 pub fn start(&mut self) {
192 self.status = StepStatus::Running;
193 self.started_at = Some(current_timestamp());
194 }
195
196 pub fn complete(&mut self, output: Option<String>) {
198 self.status = StepStatus::Completed;
199 self.completed_at = Some(current_timestamp());
200 self.output_log = output;
201 if let (Some(start), Some(end)) = (self.started_at, self.completed_at) {
202 self.duration_ms = Some((end - start) * 1000);
203 }
204 }
205
206 pub fn fail(&mut self, error: &str) {
208 self.status = StepStatus::Failed;
209 self.completed_at = Some(current_timestamp());
210 self.error_message = Some(error.to_string());
211 if let (Some(start), Some(end)) = (self.started_at, self.completed_at) {
212 self.duration_ms = Some((end - start) * 1000);
213 }
214 }
215
216 pub fn skip(&mut self) {
218 self.status = StepStatus::Skipped;
219 }
220}
221
222#[derive(Debug, Clone)]
224pub struct StateFile {
225 pub run_id: String,
227 pub step_id: String,
229 pub file_path: PathBuf,
231 pub content_hash: String,
233 pub backed_up_at: Option<u64>,
235 pub backup_path: Option<PathBuf>,
237}
238
239impl StateFile {
240 pub fn new(run_id: &str, step_id: &str, file_path: &Path, content_hash: &str) -> Self {
242 Self {
243 run_id: run_id.to_string(),
244 step_id: step_id.to_string(),
245 file_path: file_path.to_path_buf(),
246 content_hash: content_hash.to_string(),
247 backed_up_at: None,
248 backup_path: None,
249 }
250 }
251
252 pub fn set_backup(&mut self, backup_path: &Path) {
254 self.backed_up_at = Some(current_timestamp());
255 self.backup_path = Some(backup_path.to_path_buf());
256 }
257}
258
259#[derive(Debug)]
261pub struct CheckpointStore {
262 checkpoint_dir: PathBuf,
264 current_run: Option<InstallerRun>,
266 steps: Vec<StepCheckpoint>,
268 state_files: Vec<StateFile>,
270}
271
272impl CheckpointStore {
273 pub fn new(checkpoint_dir: &Path) -> Result<Self> {
275 std::fs::create_dir_all(checkpoint_dir).map_err(|e| {
277 Error::Io(std::io::Error::new(
278 e.kind(),
279 format!("Failed to create checkpoint directory: {}", e),
280 ))
281 })?;
282
283 Ok(Self {
284 checkpoint_dir: checkpoint_dir.to_path_buf(),
285 current_run: None,
286 steps: Vec::new(),
287 state_files: Vec::new(),
288 })
289 }
290
291 pub fn start_run(&mut self, installer_name: &str, installer_version: &str) -> Result<String> {
293 let run = InstallerRun::new(installer_name, installer_version);
294 let run_id = run.run_id.clone();
295 self.current_run = Some(run);
296 self.steps.clear();
297 self.state_files.clear();
298 self.save()?;
299 Ok(run_id)
300 }
301
302 pub fn start_hermetic_run(
304 &mut self,
305 installer_name: &str,
306 installer_version: &str,
307 lockfile_hash: &str,
308 ) -> Result<String> {
309 let run = InstallerRun::new_hermetic(installer_name, installer_version, lockfile_hash);
310 let run_id = run.run_id.clone();
311 self.current_run = Some(run);
312 self.steps.clear();
313 self.state_files.clear();
314 self.save()?;
315 Ok(run_id)
316 }
317
318 pub fn current_run_id(&self) -> Option<&str> {
320 self.current_run.as_ref().map(|r| r.run_id.as_str())
321 }
322
323 pub fn add_step(&mut self, step_id: &str) -> Result<()> {
325 let run_id = self
326 .current_run
327 .as_ref()
328 .ok_or_else(|| Error::Validation("No active run".to_string()))?
329 .run_id
330 .clone();
331
332 let checkpoint = StepCheckpoint::new(&run_id, step_id);
333 self.steps.push(checkpoint);
334 self.save()
335 }
336
337 pub fn start_step(&mut self, step_id: &str) -> Result<()> {
339 let step = self
340 .steps
341 .iter_mut()
342 .find(|s| s.step_id == step_id)
343 .ok_or_else(|| Error::Validation(format!("Step not found: {}", step_id)))?;
344
345 step.start();
346 self.save()
347 }
348
349 pub fn complete_step(&mut self, step_id: &str, output: Option<String>) -> Result<()> {
351 let step = self
352 .steps
353 .iter_mut()
354 .find(|s| s.step_id == step_id)
355 .ok_or_else(|| Error::Validation(format!("Step not found: {}", step_id)))?;
356
357 step.complete(output);
358 self.save()
359 }
360
361 pub fn fail_step(&mut self, step_id: &str, error: &str) -> Result<()> {
363 let step = self
364 .steps
365 .iter_mut()
366 .find(|s| s.step_id == step_id)
367 .ok_or_else(|| Error::Validation(format!("Step not found: {}", step_id)))?;
368
369 step.fail(error);
370
371 if let Some(ref mut run) = self.current_run {
373 run.fail();
374 }
375
376 self.save()
377 }
378
379 pub fn complete_run(&mut self) -> Result<()> {
381 if let Some(ref mut run) = self.current_run {
382 run.complete();
383 }
384 self.save()
385 }
386
387 pub fn last_successful_step(&self) -> Option<&StepCheckpoint> {
389 self.steps
390 .iter()
391 .rev()
392 .find(|s| s.status == StepStatus::Completed)
393 }
394
395 pub fn get_step(&self, step_id: &str) -> Option<&StepCheckpoint> {
397 self.steps.iter().find(|s| s.step_id == step_id)
398 }
399
400 pub fn steps(&self) -> &[StepCheckpoint] {
402 &self.steps
403 }
404
405 pub fn track_file(
407 &mut self,
408 step_id: &str,
409 file_path: &Path,
410 content_hash: &str,
411 ) -> Result<()> {
412 let run_id = self
413 .current_run
414 .as_ref()
415 .ok_or_else(|| Error::Validation("No active run".to_string()))?
416 .run_id
417 .clone();
418
419 let state_file = StateFile::new(&run_id, step_id, file_path, content_hash);
420 self.state_files.push(state_file);
421 self.save()
422 }
423
424 pub fn state_files_for_step(&self, step_id: &str) -> Vec<&StateFile> {
426 self.state_files
427 .iter()
428 .filter(|sf| sf.step_id == step_id)
429 .collect()
430 }
431
432 pub fn is_hermetic(&self) -> bool {
434 self.current_run
435 .as_ref()
436 .map(|r| r.hermetic_mode)
437 .unwrap_or(false)
438 }
439
440 pub fn verify_hermetic_consistency(&self, current_lockfile_hash: &str) -> Result<()> {
442 if let Some(ref run) = self.current_run {
443 if run.hermetic_mode {
444 if let Some(ref saved_hash) = run.lockfile_hash {
445 if saved_hash != current_lockfile_hash {
446 return Err(Error::Validation(format!(
447 "Lockfile drift detected: checkpoint={}, current={}",
448 saved_hash, current_lockfile_hash
449 )));
450 }
451 }
452 }
453 }
454 Ok(())
455 }
456
457 fn save(&self) -> Result<()> {
459 let checkpoint_file = self.checkpoint_dir.join("checkpoint.json");
461
462 let data = CheckpointData {
463 run: self.current_run.clone(),
464 steps: self.steps.clone(),
465 state_files: self.state_files.clone(),
466 };
467
468 let json = serde_json::to_string_pretty(&data)
469 .map_err(|e| Error::Validation(format!("Failed to serialize checkpoint: {}", e)))?;
470
471 std::fs::write(&checkpoint_file, json).map_err(|e| {
472 Error::Io(std::io::Error::new(
473 e.kind(),
474 format!("Failed to write checkpoint: {}", e),
475 ))
476 })
477 }
478
479 pub fn load(checkpoint_dir: &Path) -> Result<Self> {
481 let checkpoint_file = checkpoint_dir.join("checkpoint.json");
482
483 if !checkpoint_file.exists() {
484 return Self::new(checkpoint_dir);
485 }
486
487 let json = std::fs::read_to_string(&checkpoint_file).map_err(|e| {
488 Error::Io(std::io::Error::new(
489 e.kind(),
490 format!("Failed to read checkpoint: {}", e),
491 ))
492 })?;
493
494 let data: CheckpointData = serde_json::from_str(&json)
495 .map_err(|e| Error::Validation(format!("Failed to parse checkpoint: {}", e)))?;
496
497 Ok(Self {
498 checkpoint_dir: checkpoint_dir.to_path_buf(),
499 current_run: data.run,
500 steps: data.steps,
501 state_files: data.state_files,
502 })
503 }
504}
505
506#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
508struct CheckpointData {
509 run: Option<InstallerRun>,
510 steps: Vec<StepCheckpoint>,
511 state_files: Vec<StateFile>,
512}
513
514impl serde::Serialize for InstallerRun {
516 fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
517 where
518 S: serde::Serializer,
519 {
520 use serde::ser::SerializeStruct;
521 let mut state = serializer.serialize_struct("InstallerRun", 8)?;
522 state.serialize_field("run_id", &self.run_id)?;
523 state.serialize_field("installer_name", &self.installer_name)?;
524 state.serialize_field("installer_version", &self.installer_version)?;
525 state.serialize_field("started_at", &self.started_at)?;
526 state.serialize_field("completed_at", &self.completed_at)?;
527 state.serialize_field("status", &self.status.as_str())?;
528 state.serialize_field("hermetic_mode", &self.hermetic_mode)?;
529 state.serialize_field("lockfile_hash", &self.lockfile_hash)?;
530 state.end()
531 }
532}
533
534impl<'de> serde::Deserialize<'de> for InstallerRun {
535 fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
536 where
537 D: serde::Deserializer<'de>,
538 {
539 #[derive(serde::Deserialize)]
540 struct RunHelper {
541 run_id: String,
542 installer_name: String,
543 installer_version: String,
544 started_at: u64,
545 completed_at: Option<u64>,
546 status: String,
547 hermetic_mode: bool,
548 lockfile_hash: Option<String>,
549 }
550
551 let helper = RunHelper::deserialize(deserializer)?;
552 let status = RunStatus::parse(&helper.status)
553 .ok_or_else(|| serde::de::Error::custom("Invalid status"))?;
554
555 Ok(InstallerRun {
556 run_id: helper.run_id,
557 installer_name: helper.installer_name,
558 installer_version: helper.installer_version,
559 started_at: helper.started_at,
560 completed_at: helper.completed_at,
561 status,
562 hermetic_mode: helper.hermetic_mode,
563 lockfile_hash: helper.lockfile_hash,
564 })
565 }
566}
567
568impl serde::Serialize for StepCheckpoint {
569 fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
570 where
571 S: serde::Serializer,
572 {
573 use serde::ser::SerializeStruct;
574 let mut state = serializer.serialize_struct("StepCheckpoint", 9)?;
575 state.serialize_field("run_id", &self.run_id)?;
576 state.serialize_field("step_id", &self.step_id)?;
577 state.serialize_field("status", &self.status.as_str())?;
578 state.serialize_field("started_at", &self.started_at)?;
579 state.serialize_field("completed_at", &self.completed_at)?;
580 state.serialize_field("duration_ms", &self.duration_ms)?;
581 state.serialize_field("state_snapshot", &self.state_snapshot)?;
582 state.serialize_field("output_log", &self.output_log)?;
583 state.serialize_field("error_message", &self.error_message)?;
584 state.end()
585 }
586}
587
588impl<'de> serde::Deserialize<'de> for StepCheckpoint {
589 fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
590 where
591 D: serde::Deserializer<'de>,
592 {
593 #[derive(serde::Deserialize)]
594 struct StepHelper {
595 run_id: String,
596 step_id: String,
597 status: String,
598 started_at: Option<u64>,
599 completed_at: Option<u64>,
600 duration_ms: Option<u64>,
601 state_snapshot: Option<String>,
602 output_log: Option<String>,
603 error_message: Option<String>,
604 }
605
606 let helper = StepHelper::deserialize(deserializer)?;
607 let status = StepStatus::parse(&helper.status)
608 .ok_or_else(|| serde::de::Error::custom("Invalid status"))?;
609
610 Ok(StepCheckpoint {
611 run_id: helper.run_id,
612 step_id: helper.step_id,
613 status,
614 started_at: helper.started_at,
615 completed_at: helper.completed_at,
616 duration_ms: helper.duration_ms,
617 state_snapshot: helper.state_snapshot,
618 output_log: helper.output_log,
619 error_message: helper.error_message,
620 })
621 }
622}
623
624impl serde::Serialize for StateFile {
625 fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
626 where
627 S: serde::Serializer,
628 {
629 use serde::ser::SerializeStruct;
630 let mut state = serializer.serialize_struct("StateFile", 6)?;
631 state.serialize_field("run_id", &self.run_id)?;
632 state.serialize_field("step_id", &self.step_id)?;
633 state.serialize_field("file_path", &self.file_path)?;
634 state.serialize_field("content_hash", &self.content_hash)?;
635 state.serialize_field("backed_up_at", &self.backed_up_at)?;
636 state.serialize_field("backup_path", &self.backup_path)?;
637 state.end()
638 }
639}
640
641impl<'de> serde::Deserialize<'de> for StateFile {
642 fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
643 where
644 D: serde::Deserializer<'de>,
645 {
646 #[derive(serde::Deserialize)]
647 struct FileHelper {
648 run_id: String,
649 step_id: String,
650 file_path: PathBuf,
651 content_hash: String,
652 backed_up_at: Option<u64>,
653 backup_path: Option<PathBuf>,
654 }
655
656 let helper = FileHelper::deserialize(deserializer)?;
657
658 Ok(StateFile {
659 run_id: helper.run_id,
660 step_id: helper.step_id,
661 file_path: helper.file_path,
662 content_hash: helper.content_hash,
663 backed_up_at: helper.backed_up_at,
664 backup_path: helper.backup_path,
665 })
666 }
667}
668
669fn generate_run_id() -> String {
671 use std::hash::{Hash, Hasher};
672 let mut hasher = std::collections::hash_map::DefaultHasher::new();
673 current_timestamp().hash(&mut hasher);
674 std::process::id().hash(&mut hasher);
675 format!("run-{:016x}", hasher.finish())
676}
677
678fn current_timestamp() -> u64 {
680 SystemTime::now()
681 .duration_since(UNIX_EPOCH)
682 .map(|d| d.as_secs())
683 .unwrap_or(0)
684}
685
686#[cfg(test)]
687mod tests {
688 use super::*;
689 use tempfile::TempDir;
690
691 #[test]
698 fn test_CHECKPOINT_106_create_store() {
699 let temp_dir = TempDir::new().unwrap();
700 let store = CheckpointStore::new(temp_dir.path()).unwrap();
701 assert!(store.current_run_id().is_none());
702 }
703
704 #[test]
705 fn test_CHECKPOINT_106_start_run() {
706 let temp_dir = TempDir::new().unwrap();
707 let mut store = CheckpointStore::new(temp_dir.path()).unwrap();
708
709 let run_id = store.start_run("my-installer", "1.0.0").unwrap();
710 assert!(run_id.starts_with("run-"));
711 assert!(store.current_run_id().is_some());
712 }
713
714 #[test]
715 fn test_CHECKPOINT_106_add_step() {
716 let temp_dir = TempDir::new().unwrap();
717 let mut store = CheckpointStore::new(temp_dir.path()).unwrap();
718
719 store.start_run("my-installer", "1.0.0").unwrap();
720 store.add_step("step-1").unwrap();
721
722 let step = store.get_step("step-1").unwrap();
723 assert_eq!(step.status, StepStatus::Pending);
724 }
725
726 #[test]
727 fn test_CHECKPOINT_106_step_lifecycle() {
728 let temp_dir = TempDir::new().unwrap();
729 let mut store = CheckpointStore::new(temp_dir.path()).unwrap();
730
731 store.start_run("my-installer", "1.0.0").unwrap();
732 store.add_step("step-1").unwrap();
733
734 store.start_step("step-1").unwrap();
736 assert_eq!(
737 store.get_step("step-1").unwrap().status,
738 StepStatus::Running
739 );
740
741 store
743 .complete_step("step-1", Some("output".to_string()))
744 .unwrap();
745 let step = store.get_step("step-1").unwrap();
746 assert_eq!(step.status, StepStatus::Completed);
747 assert_eq!(step.output_log, Some("output".to_string()));
748 }
749
750 #[test]
751 fn test_CHECKPOINT_106_step_failure() {
752 let temp_dir = TempDir::new().unwrap();
753 let mut store = CheckpointStore::new(temp_dir.path()).unwrap();
754
755 store.start_run("my-installer", "1.0.0").unwrap();
756 store.add_step("step-1").unwrap();
757 store.start_step("step-1").unwrap();
758 store.fail_step("step-1", "Something went wrong").unwrap();
759
760 let step = store.get_step("step-1").unwrap();
761 assert_eq!(step.status, StepStatus::Failed);
762 assert_eq!(step.error_message, Some("Something went wrong".to_string()));
763 }
764
765 #[test]
766 fn test_CHECKPOINT_106_last_successful_step() {
767 let temp_dir = TempDir::new().unwrap();
768 let mut store = CheckpointStore::new(temp_dir.path()).unwrap();
769
770 store.start_run("my-installer", "1.0.0").unwrap();
771
772 store.add_step("step-1").unwrap();
773 store.start_step("step-1").unwrap();
774 store.complete_step("step-1", None).unwrap();
775
776 store.add_step("step-2").unwrap();
777 store.start_step("step-2").unwrap();
778 store.complete_step("step-2", None).unwrap();
779
780 store.add_step("step-3").unwrap();
781 store.start_step("step-3").unwrap();
782 store.fail_step("step-3", "error").unwrap();
783
784 let last = store.last_successful_step().unwrap();
785 assert_eq!(last.step_id, "step-2");
786 }
787
788 #[test]
789 fn test_CHECKPOINT_106_hermetic_mode() {
790 let temp_dir = TempDir::new().unwrap();
791 let mut store = CheckpointStore::new(temp_dir.path()).unwrap();
792
793 store
794 .start_hermetic_run("my-installer", "1.0.0", "abc123")
795 .unwrap();
796 assert!(store.is_hermetic());
797
798 store.verify_hermetic_consistency("abc123").unwrap();
800
801 let result = store.verify_hermetic_consistency("different");
803 assert!(result.is_err());
804 }
805
806 #[test]
807 fn test_CHECKPOINT_106_track_file() {
808 let temp_dir = TempDir::new().unwrap();
809 let mut store = CheckpointStore::new(temp_dir.path()).unwrap();
810
811 store.start_run("my-installer", "1.0.0").unwrap();
812 store.add_step("step-1").unwrap();
813
814 store
815 .track_file("step-1", Path::new("/etc/config.txt"), "sha256:abc")
816 .unwrap();
817
818 let files = store.state_files_for_step("step-1");
819 assert_eq!(files.len(), 1);
820 assert_eq!(files[0].content_hash, "sha256:abc");
821 }
822
823 #[test]
824 fn test_CHECKPOINT_106_persistence() {
825 let temp_dir = TempDir::new().unwrap();
826
827 {
829 let mut store = CheckpointStore::new(temp_dir.path()).unwrap();
830 store.start_run("my-installer", "1.0.0").unwrap();
831 store.add_step("step-1").unwrap();
832 store.start_step("step-1").unwrap();
833 store
834 .complete_step("step-1", Some("done".to_string()))
835 .unwrap();
836 }
837
838 {
840 let store = CheckpointStore::load(temp_dir.path()).unwrap();
841 assert!(store.current_run_id().is_some());
842 let step = store.get_step("step-1").unwrap();
843 assert_eq!(step.status, StepStatus::Completed);
844 }
845 }
846
847 #[test]
848 fn test_CHECKPOINT_106_run_status_roundtrip() {
849 for status in [
850 RunStatus::Running,
851 RunStatus::Completed,
852 RunStatus::Failed,
853 RunStatus::Aborted,
854 ] {
855 let s = status.as_str();
856 assert_eq!(RunStatus::parse(s), Some(status));
857 }
858 }
859
860 #[test]
861 fn test_CHECKPOINT_106_step_status_roundtrip() {
862 for status in [
863 StepStatus::Pending,
864 StepStatus::Running,
865 StepStatus::Completed,
866 StepStatus::Failed,
867 StepStatus::Skipped,
868 ] {
869 let s = status.as_str();
870 assert_eq!(StepStatus::parse(s), Some(status));
871 }
872 }
873}
874
875#[cfg(test)]
876mod property_tests {
877 use super::*;
878 use proptest::prelude::*;
879 use tempfile::TempDir;
880
881 proptest! {
882 #[test]
884 fn prop_store_handles_any_name(name in ".*") {
885 let temp_dir = TempDir::new().unwrap();
886 let mut store = CheckpointStore::new(temp_dir.path()).unwrap();
887 let _ = store.start_run(&name, "1.0.0");
889 }
890
891 #[test]
893 fn prop_step_id_preserved(step_id in "[a-zA-Z][a-zA-Z0-9_-]{0,50}") {
894 let temp_dir = TempDir::new().unwrap();
895 let mut store = CheckpointStore::new(temp_dir.path()).unwrap();
896 store.start_run("test", "1.0.0").unwrap();
897 store.add_step(&step_id).unwrap();
898
899 let step = store.get_step(&step_id);
900 prop_assert!(step.is_some());
901 prop_assert_eq!(&step.unwrap().step_id, &step_id);
902 }
903 }
904}