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

This commit is contained in:
Mario Zechner 2025-07-11 13:16:47 +02:00
parent 4e3d2be023
commit d973417106
6 changed files with 181 additions and 14 deletions

1
.gitignore vendored
View File

@ -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

View File

@ -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;

View File

@ -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;

View File

@ -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
tests/README.md Normal file
View File

@ -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)

View File

@ -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();
}