Quellcode durchsuchen

[tests] Fix locale in all debug printers, add tests/README.md, build if sources changed in compare-with-reference-impl.ts

Mario Zechner vor 2 Monaten
Ursprung
Commit
d973417106

+ 1 - 0
.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

+ 4 - 0
spine-c/tests/debug-printer.c

@@ -32,6 +32,7 @@
 #include <stdlib.h>
 #include <string.h>
 #include <stdarg.h>
+#include <locale.h>
 
 // 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 <skeleton-path> <atlas-path> [animation-name]\n");
 		return 1;

+ 4 - 0
spine-cpp/tests/DebugPrinter.cpp

@@ -32,6 +32,7 @@
 #include <stdlib.h>
 #include <string.h>
 #include <stdarg.h>
+#include <locale.h>
 
 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 <skeleton-path> <atlas-path> [animation-name]\n");
 		return 1;

+ 3 - 2
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

+ 90 - 0
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="<skeleton-path> <atlas-path> [animation-name]"
+```
+
+### C++ (spine-cpp)
+```bash
+cd spine-cpp
+./build.sh  # Build if needed
+./build/debug-printer <skeleton-path> <atlas-path> [animation-name]
+```
+
+### C (spine-c)
+```bash
+cd spine-c
+./build.sh  # Build if needed
+./build/debug-printer <skeleton-path> <atlas-path> [animation-name]
+```
+
+### TypeScript (spine-ts)
+```bash
+cd spine-ts/spine-core
+npx tsx tests/DebugPrinter.ts <skeleton-path> <atlas-path> [animation-name]
+```
+
+## Running the Comparison Test
+
+The main test runner compares all runtime outputs automatically:
+
+```bash
+./tests/compare-with-reference-impl.ts <skeleton-path> <atlas-path> [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)

+ 79 - 12
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<number> {
+    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<number> {
+    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<boolean>;
     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();
             }