Add framework for executing tests against multiple postgresql versions using docker compose.
This commit is contained in:
parent
655c6fceb1
commit
c3bf9e88e4
13 changed files with 457 additions and 0 deletions
|
|
@ -3,3 +3,5 @@ find_package(GTest CONFIG REQUIRED)
|
|||
add_subdirectory(mygtestutils)
|
||||
add_subdirectory(PgsqlTests)
|
||||
add_subdirectory(pglabtests)
|
||||
add_subdirectory(pgintegration)
|
||||
add_subdirectory(integration)
|
||||
|
|
|
|||
43
tests/integration/CMakeLists.txt
Normal file
43
tests/integration/CMakeLists.txt
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
add_executable(IntegrationTests
|
||||
main.cpp
|
||||
tst_Smoke.cpp
|
||||
)
|
||||
|
||||
target_link_libraries(IntegrationTests PRIVATE
|
||||
pgintegration
|
||||
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)
|
||||
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 ()
|
||||
16
tests/integration/docker-compose.yml
Normal file
16
tests/integration/docker-compose.yml
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
x-pg: &pg
|
||||
environment: { POSTGRES_PASSWORD: test }
|
||||
tmpfs: [/var/lib/postgresql/data]
|
||||
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"] }
|
||||
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();
|
||||
}
|
||||
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
|
||||
)
|
||||
55
tests/pgintegration/DbTest.cpp
Normal file
55
tests/pgintegration/DbTest.cpp
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
#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_);
|
||||
}
|
||||
|
||||
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
|
||||
26
tests/pgintegration/DbTest.h
Normal file
26
tests/pgintegration/DbTest.h
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
#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;
|
||||
|
||||
private:
|
||||
QString dbName_;
|
||||
std::unique_ptr<Pgsql::Connection> conn_;
|
||||
};
|
||||
|
||||
} // namespace pgtest
|
||||
125
tests/pgintegration/PgTestEnvironment.cpp
Normal file
125
tests/pgintegration/PgTestEnvironment.cpp
Normal file
|
|
@ -0,0 +1,125 @@
|
|||
#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;
|
||||
|
||||
} // 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 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)));
|
||||
}
|
||||
{
|
||||
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)));
|
||||
}
|
||||
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())));
|
||||
}
|
||||
catch (const std::exception &e) {
|
||||
ADD_FAILURE() << "Dropping template database failed: " << e.what();
|
||||
}
|
||||
g_available = false;
|
||||
}
|
||||
|
||||
} // namespace pgtest
|
||||
46
tests/pgintegration/PgTestEnvironment.h
Normal file
46
tests/pgintegration/PgTestEnvironment.h
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
#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();
|
||||
|
||||
/// 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