testsAndMisc/linux_configuration/fixes/lib/tests/test_bt_pairing_usb.sh
Krzysztof kuhy Rudnicki 4f2b0ccdf7 Dissolve linux_configuration/scripts/ and single_use/
Two levels of pure taxonomy carrying no information, and a 2-directory
cap leaves no room for them. Categories now sit directly under
linux_configuration/: features, fixes, utils, misc, fresh-install,
installers, diagnostics, gaming, boot_recovery, periodic_background, lib,
meta, tests. A script's own lib/ and tests/ are exempt from the cap, so
those stay put.

Depth violations 248 -> 139; roughly 40 of the remainder are inside
digital_wellbeing and hosts, deferred until the guard-lib repoint.

The moves are the easy half. What breaks is every path that counted
levels, and three classes of it had to be found by resolving paths rather
than reading diffs:

  - 52 relative `source` lines for lib/common.sh and lib/android.sh. The
    correct prefix differs per file: 40 were ../../ and 12 were ../../../,
    so a blanket replace would have fixed one set and broken the other.
  - REPO_ROOT in 7 fixes/lib/tests harnesses and setup_syncyomi_guard.sh,
    each walking up four levels from a directory now two levels shallower.
    This caused a real regression -- the fixes suite went red with
    "log_warn: command not found" -- caught by running it and comparing
    against the pre-move baseline worktree.
  - conftest.py's sys.path entries, which took the pytest gate down with
    14 collection errors, and the `# shellcheck source=` directives, which
    took shellcheck down with SC1091.

16 symlinks in $HOME broke, including the entire i3blocks bar. All
repointed, and cpu_monitor/memory/disk/time/volume were run to confirm
they emit real output rather than merely resolving.

Verified: 1798 tests pass at 100% coverage, 8 of 9 lib/tests suites pass
(the ninth needs the coverage jail, identically at baseline), pre-commit
is clean, and the two untracked secret configs are still ignored after
their gitignore paths changed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PJrqKERAo5QUeEHWS5Asgp
2026-08-23 18:53:30 +02:00

168 lines
7.3 KiB
Bash
Executable File

#!/usr/bin/env bash
# Tests for lib/bt_pairing.sh: fix_usb_autosuspend and
# _disable_usb_autosuspend, split from test_bt_pairing.sh for the 250-line cap.
set -uo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=bt_harness.sh
. "${SCRIPT_DIR}/bt_harness.sh"
# shellcheck source=../bt_pairing.sh
. "${FIXES_DIR}/lib/bt_pairing.sh"
# --- fix_usb_autosuspend ----------------------------------------------------
#
# This function globs the REAL /sys/bus/usb/devices/*/ -- the path is
# hardcoded, with no env override to point elsewhere. Rather than bind-mount
# or write anything, each case drives the branch it wants through the stubbed
# `lsusb` output:
#
# * a vendor:product of ffff:ffff matches no device on any machine, so the
# loop runs to exhaustion and takes the "could not find sysfs path" arm;
# * a vendor:product that IS present on this host reaches the power/control
# read, and _disable_usb_autosuspend is stubbed IN A SUBSHELL for those
# cases so the udev rule and the sysfs write never happen.
#
# A top-level redefinition would shadow the real function for every later
# assertion, so each override is confined to its own subshell.
# _lsusb_line TEXT — stub lsusb to emit a single line of TEXT.
_lsusb_line() {
printf '%s\n' "$1" >"${DEV}/lsusb_out"
_t_stub_cat lsusb lsusb_out
}
# No bluetooth device in lsusb at all: the function returns before any lookup.
bt_reset
_t_stub lsusb 'echo "Bus 001 Device 002: ID 046d:c077 Logitech, Inc. Mouse"'
out="$(fix_usb_autosuspend 2>&1)"
_t_contains "$out" "Checking USB autosuspend" \
"fix_usb_autosuspend: announces the check"
_t_eq "" "$(_t_fixes)" \
"fix_usb_autosuspend: applies no fix when lsusb lists no bluetooth device"
# A bluetooth line with no parseable ID: the usb_id guard returns early.
bt_reset
_lsusb_line "Bus 001 Device 003: Bluetooth adapter with no id field"
out="$(fix_usb_autosuspend 2>&1)"
_t_eq "" "$(_t_fixes)" \
"fix_usb_autosuspend: applies no fix when the bluetooth line has no ID"
# A well-formed ID that matches no real device: the sysfs scan finds nothing.
bt_reset
_lsusb_line "Bus 001 Device 004: ID ffff:ffff Bluetooth Device"
out="$(fix_usb_autosuspend 2>&1)"
_t_contains "$out" "Could not find sysfs path" \
"fix_usb_autosuspend: warns when no sysfs device matches the USB ID"
_t_eq "" "$(_t_fixes)" \
"fix_usb_autosuspend: applies no fix when the sysfs path is not found"
# A device whose power/control is already "on" needs no change. 046d:c077 is
# present on this host with control=on; skipped if that ever stops holding.
bt_reset
if grep -qx "on" /sys/bus/usb/devices/1-11/power/control 2>/dev/null; then
_lsusb_line "Bus 001 Device 005: ID 046d:c077 Bluetooth Device"
out="$(fix_usb_autosuspend 2>&1)"
_t_contains "$out" "already disabled" \
"fix_usb_autosuspend: reports autosuspend already disabled when control is on"
_t_eq "" "$(_t_fixes)" \
"fix_usb_autosuspend: applies no fix when control is already on"
else
_t_pass "fix_usb_autosuspend: SKIP already-on case (1-11 control is not 'on' here)"
fi
# _record_disable ARGS... — stand-in for the real writer, recording only.
_record_disable() {
printf 'disable_called %s\n' "$*" >>"${DEV}/fixes"
}
# A device whose power/control is "auto" gets the fix. _disable_usb_autosuspend
# is replaced INSIDE the subshell so this case proves the arguments handed to
# the writer without letting the real writer touch /sys or /etc; the real
# writer gets its own direct tests further down. The redefinition must stay in
# the subshell, since a top-level one would shadow the real function for every
# later assertion in this file.
bt_reset
if grep -qx "auto" /sys/bus/usb/devices/1-10/power/control 2>/dev/null; then
_lsusb_line "Bus 001 Device 006: ID 046d:0825 Bluetooth Device"
# _record_disable is defined at top level (so it is plainly a called
# function) but only BOUND to the writer's name inside the subshell.
out="$(
eval '_disable_usb_autosuspend() { _record_disable "$@"; }'
fix_usb_autosuspend 2>&1
)"
_t_contains "$out" "USB autosuspend is enabled" \
"fix_usb_autosuspend: warns when autosuspend is enabled"
_t_contains "$out" "Disabling USB autosuspend" \
"fix_usb_autosuspend: applies the disable fix when control is auto"
_t_contains "$(_t_fixes)" "disable_called /sys/bus/usb/devices/1-10/power/control 046d 0825" \
"fix_usb_autosuspend: passes the control path, vendor and product to the writer"
else
_t_pass "fix_usb_autosuspend: SKIP auto case (1-10 control is not 'auto' here)"
fi
# --- _disable_usb_autosuspend ----------------------------------------------
#
# Called directly, with the REAL body running. Both of its writes are
# redirected out of the way rather than stubbed:
#
# * the sysfs power/control file is already a parameter, so a tmpdir file
# covers it;
# * the udev rule path is the UDEV_RULES_DIR override the lib exposes for
# exactly this. A bind mount was rejected: run_all.sh runs UN-jailed in
# ci_mirror.sh and in CI, so binding /etc would make the coverage run
# safe and leave every bare run writing to the real file.
#
# udevadm is already stubbed by _bt_default_stubs.
bt_reset
control_file="${TEST_TMPDIR}/power_control"
rules_dir="${TEST_TMPDIR}/udev_rules"
rule_file="${rules_dir}/50-bluetooth-no-autosuspend.rules"
rm -rf "$rules_dir"
mkdir -p "$rules_dir"
echo "auto" >"$control_file"
out="$(UDEV_RULES_DIR="$rules_dir" \
_disable_usb_autosuspend "$control_file" "046d" "0825" 2>&1)"
_t_eq "on" "$(cat "$control_file")" \
"_disable_usb_autosuspend: writes 'on' to the sysfs power/control file"
_t_contains "$out" "Created persistent udev rule" \
"_disable_usb_autosuspend: reports creating the udev rule"
_t_contains "$(cat "$rule_file")" 'ATTR{idVendor}=="046d"' \
"_disable_usb_autosuspend: writes the vendor into the udev rule"
_t_contains "$(cat "$rule_file")" 'ATTR{idProduct}=="0825"' \
"_disable_usb_autosuspend: writes the product into the udev rule"
_t_contains "$(cat "$rule_file")" 'ATTR{power/control}="on"' \
"_disable_usb_autosuspend: pins power/control to on in the udev rule"
_t_contains "$(_t_calls)" "udevadm control --reload-rules" \
"_disable_usb_autosuspend: reloads udev rules after writing"
# Second call with the rule already present for this vendor: the guard must
# skip the rewrite, so the existing file survives untouched.
bt_reset
printf '%s\n' "# pre-existing rule for 046d" >"$rule_file"
echo "auto" >"$control_file"
out="$(UDEV_RULES_DIR="$rules_dir" \
_disable_usb_autosuspend "$control_file" "046d" "0825" 2>&1)"
_t_eq "# pre-existing rule for 046d" "$(cat "$rule_file")" \
"_disable_usb_autosuspend: leaves an existing rule for the same vendor alone"
_t_lacks "$out" "Created persistent udev rule" \
"_disable_usb_autosuspend: does not claim to create a rule that already existed"
_t_eq "on" "$(cat "$control_file")" \
"_disable_usb_autosuspend: still writes power/control on the idempotent path"
# A rule file that exists but covers a DIFFERENT vendor must be rewritten.
bt_reset
printf '%s\n' "# rule for some other vendor 1234" >"$rule_file"
echo "auto" >"$control_file"
out="$(UDEV_RULES_DIR="$rules_dir" \
_disable_usb_autosuspend "$control_file" "046d" "0825" 2>&1)"
_t_contains "$(cat "$rule_file")" 'ATTR{idVendor}=="046d"' \
"_disable_usb_autosuspend: rewrites a rule file that covers a different vendor"
echo
echo "bt_pairing (usb autosuspend): ${PASS} passed, ${FAIL} failed"
((FAIL == 0))