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"));
}