diff --git a/.gitignore b/.gitignore index d72afed9b..1842e322c 100644 --- a/.gitignore +++ b/.gitignore @@ -244,3 +244,4 @@ spine-c-new/codegen/all-spine-types.json spine-c-new/codegen/spine-cpp-types.json docs/spine-runtimes-types.md spine-c/codegen/dist +tests/output diff --git a/spine-c/tests/debug-printer.c b/spine-c/tests/debug-printer.c index 3448ba6f2..826b40cde 100644 --- a/spine-c/tests/debug-printer.c +++ b/spine-c/tests/debug-printer.c @@ -32,6 +32,7 @@ #include #include #include +#include // Custom texture loader that doesn't load actual textures void *headlessTextureLoader(const char *path) { @@ -128,6 +129,9 @@ uint8_t *read_file(const char *path, int *length) { } int main(int argc, char *argv[]) { + // Set locale to ensure consistent number formatting + setlocale(LC_ALL, "C"); + if (argc < 3) { fprintf(stderr, "Usage: DebugPrinter [animation-name]\n"); return 1; diff --git a/spine-cpp/tests/DebugPrinter.cpp b/spine-cpp/tests/DebugPrinter.cpp index fa6831de1..16cb6cc74 100644 --- a/spine-cpp/tests/DebugPrinter.cpp +++ b/spine-cpp/tests/DebugPrinter.cpp @@ -32,6 +32,7 @@ #include #include #include +#include using namespace spine; @@ -129,6 +130,9 @@ public: }; int main(int argc, char *argv[]) { + // Set locale to ensure consistent number formatting + setlocale(LC_ALL, "C"); + if (argc < 3) { fprintf(stderr, "Usage: DebugPrinter [animation-name]\n"); return 1; diff --git a/spine-libgdx/spine-libgdx-tests/src/com/esotericsoftware/spine/DebugPrinter.java b/spine-libgdx/spine-libgdx-tests/src/com/esotericsoftware/spine/DebugPrinter.java index ec2b55989..1d95a4c0b 100644 --- a/spine-libgdx/spine-libgdx-tests/src/com/esotericsoftware/spine/DebugPrinter.java +++ b/spine-libgdx/spine-libgdx-tests/src/com/esotericsoftware/spine/DebugPrinter.java @@ -39,6 +39,8 @@ import com.badlogic.gdx.graphics.g2d.TextureAtlas; import com.badlogic.gdx.graphics.g2d.TextureAtlas.AtlasRegion; import com.badlogic.gdx.graphics.g2d.TextureAtlas.TextureAtlasData; +import java.util.Locale; + public class DebugPrinter implements ApplicationListener { private String skeletonPath; private String atlasPath; @@ -68,7 +70,7 @@ public class DebugPrinter implements ApplicationListener { print(name + ": \"" + value + "\""); } else if (value instanceof Float) { // Format floats to 6 decimal places to match other runtimes - print(name + ": " + String.format("%.6f", value)); + print(name + ": " + String.format(Locale.US, "%.6f", value)); } else { print(name + ": " + value); } @@ -238,7 +240,6 @@ public class DebugPrinter implements ApplicationListener { state.apply(skeleton); } - skeleton.update(0.016f); skeleton.updateWorldTransform(Physics.update); // Print skeleton state diff --git a/tests/README.md b/tests/README.md new file mode 100644 index 000000000..fbbcd66b1 --- /dev/null +++ b/tests/README.md @@ -0,0 +1,90 @@ +# Spine Runtimes Test Suite + +This test suite is designed to ensure consistency across all Spine runtime implementations by comparing their outputs against the reference implementation (spine-libgdx). + +## Purpose + +Unlike traditional unit tests, this test suite: +- Loads skeleton data and animations in each runtime +- Outputs all internal state in a consistent, diffable text format +- Compares outputs between runtimes to detect discrepancies +- Helps maintain consistency when porting changes from the reference implementation + +## DebugPrinter Locations + +Each runtime has a DebugPrinter program that outputs skeleton data in a standardized format: + +- **Java (Reference)**: `spine-libgdx/spine-libgdx-tests/src/com/esotericsoftware/spine/DebugPrinter.java` +- **C++**: `spine-cpp/tests/DebugPrinter.cpp` +- **C**: `spine-c/tests/debug-printer.c` +- **TypeScript**: `spine-ts/spine-core/tests/DebugPrinter.ts` + +## Running Individual DebugPrinters + +### Java (spine-libgdx) +```bash +cd spine-libgdx +./gradlew :spine-libgdx-tests:runDebugPrinter -Pargs=" [animation-name]" +``` + +### C++ (spine-cpp) +```bash +cd spine-cpp +./build.sh # Build if needed +./build/debug-printer [animation-name] +``` + +### C (spine-c) +```bash +cd spine-c +./build.sh # Build if needed +./build/debug-printer [animation-name] +``` + +### TypeScript (spine-ts) +```bash +cd spine-ts/spine-core +npx tsx tests/DebugPrinter.ts [animation-name] +``` + +## Running the Comparison Test + +The main test runner compares all runtime outputs automatically: + +```bash +./tests/compare-with-reference-impl.ts [animation-name] +``` + +This script will: +1. Check if each runtime's DebugPrinter needs rebuilding +2. Build any out-of-date DebugPrinters +3. Run each DebugPrinter with the same inputs +4. Compare outputs and report any differences +5. Save individual outputs to `tests/output/` for manual inspection + +### Example Usage + +```bash +# Test with spineboy walk animation +./tests/compare-with-reference-impl.ts \ + examples/spineboy/export/spineboy-pro.json \ + examples/spineboy/export/spineboy-pma.atlas \ + walk + +# Test without animation (setup pose only) +./tests/compare-with-reference-impl.ts \ + examples/spineboy/export/spineboy-pro.json \ + examples/spineboy/export/spineboy-pma.atlas +``` + +## Output Format + +Each DebugPrinter outputs: +- **SKELETON DATA**: Static setup pose data (bones, slots, skins, animations metadata) +- **SKELETON STATE**: Runtime state after applying animations + +The output uses consistent formatting: +- Hierarchical structure with 2-space indentation +- Float values formatted to 6 decimal places +- Strings quoted, nulls explicitly shown +- Locale-independent number formatting (always uses `.` for decimals) \ No newline at end of file diff --git a/tests/compare-with-reference-impl.ts b/tests/compare-with-reference-impl.ts index 6192b9132..59f8edaa7 100755 --- a/tests/compare-with-reference-impl.ts +++ b/tests/compare-with-reference-impl.ts @@ -7,6 +7,36 @@ import { promisify } from 'util'; const writeFile = promisify(fs.writeFile); const mkdir = promisify(fs.mkdir); +const stat = promisify(fs.stat); + +// Helper function to get modification time of a file +async function getMTime(filePath: string): Promise { + try { + const stats = await stat(filePath); + return stats.mtimeMs; + } catch { + return 0; + } +} + +// Helper function to find newest file in a directory pattern +async function getNewestFileTime(baseDir: string, patterns: string[]): Promise { + let newest = 0; + + for (const pattern of patterns) { + const globPattern = path.join(baseDir, pattern); + const files = execSync(`find "${baseDir}" -name "${pattern.split('/').pop()}" -type f 2>/dev/null || true`, { + encoding: 'utf8' + }).trim().split('\n').filter(f => f); + + for (const file of files) { + const mtime = await getMTime(file); + if (mtime > newest) newest = mtime; + } + } + + return newest; +} // Parse command line arguments const args = process.argv.slice(2); @@ -28,7 +58,7 @@ const outputDir = path.join(scriptDir, 'output'); interface RuntimeConfig { name: string; - buildCheck: () => boolean; + buildCheck: () => Promise; build: () => void; run: () => string; } @@ -37,9 +67,18 @@ interface RuntimeConfig { const runtimes: RuntimeConfig[] = [ { name: 'java', - buildCheck: () => { + buildCheck: async () => { const classPath = path.join(rootDir, 'spine-libgdx/spine-libgdx-tests/build/classes/java/main/com/esotericsoftware/spine/DebugPrinter.class'); - return fs.existsSync(classPath); + if (!fs.existsSync(classPath)) return false; + + // Check if any source files are newer than the class file + const classTime = await getMTime(classPath); + const sourceTime = await getNewestFileTime( + path.join(rootDir, 'spine-libgdx'), + ['spine-libgdx/src/**/*.java', 'spine-libgdx-tests/src/**/*.java'] + ); + + return sourceTime <= classTime; }, build: () => { console.log(' Building Java runtime...'); @@ -71,9 +110,18 @@ const runtimes: RuntimeConfig[] = [ }, { name: 'cpp', - buildCheck: () => { + buildCheck: async () => { const execPath = path.join(rootDir, 'spine-cpp/build/debug-printer'); - return fs.existsSync(execPath); + if (!fs.existsSync(execPath)) return false; + + // Check if any source files are newer than the executable + const execTime = await getMTime(execPath); + const sourceTime = await getNewestFileTime( + path.join(rootDir, 'spine-cpp'), + ['spine-cpp/src/**/*.cpp', 'spine-cpp/include/**/*.h', 'tests/DebugPrinter.cpp'] + ); + + return sourceTime <= execTime; }, build: () => { console.log(' Building C++ runtime...'); @@ -84,7 +132,9 @@ const runtimes: RuntimeConfig[] = [ }, run: () => { return execSync( - `./build/debug-printer "${absoluteSkeletonPath}" "${absoluteAtlasPath}" "${animationName}"`, + animationName + ? `./build/debug-printer "${absoluteSkeletonPath}" "${absoluteAtlasPath}" "${animationName}"` + : `./build/debug-printer "${absoluteSkeletonPath}" "${absoluteAtlasPath}"`, { cwd: path.join(rootDir, 'spine-cpp'), encoding: 'utf8' @@ -94,9 +144,18 @@ const runtimes: RuntimeConfig[] = [ }, { name: 'c', - buildCheck: () => { + buildCheck: async () => { const execPath = path.join(rootDir, 'spine-c/build/debug-printer'); - return fs.existsSync(execPath); + if (!fs.existsSync(execPath)) return false; + + // Check if any source files are newer than the executable + const execTime = await getMTime(execPath); + const sourceTime = await getNewestFileTime( + path.join(rootDir, 'spine-c'), + ['src/**/*.c', 'include/**/*.h', 'tests/debug-printer.c'] + ); + + return sourceTime <= execTime; }, build: () => { console.log(' Building C runtime...'); @@ -107,7 +166,9 @@ const runtimes: RuntimeConfig[] = [ }, run: () => { return execSync( - `./build/debug-printer "${absoluteSkeletonPath}" "${absoluteAtlasPath}" "${animationName}"`, + animationName + ? `./build/debug-printer "${absoluteSkeletonPath}" "${absoluteAtlasPath}" "${animationName}"` + : `./build/debug-printer "${absoluteSkeletonPath}" "${absoluteAtlasPath}"`, { cwd: path.join(rootDir, 'spine-c'), encoding: 'utf8' @@ -117,11 +178,17 @@ const runtimes: RuntimeConfig[] = [ }, { name: 'ts', - buildCheck: () => true, // No build needed + buildCheck: async () => { + // For TypeScript, just check if the DebugPrinter.ts file exists + const debugPrinterPath = path.join(rootDir, 'spine-ts/spine-core/tests/DebugPrinter.ts'); + return fs.existsSync(debugPrinterPath); + }, build: () => {}, // No build needed run: () => { return execSync( - `npx tsx tests/DebugPrinter.ts "${absoluteSkeletonPath}" "${absoluteAtlasPath}" "${animationName}"`, + animationName + ? `npx tsx tests/DebugPrinter.ts "${absoluteSkeletonPath}" "${absoluteAtlasPath}" "${animationName}"` + : `npx tsx tests/DebugPrinter.ts "${absoluteSkeletonPath}" "${absoluteAtlasPath}"`, { cwd: path.join(rootDir, 'spine-ts/spine-core'), encoding: 'utf8' @@ -149,7 +216,7 @@ async function main() { try { // Build if needed - if (!runtime.buildCheck()) { + if (!(await runtime.buildCheck())) { runtime.build(); }