blob: 99dfa660f6237a7a590795f61e42d84f503a252f [file] [log] [blame]
// Copyright 2018 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include <memory>
#include <string>
#include <vector>
#include "base/files/file_path.h"
#include "base/files/file_util.h"
#include "base/files/scoped_temp_dir.h"
#include "base/run_loop.h"
#include "base/strings/stringprintf.h"
#include "base/task/sequenced_task_runner.h"
#include "base/test/metrics/histogram_tester.h"
#include "base/threading/thread_restrictions.h"
#include "chrome/browser/download/download_prefs.h"
#include "chrome/browser/predictors/loading_predictor_config.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/browser/ui/tabs/tab_strip_model.h"
#include "chrome/common/pref_names.h"
#include "chrome/test/base/in_process_browser_test.h"
#include "chrome/test/base/ui_test_utils.h"
#include "components/metrics/content/subprocess_metrics_provider.h"
#include "components/prefs/pref_service.h"
#include "content/public/browser/web_contents_observer.h"
#include "content/public/test/browser_test.h"
#include "content/public/test/browser_test_utils.h"
#include "content/public/test/download_test_observer.h"
#include "content/public/test/test_navigation_observer.h"
#include "net/base/filename_util.h"
#include "net/base/net_errors.h"
#include "net/http/http_connection_info.h"
#include "net/test/embedded_test_server/controllable_http_response.h"
#include "net/test/embedded_test_server/embedded_test_server.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/blink/public/mojom/loader/resource_load_info.mojom.h"
#include "ui/base/window_open_disposition.h"
#include "url/gurl.h"
namespace content {
namespace {
// Used for the main frame, in subresource tests.
constexpr char kUninterestingMainFramePath[] =
"/uninteresting/main_frame/path/";
// Used for the "interesting" request individual tests focus on.
constexpr char kInterestingPath[] = "/interesting/path/";
enum class RequestType {
kMainFrame,
kSubFrame,
kImage,
kScript,
};
enum class HeadersReceived {
kHeadersReceived,
kNoHeadersReceived,
};
enum class NetworkAccessed {
kNetworkAccessed,
kNoNetworkAccessed,
};
// Utility class to wait until the main resource load is complete. This is to
// make sure, in the cancel tests, the main resource is fully loaded before the
// navigation is cancelled, to ensure the main frame load histograms are in a
// consistent state, and can be checked at the end of each test. To avoid races,
// create this class before starting a navigation.
class WaitForMainFrameResourceObserver : public content::WebContentsObserver {
public:
explicit WaitForMainFrameResourceObserver(WebContents* web_contents)
: content::WebContentsObserver(web_contents) {}
WaitForMainFrameResourceObserver(const WaitForMainFrameResourceObserver&) =
delete;
WaitForMainFrameResourceObserver& operator=(
const WaitForMainFrameResourceObserver&) = delete;
~WaitForMainFrameResourceObserver() override {}
// content::WebContentsObserver implementation:
void ResourceLoadComplete(
RenderFrameHost* render_frame_host,
const content::GlobalRequestID& request_id,
const blink::mojom::ResourceLoadInfo& resource_load_info) override {
EXPECT_EQ(network::mojom::RequestDestination::kDocument,
resource_load_info.request_destination);
EXPECT_EQ(net::OK, resource_load_info.net_error);
run_loop_.Quit();
}
void Wait() { run_loop_.Run(); }
private:
base::RunLoop run_loop_;
};
// This test fixture tests code in content/. The fixture itself is in chrome/
// because SubprocessMetricsProvider is a chrome-only test class.
class NetworkRequestMetricsBrowserTest
: public InProcessBrowserTest,
public testing::WithParamInterface<RequestType> {
public:
~NetworkRequestMetricsBrowserTest() override = default;
// ContentBrowserTest implementation:
void SetUpOnMainThread() override {
uninteresting_main_frame_response_ =
std::make_unique<net::test_server::ControllableHttpResponse>(
embedded_test_server(), kUninterestingMainFramePath);
interesting_http_response_ =
std::make_unique<net::test_server::ControllableHttpResponse>(
embedded_test_server(), kInterestingPath);
ASSERT_TRUE(embedded_test_server()->Start());
// Need to make this after test setup, to make sure the initial about:blank
// load is not counted.
histograms_ = std::make_unique<base::HistogramTester>();
}
// For non-RequestType::kMainFrame tests, returns the contents of the main
// frame, based on the RequestType.
std::string GetMainFrameContents(const std::string subresource_path) {
switch (GetParam()) {
case RequestType::kSubFrame:
return base::StringPrintf("<iframe src='%s'></iframe>",
subresource_path.c_str());
case RequestType::kImage:
return base::StringPrintf("<img src='%s'>", subresource_path.c_str());
case RequestType::kScript:
return base::StringPrintf("<script src='%s'></script>",
subresource_path.c_str());
case RequestType::kMainFrame:
NOTREACHED();
}
return std::string();
}
void StartNavigatingAndWaitForRequest() {
GURL interesting_url = embedded_test_server()->GetURL(kInterestingPath);
if (GetParam() == RequestType::kMainFrame) {
ui_test_utils::NavigateToURLWithDisposition(
browser(), interesting_url, WindowOpenDisposition::CURRENT_TAB,
ui_test_utils::BROWSER_TEST_NO_WAIT);
} else {
WaitForMainFrameResourceObserver wait_for_main_frame_resource_observer(
active_web_contents());
ui_test_utils::NavigateToURLWithDisposition(
browser(),
embedded_test_server()->GetURL(kUninterestingMainFramePath),
WindowOpenDisposition::CURRENT_TAB,
ui_test_utils::BROWSER_TEST_NO_WAIT);
uninteresting_main_frame_response_->WaitForRequest();
uninteresting_main_frame_response_->Send(
"HTTP/1.1 200 Peachy\r\n"
"Content-Type: text/html\r\n"
"\r\n");
uninteresting_main_frame_response_->Send(
GetMainFrameContents(interesting_url.spec()));
uninteresting_main_frame_response_->Done();
wait_for_main_frame_resource_observer.Wait();
}
interesting_http_response_->WaitForRequest();
}
net::test_server::ControllableHttpResponse* interesting_http_response() {
return interesting_http_response_.get();
}
// Checks all relevant histograms. |expected_net_error| is the expected result
// of the RequestType specified by the test parameter.
void CheckHistograms(int expected_net_error,
HeadersReceived headers_received,
NetworkAccessed network_accessed) {
// Some metrics may come from the renderer. This call ensures that those
// metrics are available.
metrics::SubprocessMetricsProvider::MergeHistogramDeltasForTesting();
if (GetParam() == RequestType::kMainFrame) {
histograms_->ExpectUniqueSample("Net.ErrorCodesForMainFrame4",
-expected_net_error, 1);
if (headers_received == HeadersReceived::kHeadersReceived) {
histograms_->ExpectUniqueSample("Net.ConnectionInfo.MainFrame",
net::HttpConnectionInfo::kHTTP1_1, 1);
} else {
histograms_->ExpectTotalCount("Net.ConnectionInfo.MainFrame", 0);
}
// Favicon may or may not have been loaded.
EXPECT_GE(
1u,
histograms_->GetAllSamples("Net.ErrorCodesForSubresources3").size());
EXPECT_GE(
1u,
histograms_->GetAllSamples("Net.ConnectionInfo.SubResource").size());
return;
}
// If not testing the main frame, there should also be just one result for
// the main frame.
histograms_->ExpectUniqueSample("Net.ErrorCodesForMainFrame4", -net::OK, 1);
// Some fuzziness here because of the favicon. It should typically succeed,
// but allow it to have been aborted, too, since the test server won't
// return a valid icon.
std::vector<base::Bucket> buckets =
histograms_->GetAllSamples("Net.ErrorCodesForSubresources3");
bool found_expected_load = false;
bool found_favicon_load = false;
for (auto& bucket : buckets) {
if (!found_expected_load && bucket.min == -expected_net_error) {
found_expected_load = true;
bucket.count--;
}
if (!found_favicon_load && bucket.count > 0 &&
(bucket.min == -net::OK || bucket.min == -net::ERR_ABORTED)) {
found_favicon_load = true;
bucket.count--;
}
EXPECT_EQ(0, bucket.count)
<< "Found unexpected load with result: " << bucket.min;
}
EXPECT_TRUE(found_expected_load);
// A subresource load requires a main frame load, which is only logged for
// network URLs.
if (network_accessed == NetworkAccessed::kNetworkAccessed) {
histograms_->ExpectUniqueSample("Net.ConnectionInfo.MainFrame",
net::HttpConnectionInfo::kHTTP1_1, 1);
if (headers_received == HeadersReceived::kHeadersReceived) {
// Favicon request may or may not have received a response.
size_t subresources =
histograms_->GetAllSamples("Net.ConnectionInfo.SubResource").size();
EXPECT_LE(1u, subresources);
EXPECT_GE(2u, subresources);
} else {
histograms_->ExpectTotalCount("Net.ConnectionInfo.SubResource", 0);
}
} else {
histograms_->ExpectTotalCount("Net.ConnectionInfo.MainFrame", 0);
histograms_->ExpectTotalCount("Net.ConnectionInfo.SubResource", 0);
}
}
// Checks all relevant histograms in the case the navigation is interrupted.
// The request identified by GetParam() is expected to fail with
// net::ERR_ABORTED.
void CheckHistogramsAfterMainFrameInterruption() {
// Some metrics may come from the renderer. These call ensures that those
// metrics are available.
FetchHistogramsFromChildProcesses();
metrics::SubprocessMetricsProvider::MergeHistogramDeltasForTesting();
if (GetParam() == RequestType::kMainFrame) {
// Can't check Net.ErrorCodesForSubresources3, due to the favicon, which
// Chrome may or may not have attempted to load.
histograms_->ExpectTotalCount("Net.ErrorCodesForMainFrame4", 1);
EXPECT_EQ(1, histograms_->GetBucketCount("Net.ErrorCodesForMainFrame4",
-net::ERR_ABORTED));
return;
}
histograms_->ExpectUniqueSample("Net.ErrorCodesForMainFrame4", -net::OK, 1);
// Some fuzziness here because of the favicon. It should typically succeed,
// but allow it to have been aborted, too, since the test server won't
// return a valid icon.
std::vector<base::Bucket> buckets =
histograms_->GetAllSamples("Net.ErrorCodesForSubresources3");
bool found_expected_load = false;
int found_favicon_loads = 0;
for (auto& bucket : buckets) {
if (!found_expected_load && bucket.min == -net::ERR_ABORTED) {
found_expected_load = true;
bucket.count--;
}
// Allow up to two favicon loads, one for the original page load, that was
// interrupted by a new load, and one for the new page load.
if (found_favicon_loads < 2 && bucket.count > 0 &&
(bucket.min == -net::OK || bucket.min == -net::ERR_ABORTED)) {
found_favicon_loads++;
bucket.count--;
}
EXPECT_EQ(0, bucket.count)
<< "Found unexpected load with result: " << bucket.min;
}
EXPECT_TRUE(found_expected_load);
}
// Send headers and a partial body to |interesting_http_response_|. Doesn't
// terminate the response, so the socket can either be closed, or the request
// aborted.
void SendHeadersPartialBody() {
// Sending a body that's too short will result in an error after all the
// bytes are read.
interesting_http_response_->Send("HTTP/1.1 200 Peachy\r\n");
// Send a MIME type to avoid MIME sniffing (Shouldn't matter, but it's one
// less thing to worry about).
if (GetParam() == RequestType::kImage) {
interesting_http_response_->Send("Content-Type: image/png\r\n");
} else if (GetParam() == RequestType::kScript) {
interesting_http_response_->Send("Content-Type: text/css\r\n");
} else {
interesting_http_response_->Send("Content-Type: text/html\r\n");
}
interesting_http_response_->Send("Content-Length: 50\r\n\r\n");
// This is the first byte of the PNG header, to avoid any chance of the
// request being cancelled for not looking like a PNG.
interesting_http_response_->Send("\x89");
}
content::WebContents* active_web_contents() {
return browser()->tab_strip_model()->GetActiveWebContents();
}
base::HistogramTester* histograms() { return histograms_.get(); }
private:
std::unique_ptr<net::test_server::ControllableHttpResponse>
uninteresting_main_frame_response_;
std::unique_ptr<net::test_server::ControllableHttpResponse>
interesting_http_response_;
std::unique_ptr<base::HistogramTester> histograms_;
};
// Testing before headers / during body is most interesting in the frame case,
// as it checks the before and after commit case, which follow very different
// paths.
IN_PROC_BROWSER_TEST_P(NetworkRequestMetricsBrowserTest,
NetErrorBeforeHeaders) {
TestNavigationObserver navigation_observer(active_web_contents(), 1);
StartNavigatingAndWaitForRequest();
interesting_http_response()->Send(
"HTTP/1.1 200 OK\r\nContent-Length: 42\r\nContent-Length: 43\r\n\r\n");
interesting_http_response()->Done();
navigation_observer.Wait();
CheckHistograms(net::ERR_RESPONSE_HEADERS_MULTIPLE_CONTENT_LENGTH,
HeadersReceived::kNoHeadersReceived,
NetworkAccessed::kNetworkAccessed);
}
IN_PROC_BROWSER_TEST_P(NetworkRequestMetricsBrowserTest, NetErrorDuringBody) {
TestNavigationObserver navigation_observer(active_web_contents(), 1);
StartNavigatingAndWaitForRequest();
SendHeadersPartialBody();
interesting_http_response()->Done();
navigation_observer.Wait();
CheckHistograms(net::ERR_CONTENT_LENGTH_MISMATCH,
HeadersReceived::kHeadersReceived,
NetworkAccessed::kNetworkAccessed);
}
IN_PROC_BROWSER_TEST_P(NetworkRequestMetricsBrowserTest, CancelBeforeHeaders) {
TestNavigationObserver navigation_observer(active_web_contents(), 1);
StartNavigatingAndWaitForRequest();
active_web_contents()->Stop();
navigation_observer.Wait();
CheckHistograms(net::ERR_ABORTED, HeadersReceived::kNoHeadersReceived,
NetworkAccessed::kNetworkAccessed);
}
IN_PROC_BROWSER_TEST_P(NetworkRequestMetricsBrowserTest, CancelDuringBody) {
TestNavigationObserver navigation_observer(active_web_contents(), 1);
StartNavigatingAndWaitForRequest();
SendHeadersPartialBody();
// Unfortunately, there's no way to ensure that the body has partially been
// received, so can only wait and hope. If the partial body hasn't been
// recieved by the time Stop() is called, the test should still pass, however.
base::RunLoop run_loop;
base::SequencedTaskRunner::GetCurrentDefault()->PostDelayedTask(
FROM_HERE, run_loop.QuitClosure(), base::Seconds(1));
run_loop.Run();
active_web_contents()->Stop();
navigation_observer.Wait();
CheckHistograms(net::ERR_ABORTED, HeadersReceived::kHeadersReceived,
NetworkAccessed::kNetworkAccessed);
}
IN_PROC_BROWSER_TEST_P(NetworkRequestMetricsBrowserTest,
InterruptedBeforeHeaders) {
TestNavigationObserver navigation_observer(active_web_contents(), 1);
StartNavigatingAndWaitForRequest();
// Stop navigation to record histograms.
active_web_contents()->Stop();
navigation_observer.Wait();
CheckHistogramsAfterMainFrameInterruption();
}
IN_PROC_BROWSER_TEST_P(NetworkRequestMetricsBrowserTest,
InterruptedCancelDuringBody) {
TestNavigationObserver navigation_observer(active_web_contents(), 1);
StartNavigatingAndWaitForRequest();
SendHeadersPartialBody();
// Unfortunately, there's no way to ensure that the body has partially been
// received, so can only wait and hope. If the partial body hasn't been
// recieved by the time Stop() is called, the test should still pass, however.
base::RunLoop run_loop;
base::SequencedTaskRunner::GetCurrentDefault()->PostDelayedTask(
FROM_HERE, run_loop.QuitClosure(), base::Seconds(1));
run_loop.Run();
// Stop navigation to record histograms.
active_web_contents()->Stop();
navigation_observer.Wait();
CheckHistogramsAfterMainFrameInterruption();
}
IN_PROC_BROWSER_TEST_P(NetworkRequestMetricsBrowserTest, SuccessWithBody) {
TestNavigationObserver navigation_observer(active_web_contents(), 1);
StartNavigatingAndWaitForRequest();
interesting_http_response()->Send("HTTP/1.1 200 Peachy\r\n\r\n");
interesting_http_response()->Send("Grapefruit");
interesting_http_response()->Done();
navigation_observer.Wait();
CheckHistograms(net::OK, HeadersReceived::kHeadersReceived,
NetworkAccessed::kNetworkAccessed);
}
IN_PROC_BROWSER_TEST_P(NetworkRequestMetricsBrowserTest, SuccessWithEmptyBody) {
TestNavigationObserver navigation_observer(active_web_contents(), 1);
StartNavigatingAndWaitForRequest();
interesting_http_response()->Send("HTTP/1.1 200 Peachy\r\n");
interesting_http_response()->Send("Content-Length: 0\r\n\r\n");
interesting_http_response()->Done();
navigation_observer.Wait();
CheckHistograms(net::OK, HeadersReceived::kHeadersReceived,
NetworkAccessed::kNetworkAccessed);
}
// Downloads should not be logged (Either as successes or failures).
IN_PROC_BROWSER_TEST_P(NetworkRequestMetricsBrowserTest, Download) {
// Only frames can be converted to downloads.
if (GetParam() != RequestType::kMainFrame &&
GetParam() != RequestType::kSubFrame) {
return;
}
browser()->profile()->GetPrefs()->SetInteger(
prefs::kDownloadRestrictions,
static_cast<int>(DownloadPrefs::DownloadRestriction::ALL_FILES));
browser()->profile()->GetPrefs()->SetBoolean(prefs::kPromptForDownload,
false);
// Need this to wait for the download to be fully cancelled to avoid a
// confirmation prompt on quit.
DownloadTestObserverTerminal download_test_observer_terminal(
browser()->profile()->GetDownloadManager(), 1,
DownloadTestObserver::ON_DANGEROUS_DOWNLOAD_IGNORE);
TestNavigationObserver navigation_observer(active_web_contents(), 1);
StartNavigatingAndWaitForRequest();
interesting_http_response()->Send("HTTP/1.1 200 Peachy\r\n");
interesting_http_response()->Send(
"Content-Type: binary/octet-stream\r\n\r\n");
interesting_http_response()->Send("\x01");
interesting_http_response()->Done();
navigation_observer.Wait();
download_test_observer_terminal.WaitForFinished();
// Some metrics may come from the renderer. This call ensures that those
// metrics are available.
metrics::SubprocessMetricsProvider::MergeHistogramDeltasForTesting();
if (GetParam() == RequestType::kMainFrame) {
histograms()->ExpectTotalCount("Net.ErrorCodesForMainFrame4", 0);
histograms()->ExpectTotalCount("Net.ConnectionInfo.MainFrame", 0);
// Favicon may or may not have been loaded.
EXPECT_GE(
1u,
histograms()->GetAllSamples("Net.ErrorCodesForSubresources3").size());
EXPECT_GE(
1u,
histograms()->GetAllSamples("Net.ConnectionInfo.SubResource").size());
return;
}
// If not testing the main frame, there should also be just one result for
// the main frame.
histograms()->ExpectUniqueSample("Net.ErrorCodesForMainFrame4", -net::OK, 1);
// Some fuzziness here because of the favicon. It should typically succeed,
// but allow it to have been aborted, too, since the test server won't
// return a valid icon.
std::vector<base::Bucket> buckets =
histograms()->GetAllSamples("Net.ErrorCodesForSubresources3");
bool found_favicon_load = false;
for (auto& bucket : buckets) {
if (!found_favicon_load && bucket.count > 0 &&
(bucket.min == -net::OK || bucket.min == -net::ERR_ABORTED)) {
found_favicon_load = true;
bucket.count--;
}
EXPECT_EQ(0, bucket.count)
<< "Found unexpected load with result: " << bucket.min;
}
histograms()->ExpectUniqueSample("Net.ConnectionInfo.MainFrame",
net::HttpConnectionInfo::kHTTP1_1, 1);
// Favicon request may or may not have received a response.
size_t subresources =
histograms()->GetAllSamples("Net.ConnectionInfo.SubResource").size();
EXPECT_LE(0u, subresources);
EXPECT_GE(1u, subresources);
}
// A few tests for file:// URLs, so that URLs not handled by the network service
// itself have some coverage.
IN_PROC_BROWSER_TEST_P(NetworkRequestMetricsBrowserTest, FileURLError) {
base::ScopedAllowBlockingForTesting allow_blocking;
base::ScopedTempDir temp_dir_;
ASSERT_TRUE(temp_dir_.CreateUniqueTempDir());
base::FilePath main_frame_path = temp_dir_.GetPath().AppendASCII("main.html");
if (GetParam() != RequestType::kMainFrame) {
std::string main_frame_data = GetMainFrameContents("subresource");
ASSERT_TRUE(base::WriteFile(main_frame_path, main_frame_data));
}
ASSERT_TRUE(ui_test_utils::NavigateToURL(
browser(), net::FilePathToFileURL(main_frame_path)));
CheckHistograms(net::ERR_FILE_NOT_FOUND, HeadersReceived::kNoHeadersReceived,
NetworkAccessed::kNoNetworkAccessed);
}
IN_PROC_BROWSER_TEST_P(NetworkRequestMetricsBrowserTest, FileURLSuccess) {
const char kSubresourcePath[] = "subresource";
base::ScopedAllowBlockingForTesting allow_blocking;
base::ScopedTempDir temp_dir_;
ASSERT_TRUE(temp_dir_.CreateUniqueTempDir());
base::FilePath main_frame_path = temp_dir_.GetPath().AppendASCII("main.html");
std::string main_frame_data = "foo";
if (GetParam() != RequestType::kMainFrame)
main_frame_data = GetMainFrameContents(kSubresourcePath);
ASSERT_TRUE(base::WriteFile(main_frame_path, main_frame_data));
if (GetParam() != RequestType::kMainFrame) {
std::string subresource_data = "foo";
ASSERT_TRUE(base::WriteFile(
temp_dir_.GetPath().AppendASCII(kSubresourcePath), subresource_data));
}
ASSERT_TRUE(ui_test_utils::NavigateToURL(
browser(), net::FilePathToFileURL(main_frame_path)));
CheckHistograms(net::OK, HeadersReceived::kNoHeadersReceived,
NetworkAccessed::kNoNetworkAccessed);
}
INSTANTIATE_TEST_SUITE_P(All,
NetworkRequestMetricsBrowserTest,
testing::Values(RequestType::kMainFrame,
RequestType::kSubFrame,
RequestType::kImage,
RequestType::kScript));
} // namespace
} // namespace content