Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/sqlitedb.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -742,6 +742,7 @@ bool DBBrowserDB::create ( const QString & db)
// Close database and open it through the code for opening existing database files. This is slightly less efficient but saves us some duplicate
// code.
sqlite3_close_v2(_db);
_db = nullptr; // open() would try to close the handle again if it thinks the database is still open
return open(db);
} else {
return false;
Expand Down
19 changes: 18 additions & 1 deletion src/sqlitetablemodel.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -509,9 +509,26 @@ bool SqliteTableModel::setTypedData(const QModelIndex& index, bool isBlob, const

if(index.isValid() && role == Qt::EditRole)
{
// The cache is sparse: rows beyond the chunk loaded so far are valid model rows but not cached yet (e.g. when pasting a large
// clipboard). Load the row first, like find() does, instead of letting at() throw.
const size_t row = static_cast<size_t>(index.row());
bool cached;
{
std::unique_lock<std::mutex> lock(m_mutexDataCache);
cached = m_cache.count(row);
}
if(!cached)
{
triggerCacheLoad(index.row());
waitUntilIdle();
}

std::unique_lock<std::mutex> lock(m_mutexDataCache);

auto & cached_row = m_cache.at(static_cast<size_t>(index.row()));
if(!m_cache.count(row))
return false;

auto & cached_row = m_cache.at(row);
const size_t column = static_cast<size_t>(index.column());

QByteArray newValue = encode(value.toByteArray());
Expand Down
36 changes: 36 additions & 0 deletions src/tests/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -123,3 +123,39 @@ set(TESTCACHE_HDR
add_executable(test-cache ${TESTCACHE_HDR} ${TESTCACHE_SRC})
target_link_libraries(test-cache ${QT_MAJOR}::Test)
add_test(NAME test-cache COMMAND $<TARGET_FILE:test-cache>)

# test table model

set(TESTTABLEMODEL_SRC
TestTableModel.cpp
../sqlitetablemodel.cpp
../RowLoader.cpp
../sqlitedb.cpp
../CipherSettings.cpp
../CipherDialog.cpp
../CondFormat.cpp
../Data.cpp
../Settings.cpp
../sql/sqlitetypes.cpp
../sql/Query.cpp
../sql/ObjectIdentifier.cpp
../sql/parser/ParserDriver.cpp
../sql/parser/sqlite3_lexer.cpp
../sql/parser/sqlite3_parser.cpp
)

set(TESTTABLEMODEL_HDR
TestTableModel.h
)

add_executable(test-table-model ${TESTTABLEMODEL_HDR} ${TESTTABLEMODEL_SRC})
target_link_libraries(test-table-model ${QT_MAJOR}::Test ${QT_MAJOR}::Widgets ${QT_MAJOR}::Concurrent ${LIBSQLITE_NAME} ${QT5_COMPAT})
if(QT_MAJOR STREQUAL "Qt6")
# Same as for the main target: the .ui files connect to slots that only exist in Qt5 unless connections are by string
set_target_properties(test-table-model PROPERTIES AUTOUIC_OPTIONS "--connections=string")
endif()
add_test(NAME test-table-model COMMAND $<TARGET_FILE:test-table-model>)
if(WIN32)
# Like test-global-filter, this needs to find the SQLite DLL built by the CI jobs
set_tests_properties(test-table-model PROPERTIES ENVIRONMENT "PATH=${TESTGLOBALFILTER_PATH}")
endif()
110 changes: 110 additions & 0 deletions src/tests/TestTableModel.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
#include <QtTest/QTest>
#include <QApplication>
#include <QTemporaryDir>
#include <QFile>

#include "TestTableModel.h"
#include "../sqlitedb.h"
#include "../sqlitetablemodel.h"
#include "../Settings.h"
#include "../sql/ObjectIdentifier.h"

namespace
{
const int rowCount = 100;

// Column 0 of the model is the rowid column, so the 'val' column is column 2.
// Creates a table with rowCount rows and returns a model showing it. The prefetch size is set small enough that
// most rows are not loaded into the cache when the model is created.
struct Fixture
{
QTemporaryDir dir;
DBBrowserDB db;
std::unique_ptr<SqliteTableModel> model;

Fixture()
{
Settings::setValue("db", "prefetchsize", 10);
// Open an empty file instead of using create(), which closes the database handle twice
const QString filename = dir.filePath("test.db");
QFile(filename).open(QIODevice::WriteOnly);
db.open(filename);
db.executeSQL("CREATE TABLE t (id INTEGER PRIMARY KEY, val TEXT);");
db.executeSQL("WITH RECURSIVE c(x) AS (SELECT 1 UNION ALL SELECT x+1 FROM c WHERE x < " + std::to_string(rowCount) +
") INSERT INTO t(id, val) SELECT x, 'v' || x FROM c;");
model = std::make_unique<SqliteTableModel>(db, nullptr, QString(), true);
model->setQuery(sqlb::Query(sqlb::ObjectIdentifier("main", "t")));
model->waitUntilIdle();
// The worker reports back through queued connections
QTRY_VERIFY_WITH_TIMEOUT(model->rowCount() == rowCount, 5000);
model->waitUntilIdle();
QCoreApplication::processEvents();
}
};
}

void TestTableModel::setDataOnCachedRow()
{
Fixture f;
QCOMPARE(f.model->rowCount(), rowCount);

const QModelIndex idx = f.model->index(0, 2);
QVERIFY(f.model->setData(idx, "changed"));
QCOMPARE(f.model->data(idx, Qt::EditRole).toString(), QString("changed"));
}

void TestTableModel::setDataOnUncachedRowLoadsRowAndUpdates()
{
Fixture f;
QCOMPARE(f.model->rowCount(), rowCount);

// Rows beyond the first chunk are valid model rows, but not in the cache yet. Editing them (e.g. by pasting a
// large clipboard) used to throw std::out_of_range from the cache lookup and terminate the application.
const QModelIndex idx = f.model->index(rowCount - 1, 2);
try {
QVERIFY(f.model->setData(idx, "changed"));
} catch(const std::exception& e) {
QFAIL(qPrintable(QString("setData threw: ") + e.what()));
}
QCOMPARE(f.model->data(idx, Qt::EditRole).toString(), QString("changed"));
}

namespace
{
QStringList capturedWarnings;

void captureWarnings(QtMsgType type, const QMessageLogContext&, const QString& msg)
{
if(type == QtWarningMsg)
capturedWarnings << msg;
}
}

void TestTableModel::createClosesTheHandleOnlyOnce()
{
QTemporaryDir dir;
DBBrowserDB db;

// create() closes the handle it used to write the file header and opens the file again. Closing the stale handle a
// second time is undefined behaviour: it is reported as a warning on some platforms and crashes on others.
capturedWarnings.clear();
const auto previous_handler = qInstallMessageHandler(captureWarnings);
const bool created = db.create(dir.filePath("created.db"));
qInstallMessageHandler(previous_handler);

QVERIFY(created);
QVERIFY(db.isOpen());
QVERIFY2(capturedWarnings.isEmpty(), qPrintable(capturedWarnings.join("; ")));
QVERIFY(db.executeSQL("CREATE TABLE t (id INTEGER);"));
}

int main(int argc, char* argv[])
{
// The model needs a QApplication, but the tests must also run on machines without a display (e.g. CI)
if(qEnvironmentVariableIsEmpty("QT_QPA_PLATFORM"))
qputenv("QT_QPA_PLATFORM", "offscreen");

QApplication app(argc, argv);
TestTableModel test;
return QTest::qExec(&test, argc, argv);
}
16 changes: 16 additions & 0 deletions src/tests/TestTableModel.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
#ifndef TESTTABLEMODEL_H
#define TESTTABLEMODEL_H

#include <QObject>

class TestTableModel : public QObject
{
Q_OBJECT

private slots:
void setDataOnCachedRow();
void setDataOnUncachedRowLoadsRowAndUpdates();
void createClosesTheHandleOnlyOnce();
};

#endif // TESTTABLEMODEL_H
Loading