From 0ee8dceee2b9c29ec02f5c363a092549e8506528 Mon Sep 17 00:00:00 2001 From: Qubasa Date: Fri, 22 Mar 2024 19:08:35 +0100 Subject: [PATCH] clan-vm-manager: Basic pytest framework established --- .../clan_vm_manager/__init__.py | 4 +- .../clan_vm_manager/windows/main_window.py | 2 +- pkgs/clan-vm-manager/default.nix | 22 +++++++ pkgs/clan-vm-manager/pyproject.toml | 10 ++- pkgs/clan-vm-manager/shell.nix | 6 +- pkgs/clan-vm-manager/tests/command.py | 64 +++++++++++++++++++ pkgs/clan-vm-manager/tests/conftest.py | 44 +++++++++++++ pkgs/clan-vm-manager/tests/helpers/cli.py | 15 +++++ pkgs/clan-vm-manager/tests/root.py | 35 ++++++++++ pkgs/clan-vm-manager/tests/temporary_dir.py | 27 ++++++++ pkgs/clan-vm-manager/tests/test_cli.py | 8 +++ pkgs/clan-vm-manager/tests/wayland.py | 24 +++++++ 12 files changed, 254 insertions(+), 7 deletions(-) create mode 100644 pkgs/clan-vm-manager/tests/command.py create mode 100644 pkgs/clan-vm-manager/tests/conftest.py create mode 100644 pkgs/clan-vm-manager/tests/helpers/cli.py create mode 100644 pkgs/clan-vm-manager/tests/root.py create mode 100644 pkgs/clan-vm-manager/tests/temporary_dir.py create mode 100644 pkgs/clan-vm-manager/tests/test_cli.py create mode 100644 pkgs/clan-vm-manager/tests/wayland.py diff --git a/pkgs/clan-vm-manager/clan_vm_manager/__init__.py b/pkgs/clan-vm-manager/clan_vm_manager/__init__.py index 97d3317c..d1cc2a96 100644 --- a/pkgs/clan-vm-manager/clan_vm_manager/__init__.py +++ b/pkgs/clan-vm-manager/clan_vm_manager/__init__.py @@ -9,6 +9,6 @@ log = logging.getLogger(__name__) @profile -def main() -> int: +def main(argv: list[str] = sys.argv) -> int: app = MainApplication() - return app.run(sys.argv) + return app.run(argv) diff --git a/pkgs/clan-vm-manager/clan_vm_manager/windows/main_window.py b/pkgs/clan-vm-manager/clan_vm_manager/windows/main_window.py index 88702732..62242081 100644 --- a/pkgs/clan-vm-manager/clan_vm_manager/windows/main_window.py +++ b/pkgs/clan-vm-manager/clan_vm_manager/windows/main_window.py @@ -25,7 +25,7 @@ class MainWindow(Adw.ApplicationWindow): def __init__(self, config: ClanConfig) -> None: super().__init__() self.set_title("cLAN Manager") - self.set_default_size(980, 650) + self.set_default_size(980, 850) overlay = ToastOverlay.use().overlay view = Adw.ToolbarView() diff --git a/pkgs/clan-vm-manager/default.nix b/pkgs/clan-vm-manager/default.nix index 71ab3d60..521c6276 100644 --- a/pkgs/clan-vm-manager/default.nix +++ b/pkgs/clan-vm-manager/default.nix @@ -12,6 +12,7 @@ clan-cli, makeDesktopItem, libadwaita, + weston, }: let source = ./.; @@ -54,6 +55,16 @@ python3.pkgs.buildPythonApplication rec { 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 passthru = { inherit desktop-file; @@ -65,6 +76,17 @@ python3.pkgs.buildPythonApplication rec { pygobject-stubs ]; 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" { } '' if grep --include \*.py -Rq "breakpoint()" ${source}; then echo "breakpoint() found in ${source}:" diff --git a/pkgs/clan-vm-manager/pyproject.toml b/pkgs/clan-vm-manager/pyproject.toml index 8016c21e..bdacae4c 100644 --- a/pkgs/clan-vm-manager/pyproject.toml +++ b/pkgs/clan-vm-manager/pyproject.toml @@ -3,7 +3,6 @@ requires = ["setuptools"] build-backend = "setuptools.build_meta" - [project] name = "clan-vm-manager" dynamic = ["version"] @@ -15,6 +14,15 @@ exclude = ["result"] [tool.setuptools.package-data] 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] python_version = "3.11" warn_redundant_casts = true diff --git a/pkgs/clan-vm-manager/shell.nix b/pkgs/clan-vm-manager/shell.nix index 36094422..6d1d8565 100644 --- a/pkgs/clan-vm-manager/shell.nix +++ b/pkgs/clan-vm-manager/shell.nix @@ -33,7 +33,7 @@ mkShell ( --add-flags '-ex "source ${python3}/share/gdb/libpython.py"' ''; in - { + rec { inherit (clan-vm-manager) propagatedBuildInputs buildInputs; linuxOnlyPackages = lib.optionals stdenv.isLinux [ @@ -42,14 +42,14 @@ mkShell ( ]; # To debug clan-vm-manger execute pygdb --args python ./bin/clan-vm-manager - nativeBuildInputs = [ + packages = [ ruff desktop-file-utils mypy python3Packages.ipdb gtk4.dev 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"; diff --git a/pkgs/clan-vm-manager/tests/command.py b/pkgs/clan-vm-manager/tests/command.py new file mode 100644 index 00000000..f951c8dd --- /dev/null +++ b/pkgs/clan-vm-manager/tests/command.py @@ -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() diff --git a/pkgs/clan-vm-manager/tests/conftest.py b/pkgs/clan-vm-manager/tests/conftest.py new file mode 100644 index 00000000..1841dc81 --- /dev/null +++ b/pkgs/clan-vm-manager/tests/conftest.py @@ -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 diff --git a/pkgs/clan-vm-manager/tests/helpers/cli.py b/pkgs/clan-vm-manager/tests/helpers/cli.py new file mode 100644 index 00000000..1c2532d5 --- /dev/null +++ b/pkgs/clan-vm-manager/tests/helpers/cli.py @@ -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) diff --git a/pkgs/clan-vm-manager/tests/root.py b/pkgs/clan-vm-manager/tests/root.py new file mode 100644 index 00000000..0cac067b --- /dev/null +++ b/pkgs/clan-vm-manager/tests/root.py @@ -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 diff --git a/pkgs/clan-vm-manager/tests/temporary_dir.py b/pkgs/clan-vm-manager/tests/temporary_dir.py new file mode 100644 index 00000000..aaa54ca2 --- /dev/null +++ b/pkgs/clan-vm-manager/tests/temporary_dir.py @@ -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) diff --git a/pkgs/clan-vm-manager/tests/test_cli.py b/pkgs/clan-vm-manager/tests/test_cli.py new file mode 100644 index 00000000..654fa82a --- /dev/null +++ b/pkgs/clan-vm-manager/tests/test_cli.py @@ -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"]) diff --git a/pkgs/clan-vm-manager/tests/wayland.py b/pkgs/clan-vm-manager/tests/wayland.py new file mode 100644 index 00000000..a9150dd2 --- /dev/null +++ b/pkgs/clan-vm-manager/tests/wayland.py @@ -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()