blob: 263664e58f230ba94582f68171c015d6c385fcbc [file] [log] [blame]
// Copyright 2018 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 <map>
#include <memory>
#include <set>
#include <string>
#include <vector>
#include "base/base64.h"
#include "base/command_line.h"
#include "base/files/file_path.h"
#include "base/macros.h"
#include "base/memory/scoped_refptr.h"
#include "base/single_thread_task_runner.h"
#include "base/test/scoped_feature_list.h"
#include "chrome/browser/predictors/loading_predictor.h"
#include "chrome/browser/predictors/loading_predictor_factory.h"
#include "chrome/browser/predictors/loading_test_util.h"
#include "chrome/browser/predictors/preconnect_manager.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/browser/ui/browser_commands.h"
#include "chrome/common/chrome_switches.h"
#include "chrome/test/base/in_process_browser_test.h"
#include "chrome/test/base/ui_test_utils.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/navigation_handle.h"
#include "content/public/browser/page_navigator.h"
#include "content/public/browser/storage_partition.h"
#include "content/public/browser/web_contents.h"
#include "content/public/browser/web_contents_observer.h"
#include "content/public/common/referrer.h"
#include "content/public/test/browser_test_utils.h"
#include "net/dns/mock_host_resolver.h"
#include "net/test/embedded_test_server/embedded_test_server_connection_listener.h"
#include "net/test/embedded_test_server/request_handler_util.h"
#include "net/url_request/url_request_filter.h"
#include "net/url_request/url_request_interceptor.h"
#include "net/url_request/url_request_test_job.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "url/gurl.h"
using content::BrowserThread;
namespace predictors {
const char kChromiumUrl[] = "http://chromium.org";
const char kInvalidLongUrl[] =
"http://"
"illegally-long-hostname-over-255-characters-should-not-send-an-ipc-"
"message-to-the-browser-"
"00000000000000000000000000000000000000000000000000000000000000000000000000"
"00000000000000000000000000000000000000000000000000000000000000000000000000"
"0000000000000000000000000000000000000000000000000000.org";
const char kHtmlSubresourcesPath[] = "/predictors/html_subresources.html";
const std::string kHtmlSubresourcesHosts[] = {"test.com", "baz.com", "foo.com",
"bar.com"};
std::string GetPathWithPortReplacement(const std::string& path, uint16_t port) {
std::string string_port = base::StringPrintf("%d", port);
return net::test_server::GetFilePathWithReplacements(
path, {{"REPLACE_WITH_PORT", string_port}});
}
GURL GetDataURLWithContent(const std::string& content) {
std::string encoded_content;
base::Base64Encode(content, &encoded_content);
std::string data_uri_content = "data:text/html;base64," + encoded_content;
return GURL(data_uri_content);
}
// Helper class to track and allow waiting for ResourcePrefetchPredictor
// initialization. WARNING: OnPredictorInitialized event will not be fired if
// ResourcePrefetchPredictor is initialized before the observer creation.
class PredictorInitializer : public TestObserver {
public:
explicit PredictorInitializer(ResourcePrefetchPredictor* predictor)
: TestObserver(predictor), predictor_(predictor) {}
void EnsurePredictorInitialized() {
if (predictor_->initialization_state_ ==
ResourcePrefetchPredictor::INITIALIZED) {
return;
}
if (predictor_->initialization_state_ ==
ResourcePrefetchPredictor::NOT_INITIALIZED) {
predictor_->StartInitialization();
}
run_loop_.Run();
}
void OnPredictorInitialized() override { run_loop_.Quit(); }
private:
ResourcePrefetchPredictor* predictor_;
base::RunLoop run_loop_;
DISALLOW_COPY_AND_ASSIGN(PredictorInitializer);
};
// Keeps track of incoming connections being accepted or read from and exposes
// that info to the tests.
// A port being reused is currently considered an error.
// If a test needs to verify multiple connections are opened in sequence, that
// will need to be changed.
class ConnectionTracker {
public:
ConnectionTracker() {}
void AcceptedSocketWithPort(uint16_t port) {
EXPECT_FALSE(base::ContainsKey(sockets_, port));
sockets_[port] = SocketStatus::kAccepted;
CheckAccepted();
first_accept_loop_.Quit();
}
void ReadFromSocketWithPort(uint16_t port) {
EXPECT_TRUE(base::ContainsKey(sockets_, port));
sockets_[port] = SocketStatus::kReadFrom;
first_read_loop_.Quit();
}
// Returns the number of sockets that were accepted by the server.
size_t GetAcceptedSocketCount() const { return sockets_.size(); }
// Returns the number of sockets that were read from by the server.
size_t GetReadSocketCount() const {
size_t read_sockets = 0;
for (const auto& socket : sockets_) {
if (socket.second == SocketStatus::kReadFrom)
++read_sockets;
}
return read_sockets;
}
void WaitUntilFirstConnectionAccepted() { first_accept_loop_.Run(); }
void WaitUntilFirstConnectionRead() { first_read_loop_.Run(); }
// The UI thread will wait for exactly |num_connections| items in |sockets_|.
// This method expects the server will not accept more than |num_connections|
// connections. |num_connections| must be greater than 0.
void WaitForAcceptedConnections(size_t num_connections) {
DCHECK_CURRENTLY_ON(BrowserThread::UI);
DCHECK(!num_accepted_connections_loop_);
DCHECK_GT(num_connections, 0u);
base::RunLoop run_loop;
EXPECT_GE(num_connections, sockets_.size());
num_accepted_connections_loop_ = &run_loop;
num_accepted_connections_needed_ = num_connections;
CheckAccepted();
// Note that the previous call to CheckAccepted can quit this run loop
// before this call, which will make this call a no-op.
run_loop.Run();
EXPECT_EQ(num_connections, sockets_.size());
}
// Helper function to stop the waiting for sockets to be accepted for
// WaitForAcceptedConnections. |num_accepted_connections_loop_| spins
// until |num_accepted_connections_needed_| sockets are accepted by the test
// server. The values will be null/0 if the loop is not running.
void CheckAccepted() {
// |num_accepted_connections_loop_| null implies
// |num_accepted_connections_needed_| == 0.
DCHECK(num_accepted_connections_loop_ ||
num_accepted_connections_needed_ == 0);
if (!num_accepted_connections_loop_ ||
num_accepted_connections_needed_ != sockets_.size()) {
return;
}
num_accepted_connections_loop_->Quit();
num_accepted_connections_needed_ = 0;
num_accepted_connections_loop_ = nullptr;
}
void ResetCounts() { sockets_.clear(); }
private:
enum class SocketStatus { kAccepted, kReadFrom };
base::RunLoop first_accept_loop_;
base::RunLoop first_read_loop_;
// Port -> SocketStatus.
using SocketContainer = std::map<uint16_t, SocketStatus>;
SocketContainer sockets_;
// If |num_accepted_connections_needed_| is non zero, then the object is
// waiting for |num_accepted_connections_needed_| sockets to be accepted
// before quitting the |num_accepted_connections_loop_|.
size_t num_accepted_connections_needed_ = 0;
base::RunLoop* num_accepted_connections_loop_ = nullptr;
DISALLOW_COPY_AND_ASSIGN(ConnectionTracker);
};
// Gets notified by the EmbeddedTestServer on incoming connections being
// accepted or read from and transfers this information to ConnectionTracker.
class ConnectionListener
: public net::test_server::EmbeddedTestServerConnectionListener {
public:
// This class should be constructed on the browser UI thread.
explicit ConnectionListener(ConnectionTracker* tracker)
: task_runner_(base::ThreadTaskRunnerHandle::Get()), tracker_(tracker) {}
~ConnectionListener() override {}
// Get called from the EmbeddedTestServer thread to be notified that
// a connection was accepted.
void AcceptedSocket(const net::StreamSocket& connection) override {
uint16_t port = GetPort(connection);
task_runner_->PostTask(
FROM_HERE, base::BindOnce(&ConnectionTracker::AcceptedSocketWithPort,
base::Unretained(tracker_), port));
}
// Get called from the EmbeddedTestServer thread to be notified that
// a connection was read from.
void ReadFromSocket(const net::StreamSocket& connection, int rv) override {
// Don't log a read if no data was transferred. This case often happens if
// the sockets of the test server are being flushed and disconnected.
if (rv <= 0)
return;
uint16_t port = GetPort(connection);
task_runner_->PostTask(
FROM_HERE, base::BindOnce(&ConnectionTracker::ReadFromSocketWithPort,
base::Unretained(tracker_), port));
}
private:
static uint16_t GetPort(const net::StreamSocket& connection) {
// Get the remote port of the peer, since the local port will always be the
// port the test server is listening on. This isn't strictly correct - it's
// possible for multiple peers to connect with the same remote port but
// different remote IPs - but the tests here assume that connections to the
// test server (running on localhost) will always come from localhost, and
// thus the peer port is all that's needed to distinguish two connections.
// This also would be problematic if the OS reused ports, but that's not
// something to worry about for these tests.
net::IPEndPoint address;
EXPECT_EQ(net::OK, connection.GetPeerAddress(&address));
return address.port();
}
// Task runner associated with the browser UI thread.
scoped_refptr<base::SingleThreadTaskRunner> task_runner_;
// This pointer should be only accessed on the browser UI thread.
ConnectionTracker* tracker_;
};
class TestPreconnectManagerObserver : public PreconnectManager::Observer {
public:
explicit TestPreconnectManagerObserver(
PreconnectManager* preconnect_manager_) {
preconnect_manager_->SetObserverForTesting(this);
}
void OnPreconnectUrl(const GURL& url,
int num_sockets,
bool allow_credentials) override {
preconnect_url_attempts_.insert(url.GetOrigin());
}
void OnPreresolveFinished(const GURL& url, bool success) override {
if (success)
successful_dns_lookups_.insert(url.host());
else
unsuccessful_dns_lookups_.insert(url.host());
CheckForWaitingLoop();
}
void OnProxyLookupFinished(const GURL& url, bool success) override {
GURL origin = url.GetOrigin();
if (success)
successful_proxy_lookups_.insert(origin);
else
unsuccessful_proxy_lookups_.insert(origin);
CheckForWaitingLoop();
}
void WaitUntilHostLookedUp(const std::string& host) {
wait_event_ = WaitEvent::kDns;
DCHECK(waiting_on_dns_.empty());
waiting_on_dns_ = host;
Wait();
}
void WaitUntilProxyLookedUp(const GURL& url) {
wait_event_ = WaitEvent::kProxy;
DCHECK(waiting_on_proxy_.is_empty());
waiting_on_proxy_ = url;
Wait();
}
bool HasOriginAttemptedToPreconnect(const GURL& origin) {
DCHECK_EQ(origin, origin.GetOrigin());
return base::ContainsKey(preconnect_url_attempts_, origin);
}
bool HasHostBeenLookedUp(const std::string& host) {
return base::ContainsKey(successful_dns_lookups_, host) ||
base::ContainsKey(unsuccessful_dns_lookups_, host);
}
bool HostFound(const std::string& host) {
return base::ContainsKey(successful_dns_lookups_, host);
}
bool HasProxyBeenLookedUp(const GURL& url) {
return base::ContainsKey(successful_proxy_lookups_, url.GetOrigin()) ||
base::ContainsKey(unsuccessful_proxy_lookups_, url.GetOrigin());
}
bool ProxyFound(const GURL& url) {
return base::ContainsKey(successful_proxy_lookups_, url.GetOrigin());
}
private:
enum class WaitEvent { kNone, kDns, kProxy };
void Wait() {
base::RunLoop run_loop;
DCHECK(!run_loop_);
run_loop_ = &run_loop;
CheckForWaitingLoop();
run_loop.Run();
}
void CheckForWaitingLoop() {
switch (wait_event_) {
case WaitEvent::kNone:
return;
case WaitEvent::kDns:
if (!HasHostBeenLookedUp(waiting_on_dns_))
return;
waiting_on_dns_ = std::string();
break;
case WaitEvent::kProxy:
if (!HasProxyBeenLookedUp(waiting_on_proxy_))
return;
waiting_on_proxy_ = GURL();
break;
}
DCHECK(run_loop_);
run_loop_->Quit();
run_loop_ = nullptr;
wait_event_ = WaitEvent::kNone;
}
WaitEvent wait_event_ = WaitEvent::kNone;
base::RunLoop* run_loop_ = nullptr;
std::string waiting_on_dns_;
std::set<std::string> successful_dns_lookups_;
std::set<std::string> unsuccessful_dns_lookups_;
GURL waiting_on_proxy_;
std::set<GURL> successful_proxy_lookups_;
std::set<GURL> unsuccessful_proxy_lookups_;
std::set<GURL> preconnect_url_attempts_;
};
class LoadingPredictorBrowserTest : public InProcessBrowserTest {
public:
LoadingPredictorBrowserTest() {}
~LoadingPredictorBrowserTest() override {}
void SetUp() override {
ASSERT_TRUE(embedded_test_server()->InitializeAndListen());
InProcessBrowserTest::SetUp();
}
void SetUpOnMainThread() override {
host_resolver()->AddRule("*", "127.0.0.1");
connection_tracker_ = std::make_unique<ConnectionTracker>();
connection_listener_ =
std::make_unique<ConnectionListener>(connection_tracker_.get());
embedded_test_server()->SetConnectionListener(connection_listener_.get());
embedded_test_server()->StartAcceptingConnections();
loading_predictor_ =
LoadingPredictorFactory::GetForProfile(browser()->profile());
ASSERT_TRUE(loading_predictor_);
preconnect_manager_observer_ =
std::make_unique<TestPreconnectManagerObserver>(
loading_predictor_->preconnect_manager());
PredictorInitializer initializer(
loading_predictor_->resource_prefetch_predictor());
initializer.EnsurePredictorInitialized();
}
// Navigates to an URL without blocking until the navigation finishes.
// Returns an observer that can be used to wait for the navigation
// completion.
// This function creates a new tab for each navigation that allows multiple
// simultaneous navigations and avoids triggering the reload behavior.
std::unique_ptr<content::TestNavigationManager> NavigateToURLAsync(
const GURL& url) {
chrome::NewTab(browser());
content::WebContents* tab =
browser()->tab_strip_model()->GetActiveWebContents();
DCHECK(tab);
auto observer = std::make_unique<content::TestNavigationManager>(tab, url);
tab->GetController().LoadURL(url, content::Referrer(),
ui::PAGE_TRANSITION_TYPED, std::string());
return observer;
}
void ResetNetworkState() {
auto* network_context = content::BrowserContext::GetDefaultStoragePartition(
browser()->profile())
->GetNetworkContext();
base::RunLoop clear_host_cache_loop;
base::RunLoop close_all_connections_loop;
network_context->ClearHostCache(nullptr,
clear_host_cache_loop.QuitClosure());
network_context->CloseAllConnections(
close_all_connections_loop.QuitClosure());
clear_host_cache_loop.Run();
close_all_connections_loop.Run();
connection_tracker()->ResetCounts();
}
void ResetPredictorState() {
loading_predictor_->resource_prefetch_predictor()->DeleteAllUrls();
}
std::unique_ptr<PreconnectPrediction> GetPreconnectPrediction(
const GURL& url) {
auto prediction = std::make_unique<PreconnectPrediction>();
bool has_prediction = loading_predictor_->resource_prefetch_predictor()
->PredictPreconnectOrigins(url, prediction.get());
if (!has_prediction)
return nullptr;
return prediction;
}
LoadingPredictor* loading_predictor() { return loading_predictor_; }
TestPreconnectManagerObserver* preconnect_manager_observer() {
return preconnect_manager_observer_.get();
}
ConnectionTracker* connection_tracker() { return connection_tracker_.get(); }
private:
LoadingPredictor* loading_predictor_ = nullptr;
std::unique_ptr<ConnectionListener> connection_listener_;
std::unique_ptr<ConnectionTracker> connection_tracker_;
std::unique_ptr<TestPreconnectManagerObserver> preconnect_manager_observer_;
DISALLOW_COPY_AND_ASSIGN(LoadingPredictorBrowserTest);
};
// Tests that a navigation triggers the LoadingPredictor.
IN_PROC_BROWSER_TEST_F(LoadingPredictorBrowserTest, SimpleNavigation) {
GURL url = embedded_test_server()->GetURL("/nocontent");
auto observer = NavigateToURLAsync(url);
EXPECT_TRUE(observer->WaitForRequestStart());
EXPECT_EQ(1u, loading_predictor()->GetActiveNavigationsSizeForTesting());
// Checking GetActiveHintsSizeForTesting() is racy since the active hint
// is removed after the preconnect finishes. Instead check for total
// hints activated.
EXPECT_LE(1u, loading_predictor()->GetTotalHintsActivatedForTesting());
EXPECT_GE(2u, loading_predictor()->GetTotalHintsActivatedForTesting());
observer->WaitForNavigationFinished();
EXPECT_EQ(0u, loading_predictor()->GetActiveNavigationsSizeForTesting());
EXPECT_EQ(0u, loading_predictor()->GetActiveHintsSizeForTesting());
EXPECT_LE(1u, loading_predictor()->GetTotalHintsActivatedForTesting());
EXPECT_GE(2u, loading_predictor()->GetTotalHintsActivatedForTesting());
}
// Tests that two concurrenct navigations are recorded correctly by the
// predictor.
IN_PROC_BROWSER_TEST_F(LoadingPredictorBrowserTest, TwoConcurrentNavigations) {
GURL url1 = embedded_test_server()->GetURL("/echo-raw?1");
GURL url2 = embedded_test_server()->GetURL("/echo-raw?2");
auto observer1 = NavigateToURLAsync(url1);
auto observer2 = NavigateToURLAsync(url2);
EXPECT_TRUE(observer1->WaitForRequestStart());
EXPECT_TRUE(observer2->WaitForRequestStart());
EXPECT_EQ(2u, loading_predictor()->GetActiveNavigationsSizeForTesting());
// Checking GetActiveHintsSizeForTesting() is racy since the active hint
// is removed after the preconnect finishes. Instead check for total
// hints activated.
EXPECT_LE(2u, loading_predictor()->GetTotalHintsActivatedForTesting());
EXPECT_GE(4u, loading_predictor()->GetTotalHintsActivatedForTesting());
observer1->WaitForNavigationFinished();
observer2->WaitForNavigationFinished();
EXPECT_EQ(0u, loading_predictor()->GetActiveNavigationsSizeForTesting());
EXPECT_EQ(0u, loading_predictor()->GetActiveHintsSizeForTesting());
EXPECT_LE(2u, loading_predictor()->GetTotalHintsActivatedForTesting());
EXPECT_GE(4u, loading_predictor()->GetTotalHintsActivatedForTesting());
}
// Tests that two navigations to the same URL are deduplicated.
IN_PROC_BROWSER_TEST_F(LoadingPredictorBrowserTest,
TwoNavigationsToTheSameURL) {
GURL url = embedded_test_server()->GetURL("/nocontent");
auto observer1 = NavigateToURLAsync(url);
auto observer2 = NavigateToURLAsync(url);
EXPECT_TRUE(observer1->WaitForRequestStart());
EXPECT_TRUE(observer2->WaitForRequestStart());
EXPECT_EQ(2u, loading_predictor()->GetActiveNavigationsSizeForTesting());
// Checking GetActiveHintsSizeForTesting() is racy since the active hint
// is removed after the preconnect finishes. Instead check for total
// hints activated. The total hints activated may be only 1 if the second
// navigation arrives before the first preconnect finishes. However, if the
// second navigation arrives later, then two hints may get activated.
EXPECT_LE(1u, loading_predictor()->GetTotalHintsActivatedForTesting());
EXPECT_GE(4u, loading_predictor()->GetTotalHintsActivatedForTesting());
observer1->WaitForNavigationFinished();
observer2->WaitForNavigationFinished();
EXPECT_EQ(0u, loading_predictor()->GetActiveNavigationsSizeForTesting());
EXPECT_EQ(0u, loading_predictor()->GetActiveHintsSizeForTesting());
EXPECT_LE(1u, loading_predictor()->GetTotalHintsActivatedForTesting());
EXPECT_GE(4u, loading_predictor()->GetTotalHintsActivatedForTesting());
}
// Tests that the LoadingPredictor doesn't record non-http(s) navigations.
IN_PROC_BROWSER_TEST_F(LoadingPredictorBrowserTest, NonHttpNavigation) {
std::string content = "<body>Hello world!</body>";
GURL url = GetDataURLWithContent(content);
auto observer = NavigateToURLAsync(url);
EXPECT_TRUE(observer->WaitForRequestStart());
EXPECT_EQ(0u, loading_predictor()->GetActiveNavigationsSizeForTesting());
EXPECT_EQ(0u, loading_predictor()->GetActiveHintsSizeForTesting());
}
// Tests that the LoadingPredictor doesn't preconnect to non-http(s) urls.
IN_PROC_BROWSER_TEST_F(LoadingPredictorBrowserTest,
PrepareForPageLoadNonHttpScheme) {
std::string content = "<body>Hello world!</body>";
GURL url = GetDataURLWithContent(content);
ui_test_utils::NavigateToURL(browser(), url);
// Ensure that no backgound task would make a host lookup or attempt to
// preconnect.
base::RunLoop().RunUntilIdle();
EXPECT_FALSE(preconnect_manager_observer()->HasHostBeenLookedUp(url.host()));
EXPECT_FALSE(preconnect_manager_observer()->HasHostBeenLookedUp(""));
EXPECT_FALSE(preconnect_manager_observer()->HasOriginAttemptedToPreconnect(
url.GetOrigin()));
EXPECT_FALSE(
preconnect_manager_observer()->HasOriginAttemptedToPreconnect(GURL()));
}
// Tests that the LoadingPredictor preconnects to the main frame origin even if
// it doesn't have any prediction for this origin.
IN_PROC_BROWSER_TEST_F(LoadingPredictorBrowserTest,
PrepareForPageLoadWithoutPrediction) {
// Navigate the first time to fill the HTTP cache.
GURL url = embedded_test_server()->GetURL(
"test.com", GetPathWithPortReplacement(kHtmlSubresourcesPath,
embedded_test_server()->port()));
ui_test_utils::NavigateToURL(browser(), url);
ResetNetworkState();
ResetPredictorState();
auto observer = NavigateToURLAsync(url);
EXPECT_TRUE(observer->WaitForRequestStart());
preconnect_manager_observer()->WaitUntilHostLookedUp(url.host());
EXPECT_TRUE(preconnect_manager_observer()->HostFound(url.host()));
// We should preconnect only 2 sockets for the main frame host.
const size_t expected_connections = 2;
connection_tracker()->WaitForAcceptedConnections(expected_connections);
EXPECT_EQ(expected_connections,
connection_tracker()->GetAcceptedSocketCount());
// No reads since all resources should be cached.
EXPECT_EQ(0u, connection_tracker()->GetReadSocketCount());
}
// Tests that the LoadingPredictor has a prediction for a host after navigating
// to it.
IN_PROC_BROWSER_TEST_F(LoadingPredictorBrowserTest, LearnFromNavigation) {
GURL url = embedded_test_server()->GetURL(
"test.com", GetPathWithPortReplacement(kHtmlSubresourcesPath,
embedded_test_server()->port()));
std::vector<PreconnectRequest> requests;
for (const auto& host : kHtmlSubresourcesHosts)
requests.emplace_back(embedded_test_server()->GetURL(host, "/"), 1);
ui_test_utils::NavigateToURL(browser(), url);
auto prediction = GetPreconnectPrediction(url);
ASSERT_TRUE(prediction);
EXPECT_EQ(prediction->is_redirected, false);
EXPECT_EQ(prediction->host, url.host());
EXPECT_THAT(prediction->requests,
testing::UnorderedElementsAreArray(requests));
}
// Tests that the LoadingPredictor correctly learns from navigations with
// redirect.
IN_PROC_BROWSER_TEST_F(LoadingPredictorBrowserTest,
LearnFromNavigationWithRedirect) {
GURL redirect_url = embedded_test_server()->GetURL(
"test.com", GetPathWithPortReplacement(kHtmlSubresourcesPath,
embedded_test_server()->port()));
GURL original_url = embedded_test_server()->GetURL(
"redirect.com",
base::StringPrintf("/server-redirect?%s", redirect_url.spec().c_str()));
std::vector<PreconnectRequest> requests;
for (const auto& host : kHtmlSubresourcesHosts)
requests.emplace_back(embedded_test_server()->GetURL(host, "/"), 1);
ui_test_utils::NavigateToURL(browser(), original_url);
// The predictor can correctly predict hosts by |redirect_url|
auto prediction = GetPreconnectPrediction(redirect_url);
ASSERT_TRUE(prediction);
EXPECT_EQ(prediction->is_redirected, false);
EXPECT_EQ(prediction->host, redirect_url.host());
EXPECT_THAT(prediction->requests,
testing::UnorderedElementsAreArray(requests));
// The predictor needs minimum two redirect hits to be confident in the
// redirect.
prediction = GetPreconnectPrediction(original_url);
EXPECT_FALSE(prediction);
// The predictor will start predict a redirect after the second navigation.
ui_test_utils::NavigateToURL(browser(), original_url);
prediction = GetPreconnectPrediction(original_url);
ASSERT_TRUE(prediction);
EXPECT_EQ(prediction->is_redirected, true);
EXPECT_EQ(prediction->host, redirect_url.host());
EXPECT_THAT(prediction->requests,
testing::UnorderedElementsAreArray(requests));
}
// Tests that the LoadingPredictor performs preresolving/preconnecting for a
// navigation which it has a prediction for.
IN_PROC_BROWSER_TEST_F(LoadingPredictorBrowserTest,
PrepareForPageLoadWithPrediction) {
// Navigate the first time to fill the predictor's database and the HTTP
// cache.
GURL url = embedded_test_server()->GetURL(
"test.com", GetPathWithPortReplacement(kHtmlSubresourcesPath,
embedded_test_server()->port()));
ui_test_utils::NavigateToURL(browser(), url);
ResetNetworkState();
auto observer = NavigateToURLAsync(url);
EXPECT_TRUE(observer->WaitForRequestStart());
for (const auto& host : kHtmlSubresourcesHosts) {
GURL url(base::StringPrintf("http://%s", host.c_str()));
preconnect_manager_observer()->WaitUntilHostLookedUp(url.host());
EXPECT_TRUE(preconnect_manager_observer()->HostFound(url.host()));
}
// 2 connections to the main frame host + 1 connection per host for others.
const size_t expected_connections = base::size(kHtmlSubresourcesHosts) + 1;
connection_tracker()->WaitForAcceptedConnections(expected_connections);
EXPECT_EQ(expected_connections,
connection_tracker()->GetAcceptedSocketCount());
// No reads since all resources should be cached.
EXPECT_EQ(0u, connection_tracker()->GetReadSocketCount());
}
// Tests that a host requested by <link rel="dns-prefetch"> is looked up.
IN_PROC_BROWSER_TEST_F(LoadingPredictorBrowserTest, DnsPrefetch) {
ui_test_utils::NavigateToURL(browser(), embedded_test_server()->GetURL(
"/predictor/dns_prefetch.html"));
preconnect_manager_observer()->WaitUntilHostLookedUp(
GURL(kChromiumUrl).host());
EXPECT_FALSE(preconnect_manager_observer()->HasHostBeenLookedUp(
GURL(kInvalidLongUrl).host()));
EXPECT_TRUE(
preconnect_manager_observer()->HostFound(GURL(kChromiumUrl).host()));
}
// Tests that preconnect warms up a socket connection to a test server.
// Note: This test uses a data URI to serve the preconnect hint, to make sure
// that the network stack doesn't just re-use its connection to the test server.
IN_PROC_BROWSER_TEST_F(LoadingPredictorBrowserTest, PreconnectNonCors) {
GURL preconnect_url = embedded_test_server()->base_url();
std::string preconnect_content =
"<link rel=\"preconnect\" href=\"" + preconnect_url.spec() + "\">";
ui_test_utils::NavigateToURL(browser(),
GetDataURLWithContent(preconnect_content));
connection_tracker()->WaitUntilFirstConnectionAccepted();
EXPECT_EQ(1u, connection_tracker()->GetAcceptedSocketCount());
EXPECT_EQ(0u, connection_tracker()->GetReadSocketCount());
}
// Tests that preconnect warms up a socket connection to a test server,
// and that that socket is later used when fetching a resource.
// Note: This test uses a data URI to serve the preconnect hint, to make sure
// that the network stack doesn't just re-use its connection to the test server.
IN_PROC_BROWSER_TEST_F(LoadingPredictorBrowserTest, PreconnectAndFetchNonCors) {
GURL preconnect_url = embedded_test_server()->base_url();
// First navigation to content with a preconnect hint.
std::string preconnect_content =
"<link rel=\"preconnect\" href=\"" + preconnect_url.spec() + "\">";
ui_test_utils::NavigateToURL(browser(),
GetDataURLWithContent(preconnect_content));
connection_tracker()->WaitUntilFirstConnectionAccepted();
EXPECT_EQ(1u, connection_tracker()->GetAcceptedSocketCount());
EXPECT_EQ(0u, connection_tracker()->GetReadSocketCount());
// Second navigation to content with an img.
std::string img_content =
"<img src=\"" + preconnect_url.spec() + "test.gif\">";
ui_test_utils::NavigateToURL(browser(), GetDataURLWithContent(img_content));
connection_tracker()->WaitUntilFirstConnectionRead();
EXPECT_EQ(1u, connection_tracker()->GetAcceptedSocketCount());
EXPECT_EQ(1u, connection_tracker()->GetReadSocketCount());
}
// Tests that preconnect warms up a CORS connection to a test
// server, and that socket is later used when fetching a CORS resource.
// Note: This test uses a data URI to serve the preconnect hint, to make sure
// that the network stack doesn't just re-use its connection to the test server.
IN_PROC_BROWSER_TEST_F(LoadingPredictorBrowserTest, PreconnectAndFetchCors) {
GURL preconnect_url = embedded_test_server()->base_url();
// First navigation to content with a preconnect hint.
std::string preconnect_content = "<link rel=\"preconnect\" href=\"" +
preconnect_url.spec() + "\" crossorigin>";
ui_test_utils::NavigateToURL(browser(),
GetDataURLWithContent(preconnect_content));
connection_tracker()->WaitUntilFirstConnectionAccepted();
EXPECT_EQ(1u, connection_tracker()->GetAcceptedSocketCount());
EXPECT_EQ(0u, connection_tracker()->GetReadSocketCount());
// Second navigation to content with a font.
std::string font_content = "<script>var font = new FontFace('FontA', 'url(" +
preconnect_url.spec() +
"test.woff2)');font.load();</script>";
ui_test_utils::NavigateToURL(browser(), GetDataURLWithContent(font_content));
connection_tracker()->WaitUntilFirstConnectionRead();
EXPECT_EQ(1u, connection_tracker()->GetAcceptedSocketCount());
EXPECT_EQ(1u, connection_tracker()->GetReadSocketCount());
}
// Tests that preconnect warms up a non-CORS connection to a test
// server, but that socket is not used when fetching a CORS resource.
// Note: This test uses a data URI to serve the preconnect hint, to make sure
// that the network stack doesn't just re-use its connection to the test server.
IN_PROC_BROWSER_TEST_F(LoadingPredictorBrowserTest,
PreconnectNonCorsAndFetchCors) {
GURL preconnect_url = embedded_test_server()->base_url();
// First navigation to content with a preconnect hint.
std::string preconnect_content =
"<link rel=\"preconnect\" href=\"" + preconnect_url.spec() + "\">";
ui_test_utils::NavigateToURL(browser(),
GetDataURLWithContent(preconnect_content));
connection_tracker()->WaitUntilFirstConnectionAccepted();
EXPECT_EQ(1u, connection_tracker()->GetAcceptedSocketCount());
EXPECT_EQ(0u, connection_tracker()->GetReadSocketCount());
// Second navigation to content with a font.
std::string font_content = "<script>var font = new FontFace('FontA', 'url(" +
preconnect_url.spec() +
"test.woff2)');font.load();</script>";
ui_test_utils::NavigateToURL(browser(), GetDataURLWithContent(font_content));
connection_tracker()->WaitUntilFirstConnectionRead();
EXPECT_EQ(2u, connection_tracker()->GetAcceptedSocketCount());
EXPECT_EQ(1u, connection_tracker()->GetReadSocketCount());
}
// Tests that preconnect warms up a CORS connection to a test server,
// but that socket is not used when fetching a non-CORS resource.
// Note: This test uses a data URI to serve the preconnect hint, to make sure
// that the network stack doesn't just re-use its connection to the test server.
IN_PROC_BROWSER_TEST_F(LoadingPredictorBrowserTest,
PreconnectCorsAndFetchNonCors) {
GURL preconnect_url = embedded_test_server()->base_url();
// First navigation to content with a preconnect hint.
std::string preconnect_content = "<link rel=\"preconnect\" href=\"" +
preconnect_url.spec() + "\" crossorigin>";
ui_test_utils::NavigateToURL(browser(),
GetDataURLWithContent(preconnect_content));
connection_tracker()->WaitUntilFirstConnectionAccepted();
EXPECT_EQ(1u, connection_tracker()->GetAcceptedSocketCount());
EXPECT_EQ(0u, connection_tracker()->GetReadSocketCount());
// Second navigation to content with an img.
std::string img_content =
"<img src=\"" + preconnect_url.spec() + "test.gif\">";
ui_test_utils::NavigateToURL(browser(), GetDataURLWithContent(img_content));
connection_tracker()->WaitUntilFirstConnectionRead();
EXPECT_EQ(2u, connection_tracker()->GetAcceptedSocketCount());
EXPECT_EQ(1u, connection_tracker()->GetReadSocketCount());
}
class LoadingPredictorBrowserTestWithProxy
: public LoadingPredictorBrowserTest {
public:
void SetUp() override {
pac_script_server_ = std::make_unique<net::EmbeddedTestServer>();
pac_script_server_->AddDefaultHandlers(
base::FilePath(FILE_PATH_LITERAL("chrome/test/data")));
ASSERT_TRUE(pac_script_server_->InitializeAndListen());
LoadingPredictorBrowserTest::SetUp();
}
void SetUpOnMainThread() override {
LoadingPredictorBrowserTest::SetUpOnMainThread();
// This will make all dns requests fail.
host_resolver()->ClearRules();
pac_script_server_->StartAcceptingConnections();
}
void SetUpCommandLine(base::CommandLine* command_line) override {
GURL pac_url = pac_script_server_->GetURL(GetPathWithPortReplacement(
"/predictors/proxy.pac", embedded_test_server()->port()));
command_line->AppendSwitchASCII(switches::kProxyPacUrl, pac_url.spec());
}
private:
std::unique_ptr<net::EmbeddedTestServer> pac_script_server_;
};
IN_PROC_BROWSER_TEST_F(LoadingPredictorBrowserTestWithProxy,
PrepareForPageLoadWithoutPrediction) {
// Navigate the first time to fill the HTTP cache.
GURL url = embedded_test_server()->GetURL(
"test.com", GetPathWithPortReplacement(kHtmlSubresourcesPath,
embedded_test_server()->port()));
ui_test_utils::NavigateToURL(browser(), url);
ResetNetworkState();
ResetPredictorState();
auto observer = NavigateToURLAsync(url);
EXPECT_TRUE(observer->WaitForRequestStart());
preconnect_manager_observer()->WaitUntilProxyLookedUp(url);
EXPECT_TRUE(preconnect_manager_observer()->ProxyFound(url));
// We should preconnect only 2 sockets for the main frame host.
const size_t expected_connections = 2;
connection_tracker()->WaitForAcceptedConnections(expected_connections);
EXPECT_EQ(expected_connections,
connection_tracker()->GetAcceptedSocketCount());
// No reads since all resources should be cached.
EXPECT_EQ(0u, connection_tracker()->GetReadSocketCount());
}
IN_PROC_BROWSER_TEST_F(LoadingPredictorBrowserTestWithProxy,
PrepareForPageLoadWithPrediction) {
// Navigate the first time to fill the predictor's database and the HTTP
// cache.
GURL url = embedded_test_server()->GetURL(
"test.com", GetPathWithPortReplacement(kHtmlSubresourcesPath,
embedded_test_server()->port()));
ui_test_utils::NavigateToURL(browser(), url);
ResetNetworkState();
auto observer = NavigateToURLAsync(url);
EXPECT_TRUE(observer->WaitForRequestStart());
for (const auto& host : kHtmlSubresourcesHosts) {
GURL url = embedded_test_server()->GetURL(host, "/");
preconnect_manager_observer()->WaitUntilProxyLookedUp(url);
EXPECT_TRUE(preconnect_manager_observer()->ProxyFound(url));
}
// 2 connections to the main frame host + 1 connection per host for others.
const size_t expected_connections = base::size(kHtmlSubresourcesHosts) + 1;
connection_tracker()->WaitForAcceptedConnections(expected_connections);
EXPECT_EQ(expected_connections,
connection_tracker()->GetAcceptedSocketCount());
// No reads since all resources should be cached.
EXPECT_EQ(0u, connection_tracker()->GetReadSocketCount());
}
} // namespace predictors