diff --git a/docs/superpowers/contracts/arch-cpu-tests-2026-08-22.json b/docs/superpowers/contracts/arch-cpu-tests-2026-08-22.json new file mode 100644 index 00000000..515addf3 --- /dev/null +++ b/docs/superpowers/contracts/arch-cpu-tests-2026-08-22.json @@ -0,0 +1,18 @@ +{ + "title": "Clear arch_cpu.sh from the shell coverage allowlist", + "objective": "Bring arch_cpu.sh to 100% measured line coverage through tests/run_all.sh and remove its allowlist exemption, taking the allowlist from 91 entries to 90.", + "acceptance_criteria": [ + "The jail reports arch_cpu.sh at 100.00% measured through tests/run_all.sh, not through a single test file.", + "A second measurement at --min 100 exits 0, so a shortfall would be a non-zero exit rather than a number to read.", + "With the allowlist line removed, check_shell_coverage.sh on arch_cpu.sh prints 'every checked shell library meets the coverage bar'.", + "Both functions are covered across every branch: no cpufreq at all, all cores already tuned, a mixed set where only some cores lag, cpupower absent, cpupower present and unconfigured, cpupower already configured, and all three I/O scheduler targets (none for NVMe, mq-deadline for SSD, bfq for HDD).", + "arch_cpu.sh's production behaviour is unchanged: every new override defaults to the real path it replaced.", + "The whole suite stays green and shellcheck reports nothing, with zero suppressions." + ], + "out_of_scope": [ + "arch_sysctl.sh's kcov registration failure, which is tracked separately and stays on the allowlist.", + "Changing the coverage instrument, which is scheduled after the eight libs.", + "arch_hardware.sh, the third lib of this family, which reuses this harness in a later commit." + ], + "verifier": "timeout 900s bash meta/scripts/shell_coverage_jail.sh --subject linux_configuration/scripts/single_use/fixes/lib/tests/run_all.sh --measure arch_cpu.sh --min 100 --fail-on-case-error -- \"\" (expects 57/57 = 100.00%, exit 0), followed by bash meta/scripts/check_shell_coverage.sh on the lib with its allowlist line removed." +} diff --git a/docs/superpowers/evidence/arch-cpu-tests-2026-08-22.json b/docs/superpowers/evidence/arch-cpu-tests-2026-08-22.json new file mode 100644 index 00000000..df3f7db4 --- /dev/null +++ b/docs/superpowers/evidence/arch-cpu-tests-2026-08-22.json @@ -0,0 +1,51 @@ +{ + "intent": "Test arch_cpu.sh to 100% line coverage and take it off the shell coverage allowlist, clearing the second of the eight untested fixes/lib libraries.", + "scope": [ + "linux_configuration/scripts/single_use/fixes/lib/arch_cpu.sh (test seams only)", + "linux_configuration/scripts/single_use/fixes/lib/tests/test_arch_cpu.sh (new)", + "linux_configuration/scripts/single_use/fixes/lib/tests/arch_desktop_harness.sh (sysfs helpers)", + "meta/shell-coverage-allowlist.txt (one line removed)", + "Non-goal: arch_sysctl.sh's measurement problem, which this commit deliberately leaves untouched" + ], + "changes": [ + "Added CPU_SYSFS_ROOT, BLOCK_SYSFS_ROOT, UDEV_RULES_DIR and CPUPOWER_CONF_FILE overrides to arch_cpu.sh, each defaulting to the real path so production behaviour is unchanged", + "Added _t_make_cpu, _t_make_block, _t_sched and _t_udev to arch_desktop_harness.sh so a case can present hardware this host does not have, such as a rotational disk", + "Added test_arch_cpu.sh: 28 assertions over 14 cases covering both functions, including the mixed-governor case, all three I/O scheduler targets, and the already-configured cpupower path", + "Removed arch_cpu.sh from meta/shell-coverage-allowlist.txt, taking it from 91 to 90 entries" + ], + "verification": [ + { + "command": "shell_coverage_jail.sh --subject .../tests/run_all.sh --measure arch_cpu.sh --min 1 --fail-on-case-error", + "result": "pass", + "evidence": "arch_cpu.sh: 57/57 lines = 100.00%" + }, + { + "command": "shell_coverage_jail.sh ... --measure arch_cpu.sh --min 100 --fail-on-case-error", + "result": "pass", + "evidence": "arch_cpu.sh: 57/57 lines = 100.00%, exit 0, so a shortfall would have been a non-zero exit." + }, + { + "command": "bash meta/scripts/check_shell_coverage.sh .../fixes/lib/arch_cpu.sh (after removing the allowlist line)", + "result": "pass", + "evidence": "check_shell_coverage.sh: every checked shell library meets the coverage bar" + }, + { + "command": "bash linux_configuration/scripts/single_use/fixes/lib/tests/run_all.sh", + "result": "pass", + "evidence": "All fixes lib test files passed; arch_cpu: 28 passed, 0 failed." + }, + { + "command": "shellcheck arch_cpu.sh test_arch_cpu.sh arch_desktop_harness.sh", + "result": "pass", + "evidence": "No findings, zero suppressions." + } + ], + "risks": [ + "A test asserting cpupower is absent initially passed for the wrong reason: _t_hide keeps $FAKE_BIN on PATH, so the harness's own default stub still satisfied has_cmd. Fixed by unstubbing before hiding, and the assertion now fails if that ordering is lost.", + "The measurement of arch_cpu.sh through the same harness that arch_sysctl.sh uses shows the harness is not the cause of arch_sysctl.sh's unmeasurability" + ], + "rollback": [ + "git revert the commit, then re-add the arch_cpu.sh line to meta/shell-coverage-allowlist.txt", + "Re-run check_shell_coverage.sh to confirm the gate passes again with the exemption restored" + ] +} diff --git a/linux_configuration/scripts/single_use/fixes/lib/arch_cpu.sh b/linux_configuration/scripts/single_use/fixes/lib/arch_cpu.sh index 1a33f3f8..9529fef5 100755 --- a/linux_configuration/scripts/single_use/fixes/lib/arch_cpu.sh +++ b/linux_configuration/scripts/single_use/fixes/lib/arch_cpu.sh @@ -10,7 +10,10 @@ # =================================================================== tweak_cpu_governor() { local gov_files - gov_files=$(find /sys/devices/system/cpu -maxdepth 3 -name scaling_governor 2>/dev/null || true) + # CPU_SYSFS_ROOT and the two write targets below default to the real + # locations and are overridden only by the test harness, which runs + # un-jailed in ci_mirror.sh and in CI. + gov_files=$(find "${CPU_SYSFS_ROOT:-/sys/devices/system/cpu}" -maxdepth 3 -name scaling_governor 2>/dev/null || true) if [[ -z $gov_files ]]; then log_warn "No CPU governor sysfs files found — skipping." @@ -37,7 +40,7 @@ tweak_cpu_governor() { done # Make it persistent via a sysctl-style drop-in using udev rule - local udev_rule="/etc/udev/rules.d/60-cpu-governor-performance.rules" + local udev_rule="${UDEV_RULES_DIR:-/etc/udev/rules.d}/60-cpu-governor-performance.rules" if [[ ! -f $udev_rule ]]; then cat >"$udev_rule" <<'UDEVEOF' # Set CPU governor to performance on all cores at boot @@ -46,7 +49,7 @@ UDEVEOF fi # Also install cpupower hook as a more reliable persistence method - local cpupower_conf="/etc/default/cpupower" + local cpupower_conf="${CPUPOWER_CONF_FILE:-/etc/default/cpupower}" if has_cmd cpupower; then if [[ ! -f $cpupower_conf ]] || ! grep -q "^governor='performance'" "$cpupower_conf" 2>/dev/null; then mkdir -p "$(dirname "$cpupower_conf")" @@ -68,7 +71,8 @@ tweak_io_scheduler() { local changed=false local block_dev - for block_dev in /sys/block/sd* /sys/block/nvme* /sys/block/vd*; do + local block_root="${BLOCK_SYSFS_ROOT:-/sys/block}" + for block_dev in "$block_root"/sd* "$block_root"/nvme* "$block_root"/vd*; do [[ -d $block_dev ]] || continue local sched_file="$block_dev/queue/scheduler" [[ -f $sched_file ]] || continue @@ -100,7 +104,7 @@ tweak_io_scheduler() { done # Persist via udev rule - local udev_rule="/etc/udev/rules.d/60-io-scheduler.rules" + local udev_rule="${UDEV_RULES_DIR:-/etc/udev/rules.d}/60-io-scheduler.rules" if [[ ! -f $udev_rule ]]; then cat >"$udev_rule" <<'IOEOF' # NVMe: no scheduler (multi-queue hardware handles it) diff --git a/linux_configuration/scripts/single_use/fixes/lib/tests/arch_desktop_harness.sh b/linux_configuration/scripts/single_use/fixes/lib/tests/arch_desktop_harness.sh index 4b370933..0552cf57 100755 --- a/linux_configuration/scripts/single_use/fixes/lib/tests/arch_desktop_harness.sh +++ b/linux_configuration/scripts/single_use/fixes/lib/tests/arch_desktop_harness.sh @@ -38,6 +38,11 @@ REPO_ROOT="$(cd "${FIXES_DIR}/../../../.." && pwd)" export SYSCTL_DROPIN_DIR="${TEST_TMPDIR}/sysctl.d" export UDEV_RULES_DIR="${TEST_TMPDIR}/udev.rules.d" export CPUPOWER_CONF_FILE="${TEST_TMPDIR}/cpupower" +# arch_cpu.sh reads CPU and block-device state from sysfs; the tests build a +# fake tree under these roots so a case can present hardware this host does +# not have (a rotational disk, a core not on the performance governor). +export CPU_SYSFS_ROOT="${TEST_TMPDIR}/sys_cpu" +export BLOCK_SYSFS_ROOT="${TEST_TMPDIR}/sys_block" export SYSTEMD_UNIT_DIR="${TEST_TMPDIR}/systemd_units" export JOURNALD_CONF_DIR="${TEST_TMPDIR}/journald.conf.d" export DRY_RUN="false" @@ -95,14 +100,47 @@ arch_desktop_reset() { INTERACTIVE_MODE="false" export DRY_RUN AGGRESSIVE INTERACTIVE_MODE rm -rf "${SYSCTL_DROPIN_DIR}" "${UDEV_RULES_DIR}" "${SYSTEMD_UNIT_DIR}" \ - "${JOURNALD_CONF_DIR}" + "${JOURNALD_CONF_DIR}" "${CPU_SYSFS_ROOT}" "${BLOCK_SYSFS_ROOT}" mkdir -p "${SYSCTL_DROPIN_DIR}" "${UDEV_RULES_DIR}" "${SYSTEMD_UNIT_DIR}" \ - "${JOURNALD_CONF_DIR}" + "${JOURNALD_CONF_DIR}" "${CPU_SYSFS_ROOT}" "${BLOCK_SYSFS_ROOT}" rm -f "${CPUPOWER_CONF_FILE}" _t_full_path _arch_desktop_default_stubs } +# _t_make_cpu N GOVERNOR — create N fake cores whose scaling_governor reads +# GOVERNOR. The layout mirrors the real one closely enough for the lib's +# `find -maxdepth 3 -name scaling_governor` to reach it. +_t_make_cpu() { + local count="$1" governor="$2" i dir + for ((i = 0; i < count; i++)); do + dir="${CPU_SYSFS_ROOT}/cpu${i}/cpufreq" + mkdir -p "$dir" + printf '%s\n' "$governor" >"${dir}/scaling_governor" + done +} + +# _t_make_block NAME ROTATIONAL SCHEDULER — create one fake block device. +# SCHEDULER is written in the kernel's own format, with the active entry in +# brackets, because that is what the lib parses. +_t_make_block() { + local name="$1" rotational="$2" scheduler="$3" dir + dir="${BLOCK_SYSFS_ROOT}/${name}/queue" + mkdir -p "$dir" + printf '%s\n' "$rotational" >"${dir}/rotational" + printf '%s\n' "$scheduler" >"${dir}/scheduler" +} + +# _t_sched NAME — the scheduler file's contents for a fake device. +_t_sched() { + cat "${BLOCK_SYSFS_ROOT}/$1/queue/scheduler" 2>/dev/null || true +} + +# _t_udev FILE — the contents of a udev rule written this round. +_t_udev() { + cat "${UDEV_RULES_DIR}/$1" 2>/dev/null || true +} + # _t_dropin FILE — the contents of a sysctl drop-in written this round. _t_dropin() { cat "${SYSCTL_DROPIN_DIR}/$1" 2>/dev/null || true diff --git a/linux_configuration/scripts/single_use/fixes/lib/tests/test_arch_cpu.sh b/linux_configuration/scripts/single_use/fixes/lib/tests/test_arch_cpu.sh new file mode 100755 index 00000000..f32e4f6c --- /dev/null +++ b/linux_configuration/scripts/single_use/fixes/lib/tests/test_arch_cpu.sh @@ -0,0 +1,160 @@ +#!/usr/bin/env bash +# lib/tests/test_arch_cpu.sh — tests for arch_cpu.sh. +# +# Both functions read hardware state from sysfs and write persistence rules, +# so every case builds a fake sysfs tree under $CPU_SYSFS_ROOT / +# $BLOCK_SYSFS_ROOT first. That is what lets a case present hardware this +# host does not have -- a rotational disk, or a core that is not already on +# the performance governor. +# +# Calls go through _t_run rather than `out="$(...)"`: command substitution +# forks a subshell, and kcov does not register a lib whose first execution +# happens inside one. +set -uo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +# shellcheck source=arch_desktop_harness.sh +. "${SCRIPT_DIR}/arch_desktop_harness.sh" + +# shellcheck source=../arch_cpu.sh +. "${FIXES_DIR}/lib/arch_cpu.sh" + +printf '\n-- tweak_cpu_governor --\n' + +# Case 1: no governor files at all (a VM, or a kernel without cpufreq). +arch_desktop_reset +_t_run tweak_cpu_governor +_t_eq "0" "$?" "tweak_cpu_governor: returns 0 with no governor files" +_t_contains "$out" "No CPU governor sysfs files found" \ + "tweak_cpu_governor: warns when cpufreq is absent" +_t_eq "" "$(_t_udev 60-cpu-governor-performance.rules)" \ + "tweak_cpu_governor: writes no udev rule when there is nothing to tune" + +# Case 2: every core already on performance -> skip without writing. +arch_desktop_reset +_t_make_cpu 4 performance +_t_run tweak_cpu_governor +_t_contains "$out" "already on 'performance' governor" \ + "tweak_cpu_governor: reports the skip when all cores are tuned" +_t_eq "" "$(_t_udev 60-cpu-governor-performance.rules)" \ + "tweak_cpu_governor: writes no udev rule when skipping" + +# Case 3: cores on powersave -> every core is switched and the rule written. +arch_desktop_reset +_t_make_cpu 3 powersave +# _t_hide keeps $FAKE_BIN on PATH, so the default stub has to go first or +# has_cmd still finds cpupower and the "not installed" branch never runs. +_t_unstub cpupower +_t_hide cpupower +_t_run tweak_cpu_governor +_t_eq "performance" "$(cat "${CPU_SYSFS_ROOT}/cpu0/cpufreq/scaling_governor")" \ + "tweak_cpu_governor: switches cpu0 to performance" +_t_eq "performance" "$(cat "${CPU_SYSFS_ROOT}/cpu2/cpufreq/scaling_governor")" \ + "tweak_cpu_governor: switches the last core too" +_t_contains "$(_t_udev 60-cpu-governor-performance.rules)" "scaling_governor" \ + "tweak_cpu_governor: writes the persistence udev rule" +_t_eq "" "$(cat "${CPUPOWER_CONF_FILE}" 2>/dev/null || true)" \ + "tweak_cpu_governor: writes no cpupower config when cpupower is absent" +_t_full_path + +# Case 4: only SOME cores need changing -- the mixed case, which the +# all_performance loop must break out of on the first mismatch. +arch_desktop_reset +_t_make_cpu 2 performance +mkdir -p "${CPU_SYSFS_ROOT}/cpu2/cpufreq" +printf 'powersave\n' >"${CPU_SYSFS_ROOT}/cpu2/cpufreq/scaling_governor" +_t_unstub cpupower +_t_hide cpupower +_t_run tweak_cpu_governor +_t_eq "performance" "$(cat "${CPU_SYSFS_ROOT}/cpu2/cpufreq/scaling_governor")" \ + "tweak_cpu_governor: switches the one core that lagged" +_t_lacks "$out" "already on 'performance'" \ + "tweak_cpu_governor: does not claim a skip when one core differed" +_t_full_path + +# Case 5: cpupower present and unconfigured -> config written and unit enabled. +arch_desktop_reset +_t_make_cpu 1 powersave +_t_stub cpupower 'exit 0' +_t_run tweak_cpu_governor +_t_contains "$(cat "${CPUPOWER_CONF_FILE}")" "governor='performance'" \ + "tweak_cpu_governor: writes the cpupower default" +_t_contains "$(_t_calls)" "systemctl enable cpupower.service" \ + "tweak_cpu_governor: enables the cpupower unit" + +# Case 6: cpupower present and ALREADY configured -> left alone, not re-enabled. +arch_desktop_reset +_t_make_cpu 1 powersave +_t_stub cpupower 'exit 0' +printf "governor='performance'\n" >"${CPUPOWER_CONF_FILE}" +_t_run tweak_cpu_governor +_t_lacks "$(_t_calls)" "systemctl enable cpupower.service" \ + "tweak_cpu_governor: does not re-enable when already configured" + +printf '\n-- tweak_io_scheduler --\n' + +# Case 7: no block devices -> the udev rule is still written, and the +# "already optimal" line reports that nothing changed. +arch_desktop_reset +_t_run tweak_io_scheduler +_t_eq "0" "$?" "tweak_io_scheduler: returns 0 with no block devices" +_t_contains "$out" "All I/O schedulers already optimal" \ + "tweak_io_scheduler: reports nothing changed" +_t_contains "$(_t_udev 60-io-scheduler.rules)" "mq-deadline" \ + "tweak_io_scheduler: writes the persistence udev rule regardless" + +# Case 8: an NVMe drive wants 'none'. +arch_desktop_reset +_t_make_block nvme0n1 0 '[mq-deadline] none' +_t_run tweak_io_scheduler +_t_eq "none" "$(_t_sched nvme0n1)" "tweak_io_scheduler: sets NVMe to none" +_t_contains "$out" "scheduler changed from 'mq-deadline' to 'none'" \ + "tweak_io_scheduler: logs the NVMe change" + +# Case 9: a SATA SSD (rotational=0) wants mq-deadline. +arch_desktop_reset +_t_make_block sda 0 '[none] mq-deadline bfq' +_t_run tweak_io_scheduler +_t_eq "mq-deadline" "$(_t_sched sda)" "tweak_io_scheduler: sets SSD to mq-deadline" + +# Case 10: a spinning disk (rotational=1) wants bfq. +arch_desktop_reset +_t_make_block sdb 1 '[mq-deadline] bfq none' +_t_run tweak_io_scheduler +_t_eq "bfq" "$(_t_sched sdb)" "tweak_io_scheduler: sets HDD to bfq" + +# Case 11: a device already on its target is left alone. +arch_desktop_reset +_t_make_block nvme1n1 0 'mq-deadline [none]' +_t_run tweak_io_scheduler +_t_contains "$out" "already using 'none' scheduler" \ + "tweak_io_scheduler: reports an already-optimal device" +_t_contains "$out" "All I/O schedulers already optimal" \ + "tweak_io_scheduler: counts it as no change" + +# Case 12: several devices of different kinds in one pass. +arch_desktop_reset +_t_make_block nvme0n1 0 '[mq-deadline] none' +_t_make_block sda 0 '[bfq] mq-deadline' +_t_make_block sdb 1 '[mq-deadline] bfq' +_t_run tweak_io_scheduler +_t_eq "none" "$(_t_sched nvme0n1)" "tweak_io_scheduler: NVMe handled in a mixed pass" +_t_eq "mq-deadline" "$(_t_sched sda)" "tweak_io_scheduler: SSD handled in a mixed pass" +_t_eq "bfq" "$(_t_sched sdb)" "tweak_io_scheduler: HDD handled in a mixed pass" + +# Case 13: a directory without a queue/scheduler file is skipped, not fatal. +arch_desktop_reset +mkdir -p "${BLOCK_SYSFS_ROOT}/sdc" +_t_run tweak_io_scheduler +_t_eq "0" "$?" "tweak_io_scheduler: skips a device with no scheduler file" + +# Case 14: an existing udev rule is not overwritten. +arch_desktop_reset +printf 'CUSTOM RULE\n' >"${UDEV_RULES_DIR}/60-io-scheduler.rules" +_t_run tweak_io_scheduler +_t_eq "CUSTOM RULE" "$(_t_udev 60-io-scheduler.rules)" \ + "tweak_io_scheduler: leaves an existing udev rule alone" + +printf '\narch_cpu: %d passed, %d failed\n' "$PASS" "$FAIL" +[[ $FAIL -eq 0 ]] diff --git a/meta/shell-coverage-allowlist.txt b/meta/shell-coverage-allowlist.txt index 41eb564f..557c5768 100644 --- a/meta/shell-coverage-allowlist.txt +++ b/meta/shell-coverage-allowlist.txt @@ -73,7 +73,6 @@ linux_configuration/scripts/single_use/features/lib/website_build.sh linux_configuration/scripts/single_use/features/lib/wg_firewall.sh linux_configuration/scripts/single_use/features/lib/wg_keys.sh linux_configuration/scripts/single_use/features/lib/wg_sshd.sh -linux_configuration/scripts/single_use/fixes/lib/arch_cpu.sh linux_configuration/scripts/single_use/fixes/lib/arch_hardware.sh linux_configuration/scripts/single_use/fixes/lib/arch_sysctl.sh linux_configuration/scripts/single_use/fixes/lib/bt_adapter.sh