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,164 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Sequence
4
+ from pathlib import Path
5
+ from typing import Callable
6
+
7
+ from .runner import Runner
8
+ from .ext_runner import ExtRunner
9
+ from .tql_runner import TqlRunner
10
+ from .diff_runner import DiffRunner
11
+ from .shell_runner import ShellRunner
12
+ from .custom_python_fixture_runner import CustomPythonFixture
13
+ from .tenzir_runner import TenzirRunner
14
+
15
+ _REGISTERED: dict[str, Runner] = {}
16
+ _ALIASES: dict[str, str] = {}
17
+ RUNNERS: list[Runner] = []
18
+ RUNNERS_BY_NAME: dict[str, Runner] = {}
19
+
20
+
21
+ def _refresh_exports() -> None:
22
+ global RUNNERS, RUNNERS_BY_NAME
23
+ RUNNERS = list(_REGISTERED.values())
24
+ mapping: dict[str, Runner] = dict(_REGISTERED)
25
+ for alias, target in _ALIASES.items():
26
+ runner = mapping.get(target)
27
+ if runner is None:
28
+ continue
29
+ mapping[alias] = runner
30
+ RUNNERS_BY_NAME = mapping
31
+
32
+
33
+ def _set_alias(alias: str, target: str, *, replace: bool) -> None:
34
+ if target not in _REGISTERED:
35
+ raise ValueError(f"runner '{target}' is not registered")
36
+ if alias in _REGISTERED and alias != target:
37
+ raise ValueError(f"cannot alias '{alias}': name already registered")
38
+ if not replace and alias in _ALIASES and _ALIASES[alias] != target:
39
+ raise ValueError(f"alias '{alias}' already targets '{_ALIASES[alias]}'")
40
+ _ALIASES[alias] = target
41
+
42
+
43
+ def register(
44
+ runner: Runner,
45
+ *,
46
+ replace: bool = False,
47
+ aliases: Sequence[str] | None = None,
48
+ ) -> Runner:
49
+ name = runner.name
50
+ if not replace and name in _REGISTERED and _REGISTERED[name] is not runner:
51
+ raise ValueError(f"runner '{name}' already registered")
52
+ _REGISTERED[name] = runner
53
+ if aliases:
54
+ for alias in aliases:
55
+ _set_alias(alias, name, replace=replace)
56
+ _refresh_exports()
57
+ return runner
58
+
59
+
60
+ def unregister(name: str) -> Runner:
61
+ try:
62
+ runner = _REGISTERED.pop(name)
63
+ except KeyError as exc: # pragma: no cover - defensive guard
64
+ raise ValueError(f"runner '{name}' is not registered") from exc
65
+ to_remove = [alias for alias, target in _ALIASES.items() if target == name]
66
+ for alias in to_remove:
67
+ _ALIASES.pop(alias, None)
68
+ _refresh_exports()
69
+ return runner
70
+
71
+
72
+ def register_alias(alias: str, target: str, *, replace: bool = False) -> None:
73
+ _set_alias(alias, target, replace=replace)
74
+ _refresh_exports()
75
+
76
+
77
+ RunnerFactory = Callable[[], Runner]
78
+
79
+
80
+ def startup(
81
+ *,
82
+ replace: bool = False,
83
+ aliases: Sequence[str] | None = None,
84
+ ) -> Callable[[RunnerFactory], RunnerFactory]:
85
+ def _decorator(factory: RunnerFactory) -> RunnerFactory:
86
+ runner = factory()
87
+ if not isinstance(runner, Runner):
88
+ raise TypeError("runner factory must return a Runner instance")
89
+ register(runner, replace=replace, aliases=aliases)
90
+ return factory
91
+
92
+ return _decorator
93
+
94
+
95
+ def iter_runners() -> tuple[Runner, ...]:
96
+ return tuple(RUNNERS)
97
+
98
+
99
+ def runner_map(*, include_aliases: bool = True) -> dict[str, Runner]:
100
+ if include_aliases:
101
+ return dict(RUNNERS_BY_NAME)
102
+ return dict(_REGISTERED)
103
+
104
+
105
+ def runner_names() -> set[str]:
106
+ return set(RUNNERS_BY_NAME.keys())
107
+
108
+
109
+ def allowed_extensions() -> set[str]:
110
+ extensions: set[str] = set()
111
+ for runner in RUNNERS:
112
+ ext = getattr(runner, "_ext", None)
113
+ if isinstance(ext, str):
114
+ extensions.add(ext)
115
+ return extensions
116
+
117
+
118
+ def get_runner_for_test(test_path: Path) -> Runner:
119
+ from tenzir_test import run as run_module
120
+
121
+ try:
122
+ config = run_module.parse_test_config(test_path)
123
+ except ValueError:
124
+ suffix = test_path.suffix.lower()
125
+ default_name = run_module.default_runner_for_suffix(suffix)
126
+ if not default_name:
127
+ raise
128
+ runner_name = default_name
129
+ else:
130
+ runner_value = config.get("runner")
131
+ if not isinstance(runner_value, str):
132
+ raise ValueError("Runner 'runner' must be a string")
133
+ runner_name = runner_value
134
+ if runner_name in RUNNERS_BY_NAME:
135
+ return RUNNERS_BY_NAME[runner_name]
136
+ raise ValueError(f"Runner '{runner_name}' not found - this is a bug")
137
+
138
+
139
+ register(ShellRunner())
140
+ register(CustomPythonFixture())
141
+ register(TenzirRunner())
142
+
143
+ _refresh_exports()
144
+
145
+ __all__ = [
146
+ "Runner",
147
+ "ExtRunner",
148
+ "TqlRunner",
149
+ "ShellRunner",
150
+ "CustomPythonFixture",
151
+ "TenzirRunner",
152
+ "DiffRunner",
153
+ "RUNNERS",
154
+ "RUNNERS_BY_NAME",
155
+ "register",
156
+ "unregister",
157
+ "register_alias",
158
+ "startup",
159
+ "iter_runners",
160
+ "runner_map",
161
+ "runner_names",
162
+ "allowed_extensions",
163
+ "get_runner_for_test",
164
+ ]
@@ -0,0 +1,17 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ from types import ModuleType
5
+
6
+
7
+ def get_run_module() -> ModuleType:
8
+ from tenzir_test import run as run_module
9
+
10
+ return run_module
11
+
12
+
13
+ def resolve_run_module_dir(run_mod: ModuleType) -> str:
14
+ module_path = getattr(run_mod, "__file__", None)
15
+ if not isinstance(module_path, str):
16
+ raise RuntimeError("tenzir_test.run module path is not available")
17
+ return os.path.dirname(os.path.realpath(module_path))
@@ -0,0 +1,171 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import os
5
+ import subprocess
6
+ import sys
7
+ import typing
8
+ from pathlib import Path
9
+
10
+ from tenzir_test import fixtures as fixture_api
11
+
12
+ from ._utils import get_run_module, resolve_run_module_dir
13
+ from .ext_runner import ExtRunner
14
+
15
+
16
+ def _jsonify_config(config: dict[str, typing.Any]) -> dict[str, typing.Any]:
17
+ def _convert(value: typing.Any) -> typing.Any:
18
+ if isinstance(value, tuple):
19
+ return [_convert(item) for item in value]
20
+ if isinstance(value, list):
21
+ return [_convert(item) for item in value]
22
+ if isinstance(value, dict):
23
+ return {str(k): _convert(v) for k, v in value.items()}
24
+ return value
25
+
26
+ return {str(key): _convert(val) for key, val in config.items()}
27
+
28
+
29
+ class CustomPythonFixture(ExtRunner):
30
+ def __init__(self) -> None:
31
+ super().__init__(name="python", ext="py")
32
+
33
+ def run(self, test: Path, update: bool, coverage: bool = False) -> bool:
34
+ run_mod = get_run_module()
35
+ test_config = run_mod.parse_test_config(test, coverage=coverage)
36
+ binary = run_mod.TENZIR_BINARY
37
+ if not binary:
38
+ raise RuntimeError("TENZIR_BINARY must be configured for python fixtures")
39
+ passthrough = run_mod.is_passthrough_enabled()
40
+ try:
41
+ cmd = [
42
+ sys.executable,
43
+ "-m",
44
+ "tenzir_test._python_runner",
45
+ str(test),
46
+ ]
47
+ inputs_override = typing.cast(str | None, test_config.get("inputs"))
48
+ env, _config_args = run_mod.get_test_env_and_config_args(test, inputs=inputs_override)
49
+ fixtures = typing.cast(tuple[str, ...], test_config.get("fixtures", tuple()))
50
+ node_requested = "node" in fixtures
51
+ timeout = typing.cast(int, test_config["timeout"])
52
+ fixture_context = fixture_api.FixtureContext(
53
+ test=test,
54
+ config=typing.cast(dict[str, typing.Any], test_config),
55
+ coverage=coverage,
56
+ env=env,
57
+ config_args=tuple(),
58
+ tenzir_binary=run_mod.TENZIR_BINARY,
59
+ tenzir_node_binary=run_mod.TENZIR_NODE_BINARY,
60
+ )
61
+ context_token = fixture_api.push_context(fixture_context)
62
+ pythonpath_entries: list[str] = []
63
+ project_root = getattr(run_mod, "ROOT", None)
64
+ project_root_path: Path | None = None
65
+ if isinstance(project_root, Path):
66
+ project_root_path = project_root
67
+ elif isinstance(project_root, str):
68
+ project_root_path = Path(project_root)
69
+
70
+ if project_root_path is not None:
71
+ pythonpath_entries.append(str(project_root_path))
72
+ tests_dir = project_root_path / "tests"
73
+ if tests_dir.is_dir():
74
+ pythonpath_entries.append(str(tests_dir))
75
+ pythonpath_entries.append(resolve_run_module_dir(run_mod))
76
+ existing_pythonpath = env.get("PYTHONPATH")
77
+ if existing_pythonpath:
78
+ pythonpath_entries.append(existing_pythonpath)
79
+
80
+ env["TENZIR_PYTHON_FIXTURE_CONTEXT"] = json.dumps(
81
+ {
82
+ "test": str(test),
83
+ "config": _jsonify_config(test_config),
84
+ "coverage": coverage,
85
+ "config_args": list(fixture_context.config_args),
86
+ }
87
+ )
88
+ try:
89
+ with fixture_api.activate(fixtures) as fixture_env:
90
+ env.update(fixture_env)
91
+ run_mod._apply_fixture_env(env, fixtures)
92
+ if node_requested:
93
+ endpoint = env.get("TENZIR_NODE_CLIENT_ENDPOINT")
94
+ if not endpoint:
95
+ raise RuntimeError(
96
+ "node fixture did not provide TENZIR_NODE_CLIENT_ENDPOINT"
97
+ )
98
+ else:
99
+ endpoint = None
100
+ new_pythonpath = os.pathsep.join(pythonpath_entries)
101
+ env["PYTHONPATH"] = new_pythonpath
102
+ env["TENZIR_NODE_CLIENT_BINARY"] = binary
103
+ env["TENZIR_NODE_CLIENT_TIMEOUT"] = str(timeout)
104
+ env.setdefault("TENZIR_PYTHON_FIXTURE_BINARY", binary)
105
+ env["TENZIR_PYTHON_FIXTURE_TIMEOUT"] = str(timeout)
106
+ if node_requested and endpoint:
107
+ env["TENZIR_PYTHON_FIXTURE_ENDPOINT"] = endpoint
108
+ completed = run_mod.run_subprocess(
109
+ cmd,
110
+ timeout=timeout,
111
+ env=env,
112
+ capture_output=not passthrough,
113
+ check=True,
114
+ )
115
+ ref_path = test.with_suffix(".txt")
116
+ if completed.returncode != 0:
117
+ run_mod.fail(test)
118
+ return False
119
+ if passthrough:
120
+ run_mod.success(test)
121
+ return True
122
+ output = (completed.stdout or b"") + (completed.stderr or b"")
123
+ if update:
124
+ with open(ref_path, "wb") as f:
125
+ f.write(output)
126
+ else:
127
+ if not ref_path.exists():
128
+ run_mod.report_failure(
129
+ test,
130
+ run_mod.format_failure_message(
131
+ f'Failed to find ref file: "{ref_path}"'
132
+ ),
133
+ )
134
+ return False
135
+ run_mod.log_comparison(test, ref_path, mode="comparing")
136
+ expected = ref_path.read_bytes()
137
+ if expected != output:
138
+ if run_mod.interrupt_requested():
139
+ run_mod.report_interrupted_test(test)
140
+ else:
141
+ run_mod.report_failure(test, "")
142
+ run_mod.print_diff(expected, output, ref_path)
143
+ return False
144
+ finally:
145
+ fixture_api.pop_context(context_token)
146
+ run_mod.cleanup_test_tmp_dir(env.get(run_mod.TEST_TMP_ENV_VAR))
147
+ except subprocess.TimeoutExpired:
148
+ run_mod.report_failure(
149
+ test,
150
+ run_mod.format_failure_message(f"python fixture hit {timeout}s timeout"),
151
+ )
152
+ return False
153
+ except subprocess.CalledProcessError as e:
154
+ suppressed = run_mod.should_suppress_failure_output()
155
+ if suppressed:
156
+ return False
157
+ with run_mod.stdout_lock:
158
+ run_mod.fail(test)
159
+ if not passthrough:
160
+ if e.stdout:
161
+ sys.stdout.buffer.write(e.stdout)
162
+ if e.output and e.output is not e.stdout:
163
+ sys.stdout.buffer.write(e.output)
164
+ if e.stderr:
165
+ sys.stdout.buffer.write(e.stderr)
166
+ return False
167
+ run_mod.success(test)
168
+ return True
169
+
170
+
171
+ __all__ = ["CustomPythonFixture"]
@@ -0,0 +1,139 @@
1
+ from __future__ import annotations
2
+
3
+ import difflib
4
+ import os
5
+ import typing
6
+ from pathlib import Path
7
+
8
+ from tenzir_test import fixtures as fixture_api
9
+
10
+ from ._utils import get_run_module
11
+ from .tql_runner import TqlRunner
12
+
13
+
14
+ class DiffRunner(TqlRunner):
15
+ def __init__(self, *, a: str, b: str, name: str) -> None:
16
+ super().__init__(name=name)
17
+ self._a = a
18
+ self._b = b
19
+
20
+ def run(self, test: Path, update: bool, coverage: bool = False) -> bool | str:
21
+ run_mod = get_run_module()
22
+ test_config = run_mod.parse_test_config(test, coverage=coverage)
23
+ skip_value = test_config.get("skip")
24
+ if isinstance(skip_value, str):
25
+ return typing.cast(
26
+ bool | str,
27
+ run_mod.handle_skip(
28
+ skip_value,
29
+ test,
30
+ update=update,
31
+ output_ext=self.output_ext,
32
+ ),
33
+ )
34
+
35
+ inputs_override = typing.cast(str | None, test_config.get("inputs"))
36
+ env, config_args = run_mod.get_test_env_and_config_args(test, inputs=inputs_override)
37
+ fixtures = typing.cast(tuple[str, ...], test_config.get("fixtures", tuple()))
38
+ node_requested = "node" in fixtures
39
+ timeout = typing.cast(int, test_config["timeout"])
40
+
41
+ context_token = fixture_api.push_context(
42
+ fixture_api.FixtureContext(
43
+ test=test,
44
+ config=typing.cast(dict[str, typing.Any], test_config),
45
+ coverage=coverage,
46
+ env=env,
47
+ config_args=tuple(config_args),
48
+ tenzir_binary=run_mod.TENZIR_BINARY,
49
+ tenzir_node_binary=run_mod.TENZIR_NODE_BINARY,
50
+ )
51
+ )
52
+ try:
53
+ with fixture_api.activate(fixtures) as fixture_env:
54
+ env.update(fixture_env)
55
+ run_mod._apply_fixture_env(env, fixtures)
56
+
57
+ node_args: list[str] = []
58
+ if node_requested:
59
+ endpoint = env.get("TENZIR_NODE_CLIENT_ENDPOINT")
60
+ if not endpoint:
61
+ raise RuntimeError(
62
+ "node fixture did not provide TENZIR_NODE_CLIENT_ENDPOINT"
63
+ )
64
+ node_args.append(f"--endpoint={endpoint}")
65
+
66
+ binary = run_mod.TENZIR_BINARY
67
+ if not binary:
68
+ raise RuntimeError("TENZIR_BINARY must be configured for diff runners")
69
+ base_cmd: list[str] = [binary, *config_args]
70
+
71
+ if coverage:
72
+ coverage_dir = env.get(
73
+ "CMAKE_COVERAGE_OUTPUT_DIRECTORY",
74
+ os.path.join(os.getcwd(), "coverage"),
75
+ )
76
+ source_dir = env.get("COVERAGE_SOURCE_DIR", os.getcwd())
77
+ os.makedirs(coverage_dir, exist_ok=True)
78
+ env["COVERAGE_SOURCE_DIR"] = source_dir
79
+ env["LLVM_PROFILE_FILE"] = os.path.join(
80
+ coverage_dir, f"{test.stem}-unopt-%p.profraw"
81
+ )
82
+
83
+ unoptimized = run_mod.run_subprocess(
84
+ [*base_cmd, self._a, *node_args, "-f", str(test)],
85
+ timeout=timeout,
86
+ env=env,
87
+ capture_output=True,
88
+ check=False,
89
+ force_capture=True,
90
+ )
91
+
92
+ if coverage:
93
+ env["LLVM_PROFILE_FILE"] = os.path.join(
94
+ coverage_dir, f"{test.stem}-opt-%p.profraw"
95
+ )
96
+
97
+ optimized = run_mod.run_subprocess(
98
+ [*base_cmd, self._b, *node_args, "-f", str(test)],
99
+ timeout=timeout,
100
+ env=env,
101
+ capture_output=True,
102
+ check=False,
103
+ force_capture=True,
104
+ )
105
+ finally:
106
+ fixture_api.pop_context(context_token)
107
+ run_mod.cleanup_test_tmp_dir(env.get(run_mod.TEST_TMP_ENV_VAR))
108
+
109
+ diff_chunks = list(
110
+ difflib.diff_bytes(
111
+ difflib.unified_diff,
112
+ unoptimized.stdout.splitlines(keepends=True),
113
+ optimized.stdout.splitlines(keepends=True),
114
+ n=2**31 - 1,
115
+ )
116
+ )[3:]
117
+ if diff_chunks:
118
+ diff_bytes = b"".join(diff_chunks)
119
+ else:
120
+ diff_bytes = b"".join(
121
+ b" " + line for line in unoptimized.stdout.splitlines(keepends=True)
122
+ )
123
+ ref_path = test.with_suffix(".diff")
124
+ if update:
125
+ ref_path.write_bytes(diff_bytes)
126
+ else:
127
+ expected = ref_path.read_bytes()
128
+ if diff_bytes != expected:
129
+ if run_mod.interrupt_requested():
130
+ run_mod.report_interrupted_test(test)
131
+ else:
132
+ run_mod.report_failure(test, "")
133
+ run_mod.print_diff(expected, diff_bytes, ref_path)
134
+ return False
135
+ run_mod.success(test)
136
+ return True
137
+
138
+
139
+ __all__ = ["DiffRunner"]
@@ -0,0 +1,28 @@
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path
4
+
5
+ from ._utils import get_run_module
6
+ from .runner import Runner
7
+
8
+
9
+ class ExtRunner(Runner):
10
+ def __init__(self, *, name: str, ext: str) -> None:
11
+ super().__init__(name=name)
12
+ self._ext = ext
13
+
14
+ def collect_tests(self, path: Path) -> set[tuple[Runner, Path]]:
15
+ return self.collect_with_ext(path, self._ext)
16
+
17
+ def purge(self) -> None:
18
+ run_mod = get_run_module()
19
+ purge_base = run_mod.ROOT / self._name
20
+ if not purge_base.exists():
21
+ return
22
+ for p in purge_base.rglob("*"):
23
+ if p.is_dir() or p.suffix == f".{self._ext}":
24
+ continue
25
+ p.unlink()
26
+
27
+
28
+ __all__ = ["ExtRunner"]
@@ -0,0 +1,38 @@
1
+ from __future__ import annotations
2
+
3
+ from abc import ABC, abstractmethod
4
+ from pathlib import Path
5
+
6
+
7
+ class Runner(ABC):
8
+ def __init__(self, *, name: str) -> None:
9
+ self._name = name
10
+
11
+ @property
12
+ def name(self) -> str:
13
+ return self._name
14
+
15
+ def collect_with_ext(self, path: Path, ext: str) -> set[tuple[Runner, Path]]:
16
+ todo: set[tuple[Runner, Path]] = set()
17
+ if path.is_file():
18
+ if path.suffix == f".{ext}":
19
+ todo.add((self, path))
20
+ return todo
21
+ for test in path.glob(f"**/*.{ext}"):
22
+ todo.add((self, test))
23
+ return todo
24
+
25
+ @abstractmethod
26
+ def collect_tests(self, path: Path) -> set[tuple[Runner, Path]]:
27
+ raise NotImplementedError
28
+
29
+ @abstractmethod
30
+ def purge(self) -> None:
31
+ raise NotImplementedError
32
+
33
+ @abstractmethod
34
+ def run(self, test: Path, update: bool, coverage: bool = False) -> bool | str:
35
+ raise NotImplementedError
36
+
37
+
38
+ __all__ = ["Runner"]