feat: add adaptive SMART refresh controls (#96)
This commit is contained in:
@@ -26,13 +26,17 @@ local available = {
|
||||
|
||||
local rawFixture = {
|
||||
schema = 2,
|
||||
collector_version = mode == "outdated-raw-cache" and "0.6.0" or "2.0.0",
|
||||
collector_version = mode == "outdated-raw-cache" and "0.6.0" or "2.0.1",
|
||||
collection_id = "fixture-collection-id",
|
||||
generated_at_epoch = 1700000000,
|
||||
lsblk = { blockdevices = {} },
|
||||
smart = {},
|
||||
}
|
||||
|
||||
if mode == "raw-cache" or mode == "outdated-raw-cache" then
|
||||
files["/usr/local/libexec/noctalia-drive-health/manage-collector.sh"] = "installed"
|
||||
end
|
||||
|
||||
local function translate(key, substitutions)
|
||||
local value = key
|
||||
for name, replacement in pairs(substitutions or {}) do
|
||||
@@ -47,6 +51,7 @@ noctalia = {
|
||||
if key == "system_collector_enabled" then
|
||||
return collectorEnabled
|
||||
end
|
||||
if key == "full_smart_refresh_minutes" then return 15 end
|
||||
return nil
|
||||
end,
|
||||
pluginDir = function() return "/mock/plugin" end,
|
||||
@@ -166,14 +171,14 @@ elseif mode == "probe-timeout" then
|
||||
return
|
||||
elseif mode == "missing-lsblk" then
|
||||
assert(dependencies.ready == false and dependencies.blocking == true, "missing lsblk was not blocking")
|
||||
assert(dependencies.install_command == "sudo pacman -S --needed util-linux", "wrong lsblk install command")
|
||||
assert(dependencies.install_command == "pkexec pacman -S --needed --noconfirm util-linux", "wrong lsblk install command")
|
||||
assert(launchedCommand == nil, "collector launched without lsblk")
|
||||
assert(snapshot.collector_error ~= nil, "blocking dependency did not publish an error")
|
||||
print("collector initialization test passed: " .. mode)
|
||||
return
|
||||
elseif mode == "missing-smartctl" then
|
||||
assert(dependencies.ready == false and dependencies.blocking == false, "missing smartctl blocked inventory")
|
||||
assert(dependencies.install_command == "sudo pacman -S --needed smartmontools", "wrong smartctl install command")
|
||||
assert(dependencies.install_command == "pkexec pacman -S --needed --noconfirm smartmontools", "wrong smartctl install command")
|
||||
assert(launchedCommand and launchedCommand:match("collect_raw%.sh"), "fallback collector did not launch")
|
||||
print("collector initialization test passed: " .. mode)
|
||||
return
|
||||
@@ -181,8 +186,9 @@ elseif mode == "raw-cache" then
|
||||
assert(snapshot.source == "system-cache", "raw cache was not normalized")
|
||||
assert(snapshot.collection_id == "fixture-collection-id", "raw cache lost its collection ID")
|
||||
assert(snapshot.system_collector.status == "healthy"
|
||||
and snapshot.system_collector.version == "2.0.0"
|
||||
and snapshot.system_collector.expected_version == "2.0.0",
|
||||
and snapshot.system_collector.version == "2.0.1"
|
||||
and snapshot.system_collector.expected_version == "2.0.1"
|
||||
and snapshot.system_collector.smart_refresh_minutes == 15,
|
||||
"current system collector was not reported healthy")
|
||||
assert(not (launchedCommand or ""):match("collect_raw%.sh"), "collector launched despite a fresh raw cache")
|
||||
print("collector initialization test passed: " .. mode)
|
||||
@@ -191,7 +197,7 @@ elseif mode == "outdated-raw-cache" then
|
||||
assert(snapshot.source == "system-cache", "outdated raw cache was not normalized")
|
||||
assert(snapshot.system_collector.status == "upgrade-required"
|
||||
and snapshot.system_collector.version == "0.6.0"
|
||||
and snapshot.system_collector.expected_version == "2.0.0",
|
||||
and snapshot.system_collector.expected_version == "2.0.1",
|
||||
"older system collector did not request an upgrade")
|
||||
assert(not (launchedCommand or ""):match("collect_raw%.sh"), "collector launched despite a fresh raw cache")
|
||||
assert(#notifications == 1 and notifications[1].title == "collector.update_title"
|
||||
@@ -465,6 +471,24 @@ local healthyRaw = assert(normalizeRaw({
|
||||
assert(healthyRaw.disks[1].smart_available == true and healthyRaw.disks[1].smart_error == nil,
|
||||
"healthy SMART data retained a contradictory error message")
|
||||
|
||||
directories["/sys/class/nvme/nvme0"] = { "hwmon0" }
|
||||
files["/sys/class/nvme/nvme0/hwmon0/temp1_input"] = "51000\n"
|
||||
local liveTemperatureRaw = assert(normalizeRaw({
|
||||
schema = 2, generated_at_epoch = 1700000000,
|
||||
lsblk = { blockdevices = { {
|
||||
name = "nvme0n1", kname = "nvme0n1", path = "/dev/nvme0n1", type = "disk", tran = "nvme",
|
||||
rota = false, size = 1000000000, model = "Live Temperature NVMe", serial = "LIVE1",
|
||||
mountpoints = {}, children = {},
|
||||
} } },
|
||||
smart = { { requested_device = "/dev/nvme0", payload = {
|
||||
smart_status = { passed = true }, temperature = { current = 42 },
|
||||
} } },
|
||||
}, "test"))
|
||||
assert(liveTemperatureRaw.disks[1].temperature_c == 51
|
||||
and liveTemperatureRaw.disks[1].hotspot_temperature_c == 51
|
||||
and liveTemperatureRaw.disks[1].temperature_source == "sysfs",
|
||||
"fresh sysfs temperature did not override the stale SMART-cache value")
|
||||
|
||||
local sleeping = assert(normalizeRaw({
|
||||
schema = 2, generated_at_epoch = 1700000000,
|
||||
lsblk = { blockdevices = { {
|
||||
@@ -511,6 +535,7 @@ local oversizedId = assert(normalizeRaw({
|
||||
assert(oversizedId.collection_id == nil, "oversized collection ID was preserved")
|
||||
|
||||
files["/usr/local/libexec/noctalia-drive-health/collect_raw.sh"] = "installed"
|
||||
files["/usr/local/libexec/noctalia-drive-health/manage-collector.sh"] = "installed"
|
||||
collectorEnabled = true
|
||||
state.collector_snapshot = { summary = {}, system_collector = { status = "healthy" } }
|
||||
publishError("fixture failure", { ready = true, blocking = false })
|
||||
|
||||
@@ -77,10 +77,12 @@ state.snapshot = {
|
||||
dependencies = { ready = true },
|
||||
system_collector = { enabled = true, installed = true, status = "healthy", version = "1.0.0",
|
||||
expected_version = "1.0.0", helper_available = true, authorization_available = true,
|
||||
enable_command = "sudo systemctl enable --now noctalia-drive-health.timer",
|
||||
disable_command = "sudo systemctl disable --now noctalia-drive-health.timer",
|
||||
install_command = "sudo '/mock/plugin/packaging/install-system-collector.sh'",
|
||||
uninstall_command = "sudo '/mock/plugin/packaging/uninstall-system-collector.sh'" },
|
||||
enable_command = "pkexec '/usr/local/libexec/noctalia-drive-health/manage-collector.sh' start",
|
||||
disable_command = "pkexec '/usr/local/libexec/noctalia-drive-health/manage-collector.sh' pause",
|
||||
install_command = "pkexec '/mock/plugin/packaging/install-system-collector.sh' --interval-minutes 15",
|
||||
uninstall_command = "pkexec '/usr/local/libexec/noctalia-drive-health/uninstall-collector.sh'",
|
||||
interval_command = "pkexec '/usr/local/libexec/noctalia-drive-health/set-collector-interval.sh' 15",
|
||||
smart_refresh_minutes = 15 },
|
||||
summary = { disk_count = 2, ssd_count = 1, hdd_count = 1, smart_available_count = 2,
|
||||
ssd_smart_available_count = 1, hottest_drive_temperature_c = 70,
|
||||
hottest_drive_name = "Fixture SSD", hottest_ssd_temperature_c = 70,
|
||||
@@ -180,8 +182,14 @@ assert(containsText(rendered, "collector.settings_title")
|
||||
and containsText(rendered, "collector.full_features"),
|
||||
"collector settings did not explain Basic and Full SMART capabilities")
|
||||
onPauseCollectorClicked()
|
||||
assert(terminalCommand == "sudo systemctl disable --now noctalia-drive-health.timer",
|
||||
"collector settings did not expose the explicit service pause command")
|
||||
assert(asyncCommand == "pkexec '/usr/local/libexec/noctalia-drive-health/manage-collector.sh' pause",
|
||||
"collector settings did not use the fixed Polkit pause helper")
|
||||
assert(terminalCommand == nil, "collector pause opened a terminal")
|
||||
assert(containsText(rendered, "privileged_action.authorizing"),
|
||||
"collector pause did not show authorization progress")
|
||||
asyncCallback({ exitCode = 126, stdout = "", stderr = "", timedOut = false })
|
||||
assert(errors[#errors].body:match("privileged_action.failed"),
|
||||
"cancelled collector pause did not report a useful error")
|
||||
terminalCommand = nil
|
||||
onOpenPluginSettingsClicked()
|
||||
assert(asyncCommand == "noctalia msg settings-open plugins",
|
||||
@@ -383,10 +391,20 @@ onCancelDrivePreferencesClicked()
|
||||
|
||||
state.snapshot.dependencies = {
|
||||
ready = false, blocking = true, missing_text = "lsblk (util-linux)",
|
||||
install_command = "sudo pacman -S --needed util-linux", package_manager = "pacman", can_install = true,
|
||||
install_command = "pkexec pacman -S --needed --noconfirm util-linux", package_manager = "pacman", can_install = true,
|
||||
}
|
||||
watchers.snapshot(state.snapshot)
|
||||
assert(containsText(rendered, "dependencies.title"), "missing dependency card did not render")
|
||||
asyncCommand = nil
|
||||
asyncCallback = nil
|
||||
terminalCommand = nil
|
||||
onInstallDependenciesClicked()
|
||||
assert(asyncCommand == "pkexec pacman -S --needed --noconfirm util-linux",
|
||||
"dependency installation did not use Polkit")
|
||||
assert(terminalCommand == nil, "dependency installation opened a terminal")
|
||||
asyncCallback({ exitCode = 126, stdout = "", stderr = "", timedOut = false })
|
||||
assert(errors[#errors].body:match("privileged_action.failed"),
|
||||
"cancelled dependency installation did not report an error")
|
||||
|
||||
state.snapshot.dependencies = { ready = true }
|
||||
state.snapshot.disks = { state.snapshot.disks[2] }
|
||||
@@ -401,4 +419,39 @@ assert(not containsText(rendered, "summary.lowest_ssd_life"),
|
||||
assert(countText(rendered, "Fixture HDD") >= 2,
|
||||
"HDD-only temperature summary did not identify its drive")
|
||||
|
||||
-- Changing the full-SMART schedule uses Polkit in the background, rather than
|
||||
-- placing a password-bearing command in a terminal. Keep the panel open so
|
||||
-- the in-progress and error states are visible to the user.
|
||||
onToggleCollectorSettingsClicked()
|
||||
asyncCommand = nil
|
||||
asyncCallback = nil
|
||||
terminalCommand = nil
|
||||
local refreshNonceBeforeInterval = state.refresh_nonce or 0
|
||||
onApplyCollectorIntervalClicked()
|
||||
assert(asyncCommand == "pkexec '/usr/local/libexec/noctalia-drive-health/set-collector-interval.sh' 15",
|
||||
"collector interval update did not use the Polkit helper")
|
||||
assert(terminalCommand == nil, "collector interval update opened a terminal")
|
||||
assert(containsText(rendered, "collector.interval_authorizing"),
|
||||
"collector interval authorization progress was not shown")
|
||||
local intervalSuccess = assert(asyncCallback, "collector interval callback was not registered")
|
||||
intervalSuccess({ exitCode = 0, stdout = "", stderr = "", timedOut = false })
|
||||
assert((state.refresh_nonce or 0) == refreshNonceBeforeInterval + 1,
|
||||
"successful collector interval update did not refresh the panel")
|
||||
assert(notifications[#notifications].body == "collector.interval_applied",
|
||||
"successful collector interval update did not notify the user")
|
||||
|
||||
onApplyCollectorIntervalClicked()
|
||||
local intervalCancelled = assert(asyncCallback, "second collector interval callback was not registered")
|
||||
intervalCancelled({ exitCode = 126, stdout = "", stderr = "", timedOut = false })
|
||||
assert(containsText(rendered, "collector.interval_failed"),
|
||||
"cancelled collector interval authorization did not render an inline result")
|
||||
assert(errors[#errors].body == "collector.interval_cancelled",
|
||||
"cancelled collector interval authorization did not notify the user")
|
||||
|
||||
onApplyCollectorIntervalClicked()
|
||||
local intervalRejected = assert(asyncCallback, "failed collector interval callback was not registered")
|
||||
intervalRejected({ exitCode = 1, stdout = "", stderr = "Authentication failed", timedOut = false })
|
||||
assert(errors[#errors].body == "Authentication failed",
|
||||
"collector interval authentication failure did not show the returned error")
|
||||
|
||||
print("panel rendering tests passed")
|
||||
|
||||
@@ -7,7 +7,7 @@ fixture_bin="$project_dir/tests/fixtures/bin"
|
||||
payload=$(PATH="$fixture_bin:$PATH" sh "$project_dir/scripts/collect_raw.sh")
|
||||
printf '%s\n' "$payload" | jq -e '
|
||||
.schema == 2
|
||||
and .collector_version == "2.0.0"
|
||||
and .collector_version == "2.0.1"
|
||||
and (.collection_id | type == "string" and length > 0)
|
||||
and ([.lsblk.blockdevices[].name] | sort) == ["/dev/nvme0n1", "/dev/sda", "/dev/zram0"]
|
||||
and (.smart | length) == 2
|
||||
|
||||
@@ -4,6 +4,8 @@ set -eu
|
||||
project_dir=$(CDPATH='' cd -- "$(dirname -- "$0")/.." && pwd)
|
||||
service_template="$project_dir/packaging/noctalia-drive-health.service.in"
|
||||
timer="$project_dir/packaging/noctalia-drive-health.timer"
|
||||
interval_script="$project_dir/packaging/set-collector-interval.sh"
|
||||
manage_script="$project_dir/packaging/manage-collector.sh"
|
||||
fixture=$(mktemp -d "${TMPDIR:-/tmp}/drive-health-packaging.XXXXXX")
|
||||
trap 'rm -rf -- "$fixture"' EXIT HUP INT TERM
|
||||
|
||||
@@ -14,6 +16,12 @@ grep -q '^Group=1000$' "$fixture/noctalia-drive-health.service"
|
||||
grep -q '^RuntimeDirectoryMode=0750$' "$fixture/noctalia-drive-health.service"
|
||||
grep -q '^UMask=0027$' "$fixture/noctalia-drive-health.service"
|
||||
grep -q '^Unit=noctalia-drive-health.service$' "$fixture/noctalia-drive-health.timer"
|
||||
grep -q '^OnUnitActiveSec=15min$' "$fixture/noctalia-drive-health.timer"
|
||||
|
||||
bash -n "$interval_script"
|
||||
bash -n "$manage_script"
|
||||
grep -q 'packaging/manage-collector.sh' "$project_dir/packaging/install-system-collector.sh"
|
||||
grep -q 'packaging/uninstall-system-collector.sh' "$project_dir/packaging/install-system-collector.sh"
|
||||
|
||||
if grep -R -q 'noctalia-smart-monito[r]' "$project_dir"; then
|
||||
echo "generic legacy collector namespace must not be read, modified, or removed" >&2
|
||||
@@ -27,7 +35,7 @@ fi
|
||||
|
||||
declared_dependencies=$(sed -n 's/^dependencies = \[\(.*\)\]$/\1/p' "$project_dir/plugin.toml")
|
||||
for dependency in \
|
||||
lsblk smartctl sh date dirname mkdir mktemp rm sed cat chmod mv sudo env bash \
|
||||
lsblk smartctl sh date dirname mkdir mktemp rm sed cat chmod mv env bash \
|
||||
install systemctl pkexec id tr pacman apt-get dnf zypper apk xbps-install emerge; do
|
||||
case "$declared_dependencies" in
|
||||
*\"$dependency\"*) ;;
|
||||
|
||||
Reference in New Issue
Block a user