blob: 44f8ac1d8b2af96a8541574afef44a84e14cc435 [file] [log] [blame]
// 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 "content/services/auction_worklet/auction_v8_helper.h"
#include <limits>
#include <string>
#include <vector>
#include "base/callback.h"
#include "base/strings/stringprintf.h"
#include "base/synchronization/lock.h"
#include "base/test/bind.h"
#include "base/test/task_environment.h"
#include "base/threading/sequenced_task_runner_handle.h"
#include "base/threading/thread_task_runner_handle.h"
#include "base/time/time.h"
#include "content/services/auction_worklet/public/mojom/bidder_worklet.mojom.h"
#include "content/services/auction_worklet/worklet_devtools_debug_test_util.h"
#include "content/services/auction_worklet/worklet_v8_debug_test_util.h"
#include "gin/converter.h"
#include "mojo/public/cpp/bindings/self_owned_receiver.h"
#include "testing/gmock/include/gmock/gmock-matchers.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/abseil-cpp/absl/types/optional.h"
#include "url/gurl.h"
#include "v8/include/v8-context.h"
#include "v8/include/v8-forward.h"
#include "v8/include/v8-wasm.h"
using testing::ElementsAre;
using testing::HasSubstr;
using testing::StartsWith;
namespace auction_worklet {
// The bytes of a minimal WebAssembly module, courtesy of
// v8/test/cctest/test-api-wasm.cc
const char kMinimalWasmModuleBytes[] = {0x00, 0x61, 0x73, 0x6d,
0x01, 0x00, 0x00, 0x00};
// ConnectDevToolsAgent takes an associated interface, which normally needs to
// be passed through a different pipe to be usable. The usual way of testing
// this is by using BindNewEndpointAndPassDedicatedReceiver to force creation
// of a new pipe. Unfortunately, this doesn't appear to be compatible with how
// our threads are setup. So instead, we emulate how this would normally be
// used: by call to a worklet, and just have a mock implementation that only
// supports ConnectDevToolsAgent.
class DebugConnector : public auction_worklet::mojom::BidderWorklet {
public:
// Expected to be run on V8 thread.
static void Create(
scoped_refptr<AuctionV8Helper> auction_v8_helper,
scoped_refptr<base::SequencedTaskRunner> mojo_thread,
scoped_refptr<AuctionV8Helper::DebugId> debug_id,
mojo::PendingReceiver<auction_worklet::mojom::BidderWorklet>
pending_receiver) {
DCHECK(auction_v8_helper->v8_runner()->RunsTasksInCurrentSequence());
auto instance = base::WrapUnique(
new DebugConnector(std::move(auction_v8_helper), std::move(mojo_thread),
std::move(debug_id)));
mojo::MakeSelfOwnedReceiver(std::move(instance),
std::move(pending_receiver));
}
void GenerateBid(
auction_worklet::mojom::BidderWorkletNonSharedParamsPtr
bidder_worklet_non_shared_params,
const absl::optional<std::string>& auction_signals_json,
const absl::optional<std::string>& per_buyer_signals_json,
const absl::optional<base::TimeDelta> per_buyer_timeout,
const url::Origin& browser_signal_seller_origin,
const absl::optional<url::Origin>& browser_signal_top_level_seller_origin,
auction_worklet::mojom::BiddingBrowserSignalsPtr bidding_browser_signals,
base::Time auction_start_time,
GenerateBidCallback generate_bid_callback) override {
ADD_FAILURE() << "GenerateBid shouldn't be called on DebugConnector";
}
void ReportWin(
const std::string& interest_group_name,
const absl::optional<std::string>& auction_signals_json,
const absl::optional<std::string>& per_buyer_signals_json,
const std::string& seller_signals_json,
const GURL& browser_signal_render_url,
double browser_signal_bid,
double browser_signal_highest_scoring_other_bid,
bool browser_signal_made_highest_scoring_other_bid,
const url::Origin& browser_signal_seller_origin,
const absl::optional<url::Origin>& browser_signal_top_level_seller_origin,
uint32_t bidding_data_version,
bool has_biding_data_version,
ReportWinCallback report_win_callback) override {
ADD_FAILURE() << "ReportWin shouldn't be called on DebugConnector";
}
void SendPendingSignalsRequests() override {
ADD_FAILURE()
<< "SendPendingSignalsRequests shouldn't be called on DebugConnector";
}
void ConnectDevToolsAgent(
mojo::PendingAssociatedReceiver<blink::mojom::DevToolsAgent>
agent_receiver) override {
auction_v8_helper_->ConnectDevToolsAgent(std::move(agent_receiver),
mojo_thread_, *debug_id_);
}
private:
DebugConnector(scoped_refptr<AuctionV8Helper> auction_v8_helper,
scoped_refptr<base::SequencedTaskRunner> mojo_thread,
scoped_refptr<AuctionV8Helper::DebugId> debug_id)
: auction_v8_helper_(std::move(auction_v8_helper)),
mojo_thread_(std::move(mojo_thread)),
debug_id_(std::move(debug_id)) {}
scoped_refptr<AuctionV8Helper> auction_v8_helper_;
scoped_refptr<base::SequencedTaskRunner> mojo_thread_;
scoped_refptr<AuctionV8Helper::DebugId> debug_id_;
};
class AuctionV8HelperTest : public testing::Test {
public:
explicit AuctionV8HelperTest(
base::test::TaskEnvironment::TimeSource time_mode =
base::test::TaskEnvironment::TimeSource::SYSTEM_TIME)
: task_environment_(time_mode) {
helper_ = AuctionV8Helper::Create(base::ThreadTaskRunnerHandle::Get());
// Here since we're using the same thread for everything, we need to spin
// the event loop to let AuctionV8Helper finish initializing "off-thread";
// normally PostTask semantics will ensure that anything that uses it on its
// thread would happen after such initialization.
base::RunLoop().RunUntilIdle();
v8_scope_ =
std::make_unique<AuctionV8Helper::FullIsolateScope>(helper_.get());
}
~AuctionV8HelperTest() override = default;
void CompileAndRunScriptOnV8Thread(
scoped_refptr<AuctionV8Helper::DebugId> debug_id,
const std::string& function_name,
const GURL& url,
const std::string& body,
bool expect_success = true,
base::OnceClosure done = base::OnceClosure(),
int* result_out = nullptr) {
DCHECK(debug_id);
helper_->v8_runner()->PostTask(
FROM_HERE,
base::BindOnce(
[](scoped_refptr<AuctionV8Helper> helper,
scoped_refptr<AuctionV8Helper::DebugId> debug_id,
std::string function_name, GURL url, std::string body,
bool expect_success, base::OnceClosure done, int* result_out) {
AuctionV8Helper::FullIsolateScope isolate_scope(helper.get());
v8::Local<v8::UnboundScript> script;
{
v8::Context::Scope ctx(helper->scratch_context());
absl::optional<std::string> error_msg;
ASSERT_TRUE(
helper->Compile(body, url, debug_id.get(), error_msg)
.ToLocal(&script));
EXPECT_FALSE(error_msg.has_value());
}
v8::Local<v8::Context> context = helper->CreateContext();
std::vector<std::string> error_msgs;
v8::Context::Scope ctx(context);
v8::Local<v8::Value> result;
// This is here since it needs to be before RunScript() ---
// doing it before Compile() doesn't work.
helper->MaybeTriggerInstrumentationBreakpoint(*debug_id, "start");
helper->MaybeTriggerInstrumentationBreakpoint(*debug_id,
"start2");
bool success =
helper
->RunScript(context, script, debug_id.get(),
function_name,
base::span<v8::Local<v8::Value>>(),
/*script_timeout=*/absl::nullopt, error_msgs)
.ToLocal(&result);
EXPECT_EQ(expect_success, success);
if (result_out) {
// If the caller wants to look at *result_out (including to see
// if it's unchanged), the done callback must be used to be
// sure that the read is performed after this sequence is
// complete.
CHECK(!done.is_null());
if (success) {
ASSERT_TRUE(gin::ConvertFromV8(helper->isolate(), result,
result_out));
}
}
if (!done.is_null())
std::move(done).Run();
},
helper_, std::move(debug_id), function_name, url, body,
expect_success, std::move(done), result_out));
}
bool CompileWasmOnV8ThreadAndWait(
scoped_refptr<AuctionV8Helper::DebugId> debug_id,
const GURL& url,
const std::string& body,
absl::optional<std::string>* error_out) {
bool success = false;
base::RunLoop run_loop;
helper_->v8_runner()->PostTask(
FROM_HERE,
base::BindOnce(
[](scoped_refptr<AuctionV8Helper> helper,
scoped_refptr<AuctionV8Helper::DebugId> debug_id, GURL url,
std::string body, bool* success_out,
absl::optional<std::string>* error_out, base::OnceClosure done) {
AuctionV8Helper::FullIsolateScope isolate_scope(helper.get());
v8::Context::Scope ctx(helper->scratch_context());
*success_out =
!helper->CompileWasm(body, url, debug_id.get(), *error_out)
.IsEmpty();
std::move(done).Run();
},
helper_, std::move(debug_id), url, body, &success, error_out,
run_loop.QuitClosure()));
run_loop.Run();
return success;
}
mojo::Remote<auction_worklet::mojom::BidderWorklet> ConnectToDevToolsAgent(
scoped_refptr<AuctionV8Helper::DebugId> debug_id,
mojo::PendingAssociatedReceiver<blink::mojom::DevToolsAgent>
agent_receiver) {
DCHECK(debug_id);
mojo::Remote<auction_worklet::mojom::BidderWorklet> connector_pipe;
helper_->v8_runner()->PostTask(
FROM_HERE, base::BindOnce(&DebugConnector::Create, helper_,
base::SequencedTaskRunnerHandle::Get(),
std::move(debug_id),
connector_pipe.BindNewPipeAndPassReceiver()));
connector_pipe->ConnectDevToolsAgent(std::move(agent_receiver));
return connector_pipe;
}
protected:
base::test::TaskEnvironment task_environment_;
scoped_refptr<AuctionV8Helper> helper_;
std::unique_ptr<AuctionV8Helper::FullIsolateScope> v8_scope_;
};
// Compile a script with the scratch context, and then run it in two different
// contexts.
TEST_F(AuctionV8HelperTest, Basic) {
v8::Local<v8::UnboundScript> script;
{
v8::Context::Scope ctx(helper_->scratch_context());
absl::optional<std::string> error_msg;
ASSERT_TRUE(helper_
->Compile("function foo() { return 1;}",
GURL("https://foo.test/"),
/*debug_id=*/nullptr, error_msg)
.ToLocal(&script));
EXPECT_FALSE(error_msg.has_value());
}
for (v8::Local<v8::Context> context :
{helper_->scratch_context(), helper_->CreateContext()}) {
std::vector<std::string> error_msgs;
v8::Context::Scope ctx(context);
v8::Local<v8::Value> result;
ASSERT_TRUE(helper_
->RunScript(context, script,
/*debug_id=*/nullptr, "foo",
base::span<v8::Local<v8::Value>>(),
/*script_timeout=*/absl::nullopt, error_msgs)
.ToLocal(&result));
int int_result = 0;
ASSERT_TRUE(gin::ConvertFromV8(helper_->isolate(), result, &int_result));
EXPECT_EQ(1, int_result);
EXPECT_TRUE(error_msgs.empty());
}
}
// Check that timing out scripts works.
TEST_F(AuctionV8HelperTest, Timeout) {
struct HangingScript {
const char* script;
bool top_level_hangs;
};
const HangingScript kHangingScripts[] = {
// Script that times out when run. Its foo() method returns 1, but should
// never be called.
{R"(function foo() { return 1;}
while(1);)",
true},
// Script that times out when foo() is called.
{"function foo() {while (1);}", false},
// Script that times out when run and when foo is called.
{R"(function foo() {while (1);}
while(1);)",
true}};
struct Timeouts {
absl::optional<base::TimeDelta> script_timeout;
base::TimeDelta default_timeout;
bool test_default_timeout;
};
const Timeouts kTimeouts[] = {
// Test default timeout. Use a shorter default timeout so test runs
// faster.
{absl::nullopt, base::Milliseconds(20), true},
// Test `script_timeout` parameter of AuctionV8Helper::RunScript(). Use a
// very long default timeout, so that we know the parameter worked if the
// script timed out.
{base::Milliseconds(20), base::Days(100), false}};
for (const Timeouts& timeout : kTimeouts) {
helper_->set_script_timeout_for_testing(timeout.default_timeout);
for (const HangingScript& hanging_script : kHangingScripts) {
base::TimeTicks start_time = base::TimeTicks::Now();
v8::Local<v8::Context> context = helper_->CreateContext();
v8::Context::Scope context_scope(context);
v8::Local<v8::UnboundScript> script;
absl::optional<std::string> compile_error;
ASSERT_TRUE(helper_
->Compile(hanging_script.script,
GURL("https://foo.test/"),
/*debug_id=*/nullptr, compile_error)
.ToLocal(&script));
EXPECT_EQ(compile_error, absl::nullopt);
std::vector<std::string> error_msgs;
v8::MaybeLocal<v8::Value> result =
helper_->RunScript(context, script, /*debug_id=*/nullptr, "foo",
base::span<v8::Local<v8::Value>>(),
timeout.script_timeout, error_msgs);
EXPECT_TRUE(result.IsEmpty());
EXPECT_THAT(
error_msgs,
ElementsAre(hanging_script.top_level_hangs
? "https://foo.test/ top-level execution timed out."
: "https://foo.test/ execution of `foo` timed out."));
base::TimeDelta time_passed = timeout.test_default_timeout
? timeout.default_timeout
: timeout.script_timeout.value();
// Make sure at least `time_passed` has passed, allowing for some time
// skew between change in base::TimeTicks::Now() and the timeout. This
// mostly serves to make sure the script timed out, instead of immediately
// terminating.
EXPECT_GE(base::TimeTicks::Now() - start_time,
time_passed - base::Milliseconds(10));
}
// Make sure it's still possible to run a script with the isolate after the
// timeouts.
v8::Local<v8::Context> context = helper_->CreateContext();
v8::Context::Scope context_scope(context);
v8::Local<v8::UnboundScript> script;
absl::optional<std::string> compile_error;
ASSERT_TRUE(helper_
->Compile("function foo() { return 1;}",
GURL("https://foo.test/"),
/*debug_id=*/nullptr, compile_error)
.ToLocal(&script));
EXPECT_EQ(compile_error, absl::nullopt);
std::vector<std::string> error_msgs;
v8::Local<v8::Value> result;
ASSERT_TRUE(helper_
->RunScript(context, script,
/*debug_id=*/nullptr, "foo",
base::span<v8::Local<v8::Value>>(),
/*script_timeout=*/absl::nullopt, error_msgs)
.ToLocal(&result));
EXPECT_TRUE(error_msgs.empty());
int int_result = 0;
ASSERT_TRUE(gin::ConvertFromV8(helper_->isolate(), result, &int_result));
EXPECT_EQ(1, int_result);
}
}
// Make sure the when CreateContext() is used, there's no access to the time,
// which mitigates Specter-style attacks.
TEST_F(AuctionV8HelperTest, NoTime) {
v8::Local<v8::Context> context = helper_->CreateContext();
v8::Context::Scope context_scope(context);
// Make sure Date() is not accessible.
v8::Local<v8::UnboundScript> script;
absl::optional<std::string> compile_error;
ASSERT_TRUE(helper_
->Compile("function foo() { return Date();}",
GURL("https://foo.test/"),
/*debug_id=*/nullptr, compile_error)
.ToLocal(&script));
EXPECT_FALSE(compile_error.has_value());
std::vector<std::string> error_msgs;
EXPECT_TRUE(helper_
->RunScript(context, script,
/*debug_id=*/nullptr, "foo",
base::span<v8::Local<v8::Value>>(),
/*script_timeout=*/absl::nullopt, error_msgs)
.IsEmpty());
ASSERT_EQ(1u, error_msgs.size());
EXPECT_THAT(error_msgs[0], StartsWith("https://foo.test/:1"));
EXPECT_THAT(error_msgs[0], HasSubstr("ReferenceError"));
EXPECT_THAT(error_msgs[0], HasSubstr("Date"));
}
// A script that doesn't compile.
TEST_F(AuctionV8HelperTest, CompileError) {
v8::Local<v8::UnboundScript> script;
v8::Context::Scope ctx(helper_->scratch_context());
absl::optional<std::string> error_msg;
ASSERT_FALSE(helper_
->Compile("function foo() { ", GURL("https://foo.test/"),
/*debug_id=*/nullptr, error_msg)
.ToLocal(&script));
ASSERT_TRUE(error_msg.has_value());
EXPECT_THAT(error_msg.value(), StartsWith("https://foo.test/:1 "));
EXPECT_THAT(error_msg.value(), HasSubstr("SyntaxError"));
}
// Test for exception at runtime at top-level.
TEST_F(AuctionV8HelperTest, RunErrorTopLevel) {
v8::Local<v8::UnboundScript> script;
{
v8::Context::Scope ctx(helper_->scratch_context());
absl::optional<std::string> error_msg;
ASSERT_TRUE(helper_
->Compile("\n\nthrow new Error('I am an error');",
GURL("https://foo.test/"),
/*debug_id=*/nullptr, error_msg)
.ToLocal(&script));
EXPECT_FALSE(error_msg.has_value());
}
v8::Local<v8::Context> context = helper_->CreateContext();
std::vector<std::string> error_msgs;
v8::Context::Scope ctx(context);
v8::Local<v8::Value> result;
ASSERT_FALSE(helper_
->RunScript(context, script,
/*debug_id=*/nullptr, "foo",
base::span<v8::Local<v8::Value>>(),
/*script_timeout=*/absl::nullopt, error_msgs)
.ToLocal(&result));
EXPECT_THAT(
error_msgs,
ElementsAre("https://foo.test/:3 Uncaught Error: I am an error."));
}
// Test for when desired function isn't found
TEST_F(AuctionV8HelperTest, TargetFunctionNotFound) {
v8::Local<v8::UnboundScript> script;
{
v8::Context::Scope ctx(helper_->scratch_context());
absl::optional<std::string> error_msg;
ASSERT_TRUE(helper_
->Compile("function foo() { return 1;}",
GURL("https://foo.test/"),
/*debug_id=*/nullptr, error_msg)
.ToLocal(&script));
EXPECT_FALSE(error_msg.has_value());
}
v8::Local<v8::Context> context = helper_->CreateContext();
std::vector<std::string> error_msgs;
v8::Context::Scope ctx(context);
v8::Local<v8::Value> result;
ASSERT_FALSE(helper_
->RunScript(context, script,
/*debug_id=*/nullptr, "bar",
base::span<v8::Local<v8::Value>>(),
/*script_timeout=*/absl::nullopt, error_msgs)
.ToLocal(&result));
// This "not a function" and not "not found" since the lookup successfully
// returns `undefined`.
EXPECT_THAT(error_msgs,
ElementsAre("https://foo.test/ `bar` is not a function."));
}
TEST_F(AuctionV8HelperTest, TargetFunctionError) {
v8::Local<v8::UnboundScript> script;
{
v8::Context::Scope ctx(helper_->scratch_context());
absl::optional<std::string> error_msg;
ASSERT_TRUE(helper_
->Compile("function foo() { return notfound;}",
GURL("https://foo.test/"),
/*debug_id=*/nullptr, error_msg)
.ToLocal(&script));
EXPECT_FALSE(error_msg.has_value());
}
v8::Local<v8::Context> context = helper_->CreateContext();
std::vector<std::string> error_msgs;
v8::Context::Scope ctx(context);
v8::Local<v8::Value> result;
ASSERT_FALSE(helper_
->RunScript(context, script,
/*debug_id=*/nullptr, "foo",
base::span<v8::Local<v8::Value>>(),
/*script_timeout=*/absl::nullopt, error_msgs)
.ToLocal(&result));
ASSERT_EQ(1u, error_msgs.size());
EXPECT_THAT(error_msgs[0], StartsWith("https://foo.test/:1 "));
EXPECT_THAT(error_msgs[0], HasSubstr("ReferenceError"));
EXPECT_THAT(error_msgs[0], HasSubstr("notfound"));
}
TEST_F(AuctionV8HelperTest, ConsoleLog) {
// Console log output is reported by V8 via debugging channels.
// Need to use a separate thread for debugger stuff.
v8_scope_.reset();
helper_ = AuctionV8Helper::Create(AuctionV8Helper::CreateTaskRunner());
ScopedInspectorSupport inspector_support(helper_.get());
auto id = base::MakeRefCounted<AuctionV8Helper::DebugId>(helper_.get());
TestChannel* channel =
inspector_support.ConnectDebuggerSession(id->context_group_id());
channel->RunCommandAndWaitForResult(
1, "Runtime.enable", R"({"id":1,"method":"Runtime.enable","params":{}})");
channel->RunCommandAndWaitForResult(
2, "Debugger.enable",
R"({"id":2,"method":"Debugger.enable","params":{}})");
const char kScript[] = R"(
console.debug('debug is there');
function foo() {
console.log('can', 'log', 'multiple', 'things', true);
console.table('even table!');
}
)";
base::RunLoop run_loop;
CompileAndRunScriptOnV8Thread(id, "foo", GURL("https://foo.test/"), kScript,
/*expect_success=*/true,
run_loop.QuitClosure());
run_loop.Run();
{
TestChannel::Event message =
channel->WaitForMethodNotification("Runtime.consoleAPICalled");
const std::string* type = message.value.FindStringPath("params.type");
ASSERT_TRUE(type);
EXPECT_EQ("debug", *type);
const base::Value* args = message.value.FindListPath("params.args");
ASSERT_TRUE(args);
ASSERT_EQ(1u, args->GetListDeprecated().size());
EXPECT_EQ("string", *args->GetListDeprecated()[0].FindStringKey("type"));
EXPECT_EQ("debug is there",
*args->GetListDeprecated()[0].FindStringKey("value"));
const base::Value* stack_trace =
message.value.FindListPath("params.stackTrace.callFrames");
ASSERT_EQ(1u, stack_trace->GetListDeprecated().size());
EXPECT_EQ(
"", *stack_trace->GetListDeprecated()[0].FindStringKey("functionName"));
EXPECT_EQ("https://foo.test/",
*stack_trace->GetListDeprecated()[0].FindStringKey("url"));
EXPECT_EQ(1, *stack_trace->GetListDeprecated()[0].FindIntKey("lineNumber"));
}
{
TestChannel::Event message =
channel->WaitForMethodNotification("Runtime.consoleAPICalled");
const std::string* type = message.value.FindStringPath("params.type");
ASSERT_TRUE(type);
EXPECT_EQ("log", *type);
const base::Value* args = message.value.FindListPath("params.args");
ASSERT_TRUE(args);
ASSERT_EQ(5u, args->GetListDeprecated().size());
EXPECT_EQ("string", *args->GetListDeprecated()[0].FindStringKey("type"));
EXPECT_EQ("can", *args->GetListDeprecated()[0].FindStringKey("value"));
EXPECT_EQ("string", *args->GetListDeprecated()[1].FindStringKey("type"));
EXPECT_EQ("log", *args->GetListDeprecated()[1].FindStringKey("value"));
EXPECT_EQ("string", *args->GetListDeprecated()[2].FindStringKey("type"));
EXPECT_EQ("multiple", *args->GetListDeprecated()[2].FindStringKey("value"));
EXPECT_EQ("string", *args->GetListDeprecated()[3].FindStringKey("type"));
EXPECT_EQ("things", *args->GetListDeprecated()[3].FindStringKey("value"));
EXPECT_EQ("boolean", *args->GetListDeprecated()[4].FindStringKey("type"));
EXPECT_EQ(true, *args->GetListDeprecated()[4].FindBoolKey("value"));
const base::Value* stack_trace =
message.value.FindListPath("params.stackTrace.callFrames");
ASSERT_EQ(1u, stack_trace->GetListDeprecated().size());
EXPECT_EQ("foo", *stack_trace->GetListDeprecated()[0].FindStringKey(
"functionName"));
EXPECT_EQ("https://foo.test/",
*stack_trace->GetListDeprecated()[0].FindStringKey("url"));
EXPECT_EQ(4, *stack_trace->GetListDeprecated()[0].FindIntKey("lineNumber"));
}
{
TestChannel::Event message =
channel->WaitForMethodNotification("Runtime.consoleAPICalled");
const std::string* type = message.value.FindStringPath("params.type");
ASSERT_TRUE(type);
EXPECT_EQ("table", *type);
const base::Value* args = message.value.FindListPath("params.args");
ASSERT_TRUE(args);
ASSERT_EQ(1u, args->GetListDeprecated().size());
EXPECT_EQ("string", *args->GetListDeprecated()[0].FindStringKey("type"));
EXPECT_EQ("even table!",
*args->GetListDeprecated()[0].FindStringKey("value"));
const base::Value* stack_trace =
message.value.FindListPath("params.stackTrace.callFrames");
ASSERT_EQ(1u, stack_trace->GetListDeprecated().size());
EXPECT_EQ("foo", *stack_trace->GetListDeprecated()[0].FindStringKey(
"functionName"));
EXPECT_EQ("https://foo.test/",
*stack_trace->GetListDeprecated()[0].FindStringKey("url"));
EXPECT_EQ(5, *stack_trace->GetListDeprecated()[0].FindIntKey("lineNumber"));
}
id->AbortDebuggerPauses();
}
TEST_F(AuctionV8HelperTest, FormatScriptName) {
v8::Local<v8::UnboundScript> script;
v8::Context::Scope ctx(helper_->scratch_context());
absl::optional<std::string> error_msg;
ASSERT_TRUE(helper_
->Compile("function foo() { return 1;}",
GURL("https://foo.test:8443/foo.js?v=3"),
/*debug_id=*/nullptr, error_msg)
.ToLocal(&script));
EXPECT_EQ("https://foo.test:8443/foo.js?v=3",
helper_->FormatScriptName(script));
}
TEST_F(AuctionV8HelperTest, ContextIDs) {
int resume_callback_invocations = 0;
base::RepeatingClosure count_resume_callback_invocation =
base::BindLambdaForTesting([&]() { ++resume_callback_invocations; });
auto id1 = base::MakeRefCounted<AuctionV8Helper::DebugId>(helper_.get());
id1->SetResumeCallback(count_resume_callback_invocation);
EXPECT_GT(id1->context_group_id(), 0);
ASSERT_EQ(0, resume_callback_invocations);
// Invoking resume the first time invokes the callback.
helper_->Resume(id1->context_group_id());
ASSERT_EQ(1, resume_callback_invocations);
// Later invocations don't do anything.
helper_->Resume(id1->context_group_id());
ASSERT_EQ(1, resume_callback_invocations);
// ... including after free.
int save_id1 = id1->context_group_id();
id1->AbortDebuggerPauses();
id1.reset();
helper_->Resume(save_id1);
ASSERT_EQ(1, resume_callback_invocations);
// Or before allocation.
helper_->Resume(save_id1 + 1);
ASSERT_EQ(1, resume_callback_invocations);
// Try with free before Resume call, too.
auto id2 = base::MakeRefCounted<AuctionV8Helper::DebugId>(helper_.get());
id2->SetResumeCallback(count_resume_callback_invocation);
EXPECT_GT(id2->context_group_id(), 0);
ASSERT_EQ(1, resume_callback_invocations);
int save_id2 = id2->context_group_id();
id2->AbortDebuggerPauses();
id2.reset();
helper_->Resume(save_id2);
ASSERT_EQ(1, resume_callback_invocations);
// Rudimentary test that two live IDs aren't the same.
auto id3 = base::MakeRefCounted<AuctionV8Helper::DebugId>(helper_.get());
id3->SetResumeCallback(count_resume_callback_invocation);
auto id4 = base::MakeRefCounted<AuctionV8Helper::DebugId>(helper_.get());
id4->SetResumeCallback(count_resume_callback_invocation);
int save_id3 = id3->context_group_id();
int save_id4 = id4->context_group_id();
EXPECT_GT(save_id3, 0);
EXPECT_GT(save_id4, 0);
EXPECT_NE(save_id3, save_id4);
helper_->Resume(save_id4);
ASSERT_EQ(2, resume_callback_invocations);
helper_->Resume(save_id3);
ASSERT_EQ(3, resume_callback_invocations);
id3->AbortDebuggerPauses();
id4->AbortDebuggerPauses();
}
TEST_F(AuctionV8HelperTest, AllocWrap) {
// Check what the ID allocator does when numbers wrap around and collide.
auto id1 = base::MakeRefCounted<AuctionV8Helper::DebugId>(helper_.get());
EXPECT_GT(id1->context_group_id(), 0);
helper_->SetLastContextGroupIdForTesting(std::numeric_limits<int>::max());
auto id2 = base::MakeRefCounted<AuctionV8Helper::DebugId>(helper_.get());
// `id2` should be positive and distinct from `id1`.
EXPECT_GT(id2->context_group_id(), 0);
EXPECT_NE(id1->context_group_id(), id2->context_group_id());
id1->AbortDebuggerPauses();
id2->AbortDebuggerPauses();
}
TEST_F(AuctionV8HelperTest, DebuggerBasics) {
const char kScriptSrc[] = "function someFunction() { return 493043; }";
const char kFunctionName[] = "someFunction";
const char kURL[] = "https://foo.test/script.js";
// Need to use a separate thread for debugger stuff.
v8_scope_.reset();
helper_ = AuctionV8Helper::Create(AuctionV8Helper::CreateTaskRunner());
ScopedInspectorSupport inspector_support(helper_.get());
auto id = base::MakeRefCounted<AuctionV8Helper::DebugId>(helper_.get());
TestChannel* channel =
inspector_support.ConnectDebuggerSession(id->context_group_id());
channel->RunCommandAndWaitForResult(
1, "Runtime.enable", R"({"id":1,"method":"Runtime.enable","params":{}})");
channel->RunCommandAndWaitForResult(
2, "Debugger.enable",
R"({"id":2,"method":"Debugger.enable","params":{}})");
CompileAndRunScriptOnV8Thread(id, kFunctionName, GURL(kURL), kScriptSrc);
// Running a script in an ephemeral context produces a bunch of events.
// The first pair of context_created/destroyed is for the compilation.
TestChannel::Event context_created_event =
channel->WaitForMethodNotification("Runtime.executionContextCreated");
const std::string* name =
context_created_event.value.FindStringPath("params.context.name");
ASSERT_TRUE(name);
EXPECT_EQ(kURL, *name);
TestChannel::Event context_destroyed_event =
channel->WaitForMethodNotification("Runtime.executionContextDestroyed");
TestChannel::Event context_created2_event =
channel->WaitForMethodNotification("Runtime.executionContextCreated");
const std::string* name2 =
context_created2_event.value.FindStringPath("params.context.name");
ASSERT_TRUE(name2);
EXPECT_EQ(kURL, *name2);
TestChannel::Event script_parsed_event =
channel->WaitForMethodNotification("Debugger.scriptParsed");
const std::string* url =
script_parsed_event.value.FindStringPath("params.url");
ASSERT_TRUE(url);
EXPECT_EQ(kURL, *url);
const std::string* script_id =
script_parsed_event.value.FindStringPath("params.scriptId");
ASSERT_TRUE(script_id);
TestChannel::Event context_destroyed2_event =
channel->WaitForMethodNotification("Runtime.executionContextDestroyed");
// Can fetch the source code for a debugger using the ID from the scriptParsed
// command.
const char kGetScriptSourceTemplate[] = R"({
"id":3,
"method":"Debugger.getScriptSource",
"params":{"scriptId":"%s"}})";
TestChannel::Event source_response = channel->RunCommandAndWaitForResult(
3, "Debugger.getScriptSource",
base::StringPrintf(kGetScriptSourceTemplate, script_id->c_str()));
const std::string* parsed_src =
source_response.value.FindStringPath("result.scriptSource");
ASSERT_TRUE(parsed_src);
EXPECT_EQ(kScriptSrc, *parsed_src);
id->AbortDebuggerPauses();
}
TEST_F(AuctionV8HelperTest, DebugCompileError) {
const char kScriptSrc[] = "fuction someFunction() { return 493043; }";
const char kURL[] = "https://foo.test/script.js";
// Need to use a separate thread for debugger stuff.
v8_scope_.reset();
helper_ = AuctionV8Helper::Create(AuctionV8Helper::CreateTaskRunner());
ScopedInspectorSupport inspector_support(helper_.get());
auto id = base::MakeRefCounted<AuctionV8Helper::DebugId>(helper_.get());
TestChannel* channel =
inspector_support.ConnectDebuggerSession(id->context_group_id());
channel->RunCommandAndWaitForResult(
1, "Runtime.enable", R"({"id":1,"method":"Runtime.enable","params":{}})");
channel->RunCommandAndWaitForResult(
2, "Debugger.enable",
R"({"id":2,"method":"Debugger.enable","params":{}})");
helper_->v8_runner()->PostTask(
FROM_HERE,
base::BindOnce(
[](scoped_refptr<AuctionV8Helper> helper,
scoped_refptr<AuctionV8Helper::DebugId> debug_id, std::string url,
std::string body) {
AuctionV8Helper::FullIsolateScope isolate_scope(helper.get());
v8::Local<v8::UnboundScript> script;
{
v8::Context::Scope ctx(helper->scratch_context());
absl::optional<std::string> error_msg;
ASSERT_FALSE(
helper->Compile(body, GURL(url), debug_id.get(), error_msg)
.ToLocal(&script));
}
},
helper_, id, kURL, kScriptSrc));
// Get events for context and error.
TestChannel::Event context_created_event =
channel->WaitForMethodNotification("Runtime.executionContextCreated");
TestChannel::Event parse_error_event =
channel->WaitForMethodNotification("Debugger.scriptFailedToParse");
TestChannel::Event context_destroyed_event =
channel->WaitForMethodNotification("Runtime.executionContextDestroyed");
id->AbortDebuggerPauses();
}
TEST_F(AuctionV8HelperTest, DevToolsDebuggerBasics) {
const char kSession[] = "123-456";
const char kScript[] = R"(
var multiplier = 2;
function compute() {
return multiplier * 3;
}
)";
for (bool use_binary_protocol : {false, true}) {
SCOPED_TRACE(use_binary_protocol);
// Need to use a separate thread for debugger stuff.
v8_scope_.reset();
helper_ = AuctionV8Helper::Create(AuctionV8Helper::CreateTaskRunner());
auto id = base::MakeRefCounted<AuctionV8Helper::DebugId>(helper_.get());
mojo::AssociatedRemote<blink::mojom::DevToolsAgent> agent_remote;
auto connector = ConnectToDevToolsAgent(
id, agent_remote.BindNewEndpointAndPassReceiver());
TestDevToolsAgentClient debug_client(std::move(agent_remote), kSession,
use_binary_protocol);
debug_client.RunCommandAndWaitForResult(
TestDevToolsAgentClient::Channel::kMain, 1, "Runtime.enable",
R"({"id":1,"method":"Runtime.enable","params":{}})");
debug_client.RunCommandAndWaitForResult(
TestDevToolsAgentClient::Channel::kMain, 2, "Debugger.enable",
R"({"id":2,"method":"Debugger.enable","params":{}})");
const char kBreakpointCommand[] = R"({
"id":3,
"method":"Debugger.setBreakpointByUrl",
"params": {
"lineNumber": 2,
"url": "https://example.com/test.js",
"columnNumber": 0,
"condition": ""
}})";
debug_client.RunCommandAndWaitForResult(
TestDevToolsAgentClient::Channel::kMain, 3,
"Debugger.setBreakpointByUrl", kBreakpointCommand);
int result = -1;
base::RunLoop result_run_loop;
CompileAndRunScriptOnV8Thread(
id, "compute", GURL("https://example.com/test.js"), kScript,
/*expect_success=*/true, result_run_loop.QuitClosure(), &result);
// Eat completion from parsing.
debug_client.WaitForMethodNotification("Runtime.executionContextDestroyed");
TestDevToolsAgentClient::Event script_parsed =
debug_client.WaitForMethodNotification("Debugger.scriptParsed");
const std::string* url = script_parsed.value.FindStringPath("params.url");
ASSERT_TRUE(url);
EXPECT_EQ(*url, "https://example.com/test.js");
absl::optional<int> context_id =
script_parsed.value.FindIntPath("params.executionContextId");
ASSERT_TRUE(context_id.has_value());
// Wait for breakpoint to hit.
TestDevToolsAgentClient::Event breakpoint_hit =
debug_client.WaitForMethodNotification("Debugger.paused");
base::Value* hit_breakpoints =
breakpoint_hit.value.FindListPath("params.hitBreakpoints");
ASSERT_TRUE(hit_breakpoints);
base::Value::ConstListView hit_breakpoints_list =
hit_breakpoints->GetListDeprecated();
ASSERT_EQ(1u, hit_breakpoints_list.size());
ASSERT_TRUE(hit_breakpoints_list[0].is_string());
EXPECT_EQ("1:2:0:https://example.com/test.js",
hit_breakpoints_list[0].GetString());
const char kCommandTemplate[] = R"({
"id": 4,
"method": "Runtime.evaluate",
"params": {
"expression": "multiplier = 10",
"contextId": %d
}
})";
// Change the state before resuming.
// Post-breakpoint params must be run on IO pipe, any main thread commands
// won't do things yet.
debug_client.RunCommandAndWaitForResult(
TestDevToolsAgentClient::Channel::kIO, 4, "Runtime.evaluate",
base::StringPrintf(kCommandTemplate, context_id.value()));
// Resume.
debug_client.RunCommandAndWaitForResult(
TestDevToolsAgentClient::Channel::kIO, 10, "Debugger.resume",
R"({"id":10,"method":"Debugger.resume","params":{}})");
// Wait for actual completion.
debug_client.WaitForMethodNotification("Runtime.executionContextDestroyed");
// Produced value changed by the write to `multiplier`.
result_run_loop.Run();
EXPECT_EQ(30, result);
id->AbortDebuggerPauses();
}
}
TEST_F(AuctionV8HelperTest, DevToolsAgentDebuggerInstrumentationBreakpoint) {
const char kSession[] = "123-456";
const char kScript[] = R"(
function compute() {
return 42;
}
)";
for (bool use_binary_protocol : {false, true}) {
for (bool use_multiple_breakpoints : {false, true}) {
std::string test_name =
std::string(use_binary_protocol ? "Binary " : "JSON ") +
(use_multiple_breakpoints ? "Multi" : "Single");
SCOPED_TRACE(test_name);
// Need to use a separate thread for debugger stuff.
v8_scope_.reset();
helper_ = AuctionV8Helper::Create(AuctionV8Helper::CreateTaskRunner());
auto id = base::MakeRefCounted<AuctionV8Helper::DebugId>(helper_.get());
mojo::AssociatedRemote<blink::mojom::DevToolsAgent> agent_remote;
auto connector = ConnectToDevToolsAgent(
id, agent_remote.BindNewEndpointAndPassReceiver());
TestDevToolsAgentClient debug_client(std::move(agent_remote), kSession,
use_binary_protocol);
debug_client.RunCommandAndWaitForResult(
TestDevToolsAgentClient::Channel::kMain, 1, "Runtime.enable",
R"({"id":1,"method":"Runtime.enable","params":{}})");
debug_client.RunCommandAndWaitForResult(
TestDevToolsAgentClient::Channel::kMain, 2, "Debugger.enable",
R"({"id":2,"method":"Debugger.enable","params":{}})");
debug_client.RunCommandAndWaitForResult(
TestDevToolsAgentClient::Channel::kMain, 3,
"EventBreakpoints.setInstrumentationBreakpoint",
MakeInstrumentationBreakpointCommand(3, "set", "start"));
debug_client.RunCommandAndWaitForResult(
TestDevToolsAgentClient::Channel::kMain, 4,
"EventBreakpoints.setInstrumentationBreakpoint",
MakeInstrumentationBreakpointCommand(4, "set", "start2"));
debug_client.RunCommandAndWaitForResult(
TestDevToolsAgentClient::Channel::kMain, 5,
"EventBreakpoints.setInstrumentationBreakpoint",
MakeInstrumentationBreakpointCommand(5, "set", "start3"));
if (!use_multiple_breakpoints) {
debug_client.RunCommandAndWaitForResult(
TestDevToolsAgentClient::Channel::kMain, 6,
"EventBreakpoints.removeInstrumentationBreakpoint",
MakeInstrumentationBreakpointCommand(6, "remove", "start2"));
}
int result = -1;
base::RunLoop result_run_loop;
CompileAndRunScriptOnV8Thread(
id, "compute", GURL("https://example.com/test.js"), kScript,
/*expect_success=*/true, result_run_loop.QuitClosure(), &result);
// Wait for the pause.
TestDevToolsAgentClient::Event breakpoint_hit =
debug_client.WaitForMethodNotification("Debugger.paused");
// Make sure we identify the event the way DevTools frontend expects.
if (use_multiple_breakpoints) {
// Expect both 'start' and 'start2' to hit, so the event will list both
// inside the 'data.reasons' list, and top-level 'reason' field to say
// 'ambiguous' to reflect it.
const std::string* reason =
breakpoint_hit.value.FindStringPath("params.reason");
ASSERT_TRUE(reason);
EXPECT_EQ("ambiguous", *reason);
const base::Value* reasons =
breakpoint_hit.value.FindListPath("params.data.reasons");
ASSERT_TRUE(reasons);
base::Value::ConstListView reasons_list = reasons->GetListDeprecated();
ASSERT_EQ(2u, reasons_list.size());
ASSERT_TRUE(reasons_list[0].is_dict());
ASSERT_TRUE(reasons_list[1].is_dict());
const std::string* ev1 =
reasons_list[0].FindStringPath("auxData.eventName");
const std::string* ev2 =
reasons_list[1].FindStringPath("auxData.eventName");
const std::string* r1 = reasons_list[0].FindStringPath("reason");
const std::string* r2 = reasons_list[1].FindStringPath("reason");
ASSERT_TRUE(ev1);
ASSERT_TRUE(ev2);
ASSERT_TRUE(r1);
ASSERT_TRUE(r2);
EXPECT_EQ("instrumentation:start", *ev1);
EXPECT_EQ("instrumentation:start2", *ev2);
EXPECT_EQ("EventListener", *r1);
EXPECT_EQ("EventListener", *r2);
} else {
// Here we expect 'start' to be the only event, since we remove
// 'start2', and 'start3' isn't checked by
// CompileAndRunScriptOnV8Thread.
EXPECT_FALSE(breakpoint_hit.value.FindPath("params.data.reasons"));
const std::string* reason =
breakpoint_hit.value.FindStringPath("params.reason");
ASSERT_TRUE(reason);
EXPECT_EQ("EventListener", *reason);
const std::string* event_name =
breakpoint_hit.value.FindStringPath("params.data.eventName");
ASSERT_TRUE(event_name);
EXPECT_EQ("instrumentation:start", *event_name);
}
// Resume.
debug_client.RunCommandAndWaitForResult(
TestDevToolsAgentClient::Channel::kIO, 10, "Debugger.resume",
R"({"id":10,"method":"Debugger.resume","params":{}})");
// Wait for result.
result_run_loop.Run();
EXPECT_EQ(42, result);
id->AbortDebuggerPauses();
}
}
}
TEST_F(AuctionV8HelperTest, DevToolsDebuggerInvalidCommand) {
const char kSession[] = "ABCD-EFGH";
for (bool use_binary_protocol : {false, true}) {
SCOPED_TRACE(use_binary_protocol);
// Need to use a separate thread for debugger stuff.
v8_scope_.reset();
helper_ = AuctionV8Helper::Create(AuctionV8Helper::CreateTaskRunner());
auto id = base::MakeRefCounted<AuctionV8Helper::DebugId>(helper_.get());
mojo::AssociatedRemote<blink::mojom::DevToolsAgent> agent_remote;
auto connector = ConnectToDevToolsAgent(
id, agent_remote.BindNewEndpointAndPassReceiver());
TestDevToolsAgentClient debug_client(std::move(agent_remote), kSession,
use_binary_protocol);
TestDevToolsAgentClient::Event result =
debug_client.RunCommandAndWaitForResult(
TestDevToolsAgentClient::Channel::kMain, 1, "NoSuchThing.enable",
R"({"id":1,"method":"NoSuchThing.enable","params":{}})");
EXPECT_TRUE(result.value.FindDictKey("error"));
id->AbortDebuggerPauses();
}
}
TEST_F(AuctionV8HelperTest, DevToolsDeleteSessionPipeLate) {
// Test that deleting session pipe after the agent is fine.
const char kSession[] = "ABCD-EFGH";
const bool use_binary_protocol = true;
v8_scope_.reset();
helper_ = AuctionV8Helper::Create(AuctionV8Helper::CreateTaskRunner());
auto id = base::MakeRefCounted<AuctionV8Helper::DebugId>(helper_.get());
mojo::AssociatedRemote<blink::mojom::DevToolsAgent> agent_remote;
auto connector =
ConnectToDevToolsAgent(id, agent_remote.BindNewEndpointAndPassReceiver());
TestDevToolsAgentClient debug_client(std::move(agent_remote), kSession,
use_binary_protocol);
task_environment_.RunUntilIdle();
id->AbortDebuggerPauses();
id.reset();
helper_.reset();
task_environment_.RunUntilIdle();
}
class MockTimeAuctionV8HelperTest : public AuctionV8HelperTest {
public:
MockTimeAuctionV8HelperTest()
: AuctionV8HelperTest(
base::test::TaskEnvironment::TimeSource::MOCK_TIME) {}
void Wait(base::TimeDelta wait_time) {
// We can't use TaskEnvironment::FastForwardBy since the v8 thread is
// blocked in debugger, so instead we post a task on the timer thread
// which then can be reasoned about with respect to the timeout.
base::RunLoop run_loop;
helper_->GetTimeoutTimerRunnerForTesting()->PostDelayedTask(
FROM_HERE, run_loop.QuitClosure(), wait_time);
task_environment_.AdvanceClock(wait_time);
run_loop.Run();
}
};
TEST_F(MockTimeAuctionV8HelperTest, TimelimitDebug) {
// Test that being paused on a breakpoint for a while doesn't trigger the
// execution time limit.
const char kSession[] = "123-456";
const char kScript[] = R"(
function compute() {
return 3;
}
)";
// Need to use a separate thread for debugger stuff.
v8_scope_.reset();
helper_ = AuctionV8Helper::Create(AuctionV8Helper::CreateTaskRunner());
auto id = base::MakeRefCounted<AuctionV8Helper::DebugId>(helper_.get());
mojo::AssociatedRemote<blink::mojom::DevToolsAgent> agent_remote;
auto connector =
ConnectToDevToolsAgent(id, agent_remote.BindNewEndpointAndPassReceiver());
TestDevToolsAgentClient debug_client(std::move(agent_remote), kSession,
/*use_binary_protocol=*/true);
debug_client.RunCommandAndWaitForResult(
TestDevToolsAgentClient::Channel::kMain, 1, "Runtime.enable",
R"({"id":1,"method":"Runtime.enable","params":{}})");
debug_client.RunCommandAndWaitForResult(
TestDevToolsAgentClient::Channel::kMain, 2, "Debugger.enable",
R"({"id":2,"method":"Debugger.enable","params":{}})");
const char kBreakpointCommand[] = R"({
"id":3,
"method":"Debugger.setBreakpointByUrl",
"params": {
"lineNumber": 0,
"url": "https://example.com/test.js",
"columnNumber": 0,
"condition": ""
}})";
debug_client.RunCommandAndWaitForResult(
TestDevToolsAgentClient::Channel::kMain, 3, "Debugger.setBreakpointByUrl",
kBreakpointCommand);
int result = -1;
base::RunLoop result_run_loop;
CompileAndRunScriptOnV8Thread(
id, "compute", GURL("https://example.com/test.js"), kScript,
/*expect_success=*/true, result_run_loop.QuitClosure(), &result);
// Wait for breakpoint to hit.
TestDevToolsAgentClient::Event breakpoint_hit =
debug_client.WaitForMethodNotification("Debugger.paused");
// Make sure more time has happened than the timeout.
Wait(2 * AuctionV8Helper::kScriptTimeout);
// Resume the script, it should still finish.
debug_client.RunCommandAndWaitForResult(
TestDevToolsAgentClient::Channel::kIO, 10, "Debugger.resume",
R"({"id":10,"method":"Debugger.resume","params":{}})");
result_run_loop.Run();
EXPECT_EQ(3, result);
id->AbortDebuggerPauses();
}
TEST_F(AuctionV8HelperTest, DebugTimeout) {
// Test that timeout still works after pausing in the debugger and resuming.
// Use a shorter timeout so test runs faster.
const base::TimeDelta kScriptTimeout = base::Milliseconds(20);
helper_->set_script_timeout_for_testing(kScriptTimeout);
const char kSession[] = "123-456";
const char kScript[] = R"(
var a = 42;
function compute() {
while (true) {}
}
)";
// Need to use a separate thread for debugger stuff.
v8_scope_.reset();
helper_ = AuctionV8Helper::Create(AuctionV8Helper::CreateTaskRunner());
auto id = base::MakeRefCounted<AuctionV8Helper::DebugId>(helper_.get());
mojo::AssociatedRemote<blink::mojom::DevToolsAgent> agent_remote;
auto connector =
ConnectToDevToolsAgent(id, agent_remote.BindNewEndpointAndPassReceiver());
TestDevToolsAgentClient debug_client(std::move(agent_remote), kSession,
/*use_binary_protocol=*/false);
debug_client.RunCommandAndWaitForResult(
TestDevToolsAgentClient::Channel::kMain, 1, "Runtime.enable",
R"({"id":1,"method":"Runtime.enable","params":{}})");
debug_client.RunCommandAndWaitForResult(
TestDevToolsAgentClient::Channel::kMain, 2, "Debugger.enable",
R"({"id":2,"method":"Debugger.enable","params":{}})");
const char kBreakpointCommand[] = R"({
"id":3,
"method":"Debugger.setBreakpointByUrl",
"params": {
"lineNumber": 0,
"url": "https://example.com/test.js",
"columnNumber": 0,
"condition": ""
}})";
debug_client.RunCommandAndWaitForResult(
TestDevToolsAgentClient::Channel::kMain, 3, "Debugger.setBreakpointByUrl",
kBreakpointCommand);
int result = -1;
base::RunLoop result_run_loop;
CompileAndRunScriptOnV8Thread(
id, "compute", GURL("https://example.com/test.js"), kScript,
/*expect_success=*/false, result_run_loop.QuitClosure(), &result);
// Wait for breakpoint to hit.
TestDevToolsAgentClient::Event breakpoint_hit =
debug_client.WaitForMethodNotification("Debugger.paused");
EXPECT_FALSE(result_run_loop.AnyQuitCalled());
// Resume the script, it should still timeout.
debug_client.RunCommandAndWaitForResult(
TestDevToolsAgentClient::Channel::kIO, 10, "Debugger.resume",
R"({"id":10,"method":"Debugger.resume","params":{}})");
result_run_loop.Run();
EXPECT_EQ(-1, result);
id->AbortDebuggerPauses();
}
TEST_F(AuctionV8HelperTest, CompileWasm) {
v8::Local<v8::Context> context = helper_->CreateContext();
v8::Context::Scope context_scope(context);
v8::Local<v8::WasmModuleObject> wasm_module;
absl::optional<std::string> compile_error;
ASSERT_TRUE(helper_
->CompileWasm(std::string(kMinimalWasmModuleBytes,
std::size(kMinimalWasmModuleBytes)),
GURL("https://foo.test/"),
/*debug_id=*/nullptr, compile_error)
.ToLocal(&wasm_module));
EXPECT_FALSE(compile_error.has_value());
}
TEST_F(AuctionV8HelperTest, CompileWasmError) {
v8::Local<v8::Context> context = helper_->CreateContext();
v8::Context::Scope context_scope(context);
v8::Local<v8::WasmModuleObject> wasm_module;
absl::optional<std::string> compile_error;
EXPECT_FALSE(helper_
->CompileWasm("not wasm", GURL("https://foo.test/"),
/*debug_id=*/nullptr, compile_error)
.ToLocal(&wasm_module));
ASSERT_TRUE(compile_error.has_value());
EXPECT_THAT(compile_error.value(), StartsWith("https://foo.test/ "));
EXPECT_THAT(compile_error.value(),
HasSubstr("Uncaught CompileError: WasmModuleObject::Compile"));
}
TEST_F(AuctionV8HelperTest, CompileWasmDebug) {
const char kSession[] = "123-456";
// Need to use a separate thread for debugger stuff.
v8_scope_.reset();
helper_ = AuctionV8Helper::Create(AuctionV8Helper::CreateTaskRunner());
auto id = base::MakeRefCounted<AuctionV8Helper::DebugId>(helper_.get());
mojo::AssociatedRemote<blink::mojom::DevToolsAgent> agent_remote;
auto connector =
ConnectToDevToolsAgent(id, agent_remote.BindNewEndpointAndPassReceiver());
TestDevToolsAgentClient debug_client(std::move(agent_remote), kSession,
/*use_binary_protocol=*/false);
debug_client.RunCommandAndWaitForResult(
TestDevToolsAgentClient::Channel::kMain, 1, "Runtime.enable",
R"({"id":1,"method":"Runtime.enable","params":{}})");
debug_client.RunCommandAndWaitForResult(
TestDevToolsAgentClient::Channel::kMain, 2, "Debugger.enable",
R"({"id":2,"method":"Debugger.enable","params":{}})");
absl::optional<std::string> error_out;
EXPECT_TRUE(CompileWasmOnV8ThreadAndWait(
id, GURL("https://example.com"),
std::string(kMinimalWasmModuleBytes, std::size(kMinimalWasmModuleBytes)),
&error_out));
TestDevToolsAgentClient::Event script_parsed =
debug_client.WaitForMethodNotification("Debugger.scriptParsed");
const std::string* lang =
script_parsed.value.FindStringPath("params.scriptLanguage");
ASSERT_TRUE(lang);
EXPECT_EQ(*lang, "WebAssembly");
debug_client.WaitForMethodNotification("Runtime.executionContextDestroyed");
id->AbortDebuggerPauses();
}
TEST_F(AuctionV8HelperTest, CloneWasmModule) {
// Test proper CloneWasmModule() usage to prevent state persistence via
// WASM Module objects.
const char kScript[] = R"(
function probe(moduleObject) {
var result = moduleObject.weirdField ? moduleObject.weirdField : -1;
moduleObject.weirdField = 5;
return result;
}
)";
v8::Local<v8::Context> context = helper_->CreateContext();
v8::Context::Scope context_scope(context);
// Compile the WASM module...
v8::Local<v8::WasmModuleObject> wasm_module;
absl::optional<std::string> error_msg;
ASSERT_TRUE(helper_
->CompileWasm(std::string(kMinimalWasmModuleBytes,
std::size(kMinimalWasmModuleBytes)),
GURL("https://foo.test/"),
/*debug_id=*/nullptr, error_msg)
.ToLocal(&wasm_module));
EXPECT_FALSE(error_msg.has_value());
// And the test script.
v8::Local<v8::UnboundScript> script;
ASSERT_TRUE(helper_
->Compile(kScript, GURL("https://foo.test/"),
/*debug_id=*/nullptr, error_msg)
.ToLocal(&script));
EXPECT_FALSE(error_msg.has_value());
// Run the script a couple of times passing in the same module.
std::vector<v8::Local<v8::Value>> args;
args.push_back(wasm_module);
v8::Local<v8::Value> result;
std::vector<std::string> error_msgs;
ASSERT_TRUE(helper_
->RunScript(context, script,
/*debug_id=*/nullptr, "probe", args,
/*script_timeout=*/absl::nullopt, error_msgs)
.ToLocal(&result));
EXPECT_TRUE(error_msgs.empty());
int int_result = 0;
ASSERT_TRUE(gin::ConvertFromV8(helper_->isolate(), result, &int_result));
EXPECT_EQ(-1, int_result);
ASSERT_TRUE(helper_
->RunScript(context, script,
/*debug_id=*/nullptr, "probe", args,
/*script_timeout=*/absl::nullopt, error_msgs)
.ToLocal(&result));
EXPECT_TRUE(error_msgs.empty());
ASSERT_TRUE(gin::ConvertFromV8(helper_->isolate(), result, &int_result));
EXPECT_EQ(5, int_result);
// Nothing stick arounds if CloneWasmModule is consistently used, however.
args[0] = helper_->CloneWasmModule(wasm_module).ToLocalChecked();
ASSERT_TRUE(helper_
->RunScript(context, script,
/*debug_id=*/nullptr, "probe", args,
/*script_timeout=*/absl::nullopt, error_msgs)
.ToLocal(&result));
EXPECT_TRUE(error_msgs.empty());
ASSERT_TRUE(gin::ConvertFromV8(helper_->isolate(), result, &int_result));
EXPECT_EQ(-1, int_result);
args[0] = helper_->CloneWasmModule(wasm_module).ToLocalChecked();
ASSERT_TRUE(helper_
->RunScript(context, script,
/*debug_id=*/nullptr, "probe", args,
/*script_timeout=*/absl::nullopt, error_msgs)
.ToLocal(&result));
EXPECT_TRUE(error_msgs.empty());
ASSERT_TRUE(gin::ConvertFromV8(helper_->isolate(), result, &int_result));
EXPECT_EQ(-1, int_result);
}
} // namespace auction_worklet