Files
wehub-resource-sync 3e779be6f3
CI / lint (push) Failing after 13m4s
CI / test (3.11, ubuntu-latest) (push) Failing after 2m4s
CI / test (3.13, ubuntu-latest) (push) Successful in 13m30s
CI / test (3.14, ubuntu-latest) (push) Successful in 17m21s
CI / test (3.12, ubuntu-latest) (push) Successful in 17m55s
CI / discover-apps-ps (push) Successful in 1m56s
CI / test (3.9, ubuntu-latest) (push) Successful in 13m17s
CI / test (3.10, ubuntu-latest) (push) Successful in 26m21s
CI / audit (push) Successful in 13m38s
Deploy site / deploy (push) Has been cancelled
CI / test (3.14, ubuntu-24.04-arm) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:32:37 +08:00

306 lines
9.4 KiB
Python

# SPDX-License-Identifier: MIT
"""Unit tests for disk auto-grow / manual grow helpers (#318).
Pure host-side logic only -- the guest ``/exec`` calls (usage probe,
partition extend) are mocked. The actual diskpart / Resize-Partition
behaviour is covered by the real-Windows smoke gate, not here."""
from __future__ import annotations
import pytest
from winpodx.core.config import Config
from winpodx.core.disk import (
DiskError,
DiskUsage,
compute_autogrow_target,
compute_grow_target,
effective_max_bytes,
format_size,
get_guest_resources,
maybe_autogrow,
parse_size,
)
GIB = 1024**3
class _AgentResult:
def __init__(self, rc: int, stdout: str) -> None:
self.rc = rc
self.stdout = stdout
class _AgentTransport:
name = "agent"
def __init__(self, result: _AgentResult) -> None:
self._result = result
def exec(self, _payload, *, timeout=None, description=None):
return self._result
def test_get_guest_resources_parses_disk_and_ram(monkeypatch) -> None:
import json
payload = json.dumps(
{
"total": 64 * GIB,
"free": 16 * GIB,
"ramTotal": 16 * GIB,
"ramFree": 4 * GIB,
}
)
monkeypatch.setattr(
"winpodx.core.transport.dispatch",
lambda _cfg: _AgentTransport(_AgentResult(0, payload)),
)
gr = get_guest_resources(Config())
assert gr is not None
assert gr.disk is not None
assert gr.disk.total_bytes == 64 * GIB
assert gr.disk.used_bytes == 48 * GIB
assert gr.ram_total_bytes == 16 * GIB
assert gr.ram_used_bytes == 12 * GIB # 16 GiB total - 4 GiB free
def test_get_guest_resources_none_when_transport_not_agent(monkeypatch) -> None:
class _FreeRDP:
name = "freerdp"
monkeypatch.setattr("winpodx.core.transport.dispatch", lambda _cfg: _FreeRDP())
assert get_guest_resources(Config()) is None
def test_get_guest_resources_none_on_bad_rc(monkeypatch) -> None:
monkeypatch.setattr(
"winpodx.core.transport.dispatch",
lambda _cfg: _AgentTransport(_AgentResult(1, "")),
)
assert get_guest_resources(Config()) is None
def test_parse_size_units() -> None:
assert parse_size("64G") == 64 * GIB
assert parse_size("1T") == 1024 * GIB
assert parse_size("512M") == 512 * 1024**2
assert parse_size(" 128G ") == 128 * GIB # whitespace tolerated
@pytest.mark.parametrize("bad", ["", "0G", "abc", "-5G", "64X", "G"])
def test_parse_size_rejects_garbage(bad: str) -> None:
with pytest.raises(DiskError):
parse_size(bad)
def test_format_size_roundtrips_whole_units() -> None:
assert format_size(64 * GIB) == "64G"
assert format_size(1024 * GIB) == "1T"
assert format_size(96 * GIB) == "96G"
def test_format_size_rounds_up_partial_to_gib() -> None:
# Non-whole-unit byte counts round up to the next whole GiB so a grow
# never lands below the requested size.
assert format_size(64 * GIB + 1) == "65G"
def test_compute_grow_target_increment() -> None:
cfg = Config()
cfg.pod.disk_size = "64G"
cfg.pod.disk_autogrow_increment = "32G"
assert compute_grow_target(cfg) == "96G"
def test_compute_grow_target_explicit_size() -> None:
cfg = Config()
cfg.pod.disk_size = "64G"
assert compute_grow_target(cfg, target_size="200G") == "200G"
def test_compute_grow_target_custom_increment() -> None:
cfg = Config()
cfg.pod.disk_size = "64G"
assert compute_grow_target(cfg, increment="64G") == "128G"
def test_compute_grow_target_refuses_shrink_or_noop() -> None:
cfg = Config()
cfg.pod.disk_size = "128G"
with pytest.raises(DiskError):
compute_grow_target(cfg, target_size="64G")
with pytest.raises(DiskError):
compute_grow_target(cfg, target_size="128G")
def test_compute_grow_target_enforces_explicit_cap() -> None:
cfg = Config()
cfg.pod.storage_path = "" # no host probe -> cap is the explicit one
cfg.pod.disk_size = "480G"
cfg.pod.disk_max_size = "512G"
cfg.pod.disk_autogrow_increment = "64G" # 480 + 64 = 544 > 512
with pytest.raises(DiskError):
compute_grow_target(cfg)
def test_compute_grow_target_unbounded_without_cap_or_host() -> None:
# No explicit cap and no resolvable host path -> no ceiling.
cfg = Config()
cfg.pod.storage_path = ""
cfg.pod.disk_max_size = ""
cfg.pod.disk_size = "64G"
assert compute_grow_target(cfg, target_size="2000G") == "2000G"
def test_effective_max_bytes_none_when_unbounded() -> None:
cfg = Config()
cfg.pod.storage_path = ""
cfg.pod.disk_max_size = ""
assert effective_max_bytes(cfg, 64 * GIB) is None
def test_effective_max_bytes_uses_explicit_cap() -> None:
cfg = Config()
cfg.pod.storage_path = ""
cfg.pod.disk_max_size = "256G"
assert effective_max_bytes(cfg, 64 * GIB) == 256 * GIB
def test_compute_autogrow_target_restores_headroom() -> None:
# 64G disk, 58G used (~91%), target 30% free -> need total >= 58/0.7
# ~= 82.9G -> round up to whole 32G increments from 64 -> 96G.
cfg = Config()
cfg.pod.storage_path = ""
cfg.pod.disk_max_size = ""
cfg.pod.disk_size = "64G"
cfg.pod.disk_autogrow_increment = "32G"
cfg.pod.disk_autogrow_target_free_pct = 30
usage = DiskUsage(total_bytes=64 * GIB, free_bytes=6 * GIB)
assert compute_autogrow_target(cfg, usage) == "96G"
def test_compute_autogrow_target_clamped_to_cap() -> None:
cfg = Config()
cfg.pod.storage_path = ""
cfg.pod.disk_max_size = "80G" # only one 32G step fits from 64G? no -> 64 only
cfg.pod.disk_size = "64G"
cfg.pod.disk_autogrow_increment = "32G"
usage = DiskUsage(total_bytes=64 * GIB, free_bytes=2 * GIB)
# 64 + 32 = 96 > 80 cap, and no whole increment fits under 80 -> None.
assert compute_autogrow_target(cfg, usage) is None
def test_disk_usage_pct() -> None:
u = DiskUsage(total_bytes=100 * GIB, free_bytes=10 * GIB)
assert u.used_bytes == 90 * GIB
assert u.used_pct == pytest.approx(90.0)
def test_disk_usage_zero_total_is_safe() -> None:
u = DiskUsage(total_bytes=0, free_bytes=0)
assert u.used_pct == 0.0
def _cfg_autogrow() -> Config:
cfg = Config()
cfg.pod.backend = "podman"
cfg.pod.disk_size = "64G"
cfg.pod.disk_max_size = "512G"
cfg.pod.disk_autogrow = True
cfg.pod.disk_autogrow_threshold_pct = 80
cfg.pod.disk_autogrow_increment = "32G"
return cfg
def test_maybe_autogrow_disabled(monkeypatch: pytest.MonkeyPatch) -> None:
cfg = _cfg_autogrow()
cfg.pod.disk_autogrow = False
# Should never even probe when disabled.
monkeypatch.setattr(
"winpodx.core.disk.get_guest_disk_usage",
lambda *a, **k: pytest.fail("probe should not run when autogrow off"),
)
assert maybe_autogrow(cfg) is False
def test_maybe_autogrow_below_threshold(monkeypatch: pytest.MonkeyPatch) -> None:
cfg = _cfg_autogrow()
monkeypatch.setattr(
"winpodx.core.disk.get_guest_disk_usage",
lambda *a, **k: DiskUsage(total_bytes=100 * GIB, free_bytes=50 * GIB),
)
grew = {"called": False}
monkeypatch.setattr(
"winpodx.core.disk.grow_disk",
lambda *a, **k: grew.__setitem__("called", True),
)
assert maybe_autogrow(cfg) is False
assert grew["called"] is False
def test_maybe_autogrow_triggers_above_threshold(monkeypatch: pytest.MonkeyPatch) -> None:
cfg = _cfg_autogrow() # disk_size 64G, max 512G
monkeypatch.setattr(
"winpodx.core.disk.get_guest_disk_usage",
lambda *a, **k: DiskUsage(total_bytes=64 * GIB, free_bytes=3 * GIB),
)
calls = {"n": 0}
monkeypatch.setattr(
"winpodx.core.disk.grow_disk",
lambda *a, **k: calls.__setitem__("n", calls["n"] + 1),
)
assert maybe_autogrow(cfg) is True
assert calls["n"] == 1
def test_maybe_autogrow_skips_at_cap(monkeypatch: pytest.MonkeyPatch) -> None:
cfg = _cfg_autogrow()
cfg.pod.disk_size = "512G" # already at the explicit cap
cfg.pod.disk_max_size = "512G"
monkeypatch.setattr(
"winpodx.core.disk.get_guest_disk_usage",
lambda *a, **k: DiskUsage(total_bytes=512 * GIB, free_bytes=5 * GIB),
)
grew = {"called": False}
monkeypatch.setattr(
"winpodx.core.disk.grow_disk",
lambda *a, **k: grew.__setitem__("called", True),
)
# Over threshold but can't grow past the cap -> no grow.
assert maybe_autogrow(cfg) is False
assert grew["called"] is False
def test_maybe_autogrow_unreachable_guest(monkeypatch: pytest.MonkeyPatch) -> None:
cfg = _cfg_autogrow()
monkeypatch.setattr("winpodx.core.disk.get_guest_disk_usage", lambda *a, **k: None)
assert maybe_autogrow(cfg) is False
def test_config_validation_clamps_threshold() -> None:
cfg = Config()
cfg.pod.disk_autogrow_threshold_pct = 200
cfg.pod.__post_init__()
assert cfg.pod.disk_autogrow_threshold_pct == 99
cfg.pod.disk_autogrow_threshold_pct = 5
cfg.pod.__post_init__()
assert cfg.pod.disk_autogrow_threshold_pct == 50
def test_config_validation_coerces_bad_sizes() -> None:
cfg = Config()
cfg.pod.disk_autogrow_increment = "garbage"
cfg.pod.disk_max_size = "0G" # invalid -> empty (no cap)
cfg.pod.disk_autogrow_target_free_pct = 99 # out of range -> clamp to 50
cfg.pod.__post_init__()
assert cfg.pod.disk_autogrow_increment == "32G"
assert cfg.pod.disk_max_size == ""
assert cfg.pod.disk_autogrow_target_free_pct == 50
def test_config_max_size_empty_default() -> None:
cfg = Config()
assert cfg.pod.disk_max_size == ""