dbression 0.2.0__tar.gz → 0.3.0__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.0}/CHANGELOG.md +22 -4
- {dbression-0.2.0 → dbression-0.3.0}/PKG-INFO +13 -38
- {dbression-0.2.0 → dbression-0.3.0}/README.md +12 -37
- {dbression-0.2.0 → dbression-0.3.0}/pyproject.toml +1 -1
- {dbression-0.2.0 → dbression-0.3.0}/src/dbression/cli.py +72 -11
- dbression-0.3.0/src/dbression/parser/__init__.py +12 -0
- {dbression-0.2.0 → dbression-0.3.0}/src/dbression/parser/wiki.py +63 -0
- {dbression-0.2.0 → dbression-0.3.0}/src/dbression/report/__init__.py +9 -1
- dbression-0.3.0/src/dbression/report/progress.py +113 -0
- {dbression-0.2.0 → dbression-0.3.0}/src/dbression/runner.py +61 -9
- dbression-0.3.0/tests/test_progress_and_single_file.py +128 -0
- {dbression-0.2.0 → dbression-0.3.0}/uv.lock +1 -1
- dbression-0.2.0/src/dbression/parser/__init__.py +0 -4
- {dbression-0.2.0 → dbression-0.3.0}/.github/workflows/ci.yml +0 -0
- {dbression-0.2.0 → dbression-0.3.0}/.github/workflows/publish.yml +0 -0
- {dbression-0.2.0 → dbression-0.3.0}/.gitignore +0 -0
- {dbression-0.2.0 → dbression-0.3.0}/.python-version +0 -0
- {dbression-0.2.0 → dbression-0.3.0}/LICENSE +0 -0
- {dbression-0.2.0 → dbression-0.3.0}/docs/dbression_head.png +0 -0
- {dbression-0.2.0 → dbression-0.3.0}/examples/01-hello/HelloSql.test.md +0 -0
- {dbression-0.2.0 → dbression-0.3.0}/examples/01-hello/_root.wiki +0 -0
- {dbression-0.2.0 → dbression-0.3.0}/examples/01-hello/connection.properties +0 -0
- {dbression-0.2.0 → dbression-0.3.0}/examples/02-stored-procedure/BumpCounterTest.test.md +0 -0
- {dbression-0.2.0 → dbression-0.3.0}/examples/02-stored-procedure/SuiteSetUp.test.md +0 -0
- {dbression-0.2.0 → dbression-0.3.0}/examples/02-stored-procedure/SuiteTearDown.test.md +0 -0
- {dbression-0.2.0 → dbression-0.3.0}/examples/02-stored-procedure/_root.wiki +0 -0
- {dbression-0.2.0 → dbression-0.3.0}/examples/02-stored-procedure/connection.properties +0 -0
- {dbression-0.2.0 → dbression-0.3.0}/examples/03-schema-drift/SchemaSnapshotTest.test.md +0 -0
- {dbression-0.2.0 → dbression-0.3.0}/examples/03-schema-drift/SuiteSetUp.test.md +0 -0
- {dbression-0.2.0 → dbression-0.3.0}/examples/03-schema-drift/SuiteTearDown.test.md +0 -0
- {dbression-0.2.0 → dbression-0.3.0}/examples/03-schema-drift/_root.wiki +0 -0
- {dbression-0.2.0 → dbression-0.3.0}/examples/03-schema-drift/connection.properties +0 -0
- {dbression-0.2.0 → dbression-0.3.0}/examples/README.md +0 -0
- {dbression-0.2.0 → dbression-0.3.0}/src/dbression/__init__.py +0 -0
- {dbression-0.2.0 → dbression-0.3.0}/src/dbression/db/__init__.py +0 -0
- {dbression-0.2.0 → dbression-0.3.0}/src/dbression/db/connection.py +0 -0
- {dbression-0.2.0 → dbression-0.3.0}/src/dbression/db/engine.py +0 -0
- {dbression-0.2.0 → dbression-0.3.0}/src/dbression/db/errors.py +0 -0
- {dbression-0.2.0 → dbression-0.3.0}/src/dbression/fixtures/__init__.py +0 -0
- {dbression-0.2.0 → dbression-0.3.0}/src/dbression/fixtures/base.py +0 -0
- {dbression-0.2.0 → dbression-0.3.0}/src/dbression/fixtures/basic.py +0 -0
- {dbression-0.2.0 → dbression-0.3.0}/src/dbression/fixtures/inspect_and_store.py +0 -0
- {dbression-0.2.0 → dbression-0.3.0}/src/dbression/fixtures/plugins.py +0 -0
- {dbression-0.2.0 → dbression-0.3.0}/src/dbression/fixtures/suite_fixtures.py +0 -0
- {dbression-0.2.0 → dbression-0.3.0}/src/dbression/parser/ast.py +0 -0
- {dbression-0.2.0 → dbression-0.3.0}/src/dbression/parser/markdown.py +0 -0
- {dbression-0.2.0 → dbression-0.3.0}/src/dbression/parser/markdown_writer.py +0 -0
- {dbression-0.2.0 → dbression-0.3.0}/src/dbression/parser/tokenizer.py +0 -0
- {dbression-0.2.0 → dbression-0.3.0}/src/dbression/report/console.py +0 -0
- {dbression-0.2.0 → dbression-0.3.0}/src/dbression/report/json_report.py +0 -0
- {dbression-0.2.0 → dbression-0.3.0}/src/dbression/report/junit.py +0 -0
- {dbression-0.2.0 → dbression-0.3.0}/src/dbression/symbols.py +0 -0
- {dbression-0.2.0 → dbression-0.3.0}/tests/conftest.py +0 -0
- {dbression-0.2.0 → dbression-0.3.0}/tests/test_connection.py +0 -0
- {dbression-0.2.0 → dbression-0.3.0}/tests/test_markdown_parser.py +0 -0
- {dbression-0.2.0 → dbression-0.3.0}/tests/test_markdown_roundtrip.py +0 -0
- {dbression-0.2.0 → dbression-0.3.0}/tests/test_parser.py +0 -0
- {dbression-0.2.0 → dbression-0.3.0}/tests/test_phase2_fixtures.py +0 -0
- {dbression-0.2.0 → dbression-0.3.0}/tests/test_plugins.py +0 -0
- {dbression-0.2.0 → dbression-0.3.0}/tests/test_postgres_live.py +0 -0
- {dbression-0.2.0 → dbression-0.3.0}/tests/test_report_junit.py +0 -0
- {dbression-0.2.0 → dbression-0.3.0}/tests/test_smoke.py +0 -0
- {dbression-0.2.0 → dbression-0.3.0}/tests/test_sqlite_engine.py +0 -0
- {dbression-0.2.0 → dbression-0.3.0}/tests/test_symbols.py +0 -0
- {dbression-0.2.0 → dbression-0.3.0}/tests/test_tokenizer.py +0 -0
- {dbression-0.2.0 → dbression-0.3.0}/tests/test_update_fixture.py +0 -0
|
@@ -7,6 +7,23 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.3.0] — 2026-06-08
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- **Single-file runs.** `dbression run path/to/MyTest.test.md` (or `.wiki`) now runs a
|
|
15
|
+
single test, mirroring DBFit: a file is one *Test* (many fixtures), a directory is a
|
|
16
|
+
*Suite*. The containing directory's `SuiteSetUp` / `SuiteTearDown` and `_root`
|
|
17
|
+
(connection + `DatabaseEnvironment`) are included automatically. A self-contained
|
|
18
|
+
`.test.md` carrying its own `<!-- dbression:env=… -->` / `<!-- dbression:connection=… -->`
|
|
19
|
+
directives runs standalone.
|
|
20
|
+
- **Live progress.** Runs now show a spinner, an `x/y fixtures` counter, elapsed time, and
|
|
21
|
+
a status line naming the fixture/query currently executing. On by default at a TTY;
|
|
22
|
+
silenced automatically when output is piped or in CI. Toggle with `--progress` /
|
|
23
|
+
`--no-progress`.
|
|
24
|
+
- **`--details` / `-d`.** Prints each fixture's result green/red as it runs (DBFit-web-UI
|
|
25
|
+
style), so a `.test.md` viewed in `glow` has a matching colored CLI run alongside it.
|
|
26
|
+
|
|
10
27
|
## [0.2.0] — 2026-06-01
|
|
11
28
|
|
|
12
29
|
### Added
|
|
@@ -21,15 +38,15 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
|
21
38
|
|
|
22
39
|
## [0.1.2] — 2026-06-01
|
|
23
40
|
|
|
24
|
-
SQL Server live-verification release. A real-world MSSQL DBFit suite (
|
|
25
|
-
|
|
41
|
+
SQL Server live-verification release. A real-world MSSQL DBFit suite (regression tests,
|
|
42
|
+
stored-procedure-driven, square-bracket identifiers throughout)
|
|
26
43
|
runs end-to-end against an MSSQL Server 2019 via `pymssql` with no changes to the
|
|
27
44
|
underlying `.wiki` files.
|
|
28
45
|
|
|
29
46
|
### Fixed
|
|
30
47
|
|
|
31
48
|
- **Console reporter swallowed `[…]` in failure details.** Rich's inline-markup
|
|
32
|
-
parser interpreted MSSQL square-bracket identifiers like `[
|
|
49
|
+
parser interpreted MSSQL square-bracket identifiers like `[dbo].ORDER` as
|
|
33
50
|
style tags and stripped them from the displayed SQL — making failure output
|
|
34
51
|
misleading. Reporter now prints fixture details with `markup=False`, so the
|
|
35
52
|
SQL you see is the SQL that ran.
|
|
@@ -132,7 +149,8 @@ with code paths in place for SQL Server and Oracle.
|
|
|
132
149
|
reject `python-oracledb` thin-mode authentication. Configuration is via
|
|
133
150
|
`DBRESSION_ORACLE_CLIENT_LIB_DIR`.
|
|
134
151
|
|
|
135
|
-
[Unreleased]: https://github.com/angrydat/dbression/compare/v0.
|
|
152
|
+
[Unreleased]: https://github.com/angrydat/dbression/compare/v0.3.0...HEAD
|
|
153
|
+
[0.3.0]: https://github.com/angrydat/dbression/releases/tag/v0.3.0
|
|
136
154
|
[0.2.0]: https://github.com/angrydat/dbression/releases/tag/v0.2.0
|
|
137
155
|
[0.1.2]: https://github.com/angrydat/dbression/releases/tag/v0.1.2
|
|
138
156
|
[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.0
|
|
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.0 — 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,10 @@ 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> -d # --details: print each fixture green/red as it runs
|
|
283
|
+
dbression run <path> --no-progress # disable the live spinner / x-of-y counter
|
|
281
284
|
dbression run <path> -v # show every fixture table, not just the page line
|
|
282
285
|
dbression run <path> --tag critical # only run pages tagged `critical`
|
|
283
286
|
dbression run <path> --skip-tag NotOnCI # skip pages tagged `NotOnCI`
|
|
@@ -289,37 +292,9 @@ dbression convert file.wiki -o out.test.md # single file with explicit output
|
|
|
289
292
|
dbression version
|
|
290
293
|
```
|
|
291
294
|
|
|
292
|
-
|
|
293
|
-
|
|
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
|
-
```
|
|
295
|
+
A run shows a live spinner with an `x/y fixtures` counter and the currently-executing
|
|
296
|
+
query (auto-disabled when piped or in CI). Add `-d`/`--details` to stream a colored
|
|
297
|
+
pass/fail line per fixture — handy next to a `.test.md` you're previewing in `glow`.
|
|
323
298
|
|
|
324
299
|
## Status
|
|
325
300
|
|
|
@@ -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.0 — 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,10 @@ 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> -d # --details: print each fixture green/red as it runs
|
|
246
|
+
dbression run <path> --no-progress # disable the live spinner / x-of-y counter
|
|
244
247
|
dbression run <path> -v # show every fixture table, not just the page line
|
|
245
248
|
dbression run <path> --tag critical # only run pages tagged `critical`
|
|
246
249
|
dbression run <path> --skip-tag NotOnCI # skip pages tagged `NotOnCI`
|
|
@@ -252,37 +255,9 @@ dbression convert file.wiki -o out.test.md # single file with explicit output
|
|
|
252
255
|
dbression version
|
|
253
256
|
```
|
|
254
257
|
|
|
255
|
-
|
|
256
|
-
|
|
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
|
-
```
|
|
258
|
+
A run shows a live spinner with an `x/y fixtures` counter and the currently-executing
|
|
259
|
+
query (auto-disabled when piped or in CI). Add `-d`/`--details` to stream a colored
|
|
260
|
+
pass/fail line per fixture — handy next to a `.test.md` you're previewing in `glow`.
|
|
286
261
|
|
|
287
262
|
## Status
|
|
288
263
|
|
|
@@ -8,11 +8,17 @@ 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
|
+
write_json_report,
|
|
19
|
+
write_junit_xml,
|
|
20
|
+
)
|
|
21
|
+
from dbression.runner import TagFilter, build_engine_for_suite, count_fixtures, run_suite
|
|
16
22
|
|
|
17
23
|
app = typer.Typer(
|
|
18
24
|
name="dbression",
|
|
@@ -37,7 +43,12 @@ def version() -> None:
|
|
|
37
43
|
|
|
38
44
|
@app.command()
|
|
39
45
|
def run(
|
|
40
|
-
path: Annotated[
|
|
46
|
+
path: Annotated[
|
|
47
|
+
Path,
|
|
48
|
+
typer.Argument(
|
|
49
|
+
help="A suite directory (with _root.wiki) OR a single test file (.test.md / .wiki)"
|
|
50
|
+
),
|
|
51
|
+
],
|
|
41
52
|
commit_mode: Annotated[
|
|
42
53
|
str,
|
|
43
54
|
typer.Option(
|
|
@@ -48,6 +59,21 @@ def run(
|
|
|
48
59
|
verbose: Annotated[
|
|
49
60
|
bool, typer.Option("-v", "--verbose", help="Print every fixture table, not just the page line")
|
|
50
61
|
] = False,
|
|
62
|
+
details: Annotated[
|
|
63
|
+
bool,
|
|
64
|
+
typer.Option(
|
|
65
|
+
"-d",
|
|
66
|
+
"--details",
|
|
67
|
+
help="Print each fixture's result (green/red) live as it runs, DBFit-web-UI style",
|
|
68
|
+
),
|
|
69
|
+
] = False,
|
|
70
|
+
progress: Annotated[
|
|
71
|
+
bool | None,
|
|
72
|
+
typer.Option(
|
|
73
|
+
"--progress/--no-progress",
|
|
74
|
+
help="Live spinner + x/y fixture counter (default: on when stdout is a terminal)",
|
|
75
|
+
),
|
|
76
|
+
] = None,
|
|
51
77
|
tag: Annotated[
|
|
52
78
|
list[str] | None,
|
|
53
79
|
typer.Option(
|
|
@@ -68,10 +94,7 @@ def run(
|
|
|
68
94
|
typer.Option("--json", help="Write a JSON report to this path (LLM / tooling)"),
|
|
69
95
|
] = None,
|
|
70
96
|
) -> 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)
|
|
97
|
+
"""Run a dbression test (single file) or suite (directory) with live progress."""
|
|
75
98
|
if commit_mode not in ("test", "page"):
|
|
76
99
|
console.print(
|
|
77
100
|
f"[red]Invalid commit-mode: {commit_mode!r} (allowed: test, page)[/red]"
|
|
@@ -80,14 +103,52 @@ def run(
|
|
|
80
103
|
|
|
81
104
|
tag_filter = TagFilter(only=tuple(tag or ()), skip=tuple(skip_tag or ()))
|
|
82
105
|
|
|
83
|
-
|
|
106
|
+
if path.is_dir():
|
|
107
|
+
suite = parse_suite(path)
|
|
108
|
+
kind = "Suite"
|
|
109
|
+
elif path.is_file():
|
|
110
|
+
try:
|
|
111
|
+
suite = parse_test_file(path)
|
|
112
|
+
except ValueError as e:
|
|
113
|
+
console.print(f"[red]{e}[/red]")
|
|
114
|
+
raise typer.Exit(2)
|
|
115
|
+
kind = "Test"
|
|
116
|
+
else:
|
|
117
|
+
console.print(f"[red]Path does not exist:[/red] {path}")
|
|
118
|
+
raise typer.Exit(2)
|
|
119
|
+
|
|
84
120
|
engine = build_engine_for_suite(suite)
|
|
85
121
|
console.print(
|
|
86
|
-
f"dbression {__version__} —
|
|
122
|
+
f"dbression {__version__} — {kind}: [bold]{suite.name}[/bold] @ "
|
|
87
123
|
f"{engine.url.render_as_string(hide_password=True)}\n"
|
|
88
124
|
)
|
|
125
|
+
|
|
126
|
+
# Progress is on by default at a TTY; --progress / --no-progress override.
|
|
127
|
+
use_progress = console.is_terminal if progress is None else progress
|
|
128
|
+
total = count_fixtures(suite, tag_filter)
|
|
129
|
+
|
|
89
130
|
try:
|
|
90
|
-
|
|
131
|
+
if use_progress:
|
|
132
|
+
with make_progress(console) as prog:
|
|
133
|
+
task = prog.add_task("starting…", total=total or None)
|
|
134
|
+
observer = ProgressObserver(console, prog, task, details=details)
|
|
135
|
+
result = run_suite(
|
|
136
|
+
suite,
|
|
137
|
+
engine,
|
|
138
|
+
commit_mode=commit_mode, # type: ignore[arg-type]
|
|
139
|
+
tag_filter=tag_filter,
|
|
140
|
+
observer=observer,
|
|
141
|
+
)
|
|
142
|
+
else:
|
|
143
|
+
# No live bar (piped / CI). Still stream colored detail lines if asked.
|
|
144
|
+
observer = ProgressObserver(console, details=details) if details else None
|
|
145
|
+
result = run_suite(
|
|
146
|
+
suite,
|
|
147
|
+
engine,
|
|
148
|
+
commit_mode=commit_mode, # type: ignore[arg-type]
|
|
149
|
+
tag_filter=tag_filter,
|
|
150
|
+
observer=observer,
|
|
151
|
+
)
|
|
91
152
|
finally:
|
|
92
153
|
engine.dispose()
|
|
93
154
|
print_suite_result(result, console, verbose=verbose)
|
|
@@ -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,19 @@
|
|
|
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
|
|
10
12
|
|
|
11
|
-
__all__ = [
|
|
13
|
+
__all__ = [
|
|
14
|
+
"ProgressObserver",
|
|
15
|
+
"make_progress",
|
|
16
|
+
"print_suite_result",
|
|
17
|
+
"write_json_report",
|
|
18
|
+
"write_junit_xml",
|
|
19
|
+
]
|
|
@@ -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
|
+
)
|
|
@@ -18,6 +18,23 @@ from dbression.symbols import SymbolTable
|
|
|
18
18
|
CommitMode = Literal["test", "page"]
|
|
19
19
|
|
|
20
20
|
|
|
21
|
+
class RunObserver:
|
|
22
|
+
"""Hook for live run feedback. Default methods are no-ops — subclass to react.
|
|
23
|
+
|
|
24
|
+
The runner calls ``on_fixture_start`` immediately before a fixture table is evaluated
|
|
25
|
+
and ``on_fixture_end`` once its result is known, for every fixture that actually runs
|
|
26
|
+
(suite directives like ``DatabaseEnvironment`` are skipped, matching ``count_fixtures``).
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
def on_fixture_start(self, page_name: str, table: Table) -> None: # noqa: D401
|
|
30
|
+
...
|
|
31
|
+
|
|
32
|
+
def on_fixture_end(
|
|
33
|
+
self, page_name: str, table: Table, result: FixtureResult, duration: float
|
|
34
|
+
) -> None:
|
|
35
|
+
...
|
|
36
|
+
|
|
37
|
+
|
|
21
38
|
@dataclass(slots=True)
|
|
22
39
|
class TableResult:
|
|
23
40
|
name: str
|
|
@@ -70,6 +87,31 @@ def _is_suite_directive(table: Table) -> bool:
|
|
|
70
87
|
return norm in _SUITE_DIRECTIVE_FIXTURES
|
|
71
88
|
|
|
72
89
|
|
|
90
|
+
def _count_page_fixtures(page: Page | None) -> int:
|
|
91
|
+
if page is None:
|
|
92
|
+
return 0
|
|
93
|
+
return sum(1 for t in page.tables if not _is_suite_directive(t))
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def count_fixtures(suite: Suite, tag_filter: "TagFilter | None" = None) -> int:
|
|
97
|
+
"""Count the fixtures that will actually run — for an ``x/y`` progress total.
|
|
98
|
+
|
|
99
|
+
Mirrors what ``_run_page_in_tx`` executes: SuiteSetUp + tag-allowed test pages +
|
|
100
|
+
SuiteTearDown, recursively through sub-suites, skipping suite-directive tables.
|
|
101
|
+
May slightly overcount when a page or setup fails early (remaining fixtures are then
|
|
102
|
+
skipped) — that only means the bar stops short of 100%, which is itself informative.
|
|
103
|
+
"""
|
|
104
|
+
n = _count_page_fixtures(suite.setup)
|
|
105
|
+
for page in suite.pages:
|
|
106
|
+
if tag_filter is not None and not tag_filter.page_allowed(page):
|
|
107
|
+
continue
|
|
108
|
+
n += _count_page_fixtures(page)
|
|
109
|
+
n += _count_page_fixtures(suite.teardown)
|
|
110
|
+
for sub in suite.subsuites:
|
|
111
|
+
n += count_fixtures(sub, tag_filter)
|
|
112
|
+
return n
|
|
113
|
+
|
|
114
|
+
|
|
73
115
|
def _scan_engine_config(page: Page | None) -> tuple[str | None, str | None]:
|
|
74
116
|
"""Scan a page for `DatabaseEnvironment` and `ConnectUsingFile` directives."""
|
|
75
117
|
if page is None:
|
|
@@ -164,6 +206,7 @@ def run_suite(
|
|
|
164
206
|
symbols: SymbolTable | None = None,
|
|
165
207
|
tag_filter: TagFilter | None = None,
|
|
166
208
|
stored: dict[str, StoredQuery] | None = None,
|
|
209
|
+
observer: RunObserver | None = None,
|
|
167
210
|
) -> SuiteResult:
|
|
168
211
|
"""Run a suite recursively (including sub-suites).
|
|
169
212
|
|
|
@@ -186,7 +229,7 @@ def run_suite(
|
|
|
186
229
|
# SuiteSetUp — runs inside the suite TX, no commit/rollback of its own.
|
|
187
230
|
if suite.setup is not None:
|
|
188
231
|
result.setup_result = _run_page_in_tx(
|
|
189
|
-
suite.setup, conn, symbols, stored, isolate=False
|
|
232
|
+
suite.setup, conn, symbols, stored, isolate=False, observer=observer
|
|
190
233
|
)
|
|
191
234
|
if not result.setup_result.passed:
|
|
192
235
|
result.error = f"SuiteSetUp failed: {result.setup_result.name}"
|
|
@@ -197,13 +240,15 @@ def run_suite(
|
|
|
197
240
|
if tag_filter is not None and not tag_filter.page_allowed(page):
|
|
198
241
|
continue
|
|
199
242
|
isolate = commit_mode == "test"
|
|
200
|
-
pr = _run_page_in_tx(
|
|
243
|
+
pr = _run_page_in_tx(
|
|
244
|
+
page, conn, symbols, stored, isolate=isolate, observer=observer
|
|
245
|
+
)
|
|
201
246
|
result.pages.append(pr)
|
|
202
247
|
|
|
203
248
|
# Sub-suites: recursive, with their own engine if directives differ.
|
|
204
249
|
for sub in suite.subsuites:
|
|
205
250
|
sub_res = _run_subsuite(
|
|
206
|
-
sub, engine, conn, commit_mode, symbols, tag_filter, stored
|
|
251
|
+
sub, engine, conn, commit_mode, symbols, tag_filter, stored, observer
|
|
207
252
|
)
|
|
208
253
|
result.subsuites.append(sub_res)
|
|
209
254
|
|
|
@@ -212,7 +257,7 @@ def run_suite(
|
|
|
212
257
|
# connection stays valid.
|
|
213
258
|
if suite.teardown is not None:
|
|
214
259
|
result.teardown_result = _run_page_in_tx(
|
|
215
|
-
suite.teardown, conn, symbols, stored, isolate=False
|
|
260
|
+
suite.teardown, conn, symbols, stored, isolate=False, observer=observer
|
|
216
261
|
)
|
|
217
262
|
finally:
|
|
218
263
|
# Defensive rollback of the entire suite TX. If TearDown already rolled
|
|
@@ -233,6 +278,7 @@ def _run_subsuite(
|
|
|
233
278
|
symbols: SymbolTable,
|
|
234
279
|
tag_filter: TagFilter | None = None,
|
|
235
280
|
stored: dict[str, StoredQuery] | None = None,
|
|
281
|
+
observer: RunObserver | None = None,
|
|
236
282
|
) -> SuiteResult:
|
|
237
283
|
"""Run a sub-suite. If it has its own engine directives, use a local engine; otherwise
|
|
238
284
|
run on the parent engine (with its own connection, for clean TX isolation).
|
|
@@ -251,11 +297,13 @@ def _run_subsuite(
|
|
|
251
297
|
except Exception as e:
|
|
252
298
|
return SuiteResult(name=sub.name, error=f"Engine build: {type(e).__name__}: {e}")
|
|
253
299
|
try:
|
|
254
|
-
return run_suite(
|
|
300
|
+
return run_suite(
|
|
301
|
+
sub, sub_engine, commit_mode, symbols, tag_filter, stored, observer
|
|
302
|
+
)
|
|
255
303
|
finally:
|
|
256
304
|
sub_engine.dispose()
|
|
257
305
|
|
|
258
|
-
return run_suite(sub, parent_engine, commit_mode, symbols, tag_filter, stored)
|
|
306
|
+
return run_suite(sub, parent_engine, commit_mode, symbols, tag_filter, stored, observer)
|
|
259
307
|
|
|
260
308
|
|
|
261
309
|
def _run_page_in_tx(
|
|
@@ -264,6 +312,7 @@ def _run_page_in_tx(
|
|
|
264
312
|
symbols: SymbolTable,
|
|
265
313
|
stored: dict[str, StoredQuery],
|
|
266
314
|
isolate: bool,
|
|
315
|
+
observer: RunObserver | None = None,
|
|
267
316
|
) -> PageResult:
|
|
268
317
|
"""Run all fixture tables of a page inside the existing suite TX.
|
|
269
318
|
|
|
@@ -278,6 +327,8 @@ def _run_page_in_tx(
|
|
|
278
327
|
for table in page.tables:
|
|
279
328
|
if _is_suite_directive(table):
|
|
280
329
|
continue
|
|
330
|
+
if observer is not None:
|
|
331
|
+
observer.on_fixture_start(page.name, table)
|
|
281
332
|
start = time.perf_counter()
|
|
282
333
|
fixture_cls = resolve_fixture(table.name)
|
|
283
334
|
if fixture_cls is None:
|
|
@@ -294,9 +345,10 @@ def _run_page_in_tx(
|
|
|
294
345
|
message=f"Fixture crash: {type(e).__name__}",
|
|
295
346
|
details=str(e),
|
|
296
347
|
)
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
348
|
+
duration = time.perf_counter() - start
|
|
349
|
+
pr.tables.append(TableResult(name=table.name, result=res, duration=duration))
|
|
350
|
+
if observer is not None:
|
|
351
|
+
observer.on_fixture_end(page.name, table, res, duration)
|
|
300
352
|
if not res.passed:
|
|
301
353
|
if savepoint is not None and savepoint.is_active:
|
|
302
354
|
savepoint.rollback()
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
"""Tests for the run observer, fixture counting, single-file runs, and the progress
|
|
2
|
+
observer — all against in-memory SQLite (no server needed)."""
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
import pytest
|
|
8
|
+
from rich.console import Console
|
|
9
|
+
|
|
10
|
+
from dbression.fixtures.base import FixtureResult
|
|
11
|
+
from dbression.parser import parse_suite, parse_test_file
|
|
12
|
+
from dbression.parser.ast import Table
|
|
13
|
+
from dbression.report.progress import ProgressObserver, _fixture_preview, make_progress
|
|
14
|
+
from dbression.runner import (
|
|
15
|
+
RunObserver,
|
|
16
|
+
TagFilter,
|
|
17
|
+
build_engine_for_suite,
|
|
18
|
+
count_fixtures,
|
|
19
|
+
run_suite,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@pytest.fixture
|
|
24
|
+
def demo_suite(tmp_path: Path) -> Path:
|
|
25
|
+
(tmp_path / "connection.properties").write_text("service=:memory:\n")
|
|
26
|
+
(tmp_path / "_root.wiki").write_text(
|
|
27
|
+
"!|DatabaseEnvironment|sqlite|\n|ConnectUsingFile|connection.properties|\n"
|
|
28
|
+
)
|
|
29
|
+
(tmp_path / "SuiteSetUp.test.md").write_text(
|
|
30
|
+
"# Setup\n\n### Execute\n```sql\ncreate table t (n int)\n```\n\n"
|
|
31
|
+
"### Execute\n```sql\ninsert into t values (1),(2),(3)\n```\n"
|
|
32
|
+
)
|
|
33
|
+
(tmp_path / "CountTest.test.md").write_text(
|
|
34
|
+
"# Count\n\n### Query\n```sql\nselect count(*) as c from t\n```\n\n| c |\n|---|\n| 3 |\n"
|
|
35
|
+
)
|
|
36
|
+
return tmp_path
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def test_count_fixtures_matches_observed(demo_suite: Path) -> None:
|
|
40
|
+
suite = parse_suite(demo_suite)
|
|
41
|
+
total = count_fixtures(suite, TagFilter())
|
|
42
|
+
# 2 setup Execute + 1 page Query = 3 (DatabaseEnvironment/ConnectUsingFile excluded)
|
|
43
|
+
assert total == 3
|
|
44
|
+
|
|
45
|
+
class Recorder(RunObserver):
|
|
46
|
+
def __init__(self) -> None:
|
|
47
|
+
self.started: list[str] = []
|
|
48
|
+
self.ended: list[bool] = []
|
|
49
|
+
|
|
50
|
+
def on_fixture_start(self, page_name: str, table: Table) -> None:
|
|
51
|
+
self.started.append(f"{page_name}:{table.name}")
|
|
52
|
+
|
|
53
|
+
def on_fixture_end(self, page_name, table, result, duration) -> None:
|
|
54
|
+
self.ended.append(result.passed)
|
|
55
|
+
|
|
56
|
+
rec = Recorder()
|
|
57
|
+
engine = build_engine_for_suite(suite)
|
|
58
|
+
try:
|
|
59
|
+
result = run_suite(suite, engine, observer=rec)
|
|
60
|
+
finally:
|
|
61
|
+
engine.dispose()
|
|
62
|
+
|
|
63
|
+
assert result.passed_count == 1
|
|
64
|
+
assert len(rec.started) == total
|
|
65
|
+
assert len(rec.ended) == total
|
|
66
|
+
assert all(rec.ended)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def test_single_file_run_pulls_in_setup(demo_suite: Path) -> None:
|
|
70
|
+
"""Running just the test file must still run SuiteSetUp + use _root connection."""
|
|
71
|
+
suite = parse_test_file(demo_suite / "CountTest.test.md")
|
|
72
|
+
assert suite.name == "CountTest"
|
|
73
|
+
assert suite.setup is not None # SuiteSetUp pulled in
|
|
74
|
+
assert len(suite.pages) == 1 # exactly one test, no siblings
|
|
75
|
+
|
|
76
|
+
engine = build_engine_for_suite(suite)
|
|
77
|
+
try:
|
|
78
|
+
result = run_suite(suite, engine)
|
|
79
|
+
finally:
|
|
80
|
+
engine.dispose()
|
|
81
|
+
assert result.passed_count == 1, result.error
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def test_self_contained_markdown_file(tmp_path: Path) -> None:
|
|
85
|
+
(tmp_path / "connection.properties").write_text("service=:memory:\n")
|
|
86
|
+
f = tmp_path / "Solo.test.md"
|
|
87
|
+
f.write_text(
|
|
88
|
+
"# Solo\n\n<!-- dbression:env=sqlite -->\n"
|
|
89
|
+
"<!-- dbression:connection=connection.properties -->\n\n"
|
|
90
|
+
"### Query\n```sql\nselect 1 as n\n```\n\n| n |\n|---|\n| 1 |\n"
|
|
91
|
+
)
|
|
92
|
+
suite = parse_test_file(f)
|
|
93
|
+
assert suite.setup is None # no SuiteSetUp in dir
|
|
94
|
+
engine = build_engine_for_suite(suite) # config comes from the file's own directives
|
|
95
|
+
try:
|
|
96
|
+
result = run_suite(suite, engine)
|
|
97
|
+
finally:
|
|
98
|
+
engine.dispose()
|
|
99
|
+
assert result.passed_count == 1, result.error
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def test_parse_test_file_rejects_unknown_extension(tmp_path: Path) -> None:
|
|
103
|
+
bad = tmp_path / "notes.txt"
|
|
104
|
+
bad.write_text("hello")
|
|
105
|
+
with pytest.raises(ValueError):
|
|
106
|
+
parse_test_file(bad)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def test_fixture_preview_collapses_and_truncates() -> None:
|
|
110
|
+
t = Table(name="Query", header_args=["select *\n from foo"])
|
|
111
|
+
assert _fixture_preview(t) == "select * from foo"
|
|
112
|
+
long = Table(name="Query", header_args=["x" * 200])
|
|
113
|
+
assert len(_fixture_preview(long)) <= 72
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def test_progress_observer_handles_bracket_identifiers(demo_suite: Path) -> None:
|
|
117
|
+
"""A fixture name/SQL with [brackets] must not break Rich markup parsing."""
|
|
118
|
+
console = Console(force_terminal=True, width=100)
|
|
119
|
+
with make_progress(console) as prog:
|
|
120
|
+
task = prog.add_task("…", total=2)
|
|
121
|
+
obs = ProgressObserver(console, prog, task, details=True)
|
|
122
|
+
tbl = Table(name="Query", header_args=["select * from [dbo].[ORDER]"])
|
|
123
|
+
obs.on_fixture_start("PageA", tbl)
|
|
124
|
+
obs.on_fixture_end("PageA", tbl, FixtureResult(passed=True, message="OK [x]"), 0.01)
|
|
125
|
+
obs.on_fixture_end(
|
|
126
|
+
"PageA", tbl, FixtureResult(passed=False, message="boom [y]"), 0.02
|
|
127
|
+
)
|
|
128
|
+
# No exception == markup-safe.
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|