/* * 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 #include #include #include #include #include #include #include #include namespace facebook { namespace hermes { namespace inspector { namespace debugger = facebook::hermes::debugger; using namespace std::chrono_literals; using Unit = folly::Unit; static auto constexpr kDefaultTimeout = 5000ms; namespace { int getCurrentLine(const debugger::ProgramState &state) { return state.getStackTrace().callFrameForIndex(0).location.line; } debugger::SourceLocation locationForLine(int line) { debugger::SourceLocation loc; loc.line = line; return loc; } } // namespace /* * LambdaInspectorObserver is useful for sequencing calls to the debugger based * on the number of onPause() callbacks. */ using OnPauseFunction = std::function; class LambdaInspectorObserver : public InspectorObserver { public: LambdaInspectorObserver(OnPauseFunction func) : onPauseFunc_(func), pauseCount_(0) {} ~LambdaInspectorObserver() = default; void onBreakpointResolved( Inspector &inspector, const debugger::BreakpointInfo &info) override {} void onContextCreated(Inspector &inspector) override {} void onPause(Inspector &inspector, const debugger::ProgramState &state) override { pauseCount_++; onPauseFunc_(inspector, state, pauseCount_); } void onResume(Inspector &inspector) override {} void onScriptParsed(Inspector &inspector, const ScriptInfo &info) override {} void onMessageAdded(Inspector &inspector, const ConsoleMessageInfo &info) override{}; int getPauseCount() { return pauseCount_; } private: OnPauseFunction onPauseFunc_; int pauseCount_; }; /* * Helpers for running JS in a separate thread. */ struct HermesDebugContext { HermesDebugContext( InspectorObserver &observer, folly::Future &&finished) : runtime(makeHermesRuntime()), inspector( std::make_shared( runtime, runtime->getDebugger()), observer, false), stopFlag(false), finished(std::move(finished)) { runtime->global().setProperty( *runtime, "shouldStop", jsi::Function::createFromHostFunction( *runtime, jsi::PropNameID::forAscii(*runtime, "shouldStop"), 0, [this]( jsi::Runtime &, const jsi::Value &, const jsi::Value *args, size_t count) { return stopFlag.load() ? jsi::Value(true) : jsi::Value(false); })); } ~HermesDebugContext() = default; void setStopFlag() { stopFlag.store(true); } void wait(std::chrono::milliseconds timeout = kDefaultTimeout) { std::move(finished).get(timeout); } std::shared_ptr runtime; Inspector inspector; std::atomic stopFlag{}; folly::Future finished; }; static std::shared_ptr runScriptAsync( InspectorObserver &observer, const std::string &script) { auto promise = std::make_shared>(); auto future = promise->getFuture(); auto context = std::make_shared(observer, std::move(future)); std::thread t([=]() { HermesRuntime::DebugFlags flags{}; context->runtime->debugJavaScript(script, "url", flags); promise->setValue(); }); t.detach(); return context; } /* * Tests */ TEST(InspectorTests, testStepOver) { std::string script = R"( var a = 1 + 2; debugger; var b = a / 2; var c = a + b; var d = b - c; var e = c * d; var f = 10; )"; // TODO: move this vector into lambdaInspectorObserver std::vector> futures; OnPauseFunction onPauseFunc = [&futures]( Inspector &inspector, const debugger::ProgramState &state, int pauseCount) { switch (pauseCount) { case 1: { EXPECT_EQ( state.getPauseReason(), debugger::PauseReason::DebuggerStatement); EXPECT_EQ(getCurrentLine(state), 3); futures.emplace_back(inspector.stepOver()); break; } case 2: { EXPECT_EQ(state.getPauseReason(), debugger::PauseReason::StepFinish); EXPECT_EQ(getCurrentLine(state), 4); futures.emplace_back(inspector.stepOver()); break; } case 3: { EXPECT_EQ(state.getPauseReason(), debugger::PauseReason::StepFinish); EXPECT_EQ(getCurrentLine(state), 5); futures.emplace_back(inspector.resume()); break; } } }; LambdaInspectorObserver observer(onPauseFunc); std::shared_ptr context = runScriptAsync(observer, script); // TODO: temporarily do this to ensure i hit failure case std::this_thread::sleep_for(1000ms); futures.emplace_back(context->inspector.enable()); context->wait(); folly::collectAll(futures).get(kDefaultTimeout); EXPECT_EQ(observer.getPauseCount(), 3); } TEST(InspectorTests, testSetBreakpoint) { std::string script = R"( var a = 1 + 2; debugger; var b = a / 2; var c = a + b; var d = b - c; var e = c * d; var f = 10; )"; std::vector> futures; OnPauseFunction onPauseFunc = [&futures]( Inspector &inspector, const debugger::ProgramState &state, int pauseCount) { switch (pauseCount) { case 1: { EXPECT_EQ( state.getPauseReason(), debugger::PauseReason::DebuggerStatement); EXPECT_EQ(getCurrentLine(state), 3); auto stepFuture = inspector.stepOver(); futures.emplace_back(std::move(stepFuture)); break; } case 2: { EXPECT_EQ(state.getPauseReason(), debugger::PauseReason::StepFinish); EXPECT_EQ(getCurrentLine(state), 4); auto breakpointFuture = inspector.setBreakpoint(locationForLine(6)); futures.emplace_back(std::move(breakpointFuture) .thenValue([](debugger::BreakpointInfo info) { EXPECT_EQ(info.resolvedLocation.line, 6); })); auto resumeFuture = inspector.resume(); futures.emplace_back(std::move(resumeFuture)); break; } case 3: { EXPECT_EQ(state.getPauseReason(), debugger::PauseReason::Breakpoint); EXPECT_EQ(getCurrentLine(state), 6); auto resumeFuture = inspector.resume(); futures.emplace_back(std::move(resumeFuture)); break; } } }; LambdaInspectorObserver observer(onPauseFunc); std::shared_ptr context = runScriptAsync(observer, script); auto enablePromise = context->inspector.enable(); futures.emplace_back(std::move(enablePromise)); context->wait(); folly::collectAll(futures).get(kDefaultTimeout); EXPECT_EQ(observer.getPauseCount(), 3); } TEST(InspectorTests, testAsyncSetBreakpoint) { std::string script = R"( while (!shouldStop()) { var a = 1; var b = 2; var c = a + b; var d = 10; } )"; std::vector> futures; folly::Func stopFunc; OnPauseFunction onPauseFunc = [&futures, &stopFunc]( Inspector &inspector, const debugger::ProgramState &state, int pauseCount) { switch (pauseCount) { case 1: { EXPECT_EQ(state.getPauseReason(), debugger::PauseReason::Breakpoint); EXPECT_EQ(getCurrentLine(state), 4); auto stepFuture = inspector.stepOver(); futures.emplace_back(std::move(stepFuture)); break; } case 2: { EXPECT_EQ(state.getPauseReason(), debugger::PauseReason::StepFinish); EXPECT_EQ(getCurrentLine(state), 5); stopFunc(); auto resumeFuture = inspector.resume(); futures.emplace_back(std::move(resumeFuture)); break; } } }; LambdaInspectorObserver observer(onPauseFunc); std::shared_ptr context = runScriptAsync(observer, script); stopFunc = [context]() { context->setStopFlag(); }; context->inspector.enable(); auto breakpointPromise = context->inspector.setBreakpoint(locationForLine(4)) .thenValue([](debugger::BreakpointInfo info) { EXPECT_EQ(info.resolvedLocation.line, 4); }); context->wait(); futures.emplace_back(std::move(breakpointPromise)); folly::collectAll(futures).get(kDefaultTimeout); EXPECT_EQ(observer.getPauseCount(), 2); } TEST(InspectorTests, testDisable) { std::string script = R"( var a = 1 + 2; debugger; var b = a / 2; var c = a + b; var d = b - c; var e = c * d; var f = 10; )"; std::vector> futures; OnPauseFunction onPauseFunc = [&futures]( Inspector &inspector, const debugger::ProgramState &state, int pauseCount) { switch (pauseCount) { case 1: { EXPECT_EQ( state.getPauseReason(), debugger::PauseReason::DebuggerStatement); EXPECT_EQ(getCurrentLine(state), 3); auto stepFuture = inspector.stepOver(); futures.emplace_back(std::move(stepFuture)); break; } case 2: { EXPECT_EQ(state.getPauseReason(), debugger::PauseReason::StepFinish); EXPECT_EQ(getCurrentLine(state), 4); futures.emplace_back(inspector.setBreakpoint(locationForLine(6)) .thenValue([](debugger::BreakpointInfo info) { EXPECT_EQ(info.resolvedLocation.line, 6); })); futures.emplace_back(inspector.setBreakpoint(locationForLine(7)) .thenValue([](debugger::BreakpointInfo info) { EXPECT_EQ(info.resolvedLocation.line, 7); })); auto detachFuture = inspector.disable(); futures.emplace_back(std::move(detachFuture)); break; } } }; LambdaInspectorObserver observer(onPauseFunc); std::shared_ptr context = runScriptAsync(observer, script); auto enablePromise = context->inspector.enable(); futures.emplace_back(std::move(enablePromise)); context->wait(); folly::collectAll(futures).get(kDefaultTimeout); EXPECT_EQ(observer.getPauseCount(), 2); } } // namespace inspector } // namespace hermes } // namespace facebook