// Copyright (c) 2012 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/proxy/dhcp_proxy_script_fetcher_win.h" #include #include "base/bind.h" #include "base/bind_helpers.h" #include "base/message_loop/message_loop.h" #include "base/rand_util.h" #include "base/test/test_timeouts.h" #include "base/threading/platform_thread.h" #include "base/timer/elapsed_timer.h" #include "net/base/completion_callback.h" #include "net/proxy/dhcp_proxy_script_adapter_fetcher_win.h" #include "net/url_request/url_request_test_util.h" #include "testing/gtest/include/gtest/gtest.h" namespace net { namespace { TEST(DhcpProxyScriptFetcherWin, AdapterNamesAndPacURLFromDhcp) { // This tests our core Win32 implementation without any of the wrappers // we layer on top to achieve asynchronous and parallel operations. // // We don't make assumptions about the environment this unit test is // running in, so it just exercises the code to make sure there // is no crash and no error returned, but does not assert on the number // of interfaces or the information returned via DHCP. std::set adapter_names; DhcpProxyScriptFetcherWin::GetCandidateAdapterNames(&adapter_names); for (std::set::const_iterator it = adapter_names.begin(); it != adapter_names.end(); ++it) { const std::string& adapter_name = *it; std::string pac_url = DhcpProxyScriptAdapterFetcher::GetPacURLFromDhcp(adapter_name); printf("Adapter '%s' has PAC URL '%s' configured in DHCP.\n", adapter_name.c_str(), pac_url.c_str()); } } // Helper for RealFetch* tests below. class RealFetchTester { public: RealFetchTester() : context_(new TestURLRequestContext), fetcher_(new DhcpProxyScriptFetcherWin(context_.get())), finished_(false), on_completion_is_error_(false) { // Make sure the test ends. timeout_.Start(FROM_HERE, base::TimeDelta::FromSeconds(5), this, &RealFetchTester::OnTimeout); } void RunTest() { int result = fetcher_->Fetch( &pac_text_, base::Bind(&RealFetchTester::OnCompletion, base::Unretained(this))); if (result != ERR_IO_PENDING) finished_ = true; } void RunTestWithCancel() { RunTest(); fetcher_->Cancel(); } void RunTestWithDeferredCancel() { // Put the cancellation into the queue before even running the // test to avoid the chance of one of the adapter fetcher worker // threads completing before cancellation. See http://crbug.com/86756. cancel_timer_.Start(FROM_HERE, base::TimeDelta::FromMilliseconds(0), this, &RealFetchTester::OnCancelTimer); RunTest(); } void OnCompletion(int result) { if (on_completion_is_error_) { FAIL() << "Received completion for test in which this is error."; } finished_ = true; printf("Result code %d PAC data length %d\n", result, pac_text_.size()); } void OnTimeout() { printf("Timeout!"); OnCompletion(0); } void OnCancelTimer() { fetcher_->Cancel(); finished_ = true; } void WaitUntilDone() { while (!finished_) { base::MessageLoop::current()->RunUntilIdle(); } base::MessageLoop::current()->RunUntilIdle(); } // Attempts to give worker threads time to finish. This is currently // very simplistic as completion (via completion callback or cancellation) // immediately "detaches" any worker threads, so the best we can do is give // them a little time. If we start running into Valgrind leaks, we can // do something a bit more clever to track worker threads even when the // DhcpProxyScriptFetcherWin state machine has finished. void FinishTestAllowCleanup() { base::PlatformThread::Sleep(base::TimeDelta::FromMilliseconds(30)); } scoped_ptr context_; scoped_ptr fetcher_; bool finished_; base::string16 pac_text_; base::OneShotTimer timeout_; base::OneShotTimer cancel_timer_; bool on_completion_is_error_; }; TEST(DhcpProxyScriptFetcherWin, RealFetch) { // This tests a call to Fetch() with no stubbing out of dependencies. // // We don't make assumptions about the environment this unit test is // running in, so it just exercises the code to make sure there // is no crash and no unexpected error returned, but does not assert on // results beyond that. RealFetchTester fetcher; fetcher.RunTest(); fetcher.WaitUntilDone(); printf("PAC URL was %s\n", fetcher.fetcher_->GetPacURL().possibly_invalid_spec().c_str()); fetcher.FinishTestAllowCleanup(); } TEST(DhcpProxyScriptFetcherWin, RealFetchWithCancel) { // Does a Fetch() with an immediate cancel. As before, just // exercises the code without stubbing out dependencies. RealFetchTester fetcher; fetcher.RunTestWithCancel(); base::MessageLoop::current()->RunUntilIdle(); // Attempt to avoid Valgrind leak reports in case worker thread is // still running. fetcher.FinishTestAllowCleanup(); } // For RealFetchWithDeferredCancel, below. class DelayingDhcpProxyScriptAdapterFetcher : public DhcpProxyScriptAdapterFetcher { public: DelayingDhcpProxyScriptAdapterFetcher( URLRequestContext* url_request_context, scoped_refptr task_runner) : DhcpProxyScriptAdapterFetcher(url_request_context, task_runner) { } class DelayingDhcpQuery : public DhcpQuery { public: explicit DelayingDhcpQuery() : DhcpQuery() { } std::string ImplGetPacURLFromDhcp( const std::string& adapter_name) OVERRIDE { base::PlatformThread::Sleep(base::TimeDelta::FromMilliseconds(20)); return DhcpQuery::ImplGetPacURLFromDhcp(adapter_name); } }; DhcpQuery* ImplCreateDhcpQuery() OVERRIDE { return new DelayingDhcpQuery(); } }; // For RealFetchWithDeferredCancel, below. class DelayingDhcpProxyScriptFetcherWin : public DhcpProxyScriptFetcherWin { public: explicit DelayingDhcpProxyScriptFetcherWin( URLRequestContext* context) : DhcpProxyScriptFetcherWin(context) { } DhcpProxyScriptAdapterFetcher* ImplCreateAdapterFetcher() OVERRIDE { return new DelayingDhcpProxyScriptAdapterFetcher(url_request_context(), GetTaskRunner()); } }; TEST(DhcpProxyScriptFetcherWin, RealFetchWithDeferredCancel) { // Does a Fetch() with a slightly delayed cancel. As before, just // exercises the code without stubbing out dependencies, but // introduces a guaranteed 20 ms delay on the worker threads so that // the cancel is called before they complete. RealFetchTester fetcher; fetcher.fetcher_.reset( new DelayingDhcpProxyScriptFetcherWin(fetcher.context_.get())); fetcher.on_completion_is_error_ = true; fetcher.RunTestWithDeferredCancel(); fetcher.WaitUntilDone(); } // The remaining tests are to exercise our state machine in various // situations, with actual network access fully stubbed out. class DummyDhcpProxyScriptAdapterFetcher : public DhcpProxyScriptAdapterFetcher { public: DummyDhcpProxyScriptAdapterFetcher(URLRequestContext* context, scoped_refptr runner) : DhcpProxyScriptAdapterFetcher(context, runner), did_finish_(false), result_(OK), pac_script_(L"bingo"), fetch_delay_ms_(1) { } void Fetch(const std::string& adapter_name, const CompletionCallback& callback) OVERRIDE { callback_ = callback; timer_.Start(FROM_HERE, base::TimeDelta::FromMilliseconds(fetch_delay_ms_), this, &DummyDhcpProxyScriptAdapterFetcher::OnTimer); } void Cancel() OVERRIDE { timer_.Stop(); } bool DidFinish() const OVERRIDE { return did_finish_; } int GetResult() const OVERRIDE { return result_; } base::string16 GetPacScript() const OVERRIDE { return pac_script_; } void OnTimer() { callback_.Run(result_); } void Configure(bool did_finish, int result, base::string16 pac_script, int fetch_delay_ms) { did_finish_ = did_finish; result_ = result; pac_script_ = pac_script; fetch_delay_ms_ = fetch_delay_ms; } private: bool did_finish_; int result_; base::string16 pac_script_; int fetch_delay_ms_; CompletionCallback callback_; base::OneShotTimer timer_; }; class MockDhcpProxyScriptFetcherWin : public DhcpProxyScriptFetcherWin { public: class MockAdapterQuery : public AdapterQuery { public: MockAdapterQuery() { } virtual ~MockAdapterQuery() { } virtual bool ImplGetCandidateAdapterNames( std::set* adapter_names) OVERRIDE { adapter_names->insert( mock_adapter_names_.begin(), mock_adapter_names_.end()); return true; } std::vector mock_adapter_names_; }; MockDhcpProxyScriptFetcherWin(URLRequestContext* context) : DhcpProxyScriptFetcherWin(context), num_fetchers_created_(0), worker_finished_event_(true, false) { ResetTestState(); } virtual ~MockDhcpProxyScriptFetcherWin() { ResetTestState(); } using DhcpProxyScriptFetcherWin::GetTaskRunner; // Adds a fetcher object to the queue of fetchers used by // |ImplCreateAdapterFetcher()|, and its name to the list of adapters // returned by ImplGetCandidateAdapterNames. void PushBackAdapter(const std::string& adapter_name, DhcpProxyScriptAdapterFetcher* fetcher) { adapter_query_->mock_adapter_names_.push_back(adapter_name); adapter_fetchers_.push_back(fetcher); } void ConfigureAndPushBackAdapter(const std::string& adapter_name, bool did_finish, int result, base::string16 pac_script, base::TimeDelta fetch_delay) { scoped_ptr adapter_fetcher( new DummyDhcpProxyScriptAdapterFetcher(url_request_context(), GetTaskRunner())); adapter_fetcher->Configure( did_finish, result, pac_script, fetch_delay.InMilliseconds()); PushBackAdapter(adapter_name, adapter_fetcher.release()); } DhcpProxyScriptAdapterFetcher* ImplCreateAdapterFetcher() OVERRIDE { ++num_fetchers_created_; return adapter_fetchers_[next_adapter_fetcher_index_++]; } virtual AdapterQuery* ImplCreateAdapterQuery() OVERRIDE { DCHECK(adapter_query_); return adapter_query_.get(); } base::TimeDelta ImplGetMaxWait() OVERRIDE { return max_wait_; } void ImplOnGetCandidateAdapterNamesDone() OVERRIDE { worker_finished_event_.Signal(); } void ResetTestState() { // Delete any adapter fetcher objects we didn't hand out. std::vector::const_iterator it = adapter_fetchers_.begin(); for (; it != adapter_fetchers_.end(); ++it) { if (num_fetchers_created_-- <= 0) { delete (*it); } } next_adapter_fetcher_index_ = 0; num_fetchers_created_ = 0; adapter_fetchers_.clear(); adapter_query_ = new MockAdapterQuery(); max_wait_ = TestTimeouts::tiny_timeout(); } bool HasPendingFetchers() { return num_pending_fetchers() > 0; } int next_adapter_fetcher_index_; // Ownership gets transferred to the implementation class via // ImplCreateAdapterFetcher, but any objects not handed out are // deleted on destruction. std::vector adapter_fetchers_; scoped_refptr adapter_query_; base::TimeDelta max_wait_; int num_fetchers_created_; base::WaitableEvent worker_finished_event_; }; class FetcherClient { public: FetcherClient() : context_(new TestURLRequestContext), fetcher_(context_.get()), finished_(false), result_(ERR_UNEXPECTED) { } void RunTest() { int result = fetcher_.Fetch( &pac_text_, base::Bind(&FetcherClient::OnCompletion, base::Unretained(this))); ASSERT_EQ(ERR_IO_PENDING, result); } void RunMessageLoopUntilComplete() { while (!finished_) { base::MessageLoop::current()->RunUntilIdle(); } base::MessageLoop::current()->RunUntilIdle(); } void RunMessageLoopUntilWorkerDone() { DCHECK(fetcher_.adapter_query_.get()); while (!fetcher_.worker_finished_event_.TimedWait( base::TimeDelta::FromMilliseconds(10))) { base::MessageLoop::current()->RunUntilIdle(); } } void OnCompletion(int result) { finished_ = true; result_ = result; } void ResetTestState() { finished_ = false; result_ = ERR_UNEXPECTED; pac_text_ = L""; fetcher_.ResetTestState(); } scoped_refptr GetTaskRunner() { return fetcher_.GetTaskRunner(); } scoped_ptr context_; MockDhcpProxyScriptFetcherWin fetcher_; bool finished_; int result_; base::string16 pac_text_; }; // We separate out each test's logic so that we can easily implement // the ReuseFetcher test at the bottom. void TestNormalCaseURLConfiguredOneAdapter(FetcherClient* client) { TestURLRequestContext context; scoped_ptr adapter_fetcher( new DummyDhcpProxyScriptAdapterFetcher(&context, client->GetTaskRunner())); adapter_fetcher->Configure(true, OK, L"bingo", 1); client->fetcher_.PushBackAdapter("a", adapter_fetcher.release()); client->RunTest(); client->RunMessageLoopUntilComplete(); ASSERT_EQ(OK, client->result_); ASSERT_EQ(L"bingo", client->pac_text_); } TEST(DhcpProxyScriptFetcherWin, NormalCaseURLConfiguredOneAdapter) { FetcherClient client; TestNormalCaseURLConfiguredOneAdapter(&client); } void TestNormalCaseURLConfiguredMultipleAdapters(FetcherClient* client) { client->fetcher_.ConfigureAndPushBackAdapter( "most_preferred", true, ERR_PAC_NOT_IN_DHCP, L"", base::TimeDelta::FromMilliseconds(1)); client->fetcher_.ConfigureAndPushBackAdapter( "second", true, OK, L"bingo", base::TimeDelta::FromMilliseconds(50)); client->fetcher_.ConfigureAndPushBackAdapter( "third", true, OK, L"rocko", base::TimeDelta::FromMilliseconds(1)); client->RunTest(); client->RunMessageLoopUntilComplete(); ASSERT_EQ(OK, client->result_); ASSERT_EQ(L"bingo", client->pac_text_); } TEST(DhcpProxyScriptFetcherWin, NormalCaseURLConfiguredMultipleAdapters) { FetcherClient client; TestNormalCaseURLConfiguredMultipleAdapters(&client); } void TestNormalCaseURLConfiguredMultipleAdaptersWithTimeout( FetcherClient* client) { client->fetcher_.ConfigureAndPushBackAdapter( "most_preferred", true, ERR_PAC_NOT_IN_DHCP, L"", base::TimeDelta::FromMilliseconds(1)); // This will time out. client->fetcher_.ConfigureAndPushBackAdapter( "second", false, ERR_IO_PENDING, L"bingo", TestTimeouts::action_timeout()); client->fetcher_.ConfigureAndPushBackAdapter( "third", true, OK, L"rocko", base::TimeDelta::FromMilliseconds(1)); client->RunTest(); client->RunMessageLoopUntilComplete(); ASSERT_EQ(OK, client->result_); ASSERT_EQ(L"rocko", client->pac_text_); } TEST(DhcpProxyScriptFetcherWin, NormalCaseURLConfiguredMultipleAdaptersWithTimeout) { FetcherClient client; TestNormalCaseURLConfiguredMultipleAdaptersWithTimeout(&client); } void TestFailureCaseURLConfiguredMultipleAdaptersWithTimeout( FetcherClient* client) { client->fetcher_.ConfigureAndPushBackAdapter( "most_preferred", true, ERR_PAC_NOT_IN_DHCP, L"", base::TimeDelta::FromMilliseconds(1)); // This will time out. client->fetcher_.ConfigureAndPushBackAdapter( "second", false, ERR_IO_PENDING, L"bingo", TestTimeouts::action_timeout()); // This is the first non-ERR_PAC_NOT_IN_DHCP error and as such // should be chosen. client->fetcher_.ConfigureAndPushBackAdapter( "third", true, ERR_PAC_STATUS_NOT_OK, L"", base::TimeDelta::FromMilliseconds(1)); client->fetcher_.ConfigureAndPushBackAdapter( "fourth", true, ERR_NOT_IMPLEMENTED, L"", base::TimeDelta::FromMilliseconds(1)); client->RunTest(); client->RunMessageLoopUntilComplete(); ASSERT_EQ(ERR_PAC_STATUS_NOT_OK, client->result_); ASSERT_EQ(L"", client->pac_text_); } TEST(DhcpProxyScriptFetcherWin, FailureCaseURLConfiguredMultipleAdaptersWithTimeout) { FetcherClient client; TestFailureCaseURLConfiguredMultipleAdaptersWithTimeout(&client); } void TestFailureCaseNoURLConfigured(FetcherClient* client) { client->fetcher_.ConfigureAndPushBackAdapter( "most_preferred", true, ERR_PAC_NOT_IN_DHCP, L"", base::TimeDelta::FromMilliseconds(1)); // This will time out. client->fetcher_.ConfigureAndPushBackAdapter( "second", false, ERR_IO_PENDING, L"bingo", TestTimeouts::action_timeout()); // This is the first non-ERR_PAC_NOT_IN_DHCP error and as such // should be chosen. client->fetcher_.ConfigureAndPushBackAdapter( "third", true, ERR_PAC_NOT_IN_DHCP, L"", base::TimeDelta::FromMilliseconds(1)); client->RunTest(); client->RunMessageLoopUntilComplete(); ASSERT_EQ(ERR_PAC_NOT_IN_DHCP, client->result_); ASSERT_EQ(L"", client->pac_text_); } TEST(DhcpProxyScriptFetcherWin, FailureCaseNoURLConfigured) { FetcherClient client; TestFailureCaseNoURLConfigured(&client); } void TestFailureCaseNoDhcpAdapters(FetcherClient* client) { client->RunTest(); client->RunMessageLoopUntilComplete(); ASSERT_EQ(ERR_PAC_NOT_IN_DHCP, client->result_); ASSERT_EQ(L"", client->pac_text_); ASSERT_EQ(0, client->fetcher_.num_fetchers_created_); } TEST(DhcpProxyScriptFetcherWin, FailureCaseNoDhcpAdapters) { FetcherClient client; TestFailureCaseNoDhcpAdapters(&client); } void TestShortCircuitLessPreferredAdapters(FetcherClient* client) { // Here we have a bunch of adapters; the first reports no PAC in DHCP, // the second responds quickly with a PAC file, the rest take a long // time. Verify that we complete quickly and do not wait for the slow // adapters, i.e. we finish before timeout. client->fetcher_.ConfigureAndPushBackAdapter( "1", true, ERR_PAC_NOT_IN_DHCP, L"", base::TimeDelta::FromMilliseconds(1)); client->fetcher_.ConfigureAndPushBackAdapter( "2", true, OK, L"bingo", base::TimeDelta::FromMilliseconds(1)); client->fetcher_.ConfigureAndPushBackAdapter( "3", true, OK, L"wrongo", TestTimeouts::action_max_timeout()); // Increase the timeout to ensure the short circuit mechanism has // time to kick in before the timeout waiting for more adapters kicks in. client->fetcher_.max_wait_ = TestTimeouts::action_timeout(); base::ElapsedTimer timer; client->RunTest(); client->RunMessageLoopUntilComplete(); ASSERT_TRUE(client->fetcher_.HasPendingFetchers()); // Assert that the time passed is definitely less than the wait timer // timeout, to get a second signal that it was the shortcut mechanism // (in OnFetcherDone) that kicked in, and not the timeout waiting for // more adapters. ASSERT_GT(client->fetcher_.max_wait_ - (client->fetcher_.max_wait_ / 10), timer.Elapsed()); } TEST(DhcpProxyScriptFetcherWin, ShortCircuitLessPreferredAdapters) { FetcherClient client; TestShortCircuitLessPreferredAdapters(&client); } void TestImmediateCancel(FetcherClient* client) { TestURLRequestContext context; scoped_ptr adapter_fetcher( new DummyDhcpProxyScriptAdapterFetcher(&context, client->GetTaskRunner())); adapter_fetcher->Configure(true, OK, L"bingo", 1); client->fetcher_.PushBackAdapter("a", adapter_fetcher.release()); client->RunTest(); client->fetcher_.Cancel(); client->RunMessageLoopUntilWorkerDone(); ASSERT_EQ(0, client->fetcher_.num_fetchers_created_); } // Regression test to check that when we cancel immediately, no // adapter fetchers get created. TEST(DhcpProxyScriptFetcherWin, ImmediateCancel) { FetcherClient client; TestImmediateCancel(&client); } TEST(DhcpProxyScriptFetcherWin, ReuseFetcher) { FetcherClient client; // The ProxyScriptFetcher interface stipulates that only a single // |Fetch()| may be in flight at once, but allows reuse, so test // that the state transitions correctly from done to start in all // cases we're testing. typedef void (*FetcherClientTestFunction)(FetcherClient*); typedef std::vector TestVector; TestVector test_functions; test_functions.push_back(TestNormalCaseURLConfiguredOneAdapter); test_functions.push_back(TestNormalCaseURLConfiguredMultipleAdapters); test_functions.push_back( TestNormalCaseURLConfiguredMultipleAdaptersWithTimeout); test_functions.push_back( TestFailureCaseURLConfiguredMultipleAdaptersWithTimeout); test_functions.push_back(TestFailureCaseNoURLConfigured); test_functions.push_back(TestFailureCaseNoDhcpAdapters); test_functions.push_back(TestShortCircuitLessPreferredAdapters); test_functions.push_back(TestImmediateCancel); std::random_shuffle(test_functions.begin(), test_functions.end(), base::RandGenerator); for (TestVector::const_iterator it = test_functions.begin(); it != test_functions.end(); ++it) { (*it)(&client); client.ResetTestState(); } // Re-do the first test to make sure the last test that was run did // not leave things in a bad state. (*test_functions.begin())(&client); } } // namespace } // namespace net