| // 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/cached_image_fetcher.h" |
| |
| #include <map> |
| #include <memory> |
| #include <string> |
| #include <utility> |
| #include <vector> |
| |
| #include "base/bind.h" |
| #include "base/files/scoped_temp_dir.h" |
| #include "base/memory/ref_counted.h" |
| #include "base/test/metrics/histogram_tester.h" |
| #include "base/test/mock_callback.h" |
| #include "base/test/scoped_task_environment.h" |
| #include "base/test/simple_test_clock.h" |
| #include "base/threading/sequenced_task_runner_handle.h" |
| #include "components/image_fetcher/core/cache/image_cache.h" |
| #include "components/image_fetcher/core/cache/image_data_store_disk.h" |
| #include "components/image_fetcher/core/cache/image_metadata_store_leveldb.h" |
| #include "components/image_fetcher/core/cache/proto/cached_image_metadata.pb.h" |
| #include "components/image_fetcher/core/fake_image_decoder.h" |
| #include "components/image_fetcher/core/image_fetcher_impl.h" |
| #include "components/image_fetcher/core/image_fetcher_metrics_reporter.h" |
| #include "components/image_fetcher/core/image_fetcher_types.h" |
| #include "components/leveldb_proto/testing/fake_db.h" |
| #include "components/prefs/testing_pref_service.h" |
| #include "net/traffic_annotation/network_traffic_annotation_test_helper.h" |
| #include "services/network/public/cpp/shared_url_loader_factory.h" |
| #include "services/network/public/cpp/weak_wrapper_shared_url_loader_factory.h" |
| #include "services/network/test/test_url_loader_factory.h" |
| #include "testing/gmock/include/gmock/gmock.h" |
| #include "testing/gtest/include/gtest/gtest.h" |
| #include "ui/gfx/image/image.h" |
| #include "ui/gfx/image/image_unittest_util.h" |
| |
| using testing::_; |
| using leveldb_proto::test::FakeDB; |
| |
| namespace image_fetcher { |
| |
| class FakeImageDecoder; |
| |
| namespace { |
| |
| const GURL kImageUrl = GURL("http://gstatic.img.com/foo.jpg"); |
| |
| constexpr char kUmaClientName[] = "TestUma"; |
| constexpr char kImageData[] = "data"; |
| constexpr char kImageDataOther[] = "other"; |
| |
| const char kImageFetcherEventHistogramName[] = "ImageFetcher.Events"; |
| const char kCacheLoadHistogramName[] = |
| "CachedImageFetcher.ImageLoadFromCacheTime"; |
| const char kNetworkLoadHistogramName[] = |
| "CachedImageFetcher.ImageLoadFromNetworkTime"; |
| |
| } // namespace |
| |
| class CachedImageFetcherTest : public testing::Test { |
| public: |
| CachedImageFetcherTest() {} |
| |
| ~CachedImageFetcherTest() override { |
| cached_image_fetcher_.reset(); |
| // We need to run until idle after deleting the database, because |
| // ProtoDatabase deletes the actual LevelDB asynchronously. |
| RunUntilIdle(); |
| } |
| |
| void SetUp() override { |
| ASSERT_TRUE(data_dir_.CreateUniqueTempDir()); |
| ImageCache::RegisterProfilePrefs(test_prefs_.registry()); |
| CreateCachedImageFetcher(false); |
| } |
| |
| void CreateCachedImageFetcher(bool read_only) { |
| auto db = |
| std::make_unique<FakeDB<CachedImageMetadataProto>>(&metadata_store_); |
| db_ = db.get(); |
| |
| auto metadata_store = |
| std::make_unique<ImageMetadataStoreLevelDB>(std::move(db), &clock_); |
| auto data_store = std::make_unique<ImageDataStoreDisk>( |
| data_dir_.GetPath(), base::SequencedTaskRunnerHandle::Get()); |
| |
| image_cache_ = base::MakeRefCounted<ImageCache>( |
| std::move(data_store), std::move(metadata_store), &test_prefs_, &clock_, |
| base::SequencedTaskRunnerHandle::Get()); |
| |
| // Use an initial request to start the cache up. |
| image_cache_->SaveImage(kImageUrl.spec(), kImageData); |
| RunUntilIdle(); |
| db_->InitStatusCallback(leveldb_proto::Enums::InitStatus::kOK); |
| image_cache_->DeleteImage(kImageUrl.spec()); |
| RunUntilIdle(); |
| |
| shared_factory_ = |
| base::MakeRefCounted<network::WeakWrapperSharedURLLoaderFactory>( |
| &test_url_loader_factory_); |
| |
| auto decoder = std::make_unique<FakeImageDecoder>(); |
| fake_image_decoder_ = decoder.get(); |
| |
| image_fetcher_ = std::make_unique<image_fetcher::ImageFetcherImpl>( |
| std::move(decoder), shared_factory_); |
| cached_image_fetcher_ = std::make_unique<CachedImageFetcher>( |
| image_fetcher_.get(), image_cache_, read_only); |
| |
| RunUntilIdle(); |
| } |
| |
| void RunUntilIdle() { scoped_task_environment_.RunUntilIdle(); } |
| |
| CachedImageFetcher* cached_image_fetcher() { |
| return cached_image_fetcher_.get(); |
| } |
| scoped_refptr<ImageCache> image_cache() { return image_cache_; } |
| FakeImageDecoder* image_decoder() { return fake_image_decoder_; } |
| network::TestURLLoaderFactory* test_url_loader_factory() { |
| return &test_url_loader_factory_; |
| } |
| base::HistogramTester& histogram_tester() { return histogram_tester_; } |
| |
| MOCK_METHOD1(OnImageLoaded, void(std::string)); |
| |
| private: |
| std::unique_ptr<ImageFetcher> image_fetcher_; |
| std::unique_ptr<CachedImageFetcher> cached_image_fetcher_; |
| network::TestURLLoaderFactory test_url_loader_factory_; |
| scoped_refptr<network::SharedURLLoaderFactory> shared_factory_; |
| FakeImageDecoder* fake_image_decoder_; |
| |
| scoped_refptr<ImageCache> image_cache_; |
| base::SimpleTestClock clock_; |
| TestingPrefServiceSimple test_prefs_; |
| base::ScopedTempDir data_dir_; |
| FakeDB<CachedImageMetadataProto>* db_; |
| std::map<std::string, CachedImageMetadataProto> metadata_store_; |
| |
| base::test::ScopedTaskEnvironment scoped_task_environment_; |
| base::HistogramTester histogram_tester_; |
| |
| DISALLOW_COPY_AND_ASSIGN(CachedImageFetcherTest); |
| }; |
| |
| MATCHER(EmptyImage, "") { |
| return arg.IsEmpty(); |
| } |
| |
| MATCHER(NonEmptyImage, "") { |
| return !arg.IsEmpty(); |
| } |
| |
| MATCHER(NonEmptyString, "") { |
| return !arg.empty(); |
| } |
| |
| // TODO(wylieb): Write a test that creates two CachedImageFetcher and tests |
| // that they both can use what's inside. |
| TEST_F(CachedImageFetcherTest, FetchImageFromCache) { |
| // Save the image in the database. |
| image_cache()->SaveImage(kImageUrl.spec(), kImageData); |
| RunUntilIdle(); |
| |
| base::MockCallback<ImageDataFetcherCallback> data_callback; |
| base::MockCallback<ImageFetcherCallback> image_callback; |
| |
| EXPECT_CALL(data_callback, Run(kImageData, _)); |
| EXPECT_CALL(image_callback, Run(NonEmptyImage(), _)); |
| cached_image_fetcher()->FetchImageAndData( |
| kImageUrl, data_callback.Get(), image_callback.Get(), |
| ImageFetcherParams(TRAFFIC_ANNOTATION_FOR_TESTS, kUmaClientName)); |
| |
| RunUntilIdle(); |
| |
| histogram_tester().ExpectTotalCount(kCacheLoadHistogramName, 1); |
| histogram_tester().ExpectBucketCount(kImageFetcherEventHistogramName, |
| ImageFetcherEvent::kImageRequest, 1); |
| histogram_tester().ExpectBucketCount(kImageFetcherEventHistogramName, |
| ImageFetcherEvent::kCacheHit, 1); |
| } |
| |
| TEST_F(CachedImageFetcherTest, FetchImageFromCacheReadOnly) { |
| CreateCachedImageFetcher(/* read_only */ true); |
| // Save the image in the database. |
| image_cache()->SaveImage(kImageUrl.spec(), kImageData); |
| test_url_loader_factory()->AddResponse(kImageUrl.spec(), kImageData); |
| RunUntilIdle(); |
| { |
| // Even if there's a decoding error, read_only cache shouldn't alter the |
| // cache. |
| image_decoder()->SetDecodingValid(false); |
| base::MockCallback<ImageDataFetcherCallback> data_callback; |
| base::MockCallback<ImageFetcherCallback> image_callback; |
| EXPECT_CALL(image_callback, Run(EmptyImage(), _)); |
| cached_image_fetcher()->FetchImageAndData( |
| kImageUrl, data_callback.Get(), image_callback.Get(), |
| ImageFetcherParams(TRAFFIC_ANNOTATION_FOR_TESTS, kUmaClientName)); |
| RunUntilIdle(); |
| |
| histogram_tester().ExpectBucketCount(kImageFetcherEventHistogramName, |
| ImageFetcherEvent::kImageRequest, 1); |
| histogram_tester().ExpectBucketCount(kImageFetcherEventHistogramName, |
| ImageFetcherEvent::kCacheHit, 1); |
| histogram_tester().ExpectBucketCount(kImageFetcherEventHistogramName, |
| ImageFetcherEvent::kCacheDecodingError, |
| 1); |
| } |
| { |
| // Image should still be in the cache. |
| image_decoder()->SetDecodingValid(true); |
| base::MockCallback<ImageDataFetcherCallback> data_callback; |
| base::MockCallback<ImageFetcherCallback> image_callback; |
| EXPECT_CALL(image_callback, Run(NonEmptyImage(), _)); |
| cached_image_fetcher()->FetchImageAndData( |
| kImageUrl, data_callback.Get(), image_callback.Get(), |
| ImageFetcherParams(TRAFFIC_ANNOTATION_FOR_TESTS, kUmaClientName)); |
| RunUntilIdle(); |
| } |
| } |
| |
| TEST_F(CachedImageFetcherTest, FetchImagePopulatesCache) { |
| // Expect the image to be fetched by URL. |
| { |
| test_url_loader_factory()->AddResponse(kImageUrl.spec(), kImageData); |
| |
| base::MockCallback<ImageDataFetcherCallback> data_callback; |
| base::MockCallback<ImageFetcherCallback> image_callback; |
| |
| EXPECT_CALL(data_callback, Run(NonEmptyString(), _)); |
| EXPECT_CALL(image_callback, Run(NonEmptyImage(), _)); |
| cached_image_fetcher()->FetchImageAndData( |
| kImageUrl, data_callback.Get(), image_callback.Get(), |
| ImageFetcherParams(TRAFFIC_ANNOTATION_FOR_TESTS, kUmaClientName)); |
| |
| RunUntilIdle(); |
| |
| histogram_tester().ExpectTotalCount(kNetworkLoadHistogramName, 1); |
| histogram_tester().ExpectBucketCount(kImageFetcherEventHistogramName, |
| ImageFetcherEvent::kImageRequest, 1); |
| histogram_tester().ExpectBucketCount(kImageFetcherEventHistogramName, |
| ImageFetcherEvent::kCacheMiss, 1); |
| } |
| // Make sure the image data is in the database. |
| { |
| EXPECT_CALL(*this, OnImageLoaded(NonEmptyString())); |
| image_cache()->LoadImage( |
| /* read_only */ false, kImageUrl.spec(), |
| base::BindOnce(&CachedImageFetcherTest::OnImageLoaded, |
| base::Unretained(this))); |
| RunUntilIdle(); |
| } |
| // Fetch again. The cache should be populated, no network request is needed. |
| { |
| test_url_loader_factory()->ClearResponses(); |
| |
| base::MockCallback<ImageDataFetcherCallback> data_callback; |
| base::MockCallback<ImageFetcherCallback> image_callback; |
| |
| EXPECT_CALL(data_callback, Run(NonEmptyString(), _)); |
| EXPECT_CALL(image_callback, Run(NonEmptyImage(), _)); |
| cached_image_fetcher()->FetchImageAndData( |
| kImageUrl, data_callback.Get(), image_callback.Get(), |
| ImageFetcherParams(TRAFFIC_ANNOTATION_FOR_TESTS, kUmaClientName)); |
| |
| RunUntilIdle(); |
| } |
| } |
| |
| TEST_F(CachedImageFetcherTest, FetchImagePopulatesCacheReadOnly) { |
| CreateCachedImageFetcher(/* read_only */ true); |
| // Expect the image to be fetched by URL. |
| { |
| test_url_loader_factory()->AddResponse(kImageUrl.spec(), kImageData); |
| |
| base::MockCallback<ImageDataFetcherCallback> data_callback; |
| base::MockCallback<ImageFetcherCallback> image_callback; |
| |
| EXPECT_CALL(data_callback, Run(NonEmptyString(), _)); |
| EXPECT_CALL(image_callback, Run(NonEmptyImage(), _)); |
| cached_image_fetcher()->FetchImageAndData( |
| kImageUrl, data_callback.Get(), image_callback.Get(), |
| ImageFetcherParams(TRAFFIC_ANNOTATION_FOR_TESTS, kUmaClientName)); |
| |
| RunUntilIdle(); |
| |
| histogram_tester().ExpectTotalCount(kNetworkLoadHistogramName, 1); |
| histogram_tester().ExpectBucketCount(kImageFetcherEventHistogramName, |
| ImageFetcherEvent::kImageRequest, 1); |
| histogram_tester().ExpectBucketCount(kImageFetcherEventHistogramName, |
| ImageFetcherEvent::kCacheMiss, 1); |
| } |
| // Make sure the image data is not in the database. |
| { |
| EXPECT_CALL(*this, OnImageLoaded(std::string())); |
| image_cache()->LoadImage( |
| /* read_only */ false, kImageUrl.spec(), |
| base::BindOnce(&CachedImageFetcherTest::OnImageLoaded, |
| base::Unretained(this))); |
| RunUntilIdle(); |
| } |
| } |
| |
| TEST_F(CachedImageFetcherTest, FetchImageWithoutTranscodingDoesNotDecode) { |
| { |
| test_url_loader_factory()->AddResponse(kImageUrl.spec(), kImageData); |
| image_decoder()->SetDecodingValid(false); |
| |
| base::MockCallback<ImageDataFetcherCallback> data_callback; |
| |
| EXPECT_CALL(data_callback, Run(kImageData, _)); |
| ImageFetcherParams params(TRAFFIC_ANNOTATION_FOR_TESTS, kUmaClientName); |
| params.set_skip_transcoding_for_testing(true); |
| cached_image_fetcher()->FetchImageAndData(kImageUrl, data_callback.Get(), |
| ImageFetcherCallback(), params); |
| |
| RunUntilIdle(); |
| } |
| { |
| test_url_loader_factory()->ClearResponses(); |
| base::MockCallback<ImageDataFetcherCallback> data_callback; |
| EXPECT_CALL(data_callback, Run(kImageData, _)); |
| cached_image_fetcher()->FetchImageAndData( |
| kImageUrl, data_callback.Get(), ImageFetcherCallback(), |
| ImageFetcherParams(TRAFFIC_ANNOTATION_FOR_TESTS, kUmaClientName)); |
| |
| RunUntilIdle(); |
| } |
| } |
| |
| TEST_F(CachedImageFetcherTest, FetchImageWithSkipDiskCache) { |
| // Save the image in the database. |
| image_cache()->SaveImage(kImageUrl.spec(), kImageDataOther); |
| RunUntilIdle(); |
| test_url_loader_factory()->AddResponse(kImageUrl.spec(), kImageData); |
| |
| base::MockCallback<ImageDataFetcherCallback> data_callback; |
| base::MockCallback<ImageFetcherCallback> image_callback; |
| |
| ImageFetcherParams params(TRAFFIC_ANNOTATION_FOR_TESTS, kUmaClientName); |
| params.set_skip_disk_cache_read(true); |
| |
| EXPECT_CALL(data_callback, Run(kImageData, _)); |
| EXPECT_CALL(image_callback, Run(NonEmptyImage(), _)); |
| cached_image_fetcher()->FetchImageAndData(kImageUrl, data_callback.Get(), |
| image_callback.Get(), params); |
| |
| RunUntilIdle(); |
| } |
| |
| } // namespace image_fetcher |