Files
EVEngine/test/debugger.cpp
T
西风逍遥游andCursor Agent 8abbb134cb feat(devtools): pause, breakpoints, watches, snapshots, and VS Code DAP (#6)
* feat(devtools): pause, breakpoints, watches, snapshots, and VS Code DAP

Add a script-centric debugger: Pause key / DAP frame pause, Squirrel
breakpoints with watches, JSON snapshot of script state (stateless
engine), TCP Debug Adapter Protocol server, and a VS Code extension
under tools/vscode-eve-debug.

Co-authored-by: 西风逍遥游 <sunxfancy@gmail.com>

* fix(devtools): correct Snapshot JSON (Poco Ptr) and DAP includes

Use Poco::JSON::Object/Array::Ptr for script-state capture/restore so
round-trips no longer BadCast; include TimeoutException via Exception.h.

Co-authored-by: 西风逍遥游 <sunxfancy@gmail.com>

* fix(devtools): move DebugAdapter ctor to cpp for MSVC unique_ptr

MSVC rejects unique_ptr of forward-declared Poco sockets when the
default constructor is defined in the header (incomplete-type delete).

Co-authored-by: 西风逍遥游 <sunxfancy@gmail.com>

* fix(devtools): disable Poco auto-link on EVDevTools for MSVC

Including Poco Net/JSON without POCO_NO_AUTOMATIC_LIBS makes the
Windows linker request PocoFoundationd.lib instead of PocoFoundationmdd.

Co-authored-by: 西风逍遥游 <sunxfancy@gmail.com>

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
2026-08-09 09:41:58 +08:00

203 lines
5.8 KiB
C++

#include "zeroerr/assert.h"
#include "zeroerr/unittest.h"
#include "devtools/Debugger.hpp"
#include "devtools/Snapshot.hpp"
#include <simplesquirrel/simplesquirrel.hpp>
#include <squirrel.h>
#include <cstdio>
#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));
d.stepLine();
CHECK(static_cast<int>(d.mode()) == static_cast<int>(RunMode::StepLine));
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.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());
}
TEST_CASE("devtools.debugger.normalizeSource") {
CHECK_EQ(Debugger::normalizeSource("file://./a/b.nut"), std::string("a/b.nut"));
CHECK_EQ(Debugger::normalizeSource("C:\\x\\y.nut"), std::string("C:/x/y.nut"));
}