11import glob
22import json
33import os
4- import re
54import unittest
5+ import tempfile
6+ import math
7+ from time import sleep
68
79from unittest .mock import patch , MagicMock
8-
910from smartmon import (
1011 parse_device_info ,
1112 parse_if_attributes ,
1213 main ,
1314 SMARTMON_ATTRS ,
14- camel_to_snake
15+ camel_to_snake ,
16+ write_metrics_to_textfile ,
1517)
1618
1719def load_json_fixture (filename ):
@@ -75,7 +77,6 @@ def _test_parse_device_info(self, fixture_name):
7577 dev_serial = device_info ["serial" ].lower ()
7678
7779 # The device_info line should exist for every device
78- # e.g. device_info{disk="/dev/...",type="...",serial_number="..."} 1
7980 device_info_found = any (
8081 line .startswith ("device_info{" ) and
8182 f'disk="{ dev_name } "' in line and
@@ -94,32 +95,32 @@ def _test_parse_device_info(self, fixture_name):
9495 line .startswith ("device_smart_available{" ) and
9596 f'disk="{ dev_name } "' in line and
9697 f'serial_number="{ dev_serial } "' in line and
97- line .endswith (" 1" )
98+ line .endswith (" 1.0 " )
9899 for line in metrics
99100 )
100101 self .assertTrue (
101102 smart_available_found ,
102- f"Expected device_smart_available=1 for { dev_name } , not found."
103+ f"Expected device_smart_available=1.0 for { dev_name } , not found."
103104 )
104105
105106 # If smart_enabled is true, we expect device_smart_enabled = 1
106107 if device_info .get ("smart_enabled" ):
107108 smart_enabled_found = any (
108109 line .startswith ("device_smart_enabled{" ) and
109110 f'disk="{ dev_name } "' in line and
110- line .endswith (" 1" )
111+ line .endswith (" 1.0 " )
111112 for line in metrics
112113 )
113114 self .assertTrue (
114115 smart_enabled_found ,
115- f"Expected device_smart_enabled=1 for { dev_name } , not found."
116+ f"Expected device_smart_enabled=1.0 for { dev_name } , not found."
116117 )
117118
118119 # device_smart_healthy if assessment in [PASS, WARN, FAIL]
119120 # PASS => 1, otherwise => 0
120121 assessment = device_info .get ("assessment" , "" ).upper ()
121122 if assessment in ["PASS" , "WARN" , "FAIL" ]:
122- expected_val = 1 if assessment == "PASS" else 0
123+ expected_val = float ( 1 ) if assessment == "PASS" else float ( 0 )
123124 smart_healthy_found = any (
124125 line .startswith ("device_smart_healthy{" ) and
125126 f'disk="{ dev_name } "' in line and
@@ -162,9 +163,8 @@ def _test_parse_if_attributes(self, fixture_name):
162163 snake_key = camel_to_snake (attr_key )
163164
164165 if isinstance (attr_val , (int , float )) and snake_key in SMARTMON_ATTRS :
165- # We expect e.g. critical_warning{disk="/dev/..."} <value>
166166 expected_line = (
167- f"{ snake_key } {{disk=\" { dev_name } \" ,type =\" { dev_iface } \" ,serial_number =\" { dev_serial } \" }} { attr_val } "
167+ f"{ snake_key } {{disk=\" { dev_name } \" ,serial_number =\" { dev_serial } \" ,type =\" { dev_iface } \" }} { float ( attr_val ) } "
168168 )
169169 self .assertIn (
170170 expected_line ,
@@ -175,7 +175,7 @@ def _test_parse_if_attributes(self, fixture_name):
175175 # If it's not in SMARTMON_ATTRS or not numeric,
176176 # we do NOT expect a line with that name+value
177177 unexpected_line = (
178- f"{ snake_key } {{disk=\" { dev_name } \" ,type =\" { dev_iface } \" ,serial_number =\" { dev_serial } \" }} { attr_val } "
178+ f"{ snake_key } {{disk=\" { dev_name } \" ,serial_number =\" { dev_serial } \" ,type =\" { dev_iface } \" }} { float ( attr_val ) } "
179179 )
180180 self .assertNotIn (
181181 unexpected_line ,
@@ -204,28 +204,32 @@ def test_parse_if_attributes(self):
204204
205205 @patch ("smartmon.run_command" )
206206 @patch ("smartmon.DeviceList" )
207- def test_main (self , mock_devicelist_class , mock_run_cmd ):
207+ @patch ("smartmon.write_metrics_to_textfile" , wraps = write_metrics_to_textfile )
208+ def test_main (self , mock_write_metrics , mock_devicelist_class , mock_run_cmd ):
208209 """
209210 End-to-end test of main() for every JSON fixture in ./tests/.
210211 This ensures we can handle multiple disks (multiple fixture files).
212+ Checks metrics written to a temp file, and that write_metrics_to_textfile is called once.
211213 """
214+
215+ # Patch run_command to return a version & "active" power_mode
216+ def run_command_side_effect (cmd , parse_json = False ):
217+ if "--version" in cmd :
218+ return "smartctl 7.3 5422 [x86_64-linux-5.15.0]\n ..."
219+ if "-n" in cmd and "standby" in cmd and parse_json :
220+ return {"power_mode" : "active" }
221+ return ""
222+
223+ mock_run_cmd .side_effect = run_command_side_effect
224+
212225 for fixture_path in self .fixture_files :
213226 fixture_name = os .path .basename (fixture_path )
214227 with self .subTest (msg = f"Testing main() with { fixture_name } " ):
228+ mock_write_metrics .reset_mock ()
215229 data = load_json_fixture (fixture_name )
216230 device_info = data ["device_info" ]
217231 if_attrs = data .get ("if_attributes" , {})
218232
219- # Patch run_command to return a version & "active" power_mode
220- def run_command_side_effect (cmd , parse_json = False ):
221- if "--version" in cmd :
222- return "smartctl 7.3 5422 [x86_64-linux-5.15.0]\n ..."
223- if "-n" in cmd and "standby" in cmd and parse_json :
224- return {"power_mode" : "active" }
225- return ""
226-
227- mock_run_cmd .side_effect = run_command_side_effect
228-
229233 # Mock a single device from the fixture
230234 device_mock = self .create_mock_device_from_json (device_info , if_attrs )
231235
@@ -234,41 +238,41 @@ def run_command_side_effect(cmd, parse_json=False):
234238 mock_dev_list .devices = [device_mock ]
235239 mock_devicelist_class .return_value = mock_dev_list
236240
237- with patch ( "builtins.print" ) as mock_print :
238- main ()
239-
240- printed_lines = []
241- for call_args in mock_print . call_args_list :
242- printed_lines . extend ( call_args [ 0 ][ 0 ]. split ( " \n " ))
243- dev_name = device_info [ "name" ]
244- dev_iface = device_info [ "interface" ]
245- dev_serial = device_info [ "serial" ]. lower ()
246-
247- # We expect a line for the run timestamp, e.g.:
248- # smartmon_smartctl_run{disk="/dev/...",type="..."} 1671234567
249- run_line_found = any (
250- line . startswith ( "smartmon_smartctl_run{" ) and
251- f'disk=" { dev_name } "' in line and
252- f'type=" { dev_iface } "' in line
253- for line in printed_lines
254- )
255- self . assertTrue (
256- run_line_found ,
257- f"Expected 'smartmon_smartctl_run' metric line for { dev_name } not found."
258- )
259-
260- # Because we mocked "power_mode": "active", we expect device_active=1
261- active_line_found = any (
262- line . startswith ( "smartmon_device_active{" ) and
263- f'disk=" { dev_name } "' in line and
264- f'serial_number=" { dev_serial } "' in line and
265- line . endswith ( " 1" )
266- for line in printed_lines
267- )
268- self .assertTrue (
269- active_line_found ,
270- f"Expected 'device_active{{...}} 1' line for { dev_name } not found."
271- )
241+ with tempfile . NamedTemporaryFile ( mode = "r+" , delete_on_close = False ) as tmpfile :
242+ path = tmpfile . name
243+ main ( output_path = path )
244+ tmpfile . close ()
245+
246+ # Ensure write_metrics_to_textfile was called once
247+ self . assertEqual ( mock_write_metrics . call_count , 1 )
248+
249+ with open ( path , "r" ) as f :
250+ # Read the metrics from the file
251+ metrics_lines = [ line . strip () for line in f . readlines () if line . strip () and not line . startswith ( '#' )]
252+ print ( f"Metrics lines: { metrics_lines } " )
253+
254+ # Generate expected metrics using the parse functions
255+ expected_metrics = []
256+ expected_metrics . extend ( parse_device_info ( device_mock ))
257+ expected_metrics . extend ( parse_if_attributes ( device_mock ))
258+
259+ # Check that all expected metrics are present in the file
260+ for expected in expected_metrics :
261+ exp_metric , exp_val_str = expected . rsplit ( " " , 1 )
262+ exp_val = float ( exp_val_str )
263+ found = any (
264+ ( exp_metric in line ) and
265+ math . isclose ( float ( line . rsplit ( " " , 1 )[ 1 ]), exp_val )
266+ for line in metrics_lines
267+ )
268+ self . assertTrue ( found , f"Expected metric ' { expected } ' not found" )
269+
270+ # Check that smartctl_version metric is present
271+ version_found = any ( line . startswith ( "smartctl_version{" ) for line in metrics_lines )
272+ self .assertTrue (version_found , "Expected 'smartctl_version' metric not found in output file." )
273+
274+ # Check that the output file is not empty
275+ self . assertTrue ( metrics_lines , "Metrics output file is empty." )
272276
273277if __name__ == "__main__" :
274278 unittest .main ()
0 commit comments