fix(scripting): stabilize mono wrapper test teardown

This commit is contained in:
2026-04-15 14:27:21 +08:00
parent 982a877714
commit 65cb212020
8 changed files with 351 additions and 0 deletions

View File

@@ -29,6 +29,7 @@
#include <mono/metadata/reflection.h>
#include <algorithm>
#include <cstdlib>
#include <limits>
#include <utility>
@@ -40,6 +41,7 @@ namespace {
struct MonoRootState {
MonoDomain* rootDomain = nullptr;
bool initialized = false;
bool cleanupRegistered = false;
};
enum class ManagedComponentKind {
@@ -81,6 +83,19 @@ bool& GetInternalCallRegistrationState() {
return registered;
}
void CleanupMonoRootDomainAtExit() {
MonoRootState& rootState = GetMonoRootState();
if (!rootState.rootDomain) {
return;
}
mono_domain_set(rootState.rootDomain, true);
mono_jit_cleanup(rootState.rootDomain);
rootState.rootDomain = nullptr;
rootState.initialized = false;
GetInternalCallRegistrationState() = false;
}
std::string BuildFullClassName(const std::string& namespaceName, const std::string& className) {
return namespaceName.empty() ? className : namespaceName + "." + className;
}
@@ -2455,6 +2470,10 @@ bool MonoScriptRuntime::InitializeRootDomain() {
mono_domain_set(rootState.rootDomain, true);
RegisterInternalCalls();
rootState.initialized = true;
if (!rootState.cleanupRegistered) {
std::atexit(&CleanupMonoRootDomainAtExit);
rootState.cleanupRegistered = true;
}
return true;
}

View File

@@ -131,6 +131,8 @@ set(XCENGINE_SCRIPT_CORE_SOURCES
set(XCENGINE_GAME_SCRIPT_SOURCES
${CMAKE_CURRENT_SOURCE_DIR}/GameScripts/BuiltinComponentProbe.cs
${CMAKE_CURRENT_SOURCE_DIR}/GameScripts/CameraLightLookupProbe.cs
${CMAKE_CURRENT_SOURCE_DIR}/GameScripts/CameraPropertyProbe.cs
${CMAKE_CURRENT_SOURCE_DIR}/GameScripts/AddComponentProbe.cs
${CMAKE_CURRENT_SOURCE_DIR}/GameScripts/ComponentReferenceProbe.cs
${CMAKE_CURRENT_SOURCE_DIR}/GameScripts/EnumFieldProbe.cs
@@ -141,8 +143,11 @@ set(XCENGINE_GAME_SCRIPT_SOURCES
${CMAKE_CURRENT_SOURCE_DIR}/GameScripts/HierarchyProbe.cs
${CMAKE_CURRENT_SOURCE_DIR}/GameScripts/InputProbe.cs
${CMAKE_CURRENT_SOURCE_DIR}/GameScripts/LifecycleProbe.cs
${CMAKE_CURRENT_SOURCE_DIR}/GameScripts/LightPropertyProbe.cs
${CMAKE_CURRENT_SOURCE_DIR}/GameScripts/MeshComponentProbe.cs
${CMAKE_CURRENT_SOURCE_DIR}/GameScripts/MeshRendererEdgeCaseProbe.cs
${CMAKE_CURRENT_SOURCE_DIR}/GameScripts/MeshRendererFlagsProbe.cs
${CMAKE_CURRENT_SOURCE_DIR}/GameScripts/MeshRendererPathProbe.cs
${CMAKE_CURRENT_SOURCE_DIR}/GameScripts/ObjectApiProbe.cs
${CMAKE_CURRENT_SOURCE_DIR}/GameScripts/PhysicsApiProbe.cs
${CMAKE_CURRENT_SOURCE_DIR}/GameScripts/PhysicsEventProbe.cs

View File

@@ -0,0 +1,22 @@
using XCEngine;
namespace Gameplay
{
public sealed class CameraLightLookupProbe : MonoBehaviour
{
public bool HasCamera;
public bool HasLight;
public bool CameraLookupSucceeded;
public bool LightLookupSucceeded;
public void Start()
{
HasCamera = HasComponent<Camera>();
HasLight = HasComponent<Light>();
CameraLookupSucceeded = TryGetComponent(out Camera camera);
LightLookupSucceeded = TryGetComponent(out Light light);
CameraLookupSucceeded = CameraLookupSucceeded && camera != null;
LightLookupSucceeded = LightLookupSucceeded && light != null;
}
}
}

View File

@@ -0,0 +1,36 @@
using XCEngine;
namespace Gameplay
{
public sealed class CameraPropertyProbe : MonoBehaviour
{
public bool CameraLookupSucceeded;
public float ObservedFieldOfView;
public float ObservedNearClipPlane;
public float ObservedFarClipPlane;
public float ObservedDepth;
public bool ObservedPrimary;
public void Start()
{
CameraLookupSucceeded = TryGetComponent(out Camera camera);
if (camera == null)
{
CameraLookupSucceeded = false;
return;
}
ObservedFieldOfView = camera.fieldOfView;
ObservedNearClipPlane = camera.nearClipPlane;
ObservedFarClipPlane = camera.farClipPlane;
ObservedDepth = camera.depth;
ObservedPrimary = camera.primary;
camera.fieldOfView = 75.0f;
camera.nearClipPlane = 0.3f;
camera.farClipPlane = 500.0f;
camera.depth = 3.0f;
camera.primary = false;
}
}
}

View File

@@ -0,0 +1,33 @@
using XCEngine;
namespace Gameplay
{
public sealed class LightPropertyProbe : MonoBehaviour
{
public bool LightLookupSucceeded;
public float ObservedIntensity;
public float ObservedRange;
public float ObservedSpotAngle;
public bool ObservedCastsShadows;
public void Start()
{
LightLookupSucceeded = TryGetComponent(out Light light);
if (light == null)
{
LightLookupSucceeded = false;
return;
}
ObservedIntensity = light.intensity;
ObservedRange = light.range;
ObservedSpotAngle = light.spotAngle;
ObservedCastsShadows = light.castsShadows;
light.intensity = 2.5f;
light.range = 42.0f;
light.spotAngle = 55.0f;
light.castsShadows = true;
}
}
}

View File

@@ -0,0 +1,30 @@
using XCEngine;
namespace Gameplay
{
public sealed class MeshRendererFlagsProbe : MonoBehaviour
{
public bool MeshRendererLookupSucceeded;
public bool ObservedCastShadows;
public bool ObservedReceiveShadows;
public int ObservedRenderLayer;
public void Start()
{
MeshRendererLookupSucceeded = TryGetComponent(out MeshRenderer meshRenderer);
if (meshRenderer == null)
{
MeshRendererLookupSucceeded = false;
return;
}
ObservedCastShadows = meshRenderer.castShadows;
ObservedReceiveShadows = meshRenderer.receiveShadows;
ObservedRenderLayer = meshRenderer.renderLayer;
meshRenderer.castShadows = false;
meshRenderer.receiveShadows = true;
meshRenderer.renderLayer = 11;
}
}
}

View File

@@ -0,0 +1,29 @@
using XCEngine;
namespace Gameplay
{
public sealed class MeshRendererPathProbe : MonoBehaviour
{
public bool MeshRendererLookupSucceeded;
public int ObservedInitialMaterialCount;
public string ObservedInitialMaterial0Path = string.Empty;
public int ObservedUpdatedMaterialCount;
public string ObservedUpdatedMaterial1Path = string.Empty;
public void Start()
{
MeshRendererLookupSucceeded = TryGetComponent(out MeshRenderer meshRenderer);
if (meshRenderer == null)
{
MeshRendererLookupSucceeded = false;
return;
}
ObservedInitialMaterialCount = meshRenderer.materialCount;
ObservedInitialMaterial0Path = meshRenderer.GetMaterialPath(0);
meshRenderer.SetMaterialPath(1, "Materials/runtime_override.mat");
ObservedUpdatedMaterialCount = meshRenderer.materialCount;
ObservedUpdatedMaterial1Path = meshRenderer.GetMaterialPath(1);
}
}
}

View File

@@ -12,6 +12,7 @@
#include <XCEngine/Core/Math/Vector2.h>
#include <XCEngine/Core/Math/Vector3.h>
#include <XCEngine/Core/Math/Vector4.h>
#include <XCEngine/Core/Asset/ResourceManager.h>
#include <XCEngine/Input/InputManager.h>
#include <XCEngine/Physics/PhysicsWorld.h>
#include <XCEngine/Input/InputTypes.h>
@@ -97,6 +98,7 @@ protected:
engine->OnRuntimeStop();
engine->SetRuntimeFixedDeltaTime(ScriptEngine::DefaultFixedDeltaTime);
XCEngine::Input::InputManager::Get().Shutdown();
XCEngine::Resources::ResourceManager::Get().Shutdown();
XCEngine::Rendering::Pipelines::ClearManagedRenderPipelineAssetDescriptor();
runtime = std::make_unique<MonoScriptRuntime>(CreateMonoSettings());
@@ -112,6 +114,7 @@ protected:
XCEngine::Rendering::Pipelines::ClearManagedRenderPipelineAssetDescriptor();
runtime.reset();
scene.reset();
XCEngine::Resources::ResourceManager::Get().Shutdown();
}
Scene* CreateScene(const std::string& sceneName) {
@@ -2036,6 +2039,180 @@ TEST_F(MonoScriptRuntimeTest, ManagedBuiltInComponentWrappersReadAndWriteCameraA
EXPECT_TRUE(light->GetCastsShadows());
}
TEST_F(MonoScriptRuntimeTest, ManagedBuiltInComponentLookupOnlyCameraAndLight) {
Scene* runtimeScene = CreateScene("MonoRuntimeScene");
GameObject* host = runtimeScene->CreateGameObject("Host");
host->AddComponent<CameraComponent>();
host->AddComponent<LightComponent>();
ScriptComponent* component = AddScript(host, "Gameplay", "CameraLightLookupProbe");
engine->OnRuntimeStart(runtimeScene);
engine->OnUpdate(0.016f);
bool hasCamera = false;
bool hasLight = false;
bool cameraLookupSucceeded = false;
bool lightLookupSucceeded = false;
EXPECT_TRUE(runtime->TryGetFieldValue(component, "HasCamera", hasCamera));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "HasLight", hasLight));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "CameraLookupSucceeded", cameraLookupSucceeded));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "LightLookupSucceeded", lightLookupSucceeded));
EXPECT_TRUE(hasCamera);
EXPECT_TRUE(hasLight);
EXPECT_TRUE(cameraLookupSucceeded);
EXPECT_TRUE(lightLookupSucceeded);
}
TEST_F(MonoScriptRuntimeTest, ManagedCameraWrapperReadAndWriteProperties) {
Scene* runtimeScene = CreateScene("MonoRuntimeScene");
GameObject* host = runtimeScene->CreateGameObject("Host");
CameraComponent* camera = host->AddComponent<CameraComponent>();
ScriptComponent* component = AddScript(host, "Gameplay", "CameraPropertyProbe");
camera->SetFieldOfView(52.0f);
camera->SetNearClipPlane(0.2f);
camera->SetFarClipPlane(300.0f);
camera->SetDepth(1.5f);
camera->SetPrimary(true);
engine->OnRuntimeStart(runtimeScene);
engine->OnUpdate(0.016f);
bool cameraLookupSucceeded = false;
bool observedPrimary = false;
float observedFieldOfView = 0.0f;
float observedNearClipPlane = 0.0f;
float observedFarClipPlane = 0.0f;
float observedDepth = 0.0f;
EXPECT_TRUE(runtime->TryGetFieldValue(component, "CameraLookupSucceeded", cameraLookupSucceeded));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedFieldOfView", observedFieldOfView));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedNearClipPlane", observedNearClipPlane));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedFarClipPlane", observedFarClipPlane));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedDepth", observedDepth));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedPrimary", observedPrimary));
EXPECT_TRUE(cameraLookupSucceeded);
EXPECT_FLOAT_EQ(observedFieldOfView, 52.0f);
EXPECT_FLOAT_EQ(observedNearClipPlane, 0.2f);
EXPECT_FLOAT_EQ(observedFarClipPlane, 300.0f);
EXPECT_FLOAT_EQ(observedDepth, 1.5f);
EXPECT_TRUE(observedPrimary);
EXPECT_FLOAT_EQ(camera->GetFieldOfView(), 75.0f);
EXPECT_FLOAT_EQ(camera->GetNearClipPlane(), 0.3f);
EXPECT_FLOAT_EQ(camera->GetFarClipPlane(), 500.0f);
EXPECT_FLOAT_EQ(camera->GetDepth(), 3.0f);
EXPECT_FALSE(camera->IsPrimary());
}
TEST_F(MonoScriptRuntimeTest, ManagedLightWrapperReadAndWriteProperties) {
Scene* runtimeScene = CreateScene("MonoRuntimeScene");
GameObject* host = runtimeScene->CreateGameObject("Host");
LightComponent* light = host->AddComponent<LightComponent>();
ScriptComponent* component = AddScript(host, "Gameplay", "LightPropertyProbe");
light->SetIntensity(1.25f);
light->SetRange(12.0f);
light->SetSpotAngle(33.0f);
light->SetCastsShadows(false);
engine->OnRuntimeStart(runtimeScene);
engine->OnUpdate(0.016f);
bool lightLookupSucceeded = false;
bool observedCastsShadows = true;
float observedIntensity = 0.0f;
float observedRange = 0.0f;
float observedSpotAngle = 0.0f;
EXPECT_TRUE(runtime->TryGetFieldValue(component, "LightLookupSucceeded", lightLookupSucceeded));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedIntensity", observedIntensity));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedRange", observedRange));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedSpotAngle", observedSpotAngle));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedCastsShadows", observedCastsShadows));
EXPECT_TRUE(lightLookupSucceeded);
EXPECT_FLOAT_EQ(observedIntensity, 1.25f);
EXPECT_FLOAT_EQ(observedRange, 12.0f);
EXPECT_FLOAT_EQ(observedSpotAngle, 33.0f);
EXPECT_FALSE(observedCastsShadows);
EXPECT_FLOAT_EQ(light->GetIntensity(), 2.5f);
EXPECT_FLOAT_EQ(light->GetRange(), 42.0f);
EXPECT_FLOAT_EQ(light->GetSpotAngle(), 55.0f);
EXPECT_TRUE(light->GetCastsShadows());
}
TEST_F(MonoScriptRuntimeTest, ManagedMeshRendererWrapperReadAndWriteFlagsOnly) {
Scene* runtimeScene = CreateScene("MonoRuntimeScene");
GameObject* host = runtimeScene->CreateGameObject("Host");
MeshRendererComponent* meshRenderer = host->AddComponent<MeshRendererComponent>();
ScriptComponent* component = AddScript(host, "Gameplay", "MeshRendererFlagsProbe");
meshRenderer->SetCastShadows(true);
meshRenderer->SetReceiveShadows(false);
meshRenderer->SetRenderLayer(7);
engine->OnRuntimeStart(runtimeScene);
engine->OnUpdate(0.016f);
bool meshRendererLookupSucceeded = false;
bool observedCastShadows = false;
bool observedReceiveShadows = true;
int32_t observedRenderLayer = 0;
EXPECT_TRUE(runtime->TryGetFieldValue(component, "MeshRendererLookupSucceeded", meshRendererLookupSucceeded));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedCastShadows", observedCastShadows));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedReceiveShadows", observedReceiveShadows));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedRenderLayer", observedRenderLayer));
EXPECT_TRUE(meshRendererLookupSucceeded);
EXPECT_TRUE(observedCastShadows);
EXPECT_FALSE(observedReceiveShadows);
EXPECT_EQ(observedRenderLayer, 7);
EXPECT_FALSE(meshRenderer->GetCastShadows());
EXPECT_TRUE(meshRenderer->GetReceiveShadows());
EXPECT_EQ(meshRenderer->GetRenderLayer(), 11u);
}
TEST_F(MonoScriptRuntimeTest, ManagedMeshRendererWrapperReadAndWriteMaterialPathsOnly) {
Scene* runtimeScene = CreateScene("MonoRuntimeScene");
GameObject* host = runtimeScene->CreateGameObject("Host");
MeshRendererComponent* meshRenderer = host->AddComponent<MeshRendererComponent>();
ScriptComponent* component = AddScript(host, "Gameplay", "MeshRendererPathProbe");
meshRenderer->SetMaterialPath(0, "Materials/initial.mat");
engine->OnRuntimeStart(runtimeScene);
engine->OnUpdate(0.016f);
bool meshRendererLookupSucceeded = false;
int32_t observedInitialMaterialCount = 0;
std::string observedInitialMaterial0Path;
int32_t observedUpdatedMaterialCount = 0;
std::string observedUpdatedMaterial1Path;
EXPECT_TRUE(runtime->TryGetFieldValue(component, "MeshRendererLookupSucceeded", meshRendererLookupSucceeded));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedInitialMaterialCount", observedInitialMaterialCount));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedInitialMaterial0Path", observedInitialMaterial0Path));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedUpdatedMaterialCount", observedUpdatedMaterialCount));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedUpdatedMaterial1Path", observedUpdatedMaterial1Path));
EXPECT_TRUE(meshRendererLookupSucceeded);
EXPECT_EQ(observedInitialMaterialCount, 1);
EXPECT_EQ(observedInitialMaterial0Path, "Materials/initial.mat");
EXPECT_EQ(observedUpdatedMaterialCount, 2);
EXPECT_EQ(observedUpdatedMaterial1Path, "Materials/runtime_override.mat");
ASSERT_EQ(meshRenderer->GetMaterialCount(), 2u);
EXPECT_EQ(meshRenderer->GetMaterialPath(0), "Materials/initial.mat");
EXPECT_EQ(meshRenderer->GetMaterialPath(1), "Materials/runtime_override.mat");
}
TEST_F(MonoScriptRuntimeTest, ManagedMeshComponentWrappersReadAndWritePathsAndFlags) {
Scene* runtimeScene = CreateScene("MonoRuntimeScene");
GameObject* host = runtimeScene->CreateGameObject("Host");