-- 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') -- CPU load is a delta between two samples, so the counter holds state. -- The first call has no previous sample and must report nil, not a number: -- any number it invented would be wrong, and 100% on startup looks like a -- real spike. local c = data.new_cpu_counter() assert(c:sample(1000, 900) == nil, 'first sample must give nil') -- Second sample: 100 more total jiffies, 50 of them idle, so 50% busy. -- Held in a local first: calling sample() again inside the assert message would -- advance the counter a third time. local busy = c:sample(1100, 950) assert(busy == 50.0, 'second sample, got ' .. tostring(busy)) -- A counter that did not advance means no elapsed time, not 0% load. local c2 = data.new_cpu_counter() c2:sample(1000, 900) assert(c2:sample(1000, 900) == nil, 'zero delta must give nil, not a division by zero') -- === Per-core CPU ========================================================= -- The aggregate line answers "how busy is the machine"; the equaliser needs -- one entry per core. Both come from the same file, so they are parsed by the -- same rules and differ only in which lines they read. local percore = read('test/fixtures/proc_stat_percore') local cores = data.per_cpu_times(percore) assert(#cores == 4, 'one entry per cpuN line, got ' .. tostring(#cores)) -- cpu0: 6441+24+3225+1056496+1932+0+760 = 1068878 total, idle+iowait = 1058428 assert(cores[1].total == 1068878, 'core 0 total, got ' .. tostring(cores[1].total)) assert(cores[1].idle == 1058428, 'core 0 idle, got ' .. tostring(cores[1].idle)) -- Ordering matters: bar N must be core N, so the list follows the file. assert(cores[2].total == 1066406, 'core 1 total, got ' .. tostring(cores[2].total)) -- The aggregate 'cpu ' line must NOT be counted as a core: it would draw a -- seventeenth bar showing the average, which looks like a real core. for i, c in ipairs(cores) do assert(c.total < 2000000, 'entry ' .. i .. ' looks like the aggregate line') end -- A counter per core is just another instance, which is why new_cpu_counter -- holds its own previous sample rather than using a module-level one. local c0 = data.new_cpu_counter() assert(c0:sample(cores[1].total, cores[1].idle) == nil, 'first sample is nil') local busy = c0:sample(cores[1].total + 100, cores[1].idle + 25) assert(busy == 75.0, 'second sample 75%, got ' .. tostring(busy)) -- Malformed input yields an empty list, never an error: a raise here is a -- blank dashboard. assert(#data.per_cpu_times('') == 0, 'empty input gives an empty list') assert(#data.per_cpu_times('garbage\nlines\n') == 0, 'garbage gives an empty list') assert(#data.per_cpu_times(nil) == 0, 'nil gives an empty list') -- A truncated line (fewer than the 5 fields the maths needs) is skipped -- rather than producing a nonsense total. assert(#data.per_cpu_times('cpu0 1 2\n') == 0, 'a short line is skipped') print('test_data: all assertions passed')