#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 #include #include #include #include #include #include #include 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(value & 0x7f); value >>= 7; if (value != 0) { byte |= 0x80; } output.put(static_cast(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(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; }