#!/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 chassis= battery= mains=<0|1|-> lid= # external= internal= 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 ` 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'