apex-mutation-testing
Version:
Apex mutation testing plugin
118 lines • 5.14 kB
JavaScript
import { ApexClassRepository } from '../adapter/apexClassRepository.js';
import { ApexTestRunner } from '../adapter/apexTestRunner.js';
import { MutantGenerator } from './mutantGenerator.js';
export class MutationTestingService {
progress;
spinner;
connection;
apexClassName;
apexTestClassName;
constructor(progress, spinner, connection, { apexClassName, apexTestClassName }) {
this.progress = progress;
this.spinner = spinner;
this.connection = connection;
this.apexClassName = apexClassName;
this.apexTestClassName = apexTestClassName;
}
async process() {
const apexClassRepository = new ApexClassRepository(this.connection);
const apexTestRunner = new ApexTestRunner(this.connection);
this.spinner.start(`Fetching "${this.apexClassName}" ApexClass content`, undefined, {
stdout: true,
});
const apexClass = (await apexClassRepository.read(this.apexClassName));
this.spinner.stop('Done');
this.spinner.start(`Computing coverage from "${this.apexTestClassName}" Test class`, undefined, {
stdout: true,
});
const coveredLines = await apexTestRunner.getCoveredLines(this.apexTestClassName);
this.spinner.stop('Done');
this.spinner.start(`Generating mutants for "${this.apexClassName}" ApexClass`, undefined, {
stdout: true,
});
const mutantGenerator = new MutantGenerator();
const mutations = mutantGenerator.compute(apexClass.Body, coveredLines);
const mutationResults = {
sourceFile: this.apexClassName,
sourceFileContent: apexClass.Body,
testFile: this.apexTestClassName,
mutants: [],
};
this.spinner.stop(`${mutations.length} mutations generated`);
this.progress.start(mutations.length, { info: 'Starting mutation testing' }, {
title: 'MUTATION TESTING PROGRESS',
format: '%s | {bar} | {value}/{total} {info}',
});
let mutationCount = 0;
for (const mutation of mutations) {
const mutatedVersion = mutantGenerator.mutate(mutation);
this.progress.update(mutationCount, {
info: `Deploying "${mutation.replacement}" mutation at line ${mutation.token.symbol.line}`,
});
let progressMessage;
try {
await apexClassRepository.update({
Id: apexClass.Id,
Body: mutatedVersion,
});
this.progress.update(mutationCount, {
info: `Running tests for "${mutation.replacement}" mutation at line ${mutation.token.symbol.line}`,
});
const testResult = await apexTestRunner.run(this.apexTestClassName);
const mutantResult = this.buildMutantResult(mutation, testResult);
mutationResults.mutants.push(mutantResult);
progressMessage = `Mutation result: ${testResult.summary.outcome === 'Pass' ? 'zombie' : 'mutant killed'}`;
}
catch {
progressMessage = `Issue while computing "${mutation.replacement}" mutation at line ${mutation.token.symbol.line}`;
}
++mutationCount;
this.progress.update(mutationCount, {
info: progressMessage,
});
}
this.progress.finish({
info: `All mutations evaluated`,
});
try {
this.spinner.start(`Rolling back "${this.apexClassName}" ApexClass to its original state`, undefined, {
stdout: true,
});
await apexClassRepository.update(apexClass);
this.spinner.stop('Done');
}
catch {
this.spinner.stop('Class not rolled back, please do it manually');
}
return mutationResults;
}
calculateScore(mutationResult) {
return ((mutationResult.mutants.filter(mutant => mutant.status === 'Killed')
.length /
mutationResult.mutants.length) *
100 || 0);
}
buildMutantResult(mutation, testResult) {
const token = mutation.token;
// TODO Handle NoCoverage
const mutationStatus = testResult.summary.outcome === 'Pass' ? 'Survived' : 'Killed';
return {
id: `${this.apexClassName}-${token.symbol.line}-${token.symbol.charPositionInLine}-${token.symbol.tokenIndex}-${Date.now()}`,
mutatorName: mutation.mutationName,
status: mutationStatus,
location: {
start: {
line: token.symbol.line,
column: token.symbol.charPositionInLine,
},
end: {
line: token.symbol.line,
column: token.symbol.charPositionInLine + mutation.replacement.length,
},
},
replacement: mutation.replacement,
original: token.text,
};
}
}
//# sourceMappingURL=mutationTestingService.js.map