Skip to content

Commit 5d3ec71

Browse files
Merge branch 'master' into D20250816_001--external_os_ops_v001
2 parents de73028 + 0ca0f88 commit 5d3ec71

File tree

2 files changed

+212
-92
lines changed

2 files changed

+212
-92
lines changed

README.md

Lines changed: 76 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -6,204 +6,199 @@
66

77
# testgres
88

9-
PostgreSQL testing utility. Python 3.7.17+ is supported.
10-
9+
Utility for orchestrating temporary PostgreSQL clusters in Python tests. Supports Python 3.7.17 and newer.
1110

1211
## Installation
1312

14-
To install `testgres`, run:
13+
Install `testgres` from PyPI:
1514

16-
```
15+
```sh
1716
pip install testgres
1817
```
1918

20-
We encourage you to use `virtualenv` for your testing environment.
21-
19+
Use a dedicated virtual environment for isolated test dependencies.
2220

2321
## Usage
2422

2523
### Environment
2624

27-
> Note: by default testgres runs `initdb`, `pg_ctl`, `psql` provided by `PATH`.
25+
> Note: by default `testgres` invokes `initdb`, `pg_ctl`, and `psql` binaries found in `PATH`.
2826
29-
There are several ways to specify a custom postgres installation:
27+
Specify a custom PostgreSQL installation in one of the following ways:
3028

31-
* export `PG_CONFIG` environment variable pointing to the `pg_config` executable;
32-
* export `PG_BIN` environment variable pointing to the directory with executable files.
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.
3331

3432
Example:
3533

36-
```bash
37-
export PG_BIN=$HOME/pg_10/bin
34+
```sh
35+
export PG_BIN=$HOME/pg_16/bin
3836
python my_tests.py
3937
```
4038

41-
4239
### Examples
4340

44-
Here is an example of what you can do with `testgres`:
41+
Create a temporary node, run queries, and let `testgres` clean up automatically:
4542

4643
```python
47-
# create a node with random name, port, etc
44+
# create a node with a random name, port, and data directory
4845
with testgres.get_new_node() as node:
4946

50-
# run inidb
47+
# run initdb
5148
node.init()
5249

5350
# start PostgreSQL
5451
node.start()
5552

56-
# execute a query in a default DB
53+
# execute a query in the default database
5754
print(node.execute('select 1'))
5855

59-
# ... node stops and its files are about to be removed
56+
# the node is stopped and its files are removed automatically
6057
```
6158

62-
There are four API methods for running queries:
59+
### Query helpers
60+
61+
`testgres` provides four helpers for executing queries against the node:
6362

6463
| Command | Description |
65-
|----------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------|
66-
| `node.psql(query, ...)` | Runs query via `psql` command and returns tuple `(error code, stdout, stderr)`. |
67-
| `node.safe_psql(query, ...)` | Same as `psql()` except that it returns only `stdout`. If an error occurs during the execution, an exception will be thrown. |
68-
| `node.execute(query, ...)` | Connects to PostgreSQL using `psycopg2` or `pg8000` (depends on which one is installed in your system) and returns two-dimensional array with data. |
69-
| `node.connect(dbname, ...)` | Returns connection wrapper (`NodeConnection`) capable of running several queries within a single transaction. |
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:
7071

71-
The last one is the most powerful: you can use `begin(isolation_level)`, `commit()` and `rollback()`:
7272
```python
7373
with node.connect() as con:
7474
con.begin('serializable')
7575
print(con.execute('select %s', 1))
7676
con.rollback()
7777
```
7878

79-
8079
### Logging
8180

82-
By default, `cleanup()` removes all temporary files (DB files, logs etc) that were created by testgres' API methods.
83-
If you'd like to keep logs, execute `configure_testgres(node_cleanup_full=False)` before running any tests.
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.
8482

85-
> Note: context managers (aka `with`) call `stop()` and `cleanup()` automatically.
83+
> Note: context managers (the `with` statement) call `stop()` and `cleanup()` automatically.
8684
87-
`testgres` supports [python logging](https://docs.python.org/3.6/library/logging.html),
88-
which means that you can aggregate logs from several nodes into one file:
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:
8986

9087
```python
9188
import logging
9289

9390
# write everything to /tmp/testgres.log
9491
logging.basicConfig(filename='/tmp/testgres.log')
9592

96-
# enable logging, and create two different nodes
93+
# enable logging and create two nodes
9794
testgres.configure_testgres(use_python_logging=True)
9895
node1 = testgres.get_new_node().init().start()
9996
node2 = testgres.get_new_node().init().start()
10097

101-
# execute a few queries
10298
node1.execute('select 1')
10399
node2.execute('select 2')
104100

105101
# disable logging
106102
testgres.configure_testgres(use_python_logging=False)
107103
```
108104

109-
Look at `tests/test_simple.py` file for a complete example of the logging
110-
configuration.
111-
105+
See `tests/test_simple.py` for a complete logging example.
112106

113-
### Backup & replication
107+
### Backup and replication
114108

115-
It's quite easy to create a backup and start a new replica:
109+
Creating backups and spawning replicas is straightforward:
116110

117111
```python
118112
with testgres.get_new_node('master') as master:
119113
master.init().start()
120114

121-
# create a backup
122115
with master.backup() as backup:
123-
124-
# create and start a new replica
125116
replica = backup.spawn_replica('replica').start()
126-
127-
# catch up with master node
128117
replica.catchup()
129118

130-
# execute a dummy query
131119
print(replica.execute('postgres', 'select 1'))
132120
```
133121

134122
### Benchmarks
135123

136-
`testgres` is also capable of running benchmarks using `pgbench`:
124+
Use `pgbench` through `testgres` to run quick benchmarks:
137125

138126
```python
139127
with testgres.get_new_node('master') as master:
140-
# start a new node
141128
master.init().start()
142129

143-
# initialize default DB and run bench for 10 seconds
144-
res = master.pgbench_init(scale=2).pgbench_run(time=10)
145-
print(res)
130+
result = master.pgbench_init(scale=2).pgbench_run(time=10)
131+
print(result)
146132
```
147133

148-
149134
### Custom configuration
150135

151-
It's often useful to extend default configuration provided by `testgres`.
152-
153-
`testgres` has `default_conf()` function that helps control some basic
154-
options. The `append_conf()` function can be used to add custom
155-
lines to configuration lines:
136+
`testgres` ships with sensible defaults. Adjust them as needed with `default_conf()` and `append_conf()`:
156137

157138
```python
158-
ext_conf = "shared_preload_libraries = 'postgres_fdw'"
139+
extra_conf = "shared_preload_libraries = 'postgres_fdw'"
159140

160-
# initialize a new node
161141
with testgres.get_new_node().init() as master:
162-
163-
# ... do something ...
164-
165-
# reset main config file
166-
master.default_conf(fsync=True,
167-
allow_streaming=True)
168-
169-
# add a new config line
170-
master.append_conf('postgresql.conf', ext_conf)
142+
master.default_conf(fsync=True, allow_streaming=True)
143+
master.append_conf('postgresql.conf', extra_conf)
171144
```
172145

173-
Note that `default_conf()` is called by `init()` function; both of them overwrite
174-
the configuration file, which means that they should be called before `append_conf()`.
146+
`default_conf()` is called by `init()` and rewrites the configuration file. Apply `append_conf()` afterwards to keep custom lines.
175147

176148
### Remote mode
177-
Testgres supports the creation of PostgreSQL nodes on a remote host. This is useful when you want to run distributed tests involving multiple nodes spread across different machines.
178149

179-
To use this feature, you need to use the RemoteOperations class. This feature is only supported with Linux.
180-
Here is an example of how you might set this up:
150+
You can provision nodes on a remote host (Linux only) by wiring `RemoteOperations` into the configuration:
181151

182152
```python
183153
from testgres import ConnectionParams, RemoteOperations, TestgresConfig, get_remote_node
184154

185-
# Set up connection params
186155
conn_params = ConnectionParams(
187-
host='your_host', # replace with your host
188-
username='user_name', # replace with your username
189-
ssh_key='path_to_ssh_key' # replace with your SSH key path
156+
host='example.com',
157+
username='postgres',
158+
ssh_key='/path/to/ssh/key'
190159
)
191160
os_ops = RemoteOperations(conn_params)
192161

193-
# Add remote testgres config before test
194162
TestgresConfig.set_os_ops(os_ops=os_ops)
195163

196-
# Proceed with your test
197-
def test_basic_query(self):
164+
def test_basic_query():
198165
with get_remote_node(conn_params=conn_params) as node:
199166
node.init().start()
200-
res = node.execute('SELECT 1')
201-
self.assertEqual(res, [(1,)])
167+
assert node.execute('SELECT 1') == [(1,)]
202168
```
203169

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+
204199
## Authors
205200

206201
[Ildar Musin](https://github.com/zilder)
207202
[Dmitry Ivanov](https://github.com/funbringer)
208203
[Ildus Kurbangaliev](https://github.com/ildus)
209-
[Yury Zhuravlev](https://github.com/stalkerg)
204+
[Yury Zhuravlev](https://github.com/stalkerg)

0 commit comments

Comments
 (0)