dbression 0.1.2__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.
Files changed (66) hide show
  1. {dbression-0.1.2 → dbression-0.3.0}/CHANGELOG.md +35 -4
  2. {dbression-0.1.2 → dbression-0.3.0}/PKG-INFO +21 -40
  3. {dbression-0.1.2 → dbression-0.3.0}/README.md +19 -38
  4. dbression-0.3.0/docs/dbression_head.png +0 -0
  5. {dbression-0.1.2 → dbression-0.3.0}/pyproject.toml +2 -1
  6. {dbression-0.1.2 → dbression-0.3.0}/src/dbression/cli.py +72 -11
  7. {dbression-0.1.2 → dbression-0.3.0}/src/dbression/db/engine.py +27 -0
  8. dbression-0.3.0/src/dbression/parser/__init__.py +12 -0
  9. {dbression-0.1.2 → dbression-0.3.0}/src/dbression/parser/wiki.py +63 -0
  10. {dbression-0.1.2 → dbression-0.3.0}/src/dbression/report/__init__.py +9 -1
  11. dbression-0.3.0/src/dbression/report/progress.py +113 -0
  12. {dbression-0.1.2 → dbression-0.3.0}/src/dbression/runner.py +61 -9
  13. dbression-0.3.0/tests/test_progress_and_single_file.py +128 -0
  14. dbression-0.3.0/tests/test_sqlite_engine.py +60 -0
  15. {dbression-0.1.2 → dbression-0.3.0}/uv.lock +1 -1
  16. dbression-0.1.2/src/dbression/parser/__init__.py +0 -4
  17. {dbression-0.1.2 → dbression-0.3.0}/.github/workflows/ci.yml +0 -0
  18. {dbression-0.1.2 → dbression-0.3.0}/.github/workflows/publish.yml +0 -0
  19. {dbression-0.1.2 → dbression-0.3.0}/.gitignore +0 -0
  20. {dbression-0.1.2 → dbression-0.3.0}/.python-version +0 -0
  21. {dbression-0.1.2 → dbression-0.3.0}/LICENSE +0 -0
  22. {dbression-0.1.2 → dbression-0.3.0}/examples/01-hello/HelloSql.test.md +0 -0
  23. {dbression-0.1.2 → dbression-0.3.0}/examples/01-hello/_root.wiki +0 -0
  24. {dbression-0.1.2 → dbression-0.3.0}/examples/01-hello/connection.properties +0 -0
  25. {dbression-0.1.2 → dbression-0.3.0}/examples/02-stored-procedure/BumpCounterTest.test.md +0 -0
  26. {dbression-0.1.2 → dbression-0.3.0}/examples/02-stored-procedure/SuiteSetUp.test.md +0 -0
  27. {dbression-0.1.2 → dbression-0.3.0}/examples/02-stored-procedure/SuiteTearDown.test.md +0 -0
  28. {dbression-0.1.2 → dbression-0.3.0}/examples/02-stored-procedure/_root.wiki +0 -0
  29. {dbression-0.1.2 → dbression-0.3.0}/examples/02-stored-procedure/connection.properties +0 -0
  30. {dbression-0.1.2 → dbression-0.3.0}/examples/03-schema-drift/SchemaSnapshotTest.test.md +0 -0
  31. {dbression-0.1.2 → dbression-0.3.0}/examples/03-schema-drift/SuiteSetUp.test.md +0 -0
  32. {dbression-0.1.2 → dbression-0.3.0}/examples/03-schema-drift/SuiteTearDown.test.md +0 -0
  33. {dbression-0.1.2 → dbression-0.3.0}/examples/03-schema-drift/_root.wiki +0 -0
  34. {dbression-0.1.2 → dbression-0.3.0}/examples/03-schema-drift/connection.properties +0 -0
  35. {dbression-0.1.2 → dbression-0.3.0}/examples/README.md +0 -0
  36. {dbression-0.1.2 → dbression-0.3.0}/src/dbression/__init__.py +0 -0
  37. {dbression-0.1.2 → dbression-0.3.0}/src/dbression/db/__init__.py +0 -0
  38. {dbression-0.1.2 → dbression-0.3.0}/src/dbression/db/connection.py +0 -0
  39. {dbression-0.1.2 → dbression-0.3.0}/src/dbression/db/errors.py +0 -0
  40. {dbression-0.1.2 → dbression-0.3.0}/src/dbression/fixtures/__init__.py +0 -0
  41. {dbression-0.1.2 → dbression-0.3.0}/src/dbression/fixtures/base.py +0 -0
  42. {dbression-0.1.2 → dbression-0.3.0}/src/dbression/fixtures/basic.py +0 -0
  43. {dbression-0.1.2 → dbression-0.3.0}/src/dbression/fixtures/inspect_and_store.py +0 -0
  44. {dbression-0.1.2 → dbression-0.3.0}/src/dbression/fixtures/plugins.py +0 -0
  45. {dbression-0.1.2 → dbression-0.3.0}/src/dbression/fixtures/suite_fixtures.py +0 -0
  46. {dbression-0.1.2 → dbression-0.3.0}/src/dbression/parser/ast.py +0 -0
  47. {dbression-0.1.2 → dbression-0.3.0}/src/dbression/parser/markdown.py +0 -0
  48. {dbression-0.1.2 → dbression-0.3.0}/src/dbression/parser/markdown_writer.py +0 -0
  49. {dbression-0.1.2 → dbression-0.3.0}/src/dbression/parser/tokenizer.py +0 -0
  50. {dbression-0.1.2 → dbression-0.3.0}/src/dbression/report/console.py +0 -0
  51. {dbression-0.1.2 → dbression-0.3.0}/src/dbression/report/json_report.py +0 -0
  52. {dbression-0.1.2 → dbression-0.3.0}/src/dbression/report/junit.py +0 -0
  53. {dbression-0.1.2 → dbression-0.3.0}/src/dbression/symbols.py +0 -0
  54. {dbression-0.1.2 → dbression-0.3.0}/tests/conftest.py +0 -0
  55. {dbression-0.1.2 → dbression-0.3.0}/tests/test_connection.py +0 -0
  56. {dbression-0.1.2 → dbression-0.3.0}/tests/test_markdown_parser.py +0 -0
  57. {dbression-0.1.2 → dbression-0.3.0}/tests/test_markdown_roundtrip.py +0 -0
  58. {dbression-0.1.2 → dbression-0.3.0}/tests/test_parser.py +0 -0
  59. {dbression-0.1.2 → dbression-0.3.0}/tests/test_phase2_fixtures.py +0 -0
  60. {dbression-0.1.2 → dbression-0.3.0}/tests/test_plugins.py +0 -0
  61. {dbression-0.1.2 → dbression-0.3.0}/tests/test_postgres_live.py +0 -0
  62. {dbression-0.1.2 → dbression-0.3.0}/tests/test_report_junit.py +0 -0
  63. {dbression-0.1.2 → dbression-0.3.0}/tests/test_smoke.py +0 -0
  64. {dbression-0.1.2 → dbression-0.3.0}/tests/test_symbols.py +0 -0
  65. {dbression-0.1.2 → dbression-0.3.0}/tests/test_tokenizer.py +0 -0
  66. {dbression-0.1.2 → dbression-0.3.0}/tests/test_update_fixture.py +0 -0
@@ -7,17 +7,46 @@ 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
+
27
+ ## [0.2.0] — 2026-06-01
28
+
29
+ ### Added
30
+
31
+ - **SQLite as a `DatabaseEnvironment`.** Use `DatabaseEnvironment | sqlite` (alias
32
+ `sqlite3`) with `service=:memory:` or `service=<path-to-db-file>` in
33
+ `connection.properties`. No native deps — uses the Python stdlib `sqlite3`
34
+ driver via SQLAlchemy. `Execute Procedure` does not apply (SQLite has no
35
+ stored procedures); everything else (`Query`, `Execute`, `Insert`, `Delete`,
36
+ `Inspect *`, `Update`, captures, substitutions) works the same as on the
37
+ other backends.
38
+
10
39
  ## [0.1.2] — 2026-06-01
11
40
 
12
- SQL Server live-verification release. A real-world MSSQL DBFit suite (WD-Gehsteig
13
- regression tests, stored-procedure-driven, square-bracket identifiers throughout)
41
+ SQL Server live-verification release. A real-world MSSQL DBFit suite (regression tests,
42
+ stored-procedure-driven, square-bracket identifiers throughout)
14
43
  runs end-to-end against an MSSQL Server 2019 via `pymssql` with no changes to the
15
44
  underlying `.wiki` files.
16
45
 
17
46
  ### Fixed
18
47
 
19
48
  - **Console reporter swallowed `[…]` in failure details.** Rich's inline-markup
20
- parser interpreted MSSQL square-bracket identifiers like `[wdg].PLAENE` as
49
+ parser interpreted MSSQL square-bracket identifiers like `[dbo].ORDER` as
21
50
  style tags and stripped them from the displayed SQL — making failure output
22
51
  misleading. Reporter now prints fixture details with `markup=False`, so the
23
52
  SQL you see is the SQL that ran.
@@ -120,7 +149,9 @@ with code paths in place for SQL Server and Oracle.
120
149
  reject `python-oracledb` thin-mode authentication. Configuration is via
121
150
  `DBRESSION_ORACLE_CLIENT_LIB_DIR`.
122
151
 
123
- [Unreleased]: https://github.com/angrydat/dbression/compare/v0.1.2...HEAD
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
154
+ [0.2.0]: https://github.com/angrydat/dbression/releases/tag/v0.2.0
124
155
  [0.1.2]: https://github.com/angrydat/dbression/releases/tag/v0.1.2
125
156
  [0.1.1]: https://github.com/angrydat/dbression/releases/tag/v0.1.1
126
157
  [0.1.0]: https://github.com/angrydat/dbression/releases/tag/v0.1.0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dbression
3
- Version: 0.1.2
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
@@ -9,7 +9,7 @@ Project-URL: Changelog, https://github.com/angrydat/dbression/blob/main/CHANGELO
9
9
  Author-email: Jürgen Zornig <info@angrydata.info>
10
10
  License: MIT
11
11
  License-File: LICENSE
12
- Keywords: database,dbfit,fitnesse,markdown,oracle,postgres,regression,sql,sqlserver,testing
12
+ Keywords: database,dbfit,fitnesse,markdown,oracle,postgres,regression,sql,sqlite,sqlserver,testing
13
13
  Classifier: Development Status :: 4 - Beta
14
14
  Classifier: Environment :: Console
15
15
  Classifier: Intended Audience :: Developers
@@ -35,6 +35,10 @@ Requires-Dist: sqlalchemy>=2.0
35
35
  Requires-Dist: typer>=0.12
36
36
  Description-Content-Type: text/markdown
37
37
 
38
+ <p align="center">
39
+ <img src="https://raw.githubusercontent.com/angrydat/dbression/main/docs/dbression_head.png" alt="dbression — database regression testing for schema changes, migrations, and critical queries" width="820">
40
+ </p>
41
+
38
42
  # dbression
39
43
 
40
44
  > ### Rage-quit your flaky DB regressions.
@@ -48,19 +52,19 @@ anywhere. `dbression` is a Python re-implementation in the spirit of the fantast
48
52
 
49
53
  ```text
50
54
  $ dbression run tests/
51
- dbression 0.1.2 — Suite: tests @ postgresql+psycopg://wlk:***@db01/wlk
55
+ dbression 0.3.0 — Suite: tests @ postgresql+psycopg://foo:***@db01/bar
52
56
 
53
57
  ✓ HelloSql 0.004s
54
58
  CommonSuite/
55
- MerklisteSuite/
59
+ ChangelistSuite/
56
60
  ✓ AAddBasicTest 0.027s
57
61
  ✓ BAddNormalizationTest 0.029s
58
62
  ✓ CAddWhitelistTest 0.025s
59
63
  ✓ DAddInvalidArgsTest 0.014s
60
64
  ✓ ERemTest 0.052s
61
- FViewWbTest 1.236s
62
- EreignisSuite/
63
- WaldbrandSuite/
65
+ FViewTest 1.236s
66
+ EventsSuite/
67
+ FireSuite/
64
68
  ✓ LookupTest 0.157s
65
69
  ✗ SchemaTest 0.013s
66
70
 
@@ -92,7 +96,8 @@ the model gets every detail it needs to suggest a fix, no screenshots, no contex
92
96
 
93
97
  ```bash
94
98
  # install (we use uv, but pip works too)
95
- uv tool install git+https://github.com/angrydat/dbression.git
99
+ uv tool install dbression # or: pipx install dbression
100
+ # or: pip install dbression
96
101
 
97
102
  # run
98
103
  dbression run tests/
@@ -272,7 +277,10 @@ dbression:
272
277
  ## CLI cheat sheet
273
278
 
274
279
  ```bash
275
- 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
276
284
  dbression run <path> -v # show every fixture table, not just the page line
277
285
  dbression run <path> --tag critical # only run pages tagged `critical`
278
286
  dbression run <path> --skip-tag NotOnCI # skip pages tagged `NotOnCI`
@@ -284,37 +292,9 @@ dbression convert file.wiki -o out.test.md # single file with explicit output
284
292
  dbression version
285
293
  ```
286
294
 
287
- ## How it works
288
-
289
- ```
290
- ┌─────────────────────────────────────────────────────────┐
291
- │ CLI (typer) dbression run tests/ [--flags] │
292
- └─────────────────────────────────────────────────────────┘
293
-
294
-
295
- ┌─────────────────────┐ ┌───────────────────────────────┐
296
- │ Parser │ │ Engine Factory (SQLAlchemy) │
297
- │ Wiki → AST │ │ ┌──────────┐ ┌──────────┐ │
298
- │ • !- -! escapes │ │ │ psycopg │ │ oracledb │ │
299
- │ • q'~ ~' quoting │ │ │ (Postgres│ │ (thin) │ │
300
- │ • Nested suites │ │ └──────────┘ └──────────┘ │
301
- │ • YAML front matter│ │ ┌──────────────────────┐ │
302
- └─────────┬───────────┘ │ │ pymssql (SQL Server) │ │
303
- │ │ └──────────────────────┘ │
304
- ▼ └───────────┬───────────────────┘
305
- ┌─────────────────────────────────────┴───────────────────┐
306
- │ Runner — one TX per suite, savepoints per test │
307
- │ Symbol engine: >>capture, <<read, :bind, _:text-subst │
308
- │ Fixture registry — pluggable via decorator │
309
- └─────────────────────┬───────────────────────────────────┘
310
-
311
- ┌─────────────────────────────────────────────────────────┐
312
- │ Reporters │
313
- │ • Rich console (default) │
314
- │ • JUnit XML (--junit-xml) │
315
- │ • JSON (--json) │
316
- └─────────────────────────────────────────────────────────┘
317
- ```
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`.
318
298
 
319
299
  ## Status
320
300
 
@@ -328,6 +308,7 @@ Postgres, Oracle and SQL Server. Honest state of the parts:
328
308
  | PostgreSQL | ✅ verified against a real-world suite |
329
309
  | Oracle | ✅ verified against a real-world 19c suite via `oracledb` (thin) |
330
310
  | SQL Server | ✅ verified against a real-world suite via `pymssql` |
311
+ | SQLite | ✅ via stdlib `sqlite3` — no procedures (the DB has none) |
331
312
  | JUnit XML + JSON output | ✅ |
332
313
  | `.test.md` native Markdown format + `dbression convert` | ✅ |
333
314
  | Plugin entry-points for custom fixtures | ✅ |
@@ -1,3 +1,7 @@
1
+ <p align="center">
2
+ <img src="https://raw.githubusercontent.com/angrydat/dbression/main/docs/dbression_head.png" alt="dbression — database regression testing for schema changes, migrations, and critical queries" width="820">
3
+ </p>
4
+
1
5
  # dbression
2
6
 
3
7
  > ### Rage-quit your flaky DB regressions.
@@ -11,19 +15,19 @@ anywhere. `dbression` is a Python re-implementation in the spirit of the fantast
11
15
 
12
16
  ```text
13
17
  $ dbression run tests/
14
- dbression 0.1.2 — Suite: tests @ postgresql+psycopg://wlk:***@db01/wlk
18
+ dbression 0.3.0 — Suite: tests @ postgresql+psycopg://foo:***@db01/bar
15
19
 
16
20
  ✓ HelloSql 0.004s
17
21
  CommonSuite/
18
- MerklisteSuite/
22
+ ChangelistSuite/
19
23
  ✓ AAddBasicTest 0.027s
20
24
  ✓ BAddNormalizationTest 0.029s
21
25
  ✓ CAddWhitelistTest 0.025s
22
26
  ✓ DAddInvalidArgsTest 0.014s
23
27
  ✓ ERemTest 0.052s
24
- FViewWbTest 1.236s
25
- EreignisSuite/
26
- WaldbrandSuite/
28
+ FViewTest 1.236s
29
+ EventsSuite/
30
+ FireSuite/
27
31
  ✓ LookupTest 0.157s
28
32
  ✗ SchemaTest 0.013s
29
33
 
@@ -55,7 +59,8 @@ the model gets every detail it needs to suggest a fix, no screenshots, no contex
55
59
 
56
60
  ```bash
57
61
  # install (we use uv, but pip works too)
58
- uv tool install git+https://github.com/angrydat/dbression.git
62
+ uv tool install dbression # or: pipx install dbression
63
+ # or: pip install dbression
59
64
 
60
65
  # run
61
66
  dbression run tests/
@@ -235,7 +240,10 @@ dbression:
235
240
  ## CLI cheat sheet
236
241
 
237
242
  ```bash
238
- 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
239
247
  dbression run <path> -v # show every fixture table, not just the page line
240
248
  dbression run <path> --tag critical # only run pages tagged `critical`
241
249
  dbression run <path> --skip-tag NotOnCI # skip pages tagged `NotOnCI`
@@ -247,37 +255,9 @@ dbression convert file.wiki -o out.test.md # single file with explicit output
247
255
  dbression version
248
256
  ```
249
257
 
250
- ## How it works
251
-
252
- ```
253
- ┌─────────────────────────────────────────────────────────┐
254
- │ CLI (typer) dbression run tests/ [--flags] │
255
- └─────────────────────────────────────────────────────────┘
256
-
257
-
258
- ┌─────────────────────┐ ┌───────────────────────────────┐
259
- │ Parser │ │ Engine Factory (SQLAlchemy) │
260
- │ Wiki → AST │ │ ┌──────────┐ ┌──────────┐ │
261
- │ • !- -! escapes │ │ │ psycopg │ │ oracledb │ │
262
- │ • q'~ ~' quoting │ │ │ (Postgres│ │ (thin) │ │
263
- │ • Nested suites │ │ └──────────┘ └──────────┘ │
264
- │ • YAML front matter│ │ ┌──────────────────────┐ │
265
- └─────────┬───────────┘ │ │ pymssql (SQL Server) │ │
266
- │ │ └──────────────────────┘ │
267
- ▼ └───────────┬───────────────────┘
268
- ┌─────────────────────────────────────┴───────────────────┐
269
- │ Runner — one TX per suite, savepoints per test │
270
- │ Symbol engine: >>capture, <<read, :bind, _:text-subst │
271
- │ Fixture registry — pluggable via decorator │
272
- └─────────────────────┬───────────────────────────────────┘
273
-
274
- ┌─────────────────────────────────────────────────────────┐
275
- │ Reporters │
276
- │ • Rich console (default) │
277
- │ • JUnit XML (--junit-xml) │
278
- │ • JSON (--json) │
279
- └─────────────────────────────────────────────────────────┘
280
- ```
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`.
281
261
 
282
262
  ## Status
283
263
 
@@ -291,6 +271,7 @@ Postgres, Oracle and SQL Server. Honest state of the parts:
291
271
  | PostgreSQL | ✅ verified against a real-world suite |
292
272
  | Oracle | ✅ verified against a real-world 19c suite via `oracledb` (thin) |
293
273
  | SQL Server | ✅ verified against a real-world suite via `pymssql` |
274
+ | SQLite | ✅ via stdlib `sqlite3` — no procedures (the DB has none) |
294
275
  | JUnit XML + JSON output | ✅ |
295
276
  | `.test.md` native Markdown format + `dbression convert` | ✅ |
296
277
  | Plugin entry-points for custom fixtures | ✅ |
Binary file
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "dbression"
3
- version = "0.1.2"
3
+ version = "0.3.0"
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" }
@@ -16,6 +16,7 @@ keywords = [
16
16
  "postgres",
17
17
  "oracle",
18
18
  "sqlserver",
19
+ "sqlite",
19
20
  "sql",
20
21
  "markdown",
21
22
  "fitnesse",
@@ -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 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
+ 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[Path, typer.Argument(help="Path to the suite (directory containing _root.wiki)")],
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 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)
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
- suite = parse_suite(path)
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__} — Suite: [bold]{suite.name}[/bold] @ "
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
- result = run_suite(suite, engine, commit_mode=commit_mode, tag_filter=tag_filter) # type: ignore[arg-type]
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)
@@ -41,6 +41,7 @@ def _maybe_init_oracle_thick() -> None:
41
41
  _POSTGRES_ALIASES = {"postgres", "postgresql", "pg"}
42
42
  _ORACLE_ALIASES = {"oracle"}
43
43
  _MSSQL_ALIASES = {"sqlserver", "mssql", "ms-sql"}
44
+ _SQLITE_ALIASES = {"sqlite", "sqlite3"}
44
45
 
45
46
 
46
47
  def make_engine(environment: str, config: ConnectionConfig) -> Engine:
@@ -55,6 +56,8 @@ def make_engine(environment: str, config: ConnectionConfig) -> Engine:
55
56
  url = _build_oracle_url(config, extra)
56
57
  elif env in _MSSQL_ALIASES:
57
58
  url = _build_mssql_url(config, extra)
59
+ elif env in _SQLITE_ALIASES:
60
+ url = _build_sqlite_url(config, extra)
58
61
  else:
59
62
  raise ValueError(f"Unknown DatabaseEnvironment: {environment!r}")
60
63
 
@@ -116,6 +119,30 @@ def _build_mssql_url(cfg: ConnectionConfig, extra: dict[str, str]) -> URL | str:
116
119
  )
117
120
 
118
121
 
122
+ def _build_sqlite_url(cfg: ConnectionConfig, extra: dict[str, str]) -> URL | str:
123
+ """SQLite via the Python stdlib ``sqlite3`` driver. No native deps.
124
+
125
+ Path source order:
126
+
127
+ * ``connection-string=sqlite:///…`` — taken verbatim
128
+ * ``service=:memory:`` — in-memory DB
129
+ * ``service=<file-path>`` — file-backed DB (absolute or relative)
130
+ * ``database=<file-path>`` (extras) — fallback alias
131
+
132
+ SQLite has no auth — ``username`` / ``password`` in connection.properties are
133
+ ignored. Stored procedures and `Execute Procedure` won't work (SQLite has none).
134
+ """
135
+ if cfg.connection_string:
136
+ return cfg.connection_string
137
+ path = cfg.service or extra.get("database") or ":memory:"
138
+ # SQLAlchemy SQLite URLs:
139
+ # sqlite:///:memory: — in-memory
140
+ # sqlite:///relative.db — relative to cwd (3 slashes)
141
+ # sqlite:////abs/path.db — absolute (4 slashes; the leading `/` of the path
142
+ # concatenates with the 3-slash prefix)
143
+ return f"sqlite:///{path}"
144
+
145
+
119
146
  def _split_host_port(service: str | None) -> tuple[str | None, int | None]:
120
147
  if not service:
121
148
  return None, None
@@ -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__ = ["print_suite_result", "write_json_report", "write_junit_xml"]
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(page, conn, symbols, stored, isolate=isolate)
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(sub, sub_engine, commit_mode, symbols, tag_filter, stored)
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
- pr.tables.append(
298
- TableResult(name=table.name, result=res, duration=time.perf_counter() - start)
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.
@@ -0,0 +1,60 @@
1
+ """SQLite engine smoke test — confirm the DatabaseEnvironment dispatch and end-to-end
2
+ Query / Execute against an in-memory SQLite DB."""
3
+ from __future__ import annotations
4
+
5
+ from sqlalchemy import text
6
+
7
+ from dbression.db.connection import ConnectionConfig
8
+ from dbression.db.engine import make_engine
9
+ from dbression.fixtures import resolve_fixture
10
+ from dbression.fixtures.base import FixtureContext
11
+ from dbression.parser.ast import Table
12
+ from dbression.symbols import SymbolTable
13
+
14
+
15
+ def _ctx(conn):
16
+ return FixtureContext(conn=conn, symbols=SymbolTable(), stored={})
17
+
18
+
19
+ def test_sqlite_memory_via_database_environment():
20
+ cfg = ConnectionConfig(service=":memory:")
21
+ eng = make_engine("sqlite", cfg)
22
+ with eng.connect() as c:
23
+ assert c.execute(text("select 1")).scalar() == 1
24
+
25
+
26
+ def test_sqlite_file_path_url_shape(tmp_path):
27
+ db = tmp_path / "x.db"
28
+ cfg = ConnectionConfig(service=str(db))
29
+ eng = make_engine("sqlite", cfg)
30
+ # Absolute path → 4 slashes
31
+ assert str(eng.url).startswith("sqlite:////")
32
+ with eng.connect() as c:
33
+ c.execute(text("create table t (a int)"))
34
+ c.execute(text("insert into t values (42)"))
35
+ assert c.execute(text("select a from t")).scalar() == 42
36
+
37
+
38
+ def test_sqlite_runs_query_fixture():
39
+ cfg = ConnectionConfig(service=":memory:")
40
+ eng = make_engine("sqlite", cfg)
41
+ with eng.begin() as conn:
42
+ conn.execute(text("create table t (n int, name text)"))
43
+ conn.execute(text("insert into t values (1, 'a'), (2, 'b')"))
44
+ with eng.connect() as conn:
45
+ cls = resolve_fixture("Query")
46
+ tbl = Table(
47
+ name="Query",
48
+ header_args=["select n, name from t order by n"],
49
+ headers=["n", "name"],
50
+ rows=[["1", "a"], ["2", "b"]],
51
+ )
52
+ res = cls().run(tbl, _ctx(conn))
53
+ assert res.passed, res.message
54
+
55
+
56
+ def test_sqlite_alias_sqlite3_also_works():
57
+ cfg = ConnectionConfig(service=":memory:")
58
+ eng = make_engine("sqlite3", cfg)
59
+ with eng.connect() as c:
60
+ assert c.execute(text("select 1")).scalar() == 1
@@ -132,7 +132,7 @@ wheels = [
132
132
 
133
133
  [[package]]
134
134
  name = "dbression"
135
- version = "0.1.2"
135
+ version = "0.3.0"
136
136
  source = { editable = "." }
137
137
  dependencies = [
138
138
  { name = "markdown-it-py" },
@@ -1,4 +0,0 @@
1
- from dbression.parser.ast import Directive, Page, Suite, Table
2
- from dbression.parser.wiki import parse_suite, parse_wiki
3
-
4
- __all__ = ["Directive", "Page", "Suite", "Table", "parse_suite", "parse_wiki"]
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes