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.
This commit is contained in:
Executable
+189
@@ -0,0 +1,189 @@
|
||||
#!/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'
|
||||
Reference in New Issue
Block a user