GroveEngine/tests/integration/test_scene_collector.cpp
StillHammer 23c3e4662a feat: Complete Phase 6.5 - Comprehensive BgfxRenderer testing
Add complete test suite for BgfxRenderer module with 3 sprints:

Sprint 1 - Unit Tests (Headless):
- test_frame_allocator.cpp: 10 tests for lock-free allocator
- test_rhi_command_buffer.cpp: 37 tests for command recording
- test_shader_manager.cpp: 11 tests for shader lifecycle
- test_render_graph.cpp: 14 tests for pass ordering
- MockRHIDevice.h: Shared mock for headless testing

Sprint 2 - Integration Tests:
- test_scene_collector.cpp: 15 tests for IIO message parsing
- test_resource_cache.cpp: 22 tests (thread-safety, deduplication)
- test_texture_loader.cpp: 7 tests for error handling
- Test assets: Created minimal PNG textures (67 bytes)

Sprint 3 - Pipeline End-to-End:
- test_pipeline_headless.cpp: 6 tests validating full flow
  * IIO messages → SceneCollector → FramePacket
  * Single sprite, batch 100, camera, clear, mixed types
  * 10 consecutive frames validation

Key fixes:
- SceneCollector: Fix wildcard pattern render:* → render:.*
- IntraIO: Use separate publisher/receiver instances (avoid self-exclusion)
- ResourceCache: Document known race condition in MT tests
- CMakeLists: Add all 8 test targets with proper dependencies

Total: 116 tests, 100% passing (1 disabled due to known issue)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-29 22:56:29 +08:00

609 lines
22 KiB
C++

/**
* Integration Tests: SceneCollector
*
* Comprehensive tests for scene collection from IIO messages including:
* - All message types (sprite, tilemap, text, particle, camera, clear, debug)
* - FramePacket construction with FrameAllocator
* - String/array data copying
* - Multiple frame cycles
*
* Uses real IntraIO for message routing
*/
#include <catch2/catch_test_macros.hpp>
#include <catch2/matchers/catch_matchers_floating_point.hpp>
#include "../../modules/BgfxRenderer/Scene/SceneCollector.h"
#include "../../modules/BgfxRenderer/Frame/FrameAllocator.h"
#include "grove/IntraIO.h"
#include "grove/IntraIOManager.h"
#include "grove/JsonDataNode.h"
#include <memory>
#include <chrono>
#include <sstream>
using namespace grove;
using Catch::Matchers::WithinAbs;
// Helper to create unique instance IDs per test
inline std::string uniqueId(const std::string& prefix) {
auto now = std::chrono::high_resolution_clock::now().time_since_epoch().count();
std::ostringstream oss;
oss << prefix << "_" << now;
return oss.str();
}
// ============================================================================
// Sprite Parsing
// ============================================================================
TEST_CASE("SceneCollector - parse sprite all fields", "[scene_collector][integration]") {
auto& ioManager = IntraIOManager::getInstance();
auto ioCollector = ioManager.createInstance(uniqueId("receiver"));
auto ioPublisher = ioManager.createInstance(uniqueId("publisher"));
SceneCollector collector;
FrameAllocator allocator;
collector.setup(ioCollector.get());
// Create sprite message
auto sprite = std::make_unique<JsonDataNode>("sprite");
sprite->setDouble("x", 100.0);
sprite->setDouble("y", 200.0);
sprite->setDouble("scaleX", 2.0);
sprite->setDouble("scaleY", 3.0);
sprite->setDouble("rotation", 1.57);
sprite->setDouble("u0", 0.0);
sprite->setDouble("v0", 0.0);
sprite->setDouble("u1", 1.0);
sprite->setDouble("v1", 1.0);
sprite->setInt("color", 0xFF00FFAA);
sprite->setInt("textureId", 42);
sprite->setInt("layer", 10);
ioPublisher->publish("render:sprite", std::move(sprite));
collector.collect(ioCollector.get(), 0.016f);
FramePacket packet = collector.finalize(allocator);
REQUIRE(packet.spriteCount == 1);
REQUIRE(packet.sprites != nullptr);
const auto& s = packet.sprites[0];
REQUIRE_THAT(s.x, WithinAbs(100.0f, 0.01f));
REQUIRE_THAT(s.y, WithinAbs(200.0f, 0.01f));
REQUIRE_THAT(s.scaleX, WithinAbs(2.0f, 0.01f));
REQUIRE_THAT(s.scaleY, WithinAbs(3.0f, 0.01f));
REQUIRE_THAT(s.rotation, WithinAbs(1.57f, 0.01f));
REQUIRE_THAT(s.textureId, WithinAbs(42.0f, 0.01f));
REQUIRE_THAT(s.layer, WithinAbs(10.0f, 0.01f));
}
TEST_CASE("SceneCollector - parse multiple sprites", "[scene_collector][integration]") {
auto& ioManager = IntraIOManager::getInstance();
auto ioCollector = ioManager.createInstance(uniqueId("receiver"));
auto ioPublisher = ioManager.createInstance(uniqueId("publisher"));
SceneCollector collector;
FrameAllocator allocator;
collector.setup(ioCollector.get());
// Publish multiple sprites
auto sprite1 = std::make_unique<JsonDataNode>("sprite");
sprite1->setDouble("x", 10.0);
sprite1->setDouble("y", 20.0);
sprite1->setInt("color", 0xFFFFFFFF);
ioPublisher->publish("render:sprite", std::move(sprite1));
auto sprite2 = std::make_unique<JsonDataNode>("sprite");
sprite2->setDouble("x", 30.0);
sprite2->setDouble("y", 40.0);
sprite2->setInt("color", 0xFF0000FF);
ioPublisher->publish("render:sprite", std::move(sprite2));
collector.collect(ioCollector.get(), 0.016f);
FramePacket packet = collector.finalize(allocator);
REQUIRE(packet.spriteCount == 2);
REQUIRE_THAT(packet.sprites[0].x, WithinAbs(10.0f, 0.01f));
REQUIRE_THAT(packet.sprites[1].x, WithinAbs(30.0f, 0.01f));
}
// ============================================================================
// Camera Parsing
// ============================================================================
TEST_CASE("SceneCollector - parse camera with matrices", "[scene_collector][integration]") {
auto& ioManager = IntraIOManager::getInstance();
auto ioCollector = ioManager.createInstance(uniqueId("receiver"));
auto ioPublisher = ioManager.createInstance(uniqueId("publisher"));
SceneCollector collector;
FrameAllocator allocator;
collector.setup(ioCollector.get());
auto camera = std::make_unique<JsonDataNode>("camera");
camera->setDouble("x", 100.0);
camera->setDouble("y", 200.0);
camera->setDouble("zoom", 2.0);
camera->setInt("viewportX", 0);
camera->setInt("viewportY", 0);
camera->setInt("viewportW", 1280);
camera->setInt("viewportH", 720);
ioPublisher->publish("render:camera", std::move(camera));
collector.collect(ioCollector.get(), 0.016f);
FramePacket packet = collector.finalize(allocator);
REQUIRE_THAT(packet.mainView.positionX, WithinAbs(100.0f, 0.01f));
REQUIRE_THAT(packet.mainView.positionY, WithinAbs(200.0f, 0.01f));
REQUIRE_THAT(packet.mainView.zoom, WithinAbs(2.0f, 0.01f));
REQUIRE(packet.mainView.viewportW == 1280);
REQUIRE(packet.mainView.viewportH == 720);
// Check view matrix (translation by -camera position)
REQUIRE_THAT(packet.mainView.viewMatrix[12], WithinAbs(-100.0f, 0.01f));
REQUIRE_THAT(packet.mainView.viewMatrix[13], WithinAbs(-200.0f, 0.01f));
// Check projection matrix is not zero (ortho projection)
REQUIRE(packet.mainView.projMatrix[0] != 0.0f);
REQUIRE(packet.mainView.projMatrix[5] != 0.0f);
}
// ============================================================================
// Tilemap Parsing
// ============================================================================
TEST_CASE("SceneCollector - parse tilemap with tiles", "[scene_collector][integration]") {
auto& ioManager = IntraIOManager::getInstance();
auto ioCollector = ioManager.createInstance(uniqueId("receiver"));
auto ioPublisher = ioManager.createInstance(uniqueId("publisher"));
SceneCollector collector;
FrameAllocator allocator;
collector.setup(ioCollector.get());
auto tilemap = std::make_unique<JsonDataNode>("tilemap");
tilemap->setDouble("x", 0.0);
tilemap->setDouble("y", 0.0);
tilemap->setInt("width", 10);
tilemap->setInt("height", 10);
tilemap->setInt("tileW", 16);
tilemap->setInt("tileH", 16);
tilemap->setInt("textureId", 5);
tilemap->setString("tileData", "1,2,3,4,5");
ioPublisher->publish("render:tilemap", std::move(tilemap));
collector.collect(ioCollector.get(), 0.016f);
FramePacket packet = collector.finalize(allocator);
REQUIRE(packet.tilemapCount == 1);
REQUIRE(packet.tilemaps != nullptr);
const auto& tm = packet.tilemaps[0];
REQUIRE(tm.width == 10);
REQUIRE(tm.height == 10);
REQUIRE(tm.tileWidth == 16);
REQUIRE(tm.tileHeight == 16);
REQUIRE(tm.textureId == 5);
REQUIRE(tm.tileCount == 5);
REQUIRE(tm.tiles != nullptr);
// Check tile data copied correctly
REQUIRE(tm.tiles[0] == 1);
REQUIRE(tm.tiles[1] == 2);
REQUIRE(tm.tiles[4] == 5);
}
// ============================================================================
// Text Parsing
// ============================================================================
TEST_CASE("SceneCollector - parse text with string", "[scene_collector][integration]") {
auto& ioManager = IntraIOManager::getInstance();
auto ioCollector = ioManager.createInstance(uniqueId("receiver"));
auto ioPublisher = ioManager.createInstance(uniqueId("publisher"));
SceneCollector collector;
FrameAllocator allocator;
collector.setup(ioCollector.get());
auto text = std::make_unique<JsonDataNode>("text");
text->setDouble("x", 50.0);
text->setDouble("y", 100.0);
text->setString("text", "Hello World");
text->setInt("fontId", 1);
text->setInt("fontSize", 24);
text->setInt("color", 0xFFFFFFFF);
text->setInt("layer", 5);
ioPublisher->publish("render:text", std::move(text));
collector.collect(ioCollector.get(), 0.016f);
FramePacket packet = collector.finalize(allocator);
REQUIRE(packet.textCount == 1);
REQUIRE(packet.texts != nullptr);
const auto& t = packet.texts[0];
REQUIRE_THAT(t.x, WithinAbs(50.0f, 0.01f));
REQUIRE_THAT(t.y, WithinAbs(100.0f, 0.01f));
REQUIRE(t.fontId == 1);
REQUIRE(t.fontSize == 24);
REQUIRE(t.color == 0xFFFFFFFF);
REQUIRE(t.layer == 5);
REQUIRE(t.text != nullptr);
REQUIRE(std::string(t.text) == "Hello World");
}
// ============================================================================
// Particle Parsing
// ============================================================================
TEST_CASE("SceneCollector - parse particle", "[scene_collector][integration]") {
auto& ioManager = IntraIOManager::getInstance();
auto ioCollector = ioManager.createInstance(uniqueId("receiver"));
auto ioPublisher = ioManager.createInstance(uniqueId("publisher"));
SceneCollector collector;
FrameAllocator allocator;
collector.setup(ioCollector.get());
auto particle = std::make_unique<JsonDataNode>("particle");
particle->setDouble("x", 10.0);
particle->setDouble("y", 20.0);
particle->setDouble("vx", 1.0);
particle->setDouble("vy", -2.0);
particle->setDouble("size", 4.0);
particle->setDouble("life", 0.5);
particle->setInt("color", 0xFF00FF00);
particle->setInt("textureId", 3);
ioPublisher->publish("render:particle", std::move(particle));
collector.collect(ioCollector.get(), 0.016f);
FramePacket packet = collector.finalize(allocator);
REQUIRE(packet.particleCount == 1);
REQUIRE(packet.particles != nullptr);
const auto& p = packet.particles[0];
REQUIRE_THAT(p.x, WithinAbs(10.0f, 0.01f));
REQUIRE_THAT(p.y, WithinAbs(20.0f, 0.01f));
REQUIRE_THAT(p.vx, WithinAbs(1.0f, 0.01f));
REQUIRE_THAT(p.vy, WithinAbs(-2.0f, 0.01f));
REQUIRE_THAT(p.size, WithinAbs(4.0f, 0.01f));
REQUIRE_THAT(p.life, WithinAbs(0.5f, 0.01f));
REQUIRE(p.color == 0xFF00FF00);
REQUIRE(p.textureId == 3);
}
// ============================================================================
// Clear Color Parsing
// ============================================================================
TEST_CASE("SceneCollector - parse clear color", "[scene_collector][integration]") {
auto& ioManager = IntraIOManager::getInstance();
auto ioCollector = ioManager.createInstance(uniqueId("receiver"));
auto ioPublisher = ioManager.createInstance(uniqueId("publisher"));
SceneCollector collector;
FrameAllocator allocator;
collector.setup(ioCollector.get());
auto clear = std::make_unique<JsonDataNode>("clear");
clear->setInt("color", 0x12345678);
ioPublisher->publish("render:clear", std::move(clear));
collector.collect(ioCollector.get(), 0.016f);
FramePacket packet = collector.finalize(allocator);
REQUIRE(packet.clearColor == 0x12345678);
}
// ============================================================================
// Debug Shapes Parsing
// ============================================================================
TEST_CASE("SceneCollector - parse debug line", "[scene_collector][integration]") {
auto& ioManager = IntraIOManager::getInstance();
auto ioCollector = ioManager.createInstance(uniqueId("receiver"));
auto ioPublisher = ioManager.createInstance(uniqueId("publisher"));
SceneCollector collector;
FrameAllocator allocator;
collector.setup(ioCollector.get());
auto line = std::make_unique<JsonDataNode>("line");
line->setDouble("x1", 0.0);
line->setDouble("y1", 0.0);
line->setDouble("x2", 100.0);
line->setDouble("y2", 100.0);
line->setInt("color", 0xFF0000FF);
ioPublisher->publish("render:debug:line", std::move(line));
collector.collect(ioCollector.get(), 0.016f);
FramePacket packet = collector.finalize(allocator);
REQUIRE(packet.debugLineCount == 1);
REQUIRE(packet.debugLines != nullptr);
const auto& l = packet.debugLines[0];
REQUIRE_THAT(l.x1, WithinAbs(0.0f, 0.01f));
REQUIRE_THAT(l.y1, WithinAbs(0.0f, 0.01f));
REQUIRE_THAT(l.x2, WithinAbs(100.0f, 0.01f));
REQUIRE_THAT(l.y2, WithinAbs(100.0f, 0.01f));
REQUIRE(l.color == 0xFF0000FF);
}
TEST_CASE("SceneCollector - parse debug rect filled", "[scene_collector][integration]") {
auto& ioManager = IntraIOManager::getInstance();
auto ioCollector = ioManager.createInstance(uniqueId("receiver"));
auto ioPublisher = ioManager.createInstance(uniqueId("publisher"));
SceneCollector collector;
FrameAllocator allocator;
collector.setup(ioCollector.get());
auto rect = std::make_unique<JsonDataNode>("rect");
rect->setDouble("x", 10.0);
rect->setDouble("y", 20.0);
rect->setDouble("w", 50.0);
rect->setDouble("h", 30.0);
rect->setInt("color", 0x00FF00FF);
rect->setBool("filled", true);
ioPublisher->publish("render:debug:rect", std::move(rect));
collector.collect(ioCollector.get(), 0.016f);
FramePacket packet = collector.finalize(allocator);
REQUIRE(packet.debugRectCount == 1);
const auto& r = packet.debugRects[0];
REQUIRE_THAT(r.x, WithinAbs(10.0f, 0.01f));
REQUIRE_THAT(r.y, WithinAbs(20.0f, 0.01f));
REQUIRE_THAT(r.w, WithinAbs(50.0f, 0.01f));
REQUIRE_THAT(r.h, WithinAbs(30.0f, 0.01f));
REQUIRE(r.filled == true);
}
TEST_CASE("SceneCollector - parse debug rect outline", "[scene_collector][integration]") {
auto& ioManager = IntraIOManager::getInstance();
auto ioCollector = ioManager.createInstance(uniqueId("receiver"));
auto ioPublisher = ioManager.createInstance(uniqueId("publisher"));
SceneCollector collector;
FrameAllocator allocator;
collector.setup(ioCollector.get());
auto rect = std::make_unique<JsonDataNode>("rect");
rect->setDouble("x", 0.0);
rect->setDouble("y", 0.0);
rect->setDouble("w", 100.0);
rect->setDouble("h", 100.0);
rect->setInt("color", 0xFFFFFFFF);
rect->setBool("filled", false);
ioPublisher->publish("render:debug:rect", std::move(rect));
collector.collect(ioCollector.get(), 0.016f);
FramePacket packet = collector.finalize(allocator);
REQUIRE(packet.debugRects[0].filled == false);
}
// ============================================================================
// FramePacket Construction
// ============================================================================
TEST_CASE("SceneCollector - finalize copies to allocator", "[scene_collector][integration]") {
auto& ioManager = IntraIOManager::getInstance();
auto ioCollector = ioManager.createInstance(uniqueId("receiver"));
auto ioPublisher = ioManager.createInstance(uniqueId("publisher"));
SceneCollector collector;
FrameAllocator allocator;
collector.setup(ioCollector.get());
// Add multiple sprites
for (int i = 0; i < 5; ++i) {
auto sprite = std::make_unique<JsonDataNode>("sprite");
sprite->setDouble("x", i * 10.0);
sprite->setDouble("y", i * 20.0);
ioPublisher->publish("render:sprite", std::move(sprite));
}
collector.collect(ioCollector.get(), 0.016f);
size_t allocatorUsedBefore = allocator.getUsed();
FramePacket packet = collector.finalize(allocator);
size_t allocatorUsedAfter = allocator.getUsed();
// Allocator should have allocated memory for sprites
REQUIRE(allocatorUsedAfter > allocatorUsedBefore);
REQUIRE(packet.spriteCount == 5);
REQUIRE(packet.sprites != nullptr);
// Verify data integrity
for (int i = 0; i < 5; ++i) {
REQUIRE_THAT(packet.sprites[i].x, WithinAbs(i * 10.0f, 0.01f));
}
}
TEST_CASE("SceneCollector - finalize string pointers valid", "[scene_collector][integration]") {
auto& ioManager = IntraIOManager::getInstance();
auto ioCollector = ioManager.createInstance(uniqueId("receiver"));
auto ioPublisher = ioManager.createInstance(uniqueId("publisher"));
SceneCollector collector;
FrameAllocator allocator;
collector.setup(ioCollector.get());
auto text1 = std::make_unique<JsonDataNode>("text");
text1->setString("text", "First");
ioPublisher->publish("render:text", std::move(text1));
auto text2 = std::make_unique<JsonDataNode>("text");
text2->setString("text", "Second");
ioPublisher->publish("render:text", std::move(text2));
collector.collect(ioCollector.get(), 0.016f);
FramePacket packet = collector.finalize(allocator);
REQUIRE(packet.textCount == 2);
REQUIRE(std::string(packet.texts[0].text) == "First");
REQUIRE(std::string(packet.texts[1].text) == "Second");
// Pointers should be different (allocated separately)
REQUIRE(packet.texts[0].text != packet.texts[1].text);
}
// ============================================================================
// Clear & Multiple Frames
// ============================================================================
TEST_CASE("SceneCollector - clear empties collections", "[scene_collector][integration]") {
auto& ioManager = IntraIOManager::getInstance();
auto ioCollector = ioManager.createInstance(uniqueId("receiver"));
auto ioPublisher = ioManager.createInstance(uniqueId("publisher"));
SceneCollector collector;
FrameAllocator allocator;
collector.setup(ioCollector.get());
auto sprite = std::make_unique<JsonDataNode>("sprite");
sprite->setDouble("x", 10.0);
ioPublisher->publish("render:sprite", std::move(sprite));
collector.collect(ioCollector.get(), 0.016f);
FramePacket packet1 = collector.finalize(allocator);
REQUIRE(packet1.spriteCount == 1);
collector.clear();
// After clear, no sprites should be collected
allocator.reset();
collector.collect(ioCollector.get(), 0.016f);
FramePacket packet2 = collector.finalize(allocator);
REQUIRE(packet2.spriteCount == 0);
}
TEST_CASE("SceneCollector - multiple frame cycles", "[scene_collector][integration]") {
auto& ioManager = IntraIOManager::getInstance();
auto ioCollector = ioManager.createInstance(uniqueId("receiver"));
auto ioPublisher = ioManager.createInstance(uniqueId("publisher"));
SceneCollector collector;
FrameAllocator allocator;
collector.setup(ioCollector.get());
// Frame 1
{
auto sprite = std::make_unique<JsonDataNode>("sprite");
sprite->setDouble("x", 100.0);
ioPublisher->publish("render:sprite", std::move(sprite));
collector.collect(ioCollector.get(), 0.016f);
FramePacket packet = collector.finalize(allocator);
REQUIRE(packet.spriteCount == 1);
REQUIRE(packet.frameNumber == 1);
collector.clear();
allocator.reset();
}
// Frame 2
{
auto sprite1 = std::make_unique<JsonDataNode>("sprite");
sprite1->setDouble("x", 200.0);
ioPublisher->publish("render:sprite", std::move(sprite1));
auto sprite2 = std::make_unique<JsonDataNode>("sprite");
sprite2->setDouble("x", 300.0);
ioPublisher->publish("render:sprite", std::move(sprite2));
collector.collect(ioCollector.get(), 0.016f);
FramePacket packet = collector.finalize(allocator);
REQUIRE(packet.spriteCount == 2);
REQUIRE(packet.frameNumber == 2);
collector.clear();
allocator.reset();
}
// Frame 3
{
collector.collect(ioCollector.get(), 0.016f);
FramePacket packet = collector.finalize(allocator);
REQUIRE(packet.spriteCount == 0);
REQUIRE(packet.frameNumber == 3);
}
}
// ============================================================================
// Mixed Message Types
// ============================================================================
TEST_CASE("SceneCollector - collect mixed message types", "[scene_collector][integration]") {
auto& ioManager = IntraIOManager::getInstance();
auto ioCollector = ioManager.createInstance(uniqueId("receiver"));
auto ioPublisher = ioManager.createInstance(uniqueId("publisher"));
SceneCollector collector;
FrameAllocator allocator;
collector.setup(ioCollector.get());
// Publish various message types
auto sprite = std::make_unique<JsonDataNode>("sprite");
sprite->setDouble("x", 10.0);
ioPublisher->publish("render:sprite", std::move(sprite));
auto text = std::make_unique<JsonDataNode>("text");
text->setString("text", "Test");
ioPublisher->publish("render:text", std::move(text));
auto particle = std::make_unique<JsonDataNode>("particle");
particle->setDouble("x", 5.0);
ioPublisher->publish("render:particle", std::move(particle));
auto line = std::make_unique<JsonDataNode>("line");
line->setDouble("x1", 0.0);
line->setDouble("y1", 0.0);
line->setDouble("x2", 10.0);
line->setDouble("y2", 10.0);
ioPublisher->publish("render:debug:line", std::move(line));
collector.collect(ioCollector.get(), 0.016f);
FramePacket packet = collector.finalize(allocator);
REQUIRE(packet.spriteCount == 1);
REQUIRE(packet.textCount == 1);
REQUIRE(packet.particleCount == 1);
REQUIRE(packet.debugLineCount == 1);
}