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.
- {dbression-0.2.0 → dbression-0.3.1}/CHANGELOG.md +33 -4
- {dbression-0.2.0 → dbression-0.3.1}/PKG-INFO +18 -37
- {dbression-0.2.0 → dbression-0.3.1}/README.md +17 -36
- {dbression-0.2.0 → dbression-0.3.1}/pyproject.toml +1 -1
- {dbression-0.2.0 → dbression-0.3.1}/src/dbression/cli.py +101 -12
- dbression-0.3.1/src/dbression/parser/__init__.py +12 -0
- {dbression-0.2.0 → dbression-0.3.1}/src/dbression/parser/wiki.py +63 -0
- {dbression-0.2.0 → dbression-0.3.1}/src/dbression/report/__init__.py +11 -1
- dbression-0.3.1/src/dbression/report/progress.py +113 -0
- dbression-0.3.1/src/dbression/report/render.py +267 -0
- {dbression-0.2.0 → dbression-0.3.1}/src/dbression/runner.py +61 -9
- dbression-0.3.1/tests/test_progress_and_single_file.py +128 -0
- dbression-0.3.1/tests/test_render.py +120 -0
- {dbression-0.2.0 → dbression-0.3.1}/uv.lock +1 -1
- dbression-0.2.0/src/dbression/parser/__init__.py +0 -4
- {dbression-0.2.0 → dbression-0.3.1}/.github/workflows/ci.yml +0 -0
- {dbression-0.2.0 → dbression-0.3.1}/.github/workflows/publish.yml +0 -0
- {dbression-0.2.0 → dbression-0.3.1}/.gitignore +0 -0
- {dbression-0.2.0 → dbression-0.3.1}/.python-version +0 -0
- {dbression-0.2.0 → dbression-0.3.1}/LICENSE +0 -0
- {dbression-0.2.0 → dbression-0.3.1}/docs/dbression_head.png +0 -0
- {dbression-0.2.0 → dbression-0.3.1}/examples/01-hello/HelloSql.test.md +0 -0
- {dbression-0.2.0 → dbression-0.3.1}/examples/01-hello/_root.wiki +0 -0
- {dbression-0.2.0 → dbression-0.3.1}/examples/01-hello/connection.properties +0 -0
- {dbression-0.2.0 → dbression-0.3.1}/examples/02-stored-procedure/BumpCounterTest.test.md +0 -0
- {dbression-0.2.0 → dbression-0.3.1}/examples/02-stored-procedure/SuiteSetUp.test.md +0 -0
- {dbression-0.2.0 → dbression-0.3.1}/examples/02-stored-procedure/SuiteTearDown.test.md +0 -0
- {dbression-0.2.0 → dbression-0.3.1}/examples/02-stored-procedure/_root.wiki +0 -0
- {dbression-0.2.0 → dbression-0.3.1}/examples/02-stored-procedure/connection.properties +0 -0
- {dbression-0.2.0 → dbression-0.3.1}/examples/03-schema-drift/SchemaSnapshotTest.test.md +0 -0
- {dbression-0.2.0 → dbression-0.3.1}/examples/03-schema-drift/SuiteSetUp.test.md +0 -0
- {dbression-0.2.0 → dbression-0.3.1}/examples/03-schema-drift/SuiteTearDown.test.md +0 -0
- {dbression-0.2.0 → dbression-0.3.1}/examples/03-schema-drift/_root.wiki +0 -0
- {dbression-0.2.0 → dbression-0.3.1}/examples/03-schema-drift/connection.properties +0 -0
- {dbression-0.2.0 → dbression-0.3.1}/examples/README.md +0 -0
- {dbression-0.2.0 → dbression-0.3.1}/src/dbression/__init__.py +0 -0
- {dbression-0.2.0 → dbression-0.3.1}/src/dbression/db/__init__.py +0 -0
- {dbression-0.2.0 → dbression-0.3.1}/src/dbression/db/connection.py +0 -0
- {dbression-0.2.0 → dbression-0.3.1}/src/dbression/db/engine.py +0 -0
- {dbression-0.2.0 → dbression-0.3.1}/src/dbression/db/errors.py +0 -0
- {dbression-0.2.0 → dbression-0.3.1}/src/dbression/fixtures/__init__.py +0 -0
- {dbression-0.2.0 → dbression-0.3.1}/src/dbression/fixtures/base.py +0 -0
- {dbression-0.2.0 → dbression-0.3.1}/src/dbression/fixtures/basic.py +0 -0
- {dbression-0.2.0 → dbression-0.3.1}/src/dbression/fixtures/inspect_and_store.py +0 -0
- {dbression-0.2.0 → dbression-0.3.1}/src/dbression/fixtures/plugins.py +0 -0
- {dbression-0.2.0 → dbression-0.3.1}/src/dbression/fixtures/suite_fixtures.py +0 -0
- {dbression-0.2.0 → dbression-0.3.1}/src/dbression/parser/ast.py +0 -0
- {dbression-0.2.0 → dbression-0.3.1}/src/dbression/parser/markdown.py +0 -0
- {dbression-0.2.0 → dbression-0.3.1}/src/dbression/parser/markdown_writer.py +0 -0
- {dbression-0.2.0 → dbression-0.3.1}/src/dbression/parser/tokenizer.py +0 -0
- {dbression-0.2.0 → dbression-0.3.1}/src/dbression/report/console.py +0 -0
- {dbression-0.2.0 → dbression-0.3.1}/src/dbression/report/json_report.py +0 -0
- {dbression-0.2.0 → dbression-0.3.1}/src/dbression/report/junit.py +0 -0
- {dbression-0.2.0 → dbression-0.3.1}/src/dbression/symbols.py +0 -0
- {dbression-0.2.0 → dbression-0.3.1}/tests/conftest.py +0 -0
- {dbression-0.2.0 → dbression-0.3.1}/tests/test_connection.py +0 -0
- {dbression-0.2.0 → dbression-0.3.1}/tests/test_markdown_parser.py +0 -0
- {dbression-0.2.0 → dbression-0.3.1}/tests/test_markdown_roundtrip.py +0 -0
- {dbression-0.2.0 → dbression-0.3.1}/tests/test_parser.py +0 -0
- {dbression-0.2.0 → dbression-0.3.1}/tests/test_phase2_fixtures.py +0 -0
- {dbression-0.2.0 → dbression-0.3.1}/tests/test_plugins.py +0 -0
- {dbression-0.2.0 → dbression-0.3.1}/tests/test_postgres_live.py +0 -0
- {dbression-0.2.0 → dbression-0.3.1}/tests/test_report_junit.py +0 -0
- {dbression-0.2.0 → dbression-0.3.1}/tests/test_smoke.py +0 -0
- {dbression-0.2.0 → dbression-0.3.1}/tests/test_sqlite_engine.py +0 -0
- {dbression-0.2.0 → dbression-0.3.1}/tests/test_symbols.py +0 -0
- {dbression-0.2.0 → dbression-0.3.1}/tests/test_tokenizer.py +0 -0
- {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 (
|
|
25
|
-
|
|
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 `[
|
|
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.
|
|
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.
|
|
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.
|
|
55
|
+
dbression 0.3.1 — Suite: tests @ postgresql+psycopg://foo:***@db01/bar
|
|
56
56
|
|
|
57
57
|
✓ HelloSql 0.004s
|
|
58
58
|
CommonSuite/
|
|
59
|
-
|
|
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
|
-
✓
|
|
66
|
-
|
|
67
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
18
|
+
dbression 0.3.1 — Suite: tests @ postgresql+psycopg://foo:***@db01/bar
|
|
19
19
|
|
|
20
20
|
✓ HelloSql 0.004s
|
|
21
21
|
CommonSuite/
|
|
22
|
-
|
|
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
|
-
✓
|
|
29
|
-
|
|
30
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
|
@@ -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
|
|
15
|
-
|
|
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[
|
|
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
|
|
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
|
-
|
|
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__} —
|
|
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
|
-
|
|
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
|
-
|
|
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__ = [
|
|
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
|
+
)
|