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 pub eval_def_hash: crate::types::DefinitionHash,
77 pub fingerprint: Option<crate::types::Fingerprint>,
79 pub fingerprints: BTreeMap<crate::config::ArtifactName, Option<crate::types::Fingerprint>>,
81 pub key: Option<crate::types::ReuseKey>,
83 #[serde(skip_serializing_if = "Option::is_none")]
84 pub last: Option<LastRequest>,
85 #[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
101struct 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
115pub 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
128pub(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 ¶llelism,
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 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 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}