|
5 | 5 | import fcntl |
6 | 6 | from unittest.mock import patch, MagicMock, call |
7 | 7 | from io import StringIO |
8 | | -import shutil |
| 8 | +import subprocess |
9 | 9 |
|
10 | 10 | class MockFile: |
11 | 11 | def __init__(self, data=None): |
@@ -55,6 +55,17 @@ def test_module_base(self): |
55 | 55 |
|
56 | 56 | assert exception_raised |
57 | 57 |
|
| 58 | + def test_is_host_detection(self): |
| 59 | + # Test when /.dockerenv does not exist - running on host |
| 60 | + with patch('os.path.exists', return_value=False): |
| 61 | + module = ModuleBase() |
| 62 | + assert module.is_host is True |
| 63 | + |
| 64 | + # Test when /.dockerenv exists - running in container (inside pmon) |
| 65 | + with patch('os.path.exists', return_value=True): |
| 66 | + module = ModuleBase() |
| 67 | + assert module.is_host is False |
| 68 | + |
58 | 69 | def test_sensors(self): |
59 | 70 | module = ModuleBase() |
60 | 71 | assert(module.get_num_voltage_sensors() == 0) |
@@ -173,76 +184,140 @@ def test_handle_pci_rescan(self): |
173 | 184 | def test_handle_sensor_removal(self): |
174 | 185 | module = ModuleBase() |
175 | 186 |
|
| 187 | + # Test successful case on host - commands run via docker exec pmon to access container |
| 188 | + with patch.object(module, 'get_name', return_value="DPU0"), \ |
| 189 | + patch('subprocess.call') as mock_call, \ |
| 190 | + patch.object(module, '_sensord_operation_lock') as mock_lock: |
| 191 | + module.is_host = True |
| 192 | + # First call to test -f (file exists) returns 0, second call is cp, third is service restart |
| 193 | + mock_call.side_effect = [0, 0, 0] |
| 194 | + assert module.handle_sensor_removal() is True |
| 195 | + assert mock_call.call_count == 3 |
| 196 | + # When on host, commands are prefixed with docker exec pmon to run inside container |
| 197 | + mock_call.assert_any_call(['docker', 'exec', 'pmon', 'test', '-f', '/usr/share/sonic/platform/module_sensors_ignore_conf/ignore_sensors_DPU0.conf'], |
| 198 | + stdout=subprocess.DEVNULL) |
| 199 | + mock_call.assert_any_call(['docker', 'exec', 'pmon', 'cp', '/usr/share/sonic/platform/module_sensors_ignore_conf/ignore_sensors_DPU0.conf', |
| 200 | + '/etc/sensors.d/ignore_sensors_DPU0.conf'], |
| 201 | + stdout=subprocess.DEVNULL) |
| 202 | + mock_call.assert_any_call(['docker', 'exec', 'pmon', 'service', 'sensord', 'restart'], |
| 203 | + stdout=subprocess.DEVNULL) |
| 204 | + mock_lock.assert_called_once() |
| 205 | + |
| 206 | + # Test successful case inside container - commands run directly without docker exec |
176 | 207 | with patch.object(module, 'get_name', return_value="DPU0"), \ |
177 | | - patch('os.path.exists', return_value=True), \ |
178 | | - patch('shutil.copy2') as mock_copy, \ |
179 | | - patch('os.system') as mock_system, \ |
| 208 | + patch('subprocess.call') as mock_call, \ |
180 | 209 | patch.object(module, '_sensord_operation_lock') as mock_lock: |
| 210 | + module.is_host = False |
| 211 | + # First call to test -f (file exists) returns 0, second call is cp, third is service restart |
| 212 | + mock_call.side_effect = [0, 0, 0] |
181 | 213 | assert module.handle_sensor_removal() is True |
182 | | - mock_copy.assert_called_once_with("/usr/share/sonic/platform/module_sensors_ignore_conf/ignore_sensors_DPU0.conf", |
183 | | - "/etc/sensors.d/ignore_sensors_DPU0.conf") |
184 | | - mock_system.assert_called_once_with("service sensord restart") |
| 214 | + assert mock_call.call_count == 3 |
| 215 | + # When inside container, commands run directly without docker exec prefix |
| 216 | + mock_call.assert_any_call(['test', '-f', '/usr/share/sonic/platform/module_sensors_ignore_conf/ignore_sensors_DPU0.conf'], |
| 217 | + stdout=subprocess.DEVNULL) |
| 218 | + mock_call.assert_any_call(['cp', '/usr/share/sonic/platform/module_sensors_ignore_conf/ignore_sensors_DPU0.conf', |
| 219 | + '/etc/sensors.d/ignore_sensors_DPU0.conf'], |
| 220 | + stdout=subprocess.DEVNULL) |
| 221 | + mock_call.assert_any_call(['service', 'sensord', 'restart'], |
| 222 | + stdout=subprocess.DEVNULL) |
185 | 223 | mock_lock.assert_called_once() |
186 | 224 |
|
| 225 | + # Test file does not exist - should return True but not call copy or restart |
187 | 226 | with patch.object(module, 'get_name', return_value="DPU0"), \ |
188 | | - patch('os.path.exists', return_value=False), \ |
189 | | - patch('shutil.copy2') as mock_copy, \ |
190 | | - patch('os.system') as mock_system, \ |
| 227 | + patch('subprocess.call') as mock_call, \ |
191 | 228 | patch.object(module, '_sensord_operation_lock') as mock_lock: |
| 229 | + module.is_host = True |
| 230 | + # Return 1 to indicate file doesn't exist |
| 231 | + mock_call.return_value = 1 |
192 | 232 | assert module.handle_sensor_removal() is True |
193 | | - mock_copy.assert_not_called() |
194 | | - mock_system.assert_not_called() |
| 233 | + # Only the file existence check should be called (with docker exec when on host) |
| 234 | + mock_call.assert_called_once_with(['docker', 'exec', 'pmon', 'test', '-f', '/usr/share/sonic/platform/module_sensors_ignore_conf/ignore_sensors_DPU0.conf'], |
| 235 | + stdout=subprocess.DEVNULL) |
195 | 236 | mock_lock.assert_not_called() |
196 | 237 |
|
| 238 | + # Test exception handling |
197 | 239 | with patch.object(module, 'get_name', return_value="DPU0"), \ |
198 | | - patch('os.path.exists', return_value=True), \ |
199 | | - patch('shutil.copy2', side_effect=Exception("Copy failed")): |
| 240 | + patch('subprocess.call', side_effect=Exception("Copy failed")): |
| 241 | + module.is_host = True |
200 | 242 | assert module.handle_sensor_removal() is False |
201 | 243 |
|
202 | 244 | def test_handle_sensor_addition(self): |
203 | 245 | module = ModuleBase() |
204 | 246 |
|
| 247 | + # Test successful case on host - commands run via docker exec pmon to access container |
| 248 | + with patch.object(module, 'get_name', return_value="DPU0"), \ |
| 249 | + patch('subprocess.call') as mock_call, \ |
| 250 | + patch.object(module, '_sensord_operation_lock') as mock_lock: |
| 251 | + module.is_host = True |
| 252 | + # First call to test -f (file exists) returns 0, second call is rm, third is service restart |
| 253 | + mock_call.side_effect = [0, 0, 0] |
| 254 | + assert module.handle_sensor_addition() is True |
| 255 | + assert mock_call.call_count == 3 |
| 256 | + # When on host, commands are prefixed with docker exec pmon to run inside container |
| 257 | + mock_call.assert_any_call(['docker', 'exec', 'pmon', 'test', '-f', '/etc/sensors.d/ignore_sensors_DPU0.conf'], |
| 258 | + stdout=subprocess.DEVNULL) |
| 259 | + mock_call.assert_any_call(['docker', 'exec', 'pmon', 'rm', '/etc/sensors.d/ignore_sensors_DPU0.conf'], |
| 260 | + stdout=subprocess.DEVNULL) |
| 261 | + mock_call.assert_any_call(['docker', 'exec', 'pmon', 'service', 'sensord', 'restart'], |
| 262 | + stdout=subprocess.DEVNULL) |
| 263 | + mock_lock.assert_called_once() |
| 264 | + |
| 265 | + # Test successful case inside container - commands run directly without docker exec |
205 | 266 | with patch.object(module, 'get_name', return_value="DPU0"), \ |
206 | | - patch('os.path.exists', return_value=True), \ |
207 | | - patch('os.remove') as mock_remove, \ |
208 | | - patch('os.system') as mock_system, \ |
| 267 | + patch('subprocess.call') as mock_call, \ |
209 | 268 | patch.object(module, '_sensord_operation_lock') as mock_lock: |
| 269 | + module.is_host = False |
| 270 | + # First call to test -f (file exists) returns 0, second call is rm, third is service restart |
| 271 | + mock_call.side_effect = [0, 0, 0] |
210 | 272 | assert module.handle_sensor_addition() is True |
211 | | - mock_remove.assert_called_once_with("/etc/sensors.d/ignore_sensors_DPU0.conf") |
212 | | - mock_system.assert_called_once_with("service sensord restart") |
| 273 | + assert mock_call.call_count == 3 |
| 274 | + # When inside container, commands run directly without docker exec prefix |
| 275 | + mock_call.assert_any_call(['test', '-f', '/etc/sensors.d/ignore_sensors_DPU0.conf'], |
| 276 | + stdout=subprocess.DEVNULL) |
| 277 | + mock_call.assert_any_call(['rm', '/etc/sensors.d/ignore_sensors_DPU0.conf'], |
| 278 | + stdout=subprocess.DEVNULL) |
| 279 | + mock_call.assert_any_call(['service', 'sensord', 'restart'], |
| 280 | + stdout=subprocess.DEVNULL) |
213 | 281 | mock_lock.assert_called_once() |
214 | 282 |
|
| 283 | + # Test file does not exist - should return True but not call remove or restart |
215 | 284 | with patch.object(module, 'get_name', return_value="DPU0"), \ |
216 | | - patch('os.path.exists', return_value=False), \ |
217 | | - patch('os.remove') as mock_remove, \ |
218 | | - patch('os.system') as mock_system, \ |
| 285 | + patch('subprocess.call') as mock_call, \ |
219 | 286 | patch.object(module, '_sensord_operation_lock') as mock_lock: |
| 287 | + module.is_host = True |
| 288 | + # Return 1 to indicate file doesn't exist |
| 289 | + mock_call.return_value = 1 |
220 | 290 | assert module.handle_sensor_addition() is True |
221 | | - mock_remove.assert_not_called() |
222 | | - mock_system.assert_not_called() |
| 291 | + # Only the file existence check should be called (with docker exec when on host) |
| 292 | + mock_call.assert_called_once_with(['docker', 'exec', 'pmon', 'test', '-f', '/etc/sensors.d/ignore_sensors_DPU0.conf'], |
| 293 | + stdout=subprocess.DEVNULL) |
223 | 294 | mock_lock.assert_not_called() |
224 | 295 |
|
| 296 | + # Test exception handling |
225 | 297 | with patch.object(module, 'get_name', return_value="DPU0"), \ |
226 | | - patch('os.path.exists', return_value=True), \ |
227 | | - patch('os.remove', side_effect=Exception("Remove failed")): |
| 298 | + patch('subprocess.call', side_effect=Exception("Remove failed")): |
| 299 | + module.is_host = True |
228 | 300 | assert module.handle_sensor_addition() is False |
229 | 301 |
|
230 | 302 | def test_module_pre_shutdown(self): |
231 | 303 | module = ModuleBase() |
232 | 304 |
|
233 | 305 | # Test successful case |
234 | | - with patch.object(module, 'handle_pci_removal', return_value=True), \ |
235 | | - patch.object(module, 'handle_sensor_removal', return_value=True): |
| 306 | + with patch.object(module, 'handle_sensor_removal', return_value=True) as mock_sensor, \ |
| 307 | + patch.object(module, 'handle_pci_removal', return_value=True) as mock_pci: |
236 | 308 | assert module.module_pre_shutdown() is True |
| 309 | + # Verify sensor removal is called before PCI removal |
| 310 | + mock_sensor.assert_called_once() |
| 311 | + mock_pci.assert_called_once() |
237 | 312 |
|
238 | | - # Test PCI removal failure |
239 | | - with patch.object(module, 'handle_pci_removal', return_value=False), \ |
240 | | - patch.object(module, 'handle_sensor_removal', return_value=True): |
| 313 | + # Test sensor removal failure |
| 314 | + with patch.object(module, 'handle_sensor_removal', return_value=False), \ |
| 315 | + patch.object(module, 'handle_pci_removal', return_value=True): |
241 | 316 | assert module.module_pre_shutdown() is False |
242 | 317 |
|
243 | | - # Test sensor removal failure |
244 | | - with patch.object(module, 'handle_pci_removal', return_value=True), \ |
245 | | - patch.object(module, 'handle_sensor_removal', return_value=False): |
| 318 | + # Test PCI removal failure |
| 319 | + with patch.object(module, 'handle_sensor_removal', return_value=True), \ |
| 320 | + patch.object(module, 'handle_pci_removal', return_value=False): |
246 | 321 | assert module.module_pre_shutdown() is False |
247 | 322 |
|
248 | 323 | def test_module_post_startup(self): |
|
0 commit comments