blob: e5b8f1e5daf4af2a2dc417f1a365434d34070052 [file] [log] [blame]
// Copyright 2016 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "src/ledger/bin/storage/impl/leveldb_factory.h"
#include <lib/async/cpp/task.h>
#include <memory>
#include "gmock/gmock.h"
#include "gtest/gtest.h"
#include "src/ledger/bin/platform/scoped_tmp_location.h"
#include "src/ledger/bin/testing/test_with_environment.h"
#include "src/ledger/lib/callback/capture.h"
#include "src/ledger/lib/callback/set_when_called.h"
#include "src/ledger/lib/files/detached_path.h"
#include "third_party/abseil-cpp/absl/strings/str_cat.h"
namespace storage {
namespace {
using testing::AnyOf;
class LevelDbFactoryTest : public ledger::TestWithEnvironment {
public:
// Wrapper around LevelDbFactory. This class is needed because LevelDbFactory
// can only be deleted while the loop is running so that it can synchronized
// with the io dispatcher.
class LevelDbFactoryWrapper {
public:
LevelDbFactoryWrapper(async::TestLoop* loop, ledger::Environment* environment,
ledger::DetachedPath cache_path)
: loop_(loop),
environment_(environment),
db_factory_(std::make_unique<LevelDbFactory>(environment, std::move(cache_path))) {}
~LevelDbFactoryWrapper() {
async::PostTask(environment_->dispatcher(), [this] { db_factory_.reset(); });
loop_->RunUntilIdle();
}
LevelDbFactory* operator->() const { return db_factory_.get(); };
private:
async::TestLoop* loop_;
ledger::Environment* environment_;
std::unique_ptr<LevelDbFactory> db_factory_;
};
LevelDbFactoryTest()
: tmp_location_(environment_.file_system()->CreateScopedTmpLocation()),
base_path_(tmp_location_->path()),
cache_path_(base_path_.SubPath("cache")),
db_path_(base_path_.SubPath("databases")),
db_factory_(&test_loop(), &environment_, cache_path_) {}
LevelDbFactoryTest(const LevelDbFactoryTest&) = delete;
LevelDbFactoryTest& operator=(const LevelDbFactoryTest&) = delete;
~LevelDbFactoryTest() override = default;
// ledger::TestWithEnvironment:
void SetUp() override {
ledger::TestWithEnvironment::SetUp();
ASSERT_TRUE(environment_.file_system()->CreateDirectory(cache_path_));
ASSERT_TRUE(environment_.file_system()->CreateDirectory(db_path_));
db_factory_->Init();
RunLoopUntilIdle();
}
private:
std::unique_ptr<ledger::ScopedTmpLocation> tmp_location_;
ledger::DetachedPath base_path_;
protected:
ledger::DetachedPath cache_path_;
ledger::DetachedPath db_path_;
LevelDbFactoryWrapper db_factory_;
};
TEST_F(LevelDbFactoryTest, GetOrCreateDb) {
// Create a new instance.
Status status;
std::unique_ptr<Db> db;
bool called;
db_factory_->GetOrCreateDb(db_path_.SubPath("db"), DbFactory::OnDbNotFound::CREATE,
ledger::Capture(ledger::SetWhenCalled(&called), &status, &db));
RunLoopUntilIdle();
ASSERT_TRUE(called);
EXPECT_EQ(status, Status::OK);
EXPECT_NE(nullptr, db);
// Write one key-value pair.
RunInCoroutine([&](coroutine::CoroutineHandler* handler) {
std::unique_ptr<Db::Batch> batch;
EXPECT_EQ(db->StartBatch(handler, &batch), Status::OK);
EXPECT_EQ(batch->Put(handler, "key", "value"), Status::OK);
EXPECT_EQ(batch->Execute(handler), Status::OK);
});
// Close the previous instance and open it again.
db.reset();
db_factory_->GetOrCreateDb(db_path_.SubPath("db"), DbFactory::OnDbNotFound::RETURN,
ledger::Capture(ledger::SetWhenCalled(&called), &status, &db));
RunLoopUntilIdle();
ASSERT_TRUE(called);
EXPECT_EQ(status, Status::OK);
EXPECT_NE(nullptr, db);
// Expect to find the previously written key-value pair.
RunInCoroutine([&](coroutine::CoroutineHandler* handler) {
std::string value;
EXPECT_EQ(db->Get(handler, "key", &value), Status::OK);
EXPECT_EQ(value, "value");
});
}
TEST_F(LevelDbFactoryTest, GetDbOnNotFound) {
// Try to get a non existing Db and expect a |PAGE_NOT_FOUND| status.
Status status;
std::unique_ptr<Db> db;
bool called;
db_factory_->GetOrCreateDb(db_path_.SubPath("db"), DbFactory::OnDbNotFound::RETURN,
ledger::Capture(ledger::SetWhenCalled(&called), &status, &db));
RunLoopUntilIdle();
ASSERT_TRUE(called);
EXPECT_EQ(status, Status::PAGE_NOT_FOUND);
EXPECT_EQ(db, nullptr);
}
TEST_F(LevelDbFactoryTest, CreateMultipleDbs) {
int N = 5;
Status status;
std::unique_ptr<Db> db;
bool called;
// Create N LevelDb instances, one after the other. All of them will use the
// existing cached instance and then, initialize the creation of a new one.
for (int i = 0; i < N; ++i) {
ledger::DetachedPath path = db_path_.SubPath(absl::StrCat(i));
EXPECT_FALSE(environment_.file_system()->IsDirectory(path));
db_factory_->GetOrCreateDb(path, DbFactory::OnDbNotFound::CREATE,
ledger::Capture(ledger::SetWhenCalled(&called), &status, &db));
RunLoopUntilIdle();
EXPECT_TRUE(called);
EXPECT_EQ(status, Status::OK);
EXPECT_NE(nullptr, db);
// Check that the directory was created.
EXPECT_TRUE(environment_.file_system()->IsDirectory(path));
}
}
TEST_F(LevelDbFactoryTest, CreateMultipleDbsConcurrently) {
int N = 5;
Status statuses[N];
std::unique_ptr<Db> dbs[N];
bool called[N];
// Create N LevelDb instances concurrently. The first one will use the cached
// instance, the 2nd one will be queued up to get the cached one when it's
// initialized, and all the others will be created directly at the destination
// directory.
for (int i = 0; i < N; ++i) {
ledger::DetachedPath path = db_path_.SubPath(absl::StrCat(i));
EXPECT_FALSE(environment_.file_system()->IsDirectory(path));
db_factory_->GetOrCreateDb(
db_path_.SubPath(absl::StrCat(i)), DbFactory::OnDbNotFound::CREATE,
ledger::Capture(ledger::SetWhenCalled(&called[i]), &statuses[i], &dbs[i]));
}
RunLoopUntilIdle();
for (int i = 0; i < N; ++i) {
ledger::DetachedPath path = db_path_.SubPath(absl::StrCat(i));
EXPECT_TRUE(called[i]);
EXPECT_EQ(statuses[i], Status::OK);
EXPECT_NE(nullptr, dbs[i]);
// Check that the directory was created.
EXPECT_TRUE(environment_.file_system()->IsDirectory(path));
}
}
TEST_F(LevelDbFactoryTest, GetOrCreateDbInCallback) {
bool called1;
ledger::DetachedPath path1 = db_path_.SubPath("1");
bool called2;
ledger::DetachedPath path2 = db_path_.SubPath("2");
Status status2;
std::unique_ptr<Db> db2;
db_factory_->GetOrCreateDb(
path1, DbFactory::OnDbNotFound::CREATE, [&](Status status1, std::unique_ptr<Db> db1) {
called1 = true;
EXPECT_EQ(status1, Status::OK);
EXPECT_NE(nullptr, db1);
db_factory_->GetOrCreateDb(
path2, DbFactory::OnDbNotFound::CREATE,
ledger::Capture(ledger::SetWhenCalled(&called2), &status2, &db2));
});
RunLoopUntilIdle();
EXPECT_TRUE(called1);
EXPECT_TRUE(called2);
EXPECT_EQ(status2, Status::OK);
EXPECT_NE(nullptr, db2);
// Check that the directories were created.
EXPECT_TRUE(environment_.file_system()->IsDirectory(path1));
EXPECT_TRUE(environment_.file_system()->IsDirectory(path2));
}
TEST_F(LevelDbFactoryTest, InitWithCachedDbAvailable) {
// When an empty LevelDb instance is already cached from a previous
// LevelDbFactory execution, don't create a new instance, but use the existing
// one directly.
std::unique_ptr<ledger::ScopedTmpLocation> tmp_location =
environment_.file_system()->CreateScopedTmpLocation();
ledger::DetachedPath cache_path = tmp_location->path().SubPath("cache");
// Must be the same as |kCachedDbPath| in leveldb_factory.cc.
ledger::DetachedPath cached_db_path = cache_path.SubPath("cached_db");
auto db_factory =
std::make_unique<LevelDbFactoryWrapper>(&test_loop(), &environment_, cache_path);
// The cached db directory should not be created, yet.
EXPECT_FALSE(environment_.file_system()->IsDirectory(cached_db_path));
// Initialize and wait for the cached instance to be created.
(*db_factory)->Init();
RunLoopUntilIdle();
// Close the factory. This will not affect the created cached instance, which
// was created under |cached_db_path|.
db_factory.reset();
EXPECT_TRUE(environment_.file_system()->IsDirectory(cached_db_path));
// Re-initialize the factory object. It should now use the previously created
// instance.
db_factory = std::make_unique<LevelDbFactoryWrapper>(&test_loop(), &environment_, cache_path);
(*db_factory)->Init();
RunLoopUntilIdle();
}
// Make sure we can destroy the factory while a request is in progress.
TEST_F(LevelDbFactoryTest, QuitWhenBusy) {
auto db_factory_ptr =
std::make_unique<LevelDbFactoryWrapper>(&test_loop(), &environment_, cache_path_);
(*db_factory_ptr)->Init();
RunLoopUntilIdle();
Status status;
std::unique_ptr<Db> db;
bool called;
// Post the initialization code to the I/O loop.
(*db_factory_ptr)
->GetOrCreateDb(db_path_.SubPath(absl::StrCat(0)), DbFactory::OnDbNotFound::CREATE,
ledger::Capture(ledger::SetWhenCalled(&called), &status, &db));
// Delete the factory before any code is run on the I/O loop. The destructor
// will block until all I/O operation are cancelled.
db_factory_ptr.reset();
// Pump all loops.
RunLoopUntilIdle();
// The behavior depends on what code is run on the I/O loop, vs main loop. If
// the destruction happens first, no callback is ever called and |called| is
// false. Otherwise, the callback can be called with either an OK status or a
// ILLEGAL_STATE status, depending on how far the operation progressed on the
// IO thread.
if (called) {
EXPECT_THAT(status, AnyOf(Status::OK, Status::ILLEGAL_STATE));
}
}
} // namespace
} // namespace storage