Skip to main content

artifactize_core/project/
status.rs

1mod states;
2mod view;
3pub use states::{ArtifactCondition, EvalCondition, VerifyAction};
4#[cfg(test)]
5mod tests;
6
7use std::{
8    collections::{BTreeMap, BTreeSet},
9    path::Path,
10};
11
12use serde::Serialize;
13use tokio_util::sync::CancellationToken;
14
15use crate::{
16    cache,
17    config::{Profile, read_workspace_config},
18    graph::{Evidence, Graph, Readiness},
19    project::{VerifyOptions, selection::select_profiles},
20    remote::Session,
21    store::{self, Claim, LastRequest},
22};
23
24use super::selection::Selection;
25
26#[derive(Debug, Serialize)]
27#[serde(rename_all = "camelCase")]
28pub struct StatusView {
29    pub selection: Selection,
30    pub recursive: bool,
31    pub force: bool,
32    pub selected_eval_ids: Vec<crate::config::EvalId>,
33    pub included_eval_ids: Vec<crate::config::EvalId>,
34    pub satisfied: bool,
35    pub obligations: Vec<crate::config::ArtifactName>,
36    pub artifacts: Vec<ArtifactState>,
37    pub evals: Vec<EvalState>,
38    pub counts: Counts,
39    #[serde(skip_serializing_if = "Vec::is_empty")]
40    pub unreadable: Vec<store::Unreadable>,
41}
42
43#[derive(Debug, Serialize)]
44#[serde(rename_all = "camelCase")]
45pub struct ArtifactState {
46    pub id: crate::config::ArtifactName,
47    pub kind: crate::config::ArtifactKind,
48    #[serde(with = "crate::platform::path_serde")]
49    pub path: std::path::PathBuf,
50    pub tags: Vec<String>,
51    pub state: ArtifactCondition,
52    pub reason: String,
53    pub eval_ids: Vec<crate::config::EvalId>,
54    pub passed: usize,
55    pub total: usize,
56    pub satisfied: bool,
57    pub obligations: Vec<crate::config::ArtifactName>,
58}
59
60#[derive(Debug, Serialize)]
61#[serde(rename_all = "camelCase")]
62pub struct EvalState {
63    pub id: crate::config::EvalId,
64    pub target: crate::config::ArtifactName,
65    pub title: String,
66    pub profile: Profile,
67    pub selected: bool,
68    pub included: bool,
69    pub force: bool,
70    pub state: EvalCondition,
71    pub action: VerifyAction,
72    pub reason: String,
73    pub blocked_by: Vec<crate::store::Blocker>,
74    pub obligations: Vec<crate::config::ArtifactName>,
75    /// The Eval definition hash: the evaluation criteria the key covers.
76    pub eval_def_hash: crate::types::DefinitionHash,
77    /// The target's current fingerprint; null without one.
78    pub fingerprint: Option<crate::types::Fingerprint>,
79    /// Each Artifact the eval depends on, with its current fingerprint or null without one.
80    pub fingerprints: BTreeMap<crate::config::ArtifactName, Option<crate::types::Fingerprint>>,
81    /// The reuse key composed of the hash and the fingerprints; null when one is missing.
82    pub key: Option<crate::types::ReuseKey>,
83    #[serde(skip_serializing_if = "Option::is_none")]
84    pub last: Option<LastRequest>,
85    /// Why the key no longer matches the newest cached result for this Eval definition: the
86    /// target's changed files and the dependency Artifacts whose fingerprints changed.
87    #[serde(skip_serializing_if = "Option::is_none")]
88    pub changes: Option<cache::Changes>,
89}
90
91#[derive(Debug, Default, Serialize)]
92pub struct Counts {
93    pub artifacts: BTreeMap<ArtifactCondition, usize>,
94    pub evals: BTreeMap<EvalCondition, usize>,
95    pub execute: usize,
96    pub reuse: usize,
97    pub wait: usize,
98    pub blocked: usize,
99}
100
101/// All effects needed by the status projection have finished before this is assembled.
102struct Snapshot<'a> {
103    required: BTreeSet<&'a crate::config::ArtifactName>,
104    selected_eval_ids: Vec<crate::config::EvalId>,
105    included_eval_ids: Vec<crate::config::EvalId>,
106    fingerprints: BTreeMap<&'a crate::config::ArtifactName, cache::PreparedFingerprint>,
107    all_keys: BTreeMap<&'a crate::config::EvalId, cache::Key>,
108    cached: BTreeMap<crate::config::EvalId, Claim>,
109    previous: BTreeMap<(crate::config::EvalId, crate::types::DefinitionHash), store::Execution>,
110    latest: BTreeMap<crate::config::EvalId, LastRequest>,
111    waiting: Vec<store::RequestView>,
112    unreadable: Vec<store::Unreadable>,
113}
114
115/// Prepare current fingerprints without executing evals or changing saved evidence.
116pub async fn status(
117    repo: &Path,
118    state_dir: Option<&Path>,
119    selection: &Selection,
120    options: &VerifyOptions,
121    cancellation: CancellationToken,
122) -> Result<StatusView, String> {
123    scan(repo, state_dir, selection, options, cancellation)
124        .await
125        .map(|(view, _)| view)
126}
127
128/// [`status`], with the completed results it found to reuse, by eval.
129pub(crate) async fn scan(
130    repo: &Path,
131    state_dir: Option<&Path>,
132    selection: &Selection,
133    options: &VerifyOptions,
134    cancellation: CancellationToken,
135) -> Result<
136    (
137        StatusView,
138        BTreeMap<crate::config::EvalId, store::Execution>,
139    ),
140    String,
141> {
142    let parallelism = super::fingerprint_parallelism(options)?;
143    let config = read_workspace_config(repo).map_err(|error| error.to_string())?;
144    let config = select_profiles(
145        config,
146        selection,
147        options.profile.as_ref(),
148        options.recursive,
149    )?;
150    let graph = Graph::new(&config).map_err(|error| error.to_string())?;
151    let selected = selection.resolve(&config)?;
152    let required: BTreeSet<_> = graph
153        .dependency_closure(&selected.roots)
154        .map_err(|error| error.to_string())?
155        .into_iter()
156        .collect();
157    let selected_eval_ids: Vec<_> = selected.evals.iter().map(|eval| eval.id.clone()).collect();
158    let included_eval_ids: Vec<_> = selection
159        .included_evals(&config, options.recursive)?
160        .iter()
161        .map(|eval| eval.id.clone())
162        .collect();
163    let state = store::state_dir(state_dir)?;
164    let latest = store::read_latest_requests(&state, &config.root).await?;
165    let selected_ids: BTreeSet<_> = selected_eval_ids.iter().collect();
166    let fingerprints = cache::prepare(
167        &config,
168        cache::fingerprint_targets(&config, &required),
169        &state,
170        &parallelism,
171        cancellation.clone(),
172    )
173    .await?;
174    let all_keys = cache::eval_keys(&config, &fingerprints);
175    let keys: BTreeMap<_, _> = all_keys
176        .iter()
177        .filter(|(id, _)| !(options.force && selected_ids.contains(*id)))
178        .map(|(id, key)| (*id, key))
179        .collect();
180    let mut cached = store::read_profiled_executions(
181        &state,
182        config
183            .evals
184            .iter()
185            .filter_map(|eval| {
186                keys.get(&eval.id).map(|key| {
187                    (
188                        eval.id.clone(),
189                        key.value.clone(),
190                        eval.declaration.profile().into(),
191                    )
192                })
193            })
194            .collect(),
195    )
196    .await?;
197    // Read-only: a store record that completed after the local latest counts as reuse without
198    // being mirrored, as verify would take it before claiming. --force reads nothing from the
199    // store, so its prediction has none.
200    if !options.force
201        && let Some(remote) = Session::open(Some(&state), Some(&config.root))?
202    {
203        let all: Vec<_> = keys
204            .values()
205            .map(|key| key.value.clone())
206            .collect::<BTreeSet<_>>()
207            .into_iter()
208            .collect();
209        for execution in remote.lookup(&all).await? {
210            let key = execution.key.clone().expect("remote key");
211            for (id, _) in keys.iter().filter(|(_, candidate)| candidate.value == key) {
212                let newer = match cached.get(*id) {
213                    Some(Claim::Reuse(local)) => execution.completed_after(local),
214                    _ => true,
215                };
216                if newer {
217                    cached.insert((*id).clone(), Claim::Reuse(Box::new(execution.clone())));
218                }
219            }
220        }
221    }
222    let claim = |id: &crate::types::EvalId| cached.get(id);
223    // Explain changed keys against the newest cached result for the same Eval definition.
224    let stale: Vec<_> = keys
225        .iter()
226        .filter(|(id, _)| !matches!(claim(id), Some(Claim::Reuse(_))))
227        .map(|(id, key)| ((*id).clone(), key.eval_def_hash.clone()))
228        .collect();
229    let previous = store::read_latest_cached(&state, &stale).await?;
230    let waiting = store::read_waiting(&state, Some(&config.root)).await?;
231    let unreadable = store::read_unreadable(&state, Some(&config.root)).await?;
232    if cancellation.is_cancelled() {
233        return Err("Project preparation was cancelled.".into());
234    }
235    let reused = cached
236        .iter()
237        .filter_map(|(id, claim)| match claim {
238            Claim::Reuse(execution) => Some((id.clone(), (**execution).clone())),
239            _ => None,
240        })
241        .collect();
242    view::assemble(
243        &config,
244        &graph,
245        selection,
246        options,
247        Snapshot {
248            required,
249            selected_eval_ids,
250            included_eval_ids,
251            fingerprints,
252            all_keys,
253            cached,
254            previous,
255            latest,
256            waiting,
257            unreadable,
258        },
259    )
260    .map(|view| (view, reused))
261}
262
263impl Counts {
264    fn action(&mut self, action: VerifyAction) {
265        match action {
266            VerifyAction::Execute => self.execute += 1,
267            VerifyAction::Reuse => self.reuse += 1,
268            VerifyAction::Wait => self.wait += 1,
269            VerifyAction::Blocked => self.blocked += 1,
270        }
271    }
272}