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.
- tenzir_test/__init__.py +56 -0
- tenzir_test/_python_runner.py +93 -0
- tenzir_test/checks.py +31 -0
- tenzir_test/cli.py +216 -0
- tenzir_test/config.py +57 -0
- tenzir_test/engine/__init__.py +5 -0
- tenzir_test/engine/operations.py +36 -0
- tenzir_test/engine/registry.py +30 -0
- tenzir_test/engine/state.py +43 -0
- tenzir_test/engine/worker.py +8 -0
- tenzir_test/fixtures/__init__.py +614 -0
- tenzir_test/fixtures/node.py +257 -0
- tenzir_test/packages.py +33 -0
- tenzir_test/py.typed +0 -0
- tenzir_test/run.py +3678 -0
- tenzir_test/runners/__init__.py +164 -0
- tenzir_test/runners/_utils.py +17 -0
- tenzir_test/runners/custom_python_fixture_runner.py +171 -0
- tenzir_test/runners/diff_runner.py +139 -0
- tenzir_test/runners/ext_runner.py +28 -0
- tenzir_test/runners/runner.py +38 -0
- tenzir_test/runners/shell_runner.py +158 -0
- tenzir_test/runners/tenzir_runner.py +37 -0
- tenzir_test/runners/tql_runner.py +13 -0
- tenzir_test-0.12.0.dist-info/METADATA +81 -0
- tenzir_test-0.12.0.dist-info/RECORD +29 -0
- tenzir_test-0.12.0.dist-info/WHEEL +4 -0
- tenzir_test-0.12.0.dist-info/entry_points.txt +3 -0
- tenzir_test-0.12.0.dist-info/licenses/LICENSE +190 -0
|
@@ -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"]
|