/* * 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 #include #include #include #include #include #include #include #include #include #include #include #include #include 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, bool veryLazy = false) : runtime_(veryLazy), conn_(runtime_.runtime(), waitForDebugger) {} ~TestContext() { runtime_.wait(); } AsyncHermesRuntime &runtime() { return runtime_; } SyncConnection &conn() { return conn_; } private: AsyncHermesRuntime runtime_; SyncConnection conn_; }; template 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 NotificationType expectNotification(SyncConnection &conn) { NotificationType note; conn.waitForNotification([¬e](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>(); 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 &frames, const std::vector &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(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 ResponseType send(SyncConnection &conn, int id) { RequestType req; req.id = id; conn.send(req.toJson()); return expectResponse(conn, id); } template ResponseType send(SyncConnection &conn, RequestType req) { conn.send(req.toJson()); return expectResponse(conn, req.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(callFrameId); req.expression = expression; conn.send(req.toJson()); } m::runtime::ExecutionContextCreatedNotification expectExecutionContextCreated( SyncConnection &conn) { auto note = expectNotification(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(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 &infos) { auto note = expectNotification(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(conn, id); EXPECT_EQ(resp.id, id); EXPECT_FALSE(resp.breakpointId.empty()); EXPECT_NE( resp.breakpointId, folly::to(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(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(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(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 infos) { auto resp = expectResponse(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 subtype; folly::Optional value; folly::Optional unserializableValue; }; std::unordered_map expectProps( SyncConnection &conn, int msgId, const std::string &objectId, const std::unordered_map &infos, bool ownProperties = true) { m::runtime::GetPropertiesRequest req; req.id = msgId; req.objectId = objectId; req.ownProperties = ownProperties; conn.send(req.toJson()); std::unordered_map objectIds; auto resp = expectResponse(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 &infos) { auto resp = expectResponse(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(conn, 1); expectExecutionContextCreated(conn); expectNotification(conn); conn.send(R"({"id": 2, "method": "Debugger.foo"})"); conn.send(R"({"id": 3, "method": "Debugger.bar", "params": {"a": "b"}})"); expectResponse(conn, 2); expectResponse(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(conn, msgId++); expectExecutionContextCreated(conn); expectNotification(conn); // [1] (line 2) hit debugger statement, resume expectPaused(conn, "other", {{"global", 2, 1}}); send(conn, msgId++); expectNotification(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(conn, msgId++); expectExecutionContextCreated(conn); expectNotification(conn); // [1] (line 2) hit debugger statement, resume expectPaused(conn, "other", {{"global", 2, 1}}); send(conn, msgId++); expectNotification(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(conn, msgId++); expectExecutionContextCreated(conn); expectNotification(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(conn, msgId++); expectNotification(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(conn, msgId++); expectExecutionContextCreated(conn); expectNotification(conn); // [1] (line 2): hit debugger statement, step over expectPaused(conn, "other", {{"global", 2, 1}}); send(conn, msgId++); expectNotification(conn); // [2] (line 3): step over expectPaused(conn, "other", {{"global", 3, 1}}); send(conn, msgId++); expectNotification(conn); // [3] (line 4): resume expectPaused(conn, "other", {{"global", 4, 1}}); send(conn, msgId++); expectNotification(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(conn, msgId++); expectExecutionContextCreated(conn); expectNotification(conn); // [1] (line 6): hit debugger statement, step over expectPaused(conn, "other", {{"global", 6, 1}}); send(conn, msgId++); expectNotification(conn); // [2] (line 7): step in expectPaused(conn, "other", {{"global", 7, 1}}); send(conn, msgId++); expectNotification(conn); // [3] (line 2): resume expectPaused(conn, "other", {{"addOne", 2, 2}, {"global", 7, 1}}); send(conn, msgId++); expectNotification(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(conn, msgId++); expectExecutionContextCreated(conn); expectNotification(conn); // [1] (line 3) hit debugger statement, step over expectPaused(conn, "other", {{"addSquares", 3, 2}, {"global", 8, 1}}); send(conn, msgId++); expectNotification(conn); // [2] (line 4) step out expectPaused(conn, "other", {{"addSquares", 4, 2}, {"global", 8, 1}}); send(conn, msgId++); expectNotification(conn); // [3] (line 8): resume expectPaused(conn, "other", {{"global", 8, 1}}); send(conn, msgId++); expectNotification(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(conn, msgId++); expectExecutionContextCreated(conn); expectNotification(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(conn, msgId++); expectNotification(conn); // [2] (line 5) hit breakpoint, step over expectPaused(conn, "other", {{"global", 5, 1}}); send(conn, msgId++); expectNotification(conn); // [3] (line 6) resume expectPaused(conn, "other", {{"global", 6, 1}}); send(conn, msgId++); expectNotification(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(conn, ++msgId); expectExecutionContextCreated(conn); auto script = expectNotification(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(conn, req.id); EXPECT_EQ(resp.actualLocation.scriptId, script.scriptId); EXPECT_EQ(resp.actualLocation.lineNumber, 2); EXPECT_EQ(resp.actualLocation.columnNumber.value(), 4); send(conn, ++msgId); expectNotification(conn); expectPaused(conn, "other", {{"global", 2, 1}}); send(conn, ++msgId); expectNotification(conn); } TEST(ConnectionTests, testActivateBreakpoints) { TestContext context; AsyncHermesRuntime &asyncRuntime = context.runtime(); SyncConnection &conn = context.conn(); int msgId = 1; asyncRuntime.executeScriptAsync(R"( debugger; // line 1 x=100 // 2 debugger; // 3 x=101; // 4 )"); send(conn, ++msgId); expectExecutionContextCreated(conn); auto script = expectNotification(conn); expectPaused(conn, "other", {{"global", 1, 1}}); // Set breakpoint #1 m::debugger::SetBreakpointRequest req; req.id = ++msgId; req.location.scriptId = script.scriptId; req.location.lineNumber = 2; conn.send(req.toJson()); expectResponse(conn, req.id); // Set breakpoint #2 req.id = ++msgId; req.location.scriptId = script.scriptId; req.location.lineNumber = 4; conn.send(req.toJson()); expectResponse(conn, req.id); // Disable breakpoints m::debugger::SetBreakpointsActiveRequest activeReq; activeReq.id = ++msgId; activeReq.active = false; conn.send(activeReq.toJson()); expectResponse(conn, activeReq.id); // Resume send(conn, ++msgId); expectNotification(conn); // Expect first breakpoint to be skipped, now hitting line #3 expectPaused(conn, "other", {{"global", 3, 1}}); // Re-enable breakpoints activeReq.id = ++msgId; activeReq.active = true; conn.send(activeReq.toJson()); expectResponse(conn, activeReq.id); // Resume and expect breakpoints to trigger again send(conn, ++msgId); expectNotification(conn); expectPaused(conn, "other", {{"global", 4, 1}}); // Continue and exit send(conn, ++msgId); expectNotification(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(conn, ++msgId); expectExecutionContextCreated(conn); auto script = expectNotification(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(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(conn, ++msgId); expectNotification(conn); expectPaused(conn, "other", {{"global", 2, 1}}); send(conn, ++msgId); expectNotification(conn); } TEST(ConnectionTests, testSetLazyBreakpoint) { 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 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"); send(conn, msgId++); expectExecutionContextCreated(conn); expectNotification(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(conn, msgId++); expectNotification(conn); // [2] (line 7) hit breakpoint, step over expectPaused(conn, "other", {{"foo", 7, 2}, {"global", 13, 1}}); send(conn, msgId++); expectNotification(conn); // [3] (line 8) resume expectPaused(conn, "other", {{"foo", 8, 2}, {"global", 13, 1}}); send(conn, msgId++); expectNotification(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(conn, msgId++); expectExecutionContextCreated(conn); expectNotification(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(conn, msgId++); expectNotification(conn); // [2] (line 6) resume expectPaused(conn, "other", {{"global", 6, 1}}); send(conn, msgId++); expectNotification(conn); // [3] (line 4) hit breakpoint again, set stop flag, resume expectPaused(conn, "other", {{"global", 4, 1}}); asyncRuntime.stop(); send(conn, msgId++); expectNotification(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(conn, msgId++); expectExecutionContextCreated(conn); expectNotification(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("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("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("c === 5"); conn.send(req2.toJson()); expectBreakpointResponse(conn, req2.id, 6, 6); send(conn, msgId++); expectNotification(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(conn, msgId++); expectNotification(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(conn, msgId++); expectExecutionContextCreated(conn); expectNotification(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(conn, msgId++); expectNotification(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(conn, removeReq.id); send(conn, msgId++); expectNotification(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(conn, msgId++); expectExecutionContextCreated(conn); expectNotification(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(conn, msgId++); expectPaused( conn, "other", {FrameInfo("global", 0, 1).setLineNumberMax(9)}); send(conn, msgId++); expectNotification(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(conn, msgId++); expectExecutionContextCreated(conn); expectNotification(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(conn, msgId++); expectNotification(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(conn, msgId++); expectExecutionContextCreated(conn); expectNotification(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(conn, msgId++); expectExecutionContextCreated(conn); expectNotification(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(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(conn, msgId++); expectExecutionContextCreated(conn); expectNotification(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(conn, msgId++); expectNotification(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(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(conn, msgId++); expectNotification(conn); } TEST(ConnectionTests, testGetProperties) { TestContext context; AsyncHermesRuntime &asyncRuntime = context.runtime(); SyncConnection &conn = context.conn(); int msgId = 1; std::vector 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(conn, msgId++); expectExecutionContextCreated(conn); expectNotification(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(conn, msgId++); expectNotification(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{}); } send(conn, msgId++); expectNotification(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(conn, msgId++); expectExecutionContextCreated(conn); expectNotification(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(conn, msgId++); expectNotification(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(conn, msgId++); expectExecutionContextCreated(conn); expectNotification(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(conn, msgId++); expectNotification(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(conn, msgId++); expectExecutionContextCreated(conn); expectNotification(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(conn, msgId++); expectNotification(conn); send(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(conn, msgId++); expectExecutionContextCreated(conn); expectNotification(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(conn, allExceptionsReq.id); // Resume send(conn, msgId++); expectNotification(conn); // [2] line 5, pause on exception expectPaused(conn, "exception", {{"global", 5, 1}}); send(conn, msgId++); expectNotification(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(conn, msgId++); expectNotification(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(conn, msgId++); expectExecutionContextCreated(conn); expectNotification(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(conn, allExceptionsReq.id); // Resume send(conn, msgId++); expectNotification(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(conn, msgId++); expectExecutionContextCreated(conn); expectNotification(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(conn, allExceptionsReq.id); // Resume send(conn, msgId++); expectNotification(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(conn, msgId++); expectNotification(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(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(conn, msgId++); expectExecutionContextCreated(conn); expectNotification(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(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(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(conn, uncaughtExceptionsReq.id); sendEvalRequest(conn, msgId + 1, 0, shouldPauseOnThrowEvalMsg); expectEvalResponse(conn, msgId + 1, responseTrue); // Resume send(conn, msgId++); expectNotification(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(conn, msgId++); expectExecutionContextCreated(conn); expectNotification(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(conn, req.id); std::unordered_map> 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(conn, msgId++); expectNotification(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(conn, msgId++); expectExecutionContextCreated(conn); expectNotification(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(conn, msgId++); expectNotification(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(conn, msgId++); expectExecutionContextCreated(conn); expectNotification(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(conn, req.id); std::unordered_map> 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(conn, msgId++); expectNotification(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(conn, msgId++); expectExecutionContextCreated(conn); auto scriptParsed1 = expectNotification(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(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(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(conn, msgId++); expectNotification(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(conn, msgId++); expectNotification(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(conn, msgId++); expectNotification(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(conn, msgId++); expectExecutionContextCreated(conn); auto scriptParsed1 = expectNotification(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(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(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(conn, msgId++); expectNotification(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(conn, msgId++); expectNotification(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(conn, msgId++); expectNotification(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(conn, msgId++); expectExecutionContextCreated(conn); expectNotification(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(conn, msgId++); expectNotification(conn); expectPaused( conn, "other", {FrameInfo("foo", 1, 2).setColumnNumber(16), FrameInfo("global", 1, 1)}); // Resume execution send(conn, msgId++); expectNotification(conn); } TEST(ConnectionTests, canBreakOnScriptsWithSourceMap) { TestContext context; AsyncHermesRuntime &asyncRuntime = context.runtime(); SyncConnection &conn = context.conn(); int msgId = 1; send(conn, msgId++); expectExecutionContextCreated(conn); m::debugger::SetInstrumentationBreakpointRequest req; req.id = msgId++; req.instrumentation = "beforeScriptWithSourceMapExecution"; conn.send(req.toJson()); auto bpId = expectResponse( conn, req.id) .breakpointId; asyncRuntime.executeScriptAsync(R"( storeValue(42); debugger; //# sourceURL=http://example.com/source.js //# sourceMappingURL=http://example.com/source.map )"); expectNotification(conn); // We should get a pause before the first statement auto note = expectNotification(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(conn, msgId++); expectNotification(conn); expectNotification(conn); EXPECT_EQ(asyncRuntime.awaitStoredValue().asNumber(), 42); // Resume and exit send(conn, msgId++); expectNotification(conn); } TEST(ConnectionTests, wontStopOnFilesWithoutSourceMaps) { TestContext context; AsyncHermesRuntime &asyncRuntime = context.runtime(); SyncConnection &conn = context.conn(); int msgId = 1; send(conn, msgId++); expectExecutionContextCreated(conn); m::debugger::SetInstrumentationBreakpointRequest req; req.id = msgId++; req.instrumentation = "beforeScriptWithSourceMapExecution"; conn.send(req.toJson()); expectResponse( 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(conn); // Continue and verify that the JS code has now executed without first // pausing on the script load. expectNotification(conn); EXPECT_EQ(asyncRuntime.awaitStoredValue().asNumber(), 42); // Resume and exit send(conn, msgId++); expectNotification(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(conn, ++msgId); expectExecutionContextCreated(conn); expectNotification(conn); expectNotification(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(conn, ++msgId); expectNotification(conn); // We should immediately hit the 'debugger;' statement expectNotification(conn); EXPECT_EQ(1, asyncRuntime.awaitStoredValue().asNumber()); // RunIfWaitingForDebuggerResponse should be accepted but have no effect send(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(conn, msgId++); expectNotification(conn); } TEST(ConnectionTests, heapProfilerSampling) { TestContext context; AsyncHermesRuntime &asyncRuntime = context.runtime(); SyncConnection &conn = context.conn(); int msgId = 1; send(conn, msgId++); expectExecutionContextCreated(conn); asyncRuntime.executeScriptAsync(R"( debugger; function allocator() { // Do some allocation. return new Object; } (function main() { var a = []; for (var i = 0; i < 100; i++) { a[i] = allocator(); } })(); debugger; )"); expectNotification(conn); // We should get a pause before the first statement. expectNotification(conn); { m::heapProfiler::StartSamplingRequest req; req.id = msgId++; // Sample every 256 bytes to ensure there are some samples. The default is // 32768, which is too high for a small example. Note that sampling is a // random process, so there's no guarantee there will be any samples in any // finite number of allocations. In practice the likelihood is so high that // there shouldn't be any issues. req.samplingInterval = 256; send(conn, req); } // Resume, run the allocations, and once it's paused again, stop them. send(conn, msgId++); expectNotification(conn); expectNotification(conn); // Send the stop sampling request, expect the value coming back to be JSON. auto resp = send< m::heapProfiler::StopSamplingRequest, m::heapProfiler::StopSamplingResponse>(conn, msgId++); // Make sure there were some samples. EXPECT_NE(resp.profile.samples.size(), 0); // Don't test the content of the JSON, that is tested via the // SamplingHeapProfilerTest. // Resume and exit send(conn, msgId++); expectNotification(conn); } TEST(ConnectionTests, heapSnapshotRemoteObject) { TestContext context; AsyncHermesRuntime &asyncRuntime = context.runtime(); std::shared_ptr runtime = asyncRuntime.runtime(); SyncConnection &conn = context.conn(); int msgId = 1; send(conn, msgId++); expectExecutionContextCreated(conn); asyncRuntime.executeScriptAsync(R"( storeValue([1, 2, 3]); debugger; )"); expectNotification(conn); // We should get a pause before the first statement. expectNotification(conn); { // Take a heap snapshot first to assign IDs. m::heapProfiler::TakeHeapSnapshotRequest req; req.id = msgId++; req.reportProgress = false; // We don't need the response because we can directly query for object IDs // from the runtime. send(conn, req); } const uint64_t globalObjID = runtime->getUniqueID(runtime->global()); jsi::Value storedValue = asyncRuntime.awaitStoredValue(); const uint64_t storedObjID = runtime->getUniqueID(storedValue.asObject(*runtime)); auto testObject = [&msgId, &conn]( uint64_t objID, const char *type, const char *className, const char *description, const char *subtype) { // Get the object by its snapshot ID. m::heapProfiler::GetObjectByHeapObjectIdRequest req; req.id = msgId++; req.objectId = std::to_string(objID); auto resp = send< m::heapProfiler::GetObjectByHeapObjectIdRequest, m::heapProfiler::GetObjectByHeapObjectIdResponse>(conn, req); EXPECT_EQ(resp.result.type, type); EXPECT_EQ(resp.result.className, className); EXPECT_EQ(resp.result.description, description); if (subtype) { EXPECT_EQ(resp.result.subtype, subtype); } // Check that fetching the object by heap snapshot ID works. m::heapProfiler::GetHeapObjectIdRequest idReq; idReq.id = msgId++; idReq.objectId = resp.result.objectId.value(); auto idResp = send< m::heapProfiler::GetHeapObjectIdRequest, m::heapProfiler::GetHeapObjectIdResponse>(conn, idReq); EXPECT_EQ(atoi(idResp.heapSnapshotObjectId.c_str()), objID); }; // Test once before a collection. testObject(globalObjID, "object", "Object", "Object", nullptr); testObject(storedObjID, "object", "Array", "Array(3)", "array"); // Force a collection to move the heap. runtime->instrumentation().collectGarbage("test"); // A collection should not disturb the unique ID lookup, and it should be the // same object as before. Note that it won't have the same remote ID, because // Hermes doesn't do uniquing. testObject(globalObjID, "object", "Object", "Object", nullptr); testObject(storedObjID, "object", "Array", "Array(3)", "array"); // Resume and exit. send(conn, msgId++); expectNotification(conn); } } // namespace chrome } // namespace inspector } // namespace hermes } // namespace facebook