├── .editorconfig ├── .gitattributes ├── .gitignore ├── .jshintrc ├── .mutation-testing-conf.js ├── .travis.yml ├── Gruntfile.js ├── README.md ├── lib ├── Mutator.js ├── TestStatus.js ├── karma │ ├── KarmaCodeSpecsMatcher.js │ ├── KarmaServerManager.js │ ├── KarmaServerPool.js │ ├── KarmaServerStatus.js │ └── KarmaWorker.js └── reporting │ ├── ReportGenerator.js │ ├── html │ ├── FileHtmlBuilder.js │ ├── HtmlFormatter.js │ ├── HtmlReporter.js │ ├── IndexHtmlBuilder.js │ ├── StatUtils.js │ ├── Templates.js │ └── templates │ │ ├── base.hbs │ │ ├── baseScript.hbs │ │ ├── baseStyle.hbs │ │ ├── file.hbs │ │ ├── fileScript.js │ │ ├── fileStyle.css │ │ ├── folder.hbs │ │ ├── folderFileRow.hbs │ │ └── folderStyle.css │ └── json │ └── JSONReporter.js ├── license ├── mutationCommands ├── AssignmentExpressionCommand.js ├── CommandExecutor.js ├── CommandRegistry.js ├── MutateArithmeticOperatorCommand.js ├── MutateArrayExpressionCommand.js ├── MutateBaseCommand.js ├── MutateBlockStatementCommand.js ├── MutateCallExpressionCommand.js ├── MutateComparisonOperatorCommand.js ├── MutateForLoopCommand.js ├── MutateIterationCommand.js ├── MutateLiteralCommand.js ├── MutateLogicalExpressionCommand.js ├── MutateObjectCommand.js ├── MutateUnaryExpressionCommand.js └── MutateUpdateExpressionCommand.js ├── package.json ├── spikes └── karma.js ├── tasks ├── mutation-testing-karma.js ├── mutation-testing-mocha.js └── mutation-testing.js ├── test ├── .jshintrc ├── expected │ ├── arguments.json │ ├── arguments.txt │ ├── arrays.json │ ├── arrays.txt │ ├── attributes.json │ ├── attributes.txt │ ├── comparisons.json │ ├── comparisons.txt │ ├── dont-test-inside-surviving-mutations.json │ ├── dont-test-inside-surviving-mutations.txt │ ├── flag-all-mutations.json │ ├── flag-all-mutations.txt │ ├── function-calls.json │ ├── function-calls.txt │ ├── grunt.txt │ ├── html-fragments.json │ ├── html-fragments.txt │ ├── ignore.json │ ├── ignore.txt │ ├── karma.json │ ├── karma.txt │ ├── literals.json │ ├── literals.txt │ ├── logical-expressions.json │ ├── logical-expressions.txt │ ├── math-operators.json │ ├── math-operators.txt │ ├── mocha.json │ ├── mocha.txt │ ├── test-is-failing-without-mutation.json │ ├── test-is-failing-without-mutation.txt │ ├── unary-expressions.json │ ├── unary-expressions.txt │ ├── update-expressions.json │ └── update-expressions.txt ├── fixtures │ ├── karma-mocha │ │ ├── karma-endlessLoop-test.js │ │ ├── karma-mathoperators-test.js │ │ ├── karma-test.js │ │ ├── karma-update-expressions-test.js │ │ ├── karma.conf.js │ │ ├── mutation-testing-file-specs.json │ │ ├── script-endlessLoop.js │ │ ├── script-mathoperators.js │ │ ├── script-update-expressions.js │ │ ├── script1.js │ │ └── script2.js │ └── mocha │ │ ├── arguments-test.js │ │ ├── arguments.js │ │ ├── array-test.js │ │ ├── array.js │ │ ├── attribute-test.js │ │ ├── attribute.js │ │ ├── comparisons-test.js │ │ ├── comparisons.js │ │ ├── function-calls-test.js │ │ ├── function-calls.js │ │ ├── html-fragments-test.js │ │ ├── html-fragments.js │ │ ├── literals-test.js │ │ ├── literals.js │ │ ├── logicalExpression-test.js │ │ ├── logicalExpression.js │ │ ├── mathoperators-test.js │ │ ├── mathoperators.js │ │ ├── mocha-test.js │ │ ├── mocha-test2.js │ │ ├── mutationCommands │ │ └── CommandRegistrySpec.js │ │ ├── script1.js │ │ ├── script2.js │ │ ├── unaryExpression-test.js │ │ ├── unaryExpression.js │ │ ├── update-expressions-test.js │ │ ├── update-expressions.js │ │ └── utils │ │ ├── ExclusionUtilsSpec.js │ │ └── ScopeUtilsSpec.js ├── mutation-testing-itest-slow.js ├── mutation-testing-itest.js ├── mutations-test.js └── test-utils.js └── utils ├── CopyUtils.js ├── ExclusionUtils.js ├── IOUtils.js ├── LiteralUtils.js ├── MutationUtils.js ├── OptionUtils.js └── ScopeUtils.js /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 4 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text eol=lf 3 | 4 | # Custom for Visual Studio 5 | *.cs diff=csharp 6 | 7 | # Standard to msysgit 8 | *.doc diff=astextplain 9 | *.DOC diff=astextplain 10 | *.docx diff=astextplain 11 | *.DOCX diff=astextplain 12 | *.dot diff=astextplain 13 | *.DOT diff=astextplain 14 | *.pdf diff=astextplain 15 | *.PDF diff=astextplain 16 | *.rtf diff=astextplain 17 | *.RTF diff=astextplain 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log 3 | reports 4 | tmp 5 | .idea 6 | spikes/ 7 | *.iml 8 | -------------------------------------------------------------------------------- /.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 | } 14 | -------------------------------------------------------------------------------- /.mutation-testing-conf.js: -------------------------------------------------------------------------------- 1 | exports.ignore = [/^\s*console.log\(/, /^function/]; -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "0.10" 4 | before_install: npm install -g grunt-cli 5 | sudo: false 6 | -------------------------------------------------------------------------------- /lib/Mutator.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Collects, locates and applies mutations 3 | * 4 | * Copyright (c) 2014 Marco Stahl 5 | * Licensed under the MIT license. 6 | */ 7 | 8 | 'use strict'; 9 | var esprima = require('esprima'), 10 | escodegen = require('escodegen'), 11 | _ = require('lodash'), 12 | Utils = require('../utils/MutationUtils'), 13 | MutateBaseCommand = require('../mutationCommands/MutateBaseCommand'), 14 | ExclusionUtils = require('../utils/ExclusionUtils'), 15 | CommandRegistry = require('../mutationCommands/CommandRegistry'), 16 | CommandExecutor = require('../mutationCommands/CommandExecutor'); 17 | 18 | function Mutator(src, options) { 19 | var ast = esprima.parse(src, _.merge({range: true, loc: true, tokens: true, comment: true}, options)); 20 | this._src = src; 21 | this._ast = escodegen.attachComments(ast, ast.comments, ast.tokens); 22 | this._brackets = _.filter(esprima.tokenize(src, {range: true}), {"type": "Punctuator", "value": "("}); 23 | } 24 | 25 | Mutator.prototype.collectMutations = function(excludeMutations) { 26 | 27 | var src = this._src, 28 | brackets = this._brackets, 29 | globalExcludes = _.merge(CommandRegistry.getDefaultExcludes(), excludeMutations), 30 | tree = {node: this._ast, parentMutationId: _.uniqueId()}, 31 | mutations = []; 32 | 33 | function forEachMutation(subtree, processMutation) { 34 | var astNode = subtree.node, 35 | excludes = subtree.excludes || globalExcludes, 36 | Command; 37 | 38 | Command = astNode && CommandRegistry.selectCommand(astNode); 39 | if (Command) { 40 | if (excludes[Command.code]) { 41 | Command = MutateBaseCommand; //the command code is not included - revert to default command 42 | } 43 | _.forEach(CommandExecutor.executeCommand(new Command(src, subtree, processMutation)), 44 | function (subTree) { 45 | if(subTree.node) { 46 | var localExcludes = ExclusionUtils.getExclusions(subTree.node); 47 | subTree.excludes = _.merge({}, excludes, localExcludes); 48 | } 49 | 50 | forEachMutation(subTree, processMutation); 51 | } 52 | ); 53 | } 54 | } 55 | 56 | tree.excludes = _.merge({}, globalExcludes, ExclusionUtils.getExclusions(tree.node)); // add top-level local excludes 57 | forEachMutation(tree, function (mutation) { 58 | mutations.push(_.merge(mutation, calibrateBeginAndEnd(mutation.begin, mutation.end, brackets))); 59 | }); 60 | 61 | return mutations; 62 | }; 63 | 64 | Mutator.prototype.applyMutation = function(mutation) { 65 | var src = this._src; 66 | return src.substr(0, mutation.begin) + mutation.replacement + src.substr(mutation.end); 67 | }; 68 | 69 | function calibrateBeginAndEnd(begin, end, brackets) { 70 | //return {begin: begin, end: end}; 71 | var beginBracket = _.find(brackets, function (bracket) { 72 | return bracket.range[0] === begin; 73 | }), 74 | endBracket = _.find(brackets, function (bracket) { 75 | return bracket.range[1] === end; 76 | }); 77 | 78 | return { 79 | begin: beginBracket && beginBracket.value === ')' ? begin + 1 : begin, 80 | end: endBracket && endBracket.value === '(' ? end - 1 : end 81 | }; 82 | } 83 | 84 | module.exports = Mutator; 85 | -------------------------------------------------------------------------------- /lib/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 | -------------------------------------------------------------------------------- /lib/karma/KarmaServerManager.js: -------------------------------------------------------------------------------- 1 | /** 2 | * KarmaServer class that contains all functionality for managing a Karma server. This includes starting the server, 3 | * stopping the server, and running tests on the server. 4 | * 5 | * @author Martin Koster 6 | * @author Jimi van der Woning 7 | */ 8 | 'use strict'; 9 | 10 | var _ = require('lodash'), 11 | log4js = require('log4js'), 12 | path = require('path'), 13 | Q = require('q'), 14 | fork = require('child_process').fork, 15 | TestStatus = require('../TestStatus'), 16 | KarmaServerStatus = require('./KarmaServerStatus'); 17 | 18 | var logger = log4js.getLogger('KarmaServerManager'), 19 | runner = require('karma').runner; 20 | 21 | 22 | /** 23 | * Constructor for a Karma server instance 24 | * 25 | * @param config {object} Karma configuration object that should be used 26 | * @param port {number} The port on which the Karma server should run 27 | * @param runnerTimeUnlimited {boolean} if set the KarmaServerManager will not limit the running time of the runner 28 | * @constructor 29 | */ 30 | function KarmaServerManager(config, port, runnerTimeUnlimited) { 31 | this._status = null; 32 | this._serverProcess = null; 33 | this._runnerTimeUnlimited = runnerTimeUnlimited; 34 | this._config = _.merge({ waitForServerTime: 10, waitForRunnerTime: 2 }, config, { port: port }); 35 | 36 | var notIncluded = this._config.notIncluded || []; 37 | this._config.files = _.map(this._config.files, function(filename) { 38 | return { pattern: filename, included: _.indexOf(notIncluded, filename) < 0 }; 39 | }); 40 | } 41 | 42 | /** 43 | * Set the status of the server instance to the given status. 44 | * 45 | * @param status {number} The new status the server instance should have 46 | * @private 47 | */ 48 | KarmaServerManager.prototype._setStatus = function(status) { 49 | this._status = status; 50 | logger.trace('Server status changed to: %s', _.findKey(KarmaServerStatus, function(kss) { 51 | return kss === status; 52 | })); 53 | }; 54 | 55 | /** 56 | * Start the Karma server instance, if it has not been started before 57 | * 58 | * @returns {*|promise} a promise that will resolve with the instance itself when the instance starts properly within 59 | * config.waitForServerTime seconds, and reject otherwise 60 | */ 61 | KarmaServerManager.prototype.start = function() { 62 | var deferred = Q.defer(); 63 | 64 | // Only servers that have not yet been started can be started 65 | if(this._status === null) { 66 | logger.trace('Starting a Karma server on port %d...', this._config.port); 67 | this._setStatus(KarmaServerStatus.INITIALIZING); 68 | 69 | // Start a new Karma server process 70 | this._serverProcess = startServer.call(this, deferred); 71 | } else { 72 | deferred.reject('Server has already been started'); 73 | } 74 | 75 | return deferred.promise; 76 | }; 77 | 78 | /** 79 | * Run the Karma tests on the server instance, if the instance is ready to run tests 80 | * 81 | * @returns {*|promise} a promise that will resolve with the test result when no errors occur and the run does not 82 | * exceed config.waitForRunnerTime seconds, and reject otherwise 83 | */ 84 | KarmaServerManager.prototype.runTests = function() { 85 | var self = this, 86 | deferred = Q.defer(), 87 | runnerTimeout, 88 | timeoutFunction = function() { 89 | self._setStatus(KarmaServerStatus.DEFUNCT); 90 | deferred.reject({ 91 | severity: 'fatal', 92 | message: 'Warning! Infinite loop detected. This may put a strain on your CPU.' 93 | }); 94 | }; 95 | 96 | // Only idle servers can run the tests 97 | if(self._status === KarmaServerStatus.READY) { 98 | self._setStatus(KarmaServerStatus.RUNNING); 99 | 100 | setTimeout(function() { 101 | // Limit the time a run can take to config.waitForRunnerTime seconds 102 | runnerTimeout = setTimeout( 103 | self._runnerTimeUnlimited ? function() {} : timeoutFunction, 104 | self._config.waitForRunnerTime * 1000 105 | ); 106 | 107 | runner.run( 108 | self._config, 109 | function(exitCode) { 110 | clearTimeout(runnerTimeout); 111 | self._setStatus(KarmaServerStatus.READY); 112 | deferred.resolve(exitCode === 0 ? TestStatus.SURVIVED : TestStatus.KILLED); 113 | } 114 | ); 115 | }, 100); 116 | } else { 117 | deferred.reject('Server is not ready to run tests'); 118 | } 119 | 120 | return deferred.promise; 121 | }; 122 | 123 | /** 124 | * Stop the server instance, if it has not been killed previously 125 | */ 126 | KarmaServerManager.prototype.stop = function() { 127 | if(!this.isStopped()) { 128 | this._serverProcess.send({ command: 'stop' }); 129 | this._setStatus(KarmaServerStatus.STOPPED); 130 | } 131 | }; 132 | 133 | /** 134 | * forcibly stop the server instance no matter what 135 | */ 136 | KarmaServerManager.prototype.kill = function() { 137 | this._serverProcess.send({ command: 'stop' }); 138 | this._serverProcess.kill(); 139 | this._setStatus(KarmaServerStatus.KILLED); 140 | }; 141 | 142 | /** 143 | * Determine if the server instance is active, i.e. if it is either initializing, ready or running 144 | * 145 | * @returns {boolean} indication if the server instance is active 146 | */ 147 | KarmaServerManager.prototype.isActive = function() { 148 | return [KarmaServerStatus.INITIALIZING, KarmaServerStatus.READY, KarmaServerStatus.RUNNING] 149 | .indexOf(this._status) !== -1; 150 | }; 151 | 152 | /** 153 | * Determine if the server instance is no longer running 154 | * 155 | * @returns {boolean} indication if the server is no longer running 156 | */ 157 | KarmaServerManager.prototype.isStopped = function() { 158 | return this._status === KarmaServerStatus.STOPPED || this._status === KarmaServerStatus.KILLED; 159 | }; 160 | 161 | /** 162 | * start a karma server by calling node's "fork" method. 163 | * The stdio will be piped to the current process so that it can be read and interpreted 164 | */ 165 | function startServer(serverPromise) { 166 | var self = this, 167 | startTime = Date.now(), 168 | browsersStarting, 169 | serverTimeout, 170 | serverProcess = fork(__dirname + '/KarmaWorker.js', { silent: true }); 171 | 172 | // Limit the time it can take for a server to start to config.waitForServerTime seconds 173 | serverTimeout = setTimeout(function() { 174 | self._setStatus(KarmaServerStatus.DEFUNCT); 175 | serverPromise.reject( 176 | 'Could not connect to a Karma server on port ' + self._config.port + ' within ' + 177 | self._config.waitForServerTime + ' seconds' 178 | ); 179 | }, self._config.waitForServerTime * 1000); 180 | 181 | serverProcess.send({ command: 'start', config: self._config }); 182 | 183 | serverProcess.stdout.on('data', function(data) { 184 | var message = data.toString('utf-8'), 185 | messageParts = message.split(/\s/g); 186 | 187 | logger.debug(message); 188 | 189 | //this is a hack: because Karma exposes no method of determining when the server is started up we'll dissect the log messages 190 | if(message.indexOf('Starting browser') > -1) { 191 | browsersStarting = browsersStarting ? browsersStarting.concat([messageParts[4]]) : [messageParts[4]]; 192 | } 193 | if(message.indexOf('Connected on socket') > -1) { 194 | browsersStarting.pop(); 195 | if(browsersStarting && browsersStarting.length === 0) { 196 | clearTimeout(serverTimeout); 197 | self._setStatus(KarmaServerStatus.READY); 198 | 199 | logger.info( 200 | 'Karma server started after %dms and is listening on port %d', 201 | (Date.now() - startTime), self._config.port 202 | ); 203 | 204 | serverPromise.resolve(self); 205 | } 206 | } 207 | }); 208 | 209 | return serverProcess; 210 | } 211 | 212 | module.exports = KarmaServerManager; 213 | -------------------------------------------------------------------------------- /lib/karma/KarmaServerPool.js: -------------------------------------------------------------------------------- 1 | /** 2 | * KarmaServerManager class, containing functionality for managing a set of Karma servers. This includes the ability to 3 | * start new servers, and to shut them all down. 4 | * 5 | * @author Jimi van der Woning 6 | */ 7 | 'use strict'; 8 | 9 | var _ = require('lodash'), 10 | log4js = require('log4js'), 11 | Q = require('q'), 12 | KarmaServer = require('./KarmaServerManager'); 13 | 14 | // Base port from which new server instances will connect 15 | var logger = log4js.getLogger('KarmaServerPool'), 16 | nextPort; 17 | 18 | 19 | /** 20 | * Constructor for a Karma server manager 21 | * 22 | * @param config {object} Configuration object for the server. 23 | * @constructor 24 | */ 25 | function KarmaServerPool(config) { 26 | this._config = _.merge({ port: 12111, maxActiveServers: 5, startInterval: 100 }, config); 27 | this._instances = []; 28 | } 29 | 30 | 31 | /** 32 | * Get the list of active servers 33 | * 34 | * @returns {KarmaServerManager[]} The list of active Karma servers 35 | * @private 36 | */ 37 | KarmaServerPool.prototype._getActiveServers = function() { 38 | return _.filter(this._instances, function(instance) { 39 | return instance.isActive(); 40 | }); 41 | }; 42 | 43 | /** 44 | * Get the next port on which a Karma server should be started. Loops around to the initial port when 45 | * (port + maxActiveServers) is reached. 46 | * 47 | * @returns {number} Port number on which the next Karma server should be started 48 | * @private 49 | */ 50 | KarmaServerPool.prototype._getNextPort = function() { 51 | var port = nextPort || this._config.port; 52 | nextPort = (port + 1) < (this._config.port + this._config.maxActiveServers) ? port + 1 : this._config.port; 53 | return port; 54 | }; 55 | 56 | /** 57 | * Start a new Karma server instance. Waits for other servers to shut down if more than config.maxActiveServers are 58 | * currently active. It will monitor the number of active servers on an interval of config.startInterval milliseconds. 59 | * 60 | * @param config {object} The configuration the new instance should use 61 | * @param runnerTimeUnlimited {boolean} if set the KarmaServerPool will not limit the running time of the runner 62 | * @returns {*|promise} A promise that will resolve with the new server instance once it has been started, and reject 63 | * if it could not be started properly 64 | */ 65 | KarmaServerPool.prototype.startNewInstance = function(config, runnerTimeUnlimited) { 66 | var self = this, 67 | deferred = Q.defer(), 68 | server = new KarmaServer(config, this._getNextPort(), runnerTimeUnlimited); 69 | 70 | function startServer() { 71 | if(self._getActiveServers().length < self._config.maxActiveServers) { 72 | deferred.resolve(server.start()); 73 | } else { 74 | logger.trace('There are already %d servers active. Postponing start...', self._config.maxActiveServers); 75 | setTimeout(startServer, self._config.startInterval); 76 | } 77 | } 78 | 79 | this._instances.push(server); 80 | startServer(); 81 | 82 | return deferred.promise; 83 | }; 84 | 85 | /** 86 | * Stop all Karma server instances that have not been stopped already. 87 | */ 88 | KarmaServerPool.prototype.stopAllInstances = function() { 89 | _.forEach(this._instances, function(instance) { 90 | instance.isStopped() ? _.identity() : instance.stop(); 91 | }); 92 | }; 93 | 94 | module.exports = KarmaServerPool; 95 | -------------------------------------------------------------------------------- /lib/karma/KarmaServerStatus.js: -------------------------------------------------------------------------------- 1 | /** 2 | * KarmaServerStatus object, containing the various statuses a Karma server can have. 3 | * 4 | * @author Jimi van der Woning 5 | */ 6 | 'use strict'; 7 | 8 | // All possible statuses of a Karma server instance 9 | var KarmaServerStatus = { 10 | INITIALIZING: 0, 11 | READY: 1, 12 | RUNNING: 2, 13 | STOPPED: 3, 14 | KILLED: 4, 15 | DEFUNCT: 5 16 | }; 17 | 18 | module.exports = KarmaServerStatus; 19 | -------------------------------------------------------------------------------- /lib/karma/KarmaWorker.js: -------------------------------------------------------------------------------- 1 | var Server = require('karma').Server; 2 | 3 | var server; 4 | 5 | process.on('message', function(message) { 6 | if (message.command === 'start') { 7 | server = new Server(message.config); 8 | server.start(); 9 | } else if (message.command === 'stop') { 10 | var launcher = server.get('launcher'); 11 | launcher.killAll(function() { 12 | process.kill(process.pid); 13 | }); 14 | } 15 | }); 16 | -------------------------------------------------------------------------------- /lib/reporting/ReportGenerator.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Originally created by Merlin Weemaes on 2/26/15. 3 | */ 4 | var _ = require('lodash'), 5 | log4js = require('log4js'), 6 | path = require('path'), 7 | Q = require('q'); 8 | 9 | var HtmlReporter = require('./html/HtmlReporter'), 10 | IOUtils = require('../../utils/IOUtils'), 11 | JSONReporter = require('./json/JSONReporter'); 12 | 13 | var DEFAULT_BASE_DIR = path.join('reports', 'grunt-mutation-testing'); 14 | 15 | var logger = log4js.getLogger('ReportGenerator'); 16 | 17 | exports.generate = function(config, results) { 18 | var reporters = []; 19 | 20 | _.forOwn(config, function(reporterConfig, reporterType) { 21 | var dir = reporterConfig.dir || path.join(DEFAULT_BASE_DIR, reporterType); 22 | if(reporterType === 'html') { 23 | reporters.push(new HtmlReporter(dir, reporterConfig)); 24 | } else if(reporterType === 'json') { 25 | reporters.push(new JSONReporter(dir, reporterConfig)); 26 | } 27 | }); 28 | 29 | var reportCreators = _.map(reporters, function(reporter) { 30 | return reporter.create(results); 31 | }); 32 | 33 | return Q.Promise(function(resolve, reject) { 34 | Q.allSettled(reportCreators).then(function(results) { 35 | _.forEach(results, function(result) { 36 | if(result.state === 'fulfilled') { 37 | logger.info('Generated the mutation %s report in: %s', result.value.type, result.value.path); 38 | } else { 39 | logger.error('Error creating report: %s', result.reason.message || result.reason); 40 | } 41 | }); 42 | 43 | resolve(results); 44 | }); 45 | }); 46 | }; 47 | -------------------------------------------------------------------------------- /lib/reporting/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 | log4js = require('log4js'), 8 | path = require('path'), 9 | Q = require('q'); 10 | 11 | var HtmlFormatter = require('./HtmlFormatter'), 12 | IndexHtmlBuilder = require('./IndexHtmlBuilder'), 13 | StatUtils = require('./StatUtils'), 14 | Templates = require('./Templates'); 15 | 16 | 17 | var DEFAULT_CONFIG = { 18 | successThreshold: 80 19 | }; 20 | 21 | var FileHtmlBuilder = function(config) { 22 | this._config = _.merge({}, DEFAULT_CONFIG, config); 23 | }; 24 | 25 | /** 26 | * creates an HTML report for each file within the given results 27 | * @param {Array} fileResults mutation results for each file 28 | * @param {string} baseDir the base directory in which to write the reports 29 | */ 30 | FileHtmlBuilder.prototype.createFileReports = function(fileResults, baseDir) { 31 | var self = this, 32 | promises = []; 33 | 34 | _.forEach(fileResults, function(fileResult) { 35 | promises.push(Q.Promise(function(resolve) { 36 | var results = fileResult.mutationResults, 37 | formatter = new HtmlFormatter(fileResult.src); 38 | 39 | formatter.formatSourceToHtml(results, function(formattedSource) { 40 | writeReport.call(self, fileResult, formatter, formattedSource.split('\n'), baseDir); 41 | resolve(); 42 | }); 43 | })); 44 | }, this); 45 | 46 | return Q.all(promises); 47 | }; 48 | 49 | /** 50 | * write the report to file 51 | */ 52 | function writeReport(fileResult, formatter, formattedSourceLines, baseDir) { 53 | var fileName = fileResult.fileName, 54 | stats = StatUtils.decorateStatPercentages(fileResult.stats), 55 | parentDir = path.normalize(baseDir + '/..'), 56 | mutations = formatter.formatMutations(fileResult.mutationResults), 57 | breadcrumb = new IndexHtmlBuilder(baseDir).linkPathItems({ 58 | currentDir: parentDir, 59 | fileName: baseDir + '/' + fileName + '.html', 60 | separator: ' >> ', 61 | relativePath: getRelativeDistance(baseDir + '/' + fileName, baseDir ), 62 | linkDirectoryOnly: true 63 | }); 64 | 65 | var file = Templates.fileTemplate({ 66 | sourceLines: formattedSourceLines, 67 | mutations: mutations 68 | }); 69 | 70 | fs.writeFileSync( 71 | path.join(baseDir, fileName + ".html"), 72 | Templates.baseTemplate({ 73 | style: Templates.baseStyleTemplate({ additionalStyle: Templates.fileStyleCode }), 74 | script: Templates.baseScriptTemplate({ additionalScript: Templates.fileScriptCode }), 75 | fileName: path.basename(fileName), 76 | stats: stats, 77 | status: stats.successRate > this._config.successThreshold ? 'killed' : stats.all > 0 ? 'survived' : 'neutral', 78 | breadcrumb: breadcrumb, 79 | generatedAt: new Date().toLocaleString(), 80 | content: file 81 | }) 82 | ); 83 | } 84 | 85 | function getRelativeDistance(baseDir, currentDir) { 86 | var relativePath = path.relative(baseDir, currentDir), 87 | segments = relativePath.split(path.sep); 88 | 89 | return _.filter(segments, function(segment) { 90 | return segment === '.' || segment === '..'; 91 | }).join('/'); 92 | } 93 | 94 | module.exports = FileHtmlBuilder; 95 | -------------------------------------------------------------------------------- /lib/reporting/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 | var HtmlFormatter = function(src) { 15 | this._src = src; 16 | }; 17 | 18 | 19 | HtmlFormatter.prototype._getHtmlIndexes = function() { 20 | var self = this, 21 | parser, 22 | result = [], 23 | pushTagIndexes = function() { 24 | result.push({startIndex: parser.startIndex, endIndex: parser.endIndex}); 25 | }; 26 | 27 | var promise = Q.Promise(function(resolve) { 28 | parser = new HtmlParser.Parser({ 29 | onopentag: pushTagIndexes, 30 | onclosetag: pushTagIndexes, 31 | onend: resolve 32 | }, {decodeEntities:true, recognizeCDATA:true}); 33 | parser.write(self._src); 34 | parser.end(); 35 | }); 36 | 37 | return promise.thenResolve(result); 38 | }; 39 | 40 | /** 41 | * formats the list of mutations to display on the 42 | * @returns {string} markup with all mutations to be displayed 43 | */ 44 | HtmlFormatter.prototype.formatMutations = function (mutationResults) { 45 | var formattedMutations = '', 46 | orderedResults = mutationResults.sort(function(left, right) { 47 | return left.mutation.line - right.mutation.line; 48 | }); 49 | _.forEach(orderedResults, function(mutationResult){ 50 | var mutationHtml = mutationTemplate({ 51 | mutationId: getDOMMutationId(mutationResult.mutation), 52 | mutationStatus: mutationResult.survived ? 'survived' : 'killed', 53 | mutationText: mutationResult.message 54 | }); 55 | formattedMutations = formattedMutations.concat(mutationHtml); 56 | }); 57 | return formattedMutations; 58 | }; 59 | 60 | /** 61 | * format the source code by inserting markup where each of the given mutations was applied 62 | * @param {[object]} mutationResults an array of mutation results 63 | * @param {function} callback will be called with the formatted source as argument 64 | * @returns {string} the source code formatted with markup into the places where mutations have taken place 65 | */ 66 | HtmlFormatter.prototype.formatSourceToHtml = function (mutationResults, callback){ 67 | var self = this; 68 | 69 | this._getHtmlIndexes().done(function(htmlIndexes) { 70 | var srcArray = self._src.replace(/[\r]?\n/g, '\n').split(''); //split the source into character array after removing (Windows style) carriage return characters 71 | 72 | _.forEach(htmlIndexes, function(indexPair) { 73 | var i = indexPair.startIndex; 74 | for (; i < indexPair.endIndex; i++) { 75 | srcArray[i] = srcArray[i].replace(//, '>'); 76 | } 77 | }); 78 | _.forEach(mutationResults, function(mutationResult){ 79 | formatMultilineFragment(srcArray, mutationResult); 80 | }); 81 | 82 | if (typeof callback === 'function') { 83 | callback(srcArray.join('')); 84 | } 85 | }); 86 | }; 87 | 88 | /** 89 | * formats a multi line fragment in such a way that each line gets encased in its own set of html tags, 90 | * thus preventing contents of a to be broken up with
  • tags later on 91 | * @param {string} srcArray the source split up into an array of characters 92 | * @param {object} mutationResult the current mutation result 93 | */ 94 | function formatMultilineFragment (srcArray, mutationResult) { 95 | var mutation = mutationResult.mutation, 96 | classes = 'code'.concat(mutationResult.survived ? ' survived ' : ' killed ', getDOMMutationId(mutation)), 97 | i, l = mutation.end; 98 | 99 | if (!l) { return; }//not a likely scenario, but to be sure... 100 | 101 | for (i = mutation.begin; i < l; i++) { 102 | if (i === mutation.begin) { 103 | srcArray[i] = codeTemplate({classes: classes}) + srcArray[i]; 104 | } 105 | if (srcArray[i] === '\n') { 106 | if (i > 0 ) { 107 | srcArray[i-1] = srcArray[i-1] + ''; 108 | } 109 | if (i < l-1) { 110 | srcArray[i+1] = codeTemplate({classes: classes}) + srcArray[i+1]; 111 | } 112 | } 113 | } 114 | 115 | srcArray[l-1] = srcArray[l-1] + ''; 116 | } 117 | 118 | /* creates a mutation id from the given mutation result */ 119 | function getDOMMutationId(mutation) { 120 | return 'mutation_' + mutation.mutationId + '_' + mutation.begin + '_' + mutation.end; 121 | } 122 | 123 | module.exports = HtmlFormatter; 124 | -------------------------------------------------------------------------------- /lib/reporting/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 | log4js = require('log4js'), 15 | path = require('path'), 16 | Q = require('q'), 17 | _ = require('lodash'); 18 | 19 | 20 | var logger = log4js.getLogger('HtmlReporter'); 21 | 22 | /** 23 | * Constructor for the HTML reporter. 24 | * 25 | * @param {string} basePath The base path where the report should be created 26 | * @param {object=} config Configuration object 27 | * @constructor 28 | */ 29 | var HtmlReporter = function(basePath, config) { 30 | this._basePath = basePath; 31 | this._config = config; 32 | 33 | var directories = IOUtils.getDirectoryList(basePath, false); 34 | IOUtils.createPathIfNotExists(directories, './'); 35 | }; 36 | 37 | /** 38 | * creates an HTML report using the given results 39 | * @param results 40 | * @returns {*} 41 | */ 42 | HtmlReporter.prototype.create = function(results) { 43 | var self = this; 44 | 45 | logger.trace('Creating HTML report in %s...', self._basePath); 46 | 47 | return Q.Promise(function(resolve) { 48 | _.forEach(results, function(result) { 49 | IOUtils.createPathIfNotExists(IOUtils.getDirectoryList(result.fileName, true), self._basePath); 50 | }, this); 51 | new FileHtmlBuilder(self._config).createFileReports(results, self._basePath).then(function() { 52 | self._createDirectoryIndexes(results, self._basePath); 53 | 54 | logger.trace('HTML report created in %s', self._basePath); 55 | 56 | resolve(self._generateResultObject()); 57 | }); 58 | }); 59 | }; 60 | 61 | /** 62 | * Generate a report generation result object. 63 | * 64 | * @returns {{type: string, path: string}} 65 | * @private 66 | */ 67 | HtmlReporter.prototype._generateResultObject = function() { 68 | return { 69 | type: 'html', 70 | path: this._basePath 71 | }; 72 | }; 73 | 74 | /** 75 | * recursively creates index.html files for all the (sub-)directories 76 | * @param {object} results mutation results 77 | * @param {string} baseDir the base directory from which to start generating index files 78 | * @param {string=} currentDir the current directory 79 | * @returns {Array} files listed in the index.html 80 | */ 81 | HtmlReporter.prototype._createDirectoryIndexes = function(results, baseDir, currentDir) { 82 | var self = this, 83 | dirContents, 84 | files = []; 85 | 86 | function retrieveStatsFromFile(dir, file) { 87 | var html = fs.readFileSync(dir + '/' + file, 'utf-8'), 88 | regex = /data-mutation-stats="(.+?)"/g; 89 | 90 | return JSON.parse(decodeURI(regex.exec(html)[1])); 91 | } 92 | 93 | function isHtmlReportFile(file) { 94 | return _.map(results, function(result) { 95 | return IOUtils.normalizeWindowsPath(result.fileName + '.html'); 96 | }).indexOf(IOUtils.normalizeWindowsPath(path.join(path.relative(baseDir, currentDir), file))) !== -1; 97 | } 98 | 99 | currentDir = currentDir || baseDir; 100 | dirContents = fs.readdirSync(currentDir); 101 | _.forEach(dirContents, function(item) { 102 | if(fs.statSync(path.join(currentDir, item)).isDirectory()) { 103 | files = _.union(files, self._createDirectoryIndexes(results, baseDir, path.join(currentDir, item))); 104 | } else if(item !== 'index.html' && isHtmlReportFile(item)) { 105 | files.push({ 106 | fileName: path.join(path.relative(baseDir, currentDir), item), 107 | stats: retrieveStatsFromFile(currentDir, item) 108 | }); 109 | } 110 | }); 111 | 112 | new IndexHtmlBuilder(baseDir, self._config).createIndexFile(currentDir, files); 113 | return files; 114 | }; 115 | 116 | module.exports = HtmlReporter; 117 | -------------------------------------------------------------------------------- /lib/reporting/html/IndexHtmlBuilder.js: -------------------------------------------------------------------------------- 1 | /** 2 | * HTML reporter 3 | * 4 | * @author Martin Koster 5 | * @author Jimi van der Woning 6 | */ 7 | 'use strict'; 8 | 9 | var _ = require('lodash'), 10 | fs = require('fs'), 11 | path = require('path'); 12 | 13 | var IOUtils = require('../../../utils/IOUtils'), 14 | StatUtils = require('./StatUtils'), 15 | Templates = require('./Templates'); 16 | 17 | var DEFAULT_CONFIG = { 18 | successThreshold: 80 19 | }; 20 | 21 | /** 22 | * IndexHtmlBuilder constructor. 23 | * 24 | * @param {string} baseDir 25 | * @param {object=} config 26 | * @constructor 27 | */ 28 | var IndexHtmlBuilder = function(baseDir, config) { 29 | this._baseDir = baseDir; 30 | this._config = _.merge({}, DEFAULT_CONFIG, config); 31 | this._folderPercentages = {}; 32 | }; 33 | 34 | IndexHtmlBuilder.prototype._getBreadcrumb = function(toDir) { 35 | var self = this, 36 | breadcrumb = '', 37 | relativePath = path.join(path.basename(self._baseDir), path.relative(self._baseDir, toDir)), 38 | segments = relativePath.split(path.sep), 39 | currentSegment = ''; 40 | 41 | _.forEach(segments, function(folder, i) { 42 | currentSegment = path.join(currentSegment, folder); 43 | breadcrumb = breadcrumb.concat(Templates.segmentLinkTemplate({ 44 | segment: path.relative(relativePath, currentSegment) || '.', 45 | folder: folder, 46 | separator: i < segments.length - 1 ? ' >> ' : '' 47 | })); 48 | }); 49 | 50 | return breadcrumb; 51 | }; 52 | 53 | /** 54 | * create the index file for the current directory. The index file contains a list of all mutated files in all 55 | * subdirectories, including links for drilling down into the subdirectories 56 | * @param currentDir the directory for which to create the index file 57 | * @param files mutated files within the directory subtree of currentDir 58 | */ 59 | IndexHtmlBuilder.prototype.createIndexFile = function(currentDir, files) { 60 | var allStats = StatUtils.decorateStatPercentages(_.reduce(files, this._accumulateStatsAndPercentages, {}, this)), 61 | index, 62 | rows = ''; 63 | 64 | _.forEach(files, function(file) { 65 | var relativePath = path.relative(this._baseDir, currentDir), 66 | fileStats = StatUtils.decorateStatPercentages(file.stats), 67 | pathSegments = this.linkPathItems({ 68 | currentDir: relativePath, 69 | fileName: file.fileName, 70 | separator: ' / ' 71 | }); 72 | 73 | rows = rows.concat(Templates.folderFileRowTemplate({ 74 | pathSegments: pathSegments, 75 | stats: fileStats, 76 | status: fileStats.successRate > this._config.successThreshold ? 'killed' : fileStats.successRate > 0 ? 'survived' : 'neutral' 77 | })); 78 | }, this); 79 | 80 | index = Templates.folderTemplate({ 81 | rows: rows 82 | }); 83 | 84 | fs.writeFileSync( 85 | currentDir + "/index.html", 86 | Templates.baseTemplate({ 87 | style: Templates.baseStyleTemplate({ additionalStyle: Templates.folderStyleCode }), 88 | fileName: path.basename(path.relative(this._baseDir, currentDir)), 89 | stats: allStats, 90 | status: allStats.successRate > this._config.successThreshold ? 'killed' : 'survived', 91 | breadcrumb: this._getBreadcrumb(currentDir), 92 | generatedAt: new Date().toLocaleString(), 93 | content: index 94 | }) 95 | ); 96 | }; 97 | 98 | /** 99 | * Adds Hyperlinks to each path segment - linking to the relevant directory's index.html 100 | * @param {{currentDir, fileName, separator, [filePerc], [relativePath]}} options object containing the parameters to link the path items 101 | * @returns {string} 102 | */ 103 | IndexHtmlBuilder.prototype.linkPathItems = function(options) { 104 | var folderPercentages = this._folderPercentages || {}, 105 | successThreshold = this._config.successThreshold, 106 | fileName = path.basename(options.fileName, '.html'), 107 | relativeLocation = IOUtils.normalizeWindowsPath(path.relative(options.currentDir, options.fileName)), 108 | rawPath = options.relativePath || '.', 109 | directoryList = IOUtils.getDirectoryList(relativeLocation.replace(rawPath, ''), true), 110 | linkedPath = '', 111 | fileStatus = options.filePerc ? options.filePerc > successThreshold ? 'killed' : 'survived' : '', 112 | folderStatus; 113 | 114 | _.forEach(directoryList, function(folder) { 115 | var perc = options.filePerc && folderPercentages ? folderPercentages[folder] : null; 116 | rawPath = rawPath.concat('/', folder); 117 | folderStatus = perc ? perc.total / perc.weight > successThreshold ? 'killed' : 'survived' : ''; 118 | linkedPath = linkedPath.concat(Templates.segmentLinkTemplate({ 119 | segment: rawPath, 120 | folder: folder, 121 | status: folderStatus, 122 | separator: options.separator 123 | })); 124 | }); 125 | 126 | if (options.linkDirectoryOnly) { 127 | return linkedPath.concat(fileName); 128 | }else { 129 | return linkedPath.concat(Templates.fileLinkTemplate({ 130 | path: relativeLocation, 131 | separator: options.separator, 132 | file: fileName, 133 | status: fileStatus 134 | })); 135 | } 136 | }; 137 | 138 | /** 139 | * accumulates the statistics of given file in each of its path segments and returns the mutation statistics for that file 140 | * @param result result containing stats 141 | * @param file file for which to collect stats 142 | * @returns {{all, ignored, untested, survived}} stats for the given file 143 | */ 144 | IndexHtmlBuilder.prototype._accumulateStatsAndPercentages = function(result, file) { 145 | var folders = IOUtils.getDirectoryList(file.fileName, true); 146 | _.forEach(folders, function(folder) { 147 | this._folderPercentages[folder] = this._folderPercentages[folder] || {}; 148 | this._folderPercentages[folder].total = (this._folderPercentages[folder].total || 0) + 149 | (file.stats.all - file.stats.survived) / file.stats.all * 100; 150 | this._folderPercentages[folder].weight = (this._folderPercentages[folder].weight || 0) + 1; 151 | }, this); 152 | 153 | result.all = (result.all || 0) + file.stats.all; 154 | result.killed = (result.killed || 0) + file.stats.killed; 155 | result.survived = (result.survived || 0) + file.stats.survived; 156 | result.ignored = (result.ignored || 0) + file.stats.ignored; 157 | result.untested = (result.untested || 0) + file.stats.untested; 158 | return result; 159 | }; 160 | 161 | module.exports = IndexHtmlBuilder; 162 | -------------------------------------------------------------------------------- /lib/reporting/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 | -------------------------------------------------------------------------------- /lib/reporting/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 | -------------------------------------------------------------------------------- /lib/reporting/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 | -------------------------------------------------------------------------------- /lib/reporting/html/templates/baseScript.hbs: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /lib/reporting/html/templates/baseStyle.hbs: -------------------------------------------------------------------------------- 1 | 91 | -------------------------------------------------------------------------------- /lib/reporting/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 |

    27 | No mutations could be performed on this file. This may be due to one of the following reasons: 28 |

    29 |
      30 |
    • No unit tests exist for this file
    • 31 |
    • No specs were configured for this file
    • 32 |
    • Unit tests are failing already without applying any mutations
    • 33 |
    • All possible mutations in this file were excluded
    • 34 |
    35 | {{/if}} 36 |
    37 | 38 | 39 | -------------------------------------------------------------------------------- /lib/reporting/html/templates/fileScript.js: -------------------------------------------------------------------------------- 1 | function onLoad() { 2 | each(document.getElementsByClassName('code'), function(element) { 3 | element.addEventListener('mouseover', onMouseOver, false); 4 | element.addEventListener('mousemove', onMouseMove, false); 5 | element.addEventListener('mouseout', onMouseOut, false); 6 | }); 7 | } 8 | 9 | function each(list, fn, thisArg) { 10 | Array.prototype.forEach.call(list, fn, thisArg); 11 | } 12 | 13 | function onMouseOver(event) { 14 | var popup = document.getElementById('popup'); 15 | addMutations(popup, event.target); 16 | setPopupPosition(popup, event); 17 | popup.classList.add("show"); 18 | } 19 | 20 | function onMouseMove(event) { 21 | setPopupPosition(document.getElementById('popup'), event); 22 | } 23 | 24 | function onMouseOut() { 25 | document.getElementById('popup').classList.remove('show'); 26 | } 27 | 28 | function addMutations(popup, target) { 29 | var mutationEl; 30 | 31 | each(target.classList, function(_class) { 32 | if (_class.indexOf('mutation_') > -1) { 33 | mutationEl = document.getElementById(_class); 34 | if (Array.prototype.indexOf.call(mutationEl.classList, 'killed') > -1) { 35 | popup.classList.add('killed'); 36 | popup.classList.remove('survived'); 37 | } else { 38 | popup.classList.add('survived'); 39 | popup.classList.remove('killed'); 40 | } 41 | popup.classList.add(mutationEl.classList); 42 | popup.innerHTML = mutationEl.innerHTML; 43 | } 44 | }); 45 | } 46 | 47 | var showKilledMutations = true; 48 | function toggleKilledMutations() { 49 | showKilledMutations = !showKilledMutations; 50 | 51 | if(showKilledMutations) { 52 | document.querySelector('#toggleKilledMutations').innerHTML = 'Hide killed mutations'; 53 | each(document.querySelectorAll('.code.killed'), function(e) { 54 | e.style.backgroundColor = ''; 55 | }); 56 | each(document.querySelectorAll('#mutations .killed'), function(e) { 57 | e.style.display = ''; 58 | }); 59 | } else { 60 | document.querySelector('#toggleKilledMutations').innerHTML = 'Show killed mutations'; 61 | each(document.querySelectorAll('.code.killed'), function(e) { 62 | e.style.backgroundColor = 'white'; 63 | }); 64 | each(document.querySelectorAll('#mutations .killed'), function(e) { 65 | e.style.display = 'none'; 66 | }); 67 | } 68 | } 69 | 70 | function setPopupPosition(popup, event) { 71 | var top = event.clientY, 72 | left = event.clientX + 10; 73 | 74 | popup.setAttribute('style', 'top:' + top + 'px;left:' + left + 'px;'); 75 | } 76 | 77 | window.onload = onLoad; 78 | -------------------------------------------------------------------------------- /lib/reporting/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 | -------------------------------------------------------------------------------- /lib/reporting/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 | -------------------------------------------------------------------------------- /lib/reporting/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 | -------------------------------------------------------------------------------- /lib/reporting/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 | -------------------------------------------------------------------------------- /lib/reporting/json/JSONReporter.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var _ = require('lodash'), 4 | log4js = require('log4js'), 5 | path = require('path'), 6 | Q = require('q'); 7 | 8 | var IOUtils = require('../../../utils/IOUtils'); 9 | 10 | var DEFAULT_FILE_NAME = 'mutations.json'; 11 | 12 | var logger = log4js.getLogger('JSONReporter'); 13 | 14 | 15 | /** 16 | * JSON report generator. 17 | * 18 | * @param {string} dir - Report directory 19 | * @param {object=} config - Reporter configuration 20 | * @constructor 21 | */ 22 | function JSONReporter(dir, config) { 23 | this._config = config; 24 | 25 | var fileName = config.file || DEFAULT_FILE_NAME; 26 | this._filePath = path.join(dir, fileName); 27 | } 28 | 29 | /** 30 | * Create the JSON report from the given results. 31 | * 32 | * @param {object} results - Mutation testing results. 33 | * @returns {*} - A promise that will resolve when the report has been created successfully and rejected otherwise. 34 | */ 35 | JSONReporter.prototype.create = function(results) { 36 | var self = this; 37 | 38 | logger.trace('Creating JSON report in %s...', self._filePath); 39 | 40 | IOUtils.createPathIfNotExists(IOUtils.getDirectoryList(self._filePath, true), '.'); 41 | 42 | return Q.Promise(function(resolve, reject) { 43 | var jsonReport = JSON.stringify(getResultsPerDir(results), null, 4); 44 | IOUtils.promiseToWriteFile(self._filePath, jsonReport) 45 | .then(function() { 46 | logger.trace('JSON report created in %s', self._filePath); 47 | resolve(self._generateResultObject()); 48 | }).catch(function(error) { 49 | logger.trace('Could not generate JSON report: %s', error); 50 | reject(error); 51 | }); 52 | }); 53 | }; 54 | 55 | /** 56 | * Generate a report generation result object. 57 | * 58 | * @returns {{type: string, path: string}} 59 | * @private 60 | */ 61 | JSONReporter.prototype._generateResultObject = function() { 62 | return { 63 | type: 'json', 64 | path: this._filePath 65 | }; 66 | }; 67 | 68 | /** 69 | * Calculate the mutation score from a stats object. 70 | * 71 | * @param {object} stats - The stats from which to calculate the score 72 | * @returns {{total: number, killed: number, survived: number, ignored: number, untested: number}} 73 | */ 74 | function getMutationScore(stats) { 75 | return { 76 | total: (stats.all - stats.survived) / stats.all, 77 | killed: stats.killed / stats.all, 78 | survived: stats.survived / stats.all, 79 | ignored: stats.ignored / stats.all, 80 | untested: stats.untested / stats.all 81 | }; 82 | } 83 | 84 | /** 85 | * Calculate the mutation test results per directory. 86 | * 87 | * @param {object} results - Mutation testing results 88 | * @returns {object} - The mutation test results per directory 89 | */ 90 | function getResultsPerDir(results) { 91 | 92 | /** 93 | * Decorate the results object with the stats for each directory. 94 | * 95 | * @param {object} results - (part of) mutation testing results 96 | * @returns {object} - Mutation test results decorated with stats 97 | */ 98 | function addDirStats(results) { 99 | var dirStats = { 100 | all: 0, 101 | killed: 0, 102 | survived: 0, 103 | ignored: 0, 104 | untested: 0 105 | }; 106 | 107 | _.forOwn(results, function(result) { 108 | var stats = result.stats || addDirStats(result).stats; 109 | dirStats.all += stats.all; 110 | dirStats.killed += stats.killed; 111 | dirStats.survived += stats.survived; 112 | dirStats.ignored += stats.ignored; 113 | dirStats.untested += stats.untested; 114 | }); 115 | 116 | results.stats = dirStats; 117 | return results; 118 | } 119 | 120 | /** 121 | * Decorate the results object with the mutation score for each directory. 122 | * 123 | * @param {object} results - (part of) mutation testing results, decorated with mutation stats 124 | * @returns {object} - Mutation test results decorated with mutation scores 125 | */ 126 | function addMutationScores(results) { 127 | _.forOwn(results, function(result) { 128 | if(_.has(result, 'stats')) { 129 | addMutationScores(result); 130 | } 131 | }); 132 | 133 | results.mutationScore = getMutationScore(results.stats); 134 | return results; 135 | } 136 | 137 | var resultsPerDir = {}; 138 | 139 | _.forEach(_.clone(results), function(fileResult) { 140 | _.set(resultsPerDir, IOUtils.getDirectoryList(fileResult.fileName), fileResult); 141 | }); 142 | 143 | return addMutationScores(addDirStats(resultsPerDir)); 144 | } 145 | 146 | module.exports = JSONReporter; 147 | -------------------------------------------------------------------------------- /license: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Jimi van der Woning, Martin Koster 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 | -------------------------------------------------------------------------------- /mutationCommands/AssignmentExpressionCommand.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This command handles Assignment expressions. 3 | * While not creating ant mutations, it does check whether the RHS of the assignment is eligible for mutations. 4 | * If not the buck stops here, i.e. no child nodes are returned. The main reason for doing this is to prevent 5 | * mutated assignments to loop variables from causing endless loops 6 | * Created by Martin Koster on 2/26/15. 7 | */ 8 | var _ = require('lodash'), 9 | MutateBaseCommand = require('./MutateBaseCommand'); 10 | function AssignmentExpressionCommand (src, subTree, callback) { 11 | MutateBaseCommand.call(this, src, subTree, callback) 12 | }; 13 | 14 | AssignmentExpressionCommand.prototype.execute = function() { 15 | var childNodes = []; 16 | if (canMutate(this._astNode, this._loopVariables)) { 17 | _.forOwn(this._astNode, function (child) { 18 | childNodes.push({node: child, parentMutationId: this._parentMutationId, loopVariables: this._loopVariables}); 19 | }, this); 20 | } 21 | return childNodes; 22 | }; 23 | 24 | function canMutate(astNode, loopVariables) { 25 | var left = astNode.left; 26 | if (left && left.type === 'Identifier') { 27 | return (loopVariables.indexOf(left.name) < 0); 28 | } 29 | return true; 30 | } 31 | module.exports = AssignmentExpressionCommand; 32 | -------------------------------------------------------------------------------- /mutationCommands/CommandExecutor.js: -------------------------------------------------------------------------------- 1 | /** 2 | * The command executor will execute commands passed to it and do any possible housekeeping required 3 | * 4 | * Created by Martin Koster on 2/11/15. 5 | */ 6 | (function (exports) { 7 | 8 | /** 9 | * executes a given command 10 | * @param {object} mutationCommand an instance of a mutation command 11 | * @returns {array} sub-nodes to be processed 12 | */ 13 | function executeCommand(mutationCommand) { 14 | return mutationCommand.execute(); 15 | } 16 | 17 | exports.executeCommand = executeCommand; 18 | })(module.exports); 19 | -------------------------------------------------------------------------------- /mutationCommands/CommandRegistry.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This registry contains all the possible mutation commands and the predicates for which they a command will be selected. 3 | * It will select and return a command based on the given syntax tree node. 4 | * 5 | * To add new commands to the application simply create a new Mutation command based on MutationBaseCommand and add it to the registry together with an appropriate predicate. 6 | * 7 | * Created by Martin Koster on 2/20/15. 8 | */ 9 | var _ = require('lodash'), 10 | MutateComparisonOperatorCommand = require('../mutationCommands/MutateComparisonOperatorCommand'), 11 | MutateArithmeticOperatorCommand = require('../mutationCommands/MutateArithmeticOperatorCommand'), 12 | MutateArrayExpressionCommand = require('../mutationCommands/MutateArrayExpressionCommand'), 13 | MutateBlockStatementCommand = require('../mutationCommands/MutateBlockStatementCommand'), 14 | MutateObjectCommand = require('../mutationCommands/MutateObjectCommand'), 15 | MutateLiteralCommand = require('../mutationCommands/MutateLiteralCommand'), 16 | MutateUnaryExpressionCommand = require('../mutationCommands/MutateUnaryExpressionCommand'), 17 | MutateLogicalExpressionCommand = require('../mutationCommands/MutateLogicalExpressionCommand'), 18 | MutateBaseCommand = require('../mutationCommands/MutateBaseCommand'), 19 | MutateCallExpressionCommand = require('../mutationCommands/MutateCallExpressionCommand'), 20 | MutateUpdateExpressionCommand = require('../mutationCommands/MutateUpdateExpressionCommand'), 21 | MutateIterationCommand = require('../mutationCommands/MutateIterationCommand'), 22 | MutateForLoopCommand = require('../mutationCommands/MutateForLoopCommand'), 23 | AssignmentExpressionCommand = require('../mutationCommands/AssignmentExpressionCommand'); 24 | 25 | /* 26 | * Add a new command to this registry together with its predicate. It will automatically be included in the system 27 | */ 28 | var registry = [ 29 | {predicate: function(node) {return node && node.body && _.isArray(node.body);}, Command: MutateBlockStatementCommand}, 30 | {predicate: function(node) {return node && (node.type === 'WhileStatement' || node.type === 'DoWhileStatement');}, Command: MutateIterationCommand}, 31 | {predicate: function(node) {return node && node.type === 'ForStatement';}, Command: MutateForLoopCommand}, 32 | {predicate: function(node) {return node && node.type === 'AssignmentExpression';}, Command: AssignmentExpressionCommand}, 33 | {predicate: function(node) {return node && node.type === 'CallExpression';}, Command: MutateCallExpressionCommand}, 34 | {predicate: function(node) {return node && node.type === 'ObjectExpression';}, Command: MutateObjectCommand}, 35 | {predicate: function(node) {return node && node.type === 'ArrayExpression';}, Command: MutateArrayExpressionCommand}, 36 | {predicate: function(node) {return node && node.type === 'BinaryExpression' && isArithmeticExpression(node);}, Command: MutateArithmeticOperatorCommand}, 37 | {predicate: function(node) {return node && node.type === 'BinaryExpression' && !isArithmeticExpression(node);}, Command: MutateComparisonOperatorCommand}, 38 | {predicate: function(node) {return node && node.type === 'UpdateExpression';}, Command: MutateUpdateExpressionCommand}, 39 | {predicate: function(node) {return node && node.type === 'Literal';}, Command: MutateLiteralCommand}, 40 | {predicate: function(node) {return node && node.type === 'UnaryExpression';}, Command: MutateUnaryExpressionCommand}, 41 | {predicate: function(node) {return node && node.type === 'LogicalExpression';}, Command: MutateLogicalExpressionCommand}, 42 | {predicate: function(node) {return _.isObject(node);}, Command: MutateBaseCommand} 43 | ]; 44 | 45 | function isArithmeticExpression(node) { 46 | return _.indexOf(['+', '-', '*', '/', '%'], node.operator) > -1; 47 | } 48 | (function CommandRegistry(exports) { 49 | 50 | /** 51 | * Selectes a command based on the given Abstract Syntax Tree node. 52 | * @param {object} node the node for which to return a mutation command 53 | * @returns {object} The command to be executed for this node 54 | */ 55 | function selectCommand(node) { 56 | var commandRegistryItem = _.find(registry, function(registryItem) { 57 | return !!registryItem.predicate(node); 58 | }); 59 | return commandRegistryItem ? commandRegistryItem.Command : null; 60 | } 61 | 62 | /** 63 | * returns the command codes of all available mutation commands 64 | * @returns {[string]} a list of mutation codes 65 | */ 66 | function getAllCommandCodes() { 67 | return _.keys(getDefaultExcludes()); 68 | } 69 | 70 | /** 71 | * returns the default exclusion status of each mutation command 72 | * @returns {object} a list of mutation codes [key] and whether or not they're excluded [value] 73 | */ 74 | function getDefaultExcludes() { 75 | var excludes = {}; 76 | _.forEach(_.pluck(registry, 'Command'), function(Command){ 77 | if(Command.code) { 78 | excludes[Command.code] = !!Command.exclude; 79 | } 80 | }); 81 | return excludes; 82 | } 83 | 84 | exports.selectCommand = selectCommand; 85 | exports.getAllCommandCodes = getAllCommandCodes; 86 | exports.getDefaultExcludes = getDefaultExcludes; 87 | })(module.exports); 88 | -------------------------------------------------------------------------------- /mutationCommands/MutateArithmeticOperatorCommand.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 | var MutateBaseCommand = require('../mutationCommands/MutateBaseCommand'), 7 | _ = require('lodash'); 8 | var Utils = require('../utils/MutationUtils'); 9 | var operators = { 10 | '+': '-', 11 | '-': '+', 12 | '*': '/', 13 | '/': '*', 14 | '%': '*' 15 | }; 16 | 17 | function MutateArithmeticOperatorCommand(src, subTree, callback) { 18 | MutateBaseCommand.call(this, src, subTree, callback); 19 | } 20 | 21 | MutateArithmeticOperatorCommand.prototype.execute = function () { 22 | if (operators.hasOwnProperty(this._astNode.operator)) { 23 | this._callback(Utils.createOperatorMutation(this._astNode, this._parentMutationId, operators[this._astNode.operator])); 24 | } 25 | return [ 26 | {node: this._astNode.left, parentMutationId: this._parentMutationId, loopVariables: this._loopVariables}, 27 | {node: this._astNode.right, parentMutationId: this._parentMutationId, loopVariables: this._loopVariables}]; 28 | }; 29 | 30 | module.exports = MutateArithmeticOperatorCommand; 31 | module.exports.code = 'MATH'; 32 | -------------------------------------------------------------------------------- /mutationCommands/MutateArrayExpressionCommand.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This command creates mutations on a given array 3 | * Created by Martin Koster on 2/12/15. 4 | */ 5 | var _ = require('lodash'); 6 | var Utils = require('../utils/MutationUtils'); 7 | var MutateBaseCommand = require('../mutationCommands/MutateBaseCommand'); 8 | function MutateArrayCommand (src, subTree, callback) { 9 | MutateBaseCommand.call(this, src, subTree, callback); 10 | } 11 | 12 | MutateArrayCommand.prototype.execute = function () { 13 | var elements = this._astNode.elements, 14 | subNodes = []; 15 | 16 | _.each(elements, function (element, i) { 17 | var mutation = Utils.createAstArrayElementDeletionMutation(elements, element, i, this._parentMutationId); 18 | this._callback(mutation); 19 | subNodes.push({node: element, parentMutationId: mutation.mutationId, loopVariables: this._loopVariables}); 20 | }, this); 21 | return subNodes; 22 | }; 23 | 24 | module.exports = MutateArrayCommand; 25 | module.exports.code = 'ARRAY'; 26 | -------------------------------------------------------------------------------- /mutationCommands/MutateBaseCommand.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 | var _ = require('lodash'), 6 | ScopeUtils = require('../utils/ScopeUtils'); 7 | function MutateBaseCommand(src, subTree, callback) { 8 | var astNode = subTree.node, 9 | body = astNode.body; 10 | 11 | /* while selecting a command requires the node, the actual processing may 12 | * in some cases require the body of the node, which is itself a node */ 13 | this._astNode = body && _.isArray(body) ? body : astNode; 14 | this._src = src; 15 | this._callback = callback; 16 | this._parentMutationId = subTree.parentMutationId; 17 | 18 | if (body && ScopeUtils.hasScopeChanged(astNode)) { 19 | this._loopVariables = ScopeUtils.removeOverriddenLoopVariables(body, subTree.loopVariables || []); 20 | if (!_.isEmpty(this._loopVariables )) { 21 | } 22 | } else { 23 | this._loopVariables = subTree.loopVariables || []; 24 | } 25 | } 26 | 27 | MutateBaseCommand.prototype.execute = function () { 28 | var childNodes = []; 29 | _.forOwn(this._astNode, function (child) { 30 | childNodes.push({node: child, parentMutationId: this._parentMutationId, loopVariables: this._loopVariables}); //no own mutations so use parent mutation id 31 | }, this); 32 | return childNodes; 33 | }; 34 | 35 | module.exports = MutateBaseCommand; 36 | -------------------------------------------------------------------------------- /mutationCommands/MutateBlockStatementCommand.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This command creates mutations on a given array 3 | * Created by Martin Koster on 2/12/15. 4 | */ 5 | var _ = require('lodash'); 6 | var Utils = require('../utils/MutationUtils'); 7 | var MutateBaseCommand = require('../mutationCommands/MutateBaseCommand'); 8 | function MutateBlockStatementCommand (src, subTree, callback) { 9 | MutateBaseCommand.call(this, src, subTree, callback); 10 | } 11 | 12 | MutateBlockStatementCommand.prototype.execute = function () { 13 | var mutation, 14 | subTree = []; 15 | 16 | _.forEach(this._astNode, function (childNode) { 17 | mutation = Utils.createMutation(childNode, childNode.range[1], this._parentMutationId); 18 | this._callback(mutation); 19 | subTree.push({node: childNode, parentMutationId: mutation.mutationId, loopVariables: this._loopVariables}); 20 | }, this); 21 | return subTree; 22 | }; 23 | 24 | module.exports = MutateBlockStatementCommand; 25 | module.exports.code = 'BLOCK_STATEMENT'; 26 | -------------------------------------------------------------------------------- /mutationCommands/MutateCallExpressionCommand.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 | * Created by Martin Koster on 2/16/15. 5 | */ 6 | var MutateBaseCommand = require('../mutationCommands/MutateBaseCommand'); 7 | var Utils = require('../utils/MutationUtils'); 8 | var LiteralUtils = require('../utils/LiteralUtils'); 9 | function MutateCallExpressionCommand (src, subTree, callback) { 10 | MutateBaseCommand.call(this, src, subTree, callback); 11 | } 12 | 13 | MutateCallExpressionCommand.prototype.execute = function () { 14 | var astNode = this._astNode, 15 | parentMutationId = this._parentMutationId, 16 | callback = this._callback, 17 | args = astNode.arguments, 18 | astChildNodes = [], 19 | replacement; 20 | 21 | args.forEach(function (arg) { 22 | var replacement = LiteralUtils.determineReplacement(arg.value); 23 | var mutation; 24 | if (arg.type === 'Literal' && !!replacement) { 25 | // we have found a literal mutation for this argument, so we don't need to mutate more 26 | mutation = Utils.createMutation(arg, arg.range[1], parentMutationId, replacement); 27 | callback(mutation); 28 | return; 29 | } 30 | callback(mutation || Utils.createMutation(arg, arg.range[1], parentMutationId, '"MUTATION!"')); 31 | astChildNodes.push({node: arg, parentMutationId: parentMutationId, loopVariables: this._loopVariables}); 32 | }); 33 | 34 | if (args.length === 1) { 35 | replacement = this._src.substring(args[0].range[0], args[0].range[1]); 36 | callback(Utils.createMutation(astNode, astNode.range[1], parentMutationId, replacement)); 37 | } 38 | 39 | if (astNode.callee.type === 'MemberExpression') { 40 | replacement = this._src.substring(astNode.callee.object.range[0], astNode.callee.object.range[1]); 41 | callback(Utils.createMutation(astNode, astNode.range[1], parentMutationId, replacement)); 42 | } 43 | 44 | astChildNodes.push({node: astNode.callee, parentMutationId: parentMutationId}); 45 | return astChildNodes; 46 | }; 47 | 48 | module.exports = MutateCallExpressionCommand; 49 | module.exports.code = 'METHOD_CALL'; 50 | -------------------------------------------------------------------------------- /mutationCommands/MutateComparisonOperatorCommand.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 | var MutateBaseCommand = require('../mutationCommands/MutateBaseCommand'); 8 | var Utils = require('../utils/MutationUtils'); 9 | var operators = { 10 | '<': {boundary: '<=', negation: '>='}, 11 | '<=': {boundary: '<', negation: '>'}, 12 | '>': {boundary: '>=', negation: '<='}, 13 | '>=': {boundary: '>', negation: '<'}, 14 | /* TODO: reimplement these once we can execute several mutation commands on the same type of AST node */ 15 | '===': {/*boundary: '==', */negation: '!=='}, 16 | '==': {/*boundary: '===', */negation: '!='}, 17 | '!==': {/*boundary: '!=', */negation: '==='}, 18 | '!=': {/*boundary: '!==', */negation: '=='} 19 | }; 20 | 21 | function MutateComparisonOperatorCommand (src, subTree, callback) { 22 | MutateBaseCommand.call(this, src, subTree, callback); 23 | } 24 | 25 | MutateComparisonOperatorCommand.prototype.execute = function () { 26 | if (operators.hasOwnProperty(this._astNode.operator)) { 27 | var boundaryOperator = operators[this._astNode.operator].boundary; 28 | var negationOperator = operators[this._astNode.operator].negation; 29 | 30 | if (!!boundaryOperator) { 31 | this._callback(Utils.createOperatorMutation(this._astNode, this._parentMutationId, boundaryOperator)); 32 | } 33 | 34 | if (!!negationOperator) { 35 | this._callback(Utils.createOperatorMutation(this._astNode, this._parentMutationId, negationOperator)); 36 | } 37 | } 38 | return [ 39 | {node: this._astNode.left, parentMutationId: this._parentMutationId, loopVariables: this._loopVariables}, //mutations on left and right nodes aren't sub-mutations of this so use parent mutation id 40 | {node: this._astNode.right, parentMutationId: this._parentMutationId, loopVariables: this._loopVariables} 41 | ]; 42 | }; 43 | 44 | module.exports = MutateComparisonOperatorCommand; 45 | module.exports.code = 'COMPARISON'; 46 | -------------------------------------------------------------------------------- /mutationCommands/MutateForLoopCommand.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by Martin Koster on 24/02/15. 3 | */ 4 | var MutateIterationCommand = require('../mutationCommands/MutateIterationCommand'); 5 | 6 | 7 | function MutateForLoopCommand (src, subTree, callback) { 8 | MutateIterationCommand.call(this, src, subTree, callback); 9 | } 10 | 11 | MutateForLoopCommand.prototype.execute = function () { 12 | return ([ 13 | // 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 14 | {node: this._astNode.init, parentMutationId: this._parentMutationId, loopVariables: this._loopVariables}, 15 | {node: this._astNode.body, parentMutationId: this._parentMutationId, loopVariables: this._loopVariables} 16 | ]); 17 | }; 18 | 19 | module.exports = MutateForLoopCommand; 20 | -------------------------------------------------------------------------------- /mutationCommands/MutateIterationCommand.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by Martin Koster on 24/02/15. 3 | */ 4 | var esprima = require('esprima'), 5 | escodegen = require('escodegen'), 6 | _ = require('lodash'), 7 | MutateBaseCommand = require('../mutationCommands/MutateBaseCommand'); 8 | 9 | 10 | function MutateIterationCommand (src, subTree, callback) { 11 | MutateBaseCommand.call(this, src, subTree, callback); 12 | this._loopVariables = _.merge(this._loopVariables, findLoopVariables(this._astNode.test)); 13 | } 14 | 15 | MutateIterationCommand.prototype.execute = 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 | {node: this._astNode.body, parentMutationId: this._parentMutationId, loopVariables: this._loopVariables} 19 | ]); 20 | }; 21 | 22 | function findLoopVariables(testNode) { 23 | var tokens = esprima.tokenize(escodegen.generate(testNode)); 24 | 25 | return _.pluck(_.filter(tokens, {'type': 'Identifier'}), 'value'); 26 | } 27 | 28 | module.exports = MutateIterationCommand; 29 | -------------------------------------------------------------------------------- /mutationCommands/MutateLiteralCommand.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 | var _ = require('lodash'); 6 | var Utils = require('../utils/MutationUtils'); 7 | var LiteralUtils = require('../utils/LiteralUtils'); 8 | var MutateBaseCommand = require('../mutationCommands/MutateBaseCommand'); 9 | function MutateLiteralCommand (src, subTree, callback) { 10 | MutateBaseCommand.call(this, src, subTree, callback); 11 | } 12 | 13 | MutateLiteralCommand.prototype.execute = function () { 14 | var literalValue = this._astNode.value, 15 | replacement = LiteralUtils.determineReplacement(literalValue); 16 | 17 | if (replacement) { 18 | this._callback(Utils.createMutation(this._astNode, this._astNode.range[1], this._parentMutationId, replacement)); 19 | } 20 | 21 | return []; 22 | }; 23 | 24 | module.exports = MutateLiteralCommand; 25 | module.exports.code = 'LITERAL'; 26 | -------------------------------------------------------------------------------- /mutationCommands/MutateLogicalExpressionCommand.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This command creates mutations on logical operators. 3 | * 4 | * Created by Merlin Weemaes on 2/24/15. 5 | */ 6 | var MutateBaseCommand = require('../mutationCommands/MutateBaseCommand'), 7 | _ = require('lodash'); 8 | var Utils = require('../utils/MutationUtils'); 9 | var operators = { 10 | '&&': '||', 11 | '||': '&&' 12 | }; 13 | 14 | function MutateLogicalExpressionCommand(src, subTree, callback) { 15 | MutateBaseCommand.call(this, src, subTree, callback); 16 | } 17 | 18 | MutateLogicalExpressionCommand.prototype.execute = function () { 19 | 20 | if (operators.hasOwnProperty(this._astNode.operator)) { 21 | this._callback(Utils.createOperatorMutation(this._astNode, this._parentMutationId, operators[this._astNode.operator])); 22 | } 23 | 24 | return [ 25 | {node: this._astNode.left, parentMutationId: this._parentMutationId, loopVariables: this._loopVariables}, 26 | {node: this._astNode.right, parentMutationId: this._parentMutationId, loopVariables: this._loopVariables}]; 27 | }; 28 | 29 | module.exports = MutateLogicalExpressionCommand; 30 | module.exports.code = 'LOGICAL_EXPRESSION'; 31 | -------------------------------------------------------------------------------- /mutationCommands/MutateObjectCommand.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This command creates mutations on a given object 3 | * Created by Martin Koster on 2/12/15. 4 | */ 5 | var _ = require('lodash'); 6 | var MutateBaseCommand = require('../mutationCommands/MutateBaseCommand'); 7 | var Utils = require('../utils/MutationUtils'); 8 | function MutateObjectCommand (src, subTree, callback) { 9 | MutateBaseCommand.call(this, src, subTree, callback); 10 | } 11 | 12 | MutateObjectCommand.prototype.execute = function () { 13 | var properties = this._astNode.properties, 14 | subNodes = []; 15 | 16 | _.forEach(properties, function (property, i) { 17 | var mutation; 18 | if (property.kind === 'init') { 19 | mutation = Utils.createAstArrayElementDeletionMutation(properties, property, i, this._parentMutationId); 20 | this._callback(mutation); 21 | } 22 | subNodes.push({node: property.value, parentMutationId: mutation.mutationId, loopVariables: this._loopVariables}); 23 | }, this); 24 | return subNodes; 25 | }; 26 | 27 | module.exports = MutateObjectCommand; 28 | module.exports.code = 'OBJECT'; 29 | -------------------------------------------------------------------------------- /mutationCommands/MutateUnaryExpressionCommand.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 | var _ = require('lodash'); 9 | var Utils = require('../utils/MutationUtils'); 10 | var MutateBaseCommand = require('../mutationCommands/MutateBaseCommand'); 11 | function UnaryExpressionCommand(src, subTree, callback) { 12 | MutateBaseCommand.call(this, src, subTree, callback); 13 | } 14 | 15 | UnaryExpressionCommand.prototype.execute = function () { 16 | 17 | if (this._astNode.operator) { 18 | this._callback(Utils.createUnaryOperatorMutation(this._astNode, this._parentMutationId, "")); 19 | } 20 | 21 | return []; 22 | }; 23 | 24 | module.exports = UnaryExpressionCommand; 25 | module.exports.code = 'UNARY_EXPRESSION'; 26 | -------------------------------------------------------------------------------- /mutationCommands/MutateUpdateExpressionCommand.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by Martin Koster on 2/16/15. 3 | */ 4 | var MutateBaseCommand = require('../mutationCommands/MutateBaseCommand'); 5 | var Utils = require('../utils/MutationUtils'); 6 | function MutateUpdateExpressionCommand (src, subTree, callback) { 7 | MutateBaseCommand.call(this, src, subTree, callback); 8 | } 9 | 10 | MutateUpdateExpressionCommand.prototype.execute = function () { 11 | var astNode = this._astNode, 12 | updateOperatorMutation, 13 | updateOperatorReplacements = { 14 | '++': '--', 15 | '--': '++' 16 | }; 17 | 18 | if (canMutate(this._astNode, this._loopVariables) && updateOperatorReplacements.hasOwnProperty(astNode.operator)) { 19 | var replacement = updateOperatorReplacements[astNode.operator]; 20 | 21 | if (astNode.prefix) { 22 | // e.g. ++x 23 | updateOperatorMutation = Utils.createMutation(astNode, astNode.argument.range[0], this._parentMutationId, replacement); 24 | } else { 25 | // e.g. x++ 26 | updateOperatorMutation = { 27 | begin: astNode.argument.range[1], 28 | end: astNode.range[1], 29 | line: astNode.loc.end.line, 30 | col: astNode.argument.loc.end.column, 31 | replacement: replacement 32 | }; 33 | } 34 | 35 | this._callback(updateOperatorMutation); 36 | } 37 | 38 | return []; 39 | }; 40 | 41 | function canMutate(astNode, loopVariables) { 42 | return (loopVariables.indexOf(astNode.argument.name) < 0); 43 | } 44 | 45 | module.exports = MutateUpdateExpressionCommand; 46 | module.exports.code = 'UPDATE_EXPRESSION'; 47 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "grunt-mutation-testing", 3 | "version": "1.4.2", 4 | "description": "JavaScript Mutation Testing as grunt plugin. Tests your tests by mutating the code.", 5 | "repository": { 6 | "type": "git", 7 | "url": "https://github.com/jimivdw/grunt-mutation-testing" 8 | }, 9 | "author": { 10 | "name": "Jimi van der Woning", 11 | "url": "https://github.com/jimivdw" 12 | }, 13 | "contributors": [ 14 | { 15 | "name": "Martin Koster", 16 | "url": "https://github.com/paysdoc" 17 | }, 18 | { 19 | "name": "Marco Stahl", 20 | "url": "https://github.com/shybyte" 21 | } 22 | ], 23 | "keywords": [ 24 | "gruntplugin" 25 | ], 26 | "main": "Gruntfile.js", 27 | "engines": { 28 | "node": ">= 0.10.0" 29 | }, 30 | "license": "MIT", 31 | "devDependencies": { 32 | "chai": "~2.3.0", 33 | "grunt": "~0.4.5", 34 | "grunt-contrib-clean": "~0.6.0", 35 | "grunt-contrib-jshint": "~0.11.2", 36 | "grunt-karma": "~0.12.1", 37 | "grunt-mocha-test": "~0.12.7", 38 | "jshint-stylish": "~2.0.0", 39 | "karma-chai": "~0.1.0", 40 | "karma-mocha": "~0.2.2", 41 | "karma-phantomjs-launcher": "~0.2.3", 42 | "load-grunt-tasks": "~3.2.0" 43 | }, 44 | "scripts": { 45 | "test": "grunt" 46 | }, 47 | "dependencies": { 48 | "escodegen": "~1.6.1", 49 | "esprima": "~1.2.5", 50 | "fs-extra": "~0.16.5", 51 | "glob": "~5.0.10", 52 | "handlebars": "~3.0.3", 53 | "htmlparser2": "~3.8.2", 54 | "karma": "~0.13.21", 55 | "karma-coverage": "~0.5.3", 56 | "lodash": "~3.9.3", 57 | "log4js": "~0.6.25", 58 | "mocha": "~1.21.5", 59 | "q": "~2.0.3", 60 | "require-uncache": "~0.0.2", 61 | "sync-exec": "~0.5.0", 62 | "temp": "~0.8.1", 63 | "xml2js": "~0.4.8" 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /spikes/karma.js: -------------------------------------------------------------------------------- 1 | var runner = require('karma').runner; 2 | var server = require('karma').server; 3 | var path = require('path'); 4 | 5 | 6 | server.start({ 7 | background: false, 8 | singleRun: false, 9 | browsers: ['PhantomJS'], 10 | configFile: path.resolve('test/fixtures/karma-mocha/karma.conf.js'), 11 | reporters: [] 12 | }); 13 | 14 | console.log('Testing'); 15 | 16 | setTimeout(function () { 17 | runner.run({}, function (numberOfCFailingTests) { 18 | console.log('run:', numberOfCFailingTests); 19 | }); 20 | }, 2000); 21 | 22 | setTimeout(function () { 23 | runner.run({}, function (numberOfCFailingTests) { 24 | console.log('run:', numberOfCFailingTests); 25 | }); 26 | }, 30000); 27 | 28 | // https://github.com/karma-runner/karma/issues/509 29 | // https://github.com/karma-runner/karma/issues/136 30 | // killall phantomjs 31 | setTimeout(function () { 32 | //server.stop(); 33 | }, 4000); 34 | -------------------------------------------------------------------------------- /tasks/mutation-testing-karma.js: -------------------------------------------------------------------------------- 1 | /** 2 | * mutation-testing-karma 3 | * 4 | * @author Marco Stahl 5 | * @author Jimi van der Woning 6 | * @author Martin Koster 7 | */ 8 | 'use strict'; 9 | var _ = require('lodash'), 10 | log4js = require('log4js'), 11 | path = require('path'); 12 | 13 | var CopyUtils = require('../utils/CopyUtils'), 14 | IOUtils = require('../utils/IOUtils'), 15 | TestStatus = require('../lib/TestStatus'), 16 | KarmaServerPool = require('../lib/karma/KarmaServerPool'), 17 | KarmaCodeSpecsMatcher = require('../lib/karma/KarmaCodeSpecsMatcher'); 18 | 19 | var logger = log4js.getLogger('mutation-testing-karma'); 20 | 21 | exports.init = function(grunt, opts) { 22 | if(opts.testFramework !== 'karma') { 23 | return; 24 | } 25 | 26 | var karmaConfig = _.extend( 27 | { 28 | // defaults, but can be overwritten 29 | basePath: path.resolve('.'), 30 | reporters: [] 31 | }, 32 | opts.karma, 33 | { 34 | // can't be overwritten, because important for us 35 | background: false, 36 | singleRun: false, 37 | autoWatch: false 38 | } 39 | ), 40 | serverPool = new KarmaServerPool({port: karmaConfig.port, maxActiveServers: karmaConfig.maxActiveServers, startInterval: karmaConfig.startInterval}), 41 | currentInstance, 42 | fileSpecs = {}; 43 | 44 | // Extend the karma configuration with some secondary properties that cannot be overwritten 45 | _.merge(karmaConfig, { 46 | logLevel: ['INFO', 'DEBUG', 'TRACE', 'ALL'].indexOf(karmaConfig.logLevel) !== -1 ? karmaConfig.logLevel : 'INFO', 47 | configFile: karmaConfig.configFile ? path.resolve(karmaConfig.configFile) : undefined 48 | }); 49 | 50 | function startServer(config, callback) { 51 | serverPool.startNewInstance(config).done(function(instance) { 52 | callback(instance); 53 | }); 54 | } 55 | 56 | function stopServers() { 57 | serverPool.stopAllInstances(); 58 | } 59 | 60 | opts.before = function(doneBefore) { 61 | function finalizeBefore(callback) { 62 | new KarmaCodeSpecsMatcher(serverPool, _.merge({}, opts, { karma: karmaConfig })) 63 | .findCodeSpecs().then(function(codeSpecs) { 64 | fileSpecs = codeSpecs; 65 | callback(); 66 | }, function(error) { 67 | logger.warn('Code could not be automatically matched with specs: %s', error.message); 68 | logger.warn( 69 | 'It is still possible to manually provide the code-specs mappings. As a fallback, all tests ' + 70 | 'will be run against all code' 71 | ); 72 | 73 | _.forEach(opts.code, function(codeFile) { 74 | fileSpecs[codeFile] = opts.specs; 75 | }); 76 | logger.debug('Using code-specs mappings: %j', fileSpecs); 77 | callback(); 78 | }); 79 | } 80 | 81 | if(!opts.mutateProductionCode) { 82 | CopyUtils.copyToTemp(opts.code.concat(opts.specs), 'mutation-testing').done(function(tempDirPath) { 83 | logger.trace('Copied %j to %s', opts.code.concat(opts.specs), tempDirPath); 84 | 85 | // Set the basePath relative to the temp dir 86 | karmaConfig.basePath = tempDirPath; 87 | opts.basePath = path.join(tempDirPath, opts.basePath); 88 | 89 | // Set the paths to the files to be mutated relative to the temp dir 90 | opts.mutate = _.map(opts.mutate, function(file) { 91 | return path.join(tempDirPath, file); 92 | }); 93 | 94 | finalizeBefore(doneBefore); 95 | }); 96 | } else { 97 | finalizeBefore(doneBefore); 98 | } 99 | 100 | process.on('exit', function() { 101 | stopServers(); 102 | }); 103 | }; 104 | 105 | opts.beforeEach = function(done) { 106 | var currentFileSpecs; 107 | 108 | // Find the specs for the current mutation file 109 | currentFileSpecs = _.find(fileSpecs, function(specs, file) { 110 | return IOUtils.normalizeWindowsPath(opts.currentFile).indexOf(file) !== -1; 111 | }); 112 | 113 | if(currentFileSpecs && currentFileSpecs.length) { 114 | karmaConfig.files = _.union(opts.code, currentFileSpecs); 115 | 116 | startServer(karmaConfig, function(instance) { 117 | currentInstance = instance; 118 | done(true); 119 | }); 120 | } else { 121 | logger.warn('Could not find specs for file: %s', path.resolve(opts.currentFile)); 122 | done(false); 123 | } 124 | }; 125 | 126 | opts.test = function(done) { 127 | if(currentInstance) { 128 | currentInstance.runTests().then(function(testStatus) { 129 | done(testStatus); 130 | }, function(error) { 131 | logger.warn(error.message); 132 | if(error.severity === 'fatal') { 133 | logger.error('Fatal: Unfortunately the mutation test cannot recover from this error and will shut down'); 134 | 135 | stopServers(); 136 | currentInstance.kill(); 137 | 138 | done(TestStatus.FATAL); 139 | } else { 140 | startServer(karmaConfig, function(instance) { 141 | currentInstance = instance; 142 | done(TestStatus.ERROR); 143 | }); 144 | } 145 | }); 146 | } else { 147 | logger.warn('No Karma server was present to run the tests'); 148 | startServer(karmaConfig, function(instance) { 149 | currentInstance = instance; 150 | done(TestStatus.ERROR); 151 | }); 152 | } 153 | }; 154 | 155 | opts.afterEach = function(done) { 156 | if(currentInstance) { 157 | // Kill the currently active instance 158 | currentInstance.stop(); 159 | currentInstance = null; 160 | } 161 | 162 | done(); 163 | }; 164 | 165 | opts.after = function(done) { 166 | stopServers(); 167 | done(); 168 | }; 169 | }; 170 | -------------------------------------------------------------------------------- /tasks/mutation-testing-mocha.js: -------------------------------------------------------------------------------- 1 | /** 2 | * mutation-testing-mocha 3 | */ 4 | 'use strict'; 5 | 6 | var _ = require('lodash'), 7 | log4js = require('log4js'), 8 | Mocha = require('mocha'), 9 | path = require('path'), 10 | requireUncache = require('require-uncache'); 11 | 12 | var CopyUtils = require('../utils/CopyUtils'), 13 | TestStatus = require('../lib/TestStatus'); 14 | 15 | var logger = log4js.getLogger('mutation-testing-mocha'); 16 | 17 | exports.init = function(grunt, opts) { 18 | if(opts.testFramework !== 'mocha') { 19 | return; 20 | } 21 | 22 | var testFiles = opts.specs; 23 | if(testFiles.length === 0) { 24 | logger.warn('No test files configured; opts.specs is empty'); 25 | } 26 | 27 | opts.before = function(doneBefore) { 28 | if(opts.mutateProductionCode) { 29 | doneBefore(); 30 | } else { 31 | // Find which files are used in the unit test such that they can be copied 32 | CopyUtils.copyToTemp(opts.code.concat(opts.specs), 'mutation-testing').done(function(tempDirPath) { 33 | logger.trace('Copied %j to %s', opts.code.concat(opts.specs), tempDirPath); 34 | 35 | // Set the basePath relative to the temp dir 36 | opts.basePath = path.join(tempDirPath, opts.basePath); 37 | 38 | testFiles = _.map(testFiles, function(file) { 39 | return path.join(tempDirPath, file); 40 | }); 41 | 42 | // Set the paths to the files to be mutated relative to the temp dir 43 | opts.mutate = _.map(opts.mutate, function(file) { 44 | return path.join(tempDirPath, file); 45 | }); 46 | 47 | doneBefore(); 48 | }); 49 | } 50 | }; 51 | 52 | opts.test = function(done) { 53 | //https://github.com/visionmedia/mocha/wiki/Third-party-reporters 54 | var mocha = new Mocha({ 55 | reporter: function(runner) { 56 | //dummyReporter 57 | //runner.on('fail', function(test, err){ 58 | //console.log('fail: %s -- error: %s', test.fullTitle(), err.message); 59 | //}); 60 | } 61 | }); 62 | 63 | mocha.suite.on('pre-require', function(context, file) { 64 | var cache = require.cache || {}; 65 | for(var key in cache) { 66 | if(cache.hasOwnProperty(key)) { 67 | delete cache[key]; 68 | } 69 | } 70 | requireUncache(file); 71 | }); 72 | 73 | testFiles.forEach(function(testFile) { 74 | mocha.addFile(testFile); 75 | }); 76 | 77 | try { 78 | mocha.run(function(errCount) { 79 | var testStatus = (errCount === 0) ? TestStatus.SURVIVED : TestStatus.KILLED; 80 | done(testStatus); 81 | }); 82 | } catch(error) { 83 | done(TestStatus.KILLED); 84 | } 85 | }; 86 | }; 87 | -------------------------------------------------------------------------------- /test/.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "globalstrict": false, 3 | "globals": { 4 | "require": false, 5 | "describe": false, 6 | "it": false, 7 | "before": false, 8 | "beforeEach": false, 9 | "after": false, 10 | "afterEach": false, 11 | "expect": false, 12 | "spyOn": false, 13 | "module": false, 14 | "inject": false, 15 | "mockData": false 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /test/expected/arguments.json: -------------------------------------------------------------------------------- 1 | { 2 | "mocha": { 3 | "arguments.js": { 4 | "stats": { 5 | "all": 12, 6 | "killed": 6, 7 | "survived": 4, 8 | "ignored": 2, 9 | "untested": 0 10 | }, 11 | "src": "var _ = require('lodash');\n\nfunction containsName(persons, name) {\n return _.contains(_.pluck(persons, 'name'), name);\n}\n\nexports.containsName = containsName;\n", 12 | "fileName": "mocha/arguments.js", 13 | "mutationResults": [ 14 | { 15 | "mutation": { 16 | "range": [ 17 | 0, 18 | 26 19 | ], 20 | "begin": 0, 21 | "end": 26, 22 | "line": 1, 23 | "col": 0, 24 | "parentMutationId": "286", 25 | "mutationId": "287", 26 | "replacement": "" 27 | }, 28 | "survived": false, 29 | "message": "mocha/arguments.js:1:1 Removed var _ = require('lodash'); -> KILLED" 30 | }, 31 | { 32 | "mutation": { 33 | "range": [ 34 | 28, 35 | 123 36 | ], 37 | "begin": 28, 38 | "end": 123, 39 | "line": 3, 40 | "col": 0, 41 | "parentMutationId": "286", 42 | "mutationId": "288", 43 | "replacement": "" 44 | }, 45 | "survived": false, 46 | "message": "mocha/arguments.js:3:1 Removed function containsName(persons, name) { return _.contains(_.pluck(persons, 'name'), name); } -> KILLED" 47 | }, 48 | { 49 | "mutation": { 50 | "range": [ 51 | 125, 52 | 161 53 | ], 54 | "begin": 125, 55 | "end": 161, 56 | "line": 7, 57 | "col": 0, 58 | "parentMutationId": "286", 59 | "mutationId": "289", 60 | "replacement": "" 61 | }, 62 | "survived": false, 63 | "message": "mocha/arguments.js:7:1 Removed exports.containsName = containsName; -> KILLED" 64 | }, 65 | { 66 | "mutation": { 67 | "range": [ 68 | 16, 69 | 24 70 | ], 71 | "begin": 16, 72 | "end": 24, 73 | "line": 1, 74 | "col": 16, 75 | "parentMutationId": "287", 76 | "mutationId": "290", 77 | "replacement": "\"MUTATION!\"" 78 | }, 79 | "survived": false, 80 | "message": "mocha/arguments.js:1:17 Replaced 'lodash' with \"MUTATION!\" -> KILLED" 81 | }, 82 | { 83 | "mutation": { 84 | "range": [ 85 | 8, 86 | 25 87 | ], 88 | "begin": 8, 89 | "end": 25, 90 | "line": 1, 91 | "col": 8, 92 | "parentMutationId": "287", 93 | "mutationId": "291", 94 | "replacement": "'lodash'" 95 | }, 96 | "survived": false, 97 | "message": "mocha/arguments.js:1:9 Replaced require('lodash') with 'lodash' -> KILLED" 98 | }, 99 | { 100 | "mutation": { 101 | "range": [ 102 | 71, 103 | 121 104 | ], 105 | "begin": 71, 106 | "end": 121, 107 | "line": 4, 108 | "col": 4, 109 | "parentMutationId": "288", 110 | "mutationId": "292", 111 | "replacement": "" 112 | }, 113 | "survived": false, 114 | "message": "mocha/arguments.js:4:5 Removed return _.contains(_.pluck(persons, 'name'), name); -> KILLED" 115 | }, 116 | { 117 | "mutation": { 118 | "range": [ 119 | 89, 120 | 113 121 | ], 122 | "begin": 89, 123 | "end": 113, 124 | "line": 4, 125 | "col": 22, 126 | "parentMutationId": "292", 127 | "mutationId": "293", 128 | "replacement": "\"MUTATION!\"" 129 | }, 130 | "survived": true, 131 | "message": "mocha/arguments.js:4:23 Replaced _.pluck(persons, 'name') with \"MUTATION!\" -> SURVIVED" 132 | }, 133 | { 134 | "mutation": { 135 | "range": [ 136 | 115, 137 | 119 138 | ], 139 | "begin": 115, 140 | "end": 119, 141 | "line": 4, 142 | "col": 48, 143 | "parentMutationId": "292", 144 | "mutationId": "294", 145 | "replacement": "\"MUTATION!\"" 146 | }, 147 | "survived": true, 148 | "message": "mocha/arguments.js:4:49 Replaced name with \"MUTATION!\" -> SURVIVED" 149 | }, 150 | { 151 | "mutation": { 152 | "range": [ 153 | 97, 154 | 104 155 | ], 156 | "begin": 97, 157 | "end": 104, 158 | "line": 4, 159 | "col": 30, 160 | "parentMutationId": "292", 161 | "mutationId": "296", 162 | "replacement": "\"MUTATION!\"" 163 | }, 164 | "survived": true, 165 | "message": "mocha/arguments.js:4:31 Replaced persons with \"MUTATION!\" -> SURVIVED" 166 | }, 167 | { 168 | "mutation": { 169 | "range": [ 170 | 106, 171 | 112 172 | ], 173 | "begin": 106, 174 | "end": 112, 175 | "line": 4, 176 | "col": 39, 177 | "parentMutationId": "292", 178 | "mutationId": "297", 179 | "replacement": "\"MUTATION!\"" 180 | }, 181 | "survived": true, 182 | "message": "mocha/arguments.js:4:40 Replaced 'name' with \"MUTATION!\" -> SURVIVED" 183 | } 184 | ], 185 | "mutationScore": { 186 | "total": 0.6666666666666666, 187 | "killed": 0.5, 188 | "survived": 0.3333333333333333, 189 | "ignored": 0.16666666666666666, 190 | "untested": 0 191 | } 192 | }, 193 | "stats": { 194 | "all": 12, 195 | "killed": 6, 196 | "survived": 4, 197 | "ignored": 2, 198 | "untested": 0 199 | }, 200 | "mutationScore": { 201 | "total": 0.6666666666666666, 202 | "killed": 0.5, 203 | "survived": 0.3333333333333333, 204 | "ignored": 0.16666666666666666, 205 | "untested": 0 206 | } 207 | }, 208 | "stats": { 209 | "all": 12, 210 | "killed": 6, 211 | "survived": 4, 212 | "ignored": 2, 213 | "untested": 0 214 | }, 215 | "mutationScore": { 216 | "total": 0.6666666666666666, 217 | "killed": 0.5, 218 | "survived": 0.3333333333333333, 219 | "ignored": 0.16666666666666666, 220 | "untested": 0 221 | } 222 | } -------------------------------------------------------------------------------- /test/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 | -------------------------------------------------------------------------------- /test/expected/arrays.json: -------------------------------------------------------------------------------- 1 | { 2 | "mocha": { 3 | "array.js": { 4 | "stats": { 5 | "all": 9, 6 | "killed": 4, 7 | "survived": 3, 8 | "ignored": 2, 9 | "untested": 0 10 | }, 11 | "src": "'use strict';\n/** @excludeMutations ['LITERAL'] */\nfunction createArray() {\n var el = {};\n return [el, 'string', 123];\n}\n\nexports.createArray = createArray;\n", 12 | "fileName": "mocha/array.js", 13 | "mutationResults": [ 14 | { 15 | "mutation": { 16 | "range": [ 17 | 51, 18 | 126 19 | ], 20 | "begin": 51, 21 | "end": 126, 22 | "line": 3, 23 | "col": 0, 24 | "parentMutationId": "442", 25 | "mutationId": "444", 26 | "replacement": "" 27 | }, 28 | "survived": false, 29 | "message": "mocha/array.js:3:1 Removed function createArray() { var el = {}; return [el, 'string', 123]; } -> KILLED" 30 | }, 31 | { 32 | "mutation": { 33 | "range": [ 34 | 128, 35 | 162 36 | ], 37 | "begin": 128, 38 | "end": 162, 39 | "line": 8, 40 | "col": 0, 41 | "parentMutationId": "442", 42 | "mutationId": "445", 43 | "replacement": "" 44 | }, 45 | "survived": false, 46 | "message": "mocha/array.js:8:1 Removed exports.createArray = createArray; -> KILLED" 47 | }, 48 | { 49 | "mutation": { 50 | "range": [ 51 | 80, 52 | 92 53 | ], 54 | "begin": 80, 55 | "end": 92, 56 | "line": 4, 57 | "col": 4, 58 | "parentMutationId": "444", 59 | "mutationId": "447", 60 | "replacement": "" 61 | }, 62 | "survived": false, 63 | "message": "mocha/array.js:4:5 Removed var el = {}; -> KILLED" 64 | }, 65 | { 66 | "mutation": { 67 | "range": [ 68 | 97, 69 | 124 70 | ], 71 | "begin": 97, 72 | "end": 124, 73 | "line": 5, 74 | "col": 4, 75 | "parentMutationId": "444", 76 | "mutationId": "448", 77 | "replacement": "" 78 | }, 79 | "survived": false, 80 | "message": "mocha/array.js:5:5 Removed return [el, 'string', 123]; -> KILLED" 81 | }, 82 | { 83 | "mutation": { 84 | "range": [ 85 | 105, 86 | 107 87 | ], 88 | "begin": 105, 89 | "end": 109, 90 | "line": 5, 91 | "col": 12, 92 | "parentMutationId": "448", 93 | "mutationId": "449", 94 | "replacement": "" 95 | }, 96 | "survived": true, 97 | "message": "mocha/array.js:5:13 Removed el, -> SURVIVED" 98 | }, 99 | { 100 | "mutation": { 101 | "range": [ 102 | 109, 103 | 117 104 | ], 105 | "begin": 109, 106 | "end": 119, 107 | "line": 5, 108 | "col": 16, 109 | "parentMutationId": "448", 110 | "mutationId": "450", 111 | "replacement": "" 112 | }, 113 | "survived": true, 114 | "message": "mocha/array.js:5:17 Removed 'string', -> SURVIVED" 115 | }, 116 | { 117 | "mutation": { 118 | "range": [ 119 | 119, 120 | 122 121 | ], 122 | "begin": 119, 123 | "end": 122, 124 | "line": 5, 125 | "col": 26, 126 | "parentMutationId": "448", 127 | "mutationId": "451", 128 | "replacement": "" 129 | }, 130 | "survived": true, 131 | "message": "mocha/array.js:5:27 Removed 123 -> SURVIVED" 132 | } 133 | ], 134 | "mutationScore": { 135 | "total": 0.6666666666666666, 136 | "killed": 0.4444444444444444, 137 | "survived": 0.3333333333333333, 138 | "ignored": 0.2222222222222222, 139 | "untested": 0 140 | } 141 | }, 142 | "stats": { 143 | "all": 9, 144 | "killed": 4, 145 | "survived": 3, 146 | "ignored": 2, 147 | "untested": 0 148 | }, 149 | "mutationScore": { 150 | "total": 0.6666666666666666, 151 | "killed": 0.4444444444444444, 152 | "survived": 0.3333333333333333, 153 | "ignored": 0.2222222222222222, 154 | "untested": 0 155 | } 156 | }, 157 | "stats": { 158 | "all": 9, 159 | "killed": 4, 160 | "survived": 3, 161 | "ignored": 2, 162 | "untested": 0 163 | }, 164 | "mutationScore": { 165 | "total": 0.6666666666666666, 166 | "killed": 0.4444444444444444, 167 | "survived": 0.3333333333333333, 168 | "ignored": 0.2222222222222222, 169 | "untested": 0 170 | } 171 | } -------------------------------------------------------------------------------- /test/expected/arrays.txt: -------------------------------------------------------------------------------- 1 | mocha/array.js:5:13 Removed el, -> SURVIVED 2 | mocha/array.js:5:17 Removed 'string', -> SURVIVED 3 | mocha/array.js:5:27 Removed 123 -> SURVIVED 4 | 4 of 7 unignored mutations are tested (57%). 2 mutations were ignored. 5 | -------------------------------------------------------------------------------- /test/expected/attributes.json: -------------------------------------------------------------------------------- 1 | { 2 | "mocha": { 3 | "attribute.js": { 4 | "stats": { 5 | "all": 5, 6 | "killed": 3, 7 | "survived": 2, 8 | "ignored": 0, 9 | "untested": 0 10 | }, 11 | "src": "function createPerson(name, age) {\n return {\n name: name,\n age: age\n }\n}\n\nexports.createPerson = createPerson;\n", 12 | "fileName": "mocha/attribute.js", 13 | "mutationResults": [ 14 | { 15 | "mutation": { 16 | "range": [ 17 | 0, 18 | 92 19 | ], 20 | "begin": 0, 21 | "end": 92, 22 | "line": 1, 23 | "col": 0, 24 | "parentMutationId": "280", 25 | "mutationId": "281", 26 | "replacement": "" 27 | }, 28 | "survived": false, 29 | "message": "mocha/attribute.js:1:1 Removed function createPerson(name, age) { return { name: name, age: age } } -> KILLED" 30 | }, 31 | { 32 | "mutation": { 33 | "range": [ 34 | 94, 35 | 130 36 | ], 37 | "begin": 94, 38 | "end": 130, 39 | "line": 8, 40 | "col": 0, 41 | "parentMutationId": "280", 42 | "mutationId": "282", 43 | "replacement": "" 44 | }, 45 | "survived": false, 46 | "message": "mocha/attribute.js:8:1 Removed exports.createPerson = createPerson; -> KILLED" 47 | }, 48 | { 49 | "mutation": { 50 | "range": [ 51 | 39, 52 | 90 53 | ], 54 | "begin": 39, 55 | "end": 90, 56 | "line": 2, 57 | "col": 4, 58 | "parentMutationId": "281", 59 | "mutationId": "283", 60 | "replacement": "" 61 | }, 62 | "survived": false, 63 | "message": "mocha/attribute.js:2:5 Removed return { name: name, age: age } -> KILLED" 64 | }, 65 | { 66 | "mutation": { 67 | "range": [ 68 | 56, 69 | 66 70 | ], 71 | "begin": 56, 72 | "end": 76, 73 | "line": 3, 74 | "col": 8, 75 | "parentMutationId": "283", 76 | "mutationId": "284", 77 | "replacement": "" 78 | }, 79 | "survived": true, 80 | "message": "mocha/attribute.js:3:9 Removed name: name, -> SURVIVED" 81 | }, 82 | { 83 | "mutation": { 84 | "range": [ 85 | 76, 86 | 84 87 | ], 88 | "begin": 76, 89 | "end": 84, 90 | "line": 4, 91 | "col": 8, 92 | "parentMutationId": "283", 93 | "mutationId": "285", 94 | "replacement": "" 95 | }, 96 | "survived": true, 97 | "message": "mocha/attribute.js:4:9 Removed age: age -> SURVIVED" 98 | } 99 | ], 100 | "mutationScore": { 101 | "total": 0.6, 102 | "killed": 0.6, 103 | "survived": 0.4, 104 | "ignored": 0, 105 | "untested": 0 106 | } 107 | }, 108 | "stats": { 109 | "all": 5, 110 | "killed": 3, 111 | "survived": 2, 112 | "ignored": 0, 113 | "untested": 0 114 | }, 115 | "mutationScore": { 116 | "total": 0.6, 117 | "killed": 0.6, 118 | "survived": 0.4, 119 | "ignored": 0, 120 | "untested": 0 121 | } 122 | }, 123 | "stats": { 124 | "all": 5, 125 | "killed": 3, 126 | "survived": 2, 127 | "ignored": 0, 128 | "untested": 0 129 | }, 130 | "mutationScore": { 131 | "total": 0.6, 132 | "killed": 0.6, 133 | "survived": 0.4, 134 | "ignored": 0, 135 | "untested": 0 136 | } 137 | } -------------------------------------------------------------------------------- /test/expected/attributes.txt: -------------------------------------------------------------------------------- 1 | mocha/attribute.js:3:9 Removed name: name, -> SURVIVED 2 | mocha/attribute.js:4:9 Removed age: age -> SURVIVED 3 | 3 of 5 unignored mutations are tested (60%). 4 | -------------------------------------------------------------------------------- /test/expected/comparisons.txt: -------------------------------------------------------------------------------- 1 | mocha/comparisons.js:5:16 Replaced < with <= -> SURVIVED 2 | mocha/comparisons.js:5:16 Replaced < with >= -> SURVIVED 3 | mocha/comparisons.js:9:16 Replaced <= with < -> SURVIVED 4 | mocha/comparisons.js:9:16 Replaced <= with > -> SURVIVED 5 | mocha/comparisons.js:13:16 Replaced > with >= -> SURVIVED 6 | mocha/comparisons.js:13:16 Replaced > with <= -> SURVIVED 7 | mocha/comparisons.js:17:16 Replaced >= with > -> SURVIVED 8 | mocha/comparisons.js:17:16 Replaced >= with < -> SURVIVED 9 | mocha/comparisons.js:21:16 Replaced === with !== -> SURVIVED 10 | mocha/comparisons.js:25:16 Replaced == with != -> SURVIVED 11 | mocha/comparisons.js:29:16 Replaced !== with === -> SURVIVED 12 | mocha/comparisons.js:33:16 Replaced != with == -> SURVIVED 13 | 15 of 27 unignored mutations are tested (55%). 2 mutations were ignored. 14 | -------------------------------------------------------------------------------- /test/expected/dont-test-inside-surviving-mutations.txt: -------------------------------------------------------------------------------- 1 | mocha/script1.js:40:1 Removed console.log = function() { // Mock console log to prevent output from leaking to mutation test console }; -> SURVIVED 2 | mocha/script1.js:13:5 Removed sum = sum + 0; -> SURVIVED 3 | mocha/script1.js:14:5 Removed console.log(sum); -> SURVIVED 4 | mocha/script1.js:13:14 is inside a surviving mutation 5 | mocha/script1.js:13:17 is inside a surviving mutation 6 | mocha/script1.js:14:17 is inside a surviving mutation 7 | mocha/script1.js:14:5 is inside a surviving mutation 8 | mocha/script1.js:14:5 is inside a surviving mutation 9 | mocha/script2.js:5:5 Removed array = array; -> SURVIVED 10 | mocha/script2.js:6:5 Removed log(array); -> SURVIVED 11 | mocha/script2.js:6:9 is inside a surviving mutation 12 | mocha/script2.js:6:5 is inside a surviving mutation 13 | 28 of 40 unignored mutations are tested (70%). 14 | -------------------------------------------------------------------------------- /test/expected/flag-all-mutations.txt: -------------------------------------------------------------------------------- 1 | mocha/script1.js:1:1 Removed function add(array) { var sum = 0; for (var i = 0; i < array.length; i = i + 1) { sum += array[i]; } return sum; } -> SURVIVED 2 | mocha/script1.js:9:1 Removed function sub(array) { var x = array[0]; var y = array[1]; var sum = x - y; sum = sum + 0; console.log(sum); return sum; } -> SURVIVED 3 | mocha/script1.js:24:1 Removed function mul(array) { var x = array[0]; var y = array[1]; var sum = x * y; if (sum > 9){ console.log(sum); } return sum; } -> SURVIVED 4 | mocha/script1.js:34:1 Removed exports.add = add; -> SURVIVED 5 | mocha/script1.js:35:1 Removed exports.sub = sub; -> SURVIVED 6 | mocha/script1.js:38:1 Removed exports.mul = mul; -> SURVIVED 7 | mocha/script1.js:40:1 Removed console.log = function() { // Mock console log to prevent output from leaking to mutation test console }; -> SURVIVED 8 | mocha/script1.js:2:5 Removed var sum = 0; -> SURVIVED 9 | mocha/script1.js:3:5 Removed for (var i = 0; i < array.length; i = i + 1) { sum += array[i]; } -> SURVIVED 10 | mocha/script1.js:6:5 Removed return sum; -> SURVIVED 11 | mocha/script1.js:2:15 Replaced 0 with 1 -> SURVIVED 12 | mocha/script1.js:3:18 Replaced 0 with 1 -> SURVIVED 13 | mocha/script1.js:4:9 Removed sum += array[i]; -> SURVIVED 14 | mocha/script1.js:10:5 Removed var x = array[0]; -> SURVIVED 15 | mocha/script1.js:11:5 Removed var y = array[1]; -> SURVIVED 16 | mocha/script1.js:12:5 Removed var sum = x - y; -> SURVIVED 17 | mocha/script1.js:13:5 Removed sum = sum + 0; -> SURVIVED 18 | mocha/script1.js:14:5 Removed console.log(sum); -> SURVIVED 19 | mocha/script1.js:15:5 Removed return sum; -> SURVIVED 20 | mocha/script1.js:10:19 Replaced 0 with 1 -> SURVIVED 21 | mocha/script1.js:11:19 Replaced 1 with 2 -> SURVIVED 22 | mocha/script1.js:12:16 Replaced - with + -> SURVIVED 23 | mocha/script1.js:13:14 Replaced + with - -> SURVIVED 24 | mocha/script1.js:13:17 Replaced 0 with 1 -> SURVIVED 25 | mocha/script1.js:14:17 Replaced sum with "MUTATION!" -> SURVIVED 26 | mocha/script1.js:14:5 Replaced console.log(sum) with sum -> SURVIVED 27 | mocha/script1.js:14:5 Replaced console.log(sum) with console -> SURVIVED 28 | mocha/script2.js:1:1 Removed function log() { } -> SURVIVED 29 | mocha/script2.js:4:1 Removed function mul(array) { array = array; log(array); return array.reduce(function (x, y) { return x * y; }); } -> SURVIVED 30 | mocha/script2.js:12:1 Removed exports.mul = mul; -> SURVIVED 31 | mocha/script2.js:5:5 Removed array = array; -> SURVIVED 32 | mocha/script2.js:6:5 Removed log(array); -> SURVIVED 33 | mocha/script2.js:7:5 Removed return array.reduce(function (x, y) { return x * y; }); -> SURVIVED 34 | mocha/script2.js:6:9 Replaced array with "MUTATION!" -> SURVIVED 35 | mocha/script2.js:6:5 Replaced log(array) with array -> SURVIVED 36 | mocha/script2.js:7:25 Replaced function (x, y) { return x * y; } with "MUTATION!" -> SURVIVED 37 | mocha/script2.js:7:12 Replaced array.reduce(function (x, y) { return x * y; }) with function (x, y) { return x * y; } -> SURVIVED 38 | mocha/script2.js:7:12 Replaced array.reduce(function (x, y) { return x * y; }) with array -> SURVIVED 39 | mocha/script2.js:8:9 Removed return x * y; -> SURVIVED 40 | mocha/script2.js:8:17 Replaced * with / -> SURVIVED 41 | 0 of 40 unignored mutations are tested (0%). 42 | -------------------------------------------------------------------------------- /test/expected/function-calls.json: -------------------------------------------------------------------------------- 1 | { 2 | "mocha": { 3 | "function-calls.js": { 4 | "stats": { 5 | "all": 9, 6 | "killed": 7, 7 | "survived": 2, 8 | "ignored": 0, 9 | "untested": 0 10 | }, 11 | "src": "function encodeUrl(url) {\n return encodeURI(url);\n}\n\nfunction trim(string) {\n return string.trim();\n}\n\nexports.encodeUrl = encodeUrl;\nexports.trim = trim;\n", 12 | "fileName": "mocha/function-calls.js", 13 | "mutationResults": [ 14 | { 15 | "mutation": { 16 | "range": [ 17 | 0, 18 | 54 19 | ], 20 | "begin": 0, 21 | "end": 54, 22 | "line": 1, 23 | "col": 0, 24 | "parentMutationId": "452", 25 | "mutationId": "453", 26 | "replacement": "" 27 | }, 28 | "survived": false, 29 | "message": "mocha/function-calls.js:1:1 Removed function encodeUrl(url) { return encodeURI(url); } -> KILLED" 30 | }, 31 | { 32 | "mutation": { 33 | "range": [ 34 | 56, 35 | 107 36 | ], 37 | "begin": 56, 38 | "end": 107, 39 | "line": 5, 40 | "col": 0, 41 | "parentMutationId": "452", 42 | "mutationId": "454", 43 | "replacement": "" 44 | }, 45 | "survived": false, 46 | "message": "mocha/function-calls.js:5:1 Removed function trim(string) { return string.trim(); } -> KILLED" 47 | }, 48 | { 49 | "mutation": { 50 | "range": [ 51 | 109, 52 | 139 53 | ], 54 | "begin": 109, 55 | "end": 139, 56 | "line": 9, 57 | "col": 0, 58 | "parentMutationId": "452", 59 | "mutationId": "455", 60 | "replacement": "" 61 | }, 62 | "survived": false, 63 | "message": "mocha/function-calls.js:9:1 Removed exports.encodeUrl = encodeUrl; -> KILLED" 64 | }, 65 | { 66 | "mutation": { 67 | "range": [ 68 | 140, 69 | 160 70 | ], 71 | "begin": 140, 72 | "end": 160, 73 | "line": 10, 74 | "col": 0, 75 | "parentMutationId": "452", 76 | "mutationId": "456", 77 | "replacement": "" 78 | }, 79 | "survived": false, 80 | "message": "mocha/function-calls.js:10:1 Removed exports.trim = trim; -> KILLED" 81 | }, 82 | { 83 | "mutation": { 84 | "range": [ 85 | 30, 86 | 52 87 | ], 88 | "begin": 30, 89 | "end": 52, 90 | "line": 2, 91 | "col": 4, 92 | "parentMutationId": "453", 93 | "mutationId": "457", 94 | "replacement": "" 95 | }, 96 | "survived": false, 97 | "message": "mocha/function-calls.js:2:5 Removed return encodeURI(url); -> KILLED" 98 | }, 99 | { 100 | "mutation": { 101 | "range": [ 102 | 47, 103 | 50 104 | ], 105 | "begin": 47, 106 | "end": 50, 107 | "line": 2, 108 | "col": 21, 109 | "parentMutationId": "457", 110 | "mutationId": "458", 111 | "replacement": "\"MUTATION!\"" 112 | }, 113 | "survived": false, 114 | "message": "mocha/function-calls.js:2:22 Replaced url with \"MUTATION!\" -> KILLED" 115 | }, 116 | { 117 | "mutation": { 118 | "range": [ 119 | 37, 120 | 51 121 | ], 122 | "begin": 37, 123 | "end": 51, 124 | "line": 2, 125 | "col": 11, 126 | "parentMutationId": "457", 127 | "mutationId": "459", 128 | "replacement": "url" 129 | }, 130 | "survived": true, 131 | "message": "mocha/function-calls.js:2:12 Replaced encodeURI(url) with url -> SURVIVED" 132 | }, 133 | { 134 | "mutation": { 135 | "range": [ 136 | 84, 137 | 105 138 | ], 139 | "begin": 84, 140 | "end": 105, 141 | "line": 6, 142 | "col": 4, 143 | "parentMutationId": "454", 144 | "mutationId": "460", 145 | "replacement": "" 146 | }, 147 | "survived": false, 148 | "message": "mocha/function-calls.js:6:5 Removed return string.trim(); -> KILLED" 149 | }, 150 | { 151 | "mutation": { 152 | "range": [ 153 | 91, 154 | 104 155 | ], 156 | "begin": 91, 157 | "end": 104, 158 | "line": 6, 159 | "col": 11, 160 | "parentMutationId": "460", 161 | "mutationId": "461", 162 | "replacement": "string" 163 | }, 164 | "survived": true, 165 | "message": "mocha/function-calls.js:6:12 Replaced string.trim() with string -> SURVIVED" 166 | } 167 | ], 168 | "mutationScore": { 169 | "total": 0.7777777777777778, 170 | "killed": 0.7777777777777778, 171 | "survived": 0.2222222222222222, 172 | "ignored": 0, 173 | "untested": 0 174 | } 175 | }, 176 | "stats": { 177 | "all": 9, 178 | "killed": 7, 179 | "survived": 2, 180 | "ignored": 0, 181 | "untested": 0 182 | }, 183 | "mutationScore": { 184 | "total": 0.7777777777777778, 185 | "killed": 0.7777777777777778, 186 | "survived": 0.2222222222222222, 187 | "ignored": 0, 188 | "untested": 0 189 | } 190 | }, 191 | "stats": { 192 | "all": 9, 193 | "killed": 7, 194 | "survived": 2, 195 | "ignored": 0, 196 | "untested": 0 197 | }, 198 | "mutationScore": { 199 | "total": 0.7777777777777778, 200 | "killed": 0.7777777777777778, 201 | "survived": 0.2222222222222222, 202 | "ignored": 0, 203 | "untested": 0 204 | } 205 | } -------------------------------------------------------------------------------- /test/expected/function-calls.txt: -------------------------------------------------------------------------------- 1 | mocha/function-calls.js:2:12 Replaced encodeURI(url) with url -> SURVIVED 2 | mocha/function-calls.js:6:12 Replaced string.trim() with string -> SURVIVED 3 | 7 of 9 unignored mutations are tested (77%). 4 | -------------------------------------------------------------------------------- /test/expected/grunt.txt: -------------------------------------------------------------------------------- 1 | mocha/script1.js:1:1 Removed function add(array) { var sum = 0; for (var i = 0; i < array.length; i = i + 1) { sum += array[i]; } return sum; } -> SURVIVED 2 | mocha/script1.js:9:1 Removed function sub(array) { var x = array[0]; var y = array[1]; var sum = x - y; sum = sum + 0; console.log(sum); return sum; } -> SURVIVED 3 | mocha/script1.js:24:1 Removed function mul(array) { var x = array[0]; var y = array[1]; var sum = x * y; if (sum > 9){ console.log(sum); } return sum; } -> SURVIVED 4 | mocha/script1.js:34:1 Removed exports.add = add; -> SURVIVED 5 | mocha/script1.js:35:1 Removed exports.sub = sub; -> SURVIVED 6 | mocha/script1.js:38:1 Removed exports.mul = mul; -> SURVIVED 7 | mocha/script1.js:40:1 Removed console.log = function() { // Mock console log to prevent output from leaking to mutation test console }; -> SURVIVED 8 | mocha/script1.js:2:5 Removed var sum = 0; -> SURVIVED 9 | mocha/script1.js:3:5 Removed for (var i = 0; i < array.length; i = i + 1) { sum += array[i]; } -> SURVIVED 10 | mocha/script1.js:6:5 Removed return sum; -> SURVIVED 11 | mocha/script1.js:2:15 Replaced 0 with 1 -> SURVIVED 12 | mocha/script1.js:3:18 Replaced 0 with 1 -> SURVIVED 13 | mocha/script1.js:4:9 Removed sum += array[i]; -> SURVIVED 14 | mocha/script1.js:10:5 Removed var x = array[0]; -> SURVIVED 15 | mocha/script1.js:11:5 Removed var y = array[1]; -> SURVIVED 16 | mocha/script1.js:12:5 Removed var sum = x - y; -> SURVIVED 17 | mocha/script1.js:13:5 Removed sum = sum + 0; -> SURVIVED 18 | mocha/script1.js:14:5 Removed console.log(sum); -> SURVIVED 19 | mocha/script1.js:15:5 Removed return sum; -> SURVIVED 20 | mocha/script1.js:10:19 Replaced 0 with 1 -> SURVIVED 21 | mocha/script1.js:11:19 Replaced 1 with 2 -> SURVIVED 22 | mocha/script1.js:12:16 Replaced - with + -> SURVIVED 23 | mocha/script1.js:13:14 Replaced + with - -> SURVIVED 24 | mocha/script1.js:13:17 Replaced 0 with 1 -> SURVIVED 25 | mocha/script1.js:14:17 Replaced sum with "MUTATION!" -> SURVIVED 26 | mocha/script1.js:14:5 Replaced console.log(sum) with sum -> SURVIVED 27 | mocha/script1.js:14:5 Replaced console.log(sum) with console -> SURVIVED 28 | mocha/script2.js:1:1 Removed function log() { } -> SURVIVED 29 | mocha/script2.js:4:1 Removed function mul(array) { array = array; log(array); return array.reduce(function (x, y) { return x * y; }); } -> SURVIVED 30 | mocha/script2.js:12:1 Removed exports.mul = mul; -> SURVIVED 31 | mocha/script2.js:5:5 Removed array = array; -> SURVIVED 32 | mocha/script2.js:7:5 Removed return array.reduce(function (x, y) { return x * y; }); -> SURVIVED 33 | mocha/script2.js:6:9 Replaced array with "MUTATION!" -> SURVIVED 34 | mocha/script2.js:7:25 Replaced function (x, y) { return x * y; } with "MUTATION!" -> SURVIVED 35 | mocha/script2.js:7:12 Replaced array.reduce(function (x, y) { return x * y; }) with function (x, y) { return x * y; } -> SURVIVED 36 | mocha/script2.js:7:12 Replaced array.reduce(function (x, y) { return x * y; }) with array -> SURVIVED 37 | mocha/script2.js:8:9 Removed return x * y; -> SURVIVED 38 | mocha/script2.js:8:17 Replaced * with / -> SURVIVED 39 | 0 of 38 unignored mutations are tested (0%). 2 mutations were ignored. 40 | -------------------------------------------------------------------------------- /test/expected/html-fragments.txt: -------------------------------------------------------------------------------- 1 | mocha/html-fragments.js:3:5 Removed htmlPartial += 'some content
    '; -> SURVIVED 2 | mocha/html-fragments.js:4:5 Removed htmlPartial += '

    '; -> SURVIVED 3 | mocha/html-fragments.js:6:5 Removed console.log('
    ' + htmlPartial + '
    '); -> SURVIVED 4 | mocha/html-fragments.js:7:5 Removed console.log('[CDATA['); -> SURVIVED 5 | mocha/html-fragments.js:8:5 Removed console.log('
    bla
    ]]'); -> SURVIVED 6 | mocha/html-fragments.js:3:20 Replaced 'some content
    ' with "MUTATION!" -> SURVIVED 7 | mocha/html-fragments.js:4:20 Replaced '

    ' with "MUTATION!" -> SURVIVED 8 | mocha/html-fragments.js:6:17 Replaced '
    ' + htmlPartial + '
    ' with "MUTATION!" -> SURVIVED 9 | mocha/html-fragments.js:6:5 Replaced console.log('
    ' + htmlPartial + '
    ') with '
    ' + htmlPartial + '
    ' -> SURVIVED 10 | mocha/html-fragments.js:6:5 Replaced console.log('
    ' + htmlPartial + '
    ') with console -> SURVIVED 11 | mocha/html-fragments.js:6:38 Replaced + with - -> SURVIVED 12 | mocha/html-fragments.js:6:24 Replaced + with - -> SURVIVED 13 | mocha/html-fragments.js:6:17 Replaced '
    ' with "MUTATION!" -> SURVIVED 14 | mocha/html-fragments.js:6:41 Replaced '
    ' with "MUTATION!" -> SURVIVED 15 | mocha/html-fragments.js:7:17 Replaced '[CDATA[' with "MUTATION!" -> SURVIVED 16 | mocha/html-fragments.js:7:5 Replaced console.log('[CDATA[
    bla
    ') with '[CDATA[
    bla
    ' -> SURVIVED 17 | mocha/html-fragments.js:7:5 Replaced console.log('[CDATA[
    bla
    ') with console -> SURVIVED 18 | mocha/html-fragments.js:8:17 Replaced '
    bla
    ]]' with "MUTATION!" -> SURVIVED 19 | mocha/html-fragments.js:8:5 Replaced console.log(']]') with ']]' -> SURVIVED 20 | mocha/html-fragments.js:8:5 Replaced console.log(']]') with console -> SURVIVED 21 | 5 of 25 unignored mutations are tested (20%). 22 | -------------------------------------------------------------------------------- /test/expected/ignore.txt: -------------------------------------------------------------------------------- 1 | mocha/script1.js:34:1 Removed exports.add = add; -> SURVIVED 2 | mocha/script1.js:35:1 Removed exports.sub = sub; -> SURVIVED 3 | mocha/script1.js:38:1 Removed exports.mul = mul; -> SURVIVED 4 | mocha/script1.js:40:1 Removed console.log = function() { // Mock console log to prevent output from leaking to mutation test console }; -> SURVIVED 5 | mocha/script1.js:2:5 Removed var sum = 0; -> SURVIVED 6 | mocha/script1.js:3:5 Removed for (var i = 0; i < array.length; i = i + 1) { sum += array[i]; } -> SURVIVED 7 | mocha/script1.js:6:5 Removed return sum; -> SURVIVED 8 | mocha/script1.js:2:15 Replaced 0 with 1 -> SURVIVED 9 | mocha/script1.js:3:18 Replaced 0 with 1 -> SURVIVED 10 | mocha/script1.js:4:9 Removed sum += array[i]; -> SURVIVED 11 | mocha/script1.js:10:5 Removed var x = array[0]; -> SURVIVED 12 | mocha/script1.js:11:5 Removed var y = array[1]; -> SURVIVED 13 | mocha/script1.js:12:5 Removed var sum = x - y; -> SURVIVED 14 | mocha/script1.js:13:5 Removed sum = sum + 0; -> SURVIVED 15 | mocha/script1.js:15:5 Removed return sum; -> SURVIVED 16 | mocha/script1.js:10:19 Replaced 0 with 1 -> SURVIVED 17 | mocha/script1.js:11:19 Replaced 1 with 2 -> SURVIVED 18 | mocha/script1.js:12:16 Replaced - with + -> SURVIVED 19 | mocha/script1.js:13:14 Replaced + with - -> SURVIVED 20 | mocha/script1.js:13:17 Replaced 0 with 1 -> SURVIVED 21 | mocha/script1.js:14:17 Replaced sum with "MUTATION!" -> SURVIVED 22 | 0 of 21 unignored mutations are tested (0%). 6 mutations were ignored. 23 | -------------------------------------------------------------------------------- /test/expected/karma.txt: -------------------------------------------------------------------------------- 1 | karma-mocha/script-mathoperators.js:3:17 Replaced + with - -> SURVIVED 2 | karma-mocha/script-mathoperators.js:7:17 Replaced - with + -> SURVIVED 3 | karma-mocha/script-mathoperators.js:11:17 Replaced * with / -> SURVIVED 4 | karma-mocha/script-mathoperators.js:16:21 Replaced / with * -> SURVIVED 5 | karma-mocha/script-mathoperators.js:22:17 Replaced % with * -> SURVIVED 6 | karma-mocha/script-mathoperators.js:27:22 Replaced 0 with 1 -> SURVIVED 7 | karma-mocha/script-update-expressions.js:3:19 Replaced < with <= -> SURVIVED 8 | karma-mocha/script-update-expressions.js:3:16 Replaced ++ with -- -> SURVIVED 9 | karma-mocha/script-update-expressions.js:3:22 Replaced 10 with 11 -> SURVIVED 10 | karma-mocha/script-update-expressions.js:7:19 Replaced > with >= -> SURVIVED 11 | karma-mocha/script-update-expressions.js:7:16 Replaced -- with ++ -> SURVIVED 12 | karma-mocha/script-update-expressions.js:7:22 Replaced 10 with 11 -> SURVIVED 13 | karma-mocha/script-update-expressions.js:11:19 Replaced < with <= -> SURVIVED 14 | karma-mocha/script-update-expressions.js:11:17 Replaced ++ with -- -> SURVIVED 15 | karma-mocha/script-update-expressions.js:11:22 Replaced 10 with 11 -> SURVIVED 16 | karma-mocha/script-update-expressions.js:15:19 Replaced > with >= -> SURVIVED 17 | karma-mocha/script-update-expressions.js:15:17 Replaced -- with ++ -> SURVIVED 18 | karma-mocha/script-update-expressions.js:15:22 Replaced 10 with 11 -> SURVIVED 19 | karma-mocha/script1.js:14:9 Removed sum = sum + 0; -> SURVIVED 20 | karma-mocha/script1.js:15:9 Removed console.log(sum); -> SURVIVED 21 | karma-mocha/script1.js:14:18 Replaced + with - -> SURVIVED 22 | karma-mocha/script1.js:15:21 Replaced sum with "MUTATION!" -> SURVIVED 23 | karma-mocha/script1.js:15:9 Replaced console.log(sum) with sum -> SURVIVED 24 | 75 of 98 unignored mutations are tested (76%). 1 mutations were ignored. 25 | -------------------------------------------------------------------------------- /test/expected/literals.txt: -------------------------------------------------------------------------------- 1 | mocha/literals.js:2:12 Replaced 'string' with "MUTATION!" -> SURVIVED 2 | mocha/literals.js:6:12 Replaced 42 with 43 -> SURVIVED 3 | mocha/literals.js:10:12 Replaced true with false -> SURVIVED 4 | 9 of 12 unignored mutations are tested (75%). 5 | -------------------------------------------------------------------------------- /test/expected/logical-expressions.json: -------------------------------------------------------------------------------- 1 | { 2 | "mocha": { 3 | "logicalExpression.js": { 4 | "stats": { 5 | "all": 8, 6 | "killed": 6, 7 | "survived": 2, 8 | "ignored": 0, 9 | "untested": 0 10 | }, 11 | "src": "function getBooleanAnd() {\n /* @excludeMutations ['LITERAL'] */\n return true && false;\n}\n\nfunction getBooleanOr() {\n /* @excludeMutations ['LITERAL'] */\n return true || false;\n}\n\nexports.getBooleanAnd = getBooleanAnd;\nexports.getBooleanOr = getBooleanOr;\n", 12 | "fileName": "mocha/logicalExpression.js", 13 | "mutationResults": [ 14 | { 15 | "mutation": { 16 | "range": [ 17 | 0, 18 | 94 19 | ], 20 | "begin": 0, 21 | "end": 94, 22 | "line": 1, 23 | "col": 0, 24 | "parentMutationId": "433", 25 | "mutationId": "434", 26 | "replacement": "" 27 | }, 28 | "survived": false, 29 | "message": "mocha/logicalExpression.js:1:1 Removed function getBooleanAnd() { /* @excludeMutations ['LITERAL'] */ return true && false; } -> KILLED" 30 | }, 31 | { 32 | "mutation": { 33 | "range": [ 34 | 96, 35 | 189 36 | ], 37 | "begin": 96, 38 | "end": 189, 39 | "line": 6, 40 | "col": 0, 41 | "parentMutationId": "433", 42 | "mutationId": "435", 43 | "replacement": "" 44 | }, 45 | "survived": false, 46 | "message": "mocha/logicalExpression.js:6:1 Removed function getBooleanOr() { /* @excludeMutations ['LITERAL'] */ return true || false; } -> KILLED" 47 | }, 48 | { 49 | "mutation": { 50 | "range": [ 51 | 191, 52 | 229 53 | ], 54 | "begin": 191, 55 | "end": 229, 56 | "line": 11, 57 | "col": 0, 58 | "parentMutationId": "433", 59 | "mutationId": "436", 60 | "replacement": "" 61 | }, 62 | "survived": false, 63 | "message": "mocha/logicalExpression.js:11:1 Removed exports.getBooleanAnd = getBooleanAnd; -> KILLED" 64 | }, 65 | { 66 | "mutation": { 67 | "range": [ 68 | 230, 69 | 266 70 | ], 71 | "begin": 230, 72 | "end": 266, 73 | "line": 12, 74 | "col": 0, 75 | "parentMutationId": "433", 76 | "mutationId": "437", 77 | "replacement": "" 78 | }, 79 | "survived": false, 80 | "message": "mocha/logicalExpression.js:12:1 Removed exports.getBooleanOr = getBooleanOr; -> KILLED" 81 | }, 82 | { 83 | "mutation": { 84 | "range": [ 85 | 71, 86 | 92 87 | ], 88 | "begin": 71, 89 | "end": 92, 90 | "line": 3, 91 | "col": 4, 92 | "parentMutationId": "434", 93 | "mutationId": "438", 94 | "replacement": "" 95 | }, 96 | "survived": false, 97 | "message": "mocha/logicalExpression.js:3:5 Removed return true && false; -> KILLED" 98 | }, 99 | { 100 | "mutation": { 101 | "range": [ 102 | 78, 103 | 91 104 | ], 105 | "begin": 82, 106 | "end": 86, 107 | "line": 3, 108 | "col": 15, 109 | "mutationId": "439", 110 | "parentMutationId": "438", 111 | "replacement": "||" 112 | }, 113 | "survived": true, 114 | "message": "mocha/logicalExpression.js:3:16 Replaced && with || -> SURVIVED" 115 | }, 116 | { 117 | "mutation": { 118 | "range": [ 119 | 166, 120 | 187 121 | ], 122 | "begin": 166, 123 | "end": 187, 124 | "line": 8, 125 | "col": 4, 126 | "parentMutationId": "435", 127 | "mutationId": "440", 128 | "replacement": "" 129 | }, 130 | "survived": false, 131 | "message": "mocha/logicalExpression.js:8:5 Removed return true || false; -> KILLED" 132 | }, 133 | { 134 | "mutation": { 135 | "range": [ 136 | 173, 137 | 186 138 | ], 139 | "begin": 177, 140 | "end": 181, 141 | "line": 8, 142 | "col": 15, 143 | "mutationId": "441", 144 | "parentMutationId": "440", 145 | "replacement": "&&" 146 | }, 147 | "survived": true, 148 | "message": "mocha/logicalExpression.js:8:16 Replaced || with && -> SURVIVED" 149 | } 150 | ], 151 | "mutationScore": { 152 | "total": 0.75, 153 | "killed": 0.75, 154 | "survived": 0.25, 155 | "ignored": 0, 156 | "untested": 0 157 | } 158 | }, 159 | "stats": { 160 | "all": 8, 161 | "killed": 6, 162 | "survived": 2, 163 | "ignored": 0, 164 | "untested": 0 165 | }, 166 | "mutationScore": { 167 | "total": 0.75, 168 | "killed": 0.75, 169 | "survived": 0.25, 170 | "ignored": 0, 171 | "untested": 0 172 | } 173 | }, 174 | "stats": { 175 | "all": 8, 176 | "killed": 6, 177 | "survived": 2, 178 | "ignored": 0, 179 | "untested": 0 180 | }, 181 | "mutationScore": { 182 | "total": 0.75, 183 | "killed": 0.75, 184 | "survived": 0.25, 185 | "ignored": 0, 186 | "untested": 0 187 | } 188 | } -------------------------------------------------------------------------------- /test/expected/logical-expressions.txt: -------------------------------------------------------------------------------- 1 | mocha/logicalExpression.js:3:16 Replaced && with || -> SURVIVED 2 | mocha/logicalExpression.js:8:16 Replaced || with && -> SURVIVED 3 | 6 of 8 unignored mutations are tested (75%). 4 | -------------------------------------------------------------------------------- /test/expected/math-operators.txt: -------------------------------------------------------------------------------- 1 | mocha/mathoperators.js:3:21 Replaced + with - -> SURVIVED 2 | mocha/mathoperators.js:3:17 Replaced + with - -> SURVIVED 3 | mocha/mathoperators.js:7:17 Replaced - with + -> SURVIVED 4 | mocha/mathoperators.js:7:22 Replaced + with - -> SURVIVED 5 | mocha/mathoperators.js:11:17 Replaced * with / -> SURVIVED 6 | mocha/mathoperators.js:16:21 Replaced / with * -> SURVIVED 7 | mocha/mathoperators.js:23:17 Replaced % with * -> SURVIVED 8 | 19 of 26 unignored mutations are tested (73%). 9 | -------------------------------------------------------------------------------- /test/expected/mocha.txt: -------------------------------------------------------------------------------- 1 | mocha/script1.js:40:1 Removed console.log = function() { // Mock console log to prevent output from leaking to mutation test console }; -> SURVIVED 2 | mocha/script1.js:13:5 Removed sum = sum + 0; -> SURVIVED 3 | mocha/script1.js:14:5 Removed console.log(sum); -> SURVIVED 4 | mocha/script1.js:13:14 Replaced + with - -> SURVIVED 5 | mocha/script1.js:14:17 Replaced sum with "MUTATION!" -> SURVIVED 6 | mocha/script1.js:14:5 Replaced console.log(sum) with sum -> SURVIVED 7 | mocha/script2.js:5:5 Removed array = array; -> SURVIVED 8 | mocha/script2.js:6:9 Replaced array with "MUTATION!" -> SURVIVED 9 | 28 of 36 unignored mutations are tested (77%). 4 mutations were ignored. 10 | -------------------------------------------------------------------------------- /test/expected/test-is-failing-without-mutation.json: -------------------------------------------------------------------------------- 1 | { 2 | "mocha": { 3 | "script1.js": { 4 | "stats": { 5 | "all": 0, 6 | "killed": 0, 7 | "survived": 0, 8 | "ignored": 0, 9 | "untested": 0 10 | }, 11 | "src": "function add(array) {\n var sum = 0;\n for (var i = 0; i < array.length; i = i + 1) {\n sum += array[i];\n }\n return sum;\n}\n\nfunction sub(array) {\n var x = array[0];\n var y = array[1];\n var sum = x - y;\n sum = sum + 0;\n console.log(sum);\n return sum;\n}\n\n/**\n * multiplies some stuff\n * @excludeMutations\n * @param array\n * @returns {number}\n */\nfunction mul(array) {\n var x = array[0];\n var y = array[1];\n var sum = x * y;\n if (sum > 9){\n console.log(sum);\n }\n return sum;\n}\n\nexports.add = add;\nexports.sub = sub;\n\n//@excludeMutations\nexports.mul = mul;\n\nconsole.log = function() {\n // Mock console log to prevent output from leaking to mutation test console\n};\n", 12 | "fileName": "mocha/script1.js", 13 | "mutationResults": [], 14 | "mutationScore": { 15 | "total": null, 16 | "killed": null, 17 | "survived": null, 18 | "ignored": null, 19 | "untested": null 20 | } 21 | }, 22 | "script2.js": { 23 | "stats": { 24 | "all": 0, 25 | "killed": 0, 26 | "survived": 0, 27 | "ignored": 0, 28 | "untested": 0 29 | }, 30 | "src": "function log() {\n}\n\nfunction mul(array) {\n array = array;\n log(array);\n return array.reduce(function (x, y) {\n return x * y;\n });\n}\n\nexports.mul = mul;\n", 31 | "fileName": "mocha/script2.js", 32 | "mutationResults": [], 33 | "mutationScore": { 34 | "total": null, 35 | "killed": null, 36 | "survived": null, 37 | "ignored": null, 38 | "untested": null 39 | } 40 | }, 41 | "stats": { 42 | "all": 0, 43 | "killed": 0, 44 | "survived": 0, 45 | "ignored": 0, 46 | "untested": 0 47 | }, 48 | "mutationScore": { 49 | "total": null, 50 | "killed": null, 51 | "survived": null, 52 | "ignored": null, 53 | "untested": null 54 | } 55 | }, 56 | "stats": { 57 | "all": 0, 58 | "killed": 0, 59 | "survived": 0, 60 | "ignored": 0, 61 | "untested": 0 62 | }, 63 | "mutationScore": { 64 | "total": null, 65 | "killed": null, 66 | "survived": null, 67 | "ignored": null, 68 | "untested": null 69 | } 70 | } -------------------------------------------------------------------------------- /test/expected/test-is-failing-without-mutation.txt: -------------------------------------------------------------------------------- 1 | mocha/script1.js tests fail without mutations 2 | mocha/script2.js tests fail without mutations 3 | -------------------------------------------------------------------------------- /test/expected/unary-expressions.txt: -------------------------------------------------------------------------------- 1 | mocha/unaryExpression.js:1:1 Removed 'use strict'; -> SURVIVED 2 | mocha/unaryExpression.js:1:1 Replaced 'use strict' with "MUTATION!" -> SURVIVED 3 | mocha/unaryExpression.js:3:18 Removed - -> SURVIVED 4 | mocha/unaryExpression.js:7:15 Removed - -> SURVIVED 5 | mocha/unaryExpression.js:11:15 Removed ~ -> SURVIVED 6 | mocha/unaryExpression.js:15:16 Removed ! -> SURVIVED 7 | mocha/unaryExpression.js:19:17 Removed - -> SURVIVED 8 | mocha/unaryExpression.js:23:17 Removed ! -> SURVIVED 9 | 18 of 26 unignored mutations are tested (69%). 10 | -------------------------------------------------------------------------------- /test/expected/update-expressions.txt: -------------------------------------------------------------------------------- 1 | mocha/update-expressions.js:3:12 Replaced ++ with -- -> SURVIVED 2 | mocha/update-expressions.js:8:12 Replaced -- with ++ -> SURVIVED 3 | mocha/update-expressions.js:13:13 Replaced ++ with -- -> SURVIVED 4 | mocha/update-expressions.js:20:13 Replaced -- with ++ -> SURVIVED 5 | 8 of 12 unignored mutations are tested (66%). 6 | -------------------------------------------------------------------------------- /test/fixtures/karma-mocha/karma-endlessLoop-test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by martin on 25/03/15. 3 | */ 4 | describe('Endless Loop', function () { 5 | 6 | it('tests to see whether mutations cause code to loop indefinitely', function () { 7 | provokeEndlessLoop({input: '102365770045232'}); 8 | }); 9 | 10 | }); 11 | -------------------------------------------------------------------------------- /test/fixtures/karma-mocha/karma-mathoperators-test.js: -------------------------------------------------------------------------------- 1 | describe('Math operators', function () { 2 | it('add', function () { 3 | expect(addOperator(4, 0)).to.equal(4); 4 | }); 5 | 6 | it('subtract', function () { 7 | expect(subtractOperator(4, 0)).to.equal(4); 8 | }); 9 | 10 | it('multiply', function () { 11 | expect(multiplyOperator(4, 1)).to.equal(4); 12 | }); 13 | 14 | it('divide', function () { 15 | expect(divideOperator(4, 1)).to.equal(4); 16 | }); 17 | 18 | it('divide - handle dividing by 0 situation', function () { 19 | expect(divideOperator(4, 0)).to.equal(0); 20 | }); 21 | 22 | it('modulus', function () { 23 | expect(modulusOperator(0, 1)).to.equal(0); 24 | }); 25 | 26 | it('modulus', function () { 27 | expect(JSON.stringify(asserts = looping([0, 1, 2, 3]))).to.equal('[0,1,3,5]'); 28 | }); 29 | 30 | }); 31 | -------------------------------------------------------------------------------- /test/fixtures/karma-mocha/karma-test.js: -------------------------------------------------------------------------------- 1 | describe('Fixtures', function () { 2 | describe('Script 1', function () { 3 | it('add', function () { 4 | var actual = add([1, 2]); 5 | var expected = 3; 6 | assert.equal(actual, expected); 7 | }); 8 | it('sub', function () { 9 | var actual = sub([2, 1]); 10 | var expected = 1; 11 | assert.equal(actual, expected); 12 | }); 13 | }); 14 | 15 | describe('Script 2', function () { 16 | it('mul', function () { 17 | assert.equal(6, mul([2, 3])); 18 | //test.equal(6, 6); 19 | }); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /test/fixtures/karma-mocha/karma-update-expressions-test.js: -------------------------------------------------------------------------------- 1 | describe('Update expressions', function () { 2 | it('increment A', function () { 3 | var x = 1; 4 | expect(incrementA(x)).to.equal(true); 5 | }); 6 | 7 | it('decrement A', function () { 8 | var x = 1; 9 | expect(decrementA(x)).to.equal(false); 10 | }); 11 | 12 | it('increment B', function () { 13 | var x = 100; 14 | expect(incrementB(x)).to.equal(false); 15 | }); 16 | 17 | it('decrement B', function () { 18 | var x = 100; 19 | expect(decrementB(x)).to.equal(true); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /test/fixtures/karma-mocha/karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration 2 | // Generated on Wed Apr 30 2014 16:43:43 GMT+0200 (CEST) 3 | 4 | module.exports = function (config) { 5 | config.set({ 6 | 7 | // base path that will be used to resolve all patterns (eg. files, exclude) 8 | basePath: '.', 9 | 10 | // frameworks to use 11 | // available frameworks: https://npmjs.org/browse/keyword/karma-adapter 12 | frameworks: ['mocha', 'chai'], 13 | 14 | // list of files / patterns to load in the browser 15 | files: [ 16 | {pattern: 'script*.js'}, 17 | 'karma-test.js', 18 | 'karma-update-expressions-test.js', 19 | 'karma-endlessLoop-test.js', 20 | 'karma-mathoperators-test.js' 21 | ], 22 | 23 | // list of files to exclude 24 | exclude: [ 25 | //'app/scripts/sidebar.js' 26 | ], 27 | 28 | 29 | // preprocess matching files before serving them to the browser 30 | // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor 31 | preprocessors: {}, 32 | 33 | 34 | // test results reporter to use 35 | // possible values: 'dots', 'progress' 36 | // available reporters: https://npmjs.org/browse/keyword/karma-reporter 37 | reporters: ['progress'], 38 | 39 | // web server port 40 | port: 9876, 41 | 42 | captureTimeout: 60000, 43 | 44 | // enable / disable colors in the output (reporters and logs) 45 | colors: true, 46 | 47 | // level of logging 48 | // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG 49 | logLevel: config.LOG_INFO, 50 | 51 | // enable / disable watching file and executing tests whenever any file changes 52 | autoWatch: true, 53 | 54 | // start these browsers 55 | // available browser launchers: https://npmjs.org/browse/keyword/karma-launcher 56 | browsers: ['PhantomJS'], 57 | 58 | // Continuous Integration mode 59 | // if true, Karma captures browsers, runs the tests and exits 60 | singleRun: false, 61 | 62 | mutationTest: {excludeMutations: { 63 | 'INCREMENT': false, 64 | 'MATH': false 65 | }} 66 | }); 67 | }; 68 | -------------------------------------------------------------------------------- /test/fixtures/karma-mocha/mutation-testing-file-specs.json: -------------------------------------------------------------------------------- 1 | { 2 | "karma-mocha/script-mathoperators.js": ["karma-mocha/karma-mathoperators-test.js"], 3 | "karma-mocha/script-update-expressions.js": ["karma-mocha/karma-update-expressions-test.js"], 4 | "karma-mocha/script1.js": ["karma-mocha/karma-test.js"], 5 | "karma-mocha/script2.js": ["karma-mocha/karma-test.js"] 6 | } 7 | -------------------------------------------------------------------------------- /test/fixtures/karma-mocha/script-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 | })(this); 28 | -------------------------------------------------------------------------------- /test/fixtures/karma-mocha/script-mathoperators.js: -------------------------------------------------------------------------------- 1 | (function (exports) { 2 | exports.addOperator = function (a, b) { 3 | return a + b; 4 | }; 5 | 6 | exports.subtractOperator = function (a, b) { 7 | return a - 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 | exports.modulusOperator = function (a, b) { 22 | return a % b; 23 | }; 24 | 25 | exports.looping = function (array) { 26 | var prev = 0, _new; 27 | for (var i = 0; i < array.length; i = i + 1) { // purposefully incrementing like this to check that the arithmetic mutation doesn't cause an infinite loop 28 | _new = prev + array[i]; 29 | prev = array[i]; 30 | array[i] = _new; 31 | } 32 | return array; 33 | }; 34 | })(this); 35 | -------------------------------------------------------------------------------- /test/fixtures/karma-mocha/script-update-expressions.js: -------------------------------------------------------------------------------- 1 | (function (exports) { 2 | exports.incrementA = function (x) { 3 | return ++x < 10; 4 | }; 5 | 6 | exports.decrementA = function (x) { 7 | return --x > 10; 8 | }; 9 | 10 | exports.incrementB = function (x) { 11 | return x++ < 10; 12 | }; 13 | 14 | exports.decrementB = function (x) { 15 | return x-- > 10; 16 | }; 17 | })(this); 18 | -------------------------------------------------------------------------------- /test/fixtures/karma-mocha/script1.js: -------------------------------------------------------------------------------- 1 | (function (exports) { 2 | function add(array) { 3 | var sum = 0; 4 | for (var i = 0; i < array.length; i++) { 5 | sum += array[i]; 6 | } 7 | return sum; 8 | } 9 | 10 | function sub(array) { 11 | var x = array[0]; 12 | var y = array[1]; 13 | var sum = x - y; 14 | sum = sum + 0; 15 | console.log(sum); 16 | return sum; 17 | } 18 | 19 | exports.add = add; 20 | exports.sub = sub; 21 | })(this); 22 | 23 | 24 | -------------------------------------------------------------------------------- /test/fixtures/karma-mocha/script2.js: -------------------------------------------------------------------------------- 1 | (function (exports) { 2 | function mul(array) { 3 | return array.reduce(function (x, y) { 4 | return x * y; 5 | }); 6 | } 7 | 8 | exports.mul = mul; 9 | })(this); 10 | 11 | -------------------------------------------------------------------------------- /test/fixtures/mocha/arguments-test.js: -------------------------------------------------------------------------------- 1 | var assert = require("chai").assert; 2 | var args = require("./arguments"); 3 | 4 | describe('Arguments', function () { 5 | it('containsName', function () { 6 | var result = args.containsName([ 7 | {name: 'Nora'}, 8 | {name: 'Marco'} 9 | ], 'Stefe'); 10 | assert.equal(result, false); 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /test/fixtures/mocha/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/fixtures/mocha/array-test.js: -------------------------------------------------------------------------------- 1 | var assert = require("chai").assert; 2 | var arrayModule = require("./array"); 3 | 4 | describe('Arrays', function () { 5 | it('createArray', function () { 6 | var array = arrayModule.createArray(); 7 | assert.isArray(array); 8 | // assert.deepEqual(array, [ 9 | // {}, 10 | // 'string', 11 | // 123 12 | // ]); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /test/fixtures/mocha/array.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | /** @excludeMutations ['LITERAL'] */ 3 | function createArray() { 4 | var el = {}; 5 | return [el, 'string', 123]; 6 | } 7 | 8 | exports.createArray = createArray; 9 | -------------------------------------------------------------------------------- /test/fixtures/mocha/attribute-test.js: -------------------------------------------------------------------------------- 1 | var assert = require("chai").assert; 2 | var attribute = require("./attribute"); 3 | 4 | describe('Attributes', function () { 5 | it('createPerson', function () { 6 | var person = attribute.createPerson('Marco', 36); 7 | assert.isObject(person); 8 | // assert.equal('Marco', person.name); 9 | // assert.equal(36, person.age); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /test/fixtures/mocha/attribute.js: -------------------------------------------------------------------------------- 1 | function createPerson(name, age) { 2 | return { 3 | name: name, 4 | age: age 5 | } 6 | } 7 | 8 | exports.createPerson = createPerson; 9 | -------------------------------------------------------------------------------- /test/fixtures/mocha/comparisons-test.js: -------------------------------------------------------------------------------- 1 | var expect = require("chai").expect; 2 | var comparisons = require("./comparisons"); 3 | describe('Comparisons', function () { 4 | it('lessThan', function () { 5 | comparisons.lessThan(1, 2) 6 | }); 7 | it('notGreaterThan', function () { 8 | expect(comparisons.notGreaterThan(1, 2)).to.not.equal(undefined); 9 | }); 10 | it('greaterThan', function () { 11 | expect(comparisons.greaterThan(3, 2)).to.not.equal(undefined); 12 | }); 13 | it('notLessThan', function () { 14 | expect(comparisons.notLessThan(3, 2)).to.not.equal(undefined); 15 | }); 16 | it('equalsStrict', function () { 17 | expect(comparisons.equalsStrict(3, 3)).to.not.equal(undefined); 18 | }); 19 | it('equalsLenient', function () { 20 | expect(comparisons.equalsLenient(3, 3)).to.not.equal(undefined); 21 | }); 22 | it('unequalsStrict', function () { 23 | expect(comparisons.unequalsStrict(3, 4)).to.not.equal(undefined); 24 | }); 25 | it('unequalsLenient', function () { 26 | expect(comparisons.unequalsLenient(3, 4)).to.not.equal(undefined); 27 | }); 28 | }); 29 | -------------------------------------------------------------------------------- /test/fixtures/mocha/comparisons.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // @excludeMutations ['BLOCK_STATEMENT'] 4 | exports.lessThan = function (left, right) { 5 | return left < right; 6 | }; 7 | 8 | exports.notGreaterThan = function (left, right) { 9 | return left <= right; 10 | }; 11 | 12 | exports.greaterThan = function (left, right) { 13 | return left > right; 14 | }; 15 | 16 | exports.notLessThan = function (left, right) { 17 | return left >= right; 18 | }; 19 | 20 | exports.equalsStrict = function (left, right) { 21 | return left === right; 22 | }; 23 | 24 | exports.equalsLenient = function (left, right) { 25 | return left == right; 26 | }; 27 | 28 | exports.unequalsStrict = function (left, right) { 29 | return left !== right; 30 | }; 31 | 32 | exports.unequalsLenient = function (left, right) { 33 | return left != right; 34 | }; 35 | -------------------------------------------------------------------------------- /test/fixtures/mocha/function-calls-test.js: -------------------------------------------------------------------------------- 1 | var assert = require("chai").assert; 2 | var functionCalls = require("./function-calls"); 3 | 4 | describe('Function Calls', function () { 5 | 6 | it('encodeUrl', function () { 7 | var result = functionCalls.encodeUrl('abc'); 8 | assert.equal(result, 'abc'); 9 | }); 10 | 11 | it('trim', function () { 12 | var result = functionCalls.trim('abc'); 13 | assert.equal(result, 'abc'); 14 | }); 15 | 16 | }); 17 | -------------------------------------------------------------------------------- /test/fixtures/mocha/function-calls.js: -------------------------------------------------------------------------------- 1 | function encodeUrl(url) { 2 | return encodeURI(url); 3 | } 4 | 5 | function trim(string) { 6 | return string.trim(); 7 | } 8 | 9 | exports.encodeUrl = encodeUrl; 10 | exports.trim = trim; 11 | -------------------------------------------------------------------------------- /test/fixtures/mocha/html-fragments-test.js: -------------------------------------------------------------------------------- 1 | var assert = require("chai").assert; 2 | var expect = require("chai").expect; 3 | var scriptWithHtml = require("./html-fragments"); 4 | 5 | describe('Script with HTML', function () { 6 | it('prints HTML', function () { 7 | expect(scriptWithHtml.printHtml()).to.contain('

    '); 8 | }); 9 | }); 10 | -------------------------------------------------------------------------------- /test/fixtures/mocha/html-fragments.js: -------------------------------------------------------------------------------- 1 | function printHtml() { 2 | var htmlPartial = '

    '; 3 | htmlPartial += 'some content
    '; 4 | htmlPartial += '

    '; 5 | 6 | console.log('
    ' + htmlPartial + '
    '); 7 | console.log('[CDATA['); 8 | console.log('
    bla
    ]]'); 9 | 10 | return htmlPartial; 11 | } 12 | 13 | exports.printHtml = printHtml; -------------------------------------------------------------------------------- /test/fixtures/mocha/literals-test.js: -------------------------------------------------------------------------------- 1 | var assert = require("chai").assert; 2 | var literals = require("./literals"); 3 | 4 | describe('Literals', function () { 5 | it('getString', function () { 6 | assert.isString(literals.getString()); 7 | }); 8 | it('getNumber', function () { 9 | assert.isNumber(literals.getNumber()); 10 | }); 11 | it('getString', function () { 12 | assert.isBoolean(literals.getBoolean()); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /test/fixtures/mocha/literals.js: -------------------------------------------------------------------------------- 1 | function getString() { 2 | return 'string'; 3 | } 4 | 5 | function getNumber() { 6 | return 42; 7 | } 8 | 9 | function getBoolean() { 10 | return true; 11 | } 12 | 13 | exports.getString = getString; 14 | exports.getNumber = getNumber; 15 | exports.getBoolean = getBoolean; 16 | -------------------------------------------------------------------------------- /test/fixtures/mocha/logicalExpression-test.js: -------------------------------------------------------------------------------- 1 | 2 | var assert = require("chai").assert; 3 | var expect = require("chai").expect; 4 | var logicalExpression = require("./logicalExpression"); 5 | 6 | describe('Logical Expression', function () { 7 | it('getBooleanAnd', function () { 8 | assert.isBoolean(logicalExpression.getBooleanAnd()); 9 | }); 10 | it('getBooleanOr', function () { 11 | assert.isBoolean(logicalExpression.getBooleanOr()); 12 | }); 13 | }); 14 | -------------------------------------------------------------------------------- /test/fixtures/mocha/logicalExpression.js: -------------------------------------------------------------------------------- 1 | function getBooleanAnd() { 2 | /* @excludeMutations ['LITERAL'] */ 3 | return true && false; 4 | } 5 | 6 | function getBooleanOr() { 7 | /* @excludeMutations ['LITERAL'] */ 8 | return true || false; 9 | } 10 | 11 | exports.getBooleanAnd = getBooleanAnd; 12 | exports.getBooleanOr = getBooleanOr; 13 | -------------------------------------------------------------------------------- /test/fixtures/mocha/mathoperators-test.js: -------------------------------------------------------------------------------- 1 | var expect = require("chai").expect; 2 | var mathoperators = require("./mathoperators"); 3 | describe('Math operators', function () { 4 | it('add', function () { 5 | expect(mathoperators.addOperator(4, 0)).to.equal(4); 6 | }); 7 | 8 | it('subtract', function () { 9 | expect(mathoperators.subtractOperator(4, 0)).to.equal(4); 10 | }); 11 | 12 | it('multiply', function () { 13 | expect(mathoperators.multiplyOperator(4, 1)).to.equal(4); 14 | }); 15 | 16 | it('divide', function () { 17 | expect(mathoperators.divideOperator(4, 1)).to.equal(4); 18 | }); 19 | 20 | it('divide - handle dividing by 0 situation', function () { 21 | expect(mathoperators.divideOperator(4, 0)).to.equal(0); 22 | }); 23 | 24 | it('modulus', function () { 25 | expect(mathoperators.modulusOperator(0, 1)).to.equal(0); 26 | }); 27 | 28 | }); 29 | -------------------------------------------------------------------------------- /test/fixtures/mocha/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 | -------------------------------------------------------------------------------- /test/fixtures/mocha/mocha-test.js: -------------------------------------------------------------------------------- 1 | var assert = require("assert"); 2 | var code1 = require("./script1"); 3 | 4 | describe('Fixtures', function () { 5 | describe('Script 1', function () { 6 | it('add', function () { 7 | var actual = code1.add([1, 2]); 8 | var expected = 3; 9 | assert.equal(actual, expected); 10 | }); 11 | it('sub', function () { 12 | var actual = code1.sub([2, 1]); 13 | var expected = 1; 14 | assert.equal(actual, expected); 15 | }); 16 | it('mul', function () { 17 | var actual = code1.mul([2, 5]); 18 | }); 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /test/fixtures/mocha/mocha-test2.js: -------------------------------------------------------------------------------- 1 | var assert = require("assert"); 2 | var code2 = require("./script2"); 3 | 4 | describe('Fixtures', function () { 5 | describe('Script 2', function () { 6 | it('mul', function () { 7 | assert.equal(6, code2.mul([2, 3])); 8 | //test.equal(6, 6); 9 | }); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /test/fixtures/mocha/mutationCommands/CommandRegistrySpec.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by martin on 20/03/15. 3 | */ 4 | var _ = require('lodash'), 5 | expect = require('chai').expect, 6 | CommandRegistry = require('../../../../mutationCommands/CommandRegistry'); 7 | 8 | describe('Command Registry', function(){ 9 | 10 | describe('Command Codes', function () { 11 | it('returns the command codes', function() { 12 | var defaultExclusions = CommandRegistry.getAllCommandCodes(); 13 | expect(defaultExclusions).to.contain("ARRAY", "BLOCK_STATEMENT", "COMPARISON", "LITERAL", "LOGICAL_EXPRESSION", "MATH", "METHOD_CALL", "OBJECT", "UNARY_EXPRESSION", "UPDATE_EXPRESSION"); 14 | }); 15 | 16 | }); 17 | 18 | describe('Default Excludes', function() { 19 | it('returns the command codes and their default values', function() { 20 | var defaultExclusions = CommandRegistry.getDefaultExcludes(); 21 | expect(defaultExclusions).to.contain({ 22 | "ARRAY": false, 23 | "BLOCK_STATEMENT": false, 24 | "COMPARISON": false, 25 | "LITERAL": false, 26 | "LOGICAL_EXPRESSION": false, 27 | "MATH": false, 28 | "METHOD_CALL": false, 29 | "OBJECT": false, 30 | "UNARY_EXPRESSION": false, 31 | "UPDATE_EXPRESSION": false 32 | }); 33 | }); 34 | }); 35 | 36 | describe('Select Command', function(){ 37 | it('selects a BlockStatementCommand if the node contains an array in its body', function() { 38 | expect(CommandRegistry.selectCommand({body:[]}).name).to.equal('MutateBlockStatementCommand'); 39 | }); 40 | 41 | it('selects a MutateIterationCommand if the node type is a (do) while statement', function() { 42 | expect(CommandRegistry.selectCommand({type:"WhileStatement"}).name).to.equal('MutateIterationCommand'); 43 | expect(CommandRegistry.selectCommand({type:"DoWhileStatement"}).name).to.equal('MutateIterationCommand'); 44 | }); 45 | 46 | it('selects a MutateForLoopCommand if the node type is a while statement', function() { 47 | expect(CommandRegistry.selectCommand({type:"ForStatement"}).name).to.equal('MutateForLoopCommand'); 48 | }); 49 | 50 | it('selects a AssignmentExpressionCommand if the node type is an assignment expression', function() { 51 | expect(CommandRegistry.selectCommand({type:"AssignmentExpression"}).name).to.equal('AssignmentExpressionCommand'); 52 | }); 53 | 54 | it('selects a MutateCallExpressionCommand if the node type is a function call', function() { 55 | expect(CommandRegistry.selectCommand({type:"CallExpression"}).name).to.equal('MutateCallExpressionCommand'); 56 | }); 57 | 58 | it('selects a MutateObjectCommand if the node type is an object', function() { 59 | expect(CommandRegistry.selectCommand({type:"ObjectExpression"}).name).to.equal('MutateObjectCommand'); 60 | }); 61 | 62 | it('selects a MutateArrayExpressionCommand if the node type is an array', function() { 63 | expect(CommandRegistry.selectCommand({type:"ArrayExpression"}).name).to.equal('MutateArrayCommand'); 64 | }); 65 | 66 | it('selects a MutateUpdateExpressionCommand if the node type is an update expression', function() { 67 | expect(CommandRegistry.selectCommand({type:"UpdateExpression"}).name).to.equal('MutateUpdateExpressionCommand'); 68 | }); 69 | 70 | it('selects a MutateLiteralCommand if the node type is a literal', function() { 71 | expect(CommandRegistry.selectCommand({type:"Literal"}).name).to.equal('MutateLiteralCommand'); 72 | }); 73 | 74 | it('selects a UnaryExpressionCommand if the node type is a unary expression', function() { 75 | expect(CommandRegistry.selectCommand({type:"UnaryExpression"}).name).to.equal('UnaryExpressionCommand'); 76 | }); 77 | 78 | it('selects a MutateLogicalExpressionCommand if the node type is a logical expression', function() { 79 | expect(CommandRegistry.selectCommand({type:"LogicalExpression"}).name).to.equal('MutateLogicalExpressionCommand'); 80 | }); 81 | 82 | it('returns null if the given node is not an object', function() { 83 | expect(CommandRegistry.selectCommand("notAnObject")).to.be.null; 84 | }); 85 | }); 86 | }); 87 | 88 | -------------------------------------------------------------------------------- /test/fixtures/mocha/script1.js: -------------------------------------------------------------------------------- 1 | function add(array) { 2 | var sum = 0; 3 | for (var i = 0; i < array.length; i = i + 1) { 4 | sum += array[i]; 5 | } 6 | return sum; 7 | } 8 | 9 | function sub(array) { 10 | var x = array[0]; 11 | var y = array[1]; 12 | var sum = x - y; 13 | sum = sum + 0; 14 | console.log(sum); 15 | return sum; 16 | } 17 | 18 | /** 19 | * multiplies some stuff 20 | * @excludeMutations 21 | * @param array 22 | * @returns {number} 23 | */ 24 | function mul(array) { 25 | var x = array[0]; 26 | var y = array[1]; 27 | var sum = x * y; 28 | if (sum > 9){ 29 | console.log(sum); 30 | } 31 | return sum; 32 | } 33 | 34 | exports.add = add; 35 | exports.sub = sub; 36 | 37 | //@excludeMutations 38 | exports.mul = mul; 39 | 40 | console.log = function() { 41 | // Mock console log to prevent output from leaking to mutation test console 42 | }; 43 | -------------------------------------------------------------------------------- /test/fixtures/mocha/script2.js: -------------------------------------------------------------------------------- 1 | function log() { 2 | } 3 | 4 | function mul(array) { 5 | array = array; 6 | log(array); 7 | return array.reduce(function (x, y) { 8 | return x * y; 9 | }); 10 | } 11 | 12 | exports.mul = mul; 13 | -------------------------------------------------------------------------------- /test/fixtures/mocha/unaryExpression-test.js: -------------------------------------------------------------------------------- 1 | var assert = require("chai").assert; 2 | var expect = require("chai").expect; 3 | var unaryExpression = require("./unaryExpression"); 4 | 5 | describe('Unary Expression', function () { 6 | it('getBinaryExpression', function () { 7 | assert.isNumber(unaryExpression.getBinaryExpression()); 8 | }); 9 | it('getNumber', function () { 10 | assert.isNumber(unaryExpression.getNumber()); 11 | }); 12 | it('getBitwiseNotNumber', function () { 13 | assert.isNumber(unaryExpression.getBitwiseNotNumber()); 14 | }); 15 | it('getNegativeBoolean', function () { 16 | expect(!!unaryExpression.getNegativeBoolean()).to.be.true; 17 | }); 18 | it('getBoolean', function () { 19 | assert.isBoolean(unaryExpression.getBoolean()); 20 | }); 21 | it('getNumberBoolean', function () { 22 | assert.isBoolean(unaryExpression.getNumberBoolean()); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /test/fixtures/mocha/unaryExpression.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | function getBinaryExpression() { 3 | return -(6*7); 4 | } 5 | 6 | function getNumber() { 7 | return -43; 8 | } 9 | 10 | function getBitwiseNotNumber() { 11 | return ~43; 12 | } 13 | 14 | function getNumberBoolean() { 15 | return !!43; 16 | } 17 | 18 | function getNegativeBoolean() { 19 | return -true; 20 | } 21 | 22 | function getBoolean() { 23 | return !true; 24 | } 25 | 26 | exports.getNumber = getNumber; 27 | exports.getBinaryExpression = getBinaryExpression; 28 | exports.getBoolean = getBoolean; 29 | exports.getNegativeBoolean = getNegativeBoolean; 30 | exports.getNumberBoolean = getNumberBoolean; 31 | exports.getBitwiseNotNumber = getBitwiseNotNumber; 32 | -------------------------------------------------------------------------------- /test/fixtures/mocha/update-expressions-test.js: -------------------------------------------------------------------------------- 1 | var expect = require("chai").expect; 2 | var updateExpressions = require("./update-expressions"); 3 | describe('Update expressions', function () { 4 | it('increment A', function () { 5 | var x = 1; 6 | expect(updateExpressions.incrementA(x)).to.equal(true); 7 | }); 8 | 9 | it('decrement A', function () { 10 | var x = 1; 11 | expect(updateExpressions.decrementA(x)).to.equal(false); 12 | }); 13 | 14 | it('increment B', function () { 15 | var x = 100; 16 | expect(updateExpressions.incrementB(x)).to.equal(false); 17 | }); 18 | 19 | it('decrement B', function () { 20 | var x = 100; 21 | expect(updateExpressions.decrementB(x)).to.equal(true); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /test/fixtures/mocha/update-expressions.js: -------------------------------------------------------------------------------- 1 | exports.incrementA = function (x) { 2 | // @excludeMutations ['COMPARISON', 'LITERAL'] 3 | return ++x < 10; 4 | }; 5 | 6 | exports.decrementA = function (x) { 7 | /* @excludeMutations ['COMPARISON', 'LITERAL'] */ 8 | return --x > 10; 9 | }; 10 | 11 | // @excludeMutations ['COMPARISON', 'LITERAL'] 12 | exports.incrementB = function (x) { 13 | return x++ < 10; 14 | }; 15 | 16 | /** 17 | * @excludeMutations ['COMPARISON', 'LITERAL'] 18 | */ 19 | exports.decrementB = function (x) { 20 | return x-- > 10; 21 | }; 22 | -------------------------------------------------------------------------------- /test/fixtures/mocha/utils/ExclusionUtilsSpec.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by Martin Koster on 25/02/15. 3 | */ 4 | var assert = require('chai').assert, 5 | ExclusionUtils = require('../../../../utils/ExclusionUtils'); 6 | 7 | 8 | describe('ExclusionUtils', function(){ 9 | it('select only valid exclusions from the given block comments', function() { 10 | var exclusions = ExclusionUtils.getExclusions({leadingComments:[ 11 | {type: "Block", value: "/**\n * @excludeMutations = ['MATH', 'LITERAL']"}, 12 | {type: "somethingElse", value: "//@excludeMutations = ['APPLE']"} 13 | ]}); 14 | 15 | assert.deepEqual(exclusions, {MATH: true, LITERAL: true}); 16 | }); 17 | 18 | it('select only valid exclusions from the given line comments', function() { 19 | var exclusions = ExclusionUtils.getExclusions({leadingComments:[ 20 | {type: "Block", value: "/**\n * excludeMutations = ['MATH', 'LITERAL']"}, 21 | {type: "somethingElse", value: "//@excludeMutations bladibla ['COMPARISON']"} 22 | ]}); 23 | 24 | assert.deepEqual(exclusions, {COMPARISON: true}); 25 | }); 26 | 27 | it('select all exclusions as no specific exclusions were given', function() { 28 | var exclusions = ExclusionUtils.getExclusions({leadingComments:[ 29 | {type: "Block", value: "/**\n * @excludeMutations "} 30 | ]}); 31 | 32 | assert.deepEqual(exclusions, { 33 | "ARRAY": true, 34 | "BLOCK_STATEMENT": true, 35 | "COMPARISON": true, 36 | "LITERAL": true, 37 | "LOGICAL_EXPRESSION": true, 38 | "MATH": true, 39 | "METHOD_CALL": true, 40 | "OBJECT": true, 41 | "UNARY_EXPRESSION": true, 42 | "UPDATE_EXPRESSION": true 43 | }); 44 | }) 45 | }); 46 | -------------------------------------------------------------------------------- /test/fixtures/mocha/utils/ScopeUtilsSpec.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by Martin Koster on 25/02/15. 3 | */ 4 | var esprima = require('esprima'), 5 | expect = require('chai').expect, 6 | FunctionScopeUtils = require('../../../../utils/ScopeUtils'); 7 | 8 | 9 | describe('ScopeUtils', function(){ 10 | it('should remove loop variables if the function scope has an overriding variable with same identifier', function(){ 11 | var node = esprima.parse('var x=1;'), 12 | loopVariables = ['x', 'y', 'z']; 13 | 14 | expect(JSON.stringify(FunctionScopeUtils.removeOverriddenLoopVariables(node, loopVariables))).to.equal('["y","z"]'); 15 | }); 16 | 17 | it('should remove nested loop variables if somewhere in the code block a variable with same identifier is overridden', function(){ 18 | var node, 19 | loopVariables = ['x', 'y', 'z']; 20 | 21 | function nestedBlocks(d) { 22 | var z = 1; 23 | if (d < z) { 24 | for (var i=z; i 0) { 78 | setTimeout(function() { 79 | dfd.resolve(self.readFileEventually(fileName, maxWait - interval, interval)); 80 | }, interval); 81 | } else { 82 | dfd.reject(error); 83 | } 84 | }); 85 | 86 | return dfd.promise; 87 | }; 88 | 89 | /** 90 | * Try to find a file eventually. Keep trying every (interval || 100) milliseconds. 91 | * 92 | * @param fileName {string} name of the file that should be found 93 | * @param path {string} path to the directory in which the file should be found 94 | * @param maxDepth {number=} maximum directory depth for finding the file. When undefined or negative, it will continue indefinitely 95 | * @param maxWait {number=} maximum time that should be waited for the file to be read 96 | * @param interval {number=} [interval] number of milliseconds between each try, DEFAULT: 100 97 | * @returns {promise|*|Q.promise} a promise that will resolve with the file contents or rejected with any error 98 | */ 99 | module.exports.findEventually = function(fileName, path, maxDepth, maxWait, interval) { 100 | var self = this, 101 | dfd = Q.defer(); 102 | 103 | interval = interval || 100; 104 | 105 | self.find(fileName, path, maxDepth).then(function(filePath) { 106 | dfd.resolve(filePath); 107 | }, function(error) { 108 | if(maxWait > 0) { 109 | setTimeout(function() { 110 | dfd.resolve(self.findEventually(fileName, path, maxDepth, maxWait - interval, interval)); 111 | }, interval); 112 | } else { 113 | dfd.reject(error); 114 | } 115 | }); 116 | 117 | return dfd.promise; 118 | }; 119 | 120 | /** 121 | * Find a file recursively in a given path. 122 | * 123 | * @param fileName {string} name of the file that should be found 124 | * @param path {string} path to the directory in which the file should be found 125 | * @param maxDepth {number=} maximum directory depth for finding the file. When undefined or negative, it will continue indefinitely 126 | * @returns {*|promise} a promise that will resolve with the path to the file, or be rejected with any error 127 | */ 128 | module.exports.find = function(fileName, path, maxDepth) { 129 | var self = this, 130 | deferred = Q.defer(); 131 | 132 | self.promiseToReadDir(path).then(function(directoryContents) { 133 | var contentPromises = []; 134 | _.forEach(directoryContents, function(item) { 135 | var itemPath = pathAux.join(path, item); 136 | contentPromises.push(Q.Promise(function(resolve, reject) { 137 | self.promiseToStat(itemPath).then(function(stats) { 138 | if(stats.isDirectory()) { 139 | if(maxDepth !== 0) { 140 | resolve(self.find(fileName, itemPath, maxDepth - 1)); 141 | } else { 142 | reject(new Error('Reached max. depth of ' + maxDepth)); 143 | } 144 | } else { 145 | if(item === fileName) { 146 | resolve(itemPath); 147 | } else { 148 | reject(new Error('File ' + item + ' does not match ' + fileName)); 149 | } 150 | } 151 | }, function(error) { 152 | reject(error); 153 | }) 154 | })); 155 | }); 156 | 157 | Q.allSettled(contentPromises).spread(function() { 158 | var resolvedPromise = _.find(arguments, function(contentPromise) { 159 | return contentPromise.state === "fulfilled"; 160 | }); 161 | resolvedPromise ? deferred.resolve(resolvedPromise.value) : 162 | deferred.reject(new Error('Could not find ' + fileName)); 163 | }); 164 | }, function(error) { 165 | deferred.reject(new Error('Could not read dir "' + path + '": ' + error.message)); 166 | }); 167 | 168 | return deferred.promise; 169 | }; 170 | 171 | module.exports.promiseToReadFile = function promiseToReadFile(fileName) { 172 | return Q.Promise(function(resolve, reject){ 173 | fs.readFile(fileName, 'utf-8', function(error, data) { 174 | error ? reject(error) : resolve(data); 175 | }); 176 | }); 177 | }; 178 | 179 | module.exports.promiseToWriteFile = function promiseToWriteFile(fileName, data) { 180 | return Q.Promise(function(resolve, reject){ 181 | fs.writeFile(fileName, data, function(error, data) { 182 | error ? reject(error) : resolve(data); 183 | }); 184 | }); 185 | }; 186 | 187 | module.exports.promiseToReadDir = function promiseToReadDir(directory) { 188 | return Q.Promise(function(resolve, reject){ 189 | fs.readdir(directory, function(error, data) { 190 | error ? reject(error) : resolve(data); 191 | }); 192 | }); 193 | }; 194 | 195 | module.exports.promiseToStat = function promiseToStat(directory) { 196 | return Q.Promise(function(resolve, reject){ 197 | fs.stat(directory, function(error, data) { 198 | error ? reject(error) : resolve(data); 199 | }); 200 | }); 201 | }; 202 | 203 | -------------------------------------------------------------------------------- /utils/LiteralUtils.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by Martin Koster on 2/18/15. 3 | */ 4 | var _ = require('lodash'); 5 | module.exports.determineReplacement = function (literalValue) { 6 | var replacement; 7 | 8 | if (_.isString(literalValue)) { 9 | replacement = '"MUTATION!"'; 10 | } else if (_.isNumber(literalValue)) { 11 | replacement = (literalValue + 1) + ""; 12 | } else if (_.isBoolean(literalValue)) { 13 | replacement = (!literalValue) + ''; 14 | } 15 | return replacement; 16 | }; 17 | -------------------------------------------------------------------------------- /utils/MutationUtils.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by Martin Koster on 2/16/15. 3 | */ 4 | var _ = require('lodash'); 5 | 6 | var createMutation = function (astNode, endOffset, parentMutationId, replacement) { 7 | replacement = replacement || ''; 8 | return { 9 | range: astNode.range, 10 | begin: astNode.range[0], 11 | end: endOffset, 12 | line: astNode.loc.start.line, 13 | col: astNode.loc.start.column, 14 | parentMutationId: parentMutationId, 15 | mutationId: _.uniqueId(), 16 | replacement: replacement 17 | }; 18 | }; 19 | 20 | var createAstArrayElementDeletionMutation = function (astArray, element, elementIndex, parentMutationId) { 21 | var endOffset = (elementIndex === astArray.length - 1) ? // is last element ? 22 | element.range[1] : // handle last element 23 | astArray[elementIndex + 1].range[0]; // care for commas by extending to start of next element 24 | return createMutation(element, endOffset, parentMutationId); 25 | }; 26 | 27 | var createOperatorMutation = function (astNode, parentMutationId, replacement) { 28 | return { 29 | range: astNode.range, 30 | begin: astNode.left.range[1], 31 | end: astNode.right.range[0], 32 | line: astNode.left.loc.end.line, 33 | col: astNode.left.loc.end.column, 34 | mutationId: _.uniqueId(), 35 | parentMutationId: parentMutationId, 36 | replacement: replacement 37 | }; 38 | }; 39 | var createUnaryOperatorMutation = function (astNode, parentMutationId, replacement) { 40 | return { 41 | range: astNode.range, 42 | begin: astNode.range[0], 43 | end: astNode.range[0]+1, 44 | line: astNode.loc.end.line, 45 | col: astNode.loc.end.column, 46 | mutationId: _.uniqueId(), 47 | parentMutationId: parentMutationId, 48 | replacement: replacement 49 | }; 50 | }; 51 | 52 | module.exports.createMutation = createMutation; 53 | module.exports.createAstArrayElementDeletionMutation = createAstArrayElementDeletionMutation; 54 | module.exports.createOperatorMutation = createOperatorMutation; 55 | module.exports.createUnaryOperatorMutation = createUnaryOperatorMutation; 56 | -------------------------------------------------------------------------------- /utils/OptionUtils.js: -------------------------------------------------------------------------------- 1 | /** 2 | * OptionUtils 3 | * 4 | * @author Jimi van der Woning 5 | */ 6 | 'use strict'; 7 | 8 | var _ = require('lodash'), 9 | glob = require('glob'), 10 | log4js = require('log4js'), 11 | path = require('path'); 12 | 13 | 14 | // Placeholder function for when no explicit before, after, or test function is provided 15 | function CALL_DONE(done) { 16 | done(true); 17 | } 18 | 19 | var DEFAULT_OPTIONS = { 20 | before: CALL_DONE, 21 | beforeEach: CALL_DONE, 22 | test: CALL_DONE, 23 | afterEach: CALL_DONE, 24 | after: CALL_DONE, 25 | 26 | basePath: '.', 27 | testFramework: 'karma', 28 | logLevel: 'INFO', 29 | maxReportedMutationLength: 80, 30 | mutateProductionCode: false 31 | }; 32 | 33 | // By default, report only to the console, which takes no additional configuration 34 | var DEFAULT_REPORTER = { 35 | console: true 36 | }; 37 | 38 | var LOG_OPTIONS = { 39 | appenders: [ 40 | { 41 | type: 'console', 42 | layout: { 43 | type: 'pattern', 44 | pattern: '%[(%d{ABSOLUTE}) %p [%c]:%] %m' 45 | } 46 | } 47 | ] 48 | }; 49 | 50 | // By default, always ignore mutations of the 'use strict' keyword 51 | var DEFAULT_IGNORE = /('use strict'|"use strict");/; 52 | 53 | 54 | /** 55 | * @private 56 | * Check if all required options are set on the given opts object 57 | * @param opts the options object to check 58 | * @returns {String|null} indicator of all required options have been set or not. String with error message or null if no error was set 59 | */ 60 | function areRequiredOptionsSet(opts) { 61 | return !opts.hasOwnProperty('code') ? 'Options has no own code property' : 62 | !opts.hasOwnProperty('specs') ? 'Options has no own specs property' : 63 | !opts.hasOwnProperty('mutate') ? 'Options has no own mutate property' : 64 | opts.code.length === 0 ? 'Code property is empty' : 65 | opts.specs.length === 0 ? 'Specs property is empty' : 66 | opts.mutate.length === 0 ? 'Mutate property is empty' : 67 | null; 68 | } 69 | 70 | function ensureReportersConfig(opts) { 71 | // Only set the default reporter when no explicit reporter configuration is provided 72 | if(!opts.hasOwnProperty('reporters')) { 73 | opts.reporters = DEFAULT_REPORTER; 74 | } 75 | } 76 | 77 | function ensureIgnoreConfig(opts) { 78 | if(!opts.discardDefaultIgnore) { 79 | opts.ignore = opts.ignore ? [DEFAULT_IGNORE].concat(opts.ignore) : DEFAULT_IGNORE; 80 | } 81 | } 82 | 83 | /** 84 | * Prepend all given files with the provided basepath and expand all wildcards 85 | * @param files the files to expand 86 | * @param basePath the basepath from which the files should be expanded 87 | * @returns {Array} list of expanded files 88 | */ 89 | function expandFiles(files, basePath) { 90 | var expandedFiles = []; 91 | files = files ? _.isArray(files) ? files : [files] : []; 92 | 93 | _.forEach(files, function(fileName) { 94 | expandedFiles = _.union( 95 | expandedFiles, 96 | glob.sync(path.join(basePath, fileName), { dot: true, nodir: true }) 97 | ); 98 | }); 99 | 100 | return expandedFiles; 101 | } 102 | 103 | /** 104 | * Get the options for a given mutationTest grunt task 105 | * @param grunt 106 | * @param task the grunt task 107 | * @returns {*} the found options, or [null] when not all required options have been set 108 | */ 109 | function getOptions(grunt, task) { 110 | var globalOpts = grunt.config(task.name).options; 111 | var localOpts = grunt.config([task.name, task.target]).options; 112 | var opts = _.merge({}, DEFAULT_OPTIONS, globalOpts, localOpts); 113 | 114 | // Set logging options 115 | log4js.setGlobalLogLevel(log4js.levels[opts.logLevel]); 116 | log4js.configure(LOG_OPTIONS); 117 | 118 | opts.code = expandFiles(opts.code, opts.basePath); 119 | opts.specs = expandFiles(opts.specs, opts.basePath); 120 | opts.mutate = expandFiles(opts.mutate, opts.basePath); 121 | 122 | if (opts.karma) { 123 | opts.karma.notIncluded = expandFiles(opts.karma.notIncluded, opts.basePath); 124 | } 125 | 126 | var requiredOptionsSetErr = areRequiredOptionsSet(opts); 127 | if(requiredOptionsSetErr !== null) { 128 | grunt.warn('Not all required options have been set properly. ' + requiredOptionsSetErr); 129 | return null; 130 | } 131 | 132 | ensureReportersConfig(opts); 133 | ensureIgnoreConfig(opts); 134 | 135 | return opts; 136 | } 137 | 138 | module.exports.getOptions = getOptions; 139 | -------------------------------------------------------------------------------- /utils/ScopeUtils.js: -------------------------------------------------------------------------------- 1 | /** 2 | * remove variables from the 'loopVariables' array if they are being redefined in the new function scope. 3 | * All variables are retrieved from the current AST node and iteratively compared with the content in loopVariables 4 | * 5 | * Created by Martin Koster on 2/25/15. 6 | */ 7 | var _ = require('lodash'); 8 | 9 | /** 10 | * remove variables from the 'loopVariables' array if they are being redefined in the new function scope. 11 | * All variables are retrieved from the current AST node and iteratively compared with the content in loopVariables 12 | * If the given node contains child nodes this function will be called (recursively) with the child nodes 13 | * 14 | * @param astNode the node to search for overriding variables in 15 | * @param loopVariables array of variables that belong to a loop invariant and therefore should be exempt from mutations 16 | * @returns {array} a new array containing loop variables - fewer than the original if some were being overridden in the new function scope 17 | */ 18 | function removeOverriddenLoopVariables(astNode, loopVariables) { 19 | var result = loopVariables; 20 | 21 | if (astNode && astNode.type === 'VariableDeclarator') { 22 | } 23 | if (!astNode || hasScopeChanged(astNode)) { 24 | return loopVariables; // new scope: any variables therein have no bearing on current scope 25 | } 26 | if (astNode.type === 'VariableDeclaration') { 27 | result = processScopeVariables(astNode, loopVariables); 28 | } 29 | _.forOwn(astNode, function (childNode) { 30 | if(astNode !== childNode) { 31 | result = removeOverriddenLoopVariables(childNode, result); 32 | } 33 | }); 34 | 35 | return result; 36 | } 37 | 38 | /** 39 | * determines a scope change. 40 | * @param astNode 41 | * @returns {boolean} 42 | */ 43 | function hasScopeChanged(astNode) { 44 | return astNode.type === 'FunctionDeclaration' || astNode.type === 'FunctionExpression'; 45 | } 46 | 47 | /** 48 | * Filters Identifiers within given variableDeclaration out of the loopVariables array. 49 | * 50 | * Filtering occurs as follows: 51 | * XOR : (intermediate) result + variable identifiers found => a combined array minus identifiers that overlap 52 | * INTERSECTION: (intermediate) result + combined array to filter out variables that weren't in the original loopVariables array 53 | * @param {object} variableDeclaration declaration block of one or more variables 54 | * @param {loopVariables} loopVariables list of variables that are part of a loop invariable and should therefore not undergo mutations - unless overridden by another variable in the current function scope 55 | */ 56 | function processScopeVariables(variableDeclaration, loopVariables) { 57 | var identifiers = [], exclusiveCombination; 58 | 59 | _.forEach(variableDeclaration.declarations, function(declaration) { 60 | identifiers.push(declaration.id.name); 61 | }); 62 | 63 | exclusiveCombination = _.xor(loopVariables, identifiers); 64 | return _.intersection(loopVariables, exclusiveCombination); 65 | } 66 | 67 | module.exports.removeOverriddenLoopVariables = removeOverriddenLoopVariables; 68 | module.exports.hasScopeChanged = hasScopeChanged; 69 | --------------------------------------------------------------------------------