Skip to main content

ic_testkit/artifacts/
icp.rs

1use std::{fs, io, path::Path, time::SystemTime};
2
3///
4/// WatchedInputSnapshot
5///
6
7#[derive(Clone, Copy, Debug)]
8pub struct WatchedInputSnapshot {
9    newest_input_mtime: SystemTime,
10}
11
12impl WatchedInputSnapshot {
13    /// Capture the newest modification time across all watched inputs once.
14    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    /// Check whether one artifact is newer than the captured watched inputs.
21    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/// Check whether an ICP artifact exists, is fresh, and matches the expected build env.
28#[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/// Check one ICP artifact against one already-captured watched-input snapshot.
43#[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
59// Walk watched files and directories and return the newest modification time.
60fn 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
74// Recursively compute the newest modification time under one watched path.
75fn 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/root")).expect("create temp workspace");
107        path
108    }
109
110    #[test]
111    fn icp_artifact_ready_requires_fresh_nonempty_artifact() {
112        let workspace_root = temp_workspace();
113        let artifact_relative_path = ".icp/local/canisters/root/root.wasm.gz";
114        let artifact_path = workspace_root.join(artifact_relative_path);
115        fs::write(workspace_root.join("Cargo.toml"), "workspace").expect("write watched input");
116        sleep(Duration::from_millis(20));
117        fs::write(&artifact_path, b"wasm").expect("write artifact");
118
119        assert!(icp_artifact_ready_for_build(
120            &workspace_root,
121            artifact_relative_path,
122            &["Cargo.toml"],
123        ));
124
125        sleep(Duration::from_millis(20));
126        fs::write(workspace_root.join("Cargo.toml"), "changed").expect("update watched input");
127        assert!(!icp_artifact_ready_for_build(
128            &workspace_root,
129            artifact_relative_path,
130            &["Cargo.toml"],
131        ));
132
133        let _ = fs::remove_dir_all(workspace_root);
134    }
135}