tenzir-test 0.12.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,56 @@
1
+ """Test execution utilities for the Tenzir ecosystem."""
2
+
3
+ from importlib.metadata import PackageNotFoundError, version
4
+
5
+ from . import run
6
+ from .run import ExecutionResult, HarnessError, ProjectResult, ensure_settings, execute
7
+ from .config import Settings, discover_settings
8
+ from .fixtures import (
9
+ Executor,
10
+ FixtureHandle,
11
+ FixtureSelection,
12
+ FixtureController,
13
+ activate,
14
+ acquire_fixture,
15
+ fixture,
16
+ has,
17
+ register,
18
+ fixtures_api,
19
+ require,
20
+ )
21
+
22
+ fixtures = fixtures_api
23
+
24
+ __all__ = [
25
+ "__version__",
26
+ "Executor",
27
+ "FixtureHandle",
28
+ "FixtureSelection",
29
+ "FixtureController",
30
+ "activate",
31
+ "acquire_fixture",
32
+ "fixture",
33
+ "fixtures",
34
+ "Settings",
35
+ "discover_settings",
36
+ "ensure_settings",
37
+ "execute",
38
+ "has",
39
+ "register",
40
+ "require",
41
+ "ExecutionResult",
42
+ "ProjectResult",
43
+ "HarnessError",
44
+ "run",
45
+ ]
46
+
47
+
48
+ def _get_version() -> str:
49
+ """Return the installed package version or a development placeholder."""
50
+ try:
51
+ return version("tenzir-test")
52
+ except PackageNotFoundError: # pragma: no cover - missing metadata during dev
53
+ return "0.0.0"
54
+
55
+
56
+ __version__ = _get_version()
@@ -0,0 +1,93 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import os
5
+ import runpy
6
+ import sys
7
+ from contextvars import Token
8
+ from pathlib import Path
9
+ from typing import Any, Dict, cast
10
+
11
+ from . import fixtures as _fixtures
12
+ from .fixtures import acquire_fixture as _acquire_fixture
13
+
14
+
15
+ _EXPORTED_HELPERS: dict[str, Any] = {
16
+ "fixtures": _fixtures.fixtures_api,
17
+ "fixtures_module": _fixtures,
18
+ "fixtures_selection": _fixtures.fixtures,
19
+ "activate": _fixtures.activate,
20
+ "require": _fixtures.require,
21
+ "has": _fixtures.has,
22
+ "FixtureContext": _fixtures.FixtureContext,
23
+ "FixtureHandle": _fixtures.FixtureHandle,
24
+ "FixtureSelection": _fixtures.FixtureSelection,
25
+ "FixtureController": _fixtures.FixtureController,
26
+ "Executor": _fixtures.Executor,
27
+ }
28
+
29
+ _EXPORTED_HELPERS["acquire_fixture"] = _acquire_fixture
30
+
31
+
32
+ def _build_init_globals() -> Dict[str, Any]:
33
+ init_globals: Dict[str, Any] = {}
34
+ init_globals.update(_EXPORTED_HELPERS)
35
+ return init_globals
36
+
37
+
38
+ def _push_context_from_env() -> Token[Any] | None:
39
+ payload_raw = os.environ.get("TENZIR_PYTHON_FIXTURE_CONTEXT")
40
+ if not payload_raw:
41
+ return None
42
+ try:
43
+ payload = json.loads(payload_raw)
44
+ except json.JSONDecodeError:
45
+ return None
46
+ test_path = Path(payload.get("test", ""))
47
+ config = cast(Dict[str, Any], payload.get("config", {}))
48
+ coverage = bool(payload.get("coverage", False))
49
+ config_args_raw = payload.get("config_args", ())
50
+ config_args = tuple(str(arg) for arg in config_args_raw)
51
+ env = dict(os.environ)
52
+ context = _fixtures.FixtureContext(
53
+ test=test_path,
54
+ config=config,
55
+ coverage=coverage,
56
+ env=env,
57
+ config_args=config_args,
58
+ tenzir_binary=env.get("TENZIR_PYTHON_FIXTURE_BINARY") or env.get("TENZIR_BINARY"),
59
+ tenzir_node_binary=env.get("TENZIR_NODE_BINARY"),
60
+ )
61
+ token = _fixtures.push_context(context)
62
+ return cast(Token[Any], token)
63
+
64
+
65
+ def _run_script(script_path: Path, args: list[str]) -> None:
66
+ sys.argv = [str(script_path), *args]
67
+ init_globals = _build_init_globals()
68
+ token = _push_context_from_env()
69
+ try:
70
+ runpy.run_path(str(script_path), run_name="__main__", init_globals=init_globals)
71
+ except KeyboardInterrupt: # pragma: no cover - forwarded to harness
72
+ raise SystemExit(130)
73
+ finally:
74
+ if token is not None:
75
+ _fixtures.pop_context(token)
76
+
77
+
78
+ def main(argv: list[str] | None = None) -> None:
79
+ args = list(sys.argv if argv is None else argv)
80
+ if len(args) < 2:
81
+ raise SystemExit("usage: python -m tenzir_test._python_runner <path> [args...]")
82
+
83
+ script_arg = args[1]
84
+ script_path = Path(script_arg)
85
+ if not script_path.exists():
86
+ raise SystemExit(f"python runner could not find test file: {script_arg}")
87
+
88
+ extra_args = args[2:]
89
+ _run_script(script_path, extra_args)
90
+
91
+
92
+ if __name__ == "__main__":
93
+ main()
tenzir_test/checks.py ADDED
@@ -0,0 +1,31 @@
1
+ from __future__ import annotations
2
+
3
+ import shlex
4
+ import subprocess
5
+ from typing import Sequence
6
+
7
+ _COMMANDS: Sequence[Sequence[str]] = (
8
+ ("ruff", "check"),
9
+ ("ruff", "format", "--check"),
10
+ ("mypy",),
11
+ ("pytest",),
12
+ ("uv", "build"),
13
+ )
14
+
15
+
16
+ def _run(command: Sequence[str]) -> None:
17
+ printable = " ".join(shlex.quote(part) for part in command)
18
+ print(f"> {printable}")
19
+ result = subprocess.run(command, check=False)
20
+ if result.returncode != 0:
21
+ raise SystemExit(result.returncode)
22
+
23
+
24
+ def main() -> int:
25
+ for command in _COMMANDS:
26
+ _run(command)
27
+ return 0
28
+
29
+
30
+ if __name__ == "__main__":
31
+ raise SystemExit(main())
tenzir_test/cli.py ADDED
@@ -0,0 +1,216 @@
1
+ """Command line entry point for the tenzir-test runner."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Sequence
6
+ from pathlib import Path
7
+ import sys
8
+
9
+ import click
10
+
11
+ from . import __version__, run as runtime
12
+
13
+
14
+ def _normalize_exit_code(value: object) -> int:
15
+ """Cast arbitrary exit codes to integers."""
16
+
17
+ if value is None:
18
+ return 0
19
+ if isinstance(value, int):
20
+ return value
21
+ return 1
22
+
23
+
24
+ @click.command(
25
+ context_settings={"help_option_names": ["-h", "--help"]},
26
+ )
27
+ @click.version_option(
28
+ __version__,
29
+ "-V",
30
+ "--version",
31
+ prog_name="tenzir-test",
32
+ message="%(prog)s %(version)s",
33
+ )
34
+ @click.option(
35
+ "root",
36
+ "--root",
37
+ type=click.Path(
38
+ path_type=Path, file_okay=False, dir_okay=True, writable=False, resolve_path=False
39
+ ),
40
+ help="Project root to scan for tests.",
41
+ )
42
+ @click.option(
43
+ "tenzir_binary",
44
+ "--tenzir-binary",
45
+ type=click.Path(path_type=Path, dir_okay=False, writable=False, resolve_path=False),
46
+ help="Path to the tenzir executable.",
47
+ )
48
+ @click.option(
49
+ "tenzir_node_binary",
50
+ "--tenzir-node-binary",
51
+ type=click.Path(path_type=Path, dir_okay=False, writable=False, resolve_path=False),
52
+ help="Path to the tenzir-node executable.",
53
+ )
54
+ @click.argument(
55
+ "tests",
56
+ nargs=-1,
57
+ type=click.Path(path_type=Path, resolve_path=False),
58
+ )
59
+ @click.option("-u", "--update", is_flag=True, help="Update reference outputs.")
60
+ @click.option(
61
+ "-d",
62
+ "--debug",
63
+ is_flag=True,
64
+ help="Enable debug logging.",
65
+ )
66
+ @click.option("--purge", is_flag=True, help="Delete cached runner artifacts and exit.")
67
+ @click.option(
68
+ "--coverage",
69
+ is_flag=True,
70
+ help="Enable code coverage collection (increases timeouts by 5x).",
71
+ )
72
+ @click.option(
73
+ "--coverage-source-dir",
74
+ type=click.Path(path_type=Path, resolve_path=False),
75
+ help="Source directory for coverage path mapping (defaults to current directory).",
76
+ )
77
+ @click.option(
78
+ "--runner-summary",
79
+ is_flag=True,
80
+ help="Include per-runner statistics in the summary table.",
81
+ )
82
+ @click.option(
83
+ "--fixture-summary",
84
+ is_flag=True,
85
+ help="Include per-fixture statistics in the summary table.",
86
+ )
87
+ @click.option(
88
+ "--summary",
89
+ "show_summary",
90
+ is_flag=True,
91
+ help="Show an aggregate table and detailed failure summary after execution.",
92
+ )
93
+ @click.option(
94
+ "--diff/--no-diff",
95
+ "show_diff_output",
96
+ default=True,
97
+ help="Show unified diffs when expectations differ.",
98
+ )
99
+ @click.option(
100
+ "--diff-stat/--no-diff-stat",
101
+ "show_diff_stat",
102
+ default=True,
103
+ help="Include per-file diff statistics and change counters when expectations differ.",
104
+ )
105
+ @click.option(
106
+ "-k",
107
+ "--keep",
108
+ "keep_tmp_dirs",
109
+ is_flag=True,
110
+ help="Preserve per-test temporary directories instead of deleting them.",
111
+ )
112
+ @click.option(
113
+ "-j",
114
+ "--jobs",
115
+ type=click.IntRange(min=1),
116
+ default=runtime.get_default_jobs(),
117
+ show_default=True,
118
+ metavar="N",
119
+ help="Number of parallel worker threads.",
120
+ )
121
+ @click.option(
122
+ "-p",
123
+ "--passthrough",
124
+ is_flag=True,
125
+ help="Stream raw test output directly to the terminal.",
126
+ )
127
+ @click.option(
128
+ "-a",
129
+ "--all-projects",
130
+ is_flag=True,
131
+ help="Run the root project alongside any selected satellites.",
132
+ )
133
+ @click.pass_context
134
+ def cli(
135
+ ctx: click.Context,
136
+ *,
137
+ root: Path | None,
138
+ tenzir_binary: Path | None,
139
+ tenzir_node_binary: Path | None,
140
+ tests: tuple[Path, ...],
141
+ update: bool,
142
+ debug: bool,
143
+ purge: bool,
144
+ coverage: bool,
145
+ coverage_source_dir: Path | None,
146
+ runner_summary: bool,
147
+ fixture_summary: bool,
148
+ show_summary: bool,
149
+ show_diff_output: bool,
150
+ show_diff_stat: bool,
151
+ keep_tmp_dirs: bool,
152
+ jobs: int,
153
+ passthrough: bool,
154
+ all_projects: bool,
155
+ ) -> int:
156
+ """Execute tenzir-test scenarios."""
157
+
158
+ jobs_source = ctx.get_parameter_source("jobs")
159
+ jobs_overridden = jobs_source is not click.core.ParameterSource.DEFAULT
160
+
161
+ try:
162
+ result = runtime.run_cli(
163
+ root=root,
164
+ tenzir_binary=tenzir_binary,
165
+ tenzir_node_binary=tenzir_node_binary,
166
+ tests=list(tests),
167
+ update=update,
168
+ debug=debug,
169
+ purge=purge,
170
+ coverage=coverage,
171
+ coverage_source_dir=coverage_source_dir,
172
+ runner_summary=runner_summary,
173
+ fixture_summary=fixture_summary,
174
+ show_summary=show_summary,
175
+ show_diff_output=show_diff_output,
176
+ show_diff_stat=show_diff_stat,
177
+ keep_tmp_dirs=keep_tmp_dirs,
178
+ jobs=jobs,
179
+ passthrough=passthrough,
180
+ jobs_overridden=jobs_overridden,
181
+ all_projects=all_projects,
182
+ )
183
+ except runtime.HarnessError as exc:
184
+ if exc.show_message and exc.args:
185
+ raise click.ClickException(str(exc)) from exc
186
+ return exc.exit_code
187
+ return result.exit_code
188
+
189
+
190
+ def main(argv: Sequence[str] | None = None) -> int:
191
+ """Run the Click command and translate Click exits to integer codes."""
192
+
193
+ command_main = getattr(cli, "main")
194
+ previous_color_mode = runtime.get_color_mode()
195
+ runtime.set_color_mode(runtime.ColorMode.AUTO)
196
+ try:
197
+ result = command_main(
198
+ args=list(argv) if argv is not None else None,
199
+ standalone_mode=False,
200
+ )
201
+ except click.exceptions.Exit as exc: # pragma: no cover - passthrough CLI termination
202
+ return _normalize_exit_code(exc.exit_code)
203
+ except click.exceptions.ClickException as exc:
204
+ exc.show(file=sys.stderr)
205
+ exit_code = getattr(exc, "exit_code", None)
206
+ return _normalize_exit_code(exit_code)
207
+ except SystemExit as exc: # pragma: no cover - propagate runner exits
208
+ return _normalize_exit_code(exc.code)
209
+ else:
210
+ return _normalize_exit_code(result)
211
+ finally:
212
+ runtime.set_color_mode(previous_color_mode)
213
+
214
+
215
+ if __name__ == "__main__":
216
+ raise SystemExit(main())
tenzir_test/config.py ADDED
@@ -0,0 +1,57 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ import shutil
5
+ from dataclasses import dataclass
6
+ from pathlib import Path
7
+ from typing import Mapping
8
+
9
+
10
+ @dataclass(slots=True)
11
+ class Settings:
12
+ """Configuration values steering how the harness discovers binaries and data."""
13
+
14
+ root: Path
15
+ tenzir_binary: str | None
16
+ tenzir_node_binary: str | None
17
+
18
+ @property
19
+ def inputs_dir(self) -> Path:
20
+ direct = self.root / "inputs"
21
+ if direct.exists():
22
+ return direct
23
+ nested = self.root / "tests" / "inputs"
24
+ if nested.exists():
25
+ return nested
26
+ return direct
27
+
28
+
29
+ def _coerce_binary(value: str | os.PathLike[str] | None) -> str | None:
30
+ if value is None:
31
+ return None
32
+ return str(value)
33
+
34
+
35
+ def discover_settings(
36
+ *,
37
+ root: Path | None = None,
38
+ tenzir_binary: str | os.PathLike[str] | None = None,
39
+ tenzir_node_binary: str | os.PathLike[str] | None = None,
40
+ env: Mapping[str, str] | None = None,
41
+ ) -> Settings:
42
+ """Produce harness settings by combining CLI overrides with environment defaults."""
43
+
44
+ environment = dict(env or os.environ)
45
+
46
+ chosen_root = root or environment.get("TENZIR_TEST_ROOT") or Path.cwd()
47
+ root_path = Path(chosen_root).resolve()
48
+
49
+ binary_cli = _coerce_binary(tenzir_binary)
50
+ binary_env = environment.get("TENZIR_BINARY")
51
+ tenzir_path = binary_cli or binary_env or shutil.which("tenzir")
52
+
53
+ node_cli = _coerce_binary(tenzir_node_binary)
54
+ node_env = environment.get("TENZIR_NODE_BINARY")
55
+ node_path = node_cli or node_env or shutil.which("tenzir-node")
56
+
57
+ return Settings(root=root_path, tenzir_binary=tenzir_path, tenzir_node_binary=node_path)
@@ -0,0 +1,5 @@
1
+ """Engine utilities for tenzir-test."""
2
+
3
+ from .worker import Summary, Worker
4
+
5
+ __all__ = ["Summary", "Worker"]
@@ -0,0 +1,36 @@
1
+ from __future__ import annotations
2
+
3
+ from tenzir_test import run
4
+
5
+ stdout_lock = run.stdout_lock
6
+
7
+ update_registry_metadata = run.update_registry_metadata
8
+ get_allowed_extensions = run.get_allowed_extensions
9
+ get_test_env_and_config_args = run.get_test_env_and_config_args
10
+ parse_test_config = run.parse_test_config
11
+ report_failure = run.report_failure
12
+ handle_skip = run.handle_skip
13
+ run_simple_test = run.run_simple_test
14
+ print_diff = run.print_diff
15
+ success = run.success
16
+ fail = run.fail
17
+ run_subprocess = run.run_subprocess
18
+ is_passthrough_enabled = run.is_passthrough_enabled
19
+ get_harness_mode = run.get_harness_mode
20
+
21
+ __all__ = [
22
+ "stdout_lock",
23
+ "update_registry_metadata",
24
+ "get_allowed_extensions",
25
+ "get_test_env_and_config_args",
26
+ "parse_test_config",
27
+ "report_failure",
28
+ "handle_skip",
29
+ "run_simple_test",
30
+ "print_diff",
31
+ "success",
32
+ "fail",
33
+ "run_subprocess",
34
+ "is_passthrough_enabled",
35
+ "get_harness_mode",
36
+ ]
@@ -0,0 +1,30 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Iterable
4
+
5
+ from tenzir_test import runners as runner_module
6
+ from tenzir_test.runners import Runner
7
+
8
+
9
+ def iter_runners() -> Iterable[Runner]:
10
+ return runner_module.iter_runners()
11
+
12
+
13
+ def get_runner(name: str) -> Runner:
14
+ return runner_module.runner_map()[name]
15
+
16
+
17
+ def has_runner(name: str) -> bool:
18
+ return name in runner_module.runner_map()
19
+
20
+
21
+ def allowed_extensions() -> set[str]:
22
+ return runner_module.allowed_extensions()
23
+
24
+
25
+ __all__ = [
26
+ "iter_runners",
27
+ "get_runner",
28
+ "has_runner",
29
+ "allowed_extensions",
30
+ ]
@@ -0,0 +1,43 @@
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path
4
+ from typing import Optional
5
+
6
+ from ..config import Settings
7
+ from tenzir_test import run
8
+
9
+ TENZIR_BINARY: Optional[str] = run.TENZIR_BINARY
10
+ TENZIR_NODE_BINARY: Optional[str] = run.TENZIR_NODE_BINARY
11
+ ROOT: Path = run.ROOT
12
+ INPUTS_DIR: Path = run.INPUTS_DIR
13
+
14
+
15
+ def apply_settings(settings: Settings) -> None:
16
+ run.apply_settings(settings)
17
+ _refresh()
18
+
19
+
20
+ def _refresh() -> None:
21
+ global TENZIR_BINARY, TENZIR_NODE_BINARY, ROOT, INPUTS_DIR
22
+ TENZIR_BINARY = run.TENZIR_BINARY
23
+ TENZIR_NODE_BINARY = run.TENZIR_NODE_BINARY
24
+ ROOT = run.ROOT
25
+ INPUTS_DIR = run.INPUTS_DIR
26
+
27
+
28
+ # Ensure module state reflects current run module state by default.
29
+ _refresh()
30
+
31
+
32
+ def refresh() -> None:
33
+ _refresh()
34
+
35
+
36
+ __all__ = [
37
+ "TENZIR_BINARY",
38
+ "TENZIR_NODE_BINARY",
39
+ "ROOT",
40
+ "INPUTS_DIR",
41
+ "apply_settings",
42
+ "refresh",
43
+ ]
@@ -0,0 +1,8 @@
1
+ from __future__ import annotations
2
+
3
+ from tenzir_test import run
4
+
5
+ Summary = run.Summary
6
+ Worker = run.Worker
7
+
8
+ __all__ = ["Summary", "Worker"]