testgres 1.11.0__tar.gz → 1.12.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.
- testgres-1.12.0/PKG-INFO +232 -0
- testgres-1.12.0/README.md +204 -0
- {testgres-1.11.0 → testgres-1.12.0}/setup.py +5 -3
- {testgres-1.11.0/testgres → testgres-1.12.0/src}/__init__.py +8 -6
- {testgres-1.11.0/testgres → testgres-1.12.0/src}/backup.py +18 -14
- {testgres-1.11.0/testgres → testgres-1.12.0/src}/cache.py +10 -8
- {testgres-1.11.0/testgres → testgres-1.12.0/src}/config.py +4 -3
- testgres-1.12.0/src/exceptions.py +71 -0
- testgres-1.11.0/testgres/port_manager.py → testgres-1.12.0/src/impl/port_manager__generic.py +38 -44
- testgres-1.12.0/src/impl/port_manager__this_host.py +33 -0
- {testgres-1.11.0/testgres → testgres-1.12.0/src}/node.py +286 -233
- testgres-1.12.0/src/node_app.py +317 -0
- testgres-1.12.0/src/port_manager.py +10 -0
- {testgres-1.11.0/testgres → testgres-1.12.0/src}/utils.py +28 -37
- testgres-1.12.0/testgres.egg-info/PKG-INFO +232 -0
- testgres-1.12.0/testgres.egg-info/SOURCES.txt +43 -0
- {testgres-1.11.0 → testgres-1.12.0}/testgres.egg-info/requires.txt +1 -0
- {testgres-1.11.0 → testgres-1.12.0}/tests/conftest.py +308 -142
- {testgres-1.11.0 → testgres-1.12.0}/tests/helpers/global_data.py +9 -9
- {testgres-1.11.0 → testgres-1.12.0}/tests/test_config.py +5 -5
- {testgres-1.11.0 → testgres-1.12.0}/tests/test_os_ops_common.py +327 -2
- {testgres-1.11.0 → testgres-1.12.0}/tests/test_os_ops_remote.py +1 -1
- {testgres-1.11.0 → testgres-1.12.0}/tests/test_testgres_common.py +484 -31
- {testgres-1.11.0 → testgres-1.12.0}/tests/test_testgres_local.py +25 -25
- {testgres-1.11.0 → testgres-1.12.0}/tests/test_testgres_remote.py +7 -8
- {testgres-1.11.0 → testgres-1.12.0}/tests/test_utils.py +3 -3
- testgres-1.11.0/PKG-INFO +0 -236
- testgres-1.11.0/README.md +0 -209
- testgres-1.11.0/testgres/exceptions.py +0 -113
- testgres-1.11.0/testgres/operations/helpers.py +0 -55
- testgres-1.11.0/testgres/operations/local_ops.py +0 -502
- testgres-1.11.0/testgres/operations/os_ops.py +0 -124
- testgres-1.11.0/testgres/operations/raise_error.py +0 -57
- testgres-1.11.0/testgres/operations/remote_ops.py +0 -741
- testgres-1.11.0/testgres/plugins/__init__.py +0 -8
- testgres-1.11.0/testgres/plugins/pg_probackup2/pg_probackup2/__init__.py +0 -0
- testgres-1.11.0/testgres/plugins/pg_probackup2/pg_probackup2/app.py +0 -863
- testgres-1.11.0/testgres/plugins/pg_probackup2/pg_probackup2/gdb.py +0 -349
- testgres-1.11.0/testgres/plugins/pg_probackup2/pg_probackup2/init_helpers.py +0 -226
- testgres-1.11.0/testgres/plugins/pg_probackup2/pg_probackup2/storage/__init__.py +0 -0
- testgres-1.11.0/testgres/plugins/pg_probackup2/pg_probackup2/storage/fs_backup.py +0 -104
- testgres-1.11.0/testgres/plugins/pg_probackup2/pg_probackup2/tests/__init__.py +0 -0
- testgres-1.11.0/testgres/plugins/pg_probackup2/pg_probackup2/tests/test_basic.py +0 -95
- testgres-1.11.0/testgres/plugins/pg_probackup2/setup.py +0 -18
- testgres-1.11.0/testgres.egg-info/PKG-INFO +0 -236
- testgres-1.11.0/testgres.egg-info/SOURCES.txt +0 -57
- testgres-1.11.0/tests/__init__.py +0 -0
- testgres-1.11.0/tests/helpers/__init__.py +0 -0
- {testgres-1.11.0 → testgres-1.12.0}/LICENSE +0 -0
- {testgres-1.11.0 → testgres-1.12.0}/MANIFEST.in +0 -0
- {testgres-1.11.0 → testgres-1.12.0}/setup.cfg +0 -0
- {testgres-1.11.0/testgres → testgres-1.12.0/src}/api.py +0 -0
- {testgres-1.11.0/testgres → testgres-1.12.0/src}/connection.py +0 -0
- {testgres-1.11.0/testgres → testgres-1.12.0/src}/consts.py +0 -0
- {testgres-1.11.0/testgres → testgres-1.12.0/src}/decorators.py +0 -0
- {testgres-1.11.0/testgres → testgres-1.12.0/src}/defaults.py +0 -0
- {testgres-1.11.0/testgres → testgres-1.12.0/src}/enums.py +0 -0
- {testgres-1.11.0/testgres → testgres-1.12.0/src}/logger.py +0 -0
- {testgres-1.11.0/testgres → testgres-1.12.0/src}/pubsub.py +0 -0
- {testgres-1.11.0/testgres → testgres-1.12.0/src}/standby.py +0 -0
- {testgres-1.11.0 → testgres-1.12.0}/testgres.egg-info/dependency_links.txt +0 -0
- {testgres-1.11.0 → testgres-1.12.0}/testgres.egg-info/top_level.txt +0 -0
- {testgres-1.11.0/testgres/operations → testgres-1.12.0/tests}/__init__.py +0 -0
- {testgres-1.11.0/testgres/plugins/pg_probackup2 → testgres-1.12.0/tests/helpers}/__init__.py +0 -0
- {testgres-1.11.0 → testgres-1.12.0}/tests/helpers/run_conditions.py +0 -0
- {testgres-1.11.0 → testgres-1.12.0}/tests/test_os_ops_local.py +0 -0
testgres-1.12.0/PKG-INFO
ADDED
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: testgres
|
|
3
|
+
Version: 1.12.0
|
|
4
|
+
Summary: Testing utility for PostgreSQL and its extensions
|
|
5
|
+
Home-page: https://github.com/postgrespro/testgres
|
|
6
|
+
Author: Postgres Professional
|
|
7
|
+
Author-email: testgres@postgrespro.ru
|
|
8
|
+
License: PostgreSQL
|
|
9
|
+
Keywords: test,testing,postgresql
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Requires-Dist: pg8000
|
|
13
|
+
Requires-Dist: port-for>=0.4
|
|
14
|
+
Requires-Dist: six>=1.9.0
|
|
15
|
+
Requires-Dist: psutil
|
|
16
|
+
Requires-Dist: packaging
|
|
17
|
+
Requires-Dist: testgres.os_ops<1.0.0,>=0.0.2
|
|
18
|
+
Dynamic: author
|
|
19
|
+
Dynamic: author-email
|
|
20
|
+
Dynamic: description
|
|
21
|
+
Dynamic: description-content-type
|
|
22
|
+
Dynamic: home-page
|
|
23
|
+
Dynamic: keywords
|
|
24
|
+
Dynamic: license
|
|
25
|
+
Dynamic: license-file
|
|
26
|
+
Dynamic: requires-dist
|
|
27
|
+
Dynamic: summary
|
|
28
|
+
|
|
29
|
+
[](https://travis-ci.com/github/postgrespro/testgres)
|
|
30
|
+
[](https://codecov.io/gh/postgrespro/testgres)
|
|
31
|
+
[](https://badge.fury.io/py/testgres)
|
|
32
|
+
|
|
33
|
+
[Documentation](https://postgrespro.github.io/testgres/)
|
|
34
|
+
|
|
35
|
+
# testgres
|
|
36
|
+
|
|
37
|
+
Utility for orchestrating temporary PostgreSQL clusters in Python tests. Supports Python 3.7.17 and newer.
|
|
38
|
+
|
|
39
|
+
## Installation
|
|
40
|
+
|
|
41
|
+
Install `testgres` from PyPI:
|
|
42
|
+
|
|
43
|
+
```sh
|
|
44
|
+
pip install testgres
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Use a dedicated virtual environment for isolated test dependencies.
|
|
48
|
+
|
|
49
|
+
## Usage
|
|
50
|
+
|
|
51
|
+
### Environment
|
|
52
|
+
|
|
53
|
+
> Note: by default `testgres` invokes `initdb`, `pg_ctl`, and `psql` binaries found in `PATH`.
|
|
54
|
+
|
|
55
|
+
Specify a custom PostgreSQL installation in one of the following ways:
|
|
56
|
+
|
|
57
|
+
- Set the `PG_CONFIG` environment variable to point to the `pg_config` executable.
|
|
58
|
+
- Set the `PG_BIN` environment variable to point to the directory with PostgreSQL binaries.
|
|
59
|
+
|
|
60
|
+
Example:
|
|
61
|
+
|
|
62
|
+
```sh
|
|
63
|
+
export PG_BIN=$HOME/pg_16/bin
|
|
64
|
+
python my_tests.py
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### Examples
|
|
68
|
+
|
|
69
|
+
Create a temporary node, run queries, and let `testgres` clean up automatically:
|
|
70
|
+
|
|
71
|
+
```python
|
|
72
|
+
# create a node with a random name, port, and data directory
|
|
73
|
+
with testgres.get_new_node() as node:
|
|
74
|
+
|
|
75
|
+
# run initdb
|
|
76
|
+
node.init()
|
|
77
|
+
|
|
78
|
+
# start PostgreSQL
|
|
79
|
+
node.start()
|
|
80
|
+
|
|
81
|
+
# execute a query in the default database
|
|
82
|
+
print(node.execute('select 1'))
|
|
83
|
+
|
|
84
|
+
# the node is stopped and its files are removed automatically
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### Query helpers
|
|
88
|
+
|
|
89
|
+
`testgres` provides four helpers for executing queries against the node:
|
|
90
|
+
|
|
91
|
+
| Command | Description |
|
|
92
|
+
|---------|-------------|
|
|
93
|
+
| `node.psql(query, ...)` | Runs the query via `psql` and returns a tuple `(returncode, stdout, stderr)`. |
|
|
94
|
+
| `node.safe_psql(query, ...)` | Same as `psql()` but returns only `stdout` and raises if the command fails. |
|
|
95
|
+
| `node.execute(query, ...)` | Connects via `psycopg2` or `pg8000` (whichever is available) and returns a list of tuples. |
|
|
96
|
+
| `node.connect(dbname, ...)` | Returns a `NodeConnection` wrapper for executing multiple statements within a transaction. |
|
|
97
|
+
|
|
98
|
+
Example of transactional usage:
|
|
99
|
+
|
|
100
|
+
```python
|
|
101
|
+
with node.connect() as con:
|
|
102
|
+
con.begin('serializable')
|
|
103
|
+
print(con.execute('select %s', 1))
|
|
104
|
+
con.rollback()
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
### Logging
|
|
108
|
+
|
|
109
|
+
By default `cleanup()` removes all temporary files (data directories, logs, and so on) created by the API. Call `configure_testgres(node_cleanup_full=False)` before starting nodes if you want to keep logs for inspection.
|
|
110
|
+
|
|
111
|
+
> Note: context managers (the `with` statement) call `stop()` and `cleanup()` automatically.
|
|
112
|
+
|
|
113
|
+
`testgres` integrates with the standard [Python logging](https://docs.python.org/3/library/logging.html) module, so you can aggregate logs from multiple nodes:
|
|
114
|
+
|
|
115
|
+
```python
|
|
116
|
+
import logging
|
|
117
|
+
|
|
118
|
+
# write everything to /tmp/testgres.log
|
|
119
|
+
logging.basicConfig(filename='/tmp/testgres.log')
|
|
120
|
+
|
|
121
|
+
# enable logging and create two nodes
|
|
122
|
+
testgres.configure_testgres(use_python_logging=True)
|
|
123
|
+
node1 = testgres.get_new_node().init().start()
|
|
124
|
+
node2 = testgres.get_new_node().init().start()
|
|
125
|
+
|
|
126
|
+
node1.execute('select 1')
|
|
127
|
+
node2.execute('select 2')
|
|
128
|
+
|
|
129
|
+
# disable logging
|
|
130
|
+
testgres.configure_testgres(use_python_logging=False)
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
See `tests/test_simple.py` for a complete logging example.
|
|
134
|
+
|
|
135
|
+
### Backup and replication
|
|
136
|
+
|
|
137
|
+
Creating backups and spawning replicas is straightforward:
|
|
138
|
+
|
|
139
|
+
```python
|
|
140
|
+
with testgres.get_new_node('master') as master:
|
|
141
|
+
master.init().start()
|
|
142
|
+
|
|
143
|
+
with master.backup() as backup:
|
|
144
|
+
replica = backup.spawn_replica('replica').start()
|
|
145
|
+
replica.catchup()
|
|
146
|
+
|
|
147
|
+
print(replica.execute('postgres', 'select 1'))
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
### Benchmarks
|
|
151
|
+
|
|
152
|
+
Use `pgbench` through `testgres` to run quick benchmarks:
|
|
153
|
+
|
|
154
|
+
```python
|
|
155
|
+
with testgres.get_new_node('master') as master:
|
|
156
|
+
master.init().start()
|
|
157
|
+
|
|
158
|
+
result = master.pgbench_init(scale=2).pgbench_run(time=10)
|
|
159
|
+
print(result)
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
### Custom configuration
|
|
163
|
+
|
|
164
|
+
`testgres` ships with sensible defaults. Adjust them as needed with `default_conf()` and `append_conf()`:
|
|
165
|
+
|
|
166
|
+
```python
|
|
167
|
+
extra_conf = "shared_preload_libraries = 'postgres_fdw'"
|
|
168
|
+
|
|
169
|
+
with testgres.get_new_node().init() as master:
|
|
170
|
+
master.default_conf(fsync=True, allow_streaming=True)
|
|
171
|
+
master.append_conf('postgresql.conf', extra_conf)
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
`default_conf()` is called by `init()` and rewrites the configuration file. Apply `append_conf()` afterwards to keep custom lines.
|
|
175
|
+
|
|
176
|
+
### Remote mode
|
|
177
|
+
|
|
178
|
+
You can provision nodes on a remote host (Linux only) by wiring `RemoteOperations` into the configuration:
|
|
179
|
+
|
|
180
|
+
```python
|
|
181
|
+
from testgres import ConnectionParams, RemoteOperations, TestgresConfig, get_remote_node
|
|
182
|
+
|
|
183
|
+
conn_params = ConnectionParams(
|
|
184
|
+
host='example.com',
|
|
185
|
+
username='postgres',
|
|
186
|
+
ssh_key='/path/to/ssh/key'
|
|
187
|
+
)
|
|
188
|
+
os_ops = RemoteOperations(conn_params)
|
|
189
|
+
|
|
190
|
+
TestgresConfig.set_os_ops(os_ops=os_ops)
|
|
191
|
+
|
|
192
|
+
def test_basic_query():
|
|
193
|
+
with get_remote_node(conn_params=conn_params) as node:
|
|
194
|
+
node.init().start()
|
|
195
|
+
assert node.execute('SELECT 1') == [(1,)]
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
### Pytest integration
|
|
199
|
+
|
|
200
|
+
Use fixtures to create and clean up nodes automatically when testing with `pytest`:
|
|
201
|
+
|
|
202
|
+
```python
|
|
203
|
+
import pytest
|
|
204
|
+
import testgres
|
|
205
|
+
|
|
206
|
+
@pytest.fixture
|
|
207
|
+
def pg_node():
|
|
208
|
+
node = testgres.get_new_node().init().start()
|
|
209
|
+
try:
|
|
210
|
+
yield node
|
|
211
|
+
finally:
|
|
212
|
+
node.stop()
|
|
213
|
+
node.cleanup()
|
|
214
|
+
|
|
215
|
+
def test_simple(pg_node):
|
|
216
|
+
assert pg_node.execute('select 1')[0][0] == 1
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
This pattern keeps tests concise and ensures that every node is stopped and removed even if the test fails.
|
|
220
|
+
|
|
221
|
+
### Scaling tips
|
|
222
|
+
|
|
223
|
+
- Run tests in parallel with `pytest -n auto` (requires `pytest-xdist`). Ensure each node uses a distinct port by setting `PGPORT` in the fixture or by passing the `port` argument to `get_new_node()`.
|
|
224
|
+
- Always call `node.cleanup()` after each test, or rely on context managers/fixtures that do it for you, to avoid leftover data directories.
|
|
225
|
+
- Prefer `node.safe_psql()` for lightweight assertions that should fail fast; use `node.execute()` when you need structured Python results.
|
|
226
|
+
|
|
227
|
+
## Authors
|
|
228
|
+
|
|
229
|
+
[Ildar Musin](https://github.com/zilder)
|
|
230
|
+
[Dmitry Ivanov](https://github.com/funbringer)
|
|
231
|
+
[Ildus Kurbangaliev](https://github.com/ildus)
|
|
232
|
+
[Yury Zhuravlev](https://github.com/stalkerg)
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
[](https://travis-ci.com/github/postgrespro/testgres)
|
|
2
|
+
[](https://codecov.io/gh/postgrespro/testgres)
|
|
3
|
+
[](https://badge.fury.io/py/testgres)
|
|
4
|
+
|
|
5
|
+
[Documentation](https://postgrespro.github.io/testgres/)
|
|
6
|
+
|
|
7
|
+
# testgres
|
|
8
|
+
|
|
9
|
+
Utility for orchestrating temporary PostgreSQL clusters in Python tests. Supports Python 3.7.17 and newer.
|
|
10
|
+
|
|
11
|
+
## Installation
|
|
12
|
+
|
|
13
|
+
Install `testgres` from PyPI:
|
|
14
|
+
|
|
15
|
+
```sh
|
|
16
|
+
pip install testgres
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Use a dedicated virtual environment for isolated test dependencies.
|
|
20
|
+
|
|
21
|
+
## Usage
|
|
22
|
+
|
|
23
|
+
### Environment
|
|
24
|
+
|
|
25
|
+
> Note: by default `testgres` invokes `initdb`, `pg_ctl`, and `psql` binaries found in `PATH`.
|
|
26
|
+
|
|
27
|
+
Specify a custom PostgreSQL installation in one of the following ways:
|
|
28
|
+
|
|
29
|
+
- Set the `PG_CONFIG` environment variable to point to the `pg_config` executable.
|
|
30
|
+
- Set the `PG_BIN` environment variable to point to the directory with PostgreSQL binaries.
|
|
31
|
+
|
|
32
|
+
Example:
|
|
33
|
+
|
|
34
|
+
```sh
|
|
35
|
+
export PG_BIN=$HOME/pg_16/bin
|
|
36
|
+
python my_tests.py
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### Examples
|
|
40
|
+
|
|
41
|
+
Create a temporary node, run queries, and let `testgres` clean up automatically:
|
|
42
|
+
|
|
43
|
+
```python
|
|
44
|
+
# create a node with a random name, port, and data directory
|
|
45
|
+
with testgres.get_new_node() as node:
|
|
46
|
+
|
|
47
|
+
# run initdb
|
|
48
|
+
node.init()
|
|
49
|
+
|
|
50
|
+
# start PostgreSQL
|
|
51
|
+
node.start()
|
|
52
|
+
|
|
53
|
+
# execute a query in the default database
|
|
54
|
+
print(node.execute('select 1'))
|
|
55
|
+
|
|
56
|
+
# the node is stopped and its files are removed automatically
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### Query helpers
|
|
60
|
+
|
|
61
|
+
`testgres` provides four helpers for executing queries against the node:
|
|
62
|
+
|
|
63
|
+
| Command | Description |
|
|
64
|
+
|---------|-------------|
|
|
65
|
+
| `node.psql(query, ...)` | Runs the query via `psql` and returns a tuple `(returncode, stdout, stderr)`. |
|
|
66
|
+
| `node.safe_psql(query, ...)` | Same as `psql()` but returns only `stdout` and raises if the command fails. |
|
|
67
|
+
| `node.execute(query, ...)` | Connects via `psycopg2` or `pg8000` (whichever is available) and returns a list of tuples. |
|
|
68
|
+
| `node.connect(dbname, ...)` | Returns a `NodeConnection` wrapper for executing multiple statements within a transaction. |
|
|
69
|
+
|
|
70
|
+
Example of transactional usage:
|
|
71
|
+
|
|
72
|
+
```python
|
|
73
|
+
with node.connect() as con:
|
|
74
|
+
con.begin('serializable')
|
|
75
|
+
print(con.execute('select %s', 1))
|
|
76
|
+
con.rollback()
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### Logging
|
|
80
|
+
|
|
81
|
+
By default `cleanup()` removes all temporary files (data directories, logs, and so on) created by the API. Call `configure_testgres(node_cleanup_full=False)` before starting nodes if you want to keep logs for inspection.
|
|
82
|
+
|
|
83
|
+
> Note: context managers (the `with` statement) call `stop()` and `cleanup()` automatically.
|
|
84
|
+
|
|
85
|
+
`testgres` integrates with the standard [Python logging](https://docs.python.org/3/library/logging.html) module, so you can aggregate logs from multiple nodes:
|
|
86
|
+
|
|
87
|
+
```python
|
|
88
|
+
import logging
|
|
89
|
+
|
|
90
|
+
# write everything to /tmp/testgres.log
|
|
91
|
+
logging.basicConfig(filename='/tmp/testgres.log')
|
|
92
|
+
|
|
93
|
+
# enable logging and create two nodes
|
|
94
|
+
testgres.configure_testgres(use_python_logging=True)
|
|
95
|
+
node1 = testgres.get_new_node().init().start()
|
|
96
|
+
node2 = testgres.get_new_node().init().start()
|
|
97
|
+
|
|
98
|
+
node1.execute('select 1')
|
|
99
|
+
node2.execute('select 2')
|
|
100
|
+
|
|
101
|
+
# disable logging
|
|
102
|
+
testgres.configure_testgres(use_python_logging=False)
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
See `tests/test_simple.py` for a complete logging example.
|
|
106
|
+
|
|
107
|
+
### Backup and replication
|
|
108
|
+
|
|
109
|
+
Creating backups and spawning replicas is straightforward:
|
|
110
|
+
|
|
111
|
+
```python
|
|
112
|
+
with testgres.get_new_node('master') as master:
|
|
113
|
+
master.init().start()
|
|
114
|
+
|
|
115
|
+
with master.backup() as backup:
|
|
116
|
+
replica = backup.spawn_replica('replica').start()
|
|
117
|
+
replica.catchup()
|
|
118
|
+
|
|
119
|
+
print(replica.execute('postgres', 'select 1'))
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
### Benchmarks
|
|
123
|
+
|
|
124
|
+
Use `pgbench` through `testgres` to run quick benchmarks:
|
|
125
|
+
|
|
126
|
+
```python
|
|
127
|
+
with testgres.get_new_node('master') as master:
|
|
128
|
+
master.init().start()
|
|
129
|
+
|
|
130
|
+
result = master.pgbench_init(scale=2).pgbench_run(time=10)
|
|
131
|
+
print(result)
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
### Custom configuration
|
|
135
|
+
|
|
136
|
+
`testgres` ships with sensible defaults. Adjust them as needed with `default_conf()` and `append_conf()`:
|
|
137
|
+
|
|
138
|
+
```python
|
|
139
|
+
extra_conf = "shared_preload_libraries = 'postgres_fdw'"
|
|
140
|
+
|
|
141
|
+
with testgres.get_new_node().init() as master:
|
|
142
|
+
master.default_conf(fsync=True, allow_streaming=True)
|
|
143
|
+
master.append_conf('postgresql.conf', extra_conf)
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
`default_conf()` is called by `init()` and rewrites the configuration file. Apply `append_conf()` afterwards to keep custom lines.
|
|
147
|
+
|
|
148
|
+
### Remote mode
|
|
149
|
+
|
|
150
|
+
You can provision nodes on a remote host (Linux only) by wiring `RemoteOperations` into the configuration:
|
|
151
|
+
|
|
152
|
+
```python
|
|
153
|
+
from testgres import ConnectionParams, RemoteOperations, TestgresConfig, get_remote_node
|
|
154
|
+
|
|
155
|
+
conn_params = ConnectionParams(
|
|
156
|
+
host='example.com',
|
|
157
|
+
username='postgres',
|
|
158
|
+
ssh_key='/path/to/ssh/key'
|
|
159
|
+
)
|
|
160
|
+
os_ops = RemoteOperations(conn_params)
|
|
161
|
+
|
|
162
|
+
TestgresConfig.set_os_ops(os_ops=os_ops)
|
|
163
|
+
|
|
164
|
+
def test_basic_query():
|
|
165
|
+
with get_remote_node(conn_params=conn_params) as node:
|
|
166
|
+
node.init().start()
|
|
167
|
+
assert node.execute('SELECT 1') == [(1,)]
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
### Pytest integration
|
|
171
|
+
|
|
172
|
+
Use fixtures to create and clean up nodes automatically when testing with `pytest`:
|
|
173
|
+
|
|
174
|
+
```python
|
|
175
|
+
import pytest
|
|
176
|
+
import testgres
|
|
177
|
+
|
|
178
|
+
@pytest.fixture
|
|
179
|
+
def pg_node():
|
|
180
|
+
node = testgres.get_new_node().init().start()
|
|
181
|
+
try:
|
|
182
|
+
yield node
|
|
183
|
+
finally:
|
|
184
|
+
node.stop()
|
|
185
|
+
node.cleanup()
|
|
186
|
+
|
|
187
|
+
def test_simple(pg_node):
|
|
188
|
+
assert pg_node.execute('select 1')[0][0] == 1
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
This pattern keeps tests concise and ensures that every node is stopped and removed even if the test fails.
|
|
192
|
+
|
|
193
|
+
### Scaling tips
|
|
194
|
+
|
|
195
|
+
- Run tests in parallel with `pytest -n auto` (requires `pytest-xdist`). Ensure each node uses a distinct port by setting `PGPORT` in the fixture or by passing the `port` argument to `get_new_node()`.
|
|
196
|
+
- Always call `node.cleanup()` after each test, or rely on context managers/fixtures that do it for you, to avoid leftover data directories.
|
|
197
|
+
- Prefer `node.safe_psql()` for lightweight assertions that should fail fast; use `node.execute()` when you need structured Python results.
|
|
198
|
+
|
|
199
|
+
## Authors
|
|
200
|
+
|
|
201
|
+
[Ildar Musin](https://github.com/zilder)
|
|
202
|
+
[Dmitry Ivanov](https://github.com/funbringer)
|
|
203
|
+
[Ildus Kurbangaliev](https://github.com/ildus)
|
|
204
|
+
[Yury Zhuravlev](https://github.com/stalkerg)
|
|
@@ -11,7 +11,8 @@ install_requires = [
|
|
|
11
11
|
"port-for>=0.4",
|
|
12
12
|
"six>=1.9.0",
|
|
13
13
|
"psutil",
|
|
14
|
-
"packaging"
|
|
14
|
+
"packaging",
|
|
15
|
+
"testgres.os_ops>=0.0.2,<1.0.0"
|
|
15
16
|
]
|
|
16
17
|
|
|
17
18
|
# Add compatibility enum class
|
|
@@ -27,9 +28,10 @@ with open('README.md', 'r') as f:
|
|
|
27
28
|
readme = f.read()
|
|
28
29
|
|
|
29
30
|
setup(
|
|
30
|
-
version='1.
|
|
31
|
+
version='1.12.0',
|
|
31
32
|
name='testgres',
|
|
32
|
-
packages=['testgres', 'testgres.
|
|
33
|
+
packages=['testgres', 'testgres.impl'],
|
|
34
|
+
package_dir={"testgres": "src"},
|
|
33
35
|
description='Testing utility for PostgreSQL and its extensions',
|
|
34
36
|
url='https://github.com/postgrespro/testgres',
|
|
35
37
|
long_description=readme,
|
|
@@ -33,8 +33,9 @@ from .enums import \
|
|
|
33
33
|
ProcessType, \
|
|
34
34
|
DumpFormat
|
|
35
35
|
|
|
36
|
-
from .node import PostgresNode
|
|
36
|
+
from .node import PostgresNode
|
|
37
37
|
from .node import PortManager
|
|
38
|
+
from .node_app import NodeApp
|
|
38
39
|
|
|
39
40
|
from .utils import \
|
|
40
41
|
reserve_port, \
|
|
@@ -50,9 +51,9 @@ from .standby import \
|
|
|
50
51
|
|
|
51
52
|
from .config import testgres_config
|
|
52
53
|
|
|
53
|
-
from .operations.os_ops import OsOperations, ConnectionParams
|
|
54
|
-
from .operations.local_ops import LocalOperations
|
|
55
|
-
from .operations.remote_ops import RemoteOperations
|
|
54
|
+
from testgres.operations.os_ops import OsOperations, ConnectionParams
|
|
55
|
+
from testgres.operations.local_ops import LocalOperations
|
|
56
|
+
from testgres.operations.remote_ops import RemoteOperations
|
|
56
57
|
|
|
57
58
|
__all__ = [
|
|
58
59
|
"get_new_node",
|
|
@@ -62,8 +63,9 @@ __all__ = [
|
|
|
62
63
|
"NodeConnection", "DatabaseError", "InternalError", "ProgrammingError", "OperationalError",
|
|
63
64
|
"TestgresException", "ExecUtilException", "QueryException", "TimeoutException", "CatchUpException", "StartNodeException", "InitNodeException", "BackupException", "InvalidOperationException",
|
|
64
65
|
"XLogMethod", "IsolationLevel", "NodeStatus", "ProcessType", "DumpFormat",
|
|
65
|
-
|
|
66
|
-
|
|
66
|
+
NodeApp.__name__,
|
|
67
|
+
PostgresNode.__name__,
|
|
68
|
+
PortManager.__name__,
|
|
67
69
|
"reserve_port", "release_port", "bound_ports", "get_bin_path", "get_pg_config", "get_pg_version",
|
|
68
70
|
"First", "Any",
|
|
69
71
|
"OsOperations", "LocalOperations", "RemoteOperations", "ConnectionParams"
|
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
# coding: utf-8
|
|
2
2
|
|
|
3
|
-
import os
|
|
4
|
-
|
|
5
3
|
from six import raise_from
|
|
6
4
|
|
|
7
5
|
from .enums import XLogMethod
|
|
@@ -15,7 +13,7 @@ from .consts import \
|
|
|
15
13
|
|
|
16
14
|
from .exceptions import BackupException
|
|
17
15
|
|
|
18
|
-
from .operations.os_ops import OsOperations
|
|
16
|
+
from testgres.operations.os_ops import OsOperations
|
|
19
17
|
|
|
20
18
|
from .utils import \
|
|
21
19
|
get_bin_path2, \
|
|
@@ -29,7 +27,9 @@ class NodeBackup(object):
|
|
|
29
27
|
"""
|
|
30
28
|
@property
|
|
31
29
|
def log_file(self):
|
|
32
|
-
|
|
30
|
+
assert self.os_ops is not None
|
|
31
|
+
assert isinstance(self.os_ops, OsOperations)
|
|
32
|
+
return self.os_ops.build_path(self.base_dir, BACKUP_LOG_FILE)
|
|
33
33
|
|
|
34
34
|
def __init__(self,
|
|
35
35
|
node,
|
|
@@ -75,7 +75,7 @@ class NodeBackup(object):
|
|
|
75
75
|
# private
|
|
76
76
|
self._available = True
|
|
77
77
|
|
|
78
|
-
data_dir =
|
|
78
|
+
data_dir = self.os_ops.build_path(self.base_dir, DATA_DIR)
|
|
79
79
|
|
|
80
80
|
_params = [
|
|
81
81
|
get_bin_path2(self.os_ops, "pg_basebackup"),
|
|
@@ -112,10 +112,13 @@ class NodeBackup(object):
|
|
|
112
112
|
available = not destroy
|
|
113
113
|
|
|
114
114
|
if available:
|
|
115
|
+
assert self.os_ops is not None
|
|
116
|
+
assert isinstance(self.os_ops, OsOperations)
|
|
117
|
+
|
|
115
118
|
dest_base_dir = self.os_ops.mkdtemp(prefix=TMP_NODE)
|
|
116
119
|
|
|
117
|
-
data1 =
|
|
118
|
-
data2 =
|
|
120
|
+
data1 = self.os_ops.build_path(self.base_dir, DATA_DIR)
|
|
121
|
+
data2 = self.os_ops.build_path(dest_base_dir, DATA_DIR)
|
|
119
122
|
|
|
120
123
|
try:
|
|
121
124
|
# Copy backup to new data dir
|
|
@@ -160,10 +163,6 @@ class NodeBackup(object):
|
|
|
160
163
|
assert type(node) == self.original_node.__class__ # noqa: E721
|
|
161
164
|
|
|
162
165
|
with clean_on_error(node) as node:
|
|
163
|
-
|
|
164
|
-
# New nodes should always remove dir tree
|
|
165
|
-
node._should_rm_dirs = True
|
|
166
|
-
|
|
167
166
|
# Set a new port
|
|
168
167
|
node.append_conf(filename=PG_CONF_FILE, line='\n')
|
|
169
168
|
node.append_conf(filename=PG_CONF_FILE, port=node.port)
|
|
@@ -184,14 +183,19 @@ class NodeBackup(object):
|
|
|
184
183
|
"""
|
|
185
184
|
|
|
186
185
|
# Build a new PostgresNode
|
|
187
|
-
|
|
188
|
-
|
|
186
|
+
node = self.spawn_primary(name=name, destroy=destroy)
|
|
187
|
+
assert node is not None
|
|
189
188
|
|
|
189
|
+
try:
|
|
190
190
|
# Assign it a master and a recovery file (private magic)
|
|
191
191
|
node._assign_master(self.original_node)
|
|
192
192
|
node._create_recovery_conf(username=self.username, slot=slot)
|
|
193
|
+
except: # noqa: E722
|
|
194
|
+
# TODO: Pass 'final=True' ?
|
|
195
|
+
node.cleanup(release_resources=True)
|
|
196
|
+
raise
|
|
193
197
|
|
|
194
|
-
|
|
198
|
+
return node
|
|
195
199
|
|
|
196
200
|
def cleanup(self):
|
|
197
201
|
"""
|
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
# coding: utf-8
|
|
2
2
|
|
|
3
|
-
import os
|
|
4
|
-
|
|
5
3
|
from six import raise_from
|
|
6
4
|
|
|
7
5
|
from .config import testgres_config
|
|
@@ -18,16 +16,20 @@ from .utils import \
|
|
|
18
16
|
get_bin_path2, \
|
|
19
17
|
execute_utility2
|
|
20
18
|
|
|
21
|
-
from .operations.local_ops import LocalOperations
|
|
22
|
-
from .operations.os_ops import OsOperations
|
|
19
|
+
from testgres.operations.local_ops import LocalOperations
|
|
20
|
+
from testgres.operations.os_ops import OsOperations
|
|
23
21
|
|
|
24
22
|
|
|
25
|
-
def cached_initdb(data_dir, logfile=None, params=None, os_ops: OsOperations =
|
|
23
|
+
def cached_initdb(data_dir, logfile=None, params=None, os_ops: OsOperations = None, bin_path=None, cached=True):
|
|
26
24
|
"""
|
|
27
25
|
Perform initdb or use cached node files.
|
|
28
26
|
"""
|
|
29
27
|
|
|
30
|
-
assert os_ops is
|
|
28
|
+
assert os_ops is None or isinstance(os_ops, OsOperations)
|
|
29
|
+
|
|
30
|
+
if os_ops is None:
|
|
31
|
+
os_ops = LocalOperations.get_single_instance()
|
|
32
|
+
|
|
31
33
|
assert isinstance(os_ops, OsOperations)
|
|
32
34
|
|
|
33
35
|
def make_utility_path(name):
|
|
@@ -35,7 +37,7 @@ def cached_initdb(data_dir, logfile=None, params=None, os_ops: OsOperations = Lo
|
|
|
35
37
|
assert type(name) == str # noqa: E721
|
|
36
38
|
|
|
37
39
|
if bin_path:
|
|
38
|
-
return
|
|
40
|
+
return os_ops.build_path(bin_path, name)
|
|
39
41
|
|
|
40
42
|
return get_bin_path2(os_ops, name)
|
|
41
43
|
|
|
@@ -68,7 +70,7 @@ def cached_initdb(data_dir, logfile=None, params=None, os_ops: OsOperations = Lo
|
|
|
68
70
|
# XXX: write new unique system id to control file
|
|
69
71
|
# Some users might rely upon unique system ids, but
|
|
70
72
|
# our initdb caching mechanism breaks this contract.
|
|
71
|
-
pg_control =
|
|
73
|
+
pg_control = os_ops.build_path(data_dir, XLOG_CONTROL_FILE)
|
|
72
74
|
system_id = generate_system_id()
|
|
73
75
|
cur_pg_control = os_ops.read(pg_control, binary=True)
|
|
74
76
|
new_pg_control = system_id + cur_pg_control[len(system_id):]
|
|
@@ -9,8 +9,8 @@ import tempfile
|
|
|
9
9
|
from contextlib import contextmanager
|
|
10
10
|
|
|
11
11
|
from .consts import TMP_CACHE
|
|
12
|
-
from .operations.os_ops import OsOperations
|
|
13
|
-
from .operations.local_ops import LocalOperations
|
|
12
|
+
from testgres.operations.os_ops import OsOperations
|
|
13
|
+
from testgres.operations.local_ops import LocalOperations
|
|
14
14
|
|
|
15
15
|
log_level = os.getenv('LOGGING_LEVEL', 'WARNING').upper()
|
|
16
16
|
log_format = os.getenv('LOGGING_FORMAT', '%(asctime)s - %(levelname)s - %(message)s')
|
|
@@ -50,8 +50,9 @@ class GlobalConfig(object):
|
|
|
50
50
|
_cached_initdb_dir = None
|
|
51
51
|
""" underlying class attribute for cached_initdb_dir property """
|
|
52
52
|
|
|
53
|
-
os_ops = LocalOperations()
|
|
53
|
+
os_ops = LocalOperations.get_single_instance()
|
|
54
54
|
""" OsOperation object that allows work on remote host """
|
|
55
|
+
|
|
55
56
|
@property
|
|
56
57
|
def cached_initdb_dir(self):
|
|
57
58
|
""" path to a temp directory for cached initdb. """
|