Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/edda

crates/nornir-build-thing/src/kernel_trim/coverage/tests.rs

//! The coverage half on synthetic kernel images: the probe option lands and is
//! refused for release media, the gcov list is walked out of a saved image and
//! refused when the image is not the vmlinux, and the counters map to
//! executed / never-executed / unmeasured symbols with the stages that ran them.

use std::collections::{BTreeMap, BTreeSet};

use super::*;
use crate::kernel::KernelBuild;

const BASE: u64 = 0xffff_ffff_8100_0000;

/// A writable synthetic kernel image.
struct Img(Vec<u8>);

impl Img {
    fn new() -> Self {
        Img(vec![0u8; 0x10000])
    }
    fn at(&mut self, addr: u64) -> &mut [u8] {
        &mut self.0[(addr - BASE) as usize..]
    }
    fn u32(&mut self, addr: u64, v: u32) {
        self.at(addr)[..4].copy_from_slice(&v.to_le_bytes());
    }
    fn u64(&mut self, addr: u64, v: u64) {
        self.at(addr)[..8].copy_from_slice(&v.to_le_bytes());
    }
    fn str(&mut self, addr: u64, s: &str) {
        self.at(addr)[..s.len()].copy_from_slice(s.as_bytes());
    }
    /// One `gcov_info` (GCC 14 layout) with `fns` functions of the given arc counters.
    fn info(&mut self, at: u64, next: u64, fname_at: u64, fname: &str, fns: &[(u64, u64, &[u64])]) {
        let l = GcovLayout::for_gcc(14);
        self.u32(at, 0x4231_342a);
        self.u64(at + 8, next);
        self.str(fname_at, fname);
        self.u64(at + GcovLayout::FILENAME, fname_at);
        self.u64(at + GcovLayout::MERGE, BASE + 0x10); // merge[0]: the arc merger
        self.u32(at + l.n_functions(), fns.len() as u32);
        let arr = at + 0x100;
        self.u64(at + l.functions(), arr);
        for (i, (fi, values_at, values)) in fns.iter().enumerate() {
            self.u64(arr + 8 * i as u64, *fi);
            self.u64(*fi, at);
            self.u32(fi + 24, values.len() as u32);
            self.u64(fi + 32, *values_at);
            for (j, v) in values.iter().enumerate() {
                self.u64(values_at + 8 * j as u64, *v);
            }
        }
    }
}

const BANNER: &str = "Linux version 6.12.104 (kcov@test) #1 SMP";

fn image() -> (KernelImage, Img) {
    let mut m = Img::new();
    m.str(BASE + 0x100, BANNER);
    m.u64(BASE + 0x200, BASE + 0x1000);
    m.info(
        BASE + 0x1000,
        BASE + 0x2000,
        BASE + 0x3000,
        "/work/src/linux-6.12.104/fs/ext4/inode.gcda",
        &[(BASE + 0x1200, BASE + 0x4000, &[0, 5, 0]), (BASE + 0x1300, BASE + 0x4100, &[0, 0])],
    );
    m.info(BASE + 0x2000, 0, BASE + 0x3100, "/work/src/linux-6.12.104/drivers/ata/libata-core.gcda", &[(BASE + 0x2200, BASE + 0x4200, &[0])]);
    let img = KernelImage {
        text: BASE,
        end: BASE + 0x10000,
        head: BASE + 0x200,
        banner: (BASE + 0x100, BANNER.as_bytes().to_vec()),
        layout: GcovLayout::for_gcc(14),
    };
    (img, m)
}

fn dump(m: &Img) -> ImageDump {
    ImageDump { base: BASE, bytes: m.0.clone() }
}

#[test]
fn the_probe_option_parses_lands_last_and_changes_the_content_id() {
    assert_eq!(KernelProbe::parse("gcov").unwrap(), Some(KernelProbe::Gcov));
    assert_eq!(KernelProbe::parse("off").unwrap(), None);
    let e = KernelProbe::parse("kcov").unwrap_err().to_string();
    assert!(e.contains("unknown probe") && e.contains("off | gcov"), "{e}");
    assert!(probe_of_pairs(&[("coverage".into(), "gcov".into()), ("coverage".into(), "gcov".into())]).is_err(), "one probe per build");

    let mut set = crate::variant::BuildVariantSet::default();
    set.kernels.push(KernelBuild { name: "k".into(), version: "6.12.104".into(), preset: "slim".into(), profile: "hardened".into(), ..Default::default() });
    let plain = crate::kernel::chosen_kernel_for(&set, "k", &[], None).unwrap();
    let opts = [crate::flags::Flag::choice("kernel.coverage", "gcov")];
    let probe = crate::kernel::chosen_kernel_for(&set, "k", &opts, None).expect("kernel.coverage=gcov is a kernel option, not an unknown symbol");
    assert_eq!(probe.probe, Some(KernelProbe::Gcov));
    assert_eq!(plain.probe, None);
    assert_ne!(plain.content_id(), probe.content_id(), "a probe kernel is another artifact");
    let frag = crate::kernel::merged_fragment(&probe, None).unwrap();
    assert!(frag.contains("CONFIG_GCOV_KERNEL=y") && frag.contains("CONFIG_GCOV_PROFILE_ALL=y") && frag.contains("CONFIG_DEBUG_FS=y"), "{frag}");
    assert!(frag.contains("# CONFIG_RANDOMIZE_BASE is not set") && frag.contains("# CONFIG_RANDOMIZE_MEMORY is not set"), "the probe needs link addresses at run time: {frag}");
    let plain_frag = crate::kernel::merged_fragment(&plain, None).unwrap();
    assert!(!plain_frag.contains("GCOV"), "{plain_frag}");
    // `kernel.coverage=off` is the plain kernel.
    let off = crate::kernel::chosen_kernel_for(&set, "k", &[crate::flags::Flag::choice("kernel.coverage", "off")], None).unwrap();
    assert_eq!(off.content_id(), plain.content_id());
    // The CLI resolver skips the probe word instead of judging it as a symbol.
    let d = crate::kernel_features::resolve_kernel_cli(
        crate::kernel::Preset::Slim,
        crate::kernel::HardeningProfile::Hardened,
        &[("coverage".into(), "gcov".into())],
    );
    assert_eq!(d.unwrap().probe, Some(KernelProbe::Gcov), "the probe rides the decision, so every door that applies one (the forge's donor path) lands it");
    // The chained walk reads the donor scope's toggles: the probe val is not a toggle.
    let flags = [crate::flags::Flag::choice("kernel.coverage", "gcov"), crate::flags::Flag::choice("kernel.toolchain", "off")];
    let t = crate::requires::toggles_in_scope(&flags, "kernel.").expect("kernel.coverage=gcov is a val, not an on|off toggle");
    assert_eq!(t.get("toolchain"), Some(&false));
    assert!(!t.contains_key("coverage"));
    assert!(crate::kernel_requires::requirements_of_consumer_line(&set, "r", "k", &["coverage=gcov".to_string()]).is_ok());
    // The tree offers it, so a `--build` line canonicalises it.
    let tree = crate::tree::SettingsTree::of_kernel_pick("k", crate::kernel::Preset::Slim, crate::kernel::HardeningProfile::Hardened);
    assert!(tree.get("coverage").is_some(), "the kernel subtree has a coverage val");
}

#[test]
fn a_probe_kernel_is_refused_by_name_for_release_and_signed_media() {
    let p = Some(KernelProbe::Gcov);
    assert!(refuse_probe(p, "k", Some("dev"), Some("dev")).is_ok());
    assert!(refuse_probe(p, "k", None, None).is_ok());
    assert!(refuse_probe(None, "k", Some("/keys/root.seed"), Some("release")).is_ok(), "a plain kernel is not judged here");
    let e = refuse_probe(p, "k", Some("dev"), Some("release")).unwrap_err().to_string();
    assert!(e.contains("[kernel-probe-release]") && e.contains("kernel.coverage=gcov"), "{e}");
    let e = refuse_probe(p, "k", Some("/keys/root.seed"), None).unwrap_err().to_string();
    assert!(e.contains("[kernel-probe-signed]") && e.contains("KORP_ROOT_SIGNING_KEY"), "{e}");
}

#[test]
fn the_gcc_major_is_read_off_the_comment_and_picks_the_layout() {
    assert_eq!(gcc_major_of_comment(b"\0GCC: (Debian 14.2.0-19) 14.2.0\0"), Some(14));
    assert_eq!(gcc_major_of_comment(b"clang version 19"), None);
    assert_eq!(GcovLayout::for_gcc(14).counters, 9);
    assert_eq!(GcovLayout::for_gcc(12).counters, 8);
    assert_eq!(GcovLayout::for_gcc(15).counters, 10);
}

#[test]
fn the_gcov_list_is_walked_out_of_a_saved_image() {
    let (img, m) = image();
    let c = walk_gcov(&img, &dump(&m)).unwrap();
    assert_eq!(c.len(), 2);
    assert_eq!(c[0].gcda, "/work/src/linux-6.12.104/fs/ext4/inode.gcda");
    assert_eq!((c[0].functions, c[0].functions_run, c[0].counters, c[0].arc_sum), (2, 1, 5, 5));
    assert!(c[0].ran());
    assert_eq!((c[1].functions, c[1].functions_run, c[1].arc_sum), (1, 0, 0));
    assert!(!c[1].ran());
}

#[test]
fn an_image_that_is_not_the_vmlinux_or_a_broken_list_is_refused() {
    let (img, mut m) = image();
    m.str(BASE + 0x100, "Linux version 6.12.105");
    let e = walk_gcov(&img, &dump(&m)).unwrap_err().to_string();
    assert!(e.contains("[kernel-coverage-image]"), "{e}");
    let (img, mut m) = image();
    m.u64(BASE + 0x2000 + 8, BASE + 0x1000);
    let e = walk_gcov(&img, &dump(&m)).unwrap_err().to_string();
    assert!(e.contains("loops"), "{e}");
    let (img, mut m) = image();
    m.u64(BASE + 0x1000 + GcovLayout::FILENAME, 0x1234);
    let e = walk_gcov(&img, &dump(&m)).unwrap_err().to_string();
    assert!(e.contains("outside the image"), "{e}");
}

#[test]
fn a_gcda_path_names_the_linked_object_by_its_longest_suffix() {
    let linked: BTreeSet<String> = ["fs/ext4/inode.o", "inode.o", "drivers/ata/libata-core.o"].iter().map(|s| s.to_string()).collect();
    assert_eq!(object_of_gcda("/work/src/linux-6.12.104/fs/ext4/inode.gcda", &linked).as_deref(), Some("fs/ext4/inode.o"));
    assert_eq!(object_of_gcda("/x/drivers/ata/libata-core.gcda", &linked).as_deref(), Some("drivers/ata/libata-core.o"));
    assert_eq!(object_of_gcda("/x/kernel/fork.gcda", &linked), None);
    assert_eq!(object_of_gcda("/x/kernel/fork.o", &linked), None);
}

#[test]
fn composite_parts_take_their_parents_gates() {
    let mk = "obj-$(CONFIG_FOO) += foo.o\nfoo-y := a.o \\\n  b.o\nfoo-$(CONFIG_FOO_EXTRA) += c.o\nobj-y += plain.o\nccflags-y += -DX\n";
    let comps = crate::kernel_trim::makefile_composites(mk);
    assert_eq!(comps, vec![("foo".to_string(), vec!["a.o".to_string(), "b.o".to_string()]), ("foo".to_string(), vec!["c.o".to_string()])]);
    let objects: BTreeMap<String, u64> = [("drivers/foo/a.o", 10u64), ("drivers/foo/c.o", 5)].iter().map(|(k, v)| (k.to_string(), *v)).collect();
    let t = TreeSizes::from_parts(&[("drivers/foo/".into(), mk.into())], objects, &|_| Some(5));
    assert_eq!(linked_gates(&t, "drivers/foo/a.o"), ["FOO".to_string()].into_iter().collect());
    assert_eq!(linked_gates(&t, "drivers/foo/c.o"), ["FOO".to_string(), "FOO_EXTRA".to_string()].into_iter().collect());
}

fn sizes() -> TreeSizes {
    let objects = [("fs/ext4/inode.o", 100u64), ("drivers/ata/libata-core.o", 200), ("kernel/printk/printk.o", 50), ("kernel/gcov/base.o", 9)]
        .iter()
        .map(|(o, b)| (o.to_string(), *b))
        .collect();
    let makefiles: Vec<(String, String)> = vec![
        ("fs/".into(), "obj-$(CONFIG_EXT4_FS) += ext4/\n".into()),
        ("drivers/".into(), "obj-$(CONFIG_ATA) += ata/\n".into()),
        ("kernel/".into(), "obj-$(CONFIG_GCOV_KERNEL) += gcov/\n".into()),
    ];
    TreeSizes::from_parts(&makefiles, objects, &|_| None)
}

fn counts(ext4: u64, ata: u64) -> Vec<ObjectCounts> {
    vec![
        ObjectCounts { gcda: "/w/linux-6.12.104/fs/ext4/inode.gcda".into(), functions: 2, functions_run: u32::from(ext4 > 0), counters: 5, arc_sum: ext4 },
        ObjectCounts { gcda: "/w/linux-6.12.104/drivers/ata/libata-core.gcda".into(), functions: 1, functions_run: u32::from(ata > 0), counters: 1, arc_sum: ata },
    ]
}

const PROBE_CONFIG: &str = "CONFIG_EXT4_FS=y\nCONFIG_ATA=y\nCONFIG_PRINTK=y\nCONFIG_GCOV_KERNEL=y\nCONFIG_NR_CPUS=64\n";

fn measured<'a>(k: &'a KernelBuild, sizes: &'a TreeSizes, snaps: &'a [Snapshot], cfg: &'a str) -> Measured<'a> {
    Measured {
        row: "node",
        kernel: k,
        fragment: "CONFIG_EXT4_FS=y\n",
        probe_config: cfg,
        sizes,
        snapshots: snaps,
        suite: "bridge",
        stages: vec![StageResult { name: "ready".into(), passed: true, detail: String::new() }],
        not_run: vec![NotRun { name: "ha".into(), why: "six VMs; not in this suite yet".into() }],
        tool: "test".into(),
    }
}

#[test]
fn counters_map_to_executed_never_and_unmeasured_symbols_with_the_stages_that_ran_them() {
    let k = KernelBuild { name: "k".into(), version: "6.12.104".into(), probe: Some(KernelProbe::Gcov), ..Default::default() };
    let t = sizes();
    let snaps = vec![
        Snapshot { node: "cp".into(), stage: "ready".into(), counts: counts(3, 0) },
        Snapshot { node: "cp".into(), stage: "checks".into(), counts: counts(3, 0) },
        Snapshot { node: "worker".into(), stage: "ready".into(), counts: counts(0, 0) },
        Snapshot { node: "worker".into(), stage: "checks".into(), counts: counts(7, 0) },
    ];
    let r = report(measured(&k, &t, &snaps, PROBE_CONFIG)).unwrap();
    let ext4 = r.symbol("CONFIG_EXT4_FS").unwrap();
    assert_eq!(ext4.status, Status::Executed);
    assert_eq!(ext4.covered_by, vec!["cp:ready".to_string(), "worker:checks".to_string()], "the stages whose counters rose, in run order");
    assert_eq!((ext4.objects, ext4.instrumented, ext4.objects_run, ext4.bytes), (1, 1, 1, 100));
    let ata = r.symbol("ATA").unwrap();
    assert_eq!(ata.status, Status::NeverExecuted);
    assert!(ata.covered_by.is_empty());
    assert_eq!(r.symbol("CONFIG_PRINTK").unwrap().status, Status::Unmeasured, "no object of its own");
    let gcov = r.symbol("CONFIG_GCOV_KERNEL").unwrap();
    assert!(gcov.probe_altered);
    assert!(r.symbol("CONFIG_NR_CPUS").is_none(), "only bool/tristate =y symbols are judged");
    assert_eq!((r.totals.symbols, r.totals.executed, r.totals.never_executed, r.totals.unmeasured), (4, 1, 1, 2));
    assert_eq!((r.totals.objects, r.totals.instrumented, r.totals.objects_run), (4, 2, 1));
    assert_eq!(r.never_run_objects, vec!["drivers/ata/libata-core.o".to_string()]);
    assert_eq!(r.fragment_sha256, nornir_hash::sha256_hex(b"CONFIG_EXT4_FS=y\n"));
    assert_eq!(r.snapshots.len(), 4);
    assert!(r.green());
    let text = r.render(5);
    assert!(text.contains("1 executed, 1 never executed, 2 unmeasured") && text.contains("not run: ha"), "{text}");
}

#[test]
fn a_probe_config_without_gcov_or_a_plain_kernel_is_refused() {
    let t = sizes();
    let snaps = vec![];
    let plain = KernelBuild { name: "k".into(), ..Default::default() };
    assert!(report(measured(&plain, &t, &snaps, PROBE_CONFIG)).unwrap_err().to_string().contains("not a probe kernel"));
    let k = KernelBuild { name: "k".into(), probe: Some(KernelProbe::Gcov), ..Default::default() };
    let e = report(measured(&k, &t, &snaps, "CONFIG_EXT4_FS=y\n")).unwrap_err().to_string();
    assert!(e.contains("[kernel-coverage-no-gcov]"), "{e}");
}

#[test]
fn a_report_is_stored_beside_the_row_and_found_only_for_its_fragment() {
    let root = tempfile::tempdir().unwrap();
    let k = KernelBuild { name: "k".into(), version: "6.12.104".into(), probe: Some(KernelProbe::Gcov), ..Default::default() };
    let t = sizes();
    let snaps = vec![Snapshot { node: "cp".into(), stage: "ready".into(), counts: counts(1, 0) }];
    let r = report(measured(&k, &t, &snaps, PROBE_CONFIG)).unwrap();
    let p = store(root.path(), &r).unwrap();
    assert!(p.starts_with(root.path().join(".nornir/kernel-coverage/node")), "{}", p.display());
    assert_eq!(load_for(root.path(), "node", "CONFIG_EXT4_FS=y\n").unwrap().unwrap(), r);
    assert!(load_for(root.path(), "node", "CONFIG_EXT4_FS=y\nCONFIG_X=y\n").unwrap().is_none(), "another fragment = another kernel = no report");
}

#[test]
fn the_protect_list_matches_globs_and_always_protects_the_probes_own_symbols() {
    let p = ProtectList::parse("# error paths\nCONFIG_*WATCHDOG* watchdogs recover a hung box\nCONFIG_BLK_DEV_NVME NVMe reset\n").unwrap();
    assert!(p.why("CONFIG_SOFT_WATCHDOG").unwrap().contains("watchdogs recover"));
    assert!(p.why("WATCHDOG_CORE").is_some(), "bare names are read as CONFIG_");
    assert!(p.why("CONFIG_BLK_DEV_NVME").is_some());
    assert!(p.why("CONFIG_BLK_DEV_NVMEX").is_none());
    assert!(p.why("CONFIG_RANDOMIZE_BASE").unwrap().contains("probe"));
    assert!(p.why("CONFIG_ATA").is_none());
    assert!(ProtectList::parse("WATCHDOG").is_err(), "not a CONFIG_ pattern");
    assert!(glob("CONFIG_A*B*C", "CONFIG_AxxBxxC") && !glob("CONFIG_A*B*C", "CONFIG_AxxCxxB"));
}