| // Copyright 2018 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/image_fetcher/core/cache/image_metadata_store_leveldb.h" |
| |
| #include <map> |
| #include <utility> |
| |
| #include "base/bind.h" |
| #include "base/test/scoped_task_environment.h" |
| #include "base/test/simple_test_clock.h" |
| #include "components/image_fetcher/core/cache/image_store_types.h" |
| #include "components/image_fetcher/core/cache/proto/cached_image_metadata.pb.h" |
| #include "components/leveldb_proto/testing/fake_db.h" |
| #include "testing/gmock/include/gmock/gmock.h" |
| #include "testing/gtest/include/gtest/gtest.h" |
| |
| using leveldb_proto::test::FakeDB; |
| using testing::Mock; |
| using testing::_; |
| |
| namespace image_fetcher { |
| |
| namespace { |
| |
| constexpr char kImageKey[] = "http://cat.org/cat.jpg"; |
| constexpr char kOtherImageKey[] = "http://cat.org/dog.jpg"; |
| constexpr int kImageDataLength = 5; |
| |
| } // namespace |
| |
| class CachedImageFetcherImageMetadataStoreLevelDBTest : public testing::Test { |
| public: |
| CachedImageFetcherImageMetadataStoreLevelDBTest() : db_(nullptr) {} |
| |
| void CreateDatabase() { |
| // Reset everything. |
| db_ = nullptr; |
| clock_ = nullptr; |
| metadata_store_.reset(); |
| |
| // Setup the clock. |
| clock_ = std::make_unique<base::SimpleTestClock>(); |
| clock_->SetNow(base::Time()); |
| |
| // Setup the fake db and the class under test. |
| auto db = std::make_unique<FakeDB<CachedImageMetadataProto>>(&db_store_); |
| db_ = db.get(); |
| metadata_store_ = std::make_unique<ImageMetadataStoreLevelDB>(std::move(db), |
| clock_.get()); |
| } |
| |
| void InitializeDatabase() { |
| EXPECT_CALL(*this, OnInitialized()); |
| metadata_store()->Initialize(base::BindOnce( |
| &CachedImageFetcherImageMetadataStoreLevelDBTest::OnInitialized, |
| base::Unretained(this))); |
| db()->InitStatusCallback(leveldb_proto::Enums::InitStatus::kOK); |
| |
| RunUntilIdle(); |
| } |
| |
| void PrepareDatabase(bool initialize) { |
| CreateDatabase(); |
| InitializeDatabase(); |
| metadata_store()->SaveImageMetadata(kImageKey, kImageDataLength); |
| ASSERT_TRUE(IsDataPresent(kImageKey)); |
| |
| if (!initialize) { |
| CreateDatabase(); |
| } |
| } |
| |
| void RunGarbageCollection(base::TimeDelta move_clock_forward, |
| base::TimeDelta time_alive, |
| KeysCallback callback) { |
| RunGarbageCollection(move_clock_forward, time_alive, std::move(callback), |
| false, 0); |
| } |
| |
| void RunGarbageCollection(base::TimeDelta move_clock_forward_time, |
| base::TimeDelta look_back_time, |
| KeysCallback callback, |
| bool specify_bytes, |
| size_t bytes_left) { |
| clock()->SetNow(clock()->Now() + move_clock_forward_time); |
| |
| if (specify_bytes) { |
| metadata_store()->EvictImageMetadata(clock()->Now() - look_back_time, |
| bytes_left, std::move(callback)); |
| } else { |
| metadata_store()->EvictImageMetadata(clock()->Now() - look_back_time, |
| std::move(callback)); |
| } |
| } |
| |
| // Returns true if the data is present for the given key. |
| bool IsDataPresent(const std::string& key) { |
| return db_store_.find(key) != db_store_.end(); |
| } |
| |
| void AssertDataPresent(const std::string& key, |
| int64_t data_size, |
| base::Time creation_time, |
| base::Time last_used_time) { |
| if (!IsDataPresent(key)) { |
| ASSERT_TRUE(false); |
| } |
| |
| auto entry = db_store_[key]; |
| ASSERT_EQ(entry.data_size(), data_size); |
| ASSERT_EQ(entry.creation_time(), |
| creation_time.since_origin().InMicroseconds()); |
| ASSERT_EQ(entry.last_used_time(), |
| last_used_time.since_origin().InMicroseconds()); |
| } |
| |
| void RunUntilIdle() { scoped_task_environment_.RunUntilIdle(); } |
| base::SimpleTestClock* clock() { return clock_.get(); } |
| ImageMetadataStore* metadata_store() { return metadata_store_.get(); } |
| FakeDB<CachedImageMetadataProto>* db() { return db_; } |
| |
| MOCK_METHOD0(OnInitialized, void()); |
| MOCK_METHOD1(OnKeysReturned, void(std::vector<std::string>)); |
| MOCK_METHOD1(OnStoreOperationComplete, void(bool)); |
| |
| private: |
| std::unique_ptr<base::SimpleTestClock> clock_; |
| FakeDB<CachedImageMetadataProto>* db_; |
| std::map<std::string, CachedImageMetadataProto> db_store_; |
| std::unique_ptr<ImageMetadataStoreLevelDB> metadata_store_; |
| |
| base::test::ScopedTaskEnvironment scoped_task_environment_; |
| |
| DISALLOW_COPY_AND_ASSIGN(CachedImageFetcherImageMetadataStoreLevelDBTest); |
| }; |
| |
| TEST_F(CachedImageFetcherImageMetadataStoreLevelDBTest, Initialize) { |
| CreateDatabase(); |
| |
| EXPECT_FALSE(metadata_store()->IsInitialized()); |
| InitializeDatabase(); |
| EXPECT_TRUE(metadata_store()->IsInitialized()); |
| } |
| |
| TEST_F(CachedImageFetcherImageMetadataStoreLevelDBTest, SaveBeforeInit) { |
| CreateDatabase(); |
| EXPECT_FALSE(metadata_store()->IsInitialized()); |
| // Start an image load before the database is initialized. |
| metadata_store()->SaveImageMetadata(kImageKey, kImageDataLength); |
| |
| InitializeDatabase(); |
| EXPECT_TRUE(metadata_store()->IsInitialized()); |
| |
| ASSERT_FALSE(IsDataPresent(kImageKey)); |
| } |
| |
| TEST_F(CachedImageFetcherImageMetadataStoreLevelDBTest, Save) { |
| CreateDatabase(); |
| InitializeDatabase(); |
| |
| metadata_store()->SaveImageMetadata(kImageKey, kImageDataLength); |
| AssertDataPresent(kImageKey, kImageDataLength, clock()->Now(), |
| clock()->Now()); |
| } |
| |
| TEST_F(CachedImageFetcherImageMetadataStoreLevelDBTest, DeleteBeforeInit) { |
| PrepareDatabase(false); |
| metadata_store()->DeleteImageMetadata(kImageKey); |
| |
| InitializeDatabase(); |
| ASSERT_TRUE(IsDataPresent(kImageKey)); |
| } |
| |
| TEST_F(CachedImageFetcherImageMetadataStoreLevelDBTest, Delete) { |
| // Put some data in the database to start. |
| CreateDatabase(); |
| InitializeDatabase(); |
| metadata_store()->SaveImageMetadata(kImageKey, kImageDataLength); |
| ASSERT_TRUE(IsDataPresent(kImageKey)); |
| |
| // Delete the data. |
| metadata_store()->DeleteImageMetadata(kImageKey); |
| RunUntilIdle(); |
| |
| ASSERT_FALSE(IsDataPresent(kImageKey)); |
| } |
| |
| TEST_F(CachedImageFetcherImageMetadataStoreLevelDBTest, DeleteDifferentKey) { |
| // Put some data in the database to start. |
| CreateDatabase(); |
| InitializeDatabase(); |
| metadata_store()->SaveImageMetadata(kImageKey, kImageDataLength); |
| ASSERT_TRUE(IsDataPresent(kImageKey)); |
| |
| // Delete the data. |
| metadata_store()->DeleteImageMetadata(kOtherImageKey); |
| RunUntilIdle(); |
| |
| ASSERT_TRUE(IsDataPresent(kImageKey)); |
| } |
| |
| TEST_F(CachedImageFetcherImageMetadataStoreLevelDBTest, |
| UpdateImageMetadataBeforeInit) { |
| PrepareDatabase(false); |
| |
| // Call should be ignored because the store isn't initialized. |
| metadata_store()->UpdateImageMetadata(kImageKey); |
| RunUntilIdle(); |
| |
| InitializeDatabase(); |
| AssertDataPresent(kImageKey, kImageDataLength, clock()->Now(), |
| clock()->Now()); |
| } |
| |
| TEST_F(CachedImageFetcherImageMetadataStoreLevelDBTest, UpdateImageMetadata) { |
| PrepareDatabase(true); |
| |
| clock()->SetNow(base::Time() + base::TimeDelta::FromHours(1)); |
| metadata_store()->UpdateImageMetadata(kImageKey); |
| db()->LoadCallback(true); |
| db()->UpdateCallback(true); |
| RunUntilIdle(); |
| |
| AssertDataPresent(kImageKey, kImageDataLength, |
| clock()->Now() - base::TimeDelta::FromHours(1), |
| clock()->Now()); |
| } |
| |
| TEST_F(CachedImageFetcherImageMetadataStoreLevelDBTest, |
| UpdateImageMetadataNoHits) { |
| PrepareDatabase(true); |
| |
| metadata_store()->UpdateImageMetadata(kOtherImageKey); |
| db()->LoadCallback(true); |
| db()->UpdateCallback(true); |
| RunUntilIdle(); |
| |
| AssertDataPresent(kImageKey, kImageDataLength, clock()->Now(), |
| clock()->Now()); |
| } |
| |
| TEST_F(CachedImageFetcherImageMetadataStoreLevelDBTest, |
| UpdateImageMetadataLoadFailed) { |
| PrepareDatabase(true); |
| |
| metadata_store()->UpdateImageMetadata(kOtherImageKey); |
| db()->LoadCallback(true); |
| RunUntilIdle(); |
| |
| AssertDataPresent(kImageKey, kImageDataLength, clock()->Now(), |
| clock()->Now()); |
| } |
| |
| TEST_F(CachedImageFetcherImageMetadataStoreLevelDBTest, GetAllKeysBeforeInit) { |
| PrepareDatabase(false); |
| |
| // A GC call before the db is initialized should be ignore. |
| EXPECT_CALL(*this, OnKeysReturned(std::vector<std::string>())); |
| metadata_store()->GetAllKeys(base::BindOnce( |
| &CachedImageFetcherImageMetadataStoreLevelDBTest::OnKeysReturned, |
| base::Unretained(this))); |
| RunUntilIdle(); |
| } |
| |
| TEST_F(CachedImageFetcherImageMetadataStoreLevelDBTest, GetAllKeys) { |
| PrepareDatabase(true); |
| metadata_store()->SaveImageMetadata(kOtherImageKey, kImageDataLength); |
| |
| // A GC call before the db is initialized should be ignore. |
| EXPECT_CALL( |
| *this, |
| OnKeysReturned(std::vector<std::string>({kImageKey, kOtherImageKey}))); |
| metadata_store()->GetAllKeys(base::BindOnce( |
| &CachedImageFetcherImageMetadataStoreLevelDBTest::OnKeysReturned, |
| base::Unretained(this))); |
| db()->LoadKeysCallback(true); |
| } |
| |
| TEST_F(CachedImageFetcherImageMetadataStoreLevelDBTest, GetAllKeysLoadFailed) { |
| PrepareDatabase(true); |
| metadata_store()->SaveImageMetadata(kOtherImageKey, kImageDataLength); |
| |
| // A GC call before the db is initialized should be ignore. |
| EXPECT_CALL(*this, OnKeysReturned(std::vector<std::string>({}))); |
| metadata_store()->GetAllKeys(base::BindOnce( |
| &CachedImageFetcherImageMetadataStoreLevelDBTest::OnKeysReturned, |
| base::Unretained(this))); |
| db()->LoadKeysCallback(false); |
| } |
| |
| TEST_F(CachedImageFetcherImageMetadataStoreLevelDBTest, GetEstimatedSize) { |
| PrepareDatabase(true); |
| |
| EXPECT_EQ(5, metadata_store()->GetEstimatedSize()); |
| } |
| |
| TEST_F(CachedImageFetcherImageMetadataStoreLevelDBTest, |
| GarbageCollectBeforeInit) { |
| PrepareDatabase(false); |
| |
| // A GC call before the db is initialized should be ignore. |
| EXPECT_CALL(*this, OnKeysReturned(std::vector<std::string>())); |
| RunGarbageCollection( |
| base::TimeDelta::FromHours(1), base::TimeDelta::FromHours(1), |
| base::BindOnce( |
| &CachedImageFetcherImageMetadataStoreLevelDBTest::OnKeysReturned, |
| base::Unretained(this)), |
| true, 0); |
| RunUntilIdle(); |
| ASSERT_TRUE(IsDataPresent(kImageKey)); |
| } |
| |
| TEST_F(CachedImageFetcherImageMetadataStoreLevelDBTest, GarbageCollect) { |
| PrepareDatabase(true); |
| |
| // Calling GC with something to be collected. |
| EXPECT_CALL(*this, OnKeysReturned(std::vector<std::string>({kImageKey}))); |
| RunGarbageCollection( |
| base::TimeDelta::FromHours(1), base::TimeDelta::FromHours(1), |
| base::BindOnce( |
| &CachedImageFetcherImageMetadataStoreLevelDBTest::OnKeysReturned, |
| base::Unretained(this))); |
| db()->LoadCallback(true); |
| db()->UpdateCallback(true); |
| |
| ASSERT_FALSE(IsDataPresent(kImageKey)); |
| } |
| |
| TEST_F(CachedImageFetcherImageMetadataStoreLevelDBTest, GarbageCollectNoHits) { |
| PrepareDatabase(true); |
| EXPECT_CALL(*this, OnKeysReturned(std::vector<std::string>())); |
| |
| // Run GC without moving the clock forward, should result in no hits. |
| RunGarbageCollection( |
| base::TimeDelta::FromHours(0), base::TimeDelta::FromHours(1), |
| base::BindOnce( |
| &CachedImageFetcherImageMetadataStoreLevelDBTest::OnKeysReturned, |
| base::Unretained(this))); |
| db()->LoadCallback(true); |
| db()->UpdateCallback(true); |
| |
| ASSERT_TRUE(IsDataPresent(kImageKey)); |
| } |
| |
| TEST_F(CachedImageFetcherImageMetadataStoreLevelDBTest, |
| GarbageCollectWithBytesProvided) { |
| PrepareDatabase(true); |
| |
| // Insert an item one our later. |
| clock()->SetNow(clock()->Now() + base::TimeDelta::FromHours(1)); |
| metadata_store()->SaveImageMetadata(kOtherImageKey, kImageDataLength); |
| clock()->SetNow(clock()->Now() - base::TimeDelta::FromHours(1)); |
| ASSERT_TRUE(IsDataPresent(kOtherImageKey)); |
| |
| EXPECT_CALL(*this, OnKeysReturned(std::vector<std::string>({kImageKey}))); |
| |
| // Run GC without moving the clock forward, should result in no hits. |
| // Byte limit set so the one garbage collected should be enough. The older |
| // entry should be gc'd kImageKey, the other should stay kOtherImageKey. |
| RunGarbageCollection( |
| base::TimeDelta::FromHours(1), base::TimeDelta::FromHours(1), |
| base::BindOnce( |
| &CachedImageFetcherImageMetadataStoreLevelDBTest::OnKeysReturned, |
| base::Unretained(this)), |
| true, 5); |
| db()->LoadCallback(true); |
| db()->UpdateCallback(true); |
| |
| ASSERT_FALSE(IsDataPresent(kImageKey)); |
| } |
| |
| TEST_F(CachedImageFetcherImageMetadataStoreLevelDBTest, |
| GarbageCollectNoHitsButBytesProvided) { |
| PrepareDatabase(true); |
| EXPECT_CALL(*this, OnKeysReturned(std::vector<std::string>({kImageKey}))); |
| |
| // Run GC without moving the clock forward, should result in no hits. |
| // Run GC with a byte limit of 0, everything should go. |
| RunGarbageCollection( |
| base::TimeDelta::FromHours(0), base::TimeDelta::FromHours(1), |
| base::BindOnce( |
| &CachedImageFetcherImageMetadataStoreLevelDBTest::OnKeysReturned, |
| base::Unretained(this)), |
| true, 0); |
| db()->LoadCallback(true); |
| db()->UpdateCallback(true); |
| |
| ASSERT_FALSE(IsDataPresent(kImageKey)); |
| } |
| |
| TEST_F(CachedImageFetcherImageMetadataStoreLevelDBTest, |
| GarbageCollectLoadFailed) { |
| PrepareDatabase(true); |
| EXPECT_CALL(*this, OnKeysReturned(std::vector<std::string>())); |
| |
| // Run GC but loading the entries failed, should return an empty list. |
| RunGarbageCollection( |
| base::TimeDelta::FromHours(1), base::TimeDelta::FromHours(1), |
| base::BindOnce( |
| &CachedImageFetcherImageMetadataStoreLevelDBTest::OnKeysReturned, |
| base::Unretained(this))); |
| db()->LoadCallback(false); |
| ASSERT_TRUE(IsDataPresent(kImageKey)); |
| } |
| |
| TEST_F(CachedImageFetcherImageMetadataStoreLevelDBTest, |
| GarbageCollectUpdateFailed) { |
| PrepareDatabase(true); |
| EXPECT_CALL(*this, OnKeysReturned(std::vector<std::string>())); |
| RunGarbageCollection( |
| base::TimeDelta::FromHours(1), base::TimeDelta::FromHours(1), |
| base::BindOnce( |
| &CachedImageFetcherImageMetadataStoreLevelDBTest::OnKeysReturned, |
| base::Unretained(this))); |
| db()->LoadCallback(true); |
| db()->UpdateCallback(false); |
| // Update failed only simlulates the callback, not the actual data behavior. |
| } |
| |
| } // namespace image_fetcher |