214 lines
9.9 KiB
Bash
Executable File
214 lines
9.9 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
|
|
# Network & Devices reads real NetworkManager and BlueZ state.
|
|
#
|
|
# Both of the bugs this contract exists to prevent were silent. Neither logged
|
|
# anything; both produced a page that looked fine and told the user something
|
|
# false:
|
|
#
|
|
# * the device lookups used enum names that do not exist
|
|
# (NetworkDeviceType.Wifi rather than DeviceType.Wifi), so they returned
|
|
# null and the page reported "No Wi-Fi adapter" on a machine whose Wi-Fi was
|
|
# connected;
|
|
# * signalStrength is 0.0-1.0, not a percentage, so thresholds written for
|
|
# 0-100 put every network including the connected one in the bottom bucket.
|
|
#
|
|
# So this compares what the service resolves against what NetworkManager itself
|
|
# reports, rather than merely checking the service does not crash.
|
|
|
|
set -euo pipefail
|
|
|
|
repo_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
|
harness="$repo_dir/config/dot/quickshell/connectivity-harness.qml"
|
|
service="$repo_dir/config/dot/quickshell/services/Connectivity.qml"
|
|
page="$repo_dir/config/dot/quickshell/modules/settings/ConnectivityPage.qml"
|
|
|
|
fail() {
|
|
printf 'connectivity contract: %s\n' "$1" >&2
|
|
exit 1
|
|
}
|
|
|
|
# ── Connectivity stays native ────────────────────────────────────────────────
|
|
#
|
|
# This service reads NetworkManager and BlueZ through Quickshell's own bindings,
|
|
# never by shelling out. That is not a style preference: a subprocess per read
|
|
# turns a property binding into a fork on every repaint, and it loses the change
|
|
# signals the whole page is built on -- the page would go back to polling and
|
|
# would go stale between polls.
|
|
#
|
|
# When Connections grew a helper (`panama-network`, driven by NetworkTools), the
|
|
# obvious shortcut was to let this service reach for it too. It must not. Every
|
|
# nmcli invocation belongs on the far side of that helper; what stays here is
|
|
# the native state and the wrappers over it.
|
|
for path in "$service" "$page"; do
|
|
[[ -r "$path" ]] || fail "missing $path"
|
|
done
|
|
service_code="$(grep -vE '^\s*//' "$service")"
|
|
! grep -qE '\bnmcli\b|\bProcess\b|execDetached|exec\(' <<<"$service_code" \
|
|
|| fail 'Connectivity.qml shells out; NetworkManager reads here are native, and nmcli belongs in panama-network'
|
|
|
|
# The page used to write NetworkManager state straight from a switch
|
|
# (`Networking.wifiEnabled = value`, `adapter.enabled = value`), which put two
|
|
# owners on one piece of state and left the service unable to react to its own
|
|
# change. The wrappers exist so the page has exactly one way in.
|
|
grep -q 'function setWifiEnabled' <<<"$service_code" \
|
|
|| fail 'the service has no Wi-Fi wrapper, so the page has to write NetworkManager itself'
|
|
grep -q 'function setBluetoothEnabled' <<<"$service_code" \
|
|
|| fail 'the service has no Bluetooth wrapper, so the page has to write the adapter itself'
|
|
|
|
page_code="$(grep -vE '^\s*//' "$page")"
|
|
grep -q 'Connectivity.setWifiEnabled' <<<"$page_code" \
|
|
|| fail 'the Wi-Fi switch does not go through the service'
|
|
grep -q 'Connectivity.setBluetoothEnabled' <<<"$page_code" \
|
|
|| fail 'the Bluetooth switch does not go through the service'
|
|
! grep -qE 'Networking\.wifiEnabled\s*=' <<<"$page_code" \
|
|
|| fail 'the page still writes Networking.wifiEnabled directly, bypassing the service'
|
|
! grep -qE 'adapter\.enabled\s*=' <<<"$page_code" \
|
|
|| fail 'the page still writes the Bluetooth adapter directly, bypassing the service'
|
|
|
|
# The card that sent people to GNOME for Wi-Fi and networking is gone, and with
|
|
# it the claim that Fedora owns this page. gnome-handoff-contract pins the rule;
|
|
# this pins the specific rows, because they are what the card was made of.
|
|
! grep -q 'Owned by Fedora' <<<"$page_code" \
|
|
|| fail 'the "Owned by Fedora" card is back on a page that now manages the network itself'
|
|
! grep -qE 'openGnomePanel\("(wifi|network)"' <<<"$page_code" \
|
|
|| fail 'Connections still hands the user to GNOME for something it now does'
|
|
|
|
# ── Every enum member the service names must exist ───────────────────────────
|
|
#
|
|
# This is the first bug in this file's header, generalized. `NetworkDeviceType
|
|
# .Wifi` does not exist, so the lookup returned null and the page said "No Wi-Fi
|
|
# adapter" on a machine whose Wi-Fi was connected. The same thing happened again
|
|
# in securityLabel: `WifiSecurityType.Wep`, `.Wpa`, `.Wpa2`, `.Wpa3` and
|
|
# `.Enterprise` are not members either -- the real ones are WpaPsk, Wpa2Psk,
|
|
# Sae, StaticWep, WpaEap and so on -- so every switch arm compared against
|
|
# `undefined`, nothing ever matched, and every secured network was labeled with
|
|
# the fallthrough. Plausible names, no error, wrong answer.
|
|
#
|
|
# QML resolves an unknown enum member to undefined and says nothing, so no test
|
|
# that runs the code can notice. This reads the member list out of the installed
|
|
# Quickshell's own type description, so it stays true across upgrades rather
|
|
# than becoming a second hand-kept list that can drift the same way.
|
|
networking_types=""
|
|
for candidate in /usr/lib64/qt6/qml/Quickshell/Networking/quickshell-network.qmltypes \
|
|
/usr/lib/qt6/qml/Quickshell/Networking/quickshell-network.qmltypes; do
|
|
[[ -r "$candidate" ]] && { networking_types="$candidate"; break; }
|
|
done
|
|
if [[ -z "$networking_types" ]]; then
|
|
printf 'connectivity contract: NOTE (no Quickshell.Networking qmltypes found; enum members unchecked)\n'
|
|
else
|
|
python3 - "$service" "$networking_types" <<'PY' || fail 'the service names enum members that do not exist, which QML resolves to undefined without complaining'
|
|
import re
|
|
import sys
|
|
|
|
service_path, types_path = sys.argv[1:3]
|
|
types_text = open(types_path, encoding="utf-8").read()
|
|
|
|
# Each exported enum singleton: the name QML sees, and the members it has.
|
|
enums = {}
|
|
for block in re.findall(r"Component \{.*?\n \}", types_text, re.S):
|
|
export = re.search(r'exports: \["Quickshell\.Networking/([A-Za-z0-9_]+) ', block)
|
|
values = re.search(r"values: \[(.*?)\]", block, re.S)
|
|
if export and values:
|
|
enums[export.group(1)] = set(re.findall(r'"([A-Za-z0-9_]+)"', values.group(1)))
|
|
if not enums:
|
|
print(f"no enums could be read from {types_path}", file=sys.stderr)
|
|
raise SystemExit(1)
|
|
|
|
source = "\n".join(line for line in open(service_path, encoding="utf-8")
|
|
if not line.lstrip().startswith("//"))
|
|
|
|
bad = []
|
|
for enum_name, members in enums.items():
|
|
for member in set(re.findall(rf"\b{re.escape(enum_name)}\.([A-Za-z0-9_]+)\b", source)):
|
|
if member not in members:
|
|
bad.append(f"{enum_name}.{member} (real members: {', '.join(sorted(members))})")
|
|
|
|
# A switch on a security type that names no member of the enum at all is the
|
|
# same failure wearing a different hat, so the reference must be there too.
|
|
if "securityLabel" in source and not re.search(r"\bWifiSecurityType\.", source):
|
|
bad.append("securityLabel decides security without naming a WifiSecurityType member")
|
|
|
|
if bad:
|
|
print("\n".join(bad), file=sys.stderr)
|
|
raise SystemExit(1)
|
|
PY
|
|
fi
|
|
|
|
if [[ "${PANAMA_CONNECTIVITY_STATIC_ONLY:-0}" == "1" ]]; then
|
|
printf 'connectivity contract: PASS (static)\n'
|
|
exit 0
|
|
fi
|
|
|
|
command -v nmcli >/dev/null || fail 'nmcli is needed to check the service against reality'
|
|
|
|
run() { qs -p "$harness" "$@"; }
|
|
harness_pid=""
|
|
|
|
cleanup() {
|
|
run ipc call connectivity-test setActive false >/dev/null 2>&1 || true
|
|
# By PID: never `pkill -f connectivity-harness`, which also matches the
|
|
# shell running this script.
|
|
[[ -n "$harness_pid" ]] && kill "$harness_pid" >/dev/null 2>&1 || true
|
|
}
|
|
trap cleanup EXIT
|
|
|
|
qs -p "$harness" --daemonize >/dev/null
|
|
for _ in $(seq 1 40); do
|
|
run ipc show 2>/dev/null | rg -q '^target connectivity-test$' && break
|
|
sleep 0.1
|
|
done
|
|
run ipc show 2>/dev/null | rg -q '^target connectivity-test$' || fail 'test IPC target did not start'
|
|
harness_pid="$(run list | awk '/Process ID:/ { print $3; exit }')"
|
|
|
|
# Scanning only runs while the page says it is visible.
|
|
run ipc call connectivity-test setActive true >/dev/null
|
|
sleep 3
|
|
|
|
state="$(run ipc call connectivity-test status)"
|
|
|
|
# ── Devices the service finds must match the ones NetworkManager reports ─────
|
|
nm_wifi="$(nmcli -t -f DEVICE,TYPE device | awk -F: '$2 == "wifi" { print $1; exit }')"
|
|
nm_wired="$(nmcli -t -f DEVICE,TYPE,STATE device | awk -F: '$2 == "ethernet" && $3 == "connected" { print $1; exit }')"
|
|
|
|
if [[ -n "$nm_wifi" ]]; then
|
|
[[ "$(jq -r .wifiDevice <<<"$state")" == "$nm_wifi" ]] \
|
|
|| fail "NetworkManager reports Wi-Fi device '$nm_wifi' but the service found '$(jq -r .wifiDevice <<<"$state")'"
|
|
fi
|
|
if [[ -n "$nm_wired" ]]; then
|
|
[[ "$(jq -r .wiredConnected <<<"$state")" == "true" ]] \
|
|
|| fail "NetworkManager reports '$nm_wired' connected but the service says it is not"
|
|
fi
|
|
|
|
# ── Signal strength is a ratio, and the labels must reflect that ─────────────
|
|
while IFS='|' read -r value expect; do
|
|
got="$(run ipc call connectivity-test labelFor "$value")"
|
|
[[ "$got" == "$expect" ]] || fail "signal $value labeled '$got', expected '$expect'"
|
|
done <<'CASES'
|
|
1.0|Excellent
|
|
0.85|Excellent
|
|
0.6|Good
|
|
0.4|Fair
|
|
0.1|Weak
|
|
0.0|No signal
|
|
CASES
|
|
|
|
# If a network is connected, it must not be described as the weakest possible
|
|
# thing -- that was the visible symptom of reading the ratio as a percentage.
|
|
active_ssid="$(jq -r .activeSsid <<<"$state")"
|
|
if [[ -n "$active_ssid" ]]; then
|
|
strength="$(jq -r .activeStrength <<<"$state")"
|
|
awk -v s="$strength" 'BEGIN { exit !(s >= 0 && s <= 1) }' \
|
|
|| fail "signalStrength $strength is outside 0.0-1.0; the label buckets assume a ratio"
|
|
fi
|
|
|
|
# ── Bluetooth ────────────────────────────────────────────────────────────────
|
|
if [[ "$(bluetoothctl list 2>/dev/null | wc -l)" -gt 0 ]]; then
|
|
[[ "$(jq -r .adapter <<<"$state")" == "true" ]] \
|
|
|| fail 'an adapter is present but the service did not find it'
|
|
fi
|
|
|
|
trap - EXIT
|
|
cleanup
|
|
printf 'connectivity contract: PASS\n'
|