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