Skip to content

Commit b8ea495

Browse files
committed
test(sensors-*): add --test hooks and fixture-based unit tests
The three `sensors-*` plugins talk to `psutil.sensors_*()` directly instead of parsing a shell command, so they previously had no fixture-based tests. Give each a `--test` hook that loads the return value from a JSON fixture mirroring psutil's shape: sensors-battery {"percent": ..., "secsleft": ..., "power_plugged": ...} or `null` for "no battery installed" sensors-fans {"<group>": [{"label": ..., "current": ...}]} sensors-temperatures {"<group>": [{"label": ..., "current": ..., "high": ..., "critical": ...}]} Each plugin gains a declarative `TESTS` list that exercises the obvious nominal / warn / crit / no-sensors cases plus a few edge cases inspired by psutil's own test suite (`tests/test_linux.py::TestSensors*`): - sensors-battery: `power_plugged = None`, `secsleft = -1` (POWER_TIME_UNKNOWN) - sensors-temperatures: the `coretemp-and-acpitz-nominal` fixture mixes sensors with and without threshold metadata Real host captures also ship as additional fixtures so the suite exercises real-world quirks the synthetic data would miss: - sensors-temperatures `dell-laptop-real-capture`: nvme reports `high = critical = 65261.85` (kelvin 65535 minus 273.15, a "no threshold configured" sentinel) and `dell_ddv` reports `high = 0.0` for the same reason. - sensors-temperatures `coolermaster-real-capture`: two acpitz zones plus `coretemp` Package + 6 cores, all nominal. - sensors-fans `dell-laptop-real-capture`: labelled `dell_ddv` fans plus the unlabelled `dell_smm` re-exposure of the same data. - sensors-fans `coolermaster-real-capture`: the "no fans detected" branch on a fanless desktop. - sensors-battery `dell-laptop-real-capture`: 79 % plugged in and charging. - sensors-battery `coolermaster-real-capture`: the "no battery installed" branch on a desktop. Two related fixes to the plugins: - **sensors-temperatures**: `_sanitize_threshold()` now rejects sentinel "no threshold" values reported by real hardware: `None`, `<= 0`, or above 200 C. Both the Dell `dell_ddv` zero and the NVMe 65261.85 kelvin-overflow value used to trip the plugin into STATE_WARN on any real host that runs those drivers; now both are silently ignored and only real thresholds drive state. Also bumps `__version__`. - **sensors-battery**: the message string now ends with the state marker (`[WARNING]` / `[CRITICAL]`), matching what sensors-fans and sensors-temperatures already do per entry.
1 parent 28a7752 commit b8ea495

26 files changed

Lines changed: 646 additions & 12 deletions

check-plugins/openvpn-client-list/unit-test/stdout/EXAMPLE01 renamed to check-plugins/openvpn-client-list/unit-test/stdout/five-clients-four-users

File renamed without changes.

check-plugins/sensors-battery/sensors-battery

Lines changed: 44 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,13 @@
1111
"""See the check's README for more details."""
1212

1313
import argparse
14+
import json
1415
import sys
1516

1617
import lib.args
1718
import lib.base
1819
import lib.human
20+
import lib.lftest
1921
from lib.globals import STATE_OK, STATE_UNKNOWN
2022

2123
try:
@@ -24,9 +26,21 @@ except ImportError:
2426
print('Python module "psutil" is not installed.')
2527
sys.exit(STATE_UNKNOWN)
2628

29+
# psutil 5.x exposes sbattery under psutil._common, psutil 6+ under
30+
# psutil._ntuples. Fall back to a plain namedtuple if neither is
31+
# available.
32+
try:
33+
from psutil._ntuples import sbattery
34+
except ImportError:
35+
try:
36+
from psutil._common import sbattery
37+
except ImportError:
38+
from collections import namedtuple
39+
sbattery = namedtuple('sbattery', ['percent', 'secsleft', 'power_plugged'])
40+
2741

2842
__author__ = 'Linuxfabrik GmbH, Zurich/Switzerland'
29-
__version__ = '2026040801'
43+
__version__ = '2026041301'
3044

3145
DESCRIPTION = """Reports battery status information including charge percentage, time remaining, and
3246
power source (AC or battery). Returns OK if no battery is installed or if metrics
@@ -64,6 +78,13 @@ def parse_args():
6478
default=DEFAULT_CRIT,
6579
)
6680

81+
parser.add_argument(
82+
'--test',
83+
help=lib.args.help('--test'),
84+
dest='TEST',
85+
type=lib.args.csv,
86+
)
87+
6788
parser.add_argument(
6889
'-w',
6990
'--warning',
@@ -77,6 +98,21 @@ def parse_args():
7798
return args
7899

79100

101+
def _load_battery_fixture(raw_json):
102+
"""Convert a test fixture into the shape that `psutil.sensors_battery()`
103+
returns: an `sbattery(percent, secsleft, power_plugged)` namedtuple,
104+
or `None` if the fixture represents "no battery installed".
105+
"""
106+
data = json.loads(raw_json)
107+
if data is None:
108+
return None
109+
return sbattery(
110+
percent=data['percent'],
111+
secsleft=data.get('secsleft', -2), # psutil.POWER_TIME_UNLIMITED == -2
112+
power_plugged=data.get('power_plugged'),
113+
)
114+
115+
80116
def main():
81117
"""The main function. This is where the magic happens."""
82118

@@ -86,11 +122,15 @@ def main():
86122
except SystemExit:
87123
sys.exit(STATE_UNKNOWN)
88124

89-
if not hasattr(psutil, 'sensors_battery'):
125+
if args.TEST is None and not hasattr(psutil, 'sensors_battery'):
90126
lib.base.cu('Platform not supported.')
91127

92128
# fetch data
93-
batt = psutil.sensors_battery()
129+
if args.TEST is None:
130+
batt = psutil.sensors_battery()
131+
else:
132+
stdout, _, _ = lib.lftest.test(args.TEST)
133+
batt = _load_battery_fixture(stdout)
94134
if batt is None:
95135
lib.base.oao('No battery installed.', STATE_OK, always_ok=args.ALWAYS_OK)
96136

@@ -121,6 +161,7 @@ def main():
121161
_min=0,
122162
)
123163
state = lib.base.get_state(batt.percent, args.WARN, args.CRIT, 'le')
164+
msg += lib.base.state2str(state, prefix=' ')
124165

125166
# over and out
126167
lib.base.oao(msg, state, perfdata, always_ok=args.ALWAYS_OK)
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
#!/usr/bin/env python3
2+
# -*- coding: utf-8; py-indent-offset: 4 -*-
3+
#
4+
# Author: Linuxfabrik GmbH, Zurich, Switzerland
5+
# Contact: info (at) linuxfabrik (dot) ch
6+
# https://www.linuxfabrik.ch/
7+
# License: The Unlicense, see LICENSE file.
8+
9+
# https://github.com/Linuxfabrik/monitoring-plugins/blob/main/CONTRIBUTING.md
10+
11+
import sys
12+
sys.path.insert(0, '..')
13+
14+
import unittest
15+
16+
from lib.globals import STATE_CRIT, STATE_OK, STATE_WARN
17+
import lib.lftest
18+
19+
20+
# The sensors-battery plugin calls `psutil.sensors_battery()` directly.
21+
# `--test` reads a JSON fixture that mirrors psutil's return shape:
22+
#
23+
# null (no battery installed)
24+
# {"percent": <float>, "secsleft": <int>, (battery present; -2
25+
# "power_plugged": <bool>} means POWER_TIME_UNLIMITED)
26+
TESTS = [
27+
{
28+
'id': 'ok-no-battery',
29+
'test': 'stdout/no-battery,,0',
30+
'assert-retc': STATE_OK,
31+
'assert-in': ['No battery installed.'],
32+
},
33+
{
34+
'id': 'ok-plugged-in-charging',
35+
'test': 'stdout/plugged-in-charging,,0',
36+
'assert-retc': STATE_OK,
37+
'assert-in': ['65.5%', 'plugged in and charging'],
38+
},
39+
{
40+
'id': 'ok-plugged-in-full',
41+
'test': 'stdout/plugged-in-full,,0',
42+
'assert-retc': STATE_OK,
43+
'assert-in': ['100.0%', 'plugged in and fully charged'],
44+
},
45+
{
46+
'id': 'warn-discharging-15-percent',
47+
'test': 'stdout/discharging-15-percent,,0',
48+
'params': '--warning 20 --critical 5',
49+
'assert-retc': STATE_WARN,
50+
'assert-in': ['15.0%', 'not plugged in and discharging', '[WARNING]'],
51+
},
52+
{
53+
'id': 'crit-discharging-3-percent',
54+
'test': 'stdout/discharging-3-percent,,0',
55+
'params': '--warning 20 --critical 5',
56+
'assert-retc': STATE_CRIT,
57+
'assert-in': ['3.0%', 'not plugged in and discharging', '[CRITICAL]'],
58+
},
59+
{
60+
'id': 'ok-always-ok-masks-crit',
61+
'test': 'stdout/discharging-3-percent,,0',
62+
'params': '--warning 20 --critical 5 --always-ok',
63+
'assert-retc': STATE_OK,
64+
'assert-in': ['3.0%'],
65+
},
66+
{
67+
# Real capture from a Dell XPS laptop plugged into AC, 79%.
68+
'id': 'ok-dell-laptop-real-capture',
69+
'test': 'stdout/dell-laptop-real-capture,,0',
70+
'assert-retc': STATE_OK,
71+
'assert-in': ['79.27%', 'plugged in and charging'],
72+
},
73+
{
74+
# Edge case from psutil's own tests: the AC/online file is
75+
# missing and /BAT0/status returns an unparseable value, so
76+
# power_plugged comes back as None. secsleft is also unknown
77+
# (POWER_TIME_UNKNOWN == -1). The plugin currently reports
78+
# the percent and falls through the "not plugged" branch.
79+
'id': 'warn-power-plugged-undetermined',
80+
'test': 'stdout/power-plugged-undetermined,,0',
81+
'params': '--warning 50 --critical 20',
82+
'assert-retc': STATE_WARN,
83+
'assert-in': ['42.0%', '[WARNING]'],
84+
},
85+
{
86+
# Real capture from a "Cooler Master" desktop: psutil returns
87+
# None for sensors_battery() and the plugin emits the
88+
# "No battery installed." branch.
89+
'id': 'ok-coolermaster-real-capture',
90+
'test': 'stdout/coolermaster-real-capture,,0',
91+
'assert-retc': STATE_OK,
92+
'assert-in': ['No battery installed.'],
93+
},
94+
]
95+
96+
97+
class TestCheck(unittest.TestCase):
98+
99+
check = '../sensors-battery'
100+
101+
def test(self):
102+
for t in TESTS:
103+
with self.subTest(id=t['id']):
104+
lib.lftest.run(self, self.check, t)
105+
106+
107+
if __name__ == '__main__':
108+
unittest.main()
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
null
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{"percent": 79.26515930113052, "secsleft": -2, "power_plugged": true}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{"percent": 15.0, "secsleft": 2400, "power_plugged": false}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{"percent": 3.0, "secsleft": 420, "power_plugged": false}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
null
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{"percent": 65.5, "secsleft": -2, "power_plugged": true}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{"percent": 100.0, "secsleft": -2, "power_plugged": true}

0 commit comments

Comments
 (0)