MetaCore/tests/MetaCoreAssetPipelineTests.cpp
2026-07-14 17:41:55 +08:00

166 lines
9.1 KiB
C++

#include "MetaCoreEditor/MetaCoreBuiltinModules.h"
#include "MetaCoreEditor/MetaCoreEditorServices.h"
#include <nlohmann/json.hpp>
#include <algorithm>
#include <chrono>
#include <cstddef>
#include <cstdint>
#include <cstdlib>
#include <filesystem>
#include <fstream>
#include <iostream>
#include <stdexcept>
#include <thread>
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<char>(pixel * 3), static_cast<char>(128), static_cast<char>(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<const char*>(positions), sizeof(positions));
buffer.write(reinterpret_cast<const char*>(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<MetaCore::MetaCoreIAssetDatabaseService>();
const auto packages = registry.ResolveService<MetaCore::MetaCoreIPackageService>();
const auto imports = registry.ResolveService<MetaCore::MetaCoreIImportPipelineService>();
const auto audit = registry.ResolveService<MetaCore::MetaCoreIAssetAuditService>();
const auto thumbnails = registry.ResolveService<MetaCore::MetaCoreIThumbnailService>();
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<unsigned char>(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;
}
}