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 nonempty, and is fresh against watched inputs.
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/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}