feat: expand editor scripting asset and viewport flow
This commit is contained in:
@@ -111,17 +111,34 @@ TEST(MeshFilterComponent_Test, SerializeAndDeserializePreservesPath) {
|
||||
|
||||
std::stringstream stream;
|
||||
source.Serialize(stream);
|
||||
const std::string serialized = stream.str();
|
||||
EXPECT_NE(serialized.find("meshRef="), std::string::npos);
|
||||
EXPECT_NE(serialized.find("meshPath=Meshes/serialized.mesh;"), std::string::npos);
|
||||
EXPECT_EQ(serialized.find("mesh=Meshes/serialized.mesh;"), std::string::npos);
|
||||
|
||||
MeshFilterComponent target;
|
||||
target.Deserialize(stream);
|
||||
std::stringstream deserializeStream(serialized);
|
||||
target.Deserialize(deserializeStream);
|
||||
|
||||
EXPECT_EQ(target.GetMeshPath(), "Meshes/serialized.mesh");
|
||||
EXPECT_EQ(target.GetMesh(), nullptr);
|
||||
EXPECT_FALSE(target.GetMeshAssetRef().IsValid());
|
||||
|
||||
source.ClearMesh();
|
||||
delete mesh;
|
||||
}
|
||||
|
||||
TEST(MeshFilterComponent_Test, DeserializeSupportsLegacyMeshKey) {
|
||||
MeshFilterComponent target;
|
||||
|
||||
std::stringstream stream("mesh=Meshes/legacy.mesh;meshRef=;");
|
||||
target.Deserialize(stream);
|
||||
|
||||
EXPECT_EQ(target.GetMeshPath(), "Meshes/legacy.mesh");
|
||||
EXPECT_EQ(target.GetMesh(), nullptr);
|
||||
EXPECT_FALSE(target.GetMeshAssetRef().IsValid());
|
||||
}
|
||||
|
||||
TEST(MeshFilterComponent_Test, SetMeshPathPreservesPathWithoutLoadedResource) {
|
||||
MeshFilterComponent component;
|
||||
|
||||
@@ -144,15 +161,17 @@ TEST(MeshFilterComponent_Test, DeferredSceneDeserializeLoadsMeshAsyncByPath) {
|
||||
manager.RegisterLoader(&fakeLoader);
|
||||
|
||||
MeshFilterComponent target;
|
||||
const auto pendingBeforeDeserialize = manager.GetAsyncPendingCount();
|
||||
{
|
||||
ResourceManager::ScopedDeferredSceneLoad deferredLoadScope;
|
||||
EXPECT_TRUE(manager.IsDeferredSceneLoadEnabled());
|
||||
std::stringstream stream("mesh=Meshes/async.mesh;meshRef=;");
|
||||
target.Deserialize(stream);
|
||||
EXPECT_GT(manager.GetAsyncPendingCount(), 0u);
|
||||
}
|
||||
|
||||
EXPECT_EQ(target.GetMeshPath(), "Meshes/async.mesh");
|
||||
EXPECT_EQ(target.GetMesh(), nullptr);
|
||||
EXPECT_GT(manager.GetAsyncPendingCount(), pendingBeforeDeserialize);
|
||||
ASSERT_TRUE(PumpAsyncLoadsUntilIdle(manager));
|
||||
ASSERT_NE(target.GetMesh(), nullptr);
|
||||
EXPECT_EQ(target.GetMeshPath(), "Meshes/async.mesh");
|
||||
@@ -200,9 +219,16 @@ TEST(MeshRendererComponent_Test, SerializeAndDeserializePreservesMaterialPathsAn
|
||||
|
||||
std::stringstream stream;
|
||||
source.Serialize(stream);
|
||||
const std::string serialized = stream.str();
|
||||
EXPECT_NE(
|
||||
serialized.find("materialPaths=Materials/serialized0.mat|Materials/serialized1.mat;"),
|
||||
std::string::npos);
|
||||
EXPECT_NE(serialized.find("materialRefs=|;"), std::string::npos);
|
||||
EXPECT_EQ(serialized.find("materials="), std::string::npos);
|
||||
|
||||
MeshRendererComponent target;
|
||||
target.Deserialize(stream);
|
||||
std::stringstream deserializeStream(serialized);
|
||||
target.Deserialize(deserializeStream);
|
||||
|
||||
ASSERT_EQ(target.GetMaterialCount(), 2u);
|
||||
EXPECT_EQ(target.GetMaterial(0), nullptr);
|
||||
@@ -229,9 +255,14 @@ TEST(MeshRendererComponent_Test, SerializeAndDeserializePreservesTrailingEmptyMa
|
||||
|
||||
std::stringstream stream;
|
||||
source.Serialize(stream);
|
||||
const std::string serialized = stream.str();
|
||||
EXPECT_NE(serialized.find("materialPaths=Materials/serialized0.mat|;"), std::string::npos);
|
||||
EXPECT_NE(serialized.find("materialRefs=|;"), std::string::npos);
|
||||
EXPECT_EQ(serialized.find("materials="), std::string::npos);
|
||||
|
||||
MeshRendererComponent target;
|
||||
target.Deserialize(stream);
|
||||
std::stringstream deserializeStream(serialized);
|
||||
target.Deserialize(deserializeStream);
|
||||
|
||||
ASSERT_EQ(target.GetMaterialCount(), 2u);
|
||||
EXPECT_EQ(target.GetMaterial(0), nullptr);
|
||||
@@ -262,6 +293,23 @@ TEST(MeshRendererComponent_Test, SetMaterialPathPreservesPathWithoutLoadedResour
|
||||
EXPECT_EQ(component.GetMaterial(1), nullptr);
|
||||
}
|
||||
|
||||
TEST(MeshRendererComponent_Test, DeserializeSupportsLegacyMaterialsKey) {
|
||||
MeshRendererComponent target;
|
||||
|
||||
std::stringstream stream(
|
||||
"materials=Materials/legacy0.mat|;materialRefs=|;castShadows=0;receiveShadows=1;renderLayer=5;");
|
||||
target.Deserialize(stream);
|
||||
|
||||
ASSERT_EQ(target.GetMaterialCount(), 2u);
|
||||
EXPECT_EQ(target.GetMaterialPath(0), "Materials/legacy0.mat");
|
||||
EXPECT_EQ(target.GetMaterialPath(1), "");
|
||||
EXPECT_EQ(target.GetMaterial(0), nullptr);
|
||||
EXPECT_EQ(target.GetMaterial(1), nullptr);
|
||||
EXPECT_FALSE(target.GetCastShadows());
|
||||
EXPECT_TRUE(target.GetReceiveShadows());
|
||||
EXPECT_EQ(target.GetRenderLayer(), 5u);
|
||||
}
|
||||
|
||||
TEST(MeshRendererComponent_Test, SerializeAndDeserializeLoadsProjectMaterialByAssetRef) {
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
@@ -300,8 +348,10 @@ TEST(MeshRendererComponent_Test, SerializeAndDeserializeLoadsProjectMaterialByAs
|
||||
std::stringstream stream;
|
||||
source.Serialize(stream);
|
||||
const std::string serialized = stream.str();
|
||||
EXPECT_NE(serialized.find("materialPaths=;"), std::string::npos);
|
||||
EXPECT_NE(serialized.find("materialRefs="), std::string::npos);
|
||||
EXPECT_EQ(serialized.find("materialRefs=;"), std::string::npos);
|
||||
EXPECT_EQ(serialized.find("materials="), std::string::npos);
|
||||
|
||||
std::stringstream deserializeStream(serialized);
|
||||
MeshRendererComponent target;
|
||||
@@ -350,16 +400,19 @@ TEST(MeshRendererComponent_Test, DeferredSceneDeserializeLoadsProjectMaterialAsy
|
||||
source.Serialize(serializedStream);
|
||||
|
||||
MeshRendererComponent target;
|
||||
const auto pendingBeforeDeserialize = manager.GetAsyncPendingCount();
|
||||
{
|
||||
ResourceManager::ScopedDeferredSceneLoad deferredLoadScope;
|
||||
EXPECT_TRUE(manager.IsDeferredSceneLoadEnabled());
|
||||
std::stringstream deserializeStream(serializedStream.str());
|
||||
target.Deserialize(deserializeStream);
|
||||
EXPECT_GT(manager.GetAsyncPendingCount(), 0u);
|
||||
EXPECT_EQ(manager.GetAsyncPendingCount(), pendingBeforeDeserialize);
|
||||
}
|
||||
|
||||
ASSERT_EQ(target.GetMaterialCount(), 1u);
|
||||
EXPECT_EQ(target.GetMaterialPath(0), "Assets/runtime.material");
|
||||
EXPECT_EQ(target.GetMaterial(0), nullptr);
|
||||
EXPECT_GT(manager.GetAsyncPendingCount(), pendingBeforeDeserialize);
|
||||
|
||||
ASSERT_TRUE(PumpAsyncLoadsUntilIdle(manager));
|
||||
ASSERT_NE(target.GetMaterial(0), nullptr);
|
||||
|
||||
@@ -285,6 +285,47 @@ TEST(InputManager, GetButtonUp) {
|
||||
mgr.Shutdown();
|
||||
}
|
||||
|
||||
TEST(InputManager, AnyKeyIncludesKeyboardAndMouseButtons) {
|
||||
InputManager& mgr = InputManager::Get();
|
||||
mgr.Initialize(nullptr);
|
||||
|
||||
EXPECT_FALSE(mgr.IsAnyKeyDown());
|
||||
|
||||
mgr.ProcessKeyDown(KeyCode::A, false, false, false, false, false);
|
||||
EXPECT_TRUE(mgr.IsAnyKeyDown());
|
||||
|
||||
mgr.ProcessKeyUp(KeyCode::A, false, false, false, false);
|
||||
EXPECT_FALSE(mgr.IsAnyKeyDown());
|
||||
|
||||
mgr.ProcessMouseButton(MouseButton::Left, true, 100, 200);
|
||||
EXPECT_TRUE(mgr.IsAnyKeyDown());
|
||||
|
||||
mgr.ProcessMouseButton(MouseButton::Left, false, 100, 200);
|
||||
EXPECT_FALSE(mgr.IsAnyKeyDown());
|
||||
|
||||
mgr.Shutdown();
|
||||
}
|
||||
|
||||
TEST(InputManager, AnyKeyPressedIsFrameScoped) {
|
||||
InputManager& mgr = InputManager::Get();
|
||||
mgr.Initialize(nullptr);
|
||||
|
||||
EXPECT_FALSE(mgr.IsAnyKeyPressed());
|
||||
|
||||
mgr.ProcessMouseButton(MouseButton::Left, true, 100, 200);
|
||||
EXPECT_TRUE(mgr.IsAnyKeyPressed());
|
||||
|
||||
mgr.Update(0.016f);
|
||||
EXPECT_FALSE(mgr.IsAnyKeyPressed());
|
||||
EXPECT_TRUE(mgr.IsAnyKeyDown());
|
||||
|
||||
mgr.ProcessMouseButton(MouseButton::Left, false, 100, 200);
|
||||
EXPECT_FALSE(mgr.IsAnyKeyPressed());
|
||||
EXPECT_FALSE(mgr.IsAnyKeyDown());
|
||||
|
||||
mgr.Shutdown();
|
||||
}
|
||||
|
||||
TEST(InputManager, RegisterAxis) {
|
||||
InputManager& mgr = InputManager::Get();
|
||||
mgr.Initialize(nullptr);
|
||||
@@ -320,7 +361,7 @@ TEST(InputManager, GetAxisRaw) {
|
||||
EXPECT_EQ(mgr.GetAxisRaw("RawTest"), 1.0f);
|
||||
|
||||
mgr.Update(0.016f);
|
||||
EXPECT_EQ(mgr.GetAxisRaw("RawTest"), 0.0f);
|
||||
EXPECT_EQ(mgr.GetAxisRaw("RawTest"), 1.0f);
|
||||
|
||||
mgr.Shutdown();
|
||||
}
|
||||
|
||||
@@ -1030,12 +1030,18 @@ TEST(Scene_ProjectSample, DeferredLoadBackpackSceneEventuallyProducesVisibleRend
|
||||
loadedScene.Load(backpackScenePath.string());
|
||||
}
|
||||
|
||||
ASSERT_TRUE(PumpAsyncLoadsUntilIdle(resourceManager, std::chrono::milliseconds(10000)));
|
||||
|
||||
const std::vector<GameObject*> backpackObjects =
|
||||
FindGameObjectsByMeshPath(loadedScene, "Assets/Models/backpack/backpack.obj");
|
||||
ASSERT_EQ(backpackObjects.size(), 2u);
|
||||
|
||||
XCEngine::Rendering::RenderSceneExtractor extractor;
|
||||
const XCEngine::Rendering::RenderSceneData initialRenderScene =
|
||||
extractor.Extract(loadedScene, nullptr, 1280u, 720u);
|
||||
|
||||
ASSERT_TRUE(initialRenderScene.HasCamera());
|
||||
EXPECT_GT(resourceManager.GetAsyncPendingCount(), 0u);
|
||||
ASSERT_TRUE(PumpAsyncLoadsUntilIdle(resourceManager, std::chrono::milliseconds(10000)));
|
||||
|
||||
std::vector<MeshFilterComponent*> backpackMeshFilters;
|
||||
for (GameObject* backpackObject : backpackObjects) {
|
||||
ASSERT_NE(backpackObject, nullptr);
|
||||
@@ -1045,7 +1051,6 @@ TEST(Scene_ProjectSample, DeferredLoadBackpackSceneEventuallyProducesVisibleRend
|
||||
backpackMeshFilters.push_back(meshFilter);
|
||||
}
|
||||
|
||||
XCEngine::Rendering::RenderSceneExtractor extractor;
|
||||
const XCEngine::Rendering::RenderSceneData renderScene =
|
||||
extractor.Extract(loadedScene, nullptr, 1280u, 720u);
|
||||
|
||||
|
||||
@@ -85,6 +85,12 @@ public:
|
||||
}
|
||||
}
|
||||
|
||||
bool TryGetAvailableScriptClasses(
|
||||
std::vector<ScriptClassDescriptor>& outClasses) const override {
|
||||
outClasses.clear();
|
||||
return false;
|
||||
}
|
||||
|
||||
bool TryGetClassFieldMetadata(
|
||||
const std::string& assemblyName,
|
||||
const std::string& namespaceName,
|
||||
|
||||
@@ -1,43 +1,61 @@
|
||||
# XCEngine 测试规范
|
||||
|
||||
最后更新:2026-03-27
|
||||
最后更新:`2026-04-02`
|
||||
|
||||
## 1. 目标
|
||||
|
||||
本文档只描述当前仓库里已经存在、已经生效、并且应当长期维护的测试规范。
|
||||
本文档描述当前仓库里已经落地、已经生效、并且应当长期维护的测试基线。
|
||||
|
||||
重点覆盖:
|
||||
|
||||
- `tests/` 的整体组织方式
|
||||
- CMake / CTest 的推荐使用方式
|
||||
- RHI 模块测试的分层边界
|
||||
- RHI 抽象层集成测试的当前约束
|
||||
- `tests/` 的真实目录结构
|
||||
- 当前 CMake / CTest 入口
|
||||
- RHI 测试分层边界
|
||||
- Rendering / Editor / Scripting 相关聚合 target
|
||||
|
||||
如果目录结构、target 名称、后端覆盖范围或 GT 图规则发生变化,必须同步更新本文档。
|
||||
如果目录结构、target 名称、场景覆盖范围或回归入口变化,必须同步更新本文档。
|
||||
|
||||
## 2. 顶层结构
|
||||
|
||||
`tests/CMakeLists.txt` 当前纳入的主要测试模块如下:
|
||||
`tests/CMakeLists.txt` 当前纳入的主模块如下:
|
||||
|
||||
```text
|
||||
tests/
|
||||
├─ math/
|
||||
├─ core/
|
||||
├─ containers/
|
||||
├─ memory/
|
||||
├─ threading/
|
||||
├─ debug/
|
||||
├─ components/
|
||||
├─ scene/
|
||||
├─ Components/
|
||||
├─ Core/
|
||||
│ ├─ Asset/
|
||||
│ ├─ Containers/
|
||||
│ ├─ IO/
|
||||
│ └─ Math/
|
||||
├─ Debug/
|
||||
├─ Editor/
|
||||
├─ Fixtures/
|
||||
├─ Input/
|
||||
├─ Memory/
|
||||
├─ Rendering/
|
||||
│ ├─ integration/
|
||||
│ └─ unit/
|
||||
├─ Resources/
|
||||
│ ├─ AudioClip/
|
||||
│ ├─ Material/
|
||||
│ ├─ Mesh/
|
||||
│ ├─ Shader/
|
||||
│ └─ Texture/
|
||||
├─ RHI/
|
||||
├─ resources/
|
||||
└─ input/
|
||||
│ ├─ D3D12/
|
||||
│ ├─ integration/
|
||||
│ ├─ OpenGL/
|
||||
│ ├─ unit/
|
||||
│ └─ Vulkan/
|
||||
├─ Scene/
|
||||
├─ Scripting/
|
||||
└─ Threading/
|
||||
```
|
||||
|
||||
说明:
|
||||
|
||||
- 新增测试模块时,必须同时补充对应目录下的 `CMakeLists.txt`,并在 `tests/CMakeLists.txt` 中注册。
|
||||
- 测试目录命名、target 命名、文档命名必须和实际仓库状态一致,不保留“计划中但未落地”的旧说明。
|
||||
- 新增测试模块时,必须同时补对应目录下的 `CMakeLists.txt`,并在 `tests/CMakeLists.txt` 注册。
|
||||
- 目录名、target 名、文档说明必须与当前 checkout 一致,不保留“计划中但未落地”的旧描述。
|
||||
|
||||
## 3. 基本构建方式
|
||||
|
||||
@@ -46,113 +64,111 @@ tests/
|
||||
### 3.1 配置
|
||||
|
||||
```bash
|
||||
cmake -S . -B build
|
||||
cmake -S . -B build -A x64
|
||||
```
|
||||
|
||||
以下场景需要重新配置:
|
||||
如果当前任务不需要 Mono:
|
||||
|
||||
```bash
|
||||
cmake -S . -B build -A x64 -DXCENGINE_ENABLE_MONO_SCRIPTING=OFF
|
||||
```
|
||||
|
||||
以下情况需要重新配置:
|
||||
|
||||
- 新增或删除源文件
|
||||
- 修改任意 `CMakeLists.txt`
|
||||
- 修改 target、依赖、编译定义或测试发现方式
|
||||
- 修改 target、依赖、编译定义、测试发现方式
|
||||
|
||||
### 3.2 增量构建
|
||||
### 3.2 通用入口
|
||||
|
||||
```bash
|
||||
cmake --build build --config Debug
|
||||
```
|
||||
|
||||
常用 RHI 聚合 target:
|
||||
|
||||
```bash
|
||||
cmake --build build --config Debug --target rhi_abstraction_tests
|
||||
cmake --build build --config Debug --target rhi_backend_tests
|
||||
cmake --build build --config Debug --target rhi_all_tests
|
||||
```
|
||||
|
||||
常用单项 target:
|
||||
|
||||
```bash
|
||||
cmake --build build --config Debug --target rhi_unit_tests
|
||||
cmake --build build --config Debug --target rhi_integration_minimal
|
||||
cmake --build build --config Debug --target rhi_integration_triangle
|
||||
cmake --build build --config Debug --target rhi_integration_quad
|
||||
cmake --build build --config Debug --target rhi_integration_sphere
|
||||
cmake --build build --config Debug --target rhi_integration_backpack
|
||||
```
|
||||
|
||||
### 3.3 运行
|
||||
|
||||
列出测试:
|
||||
|
||||
```bash
|
||||
ctest --test-dir build -N -C Debug
|
||||
```
|
||||
|
||||
运行全部测试:
|
||||
|
||||
```bash
|
||||
ctest --test-dir build -C Debug --output-on-failure
|
||||
```
|
||||
|
||||
直接运行 gtest 可执行文件也是合法入口。例如:
|
||||
## 4. 当前主要 target
|
||||
|
||||
```bash
|
||||
build\tests\RHI\unit\Debug\rhi_unit_tests.exe --gtest_brief=1
|
||||
build\tests\RHI\integration\triangle\Debug\rhi_integration_triangle.exe --gtest_filter=Vulkan/TriangleTest.RenderTriangle/0
|
||||
build\tests\RHI\integration\backpack\Debug\rhi_integration_backpack.exe --gtest_filter=D3D12/BackpackTest.RenderBackpack/0
|
||||
```
|
||||
### 4.1 Engine 基础模块
|
||||
|
||||
## 4. RHI 测试分层
|
||||
| 模块 | target |
|
||||
| --- | --- |
|
||||
| Core | `core_tests` |
|
||||
| Core/Asset | `asset_tests` |
|
||||
| Core/Containers | `containers_tests` |
|
||||
| Core/IO | `io_tests` |
|
||||
| Core/Math | `math_tests` |
|
||||
| Memory | `memory_tests` |
|
||||
| Threading | `threading_tests` |
|
||||
| Debug | `debug_tests` |
|
||||
| Components | `components_tests` |
|
||||
| Scene | `scene_tests` |
|
||||
| Input | `input_tests` |
|
||||
|
||||
RHI 当前分为四层测试:
|
||||
### 4.2 Resources
|
||||
|
||||
| 层级 | 目录 / target | 目标 |
|
||||
| --- | --- | --- |
|
||||
| 抽象层单元测试 | `tests/RHI/unit/` / `rhi_unit_tests` | 验证公共 RHI 接口在 `D3D12 / OpenGL / Vulkan` 上的统一语义 |
|
||||
| 抽象层集成测试 | `tests/RHI/integration/` / `rhi_integration_*` | 用同一套 RHI 抽象代码驱动三后端完成真实渲染并做 GT 图比对 |
|
||||
| D3D12 后端测试 | `tests/RHI/D3D12/` | 验证 D3D12 封装本身 |
|
||||
| OpenGL 后端测试 | `tests/RHI/OpenGL/` | 验证 OpenGL 封装本身 |
|
||||
| Vulkan 后端测试 | `tests/RHI/Vulkan/` | 验证 Vulkan 封装本身 |
|
||||
| 模块 | target |
|
||||
| --- | --- |
|
||||
| AudioClip | `audioclip_tests` |
|
||||
| Material | `material_tests` |
|
||||
| Mesh | `mesh_tests` |
|
||||
| Shader | `shader_tests` |
|
||||
| Texture | `texture_tests` |
|
||||
|
||||
这些测试当前不只是验证简单 loader,还会覆盖:
|
||||
|
||||
- `AssetDatabase`
|
||||
- `.meta`
|
||||
- `Library/SourceAssetDB`
|
||||
- `Library/ArtifactDB`
|
||||
- artifact 重导入逻辑
|
||||
|
||||
### 4.3 Editor / Scripting
|
||||
|
||||
| 模块 | target |
|
||||
| --- | --- |
|
||||
| Editor | `editor_tests` |
|
||||
| Scripting | `scripting_tests` |
|
||||
|
||||
补充说明:
|
||||
|
||||
- Vulkan 现在已经拥有独立的 `tests/RHI/Vulkan/` 子树。
|
||||
- `tests/RHI/unit/` 继续只保留三后端参数化的抽象层统一语义测试。
|
||||
- Vulkan 专属断言、原生句柄检查与直接依赖 Vulkan API 的测试,统一收敛到 `tests/RHI/Vulkan/unit/`。
|
||||
- Vulkan 现在已经建立独立的后端 integration 子树,当前已覆盖 `tests/RHI/Vulkan/integration/minimal/`、`triangle/`、`quad/`、`sphere/`。
|
||||
- Vulkan 后端后续仍可继续补更复杂的 backend integration,但不应再回流到 abstraction suite。
|
||||
- `editor_tests` 当前覆盖 action routing、play session、viewport camera controller、picker、move/rotate/scale gizmo、overlay renderer、script component editor utils 等。
|
||||
- `scripting_tests` 在启用 Mono 时会补入 `test_mono_script_runtime.cpp`。
|
||||
- 如果存在 `xcengine_project_managed_assemblies` target,`scripting_tests` 还会补入项目脚本程序集相关测试。
|
||||
|
||||
设计边界:
|
||||
|
||||
- `tests/RHI/unit/` 与 `tests/RHI/integration/` 只能依赖公共 RHI 抽象接口。
|
||||
- 后端私有头文件、原生句柄、后端专用 helper,只允许出现在对应后端目录。
|
||||
- 如果抽象层测试为了通过而被迫引入后端 API,优先修 RHI 本身,而不是给测试开后门。
|
||||
|
||||
## 5. 当前 RHI Target
|
||||
|
||||
### 5.1 抽象层
|
||||
### 4.4 Rendering
|
||||
|
||||
| 类别 | target |
|
||||
| --- | --- |
|
||||
| 抽象层单元测试 | `rhi_unit_tests` |
|
||||
| 抽象层集成测试 | `rhi_integration_minimal` |
|
||||
| 抽象层集成测试 | `rhi_integration_triangle` |
|
||||
| 抽象层集成测试 | `rhi_integration_quad` |
|
||||
| 抽象层集成测试 | `rhi_integration_sphere` |
|
||||
| 抽象层集成测试 | `rhi_integration_backpack` |
|
||||
| 单元测试 | `rendering_unit_tests` |
|
||||
| 单测聚合 | `rendering_unit_test_targets` |
|
||||
| 集成聚合 | `rendering_integration_tests` |
|
||||
| 全量聚合 | `rendering_all_tests` |
|
||||
| 阶段回归 | `rendering_phase_regression` |
|
||||
|
||||
### 5.2 后端专用
|
||||
当前 rendering integration targets:
|
||||
|
||||
| 类别 | target |
|
||||
| --- | --- |
|
||||
| D3D12 后端单元测试 | `rhi_d3d12_tests` |
|
||||
| OpenGL 后端单元测试 | `rhi_opengl_tests` |
|
||||
| Vulkan 后端单元测试 | `rhi_vulkan_tests` |
|
||||
| D3D12 后端集成测试 | `d3d12_minimal_test` `d3d12_triangle_test` `d3d12_quad_test` `d3d12_sphere_test` |
|
||||
| OpenGL 后端集成测试 | `opengl_minimal_test` `opengl_triangle_test` `opengl_quad_test` `opengl_sphere_test` |
|
||||
| Vulkan 后端集成测试 | `vulkan_minimal_test` `vulkan_triangle_test` `vulkan_quad_test` `vulkan_sphere_test` |
|
||||
- `rendering_integration_textured_quad_scene`
|
||||
- `rendering_integration_backpack_scene`
|
||||
- `rendering_integration_backpack_lit_scene`
|
||||
- `rendering_integration_camera_stack_scene`
|
||||
- `rendering_integration_transparent_material_scene`
|
||||
- `rendering_integration_cull_material_scene`
|
||||
- `rendering_integration_depth_sort_scene`
|
||||
- `rendering_integration_material_state_scene`
|
||||
- `rendering_integration_offscreen_scene`
|
||||
|
||||
### 5.3 聚合 target
|
||||
`rendering_phase_regression` 当前是 Windows 下的 PowerShell 回归入口,依赖:
|
||||
|
||||
- `rendering_all_tests`
|
||||
- `editor_tests`
|
||||
- `XCEditor`
|
||||
|
||||
并执行:
|
||||
|
||||
- `scripts/Run-RendererPhaseRegression.ps1`
|
||||
|
||||
### 4.5 RHI
|
||||
|
||||
| 类别 | target |
|
||||
| --- | --- |
|
||||
@@ -165,6 +181,53 @@ RHI 当前分为四层测试:
|
||||
| 后端总聚合 | `rhi_backend_tests` |
|
||||
| RHI 全量聚合 | `rhi_all_tests` |
|
||||
|
||||
抽象层单项 target:
|
||||
|
||||
- `rhi_unit_tests`
|
||||
- `rhi_integration_minimal`
|
||||
- `rhi_integration_triangle`
|
||||
- `rhi_integration_quad`
|
||||
- `rhi_integration_sphere`
|
||||
- `rhi_integration_backpack`
|
||||
|
||||
后端单元测试 target:
|
||||
|
||||
- `rhi_d3d12_tests`
|
||||
- `rhi_opengl_tests`
|
||||
- `rhi_vulkan_tests`
|
||||
|
||||
后端集成测试 target:
|
||||
|
||||
- `d3d12_minimal_test`
|
||||
- `d3d12_triangle_test`
|
||||
- `d3d12_quad_test`
|
||||
- `d3d12_sphere_test`
|
||||
- `opengl_minimal_test`
|
||||
- `opengl_triangle_test`
|
||||
- `opengl_quad_test`
|
||||
- `opengl_sphere_test`
|
||||
- `vulkan_minimal_test`
|
||||
- `vulkan_triangle_test`
|
||||
- `vulkan_quad_test`
|
||||
- `vulkan_sphere_test`
|
||||
|
||||
## 5. RHI 测试分层
|
||||
|
||||
RHI 当前分为五块:
|
||||
|
||||
- `tests/RHI/unit/`
|
||||
- `tests/RHI/integration/`
|
||||
- `tests/RHI/D3D12/`
|
||||
- `tests/RHI/OpenGL/`
|
||||
- `tests/RHI/Vulkan/`
|
||||
|
||||
边界规则:
|
||||
|
||||
- `tests/RHI/unit/` 与 `tests/RHI/integration/` 只能依赖公共 RHI 抽象接口。
|
||||
- 后端私有头文件、原生句柄、后端专用 helper,只允许出现在对应后端目录。
|
||||
- 如果抽象层测试为了通过而被迫引入后端 API,优先修 RHI,而不是给测试开后门。
|
||||
- Vulkan 专属断言、原生句柄检查与直接依赖 Vulkan API 的测试,应继续收敛在 `tests/RHI/Vulkan/` 子树。
|
||||
|
||||
## 6. RHI 抽象层集成测试规范
|
||||
|
||||
### 6.1 当前目录
|
||||
@@ -208,24 +271,10 @@ tests/RHI/integration/
|
||||
- `minimal_vulkan.ppm`
|
||||
5. 所有后端都必须与同一张 `GT.ppm` 做比对。
|
||||
6. 新场景如果暴露抽象层缺口,应先补 RHI,再补测试。
|
||||
7. `sphere` 必须继续保留对 `firstSet != 0` 绑定路径的覆盖,避免“忽略 set 语义也误通过”的假阳性。
|
||||
8. `backpack` 是当前抽象层最接近真实资源渲染的场景,后续新增复杂渲染场景时不应削弱它的回归价值。
|
||||
7. `sphere` 必须继续保留对 `firstSet != 0` 绑定路径的覆盖。
|
||||
8. `backpack` 作为最接近真实资源路径的抽象层场景,不应随意削弱。
|
||||
|
||||
### 6.4 CMake 约束
|
||||
|
||||
每个抽象层集成测试目录都应:
|
||||
|
||||
- 使用独立 target
|
||||
- 复用 `fixtures/RHIIntegrationFixture.cpp`
|
||||
- 链接 `XCEngine`、`GTest::gtest`
|
||||
- 在需要时按后端条件加入对应系统库
|
||||
- 在 `POST_BUILD` 中复制:
|
||||
- `compare_ppm.py`
|
||||
- 当前场景的 `GT.ppm`
|
||||
- 运行所需的资源文件
|
||||
- 使用 `gtest_discover_tests(...)`
|
||||
|
||||
### 6.5 推荐验证方式
|
||||
### 6.4 推荐验证方式
|
||||
|
||||
```bash
|
||||
cmake --build build --config Debug --target rhi_abstraction_integration_tests
|
||||
@@ -242,23 +291,21 @@ build\tests\RHI\integration\backpack\Debug\rhi_integration_backpack.exe --gtest_
|
||||
- 截图成功
|
||||
- 与当前场景 `GT.ppm` 比对通过
|
||||
|
||||
## 7. 当前体系的完成度与后续方向
|
||||
## 7. 推荐的验证策略
|
||||
|
||||
当前状态可以认为是“高完成度、可作为正式基线”,但不是“完全封顶”。
|
||||
按改动类型选择最小有效验证:
|
||||
|
||||
已经完成:
|
||||
- 改 `engine/RHI`:先跑 `rhi_abstraction_tests` 或 `rhi_backend_tests`
|
||||
- 改 `engine/Rendering`:先跑 `rendering_unit_tests` 和最相关的 `rendering_integration_*`
|
||||
- 改 `editor/Viewport`:先跑 `editor_tests`,必要时再跑 `rendering_phase_regression`
|
||||
- 改脚本运行时 / managed / 项目脚本程序集:先构建 `xcengine_project_managed_assemblies`,再跑 `scripting_tests`
|
||||
- 改资源导入 / `.meta` / artifact:优先跑对应 `Resources/*` tests
|
||||
|
||||
- 抽象层单测正式纳入 `D3D12 / OpenGL / Vulkan`
|
||||
- 抽象层集成测试已经具备 5 个真实渲染场景
|
||||
- GT 图回归链路已经稳定工作
|
||||
- D3D12 与 OpenGL 仍保留独立后端测试树
|
||||
在这些最小验证通过后,再决定是否扩展到:
|
||||
|
||||
仍需继续完善:
|
||||
|
||||
- 继续补更工程化的 Vulkan 后端 integration 场景覆盖
|
||||
- 把仍然合理存在的后端专属断言与 skip 场景继续收敛
|
||||
- 补充 `resize / swapchain 重建 / 长时间 soak / 多线程录制 / validation layer 负例` 等更工程化的测试
|
||||
- 保持文档、CMake target 与实际测试状态同步
|
||||
```bash
|
||||
ctest --test-dir build -C Debug --output-on-failure
|
||||
```
|
||||
|
||||
## 8. 文档维护要求
|
||||
|
||||
@@ -266,9 +313,9 @@ build\tests\RHI\integration\backpack\Debug\rhi_integration_backpack.exe --gtest_
|
||||
|
||||
- 测试目录结构变化
|
||||
- 新增或删除测试 target
|
||||
- 抽象层集成测试场景变化
|
||||
- GT 图管理规则变化
|
||||
- CMake 聚合入口变化
|
||||
- 后端覆盖范围变化
|
||||
- rendering integration 场景变化
|
||||
- RHI 抽象层 GT 图规则变化
|
||||
- PowerShell 回归入口变化
|
||||
- 项目脚本程序集参与测试的方式变化
|
||||
|
||||
禁止继续保留和当前仓库状态不一致的旧说明。
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <XCEngine/Core/Asset/AssetImportService.h>
|
||||
#include <XCEngine/Core/Asset/ProjectAssetIndex.h>
|
||||
#include <XCEngine/Core/Asset/ResourceManager.h>
|
||||
#include <XCEngine/Core/IO/IResourceLoader.h>
|
||||
#include <XCEngine/Resources/Mesh/Mesh.h>
|
||||
@@ -7,6 +9,8 @@
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <condition_variable>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <functional>
|
||||
#include <mutex>
|
||||
#include <thread>
|
||||
@@ -155,4 +159,77 @@ TEST(ResourceManager_Test, ConcurrentAsyncLoadsCoalesceSameMeshPath) {
|
||||
manager.Shutdown();
|
||||
}
|
||||
|
||||
TEST(ResourceManager_Test, AssetLookupFallbackRefreshesSnapshotForNewProjectAsset) {
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
ResourceManager& manager = ResourceManager::Get();
|
||||
manager.Initialize();
|
||||
|
||||
const fs::path projectRoot = fs::temp_directory_path() / "xc_resource_manager_asset_lookup_test";
|
||||
const fs::path assetsDir = projectRoot / "Assets";
|
||||
const fs::path materialPath = assetsDir / "runtime.material";
|
||||
|
||||
fs::remove_all(projectRoot);
|
||||
fs::create_directories(assetsDir);
|
||||
manager.SetResourceRoot(projectRoot.string().c_str());
|
||||
|
||||
{
|
||||
std::ofstream materialFile(materialPath);
|
||||
ASSERT_TRUE(materialFile.is_open());
|
||||
materialFile << "{\n";
|
||||
materialFile << " \"renderQueue\": \"geometry\"\n";
|
||||
materialFile << "}\n";
|
||||
}
|
||||
|
||||
AssetRef assetRef;
|
||||
EXPECT_TRUE(manager.TryGetAssetRef("Assets/runtime.material", ResourceType::Material, assetRef));
|
||||
EXPECT_TRUE(assetRef.IsValid());
|
||||
|
||||
XCEngine::Containers::String resolvedPath;
|
||||
EXPECT_TRUE(manager.TryResolveAssetPath(assetRef, resolvedPath));
|
||||
EXPECT_EQ(std::string(resolvedPath.CStr()), "Assets/runtime.material");
|
||||
|
||||
manager.SetResourceRoot("");
|
||||
manager.Shutdown();
|
||||
fs::remove_all(projectRoot);
|
||||
}
|
||||
|
||||
TEST(ProjectAssetIndex_Test, RefreshesSnapshotThroughImportServiceOnCacheMiss) {
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
AssetImportService importService;
|
||||
importService.Initialize();
|
||||
|
||||
const fs::path projectRoot = fs::temp_directory_path() / "xc_project_asset_index_refresh_test";
|
||||
const fs::path assetsDir = projectRoot / "Assets";
|
||||
const fs::path materialPath = assetsDir / "runtime.material";
|
||||
|
||||
fs::remove_all(projectRoot);
|
||||
fs::create_directories(assetsDir);
|
||||
|
||||
importService.SetProjectRoot(projectRoot.string().c_str());
|
||||
|
||||
ProjectAssetIndex assetIndex;
|
||||
assetIndex.RefreshFrom(importService);
|
||||
|
||||
{
|
||||
std::ofstream materialFile(materialPath);
|
||||
ASSERT_TRUE(materialFile.is_open());
|
||||
materialFile << "{\n";
|
||||
materialFile << " \"renderQueue\": \"geometry\"\n";
|
||||
materialFile << "}\n";
|
||||
}
|
||||
|
||||
AssetRef assetRef;
|
||||
EXPECT_TRUE(assetIndex.TryGetAssetRef(importService, "Assets/runtime.material", ResourceType::Material, assetRef));
|
||||
EXPECT_TRUE(assetRef.IsValid());
|
||||
|
||||
XCEngine::Containers::String resolvedPath;
|
||||
EXPECT_TRUE(assetIndex.TryResolveAssetPath(importService, assetRef, resolvedPath));
|
||||
EXPECT_EQ(std::string(resolvedPath.CStr()), "Assets/runtime.material");
|
||||
|
||||
importService.Shutdown();
|
||||
fs::remove_all(projectRoot);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
@@ -11,11 +11,15 @@ set(EDITOR_TEST_SOURCES
|
||||
test_scene_viewport_scale_gizmo.cpp
|
||||
test_scene_viewport_picker.cpp
|
||||
test_scene_viewport_overlay_renderer.cpp
|
||||
test_script_component_editor_utils.cpp
|
||||
test_viewport_host_surface_utils.cpp
|
||||
test_viewport_object_id_picker.cpp
|
||||
test_viewport_render_targets.cpp
|
||||
test_viewport_render_flow_utils.cpp
|
||||
test_builtin_icon_layout_utils.cpp
|
||||
test_editor_script_assembly_builder.cpp
|
||||
test_editor_script_assembly_builder_utils.cpp
|
||||
${CMAKE_SOURCE_DIR}/editor/src/Scripting/EditorScriptAssemblyBuilder.cpp
|
||||
${CMAKE_SOURCE_DIR}/editor/src/Core/UndoManager.cpp
|
||||
${CMAKE_SOURCE_DIR}/editor/src/Core/PlaySessionController.cpp
|
||||
${CMAKE_SOURCE_DIR}/editor/src/Managers/SceneManager.cpp
|
||||
@@ -26,13 +30,19 @@ set(EDITOR_TEST_SOURCES
|
||||
${CMAKE_SOURCE_DIR}/editor/src/Viewport/SceneViewportScaleGizmo.cpp
|
||||
)
|
||||
|
||||
if(XCENGINE_ENABLE_MONO_SCRIPTING AND TARGET xcengine_managed_assemblies)
|
||||
list(APPEND EDITOR_TEST_SOURCES
|
||||
test_play_session_controller_scripting.cpp
|
||||
)
|
||||
endif()
|
||||
|
||||
add_executable(editor_tests ${EDITOR_TEST_SOURCES})
|
||||
|
||||
if(MSVC)
|
||||
set_target_properties(editor_tests PROPERTIES
|
||||
LINK_FLAGS "/NODEFAULTLIB:libcpmt.lib /NODEFAULTLIB:libcmt.lib"
|
||||
)
|
||||
target_compile_options(editor_tests PRIVATE /FS)
|
||||
target_compile_options(editor_tests PRIVATE /FS /utf-8)
|
||||
endif()
|
||||
|
||||
target_link_libraries(editor_tests PRIVATE
|
||||
@@ -50,5 +60,28 @@ target_include_directories(editor_tests PRIVATE
|
||||
${CMAKE_BINARY_DIR}/_deps/imgui-src/backends
|
||||
)
|
||||
|
||||
file(TO_CMAKE_PATH "${CMAKE_SOURCE_DIR}" XCENGINE_EDITOR_TEST_REPO_ROOT_CMAKE)
|
||||
file(TO_CMAKE_PATH "${XCENGINE_MONO_ROOT_DIR}" XCENGINE_EDITOR_TEST_MONO_ROOT_CMAKE)
|
||||
|
||||
target_compile_definitions(editor_tests PRIVATE
|
||||
XCENGINE_EDITOR_REPO_ROOT="${XCENGINE_EDITOR_TEST_REPO_ROOT_CMAKE}"
|
||||
XCENGINE_EDITOR_MONO_ROOT_DIR="${XCENGINE_EDITOR_TEST_MONO_ROOT_CMAKE}"
|
||||
)
|
||||
|
||||
if(XCENGINE_ENABLE_MONO_SCRIPTING AND TARGET xcengine_managed_assemblies)
|
||||
add_dependencies(editor_tests xcengine_managed_assemblies)
|
||||
|
||||
file(TO_CMAKE_PATH "${XCENGINE_MANAGED_OUTPUT_DIR}" XCENGINE_MANAGED_OUTPUT_DIR_CMAKE)
|
||||
file(TO_CMAKE_PATH "${XCENGINE_SCRIPT_CORE_DLL}" XCENGINE_SCRIPT_CORE_DLL_CMAKE)
|
||||
file(TO_CMAKE_PATH "${XCENGINE_GAME_SCRIPTS_DLL}" XCENGINE_GAME_SCRIPTS_DLL_CMAKE)
|
||||
|
||||
target_compile_definitions(editor_tests PRIVATE
|
||||
XCENGINE_ENABLE_MONO_SCRIPTING
|
||||
XCENGINE_TEST_MANAGED_OUTPUT_DIR="${XCENGINE_MANAGED_OUTPUT_DIR_CMAKE}"
|
||||
XCENGINE_TEST_SCRIPT_CORE_DLL="${XCENGINE_SCRIPT_CORE_DLL_CMAKE}"
|
||||
XCENGINE_TEST_GAME_SCRIPTS_DLL="${XCENGINE_GAME_SCRIPTS_DLL_CMAKE}"
|
||||
)
|
||||
endif()
|
||||
|
||||
include(GoogleTest)
|
||||
gtest_discover_tests(editor_tests)
|
||||
|
||||
@@ -9,12 +9,14 @@
|
||||
#include "Core/EditorContext.h"
|
||||
#include "Core/PlaySessionController.h"
|
||||
|
||||
#include <XCEngine/Core/Asset/ResourceManager.h>
|
||||
#include <XCEngine/Core/Math/Quaternion.h>
|
||||
#include <XCEngine/Core/Math/Vector3.h>
|
||||
|
||||
#include <chrono>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <iterator>
|
||||
#include <string>
|
||||
|
||||
namespace fs = std::filesystem;
|
||||
@@ -343,6 +345,17 @@ TEST_F(EditorActionRoutingTest, MainMenuRouterRequestsPlayPauseResumeAndStepEven
|
||||
m_context.GetEventBus().Unsubscribe<PlayModeStepRequestedEvent>(playStepSubscription);
|
||||
}
|
||||
|
||||
TEST_F(EditorActionRoutingTest, ProjectCommandsReportWhenScriptAssembliesCanBeRebuilt) {
|
||||
EXPECT_TRUE(Commands::CanRebuildScriptAssemblies(m_context));
|
||||
|
||||
m_context.SetRuntimeMode(EditorRuntimeMode::Play);
|
||||
EXPECT_FALSE(Commands::CanRebuildScriptAssemblies(m_context));
|
||||
|
||||
m_context.SetRuntimeMode(EditorRuntimeMode::Edit);
|
||||
m_context.SetProjectPath(std::string());
|
||||
EXPECT_FALSE(Commands::CanRebuildScriptAssemblies(m_context));
|
||||
}
|
||||
|
||||
TEST_F(EditorActionRoutingTest, PlayModeAllowsRuntimeSceneUndoRedoButKeepsSceneDocumentCommandsBlocked) {
|
||||
const fs::path savedScenePath = m_projectRoot / "Assets" / "Scenes" / "PlayModeRuntimeEditing.xc";
|
||||
ASSERT_TRUE(m_context.GetSceneManager().SaveSceneAs(savedScenePath.string()));
|
||||
@@ -470,6 +483,68 @@ TEST_F(EditorActionRoutingTest, ProjectCommandsRenameAssetUpdatesSelectionAndPre
|
||||
EXPECT_EQ(m_context.GetProjectManager().GetSelectedItemPath(), renamedItem->fullPath);
|
||||
}
|
||||
|
||||
TEST_F(EditorActionRoutingTest, ProjectCommandsMigrateSceneAssetReferencesRewritesLegacyScenePayloads) {
|
||||
using ::XCEngine::Resources::ResourceManager;
|
||||
|
||||
const fs::path assetsDir = m_projectRoot / "Assets";
|
||||
const fs::path scenesDir = assetsDir / "Scenes";
|
||||
const fs::path materialPath = assetsDir / "runtime.material";
|
||||
const fs::path scenePath = scenesDir / "LegacyScene.xc";
|
||||
|
||||
{
|
||||
std::ofstream materialFile(materialPath.string(), std::ios::out | std::ios::trunc);
|
||||
ASSERT_TRUE(materialFile.is_open());
|
||||
materialFile << "{\n";
|
||||
materialFile << " \"renderQueue\": \"geometry\",\n";
|
||||
materialFile << " \"renderState\": {\n";
|
||||
materialFile << " \"cull\": \"back\"\n";
|
||||
materialFile << " }\n";
|
||||
materialFile << "}\n";
|
||||
}
|
||||
|
||||
{
|
||||
std::ofstream sceneFile(scenePath.string(), std::ios::out | std::ios::trunc);
|
||||
ASSERT_TRUE(sceneFile.is_open());
|
||||
sceneFile << "# XCEngine Scene File\n";
|
||||
sceneFile << "scene=Legacy Scene\n";
|
||||
sceneFile << "active=1\n\n";
|
||||
sceneFile << "gameobject_begin\n";
|
||||
sceneFile << "id=1\n";
|
||||
sceneFile << "uuid=1\n";
|
||||
sceneFile << "name=Legacy Object\n";
|
||||
sceneFile << "active=1\n";
|
||||
sceneFile << "parent=0\n";
|
||||
sceneFile << "transform=position=0,0,0;rotation=0,0,0,1;scale=1,1,1;\n";
|
||||
sceneFile << "component=MeshFilter;mesh=builtin://meshes/cube;meshRef=;\n";
|
||||
sceneFile << "component=MeshRenderer;materials=Assets/runtime.material;materialRefs=;castShadows=1;receiveShadows=1;renderLayer=0;\n";
|
||||
sceneFile << "gameobject_end\n";
|
||||
}
|
||||
|
||||
ASSERT_TRUE(Commands::CanMigrateSceneAssetReferences(m_context));
|
||||
const IProjectManager::SceneAssetReferenceMigrationReport report =
|
||||
Commands::MigrateSceneAssetReferences(m_context);
|
||||
|
||||
EXPECT_EQ(report.scannedSceneCount, 1u);
|
||||
EXPECT_EQ(report.migratedSceneCount, 1u);
|
||||
EXPECT_EQ(report.unchangedSceneCount, 0u);
|
||||
EXPECT_EQ(report.failedSceneCount, 0u);
|
||||
|
||||
std::ifstream migratedScene(scenePath.string(), std::ios::in | std::ios::binary);
|
||||
ASSERT_TRUE(migratedScene.is_open());
|
||||
std::string migratedText((std::istreambuf_iterator<char>(migratedScene)),
|
||||
std::istreambuf_iterator<char>());
|
||||
|
||||
EXPECT_NE(migratedText.find("meshPath=builtin://meshes/cube;"), std::string::npos);
|
||||
EXPECT_EQ(migratedText.find("component=MeshFilter;mesh=builtin://meshes/cube;"), std::string::npos);
|
||||
EXPECT_NE(migratedText.find("materialPaths=;"), std::string::npos);
|
||||
EXPECT_NE(migratedText.find("materialRefs="), std::string::npos);
|
||||
EXPECT_EQ(migratedText.find("materialRefs=;"), std::string::npos);
|
||||
EXPECT_EQ(migratedText.find("component=MeshRenderer;materials="), std::string::npos);
|
||||
|
||||
ResourceManager::Get().SetResourceRoot("");
|
||||
ResourceManager::Get().Shutdown();
|
||||
}
|
||||
|
||||
TEST_F(EditorActionRoutingTest, ProjectItemContextRequestSelectsAssetAndStoresPopupTarget) {
|
||||
const fs::path assetsDir = m_projectRoot / "Assets";
|
||||
const fs::path filePath = assetsDir / "ContextAsset.txt";
|
||||
|
||||
113
tests/editor/test_editor_script_assembly_builder.cpp
Normal file
113
tests/editor/test_editor_script_assembly_builder.cpp
Normal file
@@ -0,0 +1,113 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include "Scripting/EditorScriptAssemblyBuilder.h"
|
||||
|
||||
#ifdef XCENGINE_ENABLE_MONO_SCRIPTING
|
||||
#include <XCEngine/Scripting/Mono/MonoScriptRuntime.h>
|
||||
#endif
|
||||
|
||||
#include <chrono>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
namespace XCEngine::Editor::Scripting {
|
||||
namespace {
|
||||
|
||||
class EditorScriptAssemblyBuilderTest : public ::testing::Test {
|
||||
protected:
|
||||
static void WriteTextFile(const std::filesystem::path& path, const std::string& content) {
|
||||
std::ofstream output(path, std::ios::out | std::ios::trunc);
|
||||
ASSERT_TRUE(output.is_open());
|
||||
output << content;
|
||||
output.close();
|
||||
ASSERT_TRUE(output.good());
|
||||
}
|
||||
|
||||
void SetUp() override {
|
||||
const auto stamp = std::chrono::steady_clock::now().time_since_epoch().count();
|
||||
m_projectRoot = std::filesystem::temp_directory_path() / ("xc_script_builder_" + std::to_string(stamp));
|
||||
std::filesystem::create_directories(m_projectRoot / "Assets" / "Scripts");
|
||||
}
|
||||
|
||||
void TearDown() override {
|
||||
std::error_code ec;
|
||||
std::filesystem::remove_all(m_projectRoot, ec);
|
||||
}
|
||||
|
||||
std::filesystem::path m_projectRoot;
|
||||
};
|
||||
|
||||
TEST_F(EditorScriptAssemblyBuilderTest, RebuildsProjectScriptAssembliesIntoLibraryDirectory) {
|
||||
const std::filesystem::path scriptPath = m_projectRoot / "Assets" / "Scripts" / "BuilderProbe.cs";
|
||||
WriteTextFile(
|
||||
scriptPath,
|
||||
"using XCEngine;\n"
|
||||
"namespace BuilderTests {\n"
|
||||
" public sealed class BuilderProbe : MonoBehaviour {\n"
|
||||
" public float Speed = 4.0f;\n"
|
||||
" }\n"
|
||||
"}\n");
|
||||
|
||||
const EditorScriptAssemblyBuildResult result =
|
||||
EditorScriptAssemblyBuilder::RebuildProjectAssemblies(m_projectRoot.string());
|
||||
ASSERT_TRUE(result.succeeded) << result.message;
|
||||
|
||||
EXPECT_TRUE(std::filesystem::exists(m_projectRoot / "Library" / "ScriptAssemblies" / "XCEngine.ScriptCore.dll"));
|
||||
EXPECT_TRUE(std::filesystem::exists(m_projectRoot / "Library" / "ScriptAssemblies" / "GameScripts.dll"));
|
||||
EXPECT_TRUE(std::filesystem::exists(m_projectRoot / "Library" / "ScriptAssemblies" / "mscorlib.dll"));
|
||||
}
|
||||
|
||||
#ifdef XCENGINE_ENABLE_MONO_SCRIPTING
|
||||
TEST_F(EditorScriptAssemblyBuilderTest, RebuildFailsWhileLoadedAssemblyIsStillHeldByMonoRuntime) {
|
||||
const std::filesystem::path initialScriptPath = m_projectRoot / "Assets" / "Scripts" / "BuilderProbe.cs";
|
||||
WriteTextFile(
|
||||
initialScriptPath,
|
||||
"using XCEngine;\n"
|
||||
"namespace BuilderTests {\n"
|
||||
" public sealed class BuilderProbe : MonoBehaviour {\n"
|
||||
" public float Speed = 4.0f;\n"
|
||||
" }\n"
|
||||
"}\n");
|
||||
|
||||
EditorScriptAssemblyBuildResult result =
|
||||
EditorScriptAssemblyBuilder::RebuildProjectAssemblies(m_projectRoot.string());
|
||||
ASSERT_TRUE(result.succeeded) << result.message;
|
||||
|
||||
XCEngine::Scripting::MonoScriptRuntime::Settings settings;
|
||||
settings.assemblyDirectory = m_projectRoot / "Library" / "ScriptAssemblies";
|
||||
settings.corlibDirectory = settings.assemblyDirectory;
|
||||
settings.coreAssemblyPath = settings.assemblyDirectory / "XCEngine.ScriptCore.dll";
|
||||
settings.appAssemblyPath = settings.assemblyDirectory / "GameScripts.dll";
|
||||
|
||||
auto runtime = std::make_unique<XCEngine::Scripting::MonoScriptRuntime>(settings);
|
||||
ASSERT_TRUE(runtime->Initialize()) << runtime->GetLastError();
|
||||
|
||||
const std::filesystem::path addedScriptPath = m_projectRoot / "Assets" / "Scripts" / "TickLogProbe.cs";
|
||||
WriteTextFile(
|
||||
addedScriptPath,
|
||||
"using XCEngine;\n"
|
||||
"namespace BuilderTests {\n"
|
||||
" public sealed class TickLogProbe : MonoBehaviour {\n"
|
||||
" public int TickCount;\n"
|
||||
" }\n"
|
||||
"}\n");
|
||||
|
||||
result = EditorScriptAssemblyBuilder::RebuildProjectAssemblies(m_projectRoot.string());
|
||||
EXPECT_FALSE(result.succeeded);
|
||||
|
||||
runtime.reset();
|
||||
|
||||
result = EditorScriptAssemblyBuilder::RebuildProjectAssemblies(m_projectRoot.string());
|
||||
ASSERT_TRUE(result.succeeded) << result.message;
|
||||
|
||||
runtime = std::make_unique<XCEngine::Scripting::MonoScriptRuntime>(settings);
|
||||
ASSERT_TRUE(runtime->Initialize()) << runtime->GetLastError();
|
||||
EXPECT_TRUE(runtime->IsClassAvailable("GameScripts", "BuilderTests", "BuilderProbe"));
|
||||
EXPECT_TRUE(runtime->IsClassAvailable("GameScripts", "BuilderTests", "TickLogProbe"));
|
||||
}
|
||||
#endif
|
||||
|
||||
} // namespace
|
||||
} // namespace XCEngine::Editor::Scripting
|
||||
74
tests/editor/test_editor_script_assembly_builder_utils.cpp
Normal file
74
tests/editor/test_editor_script_assembly_builder_utils.cpp
Normal file
@@ -0,0 +1,74 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include "Scripting/EditorScriptAssemblyBuilderUtils.h"
|
||||
|
||||
#include <chrono>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
|
||||
namespace XCEngine::Editor::Scripting {
|
||||
namespace {
|
||||
|
||||
class EditorScriptAssemblyBuilderUtilsTest : public ::testing::Test {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
const auto stamp = std::chrono::steady_clock::now().time_since_epoch().count();
|
||||
m_root = std::filesystem::temp_directory_path() / ("xc_script_builder_utils_" + std::to_string(stamp));
|
||||
std::filesystem::create_directories(m_root);
|
||||
}
|
||||
|
||||
void TearDown() override {
|
||||
std::error_code ec;
|
||||
std::filesystem::remove_all(m_root, ec);
|
||||
}
|
||||
|
||||
std::filesystem::path m_root;
|
||||
};
|
||||
|
||||
TEST_F(EditorScriptAssemblyBuilderUtilsTest, CollectsAndSortsCSharpSourceFilesRecursively) {
|
||||
std::filesystem::create_directories(m_root / "B");
|
||||
std::filesystem::create_directories(m_root / "A" / "Nested");
|
||||
|
||||
std::ofstream(m_root / "B" / "Second.cs").put('\n');
|
||||
std::ofstream(m_root / "A" / "Nested" / "Third.cs").put('\n');
|
||||
std::ofstream(m_root / "A" / "First.cs").put('\n');
|
||||
std::ofstream(m_root / "Ignore.txt").put('\n');
|
||||
|
||||
const std::vector<std::filesystem::path> files = CollectCSharpSourceFiles(m_root);
|
||||
ASSERT_EQ(files.size(), 3u);
|
||||
EXPECT_EQ(files[0], (m_root / "A" / "First.cs").lexically_normal());
|
||||
EXPECT_EQ(files[1], (m_root / "A" / "Nested" / "Third.cs").lexically_normal());
|
||||
EXPECT_EQ(files[2], (m_root / "B" / "Second.cs").lexically_normal());
|
||||
}
|
||||
|
||||
TEST(EditorScriptAssemblyBuilderUtils_StandaloneTest, ParsesLatestDotnetSdkVersionFromSdkListOutput) {
|
||||
const std::string sdkListOutput =
|
||||
"7.0.410 [C:\\Program Files\\dotnet\\sdk]\n"
|
||||
"8.0.412 [C:\\Program Files\\dotnet\\sdk]\n"
|
||||
"9.0.100 [C:\\Program Files\\dotnet\\sdk]\n";
|
||||
|
||||
EXPECT_EQ(ParseLatestDotnetSdkVersion(sdkListOutput), "9.0.100");
|
||||
EXPECT_TRUE(ParseLatestDotnetSdkVersion(std::string()).empty());
|
||||
}
|
||||
|
||||
TEST_F(EditorScriptAssemblyBuilderUtilsTest, CreatesPlaceholderProjectScriptSourceWhenNoScriptsExist) {
|
||||
std::vector<std::filesystem::path> projectSources;
|
||||
std::string error;
|
||||
const std::filesystem::path placeholderPath = m_root / "Generated" / "EmptyProjectGameScripts.cs";
|
||||
|
||||
ASSERT_TRUE(EnsurePlaceholderProjectScriptSource(projectSources, placeholderPath, error)) << error;
|
||||
ASSERT_EQ(projectSources.size(), 1u);
|
||||
EXPECT_EQ(projectSources.front(), placeholderPath.lexically_normal());
|
||||
EXPECT_TRUE(std::filesystem::exists(placeholderPath));
|
||||
|
||||
std::ifstream input(placeholderPath);
|
||||
std::string content((std::istreambuf_iterator<char>(input)), std::istreambuf_iterator<char>());
|
||||
EXPECT_NE(content.find("EmptyProjectGameScriptsMarker"), std::string::npos);
|
||||
|
||||
error.clear();
|
||||
ASSERT_TRUE(EnsurePlaceholderProjectScriptSource(projectSources, placeholderPath, error)) << error;
|
||||
EXPECT_EQ(projectSources.size(), 1u);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace XCEngine::Editor::Scripting
|
||||
@@ -5,16 +5,55 @@
|
||||
#include "Core/PlaySessionController.h"
|
||||
|
||||
#include <XCEngine/Core/Math/Vector3.h>
|
||||
#include <XCEngine/Input/InputManager.h>
|
||||
|
||||
namespace XCEngine::Editor {
|
||||
namespace {
|
||||
|
||||
GameViewInputFrameEvent CreateGameViewInputFrame(
|
||||
bool focused,
|
||||
bool hovered,
|
||||
std::initializer_list<XCEngine::Input::KeyCode> keys = {},
|
||||
std::initializer_list<XCEngine::Input::MouseButton> mouseButtons = {},
|
||||
XCEngine::Math::Vector2 mousePosition = XCEngine::Math::Vector2::Zero(),
|
||||
XCEngine::Math::Vector2 mouseDelta = XCEngine::Math::Vector2::Zero(),
|
||||
float mouseWheel = 0.0f) {
|
||||
GameViewInputFrameEvent event = {};
|
||||
event.focused = focused;
|
||||
event.hovered = hovered;
|
||||
event.mousePosition = mousePosition;
|
||||
event.mouseDelta = mouseDelta;
|
||||
event.mouseWheel = mouseWheel;
|
||||
|
||||
for (const XCEngine::Input::KeyCode key : keys) {
|
||||
const size_t index = static_cast<size_t>(key);
|
||||
if (index < event.keyDown.size()) {
|
||||
event.keyDown[index] = true;
|
||||
}
|
||||
}
|
||||
|
||||
for (const XCEngine::Input::MouseButton button : mouseButtons) {
|
||||
const size_t index = static_cast<size_t>(button);
|
||||
if (index < event.mouseButtonDown.size()) {
|
||||
event.mouseButtonDown[index] = true;
|
||||
}
|
||||
}
|
||||
|
||||
return event;
|
||||
}
|
||||
|
||||
class PlaySessionControllerTest : public ::testing::Test {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
XCEngine::Input::InputManager::Get().Shutdown();
|
||||
m_context.GetSceneManager().NewScene("Play Session Scene");
|
||||
}
|
||||
|
||||
void TearDown() override {
|
||||
m_controller.Detach(m_context);
|
||||
XCEngine::Input::InputManager::Get().Shutdown();
|
||||
}
|
||||
|
||||
EditorContext m_context;
|
||||
PlaySessionController m_controller;
|
||||
};
|
||||
@@ -120,5 +159,82 @@ TEST_F(PlaySessionControllerTest, PauseResumeAndStepRequestsDrivePlayStateMachin
|
||||
m_context.GetEventBus().Unsubscribe<PlayModeResumedEvent>(resumedSubscription);
|
||||
}
|
||||
|
||||
TEST_F(PlaySessionControllerTest, GameViewInputFramesDoNotAffectInputManagerOutsidePlayMode) {
|
||||
m_controller.Attach(m_context);
|
||||
|
||||
m_context.GetEventBus().Publish(CreateGameViewInputFrame(
|
||||
true,
|
||||
true,
|
||||
{XCEngine::Input::KeyCode::A},
|
||||
{XCEngine::Input::MouseButton::Left},
|
||||
XCEngine::Math::Vector2(120.0f, 48.0f),
|
||||
XCEngine::Math::Vector2(3.0f, -2.0f),
|
||||
1.0f));
|
||||
|
||||
m_controller.Update(m_context, 0.016f);
|
||||
|
||||
auto& inputManager = XCEngine::Input::InputManager::Get();
|
||||
EXPECT_FALSE(inputManager.IsKeyDown(XCEngine::Input::KeyCode::A));
|
||||
EXPECT_FALSE(inputManager.IsMouseButtonDown(XCEngine::Input::MouseButton::Left));
|
||||
EXPECT_EQ(inputManager.GetMousePosition(), XCEngine::Math::Vector2::Zero());
|
||||
EXPECT_FLOAT_EQ(inputManager.GetMouseScrollDelta(), 0.0f);
|
||||
|
||||
m_controller.Detach(m_context);
|
||||
}
|
||||
|
||||
TEST_F(PlaySessionControllerTest, GameViewInputFramesDriveAndReleaseRuntimeInputDuringPlayMode) {
|
||||
auto* editorEntity = m_context.GetSceneManager().CreateEntity("Persistent");
|
||||
ASSERT_NE(editorEntity, nullptr);
|
||||
|
||||
m_controller.Attach(m_context);
|
||||
ASSERT_TRUE(m_controller.StartPlay(m_context));
|
||||
|
||||
m_context.GetEventBus().Publish(CreateGameViewInputFrame(
|
||||
true,
|
||||
true,
|
||||
{XCEngine::Input::KeyCode::A, XCEngine::Input::KeyCode::Space},
|
||||
{XCEngine::Input::MouseButton::Left},
|
||||
XCEngine::Math::Vector2(120.0f, 48.0f),
|
||||
XCEngine::Math::Vector2(3.0f, -2.0f),
|
||||
1.0f));
|
||||
|
||||
m_controller.Update(m_context, 0.016f);
|
||||
|
||||
auto& inputManager = XCEngine::Input::InputManager::Get();
|
||||
EXPECT_TRUE(inputManager.IsKeyDown(XCEngine::Input::KeyCode::A));
|
||||
EXPECT_TRUE(inputManager.IsKeyPressed(XCEngine::Input::KeyCode::A));
|
||||
EXPECT_TRUE(inputManager.IsKeyDown(XCEngine::Input::KeyCode::Space));
|
||||
EXPECT_TRUE(inputManager.IsMouseButtonDown(XCEngine::Input::MouseButton::Left));
|
||||
EXPECT_TRUE(inputManager.IsMouseButtonClicked(XCEngine::Input::MouseButton::Left));
|
||||
EXPECT_EQ(inputManager.GetMousePosition(), XCEngine::Math::Vector2(120.0f, 48.0f));
|
||||
EXPECT_EQ(inputManager.GetMouseDelta(), XCEngine::Math::Vector2(3.0f, -2.0f));
|
||||
EXPECT_FLOAT_EQ(inputManager.GetMouseScrollDelta(), 1.0f);
|
||||
|
||||
m_context.GetEventBus().Publish(CreateGameViewInputFrame(
|
||||
true,
|
||||
true,
|
||||
{XCEngine::Input::KeyCode::A, XCEngine::Input::KeyCode::Space},
|
||||
{XCEngine::Input::MouseButton::Left},
|
||||
XCEngine::Math::Vector2(120.0f, 48.0f)));
|
||||
|
||||
m_controller.Update(m_context, 0.016f);
|
||||
|
||||
EXPECT_TRUE(inputManager.IsKeyDown(XCEngine::Input::KeyCode::A));
|
||||
EXPECT_FALSE(inputManager.IsKeyPressed(XCEngine::Input::KeyCode::A));
|
||||
EXPECT_TRUE(inputManager.IsMouseButtonDown(XCEngine::Input::MouseButton::Left));
|
||||
EXPECT_FALSE(inputManager.IsMouseButtonClicked(XCEngine::Input::MouseButton::Left));
|
||||
EXPECT_EQ(inputManager.GetMouseDelta(), XCEngine::Math::Vector2::Zero());
|
||||
EXPECT_FLOAT_EQ(inputManager.GetMouseScrollDelta(), 0.0f);
|
||||
|
||||
m_context.GetEventBus().Publish(GameViewInputFrameEvent{});
|
||||
m_controller.Update(m_context, 0.016f);
|
||||
|
||||
EXPECT_FALSE(inputManager.IsKeyDown(XCEngine::Input::KeyCode::A));
|
||||
EXPECT_FALSE(inputManager.IsKeyDown(XCEngine::Input::KeyCode::Space));
|
||||
EXPECT_FALSE(inputManager.IsMouseButtonDown(XCEngine::Input::MouseButton::Left));
|
||||
|
||||
m_controller.Detach(m_context);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace XCEngine::Editor
|
||||
|
||||
641
tests/editor/test_play_session_controller_scripting.cpp
Normal file
641
tests/editor/test_play_session_controller_scripting.cpp
Normal file
@@ -0,0 +1,641 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include "Core/EditorContext.h"
|
||||
#include "Core/EditorRuntimeMode.h"
|
||||
#include "Core/PlaySessionController.h"
|
||||
|
||||
#include <XCEngine/Debug/ILogSink.h>
|
||||
#include <XCEngine/Debug/Logger.h>
|
||||
#include <XCEngine/Core/Math/Vector2.h>
|
||||
#include <XCEngine/Core/Math/Vector3.h>
|
||||
#include <XCEngine/Scripting/Mono/MonoScriptRuntime.h>
|
||||
#include <XCEngine/Scripting/ScriptComponent.h>
|
||||
#include <XCEngine/Scripting/ScriptEngine.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
using namespace XCEngine::Components;
|
||||
using namespace XCEngine::Scripting;
|
||||
|
||||
namespace XCEngine::Editor {
|
||||
namespace {
|
||||
|
||||
MonoScriptRuntime::Settings CreateMonoSettings() {
|
||||
MonoScriptRuntime::Settings settings;
|
||||
settings.assemblyDirectory = XCENGINE_TEST_MANAGED_OUTPUT_DIR;
|
||||
settings.corlibDirectory = XCENGINE_TEST_MANAGED_OUTPUT_DIR;
|
||||
settings.coreAssemblyPath = XCENGINE_TEST_SCRIPT_CORE_DLL;
|
||||
settings.appAssemblyPath = XCENGINE_TEST_GAME_SCRIPTS_DLL;
|
||||
return settings;
|
||||
}
|
||||
|
||||
void ExpectVector3Near(const Math::Vector3& actual, const Math::Vector3& expected, float tolerance = 0.001f) {
|
||||
EXPECT_NEAR(actual.x, expected.x, tolerance);
|
||||
EXPECT_NEAR(actual.y, expected.y, tolerance);
|
||||
EXPECT_NEAR(actual.z, expected.z, tolerance);
|
||||
}
|
||||
|
||||
class CapturingLogSink final : public Debug::ILogSink {
|
||||
public:
|
||||
void Log(const Debug::LogEntry& entry) override {
|
||||
entries.push_back(entry);
|
||||
}
|
||||
|
||||
void Flush() override {
|
||||
}
|
||||
|
||||
std::vector<std::string> CollectMessagesWithPrefix(const char* prefix) const {
|
||||
std::vector<std::string> messages;
|
||||
for (const Debug::LogEntry& entry : entries) {
|
||||
const std::string message = entry.message.CStr();
|
||||
if (message.rfind(prefix, 0) == 0) {
|
||||
messages.push_back(message);
|
||||
}
|
||||
}
|
||||
|
||||
return messages;
|
||||
}
|
||||
|
||||
std::vector<Debug::LogEntry> entries;
|
||||
};
|
||||
|
||||
ScriptComponent* FindLifecycleProbe(GameObject* gameObject) {
|
||||
if (!gameObject) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
for (ScriptComponent* component : gameObject->GetComponents<ScriptComponent>()) {
|
||||
if (!component) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (component->GetAssemblyName() == "GameScripts"
|
||||
&& component->GetNamespaceName() == "Gameplay"
|
||||
&& component->GetClassName() == "LifecycleProbe") {
|
||||
return component;
|
||||
}
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
ScriptComponent* FindInputProbe(GameObject* gameObject) {
|
||||
if (!gameObject) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
for (ScriptComponent* component : gameObject->GetComponents<ScriptComponent>()) {
|
||||
if (!component) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (component->GetAssemblyName() == "GameScripts"
|
||||
&& component->GetNamespaceName() == "Gameplay"
|
||||
&& component->GetClassName() == "InputProbe") {
|
||||
return component;
|
||||
}
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
ScriptComponent* FindTickLogProbe(GameObject* gameObject) {
|
||||
if (!gameObject) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
for (ScriptComponent* component : gameObject->GetComponents<ScriptComponent>()) {
|
||||
if (!component) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (component->GetAssemblyName() == "GameScripts"
|
||||
&& component->GetNamespaceName() == "Gameplay"
|
||||
&& component->GetClassName() == "TickLogProbe") {
|
||||
return component;
|
||||
}
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
GameViewInputFrameEvent CreateGameViewInputFrame(
|
||||
bool focused,
|
||||
bool hovered,
|
||||
std::initializer_list<XCEngine::Input::KeyCode> keys = {},
|
||||
std::initializer_list<XCEngine::Input::MouseButton> mouseButtons = {},
|
||||
XCEngine::Math::Vector2 mousePosition = XCEngine::Math::Vector2::Zero(),
|
||||
XCEngine::Math::Vector2 mouseDelta = XCEngine::Math::Vector2::Zero(),
|
||||
float mouseWheel = 0.0f) {
|
||||
GameViewInputFrameEvent event = {};
|
||||
event.focused = focused;
|
||||
event.hovered = hovered;
|
||||
event.mousePosition = mousePosition;
|
||||
event.mouseDelta = mouseDelta;
|
||||
event.mouseWheel = mouseWheel;
|
||||
|
||||
for (const XCEngine::Input::KeyCode key : keys) {
|
||||
const size_t index = static_cast<size_t>(key);
|
||||
if (index < event.keyDown.size()) {
|
||||
event.keyDown[index] = true;
|
||||
}
|
||||
}
|
||||
|
||||
for (const XCEngine::Input::MouseButton button : mouseButtons) {
|
||||
const size_t index = static_cast<size_t>(button);
|
||||
if (index < event.mouseButtonDown.size()) {
|
||||
event.mouseButtonDown[index] = true;
|
||||
}
|
||||
}
|
||||
|
||||
return event;
|
||||
}
|
||||
|
||||
class PlaySessionControllerScriptingTest : public ::testing::Test {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
engine = &ScriptEngine::Get();
|
||||
engine->OnRuntimeStop();
|
||||
|
||||
runtime = std::make_unique<MonoScriptRuntime>(CreateMonoSettings());
|
||||
ASSERT_TRUE(runtime->Initialize()) << runtime->GetLastError();
|
||||
engine->SetRuntime(runtime.get());
|
||||
|
||||
context.GetSceneManager().NewScene("Play Session Script Scene");
|
||||
controller.Attach(context);
|
||||
}
|
||||
|
||||
void TearDown() override {
|
||||
controller.Detach(context);
|
||||
engine->OnRuntimeStop();
|
||||
engine->SetRuntime(nullptr);
|
||||
runtime.reset();
|
||||
}
|
||||
|
||||
ScriptComponent* AddLifecycleProbe(GameObject* gameObject) {
|
||||
ScriptComponent* component = gameObject->AddComponent<ScriptComponent>();
|
||||
component->SetScriptClass("GameScripts", "Gameplay", "LifecycleProbe");
|
||||
return component;
|
||||
}
|
||||
|
||||
ScriptComponent* AddInputProbe(GameObject* gameObject) {
|
||||
ScriptComponent* component = gameObject->AddComponent<ScriptComponent>();
|
||||
component->SetScriptClass("GameScripts", "Gameplay", "InputProbe");
|
||||
return component;
|
||||
}
|
||||
|
||||
ScriptComponent* AddTickLogProbe(GameObject* gameObject) {
|
||||
ScriptComponent* component = gameObject->AddComponent<ScriptComponent>();
|
||||
component->SetScriptClass("GameScripts", "Gameplay", "TickLogProbe");
|
||||
return component;
|
||||
}
|
||||
|
||||
ScriptEngine* engine = nullptr;
|
||||
std::unique_ptr<MonoScriptRuntime> runtime;
|
||||
EditorContext context;
|
||||
PlaySessionController controller;
|
||||
};
|
||||
|
||||
TEST_F(PlaySessionControllerScriptingTest, StartPlayAndRuntimeTickDriveManagedLifecycleThroughPlayController) {
|
||||
GameObject* host = context.GetSceneManager().CreateEntity("Host");
|
||||
ASSERT_NE(host, nullptr);
|
||||
ScriptComponent* script = AddLifecycleProbe(host);
|
||||
ASSERT_NE(script, nullptr);
|
||||
|
||||
script->GetFieldStorage().SetFieldValue("Label", "EditorLabel");
|
||||
script->GetFieldStorage().SetFieldValue("Speed", 5.0f);
|
||||
script->GetFieldStorage().SetFieldValue("SpawnPoint", Math::Vector3(2.0f, 4.0f, 6.0f));
|
||||
|
||||
const uint64_t hostId = host->GetID();
|
||||
|
||||
ASSERT_TRUE(controller.StartPlay(context));
|
||||
EXPECT_EQ(context.GetRuntimeMode(), EditorRuntimeMode::Play);
|
||||
|
||||
GameObject* runtimeHost = context.GetSceneManager().GetEntity(hostId);
|
||||
ASSERT_NE(runtimeHost, nullptr);
|
||||
ScriptComponent* runtimeScript = FindLifecycleProbe(runtimeHost);
|
||||
ASSERT_NE(runtimeScript, nullptr);
|
||||
EXPECT_TRUE(engine->HasRuntimeInstance(runtimeScript));
|
||||
EXPECT_TRUE(runtime->HasManagedInstance(runtimeScript));
|
||||
|
||||
int32_t awakeCount = 0;
|
||||
int32_t enableCount = 0;
|
||||
int32_t startCount = 0;
|
||||
int32_t fixedUpdateCount = 0;
|
||||
int32_t updateCount = 0;
|
||||
int32_t lateUpdateCount = 0;
|
||||
std::string label;
|
||||
|
||||
ASSERT_TRUE(engine->TryGetScriptFieldValue(runtimeScript, "AwakeCount", awakeCount));
|
||||
ASSERT_TRUE(engine->TryGetScriptFieldValue(runtimeScript, "EnableCount", enableCount));
|
||||
ASSERT_TRUE(engine->TryGetScriptFieldValue(runtimeScript, "StartCount", startCount));
|
||||
ASSERT_TRUE(engine->TryGetScriptFieldValue(runtimeScript, "FixedUpdateCount", fixedUpdateCount));
|
||||
ASSERT_TRUE(engine->TryGetScriptFieldValue(runtimeScript, "UpdateCount", updateCount));
|
||||
ASSERT_TRUE(engine->TryGetScriptFieldValue(runtimeScript, "LateUpdateCount", lateUpdateCount));
|
||||
ASSERT_TRUE(engine->TryGetScriptFieldValue(runtimeScript, "Label", label));
|
||||
|
||||
EXPECT_EQ(awakeCount, 1);
|
||||
EXPECT_EQ(enableCount, 1);
|
||||
EXPECT_EQ(startCount, 0);
|
||||
EXPECT_EQ(fixedUpdateCount, 0);
|
||||
EXPECT_EQ(updateCount, 0);
|
||||
EXPECT_EQ(lateUpdateCount, 0);
|
||||
EXPECT_EQ(label, "EditorLabel|Awake");
|
||||
|
||||
controller.Update(context, 0.036f);
|
||||
|
||||
float observedFixedDeltaTime = 0.0f;
|
||||
float observedConfiguredFixedDeltaTime = 0.0f;
|
||||
float observedConfiguredFixedDeltaTimeInUpdate = 0.0f;
|
||||
float observedUpdateDeltaTime = 0.0f;
|
||||
float observedLateDeltaTime = 0.0f;
|
||||
float speed = 0.0f;
|
||||
Math::Vector3 spawnPoint;
|
||||
|
||||
ASSERT_TRUE(engine->TryGetScriptFieldValue(runtimeScript, "StartCount", startCount));
|
||||
ASSERT_TRUE(engine->TryGetScriptFieldValue(runtimeScript, "FixedUpdateCount", fixedUpdateCount));
|
||||
ASSERT_TRUE(engine->TryGetScriptFieldValue(runtimeScript, "UpdateCount", updateCount));
|
||||
ASSERT_TRUE(engine->TryGetScriptFieldValue(runtimeScript, "LateUpdateCount", lateUpdateCount));
|
||||
ASSERT_TRUE(engine->TryGetScriptFieldValue(runtimeScript, "ObservedFixedDeltaTime", observedFixedDeltaTime));
|
||||
ASSERT_TRUE(engine->TryGetScriptFieldValue(runtimeScript, "ObservedConfiguredFixedDeltaTime", observedConfiguredFixedDeltaTime));
|
||||
ASSERT_TRUE(engine->TryGetScriptFieldValue(runtimeScript, "ObservedConfiguredFixedDeltaTimeInUpdate", observedConfiguredFixedDeltaTimeInUpdate));
|
||||
ASSERT_TRUE(engine->TryGetScriptFieldValue(runtimeScript, "ObservedUpdateDeltaTime", observedUpdateDeltaTime));
|
||||
ASSERT_TRUE(engine->TryGetScriptFieldValue(runtimeScript, "ObservedLateDeltaTime", observedLateDeltaTime));
|
||||
ASSERT_TRUE(engine->TryGetScriptFieldValue(runtimeScript, "Speed", speed));
|
||||
ASSERT_TRUE(engine->TryGetScriptFieldValue(runtimeScript, "SpawnPoint", spawnPoint));
|
||||
|
||||
EXPECT_EQ(startCount, 1);
|
||||
EXPECT_EQ(fixedUpdateCount, 1);
|
||||
EXPECT_EQ(updateCount, 1);
|
||||
EXPECT_EQ(lateUpdateCount, 1);
|
||||
EXPECT_FLOAT_EQ(observedFixedDeltaTime, 0.02f);
|
||||
EXPECT_FLOAT_EQ(observedConfiguredFixedDeltaTime, 0.02f);
|
||||
EXPECT_FLOAT_EQ(observedConfiguredFixedDeltaTimeInUpdate, 0.02f);
|
||||
EXPECT_FLOAT_EQ(observedUpdateDeltaTime, 0.036f);
|
||||
EXPECT_FLOAT_EQ(observedLateDeltaTime, 0.036f);
|
||||
EXPECT_FLOAT_EQ(speed, 6.0f);
|
||||
EXPECT_EQ(runtimeHost->GetName(), "Host_Managed");
|
||||
ExpectVector3Near(runtimeHost->GetTransform()->GetLocalPosition(), Math::Vector3(8.0f, 8.0f, 9.0f));
|
||||
ExpectVector3Near(spawnPoint, Math::Vector3(3.0f, 4.0f, 6.0f));
|
||||
}
|
||||
|
||||
TEST_F(PlaySessionControllerScriptingTest, PauseAndStepGateManagedUpdatesInPlayMode) {
|
||||
GameObject* host = context.GetSceneManager().CreateEntity("Host");
|
||||
ASSERT_NE(host, nullptr);
|
||||
ScriptComponent* script = AddLifecycleProbe(host);
|
||||
ASSERT_NE(script, nullptr);
|
||||
|
||||
script->GetFieldStorage().SetFieldValue("Label", "EditorLabel");
|
||||
|
||||
const uint64_t hostId = host->GetID();
|
||||
|
||||
ASSERT_TRUE(controller.StartPlay(context));
|
||||
GameObject* runtimeHost = context.GetSceneManager().GetEntity(hostId);
|
||||
ASSERT_NE(runtimeHost, nullptr);
|
||||
ScriptComponent* runtimeScript = FindLifecycleProbe(runtimeHost);
|
||||
ASSERT_NE(runtimeScript, nullptr);
|
||||
|
||||
controller.Update(context, 0.02f);
|
||||
|
||||
int32_t startCount = 0;
|
||||
int32_t fixedUpdateCount = 0;
|
||||
int32_t updateCount = 0;
|
||||
int32_t lateUpdateCount = 0;
|
||||
|
||||
ASSERT_TRUE(engine->TryGetScriptFieldValue(runtimeScript, "StartCount", startCount));
|
||||
ASSERT_TRUE(engine->TryGetScriptFieldValue(runtimeScript, "FixedUpdateCount", fixedUpdateCount));
|
||||
ASSERT_TRUE(engine->TryGetScriptFieldValue(runtimeScript, "UpdateCount", updateCount));
|
||||
ASSERT_TRUE(engine->TryGetScriptFieldValue(runtimeScript, "LateUpdateCount", lateUpdateCount));
|
||||
|
||||
EXPECT_EQ(startCount, 1);
|
||||
EXPECT_EQ(fixedUpdateCount, 1);
|
||||
EXPECT_EQ(updateCount, 1);
|
||||
EXPECT_EQ(lateUpdateCount, 1);
|
||||
|
||||
ASSERT_TRUE(controller.PausePlay(context));
|
||||
EXPECT_EQ(context.GetRuntimeMode(), EditorRuntimeMode::Paused);
|
||||
|
||||
controller.Update(context, 0.02f);
|
||||
|
||||
ASSERT_TRUE(engine->TryGetScriptFieldValue(runtimeScript, "FixedUpdateCount", fixedUpdateCount));
|
||||
ASSERT_TRUE(engine->TryGetScriptFieldValue(runtimeScript, "UpdateCount", updateCount));
|
||||
ASSERT_TRUE(engine->TryGetScriptFieldValue(runtimeScript, "LateUpdateCount", lateUpdateCount));
|
||||
|
||||
EXPECT_EQ(fixedUpdateCount, 1);
|
||||
EXPECT_EQ(updateCount, 1);
|
||||
EXPECT_EQ(lateUpdateCount, 1);
|
||||
|
||||
ASSERT_TRUE(controller.StepPlay(context));
|
||||
controller.Update(context, 0.02f);
|
||||
|
||||
ASSERT_TRUE(engine->TryGetScriptFieldValue(runtimeScript, "FixedUpdateCount", fixedUpdateCount));
|
||||
ASSERT_TRUE(engine->TryGetScriptFieldValue(runtimeScript, "UpdateCount", updateCount));
|
||||
ASSERT_TRUE(engine->TryGetScriptFieldValue(runtimeScript, "LateUpdateCount", lateUpdateCount));
|
||||
|
||||
EXPECT_EQ(context.GetRuntimeMode(), EditorRuntimeMode::Paused);
|
||||
EXPECT_EQ(fixedUpdateCount, 2);
|
||||
EXPECT_EQ(updateCount, 2);
|
||||
EXPECT_EQ(lateUpdateCount, 2);
|
||||
|
||||
ASSERT_TRUE(controller.ResumePlay(context));
|
||||
EXPECT_EQ(context.GetRuntimeMode(), EditorRuntimeMode::Play);
|
||||
|
||||
controller.Update(context, 0.02f);
|
||||
|
||||
ASSERT_TRUE(engine->TryGetScriptFieldValue(runtimeScript, "FixedUpdateCount", fixedUpdateCount));
|
||||
ASSERT_TRUE(engine->TryGetScriptFieldValue(runtimeScript, "UpdateCount", updateCount));
|
||||
ASSERT_TRUE(engine->TryGetScriptFieldValue(runtimeScript, "LateUpdateCount", lateUpdateCount));
|
||||
|
||||
EXPECT_EQ(fixedUpdateCount, 3);
|
||||
EXPECT_EQ(updateCount, 3);
|
||||
EXPECT_EQ(lateUpdateCount, 3);
|
||||
}
|
||||
|
||||
TEST_F(PlaySessionControllerScriptingTest, StopPlayDestroysManagedInstancesAndRestoresEditorSnapshot) {
|
||||
GameObject* host = context.GetSceneManager().CreateEntity("Host");
|
||||
ASSERT_NE(host, nullptr);
|
||||
host->GetTransform()->SetLocalPosition(Math::Vector3(1.0f, 2.0f, 3.0f));
|
||||
|
||||
ScriptComponent* script = AddLifecycleProbe(host);
|
||||
ASSERT_NE(script, nullptr);
|
||||
script->GetFieldStorage().SetFieldValue("Label", "EditorLabel");
|
||||
script->GetFieldStorage().SetFieldValue("Speed", 5.0f);
|
||||
script->GetFieldStorage().SetFieldValue("SpawnPoint", Math::Vector3(2.0f, 4.0f, 6.0f));
|
||||
|
||||
const uint64_t hostId = host->GetID();
|
||||
const uint64_t scriptComponentUUID = script->GetScriptComponentUUID();
|
||||
|
||||
ASSERT_TRUE(controller.StartPlay(context));
|
||||
GameObject* runtimeHost = context.GetSceneManager().GetEntity(hostId);
|
||||
ASSERT_NE(runtimeHost, nullptr);
|
||||
ScriptComponent* runtimeScript = FindLifecycleProbe(runtimeHost);
|
||||
ASSERT_NE(runtimeScript, nullptr);
|
||||
|
||||
controller.Update(context, 0.02f);
|
||||
|
||||
EXPECT_EQ(runtime->GetManagedInstanceCount(), 1u);
|
||||
EXPECT_EQ(runtimeHost->GetName(), "Host_Managed");
|
||||
ExpectVector3Near(runtimeHost->GetTransform()->GetLocalPosition(), Math::Vector3(8.0f, 8.0f, 9.0f));
|
||||
|
||||
ASSERT_TRUE(controller.StopPlay(context));
|
||||
EXPECT_EQ(context.GetRuntimeMode(), EditorRuntimeMode::Edit);
|
||||
EXPECT_FALSE(engine->IsRuntimeRunning());
|
||||
EXPECT_EQ(runtime->GetManagedInstanceCount(), 0u);
|
||||
|
||||
GameObject* restoredHost = context.GetSceneManager().GetEntity(hostId);
|
||||
ASSERT_NE(restoredHost, nullptr);
|
||||
EXPECT_EQ(restoredHost->GetName(), "Host");
|
||||
ExpectVector3Near(restoredHost->GetTransform()->GetLocalPosition(), Math::Vector3(1.0f, 2.0f, 3.0f));
|
||||
|
||||
ScriptComponent* restoredScript = FindLifecycleProbe(restoredHost);
|
||||
ASSERT_NE(restoredScript, nullptr);
|
||||
EXPECT_EQ(restoredScript->GetScriptComponentUUID(), scriptComponentUUID);
|
||||
|
||||
std::string label;
|
||||
float speed = 0.0f;
|
||||
Math::Vector3 spawnPoint;
|
||||
int32_t awakeCount = 0;
|
||||
|
||||
ASSERT_TRUE(engine->TryGetScriptFieldValue(restoredScript, "Label", label));
|
||||
ASSERT_TRUE(engine->TryGetScriptFieldValue(restoredScript, "Speed", speed));
|
||||
ASSERT_TRUE(engine->TryGetScriptFieldValue(restoredScript, "SpawnPoint", spawnPoint));
|
||||
|
||||
EXPECT_EQ(label, "EditorLabel");
|
||||
EXPECT_FLOAT_EQ(speed, 5.0f);
|
||||
ExpectVector3Near(spawnPoint, Math::Vector3(2.0f, 4.0f, 6.0f));
|
||||
EXPECT_FALSE(engine->TryGetScriptFieldValue(restoredScript, "AwakeCount", awakeCount));
|
||||
EXPECT_FALSE(restoredScript->GetFieldStorage().Contains("AwakeCount"));
|
||||
}
|
||||
|
||||
TEST_F(PlaySessionControllerScriptingTest, GameViewInputBridgeFeedsManagedInputApiDuringPlayMode) {
|
||||
GameObject* host = context.GetSceneManager().CreateEntity("Host");
|
||||
ASSERT_NE(host, nullptr);
|
||||
ScriptComponent* script = AddInputProbe(host);
|
||||
ASSERT_NE(script, nullptr);
|
||||
|
||||
const uint64_t hostId = host->GetID();
|
||||
|
||||
controller.Attach(context);
|
||||
ASSERT_TRUE(controller.StartPlay(context));
|
||||
GameObject* runtimeHost = context.GetSceneManager().GetEntity(hostId);
|
||||
ASSERT_NE(runtimeHost, nullptr);
|
||||
ScriptComponent* runtimeScript = FindInputProbe(runtimeHost);
|
||||
ASSERT_NE(runtimeScript, nullptr);
|
||||
|
||||
context.GetEventBus().Publish(CreateGameViewInputFrame(
|
||||
true,
|
||||
true,
|
||||
{XCEngine::Input::KeyCode::A, XCEngine::Input::KeyCode::Space, XCEngine::Input::KeyCode::LeftCtrl},
|
||||
{XCEngine::Input::MouseButton::Left},
|
||||
XCEngine::Math::Vector2(120.0f, 48.0f),
|
||||
XCEngine::Math::Vector2(3.0f, -2.0f),
|
||||
1.0f));
|
||||
|
||||
controller.Update(context, 0.016f);
|
||||
|
||||
int32_t updateCount = 0;
|
||||
bool observedKeyA = false;
|
||||
bool observedKeyADown = false;
|
||||
bool observedKeyAUp = false;
|
||||
bool observedKeySpace = false;
|
||||
bool observedJump = false;
|
||||
bool observedJumpDown = false;
|
||||
bool observedJumpUp = false;
|
||||
bool observedFire1 = false;
|
||||
bool observedFire1Down = false;
|
||||
bool observedFire1Up = false;
|
||||
bool observedAnyKey = false;
|
||||
bool observedAnyKeyDown = false;
|
||||
bool observedLeftMouse = false;
|
||||
bool observedLeftMouseDown = false;
|
||||
bool observedLeftMouseUp = false;
|
||||
float observedHorizontal = 0.0f;
|
||||
float observedHorizontalRaw = 0.0f;
|
||||
XCEngine::Math::Vector2 observedMouseScrollDelta;
|
||||
XCEngine::Math::Vector3 observedMousePosition;
|
||||
|
||||
ASSERT_TRUE(engine->TryGetScriptFieldValue(runtimeScript, "UpdateCount", updateCount));
|
||||
ASSERT_TRUE(engine->TryGetScriptFieldValue(runtimeScript, "ObservedKeyA", observedKeyA));
|
||||
ASSERT_TRUE(engine->TryGetScriptFieldValue(runtimeScript, "ObservedKeyADown", observedKeyADown));
|
||||
ASSERT_TRUE(engine->TryGetScriptFieldValue(runtimeScript, "ObservedKeyAUp", observedKeyAUp));
|
||||
ASSERT_TRUE(engine->TryGetScriptFieldValue(runtimeScript, "ObservedKeySpace", observedKeySpace));
|
||||
ASSERT_TRUE(engine->TryGetScriptFieldValue(runtimeScript, "ObservedJump", observedJump));
|
||||
ASSERT_TRUE(engine->TryGetScriptFieldValue(runtimeScript, "ObservedJumpDown", observedJumpDown));
|
||||
ASSERT_TRUE(engine->TryGetScriptFieldValue(runtimeScript, "ObservedJumpUp", observedJumpUp));
|
||||
ASSERT_TRUE(engine->TryGetScriptFieldValue(runtimeScript, "ObservedFire1", observedFire1));
|
||||
ASSERT_TRUE(engine->TryGetScriptFieldValue(runtimeScript, "ObservedFire1Down", observedFire1Down));
|
||||
ASSERT_TRUE(engine->TryGetScriptFieldValue(runtimeScript, "ObservedFire1Up", observedFire1Up));
|
||||
ASSERT_TRUE(engine->TryGetScriptFieldValue(runtimeScript, "ObservedAnyKey", observedAnyKey));
|
||||
ASSERT_TRUE(engine->TryGetScriptFieldValue(runtimeScript, "ObservedAnyKeyDown", observedAnyKeyDown));
|
||||
ASSERT_TRUE(engine->TryGetScriptFieldValue(runtimeScript, "ObservedLeftMouse", observedLeftMouse));
|
||||
ASSERT_TRUE(engine->TryGetScriptFieldValue(runtimeScript, "ObservedLeftMouseDown", observedLeftMouseDown));
|
||||
ASSERT_TRUE(engine->TryGetScriptFieldValue(runtimeScript, "ObservedLeftMouseUp", observedLeftMouseUp));
|
||||
ASSERT_TRUE(engine->TryGetScriptFieldValue(runtimeScript, "ObservedHorizontal", observedHorizontal));
|
||||
ASSERT_TRUE(engine->TryGetScriptFieldValue(runtimeScript, "ObservedHorizontalRaw", observedHorizontalRaw));
|
||||
ASSERT_TRUE(engine->TryGetScriptFieldValue(runtimeScript, "ObservedMousePosition", observedMousePosition));
|
||||
ASSERT_TRUE(engine->TryGetScriptFieldValue(runtimeScript, "ObservedMouseScrollDelta", observedMouseScrollDelta));
|
||||
|
||||
EXPECT_EQ(updateCount, 1);
|
||||
EXPECT_TRUE(observedKeyA);
|
||||
EXPECT_TRUE(observedKeyADown);
|
||||
EXPECT_FALSE(observedKeyAUp);
|
||||
EXPECT_TRUE(observedKeySpace);
|
||||
EXPECT_TRUE(observedJump);
|
||||
EXPECT_TRUE(observedJumpDown);
|
||||
EXPECT_FALSE(observedJumpUp);
|
||||
EXPECT_TRUE(observedFire1);
|
||||
EXPECT_TRUE(observedFire1Down);
|
||||
EXPECT_FALSE(observedFire1Up);
|
||||
EXPECT_TRUE(observedAnyKey);
|
||||
EXPECT_TRUE(observedAnyKeyDown);
|
||||
EXPECT_TRUE(observedLeftMouse);
|
||||
EXPECT_TRUE(observedLeftMouseDown);
|
||||
EXPECT_FALSE(observedLeftMouseUp);
|
||||
EXPECT_FLOAT_EQ(observedHorizontal, -1.0f);
|
||||
EXPECT_FLOAT_EQ(observedHorizontalRaw, -1.0f);
|
||||
EXPECT_EQ(observedMousePosition, XCEngine::Math::Vector3(120.0f, 48.0f, 0.0f));
|
||||
EXPECT_EQ(observedMouseScrollDelta, XCEngine::Math::Vector2(0.0f, 1.0f));
|
||||
|
||||
context.GetEventBus().Publish(CreateGameViewInputFrame(
|
||||
true,
|
||||
true,
|
||||
{XCEngine::Input::KeyCode::A, XCEngine::Input::KeyCode::Space, XCEngine::Input::KeyCode::LeftCtrl},
|
||||
{XCEngine::Input::MouseButton::Left},
|
||||
XCEngine::Math::Vector2(120.0f, 48.0f)));
|
||||
|
||||
controller.Update(context, 0.016f);
|
||||
|
||||
ASSERT_TRUE(engine->TryGetScriptFieldValue(runtimeScript, "UpdateCount", updateCount));
|
||||
ASSERT_TRUE(engine->TryGetScriptFieldValue(runtimeScript, "ObservedKeyA", observedKeyA));
|
||||
ASSERT_TRUE(engine->TryGetScriptFieldValue(runtimeScript, "ObservedKeyADown", observedKeyADown));
|
||||
ASSERT_TRUE(engine->TryGetScriptFieldValue(runtimeScript, "ObservedKeyAUp", observedKeyAUp));
|
||||
ASSERT_TRUE(engine->TryGetScriptFieldValue(runtimeScript, "ObservedJump", observedJump));
|
||||
ASSERT_TRUE(engine->TryGetScriptFieldValue(runtimeScript, "ObservedJumpDown", observedJumpDown));
|
||||
ASSERT_TRUE(engine->TryGetScriptFieldValue(runtimeScript, "ObservedJumpUp", observedJumpUp));
|
||||
ASSERT_TRUE(engine->TryGetScriptFieldValue(runtimeScript, "ObservedFire1", observedFire1));
|
||||
ASSERT_TRUE(engine->TryGetScriptFieldValue(runtimeScript, "ObservedFire1Down", observedFire1Down));
|
||||
ASSERT_TRUE(engine->TryGetScriptFieldValue(runtimeScript, "ObservedFire1Up", observedFire1Up));
|
||||
ASSERT_TRUE(engine->TryGetScriptFieldValue(runtimeScript, "ObservedAnyKey", observedAnyKey));
|
||||
ASSERT_TRUE(engine->TryGetScriptFieldValue(runtimeScript, "ObservedAnyKeyDown", observedAnyKeyDown));
|
||||
ASSERT_TRUE(engine->TryGetScriptFieldValue(runtimeScript, "ObservedLeftMouse", observedLeftMouse));
|
||||
ASSERT_TRUE(engine->TryGetScriptFieldValue(runtimeScript, "ObservedLeftMouseDown", observedLeftMouseDown));
|
||||
ASSERT_TRUE(engine->TryGetScriptFieldValue(runtimeScript, "ObservedLeftMouseUp", observedLeftMouseUp));
|
||||
ASSERT_TRUE(engine->TryGetScriptFieldValue(runtimeScript, "ObservedHorizontalRaw", observedHorizontalRaw));
|
||||
ASSERT_TRUE(engine->TryGetScriptFieldValue(runtimeScript, "ObservedMouseScrollDelta", observedMouseScrollDelta));
|
||||
|
||||
EXPECT_EQ(updateCount, 2);
|
||||
EXPECT_TRUE(observedKeyA);
|
||||
EXPECT_FALSE(observedKeyADown);
|
||||
EXPECT_FALSE(observedKeyAUp);
|
||||
EXPECT_TRUE(observedJump);
|
||||
EXPECT_FALSE(observedJumpDown);
|
||||
EXPECT_FALSE(observedJumpUp);
|
||||
EXPECT_TRUE(observedFire1);
|
||||
EXPECT_FALSE(observedFire1Down);
|
||||
EXPECT_FALSE(observedFire1Up);
|
||||
EXPECT_TRUE(observedAnyKey);
|
||||
EXPECT_FALSE(observedAnyKeyDown);
|
||||
EXPECT_TRUE(observedLeftMouse);
|
||||
EXPECT_FALSE(observedLeftMouseDown);
|
||||
EXPECT_FALSE(observedLeftMouseUp);
|
||||
EXPECT_FLOAT_EQ(observedHorizontalRaw, -1.0f);
|
||||
EXPECT_EQ(observedMouseScrollDelta, XCEngine::Math::Vector2(0.0f, 0.0f));
|
||||
|
||||
context.GetEventBus().Publish(CreateGameViewInputFrame(
|
||||
true,
|
||||
true,
|
||||
{XCEngine::Input::KeyCode::Space},
|
||||
{},
|
||||
XCEngine::Math::Vector2(120.0f, 48.0f)));
|
||||
controller.Update(context, 0.016f);
|
||||
|
||||
ASSERT_TRUE(engine->TryGetScriptFieldValue(runtimeScript, "UpdateCount", updateCount));
|
||||
ASSERT_TRUE(engine->TryGetScriptFieldValue(runtimeScript, "ObservedKeyA", observedKeyA));
|
||||
ASSERT_TRUE(engine->TryGetScriptFieldValue(runtimeScript, "ObservedKeyAUp", observedKeyAUp));
|
||||
ASSERT_TRUE(engine->TryGetScriptFieldValue(runtimeScript, "ObservedKeySpace", observedKeySpace));
|
||||
ASSERT_TRUE(engine->TryGetScriptFieldValue(runtimeScript, "ObservedJump", observedJump));
|
||||
ASSERT_TRUE(engine->TryGetScriptFieldValue(runtimeScript, "ObservedJumpUp", observedJumpUp));
|
||||
ASSERT_TRUE(engine->TryGetScriptFieldValue(runtimeScript, "ObservedFire1", observedFire1));
|
||||
ASSERT_TRUE(engine->TryGetScriptFieldValue(runtimeScript, "ObservedFire1Up", observedFire1Up));
|
||||
ASSERT_TRUE(engine->TryGetScriptFieldValue(runtimeScript, "ObservedAnyKey", observedAnyKey));
|
||||
ASSERT_TRUE(engine->TryGetScriptFieldValue(runtimeScript, "ObservedAnyKeyDown", observedAnyKeyDown));
|
||||
ASSERT_TRUE(engine->TryGetScriptFieldValue(runtimeScript, "ObservedLeftMouse", observedLeftMouse));
|
||||
ASSERT_TRUE(engine->TryGetScriptFieldValue(runtimeScript, "ObservedLeftMouseUp", observedLeftMouseUp));
|
||||
ASSERT_TRUE(engine->TryGetScriptFieldValue(runtimeScript, "ObservedHorizontal", observedHorizontal));
|
||||
ASSERT_TRUE(engine->TryGetScriptFieldValue(runtimeScript, "ObservedHorizontalRaw", observedHorizontalRaw));
|
||||
|
||||
EXPECT_EQ(updateCount, 3);
|
||||
EXPECT_FALSE(observedKeyA);
|
||||
EXPECT_TRUE(observedKeyAUp);
|
||||
EXPECT_TRUE(observedKeySpace);
|
||||
EXPECT_TRUE(observedJump);
|
||||
EXPECT_FALSE(observedJumpUp);
|
||||
EXPECT_FALSE(observedFire1);
|
||||
EXPECT_TRUE(observedFire1Up);
|
||||
EXPECT_TRUE(observedAnyKey);
|
||||
EXPECT_FALSE(observedAnyKeyDown);
|
||||
EXPECT_FALSE(observedLeftMouse);
|
||||
EXPECT_TRUE(observedLeftMouseUp);
|
||||
EXPECT_FLOAT_EQ(observedHorizontal, 0.0f);
|
||||
EXPECT_FLOAT_EQ(observedHorizontalRaw, 0.0f);
|
||||
|
||||
context.GetEventBus().Publish(CreateGameViewInputFrame(
|
||||
true,
|
||||
true,
|
||||
{},
|
||||
{},
|
||||
XCEngine::Math::Vector2(120.0f, 48.0f)));
|
||||
controller.Update(context, 0.016f);
|
||||
|
||||
ASSERT_TRUE(engine->TryGetScriptFieldValue(runtimeScript, "UpdateCount", updateCount));
|
||||
ASSERT_TRUE(engine->TryGetScriptFieldValue(runtimeScript, "ObservedKeySpace", observedKeySpace));
|
||||
ASSERT_TRUE(engine->TryGetScriptFieldValue(runtimeScript, "ObservedJump", observedJump));
|
||||
ASSERT_TRUE(engine->TryGetScriptFieldValue(runtimeScript, "ObservedJumpUp", observedJumpUp));
|
||||
ASSERT_TRUE(engine->TryGetScriptFieldValue(runtimeScript, "ObservedAnyKey", observedAnyKey));
|
||||
ASSERT_TRUE(engine->TryGetScriptFieldValue(runtimeScript, "ObservedAnyKeyDown", observedAnyKeyDown));
|
||||
|
||||
EXPECT_EQ(updateCount, 4);
|
||||
EXPECT_FALSE(observedKeySpace);
|
||||
EXPECT_FALSE(observedJump);
|
||||
EXPECT_TRUE(observedJumpUp);
|
||||
EXPECT_FALSE(observedAnyKey);
|
||||
EXPECT_FALSE(observedAnyKeyDown);
|
||||
}
|
||||
|
||||
TEST_F(PlaySessionControllerScriptingTest, PlayModeTickWritesManagedDebugLogsToNativeLogger) {
|
||||
auto sink = std::make_unique<CapturingLogSink>();
|
||||
CapturingLogSink* sinkPtr = sink.get();
|
||||
Debug::Logger::Get().AddSink(std::move(sink));
|
||||
|
||||
GameObject* host = context.GetSceneManager().CreateEntity("Host");
|
||||
ASSERT_NE(host, nullptr);
|
||||
ScriptComponent* script = AddTickLogProbe(host);
|
||||
ASSERT_NE(script, nullptr);
|
||||
|
||||
const uint64_t hostId = host->GetID();
|
||||
|
||||
ASSERT_TRUE(controller.StartPlay(context));
|
||||
GameObject* runtimeHost = context.GetSceneManager().GetEntity(hostId);
|
||||
ASSERT_NE(runtimeHost, nullptr);
|
||||
ASSERT_NE(FindTickLogProbe(runtimeHost), nullptr);
|
||||
|
||||
controller.Update(context, 0.036f);
|
||||
|
||||
const std::vector<std::string> messages = sinkPtr->CollectMessagesWithPrefix("[TickLogProbe]");
|
||||
const std::vector<std::string> expected = {
|
||||
"[TickLogProbe] Awake",
|
||||
"[TickLogProbe] FixedUpdate 1",
|
||||
"[TickLogProbe] Start",
|
||||
"[TickLogProbe] Update 1",
|
||||
"[TickLogProbe] LateUpdate 1",
|
||||
};
|
||||
|
||||
EXPECT_EQ(messages, expected);
|
||||
|
||||
Debug::Logger::Get().RemoveSink(sinkPtr);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace XCEngine::Editor
|
||||
@@ -159,16 +159,16 @@ TEST(SceneViewportCameraController_Test, FlyInputMovesCameraAndFocalPointTogethe
|
||||
TEST(SceneViewportCameraController_Test, ZoomDoesNotChangeFlySpeed) {
|
||||
SceneViewportCameraController zoomedController;
|
||||
zoomedController.Reset();
|
||||
const Vector3 zoomedInitialPosition = zoomedController.GetPosition();
|
||||
|
||||
SceneViewportCameraController baselineController;
|
||||
baselineController.Reset();
|
||||
const Vector3 baselineInitialPosition = baselineController.GetPosition();
|
||||
|
||||
SceneViewportCameraInputState zoomInput = {};
|
||||
zoomInput.viewportHeight = 720.0f;
|
||||
zoomInput.zoomDelta = 8.0f;
|
||||
zoomedController.ApplyInput(zoomInput);
|
||||
const Vector3 zoomedPositionBeforeMove = zoomedController.GetPosition();
|
||||
const Vector3 baselinePositionBeforeMove = baselineController.GetPosition();
|
||||
|
||||
SceneViewportCameraInputState moveInput = {};
|
||||
moveInput.viewportHeight = 720.0f;
|
||||
@@ -178,8 +178,8 @@ TEST(SceneViewportCameraController_Test, ZoomDoesNotChangeFlySpeed) {
|
||||
baselineController.ApplyInput(moveInput);
|
||||
|
||||
EXPECT_FLOAT_EQ(zoomedController.GetFlySpeed(), baselineController.GetFlySpeed());
|
||||
const float zoomedTravel = (zoomedController.GetPosition() - zoomedInitialPosition).Magnitude();
|
||||
const float baselineTravel = (baselineController.GetPosition() - baselineInitialPosition).Magnitude();
|
||||
const float zoomedTravel = (zoomedController.GetPosition() - zoomedPositionBeforeMove).Magnitude();
|
||||
const float baselineTravel = (baselineController.GetPosition() - baselinePositionBeforeMove).Magnitude();
|
||||
EXPECT_NEAR(zoomedTravel, baselineTravel, 1e-3f);
|
||||
}
|
||||
|
||||
|
||||
135
tests/editor/test_script_component_editor_utils.cpp
Normal file
135
tests/editor/test_script_component_editor_utils.cpp
Normal file
@@ -0,0 +1,135 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include "ComponentEditors/ScriptComponentEditorUtils.h"
|
||||
|
||||
#include <XCEngine/Scripting/ScriptComponent.h>
|
||||
|
||||
namespace XCEngine::Editor {
|
||||
namespace {
|
||||
|
||||
TEST(ScriptComponentEditorUtils_Test, BuildsReadableScriptClassDisplayNames) {
|
||||
const ::XCEngine::Scripting::ScriptClassDescriptor gameScript{
|
||||
"GameScripts",
|
||||
"Gameplay",
|
||||
"PlayerController"
|
||||
};
|
||||
const ::XCEngine::Scripting::ScriptClassDescriptor toolScript{
|
||||
"EditorTools",
|
||||
"Gameplay",
|
||||
"PlayerController"
|
||||
};
|
||||
|
||||
EXPECT_EQ(
|
||||
ScriptComponentEditorUtils::BuildScriptClassDisplayName(gameScript),
|
||||
"Gameplay.PlayerController");
|
||||
EXPECT_EQ(
|
||||
ScriptComponentEditorUtils::BuildScriptClassDisplayName(toolScript),
|
||||
"Gameplay.PlayerController (EditorTools)");
|
||||
|
||||
::XCEngine::Scripting::ScriptComponent component;
|
||||
component.SetScriptClass("GameScripts", "ProjectScripts", "Mover");
|
||||
EXPECT_EQ(
|
||||
ScriptComponentEditorUtils::BuildScriptClassDisplayName(component),
|
||||
"ProjectScripts.Mover");
|
||||
}
|
||||
|
||||
TEST(ScriptComponentEditorUtils_Test, FindsGameObjectIdsByUuidAcrossHierarchy) {
|
||||
::XCEngine::Components::GameObject root("Root");
|
||||
::XCEngine::Components::GameObject child("Child");
|
||||
::XCEngine::Components::GameObject grandChild("GrandChild");
|
||||
|
||||
child.SetParent(&root);
|
||||
grandChild.SetParent(&child);
|
||||
|
||||
const std::vector<::XCEngine::Components::GameObject*> roots = { &root };
|
||||
EXPECT_EQ(
|
||||
ScriptComponentEditorUtils::FindGameObjectIdByUuid(roots, child.GetUUID()),
|
||||
child.GetID());
|
||||
EXPECT_EQ(
|
||||
ScriptComponentEditorUtils::FindGameObjectIdByUuid(roots, grandChild.GetUUID()),
|
||||
grandChild.GetID());
|
||||
EXPECT_EQ(
|
||||
ScriptComponentEditorUtils::FindGameObjectIdByUuid(roots, 0),
|
||||
::XCEngine::Components::GameObject::INVALID_ID);
|
||||
}
|
||||
|
||||
TEST(ScriptComponentEditorUtils_Test, ReportsFieldEditabilityClearabilityAndIssues) {
|
||||
::XCEngine::Scripting::ScriptFieldSnapshot declaredField;
|
||||
declaredField.metadata = { "Health", ::XCEngine::Scripting::ScriptFieldType::Int32 };
|
||||
declaredField.declaredInClass = true;
|
||||
|
||||
::XCEngine::Scripting::ScriptFieldSnapshot storedOnlyField;
|
||||
storedOnlyField.metadata = { "LegacyValue", ::XCEngine::Scripting::ScriptFieldType::String };
|
||||
storedOnlyField.hasValue = true;
|
||||
storedOnlyField.value = std::string("legacy");
|
||||
storedOnlyField.valueSource = ::XCEngine::Scripting::ScriptFieldValueSource::StoredValue;
|
||||
storedOnlyField.issue = ::XCEngine::Scripting::ScriptFieldIssue::StoredOnly;
|
||||
storedOnlyField.hasStoredValue = true;
|
||||
storedOnlyField.storedType = ::XCEngine::Scripting::ScriptFieldType::String;
|
||||
storedOnlyField.storedValue = std::string("legacy");
|
||||
|
||||
::XCEngine::Scripting::ScriptFieldSnapshot mismatchedField;
|
||||
mismatchedField.metadata = { "Speed", ::XCEngine::Scripting::ScriptFieldType::Float };
|
||||
mismatchedField.declaredInClass = true;
|
||||
mismatchedField.issue = ::XCEngine::Scripting::ScriptFieldIssue::TypeMismatch;
|
||||
mismatchedField.hasStoredValue = true;
|
||||
mismatchedField.storedType = ::XCEngine::Scripting::ScriptFieldType::UInt64;
|
||||
mismatchedField.storedValue = uint64_t(5);
|
||||
|
||||
EXPECT_TRUE(ScriptComponentEditorUtils::CanEditScriptField(
|
||||
::XCEngine::Scripting::ScriptFieldClassStatus::Available,
|
||||
declaredField));
|
||||
EXPECT_FALSE(ScriptComponentEditorUtils::CanEditScriptField(
|
||||
::XCEngine::Scripting::ScriptFieldClassStatus::Available,
|
||||
storedOnlyField));
|
||||
EXPECT_TRUE(ScriptComponentEditorUtils::CanEditScriptField(
|
||||
::XCEngine::Scripting::ScriptFieldClassStatus::Missing,
|
||||
storedOnlyField));
|
||||
|
||||
EXPECT_FALSE(ScriptComponentEditorUtils::CanClearScriptFieldOverride(declaredField));
|
||||
EXPECT_TRUE(ScriptComponentEditorUtils::CanClearScriptFieldOverride(storedOnlyField));
|
||||
EXPECT_TRUE(ScriptComponentEditorUtils::CanClearScriptFieldOverride(mismatchedField));
|
||||
|
||||
EXPECT_EQ(
|
||||
ScriptComponentEditorUtils::BuildScriptFieldIssueText(storedOnlyField),
|
||||
"Stored override is not declared by the selected script.");
|
||||
EXPECT_EQ(
|
||||
ScriptComponentEditorUtils::BuildScriptFieldIssueText(mismatchedField),
|
||||
"Stored override type is UInt64, but the script field expects Float.");
|
||||
}
|
||||
|
||||
TEST(ScriptComponentEditorUtils_Test, BuildsRuntimeUnavailableHintsAndReloadAvailability) {
|
||||
EditorScriptRuntimeStatus missingAssembliesStatus;
|
||||
missingAssembliesStatus.backendEnabled = true;
|
||||
missingAssembliesStatus.assemblyDirectory = "D:/Project/Library/ScriptAssemblies";
|
||||
missingAssembliesStatus.statusMessage =
|
||||
"Script assemblies were not found in D:/Project/Library/ScriptAssemblies. "
|
||||
"Script class discovery is disabled until the managed assemblies are built.";
|
||||
|
||||
EXPECT_EQ(
|
||||
ScriptComponentEditorUtils::BuildScriptRuntimeUnavailableHint(missingAssembliesStatus),
|
||||
missingAssembliesStatus.statusMessage);
|
||||
EXPECT_TRUE(ScriptComponentEditorUtils::CanReloadScriptRuntime(missingAssembliesStatus));
|
||||
EXPECT_TRUE(ScriptComponentEditorUtils::CanRebuildScriptAssemblies(missingAssembliesStatus));
|
||||
|
||||
EditorScriptRuntimeStatus fallbackStatus;
|
||||
fallbackStatus.backendEnabled = true;
|
||||
fallbackStatus.assemblyDirectory = "D:/Project/Library/ScriptAssemblies";
|
||||
|
||||
EXPECT_EQ(
|
||||
ScriptComponentEditorUtils::BuildScriptRuntimeUnavailableHint(fallbackStatus),
|
||||
"No script assemblies are currently loaded from D:/Project/Library/ScriptAssemblies.");
|
||||
EXPECT_TRUE(ScriptComponentEditorUtils::CanReloadScriptRuntime(fallbackStatus));
|
||||
EXPECT_TRUE(ScriptComponentEditorUtils::CanRebuildScriptAssemblies(fallbackStatus));
|
||||
|
||||
EditorScriptRuntimeStatus backendDisabledStatus;
|
||||
|
||||
EXPECT_EQ(
|
||||
ScriptComponentEditorUtils::BuildScriptRuntimeUnavailableHint(backendDisabledStatus),
|
||||
"This editor build does not include Mono scripting support.");
|
||||
EXPECT_FALSE(ScriptComponentEditorUtils::CanReloadScriptRuntime(backendDisabledStatus));
|
||||
EXPECT_FALSE(ScriptComponentEditorUtils::CanRebuildScriptAssemblies(backendDisabledStatus));
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace XCEngine::Editor
|
||||
@@ -12,6 +12,12 @@ if(XCENGINE_ENABLE_MONO_SCRIPTING)
|
||||
list(APPEND SCRIPTING_TEST_SOURCES
|
||||
test_mono_script_runtime.cpp
|
||||
)
|
||||
|
||||
if(TARGET xcengine_project_managed_assemblies)
|
||||
list(APPEND SCRIPTING_TEST_SOURCES
|
||||
test_project_script_assembly.cpp
|
||||
)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
add_executable(scripting_tests ${SCRIPTING_TEST_SOURCES})
|
||||
@@ -46,6 +52,20 @@ if(TARGET xcengine_managed_assemblies)
|
||||
)
|
||||
endif()
|
||||
|
||||
if(TARGET xcengine_project_managed_assemblies)
|
||||
add_dependencies(scripting_tests xcengine_project_managed_assemblies)
|
||||
|
||||
file(TO_CMAKE_PATH "${XCENGINE_PROJECT_MANAGED_OUTPUT_DIR}" XCENGINE_PROJECT_MANAGED_OUTPUT_DIR_CMAKE)
|
||||
file(TO_CMAKE_PATH "${XCENGINE_PROJECT_SCRIPT_CORE_DLL}" XCENGINE_PROJECT_SCRIPT_CORE_DLL_CMAKE)
|
||||
file(TO_CMAKE_PATH "${XCENGINE_PROJECT_GAME_SCRIPTS_DLL}" XCENGINE_PROJECT_GAME_SCRIPTS_DLL_CMAKE)
|
||||
|
||||
target_compile_definitions(scripting_tests PRIVATE
|
||||
XCENGINE_TEST_PROJECT_MANAGED_OUTPUT_DIR=\"${XCENGINE_PROJECT_MANAGED_OUTPUT_DIR_CMAKE}\"
|
||||
XCENGINE_TEST_PROJECT_SCRIPT_CORE_DLL=\"${XCENGINE_PROJECT_SCRIPT_CORE_DLL_CMAKE}\"
|
||||
XCENGINE_TEST_PROJECT_GAME_SCRIPTS_DLL=\"${XCENGINE_PROJECT_GAME_SCRIPTS_DLL_CMAKE}\"
|
||||
)
|
||||
endif()
|
||||
|
||||
if(WIN32 AND EXISTS "${CMAKE_SOURCE_DIR}/engine/third_party/assimp/bin/assimp-vc143-mt.dll")
|
||||
add_custom_command(TARGET scripting_tests POST_BUILD
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_if_different
|
||||
|
||||
@@ -4,8 +4,13 @@
|
||||
#include <XCEngine/Components/LightComponent.h>
|
||||
#include <XCEngine/Components/MeshFilterComponent.h>
|
||||
#include <XCEngine/Components/MeshRendererComponent.h>
|
||||
#include <XCEngine/Debug/ILogSink.h>
|
||||
#include <XCEngine/Debug/Logger.h>
|
||||
#include <XCEngine/Core/Math/Vector2.h>
|
||||
#include <XCEngine/Core/Math/Vector3.h>
|
||||
#include <XCEngine/Core/Math/Vector4.h>
|
||||
#include <XCEngine/Input/InputManager.h>
|
||||
#include <XCEngine/Input/InputTypes.h>
|
||||
#include <XCEngine/Scene/Scene.h>
|
||||
#include <XCEngine/Scripting/Mono/MonoScriptRuntime.h>
|
||||
#include <XCEngine/Scripting/ScriptComponent.h>
|
||||
@@ -28,6 +33,30 @@ void ExpectVector3Near(const XCEngine::Math::Vector3& actual, const XCEngine::Ma
|
||||
EXPECT_NEAR(actual.z, expected.z, tolerance);
|
||||
}
|
||||
|
||||
class CapturingLogSink final : public XCEngine::Debug::ILogSink {
|
||||
public:
|
||||
void Log(const XCEngine::Debug::LogEntry& entry) override {
|
||||
entries.push_back(entry);
|
||||
}
|
||||
|
||||
void Flush() override {
|
||||
}
|
||||
|
||||
std::vector<std::string> CollectMessagesWithPrefix(const char* prefix) const {
|
||||
std::vector<std::string> messages;
|
||||
for (const XCEngine::Debug::LogEntry& entry : entries) {
|
||||
const std::string message = entry.message.CStr();
|
||||
if (message.rfind(prefix, 0) == 0) {
|
||||
messages.push_back(message);
|
||||
}
|
||||
}
|
||||
|
||||
return messages;
|
||||
}
|
||||
|
||||
std::vector<XCEngine::Debug::LogEntry> entries;
|
||||
};
|
||||
|
||||
ScriptComponent* FindScriptComponentByClass(GameObject* gameObject, const std::string& namespaceName, const std::string& className) {
|
||||
if (!gameObject) {
|
||||
return nullptr;
|
||||
@@ -60,6 +89,8 @@ protected:
|
||||
void SetUp() override {
|
||||
engine = &ScriptEngine::Get();
|
||||
engine->OnRuntimeStop();
|
||||
engine->SetRuntimeFixedDeltaTime(ScriptEngine::DefaultFixedDeltaTime);
|
||||
XCEngine::Input::InputManager::Get().Shutdown();
|
||||
|
||||
runtime = std::make_unique<MonoScriptRuntime>(CreateMonoSettings());
|
||||
ASSERT_TRUE(runtime->Initialize()) << runtime->GetLastError();
|
||||
@@ -70,6 +101,7 @@ protected:
|
||||
void TearDown() override {
|
||||
engine->OnRuntimeStop();
|
||||
engine->SetRuntime(nullptr);
|
||||
XCEngine::Input::InputManager::Get().Shutdown();
|
||||
runtime.reset();
|
||||
scene.reset();
|
||||
}
|
||||
@@ -100,6 +132,44 @@ TEST_F(MonoScriptRuntimeTest, InitializesAndDiscoversConcreteMonoBehaviourClasse
|
||||
EXPECT_EQ(std::find(classNames.begin(), classNames.end(), "Gameplay.UtilityHelper"), classNames.end());
|
||||
}
|
||||
|
||||
TEST_F(MonoScriptRuntimeTest, ScriptClassDescriptorApiReturnsConcreteManagedTypes) {
|
||||
std::vector<ScriptClassDescriptor> classes;
|
||||
ASSERT_TRUE(runtime->TryGetAvailableScriptClasses(classes));
|
||||
ASSERT_FALSE(classes.empty());
|
||||
|
||||
EXPECT_TRUE(std::is_sorted(
|
||||
classes.begin(),
|
||||
classes.end(),
|
||||
[](const ScriptClassDescriptor& lhs, const ScriptClassDescriptor& rhs) {
|
||||
if (lhs.assemblyName != rhs.assemblyName) {
|
||||
return lhs.assemblyName < rhs.assemblyName;
|
||||
}
|
||||
if (lhs.namespaceName != rhs.namespaceName) {
|
||||
return lhs.namespaceName < rhs.namespaceName;
|
||||
}
|
||||
return lhs.className < rhs.className;
|
||||
}));
|
||||
|
||||
EXPECT_NE(
|
||||
std::find(
|
||||
classes.begin(),
|
||||
classes.end(),
|
||||
ScriptClassDescriptor{"GameScripts", "Gameplay", "LifecycleProbe"}),
|
||||
classes.end());
|
||||
EXPECT_EQ(
|
||||
std::find(
|
||||
classes.begin(),
|
||||
classes.end(),
|
||||
ScriptClassDescriptor{"GameScripts", "Gameplay", "AbstractLifecycleProbe"}),
|
||||
classes.end());
|
||||
EXPECT_EQ(
|
||||
std::find(
|
||||
classes.begin(),
|
||||
classes.end(),
|
||||
ScriptClassDescriptor{"GameScripts", "Gameplay", "UtilityHelper"}),
|
||||
classes.end());
|
||||
}
|
||||
|
||||
TEST_F(MonoScriptRuntimeTest, ClassFieldMetadataListsSupportedPublicInstanceFields) {
|
||||
std::vector<ScriptFieldMetadata> fields;
|
||||
|
||||
@@ -184,6 +254,8 @@ TEST_F(MonoScriptRuntimeTest, ScriptEngineAppliesStoredFieldsAndInvokesLifecycle
|
||||
bool observedIsActiveAndEnabled = false;
|
||||
float speed = 0.0f;
|
||||
float observedFixedDeltaTime = 0.0f;
|
||||
float observedConfiguredFixedDeltaTime = 0.0f;
|
||||
float observedConfiguredFixedDeltaTimeInUpdate = 0.0f;
|
||||
float observedUpdateDeltaTime = 0.0f;
|
||||
float observedLateDeltaTime = 0.0f;
|
||||
std::string label;
|
||||
@@ -219,6 +291,8 @@ TEST_F(MonoScriptRuntimeTest, ScriptEngineAppliesStoredFieldsAndInvokesLifecycle
|
||||
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedIsActiveAndEnabled", observedIsActiveAndEnabled));
|
||||
EXPECT_TRUE(runtime->TryGetFieldValue(component, "Speed", speed));
|
||||
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedFixedDeltaTime", observedFixedDeltaTime));
|
||||
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedConfiguredFixedDeltaTime", observedConfiguredFixedDeltaTime));
|
||||
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedConfiguredFixedDeltaTimeInUpdate", observedConfiguredFixedDeltaTimeInUpdate));
|
||||
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedUpdateDeltaTime", observedUpdateDeltaTime));
|
||||
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedLateDeltaTime", observedLateDeltaTime));
|
||||
EXPECT_TRUE(runtime->TryGetFieldValue(component, "Label", label));
|
||||
@@ -254,6 +328,8 @@ TEST_F(MonoScriptRuntimeTest, ScriptEngineAppliesStoredFieldsAndInvokesLifecycle
|
||||
EXPECT_TRUE(observedIsActiveAndEnabled);
|
||||
EXPECT_FLOAT_EQ(speed, 6.0f);
|
||||
EXPECT_FLOAT_EQ(observedFixedDeltaTime, 0.02f);
|
||||
EXPECT_FLOAT_EQ(observedConfiguredFixedDeltaTime, 0.02f);
|
||||
EXPECT_FLOAT_EQ(observedConfiguredFixedDeltaTimeInUpdate, 0.02f);
|
||||
EXPECT_FLOAT_EQ(observedUpdateDeltaTime, 0.016f);
|
||||
EXPECT_FLOAT_EQ(observedLateDeltaTime, 0.016f);
|
||||
EXPECT_EQ(label, "Configured|Awake");
|
||||
@@ -276,6 +352,233 @@ TEST_F(MonoScriptRuntimeTest, ScriptEngineAppliesStoredFieldsAndInvokesLifecycle
|
||||
EXPECT_FLOAT_EQ(localRotation.w, 0.8660254f);
|
||||
}
|
||||
|
||||
TEST_F(MonoScriptRuntimeTest, TimeFixedDeltaTimeUsesConfiguredRuntimeStepAcrossFixedAndVariableUpdates) {
|
||||
Scene* runtimeScene = CreateScene("MonoRuntimeScene");
|
||||
GameObject* host = runtimeScene->CreateGameObject("Host");
|
||||
ScriptComponent* component = AddScript(host, "Gameplay", "LifecycleProbe");
|
||||
|
||||
engine->SetRuntimeFixedDeltaTime(0.05f);
|
||||
engine->OnRuntimeStart(runtimeScene);
|
||||
engine->OnFixedUpdate(0.05f);
|
||||
engine->OnUpdate(0.016f);
|
||||
|
||||
float observedFixedDeltaTime = 0.0f;
|
||||
float observedConfiguredFixedDeltaTime = 0.0f;
|
||||
float observedConfiguredFixedDeltaTimeInUpdate = 0.0f;
|
||||
float observedUpdateDeltaTime = 0.0f;
|
||||
|
||||
ASSERT_TRUE(runtime->TryGetFieldValue(component, "ObservedFixedDeltaTime", observedFixedDeltaTime));
|
||||
ASSERT_TRUE(runtime->TryGetFieldValue(component, "ObservedConfiguredFixedDeltaTime", observedConfiguredFixedDeltaTime));
|
||||
ASSERT_TRUE(runtime->TryGetFieldValue(component, "ObservedConfiguredFixedDeltaTimeInUpdate", observedConfiguredFixedDeltaTimeInUpdate));
|
||||
ASSERT_TRUE(runtime->TryGetFieldValue(component, "ObservedUpdateDeltaTime", observedUpdateDeltaTime));
|
||||
|
||||
EXPECT_FLOAT_EQ(observedFixedDeltaTime, 0.05f);
|
||||
EXPECT_FLOAT_EQ(observedConfiguredFixedDeltaTime, 0.05f);
|
||||
EXPECT_FLOAT_EQ(observedConfiguredFixedDeltaTimeInUpdate, 0.05f);
|
||||
EXPECT_FLOAT_EQ(observedUpdateDeltaTime, 0.016f);
|
||||
}
|
||||
|
||||
TEST_F(MonoScriptRuntimeTest, ManagedInputApiReadsCurrentNativeInputManagerState) {
|
||||
XCEngine::Input::InputManager& inputManager = XCEngine::Input::InputManager::Get();
|
||||
inputManager.Initialize(nullptr);
|
||||
|
||||
Scene* runtimeScene = CreateScene("MonoRuntimeScene");
|
||||
GameObject* host = runtimeScene->CreateGameObject("Host");
|
||||
ScriptComponent* component = AddScript(host, "Gameplay", "InputProbe");
|
||||
|
||||
engine->OnRuntimeStart(runtimeScene);
|
||||
|
||||
inputManager.ProcessKeyDown(XCEngine::Input::KeyCode::A, false, false, false, false, false);
|
||||
inputManager.ProcessKeyDown(XCEngine::Input::KeyCode::Space, false, false, false, false, false);
|
||||
inputManager.ProcessKeyDown(XCEngine::Input::KeyCode::LeftCtrl, false, false, false, false, false);
|
||||
inputManager.ProcessMouseButton(XCEngine::Input::MouseButton::Left, true, 120, 48);
|
||||
inputManager.ProcessMouseMove(120, 48, 3, -2);
|
||||
inputManager.ProcessMouseWheel(1.0f, 120, 48);
|
||||
engine->OnUpdate(0.016f);
|
||||
|
||||
int32_t updateCount = 0;
|
||||
bool observedKeyA = false;
|
||||
bool observedKeyADown = false;
|
||||
bool observedKeyAUp = false;
|
||||
bool observedKeySpace = false;
|
||||
bool observedJump = false;
|
||||
bool observedJumpDown = false;
|
||||
bool observedJumpUp = false;
|
||||
bool observedFire1 = false;
|
||||
bool observedFire1Down = false;
|
||||
bool observedFire1Up = false;
|
||||
bool observedAnyKey = false;
|
||||
bool observedAnyKeyDown = false;
|
||||
bool observedLeftMouse = false;
|
||||
bool observedLeftMouseDown = false;
|
||||
bool observedLeftMouseUp = false;
|
||||
float observedHorizontal = 0.0f;
|
||||
float observedHorizontalRaw = 0.0f;
|
||||
XCEngine::Math::Vector2 observedMouseScrollDelta;
|
||||
XCEngine::Math::Vector3 observedMousePosition;
|
||||
|
||||
EXPECT_TRUE(runtime->TryGetFieldValue(component, "UpdateCount", updateCount));
|
||||
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedKeyA", observedKeyA));
|
||||
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedKeyADown", observedKeyADown));
|
||||
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedKeyAUp", observedKeyAUp));
|
||||
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedKeySpace", observedKeySpace));
|
||||
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedJump", observedJump));
|
||||
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedJumpDown", observedJumpDown));
|
||||
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedJumpUp", observedJumpUp));
|
||||
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedFire1", observedFire1));
|
||||
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedFire1Down", observedFire1Down));
|
||||
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedFire1Up", observedFire1Up));
|
||||
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedAnyKey", observedAnyKey));
|
||||
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedAnyKeyDown", observedAnyKeyDown));
|
||||
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedLeftMouse", observedLeftMouse));
|
||||
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedLeftMouseDown", observedLeftMouseDown));
|
||||
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedLeftMouseUp", observedLeftMouseUp));
|
||||
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedHorizontal", observedHorizontal));
|
||||
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedHorizontalRaw", observedHorizontalRaw));
|
||||
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedMousePosition", observedMousePosition));
|
||||
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedMouseScrollDelta", observedMouseScrollDelta));
|
||||
|
||||
EXPECT_EQ(updateCount, 1);
|
||||
EXPECT_TRUE(observedKeyA);
|
||||
EXPECT_TRUE(observedKeyADown);
|
||||
EXPECT_FALSE(observedKeyAUp);
|
||||
EXPECT_TRUE(observedKeySpace);
|
||||
EXPECT_TRUE(observedJump);
|
||||
EXPECT_TRUE(observedJumpDown);
|
||||
EXPECT_FALSE(observedJumpUp);
|
||||
EXPECT_TRUE(observedFire1);
|
||||
EXPECT_TRUE(observedFire1Down);
|
||||
EXPECT_FALSE(observedFire1Up);
|
||||
EXPECT_TRUE(observedAnyKey);
|
||||
EXPECT_TRUE(observedAnyKeyDown);
|
||||
EXPECT_TRUE(observedLeftMouse);
|
||||
EXPECT_TRUE(observedLeftMouseDown);
|
||||
EXPECT_FALSE(observedLeftMouseUp);
|
||||
EXPECT_FLOAT_EQ(observedHorizontal, -1.0f);
|
||||
EXPECT_FLOAT_EQ(observedHorizontalRaw, -1.0f);
|
||||
EXPECT_EQ(observedMousePosition, XCEngine::Math::Vector3(120.0f, 48.0f, 0.0f));
|
||||
EXPECT_EQ(observedMouseScrollDelta, XCEngine::Math::Vector2(0.0f, 1.0f));
|
||||
|
||||
inputManager.Update(0.016f);
|
||||
engine->OnUpdate(0.016f);
|
||||
|
||||
EXPECT_TRUE(runtime->TryGetFieldValue(component, "UpdateCount", updateCount));
|
||||
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedKeyA", observedKeyA));
|
||||
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedKeyADown", observedKeyADown));
|
||||
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedKeyAUp", observedKeyAUp));
|
||||
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedJump", observedJump));
|
||||
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedJumpDown", observedJumpDown));
|
||||
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedJumpUp", observedJumpUp));
|
||||
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedFire1", observedFire1));
|
||||
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedFire1Down", observedFire1Down));
|
||||
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedFire1Up", observedFire1Up));
|
||||
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedAnyKey", observedAnyKey));
|
||||
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedAnyKeyDown", observedAnyKeyDown));
|
||||
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedLeftMouse", observedLeftMouse));
|
||||
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedLeftMouseDown", observedLeftMouseDown));
|
||||
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedLeftMouseUp", observedLeftMouseUp));
|
||||
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedHorizontalRaw", observedHorizontalRaw));
|
||||
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedMouseScrollDelta", observedMouseScrollDelta));
|
||||
|
||||
EXPECT_EQ(updateCount, 2);
|
||||
EXPECT_TRUE(observedKeyA);
|
||||
EXPECT_FALSE(observedKeyADown);
|
||||
EXPECT_FALSE(observedKeyAUp);
|
||||
EXPECT_TRUE(observedJump);
|
||||
EXPECT_FALSE(observedJumpDown);
|
||||
EXPECT_FALSE(observedJumpUp);
|
||||
EXPECT_TRUE(observedFire1);
|
||||
EXPECT_FALSE(observedFire1Down);
|
||||
EXPECT_FALSE(observedFire1Up);
|
||||
EXPECT_TRUE(observedAnyKey);
|
||||
EXPECT_FALSE(observedAnyKeyDown);
|
||||
EXPECT_TRUE(observedLeftMouse);
|
||||
EXPECT_FALSE(observedLeftMouseDown);
|
||||
EXPECT_FALSE(observedLeftMouseUp);
|
||||
EXPECT_FLOAT_EQ(observedHorizontalRaw, -1.0f);
|
||||
EXPECT_EQ(observedMouseScrollDelta, XCEngine::Math::Vector2(0.0f, 0.0f));
|
||||
|
||||
inputManager.ProcessKeyUp(XCEngine::Input::KeyCode::A, false, false, false, false);
|
||||
inputManager.ProcessKeyUp(XCEngine::Input::KeyCode::LeftCtrl, false, false, false, false);
|
||||
inputManager.ProcessMouseButton(XCEngine::Input::MouseButton::Left, false, 120, 48);
|
||||
engine->OnUpdate(0.016f);
|
||||
|
||||
EXPECT_TRUE(runtime->TryGetFieldValue(component, "UpdateCount", updateCount));
|
||||
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedKeyA", observedKeyA));
|
||||
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedKeyAUp", observedKeyAUp));
|
||||
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedJump", observedJump));
|
||||
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedJumpUp", observedJumpUp));
|
||||
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedFire1", observedFire1));
|
||||
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedFire1Up", observedFire1Up));
|
||||
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedAnyKey", observedAnyKey));
|
||||
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedAnyKeyDown", observedAnyKeyDown));
|
||||
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedLeftMouse", observedLeftMouse));
|
||||
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedLeftMouseUp", observedLeftMouseUp));
|
||||
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedHorizontal", observedHorizontal));
|
||||
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedHorizontalRaw", observedHorizontalRaw));
|
||||
|
||||
EXPECT_EQ(updateCount, 3);
|
||||
EXPECT_FALSE(observedKeyA);
|
||||
EXPECT_TRUE(observedKeyAUp);
|
||||
EXPECT_TRUE(observedJump);
|
||||
EXPECT_FALSE(observedJumpUp);
|
||||
EXPECT_FALSE(observedFire1);
|
||||
EXPECT_TRUE(observedFire1Up);
|
||||
EXPECT_TRUE(observedAnyKey);
|
||||
EXPECT_FALSE(observedAnyKeyDown);
|
||||
EXPECT_FALSE(observedLeftMouse);
|
||||
EXPECT_TRUE(observedLeftMouseUp);
|
||||
EXPECT_FLOAT_EQ(observedHorizontal, 0.0f);
|
||||
EXPECT_FLOAT_EQ(observedHorizontalRaw, 0.0f);
|
||||
|
||||
inputManager.Update(0.016f);
|
||||
inputManager.ProcessKeyUp(XCEngine::Input::KeyCode::Space, false, false, false, false);
|
||||
engine->OnUpdate(0.016f);
|
||||
|
||||
EXPECT_TRUE(runtime->TryGetFieldValue(component, "UpdateCount", updateCount));
|
||||
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedKeySpace", observedKeySpace));
|
||||
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedJump", observedJump));
|
||||
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedJumpUp", observedJumpUp));
|
||||
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedAnyKey", observedAnyKey));
|
||||
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedAnyKeyDown", observedAnyKeyDown));
|
||||
|
||||
EXPECT_EQ(updateCount, 4);
|
||||
EXPECT_FALSE(observedKeySpace);
|
||||
EXPECT_FALSE(observedJump);
|
||||
EXPECT_TRUE(observedJumpUp);
|
||||
EXPECT_FALSE(observedAnyKey);
|
||||
EXPECT_FALSE(observedAnyKeyDown);
|
||||
}
|
||||
|
||||
TEST_F(MonoScriptRuntimeTest, ManagedDebugLogBridgeWritesLifecycleTickMessagesToNativeLogger) {
|
||||
auto sink = std::make_unique<CapturingLogSink>();
|
||||
CapturingLogSink* sinkPtr = sink.get();
|
||||
XCEngine::Debug::Logger::Get().AddSink(std::move(sink));
|
||||
|
||||
Scene* runtimeScene = CreateScene("MonoRuntimeScene");
|
||||
GameObject* host = runtimeScene->CreateGameObject("Host");
|
||||
ScriptComponent* component = AddScript(host, "Gameplay", "TickLogProbe");
|
||||
ASSERT_NE(component, nullptr);
|
||||
|
||||
engine->OnRuntimeStart(runtimeScene);
|
||||
engine->OnFixedUpdate(0.02f);
|
||||
engine->OnUpdate(0.016f);
|
||||
engine->OnLateUpdate(0.016f);
|
||||
|
||||
const std::vector<std::string> messages = sinkPtr->CollectMessagesWithPrefix("[TickLogProbe]");
|
||||
const std::vector<std::string> expected = {
|
||||
"[TickLogProbe] Awake",
|
||||
"[TickLogProbe] FixedUpdate 1",
|
||||
"[TickLogProbe] Start",
|
||||
"[TickLogProbe] Update 1",
|
||||
"[TickLogProbe] LateUpdate 1",
|
||||
};
|
||||
|
||||
EXPECT_EQ(messages, expected);
|
||||
|
||||
XCEngine::Debug::Logger::Get().RemoveSink(sinkPtr);
|
||||
}
|
||||
|
||||
TEST_F(MonoScriptRuntimeTest, DeserializedSceneRebindsManagedScriptsAndRestoresStoredFields) {
|
||||
Scene originalScene("SerializedMonoScene");
|
||||
GameObject* hostA = originalScene.CreateGameObject("HostA");
|
||||
|
||||
87
tests/scripting/test_project_script_assembly.cpp
Normal file
87
tests/scripting/test_project_script_assembly.cpp
Normal file
@@ -0,0 +1,87 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <XCEngine/Scripting/Mono/MonoScriptRuntime.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <filesystem>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
using namespace XCEngine::Scripting;
|
||||
|
||||
namespace {
|
||||
|
||||
MonoScriptRuntime::Settings CreateProjectMonoSettings() {
|
||||
MonoScriptRuntime::Settings settings;
|
||||
settings.assemblyDirectory = XCENGINE_TEST_PROJECT_MANAGED_OUTPUT_DIR;
|
||||
settings.corlibDirectory = XCENGINE_TEST_PROJECT_MANAGED_OUTPUT_DIR;
|
||||
settings.coreAssemblyPath = XCENGINE_TEST_PROJECT_SCRIPT_CORE_DLL;
|
||||
settings.appAssemblyPath = XCENGINE_TEST_PROJECT_GAME_SCRIPTS_DLL;
|
||||
return settings;
|
||||
}
|
||||
|
||||
class ProjectScriptAssemblyTest : public ::testing::Test {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
ASSERT_TRUE(std::filesystem::exists(XCENGINE_TEST_PROJECT_SCRIPT_CORE_DLL));
|
||||
ASSERT_TRUE(std::filesystem::exists(XCENGINE_TEST_PROJECT_GAME_SCRIPTS_DLL));
|
||||
|
||||
runtime = std::make_unique<MonoScriptRuntime>(CreateProjectMonoSettings());
|
||||
ASSERT_TRUE(runtime->Initialize()) << runtime->GetLastError();
|
||||
}
|
||||
|
||||
std::unique_ptr<MonoScriptRuntime> runtime;
|
||||
};
|
||||
|
||||
TEST_F(ProjectScriptAssemblyTest, InitializesFromProjectScriptAssemblyDirectory) {
|
||||
EXPECT_TRUE(runtime->IsInitialized());
|
||||
EXPECT_EQ(runtime->GetSettings().assemblyDirectory, std::filesystem::path(XCENGINE_TEST_PROJECT_MANAGED_OUTPUT_DIR));
|
||||
EXPECT_EQ(runtime->GetSettings().appAssemblyPath, std::filesystem::path(XCENGINE_TEST_PROJECT_GAME_SCRIPTS_DLL));
|
||||
}
|
||||
|
||||
TEST_F(ProjectScriptAssemblyTest, DiscoversProjectAssetMonoBehaviourClassesAndFieldMetadata) {
|
||||
const std::vector<std::string> classNames = runtime->GetScriptClassNames("GameScripts");
|
||||
std::vector<ScriptClassDescriptor> classDescriptors;
|
||||
ASSERT_TRUE(runtime->TryGetAvailableScriptClasses(classDescriptors));
|
||||
|
||||
EXPECT_TRUE(runtime->IsClassAvailable("GameScripts", "ProjectScripts", "ProjectScriptProbe"));
|
||||
EXPECT_NE(
|
||||
std::find(classNames.begin(), classNames.end(), "ProjectScripts.ProjectScriptProbe"),
|
||||
classNames.end());
|
||||
EXPECT_NE(
|
||||
std::find(
|
||||
classDescriptors.begin(),
|
||||
classDescriptors.end(),
|
||||
ScriptClassDescriptor{"GameScripts", "ProjectScripts", "ProjectScriptProbe"}),
|
||||
classDescriptors.end());
|
||||
|
||||
std::vector<ScriptFieldMetadata> fields;
|
||||
ASSERT_TRUE(runtime->TryGetClassFieldMetadata("GameScripts", "ProjectScripts", "ProjectScriptProbe", fields));
|
||||
|
||||
const std::vector<ScriptFieldMetadata> expectedFields = {
|
||||
{"EnabledOnBoot", ScriptFieldType::Bool},
|
||||
{"Label", ScriptFieldType::String},
|
||||
{"Speed", ScriptFieldType::Float},
|
||||
};
|
||||
|
||||
EXPECT_EQ(fields, expectedFields);
|
||||
|
||||
std::vector<ScriptFieldDefaultValue> defaultValues;
|
||||
ASSERT_TRUE(runtime->TryGetClassFieldDefaultValues("GameScripts", "ProjectScripts", "ProjectScriptProbe", defaultValues));
|
||||
ASSERT_EQ(defaultValues.size(), 3u);
|
||||
|
||||
EXPECT_EQ(defaultValues[0].fieldName, "EnabledOnBoot");
|
||||
EXPECT_EQ(defaultValues[0].type, ScriptFieldType::Bool);
|
||||
EXPECT_EQ(std::get<bool>(defaultValues[0].value), true);
|
||||
|
||||
EXPECT_EQ(defaultValues[1].fieldName, "Label");
|
||||
EXPECT_EQ(defaultValues[1].type, ScriptFieldType::String);
|
||||
EXPECT_EQ(std::get<std::string>(defaultValues[1].value), "ProjectScriptProbe");
|
||||
|
||||
EXPECT_EQ(defaultValues[2].fieldName, "Speed");
|
||||
EXPECT_EQ(defaultValues[2].type, ScriptFieldType::Float);
|
||||
EXPECT_FLOAT_EQ(std::get<float>(defaultValues[2].value), 2.5f);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
@@ -42,6 +42,12 @@ public:
|
||||
events.push_back("RuntimeStop:" + (scene ? scene->GetName() : std::string("null")));
|
||||
}
|
||||
|
||||
bool TryGetAvailableScriptClasses(
|
||||
std::vector<ScriptClassDescriptor>& outClasses) const override {
|
||||
outClasses = scriptClasses;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool TryGetClassFieldMetadata(
|
||||
const std::string& assemblyName,
|
||||
const std::string& namespaceName,
|
||||
@@ -117,6 +123,7 @@ public:
|
||||
}
|
||||
|
||||
std::vector<std::string> events;
|
||||
std::vector<ScriptClassDescriptor> scriptClasses;
|
||||
std::vector<ScriptFieldMetadata> fieldMetadata;
|
||||
std::vector<ScriptFieldDefaultValue> fieldDefaultValues;
|
||||
std::unordered_map<std::string, ScriptFieldValue> managedFieldValues;
|
||||
@@ -325,6 +332,83 @@ TEST_F(ScriptEngineTest, RuntimeCreatedScriptComponentIsTrackedImmediatelyAndSta
|
||||
EXPECT_EQ(runtime.events, expectedAfterUpdate);
|
||||
}
|
||||
|
||||
TEST_F(ScriptEngineTest, ScriptClassDiscoveryApiReturnsSortedDescriptorsAndSupportsAssemblyFilter) {
|
||||
runtime.scriptClasses = {
|
||||
{"Tools", "", "UtilityProbe"},
|
||||
{"GameScripts", "Gameplay", "Zombie"},
|
||||
{"GameScripts", "", "Bootstrap"},
|
||||
{"GameScripts", "Gameplay", "Actor"},
|
||||
{"Broken", "", ""}
|
||||
};
|
||||
|
||||
std::vector<ScriptClassDescriptor> allClasses;
|
||||
ASSERT_TRUE(engine->TryGetAvailableScriptClasses(allClasses));
|
||||
|
||||
const std::vector<ScriptClassDescriptor> expectedAllClasses = {
|
||||
{"GameScripts", "", "Bootstrap"},
|
||||
{"GameScripts", "Gameplay", "Actor"},
|
||||
{"GameScripts", "Gameplay", "Zombie"},
|
||||
{"Tools", "", "UtilityProbe"}
|
||||
};
|
||||
EXPECT_EQ(allClasses, expectedAllClasses);
|
||||
EXPECT_EQ(allClasses[0].GetFullName(), "Bootstrap");
|
||||
EXPECT_EQ(allClasses[1].GetFullName(), "Gameplay.Actor");
|
||||
|
||||
std::vector<ScriptClassDescriptor> gameScriptClasses;
|
||||
ASSERT_TRUE(engine->TryGetAvailableScriptClasses(gameScriptClasses, "GameScripts"));
|
||||
|
||||
const std::vector<ScriptClassDescriptor> expectedGameScriptClasses = {
|
||||
{"GameScripts", "", "Bootstrap"},
|
||||
{"GameScripts", "Gameplay", "Actor"},
|
||||
{"GameScripts", "Gameplay", "Zombie"}
|
||||
};
|
||||
EXPECT_EQ(gameScriptClasses, expectedGameScriptClasses);
|
||||
}
|
||||
|
||||
TEST_F(ScriptEngineTest, ChangingScriptClassWhileRuntimeRunningRecreatesTrackedInstance) {
|
||||
Scene* runtimeScene = CreateScene("RuntimeScene");
|
||||
GameObject* host = runtimeScene->CreateGameObject("Host");
|
||||
ScriptComponent* component = AddScriptComponent(host, "Gameplay", "OldClass");
|
||||
|
||||
engine->OnRuntimeStart(runtimeScene);
|
||||
runtime.Clear();
|
||||
|
||||
component->SetScriptClass("GameScripts", "Gameplay", "NewClass");
|
||||
|
||||
const std::vector<std::string> expected = {
|
||||
"OnDisable:Host:Gameplay.NewClass",
|
||||
"OnDestroy:Host:Gameplay.NewClass",
|
||||
"Destroy:Host:Gameplay.NewClass",
|
||||
"Create:Host:Gameplay.NewClass",
|
||||
"Awake:Host:Gameplay.NewClass",
|
||||
"OnEnable:Host:Gameplay.NewClass"
|
||||
};
|
||||
EXPECT_EQ(runtime.events, expected);
|
||||
EXPECT_TRUE(engine->HasTrackedScriptComponent(component));
|
||||
EXPECT_TRUE(engine->HasRuntimeInstance(component));
|
||||
}
|
||||
|
||||
TEST_F(ScriptEngineTest, ClearingScriptClassWhileRuntimeRunningDestroysTrackedInstance) {
|
||||
Scene* runtimeScene = CreateScene("RuntimeScene");
|
||||
GameObject* host = runtimeScene->CreateGameObject("Host");
|
||||
ScriptComponent* component = AddScriptComponent(host, "Gameplay", "OldClass");
|
||||
|
||||
engine->OnRuntimeStart(runtimeScene);
|
||||
runtime.Clear();
|
||||
|
||||
component->ClearScriptClass();
|
||||
|
||||
const std::vector<std::string> expected = {
|
||||
"OnDisable:Host:",
|
||||
"OnDestroy:Host:",
|
||||
"Destroy:Host:"
|
||||
};
|
||||
EXPECT_EQ(runtime.events, expected);
|
||||
EXPECT_FALSE(engine->HasTrackedScriptComponent(component));
|
||||
EXPECT_FALSE(engine->HasRuntimeInstance(component));
|
||||
EXPECT_FALSE(component->HasScriptClass());
|
||||
}
|
||||
|
||||
TEST_F(ScriptEngineTest, FieldReadApiPrefersLiveManagedValueAndFallsBackToStoredValue) {
|
||||
Scene* runtimeScene = CreateScene("RuntimeScene");
|
||||
GameObject* host = runtimeScene->CreateGameObject("Host");
|
||||
|
||||
Reference in New Issue
Block a user