// 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/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 class FakeKeyValueTable : public KeyValueTable { public: FakeKeyValueTable() : sqlite_proto::KeyValueTable("") {} void GetAllData(std::map* 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& keys, sql::Database* db) override { for (const auto& key : keys) data_.erase(key); } void DeleteAllData(sql::Database* db) override { data_.clear(); } std::map data_; }; class FakeTableManager : public TableManager { public: FakeTableManager() : TableManager(base::ThreadTaskRunnerHandle::Get()) {} void ScheduleDBTask(const base::Location& from_here, base::OnceCallback task) override { GetTaskRunner()->PostTask( from_here, base::BindOnce(&TableManager::ExecuteDBTaskOnDBSequence, this, std::move(task))); } void ExecuteDBTaskOnDBSequence( base::OnceCallback task) override { ASSERT_TRUE(GetTaskRunner()->RunsTasksInCurrentSequence()); std::move(task).Run(DB()); } protected: ~FakeTableManager() override = default; // TableManager void CreateOrClearTablesIfNecessary() 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()), data_(manager_, &table_, absl::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 table_; scoped_refptr manager_ = base::MakeRefCounted(); KeyValueData 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{"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 table; base::test::TaskEnvironment env; auto manager = base::MakeRefCounted(); KeyValueData 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 table; base::test::TaskEnvironment env; auto manager = base::MakeRefCounted(); // Initialization: write a table of size 2 to |manager|'s backend. { KeyValueData data( manager, &table, /*max_num_entries=*/absl::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 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 data( manager, &table, /*max_num_entries=*/absl::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