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        "arch-uefi" => "Arch UEFI VM".to_string(),
162        "arch-iso" => "Arch live ISO VM".to_string(),
163        "steamdeck" => "Steam Deck stand-in VM".to_string(),
164        other => format!("{} VM", helixforge_core::printable(other)),
165    }
166}
167
168/// A container image as people name the system.
169pub fn image_label(image: &str) -> String {
170    match image {
171        "archlinux:latest" => "Arch".to_string(),
172        "debian:stable" => "Debian".to_string(),
173        "fedora:latest" => "Fedora".to_string(),
174        "opensuse/tumbleweed:latest" => "openSUSE Tumbleweed".to_string(),
175        other => match other.split_once(':') {
176            Some(("ubuntu", version)) => format!("Ubuntu {version}"),
177            _ => helixforge_core::printable(other),
178        },
179    }
180}
181
182impl Evidence {
183    /// Read this build's record: `catalog/validation.toml` and, in a
184    /// release build, `catalog/validation-results.json`.
185    pub fn load(root: &Path) -> Self {
186        let mut evidence = Evidence::default();
187        let catalog = root.join("catalog");
188        let claims = catalog.join("validation.toml");
189        match std::fs::read_to_string(&claims) {
190            Ok(text) => match toml::from_str(&text) {
191                Ok(claims) => evidence.claims = claims,
192                Err(error) => evidence.problems.push(format!(
193                    "{}: {}",
194                    claims.display(),
195                    helixforge_core::printable(error.message())
196                )),
197            },
198            Err(error) if error.kind() == std::io::ErrorKind::NotFound => {}
199            Err(error) => evidence
200                .problems
201                .push(format!("{}: {error}", claims.display())),
202        }
203        let results: PathBuf = catalog.join("validation-results.json");
204        if let Ok(text) = std::fs::read_to_string(&results) {
205            match serde_json::from_str(&text) {
206                Ok(results) => evidence.results = Some(results),
207                Err(error) => evidence
208                    .problems
209                    .push(format!("{}: {error}", results.display())),
210            }
211        }
212        evidence
213    }
214
215    /// Whether failing would leave the machine without a working boot,
216    /// login screen or display, or the entry changes disks or the whole
217    /// system.
218    pub fn critical(&self, entry: &Entry) -> bool {
219        Risk::from_flags(&entry.flags) == Risk::High
220            || self.claims.critical.entries.contains(&entry.id)
221    }
222
223    /// How an entry stands; `history` is this machine's, newest first.
224    pub fn assess(&self, entry: &Entry, history: &[history::Record]) -> Assessment {
225        let here = history
226            .iter()
227            .find(|record| record.success && record.entries.contains(&entry.id))
228            .map(|record| record.unix_timestamp);
229        let mut assessment = Assessment {
230            level: Level::NotYetValidated,
231            systems: Vec::new(),
232            verified: None,
233            evidence: Vec::new(),
234            here,
235            critical: self.critical(entry),
236        };
237        let better = |assessment: &mut Assessment, level: Level| {
238            if level < assessment.level {
239                assessment.level = level;
240            }
241        };
242        for (kind, reports, level) in [
243            (
244                "a real machine",
245                &self.claims.hardware,
246                Level::HardwareTested,
247            ),
248            ("a virtual machine", &self.claims.vm, Level::VmTested),
249        ] {
250            for report in reports.iter().filter(|report| report.entry == entry.id) {
251                better(&mut assessment, level);
252                let system = helixforge_core::printable(&report.system);
253                if !assessment.systems.contains(&system) {
254                    assessment.systems.push(system.clone());
255                }
256                let date = helixforge_core::printable(&report.date);
257                if assessment
258                    .verified
259                    .as_ref()
260                    .is_none_or(|known| *known < date)
261                {
262                    assessment.verified = Some(date.clone());
263                }
264                let mut line = format!("Ran on {kind}: {system}, {date}");
265                if !report.version.is_empty() {
266                    line.push_str(&format!(
267                        ", HelixForge {}",
268                        helixforge_core::printable(&report.version)
269                    ));
270                }
271                if !report.note.is_empty() {
272                    line.push_str(&format!(" ({})", helixforge_core::printable(&report.note)));
273                }
274                assessment.evidence.push(line);
275            }
276        }
277        if let Some(results) = &self.results {
278            let date = crate::date(results.finished);
279            let machines: Vec<String> = results
280                .vms
281                .iter()
282                .filter(|(_, found)| {
283                    found
284                        .entries
285                        .get(&entry.id)
286                        .is_some_and(|outcome| outcome == "verified")
287                })
288                .map(|(name, _)| vm_label(name))
289                .collect();
290            if !machines.is_empty() {
291                better(&mut assessment, Level::VmTested);
292                for machine in &machines {
293                    if !assessment.systems.contains(machine) {
294                        assessment.systems.push(machine.clone());
295                    }
296                }
297                if assessment
298                    .verified
299                    .as_ref()
300                    .is_none_or(|known| *known < date)
301                {
302                    assessment.verified = Some(date.clone());
303                }
304                assessment.evidence.push(format!(
305                    "In virtual machines, Distributions run {} on {date}: {}; run for real, rebooted into and checked",
306                    results.run,
307                    machines.join(", ")
308                ));
309            }
310            let mut shown: Vec<String> = Vec::new();
311            for (image, found) in &results.images {
312                if let Some(outcome) = found.entries.get(&entry.id) {
313                    shown.push(format!(
314                        "{} ({})",
315                        image_label(image),
316                        if outcome == "verified" {
317                            "installed and verified"
318                        } else {
319                            "ran"
320                        }
321                    ));
322                    let label = image_label(image);
323                    if !assessment.systems.contains(&label) {
324                        assessment.systems.push(label);
325                    }
326                }
327            }
328            if !shown.is_empty() {
329                better(&mut assessment, Level::ContainerTested);
330                if assessment
331                    .verified
332                    .as_ref()
333                    .is_none_or(|known| *known < date)
334                {
335                    assessment.verified = Some(date.clone());
336                }
337                assessment.evidence.push(format!(
338                    "In containers, Distributions run {} on {date}: {}",
339                    results.run,
340                    shown.join(", ")
341                ));
342            }
343        }
344        for stand_ins in self
345            .claims
346            .stand_ins
347            .iter()
348            .filter(|group| group.entries.contains(&entry.id))
349        {
350            better(&mut assessment, Level::StandInTested);
351            let mut line = format!(
352                "Installer tests with stand-ins ({}): {}",
353                stand_ins.tests.join(", "),
354                helixforge_core::printable(&stand_ins.covers)
355            );
356            if let Some(results) = &self.results {
357                let date = crate::date(results.finished);
358                line.push_str(&format!(
359                    "; they ran in all {} containers on {date}",
360                    results.images.len()
361                ));
362                if assessment.level == Level::StandInTested {
363                    assessment.verified = Some(date);
364                    for image in results.images.keys() {
365                        let label = image_label(image);
366                        if !assessment.systems.contains(&label) {
367                            assessment.systems.push(label);
368                        }
369                    }
370                }
371            }
372            assessment.evidence.push(line);
373        }
374        if let Action::Package { .. } = entry.action {
375            assessment.evidence.push(
376                "Its commands are built by HelixForge from package names, and tested against harmless stand-ins on every change.".to_string(),
377            );
378        }
379        if let Some(when) = here {
380            assessment.evidence.push(format!(
381                "Ran successfully on this machine, {}",
382                crate::date(when)
383            ));
384        }
385        assessment
386    }
387
388    /// A line on the record this build carries, for the screen's header.
389    pub fn summary(&self) -> String {
390        match &self.results {
391            Some(results) => format!(
392                "Container record: Distributions run {} on commit {}, {}, {} distributions{}.",
393                results.run,
394                results.commit.chars().take(12).collect::<String>(),
395                crate::date(results.finished),
396                results.images.len(),
397                match results.vms.len() {
398                    0 => String::new(),
399                    1 => " and 1 virtual machine".to_string(),
400                    count => format!(" and {count} virtual machines"),
401                }
402            ),
403            None => "No container record in this build; release builds carry one.".to_string(),
404        }
405    }
406}
407
408/// Warnings for a plan's entries that are critical and not validated past
409/// stand-ins, shown before it runs.
410pub fn warnings<'a>(
411    ids: impl IntoIterator<Item = &'a str>,
412    lookup: &dyn Fn(&str) -> Option<&'a Entry>,
413    evidence: &Evidence,
414) -> Vec<String> {
415    let mut warnings = Vec::new();
416    for id in ids {
417        let Some(entry) = lookup(id) else { continue };
418        if !evidence.critical(entry) {
419            continue;
420        }
421        let assessment = evidence.assess(entry, &[]);
422        if assessment.level >= Level::ContainerTested {
423            warnings.push(format!(
424                "{} is {}: {}",
425                entry.name,
426                assessment.level.label(),
427                match assessment.level {
428                    Level::NotYetValidated => "nothing has tested it yet",
429                    Level::StandInTested => "only tested with stand-ins, never on a real system",
430                    _ => "containers cannot test boot, login or drivers",
431                }
432            ));
433        }
434    }
435    warnings
436}
437
438#[cfg(test)]
439mod tests {
440    use super::*;
441    use helixforge_catalog::Catalog;
442
443    fn evidence(claims: &str, results: Option<&str>) -> Evidence {
444        Evidence {
445            claims: toml::from_str(claims).unwrap(),
446            results: results.map(|text| serde_json::from_str(text).unwrap()),
447            problems: Vec::new(),
448        }
449    }
450
451    const RESULTS: &str = r#"{"format": 1, "commit": "0123456789abcdef0123", "run": 42,
452        "finished": 1792172552,
453        "images": {"archlinux:latest": {"entries": {"htop": "verified"}},
454                   "ubuntu:24.04": {"entries": {"htop": "verified", "system-report": "ran"}}},
455        "vms": {"arch": {"entries": {"sddm": "verified"}}}}"#;
456
457    #[test]
458    fn levels_come_from_the_record_never_beyond_it() {
459        let catalog = Catalog::builtin(false).unwrap();
460        let found = evidence(
461            r#"
462            [critical]
463            entries = ["sddm", "grub"]
464            [[stand-ins]]
465            entries = ["sddm"]
466            tests = ["test_sddm"]
467            covers = "the theme"
468            [[hardware]]
469            entry = "grub"
470            system = "Garuda Linux"
471            date = "2026-10-20"
472            "#,
473            Some(RESULTS),
474        );
475        let assess = |id: &str| found.assess(catalog.find(id).unwrap(), &[]);
476        let htop = assess("htop");
477        assert_eq!(htop.level, Level::ContainerTested);
478        assert_eq!(htop.systems, ["Arch", "Ubuntu 24.04"]);
479        assert_eq!(htop.verified.as_deref(), Some("2026-10-16"));
480        assert!(!htop.critical);
481        assert_eq!(assess("system-report").level, Level::ContainerTested);
482        // Verified in a virtual machine: above its stand-in tests.
483        let sddm = assess("sddm");
484        assert_eq!(sddm.level, Level::VmTested);
485        assert!(sddm.critical);
486        assert!(
487            sddm.systems.contains(&"Arch VM".to_string()),
488            "{:?}",
489            sddm.systems
490        );
491        assert!(
492            sddm.evidence[0].contains("virtual machines"),
493            "{:?}",
494            sddm.evidence
495        );
496        assert!(sddm.evidence.iter().any(|line| line.contains("test_sddm")));
497        assert!(
498            found.summary().contains(" and 1 virtual machine."),
499            "{}",
500            found.summary()
501        );
502        let grub = assess("grub");
503        assert_eq!(grub.level, Level::HardwareTested);
504        assert_eq!(grub.verified.as_deref(), Some("2026-10-20"));
505        assert_eq!(assess("ripgrep").level, Level::NotYetValidated);
506        // This machine's runs show beside the level, never raise it.
507        let ran = [history::Record {
508            unix_timestamp: 1_700_000_000,
509            entries: vec!["ripgrep".into()],
510            names: Vec::new(),
511            success: true,
512            result: String::new(),
513            log: None,
514        }];
515        let ripgrep = found.assess(catalog.find("ripgrep").unwrap(), &ran);
516        assert_eq!(ripgrep.level, Level::NotYetValidated);
517        assert_eq!(ripgrep.here, Some(1_700_000_000));
518
519        // Before a run, critical entries short of a real test are named.
520        // SDDM, tested in a VM, and GRUB, on hardware, are not.
521        let warned = warnings(
522            ["sddm", "grub", "htop", "arch-server-setup"],
523            &|id| catalog.find(id),
524            &found,
525        );
526        assert_eq!(warned.len(), 1, "{warned:?}");
527        assert!(warned[0].starts_with("Arch Server Setup"), "{warned:?}");
528        // Whole-system entries count as critical by their risk.
529        assert!(found.critical(catalog.find("arch-server-setup").unwrap()));
530    }
531
532    #[test]
533    fn a_build_without_a_record_says_so() {
534        let found = evidence("", None);
535        assert!(found.summary().contains("No container record"));
536        let catalog = Catalog::builtin(false).unwrap();
537        assert_eq!(
538            found.assess(catalog.find("htop").unwrap(), &[]).level,
539            Level::NotYetValidated
540        );
541    }
542
543    #[test]
544    fn the_catalogs_own_record_reads_and_names_real_entries() {
545        let catalog = Catalog::builtin(false).unwrap();
546        let found = Evidence::load(catalog.root());
547        assert!(found.problems.is_empty(), "{:?}", found.problems);
548        assert!(!found.claims.stand_ins.is_empty());
549        for id in found
550            .claims
551            .stand_ins
552            .iter()
553            .flat_map(|group| group.entries.iter())
554            .chain(found.claims.critical.entries.iter())
555            .chain(found.claims.hardware.iter().map(|report| &report.entry))
556            .chain(found.claims.vm.iter().map(|report| &report.entry))
557        {
558            assert!(catalog.find(id).is_some(), "validation.toml names {id}");
559        }
560    }
561}