MetaCore/tests/MetaCoreScriptingTests.cpp
2026-07-16 09:01:25 +08:00

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;
}
}