pytest-dblift 0.1.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.
- pytest_dblift-0.1.0/PKG-INFO +92 -0
- pytest_dblift-0.1.0/README.md +72 -0
- pytest_dblift-0.1.0/pyproject.toml +34 -0
- pytest_dblift-0.1.0/pytest_dblift/__init__.py +3 -0
- pytest_dblift-0.1.0/pytest_dblift/_client.py +71 -0
- pytest_dblift-0.1.0/pytest_dblift/fixtures.py +84 -0
- pytest_dblift-0.1.0/pytest_dblift/plugin.py +31 -0
- pytest_dblift-0.1.0/pytest_dblift.egg-info/PKG-INFO +92 -0
- pytest_dblift-0.1.0/pytest_dblift.egg-info/SOURCES.txt +16 -0
- pytest_dblift-0.1.0/pytest_dblift.egg-info/dependency_links.txt +1 -0
- pytest_dblift-0.1.0/pytest_dblift.egg-info/entry_points.txt +2 -0
- pytest_dblift-0.1.0/pytest_dblift.egg-info/requires.txt +6 -0
- pytest_dblift-0.1.0/pytest_dblift.egg-info/top_level.txt +1 -0
- pytest_dblift-0.1.0/setup.cfg +4 -0
- pytest_dblift-0.1.0/tests/test_fixtures_sqlite.py +96 -0
- pytest_dblift-0.1.0/tests/test_plugin_options.py +20 -0
- pytest_dblift-0.1.0/tests/test_undo.py +36 -0
- pytest_dblift-0.1.0/tests/test_xdist_isolation.py +35 -0
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pytest-dblift
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: pytest plugin for DBLift migrations
|
|
5
|
+
Author: DBLift
|
|
6
|
+
License: Apache-2.0
|
|
7
|
+
Project-URL: Homepage, https://github.com/dblift/dblift
|
|
8
|
+
Classifier: Framework :: Pytest
|
|
9
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
13
|
+
Requires-Python: >=3.11
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
Requires-Dist: dblift>=3.9
|
|
16
|
+
Requires-Dist: pytest>=7.3
|
|
17
|
+
Requires-Dist: sqlalchemy>=2.0
|
|
18
|
+
Provides-Extra: dev
|
|
19
|
+
Requires-Dist: pytest-xdist>=3.3; extra == "dev"
|
|
20
|
+
|
|
21
|
+
# pytest-dblift
|
|
22
|
+
|
|
23
|
+
pytest plugin for [DBLift](https://github.com/dblift/dblift). It applies your migrations in tests and exposes a `DBLiftClient`.
|
|
24
|
+
|
|
25
|
+
This is a **separate PyPI package**, not `dblift[pytest]`.
|
|
26
|
+
|
|
27
|
+
## Install
|
|
28
|
+
|
|
29
|
+
SQLite (default, stdlib driver):
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
pip install pytest-dblift
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Any other engine: install the matching dblift extra so the native driver is present. The plugin does not install drivers and does not open a second connection — it calls `create_engine(url)` then `DBLiftClient.from_sqlalchemy`.
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
pip install pytest-dblift "dblift[postgresql]"
|
|
39
|
+
pytest --dblift-url "postgresql+psycopg://user:pass@localhost/app_test"
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Quickstart
|
|
43
|
+
|
|
44
|
+
```python
|
|
45
|
+
def test_schema(dblift_migrated_db, dblift_client):
|
|
46
|
+
info = dblift_client.info()
|
|
47
|
+
assert info.pending_migrations == []
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
`dblift_migrated_db` applies pending migrations (function scope). `dblift_client` is a session-scoped `DBLiftClient`. No fixture is autouse: request what you need.
|
|
51
|
+
|
|
52
|
+
## Fixtures
|
|
53
|
+
|
|
54
|
+
| Fixture | Scope | Role |
|
|
55
|
+
| --- | --- | --- |
|
|
56
|
+
| `dblift_config` | session | Dict with `url`, `migrations_dir`, optional `schema`. Override in `conftest.py`. |
|
|
57
|
+
| `dblift_engine` | session | SQLAlchemy engine from that URL. Override to inject your app engine. |
|
|
58
|
+
| `dblift_client` | session | `DBLiftClient.from_sqlalchemy(...)`. |
|
|
59
|
+
| `dblift_migrated_db` | function | `client.migrate()`, then yield the client. |
|
|
60
|
+
| `dblift_empty_db` | function | `client.clean(clean_enabled=True)`, then yield the client. |
|
|
61
|
+
| `dblift_validate` | function | Callable: `dblift_validate(**kwargs)` runs `client.validate` and asserts success. |
|
|
62
|
+
| `dblift_undo` | function | Callable: `dblift_undo(**kwargs)` runs `client.undo` and asserts success. Does not migrate. |
|
|
63
|
+
|
|
64
|
+
Undo uses companion `U*` scripts (same as dblift). There is no `undo()` function inside a `V*.py` file.
|
|
65
|
+
|
|
66
|
+
```python
|
|
67
|
+
def test_rollback(dblift_migrated_db, dblift_undo):
|
|
68
|
+
result = dblift_undo(target_version="0")
|
|
69
|
+
assert result.success
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Override config in `tests/conftest.py`:
|
|
73
|
+
|
|
74
|
+
```python
|
|
75
|
+
import pytest
|
|
76
|
+
|
|
77
|
+
@pytest.fixture(scope="session")
|
|
78
|
+
def dblift_config():
|
|
79
|
+
return {
|
|
80
|
+
"url": "postgresql+psycopg://user:pass@localhost/app_test",
|
|
81
|
+
"migrations_dir": "migrations",
|
|
82
|
+
}
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## CLI options
|
|
86
|
+
|
|
87
|
+
| Option | What it does |
|
|
88
|
+
| --- | --- |
|
|
89
|
+
| `--dblift-url` | Database URL when `dblift_config` is not overridden. Default: a temp SQLite **file**. |
|
|
90
|
+
| `--dblift-migrations-dir` | One migrations directory (not a comma-separated list). Default: `migrations`. |
|
|
91
|
+
|
|
92
|
+
pytest-xdist: only the default SQLite file is per-worker (`test_gw0.db`, …). A URL you pass with `--dblift-url` is used as-is.
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# pytest-dblift
|
|
2
|
+
|
|
3
|
+
pytest plugin for [DBLift](https://github.com/dblift/dblift). It applies your migrations in tests and exposes a `DBLiftClient`.
|
|
4
|
+
|
|
5
|
+
This is a **separate PyPI package**, not `dblift[pytest]`.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
SQLite (default, stdlib driver):
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
pip install pytest-dblift
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Any other engine: install the matching dblift extra so the native driver is present. The plugin does not install drivers and does not open a second connection — it calls `create_engine(url)` then `DBLiftClient.from_sqlalchemy`.
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
pip install pytest-dblift "dblift[postgresql]"
|
|
19
|
+
pytest --dblift-url "postgresql+psycopg://user:pass@localhost/app_test"
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Quickstart
|
|
23
|
+
|
|
24
|
+
```python
|
|
25
|
+
def test_schema(dblift_migrated_db, dblift_client):
|
|
26
|
+
info = dblift_client.info()
|
|
27
|
+
assert info.pending_migrations == []
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
`dblift_migrated_db` applies pending migrations (function scope). `dblift_client` is a session-scoped `DBLiftClient`. No fixture is autouse: request what you need.
|
|
31
|
+
|
|
32
|
+
## Fixtures
|
|
33
|
+
|
|
34
|
+
| Fixture | Scope | Role |
|
|
35
|
+
| --- | --- | --- |
|
|
36
|
+
| `dblift_config` | session | Dict with `url`, `migrations_dir`, optional `schema`. Override in `conftest.py`. |
|
|
37
|
+
| `dblift_engine` | session | SQLAlchemy engine from that URL. Override to inject your app engine. |
|
|
38
|
+
| `dblift_client` | session | `DBLiftClient.from_sqlalchemy(...)`. |
|
|
39
|
+
| `dblift_migrated_db` | function | `client.migrate()`, then yield the client. |
|
|
40
|
+
| `dblift_empty_db` | function | `client.clean(clean_enabled=True)`, then yield the client. |
|
|
41
|
+
| `dblift_validate` | function | Callable: `dblift_validate(**kwargs)` runs `client.validate` and asserts success. |
|
|
42
|
+
| `dblift_undo` | function | Callable: `dblift_undo(**kwargs)` runs `client.undo` and asserts success. Does not migrate. |
|
|
43
|
+
|
|
44
|
+
Undo uses companion `U*` scripts (same as dblift). There is no `undo()` function inside a `V*.py` file.
|
|
45
|
+
|
|
46
|
+
```python
|
|
47
|
+
def test_rollback(dblift_migrated_db, dblift_undo):
|
|
48
|
+
result = dblift_undo(target_version="0")
|
|
49
|
+
assert result.success
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Override config in `tests/conftest.py`:
|
|
53
|
+
|
|
54
|
+
```python
|
|
55
|
+
import pytest
|
|
56
|
+
|
|
57
|
+
@pytest.fixture(scope="session")
|
|
58
|
+
def dblift_config():
|
|
59
|
+
return {
|
|
60
|
+
"url": "postgresql+psycopg://user:pass@localhost/app_test",
|
|
61
|
+
"migrations_dir": "migrations",
|
|
62
|
+
}
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## CLI options
|
|
66
|
+
|
|
67
|
+
| Option | What it does |
|
|
68
|
+
| --- | --- |
|
|
69
|
+
| `--dblift-url` | Database URL when `dblift_config` is not overridden. Default: a temp SQLite **file**. |
|
|
70
|
+
| `--dblift-migrations-dir` | One migrations directory (not a comma-separated list). Default: `migrations`. |
|
|
71
|
+
|
|
72
|
+
pytest-xdist: only the default SQLite file is per-worker (`test_gw0.db`, …). A URL you pass with `--dblift-url` is used as-is.
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=83.0.0", "wheel>=0.46.2"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "pytest-dblift"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "pytest plugin for DBLift migrations"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.11"
|
|
11
|
+
license = { text = "Apache-2.0" }
|
|
12
|
+
authors = [{ name = "DBLift" }]
|
|
13
|
+
dependencies = [
|
|
14
|
+
"dblift>=3.9",
|
|
15
|
+
"pytest>=7.3",
|
|
16
|
+
"sqlalchemy>=2.0",
|
|
17
|
+
]
|
|
18
|
+
classifiers = [
|
|
19
|
+
"Framework :: Pytest",
|
|
20
|
+
"License :: OSI Approved :: Apache Software License",
|
|
21
|
+
"Programming Language :: Python :: 3",
|
|
22
|
+
"Programming Language :: Python :: 3.11",
|
|
23
|
+
"Programming Language :: Python :: 3.12",
|
|
24
|
+
]
|
|
25
|
+
urls = { Homepage = "https://github.com/dblift/dblift" }
|
|
26
|
+
|
|
27
|
+
[project.entry-points.pytest11]
|
|
28
|
+
dblift = "pytest_dblift.plugin"
|
|
29
|
+
|
|
30
|
+
[project.optional-dependencies]
|
|
31
|
+
dev = ["pytest-xdist>=3.3"]
|
|
32
|
+
|
|
33
|
+
[tool.setuptools.packages.find]
|
|
34
|
+
include = ["pytest_dblift*"]
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"""URL resolution and DBLiftClient construction for pytest-dblift fixtures."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
import pytest
|
|
9
|
+
|
|
10
|
+
from api import DBLiftClient
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _worker_id(config: pytest.Config) -> str:
|
|
14
|
+
"""Return xdist worker id ('gw0', ...) or 'master' when not under xdist."""
|
|
15
|
+
workerinput = getattr(config, "workerinput", None)
|
|
16
|
+
if workerinput:
|
|
17
|
+
return workerinput.get("workerid", "master")
|
|
18
|
+
return "master"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def default_sqlite_file_url(
|
|
22
|
+
tmp_path_factory: pytest.TempPathFactory, config: pytest.Config | None = None
|
|
23
|
+
) -> str:
|
|
24
|
+
"""Session-scoped temp SQLite file URL. Under xdist, suffix the filename with the worker id."""
|
|
25
|
+
base = tmp_path_factory.mktemp("dblift_pytest", numbered=True)
|
|
26
|
+
wid = _worker_id(config) if config is not None else "master"
|
|
27
|
+
if wid != "master":
|
|
28
|
+
db_path = base / f"test_{wid}.db"
|
|
29
|
+
else:
|
|
30
|
+
db_path = base / "test.db"
|
|
31
|
+
return f"sqlite:///{db_path}"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def resolve_dblift_config(
|
|
35
|
+
pytestconfig: pytest.Config,
|
|
36
|
+
*,
|
|
37
|
+
tmp_path_factory: pytest.TempPathFactory,
|
|
38
|
+
) -> dict[str, Any]:
|
|
39
|
+
"""Build config dict from CLI options + defaults.
|
|
40
|
+
|
|
41
|
+
Returns dict with 'url' and 'migrations_dir' (absolute path str).
|
|
42
|
+
A relative migrations dir is resolved against pytest rootdir.
|
|
43
|
+
"""
|
|
44
|
+
url = pytestconfig.getoption("--dblift-url")
|
|
45
|
+
if not url:
|
|
46
|
+
url = default_sqlite_file_url(tmp_path_factory, pytestconfig)
|
|
47
|
+
|
|
48
|
+
raw_mig = pytestconfig.getoption("--dblift-migrations-dir") or "migrations"
|
|
49
|
+
rootdir = getattr(pytestconfig, "rootdir", None) or Path.cwd()
|
|
50
|
+
rootdir = Path(rootdir)
|
|
51
|
+
mig_path = Path(raw_mig)
|
|
52
|
+
if not mig_path.is_absolute():
|
|
53
|
+
mig_path = (rootdir / mig_path).resolve()
|
|
54
|
+
|
|
55
|
+
return {
|
|
56
|
+
"url": url,
|
|
57
|
+
"migrations_dir": str(mig_path),
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def create_dblift_client(
|
|
62
|
+
engine: Any,
|
|
63
|
+
*,
|
|
64
|
+
migrations_dir: str | Path | list[str | Path] | None,
|
|
65
|
+
schema: str | None = None,
|
|
66
|
+
) -> DBLiftClient:
|
|
67
|
+
return DBLiftClient.from_sqlalchemy(
|
|
68
|
+
engine,
|
|
69
|
+
migrations_dir=migrations_dir,
|
|
70
|
+
schema=schema,
|
|
71
|
+
)
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"""pytest-dblift fixtures.
|
|
2
|
+
|
|
3
|
+
Session scope for config/engine/client. Function scope for migrate/clean/validate/undo.
|
|
4
|
+
No autouse: tests opt in by requesting a fixture.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from typing import Any, Callable, Iterator
|
|
10
|
+
|
|
11
|
+
import pytest
|
|
12
|
+
from sqlalchemy import create_engine
|
|
13
|
+
|
|
14
|
+
from api import DBLiftClient
|
|
15
|
+
|
|
16
|
+
from ._client import create_dblift_client, resolve_dblift_config
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@pytest.fixture(scope="session")
|
|
20
|
+
def dblift_config(
|
|
21
|
+
pytestconfig: pytest.Config, tmp_path_factory: pytest.TempPathFactory
|
|
22
|
+
) -> dict[str, Any]:
|
|
23
|
+
"""Session config from CLI or temp SQLite. Overridable in consumer conftest.py."""
|
|
24
|
+
return resolve_dblift_config(pytestconfig, tmp_path_factory=tmp_path_factory)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@pytest.fixture(scope="session")
|
|
28
|
+
def dblift_engine(dblift_config: dict[str, Any]) -> Iterator[Any]:
|
|
29
|
+
engine = create_engine(dblift_config["url"])
|
|
30
|
+
yield engine
|
|
31
|
+
engine.dispose()
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@pytest.fixture(scope="session")
|
|
35
|
+
def dblift_client(dblift_engine: Any, dblift_config: dict[str, Any]) -> Iterator[DBLiftClient]:
|
|
36
|
+
client = create_dblift_client(
|
|
37
|
+
dblift_engine,
|
|
38
|
+
migrations_dir=dblift_config.get("migrations_dir"),
|
|
39
|
+
schema=dblift_config.get("schema"),
|
|
40
|
+
)
|
|
41
|
+
yield client
|
|
42
|
+
client.close()
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@pytest.fixture
|
|
46
|
+
def dblift_migrated_db(dblift_client: DBLiftClient) -> Iterator[DBLiftClient]:
|
|
47
|
+
result = dblift_client.migrate()
|
|
48
|
+
assert getattr(result, "success", False), (
|
|
49
|
+
f"migrate failed: {getattr(result, 'error_message', result)}"
|
|
50
|
+
)
|
|
51
|
+
yield dblift_client
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@pytest.fixture
|
|
55
|
+
def dblift_empty_db(dblift_client: DBLiftClient) -> Iterator[DBLiftClient]:
|
|
56
|
+
result = dblift_client.clean(clean_enabled=True)
|
|
57
|
+
assert getattr(result, "success", False), (
|
|
58
|
+
f"clean failed: {getattr(result, 'error_message', result)}"
|
|
59
|
+
)
|
|
60
|
+
yield dblift_client
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@pytest.fixture
|
|
64
|
+
def dblift_validate(dblift_client: DBLiftClient) -> Callable[..., Any]:
|
|
65
|
+
def _run_validate(**kwargs: Any) -> Any:
|
|
66
|
+
result = dblift_client.validate(**kwargs)
|
|
67
|
+
assert getattr(result, "success", False), (
|
|
68
|
+
f"validate failed: {getattr(result, 'error_message', result)}"
|
|
69
|
+
)
|
|
70
|
+
return result
|
|
71
|
+
|
|
72
|
+
return _run_validate
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
@pytest.fixture
|
|
76
|
+
def dblift_undo(dblift_client: DBLiftClient) -> Callable[..., Any]:
|
|
77
|
+
def _run_undo(**kwargs: Any) -> Any:
|
|
78
|
+
result = dblift_client.undo(**kwargs)
|
|
79
|
+
assert getattr(result, "success", False), (
|
|
80
|
+
f"undo failed: {getattr(result, 'error_message', result)}"
|
|
81
|
+
)
|
|
82
|
+
return result
|
|
83
|
+
|
|
84
|
+
return _run_undo
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""pytest11 entry: CLI options and fixture loading."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import pytest
|
|
6
|
+
|
|
7
|
+
pytest_plugins = ["pytest_dblift.fixtures"]
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def pytest_addoption(parser: pytest.Parser) -> None:
|
|
11
|
+
group = parser.getgroup("dblift", "dblift pytest integration")
|
|
12
|
+
group.addoption(
|
|
13
|
+
"--dblift-url",
|
|
14
|
+
action="store",
|
|
15
|
+
default=None,
|
|
16
|
+
help="Database URL for dblift (e.g. sqlite:////tmp/test.db or postgresql+psycopg://...). "
|
|
17
|
+
"Used when no dblift_config fixture override is provided.",
|
|
18
|
+
)
|
|
19
|
+
group.addoption(
|
|
20
|
+
"--dblift-migrations-dir",
|
|
21
|
+
action="store",
|
|
22
|
+
default="migrations",
|
|
23
|
+
help="Path to the migrations directory. Defaults to migrations.",
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def pytest_configure(config: pytest.Config) -> None:
|
|
28
|
+
config.addinivalue_line(
|
|
29
|
+
"markers",
|
|
30
|
+
"dblift: marks tests as using dblift fixtures (provided by pytest-dblift)",
|
|
31
|
+
)
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pytest-dblift
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: pytest plugin for DBLift migrations
|
|
5
|
+
Author: DBLift
|
|
6
|
+
License: Apache-2.0
|
|
7
|
+
Project-URL: Homepage, https://github.com/dblift/dblift
|
|
8
|
+
Classifier: Framework :: Pytest
|
|
9
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
13
|
+
Requires-Python: >=3.11
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
Requires-Dist: dblift>=3.9
|
|
16
|
+
Requires-Dist: pytest>=7.3
|
|
17
|
+
Requires-Dist: sqlalchemy>=2.0
|
|
18
|
+
Provides-Extra: dev
|
|
19
|
+
Requires-Dist: pytest-xdist>=3.3; extra == "dev"
|
|
20
|
+
|
|
21
|
+
# pytest-dblift
|
|
22
|
+
|
|
23
|
+
pytest plugin for [DBLift](https://github.com/dblift/dblift). It applies your migrations in tests and exposes a `DBLiftClient`.
|
|
24
|
+
|
|
25
|
+
This is a **separate PyPI package**, not `dblift[pytest]`.
|
|
26
|
+
|
|
27
|
+
## Install
|
|
28
|
+
|
|
29
|
+
SQLite (default, stdlib driver):
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
pip install pytest-dblift
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Any other engine: install the matching dblift extra so the native driver is present. The plugin does not install drivers and does not open a second connection — it calls `create_engine(url)` then `DBLiftClient.from_sqlalchemy`.
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
pip install pytest-dblift "dblift[postgresql]"
|
|
39
|
+
pytest --dblift-url "postgresql+psycopg://user:pass@localhost/app_test"
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Quickstart
|
|
43
|
+
|
|
44
|
+
```python
|
|
45
|
+
def test_schema(dblift_migrated_db, dblift_client):
|
|
46
|
+
info = dblift_client.info()
|
|
47
|
+
assert info.pending_migrations == []
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
`dblift_migrated_db` applies pending migrations (function scope). `dblift_client` is a session-scoped `DBLiftClient`. No fixture is autouse: request what you need.
|
|
51
|
+
|
|
52
|
+
## Fixtures
|
|
53
|
+
|
|
54
|
+
| Fixture | Scope | Role |
|
|
55
|
+
| --- | --- | --- |
|
|
56
|
+
| `dblift_config` | session | Dict with `url`, `migrations_dir`, optional `schema`. Override in `conftest.py`. |
|
|
57
|
+
| `dblift_engine` | session | SQLAlchemy engine from that URL. Override to inject your app engine. |
|
|
58
|
+
| `dblift_client` | session | `DBLiftClient.from_sqlalchemy(...)`. |
|
|
59
|
+
| `dblift_migrated_db` | function | `client.migrate()`, then yield the client. |
|
|
60
|
+
| `dblift_empty_db` | function | `client.clean(clean_enabled=True)`, then yield the client. |
|
|
61
|
+
| `dblift_validate` | function | Callable: `dblift_validate(**kwargs)` runs `client.validate` and asserts success. |
|
|
62
|
+
| `dblift_undo` | function | Callable: `dblift_undo(**kwargs)` runs `client.undo` and asserts success. Does not migrate. |
|
|
63
|
+
|
|
64
|
+
Undo uses companion `U*` scripts (same as dblift). There is no `undo()` function inside a `V*.py` file.
|
|
65
|
+
|
|
66
|
+
```python
|
|
67
|
+
def test_rollback(dblift_migrated_db, dblift_undo):
|
|
68
|
+
result = dblift_undo(target_version="0")
|
|
69
|
+
assert result.success
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Override config in `tests/conftest.py`:
|
|
73
|
+
|
|
74
|
+
```python
|
|
75
|
+
import pytest
|
|
76
|
+
|
|
77
|
+
@pytest.fixture(scope="session")
|
|
78
|
+
def dblift_config():
|
|
79
|
+
return {
|
|
80
|
+
"url": "postgresql+psycopg://user:pass@localhost/app_test",
|
|
81
|
+
"migrations_dir": "migrations",
|
|
82
|
+
}
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## CLI options
|
|
86
|
+
|
|
87
|
+
| Option | What it does |
|
|
88
|
+
| --- | --- |
|
|
89
|
+
| `--dblift-url` | Database URL when `dblift_config` is not overridden. Default: a temp SQLite **file**. |
|
|
90
|
+
| `--dblift-migrations-dir` | One migrations directory (not a comma-separated list). Default: `migrations`. |
|
|
91
|
+
|
|
92
|
+
pytest-xdist: only the default SQLite file is per-worker (`test_gw0.db`, …). A URL you pass with `--dblift-url` is used as-is.
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
pytest_dblift/__init__.py
|
|
4
|
+
pytest_dblift/_client.py
|
|
5
|
+
pytest_dblift/fixtures.py
|
|
6
|
+
pytest_dblift/plugin.py
|
|
7
|
+
pytest_dblift.egg-info/PKG-INFO
|
|
8
|
+
pytest_dblift.egg-info/SOURCES.txt
|
|
9
|
+
pytest_dblift.egg-info/dependency_links.txt
|
|
10
|
+
pytest_dblift.egg-info/entry_points.txt
|
|
11
|
+
pytest_dblift.egg-info/requires.txt
|
|
12
|
+
pytest_dblift.egg-info/top_level.txt
|
|
13
|
+
tests/test_fixtures_sqlite.py
|
|
14
|
+
tests/test_plugin_options.py
|
|
15
|
+
tests/test_undo.py
|
|
16
|
+
tests/test_xdist_isolation.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
pytest_dblift
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"""SQLite tests for pytest-dblift helpers and fixtures."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
import pytest
|
|
9
|
+
from sqlalchemy import text
|
|
10
|
+
|
|
11
|
+
from api import DBLiftClient
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def test_resolve_dblift_config_reads_cli_url(
|
|
15
|
+
tmp_path_factory: pytest.TempPathFactory,
|
|
16
|
+
) -> None:
|
|
17
|
+
from pytest_dblift._client import resolve_dblift_config
|
|
18
|
+
|
|
19
|
+
class DummyConfig:
|
|
20
|
+
rootdir = "/tmp"
|
|
21
|
+
|
|
22
|
+
def getoption(self, name: str, default: Any = None) -> Any:
|
|
23
|
+
if name == "--dblift-url":
|
|
24
|
+
return "sqlite:////tmp/dblift_custom_test.db"
|
|
25
|
+
if name == "--dblift-migrations-dir":
|
|
26
|
+
return "migrations"
|
|
27
|
+
return default
|
|
28
|
+
|
|
29
|
+
cfg = resolve_dblift_config(DummyConfig(), tmp_path_factory=tmp_path_factory)
|
|
30
|
+
assert "dblift_custom_test.db" in cfg["url"]
|
|
31
|
+
assert "migrations" in cfg["migrations_dir"]
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def test_worker_id_master_without_xdist(pytestconfig: pytest.Config) -> None:
|
|
35
|
+
from pytest_dblift._client import _worker_id
|
|
36
|
+
|
|
37
|
+
if getattr(pytestconfig, "workerinput", None):
|
|
38
|
+
pytest.skip("this assertion is for a non-xdist controller process")
|
|
39
|
+
assert _worker_id(pytestconfig) == "master"
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def test_dblift_config_defaults_to_sqlite_file(dblift_config: dict[str, Any]) -> None:
|
|
43
|
+
assert "sqlite" in dblift_config["url"]
|
|
44
|
+
assert ":memory:" not in dblift_config["url"]
|
|
45
|
+
assert (Path(dblift_config["migrations_dir"]) / "V1__init.sql").is_file()
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def test_dblift_engine_connects(dblift_engine: Any, dblift_config: dict[str, Any]) -> None:
|
|
49
|
+
from sqlalchemy.engine import Engine
|
|
50
|
+
|
|
51
|
+
assert isinstance(dblift_engine, Engine)
|
|
52
|
+
with dblift_engine.connect() as conn:
|
|
53
|
+
conn.execute(text("SELECT 1"))
|
|
54
|
+
rendered = dblift_engine.url.render_as_string(hide_password=False)
|
|
55
|
+
assert rendered.startswith("sqlite")
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def test_dblift_client_is_public_client(dblift_client: DBLiftClient) -> None:
|
|
59
|
+
assert isinstance(dblift_client, DBLiftClient)
|
|
60
|
+
info = dblift_client.info()
|
|
61
|
+
assert hasattr(info, "pending_migrations")
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def test_migrated_db_applies_migrations(
|
|
65
|
+
dblift_migrated_db: DBLiftClient, dblift_engine: Any
|
|
66
|
+
) -> None:
|
|
67
|
+
assert dblift_migrated_db.info().pending_migrations == []
|
|
68
|
+
with dblift_engine.connect() as conn:
|
|
69
|
+
count = conn.execute(text("SELECT COUNT(*) FROM pytest_dblift_smoke")).scalar()
|
|
70
|
+
assert count == 0
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def test_empty_db_cleans_schema(
|
|
74
|
+
dblift_migrated_db: DBLiftClient, dblift_empty_db: DBLiftClient, dblift_engine: Any
|
|
75
|
+
) -> None:
|
|
76
|
+
with dblift_engine.connect() as conn:
|
|
77
|
+
try:
|
|
78
|
+
conn.execute(text("SELECT COUNT(*) FROM pytest_dblift_smoke"))
|
|
79
|
+
exists = True
|
|
80
|
+
except Exception:
|
|
81
|
+
exists = False
|
|
82
|
+
assert not exists
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def test_validate_callable_succeeds(
|
|
86
|
+
dblift_migrated_db: DBLiftClient, dblift_validate: Any
|
|
87
|
+
) -> None:
|
|
88
|
+
dblift_migrated_db.migrate()
|
|
89
|
+
result = dblift_validate()
|
|
90
|
+
assert result.success is True
|
|
91
|
+
result2 = dblift_validate(target_version=None)
|
|
92
|
+
assert result2.success is True
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def test_dblift_validate_is_callable(dblift_validate: Any) -> None:
|
|
96
|
+
assert callable(dblift_validate)
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"""CLI options registered by the pytest-dblift plugin."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import subprocess
|
|
6
|
+
import sys
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def test_help_lists_dblift_url_and_migrations_dir() -> None:
|
|
10
|
+
result = subprocess.run(
|
|
11
|
+
[sys.executable, "-m", "pytest", "--help"],
|
|
12
|
+
check=False,
|
|
13
|
+
capture_output=True,
|
|
14
|
+
text=True,
|
|
15
|
+
)
|
|
16
|
+
assert result.returncode == 0, result.stderr
|
|
17
|
+
help_text = result.stdout
|
|
18
|
+
assert "--dblift-url" in help_text
|
|
19
|
+
assert "--dblift-migrations-dir" in help_text
|
|
20
|
+
assert "--dblift-no-migrate" not in help_text
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"""dblift_undo uses companion U* scripts, not an undo() function inside V*.py."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from sqlalchemy import text
|
|
8
|
+
|
|
9
|
+
from api import DBLiftClient
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def test_dblift_undo_reverts_via_companion_script(
|
|
13
|
+
dblift_migrated_db: DBLiftClient,
|
|
14
|
+
dblift_undo: Any,
|
|
15
|
+
dblift_engine: Any,
|
|
16
|
+
) -> None:
|
|
17
|
+
with dblift_engine.connect() as conn:
|
|
18
|
+
conn.execute(text("INSERT INTO pytest_dblift_smoke (name) VALUES ('before-undo')"))
|
|
19
|
+
conn.commit()
|
|
20
|
+
count = conn.execute(text("SELECT COUNT(*) FROM pytest_dblift_smoke")).scalar()
|
|
21
|
+
assert count == 1
|
|
22
|
+
|
|
23
|
+
result = dblift_undo(target_version="0")
|
|
24
|
+
assert result.success is True
|
|
25
|
+
|
|
26
|
+
with dblift_engine.connect() as conn:
|
|
27
|
+
try:
|
|
28
|
+
conn.execute(text("SELECT COUNT(*) FROM pytest_dblift_smoke"))
|
|
29
|
+
exists = True
|
|
30
|
+
except Exception:
|
|
31
|
+
exists = False
|
|
32
|
+
assert not exists
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def test_dblift_undo_is_callable(dblift_undo: Any) -> None:
|
|
36
|
+
assert callable(dblift_undo)
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""Default SQLite URLs are per xdist worker."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
import pytest
|
|
8
|
+
from sqlalchemy import text
|
|
9
|
+
|
|
10
|
+
from pytest_dblift._client import _worker_id
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def test_xdist_worker_isolation(
|
|
14
|
+
pytestconfig: pytest.Config,
|
|
15
|
+
dblift_config: dict[str, Any],
|
|
16
|
+
dblift_migrated_db: Any,
|
|
17
|
+
dblift_engine: Any,
|
|
18
|
+
) -> None:
|
|
19
|
+
wid = _worker_id(pytestconfig)
|
|
20
|
+
url = dblift_config["url"]
|
|
21
|
+
|
|
22
|
+
if wid != "master":
|
|
23
|
+
assert wid in url, (
|
|
24
|
+
f"worker isolation missing: worker {wid!r} not in default url {url!r}"
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
assert "sqlite" in url and ":memory:" not in url
|
|
28
|
+
tag = f"iso-{wid}"
|
|
29
|
+
with dblift_engine.connect() as conn:
|
|
30
|
+
conn.execute(text("INSERT INTO pytest_dblift_smoke (name) VALUES (:n)"), {"n": tag})
|
|
31
|
+
conn.commit()
|
|
32
|
+
cnt = conn.execute(
|
|
33
|
+
text("SELECT COUNT(*) FROM pytest_dblift_smoke WHERE name = :n"), {"n": tag}
|
|
34
|
+
).scalar()
|
|
35
|
+
assert cnt == 1
|