[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",
"configurations": [
{
"name": "debug-printer (c)",
"name": "headless test (c)",
"type": "cppdbg",
"request": "launch",
"program": "${workspaceFolder}/build/debug-printer",
"program": "${workspaceFolder}/build/headless-test",
"args": [
"${workspaceFolder}/../examples/spineboy/export/spineboy-pro.json",
"${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
if(CMAKE_SOURCE_DIR STREQUAL CMAKE_CURRENT_SOURCE_DIR)
add_executable(debug-printer ${CMAKE_CURRENT_SOURCE_DIR}/tests/debug-printer.c)
target_link_libraries(debug-printer spine-c)
add_executable(headless-test ${CMAKE_CURRENT_SOURCE_DIR}/tests/headless-test.c)
target_link_libraries(headless-test spine-c)
endif()

View File

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

View File

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

View File

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

View File

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

View File

@ -41,12 +41,12 @@ import com.badlogic.gdx.graphics.g2d.TextureAtlas.TextureAtlasData;
import java.util.Locale;
public class DebugPrinter implements ApplicationListener {
public class HeadlessTest implements ApplicationListener {
private String skeletonPath;
private String atlasPath;
private String animationName;
public DebugPrinter (String skeletonPath, String atlasPath, String animationName) {
public HeadlessTest (String skeletonPath, String atlasPath, String animationName) {
this.skeletonPath = skeletonPath;
this.atlasPath = atlasPath;
this.animationName = animationName;
@ -277,13 +277,13 @@ public class DebugPrinter implements ApplicationListener {
public static void main (String[] args) {
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);
}
HeadlessApplicationConfiguration config = new HeadlessApplicationConfiguration();
config.updatesPerSecond = 60;
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
"version": "0.2.0",
"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",
"request": "launch",
@ -39,26 +59,5 @@
"url": "http://localhost:8080/spine-phaser/example/index.html",
"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
- 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`
- **C++**: `spine-cpp/tests/DebugPrinter.cpp`
- **C**: `spine-c/tests/debug-printer.c`
- **TypeScript**: `spine-ts/spine-core/tests/DebugPrinter.ts`
- **Java (Reference)**: `spine-libgdx/spine-libgdx-tests/src/com/esotericsoftware/spine/HeadlessTest.java`
- **C++**: `spine-cpp/tests/HeadlessTest.cpp`
- **C**: `spine-c/tests/headless-test.c`
- **TypeScript**: `spine-ts/spine-core/tests/HeadlessTest.ts`
## Running Individual DebugPrinters
## Running Individual HeadlessTests
### Java (spine-libgdx)
```bash
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)
```bash
cd spine-cpp
./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)
```bash
cd spine-c
./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)
```bash
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
@ -52,13 +52,13 @@ npx tsx tests/DebugPrinter.ts <skeleton-path> <atlas-path> [animation-name]
The main test runner compares all runtime outputs automatically:
```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:
1. Check if each runtime's DebugPrinter needs rebuilding
2. Build any out-of-date DebugPrinters
3. Run each DebugPrinter with the same inputs
1. Check if each runtime's HeadlessTest needs rebuilding
2. Build any out-of-date HeadlessTests
3. Run each HeadlessTest with the same inputs
4. Compare outputs and report any differences
5. Save individual outputs to `tests/output/` for manual inspection
@ -66,20 +66,20 @@ This script will:
```bash
# 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-pma.atlas \
walk
# 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-pma.atlas
```
## Output Format
Each DebugPrinter outputs:
Each HeadlessTest outputs:
- **SKELETON DATA**: Static setup pose data (bones, slots, skins, animations metadata)
- **SKELETON STATE**: Runtime state after applying animations
@ -88,3 +88,24 @@ The output uses consistent formatting:
- Float values formatted to 6 decimal places
- Strings quoted, nulls explicitly shown
- 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
const args = process.argv.slice(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);
}
@ -68,7 +68,7 @@ const runtimes: RuntimeConfig[] = [
{
name: 'java',
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;
// Check if any source files are newer than the class file
@ -92,7 +92,7 @@ const runtimes: RuntimeConfig[] = [
? `${absoluteSkeletonPath} ${absoluteAtlasPath} ${animationName}`
: `${absoluteSkeletonPath} ${absoluteAtlasPath}`;
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'),
encoding: 'utf8'
@ -111,7 +111,7 @@ const runtimes: RuntimeConfig[] = [
{
name: 'cpp',
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;
// Check if any source files are newer than the executable
@ -133,8 +133,8 @@ const runtimes: RuntimeConfig[] = [
run: () => {
return execSync(
animationName
? `./build/debug-printer "${absoluteSkeletonPath}" "${absoluteAtlasPath}" "${animationName}"`
: `./build/debug-printer "${absoluteSkeletonPath}" "${absoluteAtlasPath}"`,
? `./build/headless-test "${absoluteSkeletonPath}" "${absoluteAtlasPath}" "${animationName}"`
: `./build/headless-test "${absoluteSkeletonPath}" "${absoluteAtlasPath}"`,
{
cwd: path.join(rootDir, 'spine-cpp'),
encoding: 'utf8'
@ -145,7 +145,7 @@ const runtimes: RuntimeConfig[] = [
{
name: 'c',
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;
// Check if any source files are newer than the executable
@ -167,8 +167,8 @@ const runtimes: RuntimeConfig[] = [
run: () => {
return execSync(
animationName
? `./build/debug-printer "${absoluteSkeletonPath}" "${absoluteAtlasPath}" "${animationName}"`
: `./build/debug-printer "${absoluteSkeletonPath}" "${absoluteAtlasPath}"`,
? `./build/headless-test "${absoluteSkeletonPath}" "${absoluteAtlasPath}" "${animationName}"`
: `./build/headless-test "${absoluteSkeletonPath}" "${absoluteAtlasPath}"`,
{
cwd: path.join(rootDir, 'spine-c'),
encoding: 'utf8'
@ -179,16 +179,16 @@ const runtimes: RuntimeConfig[] = [
{
name: 'ts',
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);
// For TypeScript, just check if the HeadlessTest.ts file exists
const headlessTestPath = path.join(rootDir, 'spine-ts/spine-core/tests/HeadlessTest.ts');
return fs.existsSync(headlessTestPath);
},
build: () => {}, // No build needed
run: () => {
return execSync(
animationName
? `npx tsx tests/DebugPrinter.ts "${absoluteSkeletonPath}" "${absoluteAtlasPath}" "${animationName}"`
: `npx tsx tests/DebugPrinter.ts "${absoluteSkeletonPath}" "${absoluteAtlasPath}"`,
? `npx tsx tests/HeadlessTest.ts "${absoluteSkeletonPath}" "${absoluteAtlasPath}" "${animationName}"`
: `npx tsx tests/HeadlessTest.ts "${absoluteSkeletonPath}" "${absoluteAtlasPath}"`,
{
cwd: path.join(rootDir, 'spine-ts/spine-core'),
encoding: 'utf8'
@ -202,7 +202,7 @@ async function main() {
// Ensure output directory exists
await mkdir(outputDir, { recursive: true });
console.log('Comparing DebugPrinter outputs for:');
console.log('Comparing HeadlessTest outputs for:');
console.log(` Skeleton: ${absoluteSkeletonPath}`);
console.log(` Atlas: ${absoluteAtlasPath}`);
console.log(` Animation: ${animationName}`);
@ -212,7 +212,7 @@ async function main() {
const outputs: Record<string, string> = {};
for (const runtime of runtimes) {
console.log(`Running ${runtime.name.toUpperCase()} DebugPrinter...`);
console.log(`Running ${runtime.name.toUpperCase()} HeadlessTest...`);
try {
// Build if needed