Skip to main content

ic_testkit/artifacts/
icp.rs

1use std::{fs, io, path::Path, time::SystemTime};
2
3/// Newest modification time captured across a set of watched input trees.
4#[derive(Clone, Copy, Debug)]
5pub struct WatchedInputSnapshot {
6    newest_input_mtime: SystemTime,
7}
8
9impl WatchedInputSnapshot {
10    /// Recursively capture the newest modification time across all watched inputs.
11    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    /// Check whether one artifact is at least as new as the captured inputs.
18    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/// Check whether an ICP artifact exists, is nonempty, and is fresh against watched inputs.
25#[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/// Check one ICP artifact against one already-captured watched-input snapshot.
40#[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
56// Walk watched files and directories and return the newest modification time.
57fn 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
71// Recursively compute the newest modification time under one watched path.
72fn 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}