Browse Source

Remove diff output from test runner - only report file differences

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <[email protected]>
Mario Zechner 2 months ago
parent
commit
ce1fec0cb0
1 changed files with 566 additions and 576 deletions
  1. 566 576
      tests/src/headless-test-runner.ts

+ 566 - 576
tests/src/headless-test-runner.ts

@@ -6,116 +6,116 @@ import { execSync } from 'child_process';
 import { fileURLToPath } from 'url';
 import { fileURLToPath } from 'url';
 
 
 // Logging functions following formatters/logging/README.md style
 // 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);
 const __filename = fileURLToPath(import.meta.url);
@@ -123,514 +123,504 @@ const __dirname = dirname(__filename);
 const SPINE_ROOT = resolve(__dirname, '../..');
 const SPINE_ROOT = resolve(__dirname, '../..');
 
 
 interface TestArgs {
 interface TestArgs {
-    language: string;
-    skeletonPath: string;
-    atlasPath: string;
-    animationName?: string;
+	language: string;
+	skeletonPath: string;
+	atlasPath: string;
+	animationName?: string;
 }
 }
 
 
 interface SkeletonFiles {
 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 <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 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 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 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 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);
-    }
-
-    // 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 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 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 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 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'
-    ];
-
-    // 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']
-    ];
-
-    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}`);
-        }
-    }
-
-    if (allMatch) {
-        log_ok();
-    } else {
-        log_fail();
-        console.error('\n❌ Java and C++ outputs do not match');
-        process.exit(1);
-    }
+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;
+	}
+
+	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']
+	];
+
+	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')}`);
-    }
-
-    // Verify outputs match if we're testing C++
-    if (args.language === 'cpp') {
-        verifyOutputsMatch();
-    }
+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')}`);
+	}
+
+	// Verify outputs match if we're testing C++
+	if (args.language === 'cpp') {
+		verifyOutputsMatch();
+	}
 }
 }
 
 
 if (import.meta.url === `file://${process.argv[1]}`) {
 if (import.meta.url === `file://${process.argv[1]}`) {
-    main();
+	main();
 }
 }