tenzir-test 0.12.0__py3-none-any.whl → 0.13.1__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/cli.py +19 -0
- tenzir_test/config.py +1 -1
- tenzir_test/fixtures/node.py +83 -6
- tenzir_test/run.py +161 -3
- tenzir_test/runners/diff_runner.py +8 -3
- {tenzir_test-0.12.0.dist-info → tenzir_test-0.13.1.dist-info}/METADATA +7 -7
- {tenzir_test-0.12.0.dist-info → tenzir_test-0.13.1.dist-info}/RECORD +10 -11
- {tenzir_test-0.12.0.dist-info → tenzir_test-0.13.1.dist-info}/WHEEL +1 -1
- {tenzir_test-0.12.0.dist-info → tenzir_test-0.13.1.dist-info}/entry_points.txt +0 -1
- tenzir_test/checks.py +0 -31
- {tenzir_test-0.12.0.dist-info → tenzir_test-0.13.1.dist-info}/licenses/LICENSE +0 -0
tenzir_test/cli.py
CHANGED
|
@@ -51,6 +51,15 @@ def _normalize_exit_code(value: object) -> int:
|
|
|
51
51
|
type=click.Path(path_type=Path, dir_okay=False, writable=False, resolve_path=False),
|
|
52
52
|
help="Path to the tenzir-node executable.",
|
|
53
53
|
)
|
|
54
|
+
@click.option(
|
|
55
|
+
"package_dirs",
|
|
56
|
+
"--package-dirs",
|
|
57
|
+
multiple=True,
|
|
58
|
+
help=(
|
|
59
|
+
"Comma-separated list of package directories to load (repeatable). "
|
|
60
|
+
"These only control package visibility; test selection still follows the usual --root/args."
|
|
61
|
+
),
|
|
62
|
+
)
|
|
54
63
|
@click.argument(
|
|
55
64
|
"tests",
|
|
56
65
|
nargs=-1,
|
|
@@ -137,6 +146,7 @@ def cli(
|
|
|
137
146
|
root: Path | None,
|
|
138
147
|
tenzir_binary: Path | None,
|
|
139
148
|
tenzir_node_binary: Path | None,
|
|
149
|
+
package_dirs: tuple[str, ...],
|
|
140
150
|
tests: tuple[Path, ...],
|
|
141
151
|
update: bool,
|
|
142
152
|
debug: bool,
|
|
@@ -155,6 +165,14 @@ def cli(
|
|
|
155
165
|
) -> int:
|
|
156
166
|
"""Execute tenzir-test scenarios."""
|
|
157
167
|
|
|
168
|
+
package_paths: list[Path] = []
|
|
169
|
+
for entry in package_dirs:
|
|
170
|
+
for piece in entry.split(","):
|
|
171
|
+
piece = piece.strip()
|
|
172
|
+
if not piece:
|
|
173
|
+
continue
|
|
174
|
+
package_paths.append(Path(piece))
|
|
175
|
+
|
|
158
176
|
jobs_source = ctx.get_parameter_source("jobs")
|
|
159
177
|
jobs_overridden = jobs_source is not click.core.ParameterSource.DEFAULT
|
|
160
178
|
|
|
@@ -163,6 +181,7 @@ def cli(
|
|
|
163
181
|
root=root,
|
|
164
182
|
tenzir_binary=tenzir_binary,
|
|
165
183
|
tenzir_node_binary=tenzir_node_binary,
|
|
184
|
+
package_dirs=package_paths,
|
|
166
185
|
tests=list(tests),
|
|
167
186
|
update=update,
|
|
168
187
|
debug=debug,
|
tenzir_test/config.py
CHANGED
|
@@ -41,7 +41,7 @@ def discover_settings(
|
|
|
41
41
|
) -> Settings:
|
|
42
42
|
"""Produce harness settings by combining CLI overrides with environment defaults."""
|
|
43
43
|
|
|
44
|
-
environment = dict(env
|
|
44
|
+
environment = dict(os.environ if env is None else env)
|
|
45
45
|
|
|
46
46
|
chosen_root = root or environment.get("TENZIR_TEST_ROOT") or Path.cwd()
|
|
47
47
|
root_path = Path(chosen_root).resolve()
|
tenzir_test/fixtures/node.py
CHANGED
|
@@ -6,6 +6,7 @@ import atexit
|
|
|
6
6
|
import functools
|
|
7
7
|
import logging
|
|
8
8
|
import os
|
|
9
|
+
import select
|
|
9
10
|
import shlex
|
|
10
11
|
import subprocess
|
|
11
12
|
import tempfile
|
|
@@ -26,6 +27,37 @@ from . import (
|
|
|
26
27
|
if TYPE_CHECKING:
|
|
27
28
|
from . import FixtureContext
|
|
28
29
|
|
|
30
|
+
_STDERR_READ_TIMEOUT = 0.5 # seconds to wait for stderr data
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _read_available_stderr(process: subprocess.Popen[str]) -> str:
|
|
34
|
+
"""Read any available stderr output without blocking.
|
|
35
|
+
|
|
36
|
+
Uses select() to check if data is available before reading. This allows
|
|
37
|
+
capturing diagnostic output even when the process is still running but
|
|
38
|
+
has written error messages to stderr.
|
|
39
|
+
"""
|
|
40
|
+
if not process.stderr:
|
|
41
|
+
return ""
|
|
42
|
+
try:
|
|
43
|
+
fd = process.stderr.fileno()
|
|
44
|
+
readable, _, _ = select.select([fd], [], [], _STDERR_READ_TIMEOUT)
|
|
45
|
+
if not readable:
|
|
46
|
+
return ""
|
|
47
|
+
# Read available data in chunks to avoid blocking on partial reads.
|
|
48
|
+
chunks: list[str] = []
|
|
49
|
+
while True:
|
|
50
|
+
ready, _, _ = select.select([fd], [], [], 0)
|
|
51
|
+
if not ready:
|
|
52
|
+
break
|
|
53
|
+
chunk = os.read(fd, 4096).decode("utf-8", errors="replace")
|
|
54
|
+
if not chunk:
|
|
55
|
+
break
|
|
56
|
+
chunks.append(chunk)
|
|
57
|
+
return "".join(chunks).strip()
|
|
58
|
+
except Exception:
|
|
59
|
+
return ""
|
|
60
|
+
|
|
29
61
|
|
|
30
62
|
def _terminate_process(process: subprocess.Popen[str]) -> None:
|
|
31
63
|
"""Terminate the spawned node process and ensure its group is gone."""
|
|
@@ -138,16 +170,47 @@ def node() -> Iterator[dict[str, str]]:
|
|
|
138
170
|
raise RuntimeError("TENZIR_NODE_BINARY must be configured for the node fixture")
|
|
139
171
|
|
|
140
172
|
env = context.env.copy()
|
|
141
|
-
|
|
173
|
+
# Extract and filter config arguments: we handle --config and --package-dirs separately.
|
|
174
|
+
config_args: list[str] = []
|
|
175
|
+
config_package_dirs: list[str] = []
|
|
176
|
+
for arg in context.config_args:
|
|
177
|
+
if arg.startswith("--config="):
|
|
178
|
+
continue
|
|
179
|
+
if arg.startswith("--package-dirs="):
|
|
180
|
+
value = arg.split("=", 1)[1]
|
|
181
|
+
config_package_dirs.extend(entry.strip() for entry in value.split(",") if entry.strip())
|
|
182
|
+
continue
|
|
183
|
+
config_args.append(arg)
|
|
142
184
|
node_config = env.get("TENZIR_NODE_CONFIG")
|
|
143
185
|
if node_config:
|
|
144
186
|
config_args.append(f"--config={node_config}")
|
|
145
|
-
package_root = env.get("TENZIR_PACKAGE_ROOT")
|
|
146
187
|
package_args: list[str] = []
|
|
188
|
+
package_root = env.get("TENZIR_PACKAGE_ROOT")
|
|
189
|
+
package_dirs: list[str] = []
|
|
147
190
|
if package_root:
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
191
|
+
package_dirs.append(package_root)
|
|
192
|
+
|
|
193
|
+
extra_package_dirs = env.get("TENZIR_PACKAGE_DIRS")
|
|
194
|
+
if extra_package_dirs:
|
|
195
|
+
package_dirs.extend(
|
|
196
|
+
[entry.strip() for entry in extra_package_dirs.split(",") if entry.strip()]
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
# Include package directories from config_args as well.
|
|
200
|
+
package_dirs.extend(config_package_dirs)
|
|
201
|
+
|
|
202
|
+
# Deduplicate while preserving order by using resolved paths as keys.
|
|
203
|
+
seen: set[str] = set()
|
|
204
|
+
unique_dirs: list[str] = []
|
|
205
|
+
for entry in package_dirs:
|
|
206
|
+
resolved = str(Path(entry).expanduser().resolve(strict=False))
|
|
207
|
+
if resolved not in seen:
|
|
208
|
+
seen.add(resolved)
|
|
209
|
+
unique_dirs.append(entry)
|
|
210
|
+
package_dirs = unique_dirs
|
|
211
|
+
|
|
212
|
+
if package_dirs:
|
|
213
|
+
package_args.append(f"--package-dirs={','.join(package_dirs)}")
|
|
151
214
|
temp_dir = _ensure_temp_dir(context)
|
|
152
215
|
key = id(context)
|
|
153
216
|
|
|
@@ -213,7 +276,21 @@ def node() -> Iterator[dict[str, str]]:
|
|
|
213
276
|
endpoint = process.stdout.readline().strip()
|
|
214
277
|
|
|
215
278
|
if not endpoint:
|
|
216
|
-
|
|
279
|
+
# Collect diagnostic information to help debug startup failures.
|
|
280
|
+
diagnostics: list[str] = []
|
|
281
|
+
returncode = process.poll()
|
|
282
|
+
if returncode is not None:
|
|
283
|
+
diagnostics.append(f"exit code {returncode}")
|
|
284
|
+
# Try to read stderr output using non-blocking I/O. This captures
|
|
285
|
+
# diagnostics even when the process is still running (e.g., when it
|
|
286
|
+
# hangs after writing an error message but before exiting).
|
|
287
|
+
stderr_output = _read_available_stderr(process)
|
|
288
|
+
if stderr_output:
|
|
289
|
+
diagnostics.append(f"stderr:\n{stderr_output}")
|
|
290
|
+
detail = (
|
|
291
|
+
"; ".join(diagnostics) if diagnostics else "no additional diagnostics available"
|
|
292
|
+
)
|
|
293
|
+
raise RuntimeError(f"failed to obtain endpoint from tenzir-node ({detail})")
|
|
217
294
|
|
|
218
295
|
fixture_env = {
|
|
219
296
|
"TENZIR_NODE_CLIENT_ENDPOINT": endpoint,
|
tenzir_test/run.py
CHANGED
|
@@ -128,6 +128,7 @@ class ExecutionMode(enum.Enum):
|
|
|
128
128
|
|
|
129
129
|
PROJECT = "project"
|
|
130
130
|
PACKAGE = "package"
|
|
131
|
+
LIBRARY = "library"
|
|
131
132
|
|
|
132
133
|
|
|
133
134
|
class HarnessMode(enum.Enum):
|
|
@@ -146,6 +147,18 @@ class ColorMode(enum.Enum):
|
|
|
146
147
|
NEVER = "never"
|
|
147
148
|
|
|
148
149
|
|
|
150
|
+
def _is_library_root(path: Path) -> bool:
|
|
151
|
+
"""Return True when the directory looks like a library of packages."""
|
|
152
|
+
|
|
153
|
+
if packages.is_package_dir(path):
|
|
154
|
+
return False
|
|
155
|
+
try:
|
|
156
|
+
entries = list(packages.iter_package_dirs(path))
|
|
157
|
+
except OSError:
|
|
158
|
+
return False
|
|
159
|
+
return bool(entries)
|
|
160
|
+
|
|
161
|
+
|
|
149
162
|
def detect_execution_mode(root: Path) -> tuple[ExecutionMode, Path | None]:
|
|
150
163
|
"""Return the execution mode and detected package root for `root`."""
|
|
151
164
|
|
|
@@ -156,6 +169,9 @@ def detect_execution_mode(root: Path) -> tuple[ExecutionMode, Path | None]:
|
|
|
156
169
|
if parent is not None and packages.is_package_dir(parent):
|
|
157
170
|
return ExecutionMode.PACKAGE, parent
|
|
158
171
|
|
|
172
|
+
if _is_library_root(root):
|
|
173
|
+
return ExecutionMode.LIBRARY, None
|
|
174
|
+
|
|
159
175
|
return ExecutionMode.PROJECT, None
|
|
160
176
|
|
|
161
177
|
|
|
@@ -746,6 +762,23 @@ _CLI_LOGGER.setLevel(logging.DEBUG if _debug_logging else logging.WARNING)
|
|
|
746
762
|
_DIRECTORY_CONFIG_CACHE: dict[Path, "_DirectoryConfig"] = {}
|
|
747
763
|
|
|
748
764
|
_DISCOVERY_ENABLED = False
|
|
765
|
+
_CLI_PACKAGES: list[Path] = []
|
|
766
|
+
|
|
767
|
+
|
|
768
|
+
def _expand_package_dirs(path: Path) -> list[str]:
|
|
769
|
+
"""Normalize a package dir hint; if it contains packages, return those."""
|
|
770
|
+
|
|
771
|
+
resolved = path.expanduser().resolve()
|
|
772
|
+
if packages.is_package_dir(resolved):
|
|
773
|
+
return [str(resolved)]
|
|
774
|
+
expanded: list[str] = []
|
|
775
|
+
try:
|
|
776
|
+
for pkg in packages.iter_package_dirs(resolved):
|
|
777
|
+
expanded.append(str(pkg.resolve()))
|
|
778
|
+
except OSError as exc:
|
|
779
|
+
_CLI_LOGGER.debug("failed to expand package dir %s: %s", path, exc)
|
|
780
|
+
return [str(resolved)]
|
|
781
|
+
return expanded or [str(resolved)]
|
|
749
782
|
|
|
750
783
|
|
|
751
784
|
def _set_discovery_logging(enabled: bool) -> None:
|
|
@@ -753,6 +786,29 @@ def _set_discovery_logging(enabled: bool) -> None:
|
|
|
753
786
|
_DISCOVERY_ENABLED = enabled
|
|
754
787
|
|
|
755
788
|
|
|
789
|
+
def _set_cli_packages(package_paths: list[Path]) -> None:
|
|
790
|
+
global _CLI_PACKAGES
|
|
791
|
+
_CLI_PACKAGES = [path.resolve() for path in package_paths]
|
|
792
|
+
|
|
793
|
+
|
|
794
|
+
def _get_cli_packages() -> list[Path]:
|
|
795
|
+
return list(_CLI_PACKAGES)
|
|
796
|
+
|
|
797
|
+
|
|
798
|
+
def _deduplicate_package_dirs(candidates: list[str]) -> list[str]:
|
|
799
|
+
"""Remove duplicate package directories while preserving order."""
|
|
800
|
+
|
|
801
|
+
seen: set[str] = set()
|
|
802
|
+
result: list[str] = []
|
|
803
|
+
for candidate in candidates:
|
|
804
|
+
normalized = str(Path(candidate).expanduser().resolve(strict=False))
|
|
805
|
+
if normalized in seen:
|
|
806
|
+
continue
|
|
807
|
+
seen.add(normalized)
|
|
808
|
+
result.append(candidate)
|
|
809
|
+
return result
|
|
810
|
+
|
|
811
|
+
|
|
756
812
|
def _print_discovery_message(message: str) -> None:
|
|
757
813
|
if _CLI_LOGGER.isEnabledFor(logging.DEBUG):
|
|
758
814
|
_CLI_LOGGER.debug(message)
|
|
@@ -768,6 +824,7 @@ class ProjectMarker(enum.Enum):
|
|
|
768
824
|
TESTS_DIRECTORY = "tests_directory"
|
|
769
825
|
TEST_CONFIG = "test_config"
|
|
770
826
|
TEST_SUITE_DIRECTORY = "test_suite_directory"
|
|
827
|
+
LIBRARY_ROOT = "library_root"
|
|
771
828
|
|
|
772
829
|
|
|
773
830
|
@dataclasses.dataclass(frozen=True, slots=True)
|
|
@@ -790,6 +847,7 @@ _PRIMARY_PROJECT_MARKERS = {
|
|
|
790
847
|
ProjectMarker.TESTS_DIRECTORY,
|
|
791
848
|
ProjectMarker.TEST_CONFIG,
|
|
792
849
|
ProjectMarker.TEST_SUITE_DIRECTORY,
|
|
850
|
+
ProjectMarker.LIBRARY_ROOT,
|
|
793
851
|
}
|
|
794
852
|
|
|
795
853
|
|
|
@@ -819,6 +877,12 @@ def _describe_project_root(path: Path) -> ProjectSignature | None:
|
|
|
819
877
|
if resolved.name == "tests" and resolved.is_dir():
|
|
820
878
|
markers.add(ProjectMarker.TEST_SUITE_DIRECTORY)
|
|
821
879
|
|
|
880
|
+
try:
|
|
881
|
+
if _is_library_root(resolved):
|
|
882
|
+
markers.add(ProjectMarker.LIBRARY_ROOT)
|
|
883
|
+
except OSError:
|
|
884
|
+
pass
|
|
885
|
+
|
|
822
886
|
if not markers:
|
|
823
887
|
return None
|
|
824
888
|
|
|
@@ -1117,12 +1181,15 @@ def _default_test_config() -> TestConfig:
|
|
|
1117
1181
|
"inputs": None,
|
|
1118
1182
|
"retry": 1,
|
|
1119
1183
|
"suite": None,
|
|
1184
|
+
"package_dirs": tuple(),
|
|
1120
1185
|
}
|
|
1121
1186
|
|
|
1122
1187
|
|
|
1123
1188
|
def _canonical_config_key(key: str) -> str:
|
|
1124
1189
|
if key == "fixture":
|
|
1125
1190
|
return "fixtures"
|
|
1191
|
+
if key in {"package_dirs", "package-dirs"}:
|
|
1192
|
+
return "package_dirs"
|
|
1126
1193
|
return key
|
|
1127
1194
|
|
|
1128
1195
|
|
|
@@ -1224,6 +1291,50 @@ def _normalize_inputs_value(
|
|
|
1224
1291
|
return None
|
|
1225
1292
|
|
|
1226
1293
|
|
|
1294
|
+
def _normalize_package_dirs_value(
|
|
1295
|
+
value: typing.Any,
|
|
1296
|
+
*,
|
|
1297
|
+
location: Path | str,
|
|
1298
|
+
line_number: int | None = None,
|
|
1299
|
+
) -> tuple[str, ...]:
|
|
1300
|
+
if value is None:
|
|
1301
|
+
return tuple()
|
|
1302
|
+
if not isinstance(value, (list, tuple)):
|
|
1303
|
+
_raise_config_error(
|
|
1304
|
+
location,
|
|
1305
|
+
f"Invalid value for 'package-dirs', expected list of strings, got '{value}'",
|
|
1306
|
+
line_number,
|
|
1307
|
+
)
|
|
1308
|
+
return tuple()
|
|
1309
|
+
base_dir = _extract_location_path(location).parent
|
|
1310
|
+
normalized: list[str] = []
|
|
1311
|
+
for entry in value:
|
|
1312
|
+
if not isinstance(entry, (str, os.PathLike)):
|
|
1313
|
+
_raise_config_error(
|
|
1314
|
+
location,
|
|
1315
|
+
f"Invalid package-dirs entry '{entry}', expected string",
|
|
1316
|
+
line_number,
|
|
1317
|
+
)
|
|
1318
|
+
continue
|
|
1319
|
+
raw = os.fspath(entry).strip()
|
|
1320
|
+
if not raw:
|
|
1321
|
+
_raise_config_error(
|
|
1322
|
+
location,
|
|
1323
|
+
"Invalid package-dirs entry: must be non-empty string",
|
|
1324
|
+
line_number,
|
|
1325
|
+
)
|
|
1326
|
+
continue
|
|
1327
|
+
path = Path(raw)
|
|
1328
|
+
if not path.is_absolute():
|
|
1329
|
+
path = base_dir / path
|
|
1330
|
+
try:
|
|
1331
|
+
path = path.resolve()
|
|
1332
|
+
except OSError:
|
|
1333
|
+
path = path
|
|
1334
|
+
normalized.append(str(path))
|
|
1335
|
+
return tuple(normalized)
|
|
1336
|
+
|
|
1337
|
+
|
|
1227
1338
|
def _assign_config_option(
|
|
1228
1339
|
config: TestConfig,
|
|
1229
1340
|
key: str,
|
|
@@ -1234,7 +1345,16 @@ def _assign_config_option(
|
|
|
1234
1345
|
origin: ConfigOrigin,
|
|
1235
1346
|
) -> None:
|
|
1236
1347
|
canonical = _canonical_config_key(key)
|
|
1237
|
-
valid_keys: set[str] = {
|
|
1348
|
+
valid_keys: set[str] = {
|
|
1349
|
+
"error",
|
|
1350
|
+
"timeout",
|
|
1351
|
+
"runner",
|
|
1352
|
+
"skip",
|
|
1353
|
+
"fixtures",
|
|
1354
|
+
"inputs",
|
|
1355
|
+
"retry",
|
|
1356
|
+
"package_dirs",
|
|
1357
|
+
}
|
|
1238
1358
|
if origin == "directory":
|
|
1239
1359
|
valid_keys.add("suite")
|
|
1240
1360
|
if canonical not in valid_keys:
|
|
@@ -1321,6 +1441,17 @@ def _assign_config_option(
|
|
|
1321
1441
|
value, location=location, line_number=line_number
|
|
1322
1442
|
)
|
|
1323
1443
|
return
|
|
1444
|
+
if canonical == "package_dirs":
|
|
1445
|
+
if origin != "directory":
|
|
1446
|
+
_raise_config_error(
|
|
1447
|
+
location,
|
|
1448
|
+
"'package-dirs' can only be specified in directory-level test.yaml files",
|
|
1449
|
+
line_number,
|
|
1450
|
+
)
|
|
1451
|
+
config[canonical] = _normalize_package_dirs_value(
|
|
1452
|
+
value, location=location, line_number=line_number
|
|
1453
|
+
)
|
|
1454
|
+
return
|
|
1324
1455
|
if canonical == "retry":
|
|
1325
1456
|
if origin == "frontmatter" and isinstance(config.get("suite"), str):
|
|
1326
1457
|
_raise_config_error(
|
|
@@ -2710,14 +2841,27 @@ def run_simple_test(
|
|
|
2710
2841
|
expect_error = bool(test_config.get("error", False))
|
|
2711
2842
|
passthrough_mode = is_passthrough_enabled()
|
|
2712
2843
|
|
|
2844
|
+
config_package_dirs = cast(tuple[str, ...], test_config.get("package_dirs", tuple()))
|
|
2845
|
+
additional_package_dirs: list[str] = []
|
|
2846
|
+
for entry in config_package_dirs:
|
|
2847
|
+
additional_package_dirs.extend(_expand_package_dirs(Path(entry)))
|
|
2848
|
+
|
|
2713
2849
|
package_root = packages.find_package_root(test)
|
|
2714
2850
|
package_args: list[str] = []
|
|
2851
|
+
package_dir_candidates: list[str] = []
|
|
2715
2852
|
if package_root is not None:
|
|
2716
2853
|
env["TENZIR_PACKAGE_ROOT"] = str(package_root)
|
|
2717
2854
|
package_tests_root = package_root / "tests"
|
|
2718
2855
|
if inputs_override is None:
|
|
2719
2856
|
env["TENZIR_INPUTS"] = str(package_tests_root / "inputs")
|
|
2720
|
-
|
|
2857
|
+
package_dir_candidates.append(str(package_root))
|
|
2858
|
+
package_dir_candidates.extend(additional_package_dirs)
|
|
2859
|
+
for cli_path in _get_cli_packages():
|
|
2860
|
+
package_dir_candidates.extend(_expand_package_dirs(cli_path))
|
|
2861
|
+
if package_dir_candidates:
|
|
2862
|
+
merged_dirs = _deduplicate_package_dirs(package_dir_candidates)
|
|
2863
|
+
env["TENZIR_PACKAGE_DIRS"] = ",".join(merged_dirs)
|
|
2864
|
+
package_args.append(f"--package-dirs={','.join(merged_dirs)}")
|
|
2721
2865
|
|
|
2722
2866
|
context_token = fixtures_impl.push_context(
|
|
2723
2867
|
fixtures_impl.FixtureContext(
|
|
@@ -2975,12 +3119,24 @@ class Worker:
|
|
|
2975
3119
|
raise RuntimeError(f"failed to parse suite config for {primary_test}: {exc}") from exc
|
|
2976
3120
|
inputs_override = typing.cast(str | None, primary_config.get("inputs"))
|
|
2977
3121
|
env, config_args = get_test_env_and_config_args(primary_test, inputs=inputs_override)
|
|
3122
|
+
config_package_dirs = cast(tuple[str, ...], primary_config.get("package_dirs", tuple()))
|
|
3123
|
+
additional_package_dirs: list[str] = []
|
|
3124
|
+
for entry in config_package_dirs:
|
|
3125
|
+
additional_package_dirs.extend(_expand_package_dirs(Path(entry)))
|
|
2978
3126
|
package_root = packages.find_package_root(primary_test)
|
|
3127
|
+
package_dir_candidates: list[str] = []
|
|
2979
3128
|
if package_root is not None:
|
|
2980
3129
|
env["TENZIR_PACKAGE_ROOT"] = str(package_root)
|
|
2981
3130
|
if inputs_override is None:
|
|
2982
3131
|
env["TENZIR_INPUTS"] = str((package_root / "tests" / "inputs"))
|
|
2983
|
-
|
|
3132
|
+
package_dir_candidates.append(str(package_root))
|
|
3133
|
+
package_dir_candidates.extend(additional_package_dirs)
|
|
3134
|
+
for cli_path in _get_cli_packages():
|
|
3135
|
+
package_dir_candidates.extend(_expand_package_dirs(cli_path))
|
|
3136
|
+
if package_dir_candidates:
|
|
3137
|
+
merged_dirs = _deduplicate_package_dirs(package_dir_candidates)
|
|
3138
|
+
env["TENZIR_PACKAGE_DIRS"] = ",".join(merged_dirs)
|
|
3139
|
+
config_args = list(config_args) + [f"--package-dirs={','.join(merged_dirs)}"]
|
|
2984
3140
|
context_token = fixtures_impl.push_context(
|
|
2985
3141
|
fixtures_impl.FixtureContext(
|
|
2986
3142
|
test=primary_test,
|
|
@@ -3152,6 +3308,7 @@ def run_cli(
|
|
|
3152
3308
|
root: Path | None,
|
|
3153
3309
|
tenzir_binary: Path | None,
|
|
3154
3310
|
tenzir_node_binary: Path | None,
|
|
3311
|
+
package_dirs: Sequence[Path] | None = None,
|
|
3155
3312
|
tests: Sequence[Path],
|
|
3156
3313
|
update: bool,
|
|
3157
3314
|
debug: bool,
|
|
@@ -3236,6 +3393,7 @@ def run_cli(
|
|
|
3236
3393
|
tenzir_node_binary=tenzir_node_binary,
|
|
3237
3394
|
)
|
|
3238
3395
|
apply_settings(settings)
|
|
3396
|
+
_set_cli_packages(list(package_dirs or []))
|
|
3239
3397
|
selected_tests = list(tests)
|
|
3240
3398
|
|
|
3241
3399
|
plan: ExecutionPlan | None = None
|
|
@@ -106,11 +106,16 @@ class DiffRunner(TqlRunner):
|
|
|
106
106
|
fixture_api.pop_context(context_token)
|
|
107
107
|
run_mod.cleanup_test_tmp_dir(env.get(run_mod.TEST_TMP_ENV_VAR))
|
|
108
108
|
|
|
109
|
+
# Strip the ROOT prefix from paths in output to make them relative,
|
|
110
|
+
# consistent with run_simple_test behavior.
|
|
111
|
+
root_bytes = str(run_mod.ROOT).encode() + b"/"
|
|
112
|
+
unoptimized_stdout = unoptimized.stdout.replace(root_bytes, b"")
|
|
113
|
+
optimized_stdout = optimized.stdout.replace(root_bytes, b"")
|
|
109
114
|
diff_chunks = list(
|
|
110
115
|
difflib.diff_bytes(
|
|
111
116
|
difflib.unified_diff,
|
|
112
|
-
|
|
113
|
-
|
|
117
|
+
unoptimized_stdout.splitlines(keepends=True),
|
|
118
|
+
optimized_stdout.splitlines(keepends=True),
|
|
114
119
|
n=2**31 - 1,
|
|
115
120
|
)
|
|
116
121
|
)[3:]
|
|
@@ -118,7 +123,7 @@ class DiffRunner(TqlRunner):
|
|
|
118
123
|
diff_bytes = b"".join(diff_chunks)
|
|
119
124
|
else:
|
|
120
125
|
diff_bytes = b"".join(
|
|
121
|
-
b" " + line for line in
|
|
126
|
+
b" " + line for line in unoptimized_stdout.splitlines(keepends=True)
|
|
122
127
|
)
|
|
123
128
|
ref_path = test.with_suffix(".diff")
|
|
124
129
|
if update:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: tenzir-test
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.13.1
|
|
4
4
|
Summary: Reusable test execution framework extracted from the Tenzir repository.
|
|
5
5
|
Project-URL: Homepage, https://github.com/tenzir/test
|
|
6
6
|
Project-URL: Repository, https://github.com/tenzir/test
|
|
@@ -63,18 +63,18 @@ for an end-to-end walkthrough of writing tests.
|
|
|
63
63
|
We also provide a dense [reference](https://docs.tenzir.com/reference/test) that
|
|
64
64
|
explains concepts, configuration, multi-project execution, and CLI details.
|
|
65
65
|
|
|
66
|
-
## 🧑💻 Development
|
|
67
|
-
|
|
68
|
-
Contributor workflows, quality gates, and release procedures live in
|
|
69
|
-
[`DEVELOPMENT.md`](DEVELOPMENT.md). Follow that guide when you work on the
|
|
70
|
-
project locally.
|
|
71
|
-
|
|
72
66
|
## 🗞️ Releases
|
|
73
67
|
|
|
74
68
|
New versions are published to PyPI through trusted publishing when a GitHub
|
|
75
69
|
release is created. Review the latest release notes on GitHub for details about
|
|
76
70
|
what's new.
|
|
77
71
|
|
|
72
|
+
## 🤝 Contributing
|
|
73
|
+
|
|
74
|
+
Want to contribute? We're all-in on agentic coding with [Claude
|
|
75
|
+
Code](https://claude.ai/code)! The repo comes pre-configured with our [custom
|
|
76
|
+
plugins](https://github.com/tenzir/claude-plugins)—just clone and start hacking.
|
|
77
|
+
|
|
78
78
|
## 📜 License
|
|
79
79
|
|
|
80
80
|
`tenzir-test` is available under the Apache License, Version 2.0. See
|
|
@@ -1,29 +1,28 @@
|
|
|
1
1
|
tenzir_test/__init__.py,sha256=k7V6Pbjaa8SAy6t4KnaauHTyfnyVEwc1VGtH823MANU,1181
|
|
2
2
|
tenzir_test/_python_runner.py,sha256=LmghMIolsNEC2wUyJdv1h_cefOxTxET1IACrw-_hHuY,2900
|
|
3
|
-
tenzir_test/
|
|
4
|
-
tenzir_test/
|
|
5
|
-
tenzir_test/config.py,sha256=q1_VEXuxL-xsGlnooeGvXxx9cMw652UEB9a1mPzZIQs,1680
|
|
3
|
+
tenzir_test/cli.py,sha256=JJNCaF-vZSql6kvPGG4JwO9Z8XIwQWe_5bKuBiuGjLE,6553
|
|
4
|
+
tenzir_test/config.py,sha256=bVuMJlEvevZxCmvzTJ4bs4SkYLNGZtxcCDYhtC-0sp8,1697
|
|
6
5
|
tenzir_test/packages.py,sha256=cTCQdGjCS1XmuKyiwh0ew-z9tHn6J-xZ6nvBP-hU8bc,948
|
|
7
6
|
tenzir_test/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
8
|
-
tenzir_test/run.py,sha256=
|
|
7
|
+
tenzir_test/run.py,sha256=3uq2r3hqa-COrK3f-8UAO4jtD1i5-FgPn5UadspNfqA,130445
|
|
9
8
|
tenzir_test/engine/__init__.py,sha256=5APwy90YDm7rmL_qCZfToAcfbQthcZ8yV2_ExXKqaqE,110
|
|
10
9
|
tenzir_test/engine/operations.py,sha256=OCYjuMHyMAaay4s08u2Sl7oE-PmgeXumylp7R8GYIH4,950
|
|
11
10
|
tenzir_test/engine/registry.py,sha256=LXCr6TGlv1sR1m1eboTk7SrbS2IVErc3PqUuHxGA2xk,594
|
|
12
11
|
tenzir_test/engine/state.py,sha256=Ez-Q27dL5oNqiJE0A4P2OP0p6i7aYKzH4ZyVWNgEezs,889
|
|
13
12
|
tenzir_test/engine/worker.py,sha256=WwIkx1m_ANNveQjNisy5-qpbUZl_DfDXxVIfdxABKjc,140
|
|
14
13
|
tenzir_test/fixtures/__init__.py,sha256=PdkN334btlqagY-4wAwsCyfhHE8kd_RLNoVM_ULii_I,18384
|
|
15
|
-
tenzir_test/fixtures/node.py,sha256=
|
|
14
|
+
tenzir_test/fixtures/node.py,sha256=Y3YqWeqYz-mg3B5V0wIthk55zVOd4JS37zlTdHzlA6c,11193
|
|
16
15
|
tenzir_test/runners/__init__.py,sha256=M3p-TsDp231Dy58miDb467bA1kLYzgpa0pqVr_KP1ro,4616
|
|
17
16
|
tenzir_test/runners/_utils.py,sha256=BWv7UEPGa01l4tGTCg5i_22NblIyRw8vjk_5NIf1x_c,467
|
|
18
17
|
tenzir_test/runners/custom_python_fixture_runner.py,sha256=uXbSf18xTttf51k2EbjWf8AqN2R7TfpfSCkLyU5HJx4,7284
|
|
19
|
-
tenzir_test/runners/diff_runner.py,sha256=
|
|
18
|
+
tenzir_test/runners/diff_runner.py,sha256=WIEry_-4R4VIlA9HON5kl2ipyy1TKzS4jRc2wPl6r5c,5476
|
|
20
19
|
tenzir_test/runners/ext_runner.py,sha256=sKL9Mw_ksVVBWnrdIJR2WS5ueVnLKuNYYWZ22FTZIPo,730
|
|
21
20
|
tenzir_test/runners/runner.py,sha256=LtlD8huQOSmD7RyYDnKeCuI4Y6vhxGXMKsHA2qgfWN0,989
|
|
22
21
|
tenzir_test/runners/shell_runner.py,sha256=OuofgHeZN2FaO6xRI3uyqstLBymc6rmWC4HAnSn91AE,6068
|
|
23
22
|
tenzir_test/runners/tenzir_runner.py,sha256=464FFYS_mh6l-ehccc-S8cIUO1MxdapwQL5X3PmMkMI,1006
|
|
24
23
|
tenzir_test/runners/tql_runner.py,sha256=2ZLMf3TIKwcOvaOFrVvvhzK-EcWmGOUZxKkbSoByyQA,248
|
|
25
|
-
tenzir_test-0.
|
|
26
|
-
tenzir_test-0.
|
|
27
|
-
tenzir_test-0.
|
|
28
|
-
tenzir_test-0.
|
|
29
|
-
tenzir_test-0.
|
|
24
|
+
tenzir_test-0.13.1.dist-info/METADATA,sha256=WB95n9cgGgb6T9SraMuXw3ymn3ujtM5qEIg3zm3fTXo,3066
|
|
25
|
+
tenzir_test-0.13.1.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
26
|
+
tenzir_test-0.13.1.dist-info/entry_points.txt,sha256=l8DJgiEVrjScdTTo613cZ3PKodOmqrUVIbz-3awfV8w,53
|
|
27
|
+
tenzir_test-0.13.1.dist-info/licenses/LICENSE,sha256=ajMbpcBiSTXI8Rr4t17pvowV-On8DktghfZKxY_A22Q,10750
|
|
28
|
+
tenzir_test-0.13.1.dist-info/RECORD,,
|
tenzir_test/checks.py
DELETED
|
@@ -1,31 +0,0 @@
|
|
|
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())
|
|
File without changes
|