// // Unit tests for the SqlBrowser module: DataModel (double-buffered table // model with role handling) and AddModel (add-row table model). // // These are the module's pure non-GUI logic classes. No database access is // involved: the DB work lives in QueryWorker/QueryThread (QSqlDatabase on a // worker thread). The remaining SqlBrowser classes are either widgets // (Query, BrowserWnd, LoginWnd, WaitWnd, AddLines) or DB/thread driven. // PresetManager is skipped as well: its constructor calls // QApplication::arguments() and its save path uses QInputDialog, so it // requires a running GUI application. // #include "../DataModel.h" #include "../AddModel.h" #include #include #include #include #include class sqlbrowser_unit_test : public QObject { Q_OBJECT private slots: // DataModel void dataModelEmptyState(); void dataModelSwapLists(); void dataModelDataRoleHandling(); void dataModelToolTipTimestamp(); void dataModelHeaderData(); void dataModelTableEditability(); void dataModelSlotSetData(); void dataModelSetDataAsyncSemantics(); void dataModelDeleteLines(); // AddModel void addModelBasics(); void addModelSetDataCreatesRow(); void addModelStringifiesValues(); void addModelRemoveRow(); void addModelClear(); void addModelTableName(); }; // --- DataModel ---------------------------------------------------------------- void sqlbrowser_unit_test::dataModelEmptyState() { DataModel model; QCOMPARE(model.rowCount(), 0); QCOMPARE(model.columnCount(), 0); QVERIFY(model.getTableName().isEmpty()); Qt::ItemFlags flags = model.flags(QModelIndex()); QVERIFY(flags & Qt::ItemIsSelectable); QVERIFY(flags & Qt::ItemIsEnabled); QVERIFY(!(flags & Qt::ItemIsEditable)); } void sqlbrowser_unit_test::dataModelSwapLists() { DataModel model; // columns/rows are staged in the write buffer first model.addColumn(QStringLiteral("id"), QMetaType::Int); model.addColumn(QStringLiteral("name"), QMetaType::QString); model.addColumn(QStringLiteral("ts"), QMetaType::LongLong); Data row0; row0 << 1 << QStringLiteral("alpha") << static_cast(1700000000000LL); model.addLine(row0); // nothing is visible in the live model before the swap QCOMPARE(model.rowCount(), 0); QCOMPARE(model.columnCount(), 0); model.slotSwapLists(); QCOMPARE(model.rowCount(), 1); QCOMPARE(model.columnCount(), 3); const HeaderList &live = model.getHeaderList(); QCOMPARE(live.size(), 3); QCOMPARE(live.at(0).first, QStringLiteral("id")); QVERIFY(live.at(0).second == QMetaType::Int); QCOMPARE(live.at(1).first, QStringLiteral("name")); QVERIFY(live.at(1).second == QMetaType::QString); QCOMPARE(live.at(2).first, QStringLiteral("ts")); QVERIFY(live.at(2).second == QMetaType::LongLong); // the write buffer is cleared by the swap: new staging does not leak // into the live model Data row1; row1 << 999 << QStringLiteral("leak"); model.addLine(row1); QCOMPARE(model.rowCount(), 1); QCOMPARE(model.data(model.index(0, 0), Qt::DisplayRole).toInt(), 1); } void sqlbrowser_unit_test::dataModelDataRoleHandling() { DataModel model; model.addColumn(QStringLiteral("id"), QMetaType::Int); model.addColumn(QStringLiteral("name"), QMetaType::QString); model.addColumn(QStringLiteral("ts"), QMetaType::LongLong); Data row0; row0 << 1 << QStringLiteral("alpha") << static_cast(1700000000000LL); Data row1; row1 << 2 << QVariant() << static_cast(5); Data row2; row2 << 3 << QStringLiteral("gamma") << QVariant(); model.addLine(row0); model.addLine(row1); model.addLine(row2); model.slotSwapLists(); QCOMPARE(model.rowCount(), 3); QCOMPARE(model.columnCount(), 3); // DisplayRole and EditRole both return the raw value QCOMPARE(model.data(model.index(0, 0), Qt::DisplayRole).toInt(), 1); QCOMPARE(model.data(model.index(0, 1), Qt::DisplayRole).toString(), QStringLiteral("alpha")); QCOMPARE(model.data(model.index(0, 2), Qt::EditRole).toLongLong(), qint64(1700000000000)); // null cells are highlighted via BackgroundRole; filled cells are not QCOMPARE(model.data(model.index(1, 1), Qt::BackgroundRole).value(), QColor(255, 255, 215)); QVERIFY(!model.data(model.index(0, 0), Qt::BackgroundRole).isValid()); // other roles return nothing QVERIFY(!model.data(model.index(0, 1), Qt::DecorationRole).isValid()); // a valid parent index means "no children" QCOMPARE(model.rowCount(model.index(0, 0)), 0); QCOMPARE(model.columnCount(model.index(0, 0)), 0); } void sqlbrowser_unit_test::dataModelToolTipTimestamp() { DataModel model; model.addColumn(QStringLiteral("id"), QMetaType::Int); model.addColumn(QStringLiteral("name"), QMetaType::QString); model.addColumn(QStringLiteral("ts"), QMetaType::LongLong); Data row0; row0 << 1 << QStringLiteral("alpha") << static_cast(1700000000000); Data row1; row1 << 2 << QStringLiteral("beta") << static_cast(5); model.addLine(row0); model.addLine(row1); model.slotSwapLists(); // 64 bit integer values above 1000000000 are rendered as timestamps const QVariant tooltip = model.data(model.index(0, 2), Qt::ToolTipRole); QVERIFY(tooltip.isValid()); const QRegularExpression pattern(QStringLiteral("^\\d{2}\\.\\d{2}\\.\\d{4} \\d{2}:\\d{2}:\\d{2}\\.\\d{3}$")); QVERIFY(pattern.match(tooltip.toString()).hasMatch()); // below the threshold -> no tooltip QVERIFY(!model.data(model.index(1, 2), Qt::ToolTipRole).isValid()); // non-integer types -> no tooltip QVERIFY(!model.data(model.index(0, 1), Qt::ToolTipRole).isValid()); QVERIFY(!model.data(model.index(0, 0), Qt::ToolTipRole).isValid()); } void sqlbrowser_unit_test::dataModelHeaderData() { DataModel model; model.addColumn(QStringLiteral("id"), QMetaType::Int); model.addColumn(QStringLiteral("name"), QMetaType::QString); model.addColumn(QStringLiteral("ts"), QMetaType::LongLong); model.slotSwapLists(); QCOMPARE(model.headerData(0, Qt::Horizontal, Qt::DisplayRole).toString(), QStringLiteral("id")); QCOMPARE(model.headerData(1, Qt::Horizontal, Qt::DisplayRole).toString(), QStringLiteral("name")); QCOMPARE(model.headerData(2, Qt::Horizontal, Qt::DisplayRole).toString(), QStringLiteral("ts")); // ToolTipRole carries the QMetaType name of the column QCOMPARE(model.headerData(0, Qt::Horizontal, Qt::ToolTipRole).toString(), QStringLiteral("int")); QCOMPARE(model.headerData(1, Qt::Horizontal, Qt::ToolTipRole).toString(), QStringLiteral("QString")); QCOMPARE(model.headerData(2, Qt::Horizontal, Qt::ToolTipRole).toString(), QStringLiteral("qlonglong")); // out-of-range section and vertical orientation fall through to // QAbstractTableModel::headerData (1-based: section+1 horizontal, 1 vertical) QCOMPARE(model.headerData(9, Qt::Horizontal, Qt::DisplayRole).toString(), QStringLiteral("10")); QCOMPARE(model.headerData(0, Qt::Vertical, Qt::DisplayRole).toString(), QStringLiteral("1")); } void sqlbrowser_unit_test::dataModelTableEditability() { DataModel model; // custom queries (empty table name) are read-only QVERIFY(!(model.flags(QModelIndex()) & Qt::ItemIsEditable)); // table queries are editable model.setTableName(QStringLiteral("users")); QCOMPARE(model.getTableName(), QStringLiteral("users")); QVERIFY(model.flags(QModelIndex()) & Qt::ItemIsEditable); model.setTableName(QString()); QVERIFY(!(model.flags(QModelIndex()) & Qt::ItemIsEditable)); } void sqlbrowser_unit_test::dataModelSlotSetData() { DataModel model; model.addColumn(QStringLiteral("id"), QMetaType::Int); model.addColumn(QStringLiteral("name"), QMetaType::QString); Data row0; row0 << 1 << QStringLiteral("alpha"); Data row1; row1 << 2 << QStringLiteral("beta"); model.addLine(row0); model.addLine(row1); model.slotSwapLists(); // slotSetData applies the write to the live list (the DB answered) model.slotSetData(model.index(1, 1), QStringLiteral("delta")); QCOMPARE(model.data(model.index(1, 1), Qt::DisplayRole).toString(), QStringLiteral("delta")); QCOMPARE(model.data(model.index(0, 1), Qt::DisplayRole).toString(), QStringLiteral("alpha")); QCOMPARE(model.rowCount(), 2); } void sqlbrowser_unit_test::dataModelSetDataAsyncSemantics() { DataModel model; model.addColumn(QStringLiteral("id"), QMetaType::Int); Data row0; row0 << 1; model.addLine(row0); model.slotSwapLists(); bool captured = false; QVariant capturedValue; DataModel *capturedModel = nullptr; QObject::connect(&model, &DataModel::signalSetData, [&captured, &capturedValue, &capturedModel](DataModel *m, const QModelIndex &, const QVariant &v) { captured = true; capturedModel = m; capturedValue = v; }); // EditRole: value is NOT applied locally; it is forwarded to the worker // and the call reports "not done yet" QVERIFY(!model.setData(model.index(0, 0), QStringLiteral("changed"), Qt::EditRole)); QVERIFY(captured); QCOMPARE(capturedModel, static_cast(&model)); QCOMPARE(capturedValue.toString(), QStringLiteral("changed")); QCOMPARE(model.data(model.index(0, 0), Qt::DisplayRole).toInt(), 1); // untouched // non-EditRole values are accepted (true) and ignored QVERIFY(model.setData(model.index(0, 0), QStringLiteral("ignored"), Qt::DisplayRole)); QCOMPARE(model.data(model.index(0, 0), Qt::DisplayRole).toInt(), 1); } void sqlbrowser_unit_test::dataModelDeleteLines() { DataModel model; model.addColumn(QStringLiteral("id"), QMetaType::Int); Data row0; row0 << 1; Data row1; row1 << 2; Data row2; row2 << 3; model.addLine(row0); model.addLine(row1); model.addLine(row2); model.slotSwapLists(); QCOMPARE(model.rowCount(), 3); // delete in descending row order so the indices stay valid QList toDelete; toDelete << model.index(2, 0) << model.index(0, 0); model.deleteLines(toDelete); QCOMPARE(model.rowCount(), 1); QCOMPARE(model.data(model.index(0, 0), Qt::DisplayRole).toInt(), 2); // an empty index list is a no-op model.deleteLines(QList()); QCOMPARE(model.rowCount(), 1); } // --- AddModel -------------------------------------------------------------------- void sqlbrowser_unit_test::addModelBasics() { AddModel model; // rowCount is always dataList + 1 (the trailing empty row for new data) QCOMPARE(model.rowCount(), 1); QCOMPARE(model.columnCount(), 0); HeaderList headers; headers.append(QPair(QStringLiteral("id"), QMetaType::Int)); headers.append(QPair(QStringLiteral("name"), QMetaType::QString)); model.setHeaderList(headers); QCOMPARE(model.columnCount(), 2); QCOMPARE(model.rowCount(), 1); QVERIFY(model.getColumnType(0) == QMetaType::Int); QVERIFY(model.getColumnType(1) == QMetaType::QString); QVERIFY(model.getColumnType(-1) == QMetaType::UnknownType); QVERIFY(model.getColumnType(2) == QMetaType::UnknownType); QCOMPARE(model.headerData(0, Qt::Horizontal, Qt::DisplayRole).toString(), QStringLiteral("id")); QCOMPARE(model.headerData(1, Qt::Horizontal, Qt::DisplayRole).toString(), QStringLiteral("name")); // only horizontal + displayRole QVERIFY(!model.headerData(0, Qt::Vertical, Qt::DisplayRole).isValid()); QVERIFY(!model.headerData(0, Qt::Horizontal, Qt::EditRole).isValid()); const Qt::ItemFlags flags = model.flags(QModelIndex()); QVERIFY(flags & Qt::ItemIsSelectable); QVERIFY(flags & Qt::ItemIsEnabled); QVERIFY(flags & Qt::ItemIsEditable); } void sqlbrowser_unit_test::addModelSetDataCreatesRow() { AddModel model; HeaderList headers; headers.append(QPair(QStringLiteral("id"), QMetaType::Int)); headers.append(QPair(QStringLiteral("name"), QMetaType::QString)); model.setHeaderList(headers); QCOMPARE(model.rowCount(), 1); // writing into the empty row materializes it QVERIFY(model.setData(model.index(0, 0), QStringLiteral("42"), Qt::EditRole)); QCOMPARE(model.rowCount(), 2); QCOMPARE(model.data(model.index(0, 0), Qt::DisplayRole).toString(), QStringLiteral("42")); // other columns of the new row are pre-filled with empty strings QCOMPARE(model.data(model.index(0, 1), Qt::DisplayRole).toString(), QString()); // the trailing empty row still has no data QVERIFY(!model.data(model.index(1, 0), Qt::DisplayRole).isValid()); // a second write materializes row 1 QVERIFY(model.setData(model.index(1, 1), QStringLiteral("bob"), Qt::EditRole)); QCOMPARE(model.rowCount(), 3); QCOMPARE(model.data(model.index(1, 1), Qt::DisplayRole).toString(), QStringLiteral("bob")); QCOMPARE(model.data(model.index(1, 0), Qt::DisplayRole).toString(), QString()); // non-EditRole values are rejected QVERIFY(!model.setData(model.index(0, 0), QStringLiteral("x"), Qt::DisplayRole)); QCOMPARE(model.data(model.index(0, 0), Qt::DisplayRole).toString(), QStringLiteral("42")); } void sqlbrowser_unit_test::addModelStringifiesValues() { AddModel model; HeaderList headers; headers.append(QPair(QStringLiteral("id"), QMetaType::Int)); model.setHeaderList(headers); // stored values are stringified on write QVERIFY(model.setData(model.index(0, 0), 42, Qt::EditRole)); const QVariant value = model.data(model.index(0, 0), Qt::DisplayRole); QCOMPARE(value.toString(), QStringLiteral("42")); QVERIFY(value.typeId() == QMetaType::QString); } void sqlbrowser_unit_test::addModelRemoveRow() { AddModel model; HeaderList headers; headers.append(QPair(QStringLiteral("id"), QMetaType::Int)); model.setHeaderList(headers); QVERIFY(model.setData(model.index(0, 0), QStringLiteral("1"), Qt::EditRole)); QVERIFY(model.setData(model.index(1, 0), QStringLiteral("2"), Qt::EditRole)); QCOMPARE(model.rowCount(), 3); // out-of-range row QVERIFY(!model.removeRow(10)); QCOMPARE(model.rowCount(), 3); QVERIFY(model.removeRow(0)); QCOMPARE(model.rowCount(), 2); QCOMPARE(model.data(model.index(0, 0), Qt::DisplayRole).toString(), QStringLiteral("2")); } void sqlbrowser_unit_test::addModelClear() { AddModel model; HeaderList headers; headers.append(QPair(QStringLiteral("id"), QMetaType::Int)); model.setHeaderList(headers); QVERIFY(model.setData(model.index(0, 0), QStringLiteral("1"), Qt::EditRole)); QCOMPARE(model.rowCount(), 2); model.clear(); QCOMPARE(model.rowCount(), 1); QCOMPARE(model.columnCount(), 1); // header list survives clear() } void sqlbrowser_unit_test::addModelTableName() { AddModel model; QCOMPARE(model.getTableName(), QString()); model.setTablename(QStringLiteral("t_users")); QCOMPARE(model.getTableName(), QStringLiteral("t_users")); } QTEST_MAIN(sqlbrowser_unit_test) #include "sqlbrowser_unit_test.moc"