Merge pull request #219 from zenithdb/tidy-up-tests

Tidy up pytest-based tests
This commit is contained in:
Dmitry Ivanov
2021-06-01 22:06:13 +03:00
committed by GitHub
19 changed files with 350 additions and 224 deletions

View File

@@ -8,6 +8,8 @@ pytest = ">=6.0.0"
psycopg2 = "*"
[dev-packages]
yapf = "*"
flake8 = "*"
[requires]
# we need at least 3.6, but pipenv doesn't allow to say this directly

View File

@@ -1,7 +1,7 @@
{
"_meta": {
"hash": {
"sha256": "79fd1b57f1f9eb92a446948e6658880cd0a0f64ab40dd6b38986e72db3007325"
"sha256": "c0de1f00310b584a272dda58386939d53098e49d14db518804a9e56269bfec1d"
},
"pipfile-spec": 6,
"requires": {
@@ -21,6 +21,7 @@
"sha256:149e90d6d8ac20db7a955ad60cf0e6881a3f20d37096140088356da6c716b0b1",
"sha256:ef6aaac3ca6cd92904cdd0d83f629a15f18053ec84e6432106f7a4d04ae4f5fb"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
"version": "==21.2.0"
},
"iniconfig": {
@@ -35,6 +36,7 @@
"sha256:5b327ac1320dc863dca72f4514ecc086f31186744b84a230374cc1fd776feae5",
"sha256:67714da7f7bc052e064859c05c595155bd1ee9f69f76557e21f051443c20947a"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==20.9"
},
"pluggy": {
@@ -42,6 +44,7 @@
"sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0",
"sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==0.13.1"
},
"psycopg2": {
@@ -70,6 +73,7 @@
"sha256:21b81bda15b66ef5e1a777a21c4dcd9c20ad3efd0b3f817e7a809035269e1bd3",
"sha256:3b80836aa6d1feeaa108e046da6423ab8f6ceda6468545ae8d02d9d58d18818a"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==1.10.0"
},
"pyparsing": {
@@ -77,6 +81,7 @@
"sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1",
"sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b"
],
"markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==2.4.7"
},
"pytest": {
@@ -92,8 +97,49 @@
"sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b",
"sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"
],
"markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==0.10.2"
}
},
"develop": {}
"develop": {
"flake8": {
"hashes": [
"sha256:07528381786f2a6237b061f6e96610a4167b226cb926e2aa2b6b1d78057c576b",
"sha256:bf8fd333346d844f616e8d47905ef3a3384edae6b4e9beb0c5101e25e3110907"
],
"index": "pypi",
"version": "==3.9.2"
},
"mccabe": {
"hashes": [
"sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42",
"sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"
],
"version": "==0.6.1"
},
"pycodestyle": {
"hashes": [
"sha256:514f76d918fcc0b55c6680472f0a37970994e07bbb80725808c17089be302068",
"sha256:c389c1d06bf7904078ca03399a4816f974a1d590090fecea0c63ec26ebaf1cef"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==2.7.0"
},
"pyflakes": {
"hashes": [
"sha256:7893783d01b8a89811dd72d7dfd4d84ff098e5eed95cfa8905b22bbffe52efc3",
"sha256:f5bc8ecabc05bb9d291eb5203d6810b49040f6ff446a756326104746cc00c1db"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==2.3.1"
},
"yapf": {
"hashes": [
"sha256:408fb9a2b254c302f49db83c59f9aa0b4b0fd0ec25be3a5c51181327922ff63d",
"sha256:e3a234ba8455fe201eaa649cdac872d590089a18b661e39bbac7020978dd9c2e"
],
"index": "pypi",
"version": "==0.31.0"
}
}
}

View File

@@ -69,7 +69,8 @@ The tests make heavy use of pytest fixtures. You can read about how they work he
Essentially, this means that each time you see a fixture named as an input parameter, the function with that name will be run and passed as a parameter to the function.
So this code:
```
```python
def test_something(zenith_cli, pg_bin):
pass
```
@@ -77,9 +78,11 @@ def test_something(zenith_cli, pg_bin):
... will run the fixtures called `zenith_cli` and `pg_bin` and deliver those results to the test function.
Fixtures can't be imported using the normal python syntax. Instead, use this:
```
```python
pytest_plugins = ("fixtures.something")
```
That will make all the fixtures in the `fixtures/something.py` file available.
Anything that's likely to be used in multiple tests should be built into a fixture.
@@ -87,3 +90,14 @@ Anything that's likely to be used in multiple tests should be built into a fixtu
Note that fixtures can clean up after themselves if they use the `yield` syntax.
Cleanup will happen even if the test fails (raises an unhandled exception).
Python destructors, e.g. `__del__()` aren't recommended for cleanup.
### Code quality
Before submitting a patch, please consider:
* Writing a couple of docstrings to clarify the reasoning behind a new test.
* Running `flake8` (or a linter of your choice, e.g. `pycodestyle`) and fixing possible defects, if any.
* Formatting the code with `yapf -r -i .` (TODO: implement an opt-in pre-commit hook for that).
The tools can be installed with `pipenv install --dev`.

View File

@@ -1,49 +1,60 @@
import pytest
import getpass
import psycopg2
pytest_plugins = ("fixtures.zenith_fixtures")
#
# Create a couple of branches off the main branch, at a historical point in time.
#
def test_branch_behind(zenith_cli, pageserver, postgres, pg_bin):
# Branch at the point where only 100 rows were inserted
zenith_cli.run(["branch", "test_branch_behind", "empty"]);
zenith_cli.run(["branch", "test_branch_behind", "empty"])
pgmain = postgres.create_start('test_branch_behind')
print("postgres is running on 'test_branch_behind' branch")
main_pg_conn = psycopg2.connect(pgmain.connstr());
main_pg_conn = psycopg2.connect(pgmain.connstr())
main_pg_conn.set_isolation_level(psycopg2.extensions.ISOLATION_LEVEL_AUTOCOMMIT)
main_cur = main_pg_conn.cursor()
# Create table, and insert the first 100 rows
main_cur.execute('CREATE TABLE foo (t text)');
main_cur.execute("INSERT INTO foo SELECT 'long string to consume some space' || g FROM generate_series(1, 100) g");
main_cur.execute('SELECT pg_current_wal_insert_lsn()');
main_cur.execute('CREATE TABLE foo (t text)')
main_cur.execute('''
INSERT INTO foo
SELECT 'long string to consume some space' || g
FROM generate_series(1, 100) g
''')
main_cur.execute('SELECT pg_current_wal_insert_lsn()')
lsn_a = main_cur.fetchone()[0]
print('LSN after 100 rows: ' + lsn_a)
# Insert some more rows. (This generates enough WAL to fill a few segments.)
main_cur.execute("INSERT INTO foo SELECT 'long string to consume some space' || g FROM generate_series(1, 100000) g");
main_cur.execute('SELECT pg_current_wal_insert_lsn()');
main_cur.execute('''
INSERT INTO foo
SELECT 'long string to consume some space' || g
FROM generate_series(1, 100000) g
''')
main_cur.execute('SELECT pg_current_wal_insert_lsn()')
lsn_b = main_cur.fetchone()[0]
print('LSN after 100100 rows: ' + lsn_b)
# Branch at the point where only 100 rows were inserted
zenith_cli.run(["branch", "test_branch_behind_hundred", "test_branch_behind@"+lsn_a]);
zenith_cli.run(["branch", "test_branch_behind_hundred", "test_branch_behind@" + lsn_a])
# Insert many more rows. This generates enough WAL to fill a few segments.
main_cur.execute("INSERT INTO foo SELECT 'long string to consume some space' || g FROM generate_series(1, 100000) g");
main_cur.execute('SELECT pg_current_wal_insert_lsn()');
main_cur.execute('''
INSERT INTO foo
SELECT 'long string to consume some space' || g
FROM generate_series(1, 100000) g
''')
main_cur.execute('SELECT pg_current_wal_insert_lsn()')
main_cur.execute('SELECT pg_current_wal_insert_lsn()');
main_cur.execute('SELECT pg_current_wal_insert_lsn()')
lsn_c = main_cur.fetchone()[0]
print('LSN after 200100 rows: ' + lsn_c)
# Branch at the point where only 200 rows were inserted
zenith_cli.run(["branch", "test_branch_behind_more", "test_branch_behind@"+lsn_b]);
zenith_cli.run(["branch", "test_branch_behind_more", "test_branch_behind@" + lsn_b])
pg_hundred = postgres.create_start("test_branch_behind_hundred")
pg_more = postgres.create_start("test_branch_behind_more")
@@ -52,16 +63,16 @@ def test_branch_behind(zenith_cli, pageserver, postgres, pg_bin):
hundred_pg_conn = psycopg2.connect(pg_hundred.connstr())
hundred_pg_conn.set_isolation_level(psycopg2.extensions.ISOLATION_LEVEL_AUTOCOMMIT)
hundred_cur = hundred_pg_conn.cursor()
hundred_cur.execute('SELECT count(*) FROM foo');
assert(hundred_cur.fetchone()[0] == 100);
hundred_cur.execute('SELECT count(*) FROM foo')
assert hundred_cur.fetchone() == (100, )
# On the 'more' branch, we should see 100200 rows
more_pg_conn = psycopg2.connect(pg_more.connstr())
more_pg_conn.set_isolation_level(psycopg2.extensions.ISOLATION_LEVEL_AUTOCOMMIT)
more_cur = more_pg_conn.cursor()
more_cur.execute('SELECT count(*) FROM foo');
assert(more_cur.fetchone()[0] == 100100);
more_cur.execute('SELECT count(*) FROM foo')
assert more_cur.fetchone() == (100100, )
# All the rows are visible on the main branch
main_cur.execute('SELECT count(*) FROM foo');
assert(main_cur.fetchone()[0] == 200100);
main_cur.execute('SELECT count(*) FROM foo')
assert main_cur.fetchone() == (200100, )

View File

@@ -1,6 +1,3 @@
import pytest
import os
import getpass
import psycopg2
pytest_plugins = ("fixtures.zenith_fixtures")
@@ -11,20 +8,24 @@ pytest_plugins = ("fixtures.zenith_fixtures")
#
def test_config(zenith_cli, pageserver, postgres, pg_bin):
# Create a branch for us
zenith_cli.run(["branch", "test_config", "empty"]);
zenith_cli.run(["branch", "test_config", "empty"])
# change config
pg = postgres.create_start('test_config', ['log_min_messages=debug1'])
print('postgres is running on test_config branch')
pg_conn = psycopg2.connect(pg.connstr())
pg_conn.set_isolation_level(psycopg2.extensions.ISOLATION_LEVEL_AUTOCOMMIT)
cur = pg_conn.cursor()
with psycopg2.connect(pg.connstr()) as conn:
conn.set_isolation_level(psycopg2.extensions.ISOLATION_LEVEL_AUTOCOMMIT)
#check that config change was applied
cur.execute('SELECT name, setting from pg_settings WHERE source!=%s and source!=%s', ("default","override",))
for record in cur:
if record[0] == 'log_min_messages':
assert(record[1] == 'debug1')
with conn.cursor() as cur:
cur.execute('''
SELECT setting
FROM pg_settings
WHERE
source != 'default'
AND source != 'override'
AND name = 'log_min_messages'
''')
pg_conn.close()
# check that config change was applied
assert cur.fetchone() == ('debug1', )

View File

@@ -1,37 +1,34 @@
import pytest
import getpass
import psycopg2
pytest_plugins = ("fixtures.zenith_fixtures")
#
# Test CREATE DATABASE when there have been relmapper changes
#
def test_createdb(zenith_cli, pageserver, postgres, pg_bin):
zenith_cli.run(["branch", "test_createdb", "empty"]);
zenith_cli.run(["branch", "test_createdb", "empty"])
pg = postgres.create_start('test_createdb')
print("postgres is running on 'test_createdb' branch")
conn = psycopg2.connect(pg.connstr());
conn.set_isolation_level(psycopg2.extensions.ISOLATION_LEVEL_AUTOCOMMIT)
cur = conn.cursor()
with psycopg2.connect(pg.connstr()) as conn:
conn.set_isolation_level(psycopg2.extensions.ISOLATION_LEVEL_AUTOCOMMIT)
# Cause a 'relmapper' change in the original branch
cur.execute('VACUUM FULL pg_class');
with conn.cursor() as cur:
# Cause a 'relmapper' change in the original branch
cur.execute('VACUUM FULL pg_class')
cur.execute('CREATE DATABASE foodb');
cur.execute('CREATE DATABASE foodb')
cur.execute('SELECT pg_current_wal_insert_lsn()');
lsn = cur.fetchone()[0]
conn.close();
cur.execute('SELECT pg_current_wal_insert_lsn()')
lsn = cur.fetchone()[0]
# Create a branch
zenith_cli.run(["branch", "test_createdb2", "test_createdb@"+lsn]);
zenith_cli.run(["branch", "test_createdb2", "test_createdb@" + lsn])
pg2 = postgres.create_start('test_createdb2')
# Test that you can connect to the new database on both branches
conn = psycopg2.connect(pg.connstr('foodb'));
conn2 = psycopg2.connect(pg2.connstr('foodb'));
for db in (pg, pg2):
psycopg2.connect(db.connstr('foodb')).close()

View File

@@ -1,9 +1,8 @@
import pytest
import os
import psycopg2
pytest_plugins = ("fixtures.zenith_fixtures")
#
# Test multixact state after branching
# Now this test is very minimalistic -
@@ -11,7 +10,6 @@ pytest_plugins = ("fixtures.zenith_fixtures")
# since we don't have functions to check multixact internals.
#
def test_multixact(pageserver, postgres, pg_bin, zenith_cli, base_dir):
# Create a branch for us
zenith_cli.run(["branch", "test_multixact", "empty"])
pg = postgres.create_start('test_multixact')
@@ -21,10 +19,12 @@ def test_multixact(pageserver, postgres, pg_bin, zenith_cli, base_dir):
pg_conn.set_isolation_level(psycopg2.extensions.ISOLATION_LEVEL_AUTOCOMMIT)
cur = pg_conn.cursor()
cur.execute('CREATE TABLE t1(i int primary key);'
'INSERT INTO t1 select * from generate_series(1,100);')
cur.execute('''
CREATE TABLE t1(i int primary key);
INSERT INTO t1 select * from generate_series(1, 100);
''')
cur.execute('SELECT next_multixact_id FROM pg_control_checkpoint();')
cur.execute('SELECT next_multixact_id FROM pg_control_checkpoint()')
next_multixact_id_old = cur.fetchone()[0]
# Lock entries in parallel connections to set multixact
@@ -33,7 +33,7 @@ def test_multixact(pageserver, postgres, pg_bin, zenith_cli, base_dir):
for i in range(nclients):
con = psycopg2.connect(pg.connstr())
# Do not turn on autocommit. We want to hold the key-share locks.
con.cursor().execute('select * from t1 for key share;')
con.cursor().execute('select * from t1 for key share')
connections.append(con)
# We should have a multixact now. We can close the connections.
@@ -43,16 +43,16 @@ def test_multixact(pageserver, postgres, pg_bin, zenith_cli, base_dir):
# force wal flush
cur.execute('checkpoint')
cur.execute('SELECT next_multixact_id, pg_current_wal_flush_lsn() FROM pg_control_checkpoint();')
cur.execute('SELECT next_multixact_id, pg_current_wal_flush_lsn() FROM pg_control_checkpoint()')
res = cur.fetchone()
next_multixact_id = res[0]
lsn = res[1]
# Ensure that we did lock some tuples
assert(int(next_multixact_id) > int(next_multixact_id_old))
assert int(next_multixact_id) > int(next_multixact_id_old)
# Branch at this point
zenith_cli.run(["branch", "test_multixact_new", "test_multixact@"+lsn]);
zenith_cli.run(["branch", "test_multixact_new", "test_multixact@" + lsn])
pg_new = postgres.create_start('test_multixact_new')
print("postgres is running on 'test_multixact_new' branch")
@@ -60,8 +60,8 @@ def test_multixact(pageserver, postgres, pg_bin, zenith_cli, base_dir):
pg_new_conn.set_isolation_level(psycopg2.extensions.ISOLATION_LEVEL_AUTOCOMMIT)
cur_new = pg_new_conn.cursor()
cur_new.execute('SELECT next_multixact_id FROM pg_control_checkpoint();')
cur_new.execute('SELECT next_multixact_id FROM pg_control_checkpoint()')
next_multixact_id_new = cur_new.fetchone()[0]
# Check that we restored pg_controlfile correctly
assert(next_multixact_id_new == next_multixact_id)
assert next_multixact_id_new == next_multixact_id

View File

@@ -1,28 +1,27 @@
import pytest
import psycopg2
import getpass
import json
pytest_plugins = ("fixtures.zenith_fixtures")
def test_status(pageserver):
pg_conn = psycopg2.connect(pageserver.connstr())
pg_conn.autocommit = True
cur = pg_conn.cursor()
cur.execute('status;')
assert cur.fetchone() == ('hello world',)
cur.execute('status')
assert cur.fetchone() == ('hello world', )
pg_conn.close()
def test_branch_list(pageserver, zenith_cli):
def test_branch_list(pageserver, zenith_cli):
# Create a branch for us
zenith_cli.run(["branch", "test_branch_list_main", "empty"]);
zenith_cli.run(["branch", "test_branch_list_main", "empty"])
page_server_conn = psycopg2.connect(pageserver.connstr())
page_server_conn.autocommit = True
page_server_cur = page_server_conn.cursor()
page_server_cur.execute('branch_list;')
page_server_cur.execute('branch_list')
branches = json.loads(page_server_cur.fetchone()[0])
# Filter out branches created by other tests
branches = [x for x in branches if x['name'].startswith('test_branch_list')]
@@ -38,7 +37,7 @@ def test_branch_list(pageserver, zenith_cli):
zenith_cli.run(['branch', 'test_branch_list_experimental', 'test_branch_list_main'])
zenith_cli.run(['pg', 'create', 'test_branch_list_experimental'])
page_server_cur.execute('branch_list;')
page_server_cur.execute('branch_list')
new_branches = json.loads(page_server_cur.fetchone()[0])
# Filter out branches created by other tests
new_branches = [x for x in new_branches if x['name'].startswith('test_branch_list')]

View File

@@ -1,17 +1,15 @@
import pytest
pytest_plugins = ("fixtures.zenith_fixtures")
def test_pgbench(pageserver, postgres, pg_bin, zenith_cli):
# Create a branch for us
zenith_cli.run(["branch", "test_pgbench", "empty"]);
zenith_cli.run(["branch", "test_pgbench", "empty"])
pg = postgres.create_start('test_pgbench')
print("postgres is running on 'test_pgbench' branch")
connstr = pg.connstr();
connstr = pg.connstr()
pg_bin.run_capture(['pgbench', '-i', connstr])
pg_bin.run_capture(['pgbench'] + '-c 10 -T 5 -P 1 -M prepared'.split() + [connstr])

View File

@@ -1,43 +1,42 @@
import pytest
import getpass
import psycopg2
import time
pytest_plugins = ("fixtures.zenith_fixtures")
#
# Test restarting and recreating a postgres instance
#
def test_restart_compute(zenith_cli, pageserver, postgres, pg_bin):
zenith_cli.run(["branch", "test_restart_compute", "empty"]);
zenith_cli.run(["branch", "test_restart_compute", "empty"])
pg = postgres.create_start('test_restart_compute')
print("postgres is running on 'test_restart_compute' branch")
pg_conn = psycopg2.connect(pg.connstr());
pg_conn = psycopg2.connect(pg.connstr())
pg_conn.set_isolation_level(psycopg2.extensions.ISOLATION_LEVEL_AUTOCOMMIT)
cur = pg_conn.cursor()
# Create table, and insert a row
cur.execute('CREATE TABLE foo (t text)');
cur.execute("INSERT INTO foo VALUES ('bar')");
cur.execute('CREATE TABLE foo (t text)')
cur.execute("INSERT INTO foo VALUES ('bar')")
# Stop and restart the Postgres instance
pg_conn.close();
pg.stop();
pg.start();
pg_conn = psycopg2.connect(pg.connstr());
pg_conn.close()
pg.stop()
pg.start()
pg_conn = psycopg2.connect(pg.connstr())
pg_conn.set_isolation_level(psycopg2.extensions.ISOLATION_LEVEL_AUTOCOMMIT)
cur = pg_conn.cursor()
# We can still see the row
cur.execute('SELECT count(*) FROM foo');
assert(cur.fetchone()[0] == 1);
cur.execute('SELECT count(*) FROM foo')
assert cur.fetchone() == (1, )
# Insert another row
cur.execute("INSERT INTO foo VALUES ('bar2')");
cur.execute('SELECT count(*) FROM foo');
assert(cur.fetchone()[0] == 2);
cur.execute("INSERT INTO foo VALUES ('bar2')")
cur.execute('SELECT count(*) FROM foo')
assert cur.fetchone() == (2, )
# FIXME: Currently, there is no guarantee that by the time the INSERT commits, the WAL
# has been streamed safely to the WAL safekeeper or page server. It is merely stored
@@ -47,13 +46,13 @@ def test_restart_compute(zenith_cli, pageserver, postgres, pg_bin):
time.sleep(5)
# Stop, and destroy the Postgres instance. Then recreate and restart it.
pg_conn.close();
pg.stop_and_destroy();
pg.create_start('test_restart_compute');
pg_conn = psycopg2.connect(pg.connstr());
pg_conn.close()
pg.stop_and_destroy()
pg.create_start('test_restart_compute')
pg_conn = psycopg2.connect(pg.connstr())
pg_conn.set_isolation_level(psycopg2.extensions.ISOLATION_LEVEL_AUTOCOMMIT)
cur = pg_conn.cursor()
# We can still see the rows
cur.execute('SELECT count(*) FROM foo');
assert(cur.fetchone()[0] == 2);
cur.execute('SELECT count(*) FROM foo')
assert cur.fetchone() == (2, )

View File

@@ -1,50 +1,49 @@
#
# Test branching, when a transaction is in prepared state
#
import pytest
import getpass
import psycopg2
pytest_plugins = ("fixtures.zenith_fixtures")
#
# Test branching, when a transaction is in prepared state
#
def test_twophase(zenith_cli, pageserver, postgres, pg_bin):
zenith_cli.run(["branch", "test_twophase", "empty"]);
zenith_cli.run(["branch", "test_twophase", "empty"])
pg = postgres.create_start('test_twophase', ['max_prepared_transactions=5'])
print("postgres is running on 'test_twophase' branch")
conn = psycopg2.connect(pg.connstr());
conn = psycopg2.connect(pg.connstr())
conn.set_isolation_level(psycopg2.extensions.ISOLATION_LEVEL_AUTOCOMMIT)
cur = conn.cursor()
cur.execute('CREATE TABLE foo (t text)');
cur.execute('CREATE TABLE foo (t text)')
# Prepare a transaction that will insert a row
cur.execute('BEGIN');
cur.execute("INSERT INTO foo VALUES ('one')");
cur.execute("PREPARE TRANSACTION 'insert_one'");
cur.execute('BEGIN')
cur.execute("INSERT INTO foo VALUES ('one')")
cur.execute("PREPARE TRANSACTION 'insert_one'")
# Prepare another transaction that will insert a row
cur.execute('BEGIN');
cur.execute("INSERT INTO foo VALUES ('two')");
cur.execute("PREPARE TRANSACTION 'insert_two'");
cur.execute('BEGIN')
cur.execute("INSERT INTO foo VALUES ('two')")
cur.execute("PREPARE TRANSACTION 'insert_two'")
# Create a branch with the transaction in prepared state
zenith_cli.run(["branch", "test_twophase_prepared", "test_twophase"]);
zenith_cli.run(["branch", "test_twophase_prepared", "test_twophase"])
pg2 = postgres.create_start('test_twophase_prepared', ['max_prepared_transactions=5'])
conn2 = psycopg2.connect(pg2.connstr());
conn2 = psycopg2.connect(pg2.connstr())
conn2.set_isolation_level(psycopg2.extensions.ISOLATION_LEVEL_AUTOCOMMIT)
cur2 = conn2.cursor()
# On the new branch, commit one of the prepared transactions, abort the other one.
cur2.execute("COMMIT PREPARED 'insert_one'");
cur2.execute("ROLLBACK PREPARED 'insert_two'");
cur2.execute("COMMIT PREPARED 'insert_one'")
cur2.execute("ROLLBACK PREPARED 'insert_two'")
cur2.execute('SELECT * FROM foo');
assert(cur2.fetchall() == [('one',)]);
cur2.execute('SELECT * FROM foo')
assert cur2.fetchall() == [('one', )]
# Neither insert is visible on the original branch, the transactions are still
# in prepared state there.
cur.execute('SELECT * FROM foo');
assert(cur.fetchall() == []);
cur.execute('SELECT * FROM foo')
assert cur.fetchall() == []

View File

@@ -1,25 +1,26 @@
import pytest
import psycopg2
import json
pytest_plugins = ("fixtures.zenith_fixtures")
def helper_compare_branch_list(page_server_cur, zenith_cli):
"""
Compare branches list returned by CLI and directly via API.
Filters out branches created by other tests.
"""
page_server_cur.execute('branch_list;')
page_server_cur.execute('branch_list')
branches_api = sorted(map(lambda b: b['name'], json.loads(page_server_cur.fetchone()[0])))
branches_api = [b for b in branches_api if b.startswith('test_cli_') or b in ('empty', 'main')]
res = zenith_cli.run(["branch"]);
assert(res.stderr == '')
res = zenith_cli.run(["branch"])
assert res.stderr == ''
branches_cli = sorted(map(lambda b: b.split(':')[-1].strip(), res.stdout.strip().split("\n")))
branches_cli = [b for b in branches_cli if b.startswith('test_cli_') or b in ('empty', 'main')]
assert(branches_api == branches_cli)
assert branches_api == branches_cli
def test_cli_branch_list(pageserver, zenith_cli):
@@ -31,19 +32,19 @@ def test_cli_branch_list(pageserver, zenith_cli):
helper_compare_branch_list(page_server_cur, zenith_cli)
# Create a branch for us
res = zenith_cli.run(["branch", "test_cli_branch_list_main", "main"]);
assert(res.stderr == '')
res = zenith_cli.run(["branch", "test_cli_branch_list_main", "main"])
assert res.stderr == ''
helper_compare_branch_list(page_server_cur, zenith_cli)
# Create a nested branch
res = zenith_cli.run(["branch", "test_cli_branch_list_nested", "test_cli_branch_list_main"]);
assert(res.stderr == '')
res = zenith_cli.run(["branch", "test_cli_branch_list_nested", "test_cli_branch_list_main"])
assert res.stderr == ''
helper_compare_branch_list(page_server_cur, zenith_cli)
# Check that all new branches are visible via CLI
res = zenith_cli.run(["branch"]);
assert(res.stderr == '')
res = zenith_cli.run(["branch"])
assert res.stderr == ''
branches_cli = sorted(map(lambda b: b.split(':')[-1].strip(), res.stdout.strip().split("\n")))
assert('test_cli_branch_list_main' in branches_cli)
assert('test_cli_branch_list_nested' in branches_cli)
assert 'test_cli_branch_list_main' in branches_cli
assert 'test_cli_branch_list_nested' in branches_cli

View File

@@ -1,16 +1,16 @@
import pytest
from fixtures.utils import mkdir_if_needed
import getpass
import os
import psycopg2
from fixtures.utils import mkdir_if_needed
pytest_plugins = ("fixtures.zenith_fixtures")
def test_isolation(pageserver, postgres, pg_bin, zenith_cli, test_output_dir, pg_distrib_dir, base_dir, capsys):
def test_isolation(pageserver, postgres, pg_bin, zenith_cli, test_output_dir, pg_distrib_dir,
base_dir, capsys):
# Create a branch for us
zenith_cli.run(["branch", "test_isolation", "empty"]);
zenith_cli.run(["branch", "test_isolation", "empty"])
# Connect to postgres and create a database called "regression".
# isolation tests use prepared transactions, so enable them
@@ -27,10 +27,8 @@ def test_isolation(pageserver, postgres, pg_bin, zenith_cli, test_output_dir, pg
mkdir_if_needed(os.path.join(runpath, 'testtablespace'))
# Compute all the file locations that pg_isolation_regress will need.
build_path = os.path.join(
pg_distrib_dir, 'build/src/test/isolation')
src_path = os.path.join(
base_dir, 'vendor/postgres/src/test/isolation')
build_path = os.path.join(pg_distrib_dir, 'build/src/test/isolation')
src_path = os.path.join(base_dir, 'vendor/postgres/src/test/isolation')
bindir = os.path.join(pg_distrib_dir, 'bin')
schedule = os.path.join(src_path, 'isolation_schedule')
pg_isolation_regress = os.path.join(build_path, 'pg_isolation_regress')
@@ -41,7 +39,7 @@ def test_isolation(pageserver, postgres, pg_bin, zenith_cli, test_output_dir, pg
'--bindir={}'.format(bindir),
'--dlpath={}'.format(build_path),
'--inputdir={}'.format(src_path),
'--schedule={}'.format(schedule)
'--schedule={}'.format(schedule),
]
env = {

View File

@@ -1,15 +1,16 @@
import pytest
from fixtures.utils import mkdir_if_needed
import getpass
import os
import psycopg2
from fixtures.utils import mkdir_if_needed
pytest_plugins = ("fixtures.zenith_fixtures")
def test_pg_regress(pageserver, postgres, pg_bin, zenith_cli, test_output_dir, pg_distrib_dir, base_dir, capsys):
def test_pg_regress(pageserver, postgres, pg_bin, zenith_cli, test_output_dir, pg_distrib_dir,
base_dir, capsys):
# Create a branch for us
zenith_cli.run(["branch", "test_pg_regress", "empty"]);
zenith_cli.run(["branch", "test_pg_regress", "empty"])
# Connect to postgres and create a database called "regression".
pg = postgres.create_start('test_pg_regress')
@@ -25,10 +26,8 @@ def test_pg_regress(pageserver, postgres, pg_bin, zenith_cli, test_output_dir, p
mkdir_if_needed(os.path.join(runpath, 'testtablespace'))
# Compute all the file locations that pg_regress will need.
build_path = os.path.join(
pg_distrib_dir, 'build/src/test/regress')
src_path = os.path.join(
base_dir, 'vendor/postgres/src/test/regress')
build_path = os.path.join(pg_distrib_dir, 'build/src/test/regress')
src_path = os.path.join(base_dir, 'vendor/postgres/src/test/regress')
bindir = os.path.join(pg_distrib_dir, 'bin')
schedule = os.path.join(src_path, 'parallel_schedule')
pg_regress = os.path.join(build_path, 'pg_regress')

View File

@@ -1,15 +1,16 @@
import pytest
from fixtures.utils import mkdir_if_needed
import getpass
import os
import psycopg2
from fixtures.utils import mkdir_if_needed
pytest_plugins = ("fixtures.zenith_fixtures")
def test_zenith_regress(pageserver, postgres, pg_bin, zenith_cli, test_output_dir, pg_distrib_dir, base_dir, capsys):
def test_zenith_regress(pageserver, postgres, pg_bin, zenith_cli, test_output_dir, pg_distrib_dir,
base_dir, capsys):
# Create a branch for us
zenith_cli.run(["branch", "test_zenith_regress", "empty"]);
zenith_cli.run(["branch", "test_zenith_regress", "empty"])
# Connect to postgres and create a database called "regression".
pg = postgres.create_start('test_zenith_regress')
@@ -26,10 +27,8 @@ def test_zenith_regress(pageserver, postgres, pg_bin, zenith_cli, test_output_di
# Compute all the file locations that pg_regress will need.
# This test runs zenith specific tests
build_path = os.path.join(
pg_distrib_dir, 'build/src/test/regress')
src_path = os.path.join(
base_dir, 'test_runner/zenith_regress')
build_path = os.path.join(pg_distrib_dir, 'build/src/test/regress')
src_path = os.path.join(base_dir, 'test_runner/zenith_regress')
bindir = os.path.join(pg_distrib_dir, 'bin')
schedule = os.path.join(src_path, 'parallel_schedule')
pg_regress = os.path.join(build_path, 'pg_regress')

View File

@@ -1,7 +1,7 @@
import os
import subprocess
def get_self_dir():
""" Get the path to the directory where this script lives. """
return os.path.dirname(os.path.abspath(__file__))

View File

@@ -1,13 +1,10 @@
import getpass
import os
import psycopg2
import pytest
import shutil
import subprocess
import sys
from .utils import (get_self_dir, mkdir_if_needed,
subprocess_capture, global_counter)
from .utils import (get_self_dir, mkdir_if_needed, subprocess_capture)
"""
This file contains pytest fixtures. A fixture is a test resource that can be
summoned by placing its name in the test's arguments.
@@ -20,7 +17,7 @@ ZENITH_BIN, POSTGRES_DISTRIB_DIR, etc. See README.md for more information.
To use fixtures in a test file, add this line of code:
pytest_plugins = ("fixtures.zenith_fixtures")
>>> pytest_plugins = ("fixtures.zenith_fixtures")
Don't import functions from this file, or pytest will emit warnings. Instead
put directly-importable functions into utils.py or another separate file.
@@ -35,7 +32,8 @@ def determine_scope(fixture_name, config):
def zenfixture(func):
""" This is a python decorator for fixtures with a flexible scope.
"""
This is a python decorator for fixtures with a flexible scope.
By default every test function will set up and tear down a new
database. In pytest, this is called fixtures "function" scope.
@@ -43,8 +41,8 @@ def zenfixture(func):
If the environment variable TEST_SHARED_FIXTURES is set, then all
tests will share the same database. State, logs, etc. will be
stored in a directory called "shared".
"""
if os.environ.get('TEST_SHARED_FIXTURES') is None:
scope = 'function'
else:
@@ -55,6 +53,7 @@ def zenfixture(func):
@pytest.fixture(autouse=True, scope='session')
def safety_check():
""" Ensure that no unwanted daemons are running before we start testing. """
# does not use -c as it is not supported on macOS
cmd = ['pgrep', 'pageserver|postgres|wal_acceptor']
result = subprocess.run(cmd, stdout=subprocess.DEVNULL)
@@ -66,12 +65,12 @@ def safety_check():
class ZenithCli:
""" An object representing the CLI binary named "zenith".
"""
An object representing the CLI binary named "zenith".
We also store an environment that will tell the CLI to operate
on a particular ZENITH_REPO_DIR.
"""
def __init__(self, binpath, repo_dir, pg_distrib_dir):
assert os.path.isdir(binpath)
self.binpath = binpath
@@ -81,21 +80,27 @@ class ZenithCli:
self.env['POSTGRES_DISTRIB_DIR'] = pg_distrib_dir
def run(self, arguments):
""" Run "zenith" with the specified arguments.
"""
Run "zenith" with the specified arguments.
arguments must be in list form, e.g. ['pg', 'create']
Arguments must be in list form, e.g. ['pg', 'create']
Return both stdout and stderr, which can be accessed as
result = zenith_cli.run(...)
assert(result.stderr == "")
print(result.stdout)
>>> result = zenith_cli.run(...)
>>> assert result.stderr == ""
>>> print(result.stdout)
"""
assert type(arguments) == list
args = [self.bin_zenith] + arguments
print('Running command "{}"'.format(' '.join(args)))
return subprocess.run(args, env=self.env, check=True, universal_newlines=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
return subprocess.run(args,
env=self.env,
check=True,
universal_newlines=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
@zenfixture
@@ -105,7 +110,6 @@ def zenith_cli(zenith_binpath, repo_dir, pg_distrib_dir):
class ZenithPageserver:
""" An object representing a running pageserver. """
def __init__(self, zenith_cli):
self.zenith_cli = zenith_cli
self.running = False
@@ -129,34 +133,37 @@ class ZenithPageserver:
# returns a libpq connection string for connecting to it.
def connstr(self):
username = getpass.getuser()
conn_str = 'host={} port={} dbname=postgres user={}'.format(
'localhost', 64000, username)
conn_str = 'host={} port={} dbname=postgres user={}'.format('localhost', 64000, username)
return conn_str
# The 'pageserver' fixture provides a Page Server that's up and running.
#
# If TEST_SHARED_FIXTURES is set, the Page Server instance is shared by all
# the tests. To avoid clashing with other tests, don't use the 'main' branch in
# the tests directly. Instead, create a branch off the 'empty' branch and use
# that.
#
# By convention, the test branches are named after the tests. For example,
# test called 'test_foo' would create and use branches with the 'test_foo' prefix.
@zenfixture
def pageserver(zenith_cli):
"""
The 'pageserver' fixture provides a Page Server that's up and running.
If TEST_SHARED_FIXTURES is set, the Page Server instance is shared by all
the tests. To avoid clashing with other tests, don't use the 'main' branch in
the tests directly. Instead, create a branch off the 'empty' branch and use
that.
By convention, the test branches are named after the tests. For example,
test called 'test_foo' would create and use branches with the 'test_foo' prefix.
"""
ps = ZenithPageserver(zenith_cli)
ps.init()
ps.start()
# For convenience in tests, create a branch from the freshly-initialized cluster.
zenith_cli.run(["branch", "empty", "main"]);
zenith_cli.run(["branch", "empty", "main"])
yield ps
# After the yield comes any cleanup code we need.
print('Starting pageserver cleanup')
ps.stop()
class Postgres:
""" An object representing a running postgres daemon. """
def __init__(self, zenith_cli, repo_dir, instance_num):
self.zenith_cli = zenith_cli
self.instance_num = instance_num
@@ -169,22 +176,37 @@ class Postgres:
# path to conf is <repo_dir>/pgdatadirs/<branch_name>/postgresql.conf
def create(self, branch, config_lines=None):
""" create the pg data directory """
"""
Create the pg data directory.
Returns self.
"""
self.zenith_cli.run(['pg', 'create', branch])
self.branch = branch
if config_lines is None:
config_lines = []
self.config(config_lines)
return
return self
def start(self):
""" start the server """
"""
Start the Postgres instance.
Returns self.
"""
self.zenith_cli.run(['pg', 'start', self.branch])
self.running = True
return
#lines should be an array of valid postgresql.conf rows
return self
def config(self, lines):
"""
Add lines to postgresql.conf.
Lines should be an array of valid postgresql.conf rows.
Returns self.
"""
filename = 'pgdatadirs/{}/postgresql.conf'.format(self.branch)
config_name = os.path.join(self.repo_dir, filename)
with open(config_name, 'a') as conf:
@@ -192,25 +214,50 @@ class Postgres:
conf.write(line)
conf.write('\n')
return self
def stop(self):
""" stop the server """
"""
Stop the Postgres instance if it's running.
Returns self.
"""
if self.running:
self.zenith_cli.run(['pg', 'stop', self.branch])
return self
def stop_and_destroy(self):
"""
Stop the Postgres instance, then destroy it.
Returns self.
"""
self.zenith_cli.run(['pg', 'stop', '--destroy', self.branch])
def create_start(self, branch, config_lines=None):
self.create(branch, config_lines);
self.start();
return
return self
def create_start(self, branch, config_lines=None):
"""
Create a Postgres instance, then start it.
Returns self.
"""
self.create(branch, config_lines).start()
return self
# Return a libpq connection string to connect to the Postgres instance
def connstr(self, dbname='postgres'):
conn_str = 'host={} port={} dbname={} user={}'.format(
self.host, self.port, dbname, self.username)
"""
Build a libpq connection string for the Postgres instance.
"""
conn_str = 'host={} port={} dbname={} user={}'.format(self.host, self.port, dbname,
self.username)
return conn_str
class PostgresFactory:
""" An object representing multiple running postgres daemons. """
def __init__(self, zenith_cli, repo_dir):
@@ -224,13 +271,13 @@ class PostgresFactory:
pg = Postgres(self.zenith_cli, self.repo_dir, self.num_instances + 1)
self.num_instances += 1
self.instances.append(pg)
pg.create_start(branch, config_lines)
return pg
return pg.create_start(branch, config_lines)
def stop_all(self):
for pg in self.instances:
pg.stop()
@zenfixture
def postgres(zenith_cli, repo_dir):
pgfactory = PostgresFactory(zenith_cli, repo_dir)
@@ -242,7 +289,6 @@ def postgres(zenith_cli, repo_dir):
class PgBin:
""" A helper class for executing postgres binaries """
def __init__(self, log_dir, pg_distrib_dir):
self.log_dir = log_dir
self.pg_install_path = pg_distrib_dir
@@ -251,7 +297,7 @@ class PgBin:
self.env['LD_LIBRARY_PATH'] = os.path.join(self.pg_install_path, 'lib')
def _fixpath(self, command):
if not '/' in command[0]:
if '/' not in command[0]:
command[0] = os.path.join(self.pg_bin_path, command[0])
def _build_env(self, env_add):
@@ -262,7 +308,8 @@ class PgBin:
return env
def run(self, command, env=None, cwd=None):
""" Run one of the postgres binaries.
"""
Run one of the postgres binaries.
The command should be in list form, e.g. ['pgbench', '-p', '55432']
@@ -272,18 +319,20 @@ class PgBin:
characters present), then it will be edited to include the correct path.
If you want stdout/stderr captured to files, use `run_capture` instead.
"""
self._fixpath(command)
print('Running command "{}"'.format(' '.join(command)))
env = self._build_env(env)
subprocess.run(command, env=env, cwd=cwd, check=True)
def run_capture(self, command, env=None, cwd=None):
""" Run one of the postgres binaries, with stderr and stdout redirected to a file.
"""
Run one of the postgres binaries, with stderr and stdout redirected to a file.
This is just like `run`, but for chatty programs.
"""
self._fixpath(command)
print('Running command "{}"'.format(' '.join(command)))
env = self._build_env(env)
@@ -298,6 +347,7 @@ def pg_bin(test_output_dir, pg_distrib_dir):
@zenfixture
def base_dir():
""" find the base directory (currently this is the git root) """
base_dir = os.path.normpath(os.path.join(get_self_dir(), '../..'))
print('base_dir is', base_dir)
return base_dir
@@ -306,6 +356,7 @@ def base_dir():
@zenfixture
def top_output_dir(base_dir):
""" Compute the top-level directory for all tests. """
env_test_output = os.environ.get('TEST_OUTPUT')
if env_test_output is not None:
output_dir = env_test_output
@@ -318,6 +369,7 @@ def top_output_dir(base_dir):
@zenfixture
def test_output_dir(request, top_output_dir):
""" Compute the working directory for an individual test. """
if os.environ.get('TEST_SHARED_FIXTURES') is None:
# one directory per test
test_name = request.node.name
@@ -334,18 +386,21 @@ def test_output_dir(request, top_output_dir):
@zenfixture
def repo_dir(request, test_output_dir):
""" Compute the test repo_dir
"""
Compute the test repo_dir.
"repo_dir" is the place where all of the pageserver files will go.
It doesn't have anything to do with the git repo.
"""
repo_dir = os.path.join(test_output_dir, 'repo')
return repo_dir
@zenfixture
def zenith_binpath(base_dir):
""" find the zenith binaries """
""" Find the zenith binaries. """
env_zenith_bin = os.environ.get('ZENITH_BIN')
if env_zenith_bin:
zenith_dir = env_zenith_bin
@@ -358,7 +413,8 @@ def zenith_binpath(base_dir):
@zenfixture
def pg_distrib_dir(base_dir):
""" find the postgress install """
""" Find the postgres install. """
env_postgres_bin = os.environ.get('POSTGRES_DISTRIB_DIR')
if env_postgres_bin:
pg_dir = env_postgres_bin

12
test_runner/setup.cfg Normal file
View File

@@ -0,0 +1,12 @@
# Just trying to gather linter settings in one file.
# I wonder if there's a way to de-duplicate them...
[flake8]
max-line-length = 100
[pycodestyle]
max-line-length = 100
[yapf]
based_on_style = pep8
column_limit = 100

View File

@@ -2,9 +2,7 @@ import pytest
import os
pytest_plugins = ("fixtures.zenith_fixtures")
"""
Use this test to see what happens when tests fail.
We should be able to clean up after ourselves, including stopping any
@@ -12,21 +10,18 @@ postgres or pageserver processes.
Set the environment variable RUN_BROKEN to see this test run (and fail,
and hopefully not leave any server processes behind).
"""
run_broken = pytest.mark.skipif(os.environ.get('RUN_BROKEN') is None,
reason="only used for testing the fixtures")
run_broken = pytest.mark.skipif(
os.environ.get('RUN_BROKEN') == None,
reason="only used for testing the fixtures"
)
@run_broken
def test_broken(zenith_cli, pageserver, postgres, pg_bin):
# Create a branch for us
zenith_cli.run(["branch", "test_broken", "empty"]);
zenith_cli.run(["branch", "test_broken", "empty"])
pg = postgres.create_start("test_broken")
postgres.create_start("test_broken")
print('postgres is running')
print('THIS NEXT COMMAND WILL FAIL:')