mirror of
https://github.com/EsotericSoftware/spine-runtimes.git
synced 2025-12-21 09:46:02 +08:00
[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:
parent
4e3d2be023
commit
d973417106
1
.gitignore
vendored
1
.gitignore
vendored
@ -244,3 +244,4 @@ spine-c-new/codegen/all-spine-types.json
|
|||||||
spine-c-new/codegen/spine-cpp-types.json
|
spine-c-new/codegen/spine-cpp-types.json
|
||||||
docs/spine-runtimes-types.md
|
docs/spine-runtimes-types.md
|
||||||
spine-c/codegen/dist
|
spine-c/codegen/dist
|
||||||
|
tests/output
|
||||||
|
|||||||
@ -32,6 +32,7 @@
|
|||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
#include <stdarg.h>
|
#include <stdarg.h>
|
||||||
|
#include <locale.h>
|
||||||
|
|
||||||
// Custom texture loader that doesn't load actual textures
|
// Custom texture loader that doesn't load actual textures
|
||||||
void *headlessTextureLoader(const char *path) {
|
void *headlessTextureLoader(const char *path) {
|
||||||
@ -128,6 +129,9 @@ uint8_t *read_file(const char *path, int *length) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
int main(int argc, char *argv[]) {
|
int main(int argc, char *argv[]) {
|
||||||
|
// Set locale to ensure consistent number formatting
|
||||||
|
setlocale(LC_ALL, "C");
|
||||||
|
|
||||||
if (argc < 3) {
|
if (argc < 3) {
|
||||||
fprintf(stderr, "Usage: DebugPrinter <skeleton-path> <atlas-path> [animation-name]\n");
|
fprintf(stderr, "Usage: DebugPrinter <skeleton-path> <atlas-path> [animation-name]\n");
|
||||||
return 1;
|
return 1;
|
||||||
|
|||||||
@ -32,6 +32,7 @@
|
|||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
#include <stdarg.h>
|
#include <stdarg.h>
|
||||||
|
#include <locale.h>
|
||||||
|
|
||||||
using namespace spine;
|
using namespace spine;
|
||||||
|
|
||||||
@ -129,6 +130,9 @@ public:
|
|||||||
};
|
};
|
||||||
|
|
||||||
int main(int argc, char *argv[]) {
|
int main(int argc, char *argv[]) {
|
||||||
|
// Set locale to ensure consistent number formatting
|
||||||
|
setlocale(LC_ALL, "C");
|
||||||
|
|
||||||
if (argc < 3) {
|
if (argc < 3) {
|
||||||
fprintf(stderr, "Usage: DebugPrinter <skeleton-path> <atlas-path> [animation-name]\n");
|
fprintf(stderr, "Usage: DebugPrinter <skeleton-path> <atlas-path> [animation-name]\n");
|
||||||
return 1;
|
return 1;
|
||||||
|
|||||||
@ -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.AtlasRegion;
|
||||||
import com.badlogic.gdx.graphics.g2d.TextureAtlas.TextureAtlasData;
|
import com.badlogic.gdx.graphics.g2d.TextureAtlas.TextureAtlasData;
|
||||||
|
|
||||||
|
import java.util.Locale;
|
||||||
|
|
||||||
public class DebugPrinter implements ApplicationListener {
|
public class DebugPrinter implements ApplicationListener {
|
||||||
private String skeletonPath;
|
private String skeletonPath;
|
||||||
private String atlasPath;
|
private String atlasPath;
|
||||||
@ -68,7 +70,7 @@ public class DebugPrinter implements ApplicationListener {
|
|||||||
print(name + ": \"" + value + "\"");
|
print(name + ": \"" + value + "\"");
|
||||||
} else if (value instanceof Float) {
|
} else if (value instanceof Float) {
|
||||||
// Format floats to 6 decimal places to match other runtimes
|
// 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 {
|
} else {
|
||||||
print(name + ": " + value);
|
print(name + ": " + value);
|
||||||
}
|
}
|
||||||
@ -238,7 +240,6 @@ public class DebugPrinter implements ApplicationListener {
|
|||||||
state.apply(skeleton);
|
state.apply(skeleton);
|
||||||
}
|
}
|
||||||
|
|
||||||
skeleton.update(0.016f);
|
|
||||||
skeleton.updateWorldTransform(Physics.update);
|
skeleton.updateWorldTransform(Physics.update);
|
||||||
|
|
||||||
// Print skeleton state
|
// Print skeleton state
|
||||||
|
|||||||
90
tests/README.md
Normal file
90
tests/README.md
Normal 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)
|
||||||
@ -7,6 +7,36 @@ import { promisify } from 'util';
|
|||||||
|
|
||||||
const writeFile = promisify(fs.writeFile);
|
const writeFile = promisify(fs.writeFile);
|
||||||
const mkdir = promisify(fs.mkdir);
|
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
|
// Parse command line arguments
|
||||||
const args = process.argv.slice(2);
|
const args = process.argv.slice(2);
|
||||||
@ -28,7 +58,7 @@ const outputDir = path.join(scriptDir, 'output');
|
|||||||
|
|
||||||
interface RuntimeConfig {
|
interface RuntimeConfig {
|
||||||
name: string;
|
name: string;
|
||||||
buildCheck: () => boolean;
|
buildCheck: () => Promise<boolean>;
|
||||||
build: () => void;
|
build: () => void;
|
||||||
run: () => string;
|
run: () => string;
|
||||||
}
|
}
|
||||||
@ -37,9 +67,18 @@ interface RuntimeConfig {
|
|||||||
const runtimes: RuntimeConfig[] = [
|
const runtimes: RuntimeConfig[] = [
|
||||||
{
|
{
|
||||||
name: 'java',
|
name: 'java',
|
||||||
buildCheck: () => {
|
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/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: () => {
|
build: () => {
|
||||||
console.log(' Building Java runtime...');
|
console.log(' Building Java runtime...');
|
||||||
@ -71,9 +110,18 @@ const runtimes: RuntimeConfig[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'cpp',
|
name: 'cpp',
|
||||||
buildCheck: () => {
|
buildCheck: async () => {
|
||||||
const execPath = path.join(rootDir, 'spine-cpp/build/debug-printer');
|
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: () => {
|
build: () => {
|
||||||
console.log(' Building C++ runtime...');
|
console.log(' Building C++ runtime...');
|
||||||
@ -84,7 +132,9 @@ const runtimes: RuntimeConfig[] = [
|
|||||||
},
|
},
|
||||||
run: () => {
|
run: () => {
|
||||||
return execSync(
|
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'),
|
cwd: path.join(rootDir, 'spine-cpp'),
|
||||||
encoding: 'utf8'
|
encoding: 'utf8'
|
||||||
@ -94,9 +144,18 @@ const runtimes: RuntimeConfig[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'c',
|
name: 'c',
|
||||||
buildCheck: () => {
|
buildCheck: async () => {
|
||||||
const execPath = path.join(rootDir, 'spine-c/build/debug-printer');
|
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: () => {
|
build: () => {
|
||||||
console.log(' Building C runtime...');
|
console.log(' Building C runtime...');
|
||||||
@ -107,7 +166,9 @@ const runtimes: RuntimeConfig[] = [
|
|||||||
},
|
},
|
||||||
run: () => {
|
run: () => {
|
||||||
return execSync(
|
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'),
|
cwd: path.join(rootDir, 'spine-c'),
|
||||||
encoding: 'utf8'
|
encoding: 'utf8'
|
||||||
@ -117,11 +178,17 @@ const runtimes: RuntimeConfig[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'ts',
|
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
|
build: () => {}, // No build needed
|
||||||
run: () => {
|
run: () => {
|
||||||
return execSync(
|
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'),
|
cwd: path.join(rootDir, 'spine-ts/spine-core'),
|
||||||
encoding: 'utf8'
|
encoding: 'utf8'
|
||||||
@ -149,7 +216,7 @@ async function main() {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
// Build if needed
|
// Build if needed
|
||||||
if (!runtime.buildCheck()) {
|
if (!(await runtime.buildCheck())) {
|
||||||
runtime.build();
|
runtime.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user