Fix some small issues #93

Merged
eelke merged 3 commits from fix-Expected into main 2026-10-19 15:06:32 +00:00
6 changed files with 424 additions and 0 deletions
Showing only changes of commit 82cc9f14bc - Show all commits

View file

@ -3,6 +3,19 @@ add_executable(IntegrationTests
tst_Smoke.cpp
tst_CatalogLoad.cpp
tst_DatabaseSizes.cpp
tst_DataGridMapping.cpp
tst_DataGridUpdate.cpp
${CMAKE_SOURCE_DIR}/pglab/crud/CrudModel.cpp
${CMAKE_SOURCE_DIR}/pglab/crud/DataGridWidget.cpp
${CMAKE_SOURCE_DIR}/pglab/OpenDatabase.cpp
${CMAKE_SOURCE_DIR}/pglab/util/PgLabItemDelegate.cpp
${CMAKE_SOURCE_DIR}/pglab/util/Colors.cpp
${CMAKE_SOURCE_DIR}/pglab/ResultTableModelUtil.cpp
)
target_include_directories(IntegrationTests PRIVATE
${CMAKE_SOURCE_DIR}/pglab
${CMAKE_SOURCE_DIR}/pglab/crud
)
target_link_libraries(IntegrationTests PRIVATE
@ -10,6 +23,10 @@ target_link_libraries(IntegrationTests PRIVATE
pglablib
GTest::gtest
Qt6::Core
Qt6::Gui
Qt6::Widgets
Qt6::Sql
Qt6::Concurrent
)
target_compile_definitions(IntegrationTests PRIVATE

View file

@ -0,0 +1,72 @@
#pragma once
#include <gtest/gtest.h>
#include "DbTest.h"
#include "ConnectionConfig.h"
#include "OpenDatabase.h"
#include "crud/CrudModel.h"
#include "crud/DataGridWidget.h"
#include "Pgsql_Connection.h"
#include <QSortFilterProxyModel>
#include <QUrl>
#include <memory>
#include <optional>
#include <string>
#include <vector>
class DataGridTest : public pgtest::DbTest {
protected:
std::shared_ptr<OpenDatabase> openDb()
{
QUrl url(dbUrl());
ConnectionConfig cfg;
cfg.setHost(url.host());
cfg.setPort(static_cast<unsigned short>(url.port(5432)));
cfg.setUser(url.userName());
cfg.setPassword(url.password());
cfg.setDbname(url.path().mid(1));
return OpenDatabase::createOpenDatabase(cfg);
}
/// Runs the query and shows its result in a grid
void showQuery(const char *sql)
{
db = openDb();
auto result = std::make_shared<Pgsql::Result>(conn().query(sql));
model = std::make_unique<CrudModel>();
widget = std::make_unique<DataGridWidget>();
widget->setSourceModel(model.get());
model->setResult(db, result);
}
/// Editable flag of every result column, as seen through the widget's proxy
std::vector<bool> editableColumns()
{
auto proxy = widget->proxy();
std::vector<bool> r;
for (int c = 1; c < proxy->columnCount(); ++c)
r.push_back(proxy->flags(proxy->index(0, c)) & Qt::ItemIsEditable);
return r;
}
QString columnName(int resultCol)
{
return widget->proxy()->headerData(resultCol + 1, Qt::Horizontal).toString().section('\n', 0, 0);
}
/// Reads a single cell with the test's own connection
std::optional<std::string> dbValue(const char *sql)
{
auto res = conn().query(sql);
auto v = res.get(0, 0);
if (v.null())
return std::nullopt;
return std::string(v.c_str());
}
std::shared_ptr<OpenDatabase> db;
std::unique_ptr<CrudModel> model;
std::unique_ptr<DataGridWidget> widget;
};

View file

@ -4,3 +4,30 @@ CREATE TABLE fixture_item (
);
INSERT INTO fixture_item (id, name) VALUES (1, 'one'), (2, 'two');
CREATE TABLE grid_parent (
id integer PRIMARY KEY,
name text NOT NULL
);
INSERT INTO grid_parent (id, name) VALUES (1, 'alpha'), (2, 'beta'), (3, 'gamma');
CREATE TABLE grid_child (
id integer PRIMARY KEY,
parent_id integer,
note text
);
INSERT INTO grid_child (id, parent_id, note) VALUES (10, 1, 'c1'), (11, 2, 'c2'), (12, 3, 'c3');
CREATE TABLE grid_composite (
a integer,
b integer,
val text,
PRIMARY KEY (a, b)
);
INSERT INTO grid_composite (a, b, val) VALUES (1, 1, 'x'), (1, 2, 'y'), (2, 1, 'z');
CREATE TABLE grid_nopk (
x integer,
y text
);
INSERT INTO grid_nopk (x, y) VALUES (1, 'a'), (2, 'b');

View file

@ -1,9 +1,15 @@
#include <gtest/gtest.h>
#include <QApplication>
#include <QtGlobal>
#include "PgTestEnvironment.h"
int main(int argc, char *argv[])
{
if (!qEnvironmentVariableIsSet("QT_QPA_PLATFORM"))
qputenv("QT_QPA_PLATFORM", "offscreen");
QApplication app(argc, argv);
::testing::InitGoogleTest(&argc, argv);
::testing::AddGlobalTestEnvironment(new pgtest::PgEnv(PGTEST_FIXTURE_DIR));
return RUN_ALL_TESTS();

View file

@ -0,0 +1,112 @@
#include <gtest/gtest.h>
#include "catalog/PgClassContainer.h"
#include "catalog/PgDatabaseCatalog.h"
#include <QCoreApplication>
#include <QDeadlineTimer>
#include <QSortFilterProxyModel>
#include <QEventLoop>
#include "DataGridTest.h"
TEST_F(DataGridTest, SingleTableLoadsRows)
{
showQuery("SELECT id, name FROM grid_parent ORDER BY id");
EXPECT_EQ(model->rowCount(), 3);
ASSERT_NE(widget->proxy(), nullptr);
EXPECT_EQ(widget->proxy()->rowCount(), 3);
EXPECT_FALSE(model->hasPendingChanges());
}
TEST_F(DataGridTest, RowNumberColumnIsNeverEditable)
{
showQuery("SELECT id, name FROM grid_parent");
EXPECT_FALSE(widget->proxy()->flags(widget->proxy()->index(0, 0)) & Qt::ItemIsEditable);
}
TEST_F(DataGridTest, SingleTableAllColumnsEditable)
{
showQuery("SELECT id, name FROM grid_parent");
EXPECT_EQ(editableColumns(), (std::vector<bool>{true, true}));
EXPECT_EQ(columnName(0), "id");
EXPECT_EQ(columnName(1), "name");
}
TEST_F(DataGridTest, SingleTableSubsetWithPkIsEditable)
{
showQuery("SELECT name, id FROM grid_parent");
EXPECT_EQ(editableColumns(), (std::vector<bool>{true, true}));
EXPECT_EQ(columnName(0), "name");
}
TEST_F(DataGridTest, JoinOfTwoTablesWithPksIsEditable)
{
showQuery("SELECT p.id, p.name, c.id AS child_id, c.note FROM grid_parent p JOIN grid_child c ON c.parent_id = p.id");
EXPECT_EQ(editableColumns(), (std::vector<bool>{true, true, true, true}));
EXPECT_EQ(columnName(2), "child_id");
}
TEST_F(DataGridTest, JoinWithoutOnePkOnlyOtherTableEditable)
{
showQuery("SELECT p.id, p.name, c.note FROM grid_parent p JOIN grid_child c ON c.parent_id = p.id");
EXPECT_EQ(editableColumns(), (std::vector<bool>{true, true, false}));
}
TEST_F(DataGridTest, AliasedColumnStaysEditable)
{
showQuery("SELECT id, name AS title FROM grid_parent");
EXPECT_EQ(editableColumns(), (std::vector<bool>{true, true}));
EXPECT_EQ(columnName(1), "title");
}
TEST_F(DataGridTest, ExpressionColumnIsReadOnly)
{
showQuery("SELECT id, name, upper(name) AS shout, id + 1 AS next_id FROM grid_parent");
EXPECT_EQ(editableColumns(), (std::vector<bool>{true, true, false, false}));
}
TEST_F(DataGridTest, TableWithoutPkIsReadOnly)
{
showQuery("SELECT x, y FROM grid_nopk");
EXPECT_EQ(editableColumns(), (std::vector<bool>{false, false}));
}
TEST_F(DataGridTest, PkNotSelectedIsReadOnly)
{
showQuery("SELECT name FROM grid_parent");
EXPECT_EQ(editableColumns(), (std::vector<bool>{false}));
}
TEST_F(DataGridTest, CompositePkFullyPresentIsEditable)
{
showQuery("SELECT a, b, val FROM grid_composite");
EXPECT_EQ(editableColumns(), (std::vector<bool>{true, true, true}));
}
TEST_F(DataGridTest, CompositePkPartiallyPresentIsReadOnly)
{
showQuery("SELECT a, val FROM grid_composite");
EXPECT_EQ(editableColumns(), (std::vector<bool>{false, false}));
}
TEST_F(DataGridTest, WholeTableViaSetConfigLoadsAsynchronously)
{
db = openDb();
auto cls = db->catalog()->classes()->getByName("grid_parent");
ASSERT_NE(cls, nullptr);
model = std::make_unique<CrudModel>();
widget = std::make_unique<DataGridWidget>();
widget->setSourceModel(model.get());
model->setConfig(db, *cls);
model->loadData();
QDeadlineTimer deadline(10000);
while (model->rowCount() < 4 && !deadline.hasExpired())
QCoreApplication::processEvents(QEventLoop::AllEvents, 50);
// the whole table view appends an empty row for inserting
ASSERT_EQ(model->rowCount(), 4);
EXPECT_EQ(editableColumns(), (std::vector<bool>{true, true}));
}

View file

@ -0,0 +1,190 @@
#include "DataGridTest.h"
#include <QAction>
#include <QApplication>
#include <QStandardItemModel>
#include <QTimer>
#include <QToolBar>
namespace {
/// Failed saves show a modal message box, this closes it so the test can continue
void autoCloseModalDialogs(int triesLeft = 100)
{
if (auto w = QApplication::activeModalWidget())
w->close();
else if (triesLeft > 0)
QTimer::singleShot(20, [triesLeft] { autoCloseModalDialogs(triesLeft - 1); });
}
/// Records the values of CrudModel::pendingChangesChanged
struct PendingSpy {
explicit PendingSpy(CrudModel *m)
{
QObject::connect(m, &CrudModel::pendingChangesChanged, m, [this](bool b) { values.push_back(b); });
}
std::vector<bool> values;
};
class DataGridUpdateTest : public DataGridTest {
protected:
void edit(int proxyRow, int resultCol, const QString &value)
{
auto proxy = widget->proxy();
ASSERT_TRUE(proxy->setData(proxy->index(proxyRow, resultCol + 1), value, Qt::EditRole));
}
QAction *action(const QString &text)
{
for (auto a : widget->findChildren<QAction *>())
if (a->text() == text)
return a;
return nullptr;
}
};
} // namespace
TEST_F(DataGridUpdateTest, EditSingleCellAndSave)
{
showQuery("SELECT id, name FROM grid_parent");
edit(0, 1, "renamed");
EXPECT_TRUE(model->hasPendingChanges());
action("Save")->trigger();
EXPECT_FALSE(model->hasPendingChanges());
EXPECT_EQ(dbValue("SELECT name FROM grid_parent WHERE id = 1"), "renamed");
EXPECT_EQ(dbValue("SELECT name FROM grid_parent WHERE id = 2"), "beta");
}
TEST_F(DataGridUpdateTest, EditSeveralColumnsAndRows)
{
showQuery("SELECT id, parent_id, note FROM grid_child");
edit(0, 1, "3");
edit(0, 2, "first");
edit(2, 2, "last");
action("Save")->trigger();
EXPECT_FALSE(model->hasPendingChanges());
EXPECT_EQ(dbValue("SELECT parent_id FROM grid_child WHERE id = 10"), "3");
EXPECT_EQ(dbValue("SELECT note FROM grid_child WHERE id = 10"), "first");
EXPECT_EQ(dbValue("SELECT note FROM grid_child WHERE id = 11"), "c2");
EXPECT_EQ(dbValue("SELECT note FROM grid_child WHERE id = 12"), "last");
}
TEST_F(DataGridUpdateTest, EmptyValueIsSavedAsNull)
{
showQuery("SELECT id, note FROM grid_child");
edit(1, 1, "");
action("Save")->trigger();
EXPECT_EQ(dbValue("SELECT note FROM grid_child WHERE id = 11"), std::nullopt);
EXPECT_EQ(dbValue("SELECT note FROM grid_child WHERE id = 10"), "c1");
}
TEST_F(DataGridUpdateTest, JoinUpdatesTheOwningTable)
{
showQuery("SELECT p.id, p.name, c.id AS child_id, c.note FROM grid_parent p JOIN grid_child c ON c.parent_id = p.id");
edit(0, 1, "parent edited");
edit(0, 3, "child edited");
action("Save")->trigger();
EXPECT_FALSE(model->hasPendingChanges());
EXPECT_EQ(dbValue("SELECT name FROM grid_parent WHERE id = 1"), "parent edited");
EXPECT_EQ(dbValue("SELECT note FROM grid_child WHERE id = 10"), "child edited");
EXPECT_EQ(dbValue("SELECT name FROM grid_parent WHERE id = 2"), "beta");
EXPECT_EQ(dbValue("SELECT note FROM grid_child WHERE id = 11"), "c2");
}
TEST_F(DataGridUpdateTest, CompositePkUpdatesOnlyMatchingRow)
{
showQuery("SELECT a, b, val FROM grid_composite");
edit(1, 2, "changed");
action("Save")->trigger();
EXPECT_EQ(dbValue("SELECT val FROM grid_composite WHERE a = 1 AND b = 2"), "changed");
EXPECT_EQ(dbValue("SELECT val FROM grid_composite WHERE a = 1 AND b = 1"), "x");
EXPECT_EQ(dbValue("SELECT val FROM grid_composite WHERE a = 2 AND b = 1"), "z");
}
TEST_F(DataGridUpdateTest, EditAfterSortingUpdatesTheShownRow)
{
showQuery("SELECT id, name FROM grid_parent");
widget->proxy()->sort(2, Qt::DescendingOrder);
auto proxy = widget->proxy();
ASSERT_EQ(proxy->index(0, 1).data(Qt::DisplayRole).toString(), "3");
ASSERT_EQ(proxy->index(0, 2).data(Qt::DisplayRole).toString(), "gamma");
edit(0, 1, "sorted edit");
action("Save")->trigger();
EXPECT_EQ(dbValue("SELECT name FROM grid_parent WHERE id = 3"), "sorted edit");
EXPECT_EQ(dbValue("SELECT name FROM grid_parent WHERE id = 1"), "alpha");
}
TEST_F(DataGridUpdateTest, SaveAndRevertFollowPendingChanges)
{
showQuery("SELECT id, name FROM grid_parent");
auto save = action("Save");
auto revert = action("Revert");
ASSERT_NE(save, nullptr);
ASSERT_NE(revert, nullptr);
EXPECT_FALSE(save->isEnabled());
EXPECT_FALSE(revert->isEnabled());
PendingSpy spy(model.get());
edit(0, 1, "x");
EXPECT_TRUE(save->isEnabled());
EXPECT_TRUE(revert->isEnabled());
EXPECT_EQ(spy.values, (std::vector<bool>{true}));
save->trigger();
EXPECT_FALSE(save->isEnabled());
EXPECT_FALSE(revert->isEnabled());
EXPECT_EQ(spy.values, (std::vector<bool>{true, false}));
}
TEST_F(DataGridUpdateTest, RevertDiscardsEdits)
{
showQuery("SELECT id, name FROM grid_parent");
edit(0, 1, "discard me");
EXPECT_EQ(widget->proxy()->index(0, 2).data(Qt::EditRole).toString(), "discard me");
PendingSpy spy(model.get());
action("Revert")->trigger();
EXPECT_FALSE(model->hasPendingChanges());
EXPECT_FALSE(action("Save")->isEnabled());
EXPECT_EQ(spy.values, (std::vector<bool>{false}));
EXPECT_EQ(widget->proxy()->index(0, 2).data(Qt::EditRole).toString(), "alpha");
EXPECT_EQ(dbValue("SELECT name FROM grid_parent WHERE id = 1"), "alpha");
}
TEST_F(DataGridUpdateTest, FailedSaveKeepsRowPending)
{
showQuery("SELECT id, name FROM grid_parent");
edit(0, 1, ""); // NULL violates NOT NULL on name
QTimer::singleShot(0, [] { autoCloseModalDialogs(); });
action("Save")->trigger();
EXPECT_TRUE(model->hasPendingChanges());
EXPECT_TRUE(action("Save")->isEnabled());
EXPECT_EQ(dbValue("SELECT name FROM grid_parent WHERE id = 1"), "alpha");
}
TEST_F(DataGridUpdateTest, ToolbarHiddenForNonCrudModel)
{
showQuery("SELECT id, name FROM grid_parent");
auto toolbar = widget->findChild<QToolBar *>();
ASSERT_NE(toolbar, nullptr);
EXPECT_FALSE(toolbar->isHidden());
QStandardItemModel other(1, 1);
widget->setSourceModel(&other);
EXPECT_TRUE(toolbar->isHidden());
EXPECT_FALSE(action("Save")->isEnabled());
widget->setSourceModel(model.get());
EXPECT_FALSE(toolbar->isHidden());
widget->setSourceModel(nullptr);
}