blob: 743fb263fdd07b2eb7d44f2d4367342624b66c52 [file] [log] [blame]
// 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 <vector>
#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<std::string> adapter_names;
DhcpProxyScriptFetcherWin::GetCandidateAdapterNames(&adapter_names);
for (std::set<std::string>::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<URLRequestContext> context_;
scoped_ptr<DhcpProxyScriptFetcherWin> fetcher_;
bool finished_;
base::string16 pac_text_;
base::OneShotTimer<RealFetchTester> timeout_;
base::OneShotTimer<RealFetchTester> 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<base::TaskRunner> 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<base::TaskRunner> 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<DummyDhcpProxyScriptAdapterFetcher> timer_;
};
class MockDhcpProxyScriptFetcherWin : public DhcpProxyScriptFetcherWin {
public:
class MockAdapterQuery : public AdapterQuery {
public:
MockAdapterQuery() {
}
virtual ~MockAdapterQuery() {
}
virtual bool ImplGetCandidateAdapterNames(
std::set<std::string>* adapter_names) OVERRIDE {
adapter_names->insert(
mock_adapter_names_.begin(), mock_adapter_names_.end());
return true;
}
std::vector<std::string> 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<DummyDhcpProxyScriptAdapterFetcher> 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<DhcpProxyScriptAdapterFetcher*>::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<DhcpProxyScriptAdapterFetcher*> adapter_fetchers_;
scoped_refptr<MockAdapterQuery> 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<base::TaskRunner> GetTaskRunner() {
return fetcher_.GetTaskRunner();
}
scoped_ptr<URLRequestContext> 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<DummyDhcpProxyScriptAdapterFetcher> 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<DummyDhcpProxyScriptAdapterFetcher> 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<FetcherClientTestFunction> 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