Merge pull request 'Enable all pytest without core' (#3118) from enable-more-macos into main

Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/3118
This commit is contained in:
Mic92
2025-03-25 17:41:04 +00:00
10 changed files with 132 additions and 92 deletions

View File

@@ -30,7 +30,7 @@ def import_sops(args: argparse.Namespace) -> None:
if args.input_type: if args.input_type:
cmd += ["--input-type", args.input_type] cmd += ["--input-type", args.input_type]
cmd += ["--output-type", "json", "--decrypt", args.sops_file] cmd += ["--output-type", "json", "--decrypt", args.sops_file]
cmd = nix_shell(["nixpkgs#sops"], cmd) cmd = nix_shell(["nixpkgs#sops", "nixpkgs#gnupg"], cmd)
res = run(cmd, RunOpts(error_msg=f"Could not import sops file {file}")) res = run(cmd, RunOpts(error_msg=f"Could not import sops file {file}"))
secrets = json.loads(res.stdout) secrets = json.loads(res.stdout)

View File

@@ -233,7 +233,7 @@ def sops_run(
raise ClanError(msg) raise ClanError(msg)
sops_cmd.append(str(secret_path)) sops_cmd.append(str(secret_path))
cmd = nix_shell(["nixpkgs#sops"], sops_cmd) cmd = nix_shell(["nixpkgs#sops", "nixpkgs#gnupg"], sops_cmd)
opts = ( opts = (
dataclasses.replace(run_opts, env=environ) dataclasses.replace(run_opts, env=environ)
if run_opts if run_opts

View File

@@ -51,7 +51,7 @@ let
testDependencies = testRuntimeDependencies ++ [ testDependencies = testRuntimeDependencies ++ [
gnupg gnupg
stdenv.cc # Compiler used for certain native extensions stdenv.cc # Compiler used for certain native extensions
(pythonRuntime.withPackages (ps: (pyTestDeps ps) ++ (pyDeps ps))) (pythonRuntime.withPackages pyTestDeps)
]; ];
source = runCommand "clan-cli-source" { } '' source = runCommand "clan-cli-source" { } ''
@@ -127,7 +127,7 @@ pythonRuntime.pkgs.buildPythonApplication {
# Define and expose the tests and checks to run in CI # Define and expose the tests and checks to run in CI
passthru.tests = passthru.tests =
(lib.mapAttrs' (n: lib.nameValuePair "clan-dep-${n}") testRuntimeDependenciesMap) (lib.mapAttrs' (n: lib.nameValuePair "clan-dep-${n}") testRuntimeDependenciesMap)
// lib.optionalAttrs (!stdenv.isDarwin) { // {
# disabled on macOS until we fix all remaining issues # disabled on macOS until we fix all remaining issues
clan-pytest-without-core = clan-pytest-without-core =
runCommand "clan-pytest-without-core" runCommand "clan-pytest-without-core"
@@ -159,6 +159,8 @@ pythonRuntime.pkgs.buildPythonApplication {
python -m pytest -m "not impure and not with_core" -n $jobs ./tests python -m pytest -m "not impure and not with_core" -n $jobs ./tests
touch $out touch $out
''; '';
}
// lib.optionalAttrs (!stdenv.isDarwin) {
clan-pytest-with-core = clan-pytest-with-core =
runCommand "clan-pytest-with-core" runCommand "clan-pytest-with-core"
{ {

View File

@@ -1,14 +1,12 @@
import subprocess
from pathlib import Path
import pytest import pytest
from clan_cli.custom_logger import setup_logging from clan_cli.custom_logger import setup_logging
from clan_cli.nix import nix_shell
pytest_plugins = [ pytest_plugins = [
"temporary_dir", "temporary_dir",
"root", "root",
"age_keys", "age_keys",
"gpg_keys",
"git_repo",
"sshd", "sshd",
"command", "command",
"ports", "ports",
@@ -28,18 +26,3 @@ def pytest_sessionstart(session: pytest.Session) -> None:
print(f"Session config: {session.config}") print(f"Session config: {session.config}")
setup_logging(level="DEBUG") 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

@@ -294,18 +294,19 @@ def create_flake(
if tmp_store := nix_test_store(): if tmp_store := nix_test_store():
nix_options += ["--store", str(tmp_store)] nix_options += ["--store", str(tmp_store)]
sp.run( with locked_open(Path(lock_nix), "w"):
[ sp.run(
"nix", [
"flake", "nix",
"lock", "flake",
flake, "lock",
"--extra-experimental-features", flake,
"nix-command flakes", "--extra-experimental-features",
*nix_options, "nix-command flakes",
], *nix_options,
check=True, ],
) check=True,
)
if "/tmp" not in str(os.environ.get("HOME")): if "/tmp" not in str(os.environ.get("HOME")):
log.warning( log.warning(

View File

@@ -6,12 +6,44 @@
#include <string.h> #include <string.h>
#include <sys/types.h> #include <sys/types.h>
#ifdef __APPLE__
#include <sandbox.h>
#include <unistd.h>
#endif
#ifdef __APPLE__
struct dyld_interpose {
const void *replacement;
const void *replacee;
};
#define WRAPPER(ret, name) static ret _fakeroot_wrapper_##name
#define WRAPPER_DEF(name) \
__attribute__(( \
used)) static struct dyld_interpose _fakeroot_interpose_##name \
__attribute__((section("__DATA,__interpose"))) = { \
&_fakeroot_wrapper_##name, &name};
#else
#define WRAPPER(ret, name) ret name
#define WRAPPER_DEF(name)
#endif
typedef struct passwd *(*getpwnam_type)(const char *name); typedef struct passwd *(*getpwnam_type)(const char *name);
struct passwd *getpwnam(const char *name) { WRAPPER(struct passwd *, getpwnam)(const char *name) {
struct passwd *pw; struct passwd *pw;
getpwnam_type orig_getpwnam; #ifdef __APPLE__
orig_getpwnam = (getpwnam_type)dlsym(RTLD_NEXT, "getpwnam"); #define orig_getpwnam(name) getpwnam(name)
#else
static getpwnam_type orig_getpwnam = NULL;
if (!orig_getpwnam) {
orig_getpwnam = (getpwnam_type)dlsym(RTLD_NEXT, "getpwnam");
if (!orig_getpwnam) {
fprintf(stderr, "dlsym error: %s\n", dlerror());
exit(1);
}
}
#endif
pw = orig_getpwnam(name); pw = orig_getpwnam(name);
if (pw) { if (pw) {
@@ -21,6 +53,17 @@ struct passwd *getpwnam(const char *name) {
exit(1); exit(1);
} }
pw->pw_shell = strdup(shell); pw->pw_shell = strdup(shell);
fprintf(stderr, "getpwnam: %s -> %s\n", name, pw->pw_shell);
} }
return pw; return pw;
} }
WRAPPER_DEF(getpwnam)
#ifdef __APPLE__
// sandbox_init(3) doesn't work in nix build sandbox
WRAPPER(int, sandbox_init)(const char *profile, uint64_t flags, void *handle) {
return 0;
}
WRAPPER_DEF(sandbox_init)
#else
#endif

View File

@@ -0,0 +1,20 @@
import subprocess
from pathlib import Path
import pytest
from clan_cli.nix import nix_shell
# fixture for git_repo
@pytest.fixture
def git_repo(temp_dir: Path) -> Path:
# initialize a git repository
cmd = nix_shell(["nixpkgs#git"], ["git", "init"])
subprocess.run(cmd, cwd=temp_dir, check=True)
# set user.name and user.email
cmd = nix_shell(["nixpkgs#git"], ["git", "config", "user.name", "test"])
subprocess.run(cmd, cwd=temp_dir, check=True)
cmd = nix_shell(["nixpkgs#git"], ["git", "config", "user.email", "test@test.test"])
subprocess.run(cmd, cwd=temp_dir, check=True)
# return the path to the git repository
return temp_dir

View File

@@ -0,0 +1,25 @@
import shutil
from dataclasses import dataclass
from pathlib import Path
import pytest
@dataclass
class GpgKey:
fingerprint: str
gpg_home: Path
@pytest.fixture
def gpg_key(
temp_dir: Path,
monkeypatch: pytest.MonkeyPatch,
test_root: Path,
) -> GpgKey:
gpg_home = temp_dir / "gnupghome"
shutil.copytree(test_root / "data" / "gnupg-home", gpg_home)
monkeypatch.setenv("GNUPGHOME", str(gpg_home))
return GpgKey("9A9B2741C8062D3D3DF1302D8B049E262A5CA255", gpg_home)

View File

@@ -1,18 +1,16 @@
import functools
import json import json
import logging import logging
import os import os
import re import re
import subprocess
from collections.abc import Iterator from collections.abc import Iterator
from contextlib import contextmanager from contextlib import contextmanager
from pathlib import Path
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
import pytest import pytest
from age_keys import assert_secrets_file_recipients from age_keys import assert_secrets_file_recipients
from clan_cli.errors import ClanError from clan_cli.errors import ClanError
from fixtures_flakes import FlakeForTest from fixtures_flakes import FlakeForTest
from gpg_keys import GpgKey
from helpers import cli from helpers import cli
from stdout import CaptureOutput from stdout import CaptureOutput
@@ -426,12 +424,12 @@ def use_age_key(key: str, monkeypatch: pytest.MonkeyPatch) -> Iterator[None]:
@contextmanager @contextmanager
def use_gpg_key(key: str, monkeypatch: pytest.MonkeyPatch) -> Iterator[None]: def use_gpg_key(key: GpgKey, monkeypatch: pytest.MonkeyPatch) -> Iterator[None]:
old_key_file = os.environ.get("SOPS_AGE_KEY_FILE") old_key_file = os.environ.get("SOPS_AGE_KEY_FILE")
old_key = os.environ.get("SOPS_AGE_KEY") old_key = os.environ.get("SOPS_AGE_KEY")
monkeypatch.delenv("SOPS_AGE_KEY_FILE", raising=False) monkeypatch.delenv("SOPS_AGE_KEY_FILE", raising=False)
monkeypatch.delenv("SOPS_AGE_KEY", raising=False) monkeypatch.delenv("SOPS_AGE_KEY", raising=False)
monkeypatch.setenv("SOPS_PGP_FP", key) monkeypatch.setenv("SOPS_PGP_FP", key.fingerprint)
try: try:
yield yield
finally: finally:
@@ -442,54 +440,11 @@ def use_gpg_key(key: str, monkeypatch: pytest.MonkeyPatch) -> Iterator[None]:
monkeypatch.setenv("SOPS_AGE_KEY", old_key) monkeypatch.setenv("SOPS_AGE_KEY", old_key)
@pytest.fixture
def gpg_key(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
) -> str:
gpg_home = tmp_path / "gnupghome"
gpg_home.mkdir(mode=0o700)
gpg_environ = os.environ.copy()
gpg_environ["GNUPGHOME"] = str(gpg_home)
run = functools.partial(
subprocess.run,
encoding="utf-8",
check=True,
env=gpg_environ,
)
key_parameters = "\n".join(
(
"%no-protection",
"%transient-key",
"Key-Type: rsa",
"Key-Usage: cert encrypt",
"Name-Real: Foo Bar",
"Name-Comment: Test user",
"Name-Email: test@clan.lol",
"%commit",
)
)
run(["gpg", "--batch", "--quiet", "--generate-key"], input=key_parameters)
details = run(["gpg", "--list-keys", "--with-colons"], capture_output=True)
fingerprint = None
for line in details.stdout.strip().split(os.linesep):
if not line.startswith("fpr"):
continue
fingerprint = line.split(":")[9]
break
assert fingerprint is not None, "Could not generate test GPG key"
log.info(f"Created GPG key under {gpg_home}")
monkeypatch.setenv("GNUPGHOME", str(gpg_home))
return fingerprint
def test_secrets( def test_secrets(
test_flake: FlakeForTest, test_flake: FlakeForTest,
capture_output: CaptureOutput, capture_output: CaptureOutput,
monkeypatch: pytest.MonkeyPatch, monkeypatch: pytest.MonkeyPatch,
gpg_key: str, gpg_key: GpgKey,
age_keys: list["KeyPair"], age_keys: list["KeyPair"],
) -> None: ) -> None:
with capture_output as output: with capture_output as output:
@@ -716,7 +671,7 @@ def test_secrets(
"--flake", "--flake",
str(test_flake.path), str(test_flake.path),
"--pgp-key", "--pgp-key",
gpg_key, gpg_key.fingerprint,
"user2", "user2",
] ]
) )
@@ -783,7 +738,7 @@ def test_secrets_key_generate_gpg(
test_flake: FlakeForTest, test_flake: FlakeForTest,
capture_output: CaptureOutput, capture_output: CaptureOutput,
monkeypatch: pytest.MonkeyPatch, monkeypatch: pytest.MonkeyPatch,
gpg_key: str, gpg_key: GpgKey,
) -> None: ) -> None:
with use_gpg_key(gpg_key, monkeypatch): with use_gpg_key(gpg_key, monkeypatch):
# Make sure clan secrets key generate recognizes # Make sure clan secrets key generate recognizes
@@ -805,7 +760,7 @@ def test_secrets_key_generate_gpg(
cli.run(["secrets", "key", "show", "--flake", str(test_flake.path)]) cli.run(["secrets", "key", "show", "--flake", str(test_flake.path)])
key = json.loads(output.out) key = json.loads(output.out)
assert key["type"] == "pgp" assert key["type"] == "pgp"
assert key["publickey"] == gpg_key assert key["publickey"] == gpg_key.fingerprint
# Add testuser with the key that was (not) generated for the clan: # Add testuser with the key that was (not) generated for the clan:
cli.run( cli.run(
@@ -816,7 +771,7 @@ def test_secrets_key_generate_gpg(
"--flake", "--flake",
str(test_flake.path), str(test_flake.path),
"--pgp-key", "--pgp-key",
gpg_key, gpg_key.fingerprint,
"testuser", "testuser",
] ]
) )
@@ -833,7 +788,7 @@ def test_secrets_key_generate_gpg(
) )
key = json.loads(output.out) key = json.loads(output.out)
assert key["type"] == "pgp" assert key["type"] == "pgp"
assert key["publickey"] == gpg_key assert key["publickey"] == gpg_key.fingerprint
monkeypatch.setenv("SOPS_NIX_SECRET", "secret-value") monkeypatch.setenv("SOPS_NIX_SECRET", "secret-value")
cli.run(["secrets", "set", "--flake", str(test_flake.path), "secret-name"]) cli.run(["secrets", "set", "--flake", str(test_flake.path), "secret-name"])

View File

@@ -1,4 +1,5 @@
import contextlib import contextlib
import sys
from collections.abc import Generator from collections.abc import Generator
from typing import Any, NamedTuple from typing import Any, NamedTuple
@@ -127,6 +128,10 @@ def test_parse_ssh_options() -> None:
assert host.ssh_options["StrictHostKeyChecking"] == "yes" assert host.ssh_options["StrictHostKeyChecking"] == "yes"
is_darwin = sys.platform == "darwin"
@pytest.mark.skipif(is_darwin, reason="preload doesn't work on darwin")
def test_run(hosts: list[Host], runtime: AsyncRuntime) -> None: def test_run(hosts: list[Host], runtime: AsyncRuntime) -> None:
for host in hosts: for host in hosts:
proc = runtime.async_run( proc = runtime.async_run(
@@ -135,6 +140,7 @@ def test_run(hosts: list[Host], runtime: AsyncRuntime) -> None:
assert proc.wait().result.stdout == "hello\n" assert proc.wait().result.stdout == "hello\n"
@pytest.mark.skipif(is_darwin, reason="preload doesn't work on darwin")
def test_run_environment(hosts: list[Host], runtime: AsyncRuntime) -> None: def test_run_environment(hosts: list[Host], runtime: AsyncRuntime) -> None:
for host in hosts: for host in hosts:
proc = runtime.async_run( proc = runtime.async_run(
@@ -157,6 +163,7 @@ def test_run_environment(hosts: list[Host], runtime: AsyncRuntime) -> None:
assert "env_var=true" in p2.wait().result.stdout assert "env_var=true" in p2.wait().result.stdout
@pytest.mark.skipif(is_darwin, reason="preload doesn't work on darwin")
def test_run_no_shell(hosts: list[Host], runtime: AsyncRuntime) -> None: def test_run_no_shell(hosts: list[Host], runtime: AsyncRuntime) -> None:
for host in hosts: for host in hosts:
proc = runtime.async_run( proc = runtime.async_run(
@@ -165,6 +172,7 @@ def test_run_no_shell(hosts: list[Host], runtime: AsyncRuntime) -> None:
assert proc.wait().result.stdout == "hello\n" assert proc.wait().result.stdout == "hello\n"
@pytest.mark.skipif(is_darwin, reason="preload doesn't work on darwin")
def test_run_function(hosts: list[Host], runtime: AsyncRuntime) -> None: def test_run_function(hosts: list[Host], runtime: AsyncRuntime) -> None:
def some_func(h: Host) -> bool: def some_func(h: Host) -> bool:
p = h.run(["echo", "hello"]) p = h.run(["echo", "hello"])
@@ -175,6 +183,7 @@ def test_run_function(hosts: list[Host], runtime: AsyncRuntime) -> None:
assert proc.wait().result assert proc.wait().result
@pytest.mark.skipif(is_darwin, reason="preload doesn't work on darwin")
def test_timeout(hosts: list[Host], runtime: AsyncRuntime) -> None: def test_timeout(hosts: list[Host], runtime: AsyncRuntime) -> None:
for host in hosts: for host in hosts:
proc = runtime.async_run( proc = runtime.async_run(
@@ -184,6 +193,7 @@ def test_timeout(hosts: list[Host], runtime: AsyncRuntime) -> None:
assert isinstance(error, ClanCmdTimeoutError) assert isinstance(error, ClanCmdTimeoutError)
@pytest.mark.skipif(is_darwin, reason="preload doesn't work on darwin")
def test_run_exception(hosts: list[Host], runtime: AsyncRuntime) -> None: def test_run_exception(hosts: list[Host], runtime: AsyncRuntime) -> None:
for host in hosts: for host in hosts:
proc = runtime.async_run( proc = runtime.async_run(
@@ -203,6 +213,7 @@ def test_run_exception(hosts: list[Host], runtime: AsyncRuntime) -> None:
raise AssertionError(msg) raise AssertionError(msg)
@pytest.mark.skipif(is_darwin, reason="preload doesn't work on darwin")
def test_run_function_exception(hosts: list[Host], runtime: AsyncRuntime) -> None: def test_run_function_exception(hosts: list[Host], runtime: AsyncRuntime) -> None:
def some_func(h: Host) -> CmdOut: def some_func(h: Host) -> CmdOut:
return h.run_local(["exit 1"], RunOpts(shell=True)) return h.run_local(["exit 1"], RunOpts(shell=True))