Skip to content

Commit

Permalink
Add an FYI bot test to check for changes in canvas op digests.
Browse files Browse the repository at this point in the history
This test will not run on waterfall (it's intended for FYI bots and Git
bisection). The test runs a fingerprinting script contained in a file
specified by a command-line switch. The test always logs the
fingerprinting script output (the value passed to the test from
JavaScript with window.domAutomationController.send()).

Another optional command line switch specifies a test expectation -- if
absent, the test doesn't check the output (but logging still occurs).

To avoid running on waterfall, the test is disabled. The
--gtest_also_run_disabled_tests and
--gtest_filter="*CanvasInputKeyBrowserTest*" command line switches can
be used to force the test to run.

Bug: 973801
Change-Id: I048810288045c2bb6f05427d62b787289c1003a1
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2699107
Reviewed-by: Asanka Herath <asanka@chromium.org>
Commit-Queue: Caleb Raitto <caraitto@chromium.org>
Cr-Commit-Position: refs/heads/master@{#859091}
  • Loading branch information
caraitto authored and Chromium LUCI CQ committed Mar 2, 2021
1 parent 74f239b commit f9f4d73
Show file tree
Hide file tree
Showing 2 changed files with 171 additions and 1 deletion.
5 changes: 4 additions & 1 deletion chrome/browser/privacy_budget/BUILD.gn
Expand Up @@ -67,7 +67,10 @@ source_set("unit_tests") {
source_set("browser_tests") {
testonly = true

sources = [ "privacy_budget_browsertest.cc" ]
sources = [
"canvas_input_key_browsertest.cc",
"privacy_budget_browsertest.cc",
]

defines = [ "HAS_OUT_OF_PROC_TEST_RUNNER" ]

Expand Down
167 changes: 167 additions & 0 deletions chrome/browser/privacy_budget/canvas_input_key_browsertest.cc
@@ -0,0 +1,167 @@
// Copyright 2021 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 "build/build_config.h"
#include "chrome/common/privacy_budget/scoped_privacy_budget_config.h"
#include "chrome/test/base/chrome_test_utils.h"
#include "components/ukm/test_ukm_recorder.h"
#include "content/public/test/browser_test.h"
#include "content/public/test/browser_test_utils.h"
#include "content/public/test/test_utils.h"
#include "services/metrics/public/cpp/ukm_builders.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/blink/public/common/privacy_budget/identifiable_surface.h"

#if defined(OS_ANDROID)
#include "chrome/test/base/android/android_browser_test.h"
#else
#include "chrome/test/base/in_process_browser_test.h"
#endif

namespace {

using base::StringToInt64;
using base::StringToUint64;
using blink::IdentifiableSurface;

constexpr char kFingerprintingScriptUrlSwitch[] = "fingerprinting-script-url";
constexpr char kFingerprintExpectationSwitch[] = "fingerprint-expectation";
constexpr char kInputKeyExpectationSwitch[] = "input-key-expectation";
constexpr char kValueExpectationSwitch[] = "value-expectation";

// NOTE: This test is *disabled* so that it doesn't run on waterfall -- to run
// the test, invoke the test binary as follows:
//
// testing/xvfb.py out/Default/browser_tests --gtest_also_run_disabled_tests
// --gtest_filter="*CanvasInputKeyBrowserTest*"
// --fingerprinting-script-url="file URL goes here"
// [--fingerprint-expectation="optional expected fingerprint goes here"]
// [--input-key-expectation="optional key expectation goes here"]
// [--value-expectation="optional value expectation goes here"]
//
// The --fingerprinting-script-url must resolve to an HTML page that runs a
// script that calls window.domAutomationController.send() with the computed
// fingerprint.
//
// This test runs on Android as well as desktop platforms.
class DISABLED_CanvasInputKeyBrowserTest : public PlatformBrowserTest {
public:
DISABLED_CanvasInputKeyBrowserTest() {
privacy_budget_config_.Apply(test::ScopedPrivacyBudgetConfig::Parameters());
}

content::WebContents* web_contents() {
return chrome_test_utils::GetActiveWebContents(this);
}

void SetUpCommandLine(base::CommandLine* command_line) override {
fingerprinting_script_url_ =
command_line->GetSwitchValueASCII(kFingerprintingScriptUrlSwitch);
fingerprint_expectation_ =
command_line->GetSwitchValueASCII(kFingerprintExpectationSwitch);
input_key_expectation_ =
command_line->GetSwitchValueASCII(kInputKeyExpectationSwitch);
value_expectation_ =
command_line->GetSwitchValueASCII(kValueExpectationSwitch);
}

void SetUpOnMainThread() override {
ukm_recorder_ = std::make_unique<ukm::TestAutoSetUkmRecorder>();
}

ukm::TestUkmRecorder& recorder() { return *ukm_recorder_; }

protected:
std::string fingerprinting_script_url_;
std::string fingerprint_expectation_;
std::string input_key_expectation_;
std::string value_expectation_;

test::ScopedPrivacyBudgetConfig privacy_budget_config_;
std::unique_ptr<ukm::TestAutoSetUkmRecorder> ukm_recorder_;
};

struct MetricKeyValue {
uint64_t input_key;
int64_t value;
};

// Verify that there's only one entry of type |type|, and return the the
// |input_key|, |value| pair.
template <typename MapType>
base::Optional<MetricKeyValue> ExtractKeyOfType(IdentifiableSurface::Type type,
const MapType& metrics) {
MetricKeyValue last_result = {};
for (const auto& pair : metrics) {
auto surface = IdentifiableSurface::FromMetricHash(pair.first);
if (surface.GetType() == type) {
if (last_result.input_key != 0) {
ADD_FAILURE() << "Saw at least 2 surfaces of type "
<< static_cast<uint64_t>(type)
<< ". First input hash: " << last_result.input_key
<< " second input hash: " << surface.GetInputHash();
return base::nullopt;
}
last_result.input_key = surface.GetInputHash();
last_result.value = pair.second;
}
}
return last_result;
}

IN_PROC_BROWSER_TEST_F(DISABLED_CanvasInputKeyBrowserTest,
TestCanvasFingerprint) {
ASSERT_TRUE(embedded_test_server()->Start());
content::DOMMessageQueue messages;
base::RunLoop run_loop;

recorder().SetOnAddEntryCallback(ukm::builders::Identifiability::kEntryName,
run_loop.QuitClosure());

ASSERT_TRUE(
content::NavigateToURL(web_contents(), GURL(fingerprinting_script_url_)));

// The document computes the canvas fingerprint and sends a message back to
// the test. Receipt of the message indicates that the script successfully
// completed.
std::string fingerprint;
ASSERT_TRUE(messages.WaitForMessage(&fingerprint));
// Navigating away from the test page causes the document to be unloaded. That
// will cause any buffered metrics to be flushed.
content::NavigateToURLBlockUntilNavigationsComplete(web_contents(),
GURL("about:blank"), 1);
// Wait for the metrics to come down the pipe.
content::RunAllTasksUntilIdle();
run_loop.Run();
auto merged_entries = recorder().GetMergedEntriesByName(
ukm::builders::Identifiability::kEntryName);
// Shouldn't be more than one source here. If this changes, then we'd need to
// adjust this test to deal.
ASSERT_EQ(1u, merged_entries.size());

base::Optional<MetricKeyValue> canvas_key_value =
ExtractKeyOfType(IdentifiableSurface::Type::kCanvasReadback,
merged_entries.begin()->second->metrics);
ASSERT_TRUE(canvas_key_value);

LOG(INFO) << "Canvas fingerprint is: " << fingerprint;
LOG(INFO) << "Input key is: " << canvas_key_value->input_key;
LOG(INFO) << "Value is: " << canvas_key_value->value;

if (!fingerprint_expectation_.empty())
EXPECT_EQ(fingerprint_expectation_, fingerprint);
if (!input_key_expectation_.empty()) {
uint64_t parsed_input_key_expectation;
EXPECT_TRUE(
StringToUint64(input_key_expectation_, &parsed_input_key_expectation));
EXPECT_EQ(parsed_input_key_expectation, canvas_key_value->input_key);
}
if (!value_expectation_.empty()) {
int64_t parsed_value_expectation;
EXPECT_TRUE(StringToInt64(value_expectation_, &parsed_value_expectation));
EXPECT_EQ(parsed_value_expectation, canvas_key_value->value);
}
}

} // namespace

0 comments on commit f9f4d73

Please sign in to comment.