* 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>
203 lines
5.8 KiB
C++
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"));
|
|
}
|