159 lines
8.8 KiB
C++
159 lines
8.8 KiB
C++
#include "MetaCoreScripting/MetaCoreScripting.h"
|
|
|
|
#include <nlohmann/json.hpp>
|
|
|
|
#include <cmath>
|
|
#include <filesystem>
|
|
#include <fstream>
|
|
#include <iostream>
|
|
#include <memory>
|
|
#include <stdexcept>
|
|
#include <string>
|
|
#include <vector>
|
|
|
|
namespace {
|
|
|
|
void Expect(bool condition, const char* message) {
|
|
if (!condition) throw std::runtime_error(message);
|
|
}
|
|
|
|
struct Counters { int Start = 0; int Fixed = 0; int Update = 0; int Late = 0; int Stop = 0; };
|
|
|
|
class CountingBehaviour final : public MetaCore::MetaCoreIScriptBehaviour {
|
|
public:
|
|
explicit CountingBehaviour(std::shared_ptr<Counters> counters) : Counters_(std::move(counters)) {}
|
|
void OnPlayStart(MetaCore::MetaCoreScriptExecutionContext&) override { ++Counters_->Start; }
|
|
void FixedUpdate(MetaCore::MetaCoreScriptExecutionContext&, double) override { ++Counters_->Fixed; }
|
|
void Update(MetaCore::MetaCoreScriptExecutionContext&, double) override { ++Counters_->Update; }
|
|
void LateUpdate(MetaCore::MetaCoreScriptExecutionContext&, double) override { ++Counters_->Late; }
|
|
void OnPlayStop(MetaCore::MetaCoreScriptExecutionContext&) override { ++Counters_->Stop; }
|
|
private:
|
|
std::shared_ptr<Counters> Counters_;
|
|
};
|
|
|
|
class FaultBehaviour final : public MetaCore::MetaCoreIScriptBehaviour {
|
|
public:
|
|
void Update(MetaCore::MetaCoreScriptExecutionContext&, double) override { throw std::runtime_error("expected fault"); }
|
|
};
|
|
|
|
void TestRegistryFieldsAndReplacement() {
|
|
MetaCore::MetaCoreScriptRegistry registry;
|
|
MetaCore::MetaCoreScriptDefinition definition;
|
|
definition.TypeId = "Tests.Fields";
|
|
definition.ModuleId = "Tests";
|
|
MetaCore::MetaCoreScriptFieldDefinition speed{"OldSpeed", "Speed", MetaCore::MetaCoreScriptFieldType::Float, false, 2.0F};
|
|
speed.FieldId = "speed";
|
|
MetaCore::MetaCoreScriptFieldDefinition target{"Target", "Target", MetaCore::MetaCoreScriptFieldType::GameObjectRef};
|
|
target.FieldId = "target";
|
|
definition.Fields = {speed, target};
|
|
definition.Factory = [] { return std::make_unique<CountingBehaviour>(std::make_shared<Counters>()); };
|
|
std::string error;
|
|
Expect(registry.RegisterScript(definition, &error), "valid definition should register");
|
|
const auto migrated = nlohmann::json::parse(registry.EnsureFieldsJsonDefaults("Tests.Fields", R"({"OldSpeed":4.5,"unknown":7})"));
|
|
Expect(migrated.value("speed", 0.0) == 4.5, "legacy field name should migrate to stable id");
|
|
Expect(!migrated.contains("OldSpeed") && migrated.value("unknown", 0) == 7, "migration should preserve unknown fields and remove migrated key");
|
|
Expect(migrated.contains("target"), "new reference fields should receive defaults");
|
|
|
|
auto duplicate = definition;
|
|
duplicate.TypeId = "Tests.Other";
|
|
duplicate.Fields.push_back(speed);
|
|
Expect(!registry.ReplaceModule("Tests", {definition, duplicate}, &error), "duplicate field ids should reject the complete candidate");
|
|
Expect(registry.FindScript("Tests.Fields") != nullptr, "failed replacement should preserve old registry state");
|
|
}
|
|
|
|
void TestRuntimeLifecycleFaultsHandlesEventsAndTimers() {
|
|
MetaCore::MetaCoreScene scene;
|
|
auto object = scene.CreateGameObject("Scripted");
|
|
auto& scripts = object.AddComponent<MetaCore::MetaCoreScriptComponent>();
|
|
scripts.Instances.push_back({11, "Tests.Count", true, "{}"});
|
|
scripts.Instances.push_back({12, "Tests.Fault", true, "{}"});
|
|
const auto objectId = object.GetId();
|
|
|
|
auto counters = std::make_shared<Counters>();
|
|
MetaCore::MetaCoreScriptRegistry registry;
|
|
MetaCore::MetaCoreScriptDefinition count;
|
|
count.TypeId = "Tests.Count"; count.Factory = [counters] { return std::make_unique<CountingBehaviour>(counters); };
|
|
MetaCore::MetaCoreScriptDefinition fault;
|
|
fault.TypeId = "Tests.Fault"; fault.Factory = [] { return std::make_unique<FaultBehaviour>(); };
|
|
Expect(registry.RegisterScript(std::move(count)), "count script registration");
|
|
Expect(registry.RegisterScript(std::move(fault)), "fault script registration");
|
|
std::vector<std::string> logs;
|
|
MetaCore::MetaCoreScriptRuntime runtime(scene, registry, [&](std::uint32_t, std::string_view, std::string_view message) { logs.emplace_back(message); });
|
|
runtime.Start();
|
|
const MetaCoreScriptObjectHandleV1 handle{runtime.GetWorldGeneration(), objectId};
|
|
Expect(runtime.IsHandleValid(handle), "current world/object handle should be valid");
|
|
runtime.FixedUpdate(1.0 / 60.0);
|
|
runtime.Update(0.1);
|
|
runtime.LateUpdate(0.1);
|
|
Expect(counters->Start == 1 && counters->Fixed == 1 && counters->Update == 1 && counters->Late == 1, "runtime should dispatch lifecycle in all phases");
|
|
Expect(runtime.GetFaultedInstanceCount() == 1 && !logs.empty(), "one throwing behaviour should be isolated and diagnosed");
|
|
|
|
std::vector<std::string> eventOrder;
|
|
runtime.Subscribe(objectId, 11, "first", [&](std::string_view, const auto&) { eventOrder.push_back("first"); runtime.Publish("second"); });
|
|
runtime.Subscribe(objectId, 11, "second", [&](std::string_view, const auto&) { eventOrder.push_back("second"); });
|
|
runtime.Publish("first");
|
|
runtime.Update(0.1);
|
|
Expect(eventOrder == std::vector<std::string>{"first"}, "events published during dispatch should wait for the next batch");
|
|
runtime.Update(0.1);
|
|
Expect(eventOrder == std::vector<std::string>({"first", "second"}), "next event batch should preserve publication order");
|
|
|
|
int timerCount = 0;
|
|
runtime.ScheduleTimer(objectId, 11, 0.05, 0.0, false, [&] { ++timerCount; });
|
|
runtime.Update(0.1);
|
|
Expect(timerCount == 1, "one-shot timer should fire once");
|
|
runtime.ScheduleTimer(objectId, 11, 0.0, 0.01, false, [&] { ++timerCount; });
|
|
(void)scene.DeleteGameObjects({objectId});
|
|
runtime.Update(0.1);
|
|
Expect(timerCount == 1, "deleting the owner should cancel timers before callback");
|
|
runtime.Stop();
|
|
Expect(counters->Stop == 0, "deleted owner should not receive OnPlayStop");
|
|
Expect(!runtime.IsHandleValid(handle), "handles should invalidate after Stop/world generation change");
|
|
}
|
|
|
|
void TestGeneratedProjectBuildAndLoad() {
|
|
const auto root = std::filesystem::temp_directory_path() / "MetaCoreScriptingTestsProject";
|
|
std::error_code errorCode;
|
|
std::filesystem::remove_all(root, errorCode);
|
|
std::filesystem::create_directories(root, errorCode);
|
|
std::string error;
|
|
Expect(MetaCore::MetaCoreGenerateScriptProject(root, "ScriptingTests", METACORE_SOURCE_ROOT, &error), error.c_str());
|
|
Expect(MetaCore::MetaCoreGenerateScriptProject(root, "ScriptingTests", METACORE_SOURCE_ROOT, &error), "script project generation should be idempotent");
|
|
Expect(MetaCore::MetaCoreCreateScriptBehaviour(root, "ScriptingTests", "SecondBehaviour", &error), error.c_str());
|
|
Expect(std::filesystem::is_regular_file(root / "Scripts" / "SDK" / "MetaCoreScripting" / "MetaCoreScriptAbi.h"), "generated project should contain a self-contained ABI SDK");
|
|
const auto build = MetaCore::MetaCoreBuildScriptProject(root, "Debug", METACORE_SOURCE_ROOT);
|
|
Expect(build.Success, build.Output.c_str());
|
|
Expect(std::filesystem::is_regular_file(root / "compile_commands.json"), "script build should publish compile_commands.json");
|
|
MetaCore::MetaCoreScriptModuleLoader loader;
|
|
auto module = loader.LoadCandidate(build.ModulePath, &error);
|
|
Expect(module.has_value(), error.c_str());
|
|
Expect(module->ModuleId == "ScriptingTests" && module->Definitions.size() == 2, "generated module should expose all generated behaviour descriptors");
|
|
MetaCore::MetaCoreScriptRegistry registry;
|
|
Expect(registry.ReplaceModule(module->ModuleId, module->Definitions, &error), error.c_str());
|
|
Expect(registry.FindScript("ScriptingTests.Mover") != nullptr, "loaded project behaviour should be attachable");
|
|
Expect(registry.FindScript("ScriptingTests.SecondBehaviour") != nullptr, "newly created behaviour should be compiled and attachable");
|
|
{
|
|
std::ofstream corruptManifest(build.ManifestPath, std::ios::trunc);
|
|
corruptManifest << R"({"format_version":1,"abi_major":1,"abi_minor":0,"module_id":"ScriptingTests","module_file":")"
|
|
<< build.ModulePath.filename().string() << R"(","sha256":"bad"})";
|
|
}
|
|
auto rejected = loader.LoadCandidate(build.ModulePath, &error);
|
|
Expect(!rejected.has_value(), "module with a mismatched manifest hash should be rejected before activation");
|
|
Expect(registry.FindScript("ScriptingTests.Mover") != nullptr, "failed candidate validation should leave the active registry intact");
|
|
std::filesystem::remove_all(root, errorCode);
|
|
}
|
|
|
|
} // namespace
|
|
|
|
int main() {
|
|
try {
|
|
TestRegistryFieldsAndReplacement();
|
|
TestRuntimeLifecycleFaultsHandlesEventsAndTimers();
|
|
TestGeneratedProjectBuildAndLoad();
|
|
std::cout << "MetaCoreScriptingTests passed\n";
|
|
return 0;
|
|
} catch (const std::exception& error) {
|
|
std::cerr << "MetaCoreScriptingTests failed: " << error.what() << '\n';
|
|
return 1;
|
|
}
|
|
}
|