171 lines
5.0 KiB
JavaScript
171 lines
5.0 KiB
JavaScript
|
|
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: /(?<![+])\+(?![+=])/g, replacement: '-' },
|
||
|
|
{ name: 'minus-to-plus', pattern: /(?<!-)\-(?!\-|=)/g, replacement: '+' },
|
||
|
|
{ name: 'true-to-false', pattern: /: true\b/g, replacement: ': false' },
|
||
|
|
{ name: 'false-to-true', pattern: /: false\b/g, replacement: ': true' }
|
||
|
|
]
|
||
|
|
|
||
|
|
function getSourceFiles(pkg) {
|
||
|
|
const srcDir = path.join('packages', pkg, 'src')
|
||
|
|
return globSync('**/*.ts', { cwd: srcDir, absolute: true })
|
||
|
|
.filter(f => !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.')
|