Skip to main content

ic_testkit/artifacts/
wasm_batch.rs

1use std::time::{Duration, Instant};
2
3use super::wasm_cache::{
4    WasmBuildError, WasmBuildOutcome, WasmBuildProgressConfig, WasmBuildProgressEvent,
5    WasmBuildSpec, build_wasm_canisters_cached, build_wasm_canisters_cached_with_progress,
6};
7
8/// Successful outcomes from an independent sequence of exact Wasm builds.
9#[derive(Clone, Debug, Eq, PartialEq)]
10pub struct WasmBuildBatchOutcome {
11    outcomes: Vec<WasmBuildOutcome>,
12    total: Duration,
13}
14
15/// Structured progress for an independent sequence of exact Wasm builds.
16#[non_exhaustive]
17#[derive(Clone, Debug, Eq, PartialEq)]
18pub enum WasmBuildBatchProgressEvent {
19    /// One independently resolved build specification is about to start.
20    BuildStarted {
21        /// Zero-based position in the supplied specification slice.
22        index: usize,
23        /// Total number of supplied specifications.
24        total: usize,
25    },
26    /// Progress forwarded from one independent build.
27    BuildProgress {
28        /// Zero-based position in the supplied specification slice.
29        index: usize,
30        /// Event emitted by that build.
31        event: WasmBuildProgressEvent,
32    },
33    /// One independent build completed successfully.
34    BuildFinished {
35        /// Zero-based position in the supplied specification slice.
36        index: usize,
37    },
38}
39
40/// Failure from an independent Wasm build batch.
41#[derive(Debug)]
42pub struct WasmBuildBatchError {
43    failed_index: usize,
44    completed: Vec<WasmBuildOutcome>,
45    total: Duration,
46    source: WasmBuildError,
47}
48
49impl WasmBuildBatchOutcome {
50    /// Successful outcomes in specification order.
51    #[must_use]
52    pub fn outcomes(&self) -> &[WasmBuildOutcome] {
53        &self.outcomes
54    }
55
56    /// Consume the report and return its ordered outcomes.
57    #[must_use]
58    pub fn into_outcomes(self) -> Vec<WasmBuildOutcome> {
59        self.outcomes
60    }
61
62    /// Complete wall-clock time for the sequential batch.
63    #[must_use]
64    pub const fn total(&self) -> Duration {
65        self.total
66    }
67}
68
69impl WasmBuildBatchError {
70    /// Zero-based index of the failed independent specification.
71    #[must_use]
72    pub const fn failed_index(&self) -> usize {
73        self.failed_index
74    }
75
76    /// Successful outcomes completed before the failure.
77    #[must_use]
78    pub fn completed(&self) -> &[WasmBuildOutcome] {
79        &self.completed
80    }
81
82    /// Consume the failure and return the successful prefix and root cause.
83    #[must_use]
84    pub fn into_parts(self) -> (Vec<WasmBuildOutcome>, WasmBuildError) {
85        (self.completed, self.source)
86    }
87
88    /// Wall-clock time through the failure.
89    #[must_use]
90    pub const fn total(&self) -> Duration {
91        self.total
92    }
93}
94
95impl std::fmt::Display for WasmBuildBatchOutcome {
96    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
97        let reused = self
98            .outcomes
99            .iter()
100            .filter(|outcome| outcome.is_reused())
101            .count();
102        write!(
103            formatter,
104            "builds={} built={} reused={} total={:?}",
105            self.outcomes.len(),
106            self.outcomes.len().saturating_sub(reused),
107            reused,
108            self.total,
109        )
110    }
111}
112
113/// Build or reuse multiple Wasm specifications as independent Cargo invocations.
114///
115/// Specifications run sequentially and fail fast. Each entry retains its own
116/// package set, profile arguments, feature resolution, fingerprint, locks, and
117/// cache policy. The implementation deliberately never combines packages into
118/// one Cargo command, because doing so can unify shared dependency features.
119/// Completed outcomes remain valid when a later entry fails.
120pub fn build_wasm_canisters_cached_batch(
121    specs: &[WasmBuildSpec],
122) -> Result<WasmBuildBatchOutcome, WasmBuildBatchError> {
123    build_wasm_batch(specs, |spec, _index| build_wasm_canisters_cached(spec))
124}
125
126/// Build an independent Wasm batch while forwarding structured progress.
127///
128/// The same observation configuration is applied to every entry. Batch events
129/// identify the originating specification without altering the standalone
130/// build semantics.
131pub fn build_wasm_canisters_cached_batch_with_progress<F>(
132    specs: &[WasmBuildSpec],
133    config: WasmBuildProgressConfig,
134    mut observer: F,
135) -> Result<WasmBuildBatchOutcome, WasmBuildBatchError>
136where
137    F: FnMut(WasmBuildBatchProgressEvent),
138{
139    let count = specs.len();
140    build_wasm_batch(specs, |spec, index| {
141        observer(WasmBuildBatchProgressEvent::BuildStarted {
142            index,
143            total: count,
144        });
145        let outcome = build_wasm_canisters_cached_with_progress(spec, config, |event| {
146            observer(WasmBuildBatchProgressEvent::BuildProgress { index, event });
147        })?;
148        observer(WasmBuildBatchProgressEvent::BuildFinished { index });
149        Ok(outcome)
150    })
151}
152
153fn build_wasm_batch<F>(
154    specs: &[WasmBuildSpec],
155    mut build: F,
156) -> Result<WasmBuildBatchOutcome, WasmBuildBatchError>
157where
158    F: FnMut(&WasmBuildSpec, usize) -> Result<WasmBuildOutcome, WasmBuildError>,
159{
160    let started = Instant::now();
161    let mut outcomes = Vec::with_capacity(specs.len());
162    for (index, spec) in specs.iter().enumerate() {
163        match build(spec, index) {
164            Ok(outcome) => outcomes.push(outcome),
165            Err(source) => {
166                return Err(WasmBuildBatchError {
167                    failed_index: index,
168                    completed: outcomes,
169                    total: started.elapsed(),
170                    source,
171                });
172            }
173        }
174    }
175    Ok(WasmBuildBatchOutcome {
176        outcomes,
177        total: started.elapsed(),
178    })
179}
180
181impl std::fmt::Display for WasmBuildBatchError {
182    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
183        write!(
184            formatter,
185            "independent Wasm build {} failed after {} successful build(s): {}",
186            self.failed_index,
187            self.completed.len(),
188            self.source,
189        )
190    }
191}
192
193impl std::error::Error for WasmBuildBatchError {
194    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
195        Some(&self.source)
196    }
197}
198
199#[cfg(test)]
200mod tests;