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

548 lines
22 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# SPDX-License-Identifier: MIT
"""Architecture-aware compose template generation tests.
After the #287 refactor, CPU sub-flags live in the dedicated
``CPU_FLAGS:`` env (consumed by dockur's ``proc.sh``) rather than being
injected through ``ARGUMENTS:``. ``ARGUMENTS:`` now carries only the
non-``-cpu`` extras (virtio-rng device pair). dockur owns the hv-*
enlightenments via its ``HV=Y`` default and the nested-virt sub-flags
via the ``VMX=Y`` env.
The remaining x86 vs aarch64 split is in ``CPU_FLAGS``:
- x86_64: ``arch_capabilities=off`` (#141 / #140 -- Intel-only bits
crash Windows when leaked through) plus ``+invtsc`` when supported
(#215).
- aarch64: empty CPU_FLAGS -- dockur picks the right CPU on ARM.
"""
from __future__ import annotations
import pytest
import winpodx.core.config as _config_module
import winpodx.core.pod.compose as _compose_module
from winpodx.core.config import Config
from winpodx.core.pod.compose import _build_compose_content
@pytest.fixture(autouse=True)
def _stub_smbios_blob_write(monkeypatch):
"""Keep these tests hermetic: don't write a real SMBIOS blob to the OEM dir.
The disguise (#246, T1.5) writes a synthetic SMBIOS blob during compose
generation; stub the writer so the `-smbios file=` arg is still added but no
file I/O happens. tests/test_smbios.py covers the real encode + write path.
"""
monkeypatch.setattr(
_compose_module,
"_write_disguise_smbios_blob",
lambda oem_dir: "/oem/winpodx-smbios.bin",
)
def _cfg() -> Config:
cfg = Config()
cfg.pod.backend = "podman"
cfg.rdp.user = "User"
cfg.rdp.password = "TestPassword1!"
cfg.rdp.port = 3390
cfg.pod.vnc_port = 8007
cfg.pod.container_name = "winpodx-windows"
# Baseline architecture tests must stay independent of host capability
# detection (#215). Turn off the auto tuner so the CPU_FLAGS reflects
# only the architecture branch under test.
cfg.pod.tuning_profile = "off"
# Likewise isolate from the default-ON hypervisor disguise (#246) — the
# arch/tuning tests assert an exact CPU_FLAGS string. The disguise tests
# below set the level explicitly.
cfg.pod.disguise_level = "off"
return cfg
def test_compose_cpu_flags_x86_64(monkeypatch):
"""x86_64 hosts emit ``arch_capabilities=off`` via CPU_FLAGS env."""
monkeypatch.setattr(_compose_module.platform, "machine", lambda: "x86_64")
monkeypatch.setattr(_config_module.platform, "machine", lambda: "x86_64")
content = _build_compose_content(_cfg())
assert 'CPU_FLAGS: "arch_capabilities=off"' in content
def test_compose_ssd_emulation_off_by_default(monkeypatch):
monkeypatch.setattr(_compose_module.platform, "machine", lambda: "x86_64")
monkeypatch.setattr(_config_module.platform, "machine", lambda: "x86_64")
content = _build_compose_content(_cfg())
assert "rotation_rate" not in content
def test_compose_ssd_emulation_injects_rotation_rate(monkeypatch):
# #606: pod.ssd -> -global <driver>.rotation_rate=1 so Windows sees an SSD.
monkeypatch.setattr(_compose_module.platform, "machine", lambda: "x86_64")
monkeypatch.setattr(_config_module.platform, "machine", lambda: "x86_64")
cfg = _cfg()
cfg.pod.ssd = True
content = _build_compose_content(cfg)
assert "ide-hd.rotation_rate=1" in content
assert "scsi-hd.rotation_rate=1" in content
def test_compose_cpu_flags_aarch64(monkeypatch):
"""aarch64 hosts emit empty CPU_FLAGS -- dockur picks the host CPU.
Regression guard for issue #140: passing ``arch_capabilities=off`` on
aarch64 crashes QEMU with ``Property 'host-arm-cpu.arch_capabilities'
not found``.
"""
monkeypatch.setattr(_compose_module.platform, "machine", lambda: "aarch64")
monkeypatch.setattr(_config_module.platform, "machine", lambda: "aarch64")
content = _build_compose_content(_cfg())
assert 'CPU_FLAGS: ""' in content
assert "arch_capabilities" not in content
def test_compose_cpu_flags_unknown_arch_falls_through_to_x86(monkeypatch):
"""Unknown / unexpected machine() value falls through to the x86_64
behaviour. Intentional: an unsupported platform should surface a
clear QEMU error at pod start rather than silently using a partly-
correct ARM config.
"""
monkeypatch.setattr(_compose_module.platform, "machine", lambda: "riscv64")
monkeypatch.setattr(_config_module.platform, "machine", lambda: "riscv64")
content = _build_compose_content(_cfg())
assert 'CPU_FLAGS: "arch_capabilities=off"' in content
def test_compose_cpu_flags_invtsc_off_profile_does_not_append(monkeypatch):
"""``cfg.pod.tuning_profile = "off"`` keeps CPU_FLAGS at the baseline
(``arch_capabilities=off`` only) even on an invtsc-capable host."""
import winpodx.utils.specs as specs
monkeypatch.setattr(_compose_module.platform, "machine", lambda: "x86_64")
monkeypatch.setattr(_config_module.platform, "machine", lambda: "x86_64")
monkeypatch.setattr(
specs,
"detect_tuning_capability",
lambda *, vm_cpu_cores, vm_ram_gb: specs.TuningCapability(
invtsc=True,
io_uring=True,
hugepages_enabled=False,
dedicated_host=False,
kernel_version=(6, 18),
cpu_vendor="intel",
nested_kvm=False,
),
)
cfg = _cfg()
cfg.pod.tuning_profile = "off"
content = _build_compose_content(cfg)
assert 'CPU_FLAGS: "arch_capabilities=off"' in content
# --- Bare-metal compatibility / hypervisor disguise (#246, default ON) ---
_DISGUISE_FLAGS = (
"-hypervisor",
"kvm=off",
"-kvm-pv-eoi",
"-kvm-pv-unhalt",
"-kvm-pv-tlb-flush",
"-kvm-asyncpf",
)
def test_compose_disguise_on_by_default(monkeypatch):
"""disguise_level=None (absent) resolves to balanced — emits the flags."""
monkeypatch.setattr(_compose_module.platform, "machine", lambda: "x86_64")
monkeypatch.setattr(_config_module.platform, "machine", lambda: "x86_64")
cfg = _cfg()
cfg.pod.disguise_level = None # the real default (absent key)
cfg.pod.__post_init__() # re-resolve: None -> "balanced"
cfg.pod.storage_path = "" # named-volume mode → host-trusted disk bump
assert cfg.pod.disguise_level == "balanced"
assert cfg.pod.disguise_active is True
content = _build_compose_content(cfg)
for flag in _DISGUISE_FLAGS:
assert flag in content, f"disguise flag {flag!r} missing (default ON)"
# hv-vendor-id was deliberately dropped (collides with dockur hv flags).
assert "hv-vendor-id" not in content
# balanced keeps Hyper-V on (perf); the disk is bumped to a real size.
assert 'HV: "Y"' in content
assert 'DISK_SIZE: "128G"' in content
def test_compose_disguise_legacy_bool_false_maps_to_off(monkeypatch):
"""A pre-tier config (disguise_hypervisor=False) migrates to level off."""
monkeypatch.setattr(_config_module.platform, "machine", lambda: "x86_64")
cfg = _cfg()
cfg.pod.disguise_level = None
cfg.pod.disguise_hypervisor = False
cfg.pod.__post_init__()
assert cfg.pod.disguise_level == "off"
def test_compose_disguise_balanced(monkeypatch):
"""level=balanced emits the disguise flags, keeps HV=Y."""
monkeypatch.setattr(_compose_module.platform, "machine", lambda: "x86_64")
monkeypatch.setattr(_config_module.platform, "machine", lambda: "x86_64")
cfg = _cfg()
cfg.pod.disguise_level = "balanced"
content = _build_compose_content(cfg)
for flag in _DISGUISE_FLAGS:
assert flag in content, f"disguise flag {flag!r} missing from compose"
assert "hv-vendor-id" not in content
assert 'HV: "Y"' in content
def test_compose_disguise_max_uses_emulated_devices(monkeypatch):
"""level=max drops Hyper-V (HV=N) AND swaps virtio for emulated devices."""
monkeypatch.setattr(_compose_module.platform, "machine", lambda: "x86_64")
monkeypatch.setattr(_config_module.platform, "machine", lambda: "x86_64")
cfg = _cfg()
cfg.pod.disguise_level = "max"
content = _build_compose_content(cfg)
for flag in _DISGUISE_FLAGS:
assert flag in content
assert 'HV: "N"' in content # the max-only Hyper-V opt-out
assert 'DISK_TYPE: "sata"' in content # emulated AHCI disk (no viostor/vioscsi)
assert 'ADAPTER: "e1000"' in content # emulated NIC (no netkvm)
assert 'MTU: "1500"' in content # required so dockur omits host_mtu (e1000 rejects it)
assert 'VGA: "std"' in content # std VGA (no Red Hat QXL/virtio-gpu)
assert "virtio-rng-pci" not in content # the rng would re-add VEN_1AF4
def test_compose_disguise_image_used_only_at_max(monkeypatch):
"""cfg.pod.disguise_image overrides the image only at level=max (#246)."""
monkeypatch.setattr(_compose_module.platform, "machine", lambda: "x86_64")
monkeypatch.setattr(_config_module.platform, "machine", lambda: "x86_64")
cfg = _cfg()
cfg.pod.image = "docker.io/dockurr/windows:pinned"
cfg.pod.disguise_image = "winpodx-windows-disguise"
# balanced → ignore disguise_image, use the pinned image.
cfg.pod.disguise_level = "balanced"
content = _build_compose_content(cfg)
assert "image: docker.io/dockurr/windows:pinned" in content
assert "winpodx-windows-disguise" not in content
# max → use the custom patched image.
cfg.pod.disguise_level = "max"
content = _build_compose_content(cfg)
assert "image: winpodx-windows-disguise" in content
def test_compose_disguise_max_injects_sensor_ssdt(monkeypatch):
"""Max + a built disguise_image injects the sensor SSDT via -acpitable (#246)."""
monkeypatch.setattr(_compose_module.platform, "machine", lambda: "x86_64")
monkeypatch.setattr(_config_module.platform, "machine", lambda: "x86_64")
cfg = _cfg()
cfg.pod.disguise_image = "winpodx-windows-disguise"
cfg.pod.disguise_level = "max"
content = _build_compose_content(cfg)
assert "-acpitable file=/usr/share/qemu/winpodx-ssdt-sensors.aml" in content
assert "-acpitable file=/usr/share/qemu/winpodx-wsmt.aml" in content # WSMT inject
# balanced → the AML only ships in the patched max image, so no injection.
cfg.pod.disguise_level = "balanced"
assert "winpodx-ssdt-sensors.aml" not in _build_compose_content(cfg)
# max but no disguise_image built → no injection (the file wouldn't exist).
cfg.pod.disguise_level = "max"
cfg.pod.disguise_image = ""
assert "winpodx-ssdt-sensors.aml" not in _build_compose_content(cfg)
def test_disguise_changes_devices_only_at_max_boundary():
"""Only crossing the max boundary changes virtual hardware (needs a wipe)."""
from winpodx.core.config import disguise_changes_devices
assert disguise_changes_devices("balanced", "max") is True
assert disguise_changes_devices("max", "off") is True
assert disguise_changes_devices("off", "balanced") is False
assert disguise_changes_devices("balanced", "balanced") is False
assert disguise_changes_devices("max", "max") is False
def test_compose_disguise_balanced_keeps_virtio_devices(monkeypatch):
"""balanced leaves the device knobs at dockur's virtio defaults (empty)."""
monkeypatch.setattr(_compose_module.platform, "machine", lambda: "x86_64")
monkeypatch.setattr(_config_module.platform, "machine", lambda: "x86_64")
cfg = _cfg()
cfg.pod.disguise_level = "balanced"
content = _build_compose_content(cfg)
assert 'DISK_TYPE: ""' in content
assert 'ADAPTER: ""' in content
assert 'MTU: ""' in content
assert 'VGA: ""' in content
def test_compose_disguise_disk_not_bumped_when_host_small(monkeypatch):
"""A host without room for the target leaves the disk as-is (warn) — #246."""
monkeypatch.setattr(_compose_module.platform, "machine", lambda: "x86_64")
monkeypatch.setattr(_config_module.platform, "machine", lambda: "x86_64")
import winpodx.core.disk as _disk
# 50 GiB free 10 GiB floor = 40 GiB < 128 GiB target → must not bump.
monkeypatch.setattr(_disk, "_host_free_and_total", lambda cfg: (50 * 1024**3, 100 * 1024**3))
cfg = _cfg()
cfg.pod.disguise_level = "balanced"
cfg.pod.disk_size = "64G"
cfg.pod.storage_path = "/some/path" # non-empty → host free space consulted
content = _build_compose_content(cfg)
assert 'DISK_SIZE: "64G"' in content # left as-is, not enlarged
def test_compose_disguise_disk_bumped_when_host_has_room(monkeypatch):
"""Plenty of host free space → disk advertised at 128G (sparse) — #246.
Regression: the old gate reused effective_max_bytes (reserve = 10 % of the
*total* filesystem), so a multi-TB host with hundreds of GB free was still
skipped. The flat-floor gate bumps whenever the target fits.
"""
monkeypatch.setattr(_compose_module.platform, "machine", lambda: "x86_64")
monkeypatch.setattr(_config_module.platform, "machine", lambda: "x86_64")
import winpodx.core.disk as _disk
# 300 GiB free on a 4 TB disk → must bump despite the 10 %-of-total being huge.
monkeypatch.setattr(_disk, "_host_free_and_total", lambda cfg: (300 * 1024**3, 4000 * 1024**3))
cfg = _cfg()
cfg.pod.disguise_level = "balanced"
cfg.pod.disk_size = "64G"
cfg.pod.storage_path = "/some/path"
content = _build_compose_content(cfg)
assert 'DISK_SIZE: "128G"' in content
def test_compose_disguise_off_opts_out(monkeypatch):
"""level=off emits no disguise flags, keeps HV=Y and the user's disk."""
monkeypatch.setattr(_compose_module.platform, "machine", lambda: "x86_64")
monkeypatch.setattr(_config_module.platform, "machine", lambda: "x86_64")
cfg = _cfg() # _cfg() sets disguise_level = "off"
assert cfg.pod.disguise_active is False
content = _build_compose_content(cfg)
assert "-hypervisor" not in content
assert "kvm=off" not in content
assert 'HV: "Y"' in content
assert 'DISK_SIZE: "64G"' in content # off never bumps the disk
def test_compose_disguise_smbios_mirrors_host_dmi(monkeypatch):
"""T1 (#246): the host's real, shell-safe DMI fields land in `-smbios`."""
monkeypatch.setattr(_compose_module.platform, "machine", lambda: "x86_64")
monkeypatch.setattr(_config_module.platform, "machine", lambda: "x86_64")
# Synthetic fixture values (not any real machine's DMI).
dmi = {
"sys_vendor": "ACME",
"product_name": "MDL-9000",
"board_vendor": "ACME",
"board_name": "BRD-42",
"bios_vendor": "ACME",
"bios_version": "BIOS-1.0",
"bios_date": "01/01/2020",
"chassis_vendor": "ACME",
}
monkeypatch.setattr(_compose_module, "_host_dmi_field", lambda n: dmi.get(n))
cfg = _cfg()
cfg.pod.disguise_level = "balanced"
content = _build_compose_content(cfg)
assert "type=1,manufacturer=ACME,product=MDL-9000" in content
assert "type=0,vendor=ACME,version=BIOS-1.0,date=01/01/2020" in content
assert "type=3,manufacturer=ACME" in content
def test_compose_disguise_smbios_skips_unsafe_dmi(monkeypatch):
"""Fields with spaces / unsafe chars are dropped, not mangled into ARGUMENTS."""
monkeypatch.setattr(_compose_module.platform, "machine", lambda: "x86_64")
monkeypatch.setattr(_config_module.platform, "machine", lambda: "x86_64")
# Only product_name is unsafe (space); the vendor is safe. Emulate the
# real _host_dmi_field safety filter (drop values with unsafe chars).
dmi = {"sys_vendor": "ACME", "product_name": "Generic Model 9000"}
def fake(n):
v = dmi.get(n)
return v if (v and all(c.isalnum() or c in "._/+-" for c in v)) else None
monkeypatch.setattr(_compose_module, "_host_dmi_field", fake)
cfg = _cfg()
cfg.pod.disguise_level = "balanced"
content = _build_compose_content(cfg)
assert "manufacturer=ACME" in content # safe vendor kept
assert "Generic Model 9000" not in content # spaced product dropped
def test_compose_disguise_off_emits_no_smbios(monkeypatch):
"""disguise off (explicit) → no host-DMI `-smbios` override."""
monkeypatch.setattr(_compose_module.platform, "machine", lambda: "x86_64")
monkeypatch.setattr(_config_module.platform, "machine", lambda: "x86_64")
monkeypatch.setattr(_compose_module, "_host_dmi_field", lambda n: "ACME")
cfg = _cfg() # _cfg() sets disguise_level = "off"
content = _build_compose_content(cfg)
assert "-smbios" not in content
def test_compose_cpu_flags_invtsc_auto_profile_appends_when_supported(monkeypatch):
"""``tuning_profile = "auto"`` + invtsc-capable host appends
``+invtsc`` to CPU_FLAGS (#215). hv-* enlightenments are NOT
emitted by us -- dockur's HV=Y handles those.
"""
import winpodx.utils.specs as specs
monkeypatch.setattr(_compose_module.platform, "machine", lambda: "x86_64")
monkeypatch.setattr(_config_module.platform, "machine", lambda: "x86_64")
monkeypatch.setattr(
specs,
"detect_tuning_capability",
lambda *, vm_cpu_cores, vm_ram_gb: specs.TuningCapability(
invtsc=True,
io_uring=True,
hugepages_enabled=False,
dedicated_host=False,
kernel_version=(6, 18),
cpu_vendor="amd",
nested_kvm=False,
),
)
cfg = _cfg()
cfg.pod.tuning_profile = "auto"
content = _build_compose_content(cfg)
# +invtsc lands in CPU_FLAGS env now, not ARGUMENTS.
assert 'CPU_FLAGS: "arch_capabilities=off,+invtsc"' in content
# virtio-rng -device pair stays in ARGUMENTS (dockur doesn't add it).
assert "virtio-rng-pci,rng=rng0" in content
assert "rng-random,id=rng0,filename=/dev/urandom" in content
# We no longer emit hv-* explicitly -- dockur owns those via HV=Y.
assert "hv-relaxed" not in content
assert "hv-evmcs" not in content
# nested-virt CPU sub-flags also delegated -- we set VMX env instead.
assert "+svm" not in content
assert "+vmx" not in content
# QEMU 10 dropped -no-hpet -- regression guard.
assert "-no-hpet" not in content
# nested_kvm=False so VMX env reads N.
assert 'VMX: "N"' in content
def test_compose_cpu_flags_invtsc_skipped_when_host_lacks_flag(monkeypatch):
"""Even with ``tuning_profile = "auto"``, a host without
``constant_tsc + nonstop_tsc`` must not get ``+invtsc`` in CPU_FLAGS
-- QEMU would either silently drop it or refuse to start.
"""
import winpodx.utils.specs as specs
monkeypatch.setattr(_compose_module.platform, "machine", lambda: "x86_64")
monkeypatch.setattr(_config_module.platform, "machine", lambda: "x86_64")
monkeypatch.setattr(
specs,
"detect_tuning_capability",
lambda *, vm_cpu_cores, vm_ram_gb: specs.TuningCapability(
invtsc=False,
io_uring=True,
hugepages_enabled=False,
dedicated_host=False,
kernel_version=(6, 18),
cpu_vendor="intel",
nested_kvm=False,
),
)
cfg = _cfg()
cfg.pod.tuning_profile = "auto"
content = _build_compose_content(cfg)
assert "+invtsc" not in content
def test_compose_cpu_flags_aarch64_ignores_tuning_profile(monkeypatch):
"""aarch64 returns empty CPU_FLAGS regardless of profile -- invtsc
and arch_capabilities are x86-only."""
import winpodx.utils.specs as specs
monkeypatch.setattr(_compose_module.platform, "machine", lambda: "aarch64")
monkeypatch.setattr(_config_module.platform, "machine", lambda: "aarch64")
monkeypatch.setattr(
specs,
"detect_tuning_capability",
lambda *, vm_cpu_cores, vm_ram_gb: specs.TuningCapability(
invtsc=True,
io_uring=True,
hugepages_enabled=False,
dedicated_host=True,
kernel_version=(6, 18),
cpu_vendor="arm",
nested_kvm=False,
),
)
cfg = _cfg()
cfg.pod.tuning_profile = "auto"
content = _build_compose_content(cfg)
assert 'CPU_FLAGS: ""' in content
assert "+invtsc" not in content
assert "arch_capabilities" not in content
def test_compose_vmx_env_reflects_nested_virt(monkeypatch):
"""VMX env reads ``Y`` when tuning profile resolves nested virt on,
``N`` otherwise. dockur's proc.sh handles the actual +vmx/+svm
sub-flag selection per CPU vendor.
"""
import winpodx.utils.specs as specs
monkeypatch.setattr(_compose_module.platform, "machine", lambda: "x86_64")
monkeypatch.setattr(_config_module.platform, "machine", lambda: "x86_64")
monkeypatch.setattr(
specs,
"detect_tuning_capability",
lambda *, vm_cpu_cores, vm_ram_gb: specs.TuningCapability(
invtsc=True,
io_uring=True,
hugepages_enabled=False,
dedicated_host=False,
kernel_version=(6, 18),
cpu_vendor="intel",
nested_kvm=True,
),
)
cfg = _cfg()
cfg.pod.tuning_profile = "auto"
content = _build_compose_content(cfg)
# nested_kvm=True + auto profile -> VMX=Y.
assert 'VMX: "Y"' in content
def test_compose_287_workaround_no_longer_needed(monkeypatch):
"""After the refactor, no ``-cpu host,...`` token ever lands in
ARGUMENTS, so dockur's proc.sh:137 strip code path doesn't trigger
and the ``-msg timestamp=on`` workaround (#287) is no longer
appended. Regression guard.
"""
import winpodx.utils.specs as specs
monkeypatch.setattr(_compose_module.platform, "machine", lambda: "x86_64")
monkeypatch.setattr(_config_module.platform, "machine", lambda: "x86_64")
monkeypatch.setattr(
specs,
"detect_tuning_capability",
lambda *, vm_cpu_cores, vm_ram_gb: specs.TuningCapability(
invtsc=True,
io_uring=True,
hugepages_enabled=False,
dedicated_host=False,
kernel_version=(6, 18),
cpu_vendor="intel",
nested_kvm=False,
),
)
cfg = _cfg()
# Every profile shape -- marker should never appear.
for profile in ("off", "safe", "auto", "performance", "manual"):
cfg.pod.tuning_profile = profile
content = _build_compose_content(cfg)
assert "-msg timestamp=on" not in content, f"marker leaked back in under profile={profile}"
# And -cpu host, never appears in ARGUMENTS (the whole point).
# We only need to check that ARGUMENTS lines don't contain it.
for line in content.splitlines():
if "ARGUMENTS:" in line:
assert "-cpu" not in line, (
f"-cpu leaked into ARGUMENTS under profile={profile}: {line}"
)