Files
video-shader-toys/tests/ShaderPackageRegistryTests.cpp
Aiden a526887ff6
Some checks failed
CI / Native Windows Build And Tests (push) Has been cancelled
CI / React UI Build (push) Has been cancelled
CI / Windows Release Package (push) Has been cancelled
temporal manifest tests
2026-05-06 11:34:53 +10:00

216 lines
7.4 KiB
C++

#include "ShaderPackageRegistry.h"
#include <chrono>
#include <filesystem>
#include <fstream>
#include <iostream>
#include <map>
#include <string>
#include <vector>
namespace
{
int gFailures = 0;
void Expect(bool condition, const char* message)
{
if (condition)
return;
std::cerr << "FAIL: " << message << "\n";
++gFailures;
}
std::filesystem::path MakeTestRoot()
{
const auto stamp = std::chrono::steady_clock::now().time_since_epoch().count();
std::filesystem::path root = std::filesystem::temp_directory_path() / ("video-shader-registry-tests-" + std::to_string(stamp));
std::filesystem::create_directories(root);
return root;
}
void WriteFile(const std::filesystem::path& path, const std::string& contents)
{
std::filesystem::create_directories(path.parent_path());
std::ofstream output(path, std::ios::binary);
output << contents;
}
void WriteShaderPackage(const std::filesystem::path& root, const std::string& directoryName, const std::string& manifest)
{
const std::filesystem::path packageRoot = root / directoryName;
WriteFile(packageRoot / "shader.json", manifest);
WriteFile(packageRoot / "shader.slang", "float4 shadeVideo(float2 uv) { return float4(uv, 0.0, 1.0); }\n");
}
void TestValidManifest()
{
const std::filesystem::path root = MakeTestRoot();
WriteFile(root / "look" / "mask.png", "not a real png, but enough for existence checks");
WriteFile(root / "look" / "Inter.ttf", "not a real font, but enough for existence checks");
WriteShaderPackage(root, "look", R"({
"id": "look-01",
"name": "Look 01",
"description": "Test package",
"category": "Tests",
"entryPoint": "shadeVideo",
"textures": [{ "id": "maskTex", "path": "mask.png" }],
"fonts": [{ "id": "inter", "path": "Inter.ttf" }],
"temporal": { "enabled": true, "historySource": "source", "historyLength": 8 },
"parameters": [
{ "id": "gain", "label": "Gain", "type": "float", "default": 0.5, "min": 0, "max": 1 },
{ "id": "titleText", "label": "Title", "type": "text", "default": "LIVE", "font": "inter", "maxLength": 32 },
{ "id": "mode", "label": "Mode", "type": "enum", "default": "soft", "options": [
{ "value": "soft", "label": "Soft" },
{ "value": "hard", "label": "Hard" }
] }
]
})");
ShaderPackageRegistry registry(4);
ShaderPackage package;
std::string error;
Expect(registry.ParseManifest(root / "look" / "shader.json", package, error), "valid manifest parses");
Expect(package.id == "look-01", "manifest id is preserved");
Expect(package.textureAssets.size() == 1 && package.textureAssets[0].id == "maskTex", "texture assets parse");
Expect(package.fontAssets.size() == 1 && package.fontAssets[0].id == "inter", "font assets parse");
Expect(package.temporal.enabled && package.temporal.effectiveHistoryLength == 4, "temporal history is capped");
Expect(package.parameters.size() == 3, "parameters parse");
Expect(package.parameters[1].type == ShaderParameterType::Text && package.parameters[1].defaultTextValue == "LIVE", "text parameter parses");
std::filesystem::remove_all(root);
}
void TestMissingFontAsset()
{
const std::filesystem::path root = MakeTestRoot();
WriteShaderPackage(root, "bad-font", R"({
"id": "bad-font",
"name": "Bad Font",
"fonts": [{ "id": "missingFont", "path": "missing.ttf" }],
"parameters": []
})");
ShaderPackageRegistry registry(4);
ShaderPackage package;
std::string error;
Expect(!registry.ParseManifest(root / "bad-font" / "shader.json", package, error), "missing font asset is rejected");
Expect(error.find("font asset not found") != std::string::npos, "missing font error is clear");
std::filesystem::remove_all(root);
}
void TestInvalidManifest()
{
const std::filesystem::path root = MakeTestRoot();
WriteShaderPackage(root, "bad", R"({
"id": "bad",
"name": "Bad",
"entryPoint": "not-valid!",
"parameters": []
})");
ShaderPackageRegistry registry(4);
ShaderPackage package;
std::string error;
Expect(!registry.ParseManifest(root / "bad" / "shader.json", package, error), "invalid shader identifier is rejected");
Expect(error.find("entryPoint") != std::string::npos, "invalid manifest error names the bad field");
std::filesystem::remove_all(root);
}
void TestInvalidTemporalSettings()
{
struct Case
{
const char* directoryName;
const char* temporalJson;
const char* expectedError;
};
const Case cases[] = {
{ "bad-source", R"({ "enabled": true, "historySource": "previousOutput", "historyLength": 2 })", "Unsupported temporal historySource" },
{ "missing-source", R"({ "enabled": true, "historyLength": 2 })", "historySource" },
{ "missing-length", R"({ "enabled": true, "historySource": "source" })", "historyLength" },
{ "string-length", R"({ "enabled": true, "historySource": "source", "historyLength": "2" })", "historyLength" },
{ "zero-length", R"({ "enabled": true, "historySource": "source", "historyLength": 0 })", "positive integer" },
{ "negative-length", R"({ "enabled": true, "historySource": "source", "historyLength": -1 })", "positive integer" },
{ "fractional-length", R"({ "enabled": true, "historySource": "source", "historyLength": 1.5 })", "positive integer" },
};
const std::filesystem::path root = MakeTestRoot();
for (const Case& testCase : cases)
{
WriteShaderPackage(root, testCase.directoryName, std::string(R"({
"id": ")") + testCase.directoryName + R"(",
"name": "Bad Temporal",
"temporal": )" + testCase.temporalJson + R"(,
"parameters": []
})");
ShaderPackageRegistry registry(4);
ShaderPackage package;
std::string error;
Expect(!registry.ParseManifest(root / testCase.directoryName / "shader.json", package, error), "invalid temporal manifest is rejected");
Expect(error.find(testCase.expectedError) != std::string::npos, "invalid temporal error explains the rejected field");
}
std::filesystem::remove_all(root);
}
void TestDisabledTemporalSettingsAreIgnored()
{
const std::filesystem::path root = MakeTestRoot();
WriteShaderPackage(root, "disabled-temporal", R"({
"id": "disabled-temporal",
"name": "Disabled Temporal",
"temporal": { "enabled": false, "historySource": "not-supported", "historyLength": 0 },
"parameters": []
})");
ShaderPackageRegistry registry(4);
ShaderPackage package;
std::string error;
Expect(registry.ParseManifest(root / "disabled-temporal" / "shader.json", package, error), "disabled temporal settings are ignored");
Expect(!package.temporal.enabled, "disabled temporal package stays non-temporal");
Expect(package.temporal.effectiveHistoryLength == 0, "disabled temporal package has no effective history");
std::filesystem::remove_all(root);
}
void TestDuplicateScan()
{
const std::filesystem::path root = MakeTestRoot();
WriteShaderPackage(root, "a", R"({ "id": "dupe", "name": "A", "parameters": [] })");
WriteShaderPackage(root, "b", R"({ "id": "dupe", "name": "B", "parameters": [] })");
ShaderPackageRegistry registry(4);
std::map<std::string, ShaderPackage> packages;
std::vector<std::string> order;
std::string error;
Expect(!registry.Scan(root, packages, order, error), "duplicate package ids are rejected");
Expect(error.find("Duplicate shader id") != std::string::npos, "duplicate scan error is clear");
std::filesystem::remove_all(root);
}
}
int main()
{
TestValidManifest();
TestMissingFontAsset();
TestInvalidManifest();
TestInvalidTemporalSettings();
TestDisabledTemporalSettingsAreIgnored();
TestDuplicateScan();
if (gFailures != 0)
{
std::cerr << gFailures << " ShaderPackageRegistry test failure(s).\n";
return 1;
}
std::cout << "ShaderPackageRegistry tests passed.\n";
return 0;
}