diff options
| author | Danilo M. <danix@danix.xyz> | 2026-09-16 19:23:35 +0200 |
|---|---|---|
| committer | Danilo M. <danix@danix.xyz> | 2026-09-16 19:23:35 +0200 |
| commit | f822a250f9b2759207d64cc53b3d80c0c08d2082 (patch) | |
| tree | b3ea539a252b7d7c34b08410b371cd58f583ca05 | |
| parent | 4538896ce65156883dc44cc3355435d40d7e682b (diff) | |
| download | conky-theme-udt-f822a250f9b2759207d64cc53b3d80c0c08d2082.tar.gz conky-theme-udt-f822a250f9b2759207d64cc53b3d80c0c08d2082.zip | |
feat: add /proc and /sys parsers with fixture tests
Parsers take file contents as a string, not a path, so they test
against committed fixtures with no filesystem mocking.
hwmon is globbed by its name file rather than a fixed index, carried
over from the old config: indices drift across kernel reorders and a
stale one silently reads a different chip.
Failure returns nil, never 0. A sensor reading 0 C is legitimate, so 0
cannot double as an error value.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
| -rw-r--r-- | lib/data.lua | 75 | ||||
| -rw-r--r-- | test/fixtures/proc_meminfo | 7 | ||||
| -rw-r--r-- | test/fixtures/proc_stat | 5 | ||||
| -rw-r--r-- | test/fixtures/temp1_input | 1 | ||||
| -rw-r--r-- | test/test_data.lua | 39 |
5 files changed, 127 insertions, 0 deletions
diff --git a/lib/data.lua b/lib/data.lua new file mode 100644 index 0000000..96fc172 --- /dev/null +++ b/lib/data.lua @@ -0,0 +1,75 @@ +-- Readers for /proc and /sys. +-- +-- Every function takes the file *contents* as a string rather than reading the +-- file itself, so the parsers are testable against fixtures without mocking the +-- filesystem. The thin read-and-parse wrappers live at the bottom. + +local M = {} + +-- Aggregate CPU jiffies from /proc/stat. Returns total, idle. +-- iowait counts as idle: a core waiting on disk is not doing work, and +-- treating it as busy makes a disk-bound system look CPU-bound. +function M.cpu_times(stat) + local line = stat:match('^cpu%s+([^\n]+)') + if not line then return nil, nil end + local v = {} + for n in line:gmatch('%d+') do v[#v + 1] = tonumber(n) end + if #v < 5 then return nil, nil end + local total = 0 + for _, n in ipairs(v) do total = total + n end + return total, v[4] + v[5] -- idle + iowait +end + +-- /proc/meminfo, values in kB as the file states them. +function M.mem_info(meminfo) + local function field(name) + return tonumber(meminfo:match(name .. ':%s+(%d+)')) + end + local total = field('MemTotal') + local available = field('MemAvailable') + if not (total and available) then return nil end + return { + total = total, + free = field('MemFree'), + available = available, + -- MemAvailable already excludes reclaimable cache, so this is the figure a + -- user recognises as "used", unlike total-free which counts cache. + used = total - available, + } +end + +-- hwmon temp*_input is millidegrees C. Returns whole degrees, or nil. +-- nil rather than 0 on failure: 0 C is a legitimate reading. +function M.millidegrees(s) + if not s then return nil end + local n = tonumber(s:match('^%s*(-?%d+)')) + if not n then return nil end + return math.floor(n / 1000 + 0.5) +end + +-- Read a whole file, returning nil if it cannot be read. Used by the wrappers +-- so a vanished sysfs path degrades to nil instead of raising. +function M.slurp(path) + local f = io.open(path, 'r') + if not f then return nil end + local s = f:read('*a') + f:close() + return s +end + +-- Find a hwmon directory by the exact contents of its `name` file. +-- +-- Globbing by name, never by a fixed hwmon index: indices drift across kernel +-- and hardware reorders, and a stale index silently reports a different chip. +-- Carried over from the previous conky config, where this was the hard-won bit. +function M.hwmon_dir(name) + local p = io.popen('grep -lx ' .. ("%q"):format(name) + .. ' /sys/class/hwmon/hwmon*/name 2>/dev/null') + if not p then return nil end + local hit = p:read('*l') + p:close() + if not hit then return nil end + return hit:match('^(.*)/name$') +end + +return M diff --git a/test/fixtures/proc_meminfo b/test/fixtures/proc_meminfo new file mode 100644 index 0000000..9c279aa --- /dev/null +++ b/test/fixtures/proc_meminfo @@ -0,0 +1,7 @@ +MemTotal: 31943076 kB +MemFree: 4291244 kB +MemAvailable: 25627088 kB +Buffers: 123456 kB +Cached: 6789012 kB +SwapTotal: 8388604 kB +SwapFree: 8388604 kB diff --git a/test/fixtures/proc_stat b/test/fixtures/proc_stat new file mode 100644 index 0000000..2914cca --- /dev/null +++ b/test/fixtures/proc_stat @@ -0,0 +1,5 @@ +cpu 209094 2199 135469 11604898 24408 0 946 0 0 0 +cpu0 5294 11 2928 740049 460 0 592 0 0 0 +cpu1 5210 18 2801 740512 431 0 12 0 0 0 +intr 12345678 +ctxt 987654321 diff --git a/test/fixtures/temp1_input b/test/fixtures/temp1_input new file mode 100644 index 0000000..46fba94 --- /dev/null +++ b/test/fixtures/temp1_input @@ -0,0 +1 @@ +53125 diff --git a/test/test_data.lua b/test/test_data.lua new file mode 100644 index 0000000..08c20f9 --- /dev/null +++ b/test/test_data.lua @@ -0,0 +1,39 @@ +-- Parser checks for lib/data.lua. +-- Run from the repo root: lua test/test_data.lua +-- Parsers are what break silently on a kernel or hardware change, so they are +-- what gets a test. Everything else in this project is verified by screenshot. + +package.path = './?.lua;' .. package.path +local data = require 'lib.data' + +local function read(path) + local f = assert(io.open(path, 'r')) + local s = f:read('*a') + f:close() + return s +end + +-- cpu_times: total and idle jiffies from the aggregate "cpu " line. +local total, idle = data.cpu_times(read('test/fixtures/proc_stat')) +-- 209094+2199+135469+11604898+24408+0+946 = 11977014 +assert(total == 11977014, 'cpu total, got ' .. tostring(total)) +-- idle field is the 4th value, 11604898; iowait (24408) counts as idle too +assert(idle == 11629306, 'cpu idle, got ' .. tostring(idle)) + +-- mem_info: values in kB, as the file gives them. +local mem = data.mem_info(read('test/fixtures/proc_meminfo')) +assert(mem.total == 31943076, 'mem total, got ' .. tostring(mem.total)) +assert(mem.available == 25627088, 'mem available, got ' .. tostring(mem.available)) +-- used is total minus available, which is what a user means by "used" +assert(mem.used == 6315988, 'mem used, got ' .. tostring(mem.used)) + +-- millidegrees: hwmon temp*_input is millidegrees C, rounded to whole degrees. +assert(data.millidegrees(read('test/fixtures/temp1_input')) == 53, + 'temp, got ' .. tostring(data.millidegrees(read('test/fixtures/temp1_input')))) +-- A missing or unreadable sensor must yield nil, not an error and not 0: +-- 0 degrees is a plausible reading and would be indistinguishable from failure. +assert(data.millidegrees(nil) == nil, 'nil input must give nil') +assert(data.millidegrees('') == nil, 'empty input must give nil') +assert(data.millidegrees('garbage') == nil, 'unparseable input must give nil') + +print('test_data: all assertions passed') |
