|
8 | 8 |
|
9 | 9 | from aignostics.system._service import Service |
10 | 10 |
|
| 11 | +# --------------------------------------------------------------------------- |
| 12 | +# Helpers shared by uptime tests |
| 13 | +# --------------------------------------------------------------------------- |
| 14 | + |
| 15 | +FIXED_BOOT_TIME = 1_000_000.0 # arbitrary fixed epoch seconds |
| 16 | + |
| 17 | + |
| 18 | +def _make_mock_process() -> mock.MagicMock: |
| 19 | + proc = mock.MagicMock() |
| 20 | + proc.username.return_value = "testuser" |
| 21 | + return proc |
| 22 | + |
| 23 | + |
| 24 | +def _patch_info_dependencies(boot_time: float = FIXED_BOOT_TIME): |
| 25 | + """Return a context manager stack that patches all external I/O for Service.info().""" |
| 26 | + import contextlib |
| 27 | + |
| 28 | + now = boot_time + 3600.0 # pretend system has been up 1 hour |
| 29 | + |
| 30 | + vmem = mock.MagicMock() |
| 31 | + vmem.percent = 50.0 |
| 32 | + vmem.total = 8_000_000_000 |
| 33 | + vmem.available = 4_000_000_000 |
| 34 | + vmem.used = 3_500_000_000 |
| 35 | + vmem.free = 500_000_000 |
| 36 | + |
| 37 | + swap = mock.MagicMock() |
| 38 | + swap.percent = 10.0 |
| 39 | + swap.total = 2_000_000_000 |
| 40 | + swap.used = 200_000_000 |
| 41 | + swap.free = 1_800_000_000 |
| 42 | + |
| 43 | + cpu_times = mock.MagicMock() |
| 44 | + cpu_times.user = 20.0 |
| 45 | + cpu_times.system = 10.0 |
| 46 | + cpu_times.idle = 70.0 |
| 47 | + |
| 48 | + from aignostics.utils._process import ParentProcessInfo, ProcessInfo |
| 49 | + |
| 50 | + mock_process_info = ProcessInfo( |
| 51 | + project_root="/fake/root", |
| 52 | + pid=1234, |
| 53 | + parent=ParentProcessInfo(name="pytest", pid=1), |
| 54 | + ) |
| 55 | + |
| 56 | + @contextlib.contextmanager |
| 57 | + def _ctx(): |
| 58 | + with ( |
| 59 | + mock.patch("psutil.boot_time", return_value=boot_time), |
| 60 | + mock.patch("psutil.virtual_memory", return_value=vmem), |
| 61 | + mock.patch("psutil.swap_memory", return_value=swap), |
| 62 | + mock.patch("psutil.cpu_percent", return_value=15.0), |
| 63 | + mock.patch("psutil.cpu_times_percent", return_value=cpu_times), |
| 64 | + mock.patch("psutil.getloadavg", return_value=(1.0, 1.0, 1.0)), |
| 65 | + mock.patch("psutil.Process", return_value=_make_mock_process()), |
| 66 | + mock.patch("aignostics.system._service.get_process_info", return_value=mock_process_info), |
| 67 | + mock.patch("asyncio.sleep"), |
| 68 | + mock.patch.object(Service, "_get_public_ipv4", return_value=None), |
| 69 | + mock.patch.object(Service, "_collect_all_settings", return_value={}), |
| 70 | + mock.patch("aignostics.system._service.locate_subclasses", return_value=[]), |
| 71 | + mock.patch("time.time", return_value=now), |
| 72 | + ): |
| 73 | + yield |
| 74 | + |
| 75 | + return _ctx() |
| 76 | + |
11 | 77 |
|
12 | 78 | @pytest.mark.unit |
13 | 79 | def test_get_cpu_freq_info_returns_dict_with_expected_keys() -> None: |
@@ -397,3 +463,51 @@ def test_is_secret_key_real_world_examples(record_property) -> None: |
397 | 463 |
|
398 | 464 | for key in non_secret_examples: |
399 | 465 | assert not Service._is_secret_key(key), f"Expected '{key}' to NOT be identified as a secret key" |
| 466 | + |
| 467 | + |
| 468 | +# --------------------------------------------------------------------------- |
| 469 | +# Uptime tests — verify psutil-based implementation |
| 470 | +# --------------------------------------------------------------------------- |
| 471 | + |
| 472 | + |
| 473 | +@pytest.mark.unit |
| 474 | +@pytest.mark.asyncio |
| 475 | +async def test_info_uptime_keys_present() -> None: |
| 476 | + """info() uptime dict contains both 'seconds' and 'boottime' keys with non-None values.""" |
| 477 | + with _patch_info_dependencies(): |
| 478 | + result = await Service.info() |
| 479 | + |
| 480 | + uptime = result["runtime"]["host"]["uptime"] |
| 481 | + assert "seconds" in uptime |
| 482 | + assert "boottime" in uptime |
| 483 | + assert uptime["seconds"] is not None |
| 484 | + assert uptime["boottime"] is not None |
| 485 | + |
| 486 | + |
| 487 | +@pytest.mark.unit |
| 488 | +@pytest.mark.asyncio |
| 489 | +async def test_info_uptime_seconds_positive() -> None: |
| 490 | + """info() uptime seconds is a positive number (time since boot).""" |
| 491 | + with _patch_info_dependencies(): |
| 492 | + result = await Service.info() |
| 493 | + |
| 494 | + seconds = result["runtime"]["host"]["uptime"]["seconds"] |
| 495 | + assert isinstance(seconds, float) |
| 496 | + assert seconds > 0 |
| 497 | + |
| 498 | + |
| 499 | +@pytest.mark.unit |
| 500 | +@pytest.mark.asyncio |
| 501 | +async def test_info_uptime_boottime_is_iso_string() -> None: |
| 502 | + """info() uptime boottime is a non-empty ISO 8601 string.""" |
| 503 | + import datetime |
| 504 | + |
| 505 | + with _patch_info_dependencies(): |
| 506 | + result = await Service.info() |
| 507 | + |
| 508 | + boottime_str = result["runtime"]["host"]["uptime"]["boottime"] |
| 509 | + assert isinstance(boottime_str, str) |
| 510 | + assert len(boottime_str) > 0 |
| 511 | + # Must be parseable as an ISO 8601 datetime |
| 512 | + parsed = datetime.datetime.fromisoformat(boottime_str) |
| 513 | + assert parsed.tzinfo is not None # timezone-aware (UTC) |
0 commit comments