diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index 9b417f5..b365474 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -1,5 +1,6 @@ # The nightly: every pin in the current set on X64, built with gcc-14 and with rucc from tinyconfig -# and the test fragment, and graded by rk test on boot, smoke and KUnit. +# and the test fragment, and graded by rk test on boot, smoke, KUnit and the selftests, which are +# built with gcc-14 and with rucc. # # Both builds of a version run in one job, because rk test and rk mixed need both build directories # and the objects are too big to move between jobs. When the rucc kernel fails a unit the reference @@ -77,7 +78,7 @@ jobs: run: | sudo apt-get update -q sudo apt-get install -qy --no-install-recommends gcc-14 flex bison bc libelf-dev libssl-dev \ - cpio qemu-system-x86 busybox-static + cpio qemu-system-x86 busybox-static libcap-dev libnuma-dev rsync - name: let the runner user open /dev/kvm run: | echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' \ @@ -120,11 +121,25 @@ jobs: run: | target/release/rk build ${{ matrix.version }} --row X64 --config "$CONFIG" --fragment test \ --cc rucc --targets "bzImage modules" --out "$RUNNER_TEMP/rucc" --keep-going + - name: rk selftests with gcc-14 + run: | + target/release/rk selftests --build "$RUNNER_TEMP/gcc" --cc gcc-14 \ + --out "$RUNNER_TEMP/selftests-gcc" + - name: rk selftests with rucc + continue-on-error: true + run: | + target/release/rk selftests --build "$RUNNER_TEMP/gcc" --cc rucc \ + --out "$RUNNER_TEMP/selftests-rucc" - name: rk test id: test run: | + rucc_selftests=() + if [ -f "$RUNNER_TEMP/selftests-rucc/selftests.json" ]; then + rucc_selftests=(--rucc-selftests "$RUNNER_TEMP/selftests-rucc") + fi target/release/rk test --reference "$RUNNER_TEMP/gcc" --other "$RUNNER_TEMP/rucc" \ - --kinds boot,smoke,kunit --out "$RUNNER_TEMP/test" + --kinds boot,smoke,kunit,kselftest --selftests "$RUNNER_TEMP/selftests-gcc" \ + "${rucc_selftests[@]}" --out "$RUNNER_TEMP/test" - name: rk cross-modules if: always() && steps.test.outcome != 'skipped' continue-on-error: true @@ -138,16 +153,20 @@ jobs: unit=$(python3 -c 'import json, sys; t = json.load(open(sys.argv[1])); print(next((u["name"] for u in t["units"] if u["graded"] and not u["passed"] and u.get("attribution", "kernel") == "kernel"), ""))' "$RUNNER_TEMP/test/test.json") if [ -z "$unit" ]; then echo "no kernel failure to bisect"; exit 0; fi target/release/rk mixed --reference "$RUNNER_TEMP/gcc" --other "$RUNNER_TEMP/rucc" \ - --unit "$unit" --out "$RUNNER_TEMP/mixed" + --unit "$unit" --selftests "$RUNNER_TEMP/selftests-gcc" --out "$RUNNER_TEMP/mixed" - name: keep the record if: always() run: | - mkdir -p record/gcc record/rucc record/test record/mixed record/cross + mkdir -p record/gcc record/rucc record/test record/mixed record/cross record/selftests for side in gcc rucc; do cp "$RUNNER_TEMP/$side"/{build.json,summary.md,build.log,.config} "record/$side/" 2>/dev/null || true done cp -r "$RUNNER_TEMP/test/." record/test/ 2>/dev/null || true cp -r "$RUNNER_TEMP/cross/." record/cross/ 2>/dev/null || true + for side in gcc rucc; do + mkdir -p "record/selftests/$side" + cp -r "$RUNNER_TEMP/selftests-$side"/{selftests.json,logs} "record/selftests/$side/" 2>/dev/null || true + done cp "$RUNNER_TEMP"/mixed/{mixed.json,summary.md,relink.jsonl} "$RUNNER_TEMP"/mixed/*.log record/mixed/ 2>/dev/null || true cp -r "$RUNNER_TEMP/mixed/fuel" record/mixed/ 2>/dev/null || true find record/mixed/fuel -name '*.o' -delete 2>/dev/null || true diff --git a/README.md b/README.md index bae40bb..a4a0645 100644 --- a/README.md +++ b/README.md @@ -50,10 +50,12 @@ The reference compilers live in era containers built from `provision/eras` and p `rk boot --build DIR` boots a build under QEMU with `rk-init/init.sh` as PID 1 and a static busybox, and reads the smoke checks off the serial console. `rk baseline` builds and boots with the reference three times and writes the result under `results/baseline`. -`rk test --reference DIR --other DIR --kinds boot,smoke,kunit` is the graded run. It boots the reference kernel and the rucc kernel once per suite, with `rk.suite=` on the command line, and compares them unit by unit: `boot`, each smoke check, each KUnit result read from the TAP on the console, and each test module loading. The kunit boot carries the build's modules, so build with `--targets "bzImage modules"` when the test fragment makes KUnit tests modules. The units the reference passes in every run (`--runs`) are graded and the rucc kernel must pass all of them. The splats in both consoles are normalized, and one that only the rucc kernel prints fails the run too. Given a rucc-built busybox with `--rucc-busybox`, every failure is attributed to the kernel, the userland or the two together by running the other two cells of the 2x2. kselftest and LTP are refused until K5. The run directory gets `test.json`, `summary.md` and the console of every boot. +`rk test --reference DIR --other DIR --kinds boot,smoke,kunit` is the graded run. It boots the reference kernel and the rucc kernel once per suite, with `rk.suite=` on the command line, and compares them unit by unit: `boot`, each smoke check, each KUnit result read from the TAP on the console, and each test module loading. The kunit boot carries the build's modules, so build with `--targets "bzImage modules"` when the test fragment makes KUnit tests modules. The units the reference passes in every run (`--runs`) are graded and the rucc kernel must pass all of them. The splats in both consoles are normalized, and one that only the rucc kernel prints fails the run too. Given a rucc-built busybox with `--rucc-busybox`, every failure is attributed to the kernel, the userland or the two together by running the other two cells of the 2x2. With `--kinds kselftest` and `--selftests DIR` from `rk selftests`, every pinned collection is a boot of its own, and each program `run_kselftest.sh` reports is a unit, `kselftest::`. A rucc-built set passed with `--rucc-selftests` counts as part of the rucc userland for attribution. LTP is not run yet. The run directory gets `test.json`, `summary.md` and the console of every boot. `rk mixed --reference DIR --other DIR --unit UNIT` finds what makes one unit that `rk test` failed go wrong. Every object the two builds have with different bytes is a candidate. A trial copies the reference build, puts some rucc objects in place of the reference's, relinks with the reference's own make command so that nothing is compiled again, and boots the suite that reports the unit. Delta debugging finds a smallest set of rucc objects that still fails. When one object is left and the other build is rucc's, the object is compiled again with `-fpass-fuel-global=N` and the fuel is bisected to the first transformation that breaks the unit, and the `-fdump-ir=all` dumps on each side of it name the pass and the functions it changed. A trial that does not link is a finding of its own and ends the search. The run directory gets `mixed.json`, `summary.md`, the console of every boot and the dumps. +`rk selftests --build DIR --cc COMPILER` builds the kselftest collections that `rows.toml` pins for the build's row, from the build's own kernel tree and against its exported headers, and installs them the way `run_kselftest.sh` expects. Each collection is built on its own with `make -k`, so one program that needs a library the machine lacks leaves the rest standing, and the programs that did not build are listed in `selftests.json`. The programs are linked dynamically, because static linking breaks real collections, so the initramfs that `rk test` makes for a collection carries the interpreter and the shared libraries its programs need. `--collections` picks others, and the output goes to `selftests` in the build directory unless `--out` says otherwise. + `rk cross-modules --reference DIR --other DIR` loads the rucc modules into the reference kernel and the reference modules into the rucc kernel. It first checks every module against the other kernel without booting: the relocation types, the whole `vermagic`, symbols the kernel does not export and CRCs that differ from its `Module.symvers`. Then it runs the kunit suite three times per run: the reference kernel with its own modules, which finds the units that pass in QEMU without hardware, and then each kernel with the other build's modules. Both crossed boots must pass every unit the first one passed, and a splat only a crossed boot prints fails the run. Both builds need `--targets "bzImage modules"`. The run directory gets `cross-modules.json`, `summary.md` and the console of every boot. `rk config-diff`, `rk probes`, `rk flags-diff`, `rk demands` and `rk asm-inventory` read build directories and write markdown tables: the `.config` differences, the compiler probes the two compilers answered differently, the flags each unit was compiled with on one side only (read from the `.cmd` files kbuild writes), the failed units by error, and the instructions the kernel writes itself. `rk config-diff --why` also names the Kconfig expressions behind each difference and the probe that decided them, and needs the kernel tree, which it finds through `build.json` or `--source`. `rk syntax --build DIR --cc rucc` replays every unit of a reference build through rucc's front end with the reference's own command lines, `-fsyntax-only` for C and `-E` for assembly, and writes the failing units grouped by their first error. Units listed with an issue in `syntax-known.toml` are known failures, so the command fails only on new ones. diff --git a/crates/rk/src/boot.rs b/crates/rk/src/boot.rs index ae41db4..fd63a6e 100644 --- a/crates/rk/src/boot.rs +++ b/crates/rk/src/boot.rs @@ -72,7 +72,8 @@ pub fn initramfs(busybox: &[u8]) -> Vec { } /// The initramfs for a busybox binary with more files, each given by its path inside and its -/// bytes. The directories they need are made first. +/// bytes. The directories they need are made first. An ELF file or a script can be run, and +/// anything else is only read. #[must_use] pub fn initramfs_with(busybox: &[u8], files: &[(String, Vec)]) -> Vec { let base = ["bin", "dev", "proc", "sys", "tmp"]; @@ -143,7 +144,11 @@ pub fn initramfs_with(busybox: &[u8], files: &[(String, Vec)]) -> Vec { })); entries.extend(files.iter().map(|(path, data)| Entry { name: path, - mode: 0o100_644, + mode: if data.starts_with(b"\x7fELF") || data.starts_with(b"#!") { + 0o100_755 + } else { + 0o100_644 + }, data, rdev: (0, 0), })); diff --git a/crates/rk/src/build.rs b/crates/rk/src/build.rs index ee3d192..321f98f 100644 --- a/crates/rk/src/build.rs +++ b/crates/rk/src/build.rs @@ -406,7 +406,7 @@ pub fn make_in( } /// Whether the machine we run on is the row's architecture. -fn host_is(arch: &str) -> bool { +pub fn host_is(arch: &str) -> bool { matches!( (std::env::consts::ARCH, arch), ("x86_64", "x86_64" | "i386" | "x86") | ("aarch64", "arm64") | ("riscv64", "riscv") diff --git a/crates/rk/src/cli.rs b/crates/rk/src/cli.rs index e86be90..64a5153 100644 --- a/crates/rk/src/cli.rs +++ b/crates/rk/src/cli.rs @@ -79,11 +79,14 @@ fn accepted(command: &str) -> Option<(bool, &'static [&'static str])> { "kinds", "busybox", "rucc-busybox", + "selftests", + "rucc-selftests", "runs", "timeout", "out", ], ), + "selftests" => (false, &["build", "cc", "collections", "jobs", "out"]), "mixed" => ( false, &[ @@ -92,6 +95,7 @@ fn accepted(command: &str) -> Option<(bool, &'static [&'static str])> { "row", "unit", "busybox", + "selftests", "timeout", "jobs", "out", @@ -153,11 +157,14 @@ usage: rk syntax --build DIR [--cc rucc] [--allow FILE] [--jobs N] rk boot --build DIR [--row X64] [--initramfs FILE | --busybox PATH] [--timeout 300] [--append WORDS] - rk test --reference DIR --other DIR [--kinds boot,smoke,kunit] [--row X64] [--runs 1] + rk test --reference DIR --other DIR [--kinds boot,smoke,kunit,kselftest] [--row X64] [--runs 1] + [--selftests DIR] [--rucc-selftests DIR] rk mixed --reference DIR --other DIR --unit UNIT [--row X64] [--timeout 600] [--no-fuel] + [--selftests DIR] [--busybox PATH] [--rucc-busybox PATH] [--timeout 600] [--out DIR] rk cross-modules --reference DIR --other DIR [--row X64] [--runs 1] [--timeout 600] [--busybox PATH] [--out DIR] + rk selftests --build DIR [--cc COMPILER] [--collections timers,size] [--jobs N] [--out DIR] rk initramfs --out FILE [--busybox PATH] rk baseline [VERSION] --cc COMPILER [--row X64] [--config defconfig] [--fragment test] [--runs 3] [--timeout 600] [--stack-usage] diff --git a/crates/rk/src/main.rs b/crates/rk/src/main.rs index 251d9ab..0d5adff 100644 --- a/crates/rk/src/main.rs +++ b/crates/rk/src/main.rs @@ -22,6 +22,7 @@ mod pins; mod probes; mod repo; mod sections; +mod selftests; mod sets; mod symvers; mod syntax; @@ -70,6 +71,7 @@ fn main() -> ExitCode { "test" => test_command(&repo, &args), "mixed" => mixed_command(&repo, &args), "cross-modules" => cross_modules(&repo, &args), + "selftests" => selftests_command(&repo, &args), "asm-inventory" => asm_inventory(&repo, &args), "baseline" => baseline_command(&repo, &args), "initramfs" => initramfs_command(&args).map(|_| ExitCode::SUCCESS), @@ -757,6 +759,19 @@ fn test_command(repo: &Repo, args: &Args) -> Result { std::fs::canonicalize(path).map_err(|e| format!("resolving {path}: {e}")) }; let kinds = testrun::parse_kinds(args.get("kinds").unwrap_or("boot,smoke,kunit"))?; + let selftests = args + .get("selftests") + .map(|_| dir("selftests")) + .transpose()?; + let rucc_selftests = args + .get("rucc-selftests") + .map(|_| dir("rucc-selftests")) + .transpose()?; + if kinds.contains(&testrun::Kind::Kselftest) && selftests.is_none() { + return Err( + "--kinds kselftest needs --selftests, a directory rk selftests wrote".to_string(), + ); + } let reference = dir("reference")?; let other = dir("other")?; let built = outcome_of(&other).ok(); @@ -799,6 +814,8 @@ fn test_command(repo: &Repo, args: &Args) -> Result { .get("rucc-busybox") .map(|p| read_busybox(Some(p))) .transpose()?, + selftests, + rucc_selftests, kinds, runs, timeout: number("timeout", 600)?, @@ -812,6 +829,67 @@ fn test_command(repo: &Repo, args: &Args) -> Result { Ok(verdict(outcome.passed())) } +/// Build the selftests of a build's tree with a compiler, for the initramfs. +fn selftests_command(repo: &Repo, args: &Args) -> Result { + let path = args + .get("build") + .ok_or("rk selftests needs --build, a build directory")?; + let dir = std::fs::canonicalize(path).map_err(|e| format!("resolving {path}: {e}"))?; + let made = outcome_of(&dir)?; + let rows = personas::Rows::load(&repo.file("rows.toml"))?; + let row = rows.get(&made.row)?.clone(); + let collections: Vec = match args.get("collections") { + Some(list) => list + .split(',') + .map(str::trim) + .filter(|c| !c.is_empty()) + .map(str::to_string) + .collect(), + None => row.selftests.clone(), + }; + if collections.is_empty() { + return Err(format!( + "row {} pins no selftests in rows.toml; pass --collections", + row.name + )); + } + let cc = args + .get("cc") + .map_or_else(|| made.compiler.path.display().to_string(), str::to_string); + let jobs = match args.get("jobs") { + Some(n) => n + .parse() + .map_err(|_| format!("--jobs {n} is not a number"))?, + None => std::thread::available_parallelism().map_or(4, std::num::NonZero::get), + }; + let out = args + .get("out") + .map_or_else(|| dir.join("selftests"), std::path::PathBuf::from); + std::fs::create_dir_all(&out).map_err(|e| format!("creating {}: {e}", out.display()))?; + let out = + std::fs::canonicalize(&out).map_err(|e| format!("resolving {}: {e}", out.display()))?; + let plan = selftests::Plan { + source: made.source.clone(), + build: dir, + arch: row.arch.clone(), + cross: if build::host_is(&row.arch) { + String::new() + } else { + row.cross.clone() + }, + cc, + collections, + jobs, + out, + }; + let outcome = selftests::run(&plan)?; + let summary = selftests::summary(&outcome); + print!("{summary}"); + step_summary(&summary); + println!("wrote {}", plan.out.join("selftests.json").display()); + Ok(verdict(!outcome.runnable().is_empty())) +} + /// Load each build's modules into the other build's kernel and run their KUnit suites. fn cross_modules(repo: &Repo, args: &Args) -> Result { let dir = |name: &str| -> Result { @@ -915,6 +993,10 @@ fn mixed_command(repo: &Repo, args: &Args) -> Result { row, unit, busybox: read_busybox(args.get("busybox"))?, + selftests: args + .get("selftests") + .map(|p| std::fs::canonicalize(p).map_err(|e| format!("resolving {p}: {e}"))) + .transpose()?, timeout: number("timeout", 600)?, jobs, fuel: !args.has("no-fuel"), diff --git a/crates/rk/src/mixed.rs b/crates/rk/src/mixed.rs index 2377980..5be2760 100644 --- a/crates/rk/src/mixed.rs +++ b/crates/rk/src/mixed.rs @@ -286,10 +286,17 @@ pub fn recompile_args(record: &CompileRecord, object: &Path, extra: &[String]) - /// The suite a unit is reported by. #[must_use] -pub fn suite_of(unit: &str) -> &'static str { +pub fn suite_of(unit: &str) -> String { match testrun::kind_of(unit) { - Kind::Kunit => "kunit", - Kind::Boot | Kind::Smoke => "smoke", + Kind::Kunit => "kunit".to_string(), + Kind::Boot | Kind::Smoke => "smoke".to_string(), + Kind::Kselftest => { + let collection = unit + .strip_prefix("kselftest:") + .and_then(|rest| rest.split(':').next()) + .unwrap_or_default(); + format!("kselftest:{collection}") + } } } @@ -306,6 +313,8 @@ pub struct Plan { pub unit: String, /// The userland, a static busybox. pub busybox: Vec, + /// The selftests, an `rk selftests` directory, for a `kselftest:` unit. + pub selftests: Option, /// Seconds each boot may take. pub timeout: u64, /// Parallel jobs for the relink. @@ -522,6 +531,13 @@ impl Trials<'_> { let suite = suite_of(&self.plan.unit); let files = if suite == "kunit" { testrun::modules(&self.tree)? + } else if let Some(collection) = suite.strip_prefix("kselftest:") { + let dir = self + .plan + .selftests + .as_deref() + .ok_or("a kselftest unit needs --selftests, an rk selftests directory")?; + crate::selftests::files(dir, collection)? } else { Vec::new() }; @@ -540,7 +556,7 @@ impl Trials<'_> { let outcome = outcome?; let console = std::fs::read_to_string(stem.with_extension("log")).unwrap_or_default(); let kind = testrun::kind_of(&self.plan.unit); - let units = testrun::units_of(suite, &[kind], &outcome, &console); + let units = testrun::units_of(&suite, &[kind], &outcome, &console); Ok(units.get(&self.plan.unit) != Some(&Status::Pass)) } } @@ -1010,6 +1026,10 @@ mod tests { assert_eq!(suite_of("smoke:proc"), "smoke"); assert_eq!(suite_of("kunit:list.list_add"), "kunit"); assert_eq!(suite_of("kunit-module:lib/test_list.ko"), "kunit"); + assert_eq!( + suite_of("kselftest:timers:posix_timers"), + "kselftest:timers" + ); } #[test] diff --git a/crates/rk/src/personas.rs b/crates/rk/src/personas.rs index 1957365..2a7858f 100644 --- a/crates/rk/src/personas.rs +++ b/crates/rk/src/personas.rs @@ -164,6 +164,9 @@ pub struct Row { pub cpu: String, /// The serial console device name. pub console: String, + /// The kselftest collections pinned for the row. + #[serde(default)] + pub selftests: Vec, } impl Rows { diff --git a/crates/rk/src/selftests.rs b/crates/rk/src/selftests.rs new file mode 100644 index 0000000..f826ad6 --- /dev/null +++ b/crates/rk/src/selftests.rs @@ -0,0 +1,483 @@ +//! `rk selftests`: the kernel's selftests built as user programs, for the initramfs (plan 11.5). +//! +//! The collections pinned for a row in `rows.toml` are built from the kernel tree of a build +//! directory, with the compiler asked for and the build's own exported headers, and installed the +//! way `run_kselftest.sh` expects. Each collection is built on its own with `make -k`, so that one +//! program that does not build, often for a library the machine lacks, leaves the rest of its +//! collection standing. The install runs with `make -i` for the same reason. A program listed in +//! `kselftest-list.txt` that is not in the install is recorded as not built, and it stays in the +//! list, so a boot reports it as failed rather than leaving it out. +//! +//! The programs are linked dynamically. Linking them statically breaks real collections (`timers` +//! defines its own `clock_adjtime`, which clashes with the one in `libc.a`), so the initramfs +//! carries the shared libraries each program needs instead, found the way the dynamic linker +//! would find them on the machine that built them. +//! +//! What comes out is the install, `selftests.json` and a build log per collection. + +use object::Endianness; +use object::read::elf::{ElfFile64, FileHeader as _, ProgramHeader as _}; +use serde::{Deserialize, Serialize}; +use std::collections::{BTreeMap, BTreeSet}; +use std::fmt::Write as _; +use std::path::{Path, PathBuf}; +use std::process::Command; + +/// Where the install goes inside the initramfs. +pub const ROOT: &str = "kselftest"; + +/// One collection's build. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "kebab-case")] +pub struct Collection { + /// Its name, the directory under `tools/testing/selftests`. + pub name: String, + /// The programs `kselftest-list.txt` names for it. + pub programs: Vec, + /// The ones that were not built. + pub missing: Vec, +} + +/// What `rk selftests` did, written to `selftests.json` in the output directory. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "kebab-case")] +pub struct Outcome { + /// The kernel tree. + pub source: PathBuf, + /// The compiler. + pub cc: String, + /// Every collection asked for, in the order asked. + pub collections: Vec, +} + +impl Outcome { + /// Read `selftests.json` from an output directory. + pub fn load(dir: &Path) -> Result { + let path = dir.join("selftests.json"); + let text = std::fs::read_to_string(&path) + .map_err(|e| format!("reading {}: {e}; run rk selftests first", path.display()))?; + serde_json::from_str(&text).map_err(|e| format!("reading {}: {e}", path.display())) + } + + /// The names of the collections that have at least one program built. + #[must_use] + pub fn runnable(&self) -> Vec { + self.collections + .iter() + .filter(|c| c.missing.len() < c.programs.len()) + .map(|c| c.name.clone()) + .collect() + } +} + +/// Everything `rk selftests` was asked to do. +#[derive(Debug, Clone)] +pub struct Plan { + /// The kernel tree. + pub source: PathBuf, + /// The kernel build directory, whose `usr/include` the programs are built against. + pub build: PathBuf, + /// kbuild's `ARCH`. + pub arch: String, + /// `CROSS_COMPILE`, empty for a native build. + pub cross: String, + /// The compiler, as `CC`. + pub cc: String, + /// The collections. + pub collections: Vec, + /// Parallel jobs. + pub jobs: usize, + /// The output directory. + pub out: PathBuf, +} + +/// The make command in the selftests directory, with what every step shares. +fn make(plan: &Plan, collections: &[String]) -> Command { + let mut command = Command::new("make"); + command + .arg("-C") + .arg(plan.source.join("tools/testing/selftests")) + .arg(format!("O={}", plan.out.join("build").display())) + .arg(format!("ARCH={}", plan.arch)) + .arg(format!("CC={}", plan.cc)) + .arg(format!( + "KHDR_INCLUDES=-isystem {}", + plan.build.join("usr/include").display() + )) + .arg(format!("TARGETS={}", collections.join(" "))); + if !plan.cross.is_empty() { + command.arg(format!("CROSS_COMPILE={}", plan.cross)); + } + command +} + +/// Run a command with its output going to a log file. Whether it succeeded is not the question, +/// since `-k` and `-i` keep going: what got installed is. +fn logged(mut command: Command, log: &Path) -> Result<(), String> { + let file = + std::fs::File::create(log).map_err(|e| format!("creating {}: {e}", log.display()))?; + let err = file + .try_clone() + .map_err(|e| format!("opening {}: {e}", log.display()))?; + command + .stdout(file) + .stderr(err) + .status() + .map_err(|e| format!("running make: {e}"))?; + Ok(()) +} + +/// Read `kselftest-list.txt`, `collection:program` per line, into the programs of each. +#[must_use] +pub fn parse_list(text: &str) -> BTreeMap> { + let mut out: BTreeMap> = BTreeMap::new(); + for line in text.lines().map(str::trim) { + if let Some((collection, program)) = line.split_once(':') { + out.entry(collection.to_string()) + .or_default() + .push(program.to_string()); + } + } + out +} + +/// Export the headers, build every collection, install them, and write `selftests.json`. +pub fn run(plan: &Plan) -> Result { + let logs = plan.out.join("logs"); + std::fs::create_dir_all(&logs).map_err(|e| format!("creating {}: {e}", logs.display()))?; + let mut headers = Command::new("make"); + headers + .arg("-C") + .arg(&plan.source) + .arg(format!("O={}", plan.build.display())) + .arg(format!("ARCH={}", plan.arch)) + .arg("headers"); + logged(headers, &logs.join("headers.log"))?; + for collection in &plan.collections { + eprintln!("rk: selftests {collection}"); + let mut command = make(plan, std::slice::from_ref(collection)); + command.arg("-k").arg(format!("-j{}", plan.jobs)); + logged(command, &logs.join(format!("{collection}.log")))?; + } + let install = plan.out.join(ROOT); + let _ = std::fs::remove_dir_all(&install); + let mut command = make(plan, &plan.collections); + command + .arg("-i") + .arg(format!("INSTALL_PATH={}", install.display())) + .arg("install"); + logged(command, &logs.join("install.log"))?; + let list = std::fs::read_to_string(install.join("kselftest-list.txt")).unwrap_or_default(); + let listed = parse_list(&list); + let collections = plan + .collections + .iter() + .map(|name| { + let programs = listed.get(name).cloned().unwrap_or_default(); + let missing = programs + .iter() + .filter(|p| !install.join(name).join(p).is_file()) + .cloned() + .collect(); + Collection { + name: name.clone(), + programs, + missing, + } + }) + .collect(); + let outcome = Outcome { + source: plan.source.clone(), + cc: plan.cc.clone(), + collections, + }; + let json = serde_json::to_string_pretty(&outcome).unwrap_or_default(); + std::fs::write(plan.out.join("selftests.json"), json + "\n") + .map_err(|e| format!("writing selftests.json: {e}"))?; + Ok(outcome) +} + +/// The outcome as markdown. +#[must_use] +pub fn summary(o: &Outcome) -> String { + let mut s = + String::from("### rk selftests\n\n| collection | programs | not built |\n|---|---|---|\n"); + for c in &o.collections { + let _ = writeln!( + s, + "| {} | {} | {} |", + c.name, + c.programs.len(), + if c.missing.is_empty() { + String::new() + } else { + c.missing.join(", ") + } + ); + } + let empty: Vec<&str> = o + .collections + .iter() + .filter(|c| c.programs.is_empty()) + .map(|c| c.name.as_str()) + .collect(); + if !empty.is_empty() { + let _ = writeln!( + s, + "\n{} list no programs, so their directories may not exist in this tree.", + empty.join(", ") + ); + } + s +} + +/// The program interpreter and the libraries an ELF file names, or nothing for a file that is +/// not a 64-bit ELF. +fn needs(data: &[u8]) -> (Option, Vec) { + let Ok(file) = ElfFile64::::parse(data) else { + return (None, Vec::new()); + }; + let endian = file.endian(); + let interp = file + .elf_header() + .program_headers(endian, data) + .ok() + .and_then(|headers| { + headers + .iter() + .find_map(|h| h.interpreter(endian, data).ok().flatten()) + }) + .map(|bytes| String::from_utf8_lossy(bytes).into_owned()); + (interp, needed(data).unwrap_or_default()) +} + +/// The `DT_NEEDED` names of an ELF file. +fn needed(data: &[u8]) -> Option> { + use object::read::elf::{Dyn as _, SectionHeader as _}; + let file = ElfFile64::::parse(data).ok()?; + let endian = file.endian(); + let sections = file.elf_header().sections(endian, data).ok()?; + let dynamic = sections + .iter() + .find(|s| s.sh_type(endian) == object::elf::SHT_DYNAMIC)?; + let entries = dynamic.dynamic(endian, data).ok()??.0; + let strings = sections + .strings( + endian, + data, + object::SectionIndex(dynamic.sh_link(endian) as usize), + ) + .ok()?; + Some( + entries + .iter() + .filter(|d| d.tag32(endian).map(i64::from) == Some(object::elf::DT_NEEDED.0)) + .filter_map(|d| d.string(endian, strings).ok()) + .map(|name| String::from_utf8_lossy(name).into_owned()) + .collect(), + ) +} + +/// Where the dynamic linker would look for a library on this machine. +const LIBRARY_DIRS: &[&str] = &[ + "/lib/x86_64-linux-gnu", + "/usr/lib/x86_64-linux-gnu", + "/lib/aarch64-linux-gnu", + "/usr/lib/aarch64-linux-gnu", + "/usr/aarch64-linux-gnu/lib", + "/lib64", + "/usr/lib64", + "/lib", + "/usr/lib", +]; + +/// A file read by its path inside the initramfs, which is its path on this machine. +fn host_file(path: &str) -> Option<(String, Vec)> { + let data = std::fs::read(path).ok()?; + Some((path.trim_start_matches('/').to_string(), data)) +} + +/// The interpreter and the shared libraries a set of programs needs, followed through the +/// libraries' own needs, as initramfs files at the paths they have here. +#[must_use] +pub fn libraries<'a>(programs: impl IntoIterator) -> Vec<(String, Vec)> { + let mut seen: BTreeSet = BTreeSet::new(); + let mut out = Vec::new(); + let mut queue: Vec = Vec::new(); + let mut take = |data: &[u8], queue: &mut Vec, out: &mut Vec<(String, Vec)>| { + let (interp, libs) = needs(data); + if let Some(interp) = interp + && seen.insert(interp.clone()) + && let Some(file) = host_file(&interp) + { + out.push(file); + } + for lib in libs { + if seen.insert(lib.clone()) { + queue.push(lib); + } + } + }; + for data in programs { + take(data, &mut queue, &mut out); + } + while let Some(lib) = queue.pop() { + let Some(path) = LIBRARY_DIRS + .iter() + .map(|dir| format!("{dir}/{lib}")) + .find(|p| Path::new(p).is_file()) + else { + continue; + }; + if let Some(file) = host_file(&path) { + take(&file.1, &mut queue, &mut out); + out.push(file); + } + } + out +} + +/// Every file under a directory, by its path relative to `base`. +fn walk(dir: &Path, base: &Path, out: &mut Vec<(String, Vec)>) -> Result<(), String> { + let entries = std::fs::read_dir(dir).map_err(|e| format!("reading {}: {e}", dir.display()))?; + let mut paths: Vec = entries.filter_map(|e| e.ok().map(|e| e.path())).collect(); + paths.sort(); + for path in paths { + if path.is_dir() { + walk(&path, base, out)?; + } else if path.is_file() { + let data = + std::fs::read(&path).map_err(|e| format!("reading {}: {e}", path.display()))?; + let name = path + .strip_prefix(base) + .map_err(|e| e.to_string())? + .display() + .to_string(); + out.push((name, data)); + } + } + Ok(()) +} + +/// The initramfs files for one collection: the runner, the list, the collection's install and +/// the libraries its programs need. +pub fn files(out: &Path, collection: &str) -> Result)>, String> { + let install = out.join(ROOT); + let mut files = Vec::new(); + for name in ["run_kselftest.sh", "kselftest-list.txt"] { + let path = install.join(name); + let data = std::fs::read(&path).map_err(|e| format!("reading {}: {e}", path.display()))?; + files.push((format!("{ROOT}/{name}"), data)); + } + let mut inside = Vec::new(); + walk(&install.join("kselftest"), &install, &mut inside)?; + let dir = install.join(collection); + if dir.is_dir() { + walk(&dir, &install, &mut inside)?; + } + let libs = libraries(inside.iter().map(|(_, data)| data.as_slice())); + files.extend( + inside + .into_iter() + .map(|(name, data)| (format!("{ROOT}/{name}"), data)), + ); + files.extend(libs); + Ok(files) +} + +/// The results `run_kselftest.sh` printed for one collection, by program, as +/// `kselftest::`. The programs' own TAP comes behind `# ` and is not read. +#[must_use] +pub fn units(console: &str) -> BTreeMap { + let mut out = BTreeMap::new(); + for line in console.lines() { + let text = crate::boot::strip_timestamp(line.trim_end_matches('\r')).trim(); + let Some((name, status)) = crate::tap::parse_result(text) else { + continue; + }; + let Some(rest) = name.strip_prefix("selftests: ") else { + continue; + }; + if let Some((collection, program)) = rest.split_once(": ") { + out.insert( + format!("kselftest:{}:{}", collection.trim(), program.trim()), + status, + ); + } + } + out +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::tap::Status; + + #[test] + fn the_list_gives_each_collection_its_programs() { + let list = parse_list("timers:posix_timers\ntimers:nanosleep\nsize:get_size\n\n"); + assert_eq!(list["timers"], ["posix_timers", "nanosleep"]); + assert_eq!(list["size"], ["get_size"]); + } + + #[test] + fn a_collection_runs_when_one_of_its_programs_was_built() { + let o = Outcome { + source: PathBuf::new(), + cc: "gcc".to_string(), + collections: vec![ + Collection { + name: "timers".to_string(), + programs: vec!["a".to_string(), "b".to_string()], + missing: vec!["b".to_string()], + }, + Collection { + name: "clone3".to_string(), + programs: vec!["clone3".to_string()], + missing: vec!["clone3".to_string()], + }, + Collection { + name: "gone".to_string(), + programs: Vec::new(), + missing: Vec::new(), + }, + ], + }; + assert_eq!(o.runnable(), ["timers"]); + let text = summary(&o); + assert!(text.contains("| clone3 | 1 | clone3 |"), "{text}"); + assert!(text.contains("gone list no programs"), "{text}"); + } + + #[test] + fn results_are_read_per_program_and_the_programs_own_tap_is_left_alone() { + let console = "TAP version 13\n1..3\n# timeout set to 45\n# selftests: timers: posix_timers\n# ok 1 Check itimer virtual... [OK]\n# not ok 2 inner\nok 1 selftests: timers: posix_timers\n[ 3.100000] random: crng init done\nnot ok 2 selftests: timers: nanosleep # exit=1\nok 3 selftests: timers: rtcpie # SKIP\n"; + let units = units(console); + assert_eq!(units.len(), 3, "{units:?}"); + assert_eq!(units["kselftest:timers:posix_timers"], Status::Pass); + assert_eq!(units["kselftest:timers:nanosleep"], Status::Fail); + assert_eq!(units["kselftest:timers:rtcpie"], Status::Skip); + } + + #[test] + fn a_file_that_is_not_elf_needs_nothing() { + assert_eq!(needs(b"#!/bin/sh\necho hi\n"), (None, Vec::new())); + assert!(libraries([&b"#!/bin/sh\n"[..]]).is_empty()); + } + + #[cfg(all(target_os = "linux", target_arch = "x86_64"))] + #[test] + fn a_dynamic_program_brings_its_interpreter_and_libc() { + let Ok(data) = std::fs::read("/bin/sh") else { + return; + }; + let (interp, _) = needs(&data); + if interp.is_none() { + return; + } + let libs = libraries([data.as_slice()]); + let names: Vec<&str> = libs.iter().map(|(n, _)| n.as_str()).collect(); + assert!(names.iter().any(|n| n.contains("ld-linux")), "{names:?}"); + assert!(names.iter().any(|n| n.contains("libc.so")), "{names:?}"); + } +} diff --git a/crates/rk/src/testrun.rs b/crates/rk/src/testrun.rs index 7398fbb..a310818 100644 --- a/crates/rk/src/testrun.rs +++ b/crates/rk/src/testrun.rs @@ -2,7 +2,7 @@ //! same suites on both and compares them unit by unit (11.7 and 11.9 in the plan). //! //! A unit is one thing that passes or fails: `boot` for the kernel reaching the end of init with -//! no panic, `smoke:` for each smoke check, `kunit:.` for each KUnit result +//! no panic, `smoke:` for each smoke check, `kunit:.` for each KUnit result, //! and `kunit-module:` for each test module loading. Every suite is its own boot, with //! `rk.suite=` on the kernel command line, and the kunit boot carries the build's modules in its //! initramfs. The units the reference passes in every run are the graded set, and the rucc kernel @@ -20,7 +20,10 @@ //! miscompilation. One that passes both ways is an interaction. Until rucc builds the userland //! both kernels run the same busybox, so every failure is the kernel's. //! -//! kselftest and LTP come with K5 and are refused for now. +//! The selftests are units too, `kselftest::`, one boot per collection, with +//! the programs `rk selftests` built put under `/kselftest` in the initramfs with the libraries +//! they need. They are user programs, so a rucc-built set passed with `--rucc-selftests` is part +//! of the rucc userland for attribution, the way `--rucc-busybox` is. LTP is refused for now. //! //! What comes out is `test.json` and `summary.md` in the run directory, next to the console of //! every boot. @@ -43,6 +46,21 @@ pub enum Kind { Smoke, /// KUnit. Kunit, + /// The kernel's selftests, one boot per collection. + Kselftest, +} + +impl Kind { + /// The word `--kinds` takes for it. + #[must_use] + pub fn word(self) -> &'static str { + match self { + Self::Boot => "boot", + Self::Smoke => "smoke", + Self::Kunit => "kunit", + Self::Kselftest => "kselftest", + } + } } /// Read `--kinds`, a comma separated list. @@ -53,14 +71,15 @@ pub fn parse_kinds(text: &str) -> Result, String> { "boot" => Kind::Boot, "smoke" => Kind::Smoke, "kunit" => Kind::Kunit, - "kselftest" | "ltp" => { - return Err(format!( - "{word} comes with K5, when rucc builds the test programs; rk test runs boot, smoke and kunit for now" - )); + "kselftest" => Kind::Kselftest, + "ltp" => { + return Err( + "ltp is not run yet; rk test runs boot, smoke, kunit and kselftest".to_string(), + ); } other => { return Err(format!( - "unknown test kind {other}; the kinds are boot, smoke and kunit" + "unknown test kind {other}; the kinds are boot, smoke, kunit and kselftest" )); } }; @@ -75,15 +94,19 @@ pub fn parse_kinds(text: &str) -> Result, String> { Ok(kinds) } -/// The suites, each one boot, that the kinds need. +/// The suites, each one boot, that the kinds need, with one `kselftest:` suite for +/// each selftest collection. #[must_use] -pub fn suites(kinds: &[Kind]) -> Vec<&'static str> { +pub fn suites(kinds: &[Kind], collections: &[String]) -> Vec { let mut out = Vec::new(); if kinds.contains(&Kind::Boot) || kinds.contains(&Kind::Smoke) { - out.push("smoke"); + out.push("smoke".to_string()); } if kinds.contains(&Kind::Kunit) { - out.push("kunit"); + out.push("kunit".to_string()); + } + if kinds.contains(&Kind::Kselftest) { + out.extend(collections.iter().map(|c| format!("kselftest:{c}"))); } out } @@ -93,6 +116,8 @@ pub fn suites(kinds: &[Kind]) -> Vec<&'static str> { pub fn kind_of(unit: &str) -> Kind { if unit.starts_with("smoke:") { Kind::Smoke + } else if unit.starts_with("kselftest:") { + Kind::Kselftest } else if unit.starts_with("kunit") { Kind::Kunit } else { @@ -148,6 +173,9 @@ pub fn units_of(suite: &str, kinds: &[Kind], outcome: &boot::Outcome, console: & } } } + if suite.starts_with("kselftest:") { + units.extend(crate::selftests::units(console)); + } units.retain(|unit, _| kinds.contains(&kind_of(unit))); units } @@ -237,6 +265,10 @@ pub struct Plan { pub busybox: Vec, /// The rucc userland, when there is one. Without it both kernels run `busybox`. pub rucc_busybox: Option>, + /// The GCC-built selftests, an `rk selftests` output directory. + pub selftests: Option, + /// The rucc-built selftests. Without them both kernels run `selftests`. + pub rucc_selftests: Option, /// The kinds of test. pub kinds: Vec, /// How many times each kernel runs every suite. @@ -277,20 +309,59 @@ pub fn modules(build: &Path) -> Result)>, String> { Ok(files) } +impl Plan { + /// The GCC userland and the rucc one, which is the GCC one where nothing else was given. + fn userlands(&self) -> (Userland<'_>, Userland<'_>) { + let gcc = Userland { + busybox: &self.busybox, + selftests: self.selftests.as_deref(), + }; + let rucc = Userland { + busybox: self.rucc_busybox.as_deref().unwrap_or(&self.busybox), + selftests: self.rucc_selftests.as_deref().or(gcc.selftests), + }; + (gcc, rucc) + } +} + +/// The kind of the units a suite reports, apart from `boot`, which every suite reports. +#[must_use] +pub fn suite_kind(suite: &str) -> Kind { + match suite { + "smoke" => Kind::Smoke, + "kunit" => Kind::Kunit, + _ if suite.starts_with("kselftest:") => Kind::Kselftest, + _ => Kind::Boot, + } +} + +/// A userland: the busybox and, for the selftests, an `rk selftests` output directory. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +struct Userland<'a> { + busybox: &'a [u8], + selftests: Option<&'a Path>, +} + /// One boot of one kernel with one userland running one suite, named by `stem` in the run /// directory. The initramfs is removed after the boot, and the console is kept. fn boot_suite( plan: &Plan, build: &Path, - busybox: &[u8], + userland: Userland, suite: &str, stem: &str, ) -> Result<(Units, BTreeSet), String> { let files = if suite == "kunit" { modules(build)? + } else if let Some(collection) = suite.strip_prefix("kselftest:") { + let dir = userland + .selftests + .ok_or("the kselftest kind needs --selftests, an rk selftests directory")?; + crate::selftests::files(dir, collection)? } else { Vec::new() }; + let busybox = userland.busybox; let initramfs = plan.out.join(format!("{stem}.cpio")); std::fs::write(&initramfs, boot::initramfs_with(busybox, &files)) .map_err(|e| format!("writing {}: {e}", initramfs.display()))?; @@ -367,9 +438,15 @@ impl Outcome { pub fn run(plan: &Plan) -> Result { std::fs::create_dir_all(&plan.out) .map_err(|e| format!("creating {}: {e}", plan.out.display()))?; - let rucc_busybox = plan.rucc_busybox.as_deref().unwrap_or(&plan.busybox); - let two_userlands = rucc_busybox != plan.busybox.as_slice(); - let suites = suites(&plan.kinds); + let (gcc, rucc) = plan.userlands(); + let two_userlands = gcc != rucc; + let collections = match &plan.selftests { + Some(dir) if plan.kinds.contains(&Kind::Kselftest) => { + crate::selftests::Outcome::load(dir)?.runnable() + } + _ => Vec::new(), + }; + let suites = suites(&plan.kinds, &collections); let mut reference = Results::new(); let mut other = Results::new(); let mut splats = Splats::default(); @@ -380,18 +457,18 @@ pub fn run(plan: &Plan) -> Result { let (units, found) = boot_suite( plan, &plan.reference, - &plan.busybox, + gcc, suite, - &format!("reference-{suite}-{n}"), + &format!("reference-{}-{n}", stem(suite)), )?; merge(&mut r_units, units); splats.reference.extend(found); let (units, found) = boot_suite( plan, &plan.other, - rucc_busybox, + rucc, suite, - &format!("other-{suite}-{n}"), + &format!("other-{}-{n}", stem(suite)), )?; merge(&mut o_units, units); splats.other.extend(found); @@ -414,26 +491,23 @@ pub fn run(plan: &Plan) -> Result { let (mut swapped_kernel, mut swapped_userland) = (Units::new(), Units::new()); if two_userlands && !failed_kinds.is_empty() { for suite in &suites { - let wanted = failed_kinds.contains(&Kind::Boot) - || (*suite == "smoke" && failed_kinds.contains(&Kind::Smoke)) - || (*suite == "kunit" && failed_kinds.contains(&Kind::Kunit)); - if !wanted { + if !failed_kinds.contains(&Kind::Boot) && !failed_kinds.contains(&suite_kind(suite)) { continue; } let (found, _) = boot_suite( plan, &plan.other, - &plan.busybox, + gcc, suite, - &format!("other-gcc-userland-{suite}"), + &format!("other-gcc-userland-{}", stem(suite)), )?; merge(&mut swapped_kernel, found); let (found, _) = boot_suite( plan, &plan.reference, - rucc_busybox, + rucc, suite, - &format!("reference-rucc-userland-{suite}"), + &format!("reference-rucc-userland-{}", stem(suite)), )?; merge(&mut swapped_userland, found); } @@ -464,6 +538,11 @@ pub fn run(plan: &Plan) -> Result { Ok(outcome) } +/// A suite as part of a file name, without the colon of `kselftest:`. +fn stem(suite: &str) -> String { + suite.replace(':', "-") +} + /// A unit's results over the runs, as words. fn statuses(list: &[Status]) -> String { list.iter().map(|s| s.word()).collect::>().join(" ") @@ -473,15 +552,7 @@ fn statuses(list: &[Status]) -> String { #[must_use] pub fn summary(o: &Outcome) -> String { let mut s = String::from("### rk test\n\n"); - let kinds: Vec<&str> = o - .kinds - .iter() - .map(|k| match k { - Kind::Boot => "boot", - Kind::Smoke => "smoke", - Kind::Kunit => "kunit", - }) - .collect(); + let kinds: Vec<&str> = o.kinds.iter().map(|k| k.word()).collect(); let graded = o.units.iter().filter(|u| u.graded).count(); let failed = o.failed(); let _ = writeln!( @@ -542,11 +613,22 @@ mod tests { parse_kinds("kunit,boot,smoke,boot").unwrap(), [Kind::Boot, Kind::Smoke, Kind::Kunit] ); - assert!(parse_kinds("boot,kselftest").unwrap_err().contains("K5")); - assert!(parse_kinds("ltp").unwrap_err().contains("K5")); + assert_eq!( + parse_kinds("boot,kselftest").unwrap(), + [Kind::Boot, Kind::Kselftest] + ); + assert!(parse_kinds("ltp").unwrap_err().contains("not run yet")); assert!(parse_kinds("fuzz").unwrap_err().contains("unknown")); - assert_eq!(suites(&[Kind::Boot]), ["smoke"]); - assert_eq!(suites(&[Kind::Kunit]), ["kunit"]); + let timers = ["timers".to_string(), "size".to_string()]; + assert_eq!(suites(&[Kind::Boot], &timers), ["smoke"]); + assert_eq!(suites(&[Kind::Kunit], &timers), ["kunit"]); + assert_eq!( + suites(&[Kind::Kselftest], &timers), + ["kselftest:timers", "kselftest:size"] + ); + assert_eq!(kind_of("kselftest:timers:posix_timers"), Kind::Kselftest); + assert_eq!(suite_kind("kselftest:timers"), Kind::Kselftest); + assert_eq!(suite_kind("kunit"), Kind::Kunit); } fn booted() -> boot::Outcome { diff --git a/rk-init/init.sh b/rk-init/init.sh index 45dc6d9..7129283 100644 --- a/rk-init/init.sh +++ b/rk-init/init.sh @@ -6,7 +6,8 @@ # suite is smoke by default. The kunit suite loads every module rk test put under /lib/modules/rk, # in the order kbuild built them, which runs the KUnit tests they hold, and tries again with the # ones that failed for as long as that loads more, since a module can need one later in the -# order. Every line rk boot and rk test read starts with RK-, apart from KUnit's own output. The +# order. Every line rk boot and rk test read starts with RK-, apart from the TAP of KUnit and of the +# selftests, which the kselftest: suite runs from /kselftest. The # C version that docs/plan/11-boot-and-tests.md describes replaces this once rucc can build it, # and until then both kernels run the same busybox, so the userland is never the difference. @@ -72,9 +73,18 @@ kunit() { sleep 1 } +# One collection of the selftests rk test put under /kselftest. run_kselftest.sh prints one TAP +# line per program, and each program's own output behind "# ". +kselftest() { + [ -x /kselftest/run_kselftest.sh ] || return 0 + cd /kselftest && ./run_kselftest.sh -c "$1" + cd / +} + case $suite in smoke) smoke ;; kunit) kunit ;; + kselftest:*) kselftest "${suite#kselftest:}" ;; *) echo "RK-SUITE $suite unknown" ;; esac diff --git a/rows.toml b/rows.toml index a4b3664..6848846 100644 --- a/rows.toml +++ b/rows.toml @@ -3,6 +3,8 @@ # `arch` is kbuild's ARCH. `cross` is the CROSS_COMPILE prefix a build on another host needs. rk # leaves it out when the host is the row's own architecture, so a row can name it and still build # natively. `image` is the make target for the boot image. The QEMU fields are read by `rk boot`. +# `selftests` are the kselftest collections pinned for the row (plan 11.5), which `rk selftests` +# builds and `rk test --kinds kselftest` runs. [[row]] name = "X64" @@ -13,6 +15,11 @@ qemu = "qemu-system-x86_64" machine = "q35" cpu = "max" console = "ttyS0" +selftests = [ + "breakpoints", "capabilities", "clone3", "core", "exec", "futex", "kcmp", "membarrier", "mm", + "mqueue", "pidfd", "proc", "rseq", "seccomp", "sigaltstack", "size", "timens", "timers", + "vDSO", "ptrace", "syscall_user_dispatch", "landlock", "splice", "sync", "user_events", "x86", +] [[row]] name = "A64" @@ -23,6 +30,11 @@ qemu = "qemu-system-aarch64" machine = "virt" cpu = "max" console = "ttyAMA0" +selftests = [ + "breakpoints", "capabilities", "clone3", "core", "exec", "futex", "kcmp", "membarrier", "mm", + "mqueue", "pidfd", "proc", "rseq", "seccomp", "sigaltstack", "size", "timens", "timers", + "vDSO", "ptrace", "syscall_user_dispatch", "landlock", "splice", "sync", "user_events", "arm64", +] [[row]] name = "X32"