1use std::collections::{BTreeMap, BTreeSet};
14use std::path::PathBuf;
15
16use serde::{Deserialize, Serialize};
17
18use crate::model::batch::BatchResult;
19use crate::model::eval::{BuildOutcome, CorpusEntry, PredictedConflict};
20use crate::model::names::{MacroName, ModuleName, ProjectName, RelativePath, vocabulary};
21use crate::model::pin::Pin;
22use crate::model::resolver_coverage::ResolverCoverage;
23use crate::model::summary::VerdictKind;
24use crate::model::verdict::{
25 ApplyConflictKind, Diagnostics, IncludeDirective, PinVerdict, Reason, SeriesEvaluation,
26 SeriesVerdict, SnapshotSide, TargetAxisSlot, TestCallSite, Verdict, non_self_tracked,
27};
28
29vocabulary!(
30 #[non_exhaustive]
32 #[derive(Hash)]
33 pub enum AggregateVerdict: "aggregate verdict" {
34 Compatible => "compatible",
36 RequiresAdaptation => "requires_adaptation",
38 Incompatible => "incompatible",
40 Inapplicable => "inapplicable",
42 Empty => "empty",
44 }
45);
46
47impl AggregateVerdict {
48 #[must_use]
51 pub fn flagged(self) -> bool {
52 matches!(self, Self::Incompatible | Self::RequiresAdaptation)
53 }
54}
55
56#[derive(Debug, Clone, Copy)]
62pub struct SeriesEvaluationView<'a> {
63 results: &'a SeriesVerdict,
64 diagnostics: &'a Diagnostics,
65}
66
67impl<'a> SeriesEvaluationView<'a> {
68 #[must_use]
70 pub fn new(results: &'a SeriesVerdict, diagnostics: &'a Diagnostics) -> Self {
71 Self {
72 results,
73 diagnostics,
74 }
75 }
76
77 #[must_use]
80 pub fn worst_verdict(self) -> AggregateVerdict {
81 if self.results.results.is_empty() {
82 return AggregateVerdict::Empty;
83 }
84 let s = &self.results.summary;
85 if s.incompatible > 0 {
86 AggregateVerdict::Incompatible
87 } else if s.requires_adaptation > 0 {
88 AggregateVerdict::RequiresAdaptation
89 } else if s.compatible > 0 {
90 AggregateVerdict::Compatible
91 } else if s.inapplicable > 0 {
92 AggregateVerdict::Inapplicable
93 } else {
94 AggregateVerdict::Empty
95 }
96 }
97
98 #[must_use]
100 pub fn has_blocking_reason(self) -> bool {
101 self.results
102 .results
103 .iter()
104 .any(|p| matches!(&p.verdict, Verdict::Incompatible { .. }))
105 }
106
107 pub fn pins_in(self, verdict: VerdictKind) -> impl Iterator<Item = &'a PinVerdict> {
109 self.results
110 .results
111 .iter()
112 .filter(move |p| VerdictKind::from(&p.verdict) == verdict)
113 }
114
115 pub fn reasons(self) -> impl Iterator<Item = &'a Reason> {
117 self.results.results.iter().flat_map(pin_reasons)
118 }
119
120 pub fn reasons_for(self, pin: &Pin) -> impl Iterator<Item = &'a Reason> {
122 self.results
123 .results
124 .iter()
125 .filter(move |p| &p.pin == pin)
126 .flat_map(pin_reasons)
127 }
128
129 #[must_use]
131 pub fn tracked_refs(self, self_projects: &BTreeSet<ProjectName>) -> u32 {
132 non_self_tracked(&self.results.results, self_projects)
133 }
134
135 #[must_use]
137 pub fn pin_by_project(self, project: &ProjectName) -> Option<&'a PinVerdict> {
138 self.results
139 .results
140 .iter()
141 .find(|p| &p.pin.project == project)
142 }
143
144 #[must_use]
149 pub fn missing_test_modules(self) -> &'a BTreeMap<RelativePath, BTreeMap<ModuleName, usize>> {
150 &self.diagnostics.missing_test_modules
151 }
152
153 fn pin_reason_pairs(self) -> impl Iterator<Item = (&'a PinVerdict, &'a Reason)> {
156 self.results
157 .results
158 .iter()
159 .flat_map(|pin| pin_reasons(pin).map(move |r| (pin, r)))
160 }
161
162 pub fn test_module_symbol_missing(
166 self,
167 ) -> impl Iterator<Item = TestModuleSymbolMissingFinding<'a>> {
168 self.pin_reason_pairs().filter_map(|(pin, r)| match r {
169 Reason::TestModuleSymbolMissing {
170 suite_path,
171 missing_module,
172 call_sites,
173 } => Some(TestModuleSymbolMissingFinding {
174 pin,
175 suite_path,
176 missing_module,
177 call_sites: call_sites.as_slice(),
178 }),
179 _ => None,
180 })
181 }
182
183 pub fn header_file_missing(self) -> impl Iterator<Item = HeaderFileMissingFinding<'a>> {
186 self.pin_reason_pairs().filter_map(|(pin, r)| match r {
187 Reason::HeaderFileMissing {
188 source_path,
189 include_directive,
190 attempted_paths,
191 } => Some(HeaderFileMissingFinding {
192 pin,
193 source_path,
194 include_directive,
195 attempted_paths: attempted_paths.as_slice(),
196 }),
197 _ => None,
198 })
199 }
200
201 pub fn behaviour_module_missing(
204 self,
205 ) -> impl Iterator<Item = BehaviourModuleMissingFinding<'a>> {
206 self.pin_reason_pairs().filter_map(|(pin, r)| match r {
207 Reason::BehaviourModuleMissing {
208 source_path,
209 behaviour,
210 } => Some(BehaviourModuleMissingFinding {
211 pin,
212 source_path,
213 behaviour,
214 }),
215 _ => None,
216 })
217 }
218
219 pub fn versioned_machine_snapshot_missing(
222 self,
223 ) -> impl Iterator<Item = VersionedMachineSnapshotMissingFinding<'a>> {
224 self.pin_reason_pairs().filter_map(|(pin, r)| match r {
225 Reason::VersionedMachineSnapshotMissing { module, side } => {
226 Some(VersionedMachineSnapshotMissingFinding {
227 pin,
228 module,
229 side: *side,
230 })
231 }
232 _ => None,
233 })
234 }
235
236 pub fn wire_constant_bindings_missing(
239 self,
240 ) -> impl Iterator<Item = WireConstantBindingsMissingFinding<'a>> {
241 self.pin_reason_pairs().filter_map(|(pin, r)| match r {
242 Reason::WireConstantBindingsMissing {
243 module,
244 macros,
245 side,
246 } => Some(WireConstantBindingsMissingFinding {
247 pin,
248 module,
249 macros: macros.as_slice(),
250 side: *side,
251 }),
252 _ => None,
253 })
254 }
255}
256
257impl From<BatchResult> for SeriesEvaluation {
263 fn from(row: BatchResult) -> Self {
264 Self {
265 verdict: row.verdict,
266 diagnostics: row.diagnostics,
267 patch_facts: row.patch_facts,
268 touched_paths: row.touched_paths,
269 pr_commits: row.pr_commits,
270 target: TargetAxisSlot::from_wire_pair(row.apply, row.target_findings),
271 }
272 }
273}
274
275impl BatchResult {
276 #[must_use]
281 pub fn evaluation(&self) -> SeriesEvaluationView<'_> {
282 SeriesEvaluationView::new(&self.verdict, &self.diagnostics)
283 }
284
285 #[must_use]
291 pub fn to_corpus_entry(
292 &self,
293 observed: BuildOutcome,
294 coverage: Option<&ResolverCoverage>,
295 ) -> Option<CorpusEntry> {
296 let fingerprint = self.verdict_fingerprint.clone()?;
297 let verdict = self.evaluation().worst_verdict();
298 if verdict == AggregateVerdict::Empty {
299 return None;
300 }
301 Some(CorpusEntry {
302 fingerprint,
303 verdict,
304 outcome: observed,
305 coverage: coverage.cloned(),
306 })
307 }
308
309 #[must_use]
317 pub fn predicted_conflicts(&self) -> Vec<PredictedConflict> {
318 let mut by_path: BTreeMap<PathBuf, ApplyConflictKind> = BTreeMap::new();
319 let mut record = |path: PathBuf, kind: ApplyConflictKind| {
320 by_path
321 .entry(path)
322 .and_modify(|seen| *seen = (*seen).max(kind))
323 .or_insert(kind);
324 };
325 for (path, kind) in self
326 .evaluation()
327 .reasons()
328 .filter_map(Reason::apply_conflict)
329 {
330 record(path.to_path_buf(), kind);
331 }
332 if let Some(forecast) = &self.apply {
333 for (path, kind) in forecast.conflicts() {
334 record(PathBuf::from(path.as_str()), kind);
335 }
336 }
337 by_path
338 .into_iter()
339 .map(|(path, kind)| PredictedConflict { path, kind })
340 .collect()
341 }
342}
343
344#[derive(Debug, Clone, Copy)]
346pub struct TestModuleSymbolMissingFinding<'a> {
347 pub pin: &'a PinVerdict,
349 pub suite_path: &'a RelativePath,
351 pub missing_module: &'a ModuleName,
353 pub call_sites: &'a [TestCallSite],
355}
356
357#[derive(Debug, Clone, Copy)]
359pub struct HeaderFileMissingFinding<'a> {
360 pub pin: &'a PinVerdict,
362 pub source_path: &'a RelativePath,
364 pub include_directive: &'a IncludeDirective,
366 pub attempted_paths: &'a [RelativePath],
368}
369
370#[derive(Debug, Clone, Copy)]
372pub struct BehaviourModuleMissingFinding<'a> {
373 pub pin: &'a PinVerdict,
375 pub source_path: &'a RelativePath,
377 pub behaviour: &'a ModuleName,
379}
380
381#[derive(Debug, Clone, Copy)]
384pub struct VersionedMachineSnapshotMissingFinding<'a> {
385 pub pin: &'a PinVerdict,
387 pub module: &'a ModuleName,
389 pub side: SnapshotSide,
391}
392
393#[derive(Debug, Clone, Copy)]
396pub struct WireConstantBindingsMissingFinding<'a> {
397 pub pin: &'a PinVerdict,
399 pub module: &'a ModuleName,
401 pub macros: &'a [MacroName],
403 pub side: SnapshotSide,
405}
406
407fn pin_reasons(pin: &PinVerdict) -> impl Iterator<Item = &Reason> {
408 pin.verdict.reasons().iter()
409}