加入5套spine官方运行时

This commit is contained in:
tianmo
2026-09-07 21:40:29 +08:00
parent 98e51a259e
commit 6837ecada9
935 changed files with 204665 additions and 8 deletions
@@ -0,0 +1,303 @@
/******************************************************************************
* Spine Runtimes License Agreement
* Last updated April 5, 2025. Replaces all prior versions.
*
* Copyright (c) 2013-2025, Esoteric Software LLC
*
* Integration of the Spine Runtimes into software or otherwise creating
* derivative works of the Spine Runtimes is permitted under the terms and
* conditions of Section 2 of the Spine Editor License Agreement:
* http://esotericsoftware.com/spine-editor-license
*
* Otherwise, it is permitted to integrate the Spine Runtimes into software
* or otherwise create derivative works of the Spine Runtimes (collectively,
* "Products"), provided that each user of the Products must obtain their own
* Spine Editor license and redistribution of the Products in any form must
* include this license and copyright notice.
*
* THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
* BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
* THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*****************************************************************************/
#include <spine/spine.h>
#include <spine/InheritTimeline.h>
#include "SkeletonSerializer.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdarg.h>
#include <locale.h>
#include <math.h>
static void printBonePoseJson(spine::BonePose &pose) {
printf("{");
printf("\"x\":%.6f,\"y\":%.6f,\"rotation\":%.6f,\"scaleX\":%.6f,\"scaleY\":%.6f,\"shearX\":%.6f,\"shearY\":%.6f,", pose.getX(), pose.getY(),
pose.getRotation(), pose.getScaleX(), pose.getScaleY(), pose.getShearX(), pose.getShearY());
printf("\"a\":%.6f,\"b\":%.6f,\"c\":%.6f,\"d\":%.6f,", pose.getA(), pose.getB(), pose.getC(), pose.getD());
printf("\"worldX\":%.6f,\"worldY\":%.6f,\"worldRotationX\":%.6f,\"worldRotationY\":%.6f,\"worldScaleX\":%.6f,\"worldScaleY\":%.6f",
pose.getWorldX(), pose.getWorldY(), pose.getWorldRotationX(), pose.getWorldRotationY(), pose.getWorldScaleX(), pose.getWorldScaleY());
printf("}");
}
static void printSlotPoseJson(spine::SlotPose &pose) {
printf("{");
printf("\"attachment\":");
spine::Attachment *attachment = pose.getAttachment();
if (attachment)
printf("\"%s\"", attachment->getName().buffer());
else
printf("null");
printf(",\"sequenceIndex\":%d,", pose.getSequenceIndex());
spine::Color &color = pose.getColor();
printf("\"color\":{\"r\":%.6f,\"g\":%.6f,\"b\":%.6f,\"a\":%.6f}", color.r, color.g, color.b, color.a);
printf("}");
}
static void printSkeletonFrameJson(spine::Skeleton &skeleton, int frame) {
printf("\n=== ANIMATION FRAME %d ===\n", frame);
printf("{");
printf("\"frame\":%d,", frame);
printf("\"time\":%.6f,", skeleton.getTime());
printf("\"bones\":[");
spine::Array<spine::Bone *> &bones = skeleton.getBones();
for (size_t i = 0; i < bones.size(); i++) {
if (i) printf(",");
spine::Bone *bone = bones[i];
printf("{");
printf("\"name\":\"%s\",", bone->getData().getName().buffer());
printf("\"pose\":");
printBonePoseJson(bone->getPose());
printf(",\"appliedPose\":");
printBonePoseJson(bone->getAppliedPose());
printf("}");
}
printf("],\"slots\":[");
spine::Array<spine::Slot *> &slots = skeleton.getSlots();
for (size_t i = 0; i < slots.size(); i++) {
if (i) printf(",");
spine::Slot *slot = slots[i];
printf("{");
printf("\"name\":\"%s\",", slot->getData().getName().buffer());
printf("\"pose\":");
printSlotPoseJson(slot->getPose());
printf(",\"appliedPose\":");
printSlotPoseJson(slot->getAppliedPose());
printf("}");
}
printf("]}");
}
using namespace spine;
// Provide the default extension implementation
namespace spine {
SpineExtension *getDefaultExtension() {
return new DefaultSpineExtension();
}
}// namespace spine
// Mock texture that doesn't require OpenGL
class MockTexture {
public:
int width = 1024;
int height = 1024;
};
// Custom texture loader that doesn't load actual textures
class HeadlessTextureLoader : public TextureLoader {
public:
virtual void load(AtlasPage &page, const String &path) override {
// Don't load actual texture, just set dimensions
page.texture = new MockTexture();
page.width = 1024;
page.height = 1024;
}
virtual void unload(void *texture) override {
delete static_cast<MockTexture *>(texture);
}
};
static bool testInheritTimelineBoneIndex() {
InheritTimeline timeline(1, 123);
if (timeline.getBoneIndex() != 123) {
fprintf(stderr, "InheritTimeline bone index was not initialized correctly\n");
return false;
}
return true;
}
int main(int argc, char *argv[]) {
// Set locale to ensure consistent number formatting
setlocale(LC_ALL, "C");
if (!testInheritTimelineBoneIndex()) return 1;
if (argc < 3) {
fprintf(stderr, "Usage: HeadlessTest <skeleton-path> <atlas-path> [animation-name] [animation-name-2]\n");
return 1;
}
Bone::setYDown(false);
const char *skeletonPath = argv[1];
const char *atlasPath = argv[2];
const char *animationName = argc >= 4 ? argv[3] : nullptr;
const char *animationName2 = argc >= 5 ? argv[4] : nullptr;
// Load atlas with headless texture loader
HeadlessTextureLoader textureLoader;
Atlas *atlas = new Atlas(atlasPath, &textureLoader);
// Load skeleton data
SkeletonData *skeletonData = nullptr;
if (strstr(skeletonPath, ".json") != nullptr) {
SkeletonJson json(*atlas);
skeletonData = json.readSkeletonDataFile(skeletonPath);
} else {
SkeletonBinary binary(*atlas);
skeletonData = binary.readSkeletonDataFile(skeletonPath);
}
if (!skeletonData) {
fprintf(stderr, "Failed to load skeleton data\n");
delete atlas;
return 1;
}
// Create skeleton instance
Skeleton skeleton(*skeletonData);
// Set animation if provided
AnimationState *state = nullptr;
AnimationStateData *stateData = nullptr;
if (animationName != nullptr) {
// Create animation state only when needed
stateData = new AnimationStateData(*skeletonData);
state = new AnimationState(*stateData);
// Find and set animation
Animation *animation = skeletonData->findAnimation(animationName);
if (!animation) {
fprintf(stderr, "Animation not found: %s\n", animationName);
delete skeletonData;
delete atlas;
return 1;
}
state->setAnimation(0, *animation, true);
// Update and apply
state->update(0.016f);
state->apply(skeleton);
}
skeleton.updateWorldTransform(Physics_Update);
// Use SkeletonSerializer for JSON output
SkeletonSerializer serializer;
// Print skeleton data
printf("=== SKELETON DATA ===\n");
printf("%s", serializer.serializeSkeletonData(skeletonData).buffer());
// Print skeleton state
printf("\n=== SKELETON STATE ===\n");
printf("%s", serializer.serializeSkeleton(&skeleton).buffer());
// Print animation state only if animation was loaded
if (state != nullptr) {
printf("\n=== ANIMATION STATE ===\n");
printf("%s", serializer.serializeAnimationState(state).buffer());
}
// Full animation sampling: if a single animation is provided, sample skeleton state for every
// frame across the full non-looping animation duration at 60 FPS.
if (state != nullptr && animationName2 == nullptr) {
Animation *animation = skeletonData->findAnimation(animationName);
if (!animation) {
fprintf(stderr, "Animation not found: %s\n", animationName);
delete state;
delete stateData;
delete skeletonData;
delete atlas;
return 1;
}
skeleton.setupPose();
state->clearTracks();
state->setAnimation(0, *animation, false);
state->apply(skeleton);
skeleton.updateWorldTransform(Physics_Update);
int frameCount = (int) ceilf(animation->getDuration() * 60.0f);
for (int i = 0; i <= frameCount; i++) {
if (i > 0) {
state->update(1 / 60.0f);
state->apply(skeleton);
skeleton.updateWorldTransform(Physics_Update);
}
printSkeletonFrameJson(skeleton, i);
}
}
// Transition test: if a second animation is provided, play A for 10 frames, transition to B,
// then sample skeleton state at frames 5, 10, 15, 20 during the mix.
if (state != nullptr && animationName2 != nullptr) {
Animation *animation2 = skeletonData->findAnimation(animationName2);
if (!animation2) {
fprintf(stderr, "Animation not found: %s\n", animationName2);
delete state;
delete stateData;
delete skeletonData;
delete atlas;
return 1;
}
// Reset skeleton and state
skeleton.setupPose();
state->clearTracks();
state->setAnimation(0, *skeletonData->findAnimation(animationName), true);
// Run 10 frames of animation A
for (int i = 0; i < 10; i++) {
state->update(1 / 60.0f);
state->apply(skeleton);
skeleton.updateWorldTransform(Physics_Update);
}
// Transition to animation B
state->setAnimation(0, *animation2, true);
// Run 20 frames through the mix, serializing at frames 5, 10, 15, 20
for (int i = 1; i <= 20; i++) {
state->update(1 / 60.0f);
state->apply(skeleton);
skeleton.updateWorldTransform(Physics_Update);
if (i == 5 || i == 10 || i == 15 || i == 20) {
SkeletonSerializer transSerializer;
printf("\n=== TRANSITION FRAME %d ===\n", i);
printf("%s", transSerializer.serializeSkeleton(&skeleton).buffer());
}
}
}
// Cleanup
if (state != nullptr) {
delete state;
}
if (stateData != nullptr) {
delete stateData;
}
delete skeletonData;
delete atlas;
return 0;
}
@@ -0,0 +1,205 @@
#ifndef Spine_JsonWriter_h
#define Spine_JsonWriter_h
#include <spine/SpineString.h>
#include <stdio.h>
namespace spine {
class JsonWriter {
private:
String buffer;
int depth;
bool needsComma;
public:
JsonWriter() : depth(0), needsComma(false) {
}
void writeObjectStart() {
writeCommaIfNeeded();
buffer.append("{");
depth++;
needsComma = false;
}
void writeObjectEnd() {
depth--;
if (needsComma) {
buffer.append("\n");
writeIndent();
}
buffer.append("}");
needsComma = true;
}
void writeArrayStart() {
writeCommaIfNeeded();
buffer.append("[");
depth++;
needsComma = false;
}
void writeArrayEnd() {
depth--;
if (needsComma) {
buffer.append("\n");
writeIndent();
}
buffer.append("]");
needsComma = true;
}
void writeName(const char *name) {
writeCommaIfNeeded();
buffer.append("\n");
writeIndent();
buffer.append("\"");
buffer.append(name);
buffer.append("\": ");
needsComma = false;
}
void writeValue(const String &value) {
writeCommaIfNeeded();
if (value.buffer() == nullptr) {
buffer.append("null");
} else {
buffer.append("\"");
buffer.append(escapeString(value));
buffer.append("\"");
}
needsComma = true;
}
void writeValue(const char *value) {
writeCommaIfNeeded();
if (value == nullptr) {
buffer.append("null");
} else {
buffer.append("\"");
buffer.append(escapeString(String(value)));
buffer.append("\"");
}
needsComma = true;
}
void writeValue(float value) {
writeCommaIfNeeded();
// Format float with 6 decimal places
char temp[32];
snprintf(temp, sizeof(temp), "%.6f", value);
// Remove trailing zeros
char *end = temp + strlen(temp) - 1;
while (end > temp && *end == '0') {
end--;
}
if (*end == '.') {
end--;
}
*(end + 1) = '\0';
buffer.append(temp);
needsComma = true;
}
void writeValue(int value) {
writeCommaIfNeeded();
char temp[32];
snprintf(temp, sizeof(temp), "%d", value);
buffer.append(temp);
needsComma = true;
}
void writeValue(bool value) {
writeCommaIfNeeded();
buffer.append(value ? "true" : "false");
needsComma = true;
}
void writeValue(size_t value) {
writeCommaIfNeeded();
char temp[32];
snprintf(temp, sizeof(temp), "%zu", value);
buffer.append(temp);
needsComma = true;
}
void writeValue(PropertyId value) {
writeCommaIfNeeded();
char temp[32];
snprintf(temp, sizeof(temp), "%lld", (long long) value);
buffer.append(temp);
needsComma = true;
}
void writeNull() {
writeCommaIfNeeded();
buffer.append("null");
needsComma = true;
}
void close() {
buffer.append("\n");
}
String getString() const {
return buffer;
}
private:
void writeCommaIfNeeded() {
if (needsComma) {
buffer.append(",");
}
}
void writeIndent() {
for (int i = 0; i < depth; i++) {
buffer.append(" ");
}
}
String escapeString(const String &str) {
String result("");
const char *chars = str.buffer();
if (chars) {
for (size_t i = 0; i < str.length(); i++) {
char c = chars[i];
switch (c) {
case '"':
result.append("\\\"");
break;
case '\\':
result.append("\\\\");
break;
case '\b':
result.append("\\b");
break;
case '\f':
result.append("\\f");
break;
case '\n':
result.append("\\n");
break;
case '\r':
result.append("\\r");
break;
case '\t':
result.append("\\t");
break;
default:
result.append(c);
break;
}
}
}
return result;
}
};
}// namespace spine
#endif
@@ -0,0 +1,296 @@
/******************************************************************************
* Spine Runtimes License Agreement
* Last updated April 5, 2025. Replaces all prior versions.
*
* Copyright (c) 2013-2025, Esoteric Software LLC
*****************************************************************************/
#include <spine/Extension.h>
#include <spine/Map.h>
#include <spine/SpineString.h>
#include <assert.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unordered_map>
using namespace spine;
namespace spine {
SpineExtension *getDefaultExtension() {
return new DefaultSpineExtension();
}
}
class TrackingExtension : public SpineExtension {
public:
TrackingExtension() : SpineExtension() {
}
size_t activeAllocations() const {
return _allocations.size();
}
size_t activeBytes() const {
size_t total = 0;
for (std::unordered_map<void *, size_t>::const_iterator i = _allocations.begin(); i != _allocations.end(); ++i) total += i->second;
return total;
}
protected:
virtual void *_alloc(size_t size, const char *file, int line) override {
SP_UNUSED(file);
SP_UNUSED(line);
if (size == 0) return NULL;
void *ptr = malloc(size);
assert(ptr);
_allocations[ptr] = size;
return ptr;
}
virtual void *_calloc(size_t size, const char *file, int line) override {
void *ptr = _alloc(size, file, line);
if (ptr) memset(ptr, 0, size);
return ptr;
}
virtual void *_realloc(void *ptr, size_t size, const char *file, int line) override {
SP_UNUSED(file);
SP_UNUSED(line);
if (ptr == NULL) return _alloc(size, file, line);
std::unordered_map<void *, size_t>::iterator existing = _allocations.find(ptr);
assert(existing != _allocations.end());
_allocations.erase(existing);
if (size == 0) {
::free(ptr);
return NULL;
}
void *newPtr = ::realloc(ptr, size);
assert(newPtr);
_allocations[newPtr] = size;
return newPtr;
}
virtual void _free(void *mem, const char *file, int line) override {
SP_UNUSED(file);
SP_UNUSED(line);
if (!mem) return;
std::unordered_map<void *, size_t>::iterator existing = _allocations.find(mem);
assert(existing != _allocations.end());
_allocations.erase(existing);
::free(mem);
}
virtual char *_readFile(const String &path, int *length) override {
SP_UNUSED(path);
SP_UNUSED(length);
return NULL;
}
private:
std::unordered_map<void *, size_t> _allocations;
};
static int failures = 0;
#define CHECK(condition) \
do { \
if (!(condition)) { \
fprintf(stderr, "CHECK failed at %s:%d: %s\n", __FILE__, __LINE__, #condition); \
failures++; \
} \
} while (false)
class BadIntHash {
public:
size_t operator()(const int &key) const {
return (size_t) (key & 7);
}
};
static String makeString(const char *prefix, int value) {
char buffer[64];
snprintf(buffer, sizeof(buffer), "%s%d", prefix, value);
return String(buffer);
}
static void testBasicOperations() {
Map<int, int, BadIntHash> map;
CHECK(map.isEmpty());
CHECK(map.size() == 0);
CHECK(map.get(123) == NULL);
CHECK(!map.containsKey(123));
CHECK(!map.remove(123));
for (int i = 0; i < 1000; i++) map.put(i, i * 3);
CHECK(map.size() == 1000);
for (int i = 0; i < 1000; i++) {
int *value = map.get(i);
CHECK(value && *value == i * 3);
CHECK(map.containsKey(i));
}
for (int i = 0; i < 1000; i += 3) map.put(i, -i);
CHECK(map.size() == 1000);
for (int i = 0; i < 1000; i++) {
int expected = i % 3 == 0 ? -i : i * 3;
CHECK(map[i] == expected);
}
for (int i = 0; i < 1000; i += 2) CHECK(map.remove(i));
CHECK(map.size() == 500);
for (int i = 0; i < 1000; i++) {
int *value = map.get(i);
if (i % 2 == 0)
CHECK(value == NULL);
else
CHECK(value && *value == (i % 3 == 0 ? -i : i * 3));
}
for (int i = 0; i < 1000; i += 2) map.put(i, i + 7);
CHECK(map.size() == 1000);
for (int i = 0; i < 1000; i++) {
int expected = i % 2 == 0 ? i + 7 : (i % 3 == 0 ? -i : i * 3);
CHECK(map[i] == expected);
}
}
static void testEntriesAndAddAll() {
Map<int, int> map;
Array<int> keys;
for (int i = 0; i < 100; i++) keys.add(i);
CHECK(map.addAll(keys, 42));
CHECK(!map.addAll(keys, 42));
CHECK(map.size() == 100);
int count = 0;
int sum = 0;
Map<int, int>::Entries entries = map.getEntries();
while (entries.hasNext()) {
Map<int, int>::Pair pair = entries.next();
CHECK(pair.value == 42);
count++;
sum += pair.key;
}
CHECK(count == 100);
CHECK(sum == 4950);
}
static void testPutMissing() {
int a = 1, b = 2;
Map<int, int *> map;
CHECK(map.putMissing(5, &a) == NULL);
CHECK(map.size() == 1);
CHECK(map.putMissing(5, &b) == &a);
CHECK(map.size() == 1);
CHECK(*map.get(5) == &a);
}
static void testCopyAndAssignment() {
Map<int, String> original;
original.put(1, String("one"));
original.put(2, String("two"));
Map<int, String> copy(original);
copy.put(1, String("changed"));
CHECK(*original.get(1) == String("one"));
CHECK(*copy.get(1) == String("changed"));
CHECK(*copy.get(2) == String("two"));
Map<int, String> assigned;
assigned.put(99, String("discard"));
assigned = original;
CHECK(assigned.get(99) == NULL);
CHECK(*assigned.get(1) == String("one"));
CHECK(*assigned.get(2) == String("two"));
}
static void testStringMemory(TrackingExtension &extension) {
size_t baselineAllocations = extension.activeAllocations();
{
Map<String, String> map;
map.ensureCapacity(64);
size_t mapOnlyAllocations = extension.activeAllocations();
CHECK(mapOnlyAllocations > baselineAllocations);
for (int i = 0; i < 200; i++) map.put(makeString("key", i), makeString("value", i));
CHECK(map.size() == 200);
CHECK(extension.activeAllocations() > mapOnlyAllocations);
for (int i = 0; i < 100; i++) CHECK(map.remove(makeString("key", i)));
CHECK(map.size() == 100);
for (int i = 100; i < 200; i++) CHECK(*map.get(makeString("key", i)) == makeString("value", i));
map.clear();
CHECK(map.size() == 0);
CHECK(extension.activeAllocations() == mapOnlyAllocations);
}
CHECK(extension.activeAllocations() == baselineAllocations);
}
static void testStress() {
Map<int, int, BadIntHash> map;
bool present[257];
int values[257];
for (int i = 0; i < 257; i++) {
present[i] = false;
values[i] = 0;
}
unsigned int seed = 0x12345678u;
for (int step = 0; step < 10000; step++) {
seed = seed * 1103515245u + 12345u;
int key = (int) ((seed >> 8) % 257);
int op = (int) ((seed >> 24) % 3);
if (op == 0) {
bool removed = map.remove(key);
CHECK(removed == present[key]);
present[key] = false;
} else {
int value = (int) seed;
map.put(key, value);
present[key] = true;
values[key] = value;
}
if ((step & 127) == 0) {
size_t expectedSize = 0;
for (int i = 0; i < 257; i++) {
if (present[i]) {
expectedSize++;
CHECK(map.get(i) && *map.get(i) == values[i]);
} else
CHECK(map.get(i) == NULL);
}
CHECK(map.size() == expectedSize);
}
}
}
int main() {
TrackingExtension extension;
SpineExtension::setInstance(&extension);
size_t baselineAllocations = extension.activeAllocations();
testBasicOperations();
CHECK(extension.activeAllocations() == baselineAllocations);
testEntriesAndAddAll();
CHECK(extension.activeAllocations() == baselineAllocations);
testPutMissing();
CHECK(extension.activeAllocations() == baselineAllocations);
testCopyAndAssignment();
CHECK(extension.activeAllocations() == baselineAllocations);
testStringMemory(extension);
CHECK(extension.activeAllocations() == baselineAllocations);
testStress();
CHECK(extension.activeAllocations() == baselineAllocations);
if (failures) {
fprintf(stderr, "MapTest failed: %d failure(s), %zu active allocation(s), %zu active byte(s)\n", failures, extension.activeAllocations(),
extension.activeBytes());
return 1;
}
printf("MapTest passed\n");
return 0;
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,84 @@
#!/bin/bash
# Spine-C++ Docker Test Runner
#
# Runs the full spine-cpp test suite inside a Linux Docker container.
# This ensures consistent testing across different host environments.
set -e
# Change to spine-runtimes root directory (parent of spine-cpp)
cd "$(dirname "$0")/../.."
# Source logging utilities
source formatters/logging/logging.sh
log_title "Spine-C++ Docker Test"
log_detail "Running tests in Linux container"
# Detect existing build type
BUILD_TYPE=""
cd spine-cpp
if [ -f "build/CMakeCache.txt" ]; then
if grep -q "CMAKE_BUILD_TYPE:STRING=Release" build/CMakeCache.txt 2>/dev/null; then
BUILD_TYPE="release"
elif grep -q "CMAKE_BUILD_TYPE:STRING=Debug" build/CMakeCache.txt 2>/dev/null; then
BUILD_TYPE="debug"
fi
log_detail "Detected existing build type: ${BUILD_TYPE:-unknown}"
fi
cd ..
# Use a lightweight Linux image with build tools
DOCKER_IMAGE="gcc:12"
log_action "Pulling Docker image"
if docker pull "$DOCKER_IMAGE" > /dev/null 2>&1; then
log_ok
else
log_fail "Failed to pull Docker image"
exit 1
fi
log_action "Running tests in container"
if DOCKER_OUTPUT=$(docker run --rm \
-v "$(pwd)":/workspace \
-w /workspace/spine-cpp \
"$DOCKER_IMAGE" \
bash -c "apt-get update -qq && apt-get install -y -qq cmake ninja-build && ./tests/test.sh" 2>&1); then
log_ok
log_action "Analyzing executables"
if ANALYSIS_OUTPUT=$(docker run --rm \
-v "$(pwd)":/workspace \
-w /workspace/spine-cpp \
"$DOCKER_IMAGE" \
bash -c "echo '=== EXECUTABLE ANALYSIS ==='; for exe in build/headless-test*; do if [ -f \"\$exe\" ] && [ -x \"\$exe\" ]; then echo; echo \"--- \$(basename \"\$exe\") ---\"; echo \"Size: \$(du -h \"\$exe\" | cut -f1)\"; echo \"Dependencies:\"; ldd \"\$exe\" 2>/dev/null || echo 'No dynamic dependencies'; fi; done" 2>&1); then
log_ok
log_detail "$ANALYSIS_OUTPUT"
else
log_fail
log_error_output "$ANALYSIS_OUTPUT"
fi
log_action "Restoring local build directory"
cd spine-cpp
if rm -rf build; then
if [ -n "$BUILD_TYPE" ]; then
log_detail "Rebuilding $BUILD_TYPE configuration"
if ./build.sh clean "$BUILD_TYPE" > /dev/null 2>&1; then
log_ok "Local build directory restored ($BUILD_TYPE)"
else
log_detail "Warning: Could not restore $BUILD_TYPE build"
fi
else
log_detail "No previous build detected, leaving clean"
log_ok "Build directory cleaned"
fi
else
log_detail "Warning: Could not remove build directory"
fi
else
log_fail
log_error_output "$DOCKER_OUTPUT"
exit 1
fi
+97
View File
@@ -0,0 +1,97 @@
#!/bin/bash
# Spine-C++ Smoke Test
#
# Tests all spine-cpp build variants with spineboy example data:
# - headless-test (regular dynamic)
# - headless-test-no-cpprt (no-cpprt dynamic)
# - headless-test-static (regular static, Linux only)
# - headless-test-no-cpprt-static (no-cpprt static, Linux only)
set -e
# Change to spine-cpp root directory
cd "$(dirname "$0")/.."
# Source logging utilities
source ../formatters/logging/logging.sh
# Test configuration - spineboy example files and animation
SPINEBOY_SKEL="../examples/spineboy/export/spineboy-pro.skel"
SPINEBOY_ATLAS="../examples/spineboy/export/spineboy-pma.atlas"
SPINEBOY_ANIM="idle"
# Expected output pattern - first 10 lines of skeleton JSON data
EXPECTED_OUTPUT="=== SKELETON DATA ===
{
\"refString\": \"<SkeletonData-spineboy-pro>\",
\"type\": \"SkeletonData\",
\"bones\": [{
\"refString\": \"<BoneData-root>\",
\"type\": \"BoneData\",
\"index\": 0,
\"parent\": null,
\"length\": 0,"
log_title "Spine-C++ Test"
log_detail "Platform: $(uname)"
log_action "Building all variants"
if BUILD_OUTPUT=$(./build.sh clean release 2>&1); then
log_ok
else
log_fail "Build failed"
log_detail "$BUILD_OUTPUT"
exit 1
fi
test_count=0
pass_count=0
log_action "Testing map-test"
test_count=$((test_count + 1))
if OUTPUT=$(build/map-test 2>&1); then
log_ok
pass_count=$((pass_count + 1))
else
log_fail "map-test - execution failed"
log_detail "$OUTPUT"
echo ""
fi
for exe in build/headless-test*; do
if [ -f "$exe" ] && [ -x "$exe" ]; then
exe_name=$(basename "$exe")
log_action "Testing $exe_name"
test_count=$((test_count + 1))
if OUTPUT=$("$exe" $SPINEBOY_SKEL $SPINEBOY_ATLAS $SPINEBOY_ANIM 2>&1); then
actual_output=$(echo "$OUTPUT" | head -10)
if [ "$actual_output" = "$EXPECTED_OUTPUT" ]; then
log_ok
pass_count=$((pass_count + 1))
else
log_fail "$exe_name - output mismatch"
log_detail "Expected:"
log_detail "$EXPECTED_OUTPUT"
log_detail ""
log_detail "Actual:"
log_detail "$actual_output"
echo ""
fi
else
log_fail "$exe_name - execution failed"
log_detail "$OUTPUT"
echo ""
fi
fi
done
if [ $pass_count -eq $test_count ] && [ $test_count -gt 0 ]; then
log_summary "✓ All tests passed ($pass_count/$test_count)"
exit 0
else
log_summary "✗ Tests failed ($pass_count/$test_count)"
exit 1
fi