From ce1fec0cb026d2d3e6c1c5b8e70d5e7e34dcf94a Mon Sep 17 00:00:00 2001 From: Mario Zechner Date: Mon, 21 Jul 2025 12:11:15 +0200 Subject: [PATCH] Remove diff output from test runner - only report file differences MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- tests/src/headless-test-runner.ts | 1108 ++++++++++++++--------------- 1 file changed, 549 insertions(+), 559 deletions(-) diff --git a/tests/src/headless-test-runner.ts b/tests/src/headless-test-runner.ts index cb40cb177..e4527c502 100755 --- a/tests/src/headless-test-runner.ts +++ b/tests/src/headless-test-runner.ts @@ -6,116 +6,116 @@ 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_title (title: string): void { + console.error(`\x1b[1m${title}\x1b[0m`); } -function log_action(action: string): void { - process.stderr.write(` ${action}... `); +function log_action (action: string): void { + process.stderr.write(` ${action}... `); } -function log_ok(): void { - console.error('\x1b[32m✓\x1b[0m'); +function log_ok (): void { + console.error('\x1b[32m✓\x1b[0m'); } -function log_fail(): void { - console.error('\x1b[31m✗\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_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 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 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 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 fat jar exists (specifically look for the headless test jar) - const fatJarFiles = execSync(`ls ${buildDir}/spine-headless-test-*.jar 2>/dev/null || true`, { encoding: 'utf8' }).trim(); - if (!fatJarFiles) return true; - - // Get jar modification time - const jarTime = statSync(fatJarFiles.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 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 fat jar exists (specifically look for the headless test jar) + const fatJarFiles = execSync(`ls ${buildDir}/spine-headless-test-*.jar 2>/dev/null || true`, { encoding: 'utf8' }).trim(); + if (!fatJarFiles) return true; + + // Get jar modification time + const jarTime = statSync(fatJarFiles.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; - } +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); @@ -123,514 +123,504 @@ const __dirname = dirname(__filename); const SPINE_ROOT = resolve(__dirname, '../..'); interface TestArgs { - language: string; - skeletonPath: string; - atlasPath: string; - animationName?: string; + language: string; + skeletonPath: string; + atlasPath: string; + animationName?: string; } interface SkeletonFiles { - jsonPath: string; - skelPath: string; - atlasPath: string; + 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 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 -s [animation-name] [-f]'); - log_detail(' headless-test-runner [animation-name] [-f]'); - log_detail('Target languages: cpp'); - log_detail('Flags: -f (fix floats and propertyIds to match Java values)'); - process.exit(1); - } +function validateArgs (): { language: string; files?: SkeletonFiles; skeletonPath?: string; atlasPath?: string; animationName?: string; fixFloats?: boolean } { + const args = process.argv.slice(2); - // 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); - } + if (args.length < 2) { + log_detail('Usage: headless-test-runner -s [animation-name] [-f]'); + log_detail(' headless-test-runner [animation-name] [-f]'); + log_detail('Target languages: cpp'); + log_detail('Flags: -f (fix floats and propertyIds to match Java values)'); + process.exit(1); + } - // Check if using -s flag - if (restArgs[0] === '-s') { - if (restArgs.length < 2) { - log_detail('Usage: headless-test-runner -s [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 [animation-name]'); - process.exit(1); - } - - const [skeletonPath, atlasPath, animationName] = restArgs; - const resolvedSkeletonPath = resolve(skeletonPath); - const resolvedAtlasPath = resolve(atlasPath); + // Check for -f flag + const fixFloats = args.includes('-f'); + const filteredArgs = args.filter(arg => arg !== '-f'); - if (!existsSync(resolvedSkeletonPath)) { - log_detail(`Skeleton file not found: ${resolvedSkeletonPath}`); - process.exit(1); - } + const [language, ...restArgs] = filteredArgs; - if (!existsSync(resolvedAtlasPath)) { - log_detail(`Atlas file not found: ${resolvedAtlasPath}`); - process.exit(1); - } + if (!['cpp'].includes(language)) { + log_detail(`Invalid target language: ${language}. Must be cpp`); + process.exit(1); + } - return { - language, - skeletonPath: resolvedSkeletonPath, - atlasPath: resolvedAtlasPath, - animationName, - fixFloats - }; - } + // Check if using -s flag + if (restArgs[0] === '-s') { + if (restArgs.length < 2) { + log_detail('Usage: headless-test-runner -s [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 [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); - } +function executeJava (args: TestArgs): string { + const javaDir = join(SPINE_ROOT, 'spine-libgdx'); + const testDir = join(javaDir, 'spine-libgdx-tests'); - // 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); - } - } + if (!existsSync(testDir)) { + log_detail(`Java test directory not found: ${testDir}`); + process.exit(1); + } - // Find the fat jar file - const buildDir = join(testDir, 'build', 'libs'); - const fatJarFiles = execSync(`ls ${buildDir}/spine-headless-test-*.jar`, { encoding: 'utf8' }).trim().split('\n'); - - if (fatJarFiles.length === 0) { - log_detail('No fat jar files found in build/libs directory'); - process.exit(1); - } - - const jarFile = fatJarFiles[0]; // Use the first fat jar file found - - // Run the HeadlessTest from jar - const testArgs = [args.skeletonPath, args.atlasPath]; - if (args.animationName) { - testArgs.push(args.animationName); - } + // 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'; - 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); - } + // 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 fat jar file + const buildDir = join(testDir, 'build', 'libs'); + const fatJarFiles = execSync(`ls ${buildDir}/spine-headless-test-*.jar`, { encoding: 'utf8' }).trim().split('\n'); + + if (fatJarFiles.length === 0) { + log_detail('No fat jar files found in build/libs directory'); + process.exit(1); + } + + const jarFile = fatJarFiles[0]; // Use the first fat 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); - } +function executeCpp (args: TestArgs): string { + const cppDir = join(SPINE_ROOT, 'spine-cpp'); + const testsDir = join(cppDir, 'tests'); - // 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); - } - } + if (!existsSync(testsDir)) { + log_detail(`C++ tests directory not found: ${testsDir}`); + process.exit(1); + } - // Now run the headless test directly - const testArgs = [args.skeletonPath, args.atlasPath]; - if (args.animationName) { - testArgs.push(args.animationName); - } + // 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); + } + } - 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); - } + // Now run the headless test directly + const testArgs = [args.skeletonPath, args.atlasPath]; + if (args.animationName) { + testArgs.push(args.animationName); + } - 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); - } + 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 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 === '' || target === '') 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 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 === '' || target === '') 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 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 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 verifyOutputsMatch(): void { - const outputDir = join(SPINE_ROOT, 'tests', 'output'); - const outputFiles = [ - 'skeleton-data-java-json.json', - 'skeleton-data-cpp-json.json', - 'skeleton-state-java-json.json', - 'skeleton-state-cpp-json.json', - 'skeleton-data-java-skel.json', - 'skeleton-data-cpp-skel.json', - 'skeleton-state-java-skel.json', - 'skeleton-state-cpp-skel.json' - ]; +function verifyOutputsMatch (): void { + const outputDir = join(SPINE_ROOT, 'tests', 'output'); + const outputFiles = [ + 'skeleton-data-java-json.json', + 'skeleton-data-cpp-json.json', + 'skeleton-state-java-json.json', + 'skeleton-state-cpp-json.json', + 'skeleton-data-java-skel.json', + 'skeleton-data-cpp-skel.json', + 'skeleton-state-java-skel.json', + 'skeleton-state-cpp-skel.json' + ]; - // Check if all files exist - const missingFiles = outputFiles.filter(file => !existsSync(join(outputDir, file))); - if (missingFiles.length > 0) { - log_detail(`Skipping diff check - missing files: ${missingFiles.join(', ')}`); - return; - } + // Check if all files exist + const missingFiles = outputFiles.filter(file => !existsSync(join(outputDir, file))); + if (missingFiles.length > 0) { + log_detail(`Skipping diff check - missing files: ${missingFiles.join(', ')}`); + return; + } - log_action('Verifying Java and C++ outputs match'); - - const comparisons = [ - ['skeleton-data-java-json.json', 'skeleton-data-cpp-json.json'] - // TODO: Add binary file comparison once C++ binary parsing is fixed - // ['skeleton-data-java-skel.json', 'skeleton-data-cpp-skel.json'] - ]; + log_action('Verifying Java and C++ outputs match'); - let allMatch = true; - - for (const [javaFile, cppFile] of comparisons) { - try { - const javaContent = execSync(`cat "${join(outputDir, javaFile)}"`, { encoding: 'utf8' }); - const cppContent = execSync(`cat "${join(outputDir, cppFile)}"`, { encoding: 'utf8' }); - - if (javaContent !== cppContent) { - allMatch = false; - console.error(`\n❌ Files differ: ${javaFile} vs ${cppFile}`); - - // Show diff using system diff command - try { - execSync(`diff "${join(outputDir, javaFile)}" "${join(outputDir, cppFile)}"`, { - stdio: 'inherit', - cwd: outputDir - }); - } catch { - // diff exits with code 1 when files differ, which is expected - } - } - } catch (error: any) { - allMatch = false; - console.error(`\n❌ Error comparing ${javaFile} vs ${cppFile}: ${error.message}`); - } - } + const comparisons = [ + ['skeleton-data-java-json.json', 'skeleton-data-cpp-json.json'] + // TODO: Add binary file comparison once C++ binary parsing is fixed + // ['skeleton-data-java-skel.json', 'skeleton-data-cpp-skel.json'] + ]; - if (allMatch) { - log_ok(); - } else { - log_fail(); - console.error('\n❌ Java and C++ outputs do not match'); - process.exit(1); - } + let allMatch = true; + + for (const [javaFile, cppFile] of comparisons) { + try { + const javaContent = execSync(`cat "${join(outputDir, javaFile)}"`, { encoding: 'utf8' }); + const cppContent = execSync(`cat "${join(outputDir, cppFile)}"`, { encoding: 'utf8' }); + + if (javaContent !== cppContent) { + allMatch = false; + console.error(`\n❌ Files differ: ${javaFile} vs ${cppFile}`); + } + } catch (error: any) { + allMatch = false; + console.error(`\n❌ Error comparing ${javaFile} vs ${cppFile}: ${error.message}`); + } + } + + if (allMatch) { + log_ok(); + } else { + log_fail(); + console.error('\n❌ Java and C++ outputs do not match'); + process.exit(1); + } } -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')}`); - } +function main (): void { + const args = validateArgs(); - // Verify outputs match if we're testing C++ - if (args.language === 'cpp') { - verifyOutputsMatch(); - } + 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')}`); + } + + // Verify outputs match if we're testing C++ + if (args.language === 'cpp') { + verifyOutputsMatch(); + } } if (import.meta.url === `file://${process.argv[1]}`) { - main(); + main(); } \ No newline at end of file