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