dbression 0.2.0__tar.gz → 0.3.1__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 (68) hide show
  1. {dbression-0.2.0 → dbression-0.3.1}/CHANGELOG.md +33 -4
  2. {dbression-0.2.0 → dbression-0.3.1}/PKG-INFO +18 -37
  3. {dbression-0.2.0 → dbression-0.3.1}/README.md +17 -36
  4. {dbression-0.2.0 → dbression-0.3.1}/pyproject.toml +1 -1
  5. {dbression-0.2.0 → dbression-0.3.1}/src/dbression/cli.py +101 -12
  6. dbression-0.3.1/src/dbression/parser/__init__.py +12 -0
  7. {dbression-0.2.0 → dbression-0.3.1}/src/dbression/parser/wiki.py +63 -0
  8. {dbression-0.2.0 → dbression-0.3.1}/src/dbression/report/__init__.py +11 -1
  9. dbression-0.3.1/src/dbression/report/progress.py +113 -0
  10. dbression-0.3.1/src/dbression/report/render.py +267 -0
  11. {dbression-0.2.0 → dbression-0.3.1}/src/dbression/runner.py +61 -9
  12. dbression-0.3.1/tests/test_progress_and_single_file.py +128 -0
  13. dbression-0.3.1/tests/test_render.py +120 -0
  14. {dbression-0.2.0 → dbression-0.3.1}/uv.lock +1 -1
  15. dbression-0.2.0/src/dbression/parser/__init__.py +0 -4
  16. {dbression-0.2.0 → dbression-0.3.1}/.github/workflows/ci.yml +0 -0
  17. {dbression-0.2.0 → dbression-0.3.1}/.github/workflows/publish.yml +0 -0
  18. {dbression-0.2.0 → dbression-0.3.1}/.gitignore +0 -0
  19. {dbression-0.2.0 → dbression-0.3.1}/.python-version +0 -0
  20. {dbression-0.2.0 → dbression-0.3.1}/LICENSE +0 -0
  21. {dbression-0.2.0 → dbression-0.3.1}/docs/dbression_head.png +0 -0
  22. {dbression-0.2.0 → dbression-0.3.1}/examples/01-hello/HelloSql.test.md +0 -0
  23. {dbression-0.2.0 → dbression-0.3.1}/examples/01-hello/_root.wiki +0 -0
  24. {dbression-0.2.0 → dbression-0.3.1}/examples/01-hello/connection.properties +0 -0
  25. {dbression-0.2.0 → dbression-0.3.1}/examples/02-stored-procedure/BumpCounterTest.test.md +0 -0
  26. {dbression-0.2.0 → dbression-0.3.1}/examples/02-stored-procedure/SuiteSetUp.test.md +0 -0
  27. {dbression-0.2.0 → dbression-0.3.1}/examples/02-stored-procedure/SuiteTearDown.test.md +0 -0
  28. {dbression-0.2.0 → dbression-0.3.1}/examples/02-stored-procedure/_root.wiki +0 -0
  29. {dbression-0.2.0 → dbression-0.3.1}/examples/02-stored-procedure/connection.properties +0 -0
  30. {dbression-0.2.0 → dbression-0.3.1}/examples/03-schema-drift/SchemaSnapshotTest.test.md +0 -0
  31. {dbression-0.2.0 → dbression-0.3.1}/examples/03-schema-drift/SuiteSetUp.test.md +0 -0
  32. {dbression-0.2.0 → dbression-0.3.1}/examples/03-schema-drift/SuiteTearDown.test.md +0 -0
  33. {dbression-0.2.0 → dbression-0.3.1}/examples/03-schema-drift/_root.wiki +0 -0
  34. {dbression-0.2.0 → dbression-0.3.1}/examples/03-schema-drift/connection.properties +0 -0
  35. {dbression-0.2.0 → dbression-0.3.1}/examples/README.md +0 -0
  36. {dbression-0.2.0 → dbression-0.3.1}/src/dbression/__init__.py +0 -0
  37. {dbression-0.2.0 → dbression-0.3.1}/src/dbression/db/__init__.py +0 -0
  38. {dbression-0.2.0 → dbression-0.3.1}/src/dbression/db/connection.py +0 -0
  39. {dbression-0.2.0 → dbression-0.3.1}/src/dbression/db/engine.py +0 -0
  40. {dbression-0.2.0 → dbression-0.3.1}/src/dbression/db/errors.py +0 -0
  41. {dbression-0.2.0 → dbression-0.3.1}/src/dbression/fixtures/__init__.py +0 -0
  42. {dbression-0.2.0 → dbression-0.3.1}/src/dbression/fixtures/base.py +0 -0
  43. {dbression-0.2.0 → dbression-0.3.1}/src/dbression/fixtures/basic.py +0 -0
  44. {dbression-0.2.0 → dbression-0.3.1}/src/dbression/fixtures/inspect_and_store.py +0 -0
  45. {dbression-0.2.0 → dbression-0.3.1}/src/dbression/fixtures/plugins.py +0 -0
  46. {dbression-0.2.0 → dbression-0.3.1}/src/dbression/fixtures/suite_fixtures.py +0 -0
  47. {dbression-0.2.0 → dbression-0.3.1}/src/dbression/parser/ast.py +0 -0
  48. {dbression-0.2.0 → dbression-0.3.1}/src/dbression/parser/markdown.py +0 -0
  49. {dbression-0.2.0 → dbression-0.3.1}/src/dbression/parser/markdown_writer.py +0 -0
  50. {dbression-0.2.0 → dbression-0.3.1}/src/dbression/parser/tokenizer.py +0 -0
  51. {dbression-0.2.0 → dbression-0.3.1}/src/dbression/report/console.py +0 -0
  52. {dbression-0.2.0 → dbression-0.3.1}/src/dbression/report/json_report.py +0 -0
  53. {dbression-0.2.0 → dbression-0.3.1}/src/dbression/report/junit.py +0 -0
  54. {dbression-0.2.0 → dbression-0.3.1}/src/dbression/symbols.py +0 -0
  55. {dbression-0.2.0 → dbression-0.3.1}/tests/conftest.py +0 -0
  56. {dbression-0.2.0 → dbression-0.3.1}/tests/test_connection.py +0 -0
  57. {dbression-0.2.0 → dbression-0.3.1}/tests/test_markdown_parser.py +0 -0
  58. {dbression-0.2.0 → dbression-0.3.1}/tests/test_markdown_roundtrip.py +0 -0
  59. {dbression-0.2.0 → dbression-0.3.1}/tests/test_parser.py +0 -0
  60. {dbression-0.2.0 → dbression-0.3.1}/tests/test_phase2_fixtures.py +0 -0
  61. {dbression-0.2.0 → dbression-0.3.1}/tests/test_plugins.py +0 -0
  62. {dbression-0.2.0 → dbression-0.3.1}/tests/test_postgres_live.py +0 -0
  63. {dbression-0.2.0 → dbression-0.3.1}/tests/test_report_junit.py +0 -0
  64. {dbression-0.2.0 → dbression-0.3.1}/tests/test_smoke.py +0 -0
  65. {dbression-0.2.0 → dbression-0.3.1}/tests/test_sqlite_engine.py +0 -0
  66. {dbression-0.2.0 → dbression-0.3.1}/tests/test_symbols.py +0 -0
  67. {dbression-0.2.0 → dbression-0.3.1}/tests/test_tokenizer.py +0 -0
  68. {dbression-0.2.0 → dbression-0.3.1}/tests/test_update_fixture.py +0 -0
@@ -7,6 +7,33 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.3.1] — 2026-06-08
11
+
12
+ ### Added
13
+
14
+ - **`--render` / `-r` — live page rendering (DBFit-browser style).** For a single
15
+ `.test.md`, the page is rendered in the terminal (prose, syntax-highlighted SQL,
16
+ expected tables) and each fixture card lights up green/red *in place* as it runs, with
17
+ the row diff shown inline on failure. The closest terminal analogue to watching a DBFit
18
+ wiki page execute in the browser.
19
+
20
+ ## [0.3.0] — 2026-06-08
21
+
22
+ ### Added
23
+
24
+ - **Single-file runs.** `dbression run path/to/MyTest.test.md` (or `.wiki`) now runs a
25
+ single test, mirroring DBFit: a file is one *Test* (many fixtures), a directory is a
26
+ *Suite*. The containing directory's `SuiteSetUp` / `SuiteTearDown` and `_root`
27
+ (connection + `DatabaseEnvironment`) are included automatically. A self-contained
28
+ `.test.md` carrying its own `<!-- dbression:env=… -->` / `<!-- dbression:connection=… -->`
29
+ directives runs standalone.
30
+ - **Live progress.** Runs now show a spinner, an `x/y fixtures` counter, elapsed time, and
31
+ a status line naming the fixture/query currently executing. On by default at a TTY;
32
+ silenced automatically when output is piped or in CI. Toggle with `--progress` /
33
+ `--no-progress`.
34
+ - **`--details` / `-d`.** Prints each fixture's result green/red as it runs (DBFit-web-UI
35
+ style), so a `.test.md` viewed in `glow` has a matching colored CLI run alongside it.
36
+
10
37
  ## [0.2.0] — 2026-06-01
11
38
 
12
39
  ### Added
@@ -21,15 +48,15 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
21
48
 
22
49
  ## [0.1.2] — 2026-06-01
23
50
 
24
- SQL Server live-verification release. A real-world MSSQL DBFit suite (WD-Gehsteig
25
- regression tests, stored-procedure-driven, square-bracket identifiers throughout)
51
+ SQL Server live-verification release. A real-world MSSQL DBFit suite (regression tests,
52
+ stored-procedure-driven, square-bracket identifiers throughout)
26
53
  runs end-to-end against an MSSQL Server 2019 via `pymssql` with no changes to the
27
54
  underlying `.wiki` files.
28
55
 
29
56
  ### Fixed
30
57
 
31
58
  - **Console reporter swallowed `[…]` in failure details.** Rich's inline-markup
32
- parser interpreted MSSQL square-bracket identifiers like `[wdg].PLAENE` as
59
+ parser interpreted MSSQL square-bracket identifiers like `[dbo].ORDER` as
33
60
  style tags and stripped them from the displayed SQL — making failure output
34
61
  misleading. Reporter now prints fixture details with `markup=False`, so the
35
62
  SQL you see is the SQL that ran.
@@ -132,7 +159,9 @@ with code paths in place for SQL Server and Oracle.
132
159
  reject `python-oracledb` thin-mode authentication. Configuration is via
133
160
  `DBRESSION_ORACLE_CLIENT_LIB_DIR`.
134
161
 
135
- [Unreleased]: https://github.com/angrydat/dbression/compare/v0.2.0...HEAD
162
+ [Unreleased]: https://github.com/angrydat/dbression/compare/v0.3.1...HEAD
163
+ [0.3.1]: https://github.com/angrydat/dbression/releases/tag/v0.3.1
164
+ [0.3.0]: https://github.com/angrydat/dbression/releases/tag/v0.3.0
136
165
  [0.2.0]: https://github.com/angrydat/dbression/releases/tag/v0.2.0
137
166
  [0.1.2]: https://github.com/angrydat/dbression/releases/tag/v0.1.2
138
167
  [0.1.1]: https://github.com/angrydat/dbression/releases/tag/v0.1.1
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dbression
3
- Version: 0.2.0
3
+ Version: 0.3.1
4
4
  Summary: Rage-quit your flaky DB regressions — modern, lightweight, multi-DB regression testing.
5
5
  Project-URL: Homepage, https://angrydata.info/dbression
6
6
  Project-URL: Repository, https://github.com/angrydat/dbression
@@ -52,19 +52,19 @@ anywhere. `dbression` is a Python re-implementation in the spirit of the fantast
52
52
 
53
53
  ```text
54
54
  $ dbression run tests/
55
- dbression 0.2.0 — Suite: tests @ postgresql+psycopg://wlk:***@db01/wlk
55
+ dbression 0.3.1 — Suite: tests @ postgresql+psycopg://foo:***@db01/bar
56
56
 
57
57
  ✓ HelloSql 0.004s
58
58
  CommonSuite/
59
- MerklisteSuite/
59
+ ChangelistSuite/
60
60
  ✓ AAddBasicTest 0.027s
61
61
  ✓ BAddNormalizationTest 0.029s
62
62
  ✓ CAddWhitelistTest 0.025s
63
63
  ✓ DAddInvalidArgsTest 0.014s
64
64
  ✓ ERemTest 0.052s
65
- FViewWbTest 1.236s
66
- EreignisSuite/
67
- WaldbrandSuite/
65
+ FViewTest 1.236s
66
+ EventsSuite/
67
+ FireSuite/
68
68
  ✓ LookupTest 0.157s
69
69
  ✗ SchemaTest 0.013s
70
70
 
@@ -277,7 +277,11 @@ dbression:
277
277
  ## CLI cheat sheet
278
278
 
279
279
  ```bash
280
- dbression run <suite-path> # run an entire (sub-)suite
280
+ dbression run <suite-path> # run an entire (sub-)suite (a directory)
281
+ dbression run <path>/MyTest.test.md # run a single test file (its SuiteSetUp/_root come along)
282
+ dbression run <path>/MyTest.test.md -r # --render: live DBFit-style page, cards light up green/red
283
+ dbression run <path> -d # --details: print each fixture green/red as it runs
284
+ dbression run <path> --no-progress # disable the live spinner / x-of-y counter
281
285
  dbression run <path> -v # show every fixture table, not just the page line
282
286
  dbression run <path> --tag critical # only run pages tagged `critical`
283
287
  dbression run <path> --skip-tag NotOnCI # skip pages tagged `NotOnCI`
@@ -289,37 +293,14 @@ dbression convert file.wiki -o out.test.md # single file with explicit output
289
293
  dbression version
290
294
  ```
291
295
 
292
- ## How it works
296
+ A run shows a live spinner with an `x/y fixtures` counter and the currently-executing
297
+ query (auto-disabled when piped or in CI). Add `-d`/`--details` to stream a colored
298
+ pass/fail line per fixture — handy next to a `.test.md` you're previewing in `glow`.
293
299
 
294
- ```
295
- ┌─────────────────────────────────────────────────────────┐
296
- │ CLI (typer) dbression run tests/ [--flags] │
297
- └─────────────────────────────────────────────────────────┘
298
-
299
-
300
- ┌─────────────────────┐ ┌───────────────────────────────┐
301
- │ Parser │ │ Engine Factory (SQLAlchemy) │
302
- │ Wiki → AST │ │ ┌──────────┐ ┌──────────┐ │
303
- │ • !- -! escapes │ │ │ psycopg │ │ oracledb │ │
304
- │ • q'~ ~' quoting │ │ │ (Postgres│ │ (thin) │ │
305
- │ • Nested suites │ │ └──────────┘ └──────────┘ │
306
- │ • YAML front matter│ │ ┌──────────────────────┐ │
307
- └─────────┬───────────┘ │ │ pymssql (SQL Server) │ │
308
- │ │ └──────────────────────┘ │
309
- ▼ └───────────┬───────────────────┘
310
- ┌─────────────────────────────────────┴───────────────────┐
311
- │ Runner — one TX per suite, savepoints per test │
312
- │ Symbol engine: >>capture, <<read, :bind, _:text-subst │
313
- │ Fixture registry — pluggable via decorator │
314
- └─────────────────────┬───────────────────────────────────┘
315
-
316
- ┌─────────────────────────────────────────────────────────┐
317
- │ Reporters │
318
- │ • Rich console (default) │
319
- │ • JUnit XML (--junit-xml) │
320
- │ • JSON (--json) │
321
- └─────────────────────────────────────────────────────────┘
322
- ```
300
+ For a single `.test.md`, `-r`/`--render` goes further: it renders the whole page in the
301
+ terminal (prose, SQL, expected tables) and lights each fixture card up green or red **in
302
+ place** as it runs — the terminal answer to watching a DBFit wiki page execute in the
303
+ browser. You see exactly where a page breaks, in document context.
323
304
 
324
305
  ## Status
325
306
 
@@ -15,19 +15,19 @@ anywhere. `dbression` is a Python re-implementation in the spirit of the fantast
15
15
 
16
16
  ```text
17
17
  $ dbression run tests/
18
- dbression 0.2.0 — Suite: tests @ postgresql+psycopg://wlk:***@db01/wlk
18
+ dbression 0.3.1 — Suite: tests @ postgresql+psycopg://foo:***@db01/bar
19
19
 
20
20
  ✓ HelloSql 0.004s
21
21
  CommonSuite/
22
- MerklisteSuite/
22
+ ChangelistSuite/
23
23
  ✓ AAddBasicTest 0.027s
24
24
  ✓ BAddNormalizationTest 0.029s
25
25
  ✓ CAddWhitelistTest 0.025s
26
26
  ✓ DAddInvalidArgsTest 0.014s
27
27
  ✓ ERemTest 0.052s
28
- FViewWbTest 1.236s
29
- EreignisSuite/
30
- WaldbrandSuite/
28
+ FViewTest 1.236s
29
+ EventsSuite/
30
+ FireSuite/
31
31
  ✓ LookupTest 0.157s
32
32
  ✗ SchemaTest 0.013s
33
33
 
@@ -240,7 +240,11 @@ dbression:
240
240
  ## CLI cheat sheet
241
241
 
242
242
  ```bash
243
- dbression run <suite-path> # run an entire (sub-)suite
243
+ dbression run <suite-path> # run an entire (sub-)suite (a directory)
244
+ dbression run <path>/MyTest.test.md # run a single test file (its SuiteSetUp/_root come along)
245
+ dbression run <path>/MyTest.test.md -r # --render: live DBFit-style page, cards light up green/red
246
+ dbression run <path> -d # --details: print each fixture green/red as it runs
247
+ dbression run <path> --no-progress # disable the live spinner / x-of-y counter
244
248
  dbression run <path> -v # show every fixture table, not just the page line
245
249
  dbression run <path> --tag critical # only run pages tagged `critical`
246
250
  dbression run <path> --skip-tag NotOnCI # skip pages tagged `NotOnCI`
@@ -252,37 +256,14 @@ dbression convert file.wiki -o out.test.md # single file with explicit output
252
256
  dbression version
253
257
  ```
254
258
 
255
- ## How it works
259
+ A run shows a live spinner with an `x/y fixtures` counter and the currently-executing
260
+ query (auto-disabled when piped or in CI). Add `-d`/`--details` to stream a colored
261
+ pass/fail line per fixture — handy next to a `.test.md` you're previewing in `glow`.
256
262
 
257
- ```
258
- ┌─────────────────────────────────────────────────────────┐
259
- │ CLI (typer) dbression run tests/ [--flags] │
260
- └─────────────────────────────────────────────────────────┘
261
-
262
-
263
- ┌─────────────────────┐ ┌───────────────────────────────┐
264
- │ Parser │ │ Engine Factory (SQLAlchemy) │
265
- │ Wiki → AST │ │ ┌──────────┐ ┌──────────┐ │
266
- │ • !- -! escapes │ │ │ psycopg │ │ oracledb │ │
267
- │ • q'~ ~' quoting │ │ │ (Postgres│ │ (thin) │ │
268
- │ • Nested suites │ │ └──────────┘ └──────────┘ │
269
- │ • YAML front matter│ │ ┌──────────────────────┐ │
270
- └─────────┬───────────┘ │ │ pymssql (SQL Server) │ │
271
- │ │ └──────────────────────┘ │
272
- ▼ └───────────┬───────────────────┘
273
- ┌─────────────────────────────────────┴───────────────────┐
274
- │ Runner — one TX per suite, savepoints per test │
275
- │ Symbol engine: >>capture, <<read, :bind, _:text-subst │
276
- │ Fixture registry — pluggable via decorator │
277
- └─────────────────────┬───────────────────────────────────┘
278
-
279
- ┌─────────────────────────────────────────────────────────┐
280
- │ Reporters │
281
- │ • Rich console (default) │
282
- │ • JUnit XML (--junit-xml) │
283
- │ • JSON (--json) │
284
- └─────────────────────────────────────────────────────────┘
285
- ```
263
+ For a single `.test.md`, `-r`/`--render` goes further: it renders the whole page in the
264
+ terminal (prose, SQL, expected tables) and lights each fixture card up green or red **in
265
+ place** as it runs — the terminal answer to watching a DBFit wiki page execute in the
266
+ browser. You see exactly where a page breaks, in document context.
286
267
 
287
268
  ## Status
288
269
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "dbression"
3
- version = "0.2.0"
3
+ version = "0.3.1"
4
4
  description = "Rage-quit your flaky DB regressions — modern, lightweight, multi-DB regression testing."
5
5
  readme = "README.md"
6
6
  license = { text = "MIT" }
@@ -8,11 +8,18 @@ import typer
8
8
  from rich.console import Console
9
9
 
10
10
  from dbression import __version__
11
- from dbression.parser import parse_suite
11
+ from dbression.parser import parse_suite, parse_test_file
12
12
  from dbression.parser.markdown_writer import page_to_markdown
13
13
  from dbression.parser.wiki import parse_wiki
14
- from dbression.report import print_suite_result, write_json_report, write_junit_xml
15
- from dbression.runner import TagFilter, build_engine_for_suite, run_suite
14
+ from dbression.report import (
15
+ ProgressObserver,
16
+ make_progress,
17
+ print_suite_result,
18
+ render_run,
19
+ write_json_report,
20
+ write_junit_xml,
21
+ )
22
+ from dbression.runner import TagFilter, build_engine_for_suite, count_fixtures, run_suite
16
23
 
17
24
  app = typer.Typer(
18
25
  name="dbression",
@@ -37,7 +44,12 @@ def version() -> None:
37
44
 
38
45
  @app.command()
39
46
  def run(
40
- path: Annotated[Path, typer.Argument(help="Path to the suite (directory containing _root.wiki)")],
47
+ path: Annotated[
48
+ Path,
49
+ typer.Argument(
50
+ help="A suite directory (with _root.wiki) OR a single test file (.test.md / .wiki)"
51
+ ),
52
+ ],
41
53
  commit_mode: Annotated[
42
54
  str,
43
55
  typer.Option(
@@ -48,6 +60,30 @@ def run(
48
60
  verbose: Annotated[
49
61
  bool, typer.Option("-v", "--verbose", help="Print every fixture table, not just the page line")
50
62
  ] = False,
63
+ details: Annotated[
64
+ bool,
65
+ typer.Option(
66
+ "-d",
67
+ "--details",
68
+ help="Print each fixture's result (green/red) live as it runs, DBFit-web-UI style",
69
+ ),
70
+ ] = False,
71
+ render: Annotated[
72
+ bool,
73
+ typer.Option(
74
+ "-r",
75
+ "--render",
76
+ help="Render a single .test.md page in the terminal, cards lighting up green/red "
77
+ "in place as fixtures run (DBFit-browser style). Single .test.md file only.",
78
+ ),
79
+ ] = False,
80
+ progress: Annotated[
81
+ bool | None,
82
+ typer.Option(
83
+ "--progress/--no-progress",
84
+ help="Live spinner + x/y fixture counter (default: on when stdout is a terminal)",
85
+ ),
86
+ ] = None,
51
87
  tag: Annotated[
52
88
  list[str] | None,
53
89
  typer.Option(
@@ -68,10 +104,7 @@ def run(
68
104
  typer.Option("--json", help="Write a JSON report to this path (LLM / tooling)"),
69
105
  ] = None,
70
106
  ) -> None:
71
- """Run a dbression suite and produce pytest-style reporting."""
72
- if not path.is_dir():
73
- console.print(f"[red]Not a directory:[/red] {path}")
74
- raise typer.Exit(2)
107
+ """Run a dbression test (single file) or suite (directory) with live progress."""
75
108
  if commit_mode not in ("test", "page"):
76
109
  console.print(
77
110
  f"[red]Invalid commit-mode: {commit_mode!r} (allowed: test, page)[/red]"
@@ -80,17 +113,73 @@ def run(
80
113
 
81
114
  tag_filter = TagFilter(only=tuple(tag or ()), skip=tuple(skip_tag or ()))
82
115
 
83
- suite = parse_suite(path)
116
+ if render and not (path.is_file() and path.name.endswith(".test.md")):
117
+ console.print(
118
+ "[red]--render works on a single .test.md file[/red] "
119
+ "(it renders one page as a live document)."
120
+ )
121
+ raise typer.Exit(2)
122
+
123
+ if path.is_dir():
124
+ suite = parse_suite(path)
125
+ kind = "Suite"
126
+ elif path.is_file():
127
+ try:
128
+ suite = parse_test_file(path)
129
+ except ValueError as e:
130
+ console.print(f"[red]{e}[/red]")
131
+ raise typer.Exit(2)
132
+ kind = "Test"
133
+ else:
134
+ console.print(f"[red]Path does not exist:[/red] {path}")
135
+ raise typer.Exit(2)
136
+
84
137
  engine = build_engine_for_suite(suite)
85
138
  console.print(
86
- f"dbression {__version__} — Suite: [bold]{suite.name}[/bold] @ "
139
+ f"dbression {__version__} — {kind}: [bold]{suite.name}[/bold] @ "
87
140
  f"{engine.url.render_as_string(hide_password=True)}\n"
88
141
  )
142
+
143
+ # Progress is on by default at a TTY; --progress / --no-progress override.
144
+ use_progress = console.is_terminal if progress is None else progress
145
+ total = count_fixtures(suite, tag_filter)
146
+
89
147
  try:
90
- result = run_suite(suite, engine, commit_mode=commit_mode, tag_filter=tag_filter) # type: ignore[arg-type]
148
+ if render:
149
+ # Live DBFit-style page render — the document IS the output.
150
+ result = render_run(
151
+ console,
152
+ suite,
153
+ engine,
154
+ source=path.read_text(encoding="utf-8"),
155
+ commit_mode=commit_mode, # type: ignore[arg-type]
156
+ tag_filter=tag_filter,
157
+ )
158
+ elif use_progress:
159
+ with make_progress(console) as prog:
160
+ task = prog.add_task("starting…", total=total or None)
161
+ observer = ProgressObserver(console, prog, task, details=details)
162
+ result = run_suite(
163
+ suite,
164
+ engine,
165
+ commit_mode=commit_mode, # type: ignore[arg-type]
166
+ tag_filter=tag_filter,
167
+ observer=observer,
168
+ )
169
+ else:
170
+ # No live bar (piped / CI). Still stream colored detail lines if asked.
171
+ observer = ProgressObserver(console, details=details) if details else None
172
+ result = run_suite(
173
+ suite,
174
+ engine,
175
+ commit_mode=commit_mode, # type: ignore[arg-type]
176
+ tag_filter=tag_filter,
177
+ observer=observer,
178
+ )
91
179
  finally:
92
180
  engine.dispose()
93
- print_suite_result(result, console, verbose=verbose)
181
+ if not render:
182
+ print_suite_result(result, console, verbose=verbose)
94
183
 
95
184
  if junit_xml is not None:
96
185
  write_junit_xml(result, junit_xml)
@@ -0,0 +1,12 @@
1
+ from dbression.parser.ast import Directive, Page, Suite, Table
2
+ from dbression.parser.wiki import parse_suite, parse_test_file, parse_wiki
3
+
4
+ __all__ = [
5
+ "Directive",
6
+ "Page",
7
+ "Suite",
8
+ "Table",
9
+ "parse_suite",
10
+ "parse_test_file",
11
+ "parse_wiki",
12
+ ]
@@ -203,3 +203,66 @@ def parse_suite(root: Path) -> Suite:
203
203
  # Stable, deterministic order of test pages (alphabetical).
204
204
  suite.pages.sort(key=lambda p: p.name)
205
205
  return suite
206
+
207
+
208
+ def _discover_special_pages(root: Path) -> dict[str, Page]:
209
+ """Parse only ``_root`` / ``SuiteSetUp`` / ``SuiteTearDown`` in `root` (md > wiki).
210
+
211
+ Used by single-file runs to pull in the surrounding directory's connection config
212
+ and setup/teardown without dragging in sibling test pages.
213
+ """
214
+ from dbression.parser.markdown import MARKDOWN_TEST_SUFFIX, parse_markdown
215
+
216
+ wanted = {"_root", "SuiteSetUp", "SuiteTearDown"}
217
+ if not root.is_dir():
218
+ return {}
219
+ found: dict[str, tuple[Path, str]] = {}
220
+ for entry in sorted(root.iterdir()):
221
+ if entry.is_dir():
222
+ continue
223
+ if entry.name.endswith(MARKDOWN_TEST_SUFFIX):
224
+ pn = entry.name[: -len(MARKDOWN_TEST_SUFFIX)]
225
+ if pn in wanted:
226
+ found[pn] = (entry, "md")
227
+ elif entry.suffix == ".wiki":
228
+ pn = entry.stem
229
+ if pn in wanted:
230
+ found.setdefault(pn, (entry, "wiki"))
231
+ out: dict[str, Page] = {}
232
+ for pn, (p, fmt) in found.items():
233
+ out[pn] = parse_markdown(p) if fmt == "md" else parse_wiki(p)
234
+ return out
235
+
236
+
237
+ def parse_test_file(path: Path) -> Suite:
238
+ """Parse a single test FILE (``.test.md`` or ``.wiki``) into a runnable one-page Suite.
239
+
240
+ DBFit semantics: a file is one **Test** (many fixtures/assertions); a directory is a
241
+ **Suite**. When a single test is run, the surrounding directory's ``SuiteSetUp`` /
242
+ ``SuiteTearDown`` and ``_root`` (``DatabaseEnvironment`` + ``ConnectUsingFile``) are
243
+ included — just like opening a single page in the DBFit web UI runs its SuiteSetUp.
244
+
245
+ A self-contained ``.test.md`` that carries its own ``<!-- dbression:env=… -->`` /
246
+ ``<!-- dbression:connection=… -->`` directives runs standalone: those become the
247
+ suite's engine config (resolved relative to the file's directory).
248
+ """
249
+ from dbression.parser.markdown import MARKDOWN_TEST_SUFFIX, parse_markdown
250
+
251
+ if not path.is_file():
252
+ raise ValueError(f"Not a file: {path}")
253
+ if path.name.endswith(MARKDOWN_TEST_SUFFIX):
254
+ page = parse_markdown(path)
255
+ elif path.suffix == ".wiki":
256
+ page = parse_wiki(path)
257
+ else:
258
+ raise ValueError(f"Not a runnable test file (expected .test.md or .wiki): {path}")
259
+
260
+ root = path.parent
261
+ suite = Suite(root=root, name=page.name, pages=[page])
262
+ specials = _discover_special_pages(root)
263
+ suite.setup = specials.get("SuiteSetUp")
264
+ suite.teardown = specials.get("SuiteTearDown")
265
+ # Engine config precedence: a real `_root` in the directory wins; otherwise the test
266
+ # file's own env/connection directives make it self-contained.
267
+ suite.root_page = specials.get("_root") or page
268
+ return suite
@@ -1,11 +1,21 @@
1
1
  """Reporters for SuiteResult data.
2
2
 
3
3
  * `console` — pytest-style rich output (default CLI output)
4
+ * `progress` — live spinner + x/y counter + per-fixture detail lines (`--details`)
4
5
  * `junit` — JUnit XML for Bitbucket Pipelines, Jenkins, GitLab, etc.
5
6
  * `json_report` — JSON for LLM tooling, custom dashboards, diff analyses
6
7
  """
7
8
  from dbression.report.console import print_suite_result
8
9
  from dbression.report.json_report import write_json_report
9
10
  from dbression.report.junit import write_junit_xml
11
+ from dbression.report.progress import ProgressObserver, make_progress
12
+ from dbression.report.render import render_run
10
13
 
11
- __all__ = ["print_suite_result", "write_json_report", "write_junit_xml"]
14
+ __all__ = [
15
+ "ProgressObserver",
16
+ "make_progress",
17
+ "print_suite_result",
18
+ "render_run",
19
+ "write_json_report",
20
+ "write_junit_xml",
21
+ ]
@@ -0,0 +1,113 @@
1
+ """Live progress + optional per-fixture detail output for ``dbression run``.
2
+
3
+ Two things this module provides:
4
+
5
+ * :func:`make_progress` — a Rich ``Progress`` with a spinner, an ``x/y`` fixture counter,
6
+ the elapsed time, and a status line showing which fixture/query is currently running.
7
+ * :class:`ProgressObserver` — a :class:`~dbression.runner.RunObserver` that drives the
8
+ progress bar and, in ``--details`` mode, prints a colored pass/fail line per fixture
9
+ (DBFit-web-UI style) above the live bar.
10
+
11
+ The observer is deliberately defensive about Rich markup: fixture names and SQL contain
12
+ ``[…]`` (MSSQL identifiers, array literals) which Rich would otherwise interpret as style
13
+ tags. All dynamic text is rendered through ``rich.text.Text`` (no markup parsing).
14
+ """
15
+ from __future__ import annotations
16
+
17
+ from rich.console import Console
18
+ from rich.progress import (
19
+ BarColumn,
20
+ MofNCompleteColumn,
21
+ Progress,
22
+ SpinnerColumn,
23
+ TextColumn,
24
+ TimeElapsedColumn,
25
+ )
26
+ from rich.text import Text
27
+
28
+ from dbression.fixtures.base import FixtureResult
29
+ from dbression.parser.ast import Table
30
+ from dbression.runner import RunObserver
31
+
32
+
33
+ def make_progress(console: Console) -> Progress:
34
+ """Build the live progress display (spinner + status + x/y counter + elapsed)."""
35
+ return Progress(
36
+ SpinnerColumn(),
37
+ TextColumn("[progress.description]{task.description}"),
38
+ BarColumn(bar_width=None),
39
+ MofNCompleteColumn(),
40
+ TextColumn("fixtures"),
41
+ TimeElapsedColumn(),
42
+ console=console,
43
+ transient=True, # clear the bar when done so the summary stands alone
44
+ )
45
+
46
+
47
+ def _fixture_preview(table: Table, maxlen: int = 72) -> str:
48
+ """A one-line, whitespace-collapsed preview of what this fixture runs."""
49
+ text = " ".join(str(a) for a in table.header_args)
50
+ text = " ".join(text.split())
51
+ if len(text) > maxlen:
52
+ text = text[: maxlen - 1] + "…"
53
+ return text
54
+
55
+
56
+ class ProgressObserver(RunObserver):
57
+ """Drive a Rich ``Progress`` task and optionally print per-fixture detail lines.
58
+
59
+ Works in two modes:
60
+
61
+ * with a ``progress``/``task_id`` (TTY): updates the live bar and, if ``details``,
62
+ prints detail lines above it via the progress console.
63
+ * without a progress bar (non-TTY but ``details`` requested): prints detail lines
64
+ straight to ``console``.
65
+ """
66
+
67
+ def __init__(
68
+ self,
69
+ console: Console,
70
+ progress: Progress | None = None,
71
+ task_id: int | None = None,
72
+ *,
73
+ details: bool = False,
74
+ ) -> None:
75
+ self.console = console
76
+ self.progress = progress
77
+ self.task_id = task_id
78
+ self.details = details
79
+
80
+ def _out(self) -> Console:
81
+ return self.progress.console if self.progress is not None else self.console
82
+
83
+ def on_fixture_start(self, page_name: str, table: Table) -> None:
84
+ if self.progress is None or self.task_id is None:
85
+ return
86
+ preview = _fixture_preview(table)
87
+ label = f"{page_name} › {table.name}"
88
+ if preview:
89
+ label += f": {preview}"
90
+ # Description is rendered with markup; Text() keeps brackets literal.
91
+ self.progress.update(self.task_id, description=Text(label, style="cyan"))
92
+
93
+ def on_fixture_end(
94
+ self, page_name: str, table: Table, result: FixtureResult, duration: float
95
+ ) -> None:
96
+ if self.progress is not None and self.task_id is not None:
97
+ self.progress.advance(self.task_id)
98
+ if not self.details:
99
+ return
100
+ line = Text()
101
+ if result.passed:
102
+ line.append("✓ ", style="bold green")
103
+ else:
104
+ line.append("✗ ", style="bold red")
105
+ line.append(page_name, style="bold")
106
+ line.append(" › ")
107
+ line.append(table.name, style="" if result.passed else "red")
108
+ line.append(f" {duration * 1000:.0f}ms", style="dim")
109
+ self._out().print(line)
110
+ if result.message:
111
+ self._out().print(
112
+ Text(" " + result.message, style="dim" if result.passed else "red")
113
+ )