clan-vm-manager: Basic pytest framework established
Some checks failed
checks / checks (pull_request) Failing after 1m10s
checks / check-links (pull_request) Successful in 21s
checks / checks-impure (pull_request) Successful in 1m46s

This commit is contained in:
Luis Hebendanz 2024-03-22 19:08:35 +01:00
parent 1d6cc49da5
commit 0ee8dceee2
12 changed files with 254 additions and 7 deletions

View File

@ -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)

View File

@ -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()

View File

@ -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}:"

View File

@ -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

View File

@ -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";

View 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()

View 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

View 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)

View 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

View 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)

View 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"])

View 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()