├── .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(/, '<').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 | {{{breadcrumb}}}
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 | Hide killed mutations
4 |
5 | {{/if}}
6 |
7 |
8 | {{#each sourceLines}}{{{this}}} {{/each}}
9 |
10 |
11 |
12 | Mutations
13 | {{#if mutations}}
14 |
15 |
16 |
17 | Mutation
18 | Result
19 |
20 |
21 |
22 | {{{mutations}}}
23 |
24 |
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 | File
6 | Success rate
7 | Killed
8 | Survived
9 | Ignored
10 | Untested
11 |
12 |
13 |
14 | {{{rows}}}
15 |
16 |
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[bla '); -> SURVIVED
5 | mocha/html-fragments.js:8:5 Removed console.log('
]]'); -> 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[bla ' 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 '
]]' 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[]]');
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 |
--------------------------------------------------------------------------------