// Copyright 2014 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 "net/sdch/sdch_owner.h" #include #include "base/location.h" #include "base/macros.h" #include "base/memory/memory_pressure_listener.h" #include "base/prefs/testing_pref_store.h" #include "base/run_loop.h" #include "base/single_thread_task_runner.h" #include "base/strings/stringprintf.h" #include "base/test/histogram_tester.h" #include "base/test/simple_test_clock.h" #include "base/thread_task_runner_handle.h" #include "base/values.h" #include "net/base/sdch_manager.h" #include "net/log/net_log.h" #include "net/url_request/url_request.h" #include "net/url_request/url_request_context.h" #include "net/url_request/url_request_error_job.h" #include "net/url_request/url_request_job.h" #include "net/url_request/url_request_job_factory.h" #include "net/url_request/url_request_test_util.h" #include "testing/gtest/include/gtest/gtest.h" namespace { bool GetDictionaryForURL(TestingPrefStore* store, const GURL& url, std::string* hash, base::DictionaryValue** dict) { base::Value* sdch_val = nullptr; base::DictionaryValue* sdch_dict = nullptr; if (!store->GetMutableValue("SDCH", &sdch_val)) return false; if (!sdch_val->GetAsDictionary(&sdch_dict)) return false; base::DictionaryValue* dicts_dict = nullptr; if (!sdch_dict->GetDictionary("dictionaries", &dicts_dict)) return false; base::DictionaryValue::Iterator it(*dicts_dict); while (!it.IsAtEnd()) { const base::DictionaryValue* d = nullptr; if (!it.value().GetAsDictionary(&d)) continue; std::string dict_url; if (d->GetString("url", &dict_url) && dict_url == url.spec()) { if (hash) *hash = it.key(); if (dict) dicts_dict->GetDictionary(it.key(), dict); return true; } it.Advance(); } return false; } } // namespace namespace net { static const char generic_url[] = "http://www.example.com"; static const char generic_domain[] = "www.example.com"; static std::string NewSdchDictionary(size_t dictionary_size) { std::string dictionary; dictionary.append("Domain: "); dictionary.append(generic_domain); dictionary.append("\n"); dictionary.append("\n"); size_t original_dictionary_size = dictionary.size(); dictionary.resize(dictionary_size); for (size_t i = original_dictionary_size; i < dictionary_size; ++i) dictionary[i] = static_cast((i % 127) + 1); return dictionary; } int outstanding_url_request_error_counting_jobs = 0; base::Closure* empty_url_request_jobs_callback = 0; // Variation of URLRequestErrorJob to count number of outstanding // instances and notify when that goes to zero. class URLRequestErrorCountingJob : public URLRequestJob { public: URLRequestErrorCountingJob(URLRequest* request, NetworkDelegate* network_delegate, int error) : URLRequestJob(request, network_delegate), error_(error), weak_factory_(this) { ++outstanding_url_request_error_counting_jobs; } void Start() override { base::ThreadTaskRunnerHandle::Get()->PostTask( FROM_HERE, base::Bind(&URLRequestErrorCountingJob::StartAsync, weak_factory_.GetWeakPtr())); } private: ~URLRequestErrorCountingJob() override { --outstanding_url_request_error_counting_jobs; if (0 == outstanding_url_request_error_counting_jobs && empty_url_request_jobs_callback) { empty_url_request_jobs_callback->Run(); } } void StartAsync() { NotifyStartError(URLRequestStatus(URLRequestStatus::FAILED, error_)); } int error_; base::WeakPtrFactory weak_factory_; DISALLOW_COPY_AND_ASSIGN(URLRequestErrorCountingJob); }; static int error_jobs_created = 0; class MockURLRequestJobFactory : public URLRequestJobFactory { public: MockURLRequestJobFactory() {} ~MockURLRequestJobFactory() override {} URLRequestJob* MaybeCreateJobWithProtocolHandler( const std::string& scheme, URLRequest* request, NetworkDelegate* network_delegate) const override { ++error_jobs_created; return new URLRequestErrorCountingJob(request, network_delegate, ERR_INTERNET_DISCONNECTED); } URLRequestJob* MaybeInterceptRedirect(URLRequest* request, NetworkDelegate* network_delegate, const GURL& location) const override { return nullptr; } URLRequestJob* MaybeInterceptResponse( URLRequest* request, NetworkDelegate* network_delegate) const override { return nullptr; } bool IsHandledProtocol(const std::string& scheme) const override { return scheme == "http"; }; bool IsHandledURL(const GURL& url) const override { return url.SchemeIs("http"); } bool IsSafeRedirectTarget(const GURL& location) const override { return false; } }; class MockSdchDictionaryFetcher : public SdchDictionaryFetcher { public: MockSdchDictionaryFetcher() : SdchDictionaryFetcher(&test_context_) {} ~MockSdchDictionaryFetcher() {} struct PendingRequest { PendingRequest(const GURL& url, const OnDictionaryFetchedCallback& callback) : url_(url), callback_(callback) {} GURL url_; OnDictionaryFetchedCallback callback_; }; virtual bool Schedule(const GURL& dictionary_url, const OnDictionaryFetchedCallback& callback) { if (!HasPendingRequest(dictionary_url)) { requests_.push_back(PendingRequest(dictionary_url, callback)); return true; } return false; } virtual bool ScheduleReload(const GURL& dictionary_url, const OnDictionaryFetchedCallback& callback) { if (!HasPendingRequest(dictionary_url)) { requests_.push_back(PendingRequest(dictionary_url, callback)); return true; } return false; } virtual void Cancel() { requests_.clear(); } bool HasPendingRequest(const GURL& dictionary_url) { for (std::vector::iterator it = requests_.begin(); it != requests_.end(); ++it) { if (it->url_ == dictionary_url) return true; } return false; } bool CompletePendingRequest(const GURL& dictionary_url, const std::string& dictionary_text, const BoundNetLog& net_log, bool was_from_cache) { for (std::vector::iterator it = requests_.begin(); it != requests_.end(); ++it) { if (it->url_ == dictionary_url) { it->callback_.Run(dictionary_text, dictionary_url, net_log, was_from_cache); requests_.erase(it); return true; } } return false; } private: TestURLRequestContext test_context_; std::vector requests_; DISALLOW_COPY_AND_ASSIGN(MockSdchDictionaryFetcher); }; // File testing infrastructure summary: // * NewSdchDictionary(): Creates a dictionary of a specific size. // * URLRequestErrorCountingJob: A URLRequestJob that returns an error // and counts the number of outstanding (started but not finished) // jobs, and calls a global callback when that number transitions to zero. // * MockURLRequestJobFactory: Factory to create the above jobs. Tracks // the number of jobs created. // * SdchOwnerTest: Interfaces // * Access manager, owner, and net log // * Return the number of jobs created in a time interval // * Return dictionary present in the manager // * Notify SdchOwner of an incoming dictionary (& wait until jobs clear) // * Attempt to add a dictionary and test for success. // Test patterns: // * Let the owner know about a Get-Dictionary header and test for // appropriate jobs being created. // * Let the owner know that a dictionary was successfully fetched // and test for appropriate outcome. // * Either of the above, having previously added dictionaries to create // a particular initial state. class SdchOwnerTest : public testing::Test { public: static const size_t kMaxSizeForTesting = 1000 * 50; static const size_t kMinFetchSpaceForTesting = 500; SdchOwnerTest() : last_jobs_created_(error_jobs_created), dictionary_creation_index_(0), pref_store_(new TestingPrefStore), sdch_owner_(new SdchOwner(&sdch_manager_, &url_request_context_)) { // Any jobs created on this context will immediately error, // which leaves the test in control of signals to SdchOwner. url_request_context_.set_job_factory(&job_factory_); // Reduce sizes to reduce time for string operations. sdch_owner_->SetMaxTotalDictionarySize(kMaxSizeForTesting); sdch_owner_->SetMinSpaceForDictionaryFetch(kMinFetchSpaceForTesting); } SdchManager& sdch_manager() { return sdch_manager_; } SdchOwner& sdch_owner() { return *(sdch_owner_.get()); } BoundNetLog& bound_net_log() { return net_log_; } TestingPrefStore& pref_store() { return *(pref_store_.get()); } int JobsRecentlyCreated() { int result = error_jobs_created - last_jobs_created_; last_jobs_created_ = error_jobs_created; return result; } bool DictionaryPresentInManager(const std::string& server_hash) { // Presumes all tests use generic url. SdchProblemCode tmp; scoped_ptr set( sdch_manager_.GetDictionarySetByHash(GURL(generic_url), server_hash, &tmp)); return !!set.get(); } void WaitForNoJobs() { if (outstanding_url_request_error_counting_jobs == 0) return; base::RunLoop run_loop; base::Closure quit_closure(run_loop.QuitClosure()); empty_url_request_jobs_callback = &quit_closure; run_loop.Run(); empty_url_request_jobs_callback = NULL; } void SignalGetDictionaryAndClearJobs(GURL request_url, GURL dictionary_url) { sdch_owner().OnGetDictionary(request_url, dictionary_url); WaitForNoJobs(); } // Create a unique (by hash) dictionary of the given size, // associate it with a unique URL, add it to the manager through // SdchOwner::OnDictionaryFetched(), and return whether that // addition was successful or not. bool CreateAndAddDictionary(size_t size, std::string* server_hash_p, base::Time last_used_time) { GURL dictionary_url( base::StringPrintf("%s/d%d", generic_url, dictionary_creation_index_)); std::string dictionary_text(NewSdchDictionary(size - 4)); dictionary_text += base::StringPrintf("%04d", dictionary_creation_index_); ++dictionary_creation_index_; std::string client_hash; std::string server_hash; SdchManager::GenerateHash(dictionary_text, &client_hash, &server_hash); if (DictionaryPresentInManager(server_hash)) return false; sdch_owner().OnDictionaryFetched(last_used_time, 0, dictionary_text, dictionary_url, net_log_, false); if (server_hash_p) *server_hash_p = server_hash; return DictionaryPresentInManager(server_hash); } void ResetOwner() { sdch_owner_.reset(new SdchOwner(&sdch_manager_, &url_request_context_)); } private: int last_jobs_created_; BoundNetLog net_log_; int dictionary_creation_index_; // The dependencies of these objects (sdch_owner_ -> {sdch_manager_, // url_request_context_}, url_request_context_->job_factory_) require // this order for correct destruction semantics. MockURLRequestJobFactory job_factory_; URLRequestContext url_request_context_; SdchManager sdch_manager_; scoped_refptr pref_store_; scoped_ptr sdch_owner_; DISALLOW_COPY_AND_ASSIGN(SdchOwnerTest); }; // Does OnGetDictionary result in a fetch when there's enough space, and not // when there's not? TEST_F(SdchOwnerTest, OnGetDictionary_Fetching) { GURL request_url(std::string(generic_url) + "/r1"); // Fetch generated when empty. GURL dict_url1(std::string(generic_url) + "/d1"); EXPECT_EQ(0, JobsRecentlyCreated()); SignalGetDictionaryAndClearJobs(request_url, dict_url1); EXPECT_EQ(1, JobsRecentlyCreated()); // Fetch generated when half full. GURL dict_url2(std::string(generic_url) + "/d2"); std::string dictionary1(NewSdchDictionary(kMaxSizeForTesting / 2)); sdch_owner().OnDictionaryFetched(base::Time::Now(), 1, dictionary1, dict_url1, bound_net_log(), false); EXPECT_EQ(0, JobsRecentlyCreated()); SignalGetDictionaryAndClearJobs(request_url, dict_url2); EXPECT_EQ(1, JobsRecentlyCreated()); // Fetch not generated when close to completely full. GURL dict_url3(std::string(generic_url) + "/d3"); std::string dictionary2(NewSdchDictionary( (kMaxSizeForTesting / 2 - kMinFetchSpaceForTesting / 2))); sdch_owner().OnDictionaryFetched(base::Time::Now(), 1, dictionary2, dict_url2, bound_net_log(), false); EXPECT_EQ(0, JobsRecentlyCreated()); SignalGetDictionaryAndClearJobs(request_url, dict_url3); EXPECT_EQ(0, JobsRecentlyCreated()); } // Make sure attempts to add dictionaries do what they should. TEST_F(SdchOwnerTest, OnDictionaryFetched_Fetching) { GURL request_url(std::string(generic_url) + "/r1"); std::string client_hash; std::string server_hash; // In the past, but still fresh for an unused dictionary. base::Time dictionary_last_used_time(base::Time::Now() - base::TimeDelta::FromMinutes(30)); // Add successful when empty. EXPECT_TRUE(CreateAndAddDictionary(kMaxSizeForTesting / 2, nullptr, dictionary_last_used_time)); EXPECT_EQ(0, JobsRecentlyCreated()); // Add successful when half full. EXPECT_TRUE(CreateAndAddDictionary(kMaxSizeForTesting / 2, nullptr, dictionary_last_used_time)); EXPECT_EQ(0, JobsRecentlyCreated()); // Add unsuccessful when full. EXPECT_FALSE(CreateAndAddDictionary(kMaxSizeForTesting / 2, nullptr, dictionary_last_used_time)); EXPECT_EQ(0, JobsRecentlyCreated()); } // Confirm auto-eviction happens if space is needed. TEST_F(SdchOwnerTest, ConfirmAutoEviction) { base::Time start_time = base::Time::Now(); std::string server_hash_d1; std::string server_hash_d2; std::string server_hash_d3; base::SimpleTestClock* test_clock = new base::SimpleTestClock(); sdch_owner().SetClockForTesting(make_scoped_ptr(test_clock)); test_clock->SetNow(base::Time::Now()); // Add two dictionaries, one recent, one more than a day in the past. base::Time fresh(base::Time::Now() - base::TimeDelta::FromHours(23)); base::Time stale(base::Time::Now() - base::TimeDelta::FromHours(25)); EXPECT_TRUE( CreateAndAddDictionary(kMaxSizeForTesting / 2, &server_hash_d1, fresh)); EXPECT_TRUE( CreateAndAddDictionary(kMaxSizeForTesting / 2, &server_hash_d2, stale)); EXPECT_TRUE(DictionaryPresentInManager(server_hash_d1)); EXPECT_TRUE(DictionaryPresentInManager(server_hash_d2)); base::HistogramTester tester; const base::TimeDelta synthetic_delta = base::TimeDelta::FromSeconds(5); test_clock->Advance(synthetic_delta); EXPECT_TRUE( CreateAndAddDictionary(kMaxSizeForTesting / 2, &server_hash_d3, fresh)); EXPECT_TRUE(DictionaryPresentInManager(server_hash_d1)); EXPECT_FALSE(DictionaryPresentInManager(server_hash_d2)); EXPECT_TRUE(DictionaryPresentInManager(server_hash_d3)); base::TimeDelta expected_proc_lifetime = synthetic_delta * 3 + base::Time::Now() - start_time; size_t expected_value_base = ((kMaxSizeForTesting / 2) * synthetic_delta.InMilliseconds()) / expected_proc_lifetime.InMilliseconds(); const char *kHistogram = "Sdch3.TimeWeightedMemoryUse"; tester.ExpectTotalCount(kHistogram, 0); // Dictionary insertions and deletions: // T = 0: insert d1 and d2 // T = 5: insert d3, which evicts d2 // T = 15: destroy SdchOwner, which evicts d1 and d3 // Therefore, d2's lifetime is synthetic_delta, d1's is synthetic_delta * 3, // and d3's is synthetic_delta * 2. The expected_value_base variable is the // base factor for d2's memory consumption, of which d1's and d3's are // multiples. test_clock->Advance(synthetic_delta * 2); ResetOwner(); tester.ExpectTotalCount(kHistogram, 3); tester.ExpectBucketCount(kHistogram, expected_value_base, 1); tester.ExpectBucketCount(kHistogram, expected_value_base * 2, 1); tester.ExpectBucketCount(kHistogram, expected_value_base * 3, 1); } // Confirm auto-eviction happens if space is needed, with a more complicated // situation TEST_F(SdchOwnerTest, ConfirmAutoEviction_2) { std::string server_hash_d1; std::string server_hash_d2; std::string server_hash_d3; // Add dictionaries, one recent, two more than a day in the past that // between them add up to the space needed. base::Time fresh(base::Time::Now() - base::TimeDelta::FromHours(23)); base::Time stale(base::Time::Now() - base::TimeDelta::FromHours(25)); EXPECT_TRUE( CreateAndAddDictionary(kMaxSizeForTesting / 2, &server_hash_d1, fresh)); EXPECT_TRUE( CreateAndAddDictionary(kMaxSizeForTesting / 4, &server_hash_d2, stale)); EXPECT_TRUE( CreateAndAddDictionary(kMaxSizeForTesting / 4, &server_hash_d3, stale)); EXPECT_TRUE(DictionaryPresentInManager(server_hash_d1)); EXPECT_TRUE(DictionaryPresentInManager(server_hash_d2)); EXPECT_TRUE(DictionaryPresentInManager(server_hash_d3)); std::string server_hash_d4; EXPECT_TRUE( CreateAndAddDictionary(kMaxSizeForTesting / 2, &server_hash_d4, fresh)); EXPECT_TRUE(DictionaryPresentInManager(server_hash_d1)); EXPECT_FALSE(DictionaryPresentInManager(server_hash_d2)); EXPECT_FALSE(DictionaryPresentInManager(server_hash_d3)); EXPECT_TRUE(DictionaryPresentInManager(server_hash_d4)); } // Confirm if only one dictionary needs to be evicted it's the oldest. TEST_F(SdchOwnerTest, ConfirmAutoEviction_Oldest) { std::string server_hash_d1; std::string server_hash_d2; std::string server_hash_d3; // Add dictionaries, one recent, one two days in the past, and one // four days in the past. base::Time fresh(base::Time::Now() - base::TimeDelta::FromHours(23)); base::Time stale_newer(base::Time::Now() - base::TimeDelta::FromHours(47)); base::Time stale_older(base::Time::Now() - base::TimeDelta::FromHours(71)); EXPECT_TRUE( CreateAndAddDictionary(kMaxSizeForTesting / 4, &server_hash_d1, fresh)); EXPECT_TRUE(CreateAndAddDictionary(kMaxSizeForTesting / 4, &server_hash_d2, stale_newer)); EXPECT_TRUE(CreateAndAddDictionary(kMaxSizeForTesting / 4, &server_hash_d3, stale_older)); EXPECT_TRUE(DictionaryPresentInManager(server_hash_d1)); EXPECT_TRUE(DictionaryPresentInManager(server_hash_d2)); EXPECT_TRUE(DictionaryPresentInManager(server_hash_d3)); // The addition of a new dictionary should succeed, evicting only the // oldest one. std::string server_hash_d4; EXPECT_TRUE( CreateAndAddDictionary(kMaxSizeForTesting / 2, &server_hash_d4, fresh)); EXPECT_TRUE(DictionaryPresentInManager(server_hash_d1)); EXPECT_TRUE(DictionaryPresentInManager(server_hash_d2)); EXPECT_FALSE(DictionaryPresentInManager(server_hash_d3)); EXPECT_TRUE(DictionaryPresentInManager(server_hash_d4)); } // Confirm using a dictionary changes eviction behavior properly. TEST_F(SdchOwnerTest, UseChangesEviction) { std::string server_hash_d1; std::string server_hash_d2; std::string server_hash_d3; // Add dictionaries, one recent, one two days in the past, and one // four days in the past. base::Time fresh(base::Time::Now() - base::TimeDelta::FromHours(23)); base::Time stale_newer(base::Time::Now() - base::TimeDelta::FromHours(47)); base::Time stale_older(base::Time::Now() - base::TimeDelta::FromHours(71)); EXPECT_TRUE( CreateAndAddDictionary(kMaxSizeForTesting / 4, &server_hash_d1, fresh)); EXPECT_TRUE(CreateAndAddDictionary(kMaxSizeForTesting / 4, &server_hash_d2, stale_newer)); EXPECT_TRUE(CreateAndAddDictionary(kMaxSizeForTesting / 4, &server_hash_d3, stale_older)); EXPECT_TRUE(DictionaryPresentInManager(server_hash_d1)); EXPECT_TRUE(DictionaryPresentInManager(server_hash_d2)); EXPECT_TRUE(DictionaryPresentInManager(server_hash_d3)); // Use the oldest dictionary. sdch_owner().OnDictionaryUsed(server_hash_d3); // The addition of a new dictionary should succeed, evicting only the // newer stale one. std::string server_hash_d4; EXPECT_TRUE( CreateAndAddDictionary(kMaxSizeForTesting / 2, &server_hash_d4, fresh)); EXPECT_TRUE(DictionaryPresentInManager(server_hash_d1)); EXPECT_FALSE(DictionaryPresentInManager(server_hash_d2)); EXPECT_TRUE(DictionaryPresentInManager(server_hash_d3)); EXPECT_TRUE(DictionaryPresentInManager(server_hash_d4)); } // Confirm using a dictionary can prevent the addition of a new dictionary. TEST_F(SdchOwnerTest, UsePreventsAddition) { std::string server_hash_d1; std::string server_hash_d2; std::string server_hash_d3; // Add dictionaries, one recent, one two days in the past, and one // four days in the past. base::Time fresh(base::Time::Now() - base::TimeDelta::FromMinutes(30)); base::Time stale_newer(base::Time::Now() - base::TimeDelta::FromHours(47)); base::Time stale_older(base::Time::Now() - base::TimeDelta::FromHours(71)); EXPECT_TRUE( CreateAndAddDictionary(kMaxSizeForTesting / 4, &server_hash_d1, fresh)); EXPECT_TRUE(CreateAndAddDictionary(kMaxSizeForTesting / 4, &server_hash_d2, stale_newer)); EXPECT_TRUE(CreateAndAddDictionary(kMaxSizeForTesting / 4, &server_hash_d3, stale_older)); EXPECT_TRUE(DictionaryPresentInManager(server_hash_d1)); EXPECT_TRUE(DictionaryPresentInManager(server_hash_d2)); EXPECT_TRUE(DictionaryPresentInManager(server_hash_d3)); // Use the older dictionaries. sdch_owner().OnDictionaryUsed(server_hash_d2); sdch_owner().OnDictionaryUsed(server_hash_d3); // The addition of a new dictionary should fail, not evicting anything. std::string server_hash_d4; EXPECT_FALSE( CreateAndAddDictionary(kMaxSizeForTesting / 2, &server_hash_d4, fresh)); EXPECT_TRUE(DictionaryPresentInManager(server_hash_d1)); EXPECT_TRUE(DictionaryPresentInManager(server_hash_d2)); EXPECT_TRUE(DictionaryPresentInManager(server_hash_d3)); EXPECT_FALSE(DictionaryPresentInManager(server_hash_d4)); } // Confirm clear gets all the space back. TEST_F(SdchOwnerTest, ClearReturnsSpace) { std::string server_hash_d1; std::string server_hash_d2; // Take up all the space. EXPECT_TRUE(CreateAndAddDictionary(kMaxSizeForTesting, &server_hash_d1, base::Time::Now())); // Addition should fail. EXPECT_FALSE(CreateAndAddDictionary(kMaxSizeForTesting, &server_hash_d2, base::Time::Now())); EXPECT_TRUE(DictionaryPresentInManager(server_hash_d1)); EXPECT_FALSE(DictionaryPresentInManager(server_hash_d2)); sdch_manager().ClearData(); EXPECT_FALSE(DictionaryPresentInManager(server_hash_d1)); EXPECT_FALSE(DictionaryPresentInManager(server_hash_d2)); // Addition should now succeed. EXPECT_TRUE( CreateAndAddDictionary(kMaxSizeForTesting, nullptr, base::Time::Now())); } // Confirm memory pressure gets all the space back. TEST_F(SdchOwnerTest, MemoryPressureReturnsSpace) { std::string server_hash_d1; std::string server_hash_d2; // Take up all the space. EXPECT_TRUE(CreateAndAddDictionary(kMaxSizeForTesting, &server_hash_d1, base::Time::Now())); // Addition should fail. EXPECT_FALSE(CreateAndAddDictionary(kMaxSizeForTesting, &server_hash_d2, base::Time::Now())); EXPECT_TRUE(DictionaryPresentInManager(server_hash_d1)); EXPECT_FALSE(DictionaryPresentInManager(server_hash_d2)); base::MemoryPressureListener::NotifyMemoryPressure( base::MemoryPressureListener::MEMORY_PRESSURE_LEVEL_MODERATE); // The notification may have (implementation note: does :-}) use a PostTask, // so we drain the local message queue. This should be safe (i.e. not have // an inifinite number of messages) in a unit test. base::RunLoop().RunUntilIdle(); EXPECT_FALSE(DictionaryPresentInManager(server_hash_d1)); EXPECT_FALSE(DictionaryPresentInManager(server_hash_d2)); // Addition should now succeed. EXPECT_TRUE( CreateAndAddDictionary(kMaxSizeForTesting, nullptr, base::Time::Now())); } // Confirm that use of a pinned dictionary after its removal works properly. TEST_F(SdchOwnerTest, PinRemoveUse) { pref_store().SetInitializationCompleted(); sdch_owner().EnablePersistentStorage(&pref_store()); std::string server_hash_d1; EXPECT_TRUE(CreateAndAddDictionary(kMaxSizeForTesting / 2, &server_hash_d1, base::Time::Now())); scoped_ptr return_set( sdch_manager().GetDictionarySet( GURL(std::string(generic_url) + "/x.html"))); ASSERT_TRUE(return_set.get()); EXPECT_TRUE(return_set->GetDictionaryText(server_hash_d1)); const base::Value* result = nullptr; const base::DictionaryValue* dict_result = nullptr; ASSERT_TRUE(pref_store().GetValue("SDCH", &result)); ASSERT_TRUE(result->GetAsDictionary(&dict_result)); EXPECT_TRUE(dict_result->Get("dictionaries", &result)); EXPECT_TRUE(dict_result->Get("dictionaries." + server_hash_d1, &result)); sdch_manager().ClearData(); ASSERT_TRUE(pref_store().GetValue("SDCH", &result)); ASSERT_TRUE(result->GetAsDictionary(&dict_result)); EXPECT_TRUE(dict_result->Get("dictionaries", &result)); EXPECT_FALSE(dict_result->Get("dictionaries." + server_hash_d1, &result)); scoped_ptr return_set2( sdch_manager().GetDictionarySet( GURL(std::string(generic_url) + "/x.html"))); EXPECT_FALSE(return_set2.get()); sdch_manager().OnDictionaryUsed(server_hash_d1); ASSERT_TRUE(pref_store().GetValue("SDCH", &result)); ASSERT_TRUE(result->GetAsDictionary(&dict_result)); EXPECT_TRUE(dict_result->Get("dictionaries", &result)); EXPECT_FALSE(dict_result->Get("dictionaries." + server_hash_d1, &result)); } class SdchOwnerPersistenceTest : public ::testing::Test { public: SdchOwnerPersistenceTest() : pref_store_(new TestingPrefStore()) { pref_store_->SetInitializationCompleted(); } virtual ~SdchOwnerPersistenceTest() {} void ClearOwner() { owner_.reset(NULL); } void ResetOwner(bool delay) { // This has to be done first, since SdchOwner may be observing SdchManager, // and SdchManager can't be destroyed with a live observer. owner_.reset(NULL); manager_.reset(new SdchManager()); fetcher_ = new MockSdchDictionaryFetcher(); owner_.reset(new SdchOwner(manager_.get(), &url_request_context_)); owner_->SetMaxTotalDictionarySize(SdchOwnerTest::kMaxSizeForTesting); owner_->SetMinSpaceForDictionaryFetch( SdchOwnerTest::kMinFetchSpaceForTesting); owner_->SetFetcherForTesting(make_scoped_ptr(fetcher_)); if (!delay) owner_->EnablePersistentStorage(pref_store_.get()); } void InsertDictionaryForURL(const GURL& url, const std::string& nonce) { owner_->OnDictionaryFetched(base::Time::Now(), 1, CreateDictionary(url, nonce), url, net_log_, false); } bool CompleteLoadFromURL(const GURL& url, const std::string& nonce, bool was_from_cache) { return fetcher_->CompletePendingRequest(url, CreateDictionary(url, nonce), net_log_, was_from_cache); } std::string CreateDictionary(const GURL& url, const std::string& nonce) { std::string dict; dict.append("Domain: "); dict.append(url.host()); dict.append("\n\n"); dict.append(url.spec()); dict.append(nonce); return dict; } protected: BoundNetLog net_log_; scoped_refptr pref_store_; scoped_ptr manager_; MockSdchDictionaryFetcher* fetcher_; scoped_ptr owner_; TestURLRequestContext url_request_context_; }; // Test an empty persistence store. TEST_F(SdchOwnerPersistenceTest, Empty) { ResetOwner(false); EXPECT_EQ(0, owner_->GetDictionaryCountForTesting()); } // Test a persistence store with an empty dictionary. TEST_F(SdchOwnerPersistenceTest, Persistent_EmptyDict) { pref_store_->SetValue("SDCH", make_scoped_ptr(new base::DictionaryValue()), WriteablePrefStore::DEFAULT_PREF_WRITE_FLAGS); ResetOwner(false); EXPECT_EQ(0, owner_->GetDictionaryCountForTesting()); } // Test a persistence store with a bad version number. TEST_F(SdchOwnerPersistenceTest, Persistent_BadVersion) { scoped_ptr sdch_dict(new base::DictionaryValue()); sdch_dict->SetInteger("version", 2); pref_store_->SetValue("SDCH", std::move(sdch_dict), WriteablePrefStore::DEFAULT_PREF_WRITE_FLAGS); ResetOwner(false); EXPECT_EQ(0, owner_->GetDictionaryCountForTesting()); } // Test a persistence store with an empty dictionaries map. TEST_F(SdchOwnerPersistenceTest, Persistent_EmptyDictList) { scoped_ptr sdch_dict(new base::DictionaryValue()); scoped_ptr dicts(new base::DictionaryValue()); sdch_dict->SetInteger("version", 1); sdch_dict->Set("dictionaries", std::move(dicts)); pref_store_->SetValue("SDCH", std::move(sdch_dict), WriteablePrefStore::DEFAULT_PREF_WRITE_FLAGS); ResetOwner(false); EXPECT_EQ(0, owner_->GetDictionaryCountForTesting()); } TEST_F(SdchOwnerPersistenceTest, OneDict) { const GURL url("http://www.example.com/dict"); ResetOwner(false); EXPECT_EQ(0, owner_->GetDictionaryCountForTesting()); InsertDictionaryForURL(url, "0"); EXPECT_EQ(1, owner_->GetDictionaryCountForTesting()); ResetOwner(false); EXPECT_EQ(0, owner_->GetDictionaryCountForTesting()); EXPECT_TRUE(CompleteLoadFromURL(url, "0", true)); EXPECT_EQ(1, owner_->GetDictionaryCountForTesting()); } TEST_F(SdchOwnerPersistenceTest, TwoDicts) { const GURL url0("http://www.example.com/dict0"); const GURL url1("http://www.example.com/dict1"); ResetOwner(false); InsertDictionaryForURL(url0, "0"); InsertDictionaryForURL(url1, "1"); ResetOwner(false); EXPECT_TRUE(CompleteLoadFromURL(url0, "0", true)); EXPECT_TRUE(CompleteLoadFromURL(url1, "1", true)); EXPECT_EQ(2, owner_->GetDictionaryCountForTesting()); EXPECT_TRUE(owner_->HasDictionaryFromURLForTesting(url0)); EXPECT_TRUE(owner_->HasDictionaryFromURLForTesting(url1)); } TEST_F(SdchOwnerPersistenceTest, OneGoodDictOneBadDict) { const GURL url0("http://www.example.com/dict0"); const GURL url1("http://www.example.com/dict1"); ResetOwner(false); InsertDictionaryForURL(url0, "0"); InsertDictionaryForURL(url1, "1"); // Mutate the pref store a bit now. Clear the owner first, to ensure that the // SdchOwner doesn't observe these changes and object. The manual dictionary // manipulation is a bit icky. ClearOwner(); base::DictionaryValue* dict = nullptr; ASSERT_TRUE(GetDictionaryForURL(pref_store_.get(), url1, nullptr, &dict)); dict->Remove("use_count", nullptr); ResetOwner(false); EXPECT_TRUE(CompleteLoadFromURL(url0, "0", true)); EXPECT_FALSE(CompleteLoadFromURL(url1, "1", true)); EXPECT_EQ(1, owner_->GetDictionaryCountForTesting()); EXPECT_TRUE(owner_->HasDictionaryFromURLForTesting(url0)); EXPECT_FALSE(owner_->HasDictionaryFromURLForTesting(url1)); } TEST_F(SdchOwnerPersistenceTest, UsingDictionaryUpdatesUseCount) { const GURL url("http://www.example.com/dict"); ResetOwner(false); InsertDictionaryForURL(url, "0"); std::string hash; int old_count; { ClearOwner(); base::DictionaryValue* dict = nullptr; ASSERT_TRUE(GetDictionaryForURL(pref_store_.get(), url, &hash, &dict)); ASSERT_TRUE(dict->GetInteger("use_count", &old_count)); } ResetOwner(false); ASSERT_TRUE(CompleteLoadFromURL(url, "0", true)); owner_->OnDictionaryUsed(hash); int new_count; { ClearOwner(); base::DictionaryValue* dict = nullptr; ASSERT_TRUE(GetDictionaryForURL(pref_store_.get(), url, nullptr, &dict)); ASSERT_TRUE(dict->GetInteger("use_count", &new_count)); } EXPECT_EQ(old_count + 1, new_count); } TEST_F(SdchOwnerPersistenceTest, LoadingDictionaryMerges) { const GURL url0("http://www.example.com/dict0"); const GURL url1("http://www.example.com/dict1"); ResetOwner(false); InsertDictionaryForURL(url1, "1"); ResetOwner(true); InsertDictionaryForURL(url0, "0"); EXPECT_EQ(1, owner_->GetDictionaryCountForTesting()); owner_->EnablePersistentStorage(pref_store_.get()); ASSERT_TRUE(CompleteLoadFromURL(url1, "1", true)); EXPECT_EQ(2, owner_->GetDictionaryCountForTesting()); } TEST_F(SdchOwnerPersistenceTest, PersistenceMetrics) { const GURL url0("http://www.example.com/dict0"); const GURL url1("http://www.example.com/dict1"); ResetOwner(false); InsertDictionaryForURL(url0, "0"); InsertDictionaryForURL(url1, "1"); ResetOwner(false); base::HistogramTester tester; EXPECT_TRUE(CompleteLoadFromURL(url0, "0", true)); EXPECT_TRUE(CompleteLoadFromURL(url1, "1", false)); tester.ExpectTotalCount("Sdch3.NetworkBytesSpent", 1); tester.ExpectUniqueSample("Sdch3.NetworkBytesSpent", CreateDictionary(url1, "1").size(), 1); } } // namespace net