367 lines
11 KiB
C++
367 lines
11 KiB
C++
#include "zeroerr/assert.h"
|
|
#include "zeroerr/unittest.h"
|
|
|
|
#include "common/Capability.h"
|
|
#include "common/IStateProvider.h"
|
|
#include "common/Module.h"
|
|
#include "devtools/DevTool.hpp"
|
|
#include "devtools/ReloadSession.h"
|
|
#include "devtools/Snapshot.hpp"
|
|
#include "scripts.h"
|
|
|
|
#include <simplesquirrel/simplesquirrel.hpp>
|
|
|
|
namespace {
|
|
|
|
const char* kReloadScript = R"(
|
|
// Mirror of the load.nut migrate_instance helper (tables fully; class
|
|
// instances via their declared members).
|
|
function migrate_inst(old, NewClass) {
|
|
local n = NewClass();
|
|
if (old == null) return n;
|
|
if (typeof old == "table") {
|
|
foreach (k, v in old) {
|
|
try { n[k] <- v; } catch (e) {}
|
|
}
|
|
return n;
|
|
}
|
|
if (typeof old != "instance") return n;
|
|
foreach (k, v in getclass(old)) {
|
|
if (typeof v == "function") continue;
|
|
try { n[k] <- old[k]; } catch (e) {}
|
|
}
|
|
return n;
|
|
}
|
|
|
|
function remap_inst(container, NewClass) {
|
|
if (typeof container == "array") {
|
|
for (local i = 0; i < container.len(); ++i) {
|
|
container[i] = migrate_inst(container[i], NewClass);
|
|
}
|
|
} else if (typeof container == "table") {
|
|
local keys = [];
|
|
foreach (k, v in container) keys.append(k);
|
|
foreach (k in keys) {
|
|
container[k] = migrate_inst(container[k], NewClass);
|
|
}
|
|
}
|
|
return container;
|
|
}
|
|
|
|
function reloadProtocolKeepsOldValuesAndNewFields() {
|
|
eve.dev.clearStateRoots();
|
|
getroottable().gameState <- { level = 3, hero = "alice" };
|
|
eve.dev.markStateRoot("gameState");
|
|
local err = eve.dev.beginStateReload();
|
|
if (err != "") return false;
|
|
// Simulate a script reload: the definition is replaced by new defaults.
|
|
getroottable().gameState <- { level = 0, hero = "nobody", quest = "intro" };
|
|
err = eve.dev.commitStateReload();
|
|
if (err != "") return false;
|
|
return gameState.level == 3 && gameState.hero == "alice" &&
|
|
gameState.quest == "intro";
|
|
}
|
|
|
|
function reloadProtocolRestoresMissingRoot() {
|
|
eve.dev.clearStateRoots();
|
|
getroottable().gameState <- { level = 7 };
|
|
eve.dev.markStateRoot("gameState");
|
|
local err = eve.dev.beginStateReload();
|
|
if (err != "") return false;
|
|
// The new script no longer defines the root.
|
|
delete getroottable().gameState;
|
|
err = eve.dev.commitStateReload();
|
|
if (err != "") return false;
|
|
return ("gameState" in getroottable()) && gameState.level == 7;
|
|
}
|
|
|
|
function reloadAbortRollsBack() {
|
|
eve.dev.clearStateRoots();
|
|
getroottable().gameState <- { hp = 50 };
|
|
eve.dev.markStateRoot("gameState");
|
|
local err = eve.dev.beginStateReload();
|
|
if (err != "") return false;
|
|
getroottable().gameState <- { hp = 1 };
|
|
err = eve.dev.abortStateReload();
|
|
if (err != "") return false;
|
|
return gameState.hp == 50;
|
|
}
|
|
|
|
function migrateInstanceCopiesFieldsAndKeepsDefaults() {
|
|
class HeroV1 {
|
|
level = 1;
|
|
name = "?";
|
|
}
|
|
class HeroV2 {
|
|
level = 1;
|
|
name = "?";
|
|
title = "novice";
|
|
}
|
|
local old = HeroV1();
|
|
old.level = 5;
|
|
old.name = "alice";
|
|
local n = migrate_inst(old, HeroV2);
|
|
return n.level == 5 && n.name == "alice" && n.title == "novice";
|
|
}
|
|
|
|
function migrateInstanceFromTable() {
|
|
class HeroV2 {
|
|
level = 1;
|
|
name = "?";
|
|
title = "novice";
|
|
}
|
|
local t = { level = 9, name = "bob" };
|
|
local n = migrate_inst(t, HeroV2);
|
|
return n.level == 9 && n.name == "bob" && n.title == "novice";
|
|
}
|
|
|
|
function remapInstancesContainer() {
|
|
class ItemV1 {
|
|
id = "?";
|
|
}
|
|
class ItemV2 {
|
|
id = "?";
|
|
tag = "new";
|
|
}
|
|
local arr = [ItemV1(), ItemV1()];
|
|
arr[0].id = "a";
|
|
arr[1].id = "b";
|
|
remap_inst(arr, ItemV2);
|
|
return arr[0].id == "a" && arr[0].tag == "new" && arr[1].id == "b" &&
|
|
arr[1].tag == "new";
|
|
}
|
|
|
|
function stateRootsReportsMarkedAndHeuristic() {
|
|
eve.dev.clearStateRoots();
|
|
eve.dev.markStateRoot("customState");
|
|
local roots = eve.dev.stateRoots();
|
|
if (roots.len() != 1 || roots[0] != "customState") return false;
|
|
|
|
eve.dev.clearStateRoots();
|
|
getroottable().myGame <- { x = 1 };
|
|
local found = false;
|
|
foreach (r in eve.dev.stateRoots()) {
|
|
if (r == "myGame") found = true;
|
|
}
|
|
return found;
|
|
}
|
|
|
|
function transientRootsAreExcludedFromCapture() {
|
|
eve.dev.clearStateRoots();
|
|
getroottable().scratchState <- { frame = 10 };
|
|
eve.dev.markTransientStateRoot("scratchState");
|
|
foreach (r in eve.dev.stateRoots()) {
|
|
if (r == "scratchState") return false;
|
|
}
|
|
local transient = eve.dev.transientStateRoots();
|
|
return transient.len() == 1 && transient[0] == "scratchState";
|
|
}
|
|
|
|
function abortRestoresStateRootPolicies() {
|
|
eve.dev.clearStateRoots();
|
|
getroottable().savedState <- { value = 1 };
|
|
getroottable().scratchState <- { value = 2 };
|
|
eve.dev.markStateRoot("savedState");
|
|
eve.dev.markTransientStateRoot("scratchState");
|
|
local err = eve.dev.beginStateReload();
|
|
if (err != "") return false;
|
|
|
|
eve.dev.clearStateRoots();
|
|
eve.dev.markStateRoot("replacementState");
|
|
err = eve.dev.abortStateReload();
|
|
if (err != "") return false;
|
|
|
|
local roots = eve.dev.stateRoots();
|
|
local transient = eve.dev.transientStateRoots();
|
|
return roots.len() == 1 && roots[0] == "savedState" &&
|
|
transient.len() == 1 && transient[0] == "scratchState";
|
|
}
|
|
|
|
function commitKeepsNewStateRootPolicies() {
|
|
eve.dev.clearStateRoots();
|
|
getroottable().savedState <- { value = 1 };
|
|
eve.dev.markStateRoot("savedState");
|
|
local err = eve.dev.beginStateReload();
|
|
if (err != "") return false;
|
|
|
|
eve.dev.clearStateRoots();
|
|
eve.dev.markTransientStateRoot("replacementState");
|
|
err = eve.dev.commitStateReload();
|
|
if (err != "") return false;
|
|
|
|
local transient = eve.dev.transientStateRoots();
|
|
return transient.len() == 1 && transient[0] == "replacementState";
|
|
}
|
|
)";
|
|
|
|
/** ScriptTest variant that also exposes `eve.dev` (DevTool script API). */
|
|
class ReloadScriptTest {
|
|
public:
|
|
explicit ReloadScriptTest(const char* script) : vm(2048, ssq::Libs::ALL) {
|
|
eve::ModuleManager::expose(vm);
|
|
eve::dev::DevTool::instance().attach(vm);
|
|
eve::dev::DevTool::instance().exposeScriptApi(vm);
|
|
ssq::Script s = vm.compileSource(script);
|
|
vm.run(s);
|
|
}
|
|
|
|
~ReloadScriptTest() { eve::dev::DevTool::instance().detach(); }
|
|
|
|
ssq::VM vm;
|
|
};
|
|
|
|
#define UnitReloadScriptTest(name, content) \
|
|
class name : public ReloadScriptTest { \
|
|
public: \
|
|
name() : ReloadScriptTest(content) {} \
|
|
}
|
|
|
|
UnitReloadScriptTest(ReloadScriptTestFixture, kReloadScript);
|
|
|
|
TEST_CASE_FIXTURE(ReloadScriptTestFixture, "reloadScript.protocolKeepsOldValuesAndNewFields") {
|
|
CHECK(vm.callFunc(vm.findFunc("reloadProtocolKeepsOldValuesAndNewFields"), vm).toBool());
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(ReloadScriptTestFixture, "reloadScript.protocolRestoresMissingRoot") {
|
|
CHECK(vm.callFunc(vm.findFunc("reloadProtocolRestoresMissingRoot"), vm).toBool());
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(ReloadScriptTestFixture, "reloadScript.abortRollsBack") {
|
|
CHECK(vm.callFunc(vm.findFunc("reloadAbortRollsBack"), vm).toBool());
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(ReloadScriptTestFixture, "reloadScript.migrateInstanceCopiesFieldsAndKeepsDefaults") {
|
|
CHECK(vm.callFunc(vm.findFunc("migrateInstanceCopiesFieldsAndKeepsDefaults"), vm).toBool());
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(ReloadScriptTestFixture, "reloadScript.migrateInstanceFromTable") {
|
|
CHECK(vm.callFunc(vm.findFunc("migrateInstanceFromTable"), vm).toBool());
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(ReloadScriptTestFixture, "reloadScript.remapInstancesContainer") {
|
|
CHECK(vm.callFunc(vm.findFunc("remapInstancesContainer"), vm).toBool());
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(ReloadScriptTestFixture, "reloadScript.stateRootsReportsMarkedAndHeuristic") {
|
|
CHECK(vm.callFunc(vm.findFunc("stateRootsReportsMarkedAndHeuristic"), vm).toBool());
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(ReloadScriptTestFixture, "reloadScript.transientRootsAreExcludedFromCapture") {
|
|
CHECK(vm.callFunc(vm.findFunc("transientRootsAreExcludedFromCapture"), vm).toBool());
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(ReloadScriptTestFixture, "reloadScript.abortRestoresStateRootPolicies") {
|
|
CHECK(vm.callFunc(vm.findFunc("abortRestoresStateRootPolicies"), vm).toBool());
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(ReloadScriptTestFixture, "reloadScript.commitKeepsNewStateRootPolicies") {
|
|
CHECK(vm.callFunc(vm.findFunc("commitKeepsNewStateRootPolicies"), vm).toBool());
|
|
}
|
|
|
|
TEST_CASE("reloadScript.embeddedLoadScriptCompiles") {
|
|
ssq::VM vm(2048, ssq::Libs::ALL);
|
|
bool compiled = false;
|
|
try {
|
|
vm.compileSource(eve::load_content);
|
|
compiled = true;
|
|
} catch (...) {
|
|
}
|
|
CHECK(compiled);
|
|
}
|
|
|
|
namespace {
|
|
|
|
class FailFirstRestoreProvider : public eve::caps::IStateProvider {
|
|
public:
|
|
const char* stateKind() const override { return "reload-failure-test"; }
|
|
|
|
bool captureState(eve::StateValue& out) override {
|
|
out = eve::StateValue::integer(42);
|
|
return true;
|
|
}
|
|
|
|
bool restoreState(const eve::StateValue&, std::string* err) override {
|
|
++restoreCount;
|
|
if (restoreCount == 1) {
|
|
if (err) *err = "intentional commit failure";
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool resetToDefaults() override { return true; }
|
|
|
|
int restoreCount = 0;
|
|
};
|
|
|
|
class ResetOnReloadProvider : public eve::caps::IStateProvider {
|
|
public:
|
|
const char* stateKind() const override { return "reload-reset-test"; }
|
|
|
|
eve::caps::StateReloadPolicy reloadPolicy() const override {
|
|
return eve::caps::StateReloadPolicy::Reset;
|
|
}
|
|
|
|
bool captureState(eve::StateValue&) override {
|
|
++captureCount;
|
|
return true;
|
|
}
|
|
|
|
bool restoreState(const eve::StateValue&, std::string*) override {
|
|
++restoreCount;
|
|
return true;
|
|
}
|
|
|
|
bool resetToDefaults() override {
|
|
++resetCount;
|
|
return true;
|
|
}
|
|
|
|
int captureCount = 0;
|
|
int restoreCount = 0;
|
|
int resetCount = 0;
|
|
};
|
|
|
|
} // namespace
|
|
|
|
TEST_CASE("reloadScript.failedCommitCanStillAbort") {
|
|
ssq::VM vm(2048, ssq::Libs::ALL);
|
|
eve::dev::Snapshot::instance().clearRoots();
|
|
|
|
FailFirstRestoreProvider provider;
|
|
eve::cap::addListener<eve::caps::IStateProvider>(&provider);
|
|
|
|
auto& session = eve::dev::ReloadSession::instance();
|
|
std::string err;
|
|
REQUIRE(session.begin(vm.getHandle(), &err));
|
|
CHECK(!session.commit(vm.getHandle(), &err));
|
|
CHECK_EQ(provider.restoreCount, 1);
|
|
|
|
err.clear();
|
|
CHECK(session.abort(vm.getHandle(), &err));
|
|
CHECK(err.empty());
|
|
CHECK_EQ(provider.restoreCount, 2);
|
|
|
|
eve::cap::removeListener<eve::caps::IStateProvider>(&provider);
|
|
}
|
|
|
|
TEST_CASE("reloadScript.nativeResetPolicySkipsCaptureAndResets") {
|
|
ssq::VM vm(2048, ssq::Libs::ALL);
|
|
eve::dev::Snapshot::instance().clearRoots();
|
|
|
|
ResetOnReloadProvider provider;
|
|
eve::cap::addListener<eve::caps::IStateProvider>(&provider);
|
|
|
|
auto& session = eve::dev::ReloadSession::instance();
|
|
std::string err;
|
|
REQUIRE(session.begin(vm.getHandle(), &err));
|
|
CHECK(session.commit(vm.getHandle(), &err));
|
|
CHECK_EQ(provider.captureCount, 0);
|
|
CHECK_EQ(provider.restoreCount, 0);
|
|
CHECK_EQ(provider.resetCount, 1);
|
|
|
|
eve::cap::removeListener<eve::caps::IStateProvider>(&provider);
|
|
}
|
|
|
|
} // namespace
|