|  | // Copyright 2020 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/sqlite_proto/key_value_data.h" | 
|  |  | 
|  | #include "base/memory/scoped_refptr.h" | 
|  | #include "base/strings/stringprintf.h" | 
|  | #include "base/task/post_task.h" | 
|  | #include "base/test/task_environment.h" | 
|  | #include "base/threading/thread_task_runner_handle.h" | 
|  | #include "components/sqlite_proto/table_manager.h" | 
|  | #include "components/sqlite_proto/test_proto.pb.h" | 
|  | #include "sql/database.h" | 
|  | #include "testing/gmock/include/gmock/gmock.h" | 
|  | #include "testing/gtest/include/gtest/gtest.h" | 
|  |  | 
|  | namespace sqlite_proto { | 
|  |  | 
|  | namespace { | 
|  |  | 
|  | template <typename T> | 
|  | class FakeKeyValueTable : public KeyValueTable<T> { | 
|  | public: | 
|  | FakeKeyValueTable() : sqlite_proto::KeyValueTable<T>("") {} | 
|  | void GetAllData(std::map<std::string, T>* data_map, | 
|  | sql::Database* db) const override { | 
|  | *data_map = data_; | 
|  | } | 
|  | void UpdateData(const std::string& key, | 
|  | const T& data, | 
|  | sql::Database* db) override { | 
|  | data_[key] = data; | 
|  | } | 
|  | void DeleteData(const std::vector<std::string>& keys, | 
|  | sql::Database* db) override { | 
|  | for (const auto& key : keys) | 
|  | data_.erase(key); | 
|  | } | 
|  | void DeleteAllData(sql::Database* db) override { data_.clear(); } | 
|  |  | 
|  | std::map<std::string, T> data_; | 
|  | }; | 
|  |  | 
|  | class FakeTableManager : public TableManager { | 
|  | public: | 
|  | FakeTableManager() : TableManager(base::ThreadTaskRunnerHandle::Get()) {} | 
|  | void ScheduleDBTask(const base::Location& from_here, | 
|  | base::OnceCallback<void(sql::Database*)> task) override { | 
|  | GetTaskRunner()->PostTask( | 
|  | from_here, base::BindOnce(&TableManager::ExecuteDBTaskOnDBSequence, | 
|  | this, std::move(task))); | 
|  | } | 
|  | void ExecuteDBTaskOnDBSequence( | 
|  | base::OnceCallback<void(sql::Database*)> task) override { | 
|  | ASSERT_TRUE(GetTaskRunner()->RunsTasksInCurrentSequence()); | 
|  | std::move(task).Run(DB()); | 
|  | } | 
|  |  | 
|  | protected: | 
|  | ~FakeTableManager() override = default; | 
|  |  | 
|  | // TableManager | 
|  | void CreateTablesIfNonExistent() override {} | 
|  | void LogDatabaseStats() override {} | 
|  | }; | 
|  |  | 
|  | MATCHER_P(EqualsProto, | 
|  | message, | 
|  | "Match a proto Message equal to the matcher's argument.") { | 
|  | std::string expected_serialized, actual_serialized; | 
|  | message.SerializeToString(&expected_serialized); | 
|  | arg.SerializeToString(&actual_serialized); | 
|  | return expected_serialized == actual_serialized; | 
|  | } | 
|  |  | 
|  | struct TestProtoCompare { | 
|  | bool operator()(const TestProto& lhs, const TestProto& rhs) { | 
|  | return lhs.value() < rhs.value(); | 
|  | } | 
|  | }; | 
|  |  | 
|  | }  // namespace | 
|  |  | 
|  | class KeyValueDataTest : public ::testing::Test { | 
|  | public: | 
|  | KeyValueDataTest() | 
|  | : manager_(base::MakeRefCounted<FakeTableManager>()), | 
|  | data_(manager_, &table_, base::nullopt, base::TimeDelta()) { | 
|  | // In these tests, we're using the current thread as the DB sequence. | 
|  | data_.InitializeOnDBSequence(); | 
|  | } | 
|  |  | 
|  | ~KeyValueDataTest() override = default; | 
|  |  | 
|  | protected: | 
|  | base::test::TaskEnvironment env_; | 
|  | FakeKeyValueTable<TestProto> table_; | 
|  | scoped_refptr<TableManager> manager_ = | 
|  | base::MakeRefCounted<FakeTableManager>(); | 
|  | KeyValueData<TestProto, TestProtoCompare> data_; | 
|  | }; | 
|  |  | 
|  | TEST_F(KeyValueDataTest, GetWhenEmpty) { | 
|  | TestProto result; | 
|  | EXPECT_FALSE(data_.TryGetData("nonexistent_key", &result)); | 
|  | } | 
|  |  | 
|  | TEST_F(KeyValueDataTest, PutAndGet) { | 
|  | TestProto first_entry, second_entry; | 
|  | first_entry.set_value(1); | 
|  | second_entry.set_value(1); | 
|  |  | 
|  | data_.UpdateData("a", first_entry); | 
|  | data_.UpdateData("b", second_entry); | 
|  |  | 
|  | TestProto result; | 
|  | ASSERT_TRUE(data_.TryGetData("a", &result)); | 
|  | EXPECT_THAT(result, EqualsProto(first_entry)); | 
|  |  | 
|  | ASSERT_TRUE(data_.TryGetData("b", &result)); | 
|  | EXPECT_THAT(result, EqualsProto(second_entry)); | 
|  | } | 
|  |  | 
|  | // Test that deleting one entry: | 
|  | // - makes that entry inaccessible, but | 
|  | // - does not affect the remaining entry. | 
|  | TEST_F(KeyValueDataTest, Delete) { | 
|  | TestProto first_entry, second_entry; | 
|  | first_entry.set_value(1); | 
|  | second_entry.set_value(1); | 
|  |  | 
|  | data_.UpdateData("a", first_entry); | 
|  | data_.UpdateData("b", second_entry); | 
|  |  | 
|  | TestProto result; | 
|  | data_.DeleteData(std::vector<std::string>{"b"}); | 
|  | EXPECT_FALSE(data_.TryGetData("b", &result)); | 
|  |  | 
|  | ASSERT_TRUE(data_.TryGetData("a", &result)); | 
|  | EXPECT_THAT(result, EqualsProto(first_entry)); | 
|  | } | 
|  |  | 
|  | TEST_F(KeyValueDataTest, DeleteAll) { | 
|  | TestProto first_entry, second_entry; | 
|  | first_entry.set_value(1); | 
|  | second_entry.set_value(1); | 
|  |  | 
|  | data_.UpdateData("a", first_entry); | 
|  | data_.UpdateData("b", second_entry); | 
|  |  | 
|  | data_.DeleteAllData(); | 
|  |  | 
|  | EXPECT_TRUE(data_.GetAllCached().empty()); | 
|  | } | 
|  |  | 
|  | TEST(KeyValueDataTestSize, CantAddToFullTable) { | 
|  | FakeKeyValueTable<TestProto> table; | 
|  | base::test::TaskEnvironment env; | 
|  |  | 
|  | auto manager = base::MakeRefCounted<FakeTableManager>(); | 
|  | KeyValueData<TestProto, TestProtoCompare> data( | 
|  | manager, &table, /*max_num_entries=*/2, | 
|  | /*flush_delay=*/base::TimeDelta()); | 
|  | // In these tests, we're using the current thread as the DB sequence. | 
|  | data.InitializeOnDBSequence(); | 
|  |  | 
|  | TestProto one_entry, two_entry, three_entry; | 
|  | one_entry.set_value(1); | 
|  | two_entry.set_value(2); | 
|  | three_entry.set_value(3); | 
|  |  | 
|  | data.UpdateData("a", one_entry); | 
|  | data.UpdateData("b", two_entry); | 
|  | data.UpdateData("c", three_entry); | 
|  |  | 
|  | EXPECT_EQ(data.GetAllCached().size(), 2u); | 
|  | } | 
|  |  | 
|  | // Test that building a KeyValueData on top of a backend table | 
|  | // with more than |max_num_entries| many entries leads to the table | 
|  | // being pruned down to a number of entries equal to the KeyValueData's | 
|  | // capacity. | 
|  | TEST(KeyValueDataTestSize, PrunesOverlargeTable) { | 
|  | FakeKeyValueTable<TestProto> table; | 
|  | base::test::TaskEnvironment env; | 
|  |  | 
|  | auto manager = base::MakeRefCounted<FakeTableManager>(); | 
|  |  | 
|  | // Initialization: write a table of size 2 to |manager|'s backend. | 
|  | { | 
|  | KeyValueData<TestProto, TestProtoCompare> data( | 
|  | manager, &table, /*max_num_entries=*/base::nullopt, | 
|  | /*flush_delay=*/base::TimeDelta()); | 
|  | // In these tests, we're using the current thread as the DB sequence. | 
|  | data.InitializeOnDBSequence(); | 
|  |  | 
|  | TestProto one_entry, two_entry; | 
|  | one_entry.set_value(1); | 
|  | two_entry.set_value(2); | 
|  |  | 
|  | data.UpdateData("a", one_entry); | 
|  | data.UpdateData("b", two_entry); | 
|  |  | 
|  | // Write changes through to the "disk." | 
|  | env.RunUntilIdle(); | 
|  | } | 
|  |  | 
|  | { | 
|  | KeyValueData<TestProto, TestProtoCompare> data( | 
|  | manager, &table, /*max_num_entries=*/1, | 
|  | /*flush_delay=*/base::TimeDelta()); | 
|  | // In these tests, we're using the current thread as the DB sequence. | 
|  | data.InitializeOnDBSequence(); | 
|  |  | 
|  | // A cache with size limit less than the size of the database | 
|  | // should load items up to its capacity (evicting the rest). | 
|  | EXPECT_EQ(data.GetAllCached().size(), 1u); | 
|  | } | 
|  |  | 
|  | { | 
|  | KeyValueData<TestProto, TestProtoCompare> data( | 
|  | manager, &table, /*max_num_entries=*/base::nullopt, | 
|  | /*flush_delay=*/base::TimeDelta()); | 
|  | // In these tests, we're using the current thread as the DB sequence. | 
|  | data.InitializeOnDBSequence(); | 
|  |  | 
|  | // The second, max_num_elements=1, cache should have pruned | 
|  | // the database to a single element upon initialization. | 
|  | EXPECT_EQ(data.GetAllCached().size(), 1u); | 
|  | } | 
|  | } | 
|  |  | 
|  | }  // namespace sqlite_proto |