mirror of
https://github.com/EsotericSoftware/spine-runtimes.git
synced 2025-12-20 17:26:01 +08:00
[tests] headless-test-runner.ts with language specific fixes (e.g. icon: null (Java) > icon: "" (C++), C++ JSON will have null as well)
This commit is contained in:
parent
1a10d185a3
commit
ab469cbefd
@ -27,7 +27,14 @@
|
||||
* THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*****************************************************************************/
|
||||
|
||||
#include <cstddef>
|
||||
// Avoid including any standard headers for maximum portability
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
typedef __SIZE_TYPE__ size_t;
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
// Stubs for C++ stdlib functions spine-cpp depends on. Used for nostdcpp builds.
|
||||
// These are weak symbols to allow overriding in custom builds, e.g. headless-test-nostdcpp
|
||||
|
||||
@ -106,14 +106,16 @@ int main(int argc, char *argv[]) {
|
||||
// Create skeleton instance
|
||||
Skeleton skeleton(*skeletonData);
|
||||
|
||||
// Create animation state
|
||||
AnimationStateData stateData(skeletonData);
|
||||
AnimationState state(&stateData);
|
||||
|
||||
skeleton.setupPose();
|
||||
|
||||
// Set animation or setup pose
|
||||
// Set animation if provided
|
||||
AnimationState *state = nullptr;
|
||||
AnimationStateData *stateData = nullptr;
|
||||
if (animationName != nullptr) {
|
||||
// Create animation state only when needed
|
||||
stateData = new AnimationStateData(skeletonData);
|
||||
state = new AnimationState(stateData);
|
||||
|
||||
// Find and set animation
|
||||
Animation *animation = skeletonData->findAnimation(animationName);
|
||||
if (!animation) {
|
||||
@ -122,10 +124,10 @@ int main(int argc, char *argv[]) {
|
||||
delete atlas;
|
||||
return 1;
|
||||
}
|
||||
state.setAnimation(0, animation, true);
|
||||
state->setAnimation(0, animation, true);
|
||||
// Update and apply
|
||||
state.update(0.016f);
|
||||
state.apply(skeleton);
|
||||
state->update(0.016f);
|
||||
state->apply(skeleton);
|
||||
}
|
||||
|
||||
skeleton.updateWorldTransform(Physics_Update);
|
||||
@ -141,11 +143,19 @@ int main(int argc, char *argv[]) {
|
||||
printf("\n=== SKELETON STATE ===\n");
|
||||
printf("%s", serializer.serializeSkeleton(&skeleton).buffer());
|
||||
|
||||
// Print animation state
|
||||
printf("\n=== ANIMATION STATE ===\n");
|
||||
printf("%s", serializer.serializeAnimationState(&state).buffer());
|
||||
// Print animation state only if animation was loaded
|
||||
if (state != nullptr) {
|
||||
printf("\n=== ANIMATION STATE ===\n");
|
||||
printf("%s", serializer.serializeAnimationState(state).buffer());
|
||||
}
|
||||
|
||||
// Cleanup
|
||||
if (state != nullptr) {
|
||||
delete state;
|
||||
}
|
||||
if (stateData != nullptr) {
|
||||
delete stateData;
|
||||
}
|
||||
delete skeletonData;
|
||||
delete atlas;
|
||||
|
||||
|
||||
@ -137,14 +137,15 @@ public class HeadlessTest implements ApplicationListener {
|
||||
// Create skeleton instance
|
||||
Skeleton skeleton = new Skeleton(skeletonData);
|
||||
|
||||
// Create animation state
|
||||
AnimationStateData stateData = new AnimationStateData(skeletonData);
|
||||
AnimationState state = new AnimationState(stateData);
|
||||
|
||||
skeleton.setupPose();
|
||||
|
||||
// Set animation or setup pose
|
||||
// Set animation if provided
|
||||
AnimationState state = null;
|
||||
if (animationName != null) {
|
||||
// Create animation state only when needed
|
||||
AnimationStateData stateData = new AnimationStateData(skeletonData);
|
||||
state = new AnimationState(stateData);
|
||||
|
||||
// Find and set animation
|
||||
Animation animation = skeletonData.findAnimation(animationName);
|
||||
if (animation == null) {
|
||||
@ -163,9 +164,11 @@ public class HeadlessTest implements ApplicationListener {
|
||||
System.out.println("\n=== SKELETON STATE ===");
|
||||
System.out.println(serializer.serializeSkeleton(skeleton));
|
||||
|
||||
// Print animation state as JSON
|
||||
System.out.println("\n=== ANIMATION STATE ===");
|
||||
System.out.println(serializer.serializeAnimationState(state));
|
||||
// Print animation state as JSON only if animation was loaded
|
||||
if (state != null) {
|
||||
System.out.println("\n=== ANIMATION STATE ===");
|
||||
System.out.println(serializer.serializeAnimationState(state));
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
|
||||
567
tests/src/headless-test-runner.ts
Executable file
567
tests/src/headless-test-runner.ts
Executable file
@ -0,0 +1,567 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { existsSync, writeFileSync, mkdirSync, rmSync, statSync } from 'fs';
|
||||
import { resolve, dirname, join } from 'path';
|
||||
import { execSync } from 'child_process';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
// Logging functions following formatters/logging/README.md style
|
||||
function log_title(title: string): void {
|
||||
console.error(`\x1b[1m${title}\x1b[0m`);
|
||||
}
|
||||
|
||||
function log_action(action: string): void {
|
||||
process.stderr.write(` ${action}... `);
|
||||
}
|
||||
|
||||
function log_ok(): void {
|
||||
console.error('\x1b[32m✓\x1b[0m');
|
||||
}
|
||||
|
||||
function log_fail(): void {
|
||||
console.error('\x1b[31m✗\x1b[0m');
|
||||
}
|
||||
|
||||
function log_detail(detail: string): void {
|
||||
console.error(`\x1b[90m${detail}\x1b[0m`);
|
||||
}
|
||||
|
||||
function log_summary(summary: string): void {
|
||||
console.error(`\x1b[1m${summary}\x1b[0m`);
|
||||
}
|
||||
|
||||
function cleanOutputDirectory(): void {
|
||||
const outputDir = join(SPINE_ROOT, 'tests', 'output');
|
||||
|
||||
log_action('Cleaning output directory');
|
||||
try {
|
||||
if (existsSync(outputDir)) {
|
||||
rmSync(outputDir, { recursive: true, force: true });
|
||||
}
|
||||
mkdirSync(outputDir, { recursive: true });
|
||||
log_ok();
|
||||
} catch (error: any) {
|
||||
log_fail();
|
||||
log_detail(`Failed to clean output directory: ${error.message}`);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
function getNewestFileTime(directory: string, pattern: string): number {
|
||||
try {
|
||||
const files = execSync(`find "${directory}" -name "${pattern}" -type f`, { encoding: 'utf8' })
|
||||
.trim()
|
||||
.split('\n')
|
||||
.filter(f => f);
|
||||
|
||||
if (files.length === 0) return 0;
|
||||
|
||||
return Math.max(...files.map(file => {
|
||||
try {
|
||||
return statSync(file).mtime.getTime();
|
||||
} catch {
|
||||
return 0;
|
||||
}
|
||||
}));
|
||||
} catch {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
function needsJavaBuild(): boolean {
|
||||
const javaDir = join(SPINE_ROOT, 'spine-libgdx');
|
||||
const testDir = join(javaDir, 'spine-libgdx-tests');
|
||||
const buildDir = join(testDir, 'build', 'libs');
|
||||
|
||||
try {
|
||||
// Check if jar exists
|
||||
const jarFiles = execSync(`ls ${buildDir}/*.jar 2>/dev/null || true`, { encoding: 'utf8' }).trim();
|
||||
if (!jarFiles) return true;
|
||||
|
||||
// Get jar modification time
|
||||
const jarTime = statSync(jarFiles.split('\n')[0]).mtime.getTime();
|
||||
|
||||
// Check Java source files
|
||||
const javaSourceTime = getNewestFileTime(join(SPINE_ROOT, 'spine-libgdx'), '*.java');
|
||||
|
||||
// Check Gradle files
|
||||
const gradleTime = getNewestFileTime(javaDir, 'build.gradle*');
|
||||
|
||||
return javaSourceTime > jarTime || gradleTime > jarTime;
|
||||
} catch {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
function needsCppBuild(): boolean {
|
||||
const cppDir = join(SPINE_ROOT, 'spine-cpp');
|
||||
const buildDir = join(cppDir, 'build');
|
||||
const headlessTest = join(buildDir, 'headless-test');
|
||||
|
||||
try {
|
||||
// Check if executable exists
|
||||
if (!existsSync(headlessTest)) return true;
|
||||
|
||||
// Get executable modification time
|
||||
const execTime = statSync(headlessTest).mtime.getTime();
|
||||
|
||||
// Check C++ source files
|
||||
const cppSourceTime = getNewestFileTime(join(SPINE_ROOT, 'spine-cpp'), '*.cpp');
|
||||
const hppSourceTime = getNewestFileTime(join(SPINE_ROOT, 'spine-cpp'), '*.h');
|
||||
const cmakeTime = getNewestFileTime(cppDir, 'CMakeLists.txt');
|
||||
|
||||
const newestSourceTime = Math.max(cppSourceTime, hppSourceTime, cmakeTime);
|
||||
|
||||
return newestSourceTime > execTime;
|
||||
} catch {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
const SPINE_ROOT = resolve(__dirname, '../..');
|
||||
|
||||
interface TestArgs {
|
||||
language: string;
|
||||
skeletonPath: string;
|
||||
atlasPath: string;
|
||||
animationName?: string;
|
||||
}
|
||||
|
||||
interface SkeletonFiles {
|
||||
jsonPath: string;
|
||||
skelPath: string;
|
||||
atlasPath: string;
|
||||
}
|
||||
|
||||
function findSkeletonFiles(skeletonName: string): SkeletonFiles {
|
||||
const examplesDir = join(SPINE_ROOT, 'examples', skeletonName);
|
||||
const exportDir = join(examplesDir, 'export');
|
||||
|
||||
if (!existsSync(exportDir)) {
|
||||
log_detail(`Export directory not found: ${exportDir}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Try to find skeleton files (pro first, then ess)
|
||||
let jsonPath: string | null = null;
|
||||
let skelPath: string | null = null;
|
||||
|
||||
const proJson = join(exportDir, `${skeletonName}-pro.json`);
|
||||
const proSkel = join(exportDir, `${skeletonName}-pro.skel`);
|
||||
const essJson = join(exportDir, `${skeletonName}-ess.json`);
|
||||
const essSkel = join(exportDir, `${skeletonName}-ess.skel`);
|
||||
|
||||
if (existsSync(proJson) && existsSync(proSkel)) {
|
||||
jsonPath = proJson;
|
||||
skelPath = proSkel;
|
||||
} else if (existsSync(essJson) && existsSync(essSkel)) {
|
||||
jsonPath = essJson;
|
||||
skelPath = essSkel;
|
||||
} else {
|
||||
log_detail(`Could not find matching .json and .skel files for ${skeletonName}`);
|
||||
log_detail(`Tried: ${proJson}, ${proSkel}, ${essJson}, ${essSkel}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Try to find atlas file (pma first, then regular)
|
||||
let atlasPath: string | null = null;
|
||||
const pmaAtlas = join(exportDir, `${skeletonName}-pma.atlas`);
|
||||
const regularAtlas = join(exportDir, `${skeletonName}.atlas`);
|
||||
|
||||
if (existsSync(pmaAtlas)) {
|
||||
atlasPath = pmaAtlas;
|
||||
} else if (existsSync(regularAtlas)) {
|
||||
atlasPath = regularAtlas;
|
||||
} else {
|
||||
log_detail(`Could not find atlas file for ${skeletonName}`);
|
||||
log_detail(`Tried: ${pmaAtlas}, ${regularAtlas}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
return {
|
||||
jsonPath,
|
||||
skelPath,
|
||||
atlasPath
|
||||
};
|
||||
}
|
||||
|
||||
function validateArgs(): { language: string; files?: SkeletonFiles; skeletonPath?: string; atlasPath?: string; animationName?: string; fixFloats?: boolean } {
|
||||
const args = process.argv.slice(2);
|
||||
|
||||
if (args.length < 2) {
|
||||
log_detail('Usage: headless-test-runner <target-language> -s <skeleton-name> [animation-name] [-f]');
|
||||
log_detail(' headless-test-runner <target-language> <skeleton-path> <atlas-path> [animation-name] [-f]');
|
||||
log_detail('Target languages: cpp');
|
||||
log_detail('Flags: -f (fix floats and propertyIds to match Java values)');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Check for -f flag
|
||||
const fixFloats = args.includes('-f');
|
||||
const filteredArgs = args.filter(arg => arg !== '-f');
|
||||
|
||||
const [language, ...restArgs] = filteredArgs;
|
||||
|
||||
if (!['cpp'].includes(language)) {
|
||||
log_detail(`Invalid target language: ${language}. Must be cpp`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Check if using -s flag
|
||||
if (restArgs[0] === '-s') {
|
||||
if (restArgs.length < 2) {
|
||||
log_detail('Usage: headless-test-runner <target-language> -s <skeleton-name> [animation-name]');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const [, skeletonName, animationName] = restArgs;
|
||||
const files = findSkeletonFiles(skeletonName);
|
||||
|
||||
return {
|
||||
language,
|
||||
files,
|
||||
animationName,
|
||||
fixFloats
|
||||
};
|
||||
} else {
|
||||
// Explicit paths mode
|
||||
if (restArgs.length < 2) {
|
||||
log_detail('Usage: headless-test-runner <target-language> <skeleton-path> <atlas-path> [animation-name]');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const [skeletonPath, atlasPath, animationName] = restArgs;
|
||||
const resolvedSkeletonPath = resolve(skeletonPath);
|
||||
const resolvedAtlasPath = resolve(atlasPath);
|
||||
|
||||
if (!existsSync(resolvedSkeletonPath)) {
|
||||
log_detail(`Skeleton file not found: ${resolvedSkeletonPath}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (!existsSync(resolvedAtlasPath)) {
|
||||
log_detail(`Atlas file not found: ${resolvedAtlasPath}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
return {
|
||||
language,
|
||||
skeletonPath: resolvedSkeletonPath,
|
||||
atlasPath: resolvedAtlasPath,
|
||||
animationName,
|
||||
fixFloats
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function executeJava(args: TestArgs): string {
|
||||
const javaDir = join(SPINE_ROOT, 'spine-libgdx');
|
||||
const testDir = join(javaDir, 'spine-libgdx-tests');
|
||||
|
||||
if (!existsSync(testDir)) {
|
||||
log_detail(`Java test directory not found: ${testDir}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Check if we need to build
|
||||
if (needsJavaBuild()) {
|
||||
// Check if we have a gradle wrapper or gradle
|
||||
const hasGradlew = existsSync(join(javaDir, 'gradlew'));
|
||||
const gradleCmd = hasGradlew ? './gradlew' : 'gradle';
|
||||
|
||||
// Build the fat jar
|
||||
log_action('Building Java HeadlessTest fat jar');
|
||||
try {
|
||||
execSync(`${gradleCmd} :spine-libgdx-tests:fatJar`, {
|
||||
cwd: javaDir,
|
||||
stdio: ['inherit', 'pipe', 'inherit']
|
||||
});
|
||||
log_ok();
|
||||
} catch (error: any) {
|
||||
log_fail();
|
||||
log_detail(`Java build failed: ${error.message}`);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// Find the jar file
|
||||
const buildDir = join(testDir, 'build', 'libs');
|
||||
const jarFiles = execSync(`ls ${buildDir}/*.jar`, { encoding: 'utf8' }).trim().split('\n');
|
||||
|
||||
if (jarFiles.length === 0) {
|
||||
log_detail('No jar files found in build/libs directory');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const jarFile = jarFiles[0]; // Use the first jar file found
|
||||
|
||||
// Run the HeadlessTest from jar
|
||||
const testArgs = [args.skeletonPath, args.atlasPath];
|
||||
if (args.animationName) {
|
||||
testArgs.push(args.animationName);
|
||||
}
|
||||
|
||||
log_action('Running Java HeadlessTest');
|
||||
try {
|
||||
const output = execSync(`java -cp "${jarFile}" com.esotericsoftware.spine.HeadlessTest ${testArgs.join(' ')}`, {
|
||||
encoding: 'utf8',
|
||||
maxBuffer: 50 * 1024 * 1024 // 50MB buffer for large output
|
||||
});
|
||||
log_ok();
|
||||
return output;
|
||||
} catch (error: any) {
|
||||
log_fail();
|
||||
log_detail(`Java execution failed: ${error.message}`);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
function executeCpp(args: TestArgs): string {
|
||||
const cppDir = join(SPINE_ROOT, 'spine-cpp');
|
||||
const testsDir = join(cppDir, 'tests');
|
||||
|
||||
if (!existsSync(testsDir)) {
|
||||
log_detail(`C++ tests directory not found: ${testsDir}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Check if we need to build
|
||||
if (needsCppBuild()) {
|
||||
// Build using build.sh
|
||||
log_action('Building C++ tests');
|
||||
try {
|
||||
execSync('./build.sh clean release', {
|
||||
cwd: cppDir,
|
||||
stdio: ['inherit', 'pipe', 'inherit']
|
||||
});
|
||||
log_ok();
|
||||
} catch (error: any) {
|
||||
log_fail();
|
||||
log_detail(`C++ build failed: ${error.message}`);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// Now run the headless test directly
|
||||
const testArgs = [args.skeletonPath, args.atlasPath];
|
||||
if (args.animationName) {
|
||||
testArgs.push(args.animationName);
|
||||
}
|
||||
|
||||
const buildDir = join(cppDir, 'build');
|
||||
const headlessTest = join(buildDir, 'headless-test');
|
||||
|
||||
if (!existsSync(headlessTest)) {
|
||||
log_detail(`C++ headless-test executable not found: ${headlessTest}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
log_action('Running C++ HeadlessTest');
|
||||
try {
|
||||
const output = execSync(`${headlessTest} ${testArgs.join(' ')}`, {
|
||||
encoding: 'utf8',
|
||||
maxBuffer: 50 * 1024 * 1024 // 50MB buffer for large output
|
||||
});
|
||||
log_ok();
|
||||
return output;
|
||||
} catch (error: any) {
|
||||
log_fail();
|
||||
log_detail(`C++ execution failed: ${error.message}`);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
function parseOutput(output: string): { skeletonData: any, skeletonState: any, animationState?: any } {
|
||||
// Split output into sections
|
||||
const sections = output.split(/=== [A-Z ]+? ===/);
|
||||
|
||||
if (sections.length < 3) {
|
||||
log_detail(`Expected at least 2 sections in output, got: ${sections.length - 1}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
try {
|
||||
const result: { skeletonData: any, skeletonState: any, animationState?: any } = {
|
||||
skeletonData: JSON.parse(sections[1].trim()),
|
||||
skeletonState: JSON.parse(sections[2].trim())
|
||||
};
|
||||
|
||||
// Animation state is optional (only present if animation was loaded)
|
||||
if (sections.length >= 4 && sections[3].trim()) {
|
||||
result.animationState = JSON.parse(sections[3].trim());
|
||||
}
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
log_detail(`Failed to parse JSON output: ${error}`);
|
||||
log_detail(`Section lengths: ${sections.map(s => s.length)}`);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
function fixFloatsAndPropertyIds(target: any, reference: any, path: string = '', targetLanguage: string = 'cpp'): any {
|
||||
// Handle null values
|
||||
if (reference === null || target === null) return target;
|
||||
|
||||
// Handle circular references
|
||||
if (reference === '<circular>' || target === '<circular>') return target;
|
||||
|
||||
// Handle arrays
|
||||
if (Array.isArray(reference) && Array.isArray(target)) {
|
||||
return target.map((item: any, index: number) => {
|
||||
if (index < reference.length) {
|
||||
return fixFloatsAndPropertyIds(item, reference[index], `${path}[${index}]`, targetLanguage);
|
||||
}
|
||||
return item;
|
||||
});
|
||||
}
|
||||
|
||||
// Handle objects
|
||||
if (typeof reference === 'object' && typeof target === 'object') {
|
||||
const result: any = {};
|
||||
|
||||
// Copy all target properties
|
||||
for (const key in target) {
|
||||
if (target.hasOwnProperty(key)) {
|
||||
if (reference.hasOwnProperty(key)) {
|
||||
// Special cases where we always use Java value
|
||||
if (key === 'propertyIds') {
|
||||
// PropertyIds are encoded differently across all languages
|
||||
result[key] = reference[key];
|
||||
} else if (targetLanguage === 'cpp' && (
|
||||
(reference[key] === null && target[key] === '') ||
|
||||
(reference[key] === '' && target[key] === null) ||
|
||||
(key === 'bones' && reference[key] === null && Array.isArray(target[key]) && target[key].length === 0)
|
||||
)) {
|
||||
// C++ specific fixes: null vs empty string, null vs empty array for bones
|
||||
result[key] = reference[key];
|
||||
} else {
|
||||
result[key] = fixFloatsAndPropertyIds(target[key], reference[key], `${path}.${key}`, targetLanguage);
|
||||
}
|
||||
} else {
|
||||
result[key] = target[key];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add any missing properties from reference (shouldn't happen, but defensive)
|
||||
for (const key in reference) {
|
||||
if (reference.hasOwnProperty(key) && !result.hasOwnProperty(key)) {
|
||||
result[key] = reference[key];
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// Handle primitive values
|
||||
if (typeof reference === 'number' && typeof target === 'number') {
|
||||
// Check if both are floats and within epsilon
|
||||
const diff = Math.abs(reference - target);
|
||||
if (diff <= 0.001 && diff > 0) {
|
||||
return reference; // Use Java value
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return target;
|
||||
}
|
||||
|
||||
function saveJsonFiles(args: TestArgs, parsed: any, javaParsed?: any, fixFloats?: boolean): void {
|
||||
// Ensure output directory exists
|
||||
const outputDir = join(SPINE_ROOT, 'tests', 'output');
|
||||
if (!existsSync(outputDir)) {
|
||||
mkdirSync(outputDir, { recursive: true });
|
||||
}
|
||||
|
||||
// Determine file format from skeleton path
|
||||
const format = args.skeletonPath.endsWith('.json') ? 'json' : 'skel';
|
||||
|
||||
// Apply float fixing if enabled and we have Java reference data
|
||||
let outputData = parsed;
|
||||
if (fixFloats && javaParsed && args.language !== 'java') {
|
||||
log_action('Fixing floats and propertyIds to match Java values');
|
||||
outputData = {
|
||||
skeletonData: fixFloatsAndPropertyIds(parsed.skeletonData, javaParsed.skeletonData, '', args.language),
|
||||
skeletonState: fixFloatsAndPropertyIds(parsed.skeletonState, javaParsed.skeletonState, '', args.language),
|
||||
animationState: parsed.animationState && javaParsed.animationState ?
|
||||
fixFloatsAndPropertyIds(parsed.animationState, javaParsed.animationState, '', args.language) : parsed.animationState
|
||||
};
|
||||
log_ok();
|
||||
}
|
||||
|
||||
// Save files with language and format in filename
|
||||
writeFileSync(join(outputDir, `skeleton-data-${args.language}-${format}.json`), JSON.stringify(outputData.skeletonData, null, 2));
|
||||
writeFileSync(join(outputDir, `skeleton-state-${args.language}-${format}.json`), JSON.stringify(outputData.skeletonState, null, 2));
|
||||
|
||||
// Only save animation state if it exists
|
||||
if (outputData.animationState) {
|
||||
writeFileSync(join(outputDir, `animation-state-${args.language}-${format}.json`), JSON.stringify(outputData.animationState, null, 2));
|
||||
}
|
||||
}
|
||||
|
||||
function runTestsForFiles(language: string, skeletonPath: string, atlasPath: string, animationName?: string, fixFloats?: boolean): void {
|
||||
const testArgs: TestArgs = {
|
||||
language,
|
||||
skeletonPath,
|
||||
atlasPath,
|
||||
animationName
|
||||
};
|
||||
|
||||
const fileType = skeletonPath.endsWith('.json') ? 'JSON' : 'BINARY';
|
||||
const fileName = skeletonPath.split('/').pop() || skeletonPath;
|
||||
|
||||
log_detail(`Testing ${fileType}: ${fileName}`);
|
||||
|
||||
// Always run Java first (reference implementation)
|
||||
const javaOutput = executeJava(testArgs);
|
||||
const javaParsed = parseOutput(javaOutput);
|
||||
saveJsonFiles({ ...testArgs, language: 'java' }, javaParsed);
|
||||
|
||||
// Run target language
|
||||
let targetOutput: string;
|
||||
if (language === 'cpp') {
|
||||
targetOutput = executeCpp(testArgs);
|
||||
} else {
|
||||
log_detail(`Unsupported target language: ${language}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const targetParsed = parseOutput(targetOutput);
|
||||
saveJsonFiles(testArgs, targetParsed, javaParsed, fixFloats);
|
||||
}
|
||||
|
||||
function main(): void {
|
||||
const args = validateArgs();
|
||||
|
||||
log_title('Spine Runtime Test');
|
||||
|
||||
// Clean output directory first
|
||||
cleanOutputDirectory();
|
||||
|
||||
if (args.files) {
|
||||
// Auto-discovery mode: run tests for both JSON and binary files
|
||||
const jsonFile = args.files.jsonPath.split('/').pop() || args.files.jsonPath;
|
||||
const skelFile = args.files.skelPath.split('/').pop() || args.files.skelPath;
|
||||
const atlasFile = args.files.atlasPath.split('/').pop() || args.files.atlasPath;
|
||||
|
||||
log_detail(`Files: ${jsonFile}, ${skelFile}, ${atlasFile}`);
|
||||
|
||||
runTestsForFiles(args.language, args.files.jsonPath, args.files.atlasPath, args.animationName, args.fixFloats);
|
||||
runTestsForFiles(args.language, args.files.skelPath, args.files.atlasPath, args.animationName, args.fixFloats);
|
||||
|
||||
log_summary('✓ All tests completed');
|
||||
log_detail(`JSON files saved to: ${join(SPINE_ROOT, 'tests', 'output')}`);
|
||||
} else {
|
||||
// Explicit paths mode: run test for single file
|
||||
runTestsForFiles(args.language, args.skeletonPath!, args.atlasPath!, args.animationName, args.fixFloats);
|
||||
log_summary('✓ Test completed');
|
||||
log_detail(`JSON files saved to: ${join(SPINE_ROOT, 'tests', 'output')}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (import.meta.url === `file://${process.argv[1]}`) {
|
||||
main();
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user