166 lines
9.1 KiB
C++
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;
|
|
}
|
|
}
|