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
311 lines
12 KiB
Python
311 lines
12 KiB
Python
# SPDX-License-Identifier: MIT
|
|
"""Tests for utils.specs — host CPU/RAM detection + tier presets (v0.2.1)."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from unittest.mock import patch
|
|
|
|
from winpodx.utils.specs import (
|
|
HostSpecs,
|
|
TuningCapability,
|
|
all_tiers,
|
|
detect_host_specs,
|
|
format_tuning_summary,
|
|
recommend_tier,
|
|
recommend_tuning_profile,
|
|
)
|
|
|
|
|
|
class TestRecommendTier:
|
|
def test_high_when_both_axes_clear(self):
|
|
t = recommend_tier(HostSpecs(cpu_threads=16, ram_gb=64))
|
|
assert t.name == "high"
|
|
assert t.cpu_cores == 8
|
|
assert t.ram_gb == 12
|
|
|
|
def test_mid_when_both_axes_clear_mid(self):
|
|
t = recommend_tier(HostSpecs(cpu_threads=8, ram_gb=20))
|
|
assert t.name == "mid"
|
|
assert t.cpu_cores == 4
|
|
assert t.ram_gb == 6
|
|
|
|
def test_mid_bumps_to_8gb_at_24gb_host(self):
|
|
# #630: a >=24 GB host (still mid CPU tier) gets an 8 GB VM instead of
|
|
# 6 — Windows 11 runs better with 8 GB and the host can spare it.
|
|
for ram in (24, 28, 31):
|
|
t = recommend_tier(HostSpecs(cpu_threads=8, ram_gb=ram))
|
|
assert t.name == "mid", ram
|
|
assert t.cpu_cores == 4, ram
|
|
assert t.ram_gb == 8, ram
|
|
# Just under 24 GB stays at 6.
|
|
assert recommend_tier(HostSpecs(cpu_threads=8, ram_gb=23)).ram_gb == 6
|
|
|
|
def test_low_for_small_machines(self):
|
|
t = recommend_tier(HostSpecs(cpu_threads=2, ram_gb=8))
|
|
assert t.name == "low"
|
|
assert t.cpu_cores == 2
|
|
assert t.ram_gb == 4
|
|
|
|
def test_ram_rich_but_cpu_poor_falls_to_low(self):
|
|
# Picking the higher tier requires BOTH axes to clear — a host
|
|
# with 64 GB but only 4 cores still gets "low" (CPU is the
|
|
# bottleneck for the VM workload).
|
|
t = recommend_tier(HostSpecs(cpu_threads=4, ram_gb=64))
|
|
assert t.name == "low"
|
|
|
|
def test_cpu_rich_but_ram_poor_falls_to_low(self):
|
|
# Symmetric case — lots of CPU but only 8 GB RAM.
|
|
t = recommend_tier(HostSpecs(cpu_threads=24, ram_gb=8))
|
|
assert t.name == "low"
|
|
|
|
def test_exactly_at_mid_threshold(self):
|
|
t = recommend_tier(HostSpecs(cpu_threads=6, ram_gb=16))
|
|
assert t.name == "mid"
|
|
|
|
def test_just_below_mid_threshold(self):
|
|
t = recommend_tier(HostSpecs(cpu_threads=5, ram_gb=16))
|
|
assert t.name == "low" # cpu under 6 -> low
|
|
t = recommend_tier(HostSpecs(cpu_threads=6, ram_gb=15))
|
|
assert t.name == "low" # ram under 16 -> low
|
|
|
|
def test_exactly_at_high_threshold(self):
|
|
t = recommend_tier(HostSpecs(cpu_threads=12, ram_gb=32))
|
|
assert t.name == "high"
|
|
|
|
|
|
class TestDetectHostSpecs:
|
|
def test_returns_usable_object(self):
|
|
# Just verify the function runs and returns sane values on the
|
|
# test host — exact numbers vary by CI runner.
|
|
s = detect_host_specs()
|
|
assert s.cpu_threads >= 1
|
|
assert s.ram_gb >= 1
|
|
|
|
|
|
class TestAllTiers:
|
|
def test_returns_three_in_order(self):
|
|
tiers = all_tiers()
|
|
assert [t.name for t in tiers] == ["low", "mid", "high"]
|
|
# CPU and RAM must be monotonically non-decreasing.
|
|
cpus = [t.cpu_cores for t in tiers]
|
|
rams = [t.ram_gb for t in tiers]
|
|
assert cpus == sorted(cpus)
|
|
assert rams == sorted(rams)
|
|
|
|
|
|
def _cap(**overrides) -> TuningCapability:
|
|
"""Build a TuningCapability with sensible defaults; override per test."""
|
|
base = {
|
|
"invtsc": True,
|
|
"io_uring": True,
|
|
"hugepages_enabled": False,
|
|
"dedicated_host": True,
|
|
"kernel_version": (6, 18),
|
|
"cpu_vendor": "intel",
|
|
"nested_kvm": False,
|
|
}
|
|
base.update(overrides)
|
|
return TuningCapability(**base)
|
|
|
|
|
|
class TestRecommendTuningProfile:
|
|
"""Profile-resolution policy for #215. The capability is a snapshot of
|
|
host state; the user_pref dial decides how aggressive to be."""
|
|
|
|
def test_off_disables_everything(self):
|
|
p = recommend_tuning_profile(_cap(nested_kvm=True), user_pref="off")
|
|
assert p.name == "off"
|
|
assert not p.apply_invtsc
|
|
assert not p.apply_platform_tick
|
|
assert not p.apply_io_uring
|
|
assert not p.apply_cpu_pinning
|
|
# #245: new flags also gated off.
|
|
assert not p.apply_hv_enlightenments
|
|
assert not p.apply_virtio_rng
|
|
assert not p.apply_evmcs
|
|
assert not p.apply_nested_virt
|
|
|
|
def test_safe_applies_only_t1_tunings(self):
|
|
p = recommend_tuning_profile(_cap(), user_pref="safe")
|
|
assert p.name == "safe"
|
|
assert p.apply_invtsc
|
|
assert p.apply_platform_tick
|
|
# Host-setup tunings are off in safe mode regardless of capability.
|
|
assert not p.apply_io_uring
|
|
assert not p.apply_hugepages
|
|
assert not p.apply_cpu_pinning
|
|
assert not p.apply_no_balloon
|
|
# #245: hv-* + virtio-rng safe -> on. evmcs + nested-virt need
|
|
# explicit host-side opt-in -> off under "safe".
|
|
assert p.apply_hv_enlightenments
|
|
assert p.apply_virtio_rng
|
|
assert not p.apply_evmcs
|
|
assert not p.apply_nested_virt
|
|
|
|
def test_auto_applies_everything_supported(self):
|
|
p = recommend_tuning_profile(
|
|
_cap(hugepages_enabled=True, nested_kvm=True), user_pref="auto"
|
|
)
|
|
assert p.name == "auto"
|
|
assert p.apply_invtsc
|
|
assert p.apply_io_uring
|
|
assert p.apply_hugepages
|
|
assert p.apply_cpu_pinning # dedicated host
|
|
assert p.apply_platform_tick
|
|
assert p.apply_no_balloon
|
|
# #245: hv-* + virtio-rng always on under auto for x86 hosts.
|
|
assert p.apply_hv_enlightenments
|
|
assert p.apply_virtio_rng
|
|
# nested-virt + evmcs gated on nested_kvm; here it's True.
|
|
assert p.apply_evmcs # intel
|
|
assert p.apply_nested_virt
|
|
|
|
def test_auto_skips_unsupported_capabilities(self):
|
|
cap = _cap(invtsc=False, io_uring=False, dedicated_host=False, nested_kvm=False)
|
|
p = recommend_tuning_profile(cap, user_pref="auto")
|
|
assert not p.apply_invtsc
|
|
assert not p.apply_io_uring
|
|
assert not p.apply_cpu_pinning
|
|
assert not p.apply_no_balloon
|
|
# platform_tick is guest-side + always safe.
|
|
assert p.apply_platform_tick
|
|
# #245: hv-* + virtio-rng still on (x86 + always-safe). evmcs +
|
|
# nested-virt gated on nested_kvm -> off here.
|
|
assert p.apply_hv_enlightenments
|
|
assert p.apply_virtio_rng
|
|
assert not p.apply_evmcs
|
|
assert not p.apply_nested_virt
|
|
|
|
def test_invtsc_requires_x86_vendor(self):
|
|
# invtsc is x86-only; ARM TSC story is different. The recommender
|
|
# must not flip it on even when /proc/cpuinfo happens to expose
|
|
# the named flags (unlikely but defensive).
|
|
p = recommend_tuning_profile(_cap(cpu_vendor="arm"), user_pref="auto")
|
|
assert not p.apply_invtsc
|
|
# #245: hv-* + nested-virt are also x86-only.
|
|
assert not p.apply_hv_enlightenments
|
|
assert not p.apply_nested_virt
|
|
assert not p.apply_evmcs
|
|
# virtio-rng is generic; on for any vendor.
|
|
assert p.apply_virtio_rng
|
|
|
|
def test_auto_nested_virt_intel_uses_vmx(self):
|
|
# Intel + nested_kvm => +vmx + hv-evmcs.
|
|
p = recommend_tuning_profile(_cap(cpu_vendor="intel", nested_kvm=True), user_pref="auto")
|
|
assert p.apply_nested_virt
|
|
assert p.apply_evmcs
|
|
|
|
def test_auto_nested_virt_amd_uses_svm_no_evmcs(self):
|
|
# AMD + nested_kvm => +svm. evmcs is Intel-only -> stays off.
|
|
p = recommend_tuning_profile(_cap(cpu_vendor="amd", nested_kvm=True), user_pref="auto")
|
|
assert p.apply_nested_virt
|
|
assert not p.apply_evmcs
|
|
|
|
def test_auto_evmcs_requires_nested_kvm(self):
|
|
# Intel without nested_kvm exposed -> evmcs off (no-op without nesting).
|
|
p = recommend_tuning_profile(_cap(cpu_vendor="intel", nested_kvm=False), user_pref="auto")
|
|
assert not p.apply_evmcs
|
|
assert not p.apply_nested_virt
|
|
|
|
def test_performance_forces_pinning_and_no_balloon(self):
|
|
# `performance` bypasses cap.dedicated_host so soft-gated knobs
|
|
# (CPU pinning + no-balloon) come on even on a shared host.
|
|
cap = _cap(dedicated_host=False)
|
|
p = recommend_tuning_profile(cap, user_pref="performance")
|
|
assert p.name == "performance"
|
|
assert p.apply_cpu_pinning
|
|
assert p.apply_no_balloon
|
|
# Hard-gated knobs still respect capability detection.
|
|
assert p.apply_invtsc # invtsc=True in fixture
|
|
assert p.apply_io_uring # io_uring=True in fixture
|
|
# hv-* + virtio-rng same as auto.
|
|
assert p.apply_hv_enlightenments
|
|
assert p.apply_virtio_rng
|
|
|
|
def test_performance_still_skips_unsupported_hard_gates(self):
|
|
# invtsc + io_uring are HARD-gated -- performance can't force a
|
|
# CPU flag QEMU would reject or a kernel feature that crashes.
|
|
cap = _cap(invtsc=False, io_uring=False, dedicated_host=False)
|
|
p = recommend_tuning_profile(cap, user_pref="performance")
|
|
assert not p.apply_invtsc
|
|
assert not p.apply_io_uring
|
|
# Soft-gated knobs forced on regardless.
|
|
assert p.apply_cpu_pinning
|
|
assert p.apply_no_balloon
|
|
|
|
def test_performance_matches_auto_when_host_already_dedicated(self):
|
|
# On a dedicated host, performance and auto resolve to the same
|
|
# set of apply_* flags (only the .name differs).
|
|
cap = _cap(dedicated_host=True, nested_kvm=True)
|
|
perf = recommend_tuning_profile(cap, user_pref="performance")
|
|
auto = recommend_tuning_profile(cap, user_pref="auto")
|
|
for field in (
|
|
"apply_invtsc",
|
|
"apply_io_uring",
|
|
"apply_hugepages",
|
|
"apply_cpu_pinning",
|
|
"apply_platform_tick",
|
|
"apply_no_balloon",
|
|
"apply_hv_enlightenments",
|
|
"apply_virtio_rng",
|
|
"apply_evmcs",
|
|
"apply_nested_virt",
|
|
):
|
|
assert getattr(perf, field) == getattr(auto, field), (
|
|
f"{field} differs between performance and auto on dedicated host"
|
|
)
|
|
|
|
def test_unknown_pref_falls_back_to_auto(self):
|
|
# Defensive: a hand-edited TOML slipping past Config validation
|
|
# with a typo like "automatic" should be treated as auto, not
|
|
# silently kill all tunings.
|
|
p = recommend_tuning_profile(_cap(), user_pref="automatic-typo")
|
|
assert p.name == "auto"
|
|
|
|
|
|
class TestFormatTuningSummary:
|
|
def test_renders_yes_no_for_every_capability(self):
|
|
cap = _cap()
|
|
profile = recommend_tuning_profile(cap, user_pref="auto")
|
|
out = format_tuning_summary(cap, profile)
|
|
assert "invtsc" in out
|
|
assert "io_uring" in out
|
|
assert "hugepages" in out
|
|
assert "Profile: auto" in out
|
|
assert "+invtsc:" in out
|
|
# #245: new rows.
|
|
assert "nested_kvm" in out
|
|
assert "hv-* + no-hpet" in out
|
|
assert "virtio-rng" in out
|
|
assert "nested virt" in out
|
|
assert "hv-evmcs" in out
|
|
|
|
|
|
class TestDetectTuningCapabilityIntegration:
|
|
"""`detect_tuning_capability` reads /proc + os.uname; smoke-test that it
|
|
runs to completion on the actual test host without raising."""
|
|
|
|
def test_runs_without_error(self):
|
|
from winpodx.utils.specs import detect_tuning_capability
|
|
|
|
cap = detect_tuning_capability(vm_cpu_cores=4, vm_ram_gb=6)
|
|
assert isinstance(cap.invtsc, bool)
|
|
assert isinstance(cap.io_uring, bool)
|
|
assert isinstance(cap.hugepages_enabled, bool)
|
|
assert isinstance(cap.dedicated_host, bool)
|
|
assert cap.cpu_vendor in ("intel", "amd", "arm", "unknown")
|
|
|
|
def test_kernel_version_parses(self):
|
|
from winpodx.utils.specs import _read_kernel_version
|
|
|
|
with patch("os.uname") as mock_uname:
|
|
mock_uname.return_value.release = "6.18.29-1-longterm"
|
|
assert _read_kernel_version() == (6, 18)
|
|
|
|
def test_kernel_version_unparseable_returns_none(self):
|
|
from winpodx.utils.specs import _read_kernel_version
|
|
|
|
with patch("os.uname") as mock_uname:
|
|
mock_uname.return_value.release = "weird-no-numbers"
|
|
assert _read_kernel_version() is None
|