Merge pull request 'postgres-integration-tests' (#89) from postgres-integration-tests into main
Reviewed-on: #89
This commit is contained in:
commit
ee404e8121
22 changed files with 661 additions and 31 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -12,3 +12,4 @@ pglabAll.pro.user.4.8-pre1
|
||||||
*.user
|
*.user
|
||||||
/pglabAll.pro.user*
|
/pglabAll.pro.user*
|
||||||
/public/
|
/public/
|
||||||
|
/Testing/Temporary
|
||||||
2
.idea/pgLab.iml
generated
2
.idea/pgLab.iml
generated
|
|
@ -1,2 +0,0 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<module classpath="CIDR" type="CPP_MODULE" version="4" />
|
|
||||||
43
.junie/skills/pg-integration-tests/SKILL.md
Normal file
43
.junie/skills/pg-integration-tests/SKILL.md
Normal file
|
|
@ -0,0 +1,43 @@
|
||||||
|
---
|
||||||
|
name: pg-integration-tests
|
||||||
|
description: Write and run pgLab tests against a real PostgreSQL server (14–17) using the pgintegration harness (DbTest fixture, template database, REQUIRE_PG_AT_LEAST). Use whenever a test needs a live server, e.g. catalog introspection, generated SQL that must be executed, version-specific behaviour, or when the user mentions integration tests, IntegrationTests, docker-compose Postgres or TEST_DATABASE_URL.
|
||||||
|
---
|
||||||
|
|
||||||
|
# Integration tests against real PostgreSQL
|
||||||
|
|
||||||
|
Build/run commands live in `AGENTS.md` (Build and test); don't duplicate them here.
|
||||||
|
|
||||||
|
## When to use
|
||||||
|
Use for behaviour that depends on a real server: catalog queries, `pg_get_*` output, executing generated SQL, version differences. Pure string/logic tests belong in `PgsqlTests` / `pglabtests` instead.
|
||||||
|
|
||||||
|
## Layout
|
||||||
|
- `tests/pgintegration/` – static lib: `PgEnv` (global environment, builds template DB `tmpl_<pid>` from `fixtures/schema.sql`), `DbTest` fixture, helpers.
|
||||||
|
- `tests/integration/` – `IntegrationTests` executable, `tst_*.cpp`, `fixtures/schema.sql`, `docker-compose.yml`.
|
||||||
|
- The binary only reads `TEST_DATABASE_URL`; unset means every `DbTest` skips.
|
||||||
|
|
||||||
|
## Writing a test
|
||||||
|
```cpp
|
||||||
|
#include "DbTest.h"
|
||||||
|
#include "PgTestEnvironment.h"
|
||||||
|
class MyFeature : public pgtest::DbTest {};
|
||||||
|
|
||||||
|
TEST_F(MyFeature, Works)
|
||||||
|
{
|
||||||
|
REQUIRE_PG_AT_LEAST(15); // skip on older servers
|
||||||
|
auto r = conn().query("SELECT 1"); // Pgsql::Connection, throws on SQL error
|
||||||
|
EXPECT_EQ(r.rows(), 1);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
- Derive from `pgtest::DbTest`. Each test gets a fresh database (`t_<n>_<pid>`) cloned from the template and dropped afterwards, so never rely on state from another test and freely modify data/schema.
|
||||||
|
- `conn()` is the test connection, `dbUrl()` gives a URL for code under test that opens its own connection (close those before the test ends; teardown uses `DROP DATABASE ... WITH (FORCE)`).
|
||||||
|
- Gate version-specific features with `REQUIRE_PG_AT_LEAST(v)`; use `pgtest::serverVersionNum()` for finer checks. Also test graceful behaviour on PG 14.
|
||||||
|
- `query()` throws on error; use `EXPECT_THROW` for expected failures.
|
||||||
|
|
||||||
|
## Adding things
|
||||||
|
- New test file: add it to the `add_executable(IntegrationTests ...)` list in `tests/integration/CMakeLists.txt` (no globbing).
|
||||||
|
- Shared schema: extend `tests/integration/fixtures/schema.sql` (plain multi-statement SQL, loaded once per process into the template). Keep it small; it must work on all of 14–17. Version-specific schema should be created inside the test.
|
||||||
|
- Cluster-wide objects (roles, tablespaces) are not isolated by cloning: use unique names (include `pgtest::processId()`) and drop them in `TearDown`.
|
||||||
|
|
||||||
|
## Running and checking
|
||||||
|
- One version manually: set `TEST_DATABASE_URL` and use `--gtest_filter` (see AGENTS.md); start servers with `docker compose -f tests/integration/docker-compose.yml up -d --wait pg16`, stop with `down -v`.
|
||||||
|
- After a run no `t_*` or `tmpl_*` databases should remain: `psql <url> -Atc "select datname from pg_database where datname ~ '^(t|tmpl)_'"`.
|
||||||
|
|
@ -18,6 +18,7 @@ cmake --build build
|
||||||
ctest --test-dir build --output-on-failure
|
ctest --test-dir build --output-on-failure
|
||||||
```
|
```
|
||||||
- Run one suite: `./build/tests/pglabtests/pglabtests --gtest_filter='SqlLexer*'` (same for `PgsqlTests`).
|
- Run one suite: `./build/tests/pglabtests/pglabtests --gtest_filter='SqlLexer*'` (same for `PgsqlTests`).
|
||||||
|
- Integration tests against real PostgreSQL 14–17 (`IntegrationTests`, label `integration`): configure with `-DPGLAB_INTEGRATION_TESTS=ON` (needs Docker), then `ctest --test-dir build -L integration -j4`. This starts `tests/integration/docker-compose.yml` (ports 54314–54317), runs the binary once per version and tears it down. Manual run against any server: `TEST_DATABASE_URL=postgresql://postgres:test@localhost:54316/postgres ./build/tests/integration/IntegrationTests --gtest_filter='Smoke*'`. Without the variable the tests skip. Developer-facing only, no reno note. See the `pg-integration-tests` skill.
|
||||||
- `-DBUILD_TESTS=OFF` skips tests. `build/` is git-ignored; the first configure builds dependencies and is slow.
|
- `-DBUILD_TESTS=OFF` skips tests. `build/` is git-ignored; the first configure builds dependencies and is slow.
|
||||||
- Run the app headless with `QT_QPA_PLATFORM=offscreen ./build/pglab/pglab`. It needs a PostgreSQL server for anything useful.
|
- Run the app headless with `QT_QPA_PLATFORM=offscreen ./build/pglab/pglab`. It needs a PostgreSQL server for anything useful.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -205,26 +205,9 @@ void DatabasesTableModel::StartLoadDatabaseSizes(std::map<Oid, int> oidIndex)
|
||||||
|
|
||||||
DatabasesTableModel::DatabaseSizes DatabasesTableModel::QueryDatabaseSizes()
|
DatabasesTableModel::DatabaseSizes DatabasesTableModel::QueryDatabaseSizes()
|
||||||
{
|
{
|
||||||
std::string q =
|
|
||||||
"SELECT pg_database.oid, "
|
|
||||||
" case when has_database_privilege(current_role, pg_database.oid, \n"
|
|
||||||
" 'connect') then pg_database_size(pg_database.oid) else -1 end \n"
|
|
||||||
"FROM pg_database";
|
|
||||||
|
|
||||||
Pgsql::Connection connection;
|
Pgsql::Connection connection;
|
||||||
connection.connect(openDatabase->config().connectionString());
|
connection.connect(openDatabase->config().connectionString());
|
||||||
Pgsql::Result sizesResult = connection.query(q.c_str());
|
return loadDatabaseSizes(connection);
|
||||||
|
|
||||||
DatabaseSizes result;
|
|
||||||
result.reserve(sizesResult.rows());
|
|
||||||
for (auto& row : sizesResult)
|
|
||||||
{
|
|
||||||
DatabaseSize ds;
|
|
||||||
ds.oid = row.get(0);
|
|
||||||
ds.totalBytes = row.get(1);
|
|
||||||
result.push_back(ds);
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void DatabasesTableModel::PopulateSizes(std::map<Oid, int> oidIndex, DatabaseSizes sizes)
|
void DatabasesTableModel::PopulateSizes(std::map<Oid, int> oidIndex, DatabaseSizes sizes)
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
|
|
||||||
#include "BaseTableModel.h"
|
#include "BaseTableModel.h"
|
||||||
#include "catalog/PgDatabase.h"
|
#include "catalog/PgDatabase.h"
|
||||||
|
#include "catalog/PgDatabaseSizes.h"
|
||||||
#include <memory>
|
#include <memory>
|
||||||
|
|
||||||
class OpenDatabase;
|
class OpenDatabase;
|
||||||
|
|
@ -45,17 +46,8 @@ public:
|
||||||
protected:
|
protected:
|
||||||
virtual QVariant getDataMeaning(const QModelIndex &index) const override;
|
virtual QVariant getDataMeaning(const QModelIndex &index) const override;
|
||||||
private:
|
private:
|
||||||
class DatabaseSize {
|
using DatabaseSize = PgDatabaseSize;
|
||||||
public:
|
using DatabaseSizes = PgDatabaseSizes;
|
||||||
Oid oid;
|
|
||||||
int64_t totalBytes;
|
|
||||||
|
|
||||||
DatabaseSize()
|
|
||||||
: oid(InvalidOid)
|
|
||||||
, totalBytes(-1)
|
|
||||||
{}
|
|
||||||
};
|
|
||||||
using DatabaseSizes = std::vector<DatabaseSize>;
|
|
||||||
|
|
||||||
class Database {
|
class Database {
|
||||||
public:
|
public:
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@ add_library(pglablib STATIC
|
||||||
catalog/PgIndexContainer.cpp
|
catalog/PgIndexContainer.cpp
|
||||||
catalog/PgConstraint.cpp
|
catalog/PgConstraint.cpp
|
||||||
catalog/PgConstraintContainer.cpp
|
catalog/PgConstraintContainer.cpp
|
||||||
|
catalog/PgDatabaseSizes.cpp
|
||||||
ParamListJson.cpp
|
ParamListJson.cpp
|
||||||
ParamListModel.cpp
|
ParamListModel.cpp
|
||||||
sqlite/SQLiteConnection.cpp
|
sqlite/SQLiteConnection.cpp
|
||||||
|
|
@ -95,6 +96,7 @@ add_library(pglablib STATIC
|
||||||
catalog/PgIndexContainer.h
|
catalog/PgIndexContainer.h
|
||||||
catalog/PgConstraint.h
|
catalog/PgConstraint.h
|
||||||
catalog/PgConstraintContainer.h
|
catalog/PgConstraintContainer.h
|
||||||
|
catalog/PgDatabaseSizes.h
|
||||||
ParamListJson.h
|
ParamListJson.h
|
||||||
ParamListModel.h
|
ParamListModel.h
|
||||||
sqlite/SQLiteConnection.h
|
sqlite/SQLiteConnection.h
|
||||||
|
|
|
||||||
25
pglablib/catalog/PgDatabaseSizes.cpp
Normal file
25
pglablib/catalog/PgDatabaseSizes.cpp
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
#include "PgDatabaseSizes.h"
|
||||||
|
|
||||||
|
#include "Pgsql_Result.h"
|
||||||
|
|
||||||
|
PgDatabaseSizes loadDatabaseSizes(Pgsql::Connection &connection)
|
||||||
|
{
|
||||||
|
std::string q =
|
||||||
|
"SELECT pg_database.oid, "
|
||||||
|
" case when has_database_privilege(current_role, pg_database.oid, \n"
|
||||||
|
" 'connect') then pg_database_size(pg_database.oid) else -1 end \n"
|
||||||
|
"FROM pg_database";
|
||||||
|
|
||||||
|
Pgsql::Result sizesResult = connection.query(q.c_str());
|
||||||
|
|
||||||
|
PgDatabaseSizes result;
|
||||||
|
result.reserve(sizesResult.rows());
|
||||||
|
for (auto& row : sizesResult)
|
||||||
|
{
|
||||||
|
PgDatabaseSize ds;
|
||||||
|
ds.oid = row.get(0);
|
||||||
|
ds.totalBytes = row.get(1);
|
||||||
|
result.push_back(ds);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
25
pglablib/catalog/PgDatabaseSizes.h
Normal file
25
pglablib/catalog/PgDatabaseSizes.h
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
#ifndef PGDATABASESIZES_H
|
||||||
|
#define PGDATABASESIZES_H
|
||||||
|
|
||||||
|
#include "Pgsql_Connection.h"
|
||||||
|
#include <libpq-fe.h>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
class PgDatabaseSize {
|
||||||
|
public:
|
||||||
|
Oid oid;
|
||||||
|
int64_t totalBytes;
|
||||||
|
|
||||||
|
PgDatabaseSize()
|
||||||
|
: oid(InvalidOid)
|
||||||
|
, totalBytes(-1)
|
||||||
|
{}
|
||||||
|
};
|
||||||
|
|
||||||
|
using PgDatabaseSizes = std::vector<PgDatabaseSize>;
|
||||||
|
|
||||||
|
/// Returns the size of every database. The size is -1 when the current role has no CONNECT privilege.
|
||||||
|
PgDatabaseSizes loadDatabaseSizes(Pgsql::Connection &connection);
|
||||||
|
|
||||||
|
#endif // PGDATABASESIZES_H
|
||||||
|
|
@ -3,3 +3,5 @@ find_package(GTest CONFIG REQUIRED)
|
||||||
add_subdirectory(mygtestutils)
|
add_subdirectory(mygtestutils)
|
||||||
add_subdirectory(PgsqlTests)
|
add_subdirectory(PgsqlTests)
|
||||||
add_subdirectory(pglabtests)
|
add_subdirectory(pglabtests)
|
||||||
|
add_subdirectory(pgintegration)
|
||||||
|
add_subdirectory(integration)
|
||||||
|
|
|
||||||
46
tests/integration/CMakeLists.txt
Normal file
46
tests/integration/CMakeLists.txt
Normal file
|
|
@ -0,0 +1,46 @@
|
||||||
|
add_executable(IntegrationTests
|
||||||
|
main.cpp
|
||||||
|
tst_Smoke.cpp
|
||||||
|
tst_CatalogLoad.cpp
|
||||||
|
tst_DatabaseSizes.cpp
|
||||||
|
)
|
||||||
|
|
||||||
|
target_link_libraries(IntegrationTests PRIVATE
|
||||||
|
pgintegration
|
||||||
|
pglablib
|
||||||
|
GTest::gtest
|
||||||
|
Qt6::Core
|
||||||
|
)
|
||||||
|
|
||||||
|
target_compile_definitions(IntegrationTests PRIVATE
|
||||||
|
PGTEST_FIXTURE_DIR="${CMAKE_CURRENT_SOURCE_DIR}/fixtures"
|
||||||
|
)
|
||||||
|
|
||||||
|
set_target_properties(IntegrationTests PROPERTIES
|
||||||
|
CXX_STANDARD 20
|
||||||
|
CXX_STANDARD_REQUIRED ON
|
||||||
|
)
|
||||||
|
|
||||||
|
option(PGLAB_INTEGRATION_TESTS "Register Docker based PostgreSQL integration tests" OFF)
|
||||||
|
|
||||||
|
if (PGLAB_INTEGRATION_TESTS)
|
||||||
|
set(PGTEST_COMPOSE_FILE ${CMAKE_CURRENT_SOURCE_DIR}/docker-compose.yml)
|
||||||
|
|
||||||
|
add_test(NAME db_up
|
||||||
|
COMMAND docker compose -f ${PGTEST_COMPOSE_FILE} up -d --wait)
|
||||||
|
add_test(NAME db_down
|
||||||
|
COMMAND docker compose -f ${PGTEST_COMPOSE_FILE} down -v)
|
||||||
|
set_tests_properties(db_up PROPERTIES FIXTURES_SETUP pgcluster LABELS integration)
|
||||||
|
set_tests_properties(db_down PROPERTIES FIXTURES_CLEANUP pgcluster LABELS integration)
|
||||||
|
|
||||||
|
foreach (v 14 15 16 17 18)
|
||||||
|
add_test(NAME integration_pg${v} COMMAND IntegrationTests)
|
||||||
|
set_tests_properties(integration_pg${v} PROPERTIES
|
||||||
|
FIXTURES_REQUIRED pgcluster
|
||||||
|
LABELS integration
|
||||||
|
ENVIRONMENT "TEST_DATABASE_URL=postgresql://postgres:test@localhost:543${v}/postgres")
|
||||||
|
endforeach ()
|
||||||
|
else ()
|
||||||
|
add_test(NAME IntegrationTests COMMAND IntegrationTests)
|
||||||
|
set_tests_properties(IntegrationTests PROPERTIES LABELS integration)
|
||||||
|
endif ()
|
||||||
17
tests/integration/docker-compose.yml
Normal file
17
tests/integration/docker-compose.yml
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
x-pg: &pg
|
||||||
|
environment: { POSTGRES_PASSWORD: test }
|
||||||
|
tmpfs: [/var/lib/postgresql]
|
||||||
|
command: >
|
||||||
|
postgres -c fsync=off -c synchronous_commit=off
|
||||||
|
-c full_page_writes=off -c max_connections=200
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "pg_isready -U postgres"]
|
||||||
|
interval: 1s
|
||||||
|
retries: 30
|
||||||
|
|
||||||
|
services:
|
||||||
|
pg14: { <<: *pg, image: "postgres:14", ports: ["54314:5432"] }
|
||||||
|
pg15: { <<: *pg, image: "postgres:15", ports: ["54315:5432"] }
|
||||||
|
pg16: { <<: *pg, image: "postgres:16", ports: ["54316:5432"] }
|
||||||
|
pg17: { <<: *pg, image: "postgres:17", ports: ["54317:5432"] }
|
||||||
|
pg18: { <<: *pg, image: "postgres:18", ports: ["54318:5432"] }
|
||||||
6
tests/integration/fixtures/schema.sql
Normal file
6
tests/integration/fixtures/schema.sql
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
CREATE TABLE fixture_item (
|
||||||
|
id integer PRIMARY KEY,
|
||||||
|
name text NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
INSERT INTO fixture_item (id, name) VALUES (1, 'one'), (2, 'two');
|
||||||
10
tests/integration/main.cpp
Normal file
10
tests/integration/main.cpp
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
#include <gtest/gtest.h>
|
||||||
|
|
||||||
|
#include "PgTestEnvironment.h"
|
||||||
|
|
||||||
|
int main(int argc, char *argv[])
|
||||||
|
{
|
||||||
|
::testing::InitGoogleTest(&argc, argv);
|
||||||
|
::testing::AddGlobalTestEnvironment(new pgtest::PgEnv(PGTEST_FIXTURE_DIR));
|
||||||
|
return RUN_ALL_TESTS();
|
||||||
|
}
|
||||||
36
tests/integration/tst_CatalogLoad.cpp
Normal file
36
tests/integration/tst_CatalogLoad.cpp
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
#include "DbTest.h"
|
||||||
|
#include "PgTestEnvironment.h"
|
||||||
|
#include "catalog/PgClassContainer.h"
|
||||||
|
#include "catalog/PgDatabaseCatalog.h"
|
||||||
|
#include "Pgsql_Result.h"
|
||||||
|
|
||||||
|
using namespace pgtest;
|
||||||
|
|
||||||
|
class CatalogLoad : public DbTest {};
|
||||||
|
|
||||||
|
TEST_F(CatalogLoad, SuperuserLoadsCatalog)
|
||||||
|
{
|
||||||
|
PgDatabaseCatalog catalog;
|
||||||
|
catalog.loadAll(conn(), {});
|
||||||
|
|
||||||
|
EXPECT_EQ(catalog.serverVersion(), serverVersionNum());
|
||||||
|
EXPECT_EQ(catalog.getDBName(), dbName());
|
||||||
|
ASSERT_NE(catalog.classes(), nullptr);
|
||||||
|
EXPECT_NE(catalog.classes()->getByObjectNsAndName("public", "fixture_item"), nullptr);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(CatalogLoad, PlainRoleLoadsCatalog)
|
||||||
|
{
|
||||||
|
auto plain = plainConn();
|
||||||
|
Pgsql::Result r = plain->query("SELECT rolsuper FROM pg_roles WHERE rolname = current_user");
|
||||||
|
ASSERT_EQ(r.rows(), 1);
|
||||||
|
EXPECT_EQ(QString::fromUtf8(r.val(0, 0)), QString("f"));
|
||||||
|
|
||||||
|
PgDatabaseCatalog catalog;
|
||||||
|
catalog.loadAll(*plain, {});
|
||||||
|
|
||||||
|
EXPECT_EQ(catalog.serverVersion(), serverVersionNum());
|
||||||
|
EXPECT_EQ(catalog.getDBName(), dbName());
|
||||||
|
ASSERT_NE(catalog.classes(), nullptr);
|
||||||
|
EXPECT_NE(catalog.classes()->getByObjectNsAndName("public", "fixture_item"), nullptr);
|
||||||
|
}
|
||||||
68
tests/integration/tst_DatabaseSizes.cpp
Normal file
68
tests/integration/tst_DatabaseSizes.cpp
Normal file
|
|
@ -0,0 +1,68 @@
|
||||||
|
#include "DbTest.h"
|
||||||
|
#include "PgTestEnvironment.h"
|
||||||
|
#include "catalog/PgDatabaseSizes.h"
|
||||||
|
#include "Pgsql_Result.h"
|
||||||
|
|
||||||
|
using namespace pgtest;
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
Oid oidOfDatabase(Pgsql::Connection &c, const QString &db)
|
||||||
|
{
|
||||||
|
Pgsql::Result r = c.query(QString("SELECT oid FROM pg_database WHERE datname = '%1'").arg(db));
|
||||||
|
if (!r || r.rows() != 1)
|
||||||
|
return InvalidOid;
|
||||||
|
return QString::fromUtf8(r.val(0, 0)).toUInt();
|
||||||
|
}
|
||||||
|
|
||||||
|
const PgDatabaseSize *findSize(const PgDatabaseSizes &sizes, Oid oid)
|
||||||
|
{
|
||||||
|
for (const auto &s : sizes) {
|
||||||
|
if (s.oid == oid)
|
||||||
|
return &s;
|
||||||
|
}
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
class DatabaseSizes : public DbTest {};
|
||||||
|
|
||||||
|
TEST_F(DatabaseSizes, ConnectableDatabaseReturnsSize)
|
||||||
|
{
|
||||||
|
auto plain = plainConn();
|
||||||
|
Oid oid = oidOfDatabase(*plain, dbName());
|
||||||
|
ASSERT_NE(oid, InvalidOid);
|
||||||
|
|
||||||
|
PgDatabaseSizes sizes = loadDatabaseSizes(*plain);
|
||||||
|
const PgDatabaseSize *s = findSize(sizes, oid);
|
||||||
|
ASSERT_NE(s, nullptr);
|
||||||
|
EXPECT_GE(s->totalBytes, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(DatabaseSizes, RevokedConnectReturnsMinusOne)
|
||||||
|
{
|
||||||
|
Oid oid = oidOfDatabase(conn(), dbName());
|
||||||
|
ASSERT_NE(oid, InvalidOid);
|
||||||
|
conn().query(QString("REVOKE CONNECT ON DATABASE %1 FROM PUBLIC").arg(quoteIdent(dbName())));
|
||||||
|
|
||||||
|
auto plain = std::make_unique<Pgsql::Connection>();
|
||||||
|
plain->connect(urlForDbAsPlainRole("postgres"));
|
||||||
|
|
||||||
|
PgDatabaseSizes sizes = loadDatabaseSizes(*plain);
|
||||||
|
const PgDatabaseSize *s = findSize(sizes, oid);
|
||||||
|
ASSERT_NE(s, nullptr);
|
||||||
|
EXPECT_EQ(s->totalBytes, -1);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(DatabaseSizes, SuperuserSeesAllDatabases)
|
||||||
|
{
|
||||||
|
PgDatabaseSizes sizes = loadDatabaseSizes(conn());
|
||||||
|
|
||||||
|
Pgsql::Result r = conn().query("SELECT count(*) FROM pg_database");
|
||||||
|
ASSERT_EQ(r.rows(), 1);
|
||||||
|
EXPECT_EQ(static_cast<size_t>(QString::fromUtf8(r.val(0, 0)).toULongLong()), sizes.size());
|
||||||
|
|
||||||
|
for (const auto &s : sizes)
|
||||||
|
EXPECT_GE(s.totalBytes, 0) << "database oid " << s.oid;
|
||||||
|
}
|
||||||
63
tests/integration/tst_Smoke.cpp
Normal file
63
tests/integration/tst_Smoke.cpp
Normal file
|
|
@ -0,0 +1,63 @@
|
||||||
|
#include "DbTest.h"
|
||||||
|
#include "PgTestEnvironment.h"
|
||||||
|
#include "Pgsql_Result.h"
|
||||||
|
|
||||||
|
using namespace pgtest;
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
int scalarInt(Pgsql::Connection &c, const char *sql)
|
||||||
|
{
|
||||||
|
Pgsql::Result r = c.query(sql);
|
||||||
|
EXPECT_EQ(r.rows(), 1);
|
||||||
|
return QString::fromUtf8(r.val(0, 0)).toInt();
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
class Smoke : public DbTest {};
|
||||||
|
|
||||||
|
TEST_F(Smoke, SelectOne)
|
||||||
|
{
|
||||||
|
EXPECT_EQ(scalarInt(conn(), "SELECT 1"), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(Smoke, RunsOnClonedDatabase)
|
||||||
|
{
|
||||||
|
Pgsql::Result r = conn().query("SELECT current_database()");
|
||||||
|
ASSERT_EQ(r.rows(), 1);
|
||||||
|
EXPECT_EQ(QString::fromUtf8(r.val(0, 0)), dbName());
|
||||||
|
EXPECT_TRUE(dbName().startsWith("t_"));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(Smoke, ServerVersionAtLeast14)
|
||||||
|
{
|
||||||
|
EXPECT_GE(serverVersionNum(), 140000);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(Smoke, FixtureSchemaPresent)
|
||||||
|
{
|
||||||
|
EXPECT_EQ(scalarInt(conn(), "SELECT count(*) FROM fixture_item"), 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
// The two tests below each modify the database and expect the pristine
|
||||||
|
// fixture state, in whichever order they run.
|
||||||
|
TEST_F(Smoke, IsolationA)
|
||||||
|
{
|
||||||
|
EXPECT_EQ(scalarInt(conn(), "SELECT count(*) FROM fixture_item"), 2);
|
||||||
|
conn().query("INSERT INTO fixture_item VALUES (100, 'a')");
|
||||||
|
EXPECT_EQ(scalarInt(conn(), "SELECT count(*) FROM fixture_item"), 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(Smoke, IsolationB)
|
||||||
|
{
|
||||||
|
EXPECT_EQ(scalarInt(conn(), "SELECT count(*) FROM fixture_item"), 2);
|
||||||
|
conn().query("DELETE FROM fixture_item");
|
||||||
|
EXPECT_EQ(scalarInt(conn(), "SELECT count(*) FROM fixture_item"), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(Smoke, VersionGate)
|
||||||
|
{
|
||||||
|
REQUIRE_PG_AT_LEAST(15);
|
||||||
|
EXPECT_GE(serverVersionNum(), 150000);
|
||||||
|
}
|
||||||
21
tests/pgintegration/CMakeLists.txt
Normal file
21
tests/pgintegration/CMakeLists.txt
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
add_library(pgintegration STATIC
|
||||||
|
PgTestEnvironment.h
|
||||||
|
PgTestEnvironment.cpp
|
||||||
|
DbTest.h
|
||||||
|
DbTest.cpp
|
||||||
|
)
|
||||||
|
|
||||||
|
target_link_libraries(pgintegration PUBLIC
|
||||||
|
pgsql
|
||||||
|
GTest::gtest
|
||||||
|
Qt6::Core
|
||||||
|
)
|
||||||
|
|
||||||
|
target_include_directories(pgintegration PUBLIC
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}
|
||||||
|
)
|
||||||
|
|
||||||
|
set_target_properties(pgintegration PROPERTIES
|
||||||
|
CXX_STANDARD 20
|
||||||
|
CXX_STANDARD_REQUIRED ON
|
||||||
|
)
|
||||||
62
tests/pgintegration/DbTest.cpp
Normal file
62
tests/pgintegration/DbTest.cpp
Normal file
|
|
@ -0,0 +1,62 @@
|
||||||
|
#include "DbTest.h"
|
||||||
|
#include "PgTestEnvironment.h"
|
||||||
|
|
||||||
|
#include <atomic>
|
||||||
|
#include <stdexcept>
|
||||||
|
|
||||||
|
namespace pgtest {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
std::atomic<int> g_counter{0};
|
||||||
|
}
|
||||||
|
|
||||||
|
QString DbTest::dbUrl() const
|
||||||
|
{
|
||||||
|
return urlForDb(dbName_);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::unique_ptr<Pgsql::Connection> DbTest::plainConn() const
|
||||||
|
{
|
||||||
|
auto c = std::make_unique<Pgsql::Connection>();
|
||||||
|
c->connect(urlForDbAsPlainRole(dbName_));
|
||||||
|
return c;
|
||||||
|
}
|
||||||
|
|
||||||
|
void DbTest::SetUp()
|
||||||
|
{
|
||||||
|
if (!available())
|
||||||
|
GTEST_SKIP() << "TEST_DATABASE_URL not set or template database unavailable";
|
||||||
|
|
||||||
|
dbName_ = QString("t_%1_%2").arg(g_counter++).arg(processId());
|
||||||
|
try {
|
||||||
|
{
|
||||||
|
Pgsql::Connection admin;
|
||||||
|
admin.connect(*baseUrl());
|
||||||
|
admin.query(QString("CREATE DATABASE %1 TEMPLATE %2")
|
||||||
|
.arg(quoteIdent(dbName_), quoteIdent(templateDbName())));
|
||||||
|
}
|
||||||
|
conn_ = std::make_unique<Pgsql::Connection>();
|
||||||
|
conn_->connect(dbUrl());
|
||||||
|
}
|
||||||
|
catch (const std::exception &e) {
|
||||||
|
FAIL() << "Creating test database failed: " << e.what();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void DbTest::TearDown()
|
||||||
|
{
|
||||||
|
conn_.reset();
|
||||||
|
if (dbName_.isEmpty())
|
||||||
|
return;
|
||||||
|
try {
|
||||||
|
Pgsql::Connection admin;
|
||||||
|
admin.connect(*baseUrl());
|
||||||
|
admin.query(QString("DROP DATABASE IF EXISTS %1 WITH (FORCE)").arg(quoteIdent(dbName_)));
|
||||||
|
}
|
||||||
|
catch (const std::exception &e) {
|
||||||
|
ADD_FAILURE() << "Dropping test database failed: " << e.what();
|
||||||
|
}
|
||||||
|
dbName_.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace pgtest
|
||||||
28
tests/pgintegration/DbTest.h
Normal file
28
tests/pgintegration/DbTest.h
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <gtest/gtest.h>
|
||||||
|
#include <memory>
|
||||||
|
#include <QString>
|
||||||
|
|
||||||
|
#include "Pgsql_Connection.h"
|
||||||
|
|
||||||
|
namespace pgtest {
|
||||||
|
|
||||||
|
/// Fixture giving every test its own database cloned from the template.
|
||||||
|
class DbTest : public ::testing::Test {
|
||||||
|
protected:
|
||||||
|
void SetUp() override;
|
||||||
|
void TearDown() override;
|
||||||
|
|
||||||
|
Pgsql::Connection& conn() { return *conn_; }
|
||||||
|
QString dbName() const { return dbName_; }
|
||||||
|
QString dbUrl() const;
|
||||||
|
/// Opens a new connection to this test's database as the plain (non-superuser) role.
|
||||||
|
std::unique_ptr<Pgsql::Connection> plainConn() const;
|
||||||
|
|
||||||
|
private:
|
||||||
|
QString dbName_;
|
||||||
|
std::unique_ptr<Pgsql::Connection> conn_;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace pgtest
|
||||||
149
tests/pgintegration/PgTestEnvironment.cpp
Normal file
149
tests/pgintegration/PgTestEnvironment.cpp
Normal file
|
|
@ -0,0 +1,149 @@
|
||||||
|
#include "PgTestEnvironment.h"
|
||||||
|
|
||||||
|
#include <QCoreApplication>
|
||||||
|
#include <QFile>
|
||||||
|
#include <QUrl>
|
||||||
|
#include <iostream>
|
||||||
|
#include <stdexcept>
|
||||||
|
|
||||||
|
#include "Pgsql_Connection.h"
|
||||||
|
#include "Pgsql_Result.h"
|
||||||
|
|
||||||
|
namespace pgtest {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
bool g_available = false;
|
||||||
|
int g_versionNum = 0;
|
||||||
|
|
||||||
|
const char *kPlainRolePassword = "pglab_plain_test";
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
std::optional<QString> baseUrl()
|
||||||
|
{
|
||||||
|
QByteArray v = qgetenv("TEST_DATABASE_URL");
|
||||||
|
if (v.isEmpty())
|
||||||
|
return std::nullopt;
|
||||||
|
return QString::fromUtf8(v);
|
||||||
|
}
|
||||||
|
|
||||||
|
QString urlForDb(const QString &db)
|
||||||
|
{
|
||||||
|
auto base = baseUrl();
|
||||||
|
if (!base)
|
||||||
|
throw std::runtime_error("TEST_DATABASE_URL is not set");
|
||||||
|
QUrl url(*base);
|
||||||
|
url.setPath("/" + db);
|
||||||
|
return url.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool available()
|
||||||
|
{
|
||||||
|
return g_available;
|
||||||
|
}
|
||||||
|
|
||||||
|
int serverVersionNum()
|
||||||
|
{
|
||||||
|
return g_versionNum;
|
||||||
|
}
|
||||||
|
|
||||||
|
qint64 processId()
|
||||||
|
{
|
||||||
|
return QCoreApplication::applicationPid();
|
||||||
|
}
|
||||||
|
|
||||||
|
QString templateDbName()
|
||||||
|
{
|
||||||
|
return QString("tmpl_%1").arg(processId());
|
||||||
|
}
|
||||||
|
|
||||||
|
QString plainRoleName()
|
||||||
|
{
|
||||||
|
return QString("tst_%1").arg(processId());
|
||||||
|
}
|
||||||
|
|
||||||
|
QString urlForDbAsPlainRole(const QString &db)
|
||||||
|
{
|
||||||
|
auto base = baseUrl();
|
||||||
|
if (!base)
|
||||||
|
throw std::runtime_error("TEST_DATABASE_URL is not set");
|
||||||
|
QUrl url(*base);
|
||||||
|
url.setPath("/" + db);
|
||||||
|
url.setUserName(plainRoleName());
|
||||||
|
url.setPassword(QString::fromUtf8(kPlainRolePassword));
|
||||||
|
return url.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
QString quoteIdent(const QString &name)
|
||||||
|
{
|
||||||
|
QString r = name;
|
||||||
|
r.replace('"', "\"\"");
|
||||||
|
return "\"" + r + "\"";
|
||||||
|
}
|
||||||
|
|
||||||
|
void PgEnv::SetUp()
|
||||||
|
{
|
||||||
|
auto base = baseUrl();
|
||||||
|
if (!base) {
|
||||||
|
std::cerr << "TEST_DATABASE_URL not set, integration tests will be skipped\n";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const QString tmpl = templateDbName();
|
||||||
|
try {
|
||||||
|
{
|
||||||
|
Pgsql::Connection admin;
|
||||||
|
admin.connect(*base);
|
||||||
|
Pgsql::Result r = admin.query("SHOW server_version_num");
|
||||||
|
if (!r || r.rows() != 1)
|
||||||
|
throw std::runtime_error("could not query server_version_num");
|
||||||
|
g_versionNum = QString::fromUtf8(r.val(0, 0)).toInt();
|
||||||
|
admin.query(QString("DROP DATABASE IF EXISTS %1 WITH (FORCE)").arg(quoteIdent(tmpl)));
|
||||||
|
admin.query(QString("CREATE DATABASE %1").arg(quoteIdent(tmpl)));
|
||||||
|
admin.query(QString("DROP ROLE IF EXISTS %1").arg(quoteIdent(plainRoleName())));
|
||||||
|
admin.query(QString("CREATE ROLE %1 LOGIN NOSUPERUSER NOCREATEDB NOCREATEROLE PASSWORD '%2'")
|
||||||
|
.arg(quoteIdent(plainRoleName()), QString::fromUtf8(kPlainRolePassword)));
|
||||||
|
}
|
||||||
|
{
|
||||||
|
QFile f(fixtureDir_ + "/schema.sql");
|
||||||
|
if (!f.open(QIODevice::ReadOnly))
|
||||||
|
throw std::runtime_error("cannot open " + f.fileName().toStdString());
|
||||||
|
QByteArray sql = f.readAll();
|
||||||
|
|
||||||
|
Pgsql::Connection c;
|
||||||
|
c.connect(urlForDb(tmpl));
|
||||||
|
c.query(sql.constData());
|
||||||
|
}
|
||||||
|
g_available = true;
|
||||||
|
}
|
||||||
|
catch (const std::exception &e) {
|
||||||
|
ADD_FAILURE() << "Setting up template database failed: " << e.what();
|
||||||
|
try {
|
||||||
|
Pgsql::Connection admin;
|
||||||
|
admin.connect(*base);
|
||||||
|
admin.query(QString("DROP DATABASE IF EXISTS %1 WITH (FORCE)").arg(quoteIdent(tmpl)));
|
||||||
|
admin.query(QString("DROP ROLE IF EXISTS %1").arg(quoteIdent(plainRoleName())));
|
||||||
|
}
|
||||||
|
catch (const std::exception &) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void PgEnv::TearDown()
|
||||||
|
{
|
||||||
|
if (!g_available)
|
||||||
|
return;
|
||||||
|
try {
|
||||||
|
Pgsql::Connection admin;
|
||||||
|
admin.connect(*baseUrl());
|
||||||
|
admin.query(QString("DROP DATABASE IF EXISTS %1 WITH (FORCE)").arg(quoteIdent(templateDbName())));
|
||||||
|
admin.query(QString("DROP ROLE IF EXISTS %1").arg(quoteIdent(plainRoleName())));
|
||||||
|
}
|
||||||
|
catch (const std::exception &e) {
|
||||||
|
ADD_FAILURE() << "Dropping template database or plain role failed: " << e.what();
|
||||||
|
}
|
||||||
|
g_available = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace pgtest
|
||||||
52
tests/pgintegration/PgTestEnvironment.h
Normal file
52
tests/pgintegration/PgTestEnvironment.h
Normal file
|
|
@ -0,0 +1,52 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <gtest/gtest.h>
|
||||||
|
#include <optional>
|
||||||
|
#include <QString>
|
||||||
|
|
||||||
|
namespace pgtest {
|
||||||
|
|
||||||
|
/// Value of the TEST_DATABASE_URL environment variable, if set and non-empty.
|
||||||
|
std::optional<QString> baseUrl();
|
||||||
|
|
||||||
|
/// Same as baseUrl() but with the database name replaced.
|
||||||
|
QString urlForDb(const QString &db);
|
||||||
|
|
||||||
|
/// True when a server is configured and the template database was created.
|
||||||
|
bool available();
|
||||||
|
|
||||||
|
/// server_version_num of the server under test (0 when unavailable).
|
||||||
|
int serverVersionNum();
|
||||||
|
|
||||||
|
/// Portable process id.
|
||||||
|
qint64 processId();
|
||||||
|
|
||||||
|
/// Name of the template database for this process.
|
||||||
|
QString templateDbName();
|
||||||
|
|
||||||
|
/// Name of the login role without superuser rights, unique per process.
|
||||||
|
QString plainRoleName();
|
||||||
|
|
||||||
|
/// Same as urlForDb() but with the plain role as user.
|
||||||
|
QString urlForDbAsPlainRole(const QString &db);
|
||||||
|
|
||||||
|
/// Quotes an identifier for use in SQL.
|
||||||
|
QString quoteIdent(const QString &name);
|
||||||
|
|
||||||
|
/// Global environment, register it in main():
|
||||||
|
/// ::testing::AddGlobalTestEnvironment(new pgtest::PgEnv(PGTEST_FIXTURE_DIR));
|
||||||
|
/// Creates a template database from <fixtureDir>/schema.sql once per process.
|
||||||
|
class PgEnv : public ::testing::Environment {
|
||||||
|
public:
|
||||||
|
explicit PgEnv(QString fixtureDir) : fixtureDir_(std::move(fixtureDir)) {}
|
||||||
|
void SetUp() override;
|
||||||
|
void TearDown() override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
QString fixtureDir_;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace pgtest
|
||||||
|
|
||||||
|
#define REQUIRE_PG_AT_LEAST(v) \
|
||||||
|
if (pgtest::serverVersionNum() < (v) * 10000) GTEST_SKIP() << "needs PG " #v
|
||||||
Loading…
Add table
Add a link
Reference in a new issue