tenzir-test 0.9.4__py3-none-any.whl → 0.10.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/run.py CHANGED
@@ -169,6 +169,7 @@ PASS_MAX_COLOR = "\033[92m"
169
169
  FAIL_COLOR = "\033[31m"
170
170
  SKIP_COLOR = "\033[90;1m"
171
171
  RESET_COLOR = "\033[0m"
172
+ DETAIL_COLOR = "\033[2;37m"
172
173
  PASS_SPECTRUM = [
173
174
  "\033[38;5;52m", # 0-9% deep red
174
175
  "\033[38;5;88m", # 10-19% red
@@ -349,6 +350,7 @@ _TMP_ROOT_NAME = ".tenzir-test"
349
350
  _TMP_SUBDIR_NAME = "tmp"
350
351
  _TMP_BASE_DIRS: set[Path] = set()
351
352
  _ACTIVE_TMP_DIRS: set[Path] = set()
353
+ _TMP_DIR_LOCK = threading.Lock()
352
354
  KEEP_TMP_DIRS = bool(os.environ.get(_TMP_KEEP_ENV_VAR))
353
355
 
354
356
  SHOW_DIFF_OUTPUT = True
@@ -506,10 +508,13 @@ def _tmp_prefix_for(test: Path) -> str:
506
508
 
507
509
 
508
510
  def _create_test_tmp_dir(test: Path) -> Path:
509
- base = _resolve_tmp_base()
510
511
  prefix = f"{_tmp_prefix_for(test)}-"
511
- path = Path(tempfile.mkdtemp(prefix=prefix, dir=str(base)))
512
- _ACTIVE_TMP_DIRS.add(path)
512
+ with _TMP_DIR_LOCK:
513
+ base = _resolve_tmp_base()
514
+ if not base.exists():
515
+ base.mkdir(parents=True, exist_ok=True)
516
+ path = Path(tempfile.mkdtemp(prefix=prefix, dir=str(base)))
517
+ _ACTIVE_TMP_DIRS.add(path)
513
518
  return path
514
519
 
515
520
 
@@ -522,22 +527,27 @@ def cleanup_test_tmp_dir(path: str | os.PathLike[str] | None) -> None:
522
527
  if not path:
523
528
  return
524
529
  tmp_path = Path(path)
525
- _ACTIVE_TMP_DIRS.discard(tmp_path)
530
+ with _TMP_DIR_LOCK:
531
+ _ACTIVE_TMP_DIRS.discard(tmp_path)
526
532
  try:
527
533
  fixtures_impl.invoke_tmp_dir_cleanup(tmp_path)
528
534
  except Exception: # pragma: no cover - defensive logging
529
535
  pass
530
536
  if KEEP_TMP_DIRS:
531
537
  return
532
- if tmp_path.exists():
533
- shutil.rmtree(tmp_path, ignore_errors=True)
534
- _cleanup_tmp_base_dirs()
538
+ with _TMP_DIR_LOCK:
539
+ if tmp_path.exists():
540
+ shutil.rmtree(tmp_path, ignore_errors=True)
541
+ _cleanup_tmp_base_dirs()
535
542
 
536
543
 
537
544
  def _cleanup_remaining_tmp_dirs() -> None:
538
- for tmp_path in list(_ACTIVE_TMP_DIRS):
545
+ with _TMP_DIR_LOCK:
546
+ remaining = list(_ACTIVE_TMP_DIRS)
547
+ for tmp_path in remaining:
539
548
  cleanup_test_tmp_dir(tmp_path)
540
- _cleanup_tmp_base_dirs()
549
+ with _TMP_DIR_LOCK:
550
+ _cleanup_tmp_base_dirs()
541
551
 
542
552
 
543
553
  def _cleanup_all_tmp_dirs() -> None:
@@ -545,9 +555,12 @@ def _cleanup_all_tmp_dirs() -> None:
545
555
 
546
556
  if KEEP_TMP_DIRS:
547
557
  return
548
- for tmp_path in list(_ACTIVE_TMP_DIRS):
558
+ with _TMP_DIR_LOCK:
559
+ remaining = list(_ACTIVE_TMP_DIRS)
560
+ for tmp_path in remaining:
549
561
  cleanup_test_tmp_dir(tmp_path)
550
- _cleanup_tmp_base_dirs()
562
+ with _TMP_DIR_LOCK:
563
+ _cleanup_tmp_base_dirs()
551
564
 
552
565
 
553
566
  def _cleanup_tmp_base_dirs() -> None:
@@ -2620,9 +2633,13 @@ def run_simple_test(
2620
2633
  )
2621
2634
  good = completed.returncode == 0
2622
2635
  output = b""
2636
+ stderr_output = b""
2623
2637
  if not passthrough_mode:
2624
- captured = completed.stdout or b""
2625
- output = captured.replace(str(ROOT).encode() + b"/", b"")
2638
+ root_bytes = str(ROOT).encode() + b"/"
2639
+ captured_stdout = completed.stdout or b""
2640
+ output = captured_stdout.replace(root_bytes, b"")
2641
+ captured_stderr = completed.stderr or b""
2642
+ stderr_output = captured_stderr.replace(root_bytes, b"")
2626
2643
  except subprocess.TimeoutExpired:
2627
2644
  report_failure(test, f"└─▶ \033[31msubprocess hit {timeout}s timeout\033[0m")
2628
2645
  return False
@@ -2642,20 +2659,30 @@ def run_simple_test(
2642
2659
  return False
2643
2660
  if interrupted:
2644
2661
  _request_interrupt()
2645
- message = (
2662
+ summary_line = (
2646
2663
  _INTERRUPTED_NOTICE
2647
2664
  if interrupted
2648
- else f"┌─▶ \033[31mgot unexpected exit code {completed.returncode}\033[0m"
2665
+ else f"└─▶ \033[31mgot unexpected exit code {completed.returncode}\033[0m"
2649
2666
  )
2650
2667
  if passthrough_mode:
2651
- report_failure(test, message)
2668
+ report_failure(test, summary_line)
2652
2669
  else:
2653
2670
  with stdout_lock:
2654
- report_failure(test, message)
2671
+ fail(test)
2655
2672
  if not interrupted:
2656
- for last, line in last_and(output.split(b"\n")):
2657
- prefix = "│ " if not last else "└─"
2658
- sys.stdout.buffer.write(prefix.encode() + line + b"\n")
2673
+ line_prefix = "│ ".encode()
2674
+ for line in output.splitlines():
2675
+ sys.stdout.buffer.write(line_prefix + line + b"\n")
2676
+ if completed.returncode != 0 and stderr_output:
2677
+ sys.stdout.write("├─▶ stderr\n")
2678
+ detail_prefix = DETAIL_COLOR.encode()
2679
+ reset_bytes = RESET_COLOR.encode()
2680
+ for line in stderr_output.splitlines():
2681
+ sys.stdout.buffer.write(
2682
+ line_prefix + detail_prefix + line + reset_bytes + b"\n"
2683
+ )
2684
+ if summary_line:
2685
+ sys.stdout.write(summary_line + "\n")
2659
2686
  return False
2660
2687
  if passthrough_mode:
2661
2688
  success(test)
@@ -64,47 +64,49 @@ class ShellRunner(ExtRunner):
64
64
  fixture_api.pop_context(context_token)
65
65
  run_mod.cleanup_test_tmp_dir(env.get(run_mod.TEST_TMP_ENV_VAR))
66
66
 
67
+ stdout_data = completed.stdout
68
+ if isinstance(stdout_data, str):
69
+ stdout_bytes: bytes = stdout_data.encode()
70
+ else:
71
+ stdout_bytes = stdout_data or b""
72
+
73
+ stderr_data = completed.stderr
74
+ if isinstance(stderr_data, str):
75
+ stderr_bytes: bytes = stderr_data.encode()
76
+ else:
77
+ stderr_bytes = stderr_data or b""
78
+
67
79
  good = completed.returncode == 0
68
80
  if expect_error == good:
69
81
  suppressed = run_mod.should_suppress_failure_output()
82
+ summary_line = f"└─▶ \033[31mgot unexpected exit code {completed.returncode}\033[0m"
70
83
  if passthrough:
71
84
  if not suppressed:
72
- run_mod.report_failure(
73
- test,
74
- f"┌─▶ \033[31mgot unexpected exit code {completed.returncode}\033[0m",
75
- )
85
+ run_mod.report_failure(test, summary_line)
76
86
  return False
77
87
  if suppressed:
78
88
  return False
89
+
79
90
  with run_mod.stdout_lock:
80
- run_mod.report_failure(
81
- test,
82
- f"┌─▶ \033[31mgot unexpected exit code {completed.returncode}\033[0m",
83
- )
84
- stdout = completed.stdout or b""
85
- for last, line in run_mod.last_and(stdout.split(b"\n")):
86
- prefix = "│ " if not last else "└─"
87
- sys.stdout.buffer.write(prefix.encode() + line + b"\n")
88
- if completed.stderr:
89
- sys.stdout.buffer.write(completed.stderr)
91
+ run_mod.fail(test)
92
+ line_prefix = "│ ".encode()
93
+ for line in stdout_bytes.splitlines():
94
+ sys.stdout.buffer.write(line_prefix + line + b"\n")
95
+ if stderr_bytes:
96
+ sys.stdout.write("├─▶ stderr\n")
97
+ detail_prefix = run_mod.DETAIL_COLOR.encode()
98
+ reset_bytes = run_mod.RESET_COLOR.encode()
99
+ for line in stderr_bytes.splitlines():
100
+ sys.stdout.buffer.write(
101
+ line_prefix + detail_prefix + line + reset_bytes + b"\n"
102
+ )
103
+ sys.stdout.write(summary_line + "\n")
90
104
  return False
91
105
 
92
106
  if passthrough:
93
107
  run_mod.success(test)
94
108
  return True
95
109
 
96
- stdout_data = completed.stdout
97
- if isinstance(stdout_data, str):
98
- stdout_bytes: bytes = stdout_data.encode()
99
- else:
100
- stdout_bytes = stdout_data or b""
101
-
102
- stderr_data = completed.stderr
103
- if isinstance(stderr_data, str):
104
- stderr_bytes: bytes = stderr_data.encode()
105
- else:
106
- stderr_bytes = stderr_data or b""
107
-
108
110
  root_prefix: bytes = (str(run_mod.ROOT) + "/").encode()
109
111
  stdout_bytes = stdout_bytes.replace(root_prefix, b"")
110
112
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tenzir-test
3
- Version: 0.9.4
3
+ Version: 0.10.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
@@ -55,36 +55,6 @@ uvx tenzir-test --help
55
55
  `uvx` downloads the newest compatible release, runs it in an isolated
56
56
  environment, and caches subsequent invocations for fast reuse.
57
57
 
58
- ## 🚀 Quick Start
59
-
60
- Create a project skeleton that mirrors the layout the harness expects:
61
-
62
- ```text
63
- project-root/
64
- ├── fixtures/
65
- │ └── http.py
66
- ├── inputs/
67
- │ └── sample.ndjson
68
- ├── runners/
69
- │ └── __init__.py
70
- └── tests/
71
- ├── alerts/
72
- │ ├── sample.py
73
- │ └── sample.txt
74
- └── regression/
75
- ├── dummy.tql
76
- └── dummy.txt
77
- ```
78
-
79
- 1. Author fixtures in `fixtures/` and register them at import time.
80
- 2. Store reusable datasets in `inputs/`—the harness exposes the path via
81
- `TENZIR_INPUTS` and provides a per-test scratch directory through
82
- `TENZIR_TMP_DIR` when tests execute.
83
- Use `--keep` (or `-k`) to preserve those temporary directories for debugging.
84
- 3. Create tests in `tests/` and pair them with reference artifacts (for example
85
- `.txt`) that the harness compares against.
86
- 4. Run `uvx tenzir-test` from the project root to execute the full suite.
87
-
88
58
  ## 📚 Documentation
89
59
 
90
60
  Consult our [user guide](https://docs.tenzir.com/guides/testing/write-tests)
@@ -5,7 +5,7 @@ tenzir_test/cli.py,sha256=O1FaxYFKWal--aqs-ZFT2URqL26_HjUtFNDACIBspXA,5452
5
5
  tenzir_test/config.py,sha256=q1_VEXuxL-xsGlnooeGvXxx9cMw652UEB9a1mPzZIQs,1680
6
6
  tenzir_test/packages.py,sha256=cTCQdGjCS1XmuKyiwh0ew-z9tHn6J-xZ6nvBP-hU8bc,948
7
7
  tenzir_test/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
8
- tenzir_test/run.py,sha256=X5k81jdAV4f3eH4pMFq3_B0YW7JIqBwE3j2vK5UgK3s,115729
8
+ tenzir_test/run.py,sha256=W0NMpftm4MG6oiVFO0rjUxgS-h-JjFKyUTDLM3U7bS8,116871
9
9
  tenzir_test/engine/__init__.py,sha256=5APwy90YDm7rmL_qCZfToAcfbQthcZ8yV2_ExXKqaqE,110
10
10
  tenzir_test/engine/operations.py,sha256=OCYjuMHyMAaay4s08u2Sl7oE-PmgeXumylp7R8GYIH4,950
11
11
  tenzir_test/engine/registry.py,sha256=LXCr6TGlv1sR1m1eboTk7SrbS2IVErc3PqUuHxGA2xk,594
@@ -19,11 +19,11 @@ tenzir_test/runners/custom_python_fixture_runner.py,sha256=bbG0ZvCL6Cs6BCKJQQPAB
19
19
  tenzir_test/runners/diff_runner.py,sha256=ah1hr1vvD6BON2PZz61mxwioRFIzHFuaAbJ0DjDSqG4,5151
20
20
  tenzir_test/runners/ext_runner.py,sha256=sKL9Mw_ksVVBWnrdIJR2WS5ueVnLKuNYYWZ22FTZIPo,730
21
21
  tenzir_test/runners/runner.py,sha256=LtlD8huQOSmD7RyYDnKeCuI4Y6vhxGXMKsHA2qgfWN0,989
22
- tenzir_test/runners/shell_runner.py,sha256=wLiHYGrZDHPof6hohDT-t6zEi8w-DlkR5QG0i5DpwWE,5892
22
+ tenzir_test/runners/shell_runner.py,sha256=EREqIaHxG5_nl8CmeQYWsiM6rZS9frCkhdaUHoRUGRw,6024
23
23
  tenzir_test/runners/tenzir_runner.py,sha256=464FFYS_mh6l-ehccc-S8cIUO1MxdapwQL5X3PmMkMI,1006
24
24
  tenzir_test/runners/tql_runner.py,sha256=2ZLMf3TIKwcOvaOFrVvvhzK-EcWmGOUZxKkbSoByyQA,248
25
- tenzir_test-0.9.4.dist-info/METADATA,sha256=V6OEJxdUSsQnUR6osqMM9L_-WZAD2xa1VXjvNaHs_zI,3982
26
- tenzir_test-0.9.4.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
27
- tenzir_test-0.9.4.dist-info/entry_points.txt,sha256=q0eD9RQ_9eMPYvFNpBElo55HQYeaPgLfe9YhLsNwl10,93
28
- tenzir_test-0.9.4.dist-info/licenses/LICENSE,sha256=ajMbpcBiSTXI8Rr4t17pvowV-On8DktghfZKxY_A22Q,10750
29
- tenzir_test-0.9.4.dist-info/RECORD,,
25
+ tenzir_test-0.10.0.dist-info/METADATA,sha256=12vESrRyq_hSo-r2ljuX6PptSeWWPPMXtx_l04JfnRE,3008
26
+ tenzir_test-0.10.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
27
+ tenzir_test-0.10.0.dist-info/entry_points.txt,sha256=q0eD9RQ_9eMPYvFNpBElo55HQYeaPgLfe9YhLsNwl10,93
28
+ tenzir_test-0.10.0.dist-info/licenses/LICENSE,sha256=ajMbpcBiSTXI8Rr4t17pvowV-On8DktghfZKxY_A22Q,10750
29
+ tenzir_test-0.10.0.dist-info/RECORD,,