Files
Panama/tests/setup/hardware-predicates-contract
T
Gabriel Brown 3c359f3f7e Notice the battery, and the machine it is or is not in
Panama had no idea whether it was running on a laptop. No upower, no
battery, no lid, no AC: hypridle.conf says "This is a desktop" in its
own header, and that was true of the code as well as the machine.

panama-hw answers hardware questions one at a time, exits 0 or 1, and
prints nothing, so scripts, services and contracts all ask the same
way. The definition the rest of the laptop work hangs on is one line:
clamshell is lid-closed AND an external monitor. A machine with no
mains supply at all reports as being on wall power, because a desktop
cannot run out of it.

The battery service follows Vitals: sysfs through FileView, an
availability flag, and no subprocess on the timer. Globbing is the one
thing QML cannot do -- a battery is BAT0 or BAT1 or CMB0, mains is AC
or ADP1 or ACAD -- so panama-battery resolves the names once and the
shell reads the files directly after. Nothing falls back to a
plausible zero: a desktop shows no indicator, no card, and no charge
limit control where the firmware has no ceiling.

Also repairs two contracts that were already failing and had not been
noticed, because only the full suite runs them. The dependency
scanner treated line-initial variable assignments, case labels,
comments and heredoc bodies as commands, and `count`, `host`, `cancel`
and `import` are all real binaries on Fedora, so `command -v` could
not filter them out. It now drops comments and heredoc bodies and
requires a command to be followed by whitespace. Verified it still
catches a genuinely undeclared dependency rather than passing quietly.
The launcher command contract had not been told about the fourteen
commands added earlier today.
2026-08-21 21:43:14 -04:00

190 lines
8.7 KiB
Bash
Executable File

#!/usr/bin/env bash
# What this machine is.
#
# These predicates decide whether a battery indicator appears, whether closing
# the lid suspends or keeps working, and which idle timings apply. Every one of
# them is asked on machines that have none of the hardware involved, so the
# property that matters most is that a desktop gets a calm "no" rather than an
# error or a wrong yes.
#
# The laptop answers cannot be tested on a desktop, so they are driven against
# fixture sysfs trees through PANAMA_HW_SYS and PANAMA_HW_ACPI. The real
# machine is only asked whether every predicate runs and answers.
#
# The one definition worth pinning hardest: clamshell is lid-closed AND an
# external monitor. Get that backwards and a laptop on a train stays awake with
# its lid shut, or a docked one suspends mid-work.
set -uo pipefail
repo_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
hw="$repo_dir/bin/panama-hw"
findings=()
note() { findings+=("$1"); }
[[ -x "$hw" ]] || { printf 'hardware predicates contract: %s is not executable\n' "$hw" >&2; exit 1; }
work="$(mktemp -d)"
trap 'rm -rf "$work"' EXIT
# Builds a fixture machine. Every argument is optional; what is absent is
# absent on the machine too, which is the case worth testing.
# fixture <name> chassis=<n> battery=<pct|-> mains=<0|1|-> lid=<open|closed|->
# external=<yes|no> internal=<yes|no>
fixture() {
local name="$1"; shift
local root="$work/$name"
local chassis="" battery="" mains="" lid="" external="no" internal="no"
local arg
for arg in "$@"; do
case "$arg" in
chassis=*) chassis="${arg#*=}" ;;
battery=*) battery="${arg#*=}" ;;
mains=*) mains="${arg#*=}" ;;
lid=*) lid="${arg#*=}" ;;
external=*) external="${arg#*=}" ;;
internal=*) internal="${arg#*=}" ;;
esac
done
mkdir -p "$root/sys/class/dmi/id" "$root/sys/class/power_supply" \
"$root/sys/class/drm" "$root/sys/class/input" \
"$root/sys/bus/pci/devices" "$root/acpi/button/lid"
[[ -n "$chassis" ]] && printf '%s\n' "$chassis" >"$root/sys/class/dmi/id/chassis_type"
if [[ -n "$battery" && "$battery" != "-" ]]; then
mkdir -p "$root/sys/class/power_supply/BAT0"
printf 'Battery\n' >"$root/sys/class/power_supply/BAT0/type"
printf '%s\n' "$battery" >"$root/sys/class/power_supply/BAT0/capacity"
fi
if [[ -n "$mains" && "$mains" != "-" ]]; then
mkdir -p "$root/sys/class/power_supply/AC0"
printf 'Mains\n' >"$root/sys/class/power_supply/AC0/type"
printf '%s\n' "$mains" >"$root/sys/class/power_supply/AC0/online"
fi
if [[ -n "$lid" && "$lid" != "-" ]]; then
mkdir -p "$root/acpi/button/lid/LID0"
printf 'state: %s\n' "$lid" >"$root/acpi/button/lid/LID0/state"
fi
if [[ "$internal" == "yes" ]]; then
mkdir -p "$root/sys/class/drm/card0-eDP-1"
printf 'connected\n' >"$root/sys/class/drm/card0-eDP-1/status"
fi
if [[ "$external" == "yes" ]]; then
mkdir -p "$root/sys/class/drm/card0-DP-1"
printf 'connected\n' >"$root/sys/class/drm/card0-DP-1/status"
else
mkdir -p "$root/sys/class/drm/card0-DP-1"
printf 'disconnected\n' >"$root/sys/class/drm/card0-DP-1/status"
fi
printf '%s\n' "$root"
}
ask() {
local root="$1" predicate="$2"
PANAMA_HW_SYS="$root/sys" PANAMA_HW_ACPI="$root/acpi" "$hw" "$predicate"
}
# Asserts a predicate's answer. `expect yes|no <root> <predicate> <why>`
expect() {
local want="$1" root="$2" predicate="$3" why="$4"
if ask "$root" "$predicate"; then
[[ "$want" == "yes" ]] || note "$why (answered yes, expected no)"
else
[[ "$want" == "no" ]] || note "$why (answered no, expected yes)"
fi
}
# ── A desktop ────────────────────────────────────────────────────────────────
# No battery, no lid, no mains supply at all. Every laptop answer must be no,
# and AC must be yes: a machine with no mains reporting is on wall power, and
# answering otherwise would apply battery timings to something that cannot run
# out of power.
desktop="$(fixture desktop chassis=3 external=yes)"
expect no "$desktop" laptop 'a desktop chassis reports as a laptop'
expect no "$desktop" battery 'a desktop reports a battery'
expect yes "$desktop" ac 'a desktop with no mains supply is not treated as on wall power'
expect no "$desktop" lid-closed 'a machine with no lid reports its lid closed'
expect no "$desktop" clamshell 'a desktop reports clamshell'
expect yes "$desktop" external-monitor 'a connected DisplayPort output is not seen as external'
# ── A laptop, lid open, undocked ─────────────────────────────────────────────
open_undocked="$(fixture open-undocked chassis=10 battery=64 mains=1 lid=open internal=yes)"
expect yes "$open_undocked" laptop 'a notebook chassis does not report as a laptop'
expect yes "$open_undocked" battery 'a battery is not detected'
expect yes "$open_undocked" ac 'a plugged-in laptop is not seen as on AC'
expect no "$open_undocked" lid-closed 'an open lid reports closed'
expect no "$open_undocked" clamshell 'an open lid reports clamshell'
expect no "$open_undocked" external-monitor \
'the built-in panel is counted as an external monitor'
# ── The same laptop, on battery ──────────────────────────────────────────────
unplugged="$(fixture unplugged chassis=10 battery=41 mains=0 lid=open internal=yes)"
expect no "$unplugged" ac 'a laptop with mains offline is still reported as on AC'
# ── Lid shut, no external monitor: this must suspend ─────────────────────────
closed_alone="$(fixture closed-alone chassis=10 battery=30 mains=0 lid=closed internal=yes)"
expect yes "$closed_alone" lid-closed 'a closed lid reports open'
expect no "$closed_alone" clamshell \
'a closed lid with no external monitor reports clamshell, which would keep a laptop awake in a bag'
# ── Lid shut with an external monitor: this must keep working ────────────────
docked="$(fixture docked chassis=10 battery=88 mains=1 lid=closed internal=yes external=yes)"
expect yes "$docked" lid-closed 'a docked closed lid reports open'
expect yes "$docked" external-monitor 'a docked external monitor is not detected'
expect yes "$docked" clamshell \
'a closed lid with an external monitor does not report clamshell, which would suspend a docked machine mid-work'
# ── An empty machine answers rather than erroring ────────────────────────────
# Nothing present at all: no DMI, no power supplies, no lid, no outputs. Every
# predicate must still exit cleanly, because a missing sysfs is what a
# container, a VM, or an unusual kernel looks like.
empty="$(fixture empty)"
for predicate in laptop battery ac lid-closed external-monitor clamshell touchpad nvidia; do
output="$(ask "$empty" "$predicate" 2>&1)"
status=$?
(( status == 0 || status == 1 )) \
|| note "$predicate exits $status on a machine with no hardware; it must answer, not fail"
[[ -n "$output" ]] \
&& note "$predicate printed '$output' instead of answering silently"
done
# ── The real machine answers every question ──────────────────────────────────
for predicate in laptop battery ac lid-closed external-monitor clamshell touchpad nvidia; do
"$hw" "$predicate" >/dev/null 2>&1
status=$?
(( status == 0 || status == 1 )) \
|| note "$predicate exits $status on this machine"
done
json="$("$hw" --json 2>/dev/null)"
if command -v jq >/dev/null 2>&1; then
jq -e . >/dev/null 2>&1 <<<"$json" || note '--json does not emit valid JSON'
for key in laptop battery ac lidClosed externalMonitor clamshell touchpad nvidia; do
jq -e "has(\"$key\")" >/dev/null 2>&1 <<<"$json" \
|| note "--json omits $key"
done
fi
# An unknown predicate is a caller's mistake and must be loud, not a silent no.
"$hw" not-a-real-predicate >/dev/null 2>&1
(( $? == 2 )) || note 'an unknown predicate does not exit 2, so a typo reads as a no'
if (( ${#findings[@]} > 0 )); then
printf 'hardware predicates contract: %d finding(s)\n' "${#findings[@]}" >&2
printf ' - %s\n' "${findings[@]}" >&2
exit 1
fi
printf 'hardware predicates contract: PASS\n'