mirror of
https://github.com/EsotericSoftware/spine-runtimes.git
synced 2025-12-21 01:36: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
|
||||
docs/spine-runtimes-types.md
|
||||
spine-c/codegen/dist
|
||||
tests/output
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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
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 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();
|
||||
}
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user