Files
kernalix7--winpodx/tests/test_device_cli.py
T
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

143 lines
5.3 KiB
Python

# SPDX-License-Identifier: MIT
"""Tests for the `winpodx device` CLI (#286, cli/device.py)."""
from __future__ import annotations
import argparse
import json
import pytest
from winpodx.cli import device as DC
from winpodx.core import devices as D
from winpodx.core.config import Config
@pytest.fixture()
def cfg(monkeypatch):
"""A throwaway Config that device handlers load/save against, plus a stub
host enumeration and a 'guest not running' default."""
c = Config()
c.pod.usb_live = True # exercise the live path (default is now off/experimental)
monkeypatch.setattr(Config, "load", classmethod(lambda cls: c))
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="NVIDIA GPU",
pci_class="03",
iommu_group="15",
),
],
)
return c
def _ns(**kw):
return argparse.Namespace(**kw)
def test_attach_usb_persists(cfg, capsys):
DC._attach(_ns(device_command="attach", id="1234:5678", type=None, label=None, force=False))
assert cfg.pod.devices == ["usb|1234:5678|ACME Dongle"]
out = capsys.readouterr().out
assert "Assigned usb 1234:5678" in out
# Guest not running -> persisted, live attach skipped with the reason.
assert "live attach skipped because the guest isn't running" in out
def test_attach_usb_explicit_label(cfg):
DC._attach(_ns(device_command="attach", id="1234:5678", type=None, label="My Key", force=False))
assert cfg.pod.devices == ["usb|1234:5678|My Key"]
def test_attach_duplicate_is_noop(cfg, capsys):
DC._attach(_ns(device_command="attach", id="1234:5678", type=None, label=None, force=False))
DC._attach(_ns(device_command="attach", id="1234:5678", type=None, label=None, force=False))
assert cfg.pod.devices == ["usb|1234:5678|ACME Dongle"]
assert "already assigned" in capsys.readouterr().out
def test_attach_pci_without_force_refuses(cfg, capsys):
with pytest.raises(SystemExit) as ei:
DC._attach(
_ns(device_command="attach", id="0000:01:00.0", type="pci", label=None, force=False)
)
assert ei.value.code == 1
assert cfg.pod.devices == [] # not persisted
err = capsys.readouterr().err
assert "without --force" in err and "GPU" in err
def test_attach_pci_with_force_persists(cfg, capsys):
DC._attach(_ns(device_command="attach", id="0000:01:00.0", type="pci", label=None, force=True))
assert cfg.pod.devices == ["pci|0000:01:00.0|NVIDIA GPU"]
# Guest not running -> deferred to next start (not an immediate recreate).
assert "pod start" in capsys.readouterr().out
def test_attach_pci_with_force_running_says_recreate(monkeypatch, cfg, capsys):
monkeypatch.setattr(DC, "_guest_running", lambda _c: True)
DC._attach(_ns(device_command="attach", id="0000:01:00.0", type="pci", label=None, force=True))
assert cfg.pod.devices == ["pci|0000:01:00.0|NVIDIA GPU"]
assert "recreate" in capsys.readouterr().out.lower()
def test_attach_invalid_id_exits(cfg):
with pytest.raises(SystemExit) as ei:
DC._attach(_ns(device_command="attach", id="nonsense", type=None, label=None, force=False))
assert ei.value.code == 2
def test_detach_removes(cfg, capsys):
cfg.pod.devices = ["usb|1234:5678|ACME Dongle"]
DC._detach(_ns(device_command="detach", id="1234:5678", type=None))
assert cfg.pod.devices == []
assert "Released usb 1234:5678" in capsys.readouterr().out
def test_detach_not_assigned_is_noop(cfg, capsys):
DC._detach(_ns(device_command="detach", id="1234:5678", type=None))
assert "not assigned" in capsys.readouterr().out
def test_list_json(cfg, capsys):
DC._list(_ns(device_command="list", json=True))
rows = json.loads(capsys.readouterr().out)
by_id = {r["id"]: r for r in rows}
assert by_id["1234:5678"]["safe"] is True
assert by_id["0000:01:00.0"]["safe"] is False
assert by_id["0000:01:00.0"]["iommu_group"] == "15"
def test_list_marks_assigned(cfg, capsys):
cfg.pod.devices = ["usb|1234:5678|ACME Dongle"]
DC._list(_ns(device_command="list", json=False))
lines = [ln for ln in capsys.readouterr().out.splitlines() if "1234:5678" in ln]
assert lines and lines[0].lstrip().startswith("*") # assigned marker
def test_status_json(cfg, capsys):
cfg.pod.devices = ["pci|0000:01:00.0|GPU"]
DC._status(_ns(device_command="status", json=True))
data = json.loads(capsys.readouterr().out)
assert data["guest_running"] is False
assert data["devices"] == [{"type": "pci", "id": "0000:01:00.0", "label": "GPU"}]
def test_attach_usb_live_when_running(monkeypatch, cfg, capsys):
calls = []
monkeypatch.setattr(DC, "_guest_running", lambda _c: True)
monkeypatch.setattr(
D, "live_attach", lambda backend, container, dc: calls.append((container, dc.key))
)
DC._attach(_ns(device_command="attach", id="1234:5678", type=None, label=None, force=False))
assert calls == [("winpodx-windows", "usb:1234:5678")]
assert "live, no restart" in capsys.readouterr().out