Files
EVEngine/test/debugger.cpp
T
西风逍遥游 040bc4576f fix(devtools): unbreak MCP eval + state capture on instance roots
DevLab's hot-reload test was failing deterministically on Linux: any --debug
run that reloads a script hit 'state reload: capture failed: root
'spritestack' is a class instance' because every module instance lives in the
Squirrel root table and Snapshot's auto-scan treated the first instance root
as a fatal capture error. With break-on-error enabled the engine paused at
the reported error, and a subsequent eve_eval deadlocked the pause pump.

- Snapshot::resolveRoots: drop OT_INSTANCE from the automatic root scan so
  module singletons / class instances are skipped instead of aborting the
  capture (plain-data roots such as gameState/lab are still captured).
- Debugger::waitWhilePaused: make the pause pump re-entrant so bytecode run
  by eve_eval / watches / breakpoint conditions while paused no longer blocks
  on the same thread's line hook (deadlock).
- test: new devtools.snapshot.skipsInstanceRootInAutoScan case.

Verified in WSL (Linux/GCC, software Vulkan + Xvfb): devlab MCP hot-reload
test 12/12 rounds, full ctest 1656/1656, all 37 example smokes pass.
2026-08-22 01:22:55 +08:00

791 lines
23 KiB
C++

#include "zeroerr/assert.h"
#include "zeroerr/unittest.h"
#include "common/Capability.h"
#include "common/IStateProvider.h"
#include "common/StateValue.h"
#include "devtools/Debugger.hpp"
#include "devtools/Snapshot.hpp"
#include <simplesquirrel/simplesquirrel.hpp>
#include <squirrel.h>
#include <cstdio>
#include <cstring>
#include <fstream>
#include <string>
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<int>(d.lastPauseReason()) == static_cast<int>(PauseReason::PauseKey));
d.stepFrame();
CHECK(d.shouldRunUpdate());
d.notifyFrameDone();
CHECK(d.isPaused());
CHECK(static_cast<int>(d.lastPauseReason()) == static_cast<int>(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("/abs/path/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<int>(d.lastPauseReason()) == static_cast<int>(PauseReason::Breakpoint));
// No VM stack → stepInto/Over open as StepInto and stop on next line.
d.stepInto();
CHECK(static_cast<int>(d.mode()) == static_cast<int>(RunMode::StepInto));
loc.line = 11;
CHECK(d.onScriptLine(loc));
CHECK(d.isPaused());
CHECK(static_cast<int>(d.lastPauseReason()) == static_cast<int>(PauseReason::Step));
// Smart step from a script stop → stepOver (depth 0 ⇒ StepInto).
d.step();
CHECK(static_cast<int>(d.mode()) == static_cast<int>(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<int>(d.mode()) == static_cast<int>(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<HookState*>(sq_getforeignptr(v));
if (!st || !st->dbg) return;
if (type != 'l') return;
SourceLoc loc;
loc.source = sourcename ? sourcename : "";
loc.line = static_cast<int>(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<SQInteger>(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<IntoState*>(sq_getforeignptr(vmh));
if (!s || !s->dbg || type != 'l') return;
SourceLoc loc;
loc.source = sourcename ? sourcename : "";
loc.line = static_cast<int>(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<SQInteger>(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<int>(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<eve::caps::IStateProvider>(&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<int>(level), 3);
sq_settop(v, top);
}
eve::cap::removeListener<eve::caps::IStateProvider>(&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<int>(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"));
}
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<int>(d.mode());
const bool stepping = m == static_cast<int>(RunMode::StepInto) ||
m == static_cast<int>(RunMode::StepOver);
CHECK(stepping);
}
loc.line = 11;
CHECK(d.onScriptLine(loc));
CHECK(d.isPaused());
CHECK(static_cast<int>(d.lastPauseReason()) == static_cast<int>(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<int>(d.lastPauseReason()) == static_cast<int>(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<St*>(sq_getforeignptr(v));
if (!st || !st->dbg || type != 'l') return;
SourceLoc loc;
loc.source = sourcename ? sourcename : "";
loc.line = static_cast<int>(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<SQInteger>(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);
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();
}