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    let parallelism = super::fingerprint_parallelism(options)?;
124    let config = read_workspace_config(repo).map_err(|error| error.to_string())?;
125    let config = select_profiles(
126        config,
127        selection,
128        options.profile.as_ref(),
129        options.recursive,
130    )?;
131    let graph = Graph::new(&config).map_err(|error| error.to_string())?;
132    let selected = selection.resolve(&config)?;
133    let required: BTreeSet<_> = graph
134        .dependency_closure(&selected.roots)
135        .map_err(|error| error.to_string())?
136        .into_iter()
137        .collect();
138    let selected_eval_ids: Vec<_> = selected.evals.iter().map(|eval| eval.id.clone()).collect();
139    let included_eval_ids: Vec<_> = selection
140        .included_evals(&config, options.recursive)?
141        .iter()
142        .map(|eval| eval.id.clone())
143        .collect();
144    let state = store::state_dir(state_dir)?;
145    let latest = store::read_latest_requests(&state, &config.root).await?;
146    let selected_ids: BTreeSet<_> = selected_eval_ids.iter().collect();
147    let fingerprints = cache::prepare(
148        &config,
149        cache::fingerprint_targets(&config, &required),
150        &state,
151        &parallelism,
152        cancellation.clone(),
153    )
154    .await?;
155    let all_keys = cache::eval_keys(&config, &fingerprints);
156    let keys: BTreeMap<_, _> = all_keys
157        .iter()
158        .filter(|(id, _)| !(options.force && selected_ids.contains(*id)))
159        .map(|(id, key)| (*id, key))
160        .collect();
161    let mut cached = store::read_profiled_executions(
162        &state,
163        config
164            .evals
165            .iter()
166            .filter_map(|eval| {
167                keys.get(&eval.id).map(|key| {
168                    (
169                        eval.id.clone(),
170                        key.value.clone(),
171                        eval.declaration.profile().into(),
172                    )
173                })
174            })
175            .collect(),
176    )
177    .await?;
178    // Read-only: a store record that completed after the local latest counts as reuse without
179    // being mirrored, as verify would take it before claiming. --force reads nothing from the
180    // store, so its prediction has none.
181    if !options.force
182        && let Some(remote) = Session::open(Some(&state), Some(&config.root))?
183    {
184        let all: Vec<_> = keys
185            .values()
186            .map(|key| key.value.clone())
187            .collect::<BTreeSet<_>>()
188            .into_iter()
189            .collect();
190        for execution in remote.lookup(&all).await? {
191            let key = execution.key.clone().expect("remote key");
192            for (id, _) in keys.iter().filter(|(_, candidate)| candidate.value == key) {
193                let newer = match cached.get(*id) {
194                    Some(Claim::Reuse(local)) => execution.completed_after(local),
195                    _ => true,
196                };
197                if newer {
198                    cached.insert((*id).clone(), Claim::Reuse(Box::new(execution.clone())));
199                }
200            }
201        }
202    }
203    let claim = |id: &crate::types::EvalId| cached.get(id);
204    // Explain changed keys against the newest cached result for the same Eval definition.
205    let stale: Vec<_> = keys
206        .iter()
207        .filter(|(id, _)| !matches!(claim(id), Some(Claim::Reuse(_))))
208        .map(|(id, key)| ((*id).clone(), key.eval_def_hash.clone()))
209        .collect();
210    let previous = store::read_latest_cached(&state, &stale).await?;
211    let waiting = store::read_waiting(&state, Some(&config.root)).await?;
212    let unreadable = store::read_unreadable(&state, Some(&config.root)).await?;
213    if cancellation.is_cancelled() {
214        return Err("Project preparation was cancelled.".into());
215    }
216    view::assemble(
217        &config,
218        &graph,
219        selection,
220        options,
221        Snapshot {
222            required,
223            selected_eval_ids,
224            included_eval_ids,
225            fingerprints,
226            all_keys,
227            cached,
228            previous,
229            latest,
230            waiting,
231            unreadable,
232        },
233    )
234}
235
236impl Counts {
237    fn action(&mut self, action: VerifyAction) {
238        match action {
239            VerifyAction::Execute => self.execute += 1,
240            VerifyAction::Reuse => self.reuse += 1,
241            VerifyAction::Wait => self.wait += 1,
242            VerifyAction::Blocked => self.blocked += 1,
243        }
244    }
245}