tdesktop/Telegram/SourceFiles/storage/cache/storage_cache_database_test...

525 lines
13 KiB
C++
Raw Normal View History

/*
This file is part of Telegram Desktop,
the official desktop application for the Telegram messaging service.
For license and copyright information please follow this link:
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
*/
#include "catch.hpp"
#include "storage/cache/storage_cache_database.h"
#include "storage/storage_encryption.h"
#include "storage/storage_encrypted_file.h"
#include "base/concurrent_timer.h"
#include <crl/crl.h>
#include <QtCore/QFile>
#include <QtWidgets/QApplication>
#include <thread>
using namespace Storage::Cache;
const auto key = Storage::EncryptionKey(bytes::make_vector(
bytes::make_span("\
abcdefgh01234567abcdefgh01234567abcdefgh01234567abcdefgh01234567\
abcdefgh01234567abcdefgh01234567abcdefgh01234567abcdefgh01234567\
abcdefgh01234567abcdefgh01234567abcdefgh01234567abcdefgh01234567\
abcdefgh01234567abcdefgh01234567abcdefgh01234567abcdefgh01234567\
").subspan(0, Storage::EncryptionKey::kSize)));
const auto name = QString("test.db");
const auto SmallSleep = [] {
static auto SleepTime = 0;
if (SleepTime > 5000) {
return false;
}
std::this_thread::sleep_for(std::chrono::milliseconds(10));
SleepTime += 10;
return true;
};
QString GetBinlogPath() {
using namespace Storage;
QFile versionFile(name + "/version");
while (!versionFile.open(QIODevice::ReadOnly)) {
if (!SmallSleep()) {
return QString();
}
}
const auto bytes = versionFile.readAll();
if (bytes.size() != 4) {
return QString();
}
const auto version = *reinterpret_cast<const int32*>(bytes.data());
return name + '/' + QString::number(version) + "/binlog";
}
const auto TestValue1 = QByteArray("testbytetestbyt");
const auto TestValue2 = QByteArray("bytetestbytetestb");
crl::semaphore Semaphore;
auto Result = Error();
const auto GetResult = [](Error error) {
Result = error;
Semaphore.release();
};
auto Value = QByteArray();
const auto GetValue = [](QByteArray value) {
Value = value;
Semaphore.release();
};
const auto Settings = [] {
auto result = Database::Settings();
result.trackEstimatedTime = false;
result.writeBundleDelay = 1 * crl::time_type(1000);
result.pruneTimeout = 1 * crl::time_type(1500);
result.maxDataSize = 20;
return result;
}();
const auto AdvanceTime = [](int32 seconds) {
std::this_thread::sleep_for(std::chrono::milliseconds(1000) * seconds);
};
TEST_CASE("encrypted cache db", "[storage_cache_database]") {
static auto init = [] {
int argc = 0;
char **argv = nullptr;
static QCoreApplication application(argc, argv);
static base::ConcurrentTimerEnvironment environment;
return true;
}();
SECTION("writing db") {
Database db(name, Settings);
db.clear(GetResult);
Semaphore.acquire();
REQUIRE(Result.type == Error::Type::None);
db.open(key, GetResult);
Semaphore.acquire();
REQUIRE(Result.type == Error::Type::None);
db.put(Key{ 0, 1 }, TestValue1, GetResult);
Semaphore.acquire();
REQUIRE(Result.type == Error::Type::None);
db.close([&] { Semaphore.release(); });
Semaphore.acquire();
}
SECTION("reading and writing db") {
Database db(name, Settings);
db.open(key, GetResult);
Semaphore.acquire();
REQUIRE(Result.type == Error::Type::None);
db.get(Key{ 0, 1 }, GetValue);
Semaphore.acquire();
REQUIRE((Value == TestValue1));
db.put(Key{ 1, 0 }, TestValue2, GetResult);
Semaphore.acquire();
REQUIRE(Result.type == Error::Type::None);
db.get(Key{ 1, 0 }, GetValue);
Semaphore.acquire();
REQUIRE((Value == TestValue2));
db.get(Key{ 1, 1 }, GetValue);
Semaphore.acquire();
REQUIRE(Value.isEmpty());
db.close([&] { Semaphore.release(); });
Semaphore.acquire();
}
SECTION("reading db") {
Database db(name, Settings);
db.open(key, GetResult);
Semaphore.acquire();
REQUIRE(Result.type == Error::Type::None);
db.get(Key{ 0, 1 }, GetValue);
Semaphore.acquire();
REQUIRE((Value == TestValue1));
db.get(Key{ 1, 0 }, GetValue);
Semaphore.acquire();
REQUIRE((Value == TestValue2));
db.close([&] { Semaphore.release(); });
Semaphore.acquire();
}
SECTION("overwriting values") {
Database db(name, Settings);
db.open(key, GetResult);
Semaphore.acquire();
REQUIRE(Result.type == Error::Type::None);
const auto path = GetBinlogPath();
db.get(Key{ 0, 1 }, GetValue);
Semaphore.acquire();
REQUIRE((Value == TestValue1));
const auto size = QFile(path).size();
db.put(Key{ 0, 1 }, TestValue2, GetResult);
Semaphore.acquire();
REQUIRE(Result.type == Error::Type::None);
const auto next = QFile(path).size();
REQUIRE(next > size);
db.get(Key{ 0, 1 }, GetValue);
Semaphore.acquire();
REQUIRE((Value == TestValue2));
db.put(Key{ 0, 1 }, TestValue2, GetResult);
Semaphore.acquire();
REQUIRE(Result.type == Error::Type::None);
const auto same = QFile(path).size();
REQUIRE(same == next);
db.close([&] { Semaphore.release(); });
Semaphore.acquire();
}
}
TEST_CASE("cache db remove", "[storage_cache_database]") {
SECTION("db remove deletes value") {
Database db(name, Settings);
db.clear(GetResult);
Semaphore.acquire();
REQUIRE(Result.type == Error::Type::None);
db.open(key, GetResult);
Semaphore.acquire();
REQUIRE(Result.type == Error::Type::None);
db.put(Key{ 0, 1 }, TestValue1, GetResult);
Semaphore.acquire();
REQUIRE(Result.type == Error::Type::None);
db.put(Key{ 1, 0 }, TestValue2, GetResult);
Semaphore.acquire();
REQUIRE(Result.type == Error::Type::None);
db.remove(Key{ 0, 1 }, nullptr);
db.get(Key{ 0, 1 }, GetValue);
Semaphore.acquire();
REQUIRE(Value.isEmpty());
db.get(Key{ 1, 0 }, GetValue);
Semaphore.acquire();
REQUIRE((Value == TestValue2));
db.close([&] { Semaphore.release(); });
Semaphore.acquire();
}
SECTION("db remove deletes value permanently") {
Database db(name, Settings);
db.open(key, GetResult);
Semaphore.acquire();
REQUIRE(Result.type == Error::Type::None);
db.get(Key{ 1, 0 }, GetValue);
Semaphore.acquire();
REQUIRE((Value == TestValue2));
db.close([&] { Semaphore.release(); });
Semaphore.acquire();
}
}
TEST_CASE("cache db bundled actions", "[storage_cache_database]") {
SECTION("db touched written lazily") {
auto settings = Settings;
settings.trackEstimatedTime = true;
Database db(name, settings);
db.clear(GetResult);
Semaphore.acquire();
REQUIRE(Result.type == Error::Type::None);
db.open(key, GetResult);
Semaphore.acquire();
REQUIRE(Result.type == Error::Type::None);
const auto path = GetBinlogPath();
db.put(Key{ 0, 1 }, TestValue1, GetResult);
Semaphore.acquire();
REQUIRE(Result.type == Error::Type::None);
const auto size = QFile(path).size();
db.get(Key{ 0, 1 }, GetValue);
Semaphore.acquire();
REQUIRE((Value == TestValue1));
const auto same = QFile(path).size();
REQUIRE(same == size);
AdvanceTime(2);
const auto next = QFile(path).size();
REQUIRE(next > size);
db.close([&] { Semaphore.release(); });
Semaphore.acquire();
}
SECTION("db touched written on close") {
auto settings = Settings;
settings.trackEstimatedTime = true;
Database db(name, settings);
db.clear(GetResult);
Semaphore.acquire();
REQUIRE(Result.type == Error::Type::None);
db.open(key, GetResult);
Semaphore.acquire();
REQUIRE(Result.type == Error::Type::None);
const auto path = GetBinlogPath();
db.put(Key{ 0, 1 }, TestValue1, GetResult);
Semaphore.acquire();
REQUIRE(Result.type == Error::Type::None);
const auto size = QFile(path).size();
db.get(Key{ 0, 1 }, GetValue);
Semaphore.acquire();
REQUIRE((Value == TestValue1));
const auto same = QFile(path).size();
REQUIRE(same == size);
db.close([&] { Semaphore.release(); });
Semaphore.acquire();
const auto next = QFile(path).size();
REQUIRE(next > size);
}
SECTION("db remove written lazily") {
Database db(name, Settings);
db.clear(GetResult);
Semaphore.acquire();
REQUIRE(Result.type == Error::Type::None);
db.open(key, GetResult);
Semaphore.acquire();
REQUIRE(Result.type == Error::Type::None);
const auto path = GetBinlogPath();
db.put(Key{ 0, 1 }, TestValue1, GetResult);
Semaphore.acquire();
REQUIRE(Result.type == Error::Type::None);
const auto size = QFile(path).size();
db.remove(Key{ 0, 1 }, [&] { Semaphore.release(); });
Semaphore.acquire();
const auto same = QFile(path).size();
REQUIRE(same == size);
AdvanceTime(2);
const auto next = QFile(path).size();
REQUIRE(next > size);
db.close([&] { Semaphore.release(); });
Semaphore.acquire();
}
SECTION("db remove written on close") {
Database db(name, Settings);
db.clear(GetResult);
Semaphore.acquire();
REQUIRE(Result.type == Error::Type::None);
db.open(key, GetResult);
Semaphore.acquire();
REQUIRE(Result.type == Error::Type::None);
const auto path = GetBinlogPath();
db.put(Key{ 0, 1 }, TestValue1, GetResult);
Semaphore.acquire();
REQUIRE(Result.type == Error::Type::None);
const auto size = QFile(path).size();
db.remove(Key{ 0, 1 }, [&] { Semaphore.release(); });
Semaphore.acquire();
const auto same = QFile(path).size();
REQUIRE(same == size);
db.close([&] { Semaphore.release(); });
Semaphore.acquire();
const auto next = QFile(path).size();
REQUIRE(next > size);
}
}
TEST_CASE("cache db limits", "[storage_cache_database]") {
SECTION("db both limit") {
auto settings = Settings;
settings.trackEstimatedTime = true;
settings.totalSizeLimit = 17 * 3 + 1;
settings.totalTimeLimit = 4;
Database db(name, settings);
db.clear(nullptr);
db.open(key, nullptr);
db.put(Key{ 0, 1 }, TestValue1, nullptr);
db.put(Key{ 1, 0 }, TestValue2, nullptr);
AdvanceTime(2);
db.get(Key{ 1, 0 }, nullptr);
AdvanceTime(3);
db.put(Key{ 1, 1 }, TestValue1, nullptr);
db.put(Key{ 2, 0 }, TestValue2, nullptr);
db.put(Key{ 0, 2 }, TestValue1, nullptr);
AdvanceTime(2);
db.get(Key{ 0, 1 }, GetValue);
Semaphore.acquire();
REQUIRE(Value.isEmpty());
db.get(Key{ 1, 0 }, GetValue);
Semaphore.acquire();
REQUIRE(Value.isEmpty());
db.get(Key{ 1, 1 }, GetValue);
Semaphore.acquire();
REQUIRE((Value == TestValue1));
db.get(Key{ 2, 0 }, GetValue);
Semaphore.acquire();
REQUIRE((Value == TestValue2));
db.get(Key{ 0, 2 }, GetValue);
Semaphore.acquire();
REQUIRE((Value == TestValue1));
db.close([&] { Semaphore.release(); });
Semaphore.acquire();
}
SECTION("db size limit") {
auto settings = Settings;
settings.trackEstimatedTime = true;
settings.totalSizeLimit = 17 * 3 + 1;
Database db(name, settings);
db.clear(nullptr);
db.open(key, nullptr);
db.put(Key{ 0, 1 }, TestValue1, nullptr);
AdvanceTime(2);
db.put(Key{ 1, 0 }, TestValue2, nullptr);
AdvanceTime(2);
db.put(Key{ 1, 1 }, TestValue1, nullptr);
db.get(Key{ 0, 1 }, nullptr);
AdvanceTime(2);
db.put(Key{ 2, 0 }, TestValue2, nullptr);
// Removing { 1, 0 } will be scheduled.
db.get(Key{ 0, 1 }, GetValue);
Semaphore.acquire();
REQUIRE((Value == TestValue1));
db.get(Key{ 1, 1 }, GetValue);
Semaphore.acquire();
REQUIRE((Value == TestValue1));
db.get(Key{ 2, 0 }, GetValue);
Semaphore.acquire();
REQUIRE((Value == TestValue2));
AdvanceTime(2);
// Removing { 1, 0 } performed.
db.get(Key{ 1, 0 }, GetValue);
Semaphore.acquire();
REQUIRE(Value.isEmpty());
db.get(Key{ 1, 1 }, GetValue);
Semaphore.acquire();
REQUIRE((Value == TestValue1));
db.put(Key{ 0, 2 }, TestValue1, nullptr);
db.put(Key{ 2, 2 }, TestValue2, GetResult);
Semaphore.acquire();
REQUIRE(Result.type == Error::Type::None);
// Removing { 0, 1 } and { 2, 0 } will be scheduled.
AdvanceTime(2);
// Removing { 0, 1 } and { 2, 0 } performed.
db.get(Key{ 0, 1 }, GetValue);
Semaphore.acquire();
REQUIRE(Value.isEmpty());
db.get(Key{ 2, 0 }, GetValue);
Semaphore.acquire();
REQUIRE(Value.isEmpty());
db.get(Key{ 1, 1 }, GetValue);
Semaphore.acquire();
REQUIRE((Value == TestValue1));
db.get(Key{ 0, 2 }, GetValue);
Semaphore.acquire();
REQUIRE((Value == TestValue1));
db.get(Key{ 2, 2 }, GetValue);
Semaphore.acquire();
REQUIRE((Value == TestValue2));
db.close([&] { Semaphore.release(); });
Semaphore.acquire();
}
SECTION("db time limit") {
auto settings = Settings;
settings.trackEstimatedTime = true;
settings.totalTimeLimit = 3;
Database db(name, settings);
db.clear(nullptr);
db.open(key, nullptr);
db.put(Key{ 0, 1 }, TestValue1, nullptr);
db.put(Key{ 1, 0 }, TestValue2, nullptr);
db.put(Key{ 1, 1 }, TestValue1, nullptr);
db.put(Key{ 2, 0 }, TestValue2, nullptr);
AdvanceTime(1);
db.get(Key{ 1, 0 }, nullptr);
db.get(Key{ 1, 1 }, nullptr);
AdvanceTime(1);
db.get(Key{ 1, 0 }, nullptr);
db.get(Key{ 0, 1 }, nullptr);
AdvanceTime(1);
db.get(Key{ 1, 0 }, nullptr);
db.get(Key{ 0, 1 }, nullptr);
AdvanceTime(3);
db.get(Key{ 2, 0 }, GetValue);
Semaphore.acquire();
REQUIRE(Value.isEmpty());
db.get(Key{ 1, 1 }, GetValue);
Semaphore.acquire();
REQUIRE(Value.isEmpty());
db.get(Key{ 1, 0 }, GetValue);
Semaphore.acquire();
REQUIRE((Value == TestValue2));
db.get(Key{ 0, 1 }, GetValue);
Semaphore.acquire();
REQUIRE((Value == TestValue1));
db.close([&] { Semaphore.release(); });
Semaphore.acquire();
}
}