clan-vm-manager: Basic pytest framework established
This commit is contained in:
parent
1d6cc49da5
commit
0ee8dceee2
@ -9,6 +9,6 @@ log = logging.getLogger(__name__)
|
|||||||
|
|
||||||
|
|
||||||
@profile
|
@profile
|
||||||
def main() -> int:
|
def main(argv: list[str] = sys.argv) -> int:
|
||||||
app = MainApplication()
|
app = MainApplication()
|
||||||
return app.run(sys.argv)
|
return app.run(argv)
|
||||||
|
@ -25,7 +25,7 @@ class MainWindow(Adw.ApplicationWindow):
|
|||||||
def __init__(self, config: ClanConfig) -> None:
|
def __init__(self, config: ClanConfig) -> None:
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.set_title("cLAN Manager")
|
self.set_title("cLAN Manager")
|
||||||
self.set_default_size(980, 650)
|
self.set_default_size(980, 850)
|
||||||
|
|
||||||
overlay = ToastOverlay.use().overlay
|
overlay = ToastOverlay.use().overlay
|
||||||
view = Adw.ToolbarView()
|
view = Adw.ToolbarView()
|
||||||
|
@ -12,6 +12,7 @@
|
|||||||
clan-cli,
|
clan-cli,
|
||||||
makeDesktopItem,
|
makeDesktopItem,
|
||||||
libadwaita,
|
libadwaita,
|
||||||
|
weston,
|
||||||
}:
|
}:
|
||||||
let
|
let
|
||||||
source = ./.;
|
source = ./.;
|
||||||
@ -54,6 +55,16 @@ python3.pkgs.buildPythonApplication rec {
|
|||||||
passthru.externalPythonDeps
|
passthru.externalPythonDeps
|
||||||
];
|
];
|
||||||
|
|
||||||
|
checkPython = python3.withPackages (_ps: clan-cli.passthru.pytestDependencies);
|
||||||
|
|
||||||
|
devDependencies = [
|
||||||
|
checkPython
|
||||||
|
weston
|
||||||
|
] ++ nativeBuildInputs ++ buildInputs ++ propagatedBuildInputs;
|
||||||
|
|
||||||
|
passthru.checkPython = checkPython;
|
||||||
|
passthru.devDependencies = devDependencies;
|
||||||
|
|
||||||
# also re-expose dependencies so we test them in CI
|
# also re-expose dependencies so we test them in CI
|
||||||
passthru = {
|
passthru = {
|
||||||
inherit desktop-file;
|
inherit desktop-file;
|
||||||
@ -65,6 +76,17 @@ python3.pkgs.buildPythonApplication rec {
|
|||||||
pygobject-stubs
|
pygobject-stubs
|
||||||
];
|
];
|
||||||
tests = {
|
tests = {
|
||||||
|
clan-vm-manager-pytest =
|
||||||
|
runCommand "clan-vm-manager-pytest" { nativeBuildInputs = devDependencies; }
|
||||||
|
''
|
||||||
|
cp -r ${source} ./src
|
||||||
|
chmod +w -R ./src
|
||||||
|
cd ./src
|
||||||
|
|
||||||
|
export NIX_STATE_DIR=$TMPDIR/nix IN_NIX_SANDBOX=1
|
||||||
|
${checkPython}/bin/python -m pytest -m "not impure" ./tests
|
||||||
|
touch $out
|
||||||
|
'';
|
||||||
clan-vm-manager-no-breakpoints = runCommand "clan-vm-manager-no-breakpoints" { } ''
|
clan-vm-manager-no-breakpoints = runCommand "clan-vm-manager-no-breakpoints" { } ''
|
||||||
if grep --include \*.py -Rq "breakpoint()" ${source}; then
|
if grep --include \*.py -Rq "breakpoint()" ${source}; then
|
||||||
echo "breakpoint() found in ${source}:"
|
echo "breakpoint() found in ${source}:"
|
||||||
|
@ -3,7 +3,6 @@ requires = ["setuptools"]
|
|||||||
build-backend = "setuptools.build_meta"
|
build-backend = "setuptools.build_meta"
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "clan-vm-manager"
|
name = "clan-vm-manager"
|
||||||
dynamic = ["version"]
|
dynamic = ["version"]
|
||||||
@ -15,6 +14,15 @@ exclude = ["result"]
|
|||||||
[tool.setuptools.package-data]
|
[tool.setuptools.package-data]
|
||||||
clan_vm_manager = ["**/assets/*"]
|
clan_vm_manager = ["**/assets/*"]
|
||||||
|
|
||||||
|
[tool.pytest.ini_options]
|
||||||
|
testpaths = "tests"
|
||||||
|
faulthandler_timeout = 60
|
||||||
|
log_level = "DEBUG"
|
||||||
|
log_format = "%(levelname)s: %(message)s\n %(pathname)s:%(lineno)d::%(funcName)s"
|
||||||
|
addopts = "--cov . --cov-report term --cov-report html:.reports/html --no-cov-on-fail --durations 5 --color=yes --new-first" # Add --pdb for debugging
|
||||||
|
norecursedirs = "tests/helpers"
|
||||||
|
markers = ["impure"]
|
||||||
|
|
||||||
[tool.mypy]
|
[tool.mypy]
|
||||||
python_version = "3.11"
|
python_version = "3.11"
|
||||||
warn_redundant_casts = true
|
warn_redundant_casts = true
|
||||||
|
@ -33,7 +33,7 @@ mkShell (
|
|||||||
--add-flags '-ex "source ${python3}/share/gdb/libpython.py"'
|
--add-flags '-ex "source ${python3}/share/gdb/libpython.py"'
|
||||||
'';
|
'';
|
||||||
in
|
in
|
||||||
{
|
rec {
|
||||||
inherit (clan-vm-manager) propagatedBuildInputs buildInputs;
|
inherit (clan-vm-manager) propagatedBuildInputs buildInputs;
|
||||||
|
|
||||||
linuxOnlyPackages = lib.optionals stdenv.isLinux [
|
linuxOnlyPackages = lib.optionals stdenv.isLinux [
|
||||||
@ -42,14 +42,14 @@ mkShell (
|
|||||||
];
|
];
|
||||||
|
|
||||||
# To debug clan-vm-manger execute pygdb --args python ./bin/clan-vm-manager
|
# To debug clan-vm-manger execute pygdb --args python ./bin/clan-vm-manager
|
||||||
nativeBuildInputs = [
|
packages = [
|
||||||
ruff
|
ruff
|
||||||
desktop-file-utils
|
desktop-file-utils
|
||||||
mypy
|
mypy
|
||||||
python3Packages.ipdb
|
python3Packages.ipdb
|
||||||
gtk4.dev
|
gtk4.dev
|
||||||
libadwaita.devdoc # has the demo called 'adwaita-1-demo'
|
libadwaita.devdoc # has the demo called 'adwaita-1-demo'
|
||||||
] ++ clan-vm-manager.nativeBuildInputs ++ clan-vm-manager.propagatedBuildInputs;
|
] ++ clan-vm-manager.devDependencies ++ linuxOnlyPackages;
|
||||||
|
|
||||||
PYTHONBREAKPOINT = "ipdb.set_trace";
|
PYTHONBREAKPOINT = "ipdb.set_trace";
|
||||||
|
|
||||||
|
64
pkgs/clan-vm-manager/tests/command.py
Normal file
64
pkgs/clan-vm-manager/tests/command.py
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
import os
|
||||||
|
import signal
|
||||||
|
import subprocess
|
||||||
|
from collections.abc import Iterator
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import IO, Any
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
_FILE = None | int | IO[Any]
|
||||||
|
|
||||||
|
|
||||||
|
class Command:
|
||||||
|
def __init__(self) -> None:
|
||||||
|
self.processes: list[subprocess.Popen[str]] = []
|
||||||
|
|
||||||
|
def run(
|
||||||
|
self,
|
||||||
|
command: list[str],
|
||||||
|
extra_env: dict[str, str] = {},
|
||||||
|
stdin: _FILE = None,
|
||||||
|
stdout: _FILE = None,
|
||||||
|
stderr: _FILE = None,
|
||||||
|
workdir: Path | None = None,
|
||||||
|
) -> subprocess.Popen[str]:
|
||||||
|
env = os.environ.copy()
|
||||||
|
env.update(extra_env)
|
||||||
|
# We start a new session here so that we can than more reliably kill all childs as well
|
||||||
|
p = subprocess.Popen(
|
||||||
|
command,
|
||||||
|
env=env,
|
||||||
|
start_new_session=True,
|
||||||
|
stdout=stdout,
|
||||||
|
stderr=stderr,
|
||||||
|
stdin=stdin,
|
||||||
|
text=True,
|
||||||
|
cwd=workdir,
|
||||||
|
)
|
||||||
|
self.processes.append(p)
|
||||||
|
return p
|
||||||
|
|
||||||
|
def terminate(self) -> None:
|
||||||
|
# Stop in reverse order in case there are dependencies.
|
||||||
|
# We just kill all processes as quickly as possible because we don't
|
||||||
|
# care about corrupted state and want to make tests fasts.
|
||||||
|
for p in reversed(self.processes):
|
||||||
|
try:
|
||||||
|
os.killpg(os.getpgid(p.pid), signal.SIGKILL)
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def command() -> Iterator[Command]:
|
||||||
|
"""
|
||||||
|
Starts a background command. The process is automatically terminated in the end.
|
||||||
|
>>> p = command.run(["some", "daemon"])
|
||||||
|
>>> print(p.pid)
|
||||||
|
"""
|
||||||
|
c = Command()
|
||||||
|
try:
|
||||||
|
yield c
|
||||||
|
finally:
|
||||||
|
c.terminate()
|
44
pkgs/clan-vm-manager/tests/conftest.py
Normal file
44
pkgs/clan-vm-manager/tests/conftest.py
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from clan_cli.custom_logger import setup_logging
|
||||||
|
from clan_cli.nix import nix_shell
|
||||||
|
|
||||||
|
sys.path.append(str(Path(__file__).parent / "helpers"))
|
||||||
|
sys.path.append(
|
||||||
|
str(Path(__file__).parent.parent)
|
||||||
|
) # Also add clan vm manager to PYTHONPATH
|
||||||
|
|
||||||
|
pytest_plugins = [
|
||||||
|
"temporary_dir",
|
||||||
|
"root",
|
||||||
|
"command",
|
||||||
|
"wayland",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
# Executed on pytest session start
|
||||||
|
def pytest_sessionstart(session: pytest.Session) -> None:
|
||||||
|
# This function will be called once at the beginning of the test session
|
||||||
|
print("Starting pytest session")
|
||||||
|
# You can access the session config, items, testsfailed, etc.
|
||||||
|
print(f"Session config: {session.config}")
|
||||||
|
|
||||||
|
setup_logging(level="DEBUG")
|
||||||
|
|
||||||
|
|
||||||
|
# fixture for git_repo
|
||||||
|
@pytest.fixture
|
||||||
|
def git_repo(tmp_path: Path) -> Path:
|
||||||
|
# initialize a git repository
|
||||||
|
cmd = nix_shell(["nixpkgs#git"], ["git", "init"])
|
||||||
|
subprocess.run(cmd, cwd=tmp_path, check=True)
|
||||||
|
# set user.name and user.email
|
||||||
|
cmd = nix_shell(["nixpkgs#git"], ["git", "config", "user.name", "test"])
|
||||||
|
subprocess.run(cmd, cwd=tmp_path, check=True)
|
||||||
|
cmd = nix_shell(["nixpkgs#git"], ["git", "config", "user.email", "test@test.test"])
|
||||||
|
subprocess.run(cmd, cwd=tmp_path, check=True)
|
||||||
|
# return the path to the git repository
|
||||||
|
return tmp_path
|
15
pkgs/clan-vm-manager/tests/helpers/cli.py
Normal file
15
pkgs/clan-vm-manager/tests/helpers/cli.py
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import logging
|
||||||
|
import shlex
|
||||||
|
|
||||||
|
from clan_cli.custom_logger import get_caller
|
||||||
|
|
||||||
|
from clan_vm_manager import main
|
||||||
|
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class Cli:
|
||||||
|
def run(self, args: list[str]) -> None:
|
||||||
|
cmd = shlex.join(["clan", *args])
|
||||||
|
log.debug(f"$ {cmd} \nCaller: {get_caller()}")
|
||||||
|
main(args)
|
35
pkgs/clan-vm-manager/tests/root.py
Normal file
35
pkgs/clan-vm-manager/tests/root.py
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
TEST_ROOT = Path(__file__).parent.resolve()
|
||||||
|
PROJECT_ROOT = TEST_ROOT.parent
|
||||||
|
if CLAN_CORE_ := os.environ.get("CLAN_CORE"):
|
||||||
|
CLAN_CORE = Path(CLAN_CORE_)
|
||||||
|
else:
|
||||||
|
CLAN_CORE = PROJECT_ROOT.parent.parent
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope="session")
|
||||||
|
def project_root() -> Path:
|
||||||
|
"""
|
||||||
|
Root directory the clan-cli
|
||||||
|
"""
|
||||||
|
return PROJECT_ROOT
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope="session")
|
||||||
|
def test_root() -> Path:
|
||||||
|
"""
|
||||||
|
Root directory of the tests
|
||||||
|
"""
|
||||||
|
return TEST_ROOT
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope="session")
|
||||||
|
def clan_core() -> Path:
|
||||||
|
"""
|
||||||
|
Directory of the clan-core flake
|
||||||
|
"""
|
||||||
|
return CLAN_CORE
|
27
pkgs/clan-vm-manager/tests/temporary_dir.py
Normal file
27
pkgs/clan-vm-manager/tests/temporary_dir.py
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import logging
|
||||||
|
import os
|
||||||
|
import tempfile
|
||||||
|
from collections.abc import Iterator
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def temporary_home(monkeypatch: pytest.MonkeyPatch) -> Iterator[Path]:
|
||||||
|
env_dir = os.getenv("TEST_TEMPORARY_DIR")
|
||||||
|
if env_dir is not None:
|
||||||
|
path = Path(env_dir).resolve()
|
||||||
|
log.debug("Temp HOME directory: %s", str(path))
|
||||||
|
monkeypatch.setenv("HOME", str(path))
|
||||||
|
monkeypatch.chdir(str(path))
|
||||||
|
yield path
|
||||||
|
else:
|
||||||
|
with tempfile.TemporaryDirectory(prefix="pytest-") as dirpath:
|
||||||
|
monkeypatch.setenv("HOME", str(dirpath))
|
||||||
|
monkeypatch.setenv("XDG_CONFIG_HOME", str(Path(dirpath) / ".config"))
|
||||||
|
monkeypatch.chdir(str(dirpath))
|
||||||
|
log.debug("Temp HOME directory: %s", str(dirpath))
|
||||||
|
yield Path(dirpath)
|
8
pkgs/clan-vm-manager/tests/test_cli.py
Normal file
8
pkgs/clan-vm-manager/tests/test_cli.py
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
import pytest
|
||||||
|
from cli import Cli
|
||||||
|
|
||||||
|
|
||||||
|
def test_help(capfd: pytest.CaptureFixture) -> None:
|
||||||
|
cli = Cli()
|
||||||
|
with pytest.raises(SystemExit):
|
||||||
|
cli.run(["clan-vm-manager", "--help"])
|
24
pkgs/clan-vm-manager/tests/wayland.py
Normal file
24
pkgs/clan-vm-manager/tests/wayland.py
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
from collections.abc import Generator
|
||||||
|
from subprocess import Popen
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope="session")
|
||||||
|
def wayland_compositor() -> Generator[Popen, None, None]:
|
||||||
|
# Start the Wayland compositor (e.g., Weston)
|
||||||
|
compositor = Popen(["weston", "--backend=headless-backend.so"])
|
||||||
|
yield compositor
|
||||||
|
# Cleanup: Terminate the compositor
|
||||||
|
compositor.terminate()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope="function")
|
||||||
|
def gtk_app(wayland_compositor: Popen) -> Generator[Popen, None, None]:
|
||||||
|
# Assuming your GTK4 app can be started via a command line
|
||||||
|
# It's important to ensure it uses the Wayland session initiated by the fixture
|
||||||
|
env = {"GDK_BACKEND": "wayland"}
|
||||||
|
app = Popen(["clan-vm-manager"], env=env)
|
||||||
|
yield app
|
||||||
|
# Cleanup: Terminate your application
|
||||||
|
app.terminate()
|
Loading…
Reference in New Issue
Block a user