|
1 | 1 | # SPDX-License-Identifier: GPL-2.0 |
2 | 2 |
|
3 | 3 | import json |
| 4 | +import tempfile |
4 | 5 | import unittest |
5 | 6 | from unittest import mock |
6 | 7 |
|
@@ -314,6 +315,129 @@ def test_sol_reboot_detected(self): |
314 | 315 | normal_output = "[ 123.456789] Normal kernel message" |
315 | 316 | self.assertFalse(_has_reboot(normal_output)) |
316 | 317 |
|
| 318 | + @mock.patch('subprocess.run') |
| 319 | + @mock.patch('time.monotonic') |
| 320 | + @mock.patch('time.sleep') |
| 321 | + def test_wait_for_results_timeout(self, _mock_sleep, |
| 322 | + mock_monotonic, mock_run): |
| 323 | + """max_test_time exceeded returns WaitResult with error string.""" |
| 324 | + from lib.deployer import wait_for_results, WaitResult |
| 325 | + |
| 326 | + mock_run.return_value = mock.Mock(returncode=0, stdout=b'', stderr=b'') |
| 327 | + mock_monotonic.side_effect = [ |
| 328 | + 0, # start_time |
| 329 | + 99999, # elapsed check -> exceeds max_test_time |
| 330 | + ] |
| 331 | + |
| 332 | + config = mock.Mock() |
| 333 | + config.getint.side_effect = lambda section, key, fallback=None: { |
| 334 | + 'max_test_time': 3600, |
| 335 | + 'sol_poll_interval': 15, |
| 336 | + 'crash_wait_time': 120, |
| 337 | + 'max_kexec_boot_timeout': 300, |
| 338 | + }.get(key, fallback) |
| 339 | + |
| 340 | + mc = mock.Mock() |
| 341 | + mc.get_sol_logs.return_value = {'last_id': 0, 'lines': []} |
| 342 | + |
| 343 | + result = wait_for_results(config, mc, 42, [1], ['10.0.0.1']) |
| 344 | + |
| 345 | + self.assertIsInstance(result, WaitResult) |
| 346 | + self.assertFalse(result.ok) |
| 347 | + self.assertIn('max test time exceeded', result.error) |
| 348 | + |
| 349 | + @mock.patch('subprocess.run') |
| 350 | + @mock.patch('time.monotonic') |
| 351 | + @mock.patch('time.sleep') |
| 352 | + def test_wait_for_results_no_results(self, _mock_sleep, |
| 353 | + mock_monotonic, mock_run): |
| 354 | + """hw-worker exits without results returns WaitResult with error.""" |
| 355 | + from lib.deployer import wait_for_results, WaitResult |
| 356 | + |
| 357 | + mock_run.return_value = mock.Mock(returncode=0, stdout=b'', stderr=b'') |
| 358 | + mock_monotonic.side_effect = [ |
| 359 | + 0, # start_time |
| 360 | + 10, # elapsed check |
| 361 | + ] |
| 362 | + |
| 363 | + config = mock.Mock() |
| 364 | + config.getint.side_effect = lambda section, key, fallback=None: { |
| 365 | + 'max_test_time': 3600, |
| 366 | + 'sol_poll_interval': 15, |
| 367 | + 'crash_wait_time': 120, |
| 368 | + 'max_kexec_boot_timeout': 300, |
| 369 | + }.get(key, fallback) |
| 370 | + |
| 371 | + mc = mock.Mock() |
| 372 | + mc.get_sol_logs.return_value = {'last_id': 0, 'lines': []} |
| 373 | + mc.reservation_refresh.return_value = {'ok': True} |
| 374 | + |
| 375 | + def ssh_retcode_side_effect(ip, cmd, timeout=30): |
| 376 | + if 'test -f' in cmd: |
| 377 | + return 1 # no results.json |
| 378 | + if 'is-active' in cmd: |
| 379 | + return 1 # service exited |
| 380 | + return 0 |
| 381 | + |
| 382 | + with tempfile.TemporaryDirectory() as tmpdir: |
| 383 | + with mock.patch('lib.deployer._ssh_retcode', |
| 384 | + side_effect=ssh_retcode_side_effect): |
| 385 | + with mock.patch('lib.deployer._ssh', |
| 386 | + return_value='Mar 14 hw-worker[123]: some log\n') as mock_ssh: |
| 387 | + result = wait_for_results(config, mc, 42, [1], ['10.0.0.1'], |
| 388 | + results_path=tmpdir) |
| 389 | + |
| 390 | + self.assertIsInstance(result, WaitResult) |
| 391 | + self.assertFalse(result.ok) |
| 392 | + self.assertIn('hw-worker exited without results', result.error) |
| 393 | + |
| 394 | + # Verify journalctl output was fetched and saved |
| 395 | + mock_ssh.assert_called_once() |
| 396 | + self.assertIn('journalctl', mock_ssh.call_args[0][1]) |
| 397 | + journal_file = os.path.join(tmpdir, 'hw-worker-journal') |
| 398 | + self.assertTrue(os.path.exists(journal_file)) |
| 399 | + with open(journal_file) as fp: |
| 400 | + self.assertIn('some log', fp.read()) |
| 401 | + |
| 402 | + @mock.patch('subprocess.run') |
| 403 | + @mock.patch('time.monotonic') |
| 404 | + @mock.patch('time.sleep') |
| 405 | + def test_wait_for_results_success(self, _mock_sleep, |
| 406 | + mock_monotonic, mock_run): |
| 407 | + """hw-worker completes with results returns WaitResult(ok=True).""" |
| 408 | + from lib.deployer import wait_for_results, WaitResult |
| 409 | + |
| 410 | + mock_run.return_value = mock.Mock(returncode=0, stdout=b'', stderr=b'') |
| 411 | + mock_monotonic.side_effect = [ |
| 412 | + 0, # start_time |
| 413 | + 10, # elapsed check |
| 414 | + ] |
| 415 | + |
| 416 | + config = mock.Mock() |
| 417 | + config.getint.side_effect = lambda section, key, fallback=None: { |
| 418 | + 'max_test_time': 3600, |
| 419 | + 'sol_poll_interval': 15, |
| 420 | + 'crash_wait_time': 120, |
| 421 | + 'max_kexec_boot_timeout': 300, |
| 422 | + }.get(key, fallback) |
| 423 | + |
| 424 | + mc = mock.Mock() |
| 425 | + mc.get_sol_logs.return_value = {'last_id': 0, 'lines': []} |
| 426 | + mc.reservation_refresh.return_value = {'ok': True} |
| 427 | + |
| 428 | + def ssh_retcode_side_effect(ip, cmd, timeout=30): |
| 429 | + if 'test -f' in cmd: |
| 430 | + return 0 # results.json exists |
| 431 | + return 0 |
| 432 | + |
| 433 | + with mock.patch('lib.deployer._ssh_retcode', |
| 434 | + side_effect=ssh_retcode_side_effect): |
| 435 | + result = wait_for_results(config, mc, 42, [1], ['10.0.0.1']) |
| 436 | + |
| 437 | + self.assertIsInstance(result, WaitResult) |
| 438 | + self.assertTrue(result.ok) |
| 439 | + self.assertEqual(result.error, '') |
| 440 | + |
317 | 441 | @mock.patch('subprocess.run') |
318 | 442 | @mock.patch('time.monotonic') |
319 | 443 | @mock.patch('time.sleep') |
|
0 commit comments