| // Copyright 2023 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "services/network/shared_dictionary/shared_dictionary_manager_on_disk.h" |
| |
| #include "base/files/file_path.h" |
| #include "base/files/file_util.h" |
| #include "base/files/scoped_temp_dir.h" |
| #include "base/functional/callback.h" |
| #include "base/strings/strcat.h" |
| #include "base/strings/string_number_conversions.h" |
| #include "base/strings/stringprintf.h" |
| #include "base/test/bind.h" |
| #include "base/test/metrics/histogram_tester.h" |
| #include "base/test/task_environment.h" |
| #include "base/test/test_file_util.h" |
| #include "base/time/time.h" |
| #include "build/build_config.h" |
| #include "crypto/secure_hash.h" |
| #include "net/base/hash_value.h" |
| #include "net/base/io_buffer.h" |
| #include "net/base/net_errors.h" |
| #include "net/base/network_isolation_key.h" |
| #include "net/base/schemeful_site.h" |
| #include "net/disk_cache/disk_cache.h" |
| #include "net/disk_cache/disk_cache_test_util.h" |
| #include "net/extras/shared_dictionary/shared_dictionary_info.h" |
| #include "net/http/http_response_headers.h" |
| #include "services/network/shared_dictionary/shared_dictionary.h" |
| #include "services/network/shared_dictionary/shared_dictionary_constants.h" |
| #include "services/network/shared_dictionary/shared_dictionary_disk_cache.h" |
| #include "services/network/shared_dictionary/shared_dictionary_manager_on_disk.h" |
| #include "services/network/shared_dictionary/shared_dictionary_storage.h" |
| #include "services/network/shared_dictionary/shared_dictionary_storage_on_disk.h" |
| #include "services/network/shared_dictionary/shared_dictionary_writer.h" |
| #include "sql/test/test_helpers.h" |
| #include "testing/gmock/include/gmock/gmock.h" |
| #include "testing/gtest/include/gtest/gtest.h" |
| #include "url/gurl.h" |
| #include "url/origin.h" |
| |
| using ::testing::ElementsAre; |
| using ::testing::Pair; |
| |
| namespace network { |
| |
| namespace { |
| |
| const GURL kUrl("https://origin.test/"); |
| const net::SchemefulSite kSite(kUrl); |
| const std::string kTestData1 = "Hello world"; |
| const std::string kTestData2 = "Bonjour le monde"; |
| |
| void WriteDictionary(SharedDictionaryStorage* storage, |
| const GURL& dictionary_url, |
| const std::string& match, |
| const std::string& data) { |
| scoped_refptr<net::HttpResponseHeaders> headers = |
| net::HttpResponseHeaders::TryToCreate(base::StrCat( |
| {"HTTP/1.1 200 OK\n", shared_dictionary::kUseAsDictionaryHeaderName, |
| ": match=\"/", match, "\"\n\n"})); |
| ASSERT_TRUE(headers); |
| scoped_refptr<SharedDictionaryWriter> writer = |
| storage->MaybeCreateWriter(dictionary_url, base::Time::Now(), *headers); |
| ASSERT_TRUE(writer); |
| writer->Append(data.c_str(), data.size()); |
| writer->Finish(); |
| } |
| void WriteDictionaryWithExpiry(SharedDictionaryStorage* storage, |
| const GURL& dictionary_url, |
| const std::string& match, |
| const base::TimeDelta& expires, |
| const std::string& data) { |
| scoped_refptr<net::HttpResponseHeaders> headers = |
| net::HttpResponseHeaders::TryToCreate(base::StrCat( |
| {"HTTP/1.1 200 OK\n", shared_dictionary::kUseAsDictionaryHeaderName, |
| ": match=\"/", match, |
| "\", expires=", base::NumberToString(expires.InSeconds()), "\n\n"})); |
| ASSERT_TRUE(headers); |
| scoped_refptr<SharedDictionaryWriter> writer = |
| storage->MaybeCreateWriter(dictionary_url, base::Time::Now(), *headers); |
| ASSERT_TRUE(writer); |
| writer->Append(data.c_str(), data.size()); |
| writer->Finish(); |
| } |
| |
| bool DiskCacheEntryExists(SharedDictionaryManager* manager, |
| const std::string& disk_cache_key) { |
| TestEntryResultCompletionCallback open_callback; |
| disk_cache::EntryResult open_result = open_callback.GetResult( |
| static_cast<SharedDictionaryManagerOnDisk*>(manager) |
| ->disk_cache() |
| .OpenOrCreateEntry(disk_cache_key, |
| /*create=*/false, open_callback.callback())); |
| return open_result.net_error() == net::OK; |
| } |
| |
| bool DiskCacheEntryExists(SharedDictionaryManager* manager, |
| const base::UnguessableToken& disk_cache_key_token) { |
| return DiskCacheEntryExists(manager, disk_cache_key_token.ToString()); |
| } |
| |
| void DoomDiskCacheEntry(SharedDictionaryManager* manager, |
| const base::UnguessableToken& disk_cache_key_token) { |
| net::TestCompletionCallback doom_callback; |
| EXPECT_EQ(net::OK, doom_callback.GetResult( |
| static_cast<SharedDictionaryManagerOnDisk*>(manager) |
| ->disk_cache() |
| .DoomEntry(disk_cache_key_token.ToString(), |
| doom_callback.callback()))); |
| } |
| |
| void WriteDiskCacheEntry(SharedDictionaryManager* manager, |
| const std::string& disk_cache_key, |
| const std::string& data) { |
| SharedDictionaryDiskCache& disk_cache = |
| static_cast<SharedDictionaryManagerOnDisk*>(manager)->disk_cache(); |
| |
| TestEntryResultCompletionCallback create_callback; |
| |
| // Create the entry. |
| disk_cache::EntryResult create_result = |
| create_callback.GetResult(disk_cache.OpenOrCreateEntry( |
| disk_cache_key, /*create=*/true, create_callback.callback())); |
| EXPECT_EQ(net::OK, create_result.net_error()); |
| disk_cache::ScopedEntryPtr created_entry; |
| created_entry.reset(create_result.ReleaseEntry()); |
| ASSERT_TRUE(created_entry); |
| |
| // Write to the entry. |
| scoped_refptr<net::StringIOBuffer> write_buffer = |
| base::MakeRefCounted<net::StringIOBuffer>(data); |
| net::TestCompletionCallback write_callback; |
| EXPECT_EQ( |
| base::checked_cast<int>(data.size()), |
| write_callback.GetResult(created_entry->WriteData( |
| /*index=*/1, /*offset=*/0, write_buffer.get(), write_buffer->size(), |
| write_callback.callback(), /*truncate=*/false))); |
| } |
| |
| base::UnguessableToken GetDiskCacheKeyTokenOfFirstDictionary( |
| const std::map<url::SchemeHostPort, |
| std::map<std::string, net::SharedDictionaryInfo>>& |
| dictionary_map, |
| const std::string& scheme_host_port_str) { |
| auto it = |
| dictionary_map.find(url::SchemeHostPort(GURL(scheme_host_port_str))); |
| CHECK(it != dictionary_map.end()) << scheme_host_port_str; |
| CHECK(!it->second.empty()); |
| return it->second.begin()->second.disk_cache_key_token(); |
| } |
| |
| } // namespace |
| |
| class SharedDictionaryManagerOnDiskTest : public ::testing::Test { |
| public: |
| SharedDictionaryManagerOnDiskTest() = default; |
| ~SharedDictionaryManagerOnDiskTest() override = default; |
| |
| SharedDictionaryManagerOnDiskTest(const SharedDictionaryManagerOnDiskTest&) = |
| delete; |
| SharedDictionaryManagerOnDiskTest& operator=( |
| const SharedDictionaryManagerOnDiskTest&) = delete; |
| |
| void SetUp() override { |
| ASSERT_TRUE(tmp_directory_.CreateUniqueTempDir()); |
| database_path_ = tmp_directory_.GetPath().Append(FILE_PATH_LITERAL("db")); |
| cache_directory_path_ = |
| tmp_directory_.GetPath().Append(FILE_PATH_LITERAL("cache")); |
| } |
| void TearDown() override { FlushCacheTasks(); } |
| |
| protected: |
| std::unique_ptr<SharedDictionaryManager> CreateSharedDictionaryManager( |
| uint64_t cache_max_size = 0) { |
| return SharedDictionaryManager::CreateOnDisk( |
| database_path_, cache_directory_path_, cache_max_size, |
| #if BUILDFLAG(IS_ANDROID) |
| /*app_status_listener=*/nullptr, |
| #endif // BUILDFLAG(IS_ANDROID) |
| /*file_operations_factory=*/nullptr); |
| } |
| const std::map<url::SchemeHostPort, |
| std::map<std::string, net::SharedDictionaryInfo>>& |
| GetOnDiskDictionaryMap(SharedDictionaryStorage* storage) { |
| return static_cast<SharedDictionaryStorageOnDisk*>(storage) |
| ->GetDictionaryMapForTesting(); |
| } |
| void FlushCacheTasks() { |
| disk_cache::FlushCacheThreadForTesting(); |
| task_environment_.RunUntilIdle(); |
| } |
| void CorruptDiskCache() { |
| // Corrupt the fake index file for the populated simple cache. |
| const base::FilePath index_file_path = |
| cache_directory_path_.Append(FILE_PATH_LITERAL("index")); |
| ASSERT_TRUE(base::WriteFile(index_file_path, "corrupted")); |
| file_permissions_restorer_ = std::make_unique<base::FilePermissionRestorer>( |
| tmp_directory_.GetPath()); |
| // Mark the parent directory unwritable, so that we can't restore the dist |
| ASSERT_TRUE(base::MakeFileUnwritable(tmp_directory_.GetPath())); |
| } |
| |
| void CorruptDatabase() { |
| CHECK(sql::test::CorruptSizeInHeader(database_path_)); |
| } |
| |
| base::test::TaskEnvironment task_environment_{ |
| base::test::TaskEnvironment::TimeSource::MOCK_TIME}; |
| |
| private: |
| base::ScopedTempDir tmp_directory_; |
| base::FilePath database_path_; |
| base::FilePath cache_directory_path_; |
| // `file_permissions_restorer_` must be below `tmp_directory_` to restore the |
| // file permission correctly. |
| std::unique_ptr<base::FilePermissionRestorer> file_permissions_restorer_; |
| }; |
| |
| TEST_F(SharedDictionaryManagerOnDiskTest, ReusingRefCountedSharedDictionary) { |
| std::unique_ptr<SharedDictionaryManager> manager = |
| CreateSharedDictionaryManager(); |
| net::SharedDictionaryStorageIsolationKey isolation_key( |
| url::Origin::Create(kUrl), kSite); |
| scoped_refptr<SharedDictionaryStorage> storage = |
| manager->GetStorage(isolation_key); |
| ASSERT_TRUE(storage); |
| |
| WriteDictionary(storage.get(), GURL("https://origin.test/dict"), "testfile*", |
| kTestData1); |
| |
| FlushCacheTasks(); |
| |
| // Check the returned dictionary from GetDictionary(). |
| std::unique_ptr<SharedDictionary> dict1 = |
| storage->GetDictionary(GURL("https://origin.test/testfile?1")); |
| ASSERT_TRUE(dict1); |
| { |
| base::RunLoop run_loop; |
| EXPECT_EQ(net::ERR_IO_PENDING, |
| dict1->ReadAll(base::BindLambdaForTesting([&](int rv) { |
| EXPECT_EQ(net::OK, rv); |
| run_loop.Quit(); |
| }))); |
| run_loop.Run(); |
| } |
| std::unique_ptr<SharedDictionary> dict2 = |
| storage->GetDictionary(GURL("https://origin.test/testfile?2")); |
| ASSERT_TRUE(dict2); |
| // `dict2` shares the same RefCountedSharedDictionary with `dict1`. So |
| // ReadAll() must synchronously return OK. |
| EXPECT_EQ(net::OK, dict2->ReadAll(base::BindLambdaForTesting( |
| [&](int rv) { NOTREACHED(); }))); |
| // `dict2` shares the same IOBuffer with `dict1`. |
| EXPECT_EQ(dict1->data(), dict2->data()); |
| EXPECT_EQ(dict1->size(), dict2->size()); |
| EXPECT_EQ(dict1->hash(), dict2->hash()); |
| EXPECT_EQ(kTestData1, |
| std::string(reinterpret_cast<const char*>(dict1->data()->data()), |
| dict1->size())); |
| } |
| |
| TEST_F(SharedDictionaryManagerOnDiskTest, |
| MaybeCreateWriterAfterManagerDeleted) { |
| std::unique_ptr<SharedDictionaryManager> manager = |
| CreateSharedDictionaryManager(); |
| net::SharedDictionaryStorageIsolationKey isolation_key( |
| url::Origin::Create(kUrl), kSite); |
| scoped_refptr<SharedDictionaryStorage> storage = |
| manager->GetStorage(isolation_key); |
| ASSERT_TRUE(storage); |
| |
| manager.reset(); |
| |
| scoped_refptr<net::HttpResponseHeaders> headers = |
| net::HttpResponseHeaders::TryToCreate(base::StrCat( |
| {"HTTP/1.1 200 OK\n", shared_dictionary::kUseAsDictionaryHeaderName, |
| ": match=\"/testfile*\"\n\n"})); |
| ASSERT_TRUE(headers); |
| |
| // MaybeCreateWriter() must return nullptr, after `manager` was deleted. |
| scoped_refptr<SharedDictionaryWriter> writer = storage->MaybeCreateWriter( |
| GURL("https://origin.test/dict"), base::Time::Now(), *headers); |
| EXPECT_FALSE(writer); |
| } |
| |
| TEST_F(SharedDictionaryManagerOnDiskTest, GetDictionaryAfterManagerDeleted) { |
| std::unique_ptr<SharedDictionaryManager> manager = |
| CreateSharedDictionaryManager(); |
| net::SharedDictionaryStorageIsolationKey isolation_key( |
| url::Origin::Create(kUrl), kSite); |
| scoped_refptr<SharedDictionaryStorage> storage = |
| manager->GetStorage(isolation_key); |
| ASSERT_TRUE(storage); |
| |
| manager.reset(); |
| |
| // GetDictionary() must return nullptr, after `manager` was deleted. |
| std::unique_ptr<SharedDictionary> dict = |
| storage->GetDictionary(GURL("https://origin.test/testfile?1")); |
| EXPECT_FALSE(dict); |
| } |
| |
| TEST_F(SharedDictionaryManagerOnDiskTest, |
| DictionaryWrittenInDiskCacheAfterManagerDeleted) { |
| std::unique_ptr<SharedDictionaryManager> manager = |
| CreateSharedDictionaryManager(); |
| net::SharedDictionaryStorageIsolationKey isolation_key( |
| url::Origin::Create(kUrl), kSite); |
| scoped_refptr<SharedDictionaryStorage> storage = |
| manager->GetStorage(isolation_key); |
| ASSERT_TRUE(storage); |
| // Write the test data to the dictionary. |
| WriteDictionary(storage.get(), GURL("https://origin.test/dict"), "testfile*", |
| kTestData1); |
| // Test that deleting `manager` while writing the dictionary doesn't cause |
| // crash. |
| manager.reset(); |
| FlushCacheTasks(); |
| } |
| |
| TEST_F(SharedDictionaryManagerOnDiskTest, OverridingDictionary) { |
| std::unique_ptr<SharedDictionaryManager> manager = |
| CreateSharedDictionaryManager(); |
| net::SharedDictionaryStorageIsolationKey isolation_key( |
| url::Origin::Create(kUrl), kSite); |
| scoped_refptr<SharedDictionaryStorage> storage = |
| manager->GetStorage(isolation_key); |
| ASSERT_TRUE(storage); |
| |
| // Write the test data to the dictionary. |
| WriteDictionary(storage.get(), GURL("https://origin.test/dict1"), "testfile*", |
| kTestData1); |
| FlushCacheTasks(); |
| |
| base::UnguessableToken disk_cache_key_token1; |
| { |
| const auto& dictionary_map = GetOnDiskDictionaryMap(storage.get()); |
| ASSERT_EQ(1u, dictionary_map.size()); |
| ASSERT_EQ(1u, dictionary_map.begin()->second.size()); |
| disk_cache_key_token1 = |
| dictionary_map.begin()->second.begin()->second.disk_cache_key_token(); |
| } |
| |
| // Check the returned dictionary from GetDictionary(). |
| std::unique_ptr<SharedDictionary> dict1 = |
| storage->GetDictionary(GURL("https://origin.test/testfile")); |
| ASSERT_TRUE(dict1); |
| |
| // The disk cache entry must exist. |
| EXPECT_TRUE(DiskCacheEntryExists(manager.get(), disk_cache_key_token1)); |
| |
| // Write different test data to the dictionary. |
| WriteDictionary(storage.get(), GURL("https://origin.test/dict2"), "testfile*", |
| kTestData2); |
| |
| FlushCacheTasks(); |
| |
| base::UnguessableToken disk_cache_key_token2; |
| { |
| const auto& dictionary_map = GetOnDiskDictionaryMap(storage.get()); |
| ASSERT_EQ(1u, dictionary_map.size()); |
| ASSERT_EQ(1u, dictionary_map.begin()->second.size()); |
| disk_cache_key_token2 = |
| dictionary_map.begin()->second.begin()->second.disk_cache_key_token(); |
| } |
| |
| EXPECT_NE(disk_cache_key_token1, disk_cache_key_token2); |
| |
| // The disk cache entry should have been doomed. |
| EXPECT_FALSE(DiskCacheEntryExists(manager.get(), disk_cache_key_token1)); |
| |
| std::unique_ptr<SharedDictionary> dict2 = |
| storage->GetDictionary(GURL("https://origin.test/testfile")); |
| ASSERT_TRUE(dict2); |
| |
| // We can read the new dictionary from `dict2`. |
| net::TestCompletionCallback read_callback2; |
| EXPECT_EQ(net::OK, read_callback2.GetResult( |
| dict2->ReadAll(read_callback2.callback()))); |
| EXPECT_EQ(kTestData2, |
| std::string(reinterpret_cast<const char*>(dict2->data()->data()), |
| dict2->size())); |
| |
| // We can still read the old dictionary from `dict1`. |
| net::TestCompletionCallback read_callback1; |
| EXPECT_EQ(net::OK, read_callback1.GetResult( |
| dict1->ReadAll(read_callback1.callback()))); |
| EXPECT_EQ(kTestData1, |
| std::string(reinterpret_cast<const char*>(dict1->data()->data()), |
| dict1->size())); |
| } |
| |
| TEST_F(SharedDictionaryManagerOnDiskTest, MultipleDictionaries) { |
| net::SharedDictionaryStorageIsolationKey isolation_key( |
| url::Origin::Create(kUrl), kSite); |
| |
| { |
| std::unique_ptr<SharedDictionaryManager> manager = |
| CreateSharedDictionaryManager(); |
| scoped_refptr<SharedDictionaryStorage> storage = |
| manager->GetStorage(isolation_key); |
| ASSERT_TRUE(storage); |
| |
| // Write the test data to the dictionary. |
| WriteDictionary(storage.get(), GURL("https://origin.test/dict1"), |
| "testfile1*", kTestData1); |
| |
| WriteDictionary(storage.get(), GURL("https://origin.test/dict2"), |
| "testfile2*", kTestData2); |
| |
| FlushCacheTasks(); |
| |
| std::unique_ptr<SharedDictionary> dict1 = |
| storage->GetDictionary(GURL("https://origin.test/testfile1")); |
| ASSERT_TRUE(dict1); |
| |
| std::unique_ptr<SharedDictionary> dict2 = |
| storage->GetDictionary(GURL("https://origin.test/testfile2")); |
| ASSERT_TRUE(dict2); |
| |
| net::TestCompletionCallback read_callback1; |
| EXPECT_EQ(net::OK, read_callback1.GetResult( |
| dict1->ReadAll(read_callback1.callback()))); |
| EXPECT_EQ(kTestData1, |
| std::string(reinterpret_cast<const char*>(dict1->data()->data()), |
| dict1->size())); |
| |
| net::TestCompletionCallback read_callback2; |
| EXPECT_EQ(net::OK, read_callback2.GetResult( |
| dict2->ReadAll(read_callback2.callback()))); |
| EXPECT_EQ(kTestData2, |
| std::string(reinterpret_cast<const char*>(dict2->data()->data()), |
| dict2->size())); |
| // Releasing `dict1`, `dict2`, `storage` and `manager`. |
| } |
| // FlushCacheTasks() to finish the persistence operation. |
| FlushCacheTasks(); |
| |
| // The dictionaries must be available after recreating `manager`. |
| std::unique_ptr<SharedDictionaryManager> manager = |
| CreateSharedDictionaryManager(); |
| scoped_refptr<SharedDictionaryStorage> storage = |
| manager->GetStorage(isolation_key); |
| ASSERT_TRUE(storage); |
| |
| // RunUntilIdle() to load from the database. |
| task_environment_.RunUntilIdle(); |
| |
| const auto& dictionary_map = GetOnDiskDictionaryMap(storage.get()); |
| ASSERT_EQ(1u, dictionary_map.size()); |
| ASSERT_EQ(2u, dictionary_map.begin()->second.size()); |
| |
| std::unique_ptr<SharedDictionary> dict1 = |
| storage->GetDictionary(GURL("https://origin.test/testfile1")); |
| ASSERT_TRUE(dict1); |
| |
| std::unique_ptr<SharedDictionary> dict2 = |
| storage->GetDictionary(GURL("https://origin.test/testfile2")); |
| ASSERT_TRUE(dict2); |
| |
| net::TestCompletionCallback read_callback1; |
| EXPECT_EQ(net::OK, read_callback1.GetResult( |
| dict1->ReadAll(read_callback1.callback()))); |
| EXPECT_EQ(kTestData1, |
| std::string(reinterpret_cast<const char*>(dict1->data()->data()), |
| dict1->size())); |
| |
| net::TestCompletionCallback read_callback2; |
| EXPECT_EQ(net::OK, read_callback2.GetResult( |
| dict2->ReadAll(read_callback2.callback()))); |
| EXPECT_EQ(kTestData2, |
| std::string(reinterpret_cast<const char*>(dict2->data()->data()), |
| dict2->size())); |
| } |
| |
| #if !BUILDFLAG(IS_FUCHSIA) |
| // Test that corruptted disk cache doesn't cause crash. |
| // CorruptDiskCache() doesn't work on Fuchsia. So disabling the following tests |
| // on Fuchsia. |
| TEST_F(SharedDictionaryManagerOnDiskTest, CorruptedDiskCacheAndWriteData) { |
| net::SharedDictionaryStorageIsolationKey isolation_key( |
| url::Origin::Create(kUrl), kSite); |
| |
| { |
| std::unique_ptr<SharedDictionaryManager> manager = |
| CreateSharedDictionaryManager(); |
| scoped_refptr<SharedDictionaryStorage> storage = |
| manager->GetStorage(isolation_key); |
| ASSERT_TRUE(storage); |
| // Write the test data to the dictionary. |
| WriteDictionary(storage.get(), GURL("https://origin.test/dict1"), |
| "testfile1*", kTestData1); |
| FlushCacheTasks(); |
| } |
| CorruptDiskCache(); |
| { |
| std::unique_ptr<SharedDictionaryManager> manager = |
| CreateSharedDictionaryManager(); |
| scoped_refptr<SharedDictionaryStorage> storage = |
| manager->GetStorage(isolation_key); |
| ASSERT_TRUE(storage); |
| FlushCacheTasks(); |
| { |
| const auto& dictionary_map = GetOnDiskDictionaryMap(storage.get()); |
| ASSERT_EQ(1u, dictionary_map.size()); |
| ASSERT_EQ(1u, dictionary_map.begin()->second.size()); |
| } |
| WriteDictionary(storage.get(), GURL("https://origin.test/dict2"), |
| "testfile2*", kTestData2); |
| FlushCacheTasks(); |
| |
| // Writing dictionary fails, so MismatchingEntryDeletionTask cleans all |
| // dictionary in the metadata store. |
| EXPECT_TRUE(GetOnDiskDictionaryMap(storage.get()).empty()); |
| } |
| } |
| |
| TEST_F(SharedDictionaryManagerOnDiskTest, CorruptedDiskCacheAndGetData) { |
| net::SharedDictionaryStorageIsolationKey isolation_key( |
| url::Origin::Create(kUrl), kSite); |
| |
| { |
| std::unique_ptr<SharedDictionaryManager> manager = |
| CreateSharedDictionaryManager(); |
| scoped_refptr<SharedDictionaryStorage> storage = |
| manager->GetStorage(isolation_key); |
| ASSERT_TRUE(storage); |
| // Write the test data to the dictionary. |
| WriteDictionary(storage.get(), GURL("https://origin.test/dict1"), |
| "testfile1*", kTestData1); |
| FlushCacheTasks(); |
| } |
| CorruptDiskCache(); |
| { |
| std::unique_ptr<SharedDictionaryManager> manager = |
| CreateSharedDictionaryManager(); |
| scoped_refptr<SharedDictionaryStorage> storage = |
| manager->GetStorage(isolation_key); |
| ASSERT_TRUE(storage); |
| FlushCacheTasks(); |
| { |
| const auto& dictionary_map = GetOnDiskDictionaryMap(storage.get()); |
| ASSERT_EQ(1u, dictionary_map.size()); |
| ASSERT_EQ(1u, dictionary_map.begin()->second.size()); |
| } |
| |
| std::unique_ptr<SharedDictionary> dict = |
| storage->GetDictionary(GURL("https://origin.test/testfile1")); |
| ASSERT_TRUE(dict); |
| |
| // Reading the dictionary should fail because the disk cache is broken. |
| net::TestCompletionCallback read_callback; |
| EXPECT_EQ(net::ERR_FAILED, |
| read_callback.GetResult(dict->ReadAll(read_callback.callback()))); |
| |
| FlushCacheTasks(); |
| |
| // After failing to read the disk cache entry, MismatchingEntryDeletionTask |
| // cleans all dictionary in the metadata store. |
| EXPECT_FALSE(storage->GetDictionary(GURL("https://origin.test/testfile1"))); |
| EXPECT_TRUE(GetOnDiskDictionaryMap(storage.get()).empty()); |
| } |
| } |
| #endif // !BUILDFLAG(IS_FUCHSIA) |
| |
| TEST_F(SharedDictionaryManagerOnDiskTest, CorruptedDatabase) { |
| net::SharedDictionaryStorageIsolationKey isolation_key( |
| url::Origin::Create(kUrl), kSite); |
| |
| base::UnguessableToken token1, token2; |
| { |
| std::unique_ptr<SharedDictionaryManager> manager = |
| CreateSharedDictionaryManager(); |
| scoped_refptr<SharedDictionaryStorage> storage = |
| manager->GetStorage(isolation_key); |
| ASSERT_TRUE(storage); |
| // Write the test data to the dictionary. |
| WriteDictionary(storage.get(), GURL("https://origin.test/dict"), |
| "testfile*", kTestData1); |
| FlushCacheTasks(); |
| { |
| const auto& dictionary_map = GetOnDiskDictionaryMap(storage.get()); |
| ASSERT_EQ(1u, dictionary_map.size()); |
| ASSERT_EQ(1u, dictionary_map.begin()->second.size()); |
| token1 = |
| dictionary_map.begin()->second.begin()->second.disk_cache_key_token(); |
| } |
| } |
| CorruptDatabase(); |
| { |
| std::unique_ptr<SharedDictionaryManager> manager = |
| CreateSharedDictionaryManager(); |
| scoped_refptr<SharedDictionaryStorage> storage = |
| manager->GetStorage(isolation_key); |
| ASSERT_TRUE(storage); |
| FlushCacheTasks(); |
| EXPECT_TRUE(GetOnDiskDictionaryMap(storage.get()).empty()); |
| WriteDictionary(storage.get(), GURL("https://origin.test/dict"), |
| "testfile*", kTestData1); |
| FlushCacheTasks(); |
| // Can't add a new entry right after the databace corruption. |
| EXPECT_TRUE(GetOnDiskDictionaryMap(storage.get()).empty()); |
| } |
| // Test that database corruption can be recovered after reboot. |
| { |
| std::unique_ptr<SharedDictionaryManager> manager = |
| CreateSharedDictionaryManager(); |
| scoped_refptr<SharedDictionaryStorage> storage = |
| manager->GetStorage(isolation_key); |
| ASSERT_TRUE(storage); |
| FlushCacheTasks(); |
| EXPECT_TRUE(GetOnDiskDictionaryMap(storage.get()).empty()); |
| WriteDictionary(storage.get(), GURL("https://origin.test/dict"), |
| "testfile*", kTestData1); |
| FlushCacheTasks(); |
| EXPECT_FALSE(GetOnDiskDictionaryMap(storage.get()).empty()); |
| |
| std::unique_ptr<SharedDictionary> dict = |
| storage->GetDictionary(GURL("https://origin.test/testfile")); |
| ASSERT_TRUE(dict); |
| |
| // We can read the new dictionary. |
| net::TestCompletionCallback read_callback; |
| EXPECT_EQ(net::OK, |
| read_callback.GetResult(dict->ReadAll(read_callback.callback()))); |
| EXPECT_EQ(kTestData1, |
| std::string(reinterpret_cast<const char*>(dict->data()->data()), |
| dict->size())); |
| |
| { |
| const auto& dictionary_map = GetOnDiskDictionaryMap(storage.get()); |
| ASSERT_EQ(1u, dictionary_map.size()); |
| ASSERT_EQ(1u, dictionary_map.begin()->second.size()); |
| token2 = |
| dictionary_map.begin()->second.begin()->second.disk_cache_key_token(); |
| } |
| |
| EXPECT_TRUE(DiskCacheEntryExists(manager.get(), token1)); |
| EXPECT_TRUE(DiskCacheEntryExists(manager.get(), token2)); |
| |
| base::RunLoop run_loop; |
| manager->ClearData( |
| base::Time::Now() - base::Days(2), base::Time::Now() - base::Days(1), |
| base::RepeatingCallback<bool(const GURL&)>(), run_loop.QuitClosure()); |
| run_loop.Run(); |
| FlushCacheTasks(); |
| |
| EXPECT_FALSE(DiskCacheEntryExists(manager.get(), token1)); |
| EXPECT_TRUE(DiskCacheEntryExists(manager.get(), token2)); |
| } |
| } |
| |
| TEST_F(SharedDictionaryManagerOnDiskTest, LastUsedTime) { |
| net::SharedDictionaryStorageIsolationKey isolation_key( |
| url::Origin::Create(kUrl), kSite); |
| base::Time last_used_time_after_second_get_dict; |
| { |
| std::unique_ptr<SharedDictionaryManager> manager = |
| CreateSharedDictionaryManager(); |
| scoped_refptr<SharedDictionaryStorage> storage = |
| manager->GetStorage(isolation_key); |
| ASSERT_TRUE(storage); |
| // Write the test data to the dictionary. |
| WriteDictionary(storage.get(), GURL("https://origin.test/dict"), |
| "testfile*", kTestData1); |
| FlushCacheTasks(); |
| |
| const auto& dictionary_map = GetOnDiskDictionaryMap(storage.get()); |
| ASSERT_EQ(1u, dictionary_map.size()); |
| ASSERT_EQ(1u, dictionary_map.begin()->second.size()); |
| |
| base::Time initial_last_used_time = |
| dictionary_map.begin()->second.begin()->second.last_used_time(); |
| |
| // Move the clock forward by 1 second. |
| task_environment_.FastForwardBy(base::Seconds(1)); |
| |
| std::unique_ptr<SharedDictionary> dict1 = |
| storage->GetDictionary(GURL("https://origin.test/testfile?1")); |
| base::Time last_used_time_after_first_get_dict = |
| dictionary_map.begin()->second.begin()->second.last_used_time(); |
| |
| // Move the clock forward by 1 second. |
| task_environment_.FastForwardBy(base::Seconds(1)); |
| |
| std::unique_ptr<SharedDictionary> dict2 = |
| storage->GetDictionary(GURL("https://origin.test/testfile?2")); |
| last_used_time_after_second_get_dict = |
| dictionary_map.begin()->second.begin()->second.last_used_time(); |
| EXPECT_NE(initial_last_used_time, last_used_time_after_first_get_dict); |
| EXPECT_NE(last_used_time_after_first_get_dict, |
| last_used_time_after_second_get_dict); |
| // Releasing `dict1`, `dict2`, `storage` and `manager`. |
| } |
| // FlushCacheTasks() to finish the persistence operation. |
| FlushCacheTasks(); |
| |
| // Move the clock forward by 1 day. |
| task_environment_.FastForwardBy(base::Days(1)); |
| |
| // The last_used_time data must be available after recreating `manager`. |
| std::unique_ptr<SharedDictionaryManager> manager = |
| CreateSharedDictionaryManager(); |
| scoped_refptr<SharedDictionaryStorage> storage = |
| manager->GetStorage(isolation_key); |
| ASSERT_TRUE(storage); |
| |
| // RunUntilIdle() to load from the database. |
| task_environment_.RunUntilIdle(); |
| |
| const auto& dictionary_map = GetOnDiskDictionaryMap(storage.get()); |
| ASSERT_EQ(1u, dictionary_map.size()); |
| ASSERT_EQ(1u, dictionary_map.begin()->second.size()); |
| EXPECT_EQ(last_used_time_after_second_get_dict, |
| dictionary_map.begin()->second.begin()->second.last_used_time()); |
| } |
| |
| MATCHER_P(DictionaryUrlIs, |
| url, |
| std::string(negation ? "doesn't have" : "has") + " " + url + |
| " as the URL") { |
| return arg.url().spec() == url; |
| } |
| |
| TEST_F(SharedDictionaryManagerOnDiskTest, ClearData) { |
| net::SharedDictionaryStorageIsolationKey isolation_key( |
| url::Origin::Create(kUrl), kSite); |
| { |
| std::unique_ptr<SharedDictionaryManager> manager = |
| CreateSharedDictionaryManager(); |
| scoped_refptr<SharedDictionaryStorage> storage = |
| manager->GetStorage(isolation_key); |
| ASSERT_TRUE(storage); |
| WriteDictionary(storage.get(), GURL("https://origin.test/1"), "p1*", |
| kTestData1); |
| WriteDictionary(storage.get(), GURL("https://target.test/1"), "p1*", |
| kTestData1); |
| FlushCacheTasks(); |
| |
| // Move the clock forward by 1 day. |
| task_environment_.FastForwardBy(base::Days(1)); |
| WriteDictionary(storage.get(), GURL("https://origin.test/2"), "p2*", |
| kTestData1); |
| WriteDictionary(storage.get(), GURL("https://target.test/2"), "p2*", |
| kTestData1); |
| WriteDictionary(storage.get(), GURL("https://target.test/3"), "p3*", |
| kTestData2); |
| FlushCacheTasks(); |
| |
| // Move the clock forward by 1 day. |
| task_environment_.FastForwardBy(base::Days(1)); |
| WriteDictionary(storage.get(), GURL("https://origin.test/3"), "p3*", |
| kTestData1); |
| WriteDictionary(storage.get(), GURL("https://target.test/4"), "p4*", |
| kTestData1); |
| FlushCacheTasks(); |
| |
| // Move the clock forward by 12 hours. |
| task_environment_.FastForwardBy(base::Hours(12)); |
| |
| // Get a dictionary before calling ClearData(). |
| std::unique_ptr<SharedDictionary> dict = |
| storage->GetDictionary(GURL("https://target.test/p3?")); |
| ASSERT_TRUE(dict); |
| |
| base::RunLoop run_loop; |
| manager->ClearData(base::Time::Now() - base::Days(2), |
| base::Time::Now() - base::Days(1), |
| base::BindRepeating([](const GURL& url) { |
| return url == GURL("https://target.test/"); |
| }), |
| run_loop.QuitClosure()); |
| run_loop.Run(); |
| |
| // The dictionaries of "https://target.test/2" and "https://target.test/3" |
| // must be deleted. |
| EXPECT_THAT( |
| GetOnDiskDictionaryMap(storage.get()), |
| ElementsAre( |
| Pair(url::SchemeHostPort(GURL("https://origin.test/")), |
| ElementsAre( |
| Pair("/p1*", DictionaryUrlIs("https://origin.test/1")), |
| Pair("/p2*", DictionaryUrlIs("https://origin.test/2")), |
| Pair("/p3*", DictionaryUrlIs("https://origin.test/3")))), |
| Pair(url::SchemeHostPort(GURL("https://target.test/")), |
| ElementsAre( |
| Pair("/p1*", DictionaryUrlIs("https://target.test/1")), |
| Pair("/p4*", DictionaryUrlIs("https://target.test/4")))))); |
| |
| // We can still read the deleted dictionary from `dict`. |
| net::TestCompletionCallback read_callback; |
| EXPECT_EQ(net::OK, |
| read_callback.GetResult(dict->ReadAll(read_callback.callback()))); |
| EXPECT_EQ(kTestData2, |
| std::string(reinterpret_cast<const char*>(dict->data()->data()), |
| dict->size())); |
| // Releasing `dict`, `storage` and `manager`. |
| } |
| // FlushCacheTasks() to finish the persistence operation. |
| FlushCacheTasks(); |
| |
| std::unique_ptr<SharedDictionaryManager> manager = |
| CreateSharedDictionaryManager(); |
| scoped_refptr<SharedDictionaryStorage> storage = |
| manager->GetStorage(isolation_key); |
| ASSERT_TRUE(storage); |
| |
| // RunUntilIdle() to load from the database. |
| task_environment_.RunUntilIdle(); |
| |
| // The dictionaries of "https://target.test/2" and "https://target.test/3" |
| // must have been deleted also from the disk. |
| EXPECT_THAT( |
| GetOnDiskDictionaryMap(storage.get()), |
| ElementsAre( |
| Pair(url::SchemeHostPort(GURL("https://origin.test/")), |
| ElementsAre( |
| Pair("/p1*", DictionaryUrlIs("https://origin.test/1")), |
| Pair("/p2*", DictionaryUrlIs("https://origin.test/2")), |
| Pair("/p3*", DictionaryUrlIs("https://origin.test/3")))), |
| Pair(url::SchemeHostPort(GURL("https://target.test/")), |
| ElementsAre( |
| Pair("/p1*", DictionaryUrlIs("https://target.test/1")), |
| Pair("/p4*", DictionaryUrlIs("https://target.test/4")))))); |
| } |
| |
| TEST_F(SharedDictionaryManagerOnDiskTest, ClearDataSerializedOperation) { |
| net::SharedDictionaryStorageIsolationKey isolation_key( |
| url::Origin::Create(kUrl), kSite); |
| std::unique_ptr<SharedDictionaryManager> manager = |
| CreateSharedDictionaryManager(); |
| scoped_refptr<SharedDictionaryStorage> storage = |
| manager->GetStorage(isolation_key); |
| ASSERT_TRUE(storage); |
| WriteDictionary(storage.get(), GURL("https://target1.test/d"), "p*", |
| kTestData1); |
| WriteDictionary(storage.get(), GURL("https://target2.test/d"), "p*", |
| kTestData1); |
| FlushCacheTasks(); |
| |
| EXPECT_THAT( |
| GetOnDiskDictionaryMap(storage.get()), |
| ElementsAre( |
| Pair(url::SchemeHostPort(GURL("https://target1.test/")), |
| ElementsAre( |
| Pair("/p*", DictionaryUrlIs("https://target1.test/d")))), |
| Pair(url::SchemeHostPort(GURL("https://target2.test/")), |
| ElementsAre( |
| Pair("/p*", DictionaryUrlIs("https://target2.test/d")))))); |
| |
| base::RunLoop run_loop1; |
| manager->ClearData(base::Time(), base::Time::Max(), |
| base::BindRepeating([](const GURL& url) { |
| return url == GURL("https://target1.test/"); |
| }), |
| run_loop1.QuitClosure()); |
| base::RunLoop run_loop2; |
| manager->ClearData(base::Time(), base::Time::Max(), |
| base::BindRepeating([](const GURL& url) { |
| return url == GURL("https://target2.test/"); |
| }), |
| run_loop2.QuitClosure()); |
| run_loop1.Run(); |
| |
| // The dictionary of "https://target2.test/" must still alive, because the |
| // operation of ClearData is serialized. |
| EXPECT_THAT(GetOnDiskDictionaryMap(storage.get()), |
| ElementsAre(Pair( |
| url::SchemeHostPort(GURL("https://target2.test/")), |
| ElementsAre(Pair( |
| "/p*", DictionaryUrlIs("https://target2.test/d")))))); |
| |
| run_loop2.Run(); |
| |
| EXPECT_TRUE(GetOnDiskDictionaryMap(storage.get()).empty()); |
| } |
| |
| TEST_F(SharedDictionaryManagerOnDiskTest, ExpiredDictionaryDeletionOnReload) { |
| net::SharedDictionaryStorageIsolationKey isolation_key( |
| url::Origin::Create(kUrl), kSite); |
| base::UnguessableToken token1, token2; |
| { |
| std::unique_ptr<SharedDictionaryManager> manager = |
| CreateSharedDictionaryManager(); |
| scoped_refptr<SharedDictionaryStorage> storage = |
| manager->GetStorage(isolation_key); |
| ASSERT_TRUE(storage); |
| WriteDictionaryWithExpiry(storage.get(), GURL("https://target1.test/d"), |
| "p*", base::Seconds(10), kTestData1); |
| WriteDictionaryWithExpiry(storage.get(), GURL("https://target2.test/d"), |
| "p*", base::Seconds(11), kTestData2); |
| // FlushCacheTasks() to finish the persistence operation. |
| FlushCacheTasks(); |
| |
| const auto& dictionary_map = GetOnDiskDictionaryMap(storage.get()); |
| token1 = GetDiskCacheKeyTokenOfFirstDictionary(dictionary_map, |
| "https://target1.test/"); |
| token2 = GetDiskCacheKeyTokenOfFirstDictionary(dictionary_map, |
| "https://target2.test/"); |
| // Releasing `storage` and `manager`. |
| } |
| |
| // Move the clock forward by 10 second. |
| task_environment_.FastForwardBy(base::Seconds(10)); |
| |
| std::unique_ptr<SharedDictionaryManager> manager = |
| CreateSharedDictionaryManager(); |
| scoped_refptr<SharedDictionaryStorage> storage = |
| manager->GetStorage(isolation_key); |
| ASSERT_TRUE(storage); |
| |
| // RunUntilIdle() to load from the database. |
| task_environment_.RunUntilIdle(); |
| |
| // The first dictionary must have been deleted. |
| EXPECT_THAT(GetOnDiskDictionaryMap(storage.get()), |
| ElementsAre(Pair( |
| url::SchemeHostPort(GURL("https://target2.test/")), |
| ElementsAre(Pair( |
| "/p*", DictionaryUrlIs("https://target2.test/d")))))); |
| EXPECT_FALSE(DiskCacheEntryExists(manager.get(), token1)); |
| EXPECT_TRUE(DiskCacheEntryExists(manager.get(), token2)); |
| } |
| |
| TEST_F(SharedDictionaryManagerOnDiskTest, |
| ExpiredDictionaryDeletionOnNewDictionary) { |
| net::SharedDictionaryStorageIsolationKey isolation_key( |
| url::Origin::Create(kUrl), kSite); |
| std::unique_ptr<SharedDictionaryManager> manager = |
| CreateSharedDictionaryManager(); |
| scoped_refptr<SharedDictionaryStorage> storage = |
| manager->GetStorage(isolation_key); |
| ASSERT_TRUE(storage); |
| // Write a first dictionary. |
| WriteDictionaryWithExpiry(storage.get(), GURL("https://target1.test/d"), "p*", |
| base::Seconds(10), kTestData1); |
| // FlushCacheTasks() to finish the persistence operation. |
| FlushCacheTasks(); |
| base::UnguessableToken token1 = GetDiskCacheKeyTokenOfFirstDictionary( |
| GetOnDiskDictionaryMap(storage.get()), "https://target1.test/"); |
| |
| // Move the clock forward by 10 second. |
| task_environment_.FastForwardBy(base::Seconds(10)); |
| // The first dictionary still exists. |
| EXPECT_THAT(GetOnDiskDictionaryMap(storage.get()), |
| ElementsAre(Pair( |
| url::SchemeHostPort(GURL("https://target1.test/")), |
| ElementsAre(Pair( |
| "/p*", DictionaryUrlIs("https://target1.test/d")))))); |
| EXPECT_TRUE(DiskCacheEntryExists(manager.get(), token1)); |
| |
| // Write a second dictionary. |
| WriteDictionaryWithExpiry(storage.get(), GURL("https://target2.test/d"), "p*", |
| base::Seconds(10), kTestData2); |
| // FlushCacheTasks() to finish the persistence operation. |
| FlushCacheTasks(); |
| base::UnguessableToken token2 = GetDiskCacheKeyTokenOfFirstDictionary( |
| GetOnDiskDictionaryMap(storage.get()), "https://target2.test/"); |
| |
| // The first dictionary must have been deleted. |
| EXPECT_THAT(GetOnDiskDictionaryMap(storage.get()), |
| ElementsAre(Pair( |
| url::SchemeHostPort(GURL("https://target2.test/")), |
| ElementsAre(Pair( |
| "/p*", DictionaryUrlIs("https://target2.test/d")))))); |
| EXPECT_FALSE(DiskCacheEntryExists(manager.get(), token1)); |
| EXPECT_TRUE(DiskCacheEntryExists(manager.get(), token2)); |
| } |
| |
| TEST_F(SharedDictionaryManagerOnDiskTest, |
| ExpiredDictionaryDeletionOnSetCacheMaxSize) { |
| net::SharedDictionaryStorageIsolationKey isolation_key( |
| url::Origin::Create(kUrl), kSite); |
| std::unique_ptr<SharedDictionaryManager> manager = |
| CreateSharedDictionaryManager(); |
| scoped_refptr<SharedDictionaryStorage> storage = |
| manager->GetStorage(isolation_key); |
| ASSERT_TRUE(storage); |
| WriteDictionaryWithExpiry(storage.get(), GURL("https://target1.test/d"), "p*", |
| base::Seconds(10), kTestData1); |
| // FlushCacheTasks() to finish the persistence operation. |
| FlushCacheTasks(); |
| base::UnguessableToken token = GetDiskCacheKeyTokenOfFirstDictionary( |
| GetOnDiskDictionaryMap(storage.get()), "https://target1.test/"); |
| |
| // Move the clock forward by 10 second. |
| task_environment_.FastForwardBy(base::Seconds(10)); |
| // The first dictionary still exists. |
| EXPECT_THAT(GetOnDiskDictionaryMap(storage.get()), |
| ElementsAre(Pair( |
| url::SchemeHostPort(GURL("https://target1.test/")), |
| ElementsAre(Pair( |
| "/p*", DictionaryUrlIs("https://target1.test/d")))))); |
| |
| // Set the max size to kTestData1.size() * 100 |
| manager->SetCacheMaxSize(kTestData1.size() * 100); |
| |
| // FlushCacheTasks() to finish the persistence operation. |
| FlushCacheTasks(); |
| |
| // The dictionary must be deleted. |
| EXPECT_TRUE(GetOnDiskDictionaryMap(storage.get()).empty()); |
| EXPECT_FALSE(DiskCacheEntryExists(manager.get(), token)); |
| } |
| |
| TEST_F(SharedDictionaryManagerOnDiskTest, |
| ExpiredDictionaryDeletionOnClearData) { |
| net::SharedDictionaryStorageIsolationKey isolation_key( |
| url::Origin::Create(kUrl), kSite); |
| std::unique_ptr<SharedDictionaryManager> manager = |
| CreateSharedDictionaryManager(); |
| scoped_refptr<SharedDictionaryStorage> storage = |
| manager->GetStorage(isolation_key); |
| ASSERT_TRUE(storage); |
| WriteDictionaryWithExpiry(storage.get(), GURL("https://target1.test/d"), "p*", |
| base::Seconds(10), kTestData1); |
| // FlushCacheTasks() to finish the persistence operation. |
| FlushCacheTasks(); |
| base::UnguessableToken token = GetDiskCacheKeyTokenOfFirstDictionary( |
| GetOnDiskDictionaryMap(storage.get()), "https://target1.test/"); |
| |
| // Move the clock forward by 10 second. |
| task_environment_.FastForwardBy(base::Seconds(10)); |
| // The first dictionary still exists. |
| EXPECT_THAT(GetOnDiskDictionaryMap(storage.get()), |
| ElementsAre(Pair( |
| url::SchemeHostPort(GURL("https://target1.test/")), |
| ElementsAre(Pair( |
| "/p*", DictionaryUrlIs("https://target1.test/d")))))); |
| |
| base::RunLoop run_loop; |
| manager->ClearData( |
| base::Time::Now() - base::Days(2), base::Time::Now() - base::Days(1), |
| base::RepeatingCallback<bool(const GURL&)>(), run_loop.QuitClosure()); |
| run_loop.Run(); |
| |
| // FlushCacheTasks() to finish the persistence operation. |
| FlushCacheTasks(); |
| |
| // The dictionary must be deleted. |
| EXPECT_TRUE(GetOnDiskDictionaryMap(storage.get()).empty()); |
| EXPECT_FALSE(DiskCacheEntryExists(manager.get(), token)); |
| } |
| |
| TEST_F(SharedDictionaryManagerOnDiskTest, CacheEvictionOnReload) { |
| net::SharedDictionaryStorageIsolationKey isolation_key( |
| url::Origin::Create(kUrl), kSite); |
| base::UnguessableToken token1, token2, token3; |
| { |
| std::unique_ptr<SharedDictionaryManager> manager = |
| CreateSharedDictionaryManager(); |
| scoped_refptr<SharedDictionaryStorage> storage = |
| manager->GetStorage(isolation_key); |
| ASSERT_TRUE(storage); |
| WriteDictionary(storage.get(), GURL("https://target1.test/d"), "p*", |
| kTestData1); |
| task_environment_.FastForwardBy(base::Seconds(1)); |
| WriteDictionary(storage.get(), GURL("https://target2.test/d"), "p*", |
| kTestData1); |
| task_environment_.FastForwardBy(base::Seconds(1)); |
| WriteDictionary(storage.get(), GURL("https://target3.test/d"), "p*", |
| kTestData1); |
| // FlushCacheTasks() to finish the persistence operation. |
| FlushCacheTasks(); |
| |
| const auto& dictionary_map = GetOnDiskDictionaryMap(storage.get()); |
| token1 = GetDiskCacheKeyTokenOfFirstDictionary(dictionary_map, |
| "https://target1.test/"); |
| token2 = GetDiskCacheKeyTokenOfFirstDictionary(dictionary_map, |
| "https://target2.test/"); |
| token3 = GetDiskCacheKeyTokenOfFirstDictionary(dictionary_map, |
| "https://target3.test/"); |
| |
| // Releasing `storage` and `manager`. |
| } |
| |
| std::unique_ptr<SharedDictionaryManager> manager = |
| CreateSharedDictionaryManager(/*cache_max_size=*/kTestData1.size() * 2); |
| scoped_refptr<SharedDictionaryStorage> storage = |
| manager->GetStorage(isolation_key); |
| ASSERT_TRUE(storage); |
| |
| // RunUntilIdle() to load from the database. |
| task_environment_.RunUntilIdle(); |
| |
| // Only the third dictionary exists. |
| EXPECT_THAT(GetOnDiskDictionaryMap(storage.get()), |
| ElementsAre(Pair( |
| url::SchemeHostPort(GURL("https://target3.test/")), |
| ElementsAre(Pair( |
| "/p*", DictionaryUrlIs("https://target3.test/d")))))); |
| EXPECT_FALSE(DiskCacheEntryExists(manager.get(), token1)); |
| EXPECT_FALSE(DiskCacheEntryExists(manager.get(), token2)); |
| EXPECT_TRUE(DiskCacheEntryExists(manager.get(), token3)); |
| } |
| |
| TEST_F(SharedDictionaryManagerOnDiskTest, CacheEvictionOnSetCacheMaxSize) { |
| net::SharedDictionaryStorageIsolationKey isolation_key( |
| url::Origin::Create(kUrl), kSite); |
| std::unique_ptr<SharedDictionaryManager> manager = |
| CreateSharedDictionaryManager(); |
| scoped_refptr<SharedDictionaryStorage> storage = |
| manager->GetStorage(isolation_key); |
| ASSERT_TRUE(storage); |
| WriteDictionary(storage.get(), GURL("https://target1.test/d"), "p*", |
| kTestData1); |
| task_environment_.FastForwardBy(base::Seconds(1)); |
| WriteDictionary(storage.get(), GURL("https://target2.test/d"), "p*", |
| kTestData1); |
| task_environment_.FastForwardBy(base::Seconds(1)); |
| WriteDictionary(storage.get(), GURL("https://target3.test/d"), "p*", |
| kTestData1); |
| // FlushCacheTasks() to finish the persistence operation. |
| FlushCacheTasks(); |
| |
| const auto& dictionary_map = GetOnDiskDictionaryMap(storage.get()); |
| base::UnguessableToken token1 = GetDiskCacheKeyTokenOfFirstDictionary( |
| dictionary_map, "https://target1.test/"); |
| base::UnguessableToken token2 = GetDiskCacheKeyTokenOfFirstDictionary( |
| dictionary_map, "https://target2.test/"); |
| base::UnguessableToken token3 = GetDiskCacheKeyTokenOfFirstDictionary( |
| dictionary_map, "https://target3.test/"); |
| |
| manager->SetCacheMaxSize(/*cache_max_size=*/kTestData1.size() * 2); |
| // RunUntilIdle() to load from the database. |
| task_environment_.RunUntilIdle(); |
| |
| // Only the third dictionary exists. |
| EXPECT_THAT(GetOnDiskDictionaryMap(storage.get()), |
| ElementsAre(Pair( |
| url::SchemeHostPort(GURL("https://target3.test/")), |
| ElementsAre(Pair( |
| "/p*", DictionaryUrlIs("https://target3.test/d")))))); |
| EXPECT_FALSE(DiskCacheEntryExists(manager.get(), token1)); |
| EXPECT_FALSE(DiskCacheEntryExists(manager.get(), token2)); |
| EXPECT_TRUE(DiskCacheEntryExists(manager.get(), token3)); |
| } |
| |
| TEST_F(SharedDictionaryManagerOnDiskTest, CacheEvictionOnNewDictionary) { |
| net::SharedDictionaryStorageIsolationKey isolation_key( |
| url::Origin::Create(kUrl), kSite); |
| std::unique_ptr<SharedDictionaryManager> manager = |
| CreateSharedDictionaryManager(); |
| manager->SetCacheMaxSize(/*cache_max_size=*/kTestData1.size() * 2); |
| |
| scoped_refptr<SharedDictionaryStorage> storage = |
| manager->GetStorage(isolation_key); |
| ASSERT_TRUE(storage); |
| |
| WriteDictionary(storage.get(), GURL("https://target1.test/d"), "p*", |
| kTestData1); |
| FlushCacheTasks(); |
| base::UnguessableToken token1 = GetDiskCacheKeyTokenOfFirstDictionary( |
| GetOnDiskDictionaryMap(storage.get()), "https://target1.test/"); |
| |
| task_environment_.FastForwardBy(base::Seconds(1)); |
| |
| WriteDictionary(storage.get(), GURL("https://target2.test/d"), "p*", |
| kTestData1); |
| FlushCacheTasks(); |
| base::UnguessableToken token2 = GetDiskCacheKeyTokenOfFirstDictionary( |
| GetOnDiskDictionaryMap(storage.get()), "https://target2.test/"); |
| |
| task_environment_.FastForwardBy(base::Seconds(1)); |
| |
| // Both the dictinaries exist. |
| EXPECT_THAT( |
| GetOnDiskDictionaryMap(storage.get()), |
| ElementsAre( |
| Pair(url::SchemeHostPort(GURL("https://target1.test/")), |
| ElementsAre( |
| Pair("/p*", DictionaryUrlIs("https://target1.test/d")))), |
| Pair(url::SchemeHostPort(GURL("https://target2.test/")), |
| ElementsAre( |
| Pair("/p*", DictionaryUrlIs("https://target2.test/d")))))); |
| EXPECT_TRUE(DiskCacheEntryExists(manager.get(), token1)); |
| EXPECT_TRUE(DiskCacheEntryExists(manager.get(), token2)); |
| |
| WriteDictionary(storage.get(), GURL("https://target3.test/d"), "p*", |
| kTestData1); |
| FlushCacheTasks(); |
| base::UnguessableToken token3 = GetDiskCacheKeyTokenOfFirstDictionary( |
| GetOnDiskDictionaryMap(storage.get()), "https://target3.test/"); |
| |
| // Only the third dictionary exists. |
| EXPECT_THAT(GetOnDiskDictionaryMap(storage.get()), |
| ElementsAre(Pair( |
| url::SchemeHostPort(GURL("https://target3.test/")), |
| ElementsAre(Pair( |
| "/p*", DictionaryUrlIs("https://target3.test/d")))))); |
| EXPECT_FALSE(DiskCacheEntryExists(manager.get(), token1)); |
| EXPECT_FALSE(DiskCacheEntryExists(manager.get(), token2)); |
| EXPECT_TRUE(DiskCacheEntryExists(manager.get(), token3)); |
| } |
| |
| TEST_F(SharedDictionaryManagerOnDiskTest, |
| CacheEvictionAfterUpdatingLastUsedTime) { |
| net::SharedDictionaryStorageIsolationKey isolation_key( |
| url::Origin::Create(kUrl), kSite); |
| std::unique_ptr<SharedDictionaryManager> manager = |
| CreateSharedDictionaryManager(); |
| scoped_refptr<SharedDictionaryStorage> storage = |
| manager->GetStorage(isolation_key); |
| ASSERT_TRUE(storage); |
| // Write dictionary 1. |
| WriteDictionary(storage.get(), GURL("https://target1.test/d"), "p*", |
| kTestData1); |
| FlushCacheTasks(); |
| base::UnguessableToken token1 = GetDiskCacheKeyTokenOfFirstDictionary( |
| GetOnDiskDictionaryMap(storage.get()), "https://target1.test/"); |
| |
| task_environment_.FastForwardBy(base::Seconds(1)); |
| |
| // Write dictionary 2. |
| WriteDictionary(storage.get(), GURL("https://target2.test/d"), "p*", |
| kTestData1); |
| FlushCacheTasks(); |
| base::UnguessableToken token2 = GetDiskCacheKeyTokenOfFirstDictionary( |
| GetOnDiskDictionaryMap(storage.get()), "https://target2.test/"); |
| |
| task_environment_.FastForwardBy(base::Seconds(1)); |
| |
| // Write dictionary 3. |
| WriteDictionary(storage.get(), GURL("https://target3.test/d"), "p*", |
| kTestData1); |
| FlushCacheTasks(); |
| base::UnguessableToken token3 = GetDiskCacheKeyTokenOfFirstDictionary( |
| GetOnDiskDictionaryMap(storage.get()), "https://target3.test/"); |
| |
| task_environment_.FastForwardBy(base::Seconds(1)); |
| |
| // Write dictionary 4. |
| WriteDictionary(storage.get(), GURL("https://target4.test/d"), "p*", |
| kTestData1); |
| FlushCacheTasks(); |
| base::UnguessableToken token4 = GetDiskCacheKeyTokenOfFirstDictionary( |
| GetOnDiskDictionaryMap(storage.get()), "https://target4.test/"); |
| |
| task_environment_.FastForwardBy(base::Seconds(1)); |
| |
| // Call GetDictionary to update the last used time of the dictionary 1. |
| std::unique_ptr<SharedDictionary> dict1 = |
| storage->GetDictionary(GURL("https://target1.test/path?")); |
| ASSERT_TRUE(dict1); |
| |
| // Set the max size to kTestData1.size() * 3. The low water mark will be |
| // kTestData1.size() * 2.7 (3 * 0.9). |
| manager->SetCacheMaxSize(kTestData1.size() * 3); |
| |
| // FlushCacheTasks() to finish the persistence operation. |
| FlushCacheTasks(); |
| // The dictionary 1 and 4 must still exist. |
| EXPECT_THAT( |
| GetOnDiskDictionaryMap(storage.get()), |
| ElementsAre( |
| Pair(url::SchemeHostPort(GURL("https://target1.test/")), |
| ElementsAre( |
| Pair("/p*", DictionaryUrlIs("https://target1.test/d")))), |
| Pair(url::SchemeHostPort(GURL("https://target4.test/")), |
| ElementsAre( |
| Pair("/p*", DictionaryUrlIs("https://target4.test/d")))))); |
| EXPECT_TRUE(DiskCacheEntryExists(manager.get(), token1)); |
| EXPECT_FALSE(DiskCacheEntryExists(manager.get(), token2)); |
| EXPECT_FALSE(DiskCacheEntryExists(manager.get(), token3)); |
| EXPECT_TRUE(DiskCacheEntryExists(manager.get(), token4)); |
| } |
| |
| TEST_F(SharedDictionaryManagerOnDiskTest, |
| MismatchingEntryDeletionMetadataUnavailableDictionary) { |
| const base::UnguessableToken token = base::UnguessableToken::Create(); |
| const std::string entry_key = token.ToString(); |
| const std::string kTestData = "Hello world"; |
| std::unique_ptr<SharedDictionaryManager> manager = |
| CreateSharedDictionaryManager(); |
| |
| WriteDiskCacheEntry(manager.get(), entry_key, kTestData); |
| EXPECT_TRUE(DiskCacheEntryExists(manager.get(), entry_key)); |
| |
| base::RunLoop run_loop; |
| manager->ClearData( |
| base::Time::Now() - base::Days(2), base::Time::Now() - base::Days(1), |
| base::RepeatingCallback<bool(const GURL&)>(), run_loop.QuitClosure()); |
| run_loop.Run(); |
| |
| EXPECT_TRUE(DiskCacheEntryExists(manager.get(), entry_key)); |
| base::HistogramTester histogram_tester; |
| task_environment_.RunUntilIdle(); |
| EXPECT_FALSE(DiskCacheEntryExists(manager.get(), entry_key)); |
| histogram_tester.ExpectUniqueSample( |
| "Net.SharedDictionaryManagerOnDisk.InvalidDiskCacheEntryCount", 0, 1); |
| histogram_tester.ExpectUniqueSample( |
| "Net.SharedDictionaryManagerOnDisk.MetadataMissingDictionaryCount", 1, 1); |
| histogram_tester.ExpectUniqueSample( |
| "Net.SharedDictionaryManagerOnDisk.DiskCacheEntryMissingDictionaryCount", |
| 0, 1); |
| } |
| |
| TEST_F(SharedDictionaryManagerOnDiskTest, |
| MismatchingEntryDeletionInvalidDiskCacheEntry) { |
| const std::string kTestKey = "test"; |
| const std::string kTestData = "Hello world"; |
| std::unique_ptr<SharedDictionaryManager> manager = |
| CreateSharedDictionaryManager(); |
| |
| WriteDiskCacheEntry(manager.get(), kTestKey, kTestData); |
| EXPECT_TRUE(DiskCacheEntryExists(manager.get(), kTestKey)); |
| |
| base::RunLoop run_loop; |
| manager->ClearData( |
| base::Time::Now() - base::Days(2), base::Time::Now() - base::Days(1), |
| base::RepeatingCallback<bool(const GURL&)>(), run_loop.QuitClosure()); |
| run_loop.Run(); |
| |
| EXPECT_TRUE(DiskCacheEntryExists(manager.get(), kTestKey)); |
| base::HistogramTester histogram_tester; |
| task_environment_.RunUntilIdle(); |
| EXPECT_FALSE(DiskCacheEntryExists(manager.get(), kTestKey)); |
| histogram_tester.ExpectUniqueSample( |
| "Net.SharedDictionaryManagerOnDisk.InvalidDiskCacheEntryCount", 1, 1); |
| histogram_tester.ExpectUniqueSample( |
| "Net.SharedDictionaryManagerOnDisk.MetadataMissingDictionaryCount", 0, 1); |
| histogram_tester.ExpectUniqueSample( |
| "Net.SharedDictionaryManagerOnDisk.DiskCacheEntryMissingDictionaryCount", |
| 0, 1); |
| } |
| |
| TEST_F(SharedDictionaryManagerOnDiskTest, |
| MismatchingEntryDeletionDiskCacheEntryUnavailableDictionary) { |
| net::SharedDictionaryStorageIsolationKey isolation_key( |
| url::Origin::Create(kUrl), kSite); |
| { |
| std::unique_ptr<SharedDictionaryManager> manager = |
| CreateSharedDictionaryManager(); |
| scoped_refptr<SharedDictionaryStorage> storage = |
| manager->GetStorage(isolation_key); |
| ASSERT_TRUE(storage); |
| WriteDictionary(storage.get(), GURL("https://target1.test/d"), "p*", |
| kTestData1); |
| // FlushCacheTasks() to finish the persistence operation. |
| FlushCacheTasks(); |
| |
| const auto& dictionary_map = GetOnDiskDictionaryMap(storage.get()); |
| EXPECT_THAT(dictionary_map, |
| ElementsAre(Pair( |
| url::SchemeHostPort(GURL("https://target1.test/")), |
| ElementsAre(Pair( |
| "/p*", DictionaryUrlIs("https://target1.test/d")))))); |
| |
| DoomDiskCacheEntry(manager.get(), |
| GetDiskCacheKeyTokenOfFirstDictionary( |
| dictionary_map, "https://target1.test/")); |
| |
| base::RunLoop run_loop; |
| manager->ClearData( |
| base::Time::Now() - base::Days(2), base::Time::Now() - base::Days(1), |
| base::RepeatingCallback<bool(const GURL&)>(), run_loop.QuitClosure()); |
| run_loop.Run(); |
| |
| EXPECT_FALSE(GetOnDiskDictionaryMap(storage.get()).empty()); |
| |
| base::HistogramTester histogram_tester; |
| task_environment_.RunUntilIdle(); |
| |
| EXPECT_TRUE(GetOnDiskDictionaryMap(storage.get()).empty()); |
| |
| histogram_tester.ExpectUniqueSample( |
| "Net.SharedDictionaryManagerOnDisk.InvalidDiskCacheEntryCount", 0, 1); |
| histogram_tester.ExpectUniqueSample( |
| "Net.SharedDictionaryManagerOnDisk.MetadataMissingDictionaryCount", 0, |
| 1); |
| histogram_tester.ExpectUniqueSample( |
| "Net.SharedDictionaryManagerOnDisk." |
| "DiskCacheEntryMissingDictionaryCount", |
| 1, 1); |
| |
| // Releasing `storage` and `manager`. |
| } |
| // FlushCacheTasks() to finish the persistence operation. |
| FlushCacheTasks(); |
| |
| std::unique_ptr<SharedDictionaryManager> manager = |
| CreateSharedDictionaryManager(); |
| scoped_refptr<SharedDictionaryStorage> storage = |
| manager->GetStorage(isolation_key); |
| ASSERT_TRUE(storage); |
| |
| // RunUntilIdle() to load from the database. |
| task_environment_.RunUntilIdle(); |
| |
| // The storage must be empty. |
| EXPECT_TRUE(GetOnDiskDictionaryMap(storage.get()).empty()); |
| } |
| |
| TEST_F(SharedDictionaryManagerOnDiskTest, |
| MismatchingEntryDeletionCanBeTriggeredOnlyOnce) { |
| std::unique_ptr<SharedDictionaryManager> manager = |
| CreateSharedDictionaryManager(); |
| // Calls ClearData() to trigger MismatchingEntryDeletionTask. |
| base::RunLoop run_loop1; |
| manager->ClearData( |
| base::Time::Now() - base::Days(2), base::Time::Now() - base::Days(1), |
| base::RepeatingCallback<bool(const GURL&)>(), run_loop1.QuitClosure()); |
| run_loop1.Run(); |
| |
| base::HistogramTester histogram_tester; |
| task_environment_.RunUntilIdle(); |
| histogram_tester.ExpectUniqueSample( |
| "Net.SharedDictionaryManagerOnDisk.InvalidDiskCacheEntryCount", 0, 1); |
| histogram_tester.ExpectUniqueSample( |
| "Net.SharedDictionaryManagerOnDisk.MetadataMissingDictionaryCount", 0, 1); |
| histogram_tester.ExpectUniqueSample( |
| "Net.SharedDictionaryManagerOnDisk.DiskCacheEntryMissingDictionaryCount", |
| 0, 1); |
| |
| const base::UnguessableToken token = base::UnguessableToken::Create(); |
| const std::string entry_key = token.ToString(); |
| const std::string kTestData = "Hello world"; |
| |
| WriteDiskCacheEntry(manager.get(), entry_key, kTestData); |
| EXPECT_TRUE(DiskCacheEntryExists(manager.get(), entry_key)); |
| |
| base::RunLoop run_loop2; |
| manager->ClearData( |
| base::Time::Now() - base::Days(2), base::Time::Now() - base::Days(1), |
| base::RepeatingCallback<bool(const GURL&)>(), run_loop2.QuitClosure()); |
| run_loop2.Run(); |
| |
| EXPECT_TRUE(DiskCacheEntryExists(manager.get(), entry_key)); |
| task_environment_.RunUntilIdle(); |
| // MismatchingEntryDeletionTask can be triggered only once. So the disk cache |
| // entry should not be deleted. |
| EXPECT_TRUE(DiskCacheEntryExists(manager.get(), entry_key)); |
| } |
| |
| TEST_F(SharedDictionaryManagerOnDiskTest, |
| MismatchingEntryDeletionWritingEntryMustNotBeDeleted) { |
| net::SharedDictionaryStorageIsolationKey isolation_key( |
| url::Origin::Create(kUrl), kSite); |
| std::unique_ptr<SharedDictionaryManager> manager = |
| CreateSharedDictionaryManager(); |
| scoped_refptr<SharedDictionaryStorage> storage = |
| manager->GetStorage(isolation_key); |
| ASSERT_TRUE(storage); |
| |
| // Start writing a dictionary. |
| scoped_refptr<net::HttpResponseHeaders> headers = |
| net::HttpResponseHeaders::TryToCreate(base::StrCat( |
| {"HTTP/1.1 200 OK\n", shared_dictionary::kUseAsDictionaryHeaderName, |
| ": match=\"/p*\"\n\n"})); |
| ASSERT_TRUE(headers); |
| scoped_refptr<SharedDictionaryWriter> writer = storage->MaybeCreateWriter( |
| GURL("https://target1.test/d"), base::Time::Now(), *headers); |
| ASSERT_TRUE(writer); |
| writer->Append(kTestData1.c_str(), kTestData1.size()); |
| |
| base::RunLoop run_loop; |
| manager->ClearData( |
| base::Time::Now() - base::Days(2), base::Time::Now() - base::Days(1), |
| base::RepeatingCallback<bool(const GURL&)>(), run_loop.QuitClosure()); |
| run_loop.Run(); |
| |
| base::HistogramTester histogram_tester; |
| // FlushCacheTasks() to finish the persistence operation. |
| FlushCacheTasks(); |
| |
| histogram_tester.ExpectUniqueSample( |
| "Net.SharedDictionaryManagerOnDisk.InvalidDiskCacheEntryCount", 0, 1); |
| histogram_tester.ExpectUniqueSample( |
| "Net.SharedDictionaryManagerOnDisk.MetadataMissingDictionaryCount", 0, 1); |
| histogram_tester.ExpectUniqueSample( |
| "Net.SharedDictionaryManagerOnDisk.DiskCacheEntryMissingDictionaryCount", |
| 0, 1); |
| |
| // Finish writing the dictionary. |
| writer->Finish(); |
| |
| // FlushCacheTasks() to finish the persistence operation. |
| FlushCacheTasks(); |
| |
| // There should be no change in MismatchingEntryDeletionTask related metrics. |
| histogram_tester.ExpectUniqueSample( |
| "Net.SharedDictionaryManagerOnDisk.InvalidDiskCacheEntryCount", 0, 1); |
| histogram_tester.ExpectUniqueSample( |
| "Net.SharedDictionaryManagerOnDisk.MetadataMissingDictionaryCount", 0, 1); |
| histogram_tester.ExpectUniqueSample( |
| "Net.SharedDictionaryManagerOnDisk.DiskCacheEntryMissingDictionaryCount", |
| 0, 1); |
| |
| const auto& dictionary_map = GetOnDiskDictionaryMap(storage.get()); |
| EXPECT_THAT(dictionary_map, |
| ElementsAre(Pair( |
| url::SchemeHostPort(GURL("https://target1.test/")), |
| ElementsAre(Pair( |
| "/p*", DictionaryUrlIs("https://target1.test/d")))))); |
| } |
| |
| TEST_F(SharedDictionaryManagerOnDiskTest, |
| MismatchingEntryDeletionWritingDiskCacheEntryMustNotBeDeleted) { |
| net::SharedDictionaryStorageIsolationKey isolation_key( |
| url::Origin::Create(kUrl), kSite); |
| std::unique_ptr<SharedDictionaryManager> manager = |
| CreateSharedDictionaryManager(); |
| scoped_refptr<SharedDictionaryStorage> storage = |
| manager->GetStorage(isolation_key); |
| ASSERT_TRUE(storage); |
| // Write a dictionary. |
| WriteDictionary(storage.get(), GURL("https://target1.test/d"), "p*", |
| kTestData1); |
| // But do not call FlushCacheTasks() to keep writing the disk cache entry. |
| base::RunLoop run_loop; |
| manager->ClearData( |
| base::Time::Now() - base::Days(2), base::Time::Now() - base::Days(1), |
| base::RepeatingCallback<bool(const GURL&)>(), run_loop.QuitClosure()); |
| run_loop.Run(); |
| |
| base::HistogramTester histogram_tester; |
| |
| // FlushCacheTasks() to finish the persistence operation. |
| FlushCacheTasks(); |
| |
| const auto& dictionary_map = GetOnDiskDictionaryMap(storage.get()); |
| EXPECT_THAT(dictionary_map, |
| ElementsAre(Pair( |
| url::SchemeHostPort(GURL("https://target1.test/")), |
| ElementsAre(Pair( |
| "/p*", DictionaryUrlIs("https://target1.test/d")))))); |
| histogram_tester.ExpectUniqueSample( |
| "Net.SharedDictionaryManagerOnDisk.InvalidDiskCacheEntryCount", 0, 1); |
| histogram_tester.ExpectUniqueSample( |
| "Net.SharedDictionaryManagerOnDisk.MetadataMissingDictionaryCount", 0, 1); |
| histogram_tester.ExpectUniqueSample( |
| "Net.SharedDictionaryManagerOnDisk.DiskCacheEntryMissingDictionaryCount", |
| 0, 1); |
| } |
| |
| } // namespace network |