aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorDanilo M. <danix@danix.xyz>2026-09-16 19:23:35 +0200
committerDanilo M. <danix@danix.xyz>2026-09-16 19:23:35 +0200
commitf822a250f9b2759207d64cc53b3d80c0c08d2082 (patch)
treeb3ea539a252b7d7c34b08410b371cd58f583ca05
parent4538896ce65156883dc44cc3355435d40d7e682b (diff)
downloadconky-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.lua75
-rw-r--r--test/fixtures/proc_meminfo7
-rw-r--r--test/fixtures/proc_stat5
-rw-r--r--test/fixtures/temp1_input1
-rw-r--r--test/test_data.lua39
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')