tenzir-test 0.9.3__tar.gz → 0.9.4__tar.gz

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.
Files changed (81) hide show
  1. {tenzir_test-0.9.3 → tenzir_test-0.9.4}/PKG-INFO +1 -1
  2. tenzir_test-0.9.4/example-project/tests/fail.tql +3 -0
  3. tenzir_test-0.9.4/example-project/tests/fail.txt +28 -0
  4. tenzir_test-0.9.4/example-project/tests/shell/http-fixture-check.txt +0 -0
  5. {tenzir_test-0.9.3 → tenzir_test-0.9.4}/pyproject.toml +1 -1
  6. {tenzir_test-0.9.3 → tenzir_test-0.9.4}/src/tenzir_test/cli.py +16 -0
  7. tenzir_test-0.9.4/src/tenzir_test/py.typed +0 -0
  8. {tenzir_test-0.9.3 → tenzir_test-0.9.4}/src/tenzir_test/run.py +121 -20
  9. {tenzir_test-0.9.3 → tenzir_test-0.9.4}/tests/test_cli.py +26 -0
  10. {tenzir_test-0.9.3 → tenzir_test-0.9.4}/tests/test_run.py +141 -0
  11. {tenzir_test-0.9.3 → tenzir_test-0.9.4}/.gitignore +0 -0
  12. {tenzir_test-0.9.3 → tenzir_test-0.9.4}/LICENSE +0 -0
  13. {tenzir_test-0.9.3 → tenzir_test-0.9.4}/README.md +0 -0
  14. {tenzir_test-0.9.3 → tenzir_test-0.9.4}/example-project/README.md +0 -0
  15. {tenzir_test-0.9.3 → tenzir_test-0.9.4}/example-project/fixtures/README.md +0 -0
  16. {tenzir_test-0.9.3 → tenzir_test-0.9.4}/example-project/fixtures/__init__.py +0 -0
  17. {tenzir_test-0.9.3 → tenzir_test-0.9.4}/example-project/fixtures/http.py +0 -0
  18. {tenzir_test-0.9.3 → tenzir_test-0.9.4}/example-project/fixtures/server.py +0 -0
  19. {tenzir_test-0.9.3 → tenzir_test-0.9.4}/example-project/inputs/events.ndjson +0 -0
  20. {tenzir_test-0.9.3 → tenzir_test-0.9.4}/example-project/runners/__init__.py +0 -0
  21. {tenzir_test-0.9.3 → tenzir_test-0.9.4}/example-project/runners/xxd.py +0 -0
  22. {tenzir_test-0.9.3 → tenzir_test-0.9.4}/example-project/tests/context/01-context-create.tql +0 -0
  23. {tenzir_test-0.9.3 → tenzir_test-0.9.4}/example-project/tests/context/01-context-create.txt +0 -0
  24. {tenzir_test-0.9.3 → tenzir_test-0.9.4}/example-project/tests/context/02-context-update.tql +0 -0
  25. {tenzir_test-0.9.3 → tenzir_test-0.9.4}/example-project/tests/context/02-context-update.txt +0 -0
  26. {tenzir_test-0.9.3 → tenzir_test-0.9.4}/example-project/tests/context/03-context-inspect.tql +0 -0
  27. {tenzir_test-0.9.3 → tenzir_test-0.9.4}/example-project/tests/context/03-context-inspect.txt +0 -0
  28. {tenzir_test-0.9.3 → tenzir_test-0.9.4}/example-project/tests/context/test.yaml +0 -0
  29. {tenzir_test-0.9.3 → tenzir_test-0.9.4}/example-project/tests/hex/hello.txt +0 -0
  30. {tenzir_test-0.9.3 → tenzir_test-0.9.4}/example-project/tests/hex/hello.xxd +0 -0
  31. {tenzir_test-0.9.3 → tenzir_test-0.9.4}/example-project/tests/http-fixture.tql +0 -0
  32. {tenzir_test-0.9.3 → tenzir_test-0.9.4}/example-project/tests/http-fixture.txt +0 -0
  33. {tenzir_test-0.9.3 → tenzir_test-0.9.4}/example-project/tests/lazy.tql +0 -0
  34. /tenzir_test-0.9.3/src/tenzir_test/py.typed → /tenzir_test-0.9.4/example-project/tests/lazy.txt +0 -0
  35. {tenzir_test-0.9.3 → tenzir_test-0.9.4}/example-project/tests/node-fixture.tql +0 -0
  36. {tenzir_test-0.9.3 → tenzir_test-0.9.4}/example-project/tests/node-fixture.txt +0 -0
  37. {tenzir_test-0.9.3 → tenzir_test-0.9.4}/example-project/tests/python/executor-only/sum.py +0 -0
  38. {tenzir_test-0.9.3 → tenzir_test-0.9.4}/example-project/tests/python/executor-only/sum.txt +0 -0
  39. {tenzir_test-0.9.3 → tenzir_test-0.9.4}/example-project/tests/python/executor-with-http-fixture/request.py +0 -0
  40. {tenzir_test-0.9.3 → tenzir_test-0.9.4}/example-project/tests/python/executor-with-http-fixture/request.txt +0 -0
  41. {tenzir_test-0.9.3 → tenzir_test-0.9.4}/example-project/tests/python/executor-with-http-fixture/test.yaml +0 -0
  42. {tenzir_test-0.9.3 → tenzir_test-0.9.4}/example-project/tests/python/executor-with-node-fixture/context-manager.py +0 -0
  43. {tenzir_test-0.9.3 → tenzir_test-0.9.4}/example-project/tests/python/executor-with-node-fixture/context-manager.txt +0 -0
  44. {tenzir_test-0.9.3 → tenzir_test-0.9.4}/example-project/tests/python/fixture-driving/manual_control.py +0 -0
  45. {tenzir_test-0.9.3 → tenzir_test-0.9.4}/example-project/tests/python/fixture-driving/manual_control.txt +0 -0
  46. {tenzir_test-0.9.3 → tenzir_test-0.9.4}/example-project/tests/python/pure-python/flaky_coin.py +0 -0
  47. {tenzir_test-0.9.3 → tenzir_test-0.9.4}/example-project/tests/python/pure-python/flaky_coin.txt +0 -0
  48. {tenzir_test-0.9.3 → tenzir_test-0.9.4}/example-project/tests/python/pure-python/hello_world.py +0 -0
  49. {tenzir_test-0.9.3 → tenzir_test-0.9.4}/example-project/tests/python/pure-python/hello_world.txt +0 -0
  50. {tenzir_test-0.9.3 → tenzir_test-0.9.4}/example-project/tests/read-inputs.tql +0 -0
  51. {tenzir_test-0.9.3 → tenzir_test-0.9.4}/example-project/tests/read-inputs.txt +0 -0
  52. {tenzir_test-0.9.3 → tenzir_test-0.9.4}/example-project/tests/shell/http-fixture-check.sh +0 -0
  53. {tenzir_test-0.9.3 → tenzir_test-0.9.4}/example-project/tests/shell/tmp-dir.sh +0 -0
  54. {tenzir_test-0.9.3 → tenzir_test-0.9.4}/example-project/tests/shell/tmp-dir.txt +0 -0
  55. {tenzir_test-0.9.3 → tenzir_test-0.9.4}/src/tenzir_test/__init__.py +0 -0
  56. {tenzir_test-0.9.3 → tenzir_test-0.9.4}/src/tenzir_test/_python_runner.py +0 -0
  57. {tenzir_test-0.9.3 → tenzir_test-0.9.4}/src/tenzir_test/checks.py +0 -0
  58. {tenzir_test-0.9.3 → tenzir_test-0.9.4}/src/tenzir_test/config.py +0 -0
  59. {tenzir_test-0.9.3 → tenzir_test-0.9.4}/src/tenzir_test/engine/__init__.py +0 -0
  60. {tenzir_test-0.9.3 → tenzir_test-0.9.4}/src/tenzir_test/engine/operations.py +0 -0
  61. {tenzir_test-0.9.3 → tenzir_test-0.9.4}/src/tenzir_test/engine/registry.py +0 -0
  62. {tenzir_test-0.9.3 → tenzir_test-0.9.4}/src/tenzir_test/engine/state.py +0 -0
  63. {tenzir_test-0.9.3 → tenzir_test-0.9.4}/src/tenzir_test/engine/worker.py +0 -0
  64. {tenzir_test-0.9.3 → tenzir_test-0.9.4}/src/tenzir_test/fixtures/__init__.py +0 -0
  65. {tenzir_test-0.9.3 → tenzir_test-0.9.4}/src/tenzir_test/fixtures/node.py +0 -0
  66. {tenzir_test-0.9.3 → tenzir_test-0.9.4}/src/tenzir_test/packages.py +0 -0
  67. {tenzir_test-0.9.3 → tenzir_test-0.9.4}/src/tenzir_test/runners/__init__.py +0 -0
  68. {tenzir_test-0.9.3 → tenzir_test-0.9.4}/src/tenzir_test/runners/_utils.py +0 -0
  69. {tenzir_test-0.9.3 → tenzir_test-0.9.4}/src/tenzir_test/runners/custom_python_fixture_runner.py +0 -0
  70. {tenzir_test-0.9.3 → tenzir_test-0.9.4}/src/tenzir_test/runners/diff_runner.py +0 -0
  71. {tenzir_test-0.9.3 → tenzir_test-0.9.4}/src/tenzir_test/runners/ext_runner.py +0 -0
  72. {tenzir_test-0.9.3 → tenzir_test-0.9.4}/src/tenzir_test/runners/runner.py +0 -0
  73. {tenzir_test-0.9.3 → tenzir_test-0.9.4}/src/tenzir_test/runners/shell_runner.py +0 -0
  74. {tenzir_test-0.9.3 → tenzir_test-0.9.4}/src/tenzir_test/runners/tenzir_runner.py +0 -0
  75. {tenzir_test-0.9.3 → tenzir_test-0.9.4}/src/tenzir_test/runners/tql_runner.py +0 -0
  76. {tenzir_test-0.9.3 → tenzir_test-0.9.4}/tests/test_config.py +0 -0
  77. {tenzir_test-0.9.3 → tenzir_test-0.9.4}/tests/test_engine_operations.py +0 -0
  78. {tenzir_test-0.9.3 → tenzir_test-0.9.4}/tests/test_python_runner.py +0 -0
  79. {tenzir_test-0.9.3 → tenzir_test-0.9.4}/tests/test_run_config.py +0 -0
  80. {tenzir_test-0.9.3 → tenzir_test-0.9.4}/tests/test_runner_registry.py +0 -0
  81. {tenzir_test-0.9.3 → tenzir_test-0.9.4}/tests/test_shell_runner.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tenzir-test
3
- Version: 0.9.3
3
+ Version: 0.9.4
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
@@ -0,0 +1,3 @@
1
+ from {x: "foo"}
2
+ repeat 10
3
+ enumerate
@@ -0,0 +1,28 @@
1
+ {
2
+ "#": 0,
3
+ x: "foo",
4
+ }
5
+ {
6
+ "#": 1,
7
+ x: "foo",
8
+ }
9
+ {
10
+ "#": -1,
11
+ x: "qux",
12
+ }
13
+ {
14
+ "#": 3,
15
+ x: "foo",
16
+ }
17
+ {
18
+ "#": 4,
19
+ x: "foo",
20
+ }
21
+ {
22
+ "#": -1,
23
+ x: "foo",
24
+ }
25
+ {
26
+ "#": -1,
27
+ x: "baz",
28
+ }
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "tenzir-test"
3
- version = "0.9.3"
3
+ version = "0.9.4"
4
4
  description = "Reusable test execution framework extracted from the Tenzir repository."
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.12"
@@ -90,6 +90,18 @@ def _normalize_exit_code(value: object) -> int:
90
90
  is_flag=True,
91
91
  help="Show an aggregate table and detailed failure summary after execution.",
92
92
  )
93
+ @click.option(
94
+ "--diff/--no-diff",
95
+ "show_diff_output",
96
+ default=True,
97
+ help="Show unified diffs when expectations differ.",
98
+ )
99
+ @click.option(
100
+ "--diff-stat/--no-diff-stat",
101
+ "show_diff_stat",
102
+ default=True,
103
+ help="Include per-file diff statistics and change counters when expectations differ.",
104
+ )
93
105
  @click.option(
94
106
  "-k",
95
107
  "--keep",
@@ -134,6 +146,8 @@ def cli(
134
146
  runner_summary: bool,
135
147
  fixture_summary: bool,
136
148
  show_summary: bool,
149
+ show_diff_output: bool,
150
+ show_diff_stat: bool,
137
151
  keep_tmp_dirs: bool,
138
152
  jobs: int,
139
153
  passthrough: bool,
@@ -157,6 +171,8 @@ def cli(
157
171
  runner_summary=runner_summary,
158
172
  fixture_summary=fixture_summary,
159
173
  show_summary=show_summary,
174
+ show_diff_output=show_diff_output,
175
+ show_diff_stat=show_diff_stat,
160
176
  keep_tmp_dirs=keep_tmp_dirs,
161
177
  jobs=jobs,
162
178
  passthrough=passthrough,
File without changes
@@ -351,6 +351,30 @@ _TMP_BASE_DIRS: set[Path] = set()
351
351
  _ACTIVE_TMP_DIRS: set[Path] = set()
352
352
  KEEP_TMP_DIRS = bool(os.environ.get(_TMP_KEEP_ENV_VAR))
353
353
 
354
+ SHOW_DIFF_OUTPUT = True
355
+ SHOW_DIFF_STAT = True
356
+ _BLOCK_INDENT = ""
357
+ _PLUS_SYMBOLS = {1: "□", 10: "▣", 100: "■"}
358
+ _MINUS_SYMBOLS = {1: "□", 10: "▣", 100: "■"}
359
+
360
+
361
+ def set_show_diff_output(enabled: bool) -> None:
362
+ global SHOW_DIFF_OUTPUT
363
+ SHOW_DIFF_OUTPUT = enabled
364
+
365
+
366
+ def should_show_diff_output() -> bool:
367
+ return SHOW_DIFF_OUTPUT
368
+
369
+
370
+ def set_show_diff_stat(enabled: bool) -> None:
371
+ global SHOW_DIFF_STAT
372
+ SHOW_DIFF_STAT = enabled
373
+
374
+
375
+ def should_show_diff_stat() -> bool:
376
+ return SHOW_DIFF_STAT
377
+
354
378
 
355
379
  def set_harness_mode(mode: HarnessMode) -> None:
356
380
  """Set the global harness execution mode."""
@@ -2015,10 +2039,9 @@ def _summarize_harness_configuration(
2015
2039
  runner_summary: bool,
2016
2040
  fixture_summary: bool,
2017
2041
  passthrough: bool,
2018
- ) -> tuple[int, str]:
2042
+ ) -> tuple[int, str, str]:
2019
2043
  enabled_flags: list[str] = []
2020
2044
  toggles = (
2021
- ("update", update),
2022
2045
  ("coverage", coverage),
2023
2046
  ("debug", debug),
2024
2047
  ("summary", show_summary),
@@ -2029,9 +2052,13 @@ def _summarize_harness_configuration(
2029
2052
  for name, flag in toggles:
2030
2053
  if flag:
2031
2054
  enabled_flags.append(name)
2032
- if passthrough:
2033
- enabled_flags.append("passthrough")
2034
- return jobs, ", ".join(enabled_flags)
2055
+ if update:
2056
+ verb = "updating"
2057
+ elif passthrough:
2058
+ verb = "showing"
2059
+ else:
2060
+ verb = "running"
2061
+ return jobs, ", ".join(enabled_flags), verb
2035
2062
 
2036
2063
 
2037
2064
  def _relativize_path(path: Path) -> Path:
@@ -2395,7 +2422,46 @@ def last_and(items: Iterable[T]) -> Iterator[tuple[bool, T]]:
2395
2422
  for item in iterator:
2396
2423
  yield (False, previous)
2397
2424
  previous = item
2398
- yield (True, previous)
2425
+
2426
+
2427
+ def _format_unary_symbols(count: int, symbols: dict[int, str]) -> str:
2428
+ if count <= 0:
2429
+ return ""
2430
+ hundreds, remainder = divmod(count, 100)
2431
+ tens, ones = divmod(remainder, 10)
2432
+ parts: list[str] = []
2433
+ if hundreds:
2434
+ parts.append(symbols[100] * hundreds)
2435
+ if tens:
2436
+ parts.append(symbols[10] * tens)
2437
+ if ones:
2438
+ parts.append(symbols[1] * ones)
2439
+ return "".join(parts)
2440
+
2441
+
2442
+ def _format_diff_counter(added: int, removed: int) -> str:
2443
+ plus_segment = _format_unary_symbols(added, _PLUS_SYMBOLS)
2444
+ minus_segment = _format_unary_symbols(removed, _MINUS_SYMBOLS)
2445
+ colored_plus = f"\033[32m{plus_segment}\033[0m" if plus_segment else ""
2446
+ colored_minus = f"\033[31m{minus_segment}\033[0m" if minus_segment else ""
2447
+ return f"{colored_plus}{colored_minus}"
2448
+
2449
+
2450
+ def _format_stat_header(path: os.PathLike[str] | str, added: int, removed: int) -> str:
2451
+ path_str = os.fspath(path)
2452
+ counter = _format_diff_counter(added, removed)
2453
+ plus_count = f"\033[32m{added}(+)\033[0m"
2454
+ minus_count = f"\033[31m{removed}(-)\033[0m"
2455
+ if counter:
2456
+ counter_segment = f" {counter}"
2457
+ else:
2458
+ counter_segment = ""
2459
+ return f"{_BLOCK_INDENT}┌ {path_str} {plus_count}/{minus_count}{counter_segment}"
2460
+
2461
+
2462
+ def _format_lines_changed(total: int) -> str:
2463
+ line = "line" if total == 1 else "lines"
2464
+ return f"{_BLOCK_INDENT}└ {total} {line} changed"
2399
2465
 
2400
2466
 
2401
2467
  def print_diff(expected: bytes, actual: bytes, path: Path) -> None:
@@ -2409,22 +2475,51 @@ def print_diff(expected: bytes, actual: bytes, path: Path) -> None:
2409
2475
  n=2,
2410
2476
  )
2411
2477
  )
2412
- with stdout_lock:
2413
- rel_path = _relativize_path(path)
2478
+ added = sum(
2479
+ 1
2480
+ for index, line in enumerate(diff)
2481
+ if index >= 2 and line.startswith(b"+") and not line.startswith(b"+++")
2482
+ )
2483
+ removed = sum(
2484
+ 1
2485
+ for index, line in enumerate(diff)
2486
+ if index >= 2 and line.startswith(b"-") and not line.startswith(b"---")
2487
+ )
2488
+ show_stat = should_show_diff_stat()
2489
+ show_diff = should_show_diff_output()
2490
+ diff_lines: list[str] = []
2491
+ if should_show_diff_output():
2414
2492
  skip = 2
2415
- for i, line in enumerate(diff):
2493
+ for raw_line in diff:
2416
2494
  if skip > 0:
2417
2495
  skip -= 1
2418
2496
  continue
2419
- if line.startswith(b"@@"):
2420
- print(f"┌─▶ \033[31m{rel_path}\033[0m")
2421
- continue
2422
- if line.startswith(b"+"):
2423
- line = b"\033[92m" + line + b"\033[0m"
2424
- elif line.startswith(b"-"):
2425
- line = b"\033[31m" + line + b"\033[0m"
2426
- prefix = ("│ " if i != len(diff) - 1 else "└─").encode()
2427
- sys.stdout.buffer.write(prefix + line)
2497
+ text = raw_line.decode("utf-8", "replace").rstrip("\r\n")
2498
+ if raw_line.startswith(b"+") and not raw_line.startswith(b"+++"):
2499
+ text = f"\033[32m{text}\033[0m"
2500
+ elif raw_line.startswith(b"-") and not raw_line.startswith(b"---"):
2501
+ text = f"\033[31m{text}\033[0m"
2502
+ diff_lines.append(text)
2503
+ rel_path = _relativize_path(path)
2504
+ rel_path_str = os.fspath(rel_path)
2505
+ lines: list[str] = []
2506
+ total_changed = added + removed
2507
+ if not show_stat and not show_diff:
2508
+ return
2509
+ header = (
2510
+ _format_stat_header(rel_path, added, removed)
2511
+ if show_stat
2512
+ else f"{_BLOCK_INDENT}┌ {rel_path_str}"
2513
+ )
2514
+ lines.append(header)
2515
+ if show_diff and diff_lines:
2516
+ for diff_line in diff_lines:
2517
+ lines.append(f"{_BLOCK_INDENT}│ {diff_line}")
2518
+ if show_stat or (show_diff and total_changed > 0):
2519
+ lines.append(_format_lines_changed(total_changed))
2520
+ with stdout_lock:
2521
+ for output_line in lines:
2522
+ print(output_line)
2428
2523
 
2429
2524
 
2430
2525
  def check_group_is_empty(pgid: int) -> None:
@@ -2889,6 +2984,8 @@ def run_cli(
2889
2984
  runner_summary: bool,
2890
2985
  fixture_summary: bool,
2891
2986
  show_summary: bool,
2987
+ show_diff_output: bool,
2988
+ show_diff_stat: bool,
2892
2989
  jobs: int,
2893
2990
  keep_tmp_dirs: bool,
2894
2991
  passthrough: bool,
@@ -2937,6 +3034,8 @@ def run_cli(
2937
3034
  fixture_logger.propagate = True
2938
3035
 
2939
3036
  set_keep_tmp_dirs(bool(os.environ.get(_TMP_KEEP_ENV_VAR)) or keep_tmp_dirs)
3037
+ set_show_diff_output(show_diff_output)
3038
+ set_show_diff_stat(show_diff_stat)
2940
3039
  if passthrough:
2941
3040
  harness_mode = HarnessMode.PASSTHROUGH
2942
3041
  elif update:
@@ -3122,7 +3221,7 @@ def run_cli(
3122
3221
  queue = _build_queue_from_paths(collected_paths, coverage=coverage)
3123
3222
  queue.sort(key=_queue_sort_key, reverse=True)
3124
3223
  project_queue_size = _count_queue_tests(queue)
3125
- job_count, enabled_flags = _summarize_harness_configuration(
3224
+ job_count, enabled_flags, verb = _summarize_harness_configuration(
3126
3225
  jobs=jobs,
3127
3226
  update=update,
3128
3227
  coverage=coverage,
@@ -3159,6 +3258,7 @@ def run_cli(
3159
3258
  queue_size=project_queue_size,
3160
3259
  job_count=job_count,
3161
3260
  enabled_flags=enabled_flags,
3261
+ verb=verb,
3162
3262
  )
3163
3263
  count_width = max((len(str(count)) for _, count, _ in runner_breakdown), default=1)
3164
3264
  for name, count, version in runner_breakdown:
@@ -3266,6 +3366,7 @@ def _print_project_start(
3266
3366
  queue_size: int,
3267
3367
  job_count: int,
3268
3368
  enabled_flags: str,
3369
+ verb: str,
3269
3370
  ) -> None:
3270
3371
  project_name = selection.root.name or selection.root.as_posix()
3271
3372
  if selection.kind == "root":
@@ -3284,5 +3385,5 @@ def _print_project_start(
3284
3385
  toggles = f"; {enabled_flags}" if enabled_flags else ""
3285
3386
  jobs_segment = f" ({job_count} jobs)" if job_count else ""
3286
3387
  print(
3287
- f"{INFO} {project_display}: running {queue_size} tests{jobs_segment} from {project_kind} at {location_display}{toggles}"
3388
+ f"{INFO} {project_display}: {verb} {queue_size} tests{jobs_segment} from {project_kind} at {location_display}{toggles}"
3288
3389
  )
@@ -38,6 +38,7 @@ def test_cli_keep_flag(monkeypatch: pytest.MonkeyPatch) -> None:
38
38
  assert captured["jobs_overridden"] is False
39
39
  assert captured["all_projects"] is False
40
40
  assert captured["show_summary"] is False
41
+ assert captured["show_diff_stat"] is True
41
42
 
42
43
 
43
44
  def test_cli_passthrough_flag(monkeypatch: pytest.MonkeyPatch) -> None:
@@ -109,6 +110,31 @@ def test_cli_summary_flag(monkeypatch: pytest.MonkeyPatch) -> None:
109
110
  assert captured["show_summary"] is True
110
111
 
111
112
 
113
+ def test_cli_no_diff_flag(monkeypatch: pytest.MonkeyPatch) -> None:
114
+ captured: dict[str, object] = {}
115
+
116
+ def fake_run_cli(**kwargs: object) -> None:
117
+ captured.update(kwargs)
118
+
119
+ monkeypatch.setattr(cli.runtime, "run_cli", fake_run_cli)
120
+
121
+ assert cli.main(["--no-diff"]) == 0
122
+ assert captured["show_diff_output"] is False
123
+ assert captured["show_diff_stat"] is True
124
+
125
+
126
+ def test_cli_no_diff_stat_flag(monkeypatch: pytest.MonkeyPatch) -> None:
127
+ captured: dict[str, object] = {}
128
+
129
+ def fake_run_cli(**kwargs: object) -> None:
130
+ captured.update(kwargs)
131
+
132
+ monkeypatch.setattr(cli.runtime, "run_cli", fake_run_cli)
133
+
134
+ assert cli.main(["--no-diff-stat"]) == 0
135
+ assert captured["show_diff_stat"] is False
136
+
137
+
112
138
  def test_cli_version_flag(capsys: pytest.CaptureFixture[str]) -> None:
113
139
  exit_code = cli.main(["--version"])
114
140
  assert exit_code == 0
@@ -769,6 +769,8 @@ def test_cli_rejects_partial_suite_selection(
769
769
  runner_summary=False,
770
770
  fixture_summary=False,
771
771
  show_summary=False,
772
+ show_diff_output=True,
773
+ show_diff_stat=True,
772
774
  jobs=1,
773
775
  keep_tmp_dirs=False,
774
776
  passthrough=False,
@@ -793,6 +795,8 @@ def test_cli_rejects_partial_suite_selection(
793
795
  runner_summary=False,
794
796
  fixture_summary=False,
795
797
  show_summary=False,
798
+ show_diff_output=True,
799
+ show_diff_stat=True,
796
800
  jobs=1,
797
801
  keep_tmp_dirs=False,
798
802
  passthrough=False,
@@ -828,6 +832,82 @@ def test_detailed_summary_order(capsys):
828
832
  assert skipped_index < failed_index < table_start
829
833
 
830
834
 
835
+ def test_print_diff_default_layout(capsys):
836
+ original_show_diff = run.should_show_diff_output()
837
+ original_show_stat = run.should_show_diff_stat()
838
+ try:
839
+ run.set_show_diff_output(True)
840
+ run.set_show_diff_stat(True)
841
+ run.print_diff(b"line\nbeta\n", b"line\ngamma\n", Path("tests/example.txt"))
842
+ finally:
843
+ run.set_show_diff_output(original_show_diff)
844
+ run.set_show_diff_stat(original_show_stat)
845
+
846
+ output = capsys.readouterr().out.splitlines()
847
+ assert output[0].startswith(f"{run._BLOCK_INDENT}┌ tests/example.txt")
848
+ expected_counter = run._format_diff_counter(1, 1)
849
+ assert "\033[32m1(+)\033[0m/\033[31m1(-)\033[0m" in output[0]
850
+ assert output[0].endswith(expected_counter)
851
+ assert output[1] == f"{run._BLOCK_INDENT}│ @@ -1,2 +1,2 @@"
852
+ assert output[2] == f"{run._BLOCK_INDENT}│ line"
853
+ assert output[3].startswith(f"{run._BLOCK_INDENT}│ \033[31m-beta")
854
+ assert output[4].startswith(f"{run._BLOCK_INDENT}│ \033[32m+gamma")
855
+ assert output[5] == f"{run._BLOCK_INDENT}└ 2 lines changed"
856
+
857
+
858
+ def test_print_diff_no_diff_outputs_stat_only(capsys):
859
+ original_show_diff = run.should_show_diff_output()
860
+ original_show_stat = run.should_show_diff_stat()
861
+ try:
862
+ run.set_show_diff_output(False)
863
+ run.set_show_diff_stat(True)
864
+ run.print_diff(b"line\nbeta\n", b"line\ngamma\n", Path("tests/example.txt"))
865
+ finally:
866
+ run.set_show_diff_output(original_show_diff)
867
+ run.set_show_diff_stat(original_show_stat)
868
+
869
+ output = capsys.readouterr().out.splitlines()
870
+ expected_header = (
871
+ f"{run._BLOCK_INDENT}┌ tests/example.txt "
872
+ f"\033[32m1(+)\033[0m/\033[31m1(-)\033[0m {run._format_diff_counter(1, 1)}"
873
+ )
874
+ assert output == [
875
+ expected_header,
876
+ f"{run._BLOCK_INDENT}└ 2 lines changed",
877
+ ]
878
+
879
+
880
+ def test_print_diff_stat_disabled_shows_only_diff(capsys):
881
+ original_show_diff = run.should_show_diff_output()
882
+ original_show_stat = run.should_show_diff_stat()
883
+ try:
884
+ run.set_show_diff_output(True)
885
+ run.set_show_diff_stat(False)
886
+ run.print_diff(b"line\nbeta\n", b"line\ngamma\n", Path("tests/example.txt"))
887
+ finally:
888
+ run.set_show_diff_output(original_show_diff)
889
+ run.set_show_diff_stat(original_show_stat)
890
+
891
+ output = capsys.readouterr().out.splitlines()
892
+ assert output[0] == f"{run._BLOCK_INDENT}┌ tests/example.txt"
893
+ assert output[1] == f"{run._BLOCK_INDENT}│ @@ -1,2 +1,2 @@"
894
+ assert output[-1] == f"{run._BLOCK_INDENT}└ 2 lines changed"
895
+
896
+
897
+ def test_print_diff_disabled_outputs_nothing(capsys):
898
+ original_show_diff = run.should_show_diff_output()
899
+ original_show_stat = run.should_show_diff_stat()
900
+ try:
901
+ run.set_show_diff_output(False)
902
+ run.set_show_diff_stat(False)
903
+ run.print_diff(b"line\nbeta\n", b"line\ngamma\n", Path("tests/example.txt"))
904
+ finally:
905
+ run.set_show_diff_output(original_show_diff)
906
+ run.set_show_diff_stat(original_show_stat)
907
+
908
+ assert capsys.readouterr().out.strip() == ""
909
+
910
+
831
911
  def test_describe_project_root_detects_standard_project(tmp_path: Path) -> None:
832
912
  project_root = tmp_path / "proj"
833
913
  tests_dir = project_root / "tests"
@@ -1143,6 +1223,7 @@ def test_print_project_start_reports_empty_projects(tmp_path, capsys):
1143
1223
  queue_size=0,
1144
1224
  job_count=0,
1145
1225
  enabled_flags="",
1226
+ verb="running",
1146
1227
  )
1147
1228
 
1148
1229
  output = capsys.readouterr().out
@@ -1152,6 +1233,66 @@ def test_print_project_start_reports_empty_projects(tmp_path, capsys):
1152
1233
  )
1153
1234
 
1154
1235
 
1236
+ def test_summarize_harness_configuration_sets_update_verb() -> None:
1237
+ job_count, enabled_flags, verb = run._summarize_harness_configuration(
1238
+ jobs=1,
1239
+ update=True,
1240
+ coverage=False,
1241
+ debug=False,
1242
+ show_summary=False,
1243
+ runner_summary=False,
1244
+ fixture_summary=False,
1245
+ passthrough=False,
1246
+ )
1247
+
1248
+ assert job_count == 1
1249
+ assert "update" not in enabled_flags
1250
+ assert verb == "updating"
1251
+
1252
+
1253
+ def test_summarize_harness_configuration_sets_passthrough_verb() -> None:
1254
+ job_count, enabled_flags, verb = run._summarize_harness_configuration(
1255
+ jobs=2,
1256
+ update=False,
1257
+ coverage=False,
1258
+ debug=False,
1259
+ show_summary=False,
1260
+ runner_summary=False,
1261
+ fixture_summary=False,
1262
+ passthrough=True,
1263
+ )
1264
+
1265
+ assert job_count == 2
1266
+ assert "passthrough" not in enabled_flags
1267
+ assert verb == "showing"
1268
+
1269
+
1270
+ def test_print_project_start_uses_custom_verb(tmp_path, capsys):
1271
+ project_root = tmp_path / "example"
1272
+ project_root.mkdir()
1273
+ selection = run.ProjectSelection(
1274
+ root=project_root,
1275
+ selectors=[],
1276
+ run_all=True,
1277
+ kind="root",
1278
+ )
1279
+
1280
+ run._print_project_start(
1281
+ selection=selection,
1282
+ display_base=tmp_path,
1283
+ queue_size=3,
1284
+ job_count=0,
1285
+ enabled_flags="",
1286
+ verb="showing",
1287
+ )
1288
+
1289
+ output = capsys.readouterr().out
1290
+ assert (
1291
+ output
1292
+ == f"{run.INFO} {run.BOLD}example{run.RESET_COLOR}: showing 3 tests from root project at ./example\n"
1293
+ )
1294
+
1295
+
1155
1296
  def test_execution_plan_single_project_summary(tmp_path, monkeypatch, capsys):
1156
1297
  root = tmp_path
1157
1298
  plan = run.ExecutionPlan(
File without changes
File without changes
File without changes