#include #include #include #include #include #include "common/storage.h" /** * Test of the storage implementation to ensure it can do the low level operations as expected. */ class StorageTest : public QObject { Q_OBJECT private: QString testDataPath; QString dbName; const char *keyPrefix = "key"; void populate(int count) { Sink::Storage storage(testDataPath, dbName, Sink::Storage::ReadWrite); auto transaction = storage.createTransaction(Sink::Storage::ReadWrite); for (int i = 0; i < count; i++) { //This should perhaps become an implementation detail of the db? if (i % 10000 == 0) { if (i > 0) { transaction.commit(); transaction = std::move(storage.createTransaction(Sink::Storage::ReadWrite)); } } transaction.openDatabase().write(keyPrefix + QByteArray::number(i), keyPrefix + QByteArray::number(i)); } transaction.commit(); } bool verify(Sink::Storage &storage, int i) { bool success = true; bool keyMatch = true; const auto reference = keyPrefix + QByteArray::number(i); storage.createTransaction(Sink::Storage::ReadOnly).openDatabase().scan(keyPrefix + QByteArray::number(i), [&keyMatch, &reference](const QByteArray &key, const QByteArray &value) -> bool { if (value != reference) { qDebug() << "Mismatch while reading"; keyMatch = false; } return keyMatch; }, [&success](const Sink::Storage::Error &error) { qDebug() << error.message; success = false; } ); return success && keyMatch; } private Q_SLOTS: void initTestCase() { testDataPath = "./testdb"; dbName = "test"; Sink::Storage storage(testDataPath, dbName); storage.removeFromDisk(); } void cleanup() { Sink::Storage storage(testDataPath, dbName); storage.removeFromDisk(); } void testCleanup() { populate(1); Sink::Storage storage(testDataPath, dbName); storage.removeFromDisk(); QFileInfo info(testDataPath + "/" + dbName); QVERIFY(!info.exists()); } void testRead() { const int count = 100; populate(count); //ensure we can read everything back correctly { Sink::Storage storage(testDataPath, dbName); for (int i = 0; i < count; i++) { QVERIFY(verify(storage, i)); } } } void testScan() { const int count = 100; populate(count); //ensure we can scan for values { int hit = 0; Sink::Storage store(testDataPath, dbName); store.createTransaction(Sink::Storage::ReadOnly).openDatabase().scan("", [&](const QByteArray &key, const QByteArray &value) -> bool { if (key == "key50") { hit++; } return true; }); QCOMPARE(hit, 1); } //ensure we can read a single value { int hit = 0; bool foundInvalidValue = false; Sink::Storage store(testDataPath, dbName); store.createTransaction(Sink::Storage::ReadOnly).openDatabase().scan("key50", [&](const QByteArray &key, const QByteArray &value) -> bool { if (key != "key50") { foundInvalidValue = true; } hit++; return true; }); QVERIFY(!foundInvalidValue); QCOMPARE(hit, 1); } } void testNestedOperations() { populate(3); Sink::Storage store(testDataPath, dbName, Sink::Storage::ReadWrite); auto transaction = store.createTransaction(Sink::Storage::ReadWrite); transaction.openDatabase().scan("key1", [&](const QByteArray &key, const QByteArray &value) -> bool { transaction.openDatabase().remove(key, [](const Sink::Storage::Error &) { QVERIFY(false); }); return false; }); } void testNestedTransactions() { populate(3); Sink::Storage store(testDataPath, dbName, Sink::Storage::ReadWrite); store.createTransaction(Sink::Storage::ReadOnly).openDatabase().scan("key1", [&](const QByteArray &key, const QByteArray &value) -> bool { store.createTransaction(Sink::Storage::ReadWrite).openDatabase().remove(key, [](const Sink::Storage::Error &) { QVERIFY(false); }); return false; }); } void testReadEmptyDb() { bool gotResult = false; bool gotError = false; Sink::Storage store(testDataPath, dbName, Sink::Storage::ReadWrite); auto transaction = store.createTransaction(Sink::Storage::ReadOnly); auto db = transaction.openDatabase("default", [&](const Sink::Storage::Error &error) { qDebug() << error.message; gotError = true; }); int numValues = db.scan("", [&](const QByteArray &key, const QByteArray &value) -> bool { gotResult = true; return false; }, [&](const Sink::Storage::Error &error) { qDebug() << error.message; gotError = true; }); QCOMPARE(numValues, 0); QVERIFY(!gotResult); QVERIFY(!gotError); } void testConcurrentRead() { //With a count of 10000 this test is more likely to expose problems, but also takes some time to execute. const int count = 1000; populate(count); // QTest::qWait(500); //We repeat the test a bunch of times since failing is relatively random for (int tries = 0; tries < 10; tries++) { bool error = false; //Try to concurrently read QList > futures; const int concurrencyLevel = 20; for (int num = 0; num < concurrencyLevel; num++) { futures << QtConcurrent::run([this, count, &error](){ Sink::Storage storage(testDataPath, dbName, Sink::Storage::ReadOnly); Sink::Storage storage2(testDataPath, dbName + "2", Sink::Storage::ReadOnly); for (int i = 0; i < count; i++) { if (!verify(storage, i)) { error = true; break; } } }); } for(auto future : futures) { future.waitForFinished(); } QVERIFY(!error); } { Sink::Storage storage(testDataPath, dbName); storage.removeFromDisk(); Sink::Storage storage2(testDataPath, dbName + "2"); storage2.removeFromDisk(); } } void testNoDuplicates() { bool gotResult = false; bool gotError = false; Sink::Storage store(testDataPath, dbName, Sink::Storage::ReadWrite); auto transaction = store.createTransaction(Sink::Storage::ReadWrite); auto db = transaction.openDatabase("default", nullptr, false); db.write("key","value"); db.write("key","value"); int numValues = db.scan("", [&](const QByteArray &key, const QByteArray &value) -> bool { gotResult = true; return true; }, [&](const Sink::Storage::Error &error) { qDebug() << error.message; gotError = true; }); QCOMPARE(numValues, 1); QVERIFY(!gotError); } void testDuplicates() { bool gotResult = false; bool gotError = false; Sink::Storage store(testDataPath, dbName, Sink::Storage::ReadWrite); auto transaction = store.createTransaction(Sink::Storage::ReadWrite); auto db = transaction.openDatabase("default", nullptr, true); db.write("key","value1"); db.write("key","value2"); int numValues = db.scan("key", [&](const QByteArray &key, const QByteArray &value) -> bool { gotResult = true; return true; }, [&](const Sink::Storage::Error &error) { qDebug() << error.message; gotError = true; }); QCOMPARE(numValues, 2); QVERIFY(!gotError); } void testNonexitingNamedDb() { bool gotResult = false; bool gotError = false; Sink::Storage store(testDataPath, dbName, Sink::Storage::ReadOnly); int numValues = store.createTransaction(Sink::Storage::ReadOnly).openDatabase("test").scan("", [&](const QByteArray &key, const QByteArray &value) -> bool { gotResult = true; return false; }, [&](const Sink::Storage::Error &error) { qDebug() << error.message; gotError = true; }); QCOMPARE(numValues, 0); QVERIFY(!gotResult); QVERIFY(!gotError); } void testWriteToNamedDb() { bool gotError = false; Sink::Storage store(testDataPath, dbName, Sink::Storage::ReadWrite); store.createTransaction(Sink::Storage::ReadWrite).openDatabase("test").write("key1", "value1", [&](const Sink::Storage::Error &error) { qDebug() << error.message; gotError = true; }); QVERIFY(!gotError); } void testWriteDuplicatesToNamedDb() { bool gotError = false; Sink::Storage store(testDataPath, dbName, Sink::Storage::ReadWrite); store.createTransaction(Sink::Storage::ReadWrite).openDatabase("test", nullptr, true).write("key1", "value1", [&](const Sink::Storage::Error &error) { qDebug() << error.message; gotError = true; }); QVERIFY(!gotError); } //By default we want only exact matches void testSubstringKeys() { Sink::Storage store(testDataPath, dbName, Sink::Storage::ReadWrite); auto transaction = store.createTransaction(Sink::Storage::ReadWrite); auto db = transaction.openDatabase("test", nullptr, true); db.write("sub","value1"); db.write("subsub","value2"); int numValues = db.scan("sub", [&](const QByteArray &key, const QByteArray &value) -> bool { return true; }); QCOMPARE(numValues, 1); } void testFindSubstringKeys() { Sink::Storage store(testDataPath, dbName, Sink::Storage::ReadWrite); auto transaction = store.createTransaction(Sink::Storage::ReadWrite); auto db = transaction.openDatabase("test", nullptr, false); db.write("sub","value1"); db.write("subsub","value2"); db.write("wubsub","value3"); int numValues = db.scan("sub", [&](const QByteArray &key, const QByteArray &value) -> bool { return true; }, nullptr, true); QCOMPARE(numValues, 2); } void testKeySorting() { Sink::Storage store(testDataPath, dbName, Sink::Storage::ReadWrite); auto transaction = store.createTransaction(Sink::Storage::ReadWrite); auto db = transaction.openDatabase("test", nullptr, false); db.write("sub_2","value2"); db.write("sub_1","value1"); db.write("sub_3","value3"); QList results; int numValues = db.scan("sub", [&](const QByteArray &key, const QByteArray &value) -> bool { results << value; return true; }, nullptr, true); QCOMPARE(numValues, 3); QCOMPARE(results.at(0), QByteArray("value1")); QCOMPARE(results.at(1), QByteArray("value2")); QCOMPARE(results.at(2), QByteArray("value3")); } //Ensure we don't retrieve a key that is greater than the current key. We only want equal keys. void testKeyRange() { Sink::Storage store(testDataPath, dbName, Sink::Storage::ReadWrite); auto transaction = store.createTransaction(Sink::Storage::ReadWrite); auto db = transaction.openDatabase("test", nullptr, true); db.write("sub1","value1"); int numValues = db.scan("sub", [&](const QByteArray &key, const QByteArray &value) -> bool { return true; }); QCOMPARE(numValues, 0); } void testFindLatest() { Sink::Storage store(testDataPath, dbName, Sink::Storage::ReadWrite); auto transaction = store.createTransaction(Sink::Storage::ReadWrite); auto db = transaction.openDatabase("test", nullptr, false); db.write("sub1","value1"); db.write("sub2","value2"); db.write("wub3","value3"); db.write("wub4","value4"); QByteArray result; db.findLatest("sub", [&](const QByteArray &key, const QByteArray &value) { result = value; }); QCOMPARE(result, QByteArray("value2")); } void testFindLatestInSingle() { Sink::Storage store(testDataPath, dbName, Sink::Storage::ReadWrite); auto transaction = store.createTransaction(Sink::Storage::ReadWrite); auto db = transaction.openDatabase("test", nullptr, false); db.write("sub2","value2"); QByteArray result; db.findLatest("sub", [&](const QByteArray &key, const QByteArray &value) { result = value; }); QCOMPARE(result, QByteArray("value2")); } void testFindLast() { Sink::Storage store(testDataPath, dbName, Sink::Storage::ReadWrite); auto transaction = store.createTransaction(Sink::Storage::ReadWrite); auto db = transaction.openDatabase("test", nullptr, false); db.write("sub2","value2"); db.write("wub3","value3"); QByteArray result; db.findLatest("wub", [&](const QByteArray &key, const QByteArray &value) { result = value; }); QCOMPARE(result, QByteArray("value3")); } void testRecordRevision() { Sink::Storage store(testDataPath, dbName, Sink::Storage::ReadWrite); auto transaction = store.createTransaction(Sink::Storage::ReadWrite); Sink::Storage::recordRevision(transaction, 1, "uid", "type"); QCOMPARE(Sink::Storage::getTypeFromRevision(transaction, 1), QByteArray("type")); QCOMPARE(Sink::Storage::getUidFromRevision(transaction, 1), QByteArray("uid")); } }; QTEST_MAIN(StorageTest) #include "storagetest.moc"