ic_testkit/artifacts/
icp.rs1use std::{fs, io, path::Path, time::SystemTime};
2
3#[derive(Clone, Copy, Debug)]
8pub struct WatchedInputSnapshot {
9 newest_input_mtime: SystemTime,
10}
11
12impl WatchedInputSnapshot {
13 pub fn capture(workspace_root: &Path, watched_relative_paths: &[&str]) -> io::Result<Self> {
15 Ok(Self {
16 newest_input_mtime: newest_watched_input_mtime(workspace_root, watched_relative_paths)?,
17 })
18 }
19
20 pub fn artifact_is_fresh(self, artifact_path: &Path) -> io::Result<bool> {
22 let artifact_mtime = fs::metadata(artifact_path)?.modified()?;
23 Ok(self.newest_input_mtime <= artifact_mtime)
24 }
25}
26
27#[must_use]
29pub fn icp_artifact_ready_for_build(
30 workspace_root: &Path,
31 artifact_relative_path: &str,
32 watched_relative_paths: &[&str],
33) -> bool {
34 let Ok(watched_inputs) = WatchedInputSnapshot::capture(workspace_root, watched_relative_paths)
35 else {
36 return false;
37 };
38
39 icp_artifact_ready_with_snapshot(workspace_root, artifact_relative_path, watched_inputs)
40}
41
42#[must_use]
44pub fn icp_artifact_ready_with_snapshot(
45 workspace_root: &Path,
46 artifact_relative_path: &str,
47 watched_inputs: WatchedInputSnapshot,
48) -> bool {
49 let artifact_path = workspace_root.join(artifact_relative_path);
50
51 match fs::metadata(&artifact_path) {
52 Ok(meta) if meta.is_file() && meta.len() > 0 => watched_inputs
53 .artifact_is_fresh(&artifact_path)
54 .unwrap_or(false),
55 _ => false,
56 }
57}
58
59fn newest_watched_input_mtime(
61 workspace_root: &Path,
62 watched_relative_paths: &[&str],
63) -> io::Result<SystemTime> {
64 let mut newest = SystemTime::UNIX_EPOCH;
65
66 for relative in watched_relative_paths {
67 let path = workspace_root.join(relative);
68 newest = newest.max(newest_path_mtime(&path)?);
69 }
70
71 Ok(newest)
72}
73
74fn newest_path_mtime(path: &Path) -> io::Result<SystemTime> {
76 let metadata = fs::metadata(path)?;
77 let mut newest = metadata.modified()?;
78
79 if metadata.is_dir() {
80 for entry in fs::read_dir(path)? {
81 let entry = entry?;
82 newest = newest.max(newest_path_mtime(&entry.path())?);
83 }
84 }
85
86 Ok(newest)
87}
88
89#[cfg(test)]
90mod tests {
91 use super::icp_artifact_ready_for_build;
92 use std::{
93 fs,
94 path::PathBuf,
95 thread::sleep,
96 time::Duration,
97 time::{SystemTime, UNIX_EPOCH},
98 };
99
100 fn temp_workspace() -> PathBuf {
101 let unique = SystemTime::now()
102 .duration_since(UNIX_EPOCH)
103 .expect("system time before epoch")
104 .as_nanos();
105 let path = std::env::temp_dir().join(format!("ic-testkit-icp-artifact-test-{unique}"));
106 fs::create_dir_all(path.join(".icp/local/canisters/counter"))
107 .expect("create temp workspace");
108 path
109 }
110
111 #[test]
112 fn icp_artifact_ready_requires_fresh_nonempty_artifact() {
113 let workspace_root = temp_workspace();
114 let artifact_relative_path = ".icp/local/canisters/counter/counter.wasm.gz";
115 let artifact_path = workspace_root.join(artifact_relative_path);
116 fs::write(workspace_root.join("Cargo.toml"), "workspace").expect("write watched input");
117 sleep(Duration::from_millis(20));
118 fs::write(&artifact_path, b"wasm").expect("write artifact");
119
120 assert!(icp_artifact_ready_for_build(
121 &workspace_root,
122 artifact_relative_path,
123 &["Cargo.toml"],
124 ));
125
126 sleep(Duration::from_millis(20));
127 fs::write(workspace_root.join("Cargo.toml"), "changed").expect("update watched input");
128 assert!(!icp_artifact_ready_for_build(
129 &workspace_root,
130 artifact_relative_path,
131 &["Cargo.toml"],
132 ));
133
134 let _ = fs::remove_dir_all(workspace_root);
135 }
136}