import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'fs' import { execSync } from 'child_process' import { globSync } from 'glob' import path from 'path' const PACKAGES = [ 'imhotep-solver', 'imhotep-geometry', 'imhotep-topology', 'imhotep-dsl' ] const MUTATIONS = [ { name: 'reverse-equality', pattern: /===/g, replacement: '!==' }, { name: 'reverse-inequality', pattern: /!==/g, replacement: '===' }, { name: 'gt-to-gte', pattern: /(?(?!=)/g, replacement: '>=' }, { name: 'lt-to-lte', pattern: /(?)<(?!-)/g, replacement: '<=' }, { name: 'gte-to-gt', pattern: />=/g, replacement: '>' }, { name: 'lte-to-lt', pattern: /<=/g, replacement: '<' }, { name: 'and-to-or', pattern: /&&/g, replacement: '||' }, { name: 'or-to-and', pattern: /\|\|/g, replacement: '&&' }, { name: 'plus-to-minus', pattern: /(? !f.endsWith('.test.ts')) } function runTests(pkg) { try { execSync('npm test', { cwd: path.join('packages', pkg), stdio: 'pipe', timeout: 30000 }) return { passed: true } } catch (e) { return { passed: false, output: e.stdout?.toString() || e.message } } } function build(pkg) { try { execSync('npm run build', { cwd: path.join('packages', pkg), stdio: 'pipe', timeout: 60000 }) return true } catch (e) { return false } } function applyMutation(filePath, mutation, index) { const content = readFileSync(filePath, 'utf8') const matches = [...content.matchAll(mutation.pattern)] if (matches.length === 0) return null if (index >= matches.length) return null const match = matches[index] const before = content.slice(0, match.index) const after = content.slice(match.index + match[0].length) const mutated = before + mutation.replacement + after writeFileSync(filePath, mutated) return { line: content.slice(0, match.index).split('\n').length, original: match[0], mutated: mutation.replacement } } function restoreFile(filePath, original) { writeFileSync(filePath, original) } const results = {} for (const pkg of PACKAGES) { console.log(`\n=== Package: ${pkg} ===`) const files = getSourceFiles(pkg) const pkgResults = { total: 0, killed: 0, survived: 0, timedOut: 0, mutants: [] } for (const filePath of files) { const content = readFileSync(filePath, 'utf8') for (const mutation of MUTATIONS) { const matches = [...content.matchAll(mutation.pattern)] for (let i = 0; i < matches.length; i++) { const info = applyMutation(filePath, mutation, i) if (!info) continue pkgResults.total++ const shortFile = path.relative(process.cwd(), filePath) process.stdout.write(` Mutant ${pkgResults.total}: ${shortFile}:${info.line} ${info.original} -> ${info.mutated} `) const built = build(pkg) if (!built) { pkgResults.timedOut++ console.log('BUILD ERROR') restoreFile(filePath, content) continue } const testResult = runTests(pkg) if (!testResult.passed) { pkgResults.killed++ console.log('KILLED') } else { pkgResults.survived++ console.log('SURVIVED') pkgResults.mutants.push({ file: shortFile, line: info.line, mutation: `${info.original} -> ${info.mutated}`, type: mutation.name }) } restoreFile(filePath, content) } } } const score = pkgResults.total > 0 ? Math.round((pkgResults.killed / pkgResults.total) * 100) : 0 pkgResults.score = score results[pkg] = pkgResults console.log(`\n Score: ${score}% (${pkgResults.killed}/${pkgResults.total})`) console.log(` Survived: ${pkgResults.survived}, TimedOut: ${pkgResults.timedOut}`) } console.log('\n\n========== MUTATION TESTING SUMMARY ==========') console.log('| Package | Score | Survived | Killed | Timeout |') console.log('|---------|-------|----------|--------|---------|') for (const pkg of PACKAGES) { const r = results[pkg] console.log(`| ${pkg.padEnd(18)} | ${String(r.score).padStart(3)}% | ${String(r.survived).padStart(8)} | ${String(r.killed).padStart(6)} | ${String(r.timedOut).padStart(7)} |`) } console.log('\n\n========== SURVIVING MUTANTS ==========') for (const pkg of PACKAGES) { const r = results[pkg] if (r.mutants.length === 0) continue console.log(`\n--- ${pkg} ---`) for (const m of r.mutants) { console.log(` ${m.file}:${m.line} ${m.mutation} (${m.type})`) } } console.log('\n\nDone.')