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

123 lines
4.5 KiB
Python

# SPDX-License-Identifier: MIT
"""Headless smoke tests for the GUI Devices tab (#286, _main_window_devices)."""
from __future__ import annotations
import os
import pytest
os.environ.setdefault("QT_QPA_PLATFORM", "offscreen")
pytest.importorskip("PySide6")
from PySide6.QtWidgets import QApplication # noqa: E402
from winpodx.cli import device as DC # noqa: E402
from winpodx.core import devices as D # noqa: E402
from winpodx.core.config import Config # noqa: E402
from winpodx.gui._main_window_devices import DevicesMixin # noqa: E402
@pytest.fixture(scope="module")
def qapp():
app = QApplication.instance() or QApplication([])
yield app
@pytest.fixture()
def host(qapp, monkeypatch, tmp_path):
# Isolate config to a temp XDG dir and stub the host enumeration / state.
monkeypatch.setenv("XDG_CONFIG_HOME", str(tmp_path))
cfg = Config()
monkeypatch.setattr(Config, "load", classmethod(lambda cls: cfg))
monkeypatch.setattr(Config, "save", lambda self: None)
monkeypatch.setattr(DC, "_guest_running", lambda _c: False)
monkeypatch.setattr(
DC,
"_enumerate_host",
lambda: [
D.HostDevice(dtype="usb", did="1234:5678", label="ACME Dongle"),
D.HostDevice(
dtype="pci", did="0000:01:00.0", label="GPU", pci_class="03", iommu_group="15"
),
],
)
class _Host(DevicesMixin):
pass
h = _Host()
# Keep a reference to the page so its Qt widget tree (and the column
# layouts) isn't garbage-collected out from under the test.
h._page = h._build_devices_page() # type: ignore[attr-defined]
h._cfg = cfg # type: ignore[attr-defined]
return h
def test_page_builds_and_populates(host):
# Both devices unassigned -> 2 rows + a stretch in the host column.
assert host._dev_host_col.count() == 3
# Guest column empty -> placeholder + stretch.
assert host._dev_guest_col.count() == 2
def test_attach_usb_persists_and_moves_column(host):
host._on_attach(D.HostDevice(dtype="usb", did="1234:5678", label="ACME Dongle"))
assert host._cfg.pod.devices == ["usb|1234:5678|ACME Dongle"]
# USB moved to guest column; only the PCI remains on the host side.
assert host._dev_guest_col.count() == 2 # 1 row + stretch
assert host._dev_host_col.count() == 2 # 1 row + stretch
assert "Assigned" in host._devices_status.text()
def test_detach_persists(host):
host._cfg.pod.devices = ["usb|1234:5678|ACME Dongle"]
host._on_detach(D.HostDevice(dtype="usb", did="1234:5678", label="ACME Dongle"))
assert host._cfg.pod.devices == []
assert "Released" in host._devices_status.text()
def test_attach_usb_live_is_nonblocking(host, monkeypatch):
# The slow live attach must run off the GUI thread — `_on_attach` returns
# immediately with a "busy" status instead of freezing the window.
import time
monkeypatch.setattr(DC, "_guest_running", lambda _c: True)
monkeypatch.setattr(host, "_render_devices", lambda: None) # no off-thread widget rebuild
started: list = []
def _slow_attach(be, c, dc):
started.append(dc.did)
time.sleep(0.5) # would freeze the GUI if run on the main thread
monkeypatch.setattr(D, "live_attach", _slow_attach)
t0 = time.monotonic()
host._on_attach(D.HostDevice(dtype="usb", did="1234:5678", label="ACME"))
elapsed = time.monotonic() - t0
assert elapsed < 0.3 # returned without waiting for the 0.5s work
assert host._dev_busy is True
assert "Attaching" in host._devices_status.text()
# drain the worker so it doesn't bleed into other tests
for _ in range(100):
if not host._dev_busy:
break
time.sleep(0.02)
assert started == ["1234:5678"]
def test_pci_attach_requires_confirmation(host, monkeypatch):
# The risky-PCI confirm is now a custom dialog (_confirm_risky_pci, which
# renders the plain-language "host will lose ..." warning callout), not a
# bare QMessageBox.warning. Mock that method's verdict.
# Decline -> nothing persisted.
monkeypatch.setattr(host, "_confirm_risky_pci", lambda host_dev, safety: False)
host._on_attach(D.HostDevice(dtype="pci", did="0000:01:00.0", label="GPU", pci_class="03"))
assert host._cfg.pod.devices == []
# Accept -> persisted.
monkeypatch.setattr(host, "_confirm_risky_pci", lambda host_dev, safety: True)
host._on_attach(D.HostDevice(dtype="pci", did="0000:01:00.0", label="GPU", pci_class="03"))
assert host._cfg.pod.devices == ["pci|0000:01:00.0|GPU"]