Skip to main content

helixforge_engine/
reliability.rs

1//! How far each entry has been tested: real installs in distribution
2//! containers (recorded by the Distributions workflow and carried by release
3//! builds), installer tests with stand-in commands, and reports from virtual
4//! and real machines (`catalog/validation.toml`), with this machine's own
5//! runs beside them. Nothing here claims more than its record shows.
6
7use crate::history;
8use helixforge_core::{Action, Entry, Risk};
9use serde::Deserialize;
10use std::{
11    collections::BTreeMap,
12    path::{Path, PathBuf},
13};
14
15/// How far an entry has been validated, best first.
16#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd, serde::Serialize)]
17#[serde(rename_all = "kebab-case")]
18pub enum Level {
19    /// Run on a real machine, from a tester's report.
20    HardwareTested,
21    /// Run in a virtual machine, from a report.
22    VmTested,
23    /// Installed (or run) and verified in a distribution container by the
24    /// Distributions workflow.
25    ContainerTested,
26    /// Its installer was run against stand-in commands and files.
27    StandInTested,
28    NotYetValidated,
29}
30
31impl Level {
32    pub fn label(self) -> &'static str {
33        match self {
34            Level::HardwareTested => "Hardware Tested",
35            Level::VmTested => "VM Tested",
36            Level::ContainerTested => "Container Tested",
37            Level::StandInTested => "Stand-in Tested",
38            Level::NotYetValidated => "Not Yet Validated",
39        }
40    }
41
42    pub fn explain(self) -> &'static str {
43        match self {
44            Level::HardwareTested => "A tester ran it on a real machine and reported the result.",
45            Level::VmTested => {
46                "It ran for real in a virtual machine: the Distributions workflow boots a distribution's cloud image or live ISO, runs the entry, reboots into the result and checks it (the configuration, the kernel command line, the boot images, the services, a graphical boot menu or login screen). Real graphics hardware and firmware are not covered."
47            }
48            Level::ContainerTested => {
49                "The Distributions workflow ran it in distribution containers and HelixForge verified the result there. Containers have no boot loader, login screen or desktop, so those parts are not covered."
50            }
51            Level::StandInTested => {
52                "Installer tests run its script, or the helpers it calls, against stand-in commands and files, on every change and in each distribution's container. They show what it would change, without changing a system."
53            }
54            Level::NotYetValidated => {
55                "No automated test runs it and nobody has reported running it. Its plan is built and checked, like every entry's."
56            }
57        }
58    }
59}
60
61/// `catalog/validation.toml`.
62#[derive(Clone, Debug, Default, Deserialize)]
63pub struct Claims {
64    #[serde(default)]
65    pub critical: Critical,
66    #[serde(default, rename = "stand-ins")]
67    pub stand_ins: Vec<StandIns>,
68    #[serde(default)]
69    pub hardware: Vec<Report>,
70    #[serde(default)]
71    pub vm: Vec<Report>,
72}
73
74#[derive(Clone, Debug, Default, Deserialize)]
75pub struct Critical {
76    #[serde(default)]
77    pub entries: Vec<String>,
78}
79
80/// Installer tests that cover entries with stand-ins.
81#[derive(Clone, Debug, Default, Deserialize)]
82pub struct StandIns {
83    pub entries: Vec<String>,
84    pub tests: Vec<String>,
85    #[serde(default)]
86    pub covers: String,
87}
88
89/// A run reported from a virtual or real machine.
90#[derive(Clone, Debug, Default, Deserialize)]
91pub struct Report {
92    pub entry: String,
93    pub system: String,
94    #[serde(default)]
95    pub version: String,
96    pub date: String,
97    #[serde(default)]
98    pub note: String,
99}
100
101/// What a Distributions run verified in each container, as release builds
102/// carry it (`catalog/validation-results.json`).
103#[derive(Clone, Debug, Default, Deserialize)]
104pub struct Results {
105    #[serde(default)]
106    pub commit: String,
107    #[serde(default)]
108    pub run: u64,
109    #[serde(default)]
110    pub url: String,
111    /// When the run finished, in Unix seconds.
112    #[serde(default)]
113    pub finished: u64,
114    /// By container image: each entry and what was shown (`verified`, or
115    /// `ran` for an entry with nothing to verify).
116    #[serde(default)]
117    pub images: BTreeMap<String, Image>,
118    /// By virtual machine (arch, debian): the entries run there, rebooted
119    /// into and checked by scripts/vm-tests.py.
120    #[serde(default)]
121    pub vms: BTreeMap<String, Image>,
122}
123
124#[derive(Clone, Debug, Default, Deserialize)]
125pub struct Image {
126    #[serde(default)]
127    pub entries: BTreeMap<String, String>,
128}
129
130/// Everything known about testing, from this build's catalog.
131#[derive(Clone, Debug, Default)]
132pub struct Evidence {
133    pub claims: Claims,
134    pub results: Option<Results>,
135    /// Why a file could not be read, if one could not.
136    pub problems: Vec<String>,
137}
138
139/// One entry's standing.
140#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize)]
141pub struct Assessment {
142    pub level: Level,
143    /// Where the tests ran: container images or reported systems.
144    pub systems: Vec<String>,
145    /// When it was last verified, as a date, when known.
146    pub verified: Option<String>,
147    /// What shows it, line by line.
148    pub evidence: Vec<String>,
149    /// Its last successful run on this machine, in Unix seconds.
150    pub here: Option<u64>,
151    /// A boot, login-screen, driver or whole-system entry.
152    pub critical: bool,
153}
154
155/// A virtual machine as people name it: "Arch VM".
156pub fn vm_label(name: &str) -> String {
157    match name {
158        "arch" | "arch-themes" => "Arch VM".to_string(),
159        "debian" => "Debian VM".to_string(),
160        "fedora" => "Fedora VM".to_string(),
161        "opensuse" => "openSUSE VM".to_string(),
162        "arch-uefi" => "Arch UEFI VM".to_string(),
163        "arch-iso" => "Arch live ISO VM".to_string(),
164        "steamdeck" => "Steam Deck stand-in VM".to_string(),
165        other => format!("{} VM", helixforge_core::printable(other)),
166    }
167}
168
169/// A container image as people name the system.
170pub fn image_label(image: &str) -> String {
171    match image {
172        "archlinux:latest" => "Arch".to_string(),
173        "debian:stable" => "Debian".to_string(),
174        "fedora:latest" => "Fedora".to_string(),
175        "opensuse/tumbleweed:latest" => "openSUSE Tumbleweed".to_string(),
176        other => match other.split_once(':') {
177            Some(("ubuntu", version)) => format!("Ubuntu {version}"),
178            _ => helixforge_core::printable(other),
179        },
180    }
181}
182
183impl Evidence {
184    /// Read this build's record: `catalog/validation.toml` and, in a
185    /// release build, `catalog/validation-results.json`.
186    pub fn load(root: &Path) -> Self {
187        let mut evidence = Evidence::default();
188        let catalog = root.join("catalog");
189        let claims = catalog.join("validation.toml");
190        match std::fs::read_to_string(&claims) {
191            Ok(text) => match toml::from_str(&text) {
192                Ok(claims) => evidence.claims = claims,
193                Err(error) => evidence.problems.push(format!(
194                    "{}: {}",
195                    claims.display(),
196                    helixforge_core::printable(error.message())
197                )),
198            },
199            Err(error) if error.kind() == std::io::ErrorKind::NotFound => {}
200            Err(error) => evidence
201                .problems
202                .push(format!("{}: {error}", claims.display())),
203        }
204        let results: PathBuf = catalog.join("validation-results.json");
205        if let Ok(text) = std::fs::read_to_string(&results) {
206            match serde_json::from_str(&text) {
207                Ok(results) => evidence.results = Some(results),
208                Err(error) => evidence
209                    .problems
210                    .push(format!("{}: {error}", results.display())),
211            }
212        }
213        evidence
214    }
215
216    /// Whether failing would leave the machine without a working boot,
217    /// login screen or display, or the entry changes disks or the whole
218    /// system.
219    pub fn critical(&self, entry: &Entry) -> bool {
220        Risk::from_flags(&entry.flags) == Risk::High
221            || self.claims.critical.entries.contains(&entry.id)
222    }
223
224    /// How an entry stands; `history` is this machine's, newest first.
225    pub fn assess(&self, entry: &Entry, history: &[history::Record]) -> Assessment {
226        let here = history
227            .iter()
228            .find(|record| record.success && record.entries.contains(&entry.id))
229            .map(|record| record.unix_timestamp);
230        let mut assessment = Assessment {
231            level: Level::NotYetValidated,
232            systems: Vec::new(),
233            verified: None,
234            evidence: Vec::new(),
235            here,
236            critical: self.critical(entry),
237        };
238        let better = |assessment: &mut Assessment, level: Level| {
239            if level < assessment.level {
240                assessment.level = level;
241            }
242        };
243        for (kind, reports, level) in [
244            (
245                "a real machine",
246                &self.claims.hardware,
247                Level::HardwareTested,
248            ),
249            ("a virtual machine", &self.claims.vm, Level::VmTested),
250        ] {
251            for report in reports.iter().filter(|report| report.entry == entry.id) {
252                better(&mut assessment, level);
253                let system = helixforge_core::printable(&report.system);
254                if !assessment.systems.contains(&system) {
255                    assessment.systems.push(system.clone());
256                }
257                let date = helixforge_core::printable(&report.date);
258                if assessment
259                    .verified
260                    .as_ref()
261                    .is_none_or(|known| *known < date)
262                {
263                    assessment.verified = Some(date.clone());
264                }
265                let mut line = format!("Ran on {kind}: {system}, {date}");
266                if !report.version.is_empty() {
267                    line.push_str(&format!(
268                        ", HelixForge {}",
269                        helixforge_core::printable(&report.version)
270                    ));
271                }
272                if !report.note.is_empty() {
273                    line.push_str(&format!(" ({})", helixforge_core::printable(&report.note)));
274                }
275                assessment.evidence.push(line);
276            }
277        }
278        if let Some(results) = &self.results {
279            let date = crate::date(results.finished);
280            let machines: Vec<String> = results
281                .vms
282                .iter()
283                .filter(|(_, found)| {
284                    found
285                        .entries
286                        .get(&entry.id)
287                        .is_some_and(|outcome| outcome == "verified")
288                })
289                .map(|(name, _)| vm_label(name))
290                .collect();
291            if !machines.is_empty() {
292                better(&mut assessment, Level::VmTested);
293                for machine in &machines {
294                    if !assessment.systems.contains(machine) {
295                        assessment.systems.push(machine.clone());
296                    }
297                }
298                if assessment
299                    .verified
300                    .as_ref()
301                    .is_none_or(|known| *known < date)
302                {
303                    assessment.verified = Some(date.clone());
304                }
305                assessment.evidence.push(format!(
306                    "In virtual machines, Distributions run {} on {date}: {}; run for real, rebooted into and checked",
307                    results.run,
308                    machines.join(", ")
309                ));
310            }
311            let mut shown: Vec<String> = Vec::new();
312            for (image, found) in &results.images {
313                if let Some(outcome) = found.entries.get(&entry.id) {
314                    shown.push(format!(
315                        "{} ({})",
316                        image_label(image),
317                        if outcome == "verified" {
318                            "installed and verified"
319                        } else {
320                            "ran"
321                        }
322                    ));
323                    let label = image_label(image);
324                    if !assessment.systems.contains(&label) {
325                        assessment.systems.push(label);
326                    }
327                }
328            }
329            if !shown.is_empty() {
330                better(&mut assessment, Level::ContainerTested);
331                if assessment
332                    .verified
333                    .as_ref()
334                    .is_none_or(|known| *known < date)
335                {
336                    assessment.verified = Some(date.clone());
337                }
338                assessment.evidence.push(format!(
339                    "In containers, Distributions run {} on {date}: {}",
340                    results.run,
341                    shown.join(", ")
342                ));
343            }
344        }
345        for stand_ins in self
346            .claims
347            .stand_ins
348            .iter()
349            .filter(|group| group.entries.contains(&entry.id))
350        {
351            better(&mut assessment, Level::StandInTested);
352            let mut line = format!(
353                "Installer tests with stand-ins ({}): {}",
354                stand_ins.tests.join(", "),
355                helixforge_core::printable(&stand_ins.covers)
356            );
357            if let Some(results) = &self.results {
358                let date = crate::date(results.finished);
359                line.push_str(&format!(
360                    "; they ran in all {} containers on {date}",
361                    results.images.len()
362                ));
363                if assessment.level == Level::StandInTested {
364                    assessment.verified = Some(date);
365                    for image in results.images.keys() {
366                        let label = image_label(image);
367                        if !assessment.systems.contains(&label) {
368                            assessment.systems.push(label);
369                        }
370                    }
371                }
372            }
373            assessment.evidence.push(line);
374        }
375        if let Action::Package { .. } = entry.action {
376            assessment.evidence.push(
377                "Its commands are built by HelixForge from package names, and tested against harmless stand-ins on every change.".to_string(),
378            );
379        }
380        if let Some(when) = here {
381            assessment.evidence.push(format!(
382                "Ran successfully on this machine, {}",
383                crate::date(when)
384            ));
385        }
386        assessment
387    }
388
389    /// A line on the record this build carries, for the screen's header.
390    pub fn summary(&self) -> String {
391        match &self.results {
392            Some(results) => format!(
393                "Container record: Distributions run {} on commit {}, {}, {} distributions{}.",
394                results.run,
395                results.commit.chars().take(12).collect::<String>(),
396                crate::date(results.finished),
397                results.images.len(),
398                match results.vms.len() {
399                    0 => String::new(),
400                    1 => " and 1 virtual machine".to_string(),
401                    count => format!(" and {count} virtual machines"),
402                }
403            ),
404            None => "No container record in this build; release builds carry one.".to_string(),
405        }
406    }
407}
408
409/// Warnings for a plan's entries that are critical and not validated past
410/// stand-ins, shown before it runs.
411pub fn warnings<'a>(
412    ids: impl IntoIterator<Item = &'a str>,
413    lookup: &dyn Fn(&str) -> Option<&'a Entry>,
414    evidence: &Evidence,
415) -> Vec<String> {
416    let mut warnings = Vec::new();
417    for id in ids {
418        let Some(entry) = lookup(id) else { continue };
419        if !evidence.critical(entry) {
420            continue;
421        }
422        let assessment = evidence.assess(entry, &[]);
423        if assessment.level >= Level::ContainerTested {
424            warnings.push(format!(
425                "{} is {}: {}",
426                entry.name,
427                assessment.level.label(),
428                match assessment.level {
429                    Level::NotYetValidated => "nothing has tested it yet",
430                    Level::StandInTested => "only tested with stand-ins, never on a real system",
431                    _ => "containers cannot test boot, login or drivers",
432                }
433            ));
434        }
435    }
436    warnings
437}
438
439#[cfg(test)]
440mod tests {
441    use super::*;
442    use helixforge_catalog::Catalog;
443
444    fn evidence(claims: &str, results: Option<&str>) -> Evidence {
445        Evidence {
446            claims: toml::from_str(claims).unwrap(),
447            results: results.map(|text| serde_json::from_str(text).unwrap()),
448            problems: Vec::new(),
449        }
450    }
451
452    const RESULTS: &str = r#"{"format": 1, "commit": "0123456789abcdef0123", "run": 42,
453        "finished": 1792172552,
454        "images": {"archlinux:latest": {"entries": {"htop": "verified"}},
455                   "ubuntu:24.04": {"entries": {"htop": "verified", "system-report": "ran"}}},
456        "vms": {"arch": {"entries": {"sddm": "verified"}}}}"#;
457
458    #[test]
459    fn levels_come_from_the_record_never_beyond_it() {
460        let catalog = Catalog::builtin(false).unwrap();
461        let found = evidence(
462            r#"
463            [critical]
464            entries = ["sddm", "grub"]
465            [[stand-ins]]
466            entries = ["sddm"]
467            tests = ["test_sddm"]
468            covers = "the theme"
469            [[hardware]]
470            entry = "grub"
471            system = "Garuda Linux"
472            date = "2026-10-20"
473            "#,
474            Some(RESULTS),
475        );
476        let assess = |id: &str| found.assess(catalog.find(id).unwrap(), &[]);
477        let htop = assess("htop");
478        assert_eq!(htop.level, Level::ContainerTested);
479        assert_eq!(htop.systems, ["Arch", "Ubuntu 24.04"]);
480        assert_eq!(htop.verified.as_deref(), Some("2026-10-16"));
481        assert!(!htop.critical);
482        assert_eq!(assess("system-report").level, Level::ContainerTested);
483        // Verified in a virtual machine: above its stand-in tests.
484        let sddm = assess("sddm");
485        assert_eq!(sddm.level, Level::VmTested);
486        assert!(sddm.critical);
487        assert!(
488            sddm.systems.contains(&"Arch VM".to_string()),
489            "{:?}",
490            sddm.systems
491        );
492        assert!(
493            sddm.evidence[0].contains("virtual machines"),
494            "{:?}",
495            sddm.evidence
496        );
497        assert!(sddm.evidence.iter().any(|line| line.contains("test_sddm")));
498        assert!(
499            found.summary().contains(" and 1 virtual machine."),
500            "{}",
501            found.summary()
502        );
503        let grub = assess("grub");
504        assert_eq!(grub.level, Level::HardwareTested);
505        assert_eq!(grub.verified.as_deref(), Some("2026-10-20"));
506        assert_eq!(assess("ripgrep").level, Level::NotYetValidated);
507        // This machine's runs show beside the level, never raise it.
508        let ran = [history::Record {
509            unix_timestamp: 1_700_000_000,
510            entries: vec!["ripgrep".into()],
511            names: Vec::new(),
512            success: true,
513            result: String::new(),
514            log: None,
515        }];
516        let ripgrep = found.assess(catalog.find("ripgrep").unwrap(), &ran);
517        assert_eq!(ripgrep.level, Level::NotYetValidated);
518        assert_eq!(ripgrep.here, Some(1_700_000_000));
519
520        // Before a run, critical entries short of a real test are named.
521        // SDDM, tested in a VM, and GRUB, on hardware, are not.
522        let warned = warnings(
523            ["sddm", "grub", "htop", "arch-server-setup"],
524            &|id| catalog.find(id),
525            &found,
526        );
527        assert_eq!(warned.len(), 1, "{warned:?}");
528        assert!(warned[0].starts_with("Arch Server Setup"), "{warned:?}");
529        // Whole-system entries count as critical by their risk.
530        assert!(found.critical(catalog.find("arch-server-setup").unwrap()));
531    }
532
533    #[test]
534    fn a_build_without_a_record_says_so() {
535        let found = evidence("", None);
536        assert!(found.summary().contains("No container record"));
537        let catalog = Catalog::builtin(false).unwrap();
538        assert_eq!(
539            found.assess(catalog.find("htop").unwrap(), &[]).level,
540            Level::NotYetValidated
541        );
542    }
543
544    #[test]
545    fn the_catalogs_own_record_reads_and_names_real_entries() {
546        let catalog = Catalog::builtin(false).unwrap();
547        let found = Evidence::load(catalog.root());
548        assert!(found.problems.is_empty(), "{:?}", found.problems);
549        assert!(!found.claims.stand_ins.is_empty());
550        for id in found
551            .claims
552            .stand_ins
553            .iter()
554            .flat_map(|group| group.entries.iter())
555            .chain(found.claims.critical.entries.iter())
556            .chain(found.claims.hardware.iter().map(|report| &report.entry))
557            .chain(found.claims.vm.iter().map(|report| &report.entry))
558        {
559            assert!(catalog.find(id).is_some(), "validation.toml names {id}");
560        }
561    }
562}