1use std::collections::{BTreeMap, BTreeSet};
6
7use serde::{Deserialize, Serialize};
8
9use crate::jigsaw::findings::{Finding, Stop, Warning, WithheldReason, WithheldSymbol};
10use crate::jigsaw::plan::{JigsawPlan, TagSourceKind};
11use crate::jigsaw::plugin_set::AppRole;
12use crate::jigsaw::references::{NotChecked, ReferenceSite, UncheckedSite};
13use crate::model::names::{ApplicationName, HostName, StoreDigest, vocabulary};
14use crate::model::non_empty::NonEmpty;
15use crate::model::release_version::{HostSeries, HostTag, OtpTag};
16
17vocabulary!(
18 #[derive(PartialOrd, Ord, Hash)]
20 pub enum JigsawCell: "cell state" {
21 Clean => "clean",
22 Broken => "broken",
23 }
24);
25
26mod private {
27 pub trait Sealed {}
28}
29
30pub trait AxisIndex: private::Sealed + Copy + Ord {
33 fn position(self) -> usize;
34 fn adjacent_to(self, earlier: Self) -> bool {
35 self.position() == earlier.position() + 1
36 }
37}
38
39#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
41pub struct HostIdx(usize);
42
43#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
45pub struct OtpIdx(usize);
46
47impl private::Sealed for HostIdx {}
48impl private::Sealed for OtpIdx {}
49
50impl AxisIndex for HostIdx {
51 fn position(self) -> usize {
52 self.0
53 }
54}
55
56impl AxisIndex for OtpIdx {
57 fn position(self) -> usize {
58 self.0
59 }
60}
61
62#[derive(Debug, Clone, Copy, PartialEq, Eq)]
64pub struct Run<I> {
65 pub first: I,
66 pub last: I,
67}
68
69#[derive(Debug, Clone, PartialEq, Eq)]
72pub struct TargetAxes {
73 hosts: Vec<HostTag>,
74 otp: Vec<OtpTag>,
75 build: HostIdx,
76}
77
78impl TargetAxes {
79 #[must_use]
80 pub(crate) fn of(plan: &JigsawPlan) -> Self {
81 let hosts: Vec<HostTag> = plan
82 .releases()
83 .iter()
84 .map(|release| release.tag.clone())
85 .collect();
86 let otp = plan
87 .otp_lines()
88 .iter()
89 .flat_map(|line| line.tags().iter().cloned())
90 .collect();
91 let build = HostIdx(hosts.len() - 1);
92 Self { hosts, otp, build }
93 }
94
95 #[must_use]
96 pub fn hosts(&self) -> &[HostTag] {
97 &self.hosts
98 }
99
100 #[must_use]
101 pub fn otp_tags(&self) -> &[OtpTag] {
102 &self.otp
103 }
104
105 #[must_use]
106 pub fn build(&self) -> HostIdx {
107 self.build
108 }
109
110 #[must_use]
111 pub fn host_tag(&self, index: HostIdx) -> &HostTag {
112 &self.hosts[index.0]
113 }
114
115 #[must_use]
116 pub fn otp_tag(&self, index: OtpIdx) -> &OtpTag {
117 &self.otp[index.0]
118 }
119
120 pub fn host_indices(&self) -> impl Iterator<Item = HostIdx> + use<> {
121 (0..self.hosts.len()).map(HostIdx)
122 }
123
124 pub fn otp_indices(&self) -> impl Iterator<Item = OtpIdx> + use<> {
125 (0..self.otp.len()).map(OtpIdx)
126 }
127
128 #[must_use]
132 pub fn runs<I: AxisIndex>(members: &BTreeSet<I>) -> Vec<Run<I>> {
133 let mut runs: Vec<Run<I>> = Vec::new();
134 for &index in members {
135 match runs.last_mut() {
136 Some(run) if index.adjacent_to(run.last) => run.last = index,
137 _ => runs.push(Run {
138 first: index,
139 last: index,
140 }),
141 }
142 }
143 runs
144 }
145}
146
147#[derive(Debug, Clone, PartialEq, Eq)]
152pub enum FindingSpan {
153 Host(BTreeSet<HostIdx>),
154 Otp(BTreeSet<OtpIdx>),
155 Everywhere,
156}
157
158impl FindingSpan {
159 #[must_use]
160 pub fn covers(&self, host: HostIdx, otp: OtpIdx) -> bool {
161 match self {
162 Self::Host(releases) => releases.contains(&host),
163 Self::Otp(tags) => tags.contains(&otp),
164 Self::Everywhere => true,
165 }
166 }
167}
168
169#[derive(Debug, Clone, PartialEq, Eq)]
170pub struct RangedFinding {
171 pub finding: Finding,
172 pub apps: NonEmpty<ApplicationName>,
173 pub sites: NonEmpty<ReferenceSite>,
174 pub span: FindingSpan,
175}
176
177#[derive(Debug, Clone, PartialEq, Eq)]
178pub struct RangedWithheld {
179 pub symbol: WithheldSymbol,
180 pub sites: NonEmpty<ReferenceSite>,
181 pub span: FindingSpan,
182}
183
184#[derive(Debug, Clone, PartialEq, Eq)]
185pub struct RangedWarning {
186 pub warning: Warning,
187 pub span: FindingSpan,
188}
189
190#[derive(Debug, Clone, PartialEq, Eq)]
196pub struct Tallies {
197 checked: usize,
198 not_checked: BTreeMap<NotChecked, usize>,
199 withheld: BTreeMap<WithheldReason, usize>,
200}
201
202impl Tallies {
203 #[must_use]
204 pub(crate) fn new(
205 checked: usize,
206 not_checked: impl IntoIterator<Item = (NotChecked, usize)>,
207 unchecked: &[UncheckedSite],
208 stopped: impl IntoIterator<Item = (Stop, usize)>,
209 withheld: &[RangedWithheld],
210 ) -> Self {
211 let mut tallies = Self {
212 checked,
213 not_checked: NotChecked::ALL.iter().map(|&r| (r, 0)).collect(),
214 withheld: WithheldReason::ALL.iter().map(|&r| (r, 0)).collect(),
215 };
216 for (reason, n) in not_checked {
217 *tallies.not_checked.entry(reason).or_default() += n;
218 }
219 for site in unchecked {
220 *tallies.not_checked.entry(site.reason()).or_default() += 1;
221 }
222 for (stop, n) in stopped {
223 *tallies.withheld.entry(stop.reason()).or_default() += n;
224 }
225 for ranged in withheld {
226 *tallies.withheld.entry(ranged.symbol.reason()).or_default() +=
227 ranged.sites.as_slice().len();
228 }
229 tallies
230 }
231
232 #[must_use]
233 pub fn checked(&self) -> usize {
234 self.checked
235 }
236
237 #[must_use]
238 pub fn not_checked(&self) -> &BTreeMap<NotChecked, usize> {
239 &self.not_checked
240 }
241
242 #[must_use]
243 pub fn withheld(&self) -> &BTreeMap<WithheldReason, usize> {
244 &self.withheld
245 }
246}
247
248pub(crate) struct ReportParts {
249 pub host: HostName,
250 pub series: HostSeries,
251 pub tag_source: TagSourceKind,
252 pub targets: TargetAxes,
253 pub apps: BTreeMap<ApplicationName, AppRole>,
254 pub findings: Vec<RangedFinding>,
255 pub warnings: Vec<RangedWarning>,
256 pub withheld: Vec<RangedWithheld>,
257 pub tallies: Tallies,
258 pub unchecked: Vec<UncheckedSite>,
259 pub host_provided: BTreeSet<ApplicationName>,
260 pub store_digest: StoreDigest,
261}
262
263#[derive(Debug, Clone, PartialEq, Eq)]
266pub struct JigsawReport {
267 host: HostName,
268 series: HostSeries,
269 tag_source: TagSourceKind,
270 targets: TargetAxes,
271 apps: BTreeMap<ApplicationName, AppRole>,
272 findings: Vec<RangedFinding>,
273 warnings: Vec<RangedWarning>,
274 withheld: Vec<RangedWithheld>,
275 tallies: Tallies,
276 unchecked: Vec<UncheckedSite>,
277 host_provided: BTreeSet<ApplicationName>,
278 store_digest: StoreDigest,
279}
280
281impl JigsawReport {
282 pub(crate) fn from_parts(parts: ReportParts) -> Self {
283 Self {
284 host: parts.host,
285 series: parts.series,
286 tag_source: parts.tag_source,
287 targets: parts.targets,
288 apps: parts.apps,
289 findings: parts.findings,
290 warnings: parts.warnings,
291 withheld: parts.withheld,
292 tallies: parts.tallies,
293 unchecked: parts.unchecked,
294 host_provided: parts.host_provided,
295 store_digest: parts.store_digest,
296 }
297 }
298
299 #[must_use]
300 pub fn host(&self) -> &HostName {
301 &self.host
302 }
303
304 #[must_use]
305 pub fn series(&self) -> HostSeries {
306 self.series
307 }
308
309 #[must_use]
310 pub fn tag_source(&self) -> TagSourceKind {
311 self.tag_source
312 }
313
314 #[must_use]
315 pub fn targets(&self) -> &TargetAxes {
316 &self.targets
317 }
318
319 #[must_use]
320 pub fn build_release(&self) -> &HostTag {
321 self.targets.host_tag(self.targets.build())
322 }
323
324 #[must_use]
325 pub fn apps(&self) -> &BTreeMap<ApplicationName, AppRole> {
326 &self.apps
327 }
328
329 #[must_use]
330 pub fn findings(&self) -> &[RangedFinding] {
331 &self.findings
332 }
333
334 #[must_use]
335 pub fn warnings(&self) -> &[RangedWarning] {
336 &self.warnings
337 }
338
339 #[must_use]
341 pub fn withheld(&self) -> &[RangedWithheld] {
342 &self.withheld
343 }
344
345 #[must_use]
346 pub fn tallies(&self) -> &Tallies {
347 &self.tallies
348 }
349
350 #[must_use]
353 pub fn unchecked(&self) -> &[UncheckedSite] {
354 &self.unchecked
355 }
356
357 #[must_use]
358 pub fn host_provided(&self) -> &BTreeSet<ApplicationName> {
359 &self.host_provided
360 }
361
362 #[must_use]
363 pub fn store_digest(&self) -> &StoreDigest {
364 &self.store_digest
365 }
366
367 #[must_use]
370 pub fn cell(&self, app: &ApplicationName, host: HostIdx, otp: OtpIdx) -> JigsawCell {
371 let broken = self.findings.iter().any(|ranged| {
372 ranged.apps.iter().any(|named| named == app) && ranged.span.covers(host, otp)
373 });
374 if broken {
375 JigsawCell::Broken
376 } else {
377 JigsawCell::Clean
378 }
379 }
380
381 #[must_use]
383 pub fn is_clean(&self) -> bool {
384 self.findings.is_empty()
385 }
386}