Merge pull request 'clan-vm-manager: Basic pytest framework established' (#1026) from Qubasa-main into main
All checks were successful
checks / check-links (push) Successful in 14s
checks / checks-impure (push) Successful in 1m52s
checks / checks (push) Successful in 4m11s

This commit is contained in:
clan-bot 2024-03-24 22:41:07 +00:00
commit 5fd4a63e17
28 changed files with 411 additions and 319 deletions

View File

@ -1,115 +0,0 @@
{
perSystem =
{
pkgs,
self',
lib,
...
}:
let
python3 = pkgs.python3;
pypkgs = python3.pkgs;
clan-cli = self'.packages.clan-cli;
clan-vm-manager = self'.packages.clan-vm-manager;
pythonWithDeps = python3.withPackages (
ps:
clan-cli.propagatedBuildInputs
++ clan-cli.devDependencies
++ [ ps.pip ]
++ [ clan-vm-manager.externalPythonDeps ]
# clan-vm-manager deps
);
linuxOnlyPackages = lib.optionals pkgs.stdenv.isLinux [ pkgs.xdg-utils ];
in
{
devShells.python = pkgs.mkShell {
inputsFrom = [ self'.devShells.default ];
packages =
[
pythonWithDeps
pypkgs.mypy
pypkgs.ipdb
pkgs.desktop-file-utils
pkgs.gtk4.dev
pkgs.ruff
pkgs.libadwaita.devdoc # has the demo called 'adwaita-1-demo'
]
++ linuxOnlyPackages
++ clan-vm-manager.nativeBuildInputs
++ clan-vm-manager.buildInputs
++ clan-cli.nativeBuildInputs;
PYTHONBREAKPOINT = "ipdb.set_trace";
shellHook = ''
ln -sfT ${clan-cli.nixpkgs} ./pkgs/clan-cli/clan_cli/nixpkgs
## PYTHON
tmp_path="$(realpath ./.direnv/python)"
repo_root=$(realpath .)
mkdir -p "$tmp_path/${pythonWithDeps.sitePackages}"
# local dependencies
localPackages=(
$repo_root/pkgs/clan-cli
$repo_root/pkgs/clan-vm-manager
)
# Install executable wrappers for local python packages scripts
# This is done by utilizing `pip install --editable`
# As a result, executables like `clan` can be executed from within the dev-shell
# while using the current version of the code and its dependencies.
for package in "''${localPackages[@]}"; do
pname=$(basename "$package")
if
[ ! -e "$tmp_path/meta/$pname/pyproject.toml" ] \
|| [ ! -e "$package/pyproject.toml" ] \
|| ! cmp -s "$tmp_path/meta/$pname/pyproject.toml" "$package/pyproject.toml"
then
echo "==== Installing local python package $pname in editable mode ===="
mkdir -p "$tmp_path/meta/$pname"
cp $package/pyproject.toml $tmp_path/meta/$pname/pyproject.toml
${python3.pkgs.pip}/bin/pip install \
--quiet \
--disable-pip-version-check \
--no-index \
--no-build-isolation \
--prefix "$tmp_path" \
--editable "$package"
fi
done
export PATH="$tmp_path/bin:$PATH"
export PYTHONPATH="''${PYTHONPATH:+$PYTHONPATH:}$tmp_path/${pythonWithDeps.sitePackages}"
for package in "''${localPackages[@]}"; do
export PYTHONPATH="$package:$PYTHONPATH"
done
## GUI
if ! command -v xdg-mime &> /dev/null; then
echo "Warning: 'xdg-mime' is not available. The desktop file cannot be installed."
fi
# install desktop file
set -eou pipefail
DESKTOP_FILE_NAME=org.clan.vm-manager.desktop
DESKTOP_DST=~/.local/share/applications/$DESKTOP_FILE_NAME
DESKTOP_SRC=${clan-vm-manager.desktop-file}/share/applications/$DESKTOP_FILE_NAME
UI_BIN="clan-vm-manager"
cp -f $DESKTOP_SRC $DESKTOP_DST
sed -i "s|Exec=.*clan-vm-manager|Exec=$UI_BIN|" $DESKTOP_DST
xdg-mime default $DESKTOP_FILE_NAME x-scheme-handler/clan
echo "==== Validating desktop file installation ===="
set -x
desktop-file-validate $DESKTOP_DST
set +xeou pipefail
'';
};
};
}

View File

@ -35,7 +35,6 @@
imports = [
./checks/flake-module.nix
./devShell.nix
./devShell-python.nix
./formatter.nix
./templates/flake-module.nix
./clanModules/flake-module.nix

View File

@ -9,9 +9,8 @@
treefmt.programs.mypy.enable = true;
treefmt.programs.mypy.directories = {
"pkgs/clan-cli".extraPythonPackages = self'.packages.clan-cli.pytestDependencies;
"pkgs/clan-vm-manager".extraPythonPackages =
self'.packages.clan-vm-manager.externalPythonDeps ++ self'.packages.clan-cli.pytestDependencies;
"pkgs/clan-cli".extraPythonPackages = self'.packages.clan-cli.testDependencies;
"pkgs/clan-vm-manager".extraPythonPackages = self'.packages.clan-vm-manager.testDependencies;
};
treefmt.settings.formatter.nix = {

View File

@ -1,4 +1,5 @@
source_up
# BUG: If this is enabled the devshell depends on clan_cli building successfully
# source_up
watch_file flake-module.nix default.nix

View File

@ -8,7 +8,7 @@ from typing import Any
from clan_cli.clan_uri import ClanURI, MachineData
from clan_cli.dirs import vm_state_dir
from qemu.qmp import QEMUMonitorProtocol
from clan_cli.qemu.qmp import QEMUMonitorProtocol
from ..cmd import run
from ..errors import ClanError

View File

@ -1,4 +1,5 @@
{
# Inputs for the package
age,
lib,
argcomplete,
@ -10,14 +11,11 @@
pytest-xdist,
pytest-subprocess,
pytest-timeout,
remote-pdb,
ipdb,
python3,
runCommand,
setuptools,
sops,
stdenv,
wheel,
fakeroot,
rsync,
bash,
@ -30,33 +28,15 @@
gnupg,
e2fsprogs,
mypy,
rope,
clan-core-path,
}:
let
dependencies = [
argcomplete # optional dependency: if not enabled, shell completion will not work
# Dependencies that are directly used in the project
pythonDependencies = [
argcomplete # Enables shell completion; without it, this feature won't work.
];
pytestDependencies =
runtimeDependencies
++ dependencies
++ [
pytest
pytest-cov
pytest-subprocess
pytest-xdist
pytest-timeout
remote-pdb
ipdb
openssh
git
gnupg
stdenv.cc
];
# Optional dependencies for clan cli, we re-expose them here to make sure they all build.
# Runtime dependencies required by the application
runtimeDependencies = [
bash
nix
@ -74,14 +54,31 @@ let
e2fsprogs
];
# Dependencies required for running tests
testDependencies =
runtimeDependencies
++ [
gnupg
stdenv.cc # Compiler used for certain native extensions
]
++ pythonDependencies
++ [
pytest # Testing framework
pytest-cov # Generate coverage reports
pytest-subprocess # fake the real subprocess behavior to make your tests more independent.
pytest-xdist # Run tests in parallel on multiple cores
pytest-timeout # Add timeouts to your tests
];
# Convert runtimeDependencies into an attribute set for easier access
runtimeDependenciesAsSet = builtins.listToAttrs (
builtins.map (p: lib.nameValuePair (lib.getName p.name) p) runtimeDependencies
);
checkPython = python3.withPackages (_ps: pytestDependencies);
# Setup Python environment with all dependencies for running tests
pythonWithTestDeps = python3.withPackages (_ps: testDependencies);
# - vendor the jsonschema nix lib (copy instead of symlink).
# Interesting fact: using nixpkgs from flakes instead of nixpkgs.path is reduces evaluation time by 5s.
# Prepare the source code for the project, including copying over jsonschema and nixpkgs
source = runCommand "clan-cli-source" { } ''
cp -r ${./.} $out
chmod -R +w $out
@ -89,6 +86,8 @@ let
ln -s ${nixpkgs'} $out/clan_cli/nixpkgs
cp -r ${../../lib/jsonschema} $out/clan_cli/config/jsonschema
'';
# Create a custom nixpkgs for use within the project
nixpkgs' = runCommand "nixpkgs" { nativeBuildInputs = [ nix ]; } ''
mkdir $out
cat > $out/flake.nix << EOF
@ -114,36 +113,36 @@ python3.pkgs.buildPythonApplication {
src = source;
format = "pyproject";
makeWrapperArgs = [
# This prevents problems with mixed glibc versions that might occur when the
# cli is called through a browser built against another glibc
"--unset LD_LIBRARY_PATH"
];
# Arguments for the wrapper to unset LD_LIBRARY_PATH to avoid glibc version issues
makeWrapperArgs = [ "--unset LD_LIBRARY_PATH" ];
# Build-time dependencies.
nativeBuildInputs = [
setuptools
installShellFiles
];
propagatedBuildInputs = dependencies;
# also re-expose dependencies so we test them in CI
propagatedBuildInputs = pythonDependencies;
# Define and expose the tests and checks to run in CI
passthru.tests =
(lib.mapAttrs' (n: lib.nameValuePair "clan-dep-${n}") runtimeDependenciesAsSet)
// rec {
clan-pytest-without-core =
runCommand "clan-pytest-without-core" { nativeBuildInputs = [ checkPython ] ++ pytestDependencies; }
runCommand "clan-pytest-without-core"
{ nativeBuildInputs = [ pythonWithTestDeps ] ++ testDependencies; }
''
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 and not with_core" ./tests
${pythonWithTestDeps}/bin/python -m pytest -m "not impure and not with_core" ./tests
touch $out
'';
# separate the tests that can never be cached
clan-pytest-with-core =
runCommand "clan-pytest-with-core" { nativeBuildInputs = [ checkPython ] ++ pytestDependencies; }
runCommand "clan-pytest-with-core"
{ nativeBuildInputs = [ pythonWithTestDeps ] ++ testDependencies; }
''
cp -r ${source} ./src
chmod +w -R ./src
@ -151,15 +150,11 @@ python3.pkgs.buildPythonApplication {
export CLAN_CORE=${clan-core-path}
export NIX_STATE_DIR=$TMPDIR/nix IN_NIX_SANDBOX=1
${checkPython}/bin/python -m pytest -m "not impure and with_core" ./tests
${pythonWithTestDeps}/bin/python -m pytest -m "not impure and with_core" ./tests
touch $out
'';
clan-pytest = runCommand "clan-pytest" { } ''
echo ${clan-pytest-without-core}
echo ${clan-pytest-with-core}
touch $out
'';
# Utility to check for leftover debugging breakpoints in the codebase
check-for-breakpoints = runCommand "breakpoints" { } ''
if grep --include \*.py -Rq "breakpoint()" ${source}; then
echo "breakpoint() found in ${source}:"
@ -170,18 +165,13 @@ python3.pkgs.buildPythonApplication {
'';
};
# Additional pass-through attributes
passthru.nixpkgs = nixpkgs';
passthru.checkPython = checkPython;
passthru.devDependencies = [
rope
setuptools
wheel
] ++ pytestDependencies;
passthru.pytestDependencies = pytestDependencies;
passthru.testDependencies = testDependencies;
passthru.pythonWithTestDeps = pythonWithTestDeps;
passthru.runtimeDependencies = runtimeDependencies;
# Install shell completions for bash and fish using the argcomplete package
postInstall = ''
cp -r ${nixpkgs'} $out/${python3.sitePackages}/clan_cli/nixpkgs
installShellCompletion --bash --name clan \
@ -189,13 +179,17 @@ python3.pkgs.buildPythonApplication {
installShellCompletion --fish --name clan.fish \
<(${argcomplete}/bin/register-python-argcomplete --shell fish clan)
'';
# Don't leak python packages into a devshell.
# It can be very confusing if you `nix run` then load the cli from the devshell instead.
# Clean up after the package to avoid leaking python packages into a devshell
postFixup = ''
rm $out/nix-support/propagated-build-inputs
'';
# Run a basic check to ensure the application is executable
checkPhase = ''
PYTHONPATH= $out/bin/clan --help
'';
# Specify the main program for this package
meta.mainProgram = "clan";
}

View File

@ -1,59 +1,29 @@
{
nix-unit,
clan-cli,
system,
mkShell,
writeScriptBin,
openssh,
ruff,
python3,
}:
let
checkScript = writeScriptBin "check" ''
nix build .#checks.${system}.{treefmt,clan-pytest} -L "$@"
'';
pythonWithDeps = python3.withPackages (
ps: clan-cli.propagatedBuildInputs ++ clan-cli.devDependencies ++ [ ps.pip ]
);
devshellTestDeps =
clan-cli.passthru.testDependencies
++ (with python3.pkgs; [
rope
setuptools
wheel
pip
]);
in
mkShell {
packages = [
buildInputs = [
nix-unit
openssh
ruff
clan-cli.checkPython
];
] ++ devshellTestDeps;
shellHook = ''
tmp_path=$(realpath ./.direnv)
repo_root=$(realpath .)
mkdir -p "$tmp_path/python/${pythonWithDeps.sitePackages}"
# Install the package in editable mode
# This allows executing `clan` from within the dev-shell using the current
# version of the code and its dependencies.
${pythonWithDeps.interpreter} -m pip install \
--quiet \
--disable-pip-version-check \
--no-index \
--no-build-isolation \
--prefix "$tmp_path/python" \
--editable $repo_root
export PATH=$(pwd)/bin:$PATH
ln -sfT ${clan-cli.nixpkgs} clan_cli/nixpkgs
export PATH="$tmp_path/python/bin:${checkScript}/bin:$PATH"
export PYTHONPATH="$repo_root:$tmp_path/python/${pythonWithDeps.sitePackages}:"
export XDG_DATA_DIRS="$tmp_path/share''${XDG_DATA_DIRS:+:$XDG_DATA_DIRS}"
export fish_complete_path="$tmp_path/share/fish/vendor_completions.d''${fish_complete_path:+:$fish_complete_path}"
mkdir -p \
$tmp_path/share/fish/vendor_completions.d \
$tmp_path/share/bash-completion/completions \
$tmp_path/share/zsh/site-functions
register-python-argcomplete --shell fish clan > $tmp_path/share/fish/vendor_completions.d/clan.fish
register-python-argcomplete --shell bash clan > $tmp_path/share/bash-completion/completions/clan
'';
}

View File

@ -5,11 +5,13 @@ from pathlib import Path
import pytest
sys.path.append(os.path.join(os.path.dirname(__file__), "helpers"))
sys.path.append(str(Path(__file__).parent.parent)) # Also add clan_cli to PYTHONPATH
from clan_cli.custom_logger import setup_logging
from clan_cli.nix import nix_shell
sys.path.append(os.path.join(os.path.dirname(__file__), "helpers"))
pytest_plugins = [
"temporary_dir",
"root",

View File

@ -12,8 +12,8 @@ from fixtures_flakes import FlakeForTest, generate_flake
from root import CLAN_CORE
from clan_cli.dirs import vm_state_dir
from qemu.qga import QgaSession
from qemu.qmp import QEMUMonitorProtocol
from clan_cli.qemu.qga import QgaSession
from clan_cli.qemu.qmp import QEMUMonitorProtocol
if TYPE_CHECKING:
from age_keys import KeyPair

View File

@ -1,4 +1,5 @@
source_up
# See comment in clan-cli/.envrc
# source_up
watch_file flake-module.nix default.nix

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

@ -0,0 +1,6 @@
import sys
from . import main
if __name__ == "__main__":
sys.exit(main())

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,11 @@
clan-cli,
makeDesktopItem,
libadwaita,
pytest, # Testing framework
pytest-cov, # Generate coverage reports
pytest-subprocess, # fake the real subprocess behavior to make your tests more independent.
pytest-xdist, # Run tests in parallel on multiple cores
pytest-timeout, # Add timeouts to your tests
}:
let
source = ./.;
@ -23,6 +28,36 @@ let
startupWMClass = "clan";
mimeTypes = [ "x-scheme-handler/clan" ];
};
# Dependencies that are directly used in the project but nor from internal python packages
externalPythonDeps = [
pygobject3
pygobject-stubs
gtk4
libadwaita
gnome.adwaita-icon-theme
];
# Deps including python packages from the local project
allPythonDeps = [ (python3.pkgs.toPythonModule clan-cli) ] ++ externalPythonDeps;
# Runtime binary dependencies required by the application
runtimeDependencies = [ ];
# Dependencies required for running tests
testDependencies =
runtimeDependencies
++ allPythonDeps
++ [
pytest # Testing framework
pytest-cov # Generate coverage reports
pytest-subprocess # fake the real subprocess behavior to make your tests more independent.
pytest-xdist # Run tests in parallel on multiple cores
pytest-timeout # Add timeouts to your tests
];
# Setup Python environment with all dependencies for running tests
pythonWithTestDeps = python3.withPackages (_ps: testDependencies);
in
python3.pkgs.buildPythonApplication rec {
name = "clan-vm-manager";
@ -35,6 +70,7 @@ python3.pkgs.buildPythonApplication rec {
"--unset LD_LIBRARY_PATH"
];
# Deps needed only at build time
nativeBuildInputs = [
setuptools
copyDesktopItems
@ -42,29 +78,28 @@ python3.pkgs.buildPythonApplication rec {
gobject-introspection
];
buildInputs = [
gtk4
libadwaita
gnome.adwaita-icon-theme
];
# We need to propagate the build inputs to nix fmt / treefmt
propagatedBuildInputs = [
(python3.pkgs.toPythonModule clan-cli)
passthru.externalPythonDeps
];
# The necessity of setting buildInputs and propagatedBuildInputs to the
# same values for your Python package within Nix largely stems from ensuring
# that all necessary dependencies are consistently available both
# at build time and runtime,
buildInputs = allPythonDeps ++ runtimeDependencies;
propagatedBuildInputs = allPythonDeps ++ runtimeDependencies;
# also re-expose dependencies so we test them in CI
passthru = {
inherit desktop-file;
# Keep external dependencies in a separate lists to refer to thm elsewhere
# This helps avoiding issues like dev-shells accidentally depending on
# nix derivations of local packages.
externalPythonDeps = [
pygobject3
pygobject-stubs
];
tests = {
clan-vm-manager-pytest =
runCommand "clan-vm-manager-pytest" { inherit buildInputs propagatedBuildInputs nativeBuildInputs; }
''
cp -r ${source} ./src
chmod +w -R ./src
cd ./src
export NIX_STATE_DIR=$TMPDIR/nix IN_NIX_SANDBOX=1
${pythonWithTestDeps}/bin/python -m pytest -s -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}:"
@ -76,6 +111,12 @@ python3.pkgs.buildPythonApplication rec {
};
};
# Additional pass-through attributes
passthru.desktop-file = desktop-file;
passthru.externalPythonDeps = externalPythonDeps;
passthru.testDependencies = testDependencies;
passthru.runtimeDependencies = runtimeDependencies;
# Don't leak python packages into a devshell.
# It can be very confusing if you `nix run` than load the cli from the devshell instead.
postFixup = ''

View File

@ -4,7 +4,7 @@
{ config, pkgs, ... }:
{
devShells.clan-vm-manager = pkgs.callPackage ./shell.nix {
inherit (config.packages) clan-cli clan-vm-manager;
inherit (config.packages) clan-vm-manager;
};
packages.clan-vm-manager = pkgs.python3.pkgs.callPackage ./default.nix {
inherit (config.packages) clan-cli;

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
@ -22,10 +30,6 @@ disallow_untyped_calls = true
disallow_untyped_defs = true
no_implicit_optional = true
[[tool.mypy.overrides]]
module = "clan_cli.*"
ignore_missing_imports = true
[tool.ruff]
target-version = "py311"
line-length = 88

View File

@ -1,84 +1,46 @@
{
lib,
runCommand,
makeWrapper,
stdenv,
clan-vm-manager,
gdb,
gtk4,
libadwaita,
clan-cli,
mkShell,
ruff,
desktop-file-utils,
xdg-utils,
mypy,
python3,
python3Packages,
gtk4,
libadwaita,
}:
mkShell (
let
pygdb =
runCommand "pygdb"
{
buildInputs = [
gdb
python3
makeWrapper
];
}
''
mkdir -p "$out/bin"
makeWrapper "${gdb}/bin/gdb" "$out/bin/pygdb" \
--add-flags '-ex "source ${python3}/share/gdb/libpython.py"'
'';
in
{
inherit (clan-vm-manager) propagatedBuildInputs buildInputs;
linuxOnlyPackages = lib.optionals stdenv.isLinux [
xdg-utils
pygdb
];
# To debug clan-vm-manger execute pygdb --args python ./bin/clan-vm-manager
nativeBuildInputs = [
ruff
desktop-file-utils
let
devshellTestDeps =
clan-vm-manager.testDependencies
++ (with python3.pkgs; [
rope
mypy
python3Packages.ipdb
gtk4.dev
ipdb
setuptools
wheel
pip
]);
in
mkShell {
inherit (clan-vm-manager) nativeBuildInputs;
buildInputs =
[
ruff
gtk4.dev # has the demo called 'gtk4-widget-factory'
libadwaita.devdoc # has the demo called 'adwaita-1-demo'
] ++ clan-vm-manager.nativeBuildInputs ++ clan-vm-manager.propagatedBuildInputs;
]
++ devshellTestDeps
PYTHONBREAKPOINT = "ipdb.set_trace";
# Dependencies for testing for linux hosts
++ (lib.optionals stdenv.isLinux [
xdg-utils # install desktop files
desktop-file-utils # verify desktop files
]);
shellHook = ''
ln -sfT ${clan-cli.nixpkgs} ../clan-cli/clan_cli/nixpkgs
# prepend clan-cli for development
export PYTHONPATH=../clan-cli:$PYTHONPATH
if ! command -v xdg-mime &> /dev/null; then
echo "Warning: 'xdg-mime' is not available. The desktop file cannot be installed."
fi
# install desktop file
set -eou pipefail
DESKTOP_FILE_NAME=org.clan.vm-manager.desktop
DESKTOP_DST=~/.local/share/applications/$DESKTOP_FILE_NAME
DESKTOP_SRC=${clan-vm-manager}/share/applications/$DESKTOP_FILE_NAME
UI_BIN="${clan-vm-manager}/bin/clan-vm-manager"
cp -f $DESKTOP_SRC $DESKTOP_DST
sleep 2
sed -i "s|Exec=.*clan-vm-manager|Exec=$UI_BIN|" $DESKTOP_DST
xdg-mime default $DESKTOP_FILE_NAME x-scheme-handler/clan
echo "==== Validating desktop file installation ===="
set -x
desktop-file-validate $DESKTOP_DST
set +xeou pipefail
'';
}
)
shellHook = ''
export PATH=$(pwd)/bin:$PATH
'';
}

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,8 @@
import time
from wayland import GtkProc
def test_open(app: GtkProc) -> None:
time.sleep(0.5)
assert app.poll() is None

View File

@ -0,0 +1,27 @@
import sys
from collections.abc import Generator
from subprocess import Popen
from typing import NewType
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"])
compositor = Popen(["weston"])
yield compositor
# Cleanup: Terminate the compositor
compositor.terminate()
GtkProc = NewType("GtkProc", Popen)
@pytest.fixture(scope="function")
def app() -> Generator[GtkProc, None, None]:
rapp = Popen([sys.executable, "-m", "clan_vm_manager"], text=True)
yield GtkProc(rapp)
# Cleanup: Terminate your application
rapp.terminate()