// 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/url_request/sdch_dictionary_fetcher.h" #include #include "base/bind.h" #include "base/callback.h" #include "base/run_loop.h" #include "base/strings/stringprintf.h" #include "base/thread_task_runner_handle.h" #include "net/base/sdch_manager.h" #include "net/url_request/url_request_data_job.h" #include "net/url_request/url_request_filter.h" #include "net/url_request/url_request_interceptor.h" #include "net/url_request/url_request_test_util.h" #include "testing/gtest/include/gtest/gtest.h" namespace net { namespace { const char kSampleBufferContext[] = "This is a sample buffer."; const char kTestDomain[] = "top.domain.test"; class URLRequestSpecifiedResponseJob : public URLRequestSimpleJob { public: URLRequestSpecifiedResponseJob(URLRequest* request, NetworkDelegate* network_delegate, const base::Closure& destruction_callback) : URLRequestSimpleJob(request, network_delegate), destruction_callback_(destruction_callback) { DCHECK(!destruction_callback.is_null()); } static std::string ExpectedResponseForURL(const GURL& url) { return base::StringPrintf("Response for %s\n%s\nEnd Response for %s\n", url.spec().c_str(), kSampleBufferContext, url.spec().c_str()); } private: ~URLRequestSpecifiedResponseJob() override { destruction_callback_.Run(); } // URLRequestSimpleJob implementation: int GetData(std::string* mime_type, std::string* charset, std::string* data, const CompletionCallback& callback) const override { GURL url(request_->url()); *data = ExpectedResponseForURL(url); return OK; } const base::Closure destruction_callback_; }; class SpecifiedResponseJobInterceptor : public URLRequestInterceptor { public: // A callback to be called whenever a URLRequestSpecifiedResponseJob is // created or destroyed. The argument will be the change in number of // jobs (i.e. +1 for created, -1 for destroyed). typedef base::Callback LifecycleCallback; explicit SpecifiedResponseJobInterceptor( const LifecycleCallback& lifecycle_callback) : lifecycle_callback_(lifecycle_callback), factory_(this) { DCHECK(!lifecycle_callback_.is_null()); } ~SpecifiedResponseJobInterceptor() override {} URLRequestJob* MaybeInterceptRequest( URLRequest* request, NetworkDelegate* network_delegate) const override { if (!lifecycle_callback_.is_null()) lifecycle_callback_.Run(1); return new URLRequestSpecifiedResponseJob( request, network_delegate, base::Bind(lifecycle_callback_, -1)); } // The caller must ensure that the callback is valid to call for the // lifetime of the SpecifiedResponseJobInterceptor (i.e. until // Unregister() is called). static void RegisterWithFilter(const LifecycleCallback& lifecycle_callback) { scoped_ptr interceptor( new SpecifiedResponseJobInterceptor(lifecycle_callback)); net::URLRequestFilter::GetInstance()->AddHostnameInterceptor( "http", kTestDomain, interceptor.Pass()); } static void Unregister() { net::URLRequestFilter::GetInstance()->RemoveHostnameHandler("http", kTestDomain); } private: void OnSpecfiedResponseJobDestruction() const { if (!lifecycle_callback_.is_null()) lifecycle_callback_.Run(-1); } LifecycleCallback lifecycle_callback_; mutable base::WeakPtrFactory factory_; }; // Local test infrastructure // * URLRequestSpecifiedResponseJob: A URLRequestJob that returns // a different but derivable response for each URL (used for all // url requests in this file). The class provides interfaces to // signal whenever the total number of jobs transitions to zero. // * SdchDictionaryFetcherTest: Registers a callback with the above // class, and provides blocking interfaces for a transition to zero jobs. // Contains an SdchDictionaryFetcher, and tracks fetcher dictionary // addition callbacks. // Most tests schedule a dictionary fetch, wait for no jobs outstanding, // then test that the fetch results are as expected. class SdchDictionaryFetcherTest : public ::testing::Test { public: struct DictionaryAdditions { DictionaryAdditions(const std::string& dictionary_text, const GURL& dictionary_url) : dictionary_text(dictionary_text), dictionary_url(dictionary_url) {} std::string dictionary_text; GURL dictionary_url; }; SdchDictionaryFetcherTest() : jobs_requested_(0), jobs_outstanding_(0), context_(new TestURLRequestContext), fetcher_(new SdchDictionaryFetcher( context_.get(), base::Bind(&SdchDictionaryFetcherTest::OnDictionaryFetched, base::Unretained(this)))), factory_(this) { SpecifiedResponseJobInterceptor::RegisterWithFilter( base::Bind(&SdchDictionaryFetcherTest::OnNumberJobsChanged, factory_.GetWeakPtr())); } ~SdchDictionaryFetcherTest() override { SpecifiedResponseJobInterceptor::Unregister(); } void OnDictionaryFetched(const std::string& dictionary_text, const GURL& dictionary_url, const BoundNetLog& net_log) { dictionary_additions.push_back( DictionaryAdditions(dictionary_text, dictionary_url)); } // Return (in |*out|) all dictionary additions since the last time // this function was called. void GetDictionaryAdditions(std::vector* out) { out->swap(dictionary_additions); dictionary_additions.clear(); } SdchDictionaryFetcher* fetcher() { return fetcher_.get(); } // May not be called outside the SetUp()/TearDown() interval. int jobs_requested() const { return jobs_requested_; } GURL PathToGurl(const char* path) { std::string gurl_string("http://"); gurl_string += kTestDomain; gurl_string += "/"; gurl_string += path; return GURL(gurl_string); } // Block until there are no outstanding URLRequestSpecifiedResponseJobs. void WaitForNoJobs() { if (jobs_outstanding_ == 0) return; run_loop_.reset(new base::RunLoop); run_loop_->Run(); run_loop_.reset(); } private: void OnNumberJobsChanged(int outstanding_jobs_delta) { if (outstanding_jobs_delta > 0) jobs_requested_ += outstanding_jobs_delta; jobs_outstanding_ += outstanding_jobs_delta; if (jobs_outstanding_ == 0 && run_loop_) run_loop_->Quit(); } int jobs_requested_; int jobs_outstanding_; scoped_ptr run_loop_; scoped_ptr context_; scoped_ptr fetcher_; std::vector dictionary_additions; base::WeakPtrFactory factory_; }; // Schedule a fetch and make sure it happens. TEST_F(SdchDictionaryFetcherTest, Basic) { GURL dictionary_url(PathToGurl("dictionary")); fetcher()->Schedule(dictionary_url); WaitForNoJobs(); EXPECT_EQ(1, jobs_requested()); std::vector additions; GetDictionaryAdditions(&additions); ASSERT_EQ(1u, additions.size()); EXPECT_EQ( URLRequestSpecifiedResponseJob::ExpectedResponseForURL(dictionary_url), additions[0].dictionary_text); } // Multiple fetches of the same URL should result in only one request. TEST_F(SdchDictionaryFetcherTest, Multiple) { GURL dictionary_url(PathToGurl("dictionary")); fetcher()->Schedule(dictionary_url); fetcher()->Schedule(dictionary_url); fetcher()->Schedule(dictionary_url); WaitForNoJobs(); EXPECT_EQ(1, jobs_requested()); std::vector additions; GetDictionaryAdditions(&additions); ASSERT_EQ(1u, additions.size()); EXPECT_EQ( URLRequestSpecifiedResponseJob::ExpectedResponseForURL(dictionary_url), additions[0].dictionary_text); } // A cancel should result in no actual requests being generated. TEST_F(SdchDictionaryFetcherTest, Cancel) { GURL dictionary_url_1(PathToGurl("dictionary_1")); GURL dictionary_url_2(PathToGurl("dictionary_2")); GURL dictionary_url_3(PathToGurl("dictionary_3")); fetcher()->Schedule(dictionary_url_1); fetcher()->Schedule(dictionary_url_2); fetcher()->Schedule(dictionary_url_3); fetcher()->Cancel(); WaitForNoJobs(); // Synchronous execution may have resulted in a single job being scheduled. EXPECT_GE(1, jobs_requested()); } // Attempt to confuse the fetcher loop processing by scheduling a // dictionary addition while another fetch is in process. TEST_F(SdchDictionaryFetcherTest, LoopRace) { GURL dictionary0_url(PathToGurl("dictionary0")); GURL dictionary1_url(PathToGurl("dictionary1")); fetcher()->Schedule(dictionary0_url); fetcher()->Schedule(dictionary1_url); WaitForNoJobs(); ASSERT_EQ(2, jobs_requested()); std::vector additions; GetDictionaryAdditions(&additions); ASSERT_EQ(2u, additions.size()); EXPECT_EQ( URLRequestSpecifiedResponseJob::ExpectedResponseForURL(dictionary0_url), additions[0].dictionary_text); EXPECT_EQ( URLRequestSpecifiedResponseJob::ExpectedResponseForURL(dictionary1_url), additions[1].dictionary_text); } } // namespace } // namespace net