Files
EVEngine/test/reload_script.cpp

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