nextcloud-desktop/test/testsyncjournaldb.cpp
Christian Kamm dcf34316fd
Vfs: Add 'availability', a simplified, user-facing pin state #7111
The idea is that the user's question is "is this folder's data available
offline?" and not "does this folder have AlwaysLocal pin state?".
The the answers to the two questions can differ: an always-local
folder can have subitems that are not always-local and are dehydrated.

The new availability enum intends to describe the answer to the user's
actual question and can be derived from pin states. If pin states aren't
stored in the database the way of calculating availability will depend
on the vfs plugin.
2020-12-15 10:58:47 +01:00

461 lines
18 KiB
C++

/*
* This software is in the public domain, furnished "as is", without technical
* support, and with no warranty, express or implied, as to its usefulness for
* any purpose.
* */
#include <QtTest>
#include <sqlite3.h>
#include "common/syncjournaldb.h"
#include "common/syncjournalfilerecord.h"
using namespace OCC;
class TestSyncJournalDB : public QObject
{
Q_OBJECT
QTemporaryDir _tempDir;
public:
TestSyncJournalDB()
: _db((_tempDir.path() + "/sync.db"))
{
QVERIFY(_tempDir.isValid());
}
qint64 dropMsecs(QDateTime time)
{
return Utility::qDateTimeToTime_t(time);
}
private slots:
void initTestCase()
{
}
void cleanupTestCase()
{
const QString file = _db.databaseFilePath();
QFile::remove(file);
}
void testFileRecord()
{
SyncJournalFileRecord record;
QVERIFY(_db.getFileRecord(QByteArrayLiteral("nonexistant"), &record));
QVERIFY(!record.isValid());
record._path = "foo";
// Use a value that exceeds uint32 and isn't representable by the
// signed int being cast to uint64 either (like uint64::max would be)
record._inode = std::numeric_limits<quint32>::max() + 12ull;
record._modtime = dropMsecs(QDateTime::currentDateTime());
record._type = ItemTypeDirectory;
record._etag = "789789";
record._fileId = "abcd";
record._remotePerm = RemotePermissions::fromDbValue("RW");
record._fileSize = 213089055;
record._checksumHeader = "MD5:mychecksum";
QVERIFY(_db.setFileRecord(record));
SyncJournalFileRecord storedRecord;
QVERIFY(_db.getFileRecord(QByteArrayLiteral("foo"), &storedRecord));
QVERIFY(storedRecord == record);
// Update checksum
record._checksumHeader = "Adler32:newchecksum";
_db.updateFileRecordChecksum("foo", "newchecksum", "Adler32");
QVERIFY(_db.getFileRecord(QByteArrayLiteral("foo"), &storedRecord));
QVERIFY(storedRecord == record);
// Update metadata
record._modtime = dropMsecs(QDateTime::currentDateTime().addDays(1));
// try a value that only fits uint64, not int64
record._inode = std::numeric_limits<quint64>::max() - std::numeric_limits<quint32>::max() - 1;
record._type = ItemTypeFile;
record._etag = "789FFF";
record._fileId = "efg";
record._remotePerm = RemotePermissions::fromDbValue("NV");
record._fileSize = 289055;
_db.setFileRecord(record);
QVERIFY(_db.getFileRecord(QByteArrayLiteral("foo"), &storedRecord));
QVERIFY(storedRecord == record);
QVERIFY(_db.deleteFileRecord("foo"));
QVERIFY(_db.getFileRecord(QByteArrayLiteral("foo"), &record));
QVERIFY(!record.isValid());
}
void testFileRecordChecksum()
{
// Try with and without a checksum
{
SyncJournalFileRecord record;
record._path = "foo-checksum";
record._remotePerm = RemotePermissions::fromDbValue(" ");
record._checksumHeader = "MD5:mychecksum";
record._modtime = Utility::qDateTimeToTime_t(QDateTime::currentDateTimeUtc());
QVERIFY(_db.setFileRecord(record));
SyncJournalFileRecord storedRecord;
QVERIFY(_db.getFileRecord(QByteArrayLiteral("foo-checksum"), &storedRecord));
QVERIFY(storedRecord._path == record._path);
QVERIFY(storedRecord._remotePerm == record._remotePerm);
QVERIFY(storedRecord._checksumHeader == record._checksumHeader);
// qDebug()<< "OOOOO " << storedRecord._modtime.toTime_t() << record._modtime.toTime_t();
// Attention: compare time_t types here, as QDateTime seem to maintain
// milliseconds internally, which disappear in sqlite. Go for full seconds here.
QVERIFY(storedRecord._modtime == record._modtime);
QVERIFY(storedRecord == record);
}
{
SyncJournalFileRecord record;
record._path = "foo-nochecksum";
record._remotePerm = RemotePermissions();
record._modtime = Utility::qDateTimeToTime_t(QDateTime::currentDateTimeUtc());
QVERIFY(_db.setFileRecord(record));
SyncJournalFileRecord storedRecord;
QVERIFY(_db.getFileRecord(QByteArrayLiteral("foo-nochecksum"), &storedRecord));
QVERIFY(storedRecord == record);
}
}
void testDownloadInfo()
{
using Info = SyncJournalDb::DownloadInfo;
Info record = _db.getDownloadInfo("nonexistant");
QVERIFY(!record._valid);
record._errorCount = 5;
record._etag = "ABCDEF";
record._valid = true;
record._tmpfile = "/tmp/foo";
_db.setDownloadInfo("foo", record);
Info storedRecord = _db.getDownloadInfo("foo");
QVERIFY(storedRecord == record);
_db.setDownloadInfo("foo", Info());
Info wipedRecord = _db.getDownloadInfo("foo");
QVERIFY(!wipedRecord._valid);
}
void testUploadInfo()
{
using Info = SyncJournalDb::UploadInfo;
Info record = _db.getUploadInfo("nonexistant");
QVERIFY(!record._valid);
record._errorCount = 5;
record._chunk = 12;
record._transferid = 812974891;
record._size = 12894789147;
record._modtime = dropMsecs(QDateTime::currentDateTime());
record._valid = true;
_db.setUploadInfo("foo", record);
Info storedRecord = _db.getUploadInfo("foo");
QVERIFY(storedRecord == record);
_db.setUploadInfo("foo", Info());
Info wipedRecord = _db.getUploadInfo("foo");
QVERIFY(!wipedRecord._valid);
}
void testNumericId()
{
SyncJournalFileRecord record;
// Typical 8-digit padded id
record._fileId = "00000001abcd";
QCOMPARE(record.numericFileId(), QByteArray("00000001"));
// When the numeric id overflows the 8-digit boundary
record._fileId = "123456789ocidblaabcd";
QCOMPARE(record.numericFileId(), QByteArray("123456789"));
}
void testConflictRecord()
{
ConflictRecord record;
record.path = "abc";
record.baseFileId = "def";
record.baseModtime = 1234;
record.baseEtag = "ghi";
QVERIFY(!_db.conflictRecord(record.path).isValid());
_db.setConflictRecord(record);
auto newRecord = _db.conflictRecord(record.path);
QVERIFY(newRecord.isValid());
QCOMPARE(newRecord.path, record.path);
QCOMPARE(newRecord.baseFileId, record.baseFileId);
QCOMPARE(newRecord.baseModtime, record.baseModtime);
QCOMPARE(newRecord.baseEtag, record.baseEtag);
_db.deleteConflictRecord(record.path);
QVERIFY(!_db.conflictRecord(record.path).isValid());
}
void testAvoidReadFromDbOnNextSync()
{
auto invalidEtag = QByteArray("_invalid_");
auto initialEtag = QByteArray("etag");
auto makeEntry = [&](const QByteArray &path, ItemType type) {
SyncJournalFileRecord record;
record._path = path;
record._type = type;
record._etag = initialEtag;
_db.setFileRecord(record);
};
auto getEtag = [&](const QByteArray &path) {
SyncJournalFileRecord record;
_db.getFileRecord(path, &record);
return record._etag;
};
makeEntry("foodir", ItemTypeDirectory);
makeEntry("otherdir", ItemTypeDirectory);
makeEntry("foo%", ItemTypeDirectory); // wildcards don't apply
makeEntry("foodi_", ItemTypeDirectory); // wildcards don't apply
makeEntry("foodir/file", ItemTypeFile);
makeEntry("foodir/subdir", ItemTypeDirectory);
makeEntry("foodir/subdir/file", ItemTypeFile);
makeEntry("foodir/otherdir", ItemTypeDirectory);
makeEntry("fo", ItemTypeDirectory); // prefix, but does not match
makeEntry("foodir/sub", ItemTypeDirectory); // prefix, but does not match
makeEntry("foodir/subdir/subsubdir", ItemTypeDirectory);
makeEntry("foodir/subdir/subsubdir/file", ItemTypeFile);
makeEntry("foodir/subdir/otherdir", ItemTypeDirectory);
_db.schedulePathForRemoteDiscovery(QByteArray("foodir/subdir"));
// Direct effects of parent directories being set to _invalid_
QCOMPARE(getEtag("foodir"), invalidEtag);
QCOMPARE(getEtag("foodir/subdir"), invalidEtag);
QCOMPARE(getEtag("foodir/subdir/subsubdir"), initialEtag);
QCOMPARE(getEtag("foodir/file"), initialEtag);
QCOMPARE(getEtag("foodir/subdir/file"), initialEtag);
QCOMPARE(getEtag("foodir/subdir/subsubdir/file"), initialEtag);
QCOMPARE(getEtag("fo"), initialEtag);
QCOMPARE(getEtag("foo%"), initialEtag);
QCOMPARE(getEtag("foodi_"), initialEtag);
QCOMPARE(getEtag("otherdir"), initialEtag);
QCOMPARE(getEtag("foodir/otherdir"), initialEtag);
QCOMPARE(getEtag("foodir/sub"), initialEtag);
QCOMPARE(getEtag("foodir/subdir/otherdir"), initialEtag);
// Indirect effects: setFileRecord() calls filter etags
initialEtag = "etag2";
makeEntry("foodir", ItemTypeDirectory);
QCOMPARE(getEtag("foodir"), invalidEtag);
makeEntry("foodir/subdir", ItemTypeDirectory);
QCOMPARE(getEtag("foodir/subdir"), invalidEtag);
makeEntry("foodir/subdir/subsubdir", ItemTypeDirectory);
QCOMPARE(getEtag("foodir/subdir/subsubdir"), initialEtag);
makeEntry("fo", ItemTypeDirectory);
QCOMPARE(getEtag("fo"), initialEtag);
makeEntry("foodir/sub", ItemTypeDirectory);
QCOMPARE(getEtag("foodir/sub"), initialEtag);
}
void testRecursiveDelete()
{
auto makeEntry = [&](const QByteArray &path) {
SyncJournalFileRecord record;
record._path = path;
_db.setFileRecord(record);
};
QByteArrayList elements;
elements
<< "foo"
<< "foo/file"
<< "bar"
<< "moo"
<< "moo/file"
<< "foo%bar"
<< "foo bla bar/file"
<< "fo_"
<< "fo_/file";
for (const auto& elem : elements)
makeEntry(elem);
auto checkElements = [&]() {
bool ok = true;
for (const auto& elem : elements) {
SyncJournalFileRecord record;
_db.getFileRecord(elem, &record);
if (!record.isValid()) {
qWarning() << "Missing record: " << elem;
ok = false;
}
}
return ok;
};
_db.deleteFileRecord("moo", true);
elements.removeAll("moo");
elements.removeAll("moo/file");
QVERIFY(checkElements());
_db.deleteFileRecord("fo_", true);
elements.removeAll("fo_");
elements.removeAll("fo_/file");
QVERIFY(checkElements());
_db.deleteFileRecord("foo%bar", true);
elements.removeAll("foo%bar");
QVERIFY(checkElements());
}
void testPinState()
{
auto make = [&](const QByteArray &path, PinState state) {
_db.internalPinStates().setForPath(path, state);
auto pinState = _db.internalPinStates().rawForPath(path);
QVERIFY(pinState);
QCOMPARE(*pinState, state);
};
auto get = [&](const QByteArray &path) -> PinState {
auto state = _db.internalPinStates().effectiveForPath(path);
if (!state) {
QTest::qFail("couldn't read pin state", __FILE__, __LINE__);
return PinState::Inherited;
}
return *state;
};
auto getRecursive = [&](const QByteArray &path) -> PinState {
auto state = _db.internalPinStates().effectiveForPathRecursive(path);
if (!state) {
QTest::qFail("couldn't read pin state", __FILE__, __LINE__);
return PinState::Inherited;
}
return *state;
};
auto getRaw = [&](const QByteArray &path) -> PinState {
auto state = _db.internalPinStates().rawForPath(path);
if (!state) {
QTest::qFail("couldn't read pin state", __FILE__, __LINE__);
return PinState::Inherited;
}
return *state;
};
_db.internalPinStates().wipeForPathAndBelow("");
auto list = _db.internalPinStates().rawList();
QCOMPARE(list->size(), 0);
// Make a thrice-nested setup
make("", PinState::AlwaysLocal);
make("local", PinState::AlwaysLocal);
make("online", PinState::OnlineOnly);
make("inherit", PinState::Inherited);
for (auto base : {"local/", "online/", "inherit/"}) {
make(QByteArray(base) + "inherit", PinState::Inherited);
make(QByteArray(base) + "local", PinState::AlwaysLocal);
make(QByteArray(base) + "online", PinState::OnlineOnly);
for (auto base2 : {"local/", "online/", "inherit/"}) {
make(QByteArray(base) + base2 + "inherit", PinState::Inherited);
make(QByteArray(base) + base2 + "local", PinState::AlwaysLocal);
make(QByteArray(base) + base2 + "online", PinState::OnlineOnly);
}
}
list = _db.internalPinStates().rawList();
QCOMPARE(list->size(), 4 + 9 + 27);
// Baseline direct checks (the fallback for unset root pinstate is AlwaysLocal)
QCOMPARE(get(""), PinState::AlwaysLocal);
QCOMPARE(get("local"), PinState::AlwaysLocal);
QCOMPARE(get("online"), PinState::OnlineOnly);
QCOMPARE(get("inherit"), PinState::AlwaysLocal);
QCOMPARE(get("nonexistant"), PinState::AlwaysLocal);
QCOMPARE(get("online/local"), PinState::AlwaysLocal);
QCOMPARE(get("local/online"), PinState::OnlineOnly);
QCOMPARE(get("inherit/local"), PinState::AlwaysLocal);
QCOMPARE(get("inherit/online"), PinState::OnlineOnly);
QCOMPARE(get("inherit/inherit"), PinState::AlwaysLocal);
QCOMPARE(get("inherit/nonexistant"), PinState::AlwaysLocal);
// Inheriting checks, level 1
QCOMPARE(get("local/inherit"), PinState::AlwaysLocal);
QCOMPARE(get("local/nonexistant"), PinState::AlwaysLocal);
QCOMPARE(get("online/inherit"), PinState::OnlineOnly);
QCOMPARE(get("online/nonexistant"), PinState::OnlineOnly);
// Inheriting checks, level 2
QCOMPARE(get("local/inherit/inherit"), PinState::AlwaysLocal);
QCOMPARE(get("local/local/inherit"), PinState::AlwaysLocal);
QCOMPARE(get("local/local/nonexistant"), PinState::AlwaysLocal);
QCOMPARE(get("local/online/inherit"), PinState::OnlineOnly);
QCOMPARE(get("local/online/nonexistant"), PinState::OnlineOnly);
QCOMPARE(get("online/inherit/inherit"), PinState::OnlineOnly);
QCOMPARE(get("online/local/inherit"), PinState::AlwaysLocal);
QCOMPARE(get("online/local/nonexistant"), PinState::AlwaysLocal);
QCOMPARE(get("online/online/inherit"), PinState::OnlineOnly);
QCOMPARE(get("online/online/nonexistant"), PinState::OnlineOnly);
// Spot check the recursive variant
QCOMPARE(getRecursive(""), PinState::Inherited);
QCOMPARE(getRecursive("local"), PinState::Inherited);
QCOMPARE(getRecursive("online"), PinState::Inherited);
QCOMPARE(getRecursive("inherit"), PinState::Inherited);
QCOMPARE(getRecursive("online/local"), PinState::Inherited);
QCOMPARE(getRecursive("online/local/inherit"), PinState::AlwaysLocal);
QCOMPARE(getRecursive("inherit/inherit/inherit"), PinState::AlwaysLocal);
QCOMPARE(getRecursive("inherit/online/inherit"), PinState::OnlineOnly);
QCOMPARE(getRecursive("inherit/online/local"), PinState::AlwaysLocal);
make("local/local/local/local", PinState::AlwaysLocal);
QCOMPARE(getRecursive("local/local/local"), PinState::AlwaysLocal);
QCOMPARE(getRecursive("local/local/local/local"), PinState::AlwaysLocal);
// Check changing the root pin state
make("", PinState::OnlineOnly);
QCOMPARE(get("local"), PinState::AlwaysLocal);
QCOMPARE(get("online"), PinState::OnlineOnly);
QCOMPARE(get("inherit"), PinState::OnlineOnly);
QCOMPARE(get("nonexistant"), PinState::OnlineOnly);
make("", PinState::AlwaysLocal);
QCOMPARE(get("local"), PinState::AlwaysLocal);
QCOMPARE(get("online"), PinState::OnlineOnly);
QCOMPARE(get("inherit"), PinState::AlwaysLocal);
QCOMPARE(get("nonexistant"), PinState::AlwaysLocal);
// Wiping
QCOMPARE(getRaw("local/local"), PinState::AlwaysLocal);
_db.internalPinStates().wipeForPathAndBelow("local/local");
QCOMPARE(getRaw("local"), PinState::AlwaysLocal);
QCOMPARE(getRaw("local/local"), PinState::Inherited);
QCOMPARE(getRaw("local/local/local"), PinState::Inherited);
QCOMPARE(getRaw("local/local/online"), PinState::Inherited);
list = _db.internalPinStates().rawList();
QCOMPARE(list->size(), 4 + 9 + 27 - 4);
// Wiping everything
_db.internalPinStates().wipeForPathAndBelow("");
QCOMPARE(getRaw(""), PinState::Inherited);
QCOMPARE(getRaw("local"), PinState::Inherited);
QCOMPARE(getRaw("online"), PinState::Inherited);
list = _db.internalPinStates().rawList();
QCOMPARE(list->size(), 0);
}
private:
SyncJournalDb _db;
};
QTEST_APPLESS_MAIN(TestSyncJournalDB)
#include "testsyncjournaldb.moc"