diff --git a/pyproject.toml b/pyproject.toml index b470684..5a9ff8b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,6 +4,11 @@ line-length = 88 select = ["E", "F", "I"] ignore = [ "E501" ] +[tool.coverage.report] +exclude_lines = [ + "pragma: no cover", + "pytest.skip", +] [tool.mypy] python_version = "3.10" warn_redundant_casts = true diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/case.py b/tests/case.py new file mode 100644 index 0000000..f7b42f5 --- /dev/null +++ b/tests/case.py @@ -0,0 +1,105 @@ +from __future__ import annotations + +import functools +import logging +import os +import shutil +import subprocess +import textwrap +from pathlib import Path + +import pytest + +CMD = Path | str + +log = logging.getLogger(__name__) + +TEST_ROOT = Path(__file__).parent + +PROJECT_ROOT = TEST_ROOT.parent + +NIX_DIRENV = PROJECT_ROOT / "direnvrc" + + +class TestCase: + @pytest.fixture(autouse=True) + def _setup(self, tmp_path: Path) -> None: + self.root = tmp_path + log.debug(f"path: {self.root}") + + @functools.cached_property + def home(self) -> Path: + return self.root / "home" + + @functools.cached_property + def path(self) -> Path: + path = self.root / "cwd" + shutil.copytree(TEST_ROOT / "testenv", path) + return path + + def _log_output( + self, + result: subprocess.CompletedProcess | subprocess.CalledProcessError, + errlevel: str = "debug", + ) -> None: + for out in ("stdout", "stderr"): + text = getattr(result, out).strip() + setattr(result, out, text) + if text: + getattr(log, errlevel if out == "stderr" else "debug")( + f"{out[3:]}:{os.linesep if os.linesep in text else ' '}{text}" + ) + + def run( + self, + *cmd: CMD, + **env: str, + ) -> subprocess.CompletedProcess: + env = dict(PATH=os.environ["PATH"], HOME=str(self.home)) | env + command = list(map(str, cmd)) + log.debug(f"$ {subprocess.list2cmdline(command)}") + try: + result = subprocess.run( + command, + capture_output=True, + check=True, + text=True, + cwd=self.path, + env=env, + ) + except subprocess.CalledProcessError as exc: # pragma: no cover + self._log_output(exc, errlevel="error") + raise + self._log_output(result) + return result + + def nix_run(self, *cmd: CMD, **env: str) -> subprocess.CompletedProcess: + return self.run("nix", *cmd, **env) + + def direnv_run(self, *cmd: CMD, **env: str) -> subprocess.CompletedProcess: + return self.run("direnv", *cmd, **env) + + def direnv_exec(self, *cmd: CMD, **env: str) -> subprocess.CompletedProcess: + return self.run("direnv", "exec", ".", *cmd, **env) + + def direnv_var(self, name: str, **env: str) -> subprocess.CompletedProcess: + return self.direnv_exec("sh", "-c", f"echo -n ${name}", **env) + + def setup_envrc( + self, content: str, strict_env: bool, **env: str + ) -> subprocess.CompletedProcess: + text = textwrap.dedent( + f""" + {'strict_env' if strict_env else ''} + source {NIX_DIRENV} + {content} + """, + ) + (self.path / ".envrc").write_text(text.strip()) + return self.direnv_run("allow", **env) + + def assert_direnv_var(self, name: str, **env: str) -> subprocess.CompletedProcess: + result = self.direnv_var(name, **env) + assert result.stdout == "OK" + assert "renewed cache" in result.stderr + return result diff --git a/tests/conftest.py b/tests/conftest.py deleted file mode 100644 index 8a849bf..0000000 --- a/tests/conftest.py +++ /dev/null @@ -1,4 +0,0 @@ -pytest_plugins = [ - "direnv_project", - "root", -] diff --git a/tests/direnv_project.py b/tests/direnv_project.py deleted file mode 100644 index 124b7fd..0000000 --- a/tests/direnv_project.py +++ /dev/null @@ -1,49 +0,0 @@ -import shutil -import textwrap -from dataclasses import dataclass -from pathlib import Path -from tempfile import TemporaryDirectory -from typing import Iterator - -import pytest -from procs import run - - -@dataclass -class DirenvProject: - dir: Path - nix_direnv: Path - - @property - def envrc(self) -> Path: - return self.dir / ".envrc" - - def setup_envrc(self, content: str, strict_env: bool) -> None: - text = textwrap.dedent( - f""" - {'strict_env' if strict_env else ''} - source {self.nix_direnv} - {content} - """ - ) - self.envrc.write_text(text) - run(["direnv", "allow"], cwd=self.dir) - - -@pytest.fixture -def direnv_project(test_root: Path, project_root: Path) -> Iterator[DirenvProject]: - """ - Setups a direnv test project - """ - with TemporaryDirectory() as _dir: - dir = Path(_dir) / "proj" - shutil.copytree(test_root / "testenv", dir) - shutil.copyfile(project_root / "flake.nix", dir / "flake.nix") - shutil.copyfile(project_root / "flake.lock", dir / "flake.lock") - nix_direnv = project_root / "direnvrc" - - c = DirenvProject(Path(dir), nix_direnv) - try: - yield c - finally: - pass diff --git a/tests/procs.py b/tests/procs.py deleted file mode 100644 index ea837d4..0000000 --- a/tests/procs.py +++ /dev/null @@ -1,23 +0,0 @@ -import subprocess -from pathlib import Path -from typing import IO, Any, List, Optional, Union - -_FILE = Union[None, int, IO[Any]] -_DIR = Union[None, Path, str] - - -def run( - cmd: List[str], - text: bool = True, - check: bool = True, - cwd: _DIR = None, - stderr: _FILE = None, - stdout: _FILE = None, - env: Optional[dict[str, str]] = None, -) -> subprocess.CompletedProcess: - if cwd is not None: - print(f"cd {cwd}") - print("$ " + " ".join(cmd)) - return subprocess.run( - cmd, text=text, check=check, cwd=cwd, stderr=stderr, stdout=stdout, env=env - ) diff --git a/tests/root.py b/tests/root.py deleted file mode 100644 index b3f0914..0000000 --- a/tests/root.py +++ /dev/null @@ -1,22 +0,0 @@ -from pathlib import Path - -import pytest - -TEST_ROOT = Path(__file__).parent.resolve() -PROJECT_ROOT = TEST_ROOT.parent - - -@pytest.fixture -def test_root() -> Path: - """ - Root directory of the tests - """ - return TEST_ROOT - - -@pytest.fixture -def project_root() -> Path: - """ - Root directory of the tests - """ - return PROJECT_ROOT diff --git a/tests/test_gc.py b/tests/test_gc.py index e46fcfd..8fdf4db 100644 --- a/tests/test_gc.py +++ b/tests/test_gc.py @@ -1,92 +1,65 @@ -import subprocess -import sys -import unittest +from __future__ import annotations + +import json import pytest -from direnv_project import DirenvProject -from procs import run + +from .case import TestCase -def common_test(direnv_project: DirenvProject) -> None: - run(["nix-collect-garbage"]) +class TestGc(TestCase): + def common_test(self) -> None: + result = self.direnv_exec("hello") + assert "renewed cache" in result.stderr + assert "Executing shellHook." in result.stderr - testenv = str(direnv_project.dir) + self.nix_run("store", "gc") - out1 = run( - ["direnv", "exec", testenv, "hello"], - stderr=subprocess.PIPE, - check=False, - cwd=direnv_project.dir, - ) - sys.stderr.write(out1.stderr) - assert out1.returncode == 0 - assert "renewed cache" in out1.stderr - assert "Executing shellHook." in out1.stderr + result = self.direnv_exec("hello") + assert "using cached dev shell" in result.stderr + assert "Executing shellHook." in result.stderr - run(["nix-collect-garbage"]) + def common_test_clean(self) -> None: + self.direnv_exec("hello") + files = [path for path in (self.path / ".direnv").iterdir() if path.is_file()] + rcs = [f for f in files if f.match("*.rc")] + profiles = [f for f in files if not f.match("*.rc")] + assert len(rcs) == 1, files + assert len(profiles) == 1, files - out2 = run( - ["direnv", "exec", testenv, "hello"], - stderr=subprocess.PIPE, - check=False, - cwd=direnv_project.dir, - ) - sys.stderr.write(out2.stderr) - assert out2.returncode == 0 - assert "using cached dev shell" in out2.stderr - assert "Executing shellHook." in out2.stderr + @pytest.mark.parametrize("strict_env", [False, True]) + def test_use_nix(self, strict_env: bool) -> None: + self.setup_envrc("use nix", strict_env=strict_env) + self.common_test() + self.setup_envrc( + "use nix --argstr shellHook 'echo Executing hijacked shellHook.'", + strict_env=strict_env, + ) + self.common_test_clean() -def common_test_clean(direnv_project: DirenvProject) -> None: - testenv = str(direnv_project.dir) + def _parse_inputs(self, inputs: dict) -> list: + paths = [inputs["path"]] + for xinput in inputs["inputs"].values(): + paths.extend(self._parse_inputs(xinput)) + return paths - out3 = run( - ["direnv", "exec", testenv, "hello"], - stderr=subprocess.PIPE, - check=False, - cwd=direnv_project.dir, - ) - sys.stderr.write(out3.stderr) + @pytest.mark.parametrize("strict_env", [False, True]) + def test_use_flake(self, strict_env: bool) -> None: + self.setup_envrc("use flake", strict_env=strict_env) + self.common_test() + inputs = list((self.path / ".direnv/flake-inputs").iterdir()) + flake_inputs = self._parse_inputs( + json.loads( + self.nix_run( + "flake", "archive", "--json", "--no-write-lock-file" + ).stdout + ) + ) + # should only contain our flake-utils flake + assert len(inputs) == len(flake_inputs) + for symlink in inputs: + assert symlink.is_dir() - files = [ - path for path in (direnv_project.dir / ".direnv").iterdir() if path.is_file() - ] - rcs = [f for f in files if f.match("*.rc")] - profiles = [f for f in files if not f.match("*.rc")] - if len(rcs) != 1 or len(profiles) != 1: - print(files) - assert len(rcs) == 1 - assert len(profiles) == 1 - - -@pytest.mark.parametrize("strict_env", [False, True]) -def test_use_nix(direnv_project: DirenvProject, strict_env: bool) -> None: - direnv_project.setup_envrc("use nix", strict_env=strict_env) - common_test(direnv_project) - - direnv_project.setup_envrc( - "use nix --argstr shellHook 'echo Executing hijacked shellHook.'", - strict_env=strict_env, - ) - common_test_clean(direnv_project) - - -@pytest.mark.parametrize("strict_env", [False, True]) -def test_use_flake(direnv_project: DirenvProject, strict_env: bool) -> None: - direnv_project.setup_envrc("use flake", strict_env=strict_env) - common_test(direnv_project) - inputs = list((direnv_project.dir / ".direnv/flake-inputs").iterdir()) - # should only contain our flake-utils flake - if len(inputs) != 4: - run(["nix", "flake", "archive", "--json"], cwd=direnv_project.dir) - print(inputs) - assert len(inputs) == 4 - for symlink in inputs: - assert symlink.is_dir() - - direnv_project.setup_envrc("use flake --impure", strict_env=strict_env) - common_test_clean(direnv_project) - - -if __name__ == "__main__": - unittest.main() + self.setup_envrc("use flake --impure", strict_env=strict_env) + self.common_test_clean() diff --git a/tests/test_use_nix.py b/tests/test_use_nix.py index 0c79fa5..e7d67ea 100644 --- a/tests/test_use_nix.py +++ b/tests/test_use_nix.py @@ -1,67 +1,35 @@ +from __future__ import annotations + import os -import subprocess -import sys -import unittest -from typing import Optional import pytest -from direnv_project import DirenvProject -from procs import run + +from .case import TestCase -def direnv_exec( - direnv_project: DirenvProject, cmd: str, env: Optional[dict[str, str]] = None -) -> None: - args = ["direnv", "exec", str(direnv_project.dir), "sh", "-c", cmd] - print("$ " + " ".join(args)) - out = run( - args, - stderr=subprocess.PIPE, - stdout=subprocess.PIPE, - check=False, - cwd=direnv_project.dir, - env=env, - ) - sys.stdout.write(out.stdout) - sys.stderr.write(out.stderr) - assert out.returncode == 0 - assert "OK\n" == out.stdout - assert "renewed cache" in out.stderr +class TestUseNix(TestCase): + @pytest.mark.parametrize("strict_env", [False, True]) + def test_attrs(self, strict_env: bool) -> None: + self.setup_envrc("use nix -A subshell", strict_env=strict_env) + self.assert_direnv_var("THIS_IS_A_SUBSHELL") + @pytest.mark.parametrize("strict_env", [False, True]) + def test_with_nix_path(self, strict_env: bool) -> None: + if (nix_path := os.environ.get("NIX_PATH")) is None: + pytest.skip("no parent NIX_PATH") + else: + self.setup_envrc( + "use nix --argstr someArg OK", strict_env=strict_env, NIX_PATH=nix_path + ) + self.assert_direnv_var("SHOULD_BE_SET", NIX_PATH=nix_path) -@pytest.mark.parametrize("strict_env", [False, True]) -def test_attrs(direnv_project: DirenvProject, strict_env: bool) -> None: - direnv_project.setup_envrc("use nix -A subshell", strict_env=strict_env) - direnv_exec(direnv_project, "echo $THIS_IS_A_SUBSHELL") + @pytest.mark.parametrize("strict_env", [False, True]) + def test_args(self, strict_env: bool) -> None: + self.setup_envrc("use nix --argstr someArg OK", strict_env=strict_env) + self.assert_direnv_var("SHOULD_BE_SET") - -@pytest.mark.parametrize("strict_env", [False, True]) -def test_no_nix_path(direnv_project: DirenvProject, strict_env: bool) -> None: - direnv_project.setup_envrc("use nix --argstr someArg OK", strict_env=strict_env) - env = os.environ.copy() - del env["NIX_PATH"] - direnv_exec(direnv_project, "echo $SHOULD_BE_SET", env=env) - - -@pytest.mark.parametrize("strict_env", [False, True]) -def test_args(direnv_project: DirenvProject, strict_env: bool) -> None: - direnv_project.setup_envrc("use nix --argstr someArg OK", strict_env=strict_env) - direnv_exec(direnv_project, "echo $SHOULD_BE_SET") - - -@pytest.mark.parametrize("strict_env", [False, True]) -def test_no_files(direnv_project: DirenvProject, strict_env: bool) -> None: - direnv_project.setup_envrc("use nix -p hello", strict_env=strict_env) - out = run( - ["direnv", "status"], - stderr=subprocess.PIPE, - stdout=subprocess.PIPE, - check=False, - cwd=direnv_project.dir, - ) - assert out.returncode == 0 - assert 'Loaded watch: "."' not in out.stdout - - -if __name__ == "__main__": - unittest.main() + @pytest.mark.parametrize("strict_env", [False, True]) + def test_no_files(self, strict_env: bool) -> None: + self.setup_envrc("use nix -p hello", strict_env=strict_env) + result = self.direnv_run("status") + assert 'Loaded watch: "."' not in result.stdout diff --git a/tests/testenv/.envrc b/tests/testenv/.envrc deleted file mode 100644 index a565315..0000000 --- a/tests/testenv/.envrc +++ /dev/null @@ -1,2 +0,0 @@ -source ../../direnvrc -use nix diff --git a/tests/testenv/flake.lock b/tests/testenv/flake.lock new file mode 100644 index 0000000..ec4f2f6 --- /dev/null +++ b/tests/testenv/flake.lock @@ -0,0 +1,61 @@ +{ + "nodes": { + "flake-utils": { + "inputs": { + "systems": "systems" + }, + "locked": { + "lastModified": 1694529238, + "narHash": "sha256-zsNZZGTGnMOf9YpHKJqMSsa0dXbfmxeoJ7xHlrt+xmY=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "ff7b65b44d01cf9ba6a71320833626af21126384", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1700856099, + "narHash": "sha256-RnEA7iJ36Ay9jI0WwP+/y4zjEhmeN6Cjs9VOFBH7eVQ=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "0bd59c54ef06bc34eca01e37d689f5e46b3fe2f1", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixpkgs-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "flake-utils": "flake-utils", + "nixpkgs": "nixpkgs" + } + }, + "systems": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/tests/testenv/flake.nix b/tests/testenv/flake.nix index 80a59f8..450ae80 100644 --- a/tests/testenv/flake.nix +++ b/tests/testenv/flake.nix @@ -1,6 +1,6 @@ { description = "A very basic flake"; - inputs.nixpkgs.url = "github:NixOS/nixpkgs"; + inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable"; inputs.flake-utils.url = "github:numtide/flake-utils"; outputs = { self, nixpkgs, flake-utils }: