351 lines
15 KiB
C++
351 lines
15 KiB
C++
#include "atlas/AtlasParser.hpp"
|
|
#include "domain/SpineVersion.hpp"
|
|
#include "editor/SpineEditorLocator.hpp"
|
|
#include "editor/SpineOutputParser.hpp"
|
|
#include "editor/SpineVersionAvailability.hpp"
|
|
#include "platform/ProcessRunner.hpp"
|
|
#include "preview/RuntimePlugin.hpp"
|
|
#include "scan/SpineDataVersionDetector.hpp"
|
|
|
|
#include <chrono>
|
|
#include <algorithm>
|
|
#include <cmath>
|
|
#include <cstdlib>
|
|
#include <filesystem>
|
|
#include <fstream>
|
|
#include <iostream>
|
|
#include <string>
|
|
|
|
namespace {
|
|
|
|
int failures = 0;
|
|
|
|
void expect(bool condition, const std::string& message) {
|
|
if (!condition) {
|
|
std::cerr << "FAILED: " << message << '\n';
|
|
++failures;
|
|
}
|
|
}
|
|
|
|
void testVersions() {
|
|
const auto supported = dts::SpineVersion::parse("3.8.99");
|
|
expect(supported.has_value(), "3.8.99 parses");
|
|
expect(supported && supported->line() == "3.8", "version line is 3.8");
|
|
expect(supported && supported->isSupported(), "3.8.99 is supported");
|
|
|
|
const auto early = dts::SpineVersion::parse("3.8.19");
|
|
expect(early && !early->isSupported(), "3.8.19 is outside formal support");
|
|
const auto current = dts::SpineVersion::parse("4.3.6");
|
|
expect(current && current->isSupported(), "4.3 is supported");
|
|
const auto beta = dts::SpineVersion::parse("4.3.75-beta");
|
|
expect(beta && beta->toString() == "4.3.75", "version suffix is accepted");
|
|
const auto old = dts::SpineVersion::parse("3.7.94");
|
|
expect(old && !old->isSupported(), "3.7 is unsupported");
|
|
expect(!dts::SpineVersion::parse("four.two"), "invalid version is rejected");
|
|
}
|
|
|
|
void testLauncherOutput() {
|
|
const std::string output =
|
|
"Spine Launcher 4.3.06 (macOS Apple Silicon)\n"
|
|
"Launching: Spine 3.8.99 Professional\n"
|
|
"Starting: Spine 3.8.99\n";
|
|
dts::ProcessResult process{true, false, 0, output, {}};
|
|
const auto result = dts::SpineOutputParser{}.parse("/Applications/Spine", process);
|
|
expect(result.status == dts::EditorProbeStatus::Available, "probe is available");
|
|
expect(result.launcherVersion && result.launcherVersion->toString() == "4.3.6",
|
|
"launcher version is parsed separately");
|
|
expect(result.editorVersion && result.editorVersion->toString() == "3.8.99",
|
|
"editor version is parsed separately");
|
|
expect(result.edition == "Professional", "edition is parsed");
|
|
}
|
|
|
|
void testTimedOutOutput() {
|
|
dts::ProcessResult process;
|
|
process.started = true;
|
|
process.timedOut = true;
|
|
process.exitCode = 143;
|
|
process.output = "Spine Launcher 4.3.06\nLaunching: Spine 4.2.83 Professional\n";
|
|
const auto result = dts::SpineOutputParser{}.parse("Spine", process);
|
|
expect(result.status == dts::EditorProbeStatus::ObservedButTimedOut,
|
|
"recognized timeout is distinguished from missing Spine");
|
|
}
|
|
|
|
void testMacBundleNormalization() {
|
|
#if defined(__APPLE__)
|
|
const auto path = dts::SpineEditorLocator{}.normalize("/Applications/Spine.app");
|
|
expect(path.has_value(), "macOS .app is normalized");
|
|
expect(path && path->filename() == "Spine", "normalized executable is Spine");
|
|
#endif
|
|
}
|
|
|
|
void testProcessTimeout() {
|
|
#if !defined(_WIN32)
|
|
auto runner = dts::createDefaultProcessRunner();
|
|
dts::ProcessRequest request;
|
|
request.program = "/bin/sleep";
|
|
request.arguments = {"5"};
|
|
request.timeout = std::chrono::milliseconds(50);
|
|
const auto result = runner->run(request);
|
|
expect(result.started, "timeout test process starts");
|
|
expect(result.timedOut, "long-running process is timed out");
|
|
#endif
|
|
}
|
|
|
|
void writeVarint(std::ofstream& output, std::size_t value) {
|
|
while (true) {
|
|
auto byte = static_cast<unsigned char>(value & 0x7f);
|
|
value >>= 7;
|
|
if (value != 0) {
|
|
byte |= 0x80;
|
|
}
|
|
output.put(static_cast<char>(byte));
|
|
if (value == 0) {
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
void writeBinaryString(std::ofstream& output, const std::string& value) {
|
|
writeVarint(output, value.size() + 1);
|
|
output.write(value.data(), static_cast<std::streamsize>(value.size()));
|
|
}
|
|
|
|
void testDataVersionDetection() {
|
|
const auto directory = std::filesystem::temp_directory_path() / "dts-core-tests";
|
|
std::filesystem::create_directories(directory);
|
|
|
|
const auto jsonPath = directory / "skeleton.json";
|
|
{
|
|
std::ofstream output(jsonPath);
|
|
output << R"({"skeleton":{"spine":"4.2.17"},"bones":[{"name":"root"}]})";
|
|
}
|
|
const auto json = dts::SpineDataVersionDetector{}.inspect(jsonPath);
|
|
expect(json.status == dts::VersionDetectionStatus::Detected, "Spine JSON is detected");
|
|
expect(json.version && json.version->toString() == "4.2.17", "JSON version is parsed");
|
|
|
|
const auto binaryPath = directory / "skeleton.skel";
|
|
{
|
|
std::ofstream output(binaryPath, std::ios::binary);
|
|
writeBinaryString(output, "fixture-hash");
|
|
writeBinaryString(output, "3.8.99");
|
|
}
|
|
const auto binary = dts::SpineDataVersionDetector{}.inspect(binaryPath);
|
|
expect(binary.status == dts::VersionDetectionStatus::Detected, "SKEL is detected");
|
|
expect(binary.version && binary.version->toString() == "3.8.99", "SKEL version is parsed");
|
|
|
|
const auto genericPath = directory / "generic.json";
|
|
{
|
|
std::ofstream output(genericPath);
|
|
output << R"({"spine":"4.2.0","items":[]})";
|
|
}
|
|
const auto generic = dts::SpineDataVersionDetector{}.inspect(genericPath);
|
|
expect(generic.status == dts::VersionDetectionStatus::InvalidData,
|
|
"ordinary JSON is not accepted as Spine data");
|
|
std::filesystem::remove_all(directory);
|
|
}
|
|
|
|
void testAtlasParsing() {
|
|
const std::string atlas =
|
|
"hero.webp\n"
|
|
"size: 1024, 512\n"
|
|
"filter: Linear, Linear\n"
|
|
"scale: 0.5\n"
|
|
"pma: true\n"
|
|
"body/arm\n"
|
|
"bounds: 10, 20, 30, 40\n"
|
|
"offsets: 1, 2, 32, 44\n"
|
|
"rotate: 90\n"
|
|
"\n"
|
|
"hero-2.jpg\n"
|
|
"size: 256, 256\n"
|
|
"face\n"
|
|
"xy: 4, 5\n"
|
|
"size: 20, 30\n"
|
|
"orig: 22, 34\n";
|
|
const auto result = dts::AtlasParser{}.parseText(atlas);
|
|
expect(result.document.has_value(), "multi-page Atlas parses");
|
|
expect(result.document && result.document->pages.size() == 2, "two pages are found");
|
|
const auto& first = result.document->pages.front();
|
|
expect(first.imagePath == "hero.webp", "WebP page path is retained");
|
|
expect(first.premultipliedAlpha == true, "PMA is parsed");
|
|
expect(first.scale && std::abs(*first.scale - 0.5) < 0.000001,
|
|
"Atlas page scale is parsed");
|
|
expect(first.regions.size() == 1 && first.regions.front().rotated,
|
|
"rotated region is parsed");
|
|
const auto& second = result.document->pages.back();
|
|
expect(second.regions.size() == 1 && second.regions.front().size.size() == 2,
|
|
"legacy region size is parsed");
|
|
}
|
|
|
|
void testInstalledVersionDiscovery() {
|
|
const auto directory = std::filesystem::temp_directory_path() / "dts-version-cache-tests";
|
|
std::filesystem::create_directories(directory);
|
|
for (const auto* name : {"4.3.18", "4.3.23", "4.2.43", "not-a-version"}) {
|
|
std::ofstream output(directory / name, std::ios::binary);
|
|
output.put('x');
|
|
}
|
|
dts::SpineVersionAvailability availability;
|
|
const auto selected = availability.newestForLine("4.3", {directory});
|
|
expect(selected && selected->toString() == "4.3.23",
|
|
"newest locally cached patch is selected for a version line");
|
|
expect(selected && availability.isCached(*selected, {directory}),
|
|
"selected exact editor version is confirmed in local cache");
|
|
expect(!availability.newestForLine("4.1", {directory}),
|
|
"missing cached version line is not reported as available");
|
|
std::filesystem::remove_all(directory);
|
|
}
|
|
|
|
void expectRuntimePlugin(const std::string& path, const std::string& versionLine) {
|
|
dts::RuntimePlugin plugin;
|
|
std::string error;
|
|
expect(plugin.load(path, error), versionLine + " Runtime plugin loads: " + error);
|
|
const auto metadata = plugin.metadata(error);
|
|
expect(metadata.has_value(), "Runtime metadata is available: " + error);
|
|
expect(metadata && metadata->apiVersion == DTS_RUNTIME_API_VERSION,
|
|
"Runtime API version matches");
|
|
expect(metadata && metadata->spineVersionLine == versionLine,
|
|
"Runtime line is " + versionLine);
|
|
}
|
|
|
|
void testRuntimePlugins() {
|
|
#if defined(DTS_RUNTIME_38_PATH)
|
|
expectRuntimePlugin(DTS_RUNTIME_38_PATH, "3.8");
|
|
expectRuntimePlugin(DTS_RUNTIME_40_PATH, "4.0");
|
|
expectRuntimePlugin(DTS_RUNTIME_41_PATH, "4.1");
|
|
expectRuntimePlugin(DTS_RUNTIME_42_PATH, "4.2");
|
|
expectRuntimePlugin(DTS_RUNTIME_43_PATH, "4.3");
|
|
#endif
|
|
}
|
|
|
|
void testRuntime43OfficialAssets() {
|
|
#if defined(DTS_TEST_SPINEBOY_JSON)
|
|
const auto jsonVersion = dts::SpineDataVersionDetector{}.inspect(DTS_TEST_SPINEBOY_JSON);
|
|
expect(jsonVersion.status == dts::VersionDetectionStatus::Detected,
|
|
"official 4.3 beta JSON version is detected");
|
|
const auto binaryVersion = dts::SpineDataVersionDetector{}.inspect(DTS_TEST_SPINEBOY_SKEL);
|
|
expect(binaryVersion.status == dts::VersionDetectionStatus::Detected,
|
|
"official 4.3 binary header version is detected");
|
|
dts::RuntimePlugin plugin;
|
|
std::string error;
|
|
expect(plugin.load(DTS_RUNTIME_43_PATH, error), "4.3 Runtime loads for official assets");
|
|
const auto json = plugin.loadSkeleton(
|
|
DTS_TEST_SPINEBOY_JSON, DTS_TEST_SPINEBOY_ATLAS, 1.0F, error);
|
|
expect(json.has_value(), "official Spineboy JSON loads: " + error);
|
|
expect(json && json->boneCount > 0, "Spineboy JSON contains bones");
|
|
expect(json && !json->animations.empty(), "Spineboy JSON contains animations");
|
|
if (json && !json->animations.empty()) {
|
|
const auto idle = std::find_if(json->animations.begin(), json->animations.end(),
|
|
[](const auto& animation) { return animation.name == "idle"; });
|
|
const auto& animated = idle == json->animations.end()
|
|
? json->animations.front() : *idle;
|
|
expect(plugin.setAnimation(animated.name, true, error),
|
|
"Spineboy animation can be selected: " + error);
|
|
const auto frame = plugin.updatePreview(1.0F / 60.0F, error);
|
|
expect(frame.has_value(), "Spineboy preview frame is sampled: " + error);
|
|
expect(frame && !frame->empty(), "Spineboy preview contains draw commands");
|
|
if (frame && !frame->empty()) {
|
|
expect(!frame->front().vertices.empty(), "draw command contains vertices");
|
|
expect(!frame->front().indices.empty(), "draw command contains triangles");
|
|
expect(std::filesystem::exists(frame->front().texturePath),
|
|
"draw command texture exists");
|
|
}
|
|
const auto laterFrame = plugin.updatePreview(0.25F, error);
|
|
bool moved = false;
|
|
if (frame && laterFrame && frame->size() == laterFrame->size()) {
|
|
for (std::size_t command = 0; command < frame->size() && !moved; ++command) {
|
|
const auto& before = (*frame)[command].vertices;
|
|
const auto& after = (*laterFrame)[command].vertices;
|
|
for (std::size_t vertex = 0; vertex < std::min(before.size(), after.size()); ++vertex) {
|
|
if (std::abs(before[vertex].x - after[vertex].x) > 0.001F
|
|
|| std::abs(before[vertex].y - after[vertex].y) > 0.001F) {
|
|
moved = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
expect(moved, "animation sampling changes vertex positions over time");
|
|
}
|
|
|
|
error.clear();
|
|
const auto binary = plugin.loadSkeleton(
|
|
DTS_TEST_SPINEBOY_SKEL, DTS_TEST_SPINEBOY_ATLAS, 1.0F, error);
|
|
expect(binary.has_value(), "official Spineboy SKEL loads: " + error);
|
|
expect(binary && binary->boneCount > 0, "Spineboy SKEL contains bones");
|
|
expect(binary && !binary->animations.empty(), "Spineboy SKEL contains animations");
|
|
#endif
|
|
}
|
|
|
|
void testRuntime42OfficialAssets() {
|
|
#if defined(DTS_TEST_42_JSON)
|
|
dts::RuntimePlugin plugin;
|
|
std::string error;
|
|
expect(plugin.load(DTS_RUNTIME_42_PATH, error), "4.2 Runtime loads for official assets");
|
|
const auto json = plugin.loadSkeleton(DTS_TEST_42_JSON, DTS_TEST_42_ATLAS, 1.0F, error);
|
|
expect(json.has_value(), "official 4.2 Spineboy JSON loads: " + error);
|
|
expect(json && !json->animations.empty(), "official 4.2 JSON contains animations");
|
|
if (json && !json->animations.empty()) {
|
|
expect(plugin.setAnimation(json->animations.front().name, true, error),
|
|
"4.2 animation can be selected: " + error);
|
|
const auto frame = plugin.updatePreview(1.0F / 60.0F, error);
|
|
expect(frame && !frame->empty(), "4.2 preview produces draw commands: " + error);
|
|
}
|
|
error.clear();
|
|
const auto binary = plugin.loadSkeleton(DTS_TEST_42_SKEL, DTS_TEST_42_ATLAS, 1.0F, error);
|
|
expect(binary.has_value(), "official 4.2 Spineboy SKEL loads: " + error);
|
|
#endif
|
|
}
|
|
|
|
void testLegacyOfficialAsset(const char* line, const char* runtimePath,
|
|
const char* jsonPath, const char* binaryPath, const char* atlasPath) {
|
|
dts::RuntimePlugin plugin;
|
|
std::string error;
|
|
expect(plugin.load(runtimePath, error), std::string(line) + " Runtime loads for assets");
|
|
const auto json = plugin.loadSkeleton(jsonPath, atlasPath, 1.0F, error);
|
|
expect(json.has_value(), std::string("official ") + line + " JSON loads: " + error);
|
|
expect(json && !json->animations.empty(), std::string(line) + " JSON contains animations");
|
|
if (json && !json->animations.empty()) {
|
|
expect(plugin.setAnimation(json->animations.front().name, true, error),
|
|
std::string(line) + " animation can be selected: " + error);
|
|
const auto first = plugin.updatePreview(1.0F / 60.0F, error);
|
|
expect(first && !first->empty(), std::string(line) + " preview draws: " + error);
|
|
const auto later = plugin.updatePreview(0.25F, error);
|
|
expect(later && !later->empty(), std::string(line) + " later frame draws: " + error);
|
|
}
|
|
error.clear();
|
|
const auto binary = plugin.loadSkeleton(binaryPath, atlasPath, 1.0F, error);
|
|
expect(binary.has_value(), std::string("official ") + line + " SKEL loads: " + error);
|
|
}
|
|
|
|
void testLegacyOfficialAssets() {
|
|
#if defined(DTS_TEST_38_JSON)
|
|
testLegacyOfficialAsset("3.8", DTS_RUNTIME_38_PATH,
|
|
DTS_TEST_38_JSON, DTS_TEST_38_SKEL, DTS_TEST_38_ATLAS);
|
|
testLegacyOfficialAsset("4.0", DTS_RUNTIME_40_PATH,
|
|
DTS_TEST_40_JSON, DTS_TEST_40_SKEL, DTS_TEST_40_ATLAS);
|
|
testLegacyOfficialAsset("4.1", DTS_RUNTIME_41_PATH,
|
|
DTS_TEST_41_JSON, DTS_TEST_41_SKEL, DTS_TEST_41_ATLAS);
|
|
#endif
|
|
}
|
|
|
|
} // namespace
|
|
|
|
int main() {
|
|
testVersions();
|
|
testLauncherOutput();
|
|
testTimedOutOutput();
|
|
testMacBundleNormalization();
|
|
testProcessTimeout();
|
|
testDataVersionDetection();
|
|
testAtlasParsing();
|
|
testInstalledVersionDiscovery();
|
|
testRuntimePlugins();
|
|
testLegacyOfficialAssets();
|
|
testRuntime42OfficialAssets();
|
|
testRuntime43OfficialAssets();
|
|
if (failures == 0) {
|
|
std::cout << "All core tests passed.\n";
|
|
}
|
|
return failures == 0 ? EXIT_SUCCESS : EXIT_FAILURE;
|
|
}
|