tenzir-test 0.12.0__py3-none-any.whl → 0.15.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/cli.py +27 -0
- tenzir_test/config.py +1 -1
- tenzir_test/fixtures/node.py +83 -6
- tenzir_test/run.py +248 -22
- tenzir_test/runners/diff_runner.py +8 -3
- {tenzir_test-0.12.0.dist-info → tenzir_test-0.15.0.dist-info}/METADATA +7 -7
- {tenzir_test-0.12.0.dist-info → tenzir_test-0.15.0.dist-info}/RECORD +10 -11
- {tenzir_test-0.12.0.dist-info → tenzir_test-0.15.0.dist-info}/WHEEL +1 -1
- {tenzir_test-0.12.0.dist-info → tenzir_test-0.15.0.dist-info}/entry_points.txt +0 -1
- tenzir_test/checks.py +0 -31
- {tenzir_test-0.12.0.dist-info → tenzir_test-0.15.0.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,
|
|
@@ -124,6 +133,12 @@ def _normalize_exit_code(value: object) -> int:
|
|
|
124
133
|
is_flag=True,
|
|
125
134
|
help="Stream raw test output directly to the terminal.",
|
|
126
135
|
)
|
|
136
|
+
@click.option(
|
|
137
|
+
"-v",
|
|
138
|
+
"--verbose",
|
|
139
|
+
is_flag=True,
|
|
140
|
+
help="Print individual test results as they complete. By default, only failures are shown. Automatically enabled in passthrough mode.",
|
|
141
|
+
)
|
|
127
142
|
@click.option(
|
|
128
143
|
"-a",
|
|
129
144
|
"--all-projects",
|
|
@@ -137,6 +152,7 @@ def cli(
|
|
|
137
152
|
root: Path | None,
|
|
138
153
|
tenzir_binary: Path | None,
|
|
139
154
|
tenzir_node_binary: Path | None,
|
|
155
|
+
package_dirs: tuple[str, ...],
|
|
140
156
|
tests: tuple[Path, ...],
|
|
141
157
|
update: bool,
|
|
142
158
|
debug: bool,
|
|
@@ -151,10 +167,19 @@ def cli(
|
|
|
151
167
|
keep_tmp_dirs: bool,
|
|
152
168
|
jobs: int,
|
|
153
169
|
passthrough: bool,
|
|
170
|
+
verbose: bool,
|
|
154
171
|
all_projects: bool,
|
|
155
172
|
) -> int:
|
|
156
173
|
"""Execute tenzir-test scenarios."""
|
|
157
174
|
|
|
175
|
+
package_paths: list[Path] = []
|
|
176
|
+
for entry in package_dirs:
|
|
177
|
+
for piece in entry.split(","):
|
|
178
|
+
piece = piece.strip()
|
|
179
|
+
if not piece:
|
|
180
|
+
continue
|
|
181
|
+
package_paths.append(Path(piece))
|
|
182
|
+
|
|
158
183
|
jobs_source = ctx.get_parameter_source("jobs")
|
|
159
184
|
jobs_overridden = jobs_source is not click.core.ParameterSource.DEFAULT
|
|
160
185
|
|
|
@@ -163,6 +188,7 @@ def cli(
|
|
|
163
188
|
root=root,
|
|
164
189
|
tenzir_binary=tenzir_binary,
|
|
165
190
|
tenzir_node_binary=tenzir_node_binary,
|
|
191
|
+
package_dirs=package_paths,
|
|
166
192
|
tests=list(tests),
|
|
167
193
|
update=update,
|
|
168
194
|
debug=debug,
|
|
@@ -177,6 +203,7 @@ def cli(
|
|
|
177
203
|
keep_tmp_dirs=keep_tmp_dirs,
|
|
178
204
|
jobs=jobs,
|
|
179
205
|
passthrough=passthrough,
|
|
206
|
+
verbose=verbose,
|
|
180
207
|
jobs_overridden=jobs_overridden,
|
|
181
208
|
all_projects=all_projects,
|
|
182
209
|
)
|
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
|
|
|
@@ -467,6 +483,7 @@ KEEP_TMP_DIRS = bool(os.environ.get(_TMP_KEEP_ENV_VAR))
|
|
|
467
483
|
|
|
468
484
|
SHOW_DIFF_OUTPUT = True
|
|
469
485
|
SHOW_DIFF_STAT = True
|
|
486
|
+
VERBOSE_OUTPUT = False
|
|
470
487
|
_BLOCK_INDENT = ""
|
|
471
488
|
_PLUS_SYMBOLS = {1: "□", 10: "▣", 100: "■"}
|
|
472
489
|
_MINUS_SYMBOLS = {1: "□", 10: "▣", 100: "■"}
|
|
@@ -490,6 +507,21 @@ def should_show_diff_stat() -> bool:
|
|
|
490
507
|
return SHOW_DIFF_STAT
|
|
491
508
|
|
|
492
509
|
|
|
510
|
+
def set_verbose_output(enabled: bool) -> None:
|
|
511
|
+
"""Enable or disable verbose output for individual test results.
|
|
512
|
+
|
|
513
|
+
When enabled, both success and skip messages are printed as tests complete.
|
|
514
|
+
When disabled (default), only failures are printed.
|
|
515
|
+
"""
|
|
516
|
+
global VERBOSE_OUTPUT
|
|
517
|
+
VERBOSE_OUTPUT = enabled
|
|
518
|
+
|
|
519
|
+
|
|
520
|
+
def is_verbose_output() -> bool:
|
|
521
|
+
"""Return whether verbose output is enabled."""
|
|
522
|
+
return VERBOSE_OUTPUT
|
|
523
|
+
|
|
524
|
+
|
|
493
525
|
def set_harness_mode(mode: HarnessMode) -> None:
|
|
494
526
|
"""Set the global harness execution mode."""
|
|
495
527
|
|
|
@@ -746,6 +778,23 @@ _CLI_LOGGER.setLevel(logging.DEBUG if _debug_logging else logging.WARNING)
|
|
|
746
778
|
_DIRECTORY_CONFIG_CACHE: dict[Path, "_DirectoryConfig"] = {}
|
|
747
779
|
|
|
748
780
|
_DISCOVERY_ENABLED = False
|
|
781
|
+
_CLI_PACKAGES: list[Path] = []
|
|
782
|
+
|
|
783
|
+
|
|
784
|
+
def _expand_package_dirs(path: Path) -> list[str]:
|
|
785
|
+
"""Normalize a package dir hint; if it contains packages, return those."""
|
|
786
|
+
|
|
787
|
+
resolved = path.expanduser().resolve()
|
|
788
|
+
if packages.is_package_dir(resolved):
|
|
789
|
+
return [str(resolved)]
|
|
790
|
+
expanded: list[str] = []
|
|
791
|
+
try:
|
|
792
|
+
for pkg in packages.iter_package_dirs(resolved):
|
|
793
|
+
expanded.append(str(pkg.resolve()))
|
|
794
|
+
except OSError as exc:
|
|
795
|
+
_CLI_LOGGER.debug("failed to expand package dir %s: %s", path, exc)
|
|
796
|
+
return [str(resolved)]
|
|
797
|
+
return expanded or [str(resolved)]
|
|
749
798
|
|
|
750
799
|
|
|
751
800
|
def _set_discovery_logging(enabled: bool) -> None:
|
|
@@ -753,6 +802,29 @@ def _set_discovery_logging(enabled: bool) -> None:
|
|
|
753
802
|
_DISCOVERY_ENABLED = enabled
|
|
754
803
|
|
|
755
804
|
|
|
805
|
+
def _set_cli_packages(package_paths: list[Path]) -> None:
|
|
806
|
+
global _CLI_PACKAGES
|
|
807
|
+
_CLI_PACKAGES = [path.resolve() for path in package_paths]
|
|
808
|
+
|
|
809
|
+
|
|
810
|
+
def _get_cli_packages() -> list[Path]:
|
|
811
|
+
return list(_CLI_PACKAGES)
|
|
812
|
+
|
|
813
|
+
|
|
814
|
+
def _deduplicate_package_dirs(candidates: list[str]) -> list[str]:
|
|
815
|
+
"""Remove duplicate package directories while preserving order."""
|
|
816
|
+
|
|
817
|
+
seen: set[str] = set()
|
|
818
|
+
result: list[str] = []
|
|
819
|
+
for candidate in candidates:
|
|
820
|
+
normalized = str(Path(candidate).expanduser().resolve(strict=False))
|
|
821
|
+
if normalized in seen:
|
|
822
|
+
continue
|
|
823
|
+
seen.add(normalized)
|
|
824
|
+
result.append(candidate)
|
|
825
|
+
return result
|
|
826
|
+
|
|
827
|
+
|
|
756
828
|
def _print_discovery_message(message: str) -> None:
|
|
757
829
|
if _CLI_LOGGER.isEnabledFor(logging.DEBUG):
|
|
758
830
|
_CLI_LOGGER.debug(message)
|
|
@@ -768,6 +840,7 @@ class ProjectMarker(enum.Enum):
|
|
|
768
840
|
TESTS_DIRECTORY = "tests_directory"
|
|
769
841
|
TEST_CONFIG = "test_config"
|
|
770
842
|
TEST_SUITE_DIRECTORY = "test_suite_directory"
|
|
843
|
+
LIBRARY_ROOT = "library_root"
|
|
771
844
|
|
|
772
845
|
|
|
773
846
|
@dataclasses.dataclass(frozen=True, slots=True)
|
|
@@ -790,6 +863,7 @@ _PRIMARY_PROJECT_MARKERS = {
|
|
|
790
863
|
ProjectMarker.TESTS_DIRECTORY,
|
|
791
864
|
ProjectMarker.TEST_CONFIG,
|
|
792
865
|
ProjectMarker.TEST_SUITE_DIRECTORY,
|
|
866
|
+
ProjectMarker.LIBRARY_ROOT,
|
|
793
867
|
}
|
|
794
868
|
|
|
795
869
|
|
|
@@ -819,6 +893,12 @@ def _describe_project_root(path: Path) -> ProjectSignature | None:
|
|
|
819
893
|
if resolved.name == "tests" and resolved.is_dir():
|
|
820
894
|
markers.add(ProjectMarker.TEST_SUITE_DIRECTORY)
|
|
821
895
|
|
|
896
|
+
try:
|
|
897
|
+
if _is_library_root(resolved):
|
|
898
|
+
markers.add(ProjectMarker.LIBRARY_ROOT)
|
|
899
|
+
except OSError:
|
|
900
|
+
pass
|
|
901
|
+
|
|
822
902
|
if not markers:
|
|
823
903
|
return None
|
|
824
904
|
|
|
@@ -1117,12 +1197,15 @@ def _default_test_config() -> TestConfig:
|
|
|
1117
1197
|
"inputs": None,
|
|
1118
1198
|
"retry": 1,
|
|
1119
1199
|
"suite": None,
|
|
1200
|
+
"package_dirs": tuple(),
|
|
1120
1201
|
}
|
|
1121
1202
|
|
|
1122
1203
|
|
|
1123
1204
|
def _canonical_config_key(key: str) -> str:
|
|
1124
1205
|
if key == "fixture":
|
|
1125
1206
|
return "fixtures"
|
|
1207
|
+
if key in {"package_dirs", "package-dirs"}:
|
|
1208
|
+
return "package_dirs"
|
|
1126
1209
|
return key
|
|
1127
1210
|
|
|
1128
1211
|
|
|
@@ -1224,6 +1307,50 @@ def _normalize_inputs_value(
|
|
|
1224
1307
|
return None
|
|
1225
1308
|
|
|
1226
1309
|
|
|
1310
|
+
def _normalize_package_dirs_value(
|
|
1311
|
+
value: typing.Any,
|
|
1312
|
+
*,
|
|
1313
|
+
location: Path | str,
|
|
1314
|
+
line_number: int | None = None,
|
|
1315
|
+
) -> tuple[str, ...]:
|
|
1316
|
+
if value is None:
|
|
1317
|
+
return tuple()
|
|
1318
|
+
if not isinstance(value, (list, tuple)):
|
|
1319
|
+
_raise_config_error(
|
|
1320
|
+
location,
|
|
1321
|
+
f"Invalid value for 'package-dirs', expected list of strings, got '{value}'",
|
|
1322
|
+
line_number,
|
|
1323
|
+
)
|
|
1324
|
+
return tuple()
|
|
1325
|
+
base_dir = _extract_location_path(location).parent
|
|
1326
|
+
normalized: list[str] = []
|
|
1327
|
+
for entry in value:
|
|
1328
|
+
if not isinstance(entry, (str, os.PathLike)):
|
|
1329
|
+
_raise_config_error(
|
|
1330
|
+
location,
|
|
1331
|
+
f"Invalid package-dirs entry '{entry}', expected string",
|
|
1332
|
+
line_number,
|
|
1333
|
+
)
|
|
1334
|
+
continue
|
|
1335
|
+
raw = os.fspath(entry).strip()
|
|
1336
|
+
if not raw:
|
|
1337
|
+
_raise_config_error(
|
|
1338
|
+
location,
|
|
1339
|
+
"Invalid package-dirs entry: must be non-empty string",
|
|
1340
|
+
line_number,
|
|
1341
|
+
)
|
|
1342
|
+
continue
|
|
1343
|
+
path = Path(raw)
|
|
1344
|
+
if not path.is_absolute():
|
|
1345
|
+
path = base_dir / path
|
|
1346
|
+
try:
|
|
1347
|
+
path = path.resolve()
|
|
1348
|
+
except OSError:
|
|
1349
|
+
path = path
|
|
1350
|
+
normalized.append(str(path))
|
|
1351
|
+
return tuple(normalized)
|
|
1352
|
+
|
|
1353
|
+
|
|
1227
1354
|
def _assign_config_option(
|
|
1228
1355
|
config: TestConfig,
|
|
1229
1356
|
key: str,
|
|
@@ -1234,7 +1361,16 @@ def _assign_config_option(
|
|
|
1234
1361
|
origin: ConfigOrigin,
|
|
1235
1362
|
) -> None:
|
|
1236
1363
|
canonical = _canonical_config_key(key)
|
|
1237
|
-
valid_keys: set[str] = {
|
|
1364
|
+
valid_keys: set[str] = {
|
|
1365
|
+
"error",
|
|
1366
|
+
"timeout",
|
|
1367
|
+
"runner",
|
|
1368
|
+
"skip",
|
|
1369
|
+
"fixtures",
|
|
1370
|
+
"inputs",
|
|
1371
|
+
"retry",
|
|
1372
|
+
"package_dirs",
|
|
1373
|
+
}
|
|
1238
1374
|
if origin == "directory":
|
|
1239
1375
|
valid_keys.add("suite")
|
|
1240
1376
|
if canonical not in valid_keys:
|
|
@@ -1321,6 +1457,17 @@ def _assign_config_option(
|
|
|
1321
1457
|
value, location=location, line_number=line_number
|
|
1322
1458
|
)
|
|
1323
1459
|
return
|
|
1460
|
+
if canonical == "package_dirs":
|
|
1461
|
+
if origin != "directory":
|
|
1462
|
+
_raise_config_error(
|
|
1463
|
+
location,
|
|
1464
|
+
"'package-dirs' can only be specified in directory-level test.yaml files",
|
|
1465
|
+
line_number,
|
|
1466
|
+
)
|
|
1467
|
+
config[canonical] = _normalize_package_dirs_value(
|
|
1468
|
+
value, location=location, line_number=line_number
|
|
1469
|
+
)
|
|
1470
|
+
return
|
|
1324
1471
|
if canonical == "retry":
|
|
1325
1472
|
if origin == "frontmatter" and isinstance(config.get("suite"), str):
|
|
1326
1473
|
_raise_config_error(
|
|
@@ -1510,20 +1657,12 @@ def _iter_project_test_directories(root: Path) -> Iterator[Path]:
|
|
|
1510
1657
|
|
|
1511
1658
|
|
|
1512
1659
|
def _is_inputs_path(path: Path) -> bool:
|
|
1513
|
-
"""Return True when the path lives under
|
|
1660
|
+
"""Return True when the path lives under any inputs directory."""
|
|
1514
1661
|
try:
|
|
1515
1662
|
parts = path.relative_to(ROOT).parts
|
|
1516
1663
|
except ValueError:
|
|
1517
1664
|
parts = path.parts
|
|
1518
|
-
|
|
1519
|
-
for index, part in enumerate(parts):
|
|
1520
|
-
if part != "inputs":
|
|
1521
|
-
continue
|
|
1522
|
-
if index == 0:
|
|
1523
|
-
return True
|
|
1524
|
-
if index > 0 and parts[index - 1] == "tests":
|
|
1525
|
-
return True
|
|
1526
|
-
return False
|
|
1665
|
+
return "inputs" in parts
|
|
1527
1666
|
|
|
1528
1667
|
|
|
1529
1668
|
def _refresh_registry() -> None:
|
|
@@ -1556,6 +1695,25 @@ def _resolve_inputs_dir(root: Path) -> Path:
|
|
|
1556
1695
|
return direct
|
|
1557
1696
|
|
|
1558
1697
|
|
|
1698
|
+
def _find_nearest_inputs_dir(test: Path, root: Path) -> Path | None:
|
|
1699
|
+
"""Walk up from test directory to find nearest inputs/ directory.
|
|
1700
|
+
|
|
1701
|
+
Returns the nearest inputs/ directory between the test and root,
|
|
1702
|
+
or None if no inputs/ directory exists in that range.
|
|
1703
|
+
"""
|
|
1704
|
+
current = test.parent
|
|
1705
|
+
root_resolved = root.resolve()
|
|
1706
|
+
while True:
|
|
1707
|
+
candidate = current / "inputs"
|
|
1708
|
+
if candidate.is_dir():
|
|
1709
|
+
return candidate
|
|
1710
|
+
# Stop when we reach or pass the root
|
|
1711
|
+
if current.resolve() == root_resolved or current == current.parent:
|
|
1712
|
+
break
|
|
1713
|
+
current = current.parent
|
|
1714
|
+
return None
|
|
1715
|
+
|
|
1716
|
+
|
|
1559
1717
|
def _looks_like_project_root(path: Path) -> bool:
|
|
1560
1718
|
"""Return True when the path or one of its parents resembles a project root."""
|
|
1561
1719
|
|
|
@@ -1687,13 +1845,22 @@ def get_test_env_and_config_args(
|
|
|
1687
1845
|
config_args = [f"--config={config_file}"] if config_file.exists() else []
|
|
1688
1846
|
env = os.environ.copy()
|
|
1689
1847
|
if inputs is None:
|
|
1690
|
-
|
|
1848
|
+
# Try nearest inputs/ directory first, fall back to project-level
|
|
1849
|
+
nearest = _find_nearest_inputs_dir(test, ROOT)
|
|
1850
|
+
if nearest is not None:
|
|
1851
|
+
inputs_path = str(nearest.resolve())
|
|
1852
|
+
else:
|
|
1853
|
+
inputs_path = str(_resolve_inputs_dir(ROOT).resolve())
|
|
1691
1854
|
else:
|
|
1692
1855
|
candidate = Path(os.fspath(inputs))
|
|
1693
1856
|
if not candidate.is_absolute():
|
|
1694
1857
|
candidate = test.parent / candidate
|
|
1695
1858
|
inputs_path = str(candidate.resolve())
|
|
1696
1859
|
env["TENZIR_INPUTS"] = inputs_path
|
|
1860
|
+
# Check for inline input file (.input extension)
|
|
1861
|
+
inline_input = test.with_suffix(".input")
|
|
1862
|
+
if inline_input.is_file():
|
|
1863
|
+
env["TENZIR_INPUT"] = str(inline_input.resolve())
|
|
1697
1864
|
if config_file.exists():
|
|
1698
1865
|
env.setdefault("TENZIR_CONFIG", str(config_file))
|
|
1699
1866
|
if node_config_file.exists():
|
|
@@ -2556,6 +2723,8 @@ def get_version() -> str:
|
|
|
2556
2723
|
|
|
2557
2724
|
|
|
2558
2725
|
def success(test: Path) -> None:
|
|
2726
|
+
if not is_verbose_output():
|
|
2727
|
+
return
|
|
2559
2728
|
with stdout_lock:
|
|
2560
2729
|
rel_test = _relativize_path(test)
|
|
2561
2730
|
suite_suffix = _format_suite_suffix()
|
|
@@ -2563,6 +2732,10 @@ def success(test: Path) -> None:
|
|
|
2563
2732
|
print(f"{CHECKMARK} {rel_test}{suite_suffix}{attempt_suffix}")
|
|
2564
2733
|
|
|
2565
2734
|
|
|
2735
|
+
# Failures always print regardless of verbose mode because failures are critical
|
|
2736
|
+
# information that users need to see immediately. In contrast, success() and
|
|
2737
|
+
# handle_skip() respect the verbose setting since passed/skipped tests are
|
|
2738
|
+
# expected outcomes that can be summarized at the end.
|
|
2566
2739
|
def fail(test: Path) -> None:
|
|
2567
2740
|
with stdout_lock:
|
|
2568
2741
|
rel_test = _relativize_path(test)
|
|
@@ -2710,14 +2883,32 @@ def run_simple_test(
|
|
|
2710
2883
|
expect_error = bool(test_config.get("error", False))
|
|
2711
2884
|
passthrough_mode = is_passthrough_enabled()
|
|
2712
2885
|
|
|
2886
|
+
config_package_dirs = cast(tuple[str, ...], test_config.get("package_dirs", tuple()))
|
|
2887
|
+
additional_package_dirs: list[str] = []
|
|
2888
|
+
for entry in config_package_dirs:
|
|
2889
|
+
additional_package_dirs.extend(_expand_package_dirs(Path(entry)))
|
|
2890
|
+
|
|
2713
2891
|
package_root = packages.find_package_root(test)
|
|
2714
2892
|
package_args: list[str] = []
|
|
2893
|
+
package_dir_candidates: list[str] = []
|
|
2715
2894
|
if package_root is not None:
|
|
2716
2895
|
env["TENZIR_PACKAGE_ROOT"] = str(package_root)
|
|
2717
2896
|
package_tests_root = package_root / "tests"
|
|
2718
2897
|
if inputs_override is None:
|
|
2719
|
-
|
|
2720
|
-
|
|
2898
|
+
# Try nearest inputs/ directory, fall back to package-level
|
|
2899
|
+
nearest = _find_nearest_inputs_dir(test, package_root)
|
|
2900
|
+
if nearest is not None:
|
|
2901
|
+
env["TENZIR_INPUTS"] = str(nearest.resolve())
|
|
2902
|
+
else:
|
|
2903
|
+
env["TENZIR_INPUTS"] = str(package_tests_root / "inputs")
|
|
2904
|
+
package_dir_candidates.append(str(package_root))
|
|
2905
|
+
package_dir_candidates.extend(additional_package_dirs)
|
|
2906
|
+
for cli_path in _get_cli_packages():
|
|
2907
|
+
package_dir_candidates.extend(_expand_package_dirs(cli_path))
|
|
2908
|
+
if package_dir_candidates:
|
|
2909
|
+
merged_dirs = _deduplicate_package_dirs(package_dir_candidates)
|
|
2910
|
+
env["TENZIR_PACKAGE_DIRS"] = ",".join(merged_dirs)
|
|
2911
|
+
package_args.append(f"--package-dirs={','.join(merged_dirs)}")
|
|
2721
2912
|
|
|
2722
2913
|
context_token = fixtures_impl.push_context(
|
|
2723
2914
|
fixtures_impl.FixtureContext(
|
|
@@ -2859,9 +3050,10 @@ def run_simple_test(
|
|
|
2859
3050
|
|
|
2860
3051
|
|
|
2861
3052
|
def handle_skip(reason: str, test: Path, update: bool, output_ext: str) -> bool | str:
|
|
2862
|
-
|
|
2863
|
-
|
|
2864
|
-
|
|
3053
|
+
if is_verbose_output():
|
|
3054
|
+
rel_path = _relativize_path(test)
|
|
3055
|
+
suite_suffix = _format_suite_suffix()
|
|
3056
|
+
print(f"{SKIP} skipped {rel_path}{suite_suffix}: {reason}")
|
|
2865
3057
|
ref_path = test.with_suffix(f".{output_ext}")
|
|
2866
3058
|
if update:
|
|
2867
3059
|
with ref_path.open("wb") as f:
|
|
@@ -2975,12 +3167,29 @@ class Worker:
|
|
|
2975
3167
|
raise RuntimeError(f"failed to parse suite config for {primary_test}: {exc}") from exc
|
|
2976
3168
|
inputs_override = typing.cast(str | None, primary_config.get("inputs"))
|
|
2977
3169
|
env, config_args = get_test_env_and_config_args(primary_test, inputs=inputs_override)
|
|
3170
|
+
config_package_dirs = cast(tuple[str, ...], primary_config.get("package_dirs", tuple()))
|
|
3171
|
+
additional_package_dirs: list[str] = []
|
|
3172
|
+
for entry in config_package_dirs:
|
|
3173
|
+
additional_package_dirs.extend(_expand_package_dirs(Path(entry)))
|
|
2978
3174
|
package_root = packages.find_package_root(primary_test)
|
|
3175
|
+
package_dir_candidates: list[str] = []
|
|
2979
3176
|
if package_root is not None:
|
|
2980
3177
|
env["TENZIR_PACKAGE_ROOT"] = str(package_root)
|
|
2981
3178
|
if inputs_override is None:
|
|
2982
|
-
|
|
2983
|
-
|
|
3179
|
+
# Try nearest inputs/ directory, fall back to package-level
|
|
3180
|
+
nearest = _find_nearest_inputs_dir(primary_test, package_root)
|
|
3181
|
+
if nearest is not None:
|
|
3182
|
+
env["TENZIR_INPUTS"] = str(nearest.resolve())
|
|
3183
|
+
else:
|
|
3184
|
+
env["TENZIR_INPUTS"] = str((package_root / "tests" / "inputs"))
|
|
3185
|
+
package_dir_candidates.append(str(package_root))
|
|
3186
|
+
package_dir_candidates.extend(additional_package_dirs)
|
|
3187
|
+
for cli_path in _get_cli_packages():
|
|
3188
|
+
package_dir_candidates.extend(_expand_package_dirs(cli_path))
|
|
3189
|
+
if package_dir_candidates:
|
|
3190
|
+
merged_dirs = _deduplicate_package_dirs(package_dir_candidates)
|
|
3191
|
+
env["TENZIR_PACKAGE_DIRS"] = ",".join(merged_dirs)
|
|
3192
|
+
config_args = list(config_args) + [f"--package-dirs={','.join(merged_dirs)}"]
|
|
2984
3193
|
context_token = fixtures_impl.push_context(
|
|
2985
3194
|
fixtures_impl.FixtureContext(
|
|
2986
3195
|
test=primary_test,
|
|
@@ -3152,6 +3361,7 @@ def run_cli(
|
|
|
3152
3361
|
root: Path | None,
|
|
3153
3362
|
tenzir_binary: Path | None,
|
|
3154
3363
|
tenzir_node_binary: Path | None,
|
|
3364
|
+
package_dirs: Sequence[Path] | None = None,
|
|
3155
3365
|
tests: Sequence[Path],
|
|
3156
3366
|
update: bool,
|
|
3157
3367
|
debug: bool,
|
|
@@ -3166,10 +3376,16 @@ def run_cli(
|
|
|
3166
3376
|
jobs: int,
|
|
3167
3377
|
keep_tmp_dirs: bool,
|
|
3168
3378
|
passthrough: bool,
|
|
3379
|
+
verbose: bool = False,
|
|
3169
3380
|
jobs_overridden: bool = False,
|
|
3170
3381
|
all_projects: bool = False,
|
|
3171
3382
|
) -> ExecutionResult:
|
|
3172
|
-
"""Execute the harness and return a structured result for library consumers.
|
|
3383
|
+
"""Execute the harness and return a structured result for library consumers.
|
|
3384
|
+
|
|
3385
|
+
Args:
|
|
3386
|
+
verbose: Print individual test results (pass/skip) as they complete.
|
|
3387
|
+
When False (default), only failures are printed during execution.
|
|
3388
|
+
"""
|
|
3173
3389
|
from tenzir_test.engine import state as engine_state
|
|
3174
3390
|
|
|
3175
3391
|
try:
|
|
@@ -3222,6 +3438,8 @@ def run_cli(
|
|
|
3222
3438
|
harness_mode = HarnessMode.COMPARE
|
|
3223
3439
|
set_harness_mode(harness_mode)
|
|
3224
3440
|
passthrough_mode = harness_mode is HarnessMode.PASSTHROUGH
|
|
3441
|
+
# Passthrough mode requires verbose output to show real-time test results
|
|
3442
|
+
set_verbose_output(verbose or passthrough_mode)
|
|
3225
3443
|
if passthrough_mode and jobs > 1:
|
|
3226
3444
|
if jobs_overridden:
|
|
3227
3445
|
print(f"{INFO} forcing --jobs=1 in passthrough mode to preserve output ordering")
|
|
@@ -3236,6 +3454,7 @@ def run_cli(
|
|
|
3236
3454
|
tenzir_node_binary=tenzir_node_binary,
|
|
3237
3455
|
)
|
|
3238
3456
|
apply_settings(settings)
|
|
3457
|
+
_set_cli_packages(list(package_dirs or []))
|
|
3239
3458
|
selected_tests = list(tests)
|
|
3240
3459
|
|
|
3241
3460
|
plan: ExecutionPlan | None = None
|
|
@@ -3494,7 +3713,8 @@ def run_cli(
|
|
|
3494
3713
|
_print_compact_summary(project_summary)
|
|
3495
3714
|
summary_enabled = show_summary or runner_summary or fixture_summary
|
|
3496
3715
|
if summary_enabled:
|
|
3497
|
-
|
|
3716
|
+
if is_verbose_output():
|
|
3717
|
+
_print_detailed_summary(project_summary)
|
|
3498
3718
|
_print_ascii_summary(
|
|
3499
3719
|
project_summary,
|
|
3500
3720
|
include_runner=runner_summary,
|
|
@@ -3592,11 +3812,16 @@ def execute(
|
|
|
3592
3812
|
jobs: int | None = None,
|
|
3593
3813
|
keep_tmp_dirs: bool = False,
|
|
3594
3814
|
passthrough: bool = False,
|
|
3815
|
+
verbose: bool = False,
|
|
3595
3816
|
jobs_overridden: bool = False,
|
|
3596
3817
|
all_projects: bool = False,
|
|
3597
3818
|
) -> ExecutionResult:
|
|
3598
|
-
"""Library-oriented wrapper around `run_cli` with defaulted parameters.
|
|
3819
|
+
"""Library-oriented wrapper around `run_cli` with defaulted parameters.
|
|
3599
3820
|
|
|
3821
|
+
Args:
|
|
3822
|
+
verbose: Print individual test results (pass/skip) as they complete.
|
|
3823
|
+
When False (default), only failures are printed during execution.
|
|
3824
|
+
"""
|
|
3600
3825
|
resolved_jobs = jobs if jobs is not None else get_default_jobs()
|
|
3601
3826
|
return run_cli(
|
|
3602
3827
|
root=root,
|
|
@@ -3616,6 +3841,7 @@ def execute(
|
|
|
3616
3841
|
jobs=resolved_jobs,
|
|
3617
3842
|
keep_tmp_dirs=keep_tmp_dirs,
|
|
3618
3843
|
passthrough=passthrough,
|
|
3844
|
+
verbose=verbose,
|
|
3619
3845
|
jobs_overridden=jobs_overridden,
|
|
3620
3846
|
all_projects=all_projects,
|
|
3621
3847
|
)
|
|
@@ -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.15.0
|
|
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=e93O5dyp6oeiuW3Im11wWyuOc6duKTwUJmcmFuoVnoE,6803
|
|
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=OmW-qaWK6Y9vCAWZmnMczz-ycaleMa9jmiPcATpCoL8,133280
|
|
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.15.0.dist-info/METADATA,sha256=MWDDBuFmf5BU8h1kBsj591GmYUPgv58-IZ357_iu0n8,3066
|
|
25
|
+
tenzir_test-0.15.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
26
|
+
tenzir_test-0.15.0.dist-info/entry_points.txt,sha256=l8DJgiEVrjScdTTo613cZ3PKodOmqrUVIbz-3awfV8w,53
|
|
27
|
+
tenzir_test-0.15.0.dist-info/licenses/LICENSE,sha256=ajMbpcBiSTXI8Rr4t17pvowV-On8DktghfZKxY_A22Q,10750
|
|
28
|
+
tenzir_test-0.15.0.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
|