Nothing showed what was using the drive, and removable media was handled by a tray helper with no surface in Settings at all. One scroll rather than tabs: space above, the device below. Every other settings page is a scrolling card stack, and a tab would not be deep-linkable from the launcher command or from search. Three things the page has to get right, each now pinned by a contract, because each is a way it could quietly lie. / and /home are one btrfs filesystem sharing one pool of free space, and a page that copies df shows double the free space that exists. zram is a block device and is not storage; counting it as a drive overstates this machine by 8 GB. Unmount and eject refuse anything not on a removable drive, because the UI is what asks and a UI can be wrong. The cheap read -- layout, usage, health -- runs when the page opens, at around 90ms. Measuring what is filling the drive means walking every file, so it happens on request and says so rather than showing an empty list that reads as "nothing here". Partitioning and formatting are deliberately absent. A settings pane is the wrong place to put erasing a disk two clicks deep; the page opens GNOME Disks for that. Adding the page found a fourth hard-coded page list in ShellState. A page missing from it does not error -- openSettings() falls back to "home", so the launcher opens the wrong page and logs nothing. A registry contract now holds the three lists together. Claude-Session: https://claude.ai/code/session_01BRvzt4H8XXLPVH5MyYdk9L
149 lines
7.5 KiB
Bash
Executable File
149 lines
7.5 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
|
|
# Storage has to be honest about three things, and each of them is a way the
|
|
# page could quietly lie:
|
|
#
|
|
# 1. btrfs subvolumes mounted at / and /home are ONE filesystem sharing one
|
|
# pool of free space. df reports them as two, and a page that copies df
|
|
# shows the user twice the free space they have.
|
|
# 2. zram is a block device and is not storage. Counting it as a drive
|
|
# overstates how much disk this machine has by 8 GB.
|
|
# 3. unmount and eject must refuse anything that is not removable. The UI is
|
|
# the thing asking, and a UI can be wrong.
|
|
#
|
|
# The expensive folder scan is NOT exercised here: measuring a terabyte-scale
|
|
# library is not something a test should do on the machine someone is using.
|
|
|
|
set -uo pipefail
|
|
|
|
repo_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
|
helper="$repo_dir/config/dot/quickshell/scripts/panama-disks"
|
|
service="$repo_dir/config/dot/quickshell/services/Disks.qml"
|
|
page="$repo_dir/config/dot/quickshell/modules/settings/StoragePage.qml"
|
|
|
|
fail() {
|
|
printf 'disks contract: %s\n' "$1" >&2
|
|
exit 1
|
|
}
|
|
|
|
[[ -x "$helper" ]] || fail 'panama-disks is missing or not executable'
|
|
[[ -r "$service" ]] || fail 'the Disks service is missing'
|
|
[[ -r "$page" ]] || fail 'StoragePage is missing'
|
|
|
|
# ── The service reports failure rather than inventing data ───────────────────
|
|
grep -q 'pragma Singleton' "$service" || fail 'Disks is not a singleton'
|
|
grep -q 'property string lastError' "$service" || fail 'Disks cannot report a failure'
|
|
grep -q 'function refresh(): void' "$service" || fail 'Disks has no refresh'
|
|
grep -q 'function scan(): void' "$service" || fail 'Disks has no folder scan'
|
|
grep -qE 'command\s*:\s*"' "$service" && fail 'Process command must be an argument array'
|
|
|
|
# The expensive read must not run on open; that is the entire reason it is a
|
|
# separate command.
|
|
grep -q 'Component.onCompleted: Disks.refresh()' "$page" \
|
|
|| fail 'the page does not read the cheap snapshot when it opens'
|
|
grep -q 'Component.onCompleted: Disks.scan()' "$page" \
|
|
&& fail 'the page starts the expensive folder walk on open'
|
|
|
|
# Partitioning stays out, deliberately and visibly.
|
|
grep -qiE 'mkfs|sfdisk|parted|wipefs|dd if=' "$helper" \
|
|
&& fail 'the helper can partition or format, which this page deliberately does not do'
|
|
|
|
# ── Snapshot shape ───────────────────────────────────────────────────────────
|
|
snapshot="$("$helper" snapshot)" || fail 'snapshot failed'
|
|
command -v jq >/dev/null 2>&1 || { printf 'disks contract: SKIP (no jq)\n'; exit 0; }
|
|
|
|
jq -e '(.drives | type == "array") and (.filesystems | type == "array") and (.swap | type == "array")' \
|
|
<<<"$snapshot" >/dev/null || fail 'snapshot is missing one of drives, filesystems, or swap'
|
|
|
|
# zram is swap, never a drive.
|
|
jq -e '[.drives[].name | startswith("zram")] | any | not' <<<"$snapshot" >/dev/null \
|
|
|| fail 'zram is reported as a drive, which overstates how much disk this machine has'
|
|
jq -e '[.swap[].kind] | index("zram") != null or (. | length == 0)' <<<"$snapshot" >/dev/null \
|
|
|| fail 'zram is not reported as swap'
|
|
|
|
# One entry per DEVICE, not per mount point. Two entries for one device would
|
|
# be the doubled-free-space bug.
|
|
duplicates="$(jq -r '[.filesystems[].device] | group_by(.) | map(select(length > 1)) | flatten | join(", ")' <<<"$snapshot")"
|
|
[[ -z "$duplicates" ]] \
|
|
|| fail "these devices appear more than once, so their free space is counted twice: $duplicates"
|
|
|
|
# Every filesystem has to carry the numbers the page draws.
|
|
jq -e '[.filesystems[] | (.sizeBytes > 0) and (.mountpoints | length > 0)] | all' <<<"$snapshot" >/dev/null \
|
|
|| fail 'a filesystem is missing its size or its mount points'
|
|
|
|
# A shared filesystem lists "/" first, because that is what it is called.
|
|
jq -e '[.filesystems[] | select(.mountpoints | index("/")) | .mountpoints[0] == "/"] | all' \
|
|
<<<"$snapshot" >/dev/null || fail 'the root filesystem does not lead with "/"'
|
|
|
|
# ── Refusals, against a recorded tree ────────────────────────────────────────
|
|
# Never against real hardware. An earlier version of this test ran unmount on
|
|
# whatever device happened to hold the root filesystem and passed only because
|
|
# the kernel refused -- which tests the kernel, not the helper, and would have
|
|
# unmounted /home the moment nothing held it open.
|
|
work="$(mktemp -d /tmp/panama-disks.XXXXXX)"
|
|
trap 'rm -rf "$work"' EXIT
|
|
|
|
cat >"$work/tree.json" <<'FIXTURE'
|
|
{"blockdevices":[
|
|
{"name":"nvme0n1","path":"/dev/nvme0n1","size":2000398934016,"type":"disk","fstype":null,
|
|
"mountpoints":[],"model":"Fixed Disk","serial":"FIXED1","rm":false,"hotplug":false,"rota":false,
|
|
"fssize":null,"fsused":null,"fsavail":null,
|
|
"children":[{"name":"nvme0n1p1","path":"/dev/nvme0n1p1","size":2000398934016,"type":"part",
|
|
"fstype":"btrfs","mountpoints":["/"],"model":null,"serial":null,"rm":false,"hotplug":false,
|
|
"rota":false,"fssize":2000398934016,"fsused":1000000000000,"fsavail":1000398934016}]},
|
|
{"name":"sdb","path":"/dev/sdb","size":32000000000,"type":"disk","fstype":null,
|
|
"mountpoints":[],"model":"USB Stick","serial":"USB1","rm":true,"hotplug":true,"rota":false,
|
|
"fssize":null,"fsused":null,"fsavail":null,
|
|
"children":[{"name":"sdb1","path":"/dev/sdb1","size":32000000000,"type":"part",
|
|
"fstype":"vfat","mountpoints":["/run/media/user/USB"],"model":null,"serial":null,
|
|
"rm":true,"hotplug":true,"rota":false,"fssize":32000000000,"fsused":1000000000,
|
|
"fsavail":31000000000}]}
|
|
]}
|
|
FIXTURE
|
|
|
|
# A fake udisksctl, so a refusal that should never reach the system is visible
|
|
# as an empty log rather than inferred from an exit code.
|
|
mkdir -p "$work/bin"
|
|
cat >"$work/bin/udisksctl" <<'STUB'
|
|
#!/usr/bin/env bash
|
|
printf '%s\n' "$*" >>"$PANAMA_DISKS_CALL_LOG"
|
|
STUB
|
|
chmod +x "$work/bin/udisksctl"
|
|
|
|
export PANAMA_DISKS_LSBLK="$work/tree.json"
|
|
export PANAMA_DISKS_CALL_LOG="$work/calls"
|
|
export PATH="$work/bin:$PATH"
|
|
: >"$PANAMA_DISKS_CALL_LOG"
|
|
|
|
# The root filesystem is not removable, and no argument may make it so.
|
|
"$helper" unmount /dev/nvme0n1p1 >/dev/null 2>&1 \
|
|
&& fail 'unmount accepted the root filesystem'
|
|
"$helper" eject /dev/nvme0n1 >/dev/null 2>&1 \
|
|
&& fail 'eject accepted a fixed drive'
|
|
[[ ! -s "$PANAMA_DISKS_CALL_LOG" ]] \
|
|
|| fail "a refused device still reached udisksctl: $(cat "$PANAMA_DISKS_CALL_LOG")"
|
|
|
|
"$helper" unmount 'not-a-device' >/dev/null 2>&1 \
|
|
&& fail 'unmount accepted something that is not a device path'
|
|
"$helper" unmount '/dev/../etc/passwd' >/dev/null 2>&1 \
|
|
&& fail 'unmount accepted a path escaping /dev'
|
|
"$helper" unmount '/dev/sdb99' >/dev/null 2>&1 \
|
|
&& fail 'unmount accepted a device that does not exist'
|
|
[[ ! -s "$PANAMA_DISKS_CALL_LOG" ]] \
|
|
|| fail "a rejected path still reached udisksctl: $(cat "$PANAMA_DISKS_CALL_LOG")"
|
|
|
|
# The removable one is allowed, and reaches udisksctl with separate arguments.
|
|
"$helper" unmount /dev/sdb1 >/dev/null 2>&1 \
|
|
|| fail 'unmount refused a removable partition'
|
|
grep -Fxq 'unmount -b /dev/sdb1' "$PANAMA_DISKS_CALL_LOG" \
|
|
|| fail "unmount did not reach udisksctl correctly: $(cat "$PANAMA_DISKS_CALL_LOG")"
|
|
|
|
"$helper" bogus-command >/dev/null 2>&1 \
|
|
&& fail 'an unknown command was accepted'
|
|
|
|
unset PANAMA_DISKS_LSBLK PANAMA_DISKS_CALL_LOG
|
|
|
|
printf 'disks contract: PASS (%d drives, %d filesystems)\n' \
|
|
"$(jq '.drives | length' <<<"$snapshot")" \
|
|
"$(jq '.filesystems | length' <<<"$snapshot")"
|