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};
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, TestCallSite, Verdict, non_self_tracked,
27};
28
29#[non_exhaustive]
31#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
32#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
33#[serde(rename_all = "snake_case")]
34pub enum AggregateVerdict {
35 Compatible,
37 RequiresAdaptation,
39 Incompatible,
41 Inapplicable,
43 Empty,
45}
46
47impl AggregateVerdict {
48 #[must_use]
51 pub fn flagged(self) -> bool {
52 matches!(self, Self::Incompatible | Self::RequiresAdaptation)
53 }
54
55 #[must_use]
57 pub fn as_str(self) -> &'static str {
58 match self {
59 Self::Compatible => "compatible",
60 Self::RequiresAdaptation => "requires_adaptation",
61 Self::Incompatible => "incompatible",
62 Self::Inapplicable => "inapplicable",
63 Self::Empty => "empty",
64 }
65 }
66}
67
68#[derive(Debug, Clone, Copy)]
74pub struct SeriesEvaluationView<'a> {
75 results: &'a SeriesVerdict,
76 diagnostics: &'a Diagnostics,
77}
78
79impl<'a> SeriesEvaluationView<'a> {
80 #[must_use]
82 pub fn new(results: &'a SeriesVerdict, diagnostics: &'a Diagnostics) -> Self {
83 Self {
84 results,
85 diagnostics,
86 }
87 }
88
89 #[must_use]
92 pub fn worst_verdict(self) -> AggregateVerdict {
93 if self.results.results.is_empty() {
94 return AggregateVerdict::Empty;
95 }
96 let s = &self.results.summary;
97 if s.incompatible > 0 {
98 AggregateVerdict::Incompatible
99 } else if s.requires_adaptation > 0 {
100 AggregateVerdict::RequiresAdaptation
101 } else if s.compatible > 0 {
102 AggregateVerdict::Compatible
103 } else if s.inapplicable > 0 {
104 AggregateVerdict::Inapplicable
105 } else {
106 AggregateVerdict::Empty
107 }
108 }
109
110 #[must_use]
112 pub fn has_blocking_reason(self) -> bool {
113 self.results
114 .results
115 .iter()
116 .any(|p| matches!(&p.verdict, Verdict::Incompatible { .. }))
117 }
118
119 pub fn pins_in(self, verdict: VerdictKind) -> impl Iterator<Item = &'a PinVerdict> {
121 self.results
122 .results
123 .iter()
124 .filter(move |p| VerdictKind::from(&p.verdict) == verdict)
125 }
126
127 pub fn reasons(self) -> impl Iterator<Item = &'a Reason> {
129 self.results.results.iter().flat_map(pin_reasons)
130 }
131
132 pub fn reasons_for(self, pin: &Pin) -> impl Iterator<Item = &'a Reason> {
134 self.results
135 .results
136 .iter()
137 .filter(move |p| &p.pin == pin)
138 .flat_map(pin_reasons)
139 }
140
141 #[must_use]
143 pub fn tracked_refs(self, self_projects: &BTreeSet<ProjectName>) -> u32 {
144 non_self_tracked(&self.results.results, self_projects)
145 }
146
147 #[must_use]
149 pub fn pin_by_project(self, project: &ProjectName) -> Option<&'a PinVerdict> {
150 self.results
151 .results
152 .iter()
153 .find(|p| &p.pin.project == project)
154 }
155
156 #[must_use]
161 pub fn missing_test_modules(self) -> &'a BTreeMap<RelativePath, BTreeMap<ModuleName, usize>> {
162 &self.diagnostics.missing_test_modules
163 }
164
165 fn pin_reason_pairs(self) -> impl Iterator<Item = (&'a PinVerdict, &'a Reason)> {
168 self.results
169 .results
170 .iter()
171 .flat_map(|pin| pin_reasons(pin).map(move |r| (pin, r)))
172 }
173
174 pub fn test_module_symbol_missing(
178 self,
179 ) -> impl Iterator<Item = TestModuleSymbolMissingFinding<'a>> {
180 self.pin_reason_pairs().filter_map(|(pin, r)| match r {
181 Reason::TestModuleSymbolMissing {
182 suite_path,
183 missing_module,
184 call_sites,
185 } => Some(TestModuleSymbolMissingFinding {
186 pin,
187 suite_path,
188 missing_module,
189 call_sites: call_sites.as_slice(),
190 }),
191 _ => None,
192 })
193 }
194
195 pub fn header_file_missing(self) -> impl Iterator<Item = HeaderFileMissingFinding<'a>> {
198 self.pin_reason_pairs().filter_map(|(pin, r)| match r {
199 Reason::HeaderFileMissing {
200 source_path,
201 include_directive,
202 attempted_paths,
203 } => Some(HeaderFileMissingFinding {
204 pin,
205 source_path,
206 include_directive,
207 attempted_paths: attempted_paths.as_slice(),
208 }),
209 _ => None,
210 })
211 }
212
213 pub fn behaviour_module_missing(
216 self,
217 ) -> impl Iterator<Item = BehaviourModuleMissingFinding<'a>> {
218 self.pin_reason_pairs().filter_map(|(pin, r)| match r {
219 Reason::BehaviourModuleMissing {
220 source_path,
221 behaviour,
222 } => Some(BehaviourModuleMissingFinding {
223 pin,
224 source_path,
225 behaviour,
226 }),
227 _ => None,
228 })
229 }
230
231 pub fn versioned_machine_snapshot_missing(
234 self,
235 ) -> impl Iterator<Item = VersionedMachineSnapshotMissingFinding<'a>> {
236 self.pin_reason_pairs().filter_map(|(pin, r)| match r {
237 Reason::VersionedMachineSnapshotMissing { module, side } => {
238 Some(VersionedMachineSnapshotMissingFinding {
239 pin,
240 module,
241 side: *side,
242 })
243 }
244 _ => None,
245 })
246 }
247
248 pub fn wire_constant_bindings_missing(
251 self,
252 ) -> impl Iterator<Item = WireConstantBindingsMissingFinding<'a>> {
253 self.pin_reason_pairs().filter_map(|(pin, r)| match r {
254 Reason::WireConstantBindingsMissing {
255 module,
256 macros,
257 side,
258 } => Some(WireConstantBindingsMissingFinding {
259 pin,
260 module,
261 macros: macros.as_slice(),
262 side: *side,
263 }),
264 _ => None,
265 })
266 }
267}
268
269impl From<BatchResult> for SeriesEvaluation {
275 fn from(row: BatchResult) -> Self {
276 Self {
277 verdict: row.verdict,
278 diagnostics: row.diagnostics,
279 patch_facts: row.patch_facts,
280 touched_paths: row.touched_paths,
281 pr_commits: row.pr_commits,
282 apply: row.apply,
283 target_findings: row.target_findings,
284 }
285 }
286}
287
288impl BatchResult {
289 #[must_use]
294 pub fn evaluation(&self) -> SeriesEvaluationView<'_> {
295 SeriesEvaluationView::new(&self.verdict, &self.diagnostics)
296 }
297
298 #[must_use]
304 pub fn to_corpus_entry(
305 &self,
306 observed: BuildOutcome,
307 coverage: Option<&ResolverCoverage>,
308 ) -> Option<CorpusEntry> {
309 let fingerprint = self.verdict_fingerprint.clone()?;
310 let verdict = self.evaluation().worst_verdict();
311 if verdict == AggregateVerdict::Empty {
312 return None;
313 }
314 Some(CorpusEntry {
315 fingerprint,
316 verdict,
317 outcome: observed,
318 coverage: coverage.cloned(),
319 })
320 }
321
322 #[must_use]
330 pub fn predicted_conflicts(&self) -> Vec<PredictedConflict> {
331 let mut by_path: BTreeMap<PathBuf, ApplyConflictKind> = BTreeMap::new();
332 let mut record = |path: PathBuf, kind: ApplyConflictKind| {
333 by_path
334 .entry(path)
335 .and_modify(|seen| *seen = (*seen).max(kind))
336 .or_insert(kind);
337 };
338 for (path, kind) in self
339 .evaluation()
340 .reasons()
341 .filter_map(Reason::apply_conflict)
342 {
343 record(path.to_path_buf(), kind);
344 }
345 if let Some(forecast) = &self.apply {
346 for (path, kind) in forecast.conflicts() {
347 record(PathBuf::from(path.as_str()), kind);
348 }
349 }
350 by_path
351 .into_iter()
352 .map(|(path, kind)| PredictedConflict { path, kind })
353 .collect()
354 }
355}
356
357#[derive(Debug, Clone, Copy)]
359pub struct TestModuleSymbolMissingFinding<'a> {
360 pub pin: &'a PinVerdict,
362 pub suite_path: &'a RelativePath,
364 pub missing_module: &'a ModuleName,
366 pub call_sites: &'a [TestCallSite],
368}
369
370#[derive(Debug, Clone, Copy)]
372pub struct HeaderFileMissingFinding<'a> {
373 pub pin: &'a PinVerdict,
375 pub source_path: &'a RelativePath,
377 pub include_directive: &'a IncludeDirective,
379 pub attempted_paths: &'a [RelativePath],
381}
382
383#[derive(Debug, Clone, Copy)]
385pub struct BehaviourModuleMissingFinding<'a> {
386 pub pin: &'a PinVerdict,
388 pub source_path: &'a RelativePath,
390 pub behaviour: &'a ModuleName,
392}
393
394#[derive(Debug, Clone, Copy)]
397pub struct VersionedMachineSnapshotMissingFinding<'a> {
398 pub pin: &'a PinVerdict,
400 pub module: &'a ModuleName,
402 pub side: SnapshotSide,
404}
405
406#[derive(Debug, Clone, Copy)]
409pub struct WireConstantBindingsMissingFinding<'a> {
410 pub pin: &'a PinVerdict,
412 pub module: &'a ModuleName,
414 pub macros: &'a [MacroName],
416 pub side: SnapshotSide,
418}
419
420fn pin_reasons(pin: &PinVerdict) -> impl Iterator<Item = &Reason> {
421 pin.verdict.reasons().iter()
422}