[tests] DebugPrinter -> HeadlessTest

This commit is contained in:
Mario Zechner 2025-07-11 14:16:24 +02:00
parent 99f9aca731
commit 73a17e88c9
13 changed files with 93 additions and 72 deletions

View File

@ -2,10 +2,10 @@
"version": "0.2.0", "version": "0.2.0",
"configurations": [ "configurations": [
{ {
"name": "debug-printer (c)", "name": "headless test (c)",
"type": "cppdbg", "type": "cppdbg",
"request": "launch", "request": "launch",
"program": "${workspaceFolder}/build/debug-printer", "program": "${workspaceFolder}/build/headless-test",
"args": [ "args": [
"${workspaceFolder}/../examples/spineboy/export/spineboy-pro.json", "${workspaceFolder}/../examples/spineboy/export/spineboy-pro.json",
"${workspaceFolder}/../examples/spineboy/export/spineboy-pma.atlas", "${workspaceFolder}/../examples/spineboy/export/spineboy-pma.atlas",

View File

@ -39,6 +39,6 @@ set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
# Create test executable only if this is the top-level project # Create test executable only if this is the top-level project
if(CMAKE_SOURCE_DIR STREQUAL CMAKE_CURRENT_SOURCE_DIR) if(CMAKE_SOURCE_DIR STREQUAL CMAKE_CURRENT_SOURCE_DIR)
add_executable(debug-printer ${CMAKE_CURRENT_SOURCE_DIR}/tests/debug-printer.c) add_executable(headless-test ${CMAKE_CURRENT_SOURCE_DIR}/tests/headless-test.c)
target_link_libraries(debug-printer spine-c) target_link_libraries(headless-test spine-c)
endif() endif()

View File

@ -2,10 +2,10 @@
"version": "0.2.0", "version": "0.2.0",
"configurations": [ "configurations": [
{ {
"name": "debug-printer (cpp)", "name": "headless test (cpp)",
"type": "cppdbg", "type": "cppdbg",
"request": "launch", "request": "launch",
"program": "${workspaceFolder}/build/debug-printer", "program": "${workspaceFolder}/build/headless-test",
"args": [ "args": [
"${workspaceFolder}/../examples/spineboy/export/spineboy-pro.json", "${workspaceFolder}/../examples/spineboy/export/spineboy-pro.json",
"${workspaceFolder}/../examples/spineboy/export/spineboy-pma.atlas", "${workspaceFolder}/../examples/spineboy/export/spineboy-pma.atlas",

View File

@ -22,6 +22,6 @@ export(
# Optional tests # Optional tests
if(CMAKE_SOURCE_DIR STREQUAL CMAKE_CURRENT_SOURCE_DIR) if(CMAKE_SOURCE_DIR STREQUAL CMAKE_CURRENT_SOURCE_DIR)
add_executable(debug-printer ${CMAKE_CURRENT_SOURCE_DIR}/tests/DebugPrinter.cpp) add_executable(headless-test ${CMAKE_CURRENT_SOURCE_DIR}/tests/HeadlessTest.cpp)
target_link_libraries(debug-printer spine-cpp) target_link_libraries(headless-test spine-cpp)
endif() endif()

View File

@ -3,9 +3,9 @@
"configurations": [ "configurations": [
{ {
"type": "java", "type": "java",
"name": "debug-printer (java)", "name": "headless test (java)",
"request": "launch", "request": "launch",
"mainClass": "com.esotericsoftware.spine.DebugPrinter", "mainClass": "com.esotericsoftware.spine.HeadlessTest",
"projectName": "spine-libgdx-tests", "projectName": "spine-libgdx-tests",
"args": [ "args": [
"${workspaceFolder}/../examples/spineboy/export/spineboy-pro.json", "${workspaceFolder}/../examples/spineboy/export/spineboy-pro.json",

View File

@ -145,9 +145,10 @@ configure(subprojects - project("spine-libgdx")) {
} }
project("spine-libgdx-tests") { project("spine-libgdx-tests") {
task runDebugPrinter(type: JavaExec) { task runHeadlessTest(type: JavaExec) {
main = 'com.esotericsoftware.spine.DebugPrinter' main = 'com.esotericsoftware.spine.HeadlessTest'
classpath = sourceSets.main.runtimeClasspath classpath = sourceSets.main.runtimeClasspath
workingDir = rootProject.projectDir
if (project.hasProperty('args')) { if (project.hasProperty('args')) {
args project.getProperty('args').split(' ') args project.getProperty('args').split(' ')
} }

View File

@ -41,12 +41,12 @@ import com.badlogic.gdx.graphics.g2d.TextureAtlas.TextureAtlasData;
import java.util.Locale; import java.util.Locale;
public class DebugPrinter implements ApplicationListener { public class HeadlessTest implements ApplicationListener {
private String skeletonPath; private String skeletonPath;
private String atlasPath; private String atlasPath;
private String animationName; private String animationName;
public DebugPrinter (String skeletonPath, String atlasPath, String animationName) { public HeadlessTest (String skeletonPath, String atlasPath, String animationName) {
this.skeletonPath = skeletonPath; this.skeletonPath = skeletonPath;
this.atlasPath = atlasPath; this.atlasPath = atlasPath;
this.animationName = animationName; this.animationName = animationName;
@ -277,13 +277,13 @@ public class DebugPrinter implements ApplicationListener {
public static void main (String[] args) { public static void main (String[] args) {
if (args.length < 2) { if (args.length < 2) {
System.err.println("Usage: DebugPrinter <skeleton-path> <atlas-path> [animation-name]"); System.err.println("Usage: HeadlessTest <skeleton-path> <atlas-path> [animation-name]");
System.exit(1); System.exit(1);
} }
HeadlessApplicationConfiguration config = new HeadlessApplicationConfiguration(); HeadlessApplicationConfiguration config = new HeadlessApplicationConfiguration();
config.updatesPerSecond = 60; config.updatesPerSecond = 60;
String animationName = args.length >= 3 ? args[2] : null; String animationName = args.length >= 3 ? args[2] : null;
new HeadlessApplication(new DebugPrinter(args[0], args[1], animationName), config); new HeadlessApplication(new HeadlessTest(args[0], args[1], animationName), config);
} }
} }

View File

@ -4,6 +4,26 @@
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0", "version": "0.2.0",
"configurations": [ "configurations": [
{
"name": "headless test (ts)",
"type": "node",
"request": "launch",
"runtimeExecutable": "npx",
"runtimeArgs": [
"tsx"
],
"program": "${workspaceFolder}/spine-core/tests/HeadlessTest.ts",
"args": [
"${workspaceFolder}/../examples/spineboy/export/spineboy-pro.json",
"${workspaceFolder}/../examples/spineboy/export/spineboy-pma.atlas",
"walk"
],
"cwd": "${workspaceFolder}/spine-core",
"console": "integratedTerminal",
"skipFiles": [
"<node_internals>/**"
]
},
{ {
"type": "pwa-chrome", "type": "pwa-chrome",
"request": "launch", "request": "launch",
@ -39,26 +59,5 @@
"url": "http://localhost:8080/spine-phaser/example/index.html", "url": "http://localhost:8080/spine-phaser/example/index.html",
"webRoot": "${workspaceFolder}" "webRoot": "${workspaceFolder}"
}, },
{
"name": "debug-printer (ts)",
"type": "node",
"request": "launch",
"runtimeExecutable": "npx",
"runtimeArgs": [
"tsx"
],
"program": "${workspaceFolder}/spine-core/tests/DebugPrinter.ts",
"args": [
"${workspaceFolder}/../examples/spineboy/export/spineboy-pro.json",
"${workspaceFolder}/../examples/spineboy/export/spineboy-pma.atlas",
"walk"
],
"cwd": "${workspaceFolder}/spine-core",
"console": "integratedTerminal",
"skipFiles": [
"<node_internals>/**"
]
}
] ]
} }

View File

@ -10,41 +10,41 @@ Unlike traditional unit tests, this test suite:
- Compares outputs between runtimes to detect discrepancies - Compares outputs between runtimes to detect discrepancies
- Helps maintain consistency when porting changes from the reference implementation - Helps maintain consistency when porting changes from the reference implementation
## DebugPrinter Locations ## HeadlessTest Locations
Each runtime has a DebugPrinter program that outputs skeleton data in a standardized format: Each runtime has a HeadlessTest program that outputs skeleton data in a standardized format:
- **Java (Reference)**: `spine-libgdx/spine-libgdx-tests/src/com/esotericsoftware/spine/DebugPrinter.java` - **Java (Reference)**: `spine-libgdx/spine-libgdx-tests/src/com/esotericsoftware/spine/HeadlessTest.java`
- **C++**: `spine-cpp/tests/DebugPrinter.cpp` - **C++**: `spine-cpp/tests/HeadlessTest.cpp`
- **C**: `spine-c/tests/debug-printer.c` - **C**: `spine-c/tests/headless-test.c`
- **TypeScript**: `spine-ts/spine-core/tests/DebugPrinter.ts` - **TypeScript**: `spine-ts/spine-core/tests/HeadlessTest.ts`
## Running Individual DebugPrinters ## Running Individual HeadlessTests
### Java (spine-libgdx) ### Java (spine-libgdx)
```bash ```bash
cd spine-libgdx cd spine-libgdx
./gradlew :spine-libgdx-tests:runDebugPrinter -Pargs="<skeleton-path> <atlas-path> [animation-name]" ./gradlew :spine-libgdx-tests:runHeadlessTest -Pargs="<skeleton-path> <atlas-path> [animation-name]"
``` ```
### C++ (spine-cpp) ### C++ (spine-cpp)
```bash ```bash
cd spine-cpp cd spine-cpp
./build.sh # Build if needed ./build.sh # Build if needed
./build/debug-printer <skeleton-path> <atlas-path> [animation-name] ./build/headless-test <skeleton-path> <atlas-path> [animation-name]
``` ```
### C (spine-c) ### C (spine-c)
```bash ```bash
cd spine-c cd spine-c
./build.sh # Build if needed ./build.sh # Build if needed
./build/debug-printer <skeleton-path> <atlas-path> [animation-name] ./build/headless-test <skeleton-path> <atlas-path> [animation-name]
``` ```
### TypeScript (spine-ts) ### TypeScript (spine-ts)
```bash ```bash
cd spine-ts/spine-core cd spine-ts/spine-core
npx tsx tests/DebugPrinter.ts <skeleton-path> <atlas-path> [animation-name] npx tsx tests/HeadlessTest.ts <skeleton-path> <atlas-path> [animation-name]
``` ```
## Running the Comparison Test ## Running the Comparison Test
@ -52,13 +52,13 @@ npx tsx tests/DebugPrinter.ts <skeleton-path> <atlas-path> [animation-name]
The main test runner compares all runtime outputs automatically: The main test runner compares all runtime outputs automatically:
```bash ```bash
./tests/compare-with-reference-impl.ts <skeleton-path> <atlas-path> [animation-name] ./tests/headless-test-runner.ts <skeleton-path> <atlas-path> [animation-name]
``` ```
This script will: This script will:
1. Check if each runtime's DebugPrinter needs rebuilding 1. Check if each runtime's HeadlessTest needs rebuilding
2. Build any out-of-date DebugPrinters 2. Build any out-of-date HeadlessTests
3. Run each DebugPrinter with the same inputs 3. Run each HeadlessTest with the same inputs
4. Compare outputs and report any differences 4. Compare outputs and report any differences
5. Save individual outputs to `tests/output/` for manual inspection 5. Save individual outputs to `tests/output/` for manual inspection
@ -66,20 +66,20 @@ This script will:
```bash ```bash
# Test with spineboy walk animation # Test with spineboy walk animation
./tests/compare-with-reference-impl.ts \ ./tests/headless-test-runner.ts \
examples/spineboy/export/spineboy-pro.json \ examples/spineboy/export/spineboy-pro.json \
examples/spineboy/export/spineboy-pma.atlas \ examples/spineboy/export/spineboy-pma.atlas \
walk walk
# Test without animation (setup pose only) # Test without animation (setup pose only)
./tests/compare-with-reference-impl.ts \ ./tests/headless-test-runner.ts \
examples/spineboy/export/spineboy-pro.json \ examples/spineboy/export/spineboy-pro.json \
examples/spineboy/export/spineboy-pma.atlas examples/spineboy/export/spineboy-pma.atlas
``` ```
## Output Format ## Output Format
Each DebugPrinter outputs: Each HeadlessTest outputs:
- **SKELETON DATA**: Static setup pose data (bones, slots, skins, animations metadata) - **SKELETON DATA**: Static setup pose data (bones, slots, skins, animations metadata)
- **SKELETON STATE**: Runtime state after applying animations - **SKELETON STATE**: Runtime state after applying animations
@ -87,4 +87,25 @@ The output uses consistent formatting:
- Hierarchical structure with 2-space indentation - Hierarchical structure with 2-space indentation
- Float values formatted to 6 decimal places - Float values formatted to 6 decimal places
- Strings quoted, nulls explicitly shown - Strings quoted, nulls explicitly shown
- Locale-independent number formatting (always uses `.` for decimals) - Locale-independent number formatting (always uses `.` for decimals)
## Troubleshooting
If outputs differ between runtimes:
1. Check `tests/output/` for the full outputs from each runtime
2. Use a diff tool to compare the files
3. Common issues:
- Number formatting differences (should be fixed by locale settings)
- Missing or extra fields in data structures
- Different default values
- Rounding differences
## Future Expansion
The current implementation prints basic skeleton data. Future expansions will include:
- Full bone and slot hierarchies
- All attachment types
- Animation timelines
- Constraint data
- Physics settings
- Complete runtime state after animation

View File

@ -41,7 +41,7 @@ async function getNewestFileTime(baseDir: string, patterns: string[]): Promise<n
// Parse command line arguments // Parse command line arguments
const args = process.argv.slice(2); const args = process.argv.slice(2);
if (args.length < 2) { if (args.length < 2) {
console.error('Usage: compare-with-reference-impl.ts <skeleton-path> <atlas-path> [animation-name]'); console.error('Usage: headless-test-runner.ts <skeleton-path> <atlas-path> [animation-name]');
process.exit(1); process.exit(1);
} }
@ -68,7 +68,7 @@ const runtimes: RuntimeConfig[] = [
{ {
name: 'java', name: 'java',
buildCheck: async () => { buildCheck: async () => {
const classPath = path.join(rootDir, 'spine-libgdx/spine-libgdx-tests/build/classes/java/main/com/esotericsoftware/spine/DebugPrinter.class'); const classPath = path.join(rootDir, 'spine-libgdx/spine-libgdx-tests/build/classes/java/main/com/esotericsoftware/spine/HeadlessTest.class');
if (!fs.existsSync(classPath)) return false; if (!fs.existsSync(classPath)) return false;
// Check if any source files are newer than the class file // Check if any source files are newer than the class file
@ -92,7 +92,7 @@ const runtimes: RuntimeConfig[] = [
? `${absoluteSkeletonPath} ${absoluteAtlasPath} ${animationName}` ? `${absoluteSkeletonPath} ${absoluteAtlasPath} ${animationName}`
: `${absoluteSkeletonPath} ${absoluteAtlasPath}`; : `${absoluteSkeletonPath} ${absoluteAtlasPath}`;
const output = execSync( const output = execSync(
`./gradlew -q :spine-libgdx-tests:runDebugPrinter -Pargs="${args}"`, `./gradlew -q :spine-libgdx-tests:runHeadlessTest -Pargs="${args}"`,
{ {
cwd: path.join(rootDir, 'spine-libgdx'), cwd: path.join(rootDir, 'spine-libgdx'),
encoding: 'utf8' encoding: 'utf8'
@ -111,7 +111,7 @@ const runtimes: RuntimeConfig[] = [
{ {
name: 'cpp', name: 'cpp',
buildCheck: async () => { buildCheck: async () => {
const execPath = path.join(rootDir, 'spine-cpp/build/debug-printer'); const execPath = path.join(rootDir, 'spine-cpp/build/headless-test');
if (!fs.existsSync(execPath)) return false; if (!fs.existsSync(execPath)) return false;
// Check if any source files are newer than the executable // Check if any source files are newer than the executable
@ -133,8 +133,8 @@ const runtimes: RuntimeConfig[] = [
run: () => { run: () => {
return execSync( return execSync(
animationName animationName
? `./build/debug-printer "${absoluteSkeletonPath}" "${absoluteAtlasPath}" "${animationName}"` ? `./build/headless-test "${absoluteSkeletonPath}" "${absoluteAtlasPath}" "${animationName}"`
: `./build/debug-printer "${absoluteSkeletonPath}" "${absoluteAtlasPath}"`, : `./build/headless-test "${absoluteSkeletonPath}" "${absoluteAtlasPath}"`,
{ {
cwd: path.join(rootDir, 'spine-cpp'), cwd: path.join(rootDir, 'spine-cpp'),
encoding: 'utf8' encoding: 'utf8'
@ -145,7 +145,7 @@ const runtimes: RuntimeConfig[] = [
{ {
name: 'c', name: 'c',
buildCheck: async () => { buildCheck: async () => {
const execPath = path.join(rootDir, 'spine-c/build/debug-printer'); const execPath = path.join(rootDir, 'spine-c/build/headless-test');
if (!fs.existsSync(execPath)) return false; if (!fs.existsSync(execPath)) return false;
// Check if any source files are newer than the executable // Check if any source files are newer than the executable
@ -167,8 +167,8 @@ const runtimes: RuntimeConfig[] = [
run: () => { run: () => {
return execSync( return execSync(
animationName animationName
? `./build/debug-printer "${absoluteSkeletonPath}" "${absoluteAtlasPath}" "${animationName}"` ? `./build/headless-test "${absoluteSkeletonPath}" "${absoluteAtlasPath}" "${animationName}"`
: `./build/debug-printer "${absoluteSkeletonPath}" "${absoluteAtlasPath}"`, : `./build/headless-test "${absoluteSkeletonPath}" "${absoluteAtlasPath}"`,
{ {
cwd: path.join(rootDir, 'spine-c'), cwd: path.join(rootDir, 'spine-c'),
encoding: 'utf8' encoding: 'utf8'
@ -179,16 +179,16 @@ const runtimes: RuntimeConfig[] = [
{ {
name: 'ts', name: 'ts',
buildCheck: async () => { buildCheck: async () => {
// For TypeScript, just check if the DebugPrinter.ts file exists // For TypeScript, just check if the HeadlessTest.ts file exists
const debugPrinterPath = path.join(rootDir, 'spine-ts/spine-core/tests/DebugPrinter.ts'); const headlessTestPath = path.join(rootDir, 'spine-ts/spine-core/tests/HeadlessTest.ts');
return fs.existsSync(debugPrinterPath); return fs.existsSync(headlessTestPath);
}, },
build: () => {}, // No build needed build: () => {}, // No build needed
run: () => { run: () => {
return execSync( return execSync(
animationName animationName
? `npx tsx tests/DebugPrinter.ts "${absoluteSkeletonPath}" "${absoluteAtlasPath}" "${animationName}"` ? `npx tsx tests/HeadlessTest.ts "${absoluteSkeletonPath}" "${absoluteAtlasPath}" "${animationName}"`
: `npx tsx tests/DebugPrinter.ts "${absoluteSkeletonPath}" "${absoluteAtlasPath}"`, : `npx tsx tests/HeadlessTest.ts "${absoluteSkeletonPath}" "${absoluteAtlasPath}"`,
{ {
cwd: path.join(rootDir, 'spine-ts/spine-core'), cwd: path.join(rootDir, 'spine-ts/spine-core'),
encoding: 'utf8' encoding: 'utf8'
@ -202,7 +202,7 @@ async function main() {
// Ensure output directory exists // Ensure output directory exists
await mkdir(outputDir, { recursive: true }); await mkdir(outputDir, { recursive: true });
console.log('Comparing DebugPrinter outputs for:'); console.log('Comparing HeadlessTest outputs for:');
console.log(` Skeleton: ${absoluteSkeletonPath}`); console.log(` Skeleton: ${absoluteSkeletonPath}`);
console.log(` Atlas: ${absoluteAtlasPath}`); console.log(` Atlas: ${absoluteAtlasPath}`);
console.log(` Animation: ${animationName}`); console.log(` Animation: ${animationName}`);
@ -212,7 +212,7 @@ async function main() {
const outputs: Record<string, string> = {}; const outputs: Record<string, string> = {};
for (const runtime of runtimes) { for (const runtime of runtimes) {
console.log(`Running ${runtime.name.toUpperCase()} DebugPrinter...`); console.log(`Running ${runtime.name.toUpperCase()} HeadlessTest...`);
try { try {
// Build if needed // Build if needed