Skip to content

Commit bab443b

Browse files
committed
Split device and attribute tests into individual test cases per fixture for better error reporting
1 parent 381d92a commit bab443b

File tree

2 files changed

+127
-115
lines changed

2 files changed

+127
-115
lines changed

etc/kayobe/ansible/scripts/smartmon.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import subprocess
44
import json
55
import re
6-
from datetime import datetime
6+
import datetime
77

88
from pySMART import DeviceList
99

@@ -197,7 +197,7 @@ def main():
197197
disk_type = dev.interface or ""
198198
serial_number = (dev.serial or "").lower()
199199

200-
run_timestamp = int(datetime.utcnow().timestamp())
200+
run_timestamp = int(datetime.datetime.now(datetime.UTC).timestamp())
201201
all_metrics.append(f'smartctl_run{{disk="{disk_name}",type="{disk_type}"}} {run_timestamp}')
202202

203203
active = 1

etc/kayobe/ansible/scripts/test_smartmon.py

Lines changed: 125 additions & 113 deletions
Original file line numberDiff line numberDiff line change
@@ -59,136 +59,148 @@ class IfAttributesMock:
5959

6060
return device
6161

62+
def _test_parse_device_info(self, fixture_name):
63+
"""
64+
Helper method to test parse_device_info() for a single JSON fixture.
65+
"""
66+
data = load_json_fixture(fixture_name)
67+
device_info = data["device_info"]
68+
69+
device = self.create_mock_device_from_json(device_info)
70+
metrics = parse_device_info(device)
71+
72+
dev_name = device_info["name"]
73+
dev_iface = device_info["interface"]
74+
dev_serial = device_info["serial"].lower()
75+
76+
# The device_info line should exist for every device
77+
# e.g. device_info{disk="/dev/...",type="...",serial_number="..."} 1
78+
device_info_found = any(
79+
line.startswith("device_info{") and
80+
f'disk="{dev_name}"' in line and
81+
f'type="{dev_iface}"' in line and
82+
f'serial_number="{dev_serial}"' in line
83+
for line in metrics
84+
)
85+
self.assertTrue(
86+
device_info_found,
87+
f"Expected a device_info metric line for {dev_name} but didn't find it."
88+
)
89+
90+
# If smart_capable is true, we expect device_smart_available = 1
91+
if device_info.get("smart_capable"):
92+
smart_available_found = any(
93+
line.startswith("device_smart_available{") and
94+
f'disk="{dev_name}"' in line and
95+
f'serial_number="{dev_serial}"' in line and
96+
line.endswith(" 1")
97+
for line in metrics
98+
)
99+
self.assertTrue(
100+
smart_available_found,
101+
f"Expected device_smart_available=1 for {dev_name}, not found."
102+
)
103+
104+
# If smart_enabled is true, we expect device_smart_enabled = 1
105+
if device_info.get("smart_enabled"):
106+
smart_enabled_found = any(
107+
line.startswith("device_smart_enabled{") and
108+
f'disk="{dev_name}"' in line and
109+
line.endswith(" 1")
110+
for line in metrics
111+
)
112+
self.assertTrue(
113+
smart_enabled_found,
114+
f"Expected device_smart_enabled=1 for {dev_name}, not found."
115+
)
116+
117+
# device_smart_healthy if assessment in [PASS, WARN, FAIL]
118+
# PASS => 1, otherwise => 0
119+
assessment = device_info.get("assessment", "").upper()
120+
if assessment in ["PASS", "WARN", "FAIL"]:
121+
expected_val = 1 if assessment == "PASS" else 0
122+
smart_healthy_found = any(
123+
line.startswith("device_smart_healthy{") and
124+
f'disk="{dev_name}"' in line and
125+
line.endswith(f" {expected_val}")
126+
for line in metrics
127+
)
128+
self.assertTrue(
129+
smart_healthy_found,
130+
f"Expected device_smart_healthy={expected_val} for {dev_name}, not found."
131+
)
132+
62133
def test_parse_device_info(self):
63134
"""
64135
Test parse_device_info() for every JSON fixture in ./drives/.
65-
We do subTest() so each fixture is tested individually.
136+
Each fixture is tested individually with clear error reporting.
66137
"""
67138
for fixture_path in self.fixture_files:
68139
fixture_name = os.path.basename(fixture_path)
69-
with self.subTest(msg=f"Testing device_info with {fixture_name}"):
70-
data = load_json_fixture(fixture_name)
71-
device_info = data["device_info"]
72-
73-
device = self.create_mock_device_from_json(device_info)
74-
metrics = parse_device_info(device)
140+
with self.subTest(fixture=fixture_name):
141+
self._test_parse_device_info(fixture_name)
75142

76-
dev_name = device_info["name"]
77-
dev_iface = device_info["interface"]
78-
dev_serial = device_info["serial"].lower()
79-
80-
# The device_info line should exist for every device
81-
# e.g. device_info{disk="/dev/...",type="...",serial_number="..."} 1
82-
device_info_found = any(
83-
line.startswith("device_info{") and
84-
f'disk="{dev_name}"' in line and
85-
f'type="{dev_iface}"' in line and
86-
f'serial_number="{dev_serial}"' in line
87-
for line in metrics
143+
def _test_parse_if_attributes(self, fixture_name):
144+
"""
145+
Helper method to test parse_if_attributes() for a single JSON fixture.
146+
"""
147+
data = load_json_fixture(fixture_name)
148+
device_info = data["device_info"]
149+
if_attrs = data.get("if_attributes", {})
150+
151+
device = self.create_mock_device_from_json(device_info, if_attrs)
152+
metrics = parse_if_attributes(device)
153+
154+
dev_name = device_info["name"]
155+
dev_iface = device_info["interface"]
156+
dev_serial = device_info["serial"].lower()
157+
158+
# For each numeric attribute in JSON, if it's in SMARTMON_ATTRS,
159+
# we expect a line in the script's output.
160+
for attr_key, attr_val in if_attrs.items():
161+
# Convert from e.g. "criticalWarning" -> "critical_warning"
162+
snake_key = re.sub(r'(?<!^)(?=[A-Z])', '_', attr_key).lower()
163+
164+
if isinstance(attr_val, (int, float)) and snake_key in SMARTMON_ATTRS:
165+
# We expect e.g. critical_warning{disk="/dev/..."} <value>
166+
expected_line = (
167+
f"{snake_key}{{disk=\"{dev_name}\",type=\"{dev_iface}\",serial_number=\"{dev_serial}\"}} {attr_val}"
88168
)
89-
self.assertTrue(
90-
device_info_found,
91-
f"Expected a device_info metric line for {dev_name} but didn't find it."
169+
self.assertIn(
170+
expected_line,
171+
metrics,
172+
f"Expected metric '{expected_line}' for attribute '{attr_key}' not found."
173+
)
174+
else:
175+
# If it's not in SMARTMON_ATTRS or not numeric,
176+
# we do NOT expect a line with that name+value
177+
unexpected_line = (
178+
f"{snake_key}{{disk=\"{dev_name}\",type=\"{dev_iface}\",serial_number=\"{dev_serial}\"}} {attr_val}"
179+
)
180+
self.assertNotIn(
181+
unexpected_line,
182+
metrics,
183+
f"Unexpected metric '{unexpected_line}' found for {attr_key}."
92184
)
93185

94-
# If smart_capable is true, we expect device_smart_available = 1
95-
if device_info.get("smart_capable"):
96-
smart_available_found = any(
97-
line.startswith("device_smart_available{") and
98-
f'disk="{dev_name}"' in line and
99-
f'serial_number="{dev_serial}"' in line and
100-
line.endswith(" 1")
101-
for line in metrics
102-
)
103-
self.assertTrue(
104-
smart_available_found,
105-
f"Expected device_smart_available=1 for {dev_name}, not found."
106-
)
107-
108-
# If smart_enabled is true, we expect device_smart_enabled = 1
109-
if device_info.get("smart_enabled"):
110-
smart_enabled_found = any(
111-
line.startswith("device_smart_enabled{") and
112-
f'disk="{dev_name}"' in line and
113-
line.endswith(" 1")
114-
for line in metrics
115-
)
116-
self.assertTrue(
117-
smart_enabled_found,
118-
f"Expected device_smart_enabled=1 for {dev_name}, not found."
119-
)
120-
121-
# device_smart_healthy if assessment in [PASS, WARN, FAIL]
122-
# PASS => 1, otherwise => 0
123-
assessment = device_info.get("assessment", "").upper()
124-
if assessment in ["PASS", "WARN", "FAIL"]:
125-
expected_val = 1 if assessment == "PASS" else 0
126-
smart_healthy_found = any(
127-
line.startswith("device_smart_healthy{") and
128-
f'disk="{dev_name}"' in line and
129-
line.endswith(f" {expected_val}")
130-
for line in metrics
131-
)
132-
self.assertTrue(
133-
smart_healthy_found,
134-
f"Expected device_smart_healthy={expected_val} for {dev_name}, not found."
135-
)
186+
# Also ensure that non-numeric or disallowed attributes do not appear
187+
# For instance "notInSmartmonAttrs" should never appear.
188+
for line in metrics:
189+
self.assertNotIn(
190+
"not_in_smartmon_attrs",
191+
line,
192+
f"'notInSmartmonAttrs' attribute unexpectedly found in metric line: {line}"
193+
)
136194

137195
def test_parse_if_attributes(self):
138196
"""
139197
Test parse_if_attributes() for every JSON fixture in ./drives/.
140-
We do subTest() so each fixture is tested individually.
198+
Each fixture is tested individually with clear error reporting.
141199
"""
142200
for fixture_path in self.fixture_files:
143201
fixture_name = os.path.basename(fixture_path)
144-
with self.subTest(msg=f"Testing if_attributes with {fixture_name}"):
145-
data = load_json_fixture(fixture_name)
146-
device_info = data["device_info"]
147-
if_attrs = data.get("if_attributes", {})
148-
149-
device = self.create_mock_device_from_json(device_info, if_attrs)
150-
metrics = parse_if_attributes(device)
151-
152-
dev_name = device_info["name"]
153-
dev_iface = device_info["interface"]
154-
dev_serial = device_info["serial"].lower()
155-
156-
# For each numeric attribute in JSON, if it's in SMARTMON_ATTRS,
157-
# we expect a line in the script's output.
158-
for attr_key, attr_val in if_attrs.items():
159-
# Convert from e.g. "criticalWarning" -> "critical_warning"
160-
snake_key = re.sub(r'(?<!^)(?=[A-Z])', '_', attr_key).lower()
161-
162-
if isinstance(attr_val, (int, float)) and snake_key in SMARTMON_ATTRS:
163-
# We expect e.g. critical_warning{disk="/dev/..."} <value>
164-
expected_line = (
165-
f"{snake_key}{{disk=\"{dev_name}\",type=\"{dev_iface}\",serial_number=\"{dev_serial}\"}} {attr_val}"
166-
)
167-
self.assertIn(
168-
expected_line,
169-
metrics,
170-
f"Expected metric '{expected_line}' for attribute '{attr_key}' not found."
171-
)
172-
else:
173-
# If it's not in SMARTMON_ATTRS or not numeric,
174-
# we do NOT expect a line with that name+value
175-
unexpected_line = (
176-
f"{snake_key}{{disk=\"{dev_name}\",type=\"{dev_iface}\",serial_number=\"{dev_serial}\"}} {attr_val}"
177-
)
178-
self.assertNotIn(
179-
unexpected_line,
180-
metrics,
181-
f"Unexpected metric '{unexpected_line}' found for {attr_key}."
182-
)
183-
184-
# Also ensure that non-numeric or disallowed attributes do not appear
185-
# For instance "notInSmartmonAttrs" should never appear.
186-
for line in metrics:
187-
self.assertNotIn(
188-
"not_in_smartmon_attrs",
189-
line,
190-
f"'notInSmartmonAttrs' attribute unexpectedly found in metric line: {line}"
191-
)
202+
with self.subTest(fixture=fixture_name):
203+
self._test_parse_if_attributes(fixture_name)
192204

193205
@patch("smartmon.run_command")
194206
@patch("smartmon.DeviceList")

0 commit comments

Comments
 (0)