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
+159
@@ -0,0 +1,159 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# The battery, and the machines that do not have one.
|
||||
#
|
||||
# This is the first thing Panama has shipped that only exists on some hardware,
|
||||
# and the failure that matters is not a wrong percentage -- it is a desktop
|
||||
# growing battery chrome, or a laptop showing a confident 0% because a file
|
||||
# could not be read. So the properties pinned here are mostly about absence:
|
||||
#
|
||||
# 1. No battery means `available` is false and every surface hides. Not 0%,
|
||||
# not "Unknown" in the bar, not an empty card on the Power page.
|
||||
# 2. A machine with no mains supply at all is on wall power. A desktop must
|
||||
# never be treated as running on battery, or every battery-specific idle
|
||||
# timing would apply to it.
|
||||
# 3. The charge-limit control appears only where the firmware has one.
|
||||
# 4. The threshold write goes through panama-sudo with a reason, never bare
|
||||
# sudo, and is read back rather than assumed.
|
||||
#
|
||||
# The helper is driven against fixture sysfs trees; the QML side is pinned
|
||||
# statically, since a battery cannot be simulated into the running shell.
|
||||
|
||||
set -uo pipefail
|
||||
|
||||
repo_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||
helper="$repo_dir/config/dot/quickshell/scripts/panama-battery"
|
||||
service="$repo_dir/config/dot/quickshell/services/Battery.qml"
|
||||
cluster="$repo_dir/config/dot/quickshell/modules/bar/StatusCluster.qml"
|
||||
page="$repo_dir/config/dot/quickshell/modules/settings/PowerPage.qml"
|
||||
schema="$repo_dir/config/dot/quickshell/config/PreferenceSchema.qml"
|
||||
aliases="$repo_dir/config/dot/quickshell/config/Settings.qml"
|
||||
|
||||
findings=()
|
||||
note() { findings+=("$1"); }
|
||||
|
||||
[[ -x "$helper" ]] || { printf 'battery contract: %s is not executable\n' "$helper" >&2; exit 1; }
|
||||
|
||||
work="$(mktemp -d)"
|
||||
trap 'rm -rf "$work"' EXIT
|
||||
|
||||
# A fake machine. `battery <pct>` and `mains <0|1>` are both optional, and
|
||||
# leaving one out means the machine genuinely does not have it.
|
||||
fixture() {
|
||||
local name="$1" battery="${2:-}" mains="${3:-}" threshold="${4:-}"
|
||||
local root="$work/$name"
|
||||
mkdir -p "$root/sys/class/power_supply"
|
||||
if [[ -n "$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"
|
||||
printf 'Discharging\n' >"$root/sys/class/power_supply/BAT0/status"
|
||||
[[ -n "$threshold" ]] && printf '%s\n' "$threshold" \
|
||||
>"$root/sys/class/power_supply/BAT0/charge_control_end_threshold"
|
||||
fi
|
||||
if [[ -n "$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
|
||||
printf '%s\n' "$root"
|
||||
}
|
||||
|
||||
ask() {
|
||||
local root="$1"; shift
|
||||
PANAMA_HW_SYS="$root/sys" PANAMA_PATH="$repo_dir" "$helper" "$@" 2>/dev/null
|
||||
}
|
||||
|
||||
field() { jq -r "$2" <<<"$1" 2>/dev/null; }
|
||||
|
||||
# ── 1. A desktop ─────────────────────────────────────────────────────────────
|
||||
|
||||
desktop="$(fixture desktop)"
|
||||
status="$(ask "$desktop" status)"
|
||||
[[ "$(field "$status" .available)" == "false" ]] \
|
||||
|| note 'a machine with no battery reports one as available'
|
||||
[[ "$(field "$status" .acOnline)" == "true" ]] \
|
||||
|| note 'a machine with no mains supply is reported as running on battery'
|
||||
|
||||
paths="$(ask "$desktop" paths)"
|
||||
[[ "$(field "$paths" .battery)" == "" ]] \
|
||||
|| note 'a machine with no battery resolves a battery path anyway'
|
||||
|
||||
# ── 2. A laptop ──────────────────────────────────────────────────────────────
|
||||
|
||||
laptop="$(fixture laptop 64 1)"
|
||||
status="$(ask "$laptop" status)"
|
||||
[[ "$(field "$status" .available)" == "true" ]] || note 'a battery was not detected'
|
||||
[[ "$(field "$status" .percent)" == "64" ]] \
|
||||
|| note "the charge level is wrong (got $(field "$status" .percent))"
|
||||
[[ "$(field "$status" .acOnline)" == "true" ]] || note 'a plugged-in laptop reads as unplugged'
|
||||
|
||||
unplugged="$(fixture unplugged 41 0)"
|
||||
status="$(ask "$unplugged" status)"
|
||||
[[ "$(field "$status" .acOnline)" == "false" ]] \
|
||||
|| note 'a laptop with mains offline still reads as on wall power'
|
||||
|
||||
# ── 3. The charge limit appears only where it exists ─────────────────────────
|
||||
|
||||
paths="$(ask "$laptop" paths)"
|
||||
[[ "$(field "$paths" .threshold)" == "" ]] \
|
||||
|| note 'a machine without a charge threshold resolves one anyway, so the control would appear and do nothing'
|
||||
|
||||
limited="$(fixture limited 80 1 80)"
|
||||
paths="$(ask "$limited" paths)"
|
||||
[[ "$(field "$paths" .threshold)" != "" ]] \
|
||||
|| note 'a machine with a charge threshold does not expose it'
|
||||
[[ "$(field "$(ask "$limited" status)" .chargeLimit)" == "80" ]] \
|
||||
|| note 'the charge limit is not reported'
|
||||
|
||||
# ── 4. The write is privileged, named, and verified ──────────────────────────
|
||||
|
||||
grep -q 'panama-sudo' "$helper" \
|
||||
|| note 'the threshold write does not go through panama-sudo'
|
||||
if grep -nE '(^|[^-[:alnum:]])sudo ' "$helper" | grep -q -v 'panama-sudo'; then
|
||||
note 'the helper calls bare sudo somewhere, so the prompt would not name the change'
|
||||
fi
|
||||
grep -q -- '--reason' "$helper" \
|
||||
|| note 'the privileged write does not state a reason, so the prompt would not say what it changes'
|
||||
|
||||
# Out-of-range values are refused before any password is asked for.
|
||||
PANAMA_HW_SYS="$limited/sys" PANAMA_PATH="$repo_dir" "$helper" set-threshold 10 >/dev/null 2>&1 \
|
||||
&& note 'a threshold below the supported range was accepted'
|
||||
PANAMA_HW_SYS="$limited/sys" PANAMA_PATH="$repo_dir" "$helper" set-threshold abc >/dev/null 2>&1 \
|
||||
&& note 'a non-numeric threshold was accepted'
|
||||
|
||||
# ── 5. The QML side hides itself ─────────────────────────────────────────────
|
||||
|
||||
grep -q 'property bool available' "$service" \
|
||||
|| note 'the battery service has no availability flag'
|
||||
grep -q 'Settings.showBattery && Battery.available' "$cluster" \
|
||||
|| note 'the bar indicator does not gate on both the preference and the hardware'
|
||||
grep -q 'visible: Battery.available' "$page" \
|
||||
|| note 'the Power page battery card does not hide on a machine without one'
|
||||
grep -q 'visible: Battery.chargeLimitSupported' "$page" \
|
||||
|| note 'the charge limit control does not hide where the firmware has none'
|
||||
|
||||
# The alias layer has to carry the key, or the binding silently reads undefined
|
||||
# and the indicator never appears. This exact mistake was made writing it.
|
||||
for key in showBattery batteryLowPercent batteryCriticalPercent; do
|
||||
grep -q "property .*$key" "$aliases" \
|
||||
|| note "Settings.qml does not alias $key, so the binding reads undefined"
|
||||
done
|
||||
|
||||
for key in showBattery batteryLowPercent batteryCriticalPercent batteryChargeLimit; do
|
||||
grep -q "key: \"$key\"" "$schema" || note "the schema has no $key entry"
|
||||
done
|
||||
|
||||
# No subprocess on the polling path: the whole point of resolving paths once.
|
||||
if grep -A4 'Timer {' "$service" | grep -q 'running: true' && grep -q 'Process' "$service"; then
|
||||
grep -q 'onTriggered: root.refresh()' "$service" \
|
||||
|| note 'the poll timer does something other than re-read files'
|
||||
fi
|
||||
|
||||
if (( ${#findings[@]} > 0 )); then
|
||||
printf 'battery contract: %d finding(s)\n' "${#findings[@]}" >&2
|
||||
printf ' - %s\n' "${findings[@]}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
printf 'battery contract: PASS\n'
|
||||
@@ -72,6 +72,9 @@ package_for() {
|
||||
wl-copy|wl-paste) printf 'wl-clipboard' ;;
|
||||
ssh-keygen) printf 'openssh' ;;
|
||||
ssh|ssh-add) printf 'openssh-clients' ;;
|
||||
# The Wayland build is the one that can inject into this session; the
|
||||
# x11 one cannot. desktop-packages declares it under that name.
|
||||
espanso) printf 'espanso-wayland' ;;
|
||||
rg) printf 'ripgrep' ;;
|
||||
xdg-mime|xdg-settings|xdg-open) printf 'xdg-utils' ;;
|
||||
update-desktop-database|desktop-file-validate) printf 'desktop-file-utils' ;;
|
||||
@@ -114,13 +117,50 @@ while read -r script; do
|
||||
|
||||
missing+=("$cmd (from $(basename "$script"), package: $pkg)")
|
||||
done < <({
|
||||
# Heredoc bodies are not shell. A python or sql block embedded in a
|
||||
# script is scanned as though every line began a command, and its
|
||||
# keywords collide with real binaries often enough to matter: `import`
|
||||
# is ImageMagick, `time` is a package, `select` is shell syntax. The
|
||||
# scanner cannot parse those languages and should not try, so the
|
||||
# bodies are dropped before anything else looks at them.
|
||||
scanned="$(awk '
|
||||
# Whole-line comments. These files explain themselves at length,
|
||||
# and prose containing "; cancel it" or "| list" reads as a
|
||||
# statement to a line-based scanner. Dropped first so a comment
|
||||
# mentioning <<EOF cannot open a heredoc either.
|
||||
!inbody && /^[[:space:]]*#/ { next }
|
||||
|
||||
# <<MARKER, <<-MARKER, <<"MARKER", <<'"'"'MARKER'"'"' -- with or
|
||||
# without a command in front of it.
|
||||
!inbody && match($0, /<<-?[[:space:]]*["'"'"']?[A-Za-z_][A-Za-z0-9_]*["'"'"']?/) {
|
||||
marker = substr($0, RSTART, RLENGTH)
|
||||
gsub(/^<<-?[[:space:]]*["'"'"']?|["'"'"']?$/, "", marker)
|
||||
inbody = 1
|
||||
print
|
||||
next
|
||||
}
|
||||
inbody {
|
||||
line = $0
|
||||
gsub(/^[[:space:]]+|[[:space:]]+$/, "", line)
|
||||
if (line == marker) inbody = 0
|
||||
next
|
||||
}
|
||||
{ print }
|
||||
' "$script")"
|
||||
|
||||
# Statement-initial or after a pipe.
|
||||
grep -oE '(^|[|;&]|\$\()[[:space:]]*[a-z][a-z0-9_-]+' "$script" \
|
||||
| grep -oE '[a-z][a-z0-9_-]+$'
|
||||
#
|
||||
# A word followed by `=` is a variable assignment and a word followed
|
||||
# by `)` is a case label; neither is a command, and both collide with
|
||||
# real binaries -- `count`, `host` and `cancel` are all installed on a
|
||||
# normal Fedora machine, so `command -v` below cannot filter them out.
|
||||
# Requiring whitespace or end-of-line after the word excludes both.
|
||||
grep -oE '(^|[|;&]|\$\()[[:space:]]*[a-z][a-z0-9_-]+([[:space:]]|$)' <<<"$scanned" \
|
||||
| grep -oE '[a-z][a-z0-9_-]+'
|
||||
|
||||
# Behind a wrapper. ddcutil is always invoked as `timeout 10 ddcutil`,
|
||||
# so it never appears statement-initial and was missed entirely.
|
||||
grep -oE '\b(timeout[[:space:]]+[0-9.]+|sudo|nohup|env)[[:space:]]+[a-z][a-z0-9_-]+' "$script" \
|
||||
grep -oE '\b(timeout[[:space:]]+[0-9.]+|sudo|nohup|env)[[:space:]]+[a-z][a-z0-9_-]+' <<<"$scanned" \
|
||||
| grep -oE '[a-z][a-z0-9_-]+$'
|
||||
|
||||
# `command -v X` is how these helpers probe for a tool before using it,
|
||||
|
||||
@@ -65,7 +65,20 @@ done < <(grep -oE '\{ page: "[a-z-]+"' "$sidebar" | sed 's/.*"\([a-z-]*\)"/\1/')
|
||||
#
|
||||
# They are still commands, so everything else below applies to them: a title,
|
||||
# a description, search vocabulary, the Panama icon, and closing quietly.
|
||||
declare -a standalone=(search-web save-project open-project)
|
||||
#
|
||||
# The OS-parity commands are standalone for the same reason. A power menu that
|
||||
# needs the shell running is a power menu you cannot reach when the shell is
|
||||
# what broke; the pick-lists render through `vicinae dmenu` and act through
|
||||
# hyprctl; reminders are systemd timers. None of them has anything to ask the
|
||||
# shell for, and routing them through panama-action would only add a way for
|
||||
# them to stop working.
|
||||
declare -a standalone=(
|
||||
search-web save-project open-project
|
||||
lock-screen suspend-system log-out reboot-system power-off
|
||||
remind-me list-reminders pick-color
|
||||
switch-window force-quit-window kill-process ssh-hosts recent-files
|
||||
copy-password
|
||||
)
|
||||
|
||||
# Generated commands must match their source. A stale command dispatches to a
|
||||
# page that has been renamed or removed, and the launcher reports nothing wrong.
|
||||
|
||||
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