├── .gitignore ├── src ├── reporter │ ├── html │ │ ├── templates │ │ │ ├── baseScript.hbs │ │ │ ├── folder.hbs │ │ │ ├── file.hbs │ │ │ ├── folderFileRow.hbs │ │ │ ├── folderStyle.css │ │ │ ├── fileStyle.css │ │ │ ├── baseStyle.hbs │ │ │ ├── base.hbs │ │ │ └── fileScript.js │ │ ├── StatUtils.js │ │ ├── Templates.js │ │ ├── HtmlReporter.js │ │ ├── FileHtmlBuilder.js │ │ └── HtmlFormatter.js │ ├── TextFileReporter.js │ └── ReportGenerator.js ├── childNodeFinder │ ├── LeftRightCNF.js │ ├── CallExpressionCNF.js │ ├── IterationCNF.js │ ├── PropertyCNF.js │ ├── ForLoopCNF.js │ ├── ArrayCNF.js │ └── ChildNodeFinder.js ├── TestStatus.js ├── utils │ ├── LiteralUtils.js │ ├── MutationUtils.js │ ├── PromiseUtils.js │ ├── CopyUtils.js │ ├── ExclusionUtils.js │ └── ArrayMutatorUtil.js ├── mutationOperator │ ├── MutationOperator.js │ ├── CallExpressionMO.js │ ├── UnaryExpressionMO.js │ ├── ArrayExpressionMO.js │ ├── LogicalExpressionMO.js │ ├── BlockStatementMO.js │ ├── ObjectMO.js │ ├── CallExpressionArgsMO.js │ ├── LiteralMO.js │ ├── ArithmeticOperatorMO.js │ ├── EqualityOperatorMO.js │ ├── CallExpressionSelfMO.js │ ├── ComparisonOperatorMO.js │ └── UpdateExpressionMO.js ├── JSParserWrapper.js ├── MutationOperatorHandler.js ├── MutationScoreCalculator.js ├── TestRunner.js ├── Mutator.js ├── MutationAnalyser.js ├── MutationTester.js ├── MutationFileTester.js └── MutationOperatorWarden.js ├── test ├── e2e │ ├── code │ │ ├── arguments.js │ │ ├── mathoperators.js │ │ └── endlessLoop.js │ ├── tests │ │ ├── arguments-test.js │ │ ├── endlessLoop-test.js │ │ └── mathoperators-test.js │ └── expected │ │ └── arguments.txt ├── unit │ ├── childNodeFinder │ │ ├── ArrayCNFSpec.js │ │ ├── IterationCNFSpec.js │ │ ├── ForLoopCNFSpec.js │ │ ├── ChildNodeFinderSpec.js │ │ ├── LeftRightCNFSpec.js │ │ ├── CallExpressionCNFSpec.js │ │ └── PropertyCNFSpec.js │ ├── mutationOperator │ │ ├── MutationOperatorSpec.js │ │ ├── ArithmeticOperatorMOSpec.js │ │ ├── UnaryExpressionMOSpec.js │ │ ├── ObjectMOSpec.js │ │ ├── EqualityOperatorMOSpec.js │ │ ├── BlockStatementMOSpec.js │ │ ├── LogicalExpressionMOSpec.js │ │ ├── ComparisonOperatorMOSpec.js │ │ ├── CallExpressionSelfMOSpec.js │ │ ├── LiteralMOSpec.js │ │ ├── CallExpressionArgsMOSpec.js │ │ ├── UpdateExpressionMOSpec.js │ │ ├── ArrayExpressionMOSpec.js │ │ └── CallExpressionMOSpec.js │ ├── utils │ │ ├── LiteralUtilsSpec.js │ │ ├── MutationUtilsSpec.js │ │ ├── PromiseUtilsSpec.js │ │ ├── ExclusionUtilsSpec.js │ │ └── ArrayMutatorUtilSpec.js │ ├── reporter │ │ ├── html │ │ │ ├── StatsUtilsSpec.js │ │ │ └── HtmlReporterSpec.js │ │ └── ReportGeneratorSpec.js │ ├── MutationScoreCalculatorSpec.js │ ├── MutationOperatorHandlerSpec.js │ ├── MutatorSpec.js │ ├── MutationAnalyserSpec.js │ ├── MutationTesterSpec.js │ ├── MutationOperatorWardenSpec.js │ ├── MutationFileTesterSpec.js │ ├── MutationConfigurationSpec.js │ └── JSParseWrapperSpec.js ├── .jshintrc └── util │ ├── DummyPromiseResolver.js │ └── JasmineCustomMatchers.js ├── gulp ├── build.js ├── codestyle.js └── test.js ├── .jshintrc ├── gulpfile.js ├── LICENSE └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | *.iml 3 | *.log 4 | node_modules 5 | coverage -------------------------------------------------------------------------------- /src/reporter/html/templates/baseScript.hbs: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/reporter/TextFileReporter.js: -------------------------------------------------------------------------------- 1 | /** 2 | * reports mutation results to a text file 3 | * Created by martin on 25/11/15. 4 | */ 5 | (function(module) { 6 | 'use strict'; 7 | 8 | 9 | })(module); -------------------------------------------------------------------------------- /test/e2e/code/arguments.js: -------------------------------------------------------------------------------- 1 | var _ = require('lodash'); 2 | 3 | function containsName(persons, name) { 4 | return _.contains(_.pluck(persons, 'name'), name); 5 | } 6 | 7 | exports.containsName = containsName; 8 | -------------------------------------------------------------------------------- /test/e2e/tests/arguments-test.js: -------------------------------------------------------------------------------- 1 | var args = require("./../code/arguments"); 2 | 3 | describe('Arguments', function () { 4 | it('containsName', function () { 5 | var result = args.containsName([ 6 | {name: 'Nora'}, 7 | {name: 'Marco'} 8 | ], 'Steve'); 9 | expect(result).toBeFalsy(); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /test/e2e/expected/arguments.txt: -------------------------------------------------------------------------------- 1 | mocha/arguments.js:4:23 Replaced _.pluck(persons, 'name') with "MUTATION!" -> SURVIVED 2 | mocha/arguments.js:4:49 Replaced name with "MUTATION!" -> SURVIVED 3 | mocha/arguments.js:4:31 Replaced persons with "MUTATION!" -> SURVIVED 4 | mocha/arguments.js:4:40 Replaced 'name' with "MUTATION!" -> SURVIVED 5 | 6 of 10 unignored mutations are tested (60%). 2 mutations were ignored. 6 | -------------------------------------------------------------------------------- /gulp/build.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * build the file 5 | * @author Martin Koster [paysdoc@gmail.com], created on 21/06/15. 6 | * Licensed under the MIT license. 7 | */ 8 | var gulp = require('gulp'), 9 | concat = require('gulp-concat'); 10 | gulp.task('build', function() { 11 | 12 | gulp.src('src/**/*.js') 13 | .pipe(concat('js-mutation-test.js')) 14 | .pipe(gulp.dest('./build/')); 15 | }); -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "strict": true, 3 | "curly": true, 4 | "eqeqeq": true, 5 | "immed": true, 6 | "latedef": "nofunc", 7 | "newcap": true, 8 | "noarg": true, 9 | "sub": true, 10 | "undef": true, 11 | "boss": true, 12 | "eqnull": true, 13 | "node": true, 14 | "expr": true, 15 | "globals": { 16 | "require": false, 17 | "describe": false, 18 | "document": false, 19 | "module": false 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | /** 3 | * main build 4 | * @author Martin Koster [paysdoc@gmail.com], created on 21/06/15. 5 | * Licensed under the MIT license. 6 | */ 7 | var gulp = require('gulp'), 8 | del = require('del'); 9 | 10 | require('require-dir')('./gulp'); 11 | gulp.task('clean', function() { 12 | del(['build', 'coverage', 'reports']); 13 | }); 14 | gulp.task('default', ['clean', 'lint', 'test', 'e2e', 'build']); 15 | gulp.task('pre-commit', ['clean', 'lint', 'test']); -------------------------------------------------------------------------------- /gulp/codestyle.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * gulp task that checks the code style (JSHint) 5 | * @author Martin Koster [paysdoc@gmail.com], created on 21/06/15. 6 | * Licensed under the MIT license. 7 | */ 8 | var gulp = require('gulp'), 9 | jshint = require('gulp-jshint'); 10 | gulp.task('lint', function() { 11 | 12 | gulp.src(['src/**/*.js', 'test/**/*.js']) 13 | .pipe(jshint()) 14 | .pipe(jshint.reporter('jshint-stylish')) 15 | .pipe(jshint.reporter('fail')); 16 | }); -------------------------------------------------------------------------------- /src/reporter/html/templates/folder.hbs: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | {{{rows}}} 15 | 16 |
FileSuccess rateKilledSurvivedIgnoredUntested
17 |
18 | -------------------------------------------------------------------------------- /test/e2e/tests/endlessLoop-test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * useless unit test for endless loop 3 | * Created by martin on 25/03/15. 4 | */ 5 | var endlessLoop = require('./../code/endlessLoop'); 6 | describe('Endless Loop', function () { 7 | 8 | it('tests to see whether mutations cause code to loop indefinitely', function () { 9 | endlessLoop.provokeEndlessLoop({input: '102365770045232'}); 10 | }); 11 | 12 | it('tests to see whether code containing an unpredictable nested loop will always complete with instrumentation', function() { 13 | endlessLoop.terribleNestedLoop(); 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /test/e2e/code/mathoperators.js: -------------------------------------------------------------------------------- 1 | (function(exports) { 2 | exports.addOperator = function (a, b) { 3 | return a + b + b; 4 | }; 5 | 6 | exports.subtractOperator = function (a, b) { 7 | return a - (b + b); 8 | }; 9 | 10 | exports.multiplyOperator = function (a, b) { 11 | return a * b; 12 | }; 13 | 14 | exports.divideOperator = function (a, b) { 15 | if (b > 0) { 16 | return a / b; 17 | } 18 | return 0; 19 | }; 20 | 21 | /* some blabla */ 22 | exports.modulusOperator = function (a, b) { 23 | return a % b; 24 | }; 25 | })(exports); 26 | -------------------------------------------------------------------------------- /src/childNodeFinder/LeftRightCNF.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Finds the child nodes of a given node 3 | * @author Martin Koster 4 | * Created on 26/05/15. 5 | */ 6 | (function(module) { 7 | 'use strict'; 8 | 9 | var ChildNodeFinder = require('./ChildNodeFinder'); 10 | 11 | var LeftRightChildNodeFinder = function(astNode) { 12 | ChildNodeFinder.call(this, astNode); 13 | }; 14 | 15 | LeftRightChildNodeFinder.prototype.find = function() { 16 | return [ 17 | this._astNode.left, 18 | this._astNode.right 19 | ]; 20 | }; 21 | 22 | module.exports = LeftRightChildNodeFinder; 23 | })(module); 24 | -------------------------------------------------------------------------------- /src/TestStatus.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Mutation Test Status object, containing the various statuses a test can have after running mutated code. 3 | * 4 | * @author Martin Koster 5 | * Created by martin on 16/04/15. 6 | */ 7 | 'use strict'; 8 | 9 | // All possible statuses of a Karma server instance 10 | var TestStatus = { 11 | SURVIVED: 'SURVIVED', // The unit test(s) survived the mutation 12 | KILLED: 'KILLED', // The mutation caused the unit test(s) to fail 13 | ERROR: 'ERROR', // an error occurred preventing the unit test(s) from reaching a conclusion 14 | FATAL: 'FATAL' // a fatal error occurred causing the process to abort 15 | }; 16 | 17 | module.exports = TestStatus; 18 | -------------------------------------------------------------------------------- /test/unit/childNodeFinder/ArrayCNFSpec.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Spec for the array child node finder 3 | * @author Martin Koster [paysdoc@gmail.com], created on 29/06/15. 4 | * Licensed under the MIT license. 5 | */ 6 | describe("ArrayCNF", function(){ 7 | 8 | var ArrayCNF = require('../../../src/childNodeFinder/ArrayCNF'); 9 | 10 | it("finds the right child node on a given array node", function() { 11 | var node = {"type": "ArrayExpression", 12 | "elements": ["elem1","elem2",{"type": "BinaryExpression", "operator": "*"}]}; 13 | expect(new ArrayCNF(node, 'elements').find()).toEqual([ 'elem1', 'elem2', { type: 'BinaryExpression', operator: '*' }]); 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /test/unit/childNodeFinder/IterationCNFSpec.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Spec for the array child node finder 3 | * @author Martin Koster [paysdoc@gmail.com], created on 29/06/15. 4 | * Licensed under the MIT license. 5 | */ 6 | describe("IterationCNF", function(){ 7 | 8 | var IterationCNF = require('../../../src/childNodeFinder/IterationCNF'); 9 | 10 | it("finds the right child nodes on a given iteration node", function() { 11 | var node = { 12 | "type": "WhileStatement", 13 | "test": {"value": "test"}, 14 | "body": {"value": "body"} 15 | }; 16 | expect(new IterationCNF(node).find()).toEqual([{ value: 'body' }]); 17 | }); 18 | }); 19 | 20 | -------------------------------------------------------------------------------- /src/childNodeFinder/CallExpressionCNF.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Finds the child nodes of a given node 3 | * @author Martin Koster 4 | * Created on 26/05/15. 5 | */ 6 | (function(module) { 7 | 'use strict'; 8 | 9 | var ArrayCNF = require('./ArrayCNF'), 10 | ChildNodeFinder = require('./ChildNodeFinder'); 11 | 12 | var CallExpressionChildNodeFinder = function(astNode) { 13 | ChildNodeFinder.call(this, astNode); 14 | }; 15 | 16 | CallExpressionChildNodeFinder.prototype.find = function() { 17 | return new ArrayCNF(this._astNode, 'arguments').find().concat([this._astNode.callee]); 18 | }; 19 | 20 | module.exports = CallExpressionChildNodeFinder; 21 | })(module); 22 | -------------------------------------------------------------------------------- /src/utils/LiteralUtils.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Utility for determining what a given literal should ve replaced with 3 | * Created by Martin Koster on 2/18/15. 4 | */ 5 | var _ = require('lodash'); 6 | module.exports.determineReplacement = function (literalValue) { 7 | 'use strict'; 8 | 9 | var replacement; 10 | 11 | if (_.isString(literalValue)) { 12 | replacement = 'MUTATION!'; 13 | } else if (_.isNumber(literalValue)) { 14 | replacement = (literalValue + 1) + ''; 15 | } else if (_.isBoolean(literalValue)) { 16 | replacement = (!literalValue) + ''; 17 | } 18 | return replacement ? {type: 'Literal', value: replacement, raw:'\'' + replacement + '\''} : null; 19 | }; 20 | -------------------------------------------------------------------------------- /src/childNodeFinder/IterationCNF.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Finds the child nodes of a given node 3 | * @author Martin Koster 4 | * Created on 26/05/15. 5 | */ 6 | (function(module) { 7 | 'use strict'; 8 | 9 | var ChildNodeFinder = require('./ChildNodeFinder'); 10 | 11 | var IterationChildNodeFinder = function(astNode) { 12 | ChildNodeFinder.call(this, astNode); 13 | }; 14 | 15 | IterationChildNodeFinder.prototype.find = function() { 16 | return ([ 17 | // only return the 'body' node as mutating 'test' node introduces too great a risk of resulting in an infinite loop 18 | this._astNode.body 19 | ]); 20 | }; 21 | 22 | module.exports = IterationChildNodeFinder; 23 | })(module); 24 | -------------------------------------------------------------------------------- /test/.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "curly": true, 3 | "eqeqeq": true, 4 | "immed": true, 5 | "latedef": "nofunc", 6 | "newcap": true, 7 | "noarg": true, 8 | "sub": true, 9 | "undef": true, 10 | "boss": true, 11 | "eqnull": true, 12 | "node": true, 13 | "expr": true, 14 | "globals": { 15 | "jasmine": false, 16 | "describe": false, 17 | "document": false, 18 | "fail": false, 19 | "it": false, 20 | "before": false, 21 | "beforeEach": false, 22 | "after": false, 23 | "afterEach": false, 24 | "expect": false, 25 | "spyOn": false, 26 | "module": false, 27 | "inject": false, 28 | "mockData": false 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/childNodeFinder/PropertyCNF.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Finds the child nodes of a given node 3 | * @author Martin Koster 4 | * Created on 26/05/15. 5 | */ 6 | (function(module) { 7 | 'use strict'; 8 | 9 | var _ = require('lodash'), 10 | ChildNodeFinder = require('./ChildNodeFinder'); 11 | 12 | var PropertyChildNodeFinder = function(astNode) { 13 | ChildNodeFinder.call(this, astNode); 14 | }; 15 | 16 | PropertyChildNodeFinder.prototype.find = function() { 17 | var childNodes = []; 18 | 19 | _.forEach(this._astNode.properties, function (property) { 20 | childNodes.push(property.value); 21 | }, this); 22 | 23 | return childNodes; 24 | }; 25 | 26 | module.exports = PropertyChildNodeFinder; 27 | })(module); 28 | -------------------------------------------------------------------------------- /test/unit/childNodeFinder/ForLoopCNFSpec.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Spec for the array child node finder 3 | * @author Martin Koster [paysdoc@gmail.com], created on 29/06/15. 4 | * Licensed under the MIT license. 5 | */ 6 | describe("ForLoopCNF", function(){ 7 | 8 | var ForLoopCNF = require('../../../src/childNodeFinder/ForLoopCNF'); 9 | 10 | it("finds the right child nodes on a given for loop", function() { 11 | var node = { 12 | "type": "ForStatement", 13 | "init": {"value":"init"}, 14 | "test": {"value": "test"}, 15 | "update": {"value": "update"}, 16 | "body": {"value": "body"} 17 | }; 18 | expect(new ForLoopCNF(node).find()).toEqual([{ value: 'init' }, { value: 'body' }]); 19 | }); 20 | }); 21 | 22 | -------------------------------------------------------------------------------- /src/childNodeFinder/ForLoopCNF.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Finds the child nodes of a given node 3 | * @author Martin Koster 4 | * Created on 26/05/15. 5 | */ 6 | (function(module) { 7 | 'use strict'; 8 | 9 | var ChildNodeFinder = require('./ChildNodeFinder'); 10 | 11 | var ForLoopChildNodeFinder = function(astNode) { 12 | ChildNodeFinder.call(this, astNode); 13 | }; 14 | 15 | ForLoopChildNodeFinder.prototype.find = function() { 16 | return ([ 17 | // only return the 'body' and 'init' nodes as mutating either 'test' or 'update' nodes introduce too great a risk of resulting in an infinite loop 18 | this._astNode.init, 19 | this._astNode.body 20 | ]); 21 | }; 22 | 23 | module.exports = ForLoopChildNodeFinder; 24 | })(module); 25 | -------------------------------------------------------------------------------- /src/mutationOperator/MutationOperator.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This command processes default nodes. All it does is expose its child nodes. 3 | * Created by Martin Koster on 2/12/15. 4 | */ 5 | (function(module) { 6 | 'use strict'; 7 | 8 | var _ = require('lodash'); 9 | 10 | function MutationOperator(astNode) { 11 | var body = astNode.body; 12 | 13 | /* while selecting a command requires the node, the actual processing may 14 | * in some cases require the body of the node, which is itself a node */ 15 | this._astNode = body && _.isArray(body) ? body : astNode; 16 | } 17 | 18 | MutationOperator.prototype.getReplacement = function() { 19 | return {value: null, begin: 0, end: 0}; 20 | }; 21 | 22 | module.exports = MutationOperator; 23 | })(module); 24 | -------------------------------------------------------------------------------- /test/unit/mutationOperator/MutationOperatorSpec.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Spec for the Mutation Operator base class 3 | * @author Martin Koster [paysdoc@gmail.com], created on 04/08/15. 4 | * Licensed under the MIT license. 5 | */ 6 | describe('MutationOperator', function() { 7 | var MutationOperator = require('../../../src/mutationOperator/MutationOperator'); 8 | 9 | it('creates a mutation operator with a private _astNode attribute', function() { 10 | var node = {someProp: 'someProp'}, 11 | instance = new MutationOperator(node); 12 | expect(instance._astNode).toEqual(node); 13 | }); 14 | 15 | it('retrieves the replacement value and its coordinates', function() { 16 | expect(new MutationOperator({someProp: 'someProp'}).getReplacement()).toEqual({value: null, begin: 0, end: 0}); 17 | }); 18 | }); -------------------------------------------------------------------------------- /src/childNodeFinder/ArrayCNF.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Finds the child nodes of a given node 3 | * @author Martin Koster 4 | * Created on 26/05/15. 5 | */ 6 | (function (module){ 7 | 'use strict'; 8 | 9 | var _ = require('lodash'), 10 | ChildNodeFinder = require('./ChildNodeFinder'); 11 | 12 | var ArrayChildNodeFinder = function(astNode, property) { 13 | ChildNodeFinder.call(this, astNode, property); 14 | }; 15 | 16 | ArrayChildNodeFinder.prototype.find = function() { 17 | var collection = this._property ? this._astNode[this._property] : this._astNode, 18 | childNodes = []; 19 | _.each(collection, function (element) { 20 | childNodes.push(element); 21 | }, this); 22 | return childNodes; 23 | }; 24 | 25 | module.exports = ArrayChildNodeFinder; 26 | })(module); -------------------------------------------------------------------------------- /test/unit/utils/LiteralUtilsSpec.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Spec for the Literals utility 3 | * @author Martin Koster [paysdoc@gmail.com], created on 24/09/15. 4 | * Licensed under the MIT license. 5 | */ 6 | describe('LiteralUtils', function() { 7 | var LiteralUtils = require('../../../src/utils/LiteralUtils'); 8 | 9 | it('determines whether there\'s a replacement for a given literal', function() { 10 | expect(LiteralUtils.determineReplacement('foo')).toEqual({ type: 'Literal', value: 'MUTATION!', raw: '\'MUTATION!\'' }); 11 | expect(LiteralUtils.determineReplacement(3)).toEqual({ type: 'Literal', value: '4', raw: '\'4\'' }); 12 | expect(LiteralUtils.determineReplacement(false)).toEqual({ type: 'Literal', value: 'true', raw: '\'true\'' }); 13 | expect(LiteralUtils.determineReplacement(/notaliteral/)).toBeNull(); 14 | }); 15 | }); -------------------------------------------------------------------------------- /test/unit/childNodeFinder/ChildNodeFinderSpec.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Spec for the array child node finder 3 | * @author Martin Koster [paysdoc@gmail.com], created on 29/06/15. 4 | * Licensed under the MIT license. 5 | */ 6 | describe("ChildNodeFinder", function(){ 7 | 8 | var ChildNodeFinder = require('../../../src/childNodeFinder/ChildNodeFinder'); 9 | 10 | it("finds the right child node on a given child node", function() { 11 | var node = {1: "bla", 2: "blo", 3: "blu"}; 12 | expect(new ChildNodeFinder(node).find()).toEqual(["bla", "blo", "blu"]); 13 | }); 14 | 15 | it("finds the right child node on the body of a given child node", function() { 16 | var node = {"body": [{1: "bla", 2: "blo", 3: "blu"}]}; 17 | expect(new ChildNodeFinder(node).find()).toEqual([{ 1: 'bla', 2: 'blo', 3: 'blu' }]); 18 | }); 19 | }); 20 | 21 | -------------------------------------------------------------------------------- /src/reporter/html/StatUtils.js: -------------------------------------------------------------------------------- 1 | /** 2 | * StatUtils 3 | * 4 | * @author Jimi van der Woning 5 | */ 6 | 'use strict'; 7 | 8 | var _ = require('lodash'); 9 | 10 | function decorateStatPercentages(stats) { 11 | var decoratedStats = _.clone(stats); 12 | decoratedStats.success = stats.all - stats.survived; 13 | decoratedStats.successRate = (decoratedStats.success / stats.all * 100 || 0).toFixed(1); 14 | decoratedStats.killedRate = (stats.killed / stats.all * 100 || 0).toFixed(1); 15 | decoratedStats.survivedRate = (stats.survived / stats.all * 100 || 0).toFixed(1); 16 | decoratedStats.ignoredRate = (stats.ignored / stats.all * 100 || 0).toFixed(1); 17 | decoratedStats.untestedRate = (stats.untested / stats.all * 100 || 0).toFixed(1); 18 | return decoratedStats; 19 | } 20 | 21 | module.exports.decorateStatPercentages = decorateStatPercentages; 22 | -------------------------------------------------------------------------------- /test/util/DummyPromiseResolver.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Creates a dummy promise and resolves in the requested manner 3 | * valid resolutions are 'resolve', 'reject' and 'notify'. Default (of on unknown resolution) is resolve 4 | * @author Martin Koster [paysdoc@gmail.com], created on 19/07/15. 5 | * Licensed under the MIT license. 6 | */ 7 | (function (module) { 8 | var Q = require('q'); 9 | 10 | module.exports = { 11 | resolve : function(resolution, info) { 12 | return Q.Promise(function(resolve, reject, notify) { 13 | if (resolution === 'reject') { 14 | reject(info); 15 | } else if (resolution === 'notify') { 16 | notify(info); 17 | } else { 18 | resolve(info); 19 | } 20 | }); 21 | } 22 | }; 23 | 24 | })(module); -------------------------------------------------------------------------------- /src/reporter/html/templates/file.hbs: -------------------------------------------------------------------------------- 1 | {{#if mutations}} 2 |
3 | 4 |
5 | {{/if}} 6 | 7 |
8 |
    {{#each sourceLines}}
  1. {{{this}}}
  2. {{/each}}
9 |
10 | 11 |
12 |

Mutations

13 | {{#if mutations}} 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | {{{mutations}}} 23 | 24 |
MutationResult
25 | {{ else }} 26 |

No mutations could be performed on this file

27 | {{/if}} 28 |
29 | 30 | 31 | -------------------------------------------------------------------------------- /src/childNodeFinder/ChildNodeFinder.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Finds the child nodes of a given node 3 | * @author Martin Koster 4 | * Created on 26/05/15. 5 | */ 6 | (function(module) { 7 | 'use strict'; 8 | 9 | var _ = require('lodash'); 10 | 11 | var ChildNodeFinder = function(astNode, property) { 12 | var body = astNode.body; 13 | 14 | /* The actual processing may in some cases require the body of the node, which is itself a node */ 15 | this._astNode = body && _.isArray(body) ? body : astNode; 16 | this._property = property; 17 | }; 18 | 19 | ChildNodeFinder.prototype.find = function() { 20 | var childNodes = []; 21 | _.forOwn(this._astNode, function (child) { 22 | childNodes.push(child); 23 | }, this); 24 | return childNodes; 25 | }; 26 | 27 | module.exports = ChildNodeFinder; 28 | })(module); 29 | -------------------------------------------------------------------------------- /test/e2e/tests/mathoperators-test.js: -------------------------------------------------------------------------------- 1 | var mathoperators = require("./../code/mathoperators"); 2 | describe('Math operators', function () { 3 | it('add', function () { 4 | expect(mathoperators.addOperator(4, 0)).toEqual(4); 5 | }); 6 | 7 | it('subtract', function () { 8 | expect(mathoperators.subtractOperator(4, 0)).toEqual(4); 9 | }); 10 | 11 | it('multiply', function () { 12 | expect(mathoperators.multiplyOperator(4, 1)).toEqual(4); 13 | }); 14 | 15 | it('divide', function () { 16 | expect(mathoperators.divideOperator(4, 1)).toEqual(4); 17 | }); 18 | 19 | it('divide - handle dividing by 0 situation', function () { 20 | expect(mathoperators.divideOperator(4, 0)).toEqual(0); 21 | }); 22 | 23 | it('modulus', function () { 24 | expect(mathoperators.modulusOperator(0, 1)).toEqual(0); 25 | }); 26 | 27 | }); 28 | -------------------------------------------------------------------------------- /src/reporter/html/templates/folderFileRow.hbs: -------------------------------------------------------------------------------- 1 | 2 | {{{pathSegments}}} 3 | 4 |
 
5 | 6 | {{stats.successRate}}% 7 | 8 | ({{stats.success}} / {{stats.all}}) 9 | 10 | {{stats.killedRate}}% 11 | 12 | ({{stats.killed}} / {{stats.all}}) 13 | 14 | {{stats.survivedRate}}% 15 | 16 | ({{stats.survived}} / {{stats.all}}) 17 | 18 | {{stats.ignoredRate}}% 19 | 20 | ({{stats.ignored}} / {{stats.all}}) 21 | 22 | {{stats.untestedRate}}% 23 | 24 | ({{stats.untested}} / {{stats.all}}) 25 | 26 | -------------------------------------------------------------------------------- /test/unit/childNodeFinder/LeftRightCNFSpec.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Spec for the array child node finder 3 | * @author Martin Koster [paysdoc@gmail.com], created on 29/06/15. 4 | * Licensed under the MIT license. 5 | */ 6 | describe("LeftRightCNF", function(){ 7 | 8 | var LeftRightCNF = require('../../../src/childNodeFinder/LeftRightCNF'); 9 | 10 | it("finds the right child nodea on a given binary expression node", function() { 11 | var node = { 12 | "type": "BinaryExpression", 13 | "operator": "*", 14 | "left": { 15 | "type": "Literal", 16 | "value": 6, 17 | "raw": "6" 18 | }, 19 | "right": { 20 | "type": "Literal", 21 | "value": 7, 22 | "raw": "7" 23 | } 24 | }; 25 | expect(new LeftRightCNF(node).find()).toEqual([{ type: 'Literal', value: 6, raw: '6' }, { type: 'Literal', value: 7, raw: '7' }]); 26 | }); 27 | }); 28 | 29 | -------------------------------------------------------------------------------- /src/reporter/html/templates/folderStyle.css: -------------------------------------------------------------------------------- 1 | #files { 2 | border-collapse: collapse; 3 | } 4 | 5 | #files th, #files td { 6 | padding: 5px 15px; 7 | border: 1px solid #666; 8 | } 9 | 10 | #files th { 11 | background-color: #eee; 12 | font-weight: normal; 13 | text-align: left; 14 | } 15 | 16 | #files .link { 17 | color: black; 18 | } 19 | 20 | #files td.filePath, #files td.rate { 21 | border-right: none; 22 | } 23 | 24 | #files td.fileCoverage, #files td.value { 25 | border-left: none; 26 | } 27 | 28 | #files td.rate { 29 | text-align: right; 30 | } 31 | 32 | #files td.value { 33 | padding-left: 0; 34 | font-size: 12px; 35 | color: #333; 36 | } 37 | 38 | .coverageBar { 39 | background-color: #eee; 40 | border: 1px solid #666; 41 | font-size: 12px; 42 | width: 120px; 43 | position: relative; 44 | display: block; 45 | } 46 | 47 | .coverageBar > span { 48 | background-color: #666; 49 | display: block; 50 | text-indent: -9999px; 51 | } 52 | -------------------------------------------------------------------------------- /test/unit/childNodeFinder/CallExpressionCNFSpec.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Spec for the array child node finder 3 | * @author Martin Koster [paysdoc@gmail.com], created on 29/06/15. 4 | * Licensed under the MIT license. 5 | */ 6 | describe("CallExpressionCNF", function(){ 7 | 8 | var CallExpressionCNF = require('../../../src/childNodeFinder/CallExpressionCNF'); 9 | 10 | it("finds the right child node on a given call expression node", function() { 11 | var node = { 12 | "type": "CallExpression", 13 | "callee": { 14 | "type": "Identifier", 15 | "name": "functionx" 16 | }, 17 | "arguments": [ 18 | { 19 | "type": "Identifier", 20 | "name": "answer" 21 | } 22 | ] 23 | }; 24 | expect(new CallExpressionCNF(node).find()).toEqual([{ type: 'Identifier', name: 'answer' }, { type: 'Identifier', name: 'functionx' } ]); 25 | }); 26 | }); 27 | 28 | -------------------------------------------------------------------------------- /src/reporter/html/templates/fileStyle.css: -------------------------------------------------------------------------------- 1 | #toggleKilledMutations { 2 | background-color: #eee; 3 | border: 1px solid #666; 4 | padding: 6px 10px; 5 | border-radius: 2px; 6 | cursor: pointer; 7 | } 8 | #toggleKilledMutations:hover { 9 | background-color: #aaa; 10 | } 11 | #toggleKilledMutations:active { 12 | background-color: #666; 13 | border-color: #eee; 14 | } 15 | 16 | #source { 17 | font-family: 'Consolas', 'Courier New', 'Lucida Sans Typewriter', monospace; 18 | white-space: pre; 19 | } 20 | 21 | #mutations { 22 | border-collapse: collapse; 23 | } 24 | 25 | #mutations th, #mutations td { 26 | padding: 5px 15px; 27 | border: 1px solid #666; 28 | } 29 | 30 | #mutations th { 31 | background-color: #eee; 32 | font-weight: normal; 33 | text-align: left; 34 | } 35 | 36 | #popup { 37 | position: fixed; 38 | border: solid 1px black; 39 | padding: 3px; 40 | display: none; 41 | } 42 | 43 | #popup.show { 44 | display: block; 45 | } 46 | 47 | code .killed, code .survived { 48 | border: solid 1px white; 49 | } 50 | -------------------------------------------------------------------------------- /test/e2e/code/endlessLoop.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by martin on 25/03/15. 3 | * 4 | * >> remove or rename the comment below to trigger an endless loop << 5 | * @excludeMutations 6 | */ 7 | (function(exports) { 8 | exports.provokeEndlessLoop = function(opts) { 9 | var modulus = -1; 10 | while(opts.input.length > 0) { 11 | var temp = ''; 12 | if (opts.input.length > 5) { 13 | temp = opts.input.substring(0, 6); 14 | } else { 15 | temp = opts.input; 16 | opts.input = ''; 17 | } 18 | 19 | var count = temp; 20 | modulus = count % 83; 21 | if (opts.input.length > 5) { 22 | opts.input = modulus + opts.input.substring(6); 23 | } 24 | } 25 | return modulus; 26 | }; 27 | 28 | exports.terribleNestedLoop = function() { 29 | while(i < 10) { 30 | while (i) { 31 | for (var i=1; i<20; i++) { 32 | i = i % 3; 33 | } 34 | } 35 | } 36 | }; 37 | })(exports); 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Martin Koster, Jimi van der Woning 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /test/unit/reporter/html/StatsUtilsSpec.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Unit test specs for StatUtils 3 | * @author Martin Koster [paysdoc@gmail.com], created on 07/08/15. 4 | * Licensed under the MIT license. 5 | */ 6 | describe('StatsUtils', function() { 7 | var StatUtils = require('../../../../src/reporter/html/StatUtils'); 8 | 9 | it('decorates the given mutation test statistics with percentages', function() { 10 | var decoratedStats = StatUtils.decorateStatPercentages({all:20, killed: 14, survived: 4, ignored: 1, untested: 1}); 11 | expect(decoratedStats).toEqual({ 12 | all: 20, killed: 14, survived: 4, ignored: 1, untested: 1, success: 16, 13 | successRate: '80.0', killedRate: '70.0', survivedRate: '20.0', ignoredRate: '5.0', untestedRate: '5.0' 14 | }); 15 | }); 16 | 17 | it('reverts to 0 percentages when stats are 0 to prevent division by 0', function() { 18 | var decoratedStats = StatUtils.decorateStatPercentages({all:0, killed: 0, survived: 0, ignored: 0, untested: 0}); 19 | expect(decoratedStats).toEqual({ 20 | all:0, killed: 0, survived: 0, ignored: 0, untested: 0, success: 0, 21 | successRate: '0.0', killedRate: '0.0', survivedRate: '0.0', ignoredRate: '0.0', untestedRate: '0.0' 22 | }); 23 | }); 24 | }); -------------------------------------------------------------------------------- /src/JSParserWrapper.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This module provides an abstraction for the JS parser, making it easier to switch parsers 3 | * if the need arises (e.g. ES6 support) 4 | * @author Martin Koster [paysdoc@gmail.com], created on 17/06/15. 5 | * Licensed under the MIT license. 6 | */ 7 | (function(exports){ 8 | 'use strict'; 9 | 10 | var _ = require('lodash'), 11 | esprima = require('esprima'), 12 | escodegen = require('escodegen'); 13 | 14 | /** 15 | * Parses the source 16 | * @param {string} src to be parsed 17 | * @param {object} [options] parsing options are: range (default = true), loc (default = true), tokens (default = false), comments (default = true) 18 | * @returns {*} 19 | */ 20 | function parse(src, options) { 21 | var ast = esprima.parse(src, _.merge({range: true, loc: true, tokens: true, comment: true}, options || {})), 22 | result = escodegen.attachComments(ast, ast.comments, ast.tokens); 23 | 24 | return options && options.tokens ? result : _.omit(result, 'tokens'); 25 | } 26 | 27 | function tokenize(src, options) { 28 | return esprima.tokenize(src, options); 29 | } 30 | 31 | function stringify(ast) { 32 | return escodegen.generate(ast); 33 | } 34 | 35 | exports.parse = parse; 36 | exports.stringify = stringify; 37 | exports.tokenize = tokenize; 38 | })(module.exports); -------------------------------------------------------------------------------- /src/mutationOperator/CallExpressionMO.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This command creates mutations on the parameters of a function call. 3 | * Please note: any parameters that are actually literals are processed via the MutateLiteralCommand 4 | * 5 | * Created by Martin Koster on 2/16/15. 6 | */ 7 | (function(module) { 8 | 'use strict'; 9 | 10 | var _ = require('lodash'), 11 | LiteralUtils = require('../utils/LiteralUtils'), 12 | CallExpressionArgsMO = require('./CallExpressionArgsMO'), 13 | CallExpressionSelfMO = require('./CallExpressionSelfMO'); 14 | 15 | var code = 'METHOD_CALL'; 16 | module.exports.create = function(astNode) { 17 | var args = astNode.arguments, 18 | replacement, 19 | mos = []; 20 | 21 | _.forEach(args, function(arg, i) { 22 | replacement = LiteralUtils.determineReplacement(arg.value); 23 | if (arg.type === 'Literal' && !!replacement) { 24 | mos.push(CallExpressionArgsMO.create(astNode, replacement, i)); 25 | } else { 26 | mos.push(CallExpressionArgsMO.create(astNode, {type: 'Literal', value: 'MUTATION!', raw:'\'MUTATION!\''}, i)); 27 | } 28 | return mos; 29 | }); 30 | 31 | if (args.length === 1) { 32 | mos.push(CallExpressionSelfMO.create(astNode, args[0])); 33 | } 34 | 35 | if (astNode.callee.type === 'MemberExpression') { 36 | mos.push(CallExpressionSelfMO.create(astNode, astNode.callee.object)); 37 | } 38 | 39 | return mos; 40 | }; 41 | module.exports.code = code; 42 | })(module); 43 | -------------------------------------------------------------------------------- /src/mutationOperator/UnaryExpressionMO.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This command removes unary expressions. 3 | * 4 | * e.g. -42 becomes 42, -true becomes true, !false becomes false, ~123 becomes 123. 5 | * 6 | * Created by Merlin Weemaes on 2/19/15. 7 | */ 8 | (function(module) { 9 | 'use strict'; 10 | 11 | var MutationUtils = require('../utils/MutationUtils'), 12 | MutationOperator = require('./MutationOperator'); 13 | 14 | var code = 'UNARY_EXPRESSION'; 15 | function UnaryExpressionMO(astNode) { 16 | MutationOperator.call(this, astNode); 17 | this.code = code; 18 | } 19 | 20 | UnaryExpressionMO.prototype.apply = function () { 21 | var mutationInfo; 22 | 23 | if (!this._original) { 24 | this._original = this._astNode; 25 | this._astNode = this._astNode.argument; 26 | mutationInfo = MutationUtils.createUnaryOperatorMutation(this._astNode, this._parentMutationId, ""); 27 | } 28 | 29 | return mutationInfo; 30 | }; 31 | 32 | UnaryExpressionMO.prototype.revert = function() { 33 | this._astNode = this._original || this._astNode; 34 | this._original = null; 35 | }; 36 | 37 | UnaryExpressionMO.prototype.getReplacement = function() { 38 | var astNode = this._astNode; 39 | 40 | return { 41 | value: null, 42 | begin: astNode.range[0], 43 | end: astNode.range[0]+1 44 | }; 45 | }; 46 | 47 | module.exports.create = function(astNode) { 48 | return astNode.operator ? [new UnaryExpressionMO(astNode)] : []; 49 | }; 50 | module.exports.code = code; 51 | })(module); 52 | -------------------------------------------------------------------------------- /src/MutationOperatorHandler.js: -------------------------------------------------------------------------------- 1 | /** 2 | * The mutation operator handler will invoke the application of a set of mutations to the source code. 3 | * The mutation operations will be stored in a stack to enable reversal of the mutation 4 | * 5 | * Mutation Operations are applied in sets. This enables multiple mutation operations to be applied between tests. 6 | * With first order mutations these sets will always contain 1 mutation each. Only once we get to implementing higher order 7 | * mutations can sets contain more than one mutation. 8 | * 9 | * Created by Martin Koster on 2/11/15. 10 | * Licensed under the MIT license. 11 | */ 12 | (function (module) { 13 | 'use strict'; 14 | 15 | var _ = require('lodash'), 16 | log4js = require('log4js'); 17 | 18 | var logger = log4js.getLogger('MutationOperatorHandler'); 19 | function MutationOperatorHandler() { 20 | this._moStack = []; 21 | } 22 | 23 | MutationOperatorHandler.prototype.applyMutation = function(mutationOperatorSet) { 24 | var result = []; 25 | _.forEach(mutationOperatorSet, function(operator) { 26 | logger.trace('applying mutation', JSON.stringify(operator.code)); 27 | result.push(operator.apply()); 28 | }); 29 | this._moStack.push(mutationOperatorSet); 30 | return result; 31 | }; 32 | 33 | MutationOperatorHandler.prototype.revertMutation = function() { 34 | var mutationOperatorSet = this._moStack.pop(); 35 | _.forEach(mutationOperatorSet, function(operator) { 36 | operator.revert(); 37 | }); 38 | }; 39 | 40 | module.exports = MutationOperatorHandler; 41 | })(module); 42 | -------------------------------------------------------------------------------- /src/utils/MutationUtils.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Utility for creating a mutation object 3 | * Created by Martin Koster on 2/16/15. 4 | */ 5 | var _ = require('lodash'), 6 | JSParserWrapper = require('../JSParserWrapper'); 7 | 8 | (function(module){ 9 | 'use strict'; 10 | 11 | var createMutation = function (astNode, endOffset, original, replacement) { 12 | replacement = replacement || ''; 13 | return { 14 | range: astNode.range, 15 | begin: astNode.range[0], 16 | end: endOffset, 17 | line: astNode.loc.start.line, 18 | col: astNode.loc.start.column, 19 | original: _.isObject(original) ? JSParserWrapper.stringify(original) : original, 20 | replacement: replacement 21 | }; 22 | }; 23 | 24 | var createUnaryOperatorMutation = function (astNode, original, replacement) { 25 | var mutation = createMutation(astNode, astNode.range[0]+1, original, replacement); 26 | return _.merge(mutation, { 27 | line: astNode.loc.end.line, 28 | col: astNode.loc.end.column 29 | }); 30 | }; 31 | 32 | var createOperatorMutation = function (astNode, original, replacement) { 33 | var mutation = createMutation(astNode, astNode.right.range[0], original, replacement); 34 | return _.merge(mutation, { 35 | begin: astNode.left.range[1], 36 | line: astNode.loc.end.line, 37 | col: astNode.loc.end.column 38 | }); 39 | }; 40 | 41 | module.exports.createMutation = createMutation; 42 | module.exports.createOperatorMutation = createOperatorMutation; 43 | module.exports.createUnaryOperatorMutation = createUnaryOperatorMutation; 44 | })(module); 45 | -------------------------------------------------------------------------------- /src/MutationScoreCalculator.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Keeps statistics and calculates mutation scores per file and overall 3 | * @author Martin Koster [paysdoc@gmail.com], created on 12/06/15. 4 | * Licensed under the MIT license. 5 | */ 6 | (function(module){ 7 | 'use strict'; 8 | var _ = require('lodash'), 9 | TestStatus = require('./TestStatus'); 10 | 11 | var MutationScoreCalculator = function() { 12 | this._scorePerFile = {}; 13 | }; 14 | 15 | MutationScoreCalculator.prototype.calculateScore = function(fileName, testStatus, ignoredMutations) { 16 | this._scorePerFile[fileName] = this._scorePerFile[fileName] || {killed: 0, survived: 0, ignored: 0, error: 0}; 17 | this._scorePerFile[fileName].ignored += ignoredMutations; 18 | if (testStatus === TestStatus.KILLED) { 19 | this._scorePerFile[fileName].killed++; 20 | } else if (testStatus === TestStatus.ERROR) { 21 | this._scorePerFile[fileName].error++; 22 | }else { 23 | this._scorePerFile[fileName].survived++; 24 | } 25 | }; 26 | 27 | MutationScoreCalculator.prototype.getScorePerFile = function(fileName) { 28 | return this._scorePerFile[fileName]; 29 | }; 30 | 31 | MutationScoreCalculator.prototype.getTotals = function() { 32 | return _.reduce(this._scorePerFile, function(result, score) { 33 | result.killed += score.killed; 34 | result.survived += score.survived; 35 | result.ignored += score.ignored; 36 | result.error += score.error; 37 | return result; 38 | }, {killed: 0, survived: 0, ignored: 0, error: 0}); 39 | }; 40 | 41 | module.exports = MutationScoreCalculator; 42 | })(module); 43 | -------------------------------------------------------------------------------- /src/utils/PromiseUtils.js: -------------------------------------------------------------------------------- 1 | /** 2 | * utility functions centred around promises 3 | * @author Martin Koster [paysdoc@gmail.com], created on 05/10/15. 4 | * Licensed under the MIT license. 5 | */ 6 | (function(module) { 7 | 'use strict'; 8 | 9 | var Q = require('q'), 10 | _ = require('lodash'); 11 | 12 | /** 13 | * turns given function into a promise 14 | * @param {function} fn function to be turned into a function 15 | * @param {boolean} [willResolve] when set to true 'fn' will call the resolver itself, otherwise the resolver will be invoked by promisify 16 | * @returns {Q} A promise that will resolve when the given function has completed 17 | */ 18 | module.exports.promisify = function(fn, willResolve) { 19 | var argsArray = Array.prototype.slice.call(arguments, 2); 20 | 21 | if (Q.isPromise(fn)) { 22 | return fn; 23 | } 24 | 25 | return Q.Promise(willResolve ? fn : function(resolve) {fn.apply({}, argsArray); resolve();}); 26 | }; 27 | 28 | /** 29 | * runs the functions in the given array as a promise sequence, returning the promise of the last function to be executed 30 | * @param {Array} functions array of functions to be run as a promise sequence 31 | * @param {Q} (promise) initial promise - if not given a new promise will be made 32 | * @param {Function} (errorCB) callback to be executed in case an error occurs. Will be called with the error reason 33 | */ 34 | module.exports.runSequence = function(functions, promise, errorCB) { 35 | var errorHandler = errorCB || function(error) {throw error;}; 36 | return _.reduce(functions, Q.when, promise || new Q({})).catch(function (reason) {errorHandler(reason);}); 37 | }; 38 | })(module); -------------------------------------------------------------------------------- /test/unit/MutationScoreCalculatorSpec.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Spec for the mutation score calculator 3 | * Created by martin on 24/11/15. 4 | */ 5 | describe('MutationScoreCalculator', function() { 6 | var MutationScoreCalculator = require('../../src/MutationScoreCalculator'), 7 | mutationScoreCalculator; 8 | var mockScorePerFile = { 9 | fileA: {killed: 6, survived: 2, ignored: 0, error: 1}, 10 | fileB: {killed: 37, survived: 5, ignored: 3, error: 0} 11 | }; 12 | 13 | function feedResult(fileName) { 14 | function doCalculation(status, numberOfTimes, ignored) { 15 | var ignoredMutations = ignored; 16 | for (var i = 0; i < numberOfTimes; i++ ) { 17 | mutationScoreCalculator.calculateScore(fileName, status, ignoredMutations); 18 | ignoredMutations = 0; 19 | } 20 | } 21 | doCalculation('KILLED', mockScorePerFile[fileName].killed, mockScorePerFile[fileName].ignored); 22 | doCalculation('SURVIVED', mockScorePerFile[fileName].survived, 0); 23 | doCalculation('ERROR', mockScorePerFile[fileName].error, 0); 24 | } 25 | 26 | beforeEach(function() { 27 | mutationScoreCalculator = new MutationScoreCalculator(); 28 | }); 29 | 30 | it('calculates the score for a file', function() { 31 | feedResult('fileA'); 32 | expect(mutationScoreCalculator.getScorePerFile('fileA')).toEqual(mockScorePerFile.fileA); 33 | }); 34 | 35 | it('calculates the totals over all files', function() { 36 | feedResult('fileA'); 37 | feedResult('fileB'); 38 | expect(mutationScoreCalculator.getTotals()).toEqual({ 39 | killed: 43, survived: 7, ignored: 3, error: 1 40 | }); 41 | }); 42 | }); 43 | -------------------------------------------------------------------------------- /src/utils/CopyUtils.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | /** 3 | * Utility functions for copying files 4 | * 5 | * @module CopyUtils 6 | * @author Jimi van der Woning 7 | */ 8 | 9 | var fs = require('fs-extra'), 10 | glob = require('glob'), 11 | _ = require('lodash'), 12 | path = require('path'), 13 | q = require('q'), 14 | temp = require('temp').track(); // use (chainable) track to remove files after the process 15 | 16 | /** 17 | * Copy a file or array of files to a temporary directory. File paths may contain wildcards, 18 | * e.g. test/*Spec.js 19 | * 20 | * @param {string | [string]} files filepath or array of filepaths that will be copied 21 | * @param {string} tempDirName (optional) name of the temporary directory to copy to 22 | * @returns a promise that will resolve when all files have successfully been copied 23 | */ 24 | function copyToTemp(files, tempDirName) { 25 | if(!_.isArray(files)) { files = [files]; } 26 | tempDirName = tempDirName || "copyToTemp-dir"; 27 | 28 | var deferred = q.defer(); 29 | 30 | temp.mkdir(tempDirName, function(err, tempDirPath) { 31 | try { 32 | if(err) { deferred.reject(err); } 33 | 34 | files.forEach(function(filePath) { 35 | // Resolve all wildcards in the filePath 36 | var globbedFilePaths = glob.sync(filePath, { dot: true }); 37 | 38 | globbedFilePaths.forEach(function(gfp) { 39 | fs.copySync(gfp, path.join(tempDirPath, gfp)); 40 | }); 41 | }); 42 | 43 | deferred.resolve(tempDirPath); 44 | } catch(err) { 45 | deferred.reject(err); 46 | } 47 | }); 48 | 49 | return deferred.promise; 50 | } 51 | 52 | module.exports.copyToTemp = copyToTemp; 53 | 54 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "js-mutation-testing", 3 | "version": "0.1.0", 4 | "description": "JavaScript Mutation Testing. Tests your tests by mutating the code.", 5 | "repository": { 6 | "type": "git", 7 | "url": "https://github.com/paysdoc/JS-mutation-testing.git" 8 | }, 9 | "author": "Martin Koster (https://github.com/paysdoc)", 10 | "contributors": [ 11 | "Jimi van der Woning (https://github.com/jimivdw)" 12 | ], 13 | "keywords": [ 14 | "jsmutationtest", 15 | "mutationtest" 16 | ], 17 | "main": "Gulpfile.js", 18 | "engines": { 19 | "node": ">= 0.10.0" 20 | }, 21 | "license": "MIT", 22 | "devDependencies": { 23 | "chai": "^3.5.0", 24 | "del": "^1.2.0", 25 | "git-guppy": "^1.1.0", 26 | "gulp": "^3.9.0", 27 | "gulp-concat": "^2.5.2", 28 | "gulp-istanbul": "^0.10.0", 29 | "gulp-jasmine": "^2.0.1", 30 | "gulp-jshint": "^2.0.0", 31 | "gulp-util": "^3.0.6", 32 | "guppy-pre-push": "^0.2.1", 33 | "istanbul": "^0.3.17", 34 | "jasmine": "^2.3.1", 35 | "jasmine-core": "^2.3.4", 36 | "jshint": "^2.9.1", 37 | "jshint-stylish": "^2.1.0", 38 | "proxyquire": "^1.6.0" 39 | }, 40 | "scripts": { 41 | "test": "gulp" 42 | }, 43 | "dependencies": { 44 | "escodegen": "^1.6.1", 45 | "esprima": "~1.2.5", 46 | "fs-extra": "^0.16.5", 47 | "glob": "^5.0.10", 48 | "handlebars": "^3.0.0", 49 | "htmlparser2": "^3.8.2", 50 | "lodash": "~3.9.3", 51 | "log4js": "^0.6.25", 52 | "q": "~2.0.3", 53 | "require-dir": "^0.3.0", 54 | "require-uncache": "0.0.2", 55 | "sync-exec": "~0.5.0", 56 | "temp": "^0.8.1", 57 | "xml2js": "^0.4.8" 58 | }, 59 | "bugs": { 60 | "url": "https://github.com/paysdoc/JS-mutation-testing/issues" 61 | }, 62 | "directories": { 63 | "test": "test" 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/mutationOperator/ArrayExpressionMO.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This command creates mutations on a given array 3 | * Created by Martin Koster on 2/12/15. 4 | */ 5 | (function(module) { 6 | 'use strict'; 7 | 8 | var _ = require('lodash'), 9 | MutationUtils = require('../utils/MutationUtils'), 10 | ArrayMutatorUtil = require('../utils/ArrayMutatorUtil'), 11 | MutationOperator = require('./MutationOperator'); 12 | 13 | var code = 'ARRAY'; 14 | function ArrayExpressionMO (astNode, index) { 15 | MutationOperator.call(this, astNode); 16 | this.code = code; 17 | this._originalArray = _.clone(this._astNode.elements); 18 | this._original = this._astNode.elements[index]; 19 | } 20 | 21 | ArrayExpressionMO.prototype.apply = function () { 22 | function createMutationInfo(element) { 23 | return MutationUtils.createMutation(element, element.range[1], element); 24 | } 25 | return ArrayMutatorUtil.removeElement(this._astNode.elements, this._original, createMutationInfo); 26 | }; 27 | 28 | ArrayExpressionMO.prototype.revert = function() { 29 | ArrayMutatorUtil.restoreElement(this._astNode.elements, this._original, this._originalArray); 30 | }; 31 | 32 | ArrayExpressionMO.prototype.getReplacement = function() { 33 | var element = this._original; 34 | 35 | return { 36 | value: null, 37 | begin: element.range[0], 38 | end: element.range[1] 39 | }; 40 | }; 41 | 42 | module.exports.code = code; 43 | module.exports.create = function(astNode) { 44 | var mos = []; 45 | 46 | _.forEach(astNode.elements, function(element, i) { 47 | mos.push(new ArrayExpressionMO(astNode, i)); 48 | }); 49 | 50 | return mos; 51 | }; 52 | 53 | })(module); 54 | -------------------------------------------------------------------------------- /src/mutationOperator/LogicalExpressionMO.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This command creates mutations on logical operators. 3 | * 4 | * TODO: What about '!'? 5 | * Created by Merlin Weemaes on 2/24/15. 6 | */ 7 | (function(module) { 8 | 'use strict'; 9 | 10 | var MutationOperator = require('./MutationOperator'), 11 | MutationUtils = require('../utils/MutationUtils'), 12 | operators = { 13 | '&&': '||', 14 | '||': '&&' 15 | }; 16 | 17 | var code = 'LOGICAL_EXPRESSION'; 18 | function LogicalExpressionMO(astNode) { 19 | MutationOperator.call(this, astNode); 20 | this.code = code; 21 | } 22 | 23 | LogicalExpressionMO.prototype.apply = function () { 24 | var mutationInfo; 25 | 26 | if (!this._original) { 27 | this._original = this._astNode.operator; 28 | this._astNode.operator = operators[this._astNode.operator]; 29 | mutationInfo = MutationUtils.createOperatorMutation(this._astNode, this._astNode.operator, this._original); 30 | } 31 | 32 | return mutationInfo; 33 | }; 34 | 35 | LogicalExpressionMO.prototype.revert = function() { 36 | this._astNode.operator = this._original || this._astNode.operator; 37 | this._original = null; 38 | }; 39 | 40 | LogicalExpressionMO.prototype.getReplacement = function() { 41 | var astNode = this._astNode; 42 | 43 | return { 44 | value: operators[this._original ? this._original : this._astNode.operator], 45 | begin: astNode.left.range[1], 46 | end: astNode.right.range[0] 47 | }; 48 | }; 49 | 50 | module.exports.create = function(astNode){ 51 | return operators.hasOwnProperty(astNode.operator) ? [new LogicalExpressionMO(astNode)] : []; 52 | }; 53 | module.exports.code = code; 54 | })(module); 55 | -------------------------------------------------------------------------------- /test/unit/utils/MutationUtilsSpec.js: -------------------------------------------------------------------------------- 1 | /** 2 | * spec for the MutationUtils class 3 | * @author Martin Koster [paysdoc@gmail.com], created on 24/09/15. 4 | * Licensed under the MIT license. 5 | */ 6 | describe('MutationUtils', function() { 7 | var MutationUtils = require('../../../src/utils/MutationUtils'), 8 | astNode = { 9 | value: 'someVal', 10 | range: [12, 46], 11 | loc: {start: {line: 3, column: 7}, end: {line: 3, column: 12}}, 12 | left: {range: [12, 33]}, 13 | right: {range: [36, 46]} 14 | }; 15 | 16 | it('creates a mutation description', function() { 17 | var mutationDescription = MutationUtils.createMutation(astNode, 34, 'foo', 'bar'); 18 | expect(mutationDescription).toEqual({range: [12,46], begin: 12, end: 34, line: 3, col: 7, original: 'foo', replacement: 'bar'}); 19 | }); 20 | 21 | it('creates an operator mutation description', function() { 22 | var originalNode = { 23 | "range": [116, 121], 24 | "loc": { 25 | "start": {"line": 5, "column": 15}, 26 | "end": {"line": 5, "column": 20} 27 | }, 28 | "type": "Literal", 29 | "value": "bla", 30 | "raw": "'bla'" 31 | }; 32 | 33 | var mutationDescription = MutationUtils.createOperatorMutation(astNode, originalNode, 'bar'); 34 | expect(mutationDescription).toEqual({range: [12, 46], begin: 33, end: 36, line: 3, col: 12, original: '\'bla\'', replacement: 'bar'}); 35 | }); 36 | 37 | it('creates a unary operator mutation description', function() { 38 | var mutationDescription = MutationUtils.createUnaryOperatorMutation(astNode, 'foo'); 39 | expect(mutationDescription).toEqual({range: [12, 46], begin: 12, end: 13, line: 3, col: 12, original: 'foo', replacement: ''}); 40 | }); 41 | }); -------------------------------------------------------------------------------- /src/mutationOperator/BlockStatementMO.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This command creates mutations on a given array of statements in a statement block 3 | * Created by Martin Koster on 2/12/15. 4 | */ 5 | (function(module) { 6 | 'use strict'; 7 | 8 | var _ = require('lodash'), 9 | MutationUtils = require('../utils/MutationUtils'), 10 | ArrayMutatorUtil = require('../utils/ArrayMutatorUtil'), 11 | MutationOperator = require('./MutationOperator'); 12 | 13 | var code = 'BLOCK_STATEMENT'; 14 | function BlockStatementMO (astNode, index) { 15 | MutationOperator.call(this, astNode); 16 | this.code = code; 17 | this._originalArray = _.clone(this._astNode); 18 | this._original = this._astNode[index]; 19 | } 20 | 21 | BlockStatementMO.prototype.apply = function () { 22 | function createMutationInfo(element) { 23 | return MutationUtils.createMutation(element, element.range[1], element); 24 | } 25 | return ArrayMutatorUtil.removeElement(this._astNode, this._original, createMutationInfo); 26 | }; 27 | 28 | BlockStatementMO.prototype.revert = function() { 29 | ArrayMutatorUtil.restoreElement(this._astNode, this._original, this._originalArray); 30 | }; 31 | 32 | BlockStatementMO.prototype.getReplacement = function() { 33 | var element = this._original; 34 | 35 | return { 36 | value: null, 37 | begin: element.range[0], 38 | end: element.range[1] 39 | }; 40 | }; 41 | 42 | module.exports.create = function(astNode) { 43 | var mos = [], 44 | nodeBody = astNode.body || []; 45 | 46 | _.forEach(nodeBody, function (childNode, i) { 47 | mos.push(new BlockStatementMO(astNode, i)); 48 | }); 49 | 50 | return mos; 51 | }; 52 | module.exports.code = code; 53 | })(module); 54 | -------------------------------------------------------------------------------- /src/mutationOperator/ObjectMO.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This command creates mutations on a given object 3 | * Created by Martin Koster on 2/12/15. 4 | */ 5 | (function(module) { 6 | 'use strict'; 7 | 8 | var _ = require('lodash'), 9 | MutationOperator = require('./MutationOperator'), 10 | ArrayMutatorUtil = require('../utils/ArrayMutatorUtil'), 11 | MutationUtils = require('../utils/MutationUtils'); 12 | 13 | var code = 'OBJECT'; 14 | function ObjectMO (astNode, index) { 15 | MutationOperator.call(this, astNode); 16 | this.code = code; 17 | this._original = this._astNode.properties[index]; 18 | this._originalArray = _.clone(this._astNode.properties); 19 | } 20 | 21 | ObjectMO.prototype.apply = function () { 22 | function createMutationInfo(element) { 23 | return MutationUtils.createMutation(element, element.range[1], element); 24 | } 25 | return ArrayMutatorUtil.removeElement(this._astNode.properties, this._original, createMutationInfo); 26 | }; 27 | 28 | ObjectMO.prototype.revert = function() { 29 | ArrayMutatorUtil.restoreElement(this._astNode.properties, this._original, this._originalArray); 30 | }; 31 | 32 | ObjectMO.prototype.getReplacement = function() { 33 | var property = this._original; 34 | 35 | return { 36 | value: null, 37 | begin: property.range[0], 38 | end: property.range[1] 39 | }; 40 | }; 41 | 42 | module.exports.create = function(astNode){ 43 | var properties = astNode.properties, 44 | mos = []; 45 | 46 | _.forEach(properties, function(property, i) { 47 | if (property.kind === 'init') { 48 | mos.push(new ObjectMO(astNode, i)); 49 | } 50 | }); 51 | 52 | return mos; 53 | }; 54 | module.exports.code = code; 55 | })(module); 56 | -------------------------------------------------------------------------------- /src/TestRunner.js: -------------------------------------------------------------------------------- 1 | /** 2 | * runs a given test with given configuration 3 | * Created by martin on 17/03/16. 4 | */ 5 | (function TestRunner(module) { 6 | 'use strict'; 7 | 8 | var PromiseUtils = require('./utils/PromiseUtils'), 9 | TestStatus = require('./TestStatus'), 10 | exec = require('sync-exec'), 11 | log4js = require('log4js'); 12 | 13 | var logger = log4js.getLogger('TestRunner'); 14 | function runTest(config, test) { 15 | var testPromise; 16 | 17 | if (typeof test === 'string') { 18 | testPromise = PromiseUtils.promisify(function(resolver) { 19 | //FIXME: this probably doesn't pick up the mutated test files 20 | resolver(exec(test).status); 21 | }, true); 22 | } else { 23 | testPromise = PromiseUtils.promisify(function(resolver) { 24 | try { 25 | logger.trace('executing test with path \'%s\', code \'%s\' and specs \'%s\'', config.get('basePath'), config.get('mutate'), config.get('specs')); 26 | test({ 27 | basePath: config.get('basePath'), 28 | lib: config.get('lib'), 29 | src: config.get('mutate'), 30 | specs: config.get('specs') 31 | }, function (status) { // TODO: what statuses do other unit test frameworks return? - this should be more generic 32 | resolver(status ? 0 : 1); 33 | }); 34 | } catch(err) { 35 | resolver(1); //test killed 36 | } 37 | 38 | }, true); 39 | } 40 | return testPromise.then(function(returnCode) { 41 | return returnCode > 0 ? TestStatus.KILLED : TestStatus.SURVIVED; 42 | }); 43 | } 44 | 45 | module.exports = { 46 | runTest: runTest 47 | }; 48 | })(module); 49 | -------------------------------------------------------------------------------- /src/mutationOperator/CallExpressionArgsMO.js: -------------------------------------------------------------------------------- 1 | /** 2 | * MutationOperator that mutates the arguments of a call expression 3 | * @author Martin Koster [paysdoc@gmail.com], created on 10/09/15. 4 | * Licensed under the MIT license. 5 | */ 6 | (function(module) { 7 | 'use strict'; 8 | 9 | var MutationOperator = require('./MutationOperator'), 10 | MutationUtils = require('../utils/MutationUtils'); 11 | 12 | var code = 'METHOD_CALL(args)'; 13 | function CallExpressionArgsMO (astNode, replacement, index) { 14 | MutationOperator.call(this, astNode); 15 | this.code = code; 16 | this._index = index; 17 | this._replacement = replacement; 18 | } 19 | 20 | CallExpressionArgsMO.prototype.apply = function() { 21 | var i = this._index, 22 | args = this._astNode['arguments'], 23 | mutationInfo; 24 | 25 | if (!this._original) { 26 | this._original = args[i]; 27 | mutationInfo = MutationUtils.createMutation(args[i], args[i].range[1], this._original, this._replacement.value); 28 | args[i] = this._replacement; 29 | } 30 | 31 | return mutationInfo; 32 | }; 33 | 34 | CallExpressionArgsMO.prototype.revert = function() { 35 | if (this._original) { 36 | this._astNode['arguments'][this._index] = this._original; 37 | this._original = null; 38 | } 39 | }; 40 | 41 | CallExpressionArgsMO.prototype.getReplacement = function() { 42 | var original = this._original || this._astNode['arguments'][this._index]; 43 | 44 | return { 45 | value: this._replacement, 46 | begin: original.range[0], 47 | end: original.range[1] 48 | }; 49 | }; 50 | 51 | module.exports.create = function(astNode, replacement, index) {return new CallExpressionArgsMO(astNode, replacement, index);}; 52 | module.exports.code = code; 53 | 54 | })(module); -------------------------------------------------------------------------------- /src/mutationOperator/LiteralMO.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This command performs mutations on literals of type string, number of boolean 3 | * Created by Martin Koster on 2/12/15. 4 | */ 5 | (function(module) { 6 | 'use strict'; 7 | 8 | var MutationUtils = require('../utils/MutationUtils'), 9 | LiteralUtils = require('../utils/LiteralUtils'), 10 | MutationOperator = require('./MutationOperator'), 11 | log4js = require('log4js'); 12 | 13 | var logger = log4js.getLogger('LiteralMO'); 14 | var code = 'LITERAL'; 15 | function LiteralMO (astNode) { 16 | MutationOperator.call(this, astNode); 17 | this.code = code; 18 | this._replacement = LiteralUtils.determineReplacement(astNode.value); 19 | } 20 | 21 | LiteralMO.prototype.apply = function () { 22 | var value = this._astNode.value, 23 | mutationInfo; 24 | 25 | if (!this._original) { 26 | this._original = value; 27 | if (this._replacement) { 28 | logger.trace('replacement', this._replacement); 29 | this._astNode.value = this._replacement.value; 30 | mutationInfo = MutationUtils.createMutation(this._astNode, this._astNode.range[1], value, this._replacement.value); 31 | } 32 | } 33 | 34 | return mutationInfo; 35 | }; 36 | 37 | LiteralMO.prototype.getReplacement = function() { 38 | var astNode = this._astNode; 39 | 40 | return { 41 | value: this._replacement, 42 | begin: astNode.range[0], 43 | end: astNode.range[1] 44 | }; 45 | }; 46 | 47 | LiteralMO.prototype.revert = function() { 48 | this._astNode.value = this._original || this._astNode.value; 49 | this._original = null; 50 | }; 51 | 52 | module.exports.create = function(astNode) { 53 | return [new LiteralMO(astNode)]; 54 | }; 55 | module.exports.code = code; 56 | })(module); 57 | -------------------------------------------------------------------------------- /src/reporter/html/templates/baseStyle.hbs: -------------------------------------------------------------------------------- 1 | 91 | -------------------------------------------------------------------------------- /src/mutationOperator/ArithmeticOperatorMO.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This command creates mutations on a given arithmetic operator. 3 | * Each operator will be mutated to it's opposite 4 | * Created by Martin Koster on 2/11/15. 5 | * 6 | * TODO: We'll have to add a mutation operator for bitwise operators 7 | */ 8 | (function(module) { 9 | 'use strict'; 10 | 11 | var MutationOperator = require('./MutationOperator'), 12 | MutationUtils = require('../utils/MutationUtils'), 13 | operators = { 14 | '+': '-', 15 | '-': '+', 16 | '*': '/', 17 | '/': '*', 18 | '%': '*' 19 | }; 20 | 21 | var code = 'MATH'; 22 | function ArithmeticOperatorMO(astNode) { 23 | MutationOperator.call(this, astNode); 24 | this.code = code; 25 | } 26 | 27 | ArithmeticOperatorMO.prototype.apply = function () { 28 | var mutationInfo = null; 29 | 30 | if (!this._original) { 31 | this._original = this._astNode.operator; 32 | this._astNode.operator = operators[this._original]; 33 | mutationInfo = MutationUtils.createOperatorMutation(this._astNode, this._astNode.operator, operators[this._astNode.operator]); 34 | } 35 | return mutationInfo; 36 | }; 37 | 38 | ArithmeticOperatorMO.prototype.revert = function () { 39 | this._astNode.operator = this._original || this._astNode.operator; 40 | this._original = null; 41 | }; 42 | 43 | ArithmeticOperatorMO.prototype.getReplacement = function() { 44 | var astNode = this._astNode; 45 | return { 46 | value: operators[this._original ? this._original : this._astNode.operator], 47 | begin: astNode.left.range[1], 48 | end: astNode.right.range[0] 49 | }; 50 | }; 51 | 52 | module.exports.code = code; 53 | module.exports.create = function(astNode) { 54 | return operators.hasOwnProperty(astNode.operator) ? [new ArithmeticOperatorMO(astNode)] : []; 55 | }; 56 | })(module); 57 | -------------------------------------------------------------------------------- /gulp/test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * test files 3 | * @author Martin Koster [paysdoc@gmail.com], created on 21/06/15. 4 | * Licensed under the MIT license. 5 | */ 6 | var gulp = require('gulp'), 7 | gulp_jasmine = require('gulp-jasmine'), 8 | istanbul = require('gulp-istanbul'), 9 | path = require('path'), 10 | MutationTester = require('../src/MutationTester'); 11 | 12 | gulp.task('test', function() { 13 | 'use strict'; 14 | 15 | gulp.src(['src/**/*.js']) 16 | .pipe(istanbul({includeUntested:true})) 17 | .pipe(istanbul.hookRequire()) 18 | .on('finish', function() { 19 | gulp.src(['test/unit/**/*Spec.js']) 20 | .pipe(gulp_jasmine()) 21 | .pipe(istanbul.writeReports()) 22 | .on('error', function(err) {throw err;}); 23 | }); 24 | }); 25 | 26 | gulp.task('e2e', function() { 27 | 'use strict'; 28 | 29 | var mutate = ['code/**/*.js'], 30 | specs = ['tests/**/*-test.js'], 31 | lib = ['../../node_modules/lodash/**/*.js']; 32 | 33 | function completionHandler(passed, cb) { 34 | cb(passed); 35 | } 36 | new MutationTester({ 37 | lib: lib, 38 | mutate: mutate, 39 | specs: specs, 40 | basePath: process.cwd() + '/test/e2e/', 41 | logLevel: 'TRACE'} 42 | ).test(function(config, cb) { 43 | var cache = require.cache || {}; 44 | var jasmine; 45 | 46 | for(var key in cache) { 47 | if(cache.hasOwnProperty(key)) { 48 | delete cache[key]; 49 | } 50 | } 51 | 52 | jasmine = new (require('jasmine'))({projectBaseDir: config.basePath}); 53 | jasmine.loadConfig({ 54 | spec_dir: '', 55 | spec_files: specs, 56 | helpers: lib 57 | }); 58 | jasmine.onComplete(function(passed) { 59 | completionHandler(passed, cb); 60 | }); 61 | jasmine.execute(); 62 | } 63 | ); 64 | }); -------------------------------------------------------------------------------- /src/mutationOperator/EqualityOperatorMO.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This command creates mutations on a given equality operator. 3 | * Each operator can be mutated to its boundary and its negation counterpart, e.g. 4 | * '===' has '==' as boundary and '!==' as negation (opposite) 5 | * Created by Martin Koster on 2/11/15. 6 | */ 7 | (function(module) { 8 | 'use strict'; 9 | 10 | var MutationOperator = require('./MutationOperator'), 11 | MutationUtils = require('../utils/MutationUtils'), 12 | operators = { 13 | '===': '!==', 14 | '==': '!=', 15 | '!==': '===', 16 | '!=': '==' 17 | }; 18 | 19 | var code = 'EQUALITY'; 20 | function EqualityOperatorMO (astNode, replacement) { 21 | MutationOperator.call(this, astNode); 22 | this.code = code; 23 | this._replacement = replacement; 24 | } 25 | 26 | EqualityOperatorMO.prototype.apply = function () { 27 | var mutationInfo; 28 | 29 | if (!this._original) { 30 | this._original = this._astNode.operator; 31 | this._astNode.operator = this._replacement; 32 | mutationInfo = MutationUtils.createOperatorMutation(this._astNode, this._original, this._replacement); 33 | } 34 | return mutationInfo; 35 | }; 36 | 37 | EqualityOperatorMO.prototype.revert = function() { 38 | this._astNode.operator = this._original || this._astNode.operator; 39 | this._original = null; 40 | }; 41 | 42 | EqualityOperatorMO.prototype.getReplacement = function() { 43 | var astNode = this._astNode; 44 | 45 | return { 46 | value: this._replacement, 47 | begin: astNode.left.range[1], 48 | end: astNode.right.range[0] 49 | }; 50 | }; 51 | 52 | module.exports.create = function(astNode) { 53 | return operators.hasOwnProperty(astNode.operator) ? [new EqualityOperatorMO(astNode, operators[astNode.operator])] : []; 54 | }; 55 | 56 | module.exports.code = code; 57 | module.exports.exclude = true; 58 | })(module); 59 | -------------------------------------------------------------------------------- /src/reporter/html/Templates.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Templates 3 | * 4 | * @author Jimi van der Woning 5 | */ 6 | 'use strict'; 7 | var fs = require('fs'), 8 | Handlebars = require('handlebars'), 9 | path = require('path'); 10 | 11 | /** 12 | * Read a file from the template folder 13 | * @param fileName name of the file to read 14 | */ 15 | function readTemplateFile(fileName) { 16 | return fs.readFileSync(path.join(__dirname, 'templates', fileName), 'utf-8'); 17 | } 18 | 19 | // Templates from files 20 | var baseTemplateCode = readTemplateFile('base.hbs'), 21 | baseScriptTemplateCode = readTemplateFile('baseScript.hbs'), 22 | baseStyleTemplateCode = readTemplateFile('baseStyle.hbs'), 23 | folderTemplateCode = readTemplateFile('folder.hbs'), 24 | fileTemplateCode = readTemplateFile('file.hbs'), 25 | folderFileRowTemplateCode = readTemplateFile('folderFileRow.hbs'); 26 | 27 | // Hardcoded templates 28 | var segmentLinkTemplateCode = '{{folder}}{{{separator}}}', 29 | fileLinkTemplateCode = '{{file}}'; 30 | 31 | // Templates 32 | module.exports.baseTemplate = Handlebars.compile(baseTemplateCode); 33 | module.exports.baseScriptTemplate = Handlebars.compile(baseScriptTemplateCode); 34 | module.exports.baseStyleTemplate = Handlebars.compile(baseStyleTemplateCode); 35 | module.exports.folderTemplate = Handlebars.compile(folderTemplateCode); 36 | module.exports.fileTemplate = Handlebars.compile(fileTemplateCode); 37 | module.exports.folderFileRowTemplate = Handlebars.compile(folderFileRowTemplateCode); 38 | module.exports.segmentLinkTemplate = Handlebars.compile(segmentLinkTemplateCode); 39 | module.exports.fileLinkTemplate = Handlebars.compile(fileLinkTemplateCode); 40 | 41 | // Static code 42 | module.exports.folderStyleCode = readTemplateFile('folderStyle.css'); 43 | module.exports.fileStyleCode = readTemplateFile('fileStyle.css'); 44 | module.exports.fileScriptCode = readTemplateFile('fileScript.js'); 45 | 46 | Handlebars.registerHelper('json', function(context) { 47 | return encodeURI(JSON.stringify(context)); 48 | }); 49 | -------------------------------------------------------------------------------- /src/mutationOperator/CallExpressionSelfMO.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Mutation operator that mutates the call expression itself 3 | * @author Martin Koster [paysdoc@gmail.com], created on 10/09/15. 4 | * Licensed under the MIT license. 5 | */ 6 | (function(module) { 7 | 'use strict'; 8 | 9 | var _ = require('lodash'), 10 | MutationOperator = require('./MutationOperator'), 11 | MutationUtils = require('../utils/MutationUtils'); 12 | 13 | var code = 'METHOD_CALL'; 14 | function CallExpressionSelfMO (astNode, replacement, index) { 15 | MutationOperator.call(this, astNode); 16 | this.code = code; 17 | this._index = index; 18 | this._replacement = replacement; 19 | } 20 | 21 | CallExpressionSelfMO.prototype.apply = function() { 22 | var self = this, 23 | astNode = this._astNode, 24 | mutationInfo; 25 | 26 | if (!this._original) { 27 | this._original = {}; 28 | _.forOwn(astNode, function(value, key) { 29 | self._original[key] = value; 30 | delete astNode[key]; 31 | }); 32 | _.forOwn(this._replacement, function(value, key) { 33 | astNode[key] = value; 34 | }); 35 | mutationInfo = MutationUtils.createMutation(this._original, this._original.range[1], this._original, this._replacement.value); 36 | } 37 | 38 | return mutationInfo; 39 | }; 40 | 41 | CallExpressionSelfMO.prototype.revert = function() { 42 | var astNode = this._astNode; 43 | 44 | _.forOwn(astNode, function(value, key) { 45 | delete astNode[key]; 46 | }); 47 | _.forOwn(this._original, function(value, key) { 48 | astNode[key] = value; 49 | }); 50 | }; 51 | 52 | CallExpressionSelfMO.prototype.getReplacement = function() { 53 | return { 54 | value: this._replacement, 55 | begin: this._astNode.range[0], 56 | end: this._astNode.range[1] 57 | }; 58 | }; 59 | 60 | module.exports.create = function(astNode, replacement, index) {return new CallExpressionSelfMO(astNode, replacement, index);}; 61 | module.exports.code = code; 62 | 63 | })(module); -------------------------------------------------------------------------------- /src/mutationOperator/ComparisonOperatorMO.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This command creates mutations on a given comparison operator. 3 | * Each operator can be mutated to its boundary and its negation counterpart, e.g. 4 | * '<' has '<=' as boundary and '>=' as negation (opposite) 5 | * Created by Martin Koster on 2/11/15. 6 | */ 7 | (function(module) { 8 | 'use strict'; 9 | 10 | var MutationOperator = require('./MutationOperator'), 11 | MutationUtils = require('../utils/MutationUtils'), 12 | operators = { 13 | '<': {boundary: '<=', negation: '>='}, 14 | '<=': {boundary: '<', negation: '>'}, 15 | '>': {boundary: '>=', negation: '<='}, 16 | '>=': {boundary: '>', negation: '<'} 17 | }; 18 | 19 | var code = 'COMPARISON'; 20 | function ComparisonOperatorMO (astNode, replacement) { 21 | MutationOperator.call(this, astNode); 22 | this.code = code; 23 | this._replacement = replacement; 24 | } 25 | 26 | ComparisonOperatorMO.prototype.apply = function () { 27 | var mutationInfo; 28 | 29 | if (!this._original) { 30 | this._original = this._astNode.operator; 31 | this._astNode.operator = this._replacement; 32 | mutationInfo = MutationUtils.createOperatorMutation(this._astNode, this._original, this._replacement); 33 | } 34 | return mutationInfo; 35 | }; 36 | 37 | ComparisonOperatorMO.prototype.revert = function() { 38 | this._astNode.operator = this._original || this._astNode.operator; 39 | this._original = null; 40 | }; 41 | 42 | ComparisonOperatorMO.prototype.getReplacement = function() { 43 | var astNode = this._astNode; 44 | 45 | return { 46 | value: this._replacement, 47 | begin: astNode.left.range[1], 48 | end: astNode.right.range[0] 49 | }; 50 | }; 51 | 52 | module.exports.create = function(astNode) { 53 | var mos = []; 54 | if (operators.hasOwnProperty(astNode.operator)) { 55 | mos.push(new ComparisonOperatorMO(astNode, operators[astNode.operator].boundary)); 56 | mos.push(new ComparisonOperatorMO(astNode, operators[astNode.operator].negation)); 57 | } 58 | return mos; 59 | }; 60 | module.exports.code = code; 61 | })(module); 62 | -------------------------------------------------------------------------------- /test/util/JasmineCustomMatchers.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Provides custom matchers for jasmine tests 3 | * @author Martin Koster [paysdoc@gmail.com], created on 18/08/15. 4 | * Licensed under the MIT license. 5 | */ 6 | var _ = require('lodash'); 7 | (function (module){ 8 | module.exports = { 9 | 10 | toHaveProperties : function() { 11 | return { 12 | /** 13 | * checks whether testee contains the properties (with the same values) as expected 14 | * @param actual the result of the test 15 | * @param expected an object containing the minimum porperties (end their values) that the result ought to contain 16 | * @returns {{pass: boolean | undefined, message: string | undefined}} 17 | */ 18 | compare: function(actual, expected) { 19 | 20 | var recursiveComparison = function(actual, expected) { 21 | var result = {pass: true}; 22 | _.forOwn(expected, function(value, key) { 23 | var actualVal = actual[key]; 24 | if (_.isPlainObject(actualVal) && _.isPlainObject(value)) { 25 | return recursiveComparison(actualVal, value); 26 | } else if (Array.isArray(actualVal) && Array.isArray(value)) { 27 | if (actualVal.length === value.length) { //just check the length of the arrays, deeper comparison should not be necessary 28 | return result; 29 | } 30 | } else if (actualVal === value) { 31 | return result; 32 | } 33 | result.pass = false; 34 | result.message = 'expected ' + JSON.stringify(actual) + ' to have property {' + key + ': ' + JSON.stringify(value) + '}'; 35 | }); 36 | return result; 37 | }; 38 | 39 | if (!_.isObject(expected)) { 40 | return {message: '\'expected\' should be an object'}; 41 | } 42 | 43 | return recursiveComparison(actual, expected); 44 | } 45 | }; 46 | } 47 | }; 48 | })(module); -------------------------------------------------------------------------------- /test/unit/mutationOperator/ArithmeticOperatorMOSpec.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Spec for the Mutation Operator base class 3 | * @author Martin Koster [paysdoc@gmail.com], created on 04/08/15. 4 | * Licensed under the MIT license. 5 | */ 6 | describe('ArithmeticOperatorMO', function() { 7 | var proxyquire = require('proxyquire'), 8 | ArithmeticOperatorMO, 9 | MutationUtilsSpy, 10 | node = {operator: '*', left: {range: [2,3]}, right: {range: [5, 6]}, someStuff: 'someStuff'}, 11 | instances; 12 | 13 | beforeEach(function() { 14 | MutationUtilsSpy = jasmine.createSpyObj('MutationUtils', ['createOperatorMutation']); 15 | ArithmeticOperatorMO = proxyquire('../../../src/mutationOperator/ArithmeticOperatorMO', { 16 | '../utils/MutationUtils': MutationUtilsSpy 17 | }); 18 | instances = ArithmeticOperatorMO.create(node); 19 | }); 20 | 21 | it('creates an empty list of mutation operators', function() { 22 | instances = ArithmeticOperatorMO.create({}); 23 | expect(instances.length).toEqual(0); 24 | }); 25 | 26 | it('mutates a node and reverts it without affecting other parts of that node', function() { 27 | var instance; 28 | 29 | expect(instances.length).toEqual(1); 30 | instance = instances[0]; 31 | 32 | instance.apply(); 33 | expect(node.operator).toEqual('/'); 34 | expect(MutationUtilsSpy.createOperatorMutation).toHaveBeenCalledWith({ operator: '/', left: {range: [2,3]}, right: {range: [5, 6]}, someStuff: 'someStuff'}, '/', '*'); 35 | 36 | instance.apply(); //applying again should have no effect: it will not increase the call count of the spy 37 | expect(node.operator).toEqual('/'); 38 | expect(MutationUtilsSpy.createOperatorMutation.calls.count()).toEqual(1); 39 | 40 | node.someStuff = 'otherStuff'; 41 | instance.revert(); 42 | expect(node.operator).toEqual('*'); 43 | expect(node.someStuff).toEqual('otherStuff'); 44 | 45 | instance.revert(); //reverting again should have no effect 46 | expect(node.operator).toEqual('*'); 47 | }); 48 | 49 | it('retrieves the replacement value and its coordinates', function() { 50 | expect(instances[0].getReplacement()).toEqual({value: '/', begin: 3, end: 5}); 51 | 52 | instances[0]._original = '/'; 53 | expect(instances[0].getReplacement()).toEqual({value: '*', begin: 3, end: 5}); 54 | }); 55 | }); -------------------------------------------------------------------------------- /src/mutationOperator/UpdateExpressionMO.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Command for mutating an update expression 3 | * Created by Martin Koster on 2/16/15. 4 | */ 5 | (function(module) { 6 | 'use strict'; 7 | 8 | var _ = require('lodash'), 9 | MutationOperator = require('./MutationOperator'), 10 | MutationUtils = require('../utils/MutationUtils'), 11 | updateOperatorReplacements = { 12 | '++': '--', 13 | '--': '++' 14 | }; 15 | 16 | var code = 'UPDATE_EXPRESSION'; 17 | function UpdateExpressionMO (astNode) { 18 | MutationOperator.call(this, astNode); 19 | this.code = code; 20 | this._replacement = updateOperatorReplacements[astNode.operator]; 21 | } 22 | 23 | UpdateExpressionMO.prototype.apply = function () { 24 | var astNode = this._astNode, 25 | mutationInfo; 26 | 27 | if (!this._original) { 28 | this._original = astNode.operator; 29 | astNode.operator = this._replacement; 30 | if (astNode.prefix) { 31 | // e.g. ++x 32 | mutationInfo = MutationUtils.createMutation(astNode, astNode.argument.range[0], this._original, this._replacement); 33 | } else { 34 | // e.g. x++ 35 | mutationInfo = _.merge(MutationUtils.createMutation(astNode, astNode.argument.range[1], this._original, this._replacement), { 36 | begin: astNode.argument.range[1], 37 | line: astNode.loc.end.line, 38 | col: astNode.argument.loc.end.column 39 | }); 40 | } 41 | } 42 | 43 | return mutationInfo; 44 | }; 45 | 46 | UpdateExpressionMO.prototype.revert = function() { 47 | this._astNode.operator = this._original || this._astNode.operator; 48 | this._original = null; 49 | }; 50 | 51 | UpdateExpressionMO.prototype.getReplacement = function() { 52 | var astNode = this._astNode, 53 | coordinates = astNode.prefix ? 54 | {begin: astNode.range[0], end: astNode.argument.range[0]} : 55 | {begin: astNode.argument.range[1], end: astNode.range[1]}; 56 | 57 | return _.merge({value: this._replacement}, coordinates); 58 | }; 59 | 60 | module.exports.create = function(astNode) { 61 | if (updateOperatorReplacements.hasOwnProperty(astNode.operator)) { 62 | return [new UpdateExpressionMO(astNode)]; 63 | } else { 64 | return []; 65 | } 66 | }; 67 | module.exports.code = code; 68 | })(module); 69 | -------------------------------------------------------------------------------- /test/unit/mutationOperator/UnaryExpressionMOSpec.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Spec for the Mutation Operator base class 3 | * @author Martin Koster [paysdoc@gmail.com], created on 04/08/15. 4 | * Licensed under the MIT license. 5 | */ 6 | describe('UnaryExpressionMO', function() { 7 | var proxyquire = require('proxyquire'), 8 | UnaryExpressionMO, 9 | MutationUtilsSpy, 10 | node = { 11 | "range": [41, 45], 12 | "type": "UpdateExpression", 13 | "operator": "-", 14 | "argument": { 15 | "range": [42,45], 16 | "type": "Identifier", 17 | "name": "a" 18 | }, 19 | "prefix": true 20 | }, 21 | instances; 22 | 23 | beforeEach(function() { 24 | MutationUtilsSpy = jasmine.createSpyObj('MutationUtils', ['createUnaryOperatorMutation']); 25 | UnaryExpressionMO = proxyquire('../../../src/mutationOperator/UnaryExpressionMO', { 26 | '../utils/MutationUtils': MutationUtilsSpy 27 | }); 28 | instances = UnaryExpressionMO.create(node); 29 | }); 30 | 31 | it('creates an empty list of mutation operators', function() { 32 | var instances = UnaryExpressionMO.create({properties: ['a', 'b']}); 33 | expect(instances.length).toEqual(0); 34 | }); 35 | 36 | it('mutates a node and reverts it without affecting other parts of that node', function() { 37 | var instance; 38 | 39 | expect(instances.length).toEqual(1); 40 | instance = instances[0]; 41 | 42 | instance.apply(); 43 | expect(node.operator).toBeFalsy(); 44 | instance.apply(); //applying again should have no effect: it will not increase the call count of the spy 45 | expect(node.operator).toBeFalsy(); 46 | expect(MutationUtilsSpy.createUnaryOperatorMutation.calls.count()).toEqual(1); 47 | 48 | instance.revert(); 49 | expect(node.operator).toEqual('-'); 50 | instance.revert(); //reverting again should have no effect 51 | expect(node.operator).toEqual('-'); 52 | 53 | expect(MutationUtilsSpy.createUnaryOperatorMutation).toHaveBeenCalledWith(node, undefined, ''); 54 | }); 55 | 56 | it('retrieves the replacement value and its coordinates', function() { 57 | expect(instances[0].getReplacement()).toEqual({value: null, begin: 41, end: 42}); 58 | 59 | instances[0].apply(); //should still be the same after the mutation has been applied 60 | expect(instances[0].getReplacement()).toEqual({value: null, begin: 41, end: 42}); 61 | }); 62 | }); -------------------------------------------------------------------------------- /test/unit/mutationOperator/ObjectMOSpec.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Spec for the Mutation Operator base class 3 | * @author Martin Koster [paysdoc@gmail.com], created on 04/08/15. 4 | * Licensed under the MIT license. 5 | */ 6 | describe('ObjectMO', function() { 7 | var proxyquire = require('proxyquire'), 8 | ObjectMO, 9 | MutationUtilsSpy, 10 | prop1 = {"range": [43,49], "kind": "init"}, 11 | prop2 = {"range": [51,57], "kind": "init"}, 12 | node = {properties: [prop1, prop2]}, 13 | instances; 14 | 15 | beforeEach(function() { 16 | MutationUtilsSpy = jasmine.createSpyObj('MutationUtils', ['createMutation']); 17 | ObjectMO = proxyquire('../../../src/mutationOperator/ObjectMO', { 18 | '../utils/MutationUtils': MutationUtilsSpy 19 | }); 20 | instances = ObjectMO.create(node); 21 | }); 22 | 23 | it('creates an empty list of mutation operators', function() { 24 | instances = ObjectMO.create({properties: ['a', 'b']}); 25 | expect(instances.length).toEqual(0); 26 | }); 27 | 28 | it('mutates a node and reverts it without affecting other parts of that node', function() { 29 | var instance; 30 | 31 | expect(instances.length).toEqual(2); 32 | instance = instances[1]; 33 | 34 | instance.apply(); 35 | expect(node.properties).toEqual([prop1]); 36 | instance.apply(); //applying again should have no effect: it will not increase the call count of the spy 37 | expect(node.properties).toEqual([prop1]); 38 | expect(MutationUtilsSpy.createMutation.calls.count()).toEqual(1); 39 | 40 | instance.revert(); 41 | expect(node.properties).toEqual([prop1, prop2]); 42 | instance.revert(); //reverting again should have no effect 43 | expect(node.properties).toEqual([prop1, prop2]); 44 | 45 | //check that the address references of each property are the same (as these properties may have been assigned to other mutation operators) 46 | expect(node.properties[0] === prop1).toBeTruthy(); 47 | expect(node.properties[1] === prop2).toBeTruthy(); 48 | 49 | expect(MutationUtilsSpy.createMutation).toHaveBeenCalledWith(prop2, 57, prop2); 50 | }); 51 | 52 | it('retrieves the replacement value and its coordinates', function() { 53 | expect(instances[0].getReplacement()).toEqual({value: null, begin: 43, end: 49}); 54 | 55 | instances[0].apply(); //should still be the same after the mutation has been applied 56 | expect(instances[0].getReplacement()).toEqual({value: null, begin: 43, end: 49}); 57 | }); 58 | }); -------------------------------------------------------------------------------- /test/unit/mutationOperator/EqualityOperatorMOSpec.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Spec for the Mutation Operator base class 3 | * @author Martin Koster [paysdoc@gmail.com], created on 04/08/15. 4 | * Licensed under the MIT license. 5 | */ 6 | describe('EqualityOperatorMO', function() { 7 | var proxyquire = require('proxyquire'), 8 | EqualityOperatorMO, 9 | MutationUtilsSpy, 10 | node = { 11 | "type": "BinaryExpression", 12 | "operator": "!==", 13 | "left": { 14 | "range": [5, 6], 15 | "type": "Identifier", 16 | "name": "a" 17 | }, 18 | "right": { 19 | "range": [10, 11], 20 | "type": "Identifier", 21 | "name": "b" 22 | }, 23 | someStuff: 'someStuff' 24 | }, 25 | instances; 26 | 27 | beforeEach(function() { 28 | MutationUtilsSpy = jasmine.createSpyObj('MutationUtils', ['createOperatorMutation']); 29 | EqualityOperatorMO = proxyquire('../../../src/mutationOperator/EqualityOperatorMO', { 30 | '../utils/MutationUtils': MutationUtilsSpy 31 | }); 32 | instances = EqualityOperatorMO.create(node); 33 | }); 34 | 35 | it('creates an empty list of mutation operators', function() { 36 | instances = EqualityOperatorMO.create({}); 37 | expect(instances.length).toEqual(0); 38 | }); 39 | 40 | it('mutates a node and reverts it without affecting other parts of that node', function() { 41 | expect(instances.length).toEqual(1); 42 | 43 | instances[0].apply(); 44 | expect(node.operator).toEqual('==='); 45 | instances[0].apply(); //applying again should have no effect: it will not increase the call count of the spy 46 | expect(node.operator).toEqual('==='); 47 | 48 | node.someStuff = 'otherStuff'; 49 | 50 | instances[0].revert(); 51 | expect(node.operator).toEqual('!=='); 52 | expect(node.someStuff).toEqual('otherStuff'); 53 | 54 | instances[0].revert(); //reverting again should have no effect 55 | expect(node.operator).toEqual('!=='); 56 | 57 | expect(MutationUtilsSpy.createOperatorMutation.calls.count()).toEqual(1); 58 | }); 59 | 60 | it('retrieves the replacement value and its coordinates', function() { 61 | expect(instances[0].getReplacement()).toEqual({value: '===', begin: 6, end: 10}); 62 | 63 | instances[0].apply(); //should still be the same after the mutation has been applied 64 | expect(instances[0].getReplacement()).toEqual({value: '===', begin: 6, end: 10}); 65 | }); 66 | }); -------------------------------------------------------------------------------- /test/unit/mutationOperator/BlockStatementMOSpec.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Spec for the Mutation Operator base class 3 | * @author Martin Koster [paysdoc@gmail.com], created on 04/08/15. 4 | * Licensed under the MIT license. 5 | */ 6 | describe('BlockStatementMO', function() { 7 | var proxyquire = require('proxyquire'), 8 | BlockStatementMO, 9 | MutationUtilsSpy, 10 | node = {body: [{range: [47,50], foo: 'foo'}, {range: [51,54], bar: 'bar'}, ['baz']], someStuff: 'someStuff'}, 11 | instances; 12 | 13 | beforeEach(function() { 14 | MutationUtilsSpy = jasmine.createSpyObj('MutationUtils', ['createMutation']); 15 | BlockStatementMO = proxyquire('../../../src/mutationOperator/BlockStatementMO', { 16 | '../utils/MutationUtils': MutationUtilsSpy 17 | }); 18 | instances = BlockStatementMO.create(node); 19 | }); 20 | 21 | it('creates an empty list of mutation operators', function() { 22 | instances = BlockStatementMO.create({}); 23 | expect(instances.length).toEqual(0); 24 | }); 25 | 26 | it('mutates a node and reverts it without affecting other parts of that node', function() { 27 | var instance; 28 | 29 | expect(instances.length).toEqual(3); 30 | instance = instances[1]; 31 | 32 | instance.apply(); 33 | expect(node.body).toEqual([{range: [47,50], foo: 'foo'}, ['baz']]); 34 | expect(MutationUtilsSpy.createMutation).toHaveBeenCalledWith({ range: [ 51, 54 ], bar: 'bar' }, 54, { range: [ 51, 54 ], bar: 'bar' }); 35 | 36 | instance.apply(); //applying again should have no effect: it will not increase the call count of the spy 37 | expect(node.body).toEqual([{range: [47,50], foo: 'foo'}, ['baz']]); 38 | expect(MutationUtilsSpy.createMutation.calls.count()).toEqual(1); 39 | 40 | node.someStuff = 'otherStuff'; 41 | instance.revert(); 42 | expect(node.body).toEqual([{range: [47,50], foo: 'foo'}, {range: [51,54], bar: 'bar'}, ['baz']]); 43 | expect(node.someStuff).toEqual('otherStuff'); 44 | 45 | instance.revert(); //reverting again should have no effect 46 | expect(node.body).toEqual([{range: [47,50], foo: 'foo'}, {range: [51,54], bar: 'bar'}, ['baz']]); 47 | }); 48 | 49 | it('retrieves the replacement value and its coordinates', function() { 50 | var second = instances[1]; 51 | expect(instances[0].getReplacement()).toEqual({value: null, begin: 47, end: 50}); 52 | 53 | second.apply(); //remove the element: it should still be returned with its coordinates 54 | expect(second.getReplacement()).toEqual({value: null, begin: 51, end: 54}); 55 | }); 56 | }); -------------------------------------------------------------------------------- /test/unit/mutationOperator/LogicalExpressionMOSpec.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Spec for the Mutation Operator base class 3 | * @author Martin Koster [paysdoc@gmail.com], created on 04/08/15. 4 | * Licensed under the MIT license. 5 | */ 6 | describe('LogicalExpressionMO', function() { 7 | var proxyquire = require('proxyquire'), 8 | LogicalExpressionMO, 9 | MutationUtilsSpy, 10 | node = { 11 | "range": [42,50], 12 | "type": "LogicalExpression", 13 | "operator": "||", 14 | "left": { 15 | "range": [42,43], 16 | "type": "Identifier", 17 | "name": "a" 18 | }, 19 | "right": { 20 | "range": [47,50], 21 | "type": "Literal", 22 | "value": "a", 23 | "raw": "'a'" 24 | } 25 | }, 26 | instances; 27 | 28 | beforeEach(function() { 29 | MutationUtilsSpy = jasmine.createSpyObj('MutationUtils', ['createOperatorMutation']); 30 | LogicalExpressionMO = proxyquire('../../../src/mutationOperator/LogicalExpressionMO', { 31 | '../utils/MutationUtils': MutationUtilsSpy 32 | }); 33 | instances = LogicalExpressionMO.create(node); 34 | }); 35 | 36 | it('creates an empty list of mutation operators', function() { 37 | instances = LogicalExpressionMO.create({}); 38 | expect(instances.length).toEqual(0); 39 | }); 40 | 41 | it('mutates a node and reverts it without affecting other parts of that node', function() { 42 | expect(instances.length).toEqual(1); 43 | 44 | instances[0].apply(); 45 | expect(node.operator).toEqual('&&'); 46 | instances[0].apply(); //applying again should have no effect: it will not increase the call count of the spy 47 | expect(node.operator).toEqual('&&'); 48 | 49 | node.someStuff = 'otherStuff'; 50 | 51 | instances[0].revert(); 52 | expect(node.operator).toEqual('||'); 53 | expect(node.someStuff).toEqual('otherStuff'); 54 | 55 | instances[0].revert(); //reverting again should have no effect 56 | expect(node.operator).toEqual('||'); 57 | 58 | expect(MutationUtilsSpy.createOperatorMutation.calls.count()).toEqual(1); 59 | }); 60 | 61 | it('retrieves the replacement value and its coordinates', function() { 62 | expect(instances[0].getReplacement()).toEqual({value: '&&', begin: 43, end: 47}); 63 | 64 | instances[0].apply(); //should still be the same after the mutation has been applied 65 | expect(instances[0].getReplacement()).toEqual({value: '&&', begin: 43, end: 47}); 66 | }); 67 | }); -------------------------------------------------------------------------------- /src/reporter/html/templates/base.hbs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | {{#if fileName}}{{fileName}} - {{/if}}Mutation test results 7 | {{{style}}} 8 | {{{script}}} 9 | 10 | 11 | 12 |
13 |

14 | Mutation test results for 15 | 16 | {{#if fileName}} 17 | {{fileName}} 18 | {{ else }} 19 | All files 20 | {{/if}} 21 | 22 |

23 | 24 |
25 |
26 | Success rate:  27 | 28 | {{stats.successRate}}%  29 | 30 | ({{stats.success}} / {{stats.all}}) 31 | 32 | 33 |
34 |
35 | Survived:  36 | 37 | {{stats.survivedRate}}%  38 | 39 | ({{stats.survived}} / {{stats.all}}) 40 | 41 | 42 |
43 |
44 | Ignored:  45 | 46 | {{stats.ignoredRate}}%  47 | 48 | ({{stats.ignored}} / {{stats.all}}) 49 | 50 | 51 |
52 |
53 | Untested:  54 | 55 | {{stats.untestedRate}}%  56 | 57 | ({{stats.untested}} / {{stats.all}}) 58 | 59 | 60 |
61 |
62 |
63 | 64 | 65 | 66 |
67 | {{{content}}} 68 |
69 | 70 | 73 | 74 | 75 | -------------------------------------------------------------------------------- /test/unit/MutationOperatorHandlerSpec.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Spec for the Mutation operator handler 3 | * @author Martin Koster [paysdoc@gmail.com], created on 24/09/15. 4 | * Licensed under the MIT license. 5 | */ 6 | describe('MutationOperatorHandler', function() { 7 | var MutationOperatorHandler = require('../../src/MutationOperatorHandler'), 8 | _ = require('lodash'), 9 | moHandler, apply1, apply2, revert1, revert2; 10 | 11 | beforeEach( function() { 12 | apply1 = spyOn({apply: function(){return 'apply1';}}, 'apply').and.callThrough(); 13 | apply2 = spyOn({apply: function(){return 'apply2';}}, 'apply').and.callThrough(); 14 | revert1 = spyOn({apply: function(){return 'revert1';}}, 'apply').and.callThrough(); 15 | revert2 = spyOn({apply: function(){return 'revert2';}}, 'apply').and.callThrough(); 16 | moHandler = new MutationOperatorHandler(); 17 | }); 18 | 19 | it('applies a mutation and adds it to its stack', function(){ 20 | expect(moHandler.applyMutation([{code: 'mutationA', apply: apply1, revert: revert1}])).toEqual(['apply1']); 21 | expect(apply1).toHaveBeenCalled(); 22 | expect(moHandler._moStack[0][0].code).toEqual('mutationA'); 23 | }); 24 | 25 | it('applies mutation sets, and reverts them all in the order in which they have been applied', function(){ 26 | var result1 = moHandler.applyMutation([{code: 'mutationA', apply: apply1, revert: revert1}, {code: 'mutationB', apply: apply2, revert: revert2}]), 27 | result2 = moHandler.applyMutation([{code: 'mutationC', apply: apply2, revert: revert1}, {code: 'mutationD', apply: apply2, revert: revert2}]); 28 | 29 | expect(result1).toEqual(['apply1', 'apply2']); 30 | expect(result2).toEqual(['apply2', 'apply2']); 31 | expect(apply1.calls.count()).toBe(1); 32 | expect(apply2.calls.count()).toBe(3); 33 | expect(revert1.calls.count()).toBe(0); 34 | expect(revert2.calls.count()).toBe(0); 35 | 36 | expect(moHandler._moStack.length).toBe(2); 37 | expect(_.pluck(moHandler._moStack[0], 'code')).toEqual(['mutationA', 'mutationB']); 38 | expect(_.pluck(moHandler._moStack[1], 'code')).toEqual(['mutationC', 'mutationD']); 39 | 40 | moHandler.revertMutation(); 41 | expect(revert1.calls.count()).toBe(1); 42 | expect(revert2.calls.count()).toBe(1); 43 | expect(moHandler._moStack.length).toBe(1); 44 | expect(_.pluck(moHandler._moStack[0], 'code')).toEqual(['mutationA', 'mutationB']); 45 | 46 | moHandler.revertMutation(); 47 | expect(revert1.calls.count()).toBe(2); 48 | expect(revert2.calls.count()).toBe(2); 49 | expect(moHandler._moStack.length).toBe(0); 50 | }); 51 | }); 52 | -------------------------------------------------------------------------------- /test/unit/MutatorSpec.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Spec for the Mutator 3 | * @author Martin Koster [paysdoc@gmail.com], created on 24/09/15. 4 | * Licensed under the MIT license. 5 | */ 6 | describe('Mutator', function() { 7 | var proxyquire = require('proxyquire'), 8 | Mutator, tokenizeSpy, applyMutationSpy, revertMutationSpy; 9 | 10 | var mockMutationHandler = {applyMutation: function() { 11 | return [ 12 | {range: [10,46], begin: 10, end: 34, line: 3, col: 7, original: 'foo', replacement: 'bar'}, 13 | {range: [10,46], begin: 12, end: 19, line: 3, col: 12, original: 'foo', replacement: ''} 14 | ]; 15 | }}; 16 | 17 | function mockTokenize() { 18 | return [ 19 | { 20 | "type": "Punctuator", 21 | "value": ")", 22 | "range": [10,11] 23 | }, 24 | { 25 | "type": "Punctuator", 26 | "value": "(", 27 | "range": [18,19] 28 | } 29 | ]; 30 | } 31 | 32 | beforeEach(function() { 33 | tokenizeSpy = spyOn({tokenize: mockTokenize}, 'tokenize').and.callThrough(); 34 | applyMutationSpy = spyOn(mockMutationHandler, 'applyMutation').and.callThrough(); 35 | revertMutationSpy = jasmine.createSpy('revertMutation'); 36 | mockMutationHandler.revertMutation = revertMutationSpy; 37 | Mutator = proxyquire('../../src/Mutator', { 38 | './MutationOperatorHandler': function() {return mockMutationHandler;}, 39 | './JSParserWrapper': {tokenize: tokenizeSpy} 40 | }); 41 | }); 42 | 43 | it('applies a mutation to the AST and returns a calibrated mutation description', function() { 44 | var mutator = new Mutator('dummySrc'); 45 | 46 | var results = mutator.mutate(['some', 'dummy', 'data']); 47 | expect(tokenizeSpy).toHaveBeenCalledWith('dummySrc', {range: true}); 48 | expect(revertMutationSpy.calls.count()).toBe(1); //mutate always calls unMutate first to ensure that no more than 1 mutation set is ever applied 49 | 50 | expect(applyMutationSpy).toHaveBeenCalledWith(['some', 'dummy', 'data']); 51 | expect(results[0]).toEqual({range: [10,46], begin: 11, end: 34, line: 3, col: 7, original: 'foo', replacement: 'bar'}); 52 | expect(results[1]).toEqual({range: [10,46], begin: 12, end: 18, line: 3, col: 12, original: 'foo', replacement: ''}); 53 | }); 54 | 55 | it('reverts the last mutations applied to the AST', function() { 56 | var mutator = new Mutator('dummySrc'); 57 | 58 | mutator.unMutate(); 59 | expect(tokenizeSpy).toHaveBeenCalledWith('dummySrc', {range: true}); 60 | expect(revertMutationSpy.calls.count()).toBe(1); 61 | }); 62 | }); -------------------------------------------------------------------------------- /test/unit/utils/PromiseUtilsSpec.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Spec for the promise utilities 3 | * @author Martin Koster [paysdoc@gmail.com], created on 13/10/15. 4 | * Licensed under the MIT license. 5 | */ 6 | describe('PromiseUtils', function() { 7 | var PromiseUtils = require('../../../src/utils/PromiseUtils'), 8 | Q = require('q'), resultOrder, promisesWithAsync, 9 | f1 = promiseWithAsync(function() {resultOrder.push('f1');}, 150), 10 | f2 = function() {resultOrder.push('f2');}, 11 | f3 = function() {resultOrder.push('f3');}; 12 | 13 | beforeEach(function() { 14 | resultOrder = []; 15 | promisesWithAsync = []; 16 | }); 17 | 18 | it('turns a function into a promise', function(done) { 19 | var result, 20 | cbPromise = PromiseUtils.promisify(function() {result = 'result';}).then(function() { 21 | expect(result).toEqual('result'); 22 | done(); 23 | }); 24 | expect(cbPromise.then).toBeTruthy(); 25 | }); 26 | 27 | it('turns a function with its own resolver into a promise', function(done) { 28 | var cbPromise = PromiseUtils.promisify(function(cb) {cb('result');}, true).then(function(result) { 29 | expect(result).toEqual('result'); 30 | done(); 31 | }); 32 | expect(cbPromise.then).toBeTruthy(); 33 | }); 34 | 35 | it('retains the existing promise if passed to promisify', function() { 36 | var existingPromise = new Q(function() {return 'dummy';}); 37 | expect(PromiseUtils.promisify(existingPromise)).toBe(existingPromise); 38 | }); 39 | 40 | it('runs all given functions as a sequence of promises', function(done) { 41 | PromiseUtils.runSequence([f1, promiseWithAsync(f2, Math.random()*100), f3], new Q({})).then(function() { 42 | expect(resultOrder).toEqual(['f1', 'f2', 'f3']); 43 | expect(promisesWithAsync.length).toBe(2); 44 | done(); 45 | }); 46 | 47 | }); 48 | 49 | it('stops running the sequence if an error occurs', function(done) { 50 | function rotten() {throw 'something smells fishy!';} 51 | PromiseUtils.runSequence([f1, f2, rotten, f3]).then(function() {}, function(error) { 52 | expect(error).toBe('something smells fishy!'); 53 | expect(resultOrder).toEqual(['f1', 'f2']); 54 | done(); 55 | }); 56 | }); 57 | 58 | function promiseWithAsync(cb, timeout) { 59 | return function() { 60 | return PromiseUtils.promisify(function(resolve) { 61 | setTimeout(function() {cb(); resolve();}, timeout); 62 | }, true).then(function() { 63 | promisesWithAsync.push(cb); 64 | }); 65 | }; 66 | } 67 | }); -------------------------------------------------------------------------------- /test/unit/mutationOperator/ComparisonOperatorMOSpec.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Spec for the Mutation Operator base class 3 | * @author Martin Koster [paysdoc@gmail.com], created on 04/08/15. 4 | * Licensed under the MIT license. 5 | */ 6 | describe('ComparisonOperatorMO', function() { 7 | var proxyquire = require('proxyquire'), 8 | ComparisonOperatorMO, 9 | MutationUtilsSpy, 10 | node = { 11 | "type": "BinaryExpression", 12 | "operator": "<", 13 | "left": { 14 | range: [5, 6], 15 | "type": "Identifier", 16 | "name": "a" 17 | }, 18 | "right": { 19 | range: [8, 9], 20 | "type": "Identifier", 21 | "name": "b" 22 | }, 23 | someStuff: 'someStuff' 24 | }, 25 | instances; 26 | 27 | beforeEach(function() { 28 | MutationUtilsSpy = jasmine.createSpyObj('MutationUtils', ['createOperatorMutation']); 29 | ComparisonOperatorMO = proxyquire('../../../src/mutationOperator/ComparisonOperatorMO', { 30 | '../utils/MutationUtils': MutationUtilsSpy 31 | }); 32 | instances = ComparisonOperatorMO.create(node); 33 | }); 34 | 35 | it('creates an empty list of mutation operators', function() { 36 | instances = ComparisonOperatorMO.create({}); 37 | expect(instances.length).toEqual(0); 38 | }); 39 | 40 | it('mutates a node and reverts it without affecting other parts of that node', function() { 41 | expect(instances.length).toEqual(2); 42 | 43 | instances[0].apply(); 44 | expect(node.operator).toEqual('<='); 45 | instances[1].apply(); 46 | expect(node.operator).toEqual('>='); 47 | 48 | instances[1].apply(); //applying again should have no effect: it will not increase the call count of the spy 49 | expect(node.operator).toEqual('>='); 50 | 51 | node.someStuff = 'otherStuff'; 52 | instances[1].revert(); 53 | expect(node.operator).toEqual('<='); 54 | expect(node.someStuff).toEqual('otherStuff'); 55 | 56 | instances[0].revert(); 57 | expect(node.operator).toEqual('<'); 58 | instances[0].revert(); //reverting again should have no effect 59 | 60 | expect(MutationUtilsSpy.createOperatorMutation.calls.count()).toEqual(2); 61 | }); 62 | 63 | it('retrieves the replacement value and its coordinates', function() { 64 | expect(instances[0].getReplacement()).toEqual({value: '<=', begin: 6, end: 8}); 65 | 66 | instances[0].apply(); //should still be the same after the mutation has been applied 67 | expect(instances[0].getReplacement()).toEqual({value: '<=', begin: 6, end: 8}); 68 | }); 69 | }); -------------------------------------------------------------------------------- /test/unit/mutationOperator/CallExpressionSelfMOSpec.js: -------------------------------------------------------------------------------- 1 | /** 2 | * specification for a call expression mutation operator that mutates the expression itself 3 | * @author Martin Koster [paysdoc@gmail.com], created on 10/09/15. 4 | * Licensed under the MIT license. 5 | */ 6 | describe('CallExpressionSelfMO', function() { 7 | var proxyquire = require('proxyquire'), 8 | CallExpressionSelfMO, MutationUtilsSpy, 9 | callee = { 10 | "range": [5, 11], 11 | "type": "MemberExpression", 12 | "computed": false, 13 | "object": { 14 | "type": "Identifier", 15 | "name": "callThis" 16 | }, 17 | "property": { 18 | "type": "Identifier", 19 | "name": "member" 20 | } 21 | }, 22 | args = [{ 23 | "range": [13, 14], 24 | "type": 'Literal', 25 | "value": 'a', 26 | "raw": '\'a\'' 27 | }], 28 | node = { 29 | "range": [0, 16], 30 | "type": 'CallExpression', 31 | "callee": callee, 32 | "arguments": args 33 | }, 34 | instance; 35 | 36 | beforeEach(function() { 37 | jasmine.addMatchers(require('../../util/JasmineCustomMatchers')); 38 | MutationUtilsSpy = jasmine.createSpyObj('MutationUtils', ['createMutation']); 39 | CallExpressionSelfMO = proxyquire('../../../src/mutationOperator/CallExpressionSelfMO', { 40 | '../utils/MutationUtils': MutationUtilsSpy 41 | }); 42 | instance = CallExpressionSelfMO.create(node, node.arguments[0]); 43 | }); 44 | 45 | it('mutates a node and reverts it without affecting other parts of that node', function() { 46 | 47 | instance.apply(); 48 | expect(node).toEqual(args[0]); 49 | 50 | instance.apply(); //applying again should have no effect: it will not increase the call count of the spy 51 | expect(node).toEqual(args[0]); 52 | 53 | instance.revert(); 54 | expect(node.type).toBe("CallExpression"); 55 | expect(node.callee).toBe(callee); 56 | expect(node.arguments).toBe(args); 57 | 58 | instance.revert(); //reverting again should have no effect 59 | expect(node.type).toBe("CallExpression"); 60 | expect(node.callee).toBe(callee); 61 | expect(node.arguments).toBe(args); 62 | 63 | expect(MutationUtilsSpy.createMutation.calls.count()).toEqual(1); 64 | expect(MutationUtilsSpy.createMutation).toHaveBeenCalledWith(node, 16, node, args[0].value); 65 | }); 66 | 67 | it('retrieves the replacement value and its coordinates', function() { 68 | expect(instance.getReplacement()).toEqual({value: args[0], begin: 0, end: 16}); 69 | }); 70 | }); -------------------------------------------------------------------------------- /src/reporter/html/templates/fileScript.js: -------------------------------------------------------------------------------- 1 | /* jshint ignore: start */ 2 | function onLoad() { 3 | each(document.getElementsByClassName('code'), function(element) { 4 | element.addEventListener('mouseover', onMouseOver, false); 5 | element.addEventListener('mousemove', onMouseMove, false); 6 | element.addEventListener('mouseout', onMouseOut, false); 7 | }); 8 | } 9 | 10 | function each(list, fn, thisArg) { 11 | Array.prototype.forEach.call(list, fn, thisArg); 12 | } 13 | 14 | function onMouseOver(event) { 15 | var popup = document.getElementById('popup'); 16 | addMutations(popup, event.target); 17 | setPopupPosition(popup, event); 18 | popup.classList.add("show"); 19 | } 20 | 21 | function onMouseMove(event) { 22 | setPopupPosition(document.getElementById('popup'), event); 23 | } 24 | 25 | function onMouseOut() { 26 | document.getElementById('popup').classList.remove('show'); 27 | } 28 | 29 | function addMutations(popup, target) { 30 | var mutationEl; 31 | 32 | each(target.classList, function(_class) { 33 | if (_class.indexOf('mutation_') > -1) { 34 | mutationEl = document.getElementById(_class); 35 | if (Array.prototype.indexOf.call(mutationEl.classList, 'killed') > -1) { 36 | popup.classList.add('killed'); 37 | popup.classList.remove('survived'); 38 | } else { 39 | popup.classList.add('survived'); 40 | popup.classList.remove('killed'); 41 | } 42 | popup.classList.add(mutationEl.classList); 43 | popup.innerHTML = mutationEl.innerHTML; 44 | } 45 | }); 46 | } 47 | 48 | var showKilledMutations = true; 49 | function toggleKilledMutations() { 50 | showKilledMutations = !showKilledMutations; 51 | 52 | if(showKilledMutations) { 53 | document.querySelector('#toggleKilledMutations').innerHTML = 'Hide killed mutations'; 54 | each(document.querySelectorAll('.code.killed'), function(e) { 55 | e.style.backgroundColor = ''; 56 | }); 57 | each(document.querySelectorAll('#mutations .killed'), function(e) { 58 | e.style.display = ''; 59 | }); 60 | } else { 61 | document.querySelector('#toggleKilledMutations').innerHTML = 'Show killed mutations'; 62 | each(document.querySelectorAll('.code.killed'), function(e) { 63 | e.style.backgroundColor = 'white'; 64 | }); 65 | each(document.querySelectorAll('#mutations .killed'), function(e) { 66 | e.style.display = 'none'; 67 | }); 68 | } 69 | } 70 | 71 | function setPopupPosition(popup, event) { 72 | var top = event.clientY, 73 | left = event.clientX + 10; 74 | 75 | popup.setAttribute('style', 'top:' + top + 'px;left:' + left + 'px;'); 76 | } 77 | 78 | window.onload = onLoad; 79 | -------------------------------------------------------------------------------- /test/unit/mutationOperator/LiteralMOSpec.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Specification for a literal Mutation operator 3 | * @author Martin Koster [paysdoc@gmail.com], created on 21/08/15. 4 | * Licensed under the MIT license. 5 | */ 6 | describe('LiteralMO', function() { 7 | var proxyquire = require('proxyquire'), 8 | LiteralUtils = require('../../../src/utils/LiteralUtils'), 9 | replacementValue = {type: 'Literal', value: 'MUTATION', raw:'\'MUTATION\''}, 10 | shouldReturnAValue, 11 | LiteralMO, MutationUtilsSpy, determineReplacementSpy, node, mos; 12 | 13 | function returnAValue() { 14 | return shouldReturnAValue ? replacementValue : null; 15 | } 16 | 17 | beforeEach(function() { 18 | determineReplacementSpy = spyOn(LiteralUtils, 'determineReplacement').and.callFake(returnAValue); 19 | shouldReturnAValue = true; 20 | MutationUtilsSpy = jasmine.createSpyObj('MutationUtils', ['createMutation']); 21 | LiteralMO = proxyquire('../../../src/mutationOperator/LiteralMO', { 22 | '../utils/LiteralUtils': {determineReplacement: determineReplacementSpy}, 23 | '../utils/MutationUtils': MutationUtilsSpy 24 | }); 25 | 26 | node = {type: "Literal", value: "a", raw: "'a'", range: [5,7]}; 27 | }); 28 | 29 | it('mutates a node and reverts it without affecting other parts of that node', function() { 30 | mos = LiteralMO.create(node); 31 | expect(mos.length).toEqual(1); 32 | 33 | mos[0].apply(); 34 | expect(node.value).toEqual('MUTATION'); 35 | mos[0].apply(); //it shouldn't repeat the mutation 36 | expect(MutationUtilsSpy.createMutation.calls.count()).toEqual(1); 37 | 38 | mos[0].revert(); 39 | expect(node.value).toEqual('a'); 40 | mos[0].revert(); //it shouldn't repeat the mutation 41 | expect(node.value).toEqual('a'); 42 | 43 | }); 44 | 45 | it('doesn\'t mutate a node if no valid replacement can be found', function() { 46 | shouldReturnAValue = false; 47 | mos = LiteralMO.create(node); 48 | mos[0].apply(); // should do nothing 49 | expect(node.value).toEqual('a'); 50 | expect(determineReplacementSpy.calls.count()).toEqual(1); 51 | expect(MutationUtilsSpy.createMutation.calls.count()).toEqual(0); 52 | 53 | mos[0].revert(); // should do nothing 54 | expect(node.value).toEqual('a'); 55 | 56 | }); 57 | 58 | it('retrieves the replacement value and its coordinates', function() { 59 | mos = LiteralMO.create(node); 60 | expect(mos[0].getReplacement()).toEqual({value: replacementValue, begin: 5, end: 7}); 61 | 62 | mos[0].apply(); //should still be the same after the mutation has been applied 63 | expect(mos[0].getReplacement()).toEqual({value: replacementValue, begin: 5, end: 7}); 64 | }); 65 | }); -------------------------------------------------------------------------------- /test/unit/mutationOperator/CallExpressionArgsMOSpec.js: -------------------------------------------------------------------------------- 1 | /** 2 | * specification for a call expression mutation operator that mutates the expression itself 3 | * @author Martin Koster [paysdoc@gmail.com], created on 10/09/15. 4 | * Licensed under the MIT license. 5 | */ 6 | describe('CallExpressionSelfMO', function () { 7 | var proxyquire = require('proxyquire'), 8 | CallExpressionArgsMO, MutationUtilsSpy, 9 | callee = { 10 | "range": [5, 11], 11 | "type": "MemberExpression", 12 | "computed": false, 13 | "object": { 14 | "type": "Identifier", 15 | "name": "callThis" 16 | }, 17 | "property": { 18 | "type": "Identifier", 19 | "name": "member" 20 | } 21 | }, 22 | args = [{ 23 | "range": [13, 14], 24 | "type": "Literal", 25 | "value": "a", 26 | "raw": "'a'" 27 | }], 28 | node = { 29 | "range": [0, 16], 30 | "type": "CallExpression", 31 | "callee": callee, 32 | "arguments": args 33 | }, 34 | replacement = {type: 'Literal', value: 'MUTATION!', raw: '\'MUTATION!\''}, 35 | instance; 36 | 37 | beforeEach(function () { 38 | jasmine.addMatchers(require('../../util/JasmineCustomMatchers')); 39 | MutationUtilsSpy = jasmine.createSpyObj('MutationUtils', ['createMutation']); 40 | CallExpressionArgsMO = proxyquire('../../../src/mutationOperator/CallExpressionArgsMO', { 41 | '../utils/MutationUtils': MutationUtilsSpy 42 | }); 43 | instance = CallExpressionArgsMO.create(node, replacement, 0); 44 | }); 45 | 46 | it('mutates a node and reverts it without affecting other parts of that node', function () { 47 | instance.apply(); 48 | expect(node.arguments[0]).toEqual(replacement); 49 | 50 | instance.apply(); //applying again should have no effect: it will not increase the call count of the spy 51 | expect(node.arguments[0]).toEqual(replacement); 52 | 53 | instance.revert(); 54 | expect(node.type).toBe("CallExpression"); 55 | expect(node.callee).toBe(callee); 56 | expect(node.arguments).toBe(args); 57 | 58 | instance.revert(); //reverting again should have no effect 59 | expect(node.type).toBe("CallExpression"); 60 | expect(node.callee).toBe(callee); 61 | expect(node.arguments).toBe(args); 62 | 63 | expect(MutationUtilsSpy.createMutation.calls.count()).toEqual(1); 64 | expect(MutationUtilsSpy.createMutation).toHaveBeenCalledWith(args[0], 14, args[0], 'MUTATION!'); 65 | }); 66 | 67 | it('retrieves the replacement value and its coordinates', function () { 68 | expect(instance.getReplacement()).toEqual({value: replacement, begin: 13, end: 14}); 69 | }); 70 | 71 | }); -------------------------------------------------------------------------------- /src/utils/ExclusionUtils.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | /** 3 | * ExclusionUtils 4 | * 5 | * @author Jimi van der Woning 6 | */ 7 | var EXCLUSION_KEYWORD = '@excludeMutations'; 8 | 9 | var _ = require('lodash'), 10 | log4js = require('log4js'); 11 | 12 | var MutationOperatorRegistry = require('../MutationOperatorRegistry'); 13 | 14 | var logger = log4js.getLogger('ExclusionUtils'); 15 | 16 | /** 17 | * Parse the comments from a given astNode. It removes all leading asterisks from multiline comments, as 18 | * well as all leading and trailing whitespace. 19 | * @param {object} astNode the AST node from which comments should be retrieved 20 | * @returns {[string]} the comments for the AST node, or an empty array if none could be found 21 | */ 22 | function parseASTComments(astNode) { 23 | var comments = []; 24 | if(astNode && astNode.leadingComments) { 25 | _.forEach(astNode.leadingComments, function(comment) { 26 | if(comment.type === 'Block') { 27 | comments = comments.concat(comment.value.split('\n').map(function(commentLine) { 28 | // Remove asterisks at the start of the line 29 | return commentLine.replace(/^\s*\*\s*/g, '').trim(); 30 | })); 31 | } else { 32 | comments.push(comment.value); 33 | } 34 | }); 35 | } 36 | 37 | return comments; 38 | } 39 | 40 | /** 41 | * Get the specific exclusions for a given [astNode], if there are any. 42 | * @param {object} astNode the AST node from which comments should be retrieved 43 | * @returns {object} a list of mutation codes [key] that are excluded. [value] is always true 44 | */ 45 | function getExclusions(astNode) { 46 | var comments = parseASTComments(astNode), 47 | mutationCodes = MutationOperatorRegistry.getAllMutationCodes(), 48 | params, 49 | exclusions = {}; 50 | 51 | _.forEach(comments, function(comment) { 52 | if(comment.indexOf(EXCLUSION_KEYWORD) !== -1) { 53 | params = comment.match(/\[.*\]/g); 54 | if(params) { 55 | // Replace all single quotes with double quotes to be able to JSON parse them 56 | _.forEach(JSON.parse(params[0].replace(/'/g, '\"')), function(exclusion) { 57 | if(mutationCodes.indexOf(exclusion) !== -1) { 58 | exclusions[exclusion] = true; 59 | } else { 60 | logger.warn('Encountered an unknown exclusion: %s', exclusion); 61 | } 62 | }); 63 | } 64 | 65 | // Exclude all mutations when none are specifically excluded 66 | if(_.keys(exclusions).length === 0){ 67 | _.forEach(mutationCodes, function(code) { 68 | exclusions[code] = true; 69 | }); 70 | } 71 | } 72 | }); 73 | 74 | return exclusions; 75 | } 76 | 77 | module.exports.getExclusions = getExclusions; 78 | -------------------------------------------------------------------------------- /test/unit/MutationAnalyserSpec.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Specification for the Mutation analyser 3 | * @author Martin Koster [paysdoc@gmail.com], created on 01/09/15. 4 | * 5 | * Licensed under the MIT license. 6 | */ 7 | describe('MutationAnalyser', function() { 8 | var proxyquire = require('proxyquire'), 9 | JSParserWrapper = require('../../src/JSParserWrapper'), 10 | MutationOperatorRegistry = require('../../src/MutationOperatorRegistry'), 11 | BlockStatementMO = require('../../src/mutationOperator/BlockStatementMO'), 12 | ArithmeticOperatorMO = require('../../src/mutationOperator/ArithmeticOperatorMO'), 13 | LiteralMO = require('../../src/mutationOperator/LiteralMO'), 14 | MutationConfiguration = require('../../src/MutationConfiguration'), 15 | src = '\'use strict\'; var x = function() {return x + 1;}', 16 | ast, mutationAnalyser, getMOStatusSpy, 17 | selectMutationOperatorsSpy, selectChildNodeFinderSpy; 18 | 19 | var mockWarden = { 20 | getMOStatus: function(node, mutationOperator, MOCode) { 21 | if (MOCode === 'BLOCK_STATEMENT') { 22 | return 'exclude'; 23 | } else if (node.range[0] === 0 && node.range[1] === 12) { 24 | return 'ignore'; 25 | } 26 | return 'include'; 27 | } 28 | }; 29 | 30 | beforeEach(function() { 31 | var MutationAnalyser; 32 | 33 | ast = JSParserWrapper.parse(src, {loc: false}); 34 | jasmine.addMatchers(require('../util/JasmineCustomMatchers')); 35 | 36 | getMOStatusSpy = spyOn(mockWarden, 'getMOStatus').and.callThrough(); 37 | selectMutationOperatorsSpy = spyOn(MutationOperatorRegistry, 'selectMutationOperators').and.callThrough(); 38 | selectChildNodeFinderSpy = spyOn(MutationOperatorRegistry, 'selectChildNodeFinder').and.callThrough(); 39 | MutationAnalyser = proxyquire('../../src/MutationAnalyser', { 40 | './MutationOperatorRegistry': MutationOperatorRegistry 41 | }); 42 | mutationAnalyser = new MutationAnalyser(ast); 43 | }); 44 | 45 | it('finds all eligible mutation operators in the given AST', function() { 46 | var mutationOperatorSets = mutationAnalyser.collectMutations(mockWarden); 47 | expect(mutationOperatorSets.length).toEqual(2); 48 | expect(selectMutationOperatorsSpy.calls.count()).toBe(45); 49 | expect(getMOStatusSpy.calls.count()).toBe(6); 50 | 51 | //if we call collect again the mutation collection should have been cached and returned => ExclusionSpy.getExclusions should not have been called any more times 52 | mutationAnalyser.collectMutations(mockWarden); 53 | expect(selectMutationOperatorsSpy.calls.count()).toBe(45); 54 | expect(getMOStatusSpy.calls.count()).toBe(6); 55 | expect(mutationAnalyser.getMutationOperators()).toBe(mutationOperatorSets); 56 | expect(mutationAnalyser.getIgnored()).toEqual([[0, 12]]); 57 | expect(mutationAnalyser.getExcluded()).toEqual([[ 0, 48 ], [ 0, 48 ], [ 33, 48 ]]); 58 | }); 59 | }); -------------------------------------------------------------------------------- /test/unit/mutationOperator/UpdateExpressionMOSpec.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Spec for the Mutation Operator base class 3 | * @author Martin Koster [paysdoc@gmail.com], created on 04/08/15. 4 | * Licensed under the MIT license. 5 | */ 6 | describe('UpdateExpressionMO', function() { 7 | var proxyquire = require('proxyquire'), 8 | MutationUtils = require('../../../src/utils/MutationUtils'), 9 | UpdateExpressionMO, 10 | createMutationSpy, 11 | node = { 12 | "range": [40, 45], 13 | "type": "UpdateExpression", 14 | "operator": "++", 15 | "argument": { 16 | "range": [42,45], 17 | "type": "Identifier", 18 | "name": "a", 19 | "loc": {"end" : {"line": 3, "column": 7}} 20 | }, 21 | "loc": {"end" : {"line": 3, "column": 5}}, 22 | "prefix": true 23 | }; 24 | 25 | beforeEach(function() { 26 | createMutationSpy = spyOn(MutationUtils, 'createMutation').and.returnValue({}); 27 | UpdateExpressionMO = proxyquire('../../../src/mutationOperator/UpdateExpressionMO', { 28 | '../utils/MutationUtils': {createMutation: createMutationSpy} 29 | }); 30 | }); 31 | 32 | it('creates an empty list of mutation operators', function() { 33 | var instances = UpdateExpressionMO.create({}); 34 | expect(instances.length).toEqual(0); 35 | }); 36 | 37 | it('mutates \'++x\' to \'--x\' and back again', function() { 38 | testUpdateExpressionMO(42); 39 | }); 40 | 41 | it('mutates \'x++\' to \'x--\' and back again', function() { 42 | delete node.prefix; 43 | node.argument.range = [40, 43]; 44 | testUpdateExpressionMO(43); 45 | }); 46 | 47 | function testUpdateExpressionMO(replacement) { 48 | var instances = UpdateExpressionMO.create(node); 49 | expect(instances.length).toEqual(1); 50 | 51 | instances[0].apply(); 52 | expect(node.operator).toEqual('--'); 53 | instances[0].apply(); //applying again should have no effect: it will not increase the call count of the spy 54 | expect(node.operator).toEqual('--'); 55 | 56 | instances[0].revert(); 57 | expect(node.operator).toEqual('++'); 58 | instances[0].revert(); //reverting again should have no effect 59 | expect(node.operator).toEqual('++'); 60 | 61 | expect(createMutationSpy.calls.count()).toEqual(1); 62 | expect(createMutationSpy).toHaveBeenCalledWith(node, replacement, '++', '--'); 63 | } 64 | 65 | it('retrieves the replacement value and its coordinates', function() { 66 | var instances = UpdateExpressionMO.create(node); 67 | expect(instances[0].getReplacement()).toEqual({value: '--', begin: 43, end: 45}); 68 | 69 | //now switch the pdate operator to the front 70 | node.prefix = true; 71 | node.argument.range = [42, 45]; 72 | instances = UpdateExpressionMO.create(node); 73 | expect(instances[0].getReplacement()).toEqual({value: '--', begin: 40, end: 42}); 74 | }); 75 | }); -------------------------------------------------------------------------------- /test/unit/childNodeFinder/PropertyCNFSpec.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Spec for the array child node finder 3 | * @author Martin Koster [paysdoc@gmail.com], created on 29/06/15. 4 | * Licensed under the MIT license. 5 | */ 6 | describe("PropertyCNF", function(){ 7 | 8 | var PropertyCNF = require('../../../src/childNodeFinder/PropertyCNF'); 9 | 10 | it("finds the right child node on a given object expression node", function() { 11 | var node = { 12 | "type": "ObjectExpression", 13 | "properties": [ 14 | { 15 | "type": "Property", 16 | "key": { 17 | "type": "Literal", 18 | "value": "1", 19 | "raw": "\"1\"" 20 | }, 21 | "computed": false, 22 | "value": { 23 | "type": "BinaryExpression", 24 | "operator": "*", 25 | "left": { 26 | "type": "Literal", 27 | "value": 6, 28 | "raw": "6" 29 | }, 30 | "right": { 31 | "type": "Literal", 32 | "value": 7, 33 | "raw": "7" 34 | } 35 | }, 36 | "kind": "init", 37 | "method": false, 38 | "shorthand": false 39 | }, 40 | { 41 | "type": "Property", 42 | "key": { 43 | "type": "Literal", 44 | "value": "2", 45 | "raw": "\"2\"" 46 | }, 47 | "computed": false, 48 | "value": { 49 | "type": "BinaryExpression", 50 | "operator": "*", 51 | "left": { 52 | "type": "Literal", 53 | "value": 3, 54 | "raw": "3" 55 | }, 56 | "right": { 57 | "type": "Literal", 58 | "value": 4, 59 | "raw": "4" 60 | } 61 | }, 62 | "kind": "init", 63 | "method": false, 64 | "shorthand": false 65 | } 66 | ] 67 | }; 68 | expect(new PropertyCNF(node).find()).toEqual([{ 69 | type: 'BinaryExpression', 70 | operator: '*', 71 | left: Object({type: 'Literal', value: 6, raw: '6'}), 72 | right: Object({type: 'Literal', value: 7, raw: '7'}) 73 | }, { 74 | type: 'BinaryExpression', 75 | operator: '*', 76 | left: Object({type: 'Literal', value: 3, raw: '3'}), 77 | right: Object({type: 'Literal', value: 4, raw: '4'}) 78 | }]); 79 | }); 80 | }); 81 | 82 | -------------------------------------------------------------------------------- /src/reporter/html/HtmlReporter.js: -------------------------------------------------------------------------------- 1 | /** 2 | * HTML reporter 3 | * 4 | * @author Martin Koster 5 | * @author Merlin Weemaes 6 | * @author Jimi van der Woning 7 | */ 8 | 'use strict'; 9 | 10 | var FileHtmlBuilder = require("./FileHtmlBuilder"), 11 | IndexHtmlBuilder = require("./IndexHtmlBuilder"), 12 | IOUtils = require("../../utils/IOUtils"), 13 | fs = require('fs'), 14 | path = require('path'), 15 | Q = require('q'), 16 | _ = require('lodash'); 17 | 18 | /** 19 | * Constructor for the HTML reporter. 20 | * 21 | * @param {string} basePath The base path where the report should be created 22 | * @param {object=} config Configuration object 23 | * @constructor 24 | */ 25 | var HtmlReporter = function(basePath, config) { 26 | this._basePath = basePath; 27 | this._config = config; 28 | 29 | var directories = IOUtils.getDirectoryList(basePath, false); 30 | IOUtils.createPathIfNotExists(directories, './'); 31 | }; 32 | 33 | /** 34 | * creates an HTML report using the given results 35 | * @param {object} results 36 | * @returns {*} 37 | */ 38 | HtmlReporter.prototype.create = function(results) { 39 | var self = this; 40 | return Q.Promise(function (resolve, reject) { 41 | IOUtils.createPathIfNotExists(IOUtils.getDirectoryList(results.fileName, true), self._basePath); 42 | new FileHtmlBuilder(self._config).createFileReports(results, self._basePath).then(function() { 43 | try { 44 | self._createDirectoryIndexes(self._basePath); 45 | } catch (error) { 46 | reject(error); 47 | } 48 | resolve(); 49 | }); 50 | }); 51 | }; 52 | 53 | /** 54 | * recursively creates index.html files for all the (sub-)directories 55 | * @param {string} baseDir the base directory from which to start generating index files 56 | * @param {string=} currentDir the current directory 57 | * @returns {Array} files listed in the index.html 58 | */ 59 | HtmlReporter.prototype._createDirectoryIndexes = function(baseDir, currentDir) { 60 | var self = this, 61 | dirContents, 62 | files = []; 63 | 64 | function retrieveStatsFromFile(dir, file) { 65 | var html = fs.readFileSync(dir + '/' + file, 'utf-8'), 66 | regex = /data-mutation-stats="(.+?)"/g; 67 | 68 | try { 69 | return JSON.parse(decodeURI(regex.exec(html)[1])); 70 | } catch (e) { 71 | throw('Unable to parse stats from file ' + file + ', reason: ' + e); 72 | } 73 | } 74 | 75 | currentDir = currentDir || baseDir; 76 | dirContents = fs.readdirSync(currentDir); 77 | _.forEach(dirContents, function(item){ 78 | if (fs.statSync(path.join(currentDir,item)).isDirectory()) { 79 | files = _.union(files, self._createDirectoryIndexes(baseDir, path.join(currentDir, item))); 80 | } else if (item !== 'index.html') { 81 | files.push({fileName: path.join(path.relative(baseDir, currentDir), item), stats: retrieveStatsFromFile(currentDir, item)}); 82 | } 83 | }); 84 | 85 | new IndexHtmlBuilder(baseDir, self._config).createIndexFile(currentDir, files); 86 | return files; 87 | }; 88 | 89 | module.exports = HtmlReporter; 90 | -------------------------------------------------------------------------------- /test/unit/utils/ExclusionUtilsSpec.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Spec for the utility handling the exclusion of mutation operators 3 | * @author Martin Koster [paysdoc@gmail.com], created on 24/09/15. 4 | * Licensed under the MIT license. 5 | */ 6 | describe('ExclusionUtils', function() { 7 | var proxyquire = require('proxyquire'), 8 | JSParserWrapper = require('../../../src/JSParserWrapper'), 9 | ExclusionUtils, loggerSpy; 10 | 11 | function findNodeWithExclusion(node) { 12 | /** 13 | * @excludeMutations #params 14 | */ 15 | return node.body[0].body.body[0]; 16 | } 17 | 18 | beforeEach(function() { 19 | loggerSpy = jasmine.createSpyObj('logger', ['warn']); 20 | ExclusionUtils = proxyquire('../../../src/utils/ExclusionUtils', { 21 | 'log4js': {getLogger: function() {return loggerSpy;}} 22 | }); 23 | }); 24 | 25 | it('excludes the MATH and OBJECT mutators as specified in a block comment of the given node', function() { 26 | var node = JSParserWrapper.parse(findNodeWithExclusion.toString().replace(/#params/, '[\'MATH\', \'OBJECT\']')), 27 | exclusionNode = findNodeWithExclusion(node); //find the node that actually contains the exclusion comment 28 | 29 | var exclusions = ExclusionUtils.getExclusions(node); 30 | expect(exclusions).toEqual({}); 31 | 32 | exclusions = ExclusionUtils.getExclusions(exclusionNode); 33 | expect(exclusions).toEqual({MATH: true, OBJECT: true}); 34 | }); 35 | 36 | it ('excludes all mutators if no operators are specified', function() { 37 | var node = JSParserWrapper.parse(findNodeWithExclusion.toString().replace(/#params/, '')), 38 | exclusionNode = findNodeWithExclusion(node); 39 | 40 | expect(ExclusionUtils.getExclusions(exclusionNode)).toEqual({ 41 | BLOCK_STATEMENT: true, 42 | METHOD_CALL: true, 43 | OBJECT: true, 44 | ARRAY: true, 45 | MATH: true, 46 | COMPARISON: true, 47 | EQUALITY: true, 48 | UPDATE_EXPRESSION: true, 49 | LITERAL: true, 50 | UNARY_EXPRESSION: true, 51 | LOGICAL_EXPRESSION: true 52 | }); 53 | }); 54 | 55 | it('issues a warning if the comments contain an invalid mutation operator', function() { 56 | var input = findNodeWithExclusion.toString().replace(/#params/, '[\'INVALID\']'); 57 | ExclusionUtils.getExclusions(findNodeWithExclusion(JSParserWrapper.parse(input))); 58 | expect(loggerSpy.warn).toHaveBeenCalledWith('Encountered an unknown exclusion: %s', 'INVALID'); 59 | }); 60 | 61 | it('excludes the MATH and OBJECT mutators as specified in a block comment of the given node', function() { 62 | var src = function foo() { 63 | // @excludeMutations #params 64 | return { 65 | baz: function (bar) { 66 | return bar; 67 | } 68 | }; 69 | }, 70 | node = JSParserWrapper.parse(src.toString().replace(/#params/, '[\'LITERAL\']')); 71 | 72 | expect(ExclusionUtils.getExclusions(findNodeWithExclusion(node))).toEqual({LITERAL: true}); 73 | }); 74 | }); -------------------------------------------------------------------------------- /src/reporter/ReportGenerator.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This generator creates reports using istanbul. 3 | * 4 | * Created by Merlin Weemaes on 2/26/15. 5 | */ 6 | (function(module) { 7 | 'use strict'; 8 | 9 | var log4js = require('log4js'), 10 | path = require('path'), 11 | HtmlReporter = require('./html/HtmlReporter'), 12 | IOUtils = require('../utils/IOUtils'), 13 | TestStatus = require('../TestStatus'), 14 | DEFAULT_DIR = path.join('reports', 'js-mutation-testing'); 15 | 16 | var logger = log4js.getLogger('ReportGenerator'); 17 | 18 | module.exports.generate = function(config, results, cb) { 19 | var dir = config.get('reportingDir') || DEFAULT_DIR, 20 | report = new HtmlReporter(dir, config); 21 | 22 | logger.trace('Generating the mutation report...' + JSON.stringify(results)); 23 | 24 | report.create(results) 25 | .then(function() { 26 | logger.info('Generated the mutation report in: %s', dir); 27 | }) 28 | .catch(function(error) { 29 | logger.error('Error creating report: %s', error.message || error); 30 | }) 31 | .done(cb); 32 | }; 33 | 34 | module.exports.createMutationLogMessage = function(config, srcFilePath, mutation, src, testStatus) { 35 | var srcFileName = IOUtils.getRelativeFilePath(config.get('basePath'), srcFilePath), 36 | currentMutationPosition = srcFileName + ':' + mutation.line + ':' + (mutation.col + 1); 37 | var message = currentMutationPosition + ( 38 | mutation.replacement ? 39 | ' Replaced ' + truncateReplacement(config, mutation.original) + ' with ' + truncateReplacement(config, mutation.replacement) + ' -> ' + testStatus : 40 | ' Removed ' + truncateReplacement(config, mutation.original) + ' -> ' + testStatus 41 | ); 42 | 43 | logger.trace('creating log message', message); 44 | return { 45 | mutation: mutation, 46 | survived: testStatus === TestStatus.SURVIVED, 47 | message: message 48 | }; 49 | }; 50 | 51 | function truncateReplacement(config, replacementArg) { 52 | var maxLength = config.get('maxReportedMutationLength'), 53 | replacement; 54 | if (typeof replacementArg === 'string') { 55 | replacement = replacementArg.replace(/\s+/g, ' '); 56 | if (maxLength > 0 && replacement.length > maxLength) { 57 | return replacement.slice(0, maxLength / 2) + ' ... ' + replacement.slice(-maxLength / 2); 58 | } 59 | } else { 60 | replacement = replacementArg; 61 | } 62 | return replacement; 63 | } 64 | 65 | function createStatsMessage(stats) { 66 | var ignoredMessage = stats.ignored ? ' ' + stats.ignored + ' mutations were ignored.' : ''; 67 | var allUnIgnored = stats.all - stats.ignored; 68 | var testedMutations = allUnIgnored - stats.untested - stats.survived; 69 | var percentTested = Math.floor((testedMutations / allUnIgnored) * 100); 70 | return testedMutations + 71 | ' of ' + allUnIgnored + ' unignored mutations are tested (' + percentTested + '%).' + ignoredMessage; 72 | } 73 | })(module); 74 | 75 | -------------------------------------------------------------------------------- /src/Mutator.js: -------------------------------------------------------------------------------- 1 | /** 2 | * modifies an AST Node with the given mutations and returns a mutation description for each mutation done 3 | * @author Martin Koster, created on 07/06/15. 4 | * Licensed under the MIT license. 5 | */ 6 | (function (module) { 7 | 'use strict'; 8 | 9 | var _ = require('lodash'), 10 | MutationOperatorHandler = require('./MutationOperatorHandler'), 11 | JSParserWrapper = require('./JSParserWrapper'), 12 | log4js = require('log4js'); 13 | 14 | var logger = log4js.getLogger('Mutator'); 15 | var Mutator = function(src) { 16 | this._brackets = _.filter(JSParserWrapper.tokenize(src, {range: true}), function(token) { 17 | return token.type === "Punctuator" && token.value.match(/^[\(\)]$/gm); 18 | }); 19 | this._handler = new MutationOperatorHandler(); 20 | }; 21 | 22 | /** 23 | * Mutates the code by applying each mutation operator in the given set 24 | * @param mutationOperatorSet set of mutation operators which can be executed to effect a mutation on the code 25 | * @returns {*} a mutation description detailing which part of the code was mutated and how 26 | */ 27 | Mutator.prototype.mutate = function(mutationOperatorSet) { 28 | var self = this, 29 | mutationDescriptions; 30 | 31 | this.unMutate(); 32 | logger.trace('handler:', this._handler, JSON.stringify(mutationOperatorSet)); 33 | mutationDescriptions = this._handler.applyMutation(mutationOperatorSet); 34 | logger.trace('applied mutation', JSON.stringify(mutationDescriptions)); 35 | return _.reduce(mutationDescriptions, function(result, mutationDescription) { 36 | result.push(_.merge(mutationDescription, calibrateBeginAndEnd(mutationDescription.begin, mutationDescription.end, self._brackets))); 37 | return result; 38 | }, []); 39 | }; 40 | 41 | /** 42 | * undo previous mutation operation, will do nothing if there is no mutation 43 | */ 44 | Mutator.prototype.unMutate = function() { 45 | this._handler.revertMutation(); 46 | }; 47 | 48 | /** 49 | * This function fixes a calibration issue with arithmetic operators. 50 | * See https://github.com/jimivdw/grunt-mutation-testing/issues/7 51 | * 52 | * Though this issue will no longer break the code (as mutations are now done on the AST) it is still useful to 53 | * calibrate the brackets for reporting purposes. 54 | * @param begin start of the range 55 | * @param end end of the range 56 | * @param brackets bracket tokens with their ranges 57 | * @returns {object} a calibrated mutation description 58 | */ 59 | function calibrateBeginAndEnd(begin, end, brackets) { 60 | //return {begin: begin, end: end}; 61 | var beginBracket = _.find(brackets, function (bracket) { 62 | return bracket.range[0] === begin; 63 | }), 64 | endBracket = _.find(brackets, function (bracket) { 65 | return bracket.range[1] === end; 66 | }); 67 | 68 | return { 69 | begin: beginBracket && beginBracket.value === ')' ? begin + 1 : begin, 70 | end: endBracket && endBracket.value === '(' ? end - 1 : end 71 | }; 72 | } 73 | 74 | module.exports = Mutator; 75 | 76 | })(module); -------------------------------------------------------------------------------- /src/MutationAnalyser.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Collects mutations by analysing the given code 3 | * 4 | * @author Martin Koster, created on on 6/7/15. 5 | * Licensed under the MIT license. 6 | */ 7 | (function(module) { 8 | 'use strict'; 9 | 10 | var _ = require('lodash'), 11 | MutationOperatorRegistry = require('./MutationOperatorRegistry'), 12 | LoopInvariantInstrumenter = require('./instrumenter/LoopInvariantInstrumenter'), 13 | log4js = require('log4js'); 14 | 15 | function MutationAnalyser(ast, config) { 16 | this._ast = ast; 17 | this._config = config; 18 | this._mutationOperatorSets = []; 19 | this._ignored = []; 20 | this._excluded = []; 21 | } 22 | 23 | /** 24 | * collects mutation operators for the given code 25 | * @param moWarden the mutation operator warden that guards the status of a mutation operator 26 | * @returns {Array} list of mutation operators that can be applied to this code 27 | */ 28 | MutationAnalyser.prototype.collectMutations = function(moWarden) { 29 | var mutationOperatorSets = this._mutationOperatorSets, 30 | ignoredMOs = this._ignored, 31 | excludedMOs = this._excluded, 32 | maxIterations = this._config.get('maxIterations'); 33 | 34 | function analyseNode(astNode) { 35 | var selectedMutationOperators, 36 | childNodeFinder; 37 | 38 | if (astNode) { 39 | selectedMutationOperators = MutationOperatorRegistry.selectMutationOperators(astNode, moWarden); 40 | Array.prototype.push.apply(mutationOperatorSets, selectedMutationOperators.included); // using push.apply to push array content instead of whole array (which can be empty) 41 | Array.prototype.push.apply(ignoredMOs, selectedMutationOperators.ignored); 42 | Array.prototype.push.apply(excludedMOs, selectedMutationOperators.excluded); 43 | childNodeFinder = MutationOperatorRegistry.selectChildNodeFinder(astNode); 44 | if (childNodeFinder) { 45 | _.forEach(childNodeFinder.find(), function (childNode) { 46 | analyseNode(childNode); 47 | }); 48 | } 49 | 50 | LoopInvariantInstrumenter.doInstrumentation(astNode, maxIterations); 51 | } 52 | } 53 | 54 | if (mutationOperatorSets && mutationOperatorSets.length) { 55 | return this._mutationOperatorSets; 56 | } 57 | 58 | analyseNode(this._ast); 59 | LoopInvariantInstrumenter.addInitialization(this._ast); //add after analyzing so that that code doesn't get mutated 60 | 61 | this._mutationOperatorSets = _.map(mutationOperatorSets, function(mutationOperator) { 62 | return [mutationOperator]; //add operator as single-element array as the rest of the system considers the result to be a list of mutation operator sets (actual implementation will follow) 63 | }); 64 | return this._mutationOperatorSets; 65 | }; 66 | 67 | MutationAnalyser.prototype.getMutationOperators = function() { 68 | return this._mutationOperatorSets; 69 | }; 70 | 71 | MutationAnalyser.prototype.getIgnored = function() { 72 | return this._ignored; 73 | }; 74 | 75 | MutationAnalyser.prototype.getExcluded = function() { 76 | return this._excluded; 77 | }; 78 | 79 | module.exports = MutationAnalyser; 80 | })(module); 81 | -------------------------------------------------------------------------------- /test/unit/reporter/ReportGeneratorSpec.js: -------------------------------------------------------------------------------- 1 | /** 2 | * spec for the report generator 3 | * @author Martin Koster [paysdoc@gmail.com], created on 16/07/15. 4 | * Licensed under the MIT license. 5 | */ 6 | describe('Report Generator', function() { 7 | 'use strict'; 8 | 9 | var loggerStub = jasmine.createSpyObj('logger', ['trace', 'info', 'error']), 10 | promiseResolver = require('../../util/DummyPromiseResolver'), 11 | proxyquire = require('proxyquire'); 12 | 13 | var reportGenerator, resolve, completionTest; 14 | var mockConfig = {}; 15 | 16 | function getMockConfig() { 17 | return { 18 | get: function(key) { 19 | return mockConfig[key]; 20 | } 21 | }; 22 | } 23 | 24 | beforeEach(function() { 25 | resolve = promiseResolver.resolve; 26 | reportGenerator = proxyquire('../../../src/reporter/ReportGenerator', { 27 | './html/HtmlReporter': function() {return {create: resolve};}, 28 | 'path': {join: function(a, b) {return a + '/' + b;}}, 29 | 'log4js': {getLogger: function() {return loggerStub;}} 30 | }); 31 | }); 32 | 33 | it("Creates an HTML report", function(done) { 34 | reportGenerator.generate(getMockConfig(), {}, done); 35 | completionTest = function() { 36 | expect(loggerStub.info.calls.mostRecent()).toEqual({ 37 | object: loggerStub, 38 | args: ['Generated the mutation report in: %s', 'reports/js-mutation-testing'], 39 | returnValue: undefined 40 | }); 41 | }; 42 | }); 43 | 44 | it("Creates an HTML report at specified directory", function(done) { 45 | mockConfig.reportingDir = '.'; 46 | reportGenerator.generate(getMockConfig(), {}, done); 47 | completionTest = function() { 48 | expect(loggerStub.info.calls.mostRecent()).toEqual({ 49 | object: loggerStub, 50 | args: ['Generated the mutation report in: %s', '.'], 51 | returnValue: undefined 52 | }); 53 | }; 54 | }); 55 | 56 | it("Creates an HTML reporter with a rejection object", function(done) { 57 | resolve = function() {return promiseResolver.resolve('reject', {message: 'rejected message'});}; 58 | reportGenerator.generate(getMockConfig(), {}, done); 59 | completionTest = function() { 60 | expect(loggerStub.error.calls.mostRecent()).toEqual({ 61 | object: loggerStub, 62 | args: ['Error creating report: %s', 'rejected message'], 63 | returnValue: undefined 64 | }); 65 | }; 66 | }); 67 | 68 | it("Creates an HTML reporter with a rejection message", function(done) { 69 | resolve = function() {return promiseResolver.resolve('reject', 'rejected');}; 70 | reportGenerator.generate(getMockConfig(), {}, done); 71 | completionTest = function() { 72 | expect(loggerStub.error.calls.mostRecent()).toEqual({ 73 | object:loggerStub, 74 | args: ['Error creating report: %s', 'rejected'], 75 | returnValue: undefined 76 | }); 77 | }; 78 | }); 79 | 80 | it('does not break if no callback is supplied', function(done) { 81 | reportGenerator.generate(getMockConfig(), {}); 82 | completionTest = function(){}; 83 | done(); 84 | }); 85 | 86 | afterEach(function() { 87 | completionTest(); 88 | }); 89 | }); 90 | -------------------------------------------------------------------------------- /src/reporter/html/FileHtmlBuilder.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Builds the HTML for each file in the given results 3 | * Created by Martin Koster on 3/2/15. 4 | */ 5 | var _ = require('lodash'), 6 | fs = require('fs'), 7 | path = require('path'), 8 | Q = require('q'); 9 | 10 | var HtmlFormatter = require('./HtmlFormatter'), 11 | IndexHtmlBuilder = require('./IndexHtmlBuilder'), 12 | StatUtils = require('./StatUtils'), 13 | Templates = require('./Templates'); 14 | 15 | 16 | (function(module) { 17 | 'use strict'; 18 | 19 | var FileHtmlBuilder = function(config) { 20 | this._config = config; 21 | }; 22 | 23 | /** 24 | * creates an HTML report for each file within the given results 25 | * @param {Array} fileResults mutation results for each file 26 | * @param {string} baseDir the base directory in which to write the reports 27 | */ 28 | FileHtmlBuilder.prototype.createFileReports = function(fileResults, baseDir) { 29 | var self = this; 30 | 31 | return Q.Promise(function(resolve) { 32 | var results = fileResults.mutationResults, 33 | formatter = new HtmlFormatter(fileResults.src); 34 | 35 | formatter.formatSourceToHtml(results, function(formattedSource) { 36 | writeReport(fileResults, formatter, formattedSource.split('\n'), baseDir, self._config); 37 | resolve(); 38 | }); 39 | }); 40 | }; 41 | 42 | /** 43 | * write the report to file 44 | */ 45 | function writeReport(fileResult, formatter, formattedSourceLines, baseDir, config) { 46 | var fileName = fileResult.fileName, 47 | stats = StatUtils.decorateStatPercentages(fileResult.stats), 48 | parentDir = path.normalize(baseDir + '/..'), 49 | mutations = formatter.formatMutations(fileResult.mutationResults), 50 | breadcrumb = new IndexHtmlBuilder(baseDir, config).linkPathItems({ 51 | currentDir: parentDir, 52 | fileName: baseDir + '/' + fileName + '.html', 53 | separator: ' >> ', 54 | relativePath: getRelativeDistance(baseDir + '/' + fileName, baseDir ), 55 | linkDirectoryOnly: true 56 | }); 57 | 58 | var file = Templates.fileTemplate({ 59 | sourceLines: formattedSourceLines, 60 | mutations: mutations 61 | }); 62 | 63 | fs.writeFileSync( 64 | path.join(baseDir, fileName + ".html"), 65 | Templates.baseTemplate({ 66 | style: Templates.baseStyleTemplate({ additionalStyle: Templates.fileStyleCode }), 67 | script: Templates.baseScriptTemplate({ additionalScript: Templates.fileScriptCode }), 68 | fileName: path.basename(fileName), 69 | stats: stats, 70 | status: stats.successRate > config.get('successThreshold') ? 'killed' : stats.all > 0 ? 'survived' : 'neutral', 71 | breadcrumb: breadcrumb, 72 | generatedAt: new Date().toLocaleString(), 73 | content: file 74 | }) 75 | ); 76 | } 77 | 78 | function getRelativeDistance(baseDir, currentDir) { 79 | var relativePath = path.relative(baseDir, currentDir), 80 | segments = relativePath.split(path.sep); 81 | 82 | return _.filter(segments, function(segment) { 83 | return segment === '.' || segment === '..'; 84 | }).join('/'); 85 | } 86 | 87 | module.exports = FileHtmlBuilder; 88 | })(module); 89 | -------------------------------------------------------------------------------- /test/unit/mutationOperator/ArrayExpressionMOSpec.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Spec for the Mutation Operator base class 3 | * @author Martin Koster [paysdoc@gmail.com], created on 04/08/15. 4 | * Licensed under the MIT license. 5 | */ 6 | describe('ArrayExpressionMO', function() { 7 | var proxyquire = require('proxyquire'), 8 | first = { 9 | "range": [48, 60], 10 | "type": "ObjectExpression", 11 | "properties": [ 12 | { 13 | "range": [49, 59], 14 | "key": { 15 | "type": "Identifier", 16 | "name": "foo" 17 | }, 18 | "value": { 19 | "type": "Literal", 20 | "value": "foo", 21 | "raw": "'foo'" 22 | } 23 | } 24 | ] 25 | }, 26 | second = { 27 | "range": [62, 67], 28 | "type": "Literal", 29 | "value": "bar" 30 | }, 31 | third = { 32 | "range": [69, 76], 33 | "type": "ArrayExpression", 34 | "elements": [ 35 | { 36 | "range": [70, 75], 37 | "type": "Literal", 38 | "value": "baz" 39 | } 40 | ] 41 | }, 42 | arrayElements = [first, second, third], 43 | node = {elements: arrayElements, someStuff: 'someStuff'}, 44 | ArrayExpressionMO, 45 | MutationUtilsSpy, 46 | instances; 47 | 48 | beforeEach(function() { 49 | MutationUtilsSpy = jasmine.createSpyObj('MutationUtils', ['createMutation']); 50 | ArrayExpressionMO = proxyquire('../../../src/mutationOperator/ArrayExpressionMO', { 51 | '../utils/MutationUtils': MutationUtilsSpy 52 | }); 53 | instances = ArrayExpressionMO.create(node); 54 | }); 55 | 56 | it('creates an empty list of mutation operators', function() { 57 | instances = ArrayExpressionMO.create({}); 58 | expect(instances.length).toEqual(0); 59 | }); 60 | 61 | it('mutates a node and reverts it without affecting other parts of that node', function() { 62 | var instance; 63 | 64 | expect(instances.length).toEqual(3); 65 | instance = instances[1]; 66 | 67 | instance.apply(); 68 | expect(node.elements.length).toBe(2); 69 | expect(node.elements[0]).toBe(first); 70 | expect(node.elements[1]).toBe(third); 71 | 72 | instance.apply(); //applying again should have no effect: it will not increase the call count of the spy 73 | expect(node.elements).toEqual([first, third]); 74 | 75 | node.someStuff = 'otherStuff'; 76 | instance.revert(); 77 | expect(node.elements).toEqual([first, second, third]); 78 | expect(node.someStuff).toEqual('otherStuff'); 79 | 80 | instance.revert(); //reverting again should have no effect 81 | expect(node.elements).toEqual([first, second, third]); 82 | 83 | expect(MutationUtilsSpy.createMutation.calls.count()).toEqual(1); 84 | expect(MutationUtilsSpy.createMutation).toHaveBeenCalledWith(arrayElements[1], 67, arrayElements[1]); 85 | }); 86 | 87 | it('retrieves the replacement value and its coordinates', function() { 88 | expect(instances[0].getReplacement()).toEqual({value: null, begin: 48, end: 60}); 89 | expect(instances[2].getReplacement()).toEqual({value: null, begin: 69, end: 76}); 90 | }); 91 | }); -------------------------------------------------------------------------------- /test/unit/utils/ArrayMutatorUtilSpec.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Specification for the array mutator 3 | * @author Martin Koster [paysdoc@gmail.com], created on 13/09/15. 4 | * Licensed under the MIT license. 5 | */ 6 | describe('ArrayMutatorUtil', function() { 7 | var _ = require('lodash'), 8 | ArrayMutatorUtil = require('../../../src/utils/ArrayMutatorUtil'), 9 | idA = {"type": "Identifier","name": "a"}, 10 | idB = {"type": "Identifier","name": "b"}, 11 | fnExpr = { 12 | "type": "FunctionExpression", 13 | "id": null, 14 | "params": [{"type": "Identifier", "name": "c"}], 15 | "defaults": [], 16 | "body": { 17 | "type": "BlockStatement", 18 | "body": [{ 19 | "type": "ExpressionStatement", 20 | "expression": { 21 | "type": "AssignmentExpression", 22 | "operator": "=", 23 | "left": { "type": "Identifier", "name": "c"}, 24 | "right": {"type": "Literal", "value": 1, "raw": "1"} 25 | } 26 | } 27 | ] 28 | }, 29 | "generator": false, 30 | "expression": false 31 | }, 32 | original = [idA, 'foo', idB, fnExpr, 'foo', idB, 'foo', 3, false]; 33 | 34 | it('Applies a given mutation on a given array by finding the element in the array and removing is', function() { 35 | var callbackSpy = jasmine.createSpy('callback'), 36 | array = _.clone(original); 37 | 38 | ArrayMutatorUtil.removeElement(array, fnExpr, callbackSpy); 39 | expect(array).toEqual([idA, 'foo', idB, 'foo', idB, 'foo', 3, false]); 40 | expect(callbackSpy.calls.count()).toBe(1); 41 | expect(callbackSpy).toHaveBeenCalledWith(fnExpr); 42 | }); 43 | 44 | it('throws an exception if an attempt is made to remove an element that is not in the array', function() { 45 | var callbackSpy = jasmine.createSpy('callback'), 46 | array = _.clone(original); 47 | 48 | expect(ArrayMutatorUtil.removeElement(array, 'fnExpr', callbackSpy)).toBeFalsy(); 49 | expect(callbackSpy).not.toHaveBeenCalled(); 50 | }); 51 | 52 | it('returns elements into an array as close as possible to their original position', function() { 53 | var array = [3]; 54 | ArrayMutatorUtil.restoreElement(array, idB, original); 55 | expect(array).toEqual([{"type": "Identifier","name": "b"}, 3]); 56 | 57 | ArrayMutatorUtil.restoreElement(array, fnExpr, original); 58 | ArrayMutatorUtil.restoreElement(array, 'foo', original); 59 | ArrayMutatorUtil.restoreElement(array, 'foo', original); 60 | ArrayMutatorUtil.restoreElement(array, idA, original); 61 | ArrayMutatorUtil.restoreElement(array, false, original); 62 | ArrayMutatorUtil.restoreElement(array, idB, original); 63 | ArrayMutatorUtil.restoreElement(array, 'foo', original); 64 | expect(array).toEqual(original); 65 | }); 66 | 67 | it('ignores an element if all of its instances are already in the array', function() { 68 | var array = _.clone(original); 69 | ArrayMutatorUtil.restoreElement(array, 'foo', original); //all 'foo's are already added - this one should be ignored 70 | expect(array).toEqual(original); 71 | }); 72 | 73 | it('throws an exception if an element is restored to the array which was not in the original', function() { 74 | var array = [{foo: 'foo'}]; 75 | function returnInvalid() { 76 | ArrayMutatorUtil.restoreElement(array, ['bla', 'ble', 'bli'], original); 77 | } 78 | 79 | expect(returnInvalid).toThrow('Element to be restored not found in original array: bla,ble,bli'); 80 | }); 81 | }); -------------------------------------------------------------------------------- /test/unit/MutationTesterSpec.js: -------------------------------------------------------------------------------- 1 | /** 2 | * spec for the mutation tester 3 | * Created by martin on 28/10/15. 4 | */ 5 | describe('MutationTester', function() { 6 | 7 | var proxyquire = require('proxyquire'), 8 | dummyResolver = function(cb) {cb && cb();}, 9 | mockAfter = dummyResolver, // will be overridden in the tests 10 | mockConfiguration = { 11 | get: function(key) { 12 | if (key === 'mutate') { 13 | return ['file1', 'file2']; 14 | } else if (key === 'after') { 15 | return mockAfter; 16 | } else { return dummyResolver; } 17 | }, 18 | onInitComplete: function(cb) {cb();} 19 | }, 20 | mockMutationScoreCalculator = { 21 | getScorePerFile: function() {return 'someScore';}, 22 | calculateScore: function() {} 23 | }, 24 | mockIOUtils = { 25 | promiseToReadFile: function(fileName) { 26 | if (fileName === 'file1') { 27 | throw ('There was an error'); 28 | } 29 | return 'some source code'; 30 | } 31 | }, 32 | mockReportGenerator = {generate: function () {arguments[2]();}}, 33 | mutationFileTesterSpy, 34 | loggerSpy, 35 | MutationTester; 36 | 37 | beforeEach(function() { 38 | loggerSpy = jasmine.createSpyObj('logger', ['trace', 'info', 'error']); 39 | mutationFileTesterSpy = jasmine.createSpy('mutationFileTester', 'testFile'); 40 | MutationTester = proxyquire('../../src/MutationTester', { 41 | './MutationScoreCalculator': function() {return mockMutationScoreCalculator;}, 42 | './MutationConfiguration': function() {return mockConfiguration;}, 43 | './MutationFileTester': function() {return {testFile: mutationFileTesterSpy};}, 44 | './utils/IOUtils': mockIOUtils, 45 | './reporter/ReportGenerator': mockReportGenerator, 46 | 'log4js': {getLogger: function() {return loggerSpy;}} 47 | }); 48 | }); 49 | 50 | it('mutates each given file and runs the given test on each mutation', function(done) { 51 | spyOn(mockReportGenerator, 'generate').and.callThrough(); 52 | mutationFileTesterSpy.and.callFake(function() {return 'mutationResults';}); 53 | mockAfter = function() { 54 | expect(mutationFileTesterSpy.calls.count()).toBe(1); 55 | expect(mutationFileTesterSpy).toHaveBeenCalledWith('some source code', jasmine.any(Function)); 56 | done(); 57 | }; 58 | new MutationTester({}).test(function() {}); 59 | }); 60 | 61 | it('handles a FATAL error by calling process.exit', function(done) { 62 | var originalExit = process.exit; 63 | var exitSpy = jasmine.createSpy('process', 'exit'); 64 | 65 | Object.defineProperty(process, 'exit', {value: exitSpy}); 66 | 67 | mockAfter = function() { 68 | expect(exitSpy).toHaveBeenCalledWith(1); 69 | expect(loggerSpy.error.calls.count()).toBe(5); 70 | expect(loggerSpy.error.calls.allArgs()).toEqual([ 71 | [ 'An exception occurred after mutating the file: %s', 'file1' ], 72 | [ 'Error message was: %s', 'There was an error' ], 73 | [ 'An exception occurred after mutating the file: %s', 'file2' ], 74 | [ 'Error message was: %s', { severity: 'FATAL' } ], 75 | [ 'Error status was FATAL. All processing will now stop.' ] 76 | ]); 77 | Object.defineProperty(process, 'exit', originalExit); 78 | done(); 79 | }; 80 | 81 | mutationFileTesterSpy.and.callFake(function() {throw {severity: 'FATAL'};}); 82 | new MutationTester({}).test(); 83 | }); 84 | }); -------------------------------------------------------------------------------- /src/MutationTester.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Tests a unit by mutating it and running the given test 3 | * Copyright (c) 2015 Martin Koster, Jimi van der Woning 4 | * Licensed under the MIT license. 5 | */ 6 | (function(module) { 7 | 'use strict'; 8 | 9 | var MutationScoreCalculator = require('./MutationScoreCalculator'), 10 | MutationConfiguration = require('./MutationConfiguration'), 11 | MutationFileTester = require('./MutationFileTester'), 12 | ReportGenerator = require('./reporter/ReportGenerator'), 13 | PromiseUtils = require('./utils/PromiseUtils'), 14 | TestStatus = require('./TestStatus'), 15 | IOUtils = require('./utils/IOUtils'), 16 | log4js = require('log4js'), 17 | path = require('path'), 18 | Q = require('q'), 19 | _ = require('lodash'); 20 | 21 | var logger = log4js.getLogger('MutationTester'); 22 | var MutationTester = function(options) { 23 | this._config = new MutationConfiguration(options); 24 | this._mutationScoreCalculator = new MutationScoreCalculator(); 25 | }; 26 | 27 | MutationTester.prototype.test = function(testCallback) { 28 | var deferred = Q.defer(), 29 | test = testCallback, 30 | self = this, 31 | fileMutationResults = []; 32 | 33 | this._config.onInitComplete(deferred.resolve); 34 | deferred.promise 35 | .then(function() { 36 | var config = self._config, 37 | mutationPromise = new Q(); 38 | 39 | config.get('before')(); //run possible pre-processing 40 | _.forEach(config.get('mutate'), function(fileName) { 41 | mutationPromise = PromiseUtils.runSequence( [ 42 | config.get('beforeEach'), // execute beforeEach 43 | function() {return IOUtils.promiseToReadFile(fileName);}, // read file 44 | function(source) {return mutateAndTestFile(fileName, source, test, self);}, // perform mutation testing on the file 45 | config.get('afterEach'), // execute afterEach 46 | function(fileMutationResult) {fileMutationResults.push(fileMutationResult);} // collect the results 47 | ], mutationPromise, function(error) {handleError(error, fileName, self);}); 48 | }); 49 | return mutationPromise; 50 | }) 51 | .finally(function() { 52 | PromiseUtils.promisify(self._config.get('after')) 53 | .then(function() { //run possible post-processing 54 | logger.info('Mutation Test complete'); 55 | return fileMutationResults; 56 | } 57 | ); 58 | }); 59 | }; 60 | 61 | function mutateAndTestFile(fileName, src, test, ctx) { 62 | var mutationFileTester = new MutationFileTester(fileName, ctx._config, ctx._mutationScoreCalculator); 63 | return mutationFileTester.testFile(src, test); 64 | } 65 | 66 | function handleError(error, fileName, ctx) { 67 | var mutationScoreCalculator = ctx._mutationScoreCalculator; 68 | 69 | logger.error('An exception occurred after mutating the file: %s', fileName); 70 | logger.error('Error message was: %s', error.message || error); 71 | if (error.severity === TestStatus.FATAL) { 72 | logger.error('Error status was FATAL. All processing will now stop.'); 73 | process.exit(1); 74 | } 75 | mutationScoreCalculator && mutationScoreCalculator.calculateScore(fileName, TestStatus.ERROR, 0); 76 | } 77 | 78 | module.exports = MutationTester; 79 | })(module); -------------------------------------------------------------------------------- /test/unit/MutationOperatorWardenSpec.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Spec for the Mutation operator warden 3 | * @author Martin Koster [paysdoc@gmail.com], created on 28/09/15. 4 | * Licensed under the MIT license. 5 | */ 6 | describe('MutationOperatorWarden', function() { 7 | var MutationOperatorWarden = require('../../src/MutationOperatorWarden'), 8 | mockConfig = {}, 9 | mockConfiguration = getMockConfig(), 10 | JSParserWrapper = require('../../src/JSParserWrapper'), 11 | src = '\'use strict\'; var question = \'uh?\';', 12 | mockMutationTypes = [{code: 'GREEN', exclude: true}, {code: 'BLUE', exclude: false}], 13 | mockReplacement = '"MUTATION!"', 14 | mockMO = {getReplacement: function() {return mockReplacement;}}, 15 | getReplacementSpy, node, moWarden; 16 | 17 | function getMockConfig() { 18 | return { 19 | get: function(key) { 20 | return mockConfig[key]; 21 | } 22 | }; 23 | } 24 | 25 | beforeEach(function() { 26 | mockConfig.excludeMutations = []; 27 | mockConfig.ignore = [/('use strict'|"use strict");/]; 28 | mockConfig.ignoreReplacements = []; 29 | mockConfig.discardDefaultIgnore = []; 30 | getReplacementSpy = spyOn(mockMO, 'getReplacement').and.callThrough(); 31 | node = JSParserWrapper.parse(src); 32 | }); 33 | 34 | it('ignores the \'use strict\' statement', function() { 35 | moWarden = new MutationOperatorWarden(src, mockConfiguration, mockMutationTypes); 36 | expect(moWarden.getMOStatus(node, mockMO)).toBe('include'); 37 | expect(moWarden.getMOStatus(node.body[0], mockMO)).toBe('ignore'); 38 | }); 39 | 40 | it('ignores the \'USE STRICT\' statement, ignoring case', function() { 41 | mockConfig.ignore = [/('USE STRICT'|"USE STRICT");/i]; 42 | moWarden = new MutationOperatorWarden(src, mockConfiguration, mockMutationTypes); 43 | expect(moWarden.getMOStatus(node, mockMO)).toBe('include'); 44 | expect(moWarden.getMOStatus(node.body[0], mockMO)).toBe('ignore'); 45 | }); 46 | 47 | it('ignores non-regex strings', function() { 48 | mockConfig.ignore = ['\'uh?\'']; 49 | moWarden = new MutationOperatorWarden(src, mockConfiguration, mockMutationTypes); 50 | expect(moWarden.getMOStatus(node.body[1].declarations[0].init, mockMO)).toBe('ignore'); 51 | }); 52 | 53 | it('ignores a given string replacement', function() { 54 | mockConfig.ignoreReplacements = ['MUTATION!']; 55 | moWarden = new MutationOperatorWarden(src, mockConfiguration, mockMutationTypes); 56 | 57 | expect(moWarden.getMOStatus(node, mockMO)).toBe('ignore'); 58 | mockReplacement = 'MUTETION'; 59 | expect(moWarden.getMOStatus(node, mockMO)).toBe('include'); 60 | }); 61 | 62 | it('ignores a mutation based on given case sensitive regex replacement', function() { 63 | mockConfig.ignoreReplacements = [/MUTA/g]; 64 | moWarden = new MutationOperatorWarden(src, mockConfiguration, mockMutationTypes); 65 | 66 | mockReplacement = 'MUTATION!'; 67 | expect(moWarden.getMOStatus(node, mockMO)).toBe('ignore'); 68 | mockReplacement = 'MUTETION!'; 69 | expect(moWarden.getMOStatus(node, mockMO)).toBe('include'); 70 | 71 | }); 72 | 73 | it('excludes a MutationOperator if its code matches any exclusion codes', function() { 74 | moWarden = new MutationOperatorWarden(src, mockConfiguration, mockMutationTypes); 75 | expect(moWarden.getMOStatus(node, mockMO, 'GREEN')).toBe('exclude'); 76 | }); 77 | 78 | it('throws an exception if it comes across a MutationOperator class without a code', function() { 79 | moWarden = new MutationOperatorWarden(src, mockConfiguration, [{}]); 80 | function callGetMOStatus() { 81 | moWarden.getMOStatus(node, mockMO); 82 | } 83 | expect(callGetMOStatus).toThrowError('expected a MutationOperation class with a code, but code was undefined'); 84 | }); 85 | }); -------------------------------------------------------------------------------- /test/unit/mutationOperator/CallExpressionMOSpec.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Specification for the call expression mutation operator 3 | * @author Martin Koster [paysdoc@gmail.com], created on 17/08/15. 4 | * Licensed under the MIT license. 5 | */ 6 | describe('CallExpressionMO', function() { 7 | var JSParserWrapper = require('../../../src/JSParserWrapper'), 8 | proxyquire = require('proxyquire'), 9 | LiteralUtilsSpy, CallExpressionSelfMOSpy, CallExpressionArgsMOSpy, 10 | CallExpressionMO; 11 | 12 | beforeEach(function() { 13 | jasmine.addMatchers(require('../../util/JasmineCustomMatchers')); 14 | LiteralUtilsSpy = spyOn(require('../../../src/utils/LiteralUtils'), ['determineReplacement']).and.returnValue('MUTATION!'); 15 | CallExpressionArgsMOSpy = jasmine.createSpyObj('CallExpressionArgsMO', ['create']); 16 | CallExpressionSelfMOSpy = jasmine.createSpyObj('CallExpressionSelfMO', ['create']); 17 | CallExpressionMO = proxyquire('../../../src/mutationOperator/CallExpressionMO', { 18 | '../utils/LiteralUtils': LiteralUtilsSpy, 19 | './CallExpressionArgsMO': CallExpressionArgsMOSpy, 20 | './CallExpressionSelfMO': CallExpressionSelfMOSpy 21 | }); 22 | }); 23 | 24 | it('creates no mutation operators for a call expression without arguments', function() { 25 | var ast = JSParserWrapper.parse('callThis();'), 26 | callExpression = ast.body[0].expression, 27 | mos = CallExpressionMO.create(callExpression); 28 | 29 | expect(mos.length).toEqual(0); 30 | }); 31 | 32 | it('creates two mutation operators for a call expression with one argument', function() { 33 | var ast = JSParserWrapper.parse('callThis(a);'), 34 | callExpression = ast.body[0].expression, 35 | mos = CallExpressionMO.create(callExpression); 36 | 37 | expect(mos.length).toEqual(2); 38 | expect(CallExpressionArgsMOSpy.create.calls.count()).toBe(1); 39 | expect(CallExpressionSelfMOSpy.create.calls.count()).toBe(1); 40 | expect(CallExpressionArgsMOSpy.create).toHaveBeenCalledWith(callExpression, {type: 'Literal', value: 'MUTATION!', raw: '\'MUTATION!\''}, 0); 41 | expect(CallExpressionSelfMOSpy.create).toHaveBeenCalledWith(callExpression, callExpression.arguments[0]); 42 | }); 43 | 44 | it('creates 3 mutation operators for a call with one argument to a member of an object', function() { 45 | var ast = JSParserWrapper.parse('callThis.member(a);'), 46 | callExpression = ast.body[0].expression, 47 | mos = CallExpressionMO.create(callExpression); 48 | 49 | expect(mos.length).toEqual(3); 50 | expect(CallExpressionArgsMOSpy.create.calls.count()).toBe(1); 51 | expect(CallExpressionSelfMOSpy.create.calls.count()).toBe(2); 52 | expect(CallExpressionArgsMOSpy.create).toHaveBeenCalledWith(callExpression, {type: 'Literal', value: 'MUTATION!', raw: '\'MUTATION!\''}, 0); 53 | expect(CallExpressionSelfMOSpy.create).toHaveBeenCalledWith(callExpression, callExpression.arguments[0]); 54 | expect(CallExpressionSelfMOSpy.create).toHaveBeenCalledWith(callExpression, callExpression.callee.object); 55 | }); 56 | 57 | it('creates 3 mutation operators for a call with one literal argument to a member of an object', function() { 58 | var ast = JSParserWrapper.parse('callThis.member(\'a\');'), 59 | callExpression = ast.body[0].expression, 60 | mos = CallExpressionMO.create(callExpression); 61 | 62 | expect(mos.length).toBe(3); 63 | expect(CallExpressionArgsMOSpy.create.calls.count()).toBe(1); 64 | expect(CallExpressionSelfMOSpy.create.calls.count()).toBe(2); 65 | expect(CallExpressionArgsMOSpy.create).toHaveBeenCalledWith(callExpression, 'MUTATION!', 0); 66 | expect(CallExpressionSelfMOSpy.create).toHaveBeenCalledWith(callExpression, callExpression.arguments[0]); 67 | expect(CallExpressionSelfMOSpy.create).toHaveBeenCalledWith(callExpression, callExpression.callee.object); 68 | }); 69 | }); -------------------------------------------------------------------------------- /test/unit/MutationFileTesterSpec.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Spec for the MutationFileTester 3 | * Created by martin on 26/10/15. 4 | */ 5 | describe('MutationFileTester', function() { 6 | var proxyquire = require('proxyquire'), 7 | PromiseUtils = require('../../src/utils/PromiseUtils'), 8 | mockArrayGetter = function() {return [];}, 9 | MutationFileTester, mockIOUtils, 10 | reportGeneratorSpy, promiseToWriteFileSpy, mutationScoreCalculatorSpy, mockTestRunner, loggerSpy; 11 | 12 | mockIOUtils = {promiseToWriteFile: function() {return PromiseUtils.promisify(promiseToWriteFileSpy);}}; 13 | beforeEach(function() { 14 | loggerSpy = jasmine.createSpyObj('logger', ['trace', 'info', 'error']); 15 | mutationScoreCalculatorSpy = jasmine.createSpyObj('mutationScoreCalculator', ['calculateScore', 'getScorePerFile']); 16 | reportGeneratorSpy = jasmine.createSpyObj('ReportGenerator', ['createMutationLogMessage', 'generate']); 17 | promiseToWriteFileSpy = jasmine.createSpy('promiseToWriteFile'); 18 | mockTestRunner = {runTest: function() {}}; 19 | MutationFileTester = proxyquire('../../src/MutationFileTester', { 20 | './MutationConfiguration' : function() {return {get: mockArrayGetter};}, 21 | './MutationOperatorRegistry': mockTestRunner, 22 | './MutationOperatorWarden': function() {}, 23 | './TestRunner': mockTestRunner, 24 | './reporter/ReportGenerator': reportGeneratorSpy, 25 | './MutationAnalyser': function() {return {collectMutations: function() {return [['ms1'], ['ms2']];}, getIgnored: function(){}};}, 26 | './JSParserWrapper': {stringify: function(){}}, 27 | './utils/IOUtils': mockIOUtils, 28 | './Mutator': function() {return {mutate: function(param) {return param;}, unMutate: function() {}};}, 29 | 'sync-exec': function() {return {status: 0};}, 30 | 'log4js': {getLogger: function() {return loggerSpy;}} 31 | }); 32 | }); 33 | 34 | it('tests a file using a function', function(done) { 35 | spyOn(mockTestRunner, 'runTest').and.returnValue('KILLED'); 36 | new MutationFileTester('some.file.name', {}, mutationScoreCalculatorSpy).testFile('someSrc', function(cb) {cb(0);}) 37 | .then(function() { 38 | expect(reportGeneratorSpy.createMutationLogMessage.calls.count()).toBe(2); 39 | expect(mutationScoreCalculatorSpy.calculateScore.calls.count()).toBe(2); 40 | expect(promiseToWriteFileSpy.calls.count()).toBe(2); 41 | expect(mutationScoreCalculatorSpy.calculateScore).toHaveBeenCalledWith('some.file.name', 'KILLED', undefined); 42 | done(); 43 | }, function(error) { 44 | console.error(error); 45 | done(); 46 | }); 47 | }); 48 | 49 | it('tests a file using an exec string', function(done) { 50 | spyOn(mockTestRunner, 'runTest').and.returnValue('SURVIVED'); 51 | new MutationFileTester('some.file.name', {}, mutationScoreCalculatorSpy).testFile('someSrc', 'gulp test') 52 | .then(function() { 53 | expect(reportGeneratorSpy.createMutationLogMessage.calls.count()).toBe(2); 54 | expect(mutationScoreCalculatorSpy.calculateScore.calls.count()).toBe(2); 55 | expect(promiseToWriteFileSpy.calls.count()).toBe(2); 56 | expect(reportGeneratorSpy.createMutationLogMessage).toHaveBeenCalledWith(jasmine.any(Object), 'some.file.name', 'ms1', 'someSrc', 'SURVIVED' ); 57 | done(); 58 | }, function(error) { 59 | console.error(error); 60 | done(); 61 | }); 62 | }); 63 | 64 | it('fails on an IOUtils problem', function() { 65 | mockIOUtils.promiseToWriteFile = PromiseUtils.promisify(function() {throw ('cannot do that!');}); 66 | new MutationFileTester('some.file.name', {getMutate: 'getMutate'}, mutationScoreCalculatorSpy).testFile('someSrc', 'gulp test') 67 | .then(function() { 68 | fail('fulfillment callback should not have been called after failure'); 69 | }); 70 | }); 71 | }); -------------------------------------------------------------------------------- /src/MutationFileTester.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Mutates a single file and runs given unit test against it 3 | * @author Martin Koster [paysdoc@gmail.com], created on 05/10/15. 4 | * Licensed under the MIT license. 5 | */ 6 | (function(module) { 7 | 'use strict'; 8 | 9 | var MutationScoreCalculator = require('./MutationScoreCalculator'), 10 | MutationOperatorRegistry = require('./MutationOperatorRegistry'), 11 | MutationOperatorWarden = require('./MutationOperatorWarden'), 12 | MutationConfiguration = require('./MutationConfiguration'), 13 | ReportGenerator = require('./reporter/ReportGenerator'), 14 | MutationAnalyser = require('./MutationAnalyser'), 15 | JSParserWrapper = require('./JSParserWrapper'), 16 | PromiseUtils = require('./utils/PromiseUtils'), 17 | TestRunner = require('./TestRunner'), 18 | IOUtils = require('./utils/IOUtils'), 19 | Mutator = require('./Mutator'), 20 | log4js = require('log4js'), 21 | _ = require('lodash'), 22 | Q = require('q'); 23 | 24 | var logger = log4js.getLogger('MutationFileTester'); 25 | var MutationFileTester = function(fileName, config, mutationScoreCalculator) { 26 | this._fileName = fileName; 27 | this._config = typeof config.onInitComplete === 'function' ? config : new MutationConfiguration(config); 28 | this._mutationScoreCalculator = mutationScoreCalculator || new MutationScoreCalculator(); 29 | }; 30 | 31 | MutationFileTester.prototype.testFile = function(src, test) { 32 | var mutationScoreCalculator = this._mutationScoreCalculator, 33 | config = this._config, 34 | fileName = this._fileName, 35 | moWarden = new MutationOperatorWarden(src, config, MutationOperatorRegistry.getMutationOperatorTypes()), 36 | ast = JSParserWrapper.parse(src), 37 | mutationAnalyser = new MutationAnalyser(ast, config), 38 | mutationDescriptions, 39 | mutationResults = [], 40 | mutator = new Mutator(src), 41 | promise = new Q({}); 42 | 43 | function mutateAndWriteFile(mutationOperatorSet) { 44 | mutationDescriptions = mutator.mutate(mutationOperatorSet); 45 | logger.debug('writing file', fileName, '\n', JSParserWrapper.stringify(ast)); 46 | return IOUtils.promiseToWriteFile(fileName, JSParserWrapper.stringify(ast)) 47 | .then(function() { 48 | return mutationDescriptions; 49 | }); 50 | } 51 | 52 | function postProcessing(result) { 53 | mutationDescriptions.forEach(function(mutationDescription) { 54 | mutationResults.push(ReportGenerator.createMutationLogMessage(config, fileName, mutationDescription, src, result)); 55 | }); 56 | mutationScoreCalculator.calculateScore(fileName, result, mutationAnalyser.getIgnored()); 57 | mutator.unMutate(); 58 | } 59 | 60 | function doReporting() { 61 | var fileMutationResult = { 62 | stats: mutationScoreCalculator.getScorePerFile(fileName), 63 | src: src, 64 | fileName: fileName, 65 | mutationResults: mutationResults 66 | }; 67 | ReportGenerator.generate(config, fileMutationResult, function() { 68 | logger.info('Done mutating file: %s', fileName); 69 | }); 70 | return fileMutationResult; 71 | } 72 | 73 | mutator = new Mutator(src); 74 | _.forEach(mutationAnalyser.collectMutations(moWarden), function (mutationOperatorSet) { 75 | promise = PromiseUtils.runSequence([ 76 | function() {return mutateAndWriteFile(mutationOperatorSet);}, // apply the mutations 77 | function() {return TestRunner.runTest(config, test);}, // run the test 78 | postProcessing // revert the mutation and generate mutation report 79 | ], promise, handleError); 80 | }); 81 | 82 | return promise.then(doReporting); 83 | }; 84 | 85 | function handleError(error) { 86 | logger.error('file processing stopped', error); 87 | throw(error); //throw up to calling party 88 | } 89 | 90 | module.exports = MutationFileTester; 91 | })(module); -------------------------------------------------------------------------------- /src/MutationOperatorWarden.js: -------------------------------------------------------------------------------- 1 | /** 2 | * There are several ways in which a mutation can be prevented from being applied: 3 | * - it is excluded via global options 4 | * - the comments of the source code to be mutated contain an explicit exclusion for a certain section 5 | * - one or more regex patterns determine sections of code for which mutations should be ignored 6 | * 7 | * A mutation operator can therefore have one of the following statuses: allowed, ignored or excluded. 8 | * The MutationOperatorWarden applies the rules above to determine that status 9 | * 10 | * @author Martin Koster [paysdoc@gmail.com], created on 28/09/15. 11 | * Licensed under the MIT license. 12 | */ 13 | (function (module){ 14 | 'use strict'; 15 | 16 | var _ = require('lodash'), 17 | ExclusionUtils = require('./utils/ExclusionUtils'); 18 | 19 | function MutationOperatorWarden(src, config, mutationOperatorTypes) { 20 | this._src = src; 21 | this._excludedMutations = config.get('excludeMutations'); 22 | this._ignore = config.get('ignore'); 23 | this._ignoreReplacements = config.get('ignoreReplacements'); 24 | this._mutationOperatorTypes = mutationOperatorTypes; 25 | } 26 | 27 | MutationOperatorWarden.prototype.getMOStatus = function(node, mutationOperator, mutationCode) { 28 | var excludes = _.merge( 29 | getDefaultExcludes(this._mutationOperatorTypes), 30 | this._excludedMutations, 31 | ExclusionUtils.getExclusions(node) 32 | ); 33 | if(isInIgnoredRange(node, this._src, this._ignore) || isReplacementIgnored(mutationOperator.getReplacement(), this._ignoreReplacements)) { 34 | return "ignore"; 35 | } else if (excludes[mutationCode]) { 36 | return "exclude"; 37 | } 38 | 39 | return 'include'; 40 | }; 41 | 42 | function isInIgnoredRange(node, src, ignore) { 43 | return _.any(getIgnoredRanges(ignore, src), function (ignoredRange) { 44 | return ignoredRange.coversRange(node.range); 45 | }); 46 | } 47 | 48 | function isReplacementIgnored(replacement, ignoreReplacements) { 49 | return _.any(ignoreReplacements, function(ignoredReplacement) { 50 | ignoredReplacement = _.isRegExp(ignoredReplacement) ? ignoredReplacement : new RegExp(ignoredReplacement); 51 | ignoredReplacement.lastIndex = 0; //reset the regex 52 | return ignoredReplacement.test(replacement); 53 | }); 54 | } 55 | 56 | function getIgnoredRanges(ignore, src) { 57 | function IgnoredRange(start, end) { 58 | this.start = start; 59 | this.end = end; 60 | 61 | this.coversRange = function(range) { 62 | return this.start <= range[0] && range[1] <= this.end; 63 | }; 64 | } 65 | 66 | var ignoredRanges = [], 67 | // Convert to array of RegExp instances with the required options (global and multiline) set 68 | ignoreParts = _.map(ignore, function(ignorePart) { 69 | if(_.isRegExp(ignorePart)) { 70 | return new RegExp(ignorePart.source, 'gm' + (ignorePart.ignoreCase ? 'i' : '')); 71 | } else { 72 | return new RegExp(ignorePart.replace(/([\/\)\(\[\]\{}'"\?\*\.\+\|\^\$])/g, function (all, group) {return '\\' + group;}), 'gm'); 73 | } 74 | }); 75 | 76 | _.forEach(ignoreParts, function(ignorePart) { 77 | var match; 78 | while(match = ignorePart.exec(src)) { 79 | ignoredRanges.push(new IgnoredRange(match.index, match.index + match[0].length)); 80 | } 81 | }); 82 | 83 | return ignoredRanges; 84 | } 85 | 86 | /** 87 | * returns the default exclusion status of each mutation command 88 | * @returns {object} a list of mutation codes [key] and whether or not they're excluded [value] 89 | */ 90 | function getDefaultExcludes(mutationOperatorTypes) { 91 | var excludes = {}; 92 | _.forEach(mutationOperatorTypes, function(operator){ 93 | if(operator.code) { 94 | excludes[operator.code] = !!operator.exclude; 95 | } else { 96 | throw new TypeError('expected a MutationOperation class with a code, but code was ' + operator.code); 97 | } 98 | }); 99 | return excludes; 100 | } 101 | 102 | module.exports = MutationOperatorWarden; 103 | })(module); -------------------------------------------------------------------------------- /test/unit/MutationConfigurationSpec.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Specification for the mutation configuration 3 | * @author Martin Koster [paysdoc@gmail.com], created on 04/09/15. 4 | * Licensed under the MIT license. 5 | */ 6 | describe('MutationConfiguration', function() { 7 | 8 | var proxyquire = require('proxyquire'), 9 | Q = require('q'), 10 | src = '\'use strict\'; var question = \'uh?\';', 11 | MutationConfiguration, mockGlob; 12 | 13 | beforeEach(function() { 14 | mockGlob = {sync: function(file) {return file;}}; 15 | spyOn(mockGlob, 'sync').and.callThrough(); 16 | MutationConfiguration = proxyquire('../../src/MutationConfiguration', { 17 | 'glob': mockGlob, 18 | './utils/CopyUtils': {copyToTemp: function() {return new Q('temp/dir/path');}} 19 | }); 20 | }); 21 | 22 | it('creates getters from the properties in config', function() { 23 | var config = new MutationConfiguration({ 24 | lib: ['some/path', 'another/path', 'some/spec/path'], 25 | mutate: ['some/path', 'another/path'], 26 | specs: 'some/spec/path', 27 | discardDefaultIgnore: false, 28 | ignore: [/use struct/], 29 | ignoreReplacements: '"MUTATION!"', 30 | reporters: {text: {dir: 'someDir', file: 'someFile'}} 31 | }); 32 | 33 | expect(mockGlob.sync).toHaveBeenCalledWith('some/path', {dot: true}); 34 | 35 | // check for the existence of the getters 36 | expect(config.get('basePath')).toBe('.'); 37 | expect(config.get('logLevel')).toBe('INFO'); 38 | expect(config.get('reporters').text).toEqual({dir: 'someDir', file: 'someFile'}); 39 | expect(config.get('ignoreReplacements')).toEqual(['"MUTATION!"']); 40 | expect(config.get('excludeMutations')).toEqual([]); 41 | expect(config.get('maxReportedMutationLength')).toBe(80); 42 | expect(config.get('mutateProductionCode')).toBeFalsy(); 43 | expect(config.get('discardDefaultIgnore')).toBeFalsy(); 44 | expect(config.get('mutate')).toEqual(['some/path', 'another/path']); 45 | expect(config.get('ignore')).toEqual([/('use strict'|"use strict");/, /use struct/]); 46 | }); 47 | 48 | it('creates defaults with minimal configuration', function() { 49 | var config = new MutationConfiguration({lib: 'some/lib/path', mutate: 'some/path', specs: 'some/spec/path'}); 50 | 51 | expect(config.get('discardDefaultIgnore')).toBeFalsy(); 52 | expect(config.get('ignoreReplacements')).toEqual([]); 53 | expect(config.get('ignore').toString()).toBe('/(\'use strict\'|"use strict");/'); 54 | }); 55 | 56 | it('does not add \'use strict\' to the defaults if discardDefaultIgnore is set', function() { 57 | var config = new MutationConfiguration({ 58 | lib: ['some/path', 'some/spec/path'], 59 | mutate: ['some/path'], 60 | specs: ['some/spec/path'], 61 | discardDefaultIgnore: true, 62 | ignore: [/use struct/] 63 | }); 64 | 65 | expect(config.get('discardDefaultIgnore')).toBeTruthy(); 66 | expect(config.get('ignoreReplacements')).toEqual([]); 67 | expect(config.get('ignore')).toEqual([/use struct/]); 68 | }); 69 | 70 | it('logs an error if required options are not set', function() { 71 | var breakit = function() {return new MutationConfiguration(src);}; 72 | expect(breakit).toThrowError('Not all required options have been set'); 73 | }); 74 | 75 | it('has maintenance functions (before, after, beforeEach, etc...) that are don\'t have getters', function() { 76 | var config = new MutationConfiguration({lib: ['some/path'], mutate: ['some/path'], specs: ['some/spec/path'], ignore: [/use struct/]}), 77 | dummySpy = jasmine.createSpy('dummy'); 78 | config.get('before')(dummySpy); 79 | config.get('beforeEach')(dummySpy); 80 | config.get('after')(dummySpy); 81 | config.get('afterEach')(dummySpy); 82 | 83 | expect(dummySpy.calls.count()).toBe(4); 84 | }); 85 | 86 | it('executes a given callback once the file initialization is complete', function(done) { 87 | var config = new MutationConfiguration({lib: 'some/path', mutate: 'some/path', specs: 'some/spec/path', ignore: [/use struct/], 'mutateProductionCode': true}), 88 | deferred = Q.defer(); 89 | 90 | config.onInitComplete(deferred.resolve); 91 | deferred.promise.then(done); //no need to expect anything: if done is called, we know that the function has done its job 92 | }); 93 | }); -------------------------------------------------------------------------------- /test/unit/reporter/html/HtmlReporterSpec.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Spec for the HtmlReporter 3 | * @author Martin Koster [paysdoc@gmail.com], created on 20/07/15. 4 | * Licensed under the MIT license. 5 | */ 6 | describe('HtmlReporter', function() { 7 | 'use strict'; 8 | 9 | var promiseResolver = require('../../../util/DummyPromiseResolver'), 10 | fileHtmlBuilderSpy = {createFileReports: promiseResolver.resolve}, 11 | indexHtmlBuilderSpy = jasmine.createSpyObj('IndexHtmlBuilder', ['createIndexFile']), 12 | proxyquire = require('proxyquire'), 13 | IOUtilsMock = {createPathIfNotExists: {}, getDirectoryList: {}}, 14 | fsMock = {readFileSync: {}, readdirSync: {}, statSync: {}}, 15 | pathMock = {join: {}, relative: {}}; 16 | 17 | 18 | var mocks = { 19 | results: {filename: 'fileA'} 20 | }; 21 | 22 | var readDirStub = function() {return arguments[0] === '.' ? mocks.dirContents : [];}; 23 | 24 | var HtmlReporter, createIndexFileCount = 0; 25 | 26 | beforeEach(function() { 27 | mocks.dirContents = []; 28 | spyOn(pathMock, 'join').and.callFake(function() {return arguments[0] + '/' + arguments[1];}); 29 | spyOn(pathMock, 'relative').and.callFake(function() {return arguments[1];}); 30 | spyOn(fsMock, 'readdirSync').and.callFake(readDirStub); 31 | spyOn(IOUtilsMock, 'createPathIfNotExists'); 32 | spyOn(IOUtilsMock, 'getDirectoryList').and.callFake(function() {return arguments[0];}); 33 | spyOn(fileHtmlBuilderSpy, 'createFileReports').and.callThrough(); 34 | HtmlReporter = proxyquire('../../../../src/reporter/html/HtmlReporter', { 35 | './FileHtmlBuilder': function() {return fileHtmlBuilderSpy;}, 36 | './IndexHtmlBuilder': function() {return indexHtmlBuilderSpy;}, 37 | '../../utils/IOUtils': IOUtilsMock, 38 | 'path': pathMock, 39 | 'fs': fsMock 40 | }); 41 | }); 42 | 43 | it('creates nothing as results are empty', function(done) { 44 | var htmlReportPromise = new HtmlReporter('.').create({}); 45 | htmlReportPromise.then(function() { 46 | expect(fileHtmlBuilderSpy.createFileReports.calls.count()).toEqual(1); 47 | createIndexFileCount++; 48 | done(); 49 | }); 50 | }); 51 | 52 | it('creates a file path for every given result', function(done) { 53 | var htmlReportPromise = new HtmlReporter('.').create(mocks.results); 54 | htmlReportPromise.then(function() { 55 | expect(IOUtilsMock.getDirectoryList.calls.allArgs()).toEqual([[ '.', false ], [ undefined, true ]]); 56 | createIndexFileCount++; 57 | done(); 58 | }); 59 | }); 60 | 61 | it('creates directory indexes for every given directory in the path of the given file', function(done) { 62 | mocks.dirContents = ['dir1', 'dir2']; 63 | spyOn(fsMock, 'statSync').and.returnValue({isDirectory: function() {return true;}}); 64 | var htmlReportPromise = new HtmlReporter('.').create(mocks.results); 65 | htmlReportPromise.then(function() { 66 | expect(fsMock.statSync.calls.allArgs()).toEqual([['./dir1'], ['./dir2']]); 67 | createIndexFileCount = createIndexFileCount + 3; 68 | done(); 69 | }); 70 | }); 71 | 72 | it('retrieves stats from a file which is not a directory', function(done) { 73 | mocks.dirContents = ['dir1']; 74 | spyOn(fsMock, 'readFileSync').and.returnValue('data-mutation-stats="%7B%22stats%22:%22json stats%22%7D"'); 75 | spyOn(fsMock, 'statSync').and.returnValue({isDirectory: function() {return false;}}); 76 | var htmlReportPromise = new HtmlReporter('.').create(mocks.results); 77 | htmlReportPromise.then(function() { 78 | expect(fsMock.statSync.calls.allArgs()).toEqual([['./dir1']]); 79 | createIndexFileCount++; 80 | done(); 81 | }); 82 | }); 83 | 84 | it('fails with a message when no stats are found', function(done) { 85 | mocks.dirContents = ['dir1']; 86 | spyOn(fsMock, 'readFileSync').and.returnValue('some dummy value'); 87 | spyOn(fsMock, 'statSync').and.returnValue({isDirectory: function() {return false;}}); 88 | var htmlReportPromise = new HtmlReporter('.').create(mocks.results); 89 | htmlReportPromise.then(function() { 90 | expect(fsMock.statSync.calls.allArgs()).toEqual([['./dir1']]); 91 | done.fail('Expected a parse error on dir1'); 92 | }, function(error) { 93 | expect(error).toEqual('Unable to parse stats from file dir1, reason: TypeError: Cannot read property \'1\' of null'); 94 | done(); 95 | }); 96 | }); 97 | 98 | it('ignores index.html when collecting stats', function(done) { 99 | mocks.dirContents = ['index.html']; 100 | spyOn(fsMock, 'statSync').and.returnValue({isDirectory: function() {return false;}}); 101 | var htmlReportPromise = new HtmlReporter('.').create(mocks.results); 102 | htmlReportPromise.then(function() { 103 | createIndexFileCount++; 104 | expect(fsMock.statSync.calls.allArgs()).toEqual([['./index.html']]); 105 | done(); 106 | }, function(error) { 107 | done.fail(error); 108 | }); 109 | }); 110 | 111 | afterEach(function() { 112 | expect(indexHtmlBuilderSpy.createIndexFile.calls.count()).toEqual(createIndexFileCount); 113 | }); 114 | }); -------------------------------------------------------------------------------- /src/utils/ArrayMutatorUtil.js: -------------------------------------------------------------------------------- 1 | /** 2 | * utility class for removing array elements and adding them in the correct order 3 | * 4 | * Restoring the elements in the correct (original) order can be of crucial importance as it determines - in the case of a 5 | * block statement - the order in which statements are executed. 6 | * 7 | * The difficulty here is with comparing array elements. 8 | * @author Martin Koster [paysdoc@gmail.com], created on 13/09/15. 9 | * Licensed under the MIT license. 10 | */ 11 | (function(module) { 12 | 'use strict'; 13 | var _ = require('lodash'); 14 | 15 | /** 16 | * remove the element from the array 17 | * @param {Array} array array from which to remove the element 18 | * @param {*} element element to be removed 19 | * @param {Function} callback function to be called if provided. Callback will be invoked with one argument: ((given)element) 20 | * @returns {*} the result of the callback 21 | */ 22 | function removeElement(array, element, callback) { 23 | var i = array.indexOf(element), 24 | cbResult; 25 | 26 | if (i > -1) { 27 | cbResult = callback && callback(element); 28 | array.splice(i, 1); 29 | } 30 | 31 | return cbResult; 32 | } 33 | 34 | /** 35 | * restores an element to an array in the same relative position as in the original array, bearing in mind that the 36 | * array may still be missing other elements that distort the position. 37 | * 38 | * The way this is done it to find the absolute position of the element in the original array by seeking its index. 39 | * If the element is represented array multiple times in the original an array of indexes is returned. This array 40 | * only contains the indexes of elements that have NOT yet been added (e.g. during previous calls to this function). 41 | * The first of these indexes is used as reference index. 42 | * 43 | * Subsequently, a suitable spot is found in the destination array. inside the search loop pending indexes (this time 44 | * of the current item of the iteration) are retrieved once again in order to see whether 45 | * a) there are more of this element in original and 46 | * b) any of these occur after the element to add (referenceIndex) 47 | * If the latter (b) is the case the element is inserted before the current item, otherwise it's appended to the end 48 | * of the array 49 | * 50 | * @param {Array} destination array to which to restore the element 51 | * @param {*} element the element to restore to its correct (relative) position 52 | * @param {Array} original the array in its original state - used for reference 53 | */ 54 | function restoreElement(destination, element, original) { 55 | var origElementIndexes = getPendingIndexes(original, destination, element, true), 56 | stillSearching = true, referenceIndex, itemIdx, pendingIndexes; 57 | 58 | if (!origElementIndexes.length) { 59 | return; //this means that all elements in the original array are already in 'array' -> nothing more to be done; 60 | } 61 | 62 | referenceIndex = origElementIndexes[0]; 63 | for(itemIdx = 0; itemIdx < destination.length && stillSearching; itemIdx++) { 64 | pendingIndexes = getPendingIndexes(original, destination.slice(0, itemIdx), destination[itemIdx]); 65 | if (pendingIndexes.length && referenceIndex < pendingIndexes[0]) { 66 | destination.splice(itemIdx, 0, element); 67 | stillSearching = false; 68 | } 69 | } 70 | 71 | if (stillSearching) { 72 | destination.push(element); 73 | } 74 | } 75 | 76 | /** 77 | * Retrieves the indexes of all occurrences in 'original' of given element that have not yet been added to 78 | * 'destination' 79 | */ 80 | function getPendingIndexes(original, destination, element, shouldBeInOriginal) { 81 | var origElementIndexes = getElementIndexes(original, element), 82 | arrayElementIndexes = getElementIndexes(destination, element); 83 | 84 | function getElementIndexes(collection, value) { 85 | return _.transform(collection, function (result, el, idx) { 86 | if (el === value) { //check for reference equality rather than for instance _.eq, as we need the original astNode references back in the array 87 | result.push(idx); 88 | } 89 | }); 90 | } 91 | 92 | // not found in original array when it should? then throw an error 93 | if (shouldBeInOriginal && !origElementIndexes.length) { 94 | throw 'Element to be restored not found in original array: ' + element; 95 | } 96 | 97 | /* Remove number of elements found in 'array' from 'original', leaving only those instances of element in 98 | * original that have not yet been added to 'array'. 99 | * The upshot of this is that equivalent elements (in 'original') are added from front to back in 'array' 100 | * (ie. first time an element is added it will be placed in the first available spot starting from the front of the array). 101 | */ 102 | return origElementIndexes.slice(Math.min(arrayElementIndexes.length, origElementIndexes.length), origElementIndexes.length); 103 | } 104 | 105 | 106 | module.exports.removeElement = removeElement; 107 | module.exports.restoreElement = restoreElement; 108 | })(module); -------------------------------------------------------------------------------- /src/reporter/html/HtmlFormatter.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This class exposes a number of formatting methods for adding specific markup to code text and mutation results 3 | * 4 | * Created by Martin Koster on 06/03/15. 5 | */ 6 | var HandleBars = require('handlebars'), 7 | HtmlParser = require('htmlparser2'), 8 | log4js = require('log4js'), 9 | Q = require('q'), 10 | _ = require('lodash'); 11 | 12 | var codeTemplate = HandleBars.compile(''); 13 | var mutationTemplate = HandleBars.compile('{{mutationText}}{{mutationStatus}}'); 14 | (function(module) { 15 | 'use strict'; 16 | 17 | var HtmlFormatter = function(src) { 18 | this._src = src; 19 | }; 20 | 21 | HtmlFormatter.prototype._getHtmlIndexes = function() { 22 | var self = this, 23 | parser, 24 | result = [], 25 | pushTagIndexes = function() { 26 | result.push({startIndex: parser.startIndex, endIndex: parser.endIndex}); 27 | }; 28 | 29 | var promise = Q.Promise(function(resolve) { 30 | parser = new HtmlParser.Parser({ 31 | onopentag: pushTagIndexes, 32 | onclosetag: pushTagIndexes, 33 | onend: resolve 34 | }, {decodeEntities:true, recognizeCDATA:true}); 35 | parser.write(self._src); 36 | parser.end(); 37 | }); 38 | 39 | return promise.thenResolve(result); 40 | }; 41 | 42 | /** 43 | * formats the list of mutations to display on the 44 | * @returns {string} markup with all mutations to be displayed 45 | */ 46 | HtmlFormatter.prototype.formatMutations = function (mutationResults) { 47 | var formattedMutations = '', 48 | orderedResults = mutationResults.sort(function(left, right) { 49 | return left.mutation.line - right.mutation.line; 50 | }); 51 | _.forEach(orderedResults, function(mutationResult){ 52 | var mutationHtml = mutationTemplate({ 53 | mutationId: getDOMMutationId(mutationResult.mutation), 54 | mutationStatus: mutationResult.survived ? 'survived' : 'killed', 55 | mutationText: mutationResult.message 56 | }); 57 | formattedMutations = formattedMutations.concat(mutationHtml); 58 | }); 59 | return formattedMutations; 60 | }; 61 | 62 | /** 63 | * format the source code by inserting markup where each of the given mutations was applied 64 | * @param {[object]} mutationResults an array of mutation results 65 | * @param {function} callback will be called with the formatted source as argument 66 | * @returns {string} the source code formatted with markup into the places where mutations have taken place 67 | */ 68 | HtmlFormatter.prototype.formatSourceToHtml = function (mutationResults, callback){ 69 | var self = this; 70 | 71 | this._getHtmlIndexes().done(function(htmlIndexes) { 72 | var srcArray = self._src.replace(/[\r]?\n/g, '\n').split(''); //split the source into character array after removing (Windows style) carriage return characters 73 | 74 | _.forEach(htmlIndexes, function(indexPair) { 75 | var i = indexPair.startIndex; 76 | for (; i < indexPair.endIndex; i++) { 77 | srcArray[i] = srcArray[i].replace(//, '>'); 78 | } 79 | }); 80 | _.forEach(mutationResults, function(mutationResult){ 81 | formatMultilineFragment(srcArray, mutationResult); 82 | }); 83 | 84 | if (typeof callback === 'function') { 85 | callback(srcArray.join('')); 86 | } 87 | }); 88 | }; 89 | 90 | /** 91 | * formats a multi line fragment in such a way that each line gets encased in its own set of html tags, 92 | * thus preventing contents of a to be broken up with
  • tags later on 93 | * @param {string} srcArray the source split up into an array of characters 94 | * @param {object} mutationResult the current mutation result 95 | */ 96 | function formatMultilineFragment (srcArray, mutationResult) { 97 | var mutation = mutationResult.mutation, 98 | classes = 'code'.concat(mutationResult.survived ? ' survived ' : ' killed ', getDOMMutationId(mutation)), 99 | i, l = mutation.end; 100 | 101 | if (!l) { return; }//not a likely scenario, but to be sure... 102 | 103 | for (i = mutation.begin; i < l; i++) { 104 | if (i === mutation.begin) { 105 | srcArray[i] = codeTemplate({classes: classes}) + srcArray[i]; 106 | } 107 | if (srcArray[i] === '\n') { 108 | if (i > 0 ) { 109 | srcArray[i-1] = srcArray[i-1] + ''; 110 | } 111 | if (i < l-1) { 112 | srcArray[i+1] = codeTemplate({classes: classes}) + srcArray[i+1]; 113 | } 114 | } 115 | } 116 | 117 | srcArray[l-1] = srcArray[l-1] + ''; 118 | } 119 | 120 | /* creates a mutation id from the given mutation result */ 121 | function getDOMMutationId(mutation) { 122 | return 'mutation_' + mutation.mutationId + '_' + mutation.begin + '_' + mutation.end; 123 | } 124 | 125 | module.exports = HtmlFormatter; 126 | })(module); 127 | -------------------------------------------------------------------------------- /test/unit/JSParseWrapperSpec.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Specification for the JSParseWrapepr 3 | * @author Martin Koster [paysdoc@gmail.com], created on 31/08/15. 4 | * Licensed under the MIT license. 5 | */ 6 | describe('JSParseWrapper', function () { 7 | var JSParserWrapper = require('../../src/JSParserWrapper'); 8 | var tokens = [{ 9 | "type": "Keyword", 10 | "value": "function", 11 | "range": [22, 30] 12 | }, { 13 | "type": "Identifier", 14 | "value": "a", 15 | "range": [31, 32] 16 | }, { 17 | "type": "Punctuator", 18 | "value": "(", 19 | "range": [32, 33] 20 | }, { 21 | "type": "Punctuator", 22 | "value": ")", 23 | "range": [33, 34] 24 | }, { 25 | "type": "Punctuator", 26 | "value": "{", 27 | "range": [35, 36] 28 | }, { 29 | "type": "Keyword", 30 | "value": "var", 31 | "range": [36, 39] 32 | }, { 33 | "type": "Identifier", 34 | "value": "a", 35 | "range": [40, 41] 36 | }, { 37 | "type": "Punctuator", 38 | "value": "=", 39 | "range": [42, 43] 40 | }, { 41 | "type": "Numeric", 42 | "value": "3", 43 | "range": [44, 45] 44 | }, { 45 | "type": "Punctuator", 46 | "value": "+", 47 | "range": [46, 47] 48 | }, { 49 | "type": "Numeric", 50 | "value": "2", 51 | "range": [48, 49] 52 | }, { 53 | "type": "Punctuator", 54 | "value": ";", 55 | "range": [49, 50] 56 | }, { 57 | "type": "Punctuator", 58 | "value": "}", 59 | "range": [50, 51] 60 | }]; 61 | var expected = { 62 | "type": "Program", 63 | "body": [{ 64 | "type": "FunctionDeclaration", 65 | "id": { 66 | "type": "Identifier", 67 | "name": "a", 68 | "range": [31, 32] 69 | }, 70 | "params": [], 71 | "defaults": [], 72 | "body": { 73 | "type": "BlockStatement", 74 | "body": [{ 75 | "type": "VariableDeclaration", 76 | "declarations": [{ 77 | "type": "VariableDeclarator", 78 | "id": { 79 | "type": "Identifier", 80 | "name": "a", 81 | "range": [40, 41] 82 | }, 83 | "init": { 84 | "type": "BinaryExpression", 85 | "operator": "+", 86 | "left": { 87 | "type": "Literal", 88 | "value": 3, 89 | "raw": "3", 90 | "range": [44, 45] 91 | }, 92 | "right": { 93 | "type": "Literal", 94 | "value": 2, 95 | "raw": "2", 96 | "range": [48, 49] 97 | }, 98 | "range": [44, 49] 99 | }, 100 | "range": [40, 49] 101 | }], 102 | "kind": "var", 103 | "range": [36, 50] 104 | }], 105 | "range": [35, 51] 106 | }, 107 | "rest": null, 108 | "generator": false, 109 | "expression": false, 110 | "range": [22, 51] 111 | }], 112 | "range": [22, 51], 113 | "comments": [{ 114 | "type": "Line", 115 | "value": "some silly function", 116 | "range": [0, 21] 117 | }], 118 | "tokens": tokens, 119 | "leadingComments": [{ 120 | "type": "Line", 121 | "value": "some silly function", 122 | "range": {"0": 0, "1": 21}, 123 | "extendedRange": [0, 22] 124 | }] 125 | }; 126 | 127 | beforeEach(function () { 128 | jasmine.addMatchers(require('../util/JasmineCustomMatchers')); 129 | }); 130 | 131 | it('parses a valid piece of source code', function () { 132 | var ast = JSParserWrapper.parse('//some silly function\nfunction a() {var a = 3 + 2;}', {tokens: true}); 133 | expect(ast).toHaveProperties(expected); 134 | }); 135 | 136 | it('throws and error if an invalid piece of source code is offered for parsing', function () { 137 | function parseInvalid() { 138 | return JSParserWrapper.parse('function a() {var a = 3 + ;}'); 139 | } 140 | 141 | expect(parseInvalid).toThrowError('Line 1: Unexpected token ;'); 142 | }); 143 | 144 | it('tokenizes a valid piece of source code', function () { 145 | var ast = JSParserWrapper.tokenize('//bla\nfunction a() {var a = 3 + 2;}'); 146 | expect(ast).toHaveProperties(tokens); 147 | }); 148 | 149 | it('stringifies an AST Node', function() { 150 | expect(JSParserWrapper.stringify(expected)).toEqual('function a() {\n var a = 3 + 2;\n}'); 151 | }); 152 | 153 | it('throws an error if an invalid AST node is passed to the stringify function', function() { 154 | function stringifyInvalid() { 155 | return JSParserWrapper.stringify({daft: 'old object'}); 156 | } 157 | expect(stringifyInvalid).toThrowError('Unknown node type: undefined'); 158 | }); 159 | 160 | }); --------------------------------------------------------------------------------