testlib: default nix fixture to also test all daemon protocols (1/3)

This first geralizes the `nix` fixture with `pytest_generate_tests` to
iterate over all protocols (unless they are marked with `no_daemon`),
though for now the list of protocols is set to be empty. The lang tests
are all tagged with `no_daemon` because they are mostly pure and running
them multiple times would be wasteful.

Co-authored-by: rootile <lix@rootile.de>
Co-authored-by: piegames <git@piegames.de>
Change-Id: Ib407edb420ba4bf434cacf9563a71f5ae6fa8eef
This commit is contained in:
eldritch horrors
2026-06-28 13:45:46 +02:00
co-authored by rootile piegames
parent 7868f52c2a
commit 84fb93a009
11 changed files with 140 additions and 80 deletions
@@ -6,6 +6,10 @@ from testlib.fixtures.file_helper import with_files, File, AssetSymlink
from testlib.fixtures.nix import Nix
from testlib.fixtures.snapshot import Snapshot
import pytest
pytestmark = pytest.mark.no_daemon
@with_files(
{
@@ -6,6 +6,10 @@ from testlib.fixtures.file_helper import with_files, CopyFile, AssetSymlink, Fil
from testlib.fixtures.nix import Nix
from testlib.fixtures.snapshot import Snapshot
import pytest
pytestmark = pytest.mark.no_daemon
@with_files(
{
@@ -6,6 +6,10 @@ from testlib.fixtures.file_helper import with_files, CopyFile, Symlink, AssetSym
from testlib.fixtures.nix import Nix
from testlib.fixtures.snapshot import Snapshot
import pytest
pytestmark = pytest.mark.no_daemon
@with_files(
{
@@ -6,6 +6,10 @@ from testlib.fixtures.file_helper import with_files, CopyFile, Symlink, AssetSym
from testlib.fixtures.nix import Nix
from testlib.fixtures.snapshot import Snapshot
import pytest
pytestmark = pytest.mark.no_daemon
@with_files(
{
@@ -1,5 +1,9 @@
from testlib.fixtures.nix import Nix
import pytest
pytestmark = pytest.mark.no_daemon
def test_err_context(nix: Nix):
# the lang test framework doesn't check this folder, as there is a custom test in here
@@ -7,6 +7,8 @@ from testlib.fixtures.nix import Nix
from testlib.fixtures.snapshot import Snapshot
from testlib.utils import functional2_base_folder
pytestmark = pytest.mark.no_daemon
@pytest.mark.parametrize(
("expr", "exit_code", "exit_code_depr"),
@@ -6,6 +6,10 @@ from testlib.fixtures.file_helper import AssetSymlink, CopyFile, CopyTree, with_
from testlib.fixtures.nix import Nix
from testlib.fixtures.snapshot import Snapshot
import pytest
pytestmark = pytest.mark.no_daemon
@with_files(
{
+2
View File
@@ -11,6 +11,8 @@ from lang.lang_util import LangTest, fetch_all_lang_tests, LangTestRunner
from testlib.fixtures.nix import Nix, with_diverted_store
from testlib.fixtures.snapshot import Snapshot
pytestmark = pytest.mark.no_daemon
def pytest_generate_tests(metafunc: Metafunc):
"""
+1 -1
View File
@@ -6,7 +6,7 @@ requires-python = ">=3.12"
[tool.pytest.ini_options]
addopts = "-p no:xonsh"
markers = ["full_sandbox: test requires a fully isolated sandbox"]
markers = ["full_sandbox: test requires a fully isolated sandbox", "no_daemon: only use the local store for this test and do not test the daemon protocol implementations"]
# xfail tests should fail when not failing as described
xfail_strict = true
@@ -10,6 +10,8 @@ from testlib.fixtures.file_helper import with_files
import pytest
from testlib.fixtures.command import Command
pytestmark = pytest.mark.no_daemon
def get_functional2_repl_files(files: FileDeclaration | None = None) -> FileDeclaration:
repl_base = functional2_base_folder / "repl_characterization"
@@ -50,7 +52,7 @@ def get_functional2_repl_files(files: FileDeclaration | None = None) -> FileDecl
)
def test_trivial_succeeds(pytest_command: Command):
res = pytest_command.run().ok()
assert "repl_basics:nya.md] PASSED" in res.stdout_s
assert "repl_basics:nya.md-None] PASSED" in res.stdout_s
@pytest.mark.parametrize("pytest_command", [(["-k", "repl_char"], False)], indirect=True)
@@ -76,7 +78,7 @@ def test_trivial_succeeds(pytest_command: Command):
def test_trivial_fails(pytest_command: Command):
res = pytest_command.run().expect(1)
assert (
"FAILED repl_characterization/test_repl.py::test_repl_char[repl_basics:nya.md]"
"FAILED repl_characterization/test_repl.py::test_repl_char[repl_basics:nya.md-None]"
in res.stdout_s
)
+107 -77
View File
@@ -2,7 +2,7 @@ import contextlib
import copy
import dataclasses
import sys
from functools import partialmethod
from functools import partialmethod, partial
from pathlib import Path
from typing import Any, Literal, get_args
from collections.abc import Callable, Generator
@@ -319,27 +319,12 @@ _fully_sandboxed = (
)
def pytest_runtest_setup(item: Any):
def pytest_runtest_setup(item: pytest.Item):
for mark in item.iter_markers(name="full_sandbox"):
if not _fully_sandboxed:
pytest.skip(f"{sys.platform} does not support full sandboxing")
@pytest.fixture
def nix(tmp_path: Path, env: ManagedEnv, logger: logging.Logger) -> Generator[Nix, Any, None]:
"""
Provides a rich way of calling `nix`.
For pre-applied commands use `nix.nix_instantiate`, `nix.nix_build` etc.
After configuring the command, use `.run()` to run it
"""
yield Nix(env, logger)
# when things are done using the nix store, the permissions for the store are read only
# after the test was executed, we set the permissions to rwx (write being the important part)
# for pytest to be able to delete the files during cleanup
cmd = Command(argv=["chmod", "-R", "+w", str(tmp_path.absolute())], _env=env)
cmd.run().ok()
type NixDaemon = Callable[..., contextlib.AbstractAsyncContextManager[Nix]]
# NOTE: the order of items here is important. the daemon fixture requires
@@ -351,70 +336,115 @@ daemon_protocols: list[NixDaemonProtocol] = get_args(NixDaemonProtocol.__value__
_daemon_protocol_xp_features: dict[NixDaemon, list[str]] = {"lix-xp-1": ["rpc-sockets"]}
# paramterize every daemon tests to run using all supported nix protocols
@contextlib.contextmanager
def _daemon_wrapper(
default_protocol: NixDaemonProtocol,
nix: Nix,
args: list[str] | None = None,
settings: dict[str, _NixSettingValue] | None = None,
protocol: NixDaemonProtocol | None = None,
**kwargs,
) -> contextlib.AbstractAsyncContextManager[Nix]:
protocol = protocol or default_protocol
daemon = copy.deepcopy(nix)
daemon.logger = nix.logger.getChild("daemon")
daemon.settings["allowed-users"] = ["*"]
daemon.settings["trusted-users"] = []
daemon.settings.store = f"local?root={nix.env.dirs.test_root}"
daemon.settings.update(settings)
if requires_features := _daemon_protocol_xp_features.get(protocol):
daemon.settings.add_xp_feature(*requires_features)
sockets_dir = Path(daemon.env.dirs.nix_state_dir) / "daemon-socket"
sockets = [sockets_dir / "socket", sockets_dir / "lix-xp-1/socket"]
for p in sockets:
p.unlink(missing_ok=True)
proc = daemon.nix(args or [], nix_exe="nix-daemon", **kwargs).start()
def log_daemon_result(result: CommandResult | None, level: int):
if result:
daemon.logger.log(level, "daemon exited with code %i", result.rc)
daemon.logger.log(level, "stdout: %s", result.stdout_s)
daemon.logger.log(level, "stderr: %s", result.stderr_s)
else:
daemon.logger.error("daemon exited unexpectedly")
# wait for daemon to come up. this may take a while under load.
# we wait only for the first socket in the list, expecting that
# it'll be the last one opened by the daemon. this is to ensure
# that we always return correctly regardless of rpc xp settings
while not sockets[0].exists():
if status := proc.wait(0.01):
log_daemon_result(status, logging.ERROR)
raise RuntimeError("daemon exited during startup")
inner = copy.deepcopy(nix)
socket_path = sockets_dir / "socket" if protocol == "legacy-combined" else sockets_dir
inner.settings.store = f"unix://{socket_path}?protocol={protocol}"
try:
timeout, level = 1, logging.ERROR
yield inner
# 5 seconds should be enough to wait for a *graceful* exit.
timeout, level = 5, logging.DEBUG
finally:
result = proc.terminate(timeout)
if not result:
result = proc.kill()
log_daemon_result(result, level)
def _nix_plain_impl(
tmp_path: Path, env: ManagedEnv, logger: logging.Logger
) -> Generator[Nix, Any, None]:
yield Nix(env, logger)
# when things are done using the nix store, the permissions for the store are read only
# after the test was executed, we set the permissions to rwx (write being the important part)
# for pytest to be able to delete the files during cleanup
cmd = Command(argv=["chmod", "-R", "+w", str(tmp_path.absolute())], _env=env)
cmd.run().ok()
@pytest.fixture
def nix(
tmp_path: Path, env: ManagedEnv, logger: logging.Logger, request: pytest.FixtureRequest
) -> Generator[Nix, Any, None]:
"""
Provides a rich way of calling `nix`.
For pre-applied commands use `nix.nix_instantiate`, `nix.nix_build` etc.
After configuring the command, use `.run()` to run it
"""
for nix in _nix_plain_impl(tmp_path, env, logger):
if getattr(request, "param", None) is None:
yield nix
else:
with _daemon_wrapper(request.param, nix) as inner:
yield inner
@pytest.fixture(params=daemon_protocols)
def daemon(request: pytest.FixtureRequest) -> NixDaemon:
default_protocol = request.param
"""
paramterize every daemon tests to run using all supported nix protocols
"""
return partial(_daemon_wrapper, request.param)
@contextlib.contextmanager
def wrapper(
nix: Nix,
args: list[str] | None = None,
settings: dict[str, _NixSettingValue] | None = None,
protocol: NixDaemonProtocol | None = None,
**kwargs,
) -> contextlib.AbstractAsyncContextManager[Nix]:
protocol = protocol or default_protocol
daemon = copy.deepcopy(nix)
daemon.logger = nix.logger.getChild("daemon")
daemon.settings["allowed-users"] = ["*"]
daemon.settings["trusted-users"] = []
daemon.settings.store = f"local?root={nix.env.dirs.test_root}"
daemon.settings.update(settings)
if requires_features := _daemon_protocol_xp_features.get(protocol):
daemon.settings.add_xp_feature(*requires_features)
sockets_dir = Path(daemon.env.dirs.nix_state_dir) / "daemon-socket"
sockets = [sockets_dir / "socket", sockets_dir / "lix-xp-1/socket"]
for p in sockets:
p.unlink(missing_ok=True)
proc = daemon.nix(args or [], nix_exe="nix-daemon", **kwargs).start()
def log_daemon_result(result: CommandResult | None, level: int):
if result:
daemon.logger.log(level, "daemon exited with code %i", result.rc)
daemon.logger.log(level, "stdout: %s", result.stdout_s)
daemon.logger.log(level, "stderr: %s", result.stderr_s)
else:
daemon.logger.error("daemon exited unexpectedly")
# wait for daemon to come up. this may take a while under load.
# we wait only for the first socket in the list, expecting that
# it'll be the last one opened by the daemon. this is to ensure
# that we always return correctly regardless of rpc xp settings
while not sockets[0].exists():
if status := proc.wait(0.01):
log_daemon_result(status, logging.ERROR)
raise RuntimeError("daemon exited during startup")
inner = copy.deepcopy(nix)
socket_path = sockets_dir / "socket" if protocol == "legacy-combined" else sockets_dir
inner.settings.store = f"unix://{socket_path}?protocol={protocol}"
try:
timeout, level = 1, logging.ERROR
yield inner
# 5 seconds should be enough to wait for a *graceful* exit.
timeout, level = 5, logging.DEBUG
finally:
result = proc.terminate(timeout)
if not result:
result = proc.kill()
log_daemon_result(result, level)
return wrapper
def pytest_generate_tests(metafunc: pytest.Metafunc):
"""
Generate parametrized tests from all tests that use the Nix fixture to test
nix with all available daemon protocols, *unless* those tests are marked with `no_daemon`
"""
if "nix" not in metafunc.fixturenames or "daemon" in metafunc.fixturenames:
return
if not list(metafunc.definition.iter_markers("no_daemon")):
protocols = [None]
# do not enable them the protocols for now
# protocols += daemon_protocols # noqa ERA001
ids = protocols
metafunc.parametrize("nix", protocols, indirect=True, ids=ids)
@pytest.fixture