blob: 183c71f8780b5efe809143b2264d01681ee92f3b [file] [log] [blame]
// Copyright 2015 The Chromium 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 "components/sync/model_impl/model_type_store_backend.h"
#include <utility>
#include "base/memory/ptr_util.h"
#include "base/test/histogram_tester.h"
#include "components/sync/protocol/model_type_store_schema_descriptor.pb.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/leveldatabase/src/include/leveldb/db.h"
#include "third_party/leveldatabase/src/include/leveldb/env.h"
#include "third_party/leveldatabase/src/include/leveldb/options.h"
#include "third_party/leveldatabase/src/include/leveldb/status.h"
#include "third_party/leveldatabase/src/include/leveldb/write_batch.h"
using sync_pb::ModelTypeStoreSchemaDescriptor;
namespace syncer {
class ModelTypeStoreBackendTest : public testing::Test {
public:
scoped_refptr<ModelTypeStoreBackend> GetOrCreateBackend() {
std::string path = "/test_db";
return GetOrCreateBackendWithPath(path);
}
scoped_refptr<ModelTypeStoreBackend> GetOrCreateBackendWithPath(
std::string custom_path) {
std::unique_ptr<leveldb::Env> in_memory_env =
ModelTypeStoreBackend::CreateInMemoryEnv();
std::string path;
in_memory_env->GetTestDirectory(&path);
path += custom_path;
ModelTypeStore::Result result;
// In-memory store backend works on the same thread as test.
scoped_refptr<ModelTypeStoreBackend> backend =
ModelTypeStoreBackend::GetOrCreateBackend(
path, std::move(in_memory_env), &result);
EXPECT_TRUE(backend.get());
EXPECT_EQ(result, ModelTypeStore::Result::SUCCESS);
return backend;
}
// Create backend with custom env. This function is used in tests that need to
// prepare env in some way (create files) before passing it to leveldb.
scoped_refptr<ModelTypeStoreBackend> CreateBackendWithEnv(
std::unique_ptr<leveldb::Env> env,
const std::string& path) {
EXPECT_FALSE(BackendExistsForPath(path));
ModelTypeStore::Result result;
scoped_refptr<ModelTypeStoreBackend> backend =
ModelTypeStoreBackend::GetOrCreateBackend(path, std::move(env),
&result);
EXPECT_TRUE(backend.get());
EXPECT_EQ(result, ModelTypeStore::Result::SUCCESS);
return backend;
}
bool BackendExistsForPath(const std::string& path) {
return ModelTypeStoreBackend::BackendExistsForTest(path);
}
std::string GetBackendPath(scoped_refptr<ModelTypeStoreBackend> backend) {
return backend->path_;
}
ModelTypeStore::Result Migrate(scoped_refptr<ModelTypeStoreBackend> backend,
int64_t current_version,
int64_t desired_version) {
return backend->Migrate(current_version, desired_version);
}
bool Migrate0To1(scoped_refptr<ModelTypeStoreBackend> backend) {
return backend->Migrate0To1();
}
int64_t GetStoreVersion(scoped_refptr<ModelTypeStoreBackend> backend) {
return backend->GetStoreVersion();
}
int64_t LatestVersion() {
return ModelTypeStoreBackend::kLatestSchemaVersion;
}
const char* SchemaId() {
return ModelTypeStoreBackend::kDBSchemaDescriptorRecordId;
}
const char* StoreInitResultHistogramName() {
return ModelTypeStoreBackend::kStoreInitResultHistogramName;
}
};
// Test that after record is written to backend it can be read back even after
// backend is destroyed and recreated in the same environment.
TEST_F(ModelTypeStoreBackendTest, WriteThenRead) {
scoped_refptr<ModelTypeStoreBackend> backend = GetOrCreateBackend();
// Write record.
std::unique_ptr<leveldb::WriteBatch> write_batch(new leveldb::WriteBatch());
write_batch->Put("prefix:id1", "data1");
ModelTypeStore::Result result =
backend->WriteModifications(std::move(write_batch));
ASSERT_EQ(ModelTypeStore::Result::SUCCESS, result);
// Read all records with prefix.
ModelTypeStore::RecordList record_list;
result = backend->ReadAllRecordsWithPrefix("prefix:", &record_list);
ASSERT_EQ(ModelTypeStore::Result::SUCCESS, result);
ASSERT_EQ(1ul, record_list.size());
ASSERT_EQ("id1", record_list[0].id);
ASSERT_EQ("data1", record_list[0].value);
record_list.clear();
// Recreate backend and read all records with prefix.
backend = GetOrCreateBackend();
result = backend->ReadAllRecordsWithPrefix("prefix:", &record_list);
ASSERT_EQ(ModelTypeStore::Result::SUCCESS, result);
ASSERT_EQ(1ul, record_list.size());
ASSERT_EQ("id1", record_list[0].id);
ASSERT_EQ("data1", record_list[0].value);
}
// Test that ReadAllRecordsWithPrefix correclty filters records by prefix.
TEST_F(ModelTypeStoreBackendTest, ReadAllRecordsWithPrefix) {
scoped_refptr<ModelTypeStoreBackend> backend = GetOrCreateBackend();
std::unique_ptr<leveldb::WriteBatch> write_batch(new leveldb::WriteBatch());
write_batch->Put("prefix1:id1", "data1");
write_batch->Put("prefix2:id2", "data2");
ModelTypeStore::Result result =
backend->WriteModifications(std::move(write_batch));
ASSERT_EQ(ModelTypeStore::Result::SUCCESS, result);
ModelTypeStore::RecordList record_list;
result = backend->ReadAllRecordsWithPrefix("prefix1:", &record_list);
ASSERT_EQ(ModelTypeStore::Result::SUCCESS, result);
ASSERT_EQ(1UL, record_list.size());
ASSERT_EQ("id1", record_list[0].id);
ASSERT_EQ("data1", record_list[0].value);
}
// Test that deleted records are correctly marked as milling in results of
// ReadRecordsWithPrefix.
TEST_F(ModelTypeStoreBackendTest, ReadDeletedRecord) {
scoped_refptr<ModelTypeStoreBackend> backend = GetOrCreateBackend();
// Create records, ensure they are returned by ReadRecordsWithPrefix.
std::unique_ptr<leveldb::WriteBatch> write_batch(new leveldb::WriteBatch());
write_batch->Put("prefix:id1", "data1");
write_batch->Put("prefix:id2", "data2");
ModelTypeStore::Result result =
backend->WriteModifications(std::move(write_batch));
ASSERT_EQ(ModelTypeStore::Result::SUCCESS, result);
ModelTypeStore::IdList id_list;
ModelTypeStore::IdList missing_id_list;
ModelTypeStore::RecordList record_list;
id_list.push_back("id1");
id_list.push_back("id2");
result = backend->ReadRecordsWithPrefix("prefix:", id_list, &record_list,
&missing_id_list);
ASSERT_EQ(ModelTypeStore::Result::SUCCESS, result);
ASSERT_EQ(2UL, record_list.size());
ASSERT_TRUE(missing_id_list.empty());
// Delete one record.
write_batch = base::MakeUnique<leveldb::WriteBatch>();
write_batch->Delete("prefix:id2");
result = backend->WriteModifications(std::move(write_batch));
ASSERT_EQ(ModelTypeStore::Result::SUCCESS, result);
// Ensure deleted record id is returned in missing_id_list.
record_list.clear();
missing_id_list.clear();
result = backend->ReadRecordsWithPrefix("prefix:", id_list, &record_list,
&missing_id_list);
ASSERT_EQ(ModelTypeStore::Result::SUCCESS, result);
ASSERT_EQ(1UL, record_list.size());
ASSERT_EQ("id1", record_list[0].id);
ASSERT_EQ(1UL, missing_id_list.size());
ASSERT_EQ("id2", missing_id_list[0]);
}
// Test that only one backend got create when we ask two backend with same path,
// and after de-reference the backend, the backend will be deleted.
TEST_F(ModelTypeStoreBackendTest, TwoSameBackendTest) {
// Create two backend with same path, check if they are reference to same
// address.
scoped_refptr<ModelTypeStoreBackend> backend = GetOrCreateBackend();
scoped_refptr<ModelTypeStoreBackend> backend_second = GetOrCreateBackend();
std::string path = GetBackendPath(backend);
ASSERT_EQ(backend.get(), backend_second.get());
// Delete one reference, check the real backend still here.
backend = nullptr;
ASSERT_FALSE(backend.get());
ASSERT_TRUE(backend_second.get());
ASSERT_TRUE(backend_second->HasOneRef());
// Delete another reference, check the real backend is deleted.
backend_second = nullptr;
ASSERT_FALSE(backend_second.get());
ASSERT_FALSE(BackendExistsForPath(path));
}
// Test that two backend got create when we ask two backend with different path,
// and after de-reference two backend, the both backend will be deleted.
TEST_F(ModelTypeStoreBackendTest, TwoDifferentBackendTest) {
// Create two backend with different path, check if they are reference to
// different address.
scoped_refptr<ModelTypeStoreBackend> backend = GetOrCreateBackend();
scoped_refptr<ModelTypeStoreBackend> backend_second =
GetOrCreateBackendWithPath("/test_db2");
std::string path = GetBackendPath(backend);
ASSERT_NE(backend.get(), backend_second.get());
ASSERT_TRUE(backend->HasOneRef());
ASSERT_TRUE(backend_second->HasOneRef());
// delete one backend, check only one got deleted.
backend = nullptr;
ASSERT_FALSE(backend.get());
ASSERT_TRUE(backend_second.get());
ASSERT_TRUE(backend_second->HasOneRef());
ASSERT_FALSE(BackendExistsForPath(path));
// delete another backend.
backend_second = nullptr;
ASSERT_FALSE(backend_second.get());
ASSERT_FALSE(BackendExistsForPath("/test_db2"));
}
// Test that initializing the database migrates it to the latest schema version.
TEST_F(ModelTypeStoreBackendTest, MigrateNoSchemaVersionToLatestVersionTest) {
scoped_refptr<ModelTypeStoreBackend> backend = GetOrCreateBackend();
ASSERT_EQ(LatestVersion(), GetStoreVersion(backend));
}
// Test that the 0 to 1 migration succeeds and sets the schema version to 1.
TEST_F(ModelTypeStoreBackendTest, Migrate0To1Test) {
scoped_refptr<ModelTypeStoreBackend> backend = GetOrCreateBackend();
std::unique_ptr<leveldb::WriteBatch> write_batch(new leveldb::WriteBatch());
write_batch->Delete(SchemaId());
ModelTypeStore::Result result =
backend->WriteModifications(std::move(write_batch));
ASSERT_EQ(ModelTypeStore::Result::SUCCESS, result);
ASSERT_TRUE(Migrate0To1(backend));
ASSERT_EQ(1, GetStoreVersion(backend));
}
// Test that migration to an unknown version fails
TEST_F(ModelTypeStoreBackendTest, MigrateWithHigherExistingVersionFails) {
scoped_refptr<ModelTypeStoreBackend> backend = GetOrCreateBackend();
ASSERT_EQ(Migrate(backend, LatestVersion() + 1, LatestVersion()),
ModelTypeStore::Result::SCHEMA_VERSION_TOO_HIGH);
}
// Tests that initializing store after corruption triggers recovery and results
// in successful store initialization.
TEST_F(ModelTypeStoreBackendTest, RecoverAfterCorruption) {
base::HistogramTester tester;
leveldb::Status s;
// Prepare environment that looks corrupt to leveldb.
std::unique_ptr<leveldb::Env> env =
base::MakeUnique<leveldb::EnvWrapper>(leveldb::Env::Default());
std::string path;
env->GetTestDirectory(&path);
path += "/corrupt_db";
// Easiest way to simulate leveldb corruption is to create empty CURRENT file.
{
s = env->CreateDir(path);
EXPECT_TRUE(s.ok());
leveldb::WritableFile* current_file_raw;
s = env->NewWritableFile(path + "/CURRENT", &current_file_raw);
EXPECT_TRUE(s.ok());
current_file_raw->Close();
delete current_file_raw;
}
// CreateBackendWithEnv will ensure backend initialization is successful.
scoped_refptr<ModelTypeStoreBackend> backend =
CreateBackendWithEnv(std::move(env), path);
// Cleanup directory after the test.
backend = nullptr;
s = leveldb::DestroyDB(path, leveldb::Options());
EXPECT_TRUE(s.ok()) << s.ToString();
// Check that both recovery and consecutive initialization are recorded in
// histograms.
tester.ExpectBucketCount(StoreInitResultHistogramName(),
STORE_INIT_RESULT_SUCCESS, 1);
tester.ExpectBucketCount(StoreInitResultHistogramName(),
STORE_INIT_RESULT_RECOVERED_AFTER_CORRUPTION, 1);
}
} // namespace syncer