#include "zeroerr/assert.h" #include "zeroerr/unittest.h" #include "common/Capability.h" #include "common/IStateProvider.h" #include "common/Module.h" #include "common/StateValue.h" #include "devtools/Debugger.hpp" #include "devtools/DevTool.hpp" #include "devtools/Snapshot.hpp" #include #include #include #include #include #include using namespace eve::dev; TEST_CASE("devtools.debugger.pauseResumeStepFrame") { Debugger& d = Debugger::instance(); d.detach(); CHECK(!d.isPaused()); CHECK(d.shouldRunUpdate()); d.pause(PauseReason::PauseKey); CHECK(d.isPaused()); CHECK(!d.shouldRunUpdate()); CHECK(static_cast(d.lastPauseReason()) == static_cast(PauseReason::PauseKey)); d.stepFrame(); CHECK(d.shouldRunUpdate()); d.notifyFrameDone(); CHECK(d.isPaused()); CHECK(static_cast(d.lastPauseReason()) == static_cast(PauseReason::Step)); d.resume(); CHECK(!d.isPaused()); CHECK(d.shouldRunUpdate()); } TEST_CASE("devtools.debugger.breakpointsMatchBasename") { Debugger& d = Debugger::instance(); d.detach(); d.clearBreakpoints(); const int id = d.setBreakpoint("scripts/main.nut", 42); CHECK(id > 0); CHECK(d.hasBreakpoint("main.nut", 42)); CHECK(d.hasBreakpoint("scripts/main.nut", 42)); REQUIRE(!d.hasBreakpoint("/game/other/main.nut", 42)); CHECK(!d.hasBreakpoint("main.nut", 43)); CHECK(d.clearBreakpoint("main.nut", 42)); CHECK(!d.hasBreakpoint("main.nut", 42)); } TEST_CASE("devtools.debugger.onScriptLineBreakpointAndStep") { Debugger& d = Debugger::instance(); d.detach(); d.clearBreakpoints(); d.setBreakpoint("t.nut", 10); SourceLoc loc; loc.source = "t.nut"; loc.line = 9; CHECK(!d.onScriptLine(loc)); loc.line = 10; CHECK(d.onScriptLine(loc)); CHECK(d.isPaused()); CHECK(static_cast(d.lastPauseReason()) == static_cast(PauseReason::Breakpoint)); // No VM stack → stepInto/Over open as StepInto and stop on next line. d.stepInto(); CHECK(static_cast(d.mode()) == static_cast(RunMode::StepInto)); loc.line = 11; CHECK(d.onScriptLine(loc)); CHECK(d.isPaused()); CHECK(static_cast(d.lastPauseReason()) == static_cast(PauseReason::Step)); // Smart step from a script stop → stepOver (depth 0 ⇒ StepInto). d.step(); CHECK(static_cast(d.mode()) == static_cast(RunMode::StepInto)); loc.line = 12; CHECK(d.onScriptLine(loc)); CHECK(d.isPaused()); d.resume(); // Frame-level pause → smart step is stepFrame. d.pause(PauseReason::PauseKey); d.step(); CHECK(static_cast(d.mode()) == static_cast(RunMode::StepFrame)); d.notifyFrameDone(); CHECK(d.isPaused()); d.resume(); } namespace { struct HookState { Debugger* dbg = nullptr; int stops = 0; int lastLine = -1; std::string lastFunc; }; void testDebugHook(HSQUIRRELVM v, SQInteger type, const SQChar* sourcename, SQInteger line, const SQChar* funcname) { auto* st = static_cast(sq_getforeignptr(v)); if (!st || !st->dbg) return; if (type != 'l') return; SourceLoc loc; loc.source = sourcename ? sourcename : ""; loc.line = static_cast(line); loc.function = funcname ? funcname : ""; if (st->dbg->onScriptLine(loc)) { ++st->stops; st->lastLine = loc.line; st->lastFunc = loc.function; // Drive step from the hook without waitWhilePaused. // First stop: stepOver; second stop ends the test script. if (st->stops == 1) { st->dbg->stepOver(); } else { st->dbg->resume(); } } } } // namespace TEST_CASE("devtools.debugger.stepOverSkipsCalleeLines") { ssq::VM vm(1024, ssq::Libs::ALL); HSQUIRRELVM v = vm.getHandle(); Debugger& d = Debugger::instance(); d.detach(); d.clearBreakpoints(); d.attach(v); // Lines roughly: // 1 function callee() { local x = 1 } // 2 function caller() { callee(); local y = 2 } // 3 caller() const char* src = "function callee() {\n" " local x = 1\n" "}\n" "function caller() {\n" " callee()\n" " local y = 2\n" "}\n" "caller()\n"; HookState st; st.dbg = &d; sq_setforeignptr(v, &st); sq_enabledebuginfo(v, SQTrue); sq_setnativedebughook(v, testDebugHook); d.setBreakpoint("buffer", 5); // callee() call site inside caller // Normalize: compile as named buffer SQRESULT rc = sq_compilebuffer(v, src, static_cast(std::strlen(src)), _SC("buffer"), SQTrue); REQUIRE(SQ_SUCCEEDED(rc)); sq_pushroottable(v); REQUIRE(SQ_SUCCEEDED(sq_call(v, 1, SQFalse, SQTrue))); sq_poptop(v); // Hit bp on call line, stepOver, then stop on `local y = 2` (not inside callee). CHECK(st.stops >= 2); CHECK_EQ(st.lastLine, 6); CHECK(st.lastFunc.find("caller") != std::string::npos); sq_setnativedebughook(v, nullptr); sq_setforeignptr(v, nullptr); d.detach(); } TEST_CASE("devtools.debugger.stepIntoEntersCallee") { ssq::VM vm(1024, ssq::Libs::ALL); HSQUIRRELVM v = vm.getHandle(); Debugger& d = Debugger::instance(); d.detach(); d.clearBreakpoints(); d.attach(v); const char* src = "function callee() {\n" " local x = 1\n" "}\n" "function caller() {\n" " callee()\n" " local y = 2\n" "}\n" "caller()\n"; struct IntoState { Debugger* dbg = nullptr; int stops = 0; int lastLine = -1; std::string lastFunc; } st; st.dbg = &d; auto hook = [](HSQUIRRELVM vmh, SQInteger type, const SQChar* sourcename, SQInteger line, const SQChar* funcname) { auto* s = static_cast(sq_getforeignptr(vmh)); if (!s || !s->dbg || type != 'l') return; SourceLoc loc; loc.source = sourcename ? sourcename : ""; loc.line = static_cast(line); loc.function = funcname ? funcname : ""; if (s->dbg->onScriptLine(loc)) { ++s->stops; s->lastLine = loc.line; s->lastFunc = loc.function; if (s->stops == 1) s->dbg->stepInto(); else s->dbg->resume(); } }; sq_setforeignptr(v, &st); sq_enabledebuginfo(v, SQTrue); sq_setnativedebughook(v, +hook); d.setBreakpoint("buffer", 5); REQUIRE(SQ_SUCCEEDED( sq_compilebuffer(v, src, static_cast(std::strlen(src)), _SC("buffer"), SQTrue))); sq_pushroottable(v); REQUIRE(SQ_SUCCEEDED(sq_call(v, 1, SQFalse, SQTrue))); sq_poptop(v); CHECK(st.stops >= 2); CHECK_EQ(st.lastLine, 2); // first line body of callee CHECK(st.lastFunc.find("callee") != std::string::npos); sq_setnativedebughook(v, nullptr); sq_setforeignptr(v, nullptr); d.detach(); } TEST_CASE("devtools.debugger.watchesAndEvaluate") { ssq::VM vm(1024, ssq::Libs::ALL); HSQUIRRELVM v = vm.getHandle(); // Seed roottable slots. { const SQInteger top = sq_gettop(v); sq_pushroottable(v); sq_pushstring(v, "score", -1); sq_pushinteger(v, 7); sq_newslot(v, -3, SQFalse); sq_pushstring(v, "player", -1); sq_newtable(v); sq_pushstring(v, "hp", -1); sq_pushinteger(v, 100); sq_newslot(v, -3, SQFalse); sq_newslot(v, -3, SQFalse); sq_settop(v, top); } Debugger& d = Debugger::instance(); d.attach(v); d.clearWatches(); d.addWatch("score"); d.addWatch("player.hp"); d.refreshWatches(); auto watches = d.watches(); REQUIRE(watches.size() == 2u); CHECK(watches[0].ok); CHECK(watches[0].value.find("7") != std::string::npos); CHECK(watches[1].ok); CHECK(watches[1].value.find("100") != std::string::npos); auto ev = d.evaluate("score"); CHECK_EQ(ev.type, std::string("integer")); CHECK(ev.value.find("7") != std::string::npos); CHECK(d.removeWatch("score")); d.clearWatches(); CHECK(d.watches().empty()); d.detach(); } TEST_CASE("devtools.snapshot.roundTripRoots") { ssq::VM vm(1024, ssq::Libs::ALL); HSQUIRRELVM v = vm.getHandle(); { const SQInteger top = sq_gettop(v); sq_pushroottable(v); sq_pushstring(v, "gameState", -1); sq_newtable(v); sq_pushstring(v, "level", -1); sq_pushinteger(v, 3); sq_newslot(v, -3, SQFalse); sq_pushstring(v, "name", -1); sq_pushstring(v, "hero", -1); sq_newslot(v, -3, SQFalse); sq_newslot(v, -3, SQFalse); sq_settop(v, top); } Snapshot& snap = Snapshot::instance(); snap.clearRoots(); // Prefer conventional gameState without markRoot. std::string err; const std::string json = snap.capture(v, &err); CHECK(err.empty()); CHECK(!json.empty()); CHECK(json.find("gameState") != std::string::npos); CHECK(json.find("hero") != std::string::npos); // Mutate then restore. { const SQInteger top = sq_gettop(v); sq_pushroottable(v); sq_pushstring(v, "gameState", -1); sq_newtable(v); sq_pushstring(v, "level", -1); sq_pushinteger(v, 99); sq_newslot(v, -3, SQFalse); sq_newslot(v, -3, SQFalse); sq_settop(v, top); } err.clear(); CHECK(snap.restore(v, json, &err)); CHECK(err.empty()); { const SQInteger top = sq_gettop(v); sq_pushroottable(v); sq_pushstring(v, "gameState", -1); REQUIRE(SQ_SUCCEEDED(sq_get(v, -2))); sq_pushstring(v, "level", -1); REQUIRE(SQ_SUCCEEDED(sq_get(v, -2))); SQInteger level = 0; sq_getinteger(v, -1, &level); CHECK_EQ(static_cast(level), 3); sq_settop(v, top); } const char* path = "eve_test_snapshot.json"; CHECK(snap.saveFile(v, path, &err)); CHECK(snap.loadFile(v, path, &err)); std::remove(path); } TEST_CASE("devtools.snapshot.markRootAndSkipEngine") { CHECK(Snapshot::isEngineBinding("eve")); CHECK(Snapshot::isEngineBinding("gfx")); CHECK(!Snapshot::isEngineBinding("gameState")); Snapshot& snap = Snapshot::instance(); snap.clearRoots(); snap.markRoot("custom"); auto roots = snap.roots(); REQUIRE(roots.size() == 1u); CHECK_EQ(roots[0], std::string("custom")); snap.unmarkRoot("custom"); CHECK(snap.roots().empty()); } namespace { /** Fake provider: reports a fixed value and remembers what it restored. */ class MockStateProvider : public eve::caps::IStateProvider { public: const char* stateKind() const override { return "mock"; } bool captureState(eve::StateValue& out) override { out = eve::StateValue::object(); out.set("value", eve::StateValue::integer(7)); return true; } bool restoreState(const eve::StateValue& in, std::string* /*err*/) override { lastRestored = in; return true; } bool resetToDefaults() override { return true; } eve::StateValue lastRestored; }; } // namespace TEST_CASE("devtools.snapshot.v2NativeSection") { ssq::VM vm(1024, ssq::Libs::ALL); HSQUIRRELVM v = vm.getHandle(); { const SQInteger top = sq_gettop(v); sq_pushroottable(v); sq_pushstring(v, "gameState", -1); sq_newtable(v); sq_pushstring(v, "level", -1); sq_pushinteger(v, 3); sq_newslot(v, -3, SQFalse); sq_newslot(v, -3, SQFalse); sq_settop(v, top); } MockStateProvider provider; eve::cap::addListener(&provider); Snapshot& snap = Snapshot::instance(); snap.clearRoots(); std::string err; const std::string json = snap.capture(v, &err); CHECK(err.empty()); CHECK(json.find("version") != std::string::npos); CHECK(json.find("native") != std::string::npos); CHECK(json.find("mock") != std::string::npos); CHECK(json.find("gameState") != std::string::npos); // Mutate script state, then restore; both the script root and the native // provider state come back. { const SQInteger top = sq_gettop(v); sq_pushroottable(v); sq_pushstring(v, "gameState", -1); sq_newtable(v); sq_pushstring(v, "level", -1); sq_pushinteger(v, 99); sq_newslot(v, -3, SQFalse); sq_newslot(v, -3, SQFalse); sq_settop(v, top); } err.clear(); CHECK(snap.restore(v, json, &err)); CHECK(err.empty()); CHECK(provider.lastRestored.isObject()); const eve::StateValue* value = provider.lastRestored.find("value"); REQUIRE(value != nullptr); CHECK_EQ(value->asInt(), int64_t(7)); { const SQInteger top = sq_gettop(v); sq_pushroottable(v); sq_pushstring(v, "gameState", -1); REQUIRE(SQ_SUCCEEDED(sq_get(v, -2))); sq_pushstring(v, "level", -1); REQUIRE(SQ_SUCCEEDED(sq_get(v, -2))); SQInteger level = 0; sq_getinteger(v, -1, &level); CHECK_EQ(static_cast(level), 3); sq_settop(v, top); } eve::cap::removeListener(&provider); } TEST_CASE("devtools.snapshot.v1Compat") { ssq::VM vm(1024, ssq::Libs::ALL); HSQUIRRELVM v = vm.getHandle(); const std::string v1 = R"({"version":1,"roots":{"gameState":{"level":3}}})"; Snapshot& snap = Snapshot::instance(); snap.clearRoots(); std::string err; CHECK(snap.restore(v, v1, &err)); CHECK(err.empty()); const SQInteger top = sq_gettop(v); sq_pushroottable(v); sq_pushstring(v, "gameState", -1); REQUIRE(SQ_SUCCEEDED(sq_get(v, -2))); sq_pushstring(v, "level", -1); REQUIRE(SQ_SUCCEEDED(sq_get(v, -2))); SQInteger level = 0; sq_getinteger(v, -1, &level); CHECK_EQ(static_cast(level), 3); sq_settop(v, top); } TEST_CASE("devtools.snapshot.rejectsFunctionInRoot") { ssq::VM vm(1024, ssq::Libs::ALL); HSQUIRRELVM v = vm.getHandle(); ssq::Script s = vm.compileSource("gameState <- { level = 3 }; gameState.tick <- function() { return 1; };"); vm.run(s); Snapshot& snap = Snapshot::instance(); snap.clearRoots(); eve::StateValue out; std::string err; CHECK(!snap.captureState(v, out, &err)); CHECK(err.find("non-serializable") != std::string::npos); CHECK(err.find("gameState") != std::string::npos); } TEST_CASE("devtools.snapshot.rejectsInstanceRoot") { ssq::VM vm(1024, ssq::Libs::ALL); HSQUIRRELVM v = vm.getHandle(); ssq::Script s = vm.compileSource("class Hero { level = 1; } gameState <- Hero();"); vm.run(s); Snapshot& snap = Snapshot::instance(); snap.clearRoots(); eve::StateValue out; std::string err; CHECK(!snap.captureState(v, out, &err)); CHECK(err.find("class instance") != std::string::npos); } TEST_CASE("devtools.snapshot.skipsInstanceRootInAutoScan") { ssq::VM vm(1024, ssq::Libs::ALL); HSQUIRRELVM v = vm.getHandle(); ssq::Script s = vm.compileSource("class Hero { level = 1; } hero <- Hero(); gameState <- { level = 3 };"); vm.run(s); Snapshot& snap = Snapshot::instance(); snap.clearRoots(); eve::StateValue out; std::string err; // Class instances at the root (module singletons, ECS worlds, ...) must not // abort the automatic state scan; plain-data roots are still captured. CHECK(snap.captureState(v, out, &err)); CHECK(err.empty()); const eve::StateValue* roots = out.find("roots"); REQUIRE(roots != nullptr); CHECK(roots->find("gameState") != nullptr); CHECK(roots->find("hero") == nullptr); } TEST_CASE("devtools.debugger.normalizeSource") { CHECK_EQ(Debugger::normalizeSource("file://./a/b.nut"), std::string("a/b.nut")); CHECK_EQ(Debugger::normalizeSource("file://localhost/Users/x/main.nut"), std::string("/Users/x/main.nut")); CHECK_EQ(Debugger::normalizeSource("C:\\x\\y.nut"), std::string("C:/x/y.nut")); CHECK(Debugger::sourcesMatch("/abs/game/main.nut", "main.nut")); CHECK(Debugger::sourcesMatch("/abs/game/scripts/a.nut", "scripts/a.nut")); CHECK(!Debugger::sourcesMatch("foo.nut", "barfoo.nut")); REQUIRE(!Debugger::sourcesMatch("/proj/a/main.nut", "/proj/b/main.nut")); REQUIRE(!Debugger::sourcesMatch("scripts/main.nut", "other/main.nut")); } TEST_CASE("devtools.debugger.clearBreakpointsMatchesAlias") { Debugger& d = Debugger::instance(); d.detach(); d.clearBreakpoints(); d.setBreakpoint("/abs/game/main.nut", 10); // VS Code may clear using the same absolute path, or a relative form. d.clearBreakpoints("main.nut"); CHECK(!d.hasBreakpoint("/abs/game/main.nut", 10)); } TEST_CASE("devtools.debugger.stepOverSkipsSameLine") { Debugger& d = Debugger::instance(); d.detach(); d.clearBreakpoints(); SourceLoc loc; loc.source = "t.nut"; loc.line = 10; d.setBreakpoint("t.nut", 10); CHECK(d.onScriptLine(loc)); CHECK(d.isPaused()); d.stepOver(); // Extra _OP_LINE on the same source line must not stop again. CHECK(!d.onScriptLine(loc)); CHECK(!d.isPaused()); { const int m = static_cast(d.mode()); const bool stepping = m == static_cast(RunMode::StepInto) || m == static_cast(RunMode::StepOver); CHECK(stepping); } loc.line = 11; CHECK(d.onScriptLine(loc)); CHECK(d.isPaused()); CHECK(static_cast(d.lastPauseReason()) == static_cast(PauseReason::Step)); d.resume(); } TEST_CASE("devtools.debugger.conditionalBreakpoint") { ssq::VM vm(1024, ssq::Libs::ALL); HSQUIRRELVM v = vm.getHandle(); Debugger& d = Debugger::instance(); d.detach(); d.clearBreakpoints(); d.attach(v); // Seed roottable flag used by the condition. { const SQInteger top = sq_gettop(v); sq_pushroottable(v); sq_pushstring(v, "flag", -1); sq_pushinteger(v, 1); sq_newslot(v, -3, SQFalse); sq_settop(v, top); } d.setBreakpoint("t.nut", 10, true, "flag == 2"); SourceLoc loc; loc.source = "t.nut"; loc.line = 10; // flag == 1: condition false -> keep running. CHECK(!d.onScriptLine(loc)); CHECK(!d.isPaused()); // flag == 2: condition true -> stop. { const SQInteger top = sq_gettop(v); sq_pushroottable(v); sq_pushstring(v, "flag", -1); sq_pushinteger(v, 2); sq_newslot(v, -3, SQFalse); sq_settop(v, top); } CHECK(d.onScriptLine(loc)); CHECK(d.isPaused()); CHECK(static_cast(d.lastPauseReason()) == static_cast(PauseReason::Breakpoint)); d.resume(); d.detach(); } TEST_CASE("devtools.debugger.expressionEvaluateAndVariableTree") { ssq::VM vm(1024, ssq::Libs::ALL); HSQUIRRELVM v = vm.getHandle(); Debugger& d = Debugger::instance(); d.detach(); d.attach(v); { const SQInteger top = sq_gettop(v); sq_pushroottable(v); sq_pushstring(v, "score", -1); sq_pushinteger(v, 7); sq_newslot(v, -3, SQFalse); sq_pushstring(v, "cfg", -1); sq_newtable(v); sq_pushstring(v, "max", -1); sq_pushinteger(v, 3); sq_newslot(v, -3, SQFalse); sq_newslot(v, -3, SQFalse); sq_pushstring(v, "list", -1); sq_newarray(v, 0); sq_pushinteger(v, 10); sq_arrayappend(v, -2); sq_pushinteger(v, 20); sq_arrayappend(v, -2); sq_pushinteger(v, 30); sq_arrayappend(v, -2); sq_newslot(v, -3, SQFalse); sq_settop(v, top); } // Full expression evaluation (arithmetic + member access + calls). auto r1 = d.evaluate("score * 2"); CHECK_EQ(r1.type, std::string("integer")); CHECK(r1.value.find("14") != std::string::npos); auto r2 = d.evaluate("cfg.max + 1"); CHECK_EQ(r2.type, std::string("integer")); CHECK(r2.value.find("4") != std::string::npos); auto r3 = d.evaluate("list.len()"); CHECK_EQ(r3.type, std::string("integer")); CHECK(r3.value.find("3") != std::string::npos); // Globals tree: containers are expandable. auto g = d.globals(); bool foundCfg = false; for (const auto& vv : g) { if (vv.name == "cfg") { foundCfg = vv.expandable; } } CHECK(foundCfg); auto cfgKids = d.containerChildren(VarKind::Globals, 0, {"cfg"}); REQUIRE(cfgKids.size() >= 1u); bool foundMax = false; for (const auto& vv : cfgKids) { if (vv.name == "max" && vv.value.find("3") != std::string::npos) foundMax = true; } CHECK(foundMax); auto listKids = d.containerChildren(VarKind::Globals, 0, {"list"}); REQUIRE(listKids.size() == 3u); CHECK(listKids[0].value.find("10") != std::string::npos); CHECK(listKids[2].value.find("30") != std::string::npos); d.detach(); } TEST_CASE("devtools.debugger.evaluateLocalsInFrame") { struct St { Debugger* dbg = nullptr; bool captured = false; std::string type; std::string value; }; auto hook = [](HSQUIRRELVM v, SQInteger type, const SQChar* sourcename, SQInteger line, const SQChar* funcname) { auto* st = static_cast(sq_getforeignptr(v)); if (!st || !st->dbg || type != 'l') return; SourceLoc loc; loc.source = sourcename ? sourcename : ""; loc.line = static_cast(line); loc.function = funcname ? funcname : ""; if (st->dbg->onScriptLine(loc)) { auto r = st->dbg->evaluate("x + y", 0); st->type = r.type; st->value = r.value; st->captured = true; st->dbg->resume(); } }; const char* src = "function f() {\n" // line 1 " local x = 5\n" // line 2 " local y = 10\n" // line 3 " local z = x + y\n" // line 4 <-- breakpoint "}\n" // line 5 "f()\n"; // line 6 ssq::VM vm(1024, ssq::Libs::ALL); HSQUIRRELVM v = vm.getHandle(); Debugger& d = Debugger::instance(); d.detach(); d.clearBreakpoints(); d.attach(v); d.setBreakpoint("buffer", 4); St st; st.dbg = &d; sq_setforeignptr(v, &st); sq_enabledebuginfo(v, SQTrue); sq_setnativedebughook(v, +hook); REQUIRE(SQ_SUCCEEDED( sq_compilebuffer(v, src, static_cast(std::strlen(src)), _SC("buffer"), SQTrue))); sq_pushroottable(v); REQUIRE(SQ_SUCCEEDED(sq_call(v, 1, SQFalse, SQTrue))); sq_poptop(v); CHECK(st.captured); CHECK_EQ(st.type, std::string("integer")); CHECK(st.value.find("15") != std::string::npos); sq_setnativedebughook(v, nullptr); sq_setforeignptr(v, nullptr); d.detach(); } TEST_CASE("devtools.debugger.breakOnErrorFlag") { Debugger& d = Debugger::instance(); d.detach(); CHECK(!d.breakOnError()); d.setBreakOnError(true); CHECK(d.breakOnError()); d.setBreakOnError(false); CHECK(!d.breakOnError()); } TEST_CASE("devtools.debugger.breakpointsEnabledMasterSwitch") { Debugger& d = Debugger::instance(); d.detach(); d.clearBreakpoints(); d.setBreakpoint("t.nut", 3); d.setBreakpointsEnabled(false); CHECK(d.hasBreakpoint("t.nut", 3)); // registered even while skip-all is on SourceLoc loc; loc.source = "t.nut"; loc.line = 3; CHECK(!d.onScriptLine(loc)); // skipped while master switch is off CHECK(!d.isPaused()); d.setBreakpointsEnabled(true); CHECK(d.onScriptLine(loc)); CHECK(d.isPaused()); d.resume(); } TEST_CASE("devtools.debugger.pauseAtRecordsLocation") { Debugger& d = Debugger::instance(); d.detach(); d.resume(); SourceLoc loc; loc.source = "err.nut"; loc.line = 12; loc.function = "boom"; d.pauseAt(PauseReason::Exception, loc); CHECK(d.isPaused()); CHECK_EQ(d.pauseLocation().source, std::string("err.nut")); CHECK_EQ(d.pauseLocation().line, 12); CHECK_EQ(d.pauseLocation().function, std::string("boom")); CHECK(static_cast(d.lastPauseReason()) == static_cast(PauseReason::Exception)); d.pauseAt(PauseReason::Snapshot, d.pauseLocation()); CHECK(d.isPaused()); CHECK(static_cast(d.lastPauseReason()) == static_cast(PauseReason::Snapshot)); CHECK_EQ(d.pauseLocation().line, 12); d.pause(PauseReason::PauseKey); CHECK(d.pauseLocation().empty()); CHECK(static_cast(d.lastPauseReason()) == static_cast(PauseReason::PauseKey)); d.resume(); } TEST_CASE("devtools.debugger.breakpointConditionReplaceAndEnableById") { Debugger& d = Debugger::instance(); d.detach(); d.clearBreakpoints(); d.setBreakpointsEnabled(true); const int id = d.setBreakpoint("t.nut", 10, true, "flag"); CHECK(id > 0); auto bps = d.breakpoints(); REQUIRE(!bps.empty()); CHECK_EQ(bps[0].condition, std::string("flag")); CHECK_EQ(d.setBreakpoint("t.nut", 10, true, ""), id); bps = d.breakpoints(); CHECK(bps[0].condition.empty()); CHECK(d.setBreakpointEnabled(id, false)); CHECK(!d.setBreakpointEnabled(99999, false)); CHECK(d.hasBreakpoint("t.nut", 10)); SourceLoc loc; loc.source = "t.nut"; loc.line = 10; CHECK(!d.onScriptLine(loc)); CHECK(!d.isPaused()); CHECK(d.setBreakpointEnabled(id, true)); CHECK(d.onScriptLine(loc)); CHECK(d.isPaused()); d.resume(); d.clearBreakpoints(); } TEST_CASE("devtools.debugger.conditionalBreakpointUsesLocals") { struct St { Debugger* dbg = nullptr; int stops = 0; }; auto hook = [](HSQUIRRELVM v, SQInteger type, const SQChar* sourcename, SQInteger line, const SQChar* funcname) { auto* st = static_cast(sq_getforeignptr(v)); if (!st || !st->dbg || type != 'l') return; SourceLoc loc; loc.source = sourcename ? sourcename : ""; loc.line = static_cast(line); loc.function = funcname ? funcname : ""; if (st->dbg->onScriptLine(loc)) { ++st->stops; st->dbg->resume(); } }; const char* body = "function f() {\n" // 1 " local n = 1\n" // 2 " n = 2\n" // 3 condition n == 2 should stop here " n = 3\n" // 4 "}\n" "f()\n"; ssq::VM vm(1024, ssq::Libs::ALL); HSQUIRRELVM v = vm.getHandle(); Debugger& d = Debugger::instance(); d.detach(); d.clearBreakpoints(); d.attach(v); d.setBreakpoint("buffer", 3, true, "n == 2"); d.setBreakpoint("buffer", 2, true, "n == 2"); // n is 1 here → skip St st; st.dbg = &d; sq_setforeignptr(v, &st); sq_enabledebuginfo(v, SQTrue); sq_setnativedebughook(v, +hook); REQUIRE(SQ_SUCCEEDED(sq_compilebuffer(v, body, static_cast(std::strlen(body)), _SC("buffer"), SQTrue))); sq_pushroottable(v); REQUIRE(SQ_SUCCEEDED(sq_call(v, 1, SQFalse, SQTrue))); sq_poptop(v); CHECK_EQ(st.stops, 1); sq_setnativedebughook(v, nullptr); sq_setforeignptr(v, nullptr); d.clearBreakpoints(); d.detach(); } TEST_CASE("devtools.debugger.conditionalBreakpointStopsOnEvalError") { struct St { Debugger* dbg = nullptr; int stops = 0; }; auto hook = [](HSQUIRRELVM v, SQInteger type, const SQChar* sourcename, SQInteger line, const SQChar* funcname) { auto* st = static_cast(sq_getforeignptr(v)); if (!st || !st->dbg || type != 'l') return; SourceLoc loc; loc.source = sourcename ? sourcename : ""; loc.line = static_cast(line); loc.function = funcname ? funcname : ""; if (st->dbg->onScriptLine(loc)) { ++st->stops; st->dbg->resume(); } }; const char* body = "function f() {\n" " local n = 1\n" "}\n" "f()\n"; ssq::VM vm(1024, ssq::Libs::ALL); HSQUIRRELVM v = vm.getHandle(); Debugger& d = Debugger::instance(); d.detach(); d.clearBreakpoints(); d.attach(v); d.setBreakpoint("buffer", 2, true, "no_such_local + 1"); St st; st.dbg = &d; sq_setforeignptr(v, &st); sq_enabledebuginfo(v, SQTrue); sq_setnativedebughook(v, +hook); REQUIRE(SQ_SUCCEEDED(sq_compilebuffer(v, body, static_cast(std::strlen(body)), _SC("buffer"), SQTrue))); sq_pushroottable(v); REQUIRE(SQ_SUCCEEDED(sq_call(v, 1, SQFalse, SQTrue))); sq_poptop(v); CHECK_EQ(st.stops, 1); sq_setnativedebughook(v, nullptr); sq_setforeignptr(v, nullptr); d.clearBreakpoints(); d.detach(); } TEST_CASE("devtools.debugger.scriptApiSetBreakpointCondition") { ssq::VM vm(1024, ssq::Libs::ALL); eve::ModuleManager::expose(vm); auto& dt = DevTool::instance(); dt.detach(); Debugger::instance().clearBreakpoints(); dt.attach(vm, false); dt.exposeScriptApi(vm); ssq::Script script = vm.compileSource( "function check() {\n" " local a = eve.dev.setBreakpoint(\"api.nut\", 10)\n" " local b = eve.dev.setBreakpoint(\"api.nut\", 11, \"n == 2\")\n" " return a > 0 && b > 0\n" "}\n"); vm.run(script); REQUIRE(vm.callFunc(vm.findFunc("check"), vm).toBool()); bool sawPlain = false; bool sawCond = false; for (const auto& bp : Debugger::instance().breakpoints()) { if (bp.line == 10) { sawPlain = true; CHECK(bp.condition.empty()); } if (bp.line == 11) { sawCond = true; REQUIRE_EQ(bp.condition, std::string("n == 2")); } } CHECK(sawPlain); REQUIRE(sawCond); Debugger::instance().clearBreakpoints(); dt.detach(); }