#include "MetaCoreEditor/MetaCoreBuiltinModules.h" #include "MetaCoreEditor/MetaCoreEditorServices.h" #include #include #include #include #include #include #include #include #include #include #include namespace { void Expect(bool condition, const char* message) { if (!condition) throw std::runtime_error(message); } void WriteProject(const std::filesystem::path& root) { std::filesystem::create_directories(root / "Assets"); std::filesystem::create_directories(root / "Scenes"); std::ofstream project(root / "MetaCore.project.json"); project << R"({"name":"AssetGolden","version":"1.0","scenes":["Scenes/Main.mcscene.json"],"startup_scene":"Scenes/Main.mcscene.json"})"; } void WriteGoldenInputs(const std::filesystem::path& root) { std::ofstream texture(root / "Assets/Color.ppm", std::ios::binary); texture << "P6\n8 8\n255\n"; for (int pixel = 0; pixel < 64; ++pixel) { const char rgb[3]{static_cast(pixel * 3), static_cast(128), static_cast(255 - pixel * 3)}; texture.write(rgb, sizeof(rgb)); } std::ofstream(root / "Assets/Broken.glb", std::ios::binary) << "not-a-glb"; std::ofstream(root / "Assets/Unsupported.obj") << "o Unsupported\n"; std::ofstream(root / "Assets/Unused.mcmaterial.json") << R"({"BaseColor":[0.4,0.5,0.6]})"; { std::ofstream buffer(root / "Assets/Triangle.bin", std::ios::binary); const float positions[]{-1.0F, -1.0F, 0.0F, 1.0F, -1.0F, 0.0F, 0.0F, 1.0F, 0.0F}; const std::uint16_t indices[]{0, 1, 2}; buffer.write(reinterpret_cast(positions), sizeof(positions)); buffer.write(reinterpret_cast(indices), sizeof(indices)); } std::ofstream(root / "Assets/Triangle.gltf") << R"({ "asset":{"version":"2.0"},"scene":0,"scenes":[{"nodes":[0]}], "nodes":[{"mesh":0}],"meshes":[{"primitives":[{"attributes":{"POSITION":0},"indices":1}]}], "buffers":[{"uri":"Triangle.bin","byteLength":42}], "bufferViews":[{"buffer":0,"byteOffset":0,"byteLength":36},{"buffer":0,"byteOffset":36,"byteLength":6}], "accessors":[{"bufferView":0,"componentType":5126,"count":3,"type":"VEC3"},{"bufferView":1,"componentType":5123,"count":3,"type":"SCALAR"}] })"; std::ofstream(root / "Scenes/Main.mcscene.json") << R"({"name":"Main","objects":[]})"; } } // namespace int main() { const auto root = std::filesystem::temp_directory_path() / "MetaCoreAssetPipelineGolden"; std::filesystem::remove_all(root); WriteProject(root); WriteGoldenInputs(root); setenv("METACORE_PROJECT_PATH", root.string().c_str(), 1); setenv("METACORE_SYNCHRONOUS_ASSET_BOOTSTRAP", "1", 1); try { MetaCore::MetaCoreEditorModuleRegistry registry; auto module = MetaCore::MetaCoreCreateBuiltinCoreServicesModule(); module->Startup(registry); const auto database = registry.ResolveService(); const auto packages = registry.ResolveService(); const auto imports = registry.ResolveService(); const auto audit = registry.ResolveService(); const auto thumbnails = registry.ResolveService(); Expect(database != nullptr && packages != nullptr && imports != nullptr && audit != nullptr && thumbnails != nullptr, "R2 services must resolve"); Expect( !std::filesystem::exists(root / "Assets/Unused.mcmaterial.json.mcasset"), "native JSON material must not generate a recursive .mcasset package" ); Expect(std::none_of(database->GetAssetRegistry().begin(), database->GetAssetRegistry().end(), [&](const auto& record) { if (record.RelativePath.extension() != ".mcasset") return false; auto source = root / record.RelativePath; source.replace_extension(); return std::filesystem::is_regular_file(source); }), "derived packages must not be registered as duplicate source assets"); const auto texture = database->FindAssetByRelativePath("Assets/Color.ppm"); Expect(texture.has_value(), "golden texture must be registered"); Expect(texture->ImportResult != MetaCore::MetaCoreModelImportResult::Error, "golden texture import must succeed"); const auto texturePackage = packages->ReadPackage(root / texture->PackagePath); Expect(texturePackage.has_value() && texturePackage->PayloadSections.size() > 1, "texture package must contain KTX2 payload"); const auto& bytes = texturePackage->PayloadSections[1]; constexpr unsigned char signature[]{0xAB, 'K', 'T', 'X', ' ', '2', '0', 0xBB, 0x0D, 0x0A, 0x1A, 0x0A}; Expect(bytes.size() >= sizeof(signature), "KTX2 payload must contain identifier"); for (std::size_t index = 0; index < sizeof(signature); ++index) { Expect(std::to_integer(bytes[index]) == signature[index], "texture payload must be KTX2"); } const auto broken = database->FindAssetByRelativePath("Assets/Broken.glb"); const auto unsupported = database->FindAssetByRelativePath("Assets/Unsupported.obj"); Expect(broken.has_value() && broken->ImportResult == MetaCore::MetaCoreModelImportResult::Error, "broken GLB must fail"); Expect(unsupported.has_value() && unsupported->ImportResult == MetaCore::MetaCoreModelImportResult::Error, "OBJ must be unsupported"); Expect(!std::filesystem::exists(root / "Assets/Broken.glb.mcasset"), "broken GLB must not create placeholder package"); Expect(!std::filesystem::exists(root / "Assets/Unsupported.obj.mcasset"), "OBJ must not create placeholder package"); const std::size_t jobCountBeforeNoOpRefresh = imports->GetJobs().size(); Expect(database->Refresh(), "no-op refresh must succeed"); std::this_thread::sleep_for(std::chrono::milliseconds(50)); Expect( imports->GetJobs().size() == jobCountBeforeNoOpRefresh, "failed unsupported assets must not be enqueued forever just because no package exists" ); const auto model = database->FindAssetByRelativePath("Assets/Triangle.gltf"); Expect(model.has_value() && model->ImportResult != MetaCore::MetaCoreModelImportResult::Error, "golden glTF must import"); const auto modelPackage = packages->ReadPackage(root / model->PackagePath); Expect(modelPackage.has_value() && modelPackage->PayloadSections.size() > 1, "model package must persist mesh payloads"); std::filesystem::path modelThumbnail; for (int attempt = 0; attempt < 100 && modelThumbnail.empty(); ++attempt) { modelThumbnail = thumbnails->RequestThumbnail(*model, 96); if (modelThumbnail.empty()) std::this_thread::sleep_for(std::chrono::milliseconds(10)); } Expect(!modelThumbnail.empty() && std::filesystem::file_size(modelThumbnail) > 100, "model geometry thumbnail must be generated"); const auto oversizedModelPath = root / "Assets/Oversized.glb"; std::ofstream(oversizedModelPath, std::ios::binary).put('\0'); std::filesystem::resize_file(oversizedModelPath, 64ULL * 1024ULL * 1024ULL + 1ULL); auto oversizedModel = *model; oversizedModel.Guid = MetaCore::MetaCoreAssetGuid::Generate(); oversizedModel.RelativePath = "Assets/Oversized.glb"; oversizedModel.SourcePath = oversizedModel.RelativePath; oversizedModel.ArtifactKey = "oversized-model-thumbnail-fallback"; std::filesystem::path oversizedThumbnail; for (int attempt = 0; attempt < 100 && oversizedThumbnail.empty(); ++attempt) { oversizedThumbnail = thumbnails->RequestThumbnail(oversizedModel, 96); if (oversizedThumbnail.empty()) std::this_thread::sleep_for(std::chrono::milliseconds(10)); } Expect( !oversizedThumbnail.empty() && std::filesystem::file_size(oversizedThumbnail) > 100, "oversized models must use a bounded fallback thumbnail instead of parsing the full source" ); std::filesystem::remove(oversizedModelPath); const auto findings = audit->AuditAll(); Expect(std::any_of(findings.begin(), findings.end(), [](const auto& finding) { return finding.Status == MetaCore::MetaCoreAssetReferenceStatus::BuildUnreachable; }), "audit must report build-unreachable assets"); module->Shutdown(registry); registry.ShutdownServices(); unsetenv("METACORE_PROJECT_PATH"); unsetenv("METACORE_SYNCHRONOUS_ASSET_BOOTSTRAP"); std::filesystem::remove_all(root); std::cout << "MetaCoreAssetPipelineTests passed\n"; return 0; } catch (const std::exception& exception) { unsetenv("METACORE_PROJECT_PATH"); unsetenv("METACORE_SYNCHRONOUS_ASSET_BOOTSTRAP"); std::filesystem::remove_all(root); std::cerr << "MetaCoreAssetPipelineTests failed: " << exception.what() << '\n'; return 1; } }