Files
react-native/ReactCommon/hermes/inspector/chrome/tests/ConnectionTests.cpp
T
Will Holen 452cb9a78a Add support for Debugger.runIfWaitingForDebugger
Summary:
This call is used to continue execution when the app has just been
started in a "wait for debugger" mode. This is the only case
in which it has an effect.

Notably, it should do nothing in the following cases, which a layperson
may be tempted to classify as "WaitingForDebugger":

* The app was running detached and hit a 'debugger;' statement
* The app is paused because of a breakpoint or hitting the Pause button
* The app stopped on an instrumentation breakpoint, and expects
  the debugger to collect data and potentially auto-resume.

Changelog: [Internal] Add Hermes support for Debugger.runIfWaitingForDebugger

Reviewed By: mhorowitz

Differential Revision: D21557446

fbshipit-source-id: 790cec7444ddc61908d2ef9d92e4649b535d678f
2020-05-15 12:42:38 -07:00

2396 lines
75 KiB
C++

/*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
#include "AsyncHermesRuntime.h"
#include "SyncConnection.h"
#include <chrono>
#include <initializer_list>
#include <iostream>
#include <limits>
#include <folly/Conv.h>
#include <folly/Unit.h>
#include <folly/dynamic.h>
#include <folly/futures/Future.h>
#include <folly/json.h>
#include <gtest/gtest.h>
#include <hermes/DebuggerAPI.h>
#include <hermes/hermes.h>
#include <hermes/inspector/chrome/MessageTypes.h>
namespace facebook {
namespace hermes {
namespace inspector {
namespace chrome {
namespace m = ::facebook::hermes::inspector::chrome::message;
using namespace std::chrono_literals;
namespace {
// This class mostly exists to call AsyncHermesRuntime::wait() before we
// destruct either AsyncHermesRuntime or SyncConnection.
//
// The reason for this is that we need to make sure the runtime is finished
// executing scripts before we destruct the debugger connection. Otherwise, if
// we destruct the connection while scripts are still executing, the script
// could perform an action (like hitting a breakpoint) that sends a message to
// the already-deallocated connection.
class TestContext {
public:
TestContext(bool waitForDebugger = false)
: conn_(runtime_.runtime(), waitForDebugger) {}
~TestContext() {
runtime_.wait();
}
AsyncHermesRuntime &runtime() {
return runtime_;
}
SyncConnection &conn() {
return conn_;
}
private:
AsyncHermesRuntime runtime_;
SyncConnection conn_;
};
template <typename ResponseType>
ResponseType expectResponse(SyncConnection &conn, int id) {
ResponseType resp;
conn.waitForResponse([id, &resp](const std::string &str) {
resp = ResponseType(folly::parseJson(str));
EXPECT_EQ(resp.id, id);
});
return resp;
}
template <typename NotificationType>
NotificationType expectNotification(SyncConnection &conn) {
NotificationType note;
conn.waitForNotification([&note](const std::string &str) {
std::string parseError;
try {
note = NotificationType(folly::parseJson(str));
} catch (const std::exception &e) {
parseError = e.what();
parseError += " (json: " + str + ")";
}
EXPECT_EQ(parseError, "");
});
return note;
}
class UnexpectedNotificationException : public std::runtime_error {
public:
UnexpectedNotificationException()
: std::runtime_error("unexpected notification") {}
};
void expectNothing(SyncConnection &conn) {
auto promise = std::make_shared<folly::Promise<folly::Unit>>();
auto future = promise->getFuture();
try {
conn.waitForNotification([promise](const std::string &str) {
// if we receive a value fail the promise
promise->setException(UnexpectedNotificationException());
});
} catch (...) {
// if no values are received it times out with an exception
// so we can say that we've succeeded at seeing nothing
promise->setValue();
}
// timeout is 2500 milliseconds in SyncConnection::waitForNotification
// so this value is mostly a redundant safety measure
std::move(future).get(3000ms);
}
struct FrameInfo {
FrameInfo(const std::string &functionName, int lineNumber, int scopeCount)
: functionName(functionName),
lineNumberMin(lineNumber),
lineNumberMax(lineNumber),
scopeCount(scopeCount),
columnNumber(debugger::kInvalidLocation) {}
FrameInfo &setLineNumberMax(int lineNumberMaxParam) {
lineNumberMax = lineNumberMaxParam;
return *this;
}
FrameInfo &setScriptId(const std::string &scriptIdParam) {
scriptId = scriptIdParam;
return *this;
}
FrameInfo &setColumnNumber(int columnNumberParam) {
columnNumber = columnNumberParam;
return *this;
}
std::string functionName;
int lineNumberMin;
int lineNumberMax;
int scopeCount;
int columnNumber;
std::string scriptId;
};
void expectCallFrames(
const std::vector<m::debugger::CallFrame> &frames,
const std::vector<FrameInfo> &infos) {
EXPECT_EQ(frames.size(), infos.size());
int i = 0;
for (const FrameInfo &info : infos) {
m::debugger::CallFrame frame = frames[i];
EXPECT_EQ(frame.callFrameId, folly::to<std::string>(i));
EXPECT_EQ(frame.functionName, info.functionName);
EXPECT_GE(frame.location.lineNumber, info.lineNumberMin);
EXPECT_LE(frame.location.lineNumber, info.lineNumberMax);
if (info.columnNumber != debugger::kInvalidLocation) {
EXPECT_EQ(frame.location.columnNumber, info.columnNumber);
}
if (info.scriptId.size() > 0) {
EXPECT_EQ(frame.location.scriptId, info.scriptId);
}
// TODO: make expectation more specific once Hermes gives us something other
// than kInvalidBreakpoint for the file id
EXPECT_FALSE(frame.location.scriptId.empty());
if (info.scopeCount > 0) {
EXPECT_EQ(frame.scopeChain.size(), info.scopeCount);
for (int j = 0; j < info.scopeCount; j++) {
EXPECT_TRUE(frame.scopeChain[j].object.objectId.hasValue());
}
}
i++;
}
}
// Helper to send a request with no params and wait for a response (defaults
// to empty) containing the req id.
template <typename RequestType, typename ResponseType = m::OkResponse>
ResponseType send(SyncConnection &conn, int id) {
RequestType req;
req.id = id;
conn.send(req.toJson());
return expectResponse<ResponseType>(conn, id);
}
void sendRuntimeEvalRequest(
SyncConnection &conn,
int id,
const std::string &expression) {
m::runtime::EvaluateRequest req;
req.id = id;
req.expression = expression;
conn.send(req.toJson());
}
void sendEvalRequest(
SyncConnection &conn,
int id,
int callFrameId,
const std::string &expression) {
m::debugger::EvaluateOnCallFrameRequest req;
req.id = id;
req.callFrameId = folly::to<std::string>(callFrameId);
req.expression = expression;
conn.send(req.toJson());
}
m::runtime::ExecutionContextCreatedNotification expectExecutionContextCreated(
SyncConnection &conn) {
auto note =
expectNotification<m::runtime::ExecutionContextCreatedNotification>(conn);
EXPECT_EQ(note.context.id, 1);
EXPECT_EQ(note.context.origin, "");
EXPECT_EQ(note.context.name, "hermes");
return note;
}
m::debugger::ScriptParsedNotification expectScriptParsed(
SyncConnection &conn,
const std::string &url,
const std::string &sourceMapURL) {
auto note = expectNotification<m::debugger::ScriptParsedNotification>(conn);
EXPECT_EQ(note.url, url);
EXPECT_GT(note.scriptId.size(), 0);
if (sourceMapURL.empty()) {
EXPECT_FALSE(note.sourceMapURL.hasValue());
} else {
EXPECT_EQ(note.sourceMapURL.value(), sourceMapURL);
}
return note;
}
m::debugger::PausedNotification expectPaused(
SyncConnection &conn,
const std::string &reason,
const std::vector<FrameInfo> &infos) {
auto note = expectNotification<m::debugger::PausedNotification>(conn);
EXPECT_EQ(note.reason, reason);
expectCallFrames(note.callFrames, infos);
// TODO: check breakpoint location for pause once hermes gives that to us
return note;
}
m::debugger::BreakpointId expectBreakpointResponse(
SyncConnection &conn,
int id,
int line,
int resolvedLine) {
auto resp = expectResponse<m::debugger::SetBreakpointByUrlResponse>(conn, id);
EXPECT_EQ(resp.id, id);
EXPECT_FALSE(resp.breakpointId.empty());
EXPECT_NE(
resp.breakpointId,
folly::to<std::string>(facebook::hermes::debugger::kInvalidBreakpoint));
if (line == -1) {
EXPECT_EQ(resp.locations.size(), 0);
} else {
EXPECT_EQ(resp.locations.size(), 1);
EXPECT_EQ(resp.locations[0].lineNumber, resolvedLine);
}
return resp.breakpointId;
}
void expectEvalResponse(
SyncConnection &conn,
int id,
const char *expectedValue) {
auto resp =
expectResponse<m::debugger::EvaluateOnCallFrameResponse>(conn, id);
EXPECT_EQ(resp.id, id);
EXPECT_EQ(resp.result.type, "string");
EXPECT_EQ(resp.result.value, expectedValue);
EXPECT_FALSE(resp.exceptionDetails.hasValue());
}
void expectEvalResponse(SyncConnection &conn, int id, bool expectedValue) {
auto resp =
expectResponse<m::debugger::EvaluateOnCallFrameResponse>(conn, id);
EXPECT_EQ(resp.id, id);
EXPECT_EQ(resp.result.type, "boolean");
EXPECT_EQ(resp.result.value, expectedValue);
EXPECT_FALSE(resp.exceptionDetails.hasValue());
}
void expectEvalResponse(SyncConnection &conn, int id, int expectedValue) {
auto resp =
expectResponse<m::debugger::EvaluateOnCallFrameResponse>(conn, id);
EXPECT_EQ(resp.id, id);
EXPECT_EQ(resp.result.type, "number");
EXPECT_EQ(resp.result.value, expectedValue);
EXPECT_FALSE(resp.exceptionDetails.hasValue());
}
void expectEvalException(
SyncConnection &conn,
int id,
const std::string &exceptionText,
const std::vector<FrameInfo> infos) {
auto resp =
expectResponse<m::debugger::EvaluateOnCallFrameResponse>(conn, id);
EXPECT_EQ(resp.id, id);
EXPECT_TRUE(resp.exceptionDetails.hasValue());
m::runtime::ExceptionDetails &details = resp.exceptionDetails.value();
EXPECT_EQ(details.text, exceptionText);
// TODO: Hermes doesn't seem to populate the line number for the exception?
EXPECT_EQ(details.lineNumber, 0);
EXPECT_TRUE(details.stackTrace.hasValue());
m::runtime::StackTrace &stackTrace = details.stackTrace.value();
EXPECT_EQ(stackTrace.callFrames.size(), infos.size());
int i = 0;
for (const FrameInfo &info : infos) {
const m::runtime::CallFrame &callFrame = stackTrace.callFrames[i];
EXPECT_GE(callFrame.lineNumber, info.lineNumberMin);
EXPECT_LE(callFrame.lineNumber, info.lineNumberMax);
EXPECT_EQ(callFrame.functionName, info.functionName);
i++;
}
}
struct PropInfo {
PropInfo(const std::string &type) : type(type) {}
PropInfo &setSubtype(const std::string &subtypeParam) {
subtype = subtypeParam;
return *this;
}
PropInfo &setValue(const folly::dynamic &valueParam) {
value = valueParam;
return *this;
}
PropInfo &setUnserializableValue(
const std::string &unserializableValueParam) {
unserializableValue = unserializableValueParam;
return *this;
}
std::string type;
folly::Optional<std::string> subtype;
folly::Optional<folly::dynamic> value;
folly::Optional<std::string> unserializableValue;
};
std::unordered_map<std::string, std::string> expectProps(
SyncConnection &conn,
int msgId,
const std::string &objectId,
const std::unordered_map<std::string, PropInfo> &infos,
bool ownProperties = true) {
m::runtime::GetPropertiesRequest req;
req.id = msgId;
req.objectId = objectId;
req.ownProperties = ownProperties;
conn.send(req.toJson());
std::unordered_map<std::string, std::string> objectIds;
auto resp = expectResponse<m::runtime::GetPropertiesResponse>(conn, msgId);
EXPECT_EQ(resp.result.size(), infos.size());
for (int i = 0; i < resp.result.size(); i++) {
m::runtime::PropertyDescriptor &desc = resp.result[i];
auto infoIt = infos.find(desc.name);
EXPECT_FALSE(infoIt == infos.end());
if (infoIt != infos.end()) {
const PropInfo &info = infoIt->second;
EXPECT_TRUE(desc.value.hasValue());
m::runtime::RemoteObject &remoteObj = desc.value.value();
EXPECT_EQ(remoteObj.type, info.type);
if (info.subtype.hasValue()) {
EXPECT_TRUE(remoteObj.subtype.hasValue());
EXPECT_EQ(remoteObj.subtype.value(), info.subtype.value());
}
if (info.value.hasValue()) {
EXPECT_TRUE(remoteObj.value.hasValue());
EXPECT_EQ(remoteObj.value.value(), info.value.value());
}
if (info.unserializableValue.hasValue()) {
EXPECT_TRUE(remoteObj.unserializableValue.hasValue());
EXPECT_EQ(
remoteObj.unserializableValue.value(),
info.unserializableValue.value());
}
if ((info.type == "object" && info.subtype != "null") ||
info.type == "function") {
EXPECT_TRUE(remoteObj.objectId.hasValue());
objectIds[desc.name] = remoteObj.objectId.value();
}
}
}
return objectIds;
}
void expectEvalResponse(
SyncConnection &conn,
int id,
const std::unordered_map<std::string, PropInfo> &infos) {
auto resp =
expectResponse<m::debugger::EvaluateOnCallFrameResponse>(conn, id);
EXPECT_EQ(resp.id, id);
EXPECT_EQ(resp.result.type, "object");
EXPECT_FALSE(resp.exceptionDetails.hasValue());
EXPECT_TRUE(resp.result.objectId.hasValue());
expectProps(conn, id + 1, resp.result.objectId.value(), infos);
}
} // namespace
TEST(ConnectionTests, testRespondsOkToUnknownRequests) {
TestContext context;
AsyncHermesRuntime &asyncRuntime = context.runtime();
SyncConnection &conn = context.conn();
asyncRuntime.executeScriptAsync(R"(
var a = 1 + 2;
var b = a / 2;
)");
send<m::debugger::EnableRequest>(conn, 1);
expectExecutionContextCreated(conn);
expectNotification<m::debugger::ScriptParsedNotification>(conn);
conn.send(R"({"id": 2, "method": "Debugger.foo"})");
conn.send(R"({"id": 3, "method": "Debugger.bar", "params": {"a": "b"}})");
expectResponse<m::OkResponse>(conn, 2);
expectResponse<m::OkResponse>(conn, 3);
}
TEST(ConnectionTests, testDebuggerStatement) {
TestContext context;
AsyncHermesRuntime &asyncRuntime = context.runtime();
SyncConnection &conn = context.conn();
int msgId = 1;
asyncRuntime.executeScriptAsync(R"(
var a = 1 + 2;
debugger; // [1] (line 2) hit debugger statement, resume
var b = a / 2;
)");
send<m::debugger::EnableRequest>(conn, msgId++);
expectExecutionContextCreated(conn);
expectNotification<m::debugger::ScriptParsedNotification>(conn);
// [1] (line 2) hit debugger statement, resume
expectPaused(conn, "other", {{"global", 2, 1}});
send<m::debugger::ResumeRequest>(conn, msgId++);
expectNotification<m::debugger::ResumedNotification>(conn);
}
TEST(ConnectionTests, testDebuggerStatementFromPausedWaitEnable) {
TestContext context;
AsyncHermesRuntime &asyncRuntime = context.runtime();
SyncConnection &conn = context.conn();
int msgId = 1;
asyncRuntime.executeScriptAsync(R"(
var a = 1 + 2;
debugger; // [1] (line 2) hit debugger statement, resume
var b = a / 2;
)");
// TODO: hack that gives JS the chance to run so that we end up in the
// PausedWaitEnable state. Will move the entire test to InspectorTests once
// I get around to refactoring InspectorTests.
std::this_thread::sleep_for(250ms);
send<m::debugger::EnableRequest>(conn, msgId++);
expectExecutionContextCreated(conn);
expectNotification<m::debugger::ScriptParsedNotification>(conn);
// [1] (line 2) hit debugger statement, resume
expectPaused(conn, "other", {{"global", 2, 1}});
send<m::debugger::ResumeRequest>(conn, msgId++);
expectNotification<m::debugger::ResumedNotification>(conn);
}
TEST(ConnectionTests, testIsDebuggerAttached) {
TestContext context;
AsyncHermesRuntime &asyncRuntime = context.runtime();
SyncConnection &conn = context.conn();
int msgId = 1;
asyncRuntime.executeScriptAsync(R"(
var a = 1 + 2;
debugger; // [1] (line 2) hit debugger statement
// [2] evaluate DebuggerInternal.isDebuggerAttached to true
var b = a / 2;
)");
send<m::debugger::EnableRequest>(conn, msgId++);
expectExecutionContextCreated(conn);
expectNotification<m::debugger::ScriptParsedNotification>(conn);
// [1] (line 2) hit debugger statement
expectPaused(conn, "other", {{"global", 2, 1}});
// [2] evaluate DebuggerInternal.IsDebuggerAttached to true
sendEvalRequest(conn, 0, 0, R"("-> " + DebuggerInternal.isDebuggerAttached)");
expectEvalResponse(conn, 0, "-> true");
send<m::debugger::ResumeRequest>(conn, msgId++);
expectNotification<m::debugger::ResumedNotification>(conn);
}
TEST(ConnectionTests, testStepOver) {
TestContext context;
AsyncHermesRuntime &asyncRuntime = context.runtime();
SyncConnection &conn = context.conn();
int msgId = 1;
asyncRuntime.executeScriptAsync(R"(
var a = 1 + 2;
debugger; // [1] (line 2) hit debugger statement, step over
var b = a / 2; // [2] (line 3) step over
var c = a + b; // [3] (line 4) resume
var d = b - c;
var e = c * d;
var f = 10;
)");
send<m::debugger::EnableRequest>(conn, msgId++);
expectExecutionContextCreated(conn);
expectNotification<m::debugger::ScriptParsedNotification>(conn);
// [1] (line 2): hit debugger statement, step over
expectPaused(conn, "other", {{"global", 2, 1}});
send<m::debugger::StepOverRequest>(conn, msgId++);
expectNotification<m::debugger::ResumedNotification>(conn);
// [2] (line 3): step over
expectPaused(conn, "other", {{"global", 3, 1}});
send<m::debugger::StepOverRequest>(conn, msgId++);
expectNotification<m::debugger::ResumedNotification>(conn);
// [3] (line 4): resume
expectPaused(conn, "other", {{"global", 4, 1}});
send<m::debugger::ResumeRequest>(conn, msgId++);
expectNotification<m::debugger::ResumedNotification>(conn);
}
TEST(ConnectionTests, testStepIn) {
TestContext context;
AsyncHermesRuntime &asyncRuntime = context.runtime();
SyncConnection &conn = context.conn();
int msgId = 1;
asyncRuntime.executeScriptAsync(R"(
function addOne(val) {
return val + 1; // [3]: resume
}
var a = 1 + 2;
debugger; // [1] (line 6) hit debugger statement, step over
var b = addOne(a); // [2] (line 7) step in
var c = a + b;
var d = b - c;
var e = c * d;
var f = 10;
)");
send<m::debugger::EnableRequest>(conn, msgId++);
expectExecutionContextCreated(conn);
expectNotification<m::debugger::ScriptParsedNotification>(conn);
// [1] (line 6): hit debugger statement, step over
expectPaused(conn, "other", {{"global", 6, 1}});
send<m::debugger::StepOverRequest>(conn, msgId++);
expectNotification<m::debugger::ResumedNotification>(conn);
// [2] (line 7): step in
expectPaused(conn, "other", {{"global", 7, 1}});
send<m::debugger::StepIntoRequest>(conn, msgId++);
expectNotification<m::debugger::ResumedNotification>(conn);
// [3] (line 2): resume
expectPaused(conn, "other", {{"addOne", 2, 2}, {"global", 7, 1}});
send<m::debugger::ResumeRequest>(conn, msgId++);
expectNotification<m::debugger::ResumedNotification>(conn);
}
TEST(ConnectionTests, testStepOut) {
TestContext context;
AsyncHermesRuntime &asyncRuntime = context.runtime();
SyncConnection &conn = context.conn();
int msgId = 1;
asyncRuntime.executeScriptAsync(R"(
function addSquares(a, b) {
var a2 = a * a;
debugger; // [1] (line 3) hit debugger statement, step over
var b2 = b * b; // [2] (line 4) step out
return a2 + b2;
}
var c = addSquares(1, 2); // [3] (line 8) resume
var d = c * c;
)");
send<m::debugger::EnableRequest>(conn, msgId++);
expectExecutionContextCreated(conn);
expectNotification<m::debugger::ScriptParsedNotification>(conn);
// [1] (line 3) hit debugger statement, step over
expectPaused(conn, "other", {{"addSquares", 3, 2}, {"global", 8, 1}});
send<m::debugger::StepOverRequest>(conn, msgId++);
expectNotification<m::debugger::ResumedNotification>(conn);
// [2] (line 4) step out
expectPaused(conn, "other", {{"addSquares", 4, 2}, {"global", 8, 1}});
send<m::debugger::StepOutRequest>(conn, msgId++);
expectNotification<m::debugger::ResumedNotification>(conn);
// [3] (line 8): resume
expectPaused(conn, "other", {{"global", 8, 1}});
send<m::debugger::ResumeRequest>(conn, msgId++);
expectNotification<m::debugger::ResumedNotification>(conn);
}
TEST(ConnectionTests, testSetBreakpoint) {
TestContext context;
AsyncHermesRuntime &asyncRuntime = context.runtime();
SyncConnection &conn = context.conn();
int msgId = 1;
asyncRuntime.executeScriptAsync(R"(
var a = 1 + 2;
debugger; // [1] (line 2) hit debugger statement,
// set breakpoint on line 5
var b = a / 2;
var c = a + b; // [2] (line 5) hit breakpoint, step over
var d = b - c; // [3] (line 6) resume
var e = c * d;
var f = 10;
)");
send<m::debugger::EnableRequest>(conn, msgId++);
expectExecutionContextCreated(conn);
expectNotification<m::debugger::ScriptParsedNotification>(conn);
// [1] (line 2) hit debugger statement, set breakpoint on line 6
expectPaused(conn, "other", {{"global", 2, 1}});
m::debugger::SetBreakpointByUrlRequest req;
req.id = msgId++;
req.lineNumber = 5;
req.columnNumber = 0;
conn.send(req.toJson());
expectBreakpointResponse(conn, req.id, 5, 5);
send<m::debugger::ResumeRequest>(conn, msgId++);
expectNotification<m::debugger::ResumedNotification>(conn);
// [2] (line 5) hit breakpoint, step over
expectPaused(conn, "other", {{"global", 5, 1}});
send<m::debugger::StepOverRequest>(conn, msgId++);
expectNotification<m::debugger::ResumedNotification>(conn);
// [3] (line 6) resume
expectPaused(conn, "other", {{"global", 6, 1}});
send<m::debugger::ResumeRequest>(conn, msgId++);
expectNotification<m::debugger::ResumedNotification>(conn);
}
TEST(ConnectionTests, testSetBreakpointById) {
TestContext context;
AsyncHermesRuntime &asyncRuntime = context.runtime();
SyncConnection &conn = context.conn();
int msgId = 1;
asyncRuntime.executeScriptAsync(R"(
debugger; // line 1
Math.random(); // 2
)");
send<m::debugger::EnableRequest>(conn, ++msgId);
expectExecutionContextCreated(conn);
auto script = expectNotification<m::debugger::ScriptParsedNotification>(conn);
expectPaused(conn, "other", {{"global", 1, 1}});
m::debugger::SetBreakpointRequest req;
req.id = ++msgId;
req.location.scriptId = script.scriptId;
req.location.lineNumber = 2;
conn.send(req.toJson());
auto resp = expectResponse<m::debugger::SetBreakpointResponse>(conn, req.id);
EXPECT_EQ(resp.actualLocation.scriptId, script.scriptId);
EXPECT_EQ(resp.actualLocation.lineNumber, 2);
EXPECT_EQ(resp.actualLocation.columnNumber.value(), 4);
send<m::debugger::ResumeRequest>(conn, ++msgId);
expectNotification<m::debugger::ResumedNotification>(conn);
expectPaused(conn, "other", {{"global", 2, 1}});
send<m::debugger::ResumeRequest>(conn, ++msgId);
expectNotification<m::debugger::ResumedNotification>(conn);
}
TEST(ConnectionTests, testSetBreakpointByIdWithColumnInIndenting) {
TestContext context;
AsyncHermesRuntime &asyncRuntime = context.runtime();
SyncConnection &conn = context.conn();
int msgId = 1;
asyncRuntime.executeScriptAsync(R"(
debugger; // line 1
Math.random(); // 2
)");
send<m::debugger::EnableRequest>(conn, ++msgId);
expectExecutionContextCreated(conn);
auto script = expectNotification<m::debugger::ScriptParsedNotification>(conn);
expectPaused(conn, "other", {{"global", 1, 1}});
m::debugger::SetBreakpointRequest req;
req.id = ++msgId;
req.location.scriptId = script.scriptId;
req.location.lineNumber = 2;
// Specify a column location *before* rather than *on* the actual location
req.location.columnNumber = 0;
conn.send(req.toJson());
auto resp = expectResponse<m::debugger::SetBreakpointResponse>(conn, req.id);
EXPECT_EQ(resp.actualLocation.scriptId, script.scriptId);
EXPECT_EQ(resp.actualLocation.lineNumber, 2);
// Check that we resolved the column to the first available location
EXPECT_EQ(resp.actualLocation.columnNumber.value(), 4);
send<m::debugger::ResumeRequest>(conn, ++msgId);
expectNotification<m::debugger::ResumedNotification>(conn);
expectPaused(conn, "other", {{"global", 2, 1}});
send<m::debugger::ResumeRequest>(conn, ++msgId);
expectNotification<m::debugger::ResumedNotification>(conn);
}
TEST(ConnectionTests, testSetLazyBreakpoint) {
TestContext context;
AsyncHermesRuntime &asyncRuntime = context.runtime();
SyncConnection &conn = context.conn();
int msgId = 1;
facebook::hermes::HermesRuntime::DebugFlags flags{};
flags.lazy = true;
asyncRuntime.executeScriptAsync(
R"(
var a = 1 + 2;
debugger; // [1] (line 2) hit debugger statement,
// set breakpoint on line 5
function foo() {
var b = a / 2;
var c = a + b; // [2] (line 7) hit breakpoint, step over
var d = b - c; // [3] (line 8) resume
var e = c * d;
var f = 10;
}
foo();
)",
"url",
flags);
send<m::debugger::EnableRequest>(conn, msgId++);
expectExecutionContextCreated(conn);
expectNotification<m::debugger::ScriptParsedNotification>(conn);
// [1] (line 2) hit debugger statement, set breakpoint on line 6
expectPaused(conn, "other", {{"global", 2, 1}});
m::debugger::SetBreakpointByUrlRequest req;
req.id = msgId++;
req.lineNumber = 7;
req.columnNumber = 0;
conn.send(req.toJson());
auto breakpointId = expectBreakpointResponse(conn, req.id, 7, 7);
send<m::debugger::ResumeRequest>(conn, msgId++);
expectNotification<m::debugger::ResumedNotification>(conn);
// [2] (line 7) hit breakpoint, step over
expectPaused(conn, "other", {{"foo", 7, 2}, {"global", 13, 1}});
send<m::debugger::StepOverRequest>(conn, msgId++);
expectNotification<m::debugger::ResumedNotification>(conn);
// [3] (line 8) resume
expectPaused(conn, "other", {{"foo", 8, 2}, {"global", 13, 1}});
send<m::debugger::ResumeRequest>(conn, msgId++);
expectNotification<m::debugger::ResumedNotification>(conn);
}
TEST(ConnectionTests, testSetBreakpointWhileRunning) {
TestContext context;
AsyncHermesRuntime &asyncRuntime = context.runtime();
SyncConnection &conn = context.conn();
int msgId = 1;
asyncRuntime.executeScriptAsync(R"(
while (!shouldStop()) {
var a = 1;
var b = 2;
var c = a + b; // [1] (line 4) first time: step over
// [3] second time: set stop flag, resume
var d = 10; // [2] (line 6) resume
}
)");
send<m::debugger::EnableRequest>(conn, msgId++);
expectExecutionContextCreated(conn);
expectNotification<m::debugger::ScriptParsedNotification>(conn);
// set breakpoint on line 4: "var c = ..."
m::debugger::SetBreakpointByUrlRequest req;
req.id = msgId++;
req.lineNumber = 4;
req.columnNumber = 0;
conn.send(req.toJson());
expectBreakpointResponse(conn, req.id, 4, 4);
// [1] (line 4) hit breakpoint, step over
expectPaused(conn, "other", {{"global", 4, 1}});
send<m::debugger::StepOverRequest>(conn, msgId++);
expectNotification<m::debugger::ResumedNotification>(conn);
// [2] (line 6) resume
expectPaused(conn, "other", {{"global", 6, 1}});
send<m::debugger::ResumeRequest>(conn, msgId++);
expectNotification<m::debugger::ResumedNotification>(conn);
// [3] (line 4) hit breakpoint again, set stop flag, resume
expectPaused(conn, "other", {{"global", 4, 1}});
asyncRuntime.stop();
send<m::debugger::ResumeRequest>(conn, msgId++);
expectNotification<m::debugger::ResumedNotification>(conn);
}
TEST(ConnectionTests, testSetBreakpointConditional) {
TestContext context;
AsyncHermesRuntime &asyncRuntime = context.runtime();
SyncConnection &conn = context.conn();
int msgId = 1;
asyncRuntime.executeScriptAsync(R"(
var a = 3;
debugger; // [1] (line 2) hit debugger statement,
// set conditional breakpoint on lines 4, 5 and 6
var b = a + 5; // [2] (line 4) skip breakpoint, condition throws
var c = b - a; // [3] (line 5) skip breakpoint, condition false
var d = b - c; // [4] (line 6) hit breakpoint, condition true
var e = c * d;
var f = 10;
)");
send<m::debugger::EnableRequest>(conn, msgId++);
expectExecutionContextCreated(conn);
expectNotification<m::debugger::ScriptParsedNotification>(conn);
// [1] (line 2) hit debugger statement,
// set conditional breakpoint on lines 4, 5 and 6
expectPaused(conn, "other", {{"global", 2, 1}});
m::debugger::SetBreakpointByUrlRequest req0;
req0.id = msgId++;
req0.lineNumber = 4;
req0.condition = folly::Optional<std::string>("throw Error('Boom!')");
conn.send(req0.toJson());
expectBreakpointResponse(conn, req0.id, 4, 4);
m::debugger::SetBreakpointByUrlRequest req1;
req1.id = msgId++;
req1.lineNumber = 5;
req1.condition = folly::Optional<std::string>("b === a");
conn.send(req1.toJson());
expectBreakpointResponse(conn, req1.id, 5, 5);
m::debugger::SetBreakpointByUrlRequest req2;
req2.id = msgId++;
req2.lineNumber = 6;
req2.condition = folly::Optional<std::string>("c === 5");
conn.send(req2.toJson());
expectBreakpointResponse(conn, req2.id, 6, 6);
send<m::debugger::ResumeRequest>(conn, msgId++);
expectNotification<m::debugger::ResumedNotification>(conn);
// [2] (line 4) skip breakpoint, condition throws
// [3] (line 5) skip breakpoint, condition false
// [4] (line 6) hit breakpoint, condition true
expectPaused(conn, "other", {{"global", 6, 1}});
send<m::debugger::ResumeRequest>(conn, msgId++);
expectNotification<m::debugger::ResumedNotification>(conn);
}
TEST(ConnectionTests, testRemoveBreakpoint) {
TestContext context;
AsyncHermesRuntime &asyncRuntime = context.runtime();
SyncConnection &conn = context.conn();
int msgId = 1;
asyncRuntime.executeScriptAsync(R"(
// [1] (line 2) hit debugger statement, set breakpoint on line 7
debugger;
var a = 1;
for (var i = 1; i <= 2; i++) {
// [1] (line 7) hit breakpoint and then remove it
a += i;
}
storeValue(a);
)");
send<m::debugger::EnableRequest>(conn, msgId++);
expectExecutionContextCreated(conn);
expectNotification<m::debugger::ScriptParsedNotification>(conn);
// [1] (line 2) hit debugger statement, set breakpoint on line 7
expectPaused(conn, "other", {{"global", 2, 1}});
m::debugger::SetBreakpointByUrlRequest req;
req.id = msgId++;
req.lineNumber = 7;
conn.send(req.toJson());
auto breakpointId = expectBreakpointResponse(conn, req.id, 7, 7);
send<m::debugger::ResumeRequest>(conn, msgId++);
expectNotification<m::debugger::ResumedNotification>(conn);
// [1] (line 7) hit breakpoint and then remove it
expectPaused(conn, "other", {{"global", 7, 1}});
m::debugger::RemoveBreakpointRequest removeReq;
removeReq.id = msgId++;
removeReq.breakpointId = breakpointId;
conn.send(removeReq.toJson());
expectResponse<m::OkResponse>(conn, removeReq.id);
send<m::debugger::ResumeRequest>(conn, msgId++);
expectNotification<m::debugger::ResumedNotification>(conn);
// check final value
jsi::Value finalValue = asyncRuntime.awaitStoredValue();
EXPECT_EQ(finalValue.asNumber(), 4);
}
TEST(ConnectionTests, testAsyncPauseWhileRunning) {
TestContext context;
AsyncHermesRuntime &asyncRuntime = context.runtime();
SyncConnection &conn = context.conn();
int msgId = 1;
asyncRuntime.executeScriptAsync(R"(
var accum = 10;
while (!shouldStop()) {
var a = 1;
var b = 2;
var c = a + b;
accum += c;
} // (line 9)
var d = -accum;
)");
send<m::debugger::EnableRequest>(conn, msgId++);
expectExecutionContextCreated(conn);
expectNotification<m::debugger::ScriptParsedNotification>(conn);
// send some number of async pauses, make sure that we always stop before
// the end of the loop on line 9
for (int i = 0; i < 10; i++) {
send<m::debugger::PauseRequest>(conn, msgId++);
expectPaused(
conn, "other", {FrameInfo("global", 0, 1).setLineNumberMax(9)});
send<m::debugger::ResumeRequest>(conn, msgId++);
expectNotification<m::debugger::ResumedNotification>(conn);
}
// break out of loop
asyncRuntime.stop();
}
TEST(ConnectionTests, testEvalOnCallFrame) {
TestContext context;
AsyncHermesRuntime &asyncRuntime = context.runtime();
SyncConnection &conn = context.conn();
int msgId = 1;
asyncRuntime.executeScriptAsync(R"(
var globalVar = "omega";
var booleanVar = true;
var numberVar = 42;
var objectVar = {number: 1, bool: false, str: "string"};
function func1(closure, f1param) { // frame 4
var f1v1 = "alpha";
var f1v2 = "beta";
function func1b() { // frame 3
var f1bv1 = "gamma";
function func1c() { // frame 2
var f1cv1 = 19;
closure();
}
func1c();
}
func1b();
}
function func2() { // frame 1
var f2v1 = "baker";
var f2v2 = "charlie";
function func2b() { // frame 0
var f2bv1 = "dog";
debugger; // [1] (line 25) hit debugger statement
// [2] run evals
// [3] resume
print(globalVar);
print(f2bv1);
}
func2b();
}
func1(func2, "tau");
)");
send<m::debugger::EnableRequest>(conn, msgId++);
expectExecutionContextCreated(conn);
expectNotification<m::debugger::ScriptParsedNotification>(conn);
// [1] (line 25) hit debugger statement
expectPaused(
conn,
"other",
{{"func2b", 25, 3},
{"func2", 31, 2},
{"func1c", 13, 4},
{"func1b", 15, 3},
{"func1", 17, 2},
{"global", 34, 1}});
// [2] run eval statements
int frame = 0;
sendEvalRequest(conn, msgId + 0, frame, R"("0: " + globalVar)");
sendEvalRequest(conn, msgId + 1, frame, R"("1: " + f2bv1)");
sendEvalRequest(conn, msgId + 2, frame, R"("2: " + f2v2)");
sendEvalRequest(conn, msgId + 3, frame, R"("3: " + f2bv1 + " && " + f2v2)");
expectEvalResponse(conn, msgId + 0, "0: omega");
expectEvalResponse(conn, msgId + 1, "1: dog");
expectEvalResponse(conn, msgId + 2, "2: charlie");
expectEvalResponse(conn, msgId + 3, "3: dog && charlie");
msgId += 4;
frame = 1;
sendEvalRequest(conn, msgId + 0, frame, R"("4: " + f2v1)");
sendEvalRequest(conn, msgId + 1, frame, R"("5: " + f2v2)");
sendEvalRequest(conn, msgId + 2, frame, R"(globalVar = "mod by debugger")");
expectEvalResponse(conn, msgId + 0, "4: baker");
expectEvalResponse(conn, msgId + 1, "5: charlie");
expectEvalResponse(conn, msgId + 2, "mod by debugger");
msgId += 3;
frame = 2;
sendEvalRequest(conn, msgId + 0, frame, R"("6: " + f1cv1 + f1bv1 + f1v1)");
sendEvalRequest(conn, msgId + 1, frame, R"("7: " + globalVar)");
expectEvalResponse(conn, msgId + 0, "6: 19gammaalpha");
expectEvalResponse(conn, msgId + 1, "7: mod by debugger");
msgId += 2;
// [2.1] run eval statements that return non-string primitive values
frame = 0;
sendEvalRequest(conn, msgId + 0, frame, "booleanVar");
sendEvalRequest(conn, msgId + 1, frame, "numberVar");
expectEvalResponse(conn, msgId + 0, true);
expectEvalResponse(conn, msgId + 1, 42);
msgId += 2;
// [2.2] run eval statement that returns object
frame = 0;
sendEvalRequest(conn, msgId + 0, frame, "objectVar");
expectEvalResponse(
conn,
msgId + 0,
{{"number", PropInfo("number").setValue(1)},
{"bool", PropInfo("boolean").setValue(false)},
{"str", PropInfo("string").setValue("string")},
{"__proto__", PropInfo("object")}});
// msgId is increased by 2 because expectEvalResponse will make additional
// request with expectProps.
msgId += 2;
// [3] resume
send<m::debugger::ResumeRequest>(conn, msgId++);
expectNotification<m::debugger::ResumedNotification>(conn);
}
TEST(ConnectionTests, testRuntimeEvaluate) {
TestContext context;
AsyncHermesRuntime &asyncRuntime = context.runtime();
SyncConnection &conn = context.conn();
int msgId = 1;
asyncRuntime.executeScriptAsync(R"(
var globalVar = "omega";
var booleanVar = true;
var numberVar = 42;
var objectVar = {number: 1, bool: false, str: "string"};
while(!shouldStop()) { // [1] (line 6) hit infinite loop
var a = 1; // [2] run evals
a++; // [3] exit run loop
}
)");
send<m::debugger::EnableRequest>(conn, msgId++);
expectExecutionContextCreated(conn);
expectNotification<m::debugger::ScriptParsedNotification>(conn);
// [1] (line 6) hit infinite loop
// [2] run eval statements
sendRuntimeEvalRequest(conn, msgId + 0, R"("0: " + globalVar)");
expectEvalResponse(conn, msgId + 0, "0: omega");
// [2.1] run eval statements that return non-string primitive values
sendRuntimeEvalRequest(conn, msgId + 1, "booleanVar");
sendRuntimeEvalRequest(conn, msgId + 2, "numberVar");
expectEvalResponse(conn, msgId + 1, true);
expectEvalResponse(conn, msgId + 2, 42);
// [2.2] run eval statement that returns object
sendRuntimeEvalRequest(conn, msgId + 3, "objectVar");
expectEvalResponse(
conn,
msgId + 3,
{{"number", PropInfo("number").setValue(1)},
{"bool", PropInfo("boolean").setValue(false)},
{"str", PropInfo("string").setValue("string")},
{"__proto__", PropInfo("object")}});
// [3] exit run loop
asyncRuntime.stop();
}
TEST(ConnectionTests, testRuntimeEvaluateReturnByValue) {
TestContext context;
AsyncHermesRuntime &asyncRuntime = context.runtime();
SyncConnection &conn = context.conn();
int msgId = 1;
asyncRuntime.executeScriptAsync("while(!shouldStop());");
send<m::debugger::EnableRequest>(conn, msgId++);
expectExecutionContextCreated(conn);
expectNotification<m::debugger::ScriptParsedNotification>(conn);
// We expect this JSON object to be evaluated and return by value, so
// that JSON encoding the result will give the same string.
auto object = "{\"key\":[1,\"two\"]}";
m::runtime::EvaluateRequest req;
req.id = msgId;
req.expression = std::string("(") + object + ")";
req.returnByValue = true;
conn.send(req.toJson());
auto resp =
expectResponse<m::debugger::EvaluateOnCallFrameResponse>(conn, msgId);
EXPECT_EQ(resp.result.type, "object");
ASSERT_TRUE(resp.result.value.hasValue());
EXPECT_EQ(folly::toJson(resp.result.value.value()), object);
// [3] exit run loop
asyncRuntime.stop();
}
TEST(ConnectionTests, testEvalOnCallFrameException) {
TestContext context;
AsyncHermesRuntime &asyncRuntime = context.runtime();
SyncConnection &conn = context.conn();
int msgId = 1;
asyncRuntime.executeScriptAsync(R"(
var count = 0;
function eventuallyThrows(x) {
if (x <= 0)
throw new Error("I frew up.");
count++;
eventuallyThrows(x-1);
}
function callme() {
print("Hello");
debugger; // [1] (line 12) hit debugger statement
// [2] run evals
// [3] resume
print("Goodbye");
}
callme();
)");
send<m::debugger::EnableRequest>(conn, msgId++);
expectExecutionContextCreated(conn);
expectNotification<m::debugger::ScriptParsedNotification>(conn);
// [1] (line 12) hit debugger statement
expectPaused(conn, "other", {{"callme", 12, 2}, {"global", 18, 1}});
// [2] run evals
int frame = 0;
sendEvalRequest(conn, msgId + 0, frame, "this is not valid javascript");
sendEvalRequest(conn, msgId + 1, frame, "eventuallyThrows(5)");
sendEvalRequest(conn, msgId + 2, frame, "count");
expectEvalException(conn, msgId + 0, "SyntaxError: 1:6:';' expected", {});
expectEvalException(
conn,
msgId + 1,
"Error: I frew up.",
{{"eventuallyThrows", 5, 0},
{"eventuallyThrows", 7, 0},
{"eventuallyThrows", 7, 0},
{"eventuallyThrows", 7, 0},
{"eventuallyThrows", 7, 0},
{"eventuallyThrows", 7, 0},
// TODO: unsure why these frames are here, but they're in hdb tests
// too. Ask Hermes about if they really should be there.
FrameInfo("eval", 0, 0).setLineNumberMax(19),
FrameInfo("callme", 12, 2),
FrameInfo("global", 0, 0).setLineNumberMax(19)});
expectEvalResponse(conn, msgId + 2, 5);
msgId += 3;
// [3] resume
send<m::debugger::ResumeRequest>(conn, msgId++);
expectNotification<m::debugger::ResumedNotification>(conn);
}
TEST(ConnectionTests, testLoadMultipleScripts) {
TestContext context;
AsyncHermesRuntime &asyncRuntime = context.runtime();
SyncConnection &conn = context.conn();
int msgId = 1;
asyncRuntime.executeScriptAsync(
R"(
function foo(x) {
debugger;
print(x);
}
var a = 1 + 1;
//# sourceMappingURL=/foo/bar/url1.js.map
)",
"url1");
send<m::debugger::EnableRequest>(conn, msgId++);
expectExecutionContextCreated(conn);
m::debugger::ScriptParsedNotification script1 =
expectScriptParsed(conn, "url1", "/foo/bar/url1.js.map");
asyncRuntime.executeScriptAsync(
R"(
var b = a + 2;
var c = b - 1;
foo(c);
//# sourceMappingURL=/foo/bar/url2.js.map
)",
"url2");
m::debugger::ScriptParsedNotification script2 =
expectScriptParsed(conn, "url2", "/foo/bar/url2.js.map");
// [1] (line 2) hit debugger statement, resume
expectPaused(
conn,
"other",
{FrameInfo("foo", 2, 2).setScriptId(script1.scriptId),
FrameInfo("global", 3, 1).setScriptId(script2.scriptId)});
send<m::debugger::ResumeRequest>(conn, msgId++);
expectNotification<m::debugger::ResumedNotification>(conn);
}
TEST(ConnectionTests, testGetProperties) {
TestContext context;
AsyncHermesRuntime &asyncRuntime = context.runtime();
SyncConnection &conn = context.conn();
int msgId = 1;
std::vector<std::string> objIds;
asyncRuntime.executeScriptAsync(R"(
function foo() {
var num = 123;
var obj = {
"depth": 0,
"value": {
"a": -1/0,
"b": 1/0,
"c": Math.sqrt(-2),
"d": -0,
"e": "e_string"
}
};
var arr = [1, 2, 3];
function bar() {
var num = 456;
var obj = {"depth": 1, "value": {"c": 5, "d": "d_string"}};
debugger;
};
bar();
debugger;
}
foo();
)");
send<m::debugger::EnableRequest>(conn, msgId++);
expectExecutionContextCreated(conn);
expectNotification<m::debugger::ScriptParsedNotification>(conn);
auto pausedNote = expectPaused(
conn, "other", {{"bar", 17, 3}, {"foo", 19, 2}, {"global", 23, 1}});
auto &scopeObj = pausedNote.callFrames.at(1).scopeChain.at(0).object;
EXPECT_TRUE(scopeObj.objectId.hasValue());
std::string scopeObjId = scopeObj.objectId.value();
objIds.push_back(scopeObjId);
auto scopeChildren = expectProps(
conn,
msgId++,
scopeObjId,
{{"this", PropInfo("undefined")},
{"num", PropInfo("number").setValue(123)},
{"obj", PropInfo("object")},
{"arr", PropInfo("object").setSubtype("array")},
{"bar", PropInfo("function")}});
EXPECT_EQ(scopeChildren.size(), 3);
EXPECT_EQ(scopeChildren.count("obj"), 1);
std::string objId = scopeChildren.at("obj");
objIds.push_back(objId);
auto objChildren = expectProps(
conn,
msgId++,
objId,
{{"depth", PropInfo("number").setValue(0)},
{"value", PropInfo("object")},
{"__proto__", PropInfo("object")}});
EXPECT_EQ(objChildren.size(), 2);
EXPECT_EQ(objChildren.count("value"), 1);
std::string valueId = objChildren.at("value");
objIds.push_back(valueId);
auto valueChildren = expectProps(
conn,
msgId++,
valueId,
{{"a", PropInfo("number").setUnserializableValue("-Infinity")},
{"b", PropInfo("number").setUnserializableValue("Infinity")},
{"c", PropInfo("number").setUnserializableValue("NaN")},
{"d", PropInfo("number").setUnserializableValue("-0")},
{"e", PropInfo("string").setValue("e_string")},
{"__proto__", PropInfo("object")}});
EXPECT_EQ(valueChildren.size(), 1);
send<m::debugger::ResumeRequest>(conn, msgId++);
expectNotification<m::debugger::ResumedNotification>(conn);
expectPaused(conn, "other", {{"foo", 20, 2}, {"global", 23, 1}});
// all old object ids should be invalid after resuming
for (std::string oldObjId : objIds) {
expectProps(
conn, msgId++, oldObjId, std::unordered_map<std::string, PropInfo>{});
}
send<m::debugger::ResumeRequest>(conn, msgId++);
expectNotification<m::debugger::ResumedNotification>(conn);
}
TEST(ConnectionTests, testGetPropertiesOnlyOwnProperties) {
TestContext context;
AsyncHermesRuntime &asyncRuntime = context.runtime();
SyncConnection &conn = context.conn();
int msgId = 1;
asyncRuntime.executeScriptAsync(R"(
function foo() {
var protoObject = {
"protoNum": 77
};
var obj = Object.create(protoObject);
obj.num = 42;
debugger;
}
foo();
)");
send<m::debugger::EnableRequest>(conn, msgId++);
expectExecutionContextCreated(conn);
expectNotification<m::debugger::ScriptParsedNotification>(conn);
// wait for a pause on debugger statement and get object ID from the local
// scope.
auto pausedNote =
expectPaused(conn, "other", {{"foo", 7, 2}, {"global", 9, 1}});
auto scopeObject = pausedNote.callFrames.at(0).scopeChain.at(0).object;
auto scopeChildren = expectProps(
conn,
msgId++,
scopeObject.objectId.value(),
{{"this", PropInfo("undefined")},
{"obj", PropInfo("object")},
{"protoObject", PropInfo("object")}});
EXPECT_EQ(scopeChildren.count("obj"), 1);
std::string objId = scopeChildren.at("obj");
// Check that GetProperties request for obj object only have own properties
// when onlyOwnProperties = true.
expectProps(
conn,
msgId++,
objId,
{{"num", PropInfo("number").setValue(42)},
{"__proto__", PropInfo("object")}},
true);
// Check that GetProperties request for obj object only have all properties
// when onlyOwnProperties = false.
// __proto__ is not returned here because all properties from proto chain
// are already included in the result.
expectProps(
conn,
msgId++,
objId,
{{"num", PropInfo("number").setValue(42)},
{"protoNum", PropInfo("number").setValue(77)}},
false);
// resume
send<m::debugger::ResumeRequest>(conn, msgId++);
expectNotification<m::debugger::ResumedNotification>(conn);
}
TEST(ConnectionTests, testDisable) {
TestContext context;
AsyncHermesRuntime &asyncRuntime = context.runtime();
SyncConnection &conn = context.conn();
int msgId = 1;
asyncRuntime.executeScriptAsync(R"(
while (!shouldStop()) {
var a = 1;
var b = 2;
var c = a + b; // [1] (line 4) disable to remove breakpoints and resume
// [2] (line 4) the breakpoint should not hit anymore
var d = 10;
}
)");
send<m::debugger::EnableRequest>(conn, msgId++);
expectExecutionContextCreated(conn);
expectNotification<m::debugger::ScriptParsedNotification>(conn);
// set breakpoint on line 4: "var c = ..."
m::debugger::SetBreakpointByUrlRequest req;
req.id = msgId++;
req.lineNumber = 4;
conn.send(req.toJson());
expectBreakpointResponse(conn, req.id, 4, 4);
// [1] (line 4) disable to remove breakpoints and resume
expectPaused(conn, "other", {{"global", 4, 1}});
send<m::debugger::DisableRequest>(conn, msgId++);
expectNotification<m::debugger::ResumedNotification>(conn);
// [2] (line 4) the breakpoint should not hit anymore
expectNothing(conn);
asyncRuntime.stop();
}
TEST(ConnectionTests, testDisableWhileRunning) {
TestContext context;
AsyncHermesRuntime &asyncRuntime = context.runtime();
SyncConnection &conn = context.conn();
int msgId = 1;
asyncRuntime.executeScriptAsync(R"(
debugger; // [1] initial pause to set the breakpoint on line 6
while (!shouldStop()) { // [2] loop running until we receive a detach request
var a = 1;
}
while (shouldStop()) {
var c = a + 1; // [3] (line 6) the breakpoint should not hit after detach
}
)");
send<m::debugger::EnableRequest>(conn, msgId++);
expectExecutionContextCreated(conn);
expectNotification<m::debugger::ScriptParsedNotification>(conn);
// [1] initial pause to set the breakpoint on line 6
expectPaused(conn, "other", {{"global", 1, 1}});
// set breakpoint on line 6: "var c = ..."
m::debugger::SetBreakpointByUrlRequest req;
req.id = msgId++;
req.lineNumber = 6;
conn.send(req.toJson());
expectBreakpointResponse(conn, req.id, 6, 6);
// [2] loop running until we receive a detach request
send<m::debugger::ResumeRequest>(conn, msgId++);
expectNotification<m::debugger::ResumedNotification>(conn);
send<m::debugger::DisableRequest>(conn, msgId++);
asyncRuntime.stop();
// [3] (line 6) the breakpoint should not hit after detach
expectNothing(conn);
asyncRuntime.start();
}
TEST(ConnectionTests, testSetPauseOnExceptionsAll) {
TestContext context;
AsyncHermesRuntime &asyncRuntime = context.runtime();
SyncConnection &conn = context.conn();
int msgId = 1;
asyncRuntime.executeScriptAsync(R"(
debugger; // [1] (line 1) initial pause, set throw on exceptions to 'All'
try {
var a = 123;
throw new Error('Caught error'); // [2] line 5, pause on exception
} catch (err) {
// Do nothing.
}
throw new Error('Uncaught exception'); // [3] line 10, pause on exception
)");
send<m::debugger::EnableRequest>(conn, msgId++);
expectExecutionContextCreated(conn);
expectNotification<m::debugger::ScriptParsedNotification>(conn);
// [1] (line 1) initial pause, set throw on exceptions to 'All'
expectPaused(conn, "other", {{"global", 1, 1}});
m::debugger::SetPauseOnExceptionsRequest allExceptionsReq;
allExceptionsReq.id = msgId++;
allExceptionsReq.state = "all";
conn.send(allExceptionsReq.toJson());
expectResponse<m::OkResponse>(conn, allExceptionsReq.id);
// Resume
send<m::debugger::ResumeRequest>(conn, msgId++);
expectNotification<m::debugger::ResumedNotification>(conn);
// [2] line 5, pause on exception
expectPaused(conn, "exception", {{"global", 5, 1}});
send<m::debugger::ResumeRequest>(conn, msgId++);
expectNotification<m::debugger::ResumedNotification>(conn);
// [3] line 10, pause on exception
expectPaused(conn, "exception", {{"global", 10, 1}});
// Send resume event and check that Hermes has thrown an exception.
send<m::debugger::ResumeRequest>(conn, msgId++);
expectNotification<m::debugger::ResumedNotification>(conn);
asyncRuntime.wait();
EXPECT_EQ(asyncRuntime.getNumberOfExceptions(), 1);
EXPECT_EQ(asyncRuntime.getLastThrownExceptionMessage(), "Uncaught exception");
}
TEST(ConnectionTests, testSetPauseOnExceptionsNone) {
TestContext context;
AsyncHermesRuntime &asyncRuntime = context.runtime();
SyncConnection &conn = context.conn();
int msgId = 1;
asyncRuntime.executeScriptAsync(R"(
debugger; // [1] (line 1) initial pause, set throw on exceptions to 'None'
try {
var a = 123;
throw new Error('Caught error');
} catch (err) {
// Do nothing.
}
throw new Error('Uncaught exception');
)");
send<m::debugger::EnableRequest>(conn, msgId++);
expectExecutionContextCreated(conn);
expectNotification<m::debugger::ScriptParsedNotification>(conn);
// [1] (line 1) initial pause, set throw on exceptions to 'None'
expectPaused(conn, "other", {{"global", 1, 1}});
m::debugger::SetPauseOnExceptionsRequest allExceptionsReq;
allExceptionsReq.id = msgId++;
allExceptionsReq.state = "none";
conn.send(allExceptionsReq.toJson());
expectResponse<m::OkResponse>(conn, allExceptionsReq.id);
// Resume
send<m::debugger::ResumeRequest>(conn, msgId++);
expectNotification<m::debugger::ResumedNotification>(conn);
// Check that Hermes has thrown an exception (without reporting it).
expectNothing(conn);
asyncRuntime.wait();
EXPECT_EQ(asyncRuntime.getNumberOfExceptions(), 1);
EXPECT_EQ(asyncRuntime.getLastThrownExceptionMessage(), "Uncaught exception");
}
TEST(ConnectionTests, testSetPauseOnExceptionsUncaught) {
TestContext context;
AsyncHermesRuntime &asyncRuntime = context.runtime();
SyncConnection &conn = context.conn();
int msgId = 1;
asyncRuntime.executeScriptAsync(R"(
debugger; // [1] (line 1) initial pause, set throw on exceptions to 'Uncaught'
try {
var a = 123;
throw new Error('Caught error');
} catch (err) {
// Do nothing.
}
throw new Error('Uncaught exception'); // [3] line 10, pause on exception
)");
send<m::debugger::EnableRequest>(conn, msgId++);
expectExecutionContextCreated(conn);
expectNotification<m::debugger::ScriptParsedNotification>(conn);
// [1] (line 1) initial pause, set throw on exceptions to 'Uncaught'
expectPaused(conn, "other", {{"global", 1, 1}});
m::debugger::SetPauseOnExceptionsRequest allExceptionsReq;
allExceptionsReq.id = msgId++;
allExceptionsReq.state = "uncaught";
conn.send(allExceptionsReq.toJson());
expectResponse<m::OkResponse>(conn, allExceptionsReq.id);
// Resume
send<m::debugger::ResumeRequest>(conn, msgId++);
expectNotification<m::debugger::ResumedNotification>(conn);
// [3] line 10, pause on exception
expectPaused(conn, "exception", {{"global", 10, 1}});
// Send resume event and check that Hermes has thrown an exception.
send<m::debugger::ResumeRequest>(conn, msgId++);
expectNotification<m::debugger::ResumedNotification>(conn);
asyncRuntime.wait();
EXPECT_EQ(asyncRuntime.getNumberOfExceptions(), 1);
EXPECT_EQ(asyncRuntime.getLastThrownExceptionMessage(), "Uncaught exception");
}
TEST(ConnectionTests, invalidPauseModeGivesError) {
TestContext context;
SyncConnection &conn = context.conn();
m::debugger::SetPauseOnExceptionsRequest req;
req.id = 1;
req.state = "badgers";
conn.send(req.toJson());
expectResponse<m::ErrorResponse>(conn, req.id);
}
TEST(ConnectionTests, testShouldPauseOnThrow) {
TestContext context;
AsyncHermesRuntime &asyncRuntime = context.runtime();
SyncConnection &conn = context.conn();
int msgId = 1;
asyncRuntime.executeScriptAsync(R"(
debugger; // [1] (line 1) initial pause, check shouldPauseOnThrow is false
// [2] set throw to 'All', check shouldPauseOnThrow is true
// [3] set throw to 'None', check shouldPauseOnThrow is false
// [4] set throw to 'Uncaught', check shouldPauseOnThrow is true
)");
send<m::debugger::EnableRequest>(conn, msgId++);
expectExecutionContextCreated(conn);
expectNotification<m::debugger::ScriptParsedNotification>(conn);
auto shouldPauseOnThrowEvalMsg =
R"("-> " + DebuggerInternal.shouldPauseOnThrow)";
auto responseTrue = "-> true";
auto responseFalse = "-> false";
// [1] (line 1) initial pause, check shouldPauseOnThrow is false
expectPaused(conn, "other", {{"global", 1, 1}});
sendEvalRequest(conn, msgId + 1, 0, shouldPauseOnThrowEvalMsg);
expectEvalResponse(conn, msgId + 1, responseFalse);
// [2] set throw to 'All', check shouldPauseOnThrow is true
m::debugger::SetPauseOnExceptionsRequest allExceptionsReq;
allExceptionsReq.id = msgId++;
allExceptionsReq.state = "all";
conn.send(allExceptionsReq.toJson());
expectResponse<m::OkResponse>(conn, allExceptionsReq.id);
sendEvalRequest(conn, msgId + 1, 0, shouldPauseOnThrowEvalMsg);
expectEvalResponse(conn, msgId + 1, responseTrue);
// [3] set throw to 'None', check shouldPauseOnThrow is false
m::debugger::SetPauseOnExceptionsRequest noExceptionsReq;
noExceptionsReq.id = msgId++;
noExceptionsReq.state = "none";
conn.send(noExceptionsReq.toJson());
expectResponse<m::OkResponse>(conn, noExceptionsReq.id);
sendEvalRequest(conn, msgId + 1, 0, shouldPauseOnThrowEvalMsg);
expectEvalResponse(conn, msgId + 1, responseFalse);
// [4] set throw to 'Uncaught', check shouldPauseOnThrow is true
m::debugger::SetPauseOnExceptionsRequest uncaughtExceptionsReq;
uncaughtExceptionsReq.id = msgId++;
uncaughtExceptionsReq.state = "uncaught";
conn.send(uncaughtExceptionsReq.toJson());
expectResponse<m::OkResponse>(conn, uncaughtExceptionsReq.id);
sendEvalRequest(conn, msgId + 1, 0, shouldPauseOnThrowEvalMsg);
expectEvalResponse(conn, msgId + 1, responseTrue);
// Resume
send<m::debugger::ResumeRequest>(conn, msgId++);
expectNotification<m::debugger::ResumedNotification>(conn);
}
TEST(ConnectionTests, testScopeVariables) {
TestContext context;
AsyncHermesRuntime &asyncRuntime = context.runtime();
SyncConnection &conn = context.conn();
int msgId = 1;
asyncRuntime.executeScriptAsync(R"(
var globalString = "global-string";
var globalObject = {number: 1, bool: false};
function func() {
var localString = "local-string";
var localObject = {number: 2, bool: true};
debugger; // [1] (line 7) hit debugger statement
// two local vars - localString and localObject
// two global vars - globalString and globalObject
}
func(); // line 12
)");
send<m::debugger::EnableRequest>(conn, msgId++);
expectExecutionContextCreated(conn);
expectNotification<m::debugger::ScriptParsedNotification>(conn);
// [1] (line 7) hit debugger statement
auto pausedNote =
expectPaused(conn, "other", {{"func", 7, 2}, {"global", 12, 1}});
auto scopeChain = pausedNote.callFrames.at(0).scopeChain;
EXPECT_EQ(scopeChain.size(), 2);
// [2] inspect local scope
EXPECT_EQ(scopeChain.at(0).type, "local");
auto localScopeObject = scopeChain.at(0).object;
auto localScopeObjectChildren = expectProps(
conn,
msgId++,
localScopeObject.objectId.value(),
{{"this", PropInfo("undefined")},
{"localString", PropInfo("string").setValue("local-string")},
{"localObject", PropInfo("object")}});
auto localObjectId = localScopeObjectChildren.at("localObject");
expectProps(
conn,
msgId++,
localObjectId,
{{"number", PropInfo("number").setValue(2)},
{"bool", PropInfo("boolean").setValue(true)},
{"__proto__", PropInfo("object")}});
// [3] inspect global scope
// Global scope can contain more properties than we have defined
// in our test code and we can't use expectProps() method here.
// As a workaround we create a Map of properties and check that
// those global properties that we have defined are in the map.
EXPECT_EQ(scopeChain.at(1).type, "global");
auto globalScopeObject = scopeChain.at(1).object;
m::runtime::GetPropertiesRequest req;
req.id = msgId++;
req.objectId = globalScopeObject.objectId.value();
conn.send(req.toJson());
auto resp = expectResponse<m::runtime::GetPropertiesResponse>(conn, req.id);
std::unordered_map<std::string, folly::Optional<m::runtime::RemoteObject>>
globalProperties;
for (auto propertyDescriptor : resp.result) {
globalProperties[propertyDescriptor.name] = propertyDescriptor.value;
}
EXPECT_GE(globalProperties.size(), 3);
// globalString should be of type "string" and have value "global-string".
EXPECT_EQ(globalProperties.count("globalString"), 1);
EXPECT_TRUE(globalProperties["globalString"].hasValue());
EXPECT_EQ(globalProperties["globalString"].value().type, "string");
EXPECT_EQ(
globalProperties["globalString"].value().value.value(), "global-string");
// func should be of type "function".
EXPECT_EQ(globalProperties.count("func"), 1);
EXPECT_TRUE(globalProperties["func"].hasValue());
EXPECT_EQ(globalProperties["func"].value().type, "function");
// globalObject should be of type "object" with "number" and "bool"
// properties.
EXPECT_EQ(globalProperties.count("globalObject"), 1);
EXPECT_TRUE(globalProperties["globalObject"].hasValue());
EXPECT_EQ(globalProperties["globalObject"].value().type, "object");
expectProps(
conn,
msgId++,
globalProperties["globalObject"].value().objectId.value(),
{{"number", PropInfo("number").setValue(1)},
{"bool", PropInfo("boolean").setValue(false)},
{"__proto__", PropInfo("object")}});
// [4] resume
send<m::debugger::ResumeRequest>(conn, msgId++);
expectNotification<m::debugger::ResumedNotification>(conn);
}
TEST(ConnectionTests, testConsoleLog) {
TestContext context;
AsyncHermesRuntime &asyncRuntime = context.runtime();
SyncConnection &conn = context.conn();
int msgId = 1;
asyncRuntime.executeScriptAsync(R"(
var object1 = {number1: 1, bool1: false};
var object2 = {number2: 2, bool2: true};
console.warn('string value', object1, object2);
debugger; // Hit debugger statement so that we receive console
// api notification before VM gets destroyed.
)");
send<m::debugger::EnableRequest>(conn, msgId++);
expectExecutionContextCreated(conn);
expectNotification<m::debugger::ScriptParsedNotification>(conn);
// Two notifications (hitting debugger and console API call) can appear
// in any order. We wait for two notifications here and later check
// that both of them were hit.
bool receivedConsoleNotification = false;
bool receivedPausedNotification = false;
for (size_t i = 0; i < 2; ++i) {
conn.waitForNotification([&receivedConsoleNotification,
&receivedPausedNotification,
&conn,
&msgId](const std::string &str) {
auto parsedNote = folly::parseJson(str);
auto method = parsedNote.at("method").asString();
if (method == "Runtime.consoleAPICalled") {
receivedConsoleNotification = true;
auto note = m::runtime::ConsoleAPICalledNotification(parsedNote);
EXPECT_EQ(note.type, "warning");
EXPECT_EQ(note.args.size(), 3);
EXPECT_EQ(note.args[0].type, "string");
EXPECT_EQ(note.args[0].value, "string value");
EXPECT_EQ(note.args[1].type, "object");
expectProps(
conn,
msgId++,
note.args[1].objectId.value(),
{{"number1", PropInfo("number").setValue(1)},
{"bool1", PropInfo("boolean").setValue(false)},
{"__proto__", PropInfo("object")}});
EXPECT_EQ(note.args[2].type, "object");
expectProps(
conn,
msgId++,
note.args[2].objectId.value(),
{{"number2", PropInfo("number").setValue(2)},
{"bool2", PropInfo("boolean").setValue(true)},
{"__proto__", PropInfo("object")}});
} else if (method == "Debugger.paused") {
receivedPausedNotification = true;
auto note = m::debugger::PausedNotification(parsedNote);
EXPECT_EQ(note.reason, "other");
EXPECT_EQ(note.callFrames.size(), 1);
EXPECT_EQ(note.callFrames[0].functionName, "global");
EXPECT_EQ(note.callFrames[0].location.lineNumber, 5);
} else {
throw UnexpectedNotificationException();
}
});
}
EXPECT_TRUE(receivedConsoleNotification);
EXPECT_TRUE(receivedPausedNotification);
// Resume and expect no further notifications
send<m::debugger::ResumeRequest>(conn, msgId++);
expectNotification<m::debugger::ResumedNotification>(conn);
expectNothing(conn);
}
TEST(ConnectionTests, testThisObject) {
TestContext context;
AsyncHermesRuntime &asyncRuntime = context.runtime();
SyncConnection &conn = context.conn();
int msgId = 1;
asyncRuntime.executeScriptAsync(R"(
var globalString = "global-string";
var object = {
someVar: "object var",
foo: function() {
var localString = "local-string";
debugger; // [1] (line 7) hit debugger statement.
}
}
object.foo(); // (line 11)
)");
send<m::debugger::EnableRequest>(conn, msgId++);
expectExecutionContextCreated(conn);
expectNotification<m::debugger::ScriptParsedNotification>(conn);
// [1] (line 1) hit debugger statement
auto pausedNote =
expectPaused(conn, "other", {{"foo", 7, 2}, {"global", 11, 1}});
// [2] inspect first call frame (foo)
auto localThisObj = pausedNote.callFrames.at(0).thisObj;
expectProps(
conn,
msgId++,
localThisObj.objectId.value(),
{{"someVar", PropInfo("string").setValue("object var")},
{"foo", PropInfo("function")},
{"__proto__", PropInfo("object")}});
// [3] inspect second call frame (global)
// Global scope can contain more properties than we have defined
// in our test code and we can't use expectProps() method here.
// As a workaround we create a Map of properties and check that
// those global properties that we have defined are in the map.
auto globalThisObj = pausedNote.callFrames.at(1).thisObj;
m::runtime::GetPropertiesRequest req;
req.id = msgId++;
req.objectId = globalThisObj.objectId.value();
conn.send(req.toJson());
auto resp = expectResponse<m::runtime::GetPropertiesResponse>(conn, req.id);
std::unordered_map<std::string, folly::Optional<m::runtime::RemoteObject>>
properties;
for (auto propertyDescriptor : resp.result) {
properties[propertyDescriptor.name] = propertyDescriptor.value;
}
// globalString should be of type "string" and have value "global-string".
EXPECT_EQ(properties.count("globalString"), 1);
EXPECT_TRUE(properties["globalString"].hasValue());
EXPECT_EQ(properties["globalString"].value().type, "string");
EXPECT_EQ(properties["globalString"].value().value.value(), "global-string");
// [4] resume
send<m::debugger::ResumeRequest>(conn, msgId++);
expectNotification<m::debugger::ResumedNotification>(conn);
}
TEST(ConnectionTests, testSetBreakpointsMultipleScripts) {
TestContext context;
AsyncHermesRuntime &asyncRuntime = context.runtime();
SyncConnection &conn = context.conn();
int msgId = 1;
std::string url1 = "first-url";
asyncRuntime.executeScriptAsync(
R"(
function foo1() {
var somevar1 = 111; // (line 2) hit breakpoint
var somevar2 = 222;
}
)",
url1);
send<m::debugger::EnableRequest>(conn, msgId++);
expectExecutionContextCreated(conn);
auto scriptParsed1 =
expectNotification<m::debugger::ScriptParsedNotification>(conn);
std::string url2 = "second-url";
asyncRuntime.executeScriptAsync(
R"(
function foo2() {
var somevar3 = 333;
var somevar4 = 444; // (line 3) hit breakpoint
}
)",
url2);
auto scriptParsed2 =
expectNotification<m::debugger::ScriptParsedNotification>(conn);
// In the third script we will set breakpoint (on debugger statement)
// and call both functions (script url doesn't matter).
asyncRuntime.executeScriptAsync(R"(
debugger; // [1] (line 1) set breakpoints in both files
foo1();
foo2();
)");
auto scriptParsed3 =
expectNotification<m::debugger::ScriptParsedNotification>(conn);
// [1] hit debugger statement
expectPaused(conn, "other", {{"global", 1, 1}});
// Set breakpoint on line 2 in the first script and line 3 in the second
// script.
m::debugger::SetBreakpointByUrlRequest req1;
req1.id = msgId++;
req1.lineNumber = 2;
req1.url = url1;
conn.send(req1.toJson());
expectBreakpointResponse(conn, req1.id, 2, 2);
m::debugger::SetBreakpointByUrlRequest req2;
req2.id = msgId++;
req2.lineNumber = 3;
req2.url = url2;
conn.send(req2.toJson());
expectBreakpointResponse(conn, req2.id, 3, 3);
// Resume and check that we hit correct breakpoints.
send<m::debugger::ResumeRequest>(conn, msgId++);
expectNotification<m::debugger::ResumedNotification>(conn);
// First we should stop on line 2 of the first script.
expectPaused(
conn,
"other",
{FrameInfo("foo1", 2, 2).setScriptId(scriptParsed1.scriptId),
FrameInfo("global", 2, 1).setScriptId(scriptParsed3.scriptId)});
send<m::debugger::ResumeRequest>(conn, msgId++);
expectNotification<m::debugger::ResumedNotification>(conn);
// Next we should stop on line 3 of the second script.
expectPaused(
conn,
"other",
{FrameInfo("foo2", 3, 2).setScriptId(scriptParsed2.scriptId),
FrameInfo("global", 3, 1).setScriptId(scriptParsed3.scriptId)});
send<m::debugger::ResumeRequest>(conn, msgId++);
expectNotification<m::debugger::ResumedNotification>(conn);
}
TEST(ConnectionTests, testSetBreakpointByUrlRegex) {
TestContext context;
AsyncHermesRuntime &asyncRuntime = context.runtime();
SyncConnection &conn = context.conn();
int msgId = 1;
std::string url1 = "https://www.example.com/123456";
asyncRuntime.executeScriptAsync(
R"(
function foo1() {
var somevar1 = 111; // (line 2) hit breakpoint
}
)",
url1);
send<m::debugger::EnableRequest>(conn, msgId++);
expectExecutionContextCreated(conn);
auto scriptParsed1 =
expectNotification<m::debugger::ScriptParsedNotification>(conn);
std::string url2 = "https://www.example.com/abcdefg";
asyncRuntime.executeScriptAsync(
R"(
function foo2() {
var aaa = 'bbb';
var somevar2 = 222; // (line 3) hit breakpoint
}
)",
url2);
auto scriptParsed2 =
expectNotification<m::debugger::ScriptParsedNotification>(conn);
// In the third script we will set breakpoint (on debugger statement)
// and call both functions (script url doesn't matter).
asyncRuntime.executeScriptAsync(R"(
debugger; // [1] (line 1) set breakpoints in both files
foo1();
foo2();
)");
auto scriptParsed3 =
expectNotification<m::debugger::ScriptParsedNotification>(conn);
// [1] hit debugger statement
expectPaused(conn, "other", {{"global", 1, 1}});
// Set breakpoint on line 2 of URL matching "www\.example\.com\/[\d]+"
// (should match url1).
m::debugger::SetBreakpointByUrlRequest req1;
req1.id = msgId++;
req1.lineNumber = 2;
req1.urlRegex = R"(https://www\.example\.com\/[\d]+)";
conn.send(req1.toJson());
expectBreakpointResponse(conn, req1.id, 2, 2);
// Set breakpoint on line 3 of URL matching "www\.example\.com\/[a-zA-z]+"
// (should match url2).
m::debugger::SetBreakpointByUrlRequest req2;
req2.id = msgId++;
req2.lineNumber = 3;
req2.urlRegex = R"(https://www\.example\.com\/[a-zA-z]+)";
conn.send(req2.toJson());
expectBreakpointResponse(conn, req2.id, 3, 3);
// Resume and check that we hit correct breakpoints.
send<m::debugger::ResumeRequest>(conn, msgId++);
expectNotification<m::debugger::ResumedNotification>(conn);
// First we should stop on line 2 of the first script.
expectPaused(
conn,
"other",
{FrameInfo("foo1", 2, 2).setScriptId(scriptParsed1.scriptId),
FrameInfo("global", 2, 1).setScriptId(scriptParsed3.scriptId)});
send<m::debugger::ResumeRequest>(conn, msgId++);
expectNotification<m::debugger::ResumedNotification>(conn);
// Next we should stop on line 3 of the second script.
expectPaused(
conn,
"other",
{FrameInfo("foo2", 3, 2).setScriptId(scriptParsed2.scriptId),
FrameInfo("global", 3, 1).setScriptId(scriptParsed3.scriptId)});
send<m::debugger::ResumeRequest>(conn, msgId++);
expectNotification<m::debugger::ResumedNotification>(conn);
}
TEST(ConnectionTests, testColumnBreakpoint) {
TestContext context;
AsyncHermesRuntime &asyncRuntime = context.runtime();
SyncConnection &conn = context.conn();
int msgId = 1;
asyncRuntime.executeScriptAsync(
R"(
function foo(){x=1}debugger;foo();
)",
"url");
send<m::debugger::EnableRequest>(conn, msgId++);
expectExecutionContextCreated(conn);
expectNotification<m::debugger::ScriptParsedNotification>(conn);
// Hit debugger statement.
expectPaused(conn, "other", {{"global", 1, 1}});
// Set breakpoint on position 1:16 (x=1).
m::debugger::SetBreakpointByUrlRequest req;
req.id = msgId++;
req.lineNumber = 1;
req.columnNumber = 16;
req.url = "url";
conn.send(req.toJson());
expectBreakpointResponse(conn, req.id, 1, 1);
// Resume and except to pause on a breakpoint.
send<m::debugger::ResumeRequest>(conn, msgId++);
expectNotification<m::debugger::ResumedNotification>(conn);
expectPaused(
conn,
"other",
{FrameInfo("foo", 1, 2).setColumnNumber(16), FrameInfo("global", 1, 1)});
// Resume execution
send<m::debugger::ResumeRequest>(conn, msgId++);
expectNotification<m::debugger::ResumedNotification>(conn);
}
TEST(ConnectionTests, canBreakOnScriptsWithSourceMap) {
TestContext context;
AsyncHermesRuntime &asyncRuntime = context.runtime();
SyncConnection &conn = context.conn();
int msgId = 1;
send<m::debugger::EnableRequest>(conn, msgId++);
expectExecutionContextCreated(conn);
m::debugger::SetInstrumentationBreakpointRequest req;
req.id = msgId++;
req.instrumentation = "beforeScriptWithSourceMapExecution";
conn.send(req.toJson());
auto bpId = expectResponse<m::debugger::SetInstrumentationBreakpointResponse>(
conn, req.id)
.breakpointId;
asyncRuntime.executeScriptAsync(R"(
storeValue(42); debugger;
//# sourceURL=http://example.com/source.js
//# sourceMappingURL=http://example.com/source.map
)");
expectNotification<m::debugger::ScriptParsedNotification>(conn);
// We should get a pause before the first statement
auto note = expectNotification<m::debugger::PausedNotification>(conn);
ASSERT_FALSE(asyncRuntime.hasStoredValue());
EXPECT_EQ(note.reason, "other");
ASSERT_TRUE(note.hitBreakpoints.hasValue());
ASSERT_EQ(note.hitBreakpoints->size(), 1);
EXPECT_EQ(note.hitBreakpoints->at(0), bpId);
// Continue and verify that the JS code has now executed
send<m::debugger::ResumeRequest>(conn, msgId++);
expectNotification<m::debugger::ResumedNotification>(conn);
expectNotification<m::debugger::PausedNotification>(conn);
EXPECT_EQ(asyncRuntime.awaitStoredValue().asNumber(), 42);
// Resume and exit
send<m::debugger::ResumeRequest>(conn, msgId++);
expectNotification<m::debugger::ResumedNotification>(conn);
}
TEST(ConnectionTests, wontStopOnFilesWithoutSourceMaps) {
TestContext context;
AsyncHermesRuntime &asyncRuntime = context.runtime();
SyncConnection &conn = context.conn();
int msgId = 1;
send<m::debugger::EnableRequest>(conn, msgId++);
expectExecutionContextCreated(conn);
m::debugger::SetInstrumentationBreakpointRequest req;
req.id = msgId++;
req.instrumentation = "beforeScriptWithSourceMapExecution";
conn.send(req.toJson());
expectResponse<m::debugger::SetInstrumentationBreakpointResponse>(
conn, req.id);
// This script has no source map, so it should not trigger a break
asyncRuntime.executeScriptAsync(R"(
storeValue(42); debugger;
//# sourceURL=http://example.com/source.js
)");
expectNotification<m::debugger::ScriptParsedNotification>(conn);
// Continue and verify that the JS code has now executed without first
// pausing on the script load.
expectNotification<m::debugger::PausedNotification>(conn);
EXPECT_EQ(asyncRuntime.awaitStoredValue().asNumber(), 42);
// Resume and exit
send<m::debugger::ResumeRequest>(conn, msgId++);
expectNotification<m::debugger::ResumedNotification>(conn);
}
TEST(ConnectionTests, runIfWaitingForDebugger) {
TestContext context(true);
AsyncHermesRuntime &asyncRuntime = context.runtime();
SyncConnection &conn = context.conn();
int msgId = 0;
asyncRuntime.executeScriptAsync(R"(
storeValue(1); debugger;
)");
send<m::debugger::EnableRequest>(conn, ++msgId);
expectExecutionContextCreated(conn);
expectNotification<m::debugger::ScriptParsedNotification>(conn);
expectNotification<m::debugger::PausedNotification>(conn);
// We should now be paused on load. Verify that we didn't run code.
ASSERT_FALSE(asyncRuntime.hasStoredValue());
// RunIfWaitingForDebugger should cause us to resume
send<m::runtime::RunIfWaitingForDebuggerRequest>(conn, ++msgId);
expectNotification<m::debugger::ResumedNotification>(conn);
// We should immediately hit the 'debugger;' statement
expectNotification<m::debugger::PausedNotification>(conn);
EXPECT_EQ(1, asyncRuntime.awaitStoredValue().asNumber());
// RunIfWaitingForDebuggerResponse should be accepted but have no effect
send<m::runtime::RunIfWaitingForDebuggerRequest>(conn, ++msgId);
// Do a dummy call so we can expect something other than a ResumeRequest
sendRuntimeEvalRequest(conn, ++msgId, "true");
expectEvalResponse(conn, msgId, true);
// Finally explicitly continue and exit
send<m::debugger::ResumeRequest>(conn, msgId++);
expectNotification<m::debugger::ResumedNotification>(conn);
}
} // namespace chrome
} // namespace inspector
} // namespace hermes
} // namespace facebook