Skip to content

Commit

Permalink
feat(hit limit): infinite loop prevention in jasmine-runner (#3199)
Browse files Browse the repository at this point in the history
Add infinite loop prevention using a hit counter to `@stryker-mutator/jasmine-runner`.
  • Loading branch information
fredericbonnet authored Oct 18, 2021
1 parent 134699e commit bc792e0
Show file tree
Hide file tree
Showing 18 changed files with 253 additions and 20 deletions.
2 changes: 1 addition & 1 deletion e2e/test/hit-limit/.mocharc.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
{
"spec": "test/**.mocha.spec.js"
"require": ["test/chai-setup.js"]
}
2 changes: 1 addition & 1 deletion e2e/test/hit-limit/karma.conf.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ module.exports = function(config) {
frameworks: ['jasmine'],
files: [
'src/*.js',
'test/*.karma.spec.js'
'test/*.spec.js'
],
reporters: ['progress'],
colors: true,
Expand Down
1 change: 1 addition & 0 deletions e2e/test/hit-limit/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
"main": "index.js",
"scripts": {
"test:mocha": "mocha",
"test:jasmine": "jasmine",
"test:karma": "karma start",
"test": "mocha --no-package --timeout 60000 --require \"../../tasks/ts-node-register.js\" verify/verify.ts"
},
Expand Down
8 changes: 8 additions & 0 deletions e2e/test/hit-limit/spec/support/jasmine.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"spec_dir": "spec",
"spec_files": [
"../test/*.spec.js"
],
"stopSpecOnExpectationFailure": false,
"random": true
}
6 changes: 6 additions & 0 deletions e2e/test/hit-limit/test/chai-setup.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
const chai = require('chai');

chai.util.addMethod(chai.Assertion.prototype, 'toEqual', function (expected) {
var obj = chai.util.flag(this, 'object');
new chai.Assertion(obj).to.deep.equal(expected);
});
7 changes: 0 additions & 7 deletions e2e/test/hit-limit/test/loop.karma.spec.js

This file was deleted.

10 changes: 0 additions & 10 deletions e2e/test/hit-limit/test/loop.mocha.spec.js

This file was deleted.

14 changes: 14 additions & 0 deletions e2e/test/hit-limit/test/loop.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
if (typeof require === 'function') {
loop = require('../src/loop').loop;
}
if (typeof expect === 'undefined') {
globalThis.expect = require('chai').expect;
}

describe('loop', () => {
it('should result in 15 for n=5 and a sum function', () => {
let result = 0;
loop(5, (n) => (result += n));
expect(result).toEqual(15);
});
});
8 changes: 8 additions & 0 deletions e2e/test/hit-limit/verify/verify.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,12 @@ describe('Limit counter', () => {
expect(timeoutResults).lengthOf(3);
timeoutResults.forEach((result) => expect(result.statusReason).eq('Hit limit reached (501/500)'));
});

it('should limit infinite loops in the jasmine-runner', async () => {
const stryker = new Stryker({ testRunner: 'jasmine' });
const results = await stryker.runMutationTest();
const timeoutResults = results.filter(res => res.status === MutantStatus.Timeout);
expect(timeoutResults).lengthOf(3);
timeoutResults.forEach((result) => expect(result.statusReason).eq('Hit limit reached (501/500)'));
});
});
8 changes: 8 additions & 0 deletions packages/jasmine-runner/src/jasmine-test-runner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
toMutantRunResult,
ErrorDryRunResult,
DryRunOptions,
determineHitLimitReached,
} from '@stryker-mutator/api/test-runner';
import { errorToString, Task, DirectoryRequireCache, I } from '@stryker-mutator/util';

Expand Down Expand Up @@ -59,6 +60,8 @@ export class JasmineTestRunner implements TestRunner {

public async mutantRun(options: MutantRunOptions): Promise<MutantRunResult> {
this.instrumenterContext.activeMutant = options.activeMutant.id;
this.instrumenterContext.hitLimit = options.hitLimit;
this.instrumenterContext.hitCount = options.hitLimit ? 0 : undefined;
const runResult = await this.run(options.testFilter, undefined, options.disableBail);
return toMutantRunResult(runResult, true);
}
Expand Down Expand Up @@ -91,6 +94,11 @@ export class JasmineTestRunner implements TestRunner {
if (coverageAnalysis === 'all' || coverageAnalysis === 'perTest') {
mutantCoverage = self.instrumenterContext.mutantCoverage;
}
const timeoutResult = determineHitLimitReached(self.instrumenterContext.hitCount, self.instrumenterContext.hitLimit);
if (timeoutResult) {
runTask.resolve(timeoutResult);
return;
}
runTask.resolve({
status: DryRunStatus.Complete,
tests,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { testInjector, factory, assertions } from '@stryker-mutator/test-helpers';
import { expect } from 'chai';

import { createJasmineTestRunnerFactory, JasmineTestRunner } from '../../src';
import { resolveFromRoot, resolveTestResource } from '../helpers/resolve-test-resource';

describe('Infinite loop', () => {
let sut: JasmineTestRunner;

beforeEach(async () => {
process.chdir(resolveTestResource('infinite-loop-instrumented'));
sut = testInjector.injector.injectFunction(createJasmineTestRunnerFactory('__stryker2__'));
});
afterEach(async () => {
process.chdir(resolveFromRoot());
await sut.dispose();
});

it('should be able to recover using a hit counter', async () => {
// Arrange
const options = factory.mutantRunOptions({
activeMutant: factory.mutant({ id: '19' }),
testFilter: ['spec2'],
hitLimit: 10,
});

// Act
const result = await sut.mutantRun(options);

// Assert
assertions.expectTimeout(result);
expect(result.reason).contains('Hit limit reached');
});

it('should reset hit counter state correctly between runs', async () => {
const firstResult = await sut.mutantRun(
factory.mutantRunOptions({
activeMutant: factory.mutant({ id: '19' }),
testFilter: ['spec2'],
hitLimit: 10,
})
);
const secondResult = await sut.mutantRun(
factory.mutantRunOptions({
// 27 is a 'normal' mutant that should be killed
activeMutant: factory.mutant({ id: '22' }),
testFilter: ['spec2'],
hitLimit: 10,
})
);

// Assert
assertions.expectTimeout(firstResult);
assertions.expectKilled(secondResult);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
// This file is generated with tasks/instrument-test-resources.js
function stryNS_9fa48() {
var g = new Function("return this")();
var ns = g.__stryker2__ || (g.__stryker2__ = {});

if (ns.activeMutant === undefined && g.process && g.process.env && g.process.env.__STRYKER_ACTIVE_MUTANT__) {
ns.activeMutant = g.process.env.__STRYKER_ACTIVE_MUTANT__;
}

function retrieveNS() {
return ns;
}

stryNS_9fa48 = retrieveNS;
return retrieveNS();
}

stryNS_9fa48();

function stryCov_9fa48() {
var ns = stryNS_9fa48();
var cov = ns.mutantCoverage || (ns.mutantCoverage = {
static: {},
perTest: {}
});

function cover() {
var c = cov.static;

if (ns.currentTestId) {
c = cov.perTest[ns.currentTestId] = cov.perTest[ns.currentTestId] || {};
}

var a = arguments;

for (var i = 0; i < a.length; i++) {
c[a[i]] = (c[a[i]] || 0) + 1;
}
}

stryCov_9fa48 = cover;
cover.apply(null, arguments);
}

function stryMutAct_9fa48(id) {
var ns = stryNS_9fa48();

function isActive(id) {
if (ns.activeMutant === id) {
if (ns.hitCount !== void 0 && ++ns.hitCount > ns.hitLimit) {
throw new Error('Stryker: Hit count limit reached (' + ns.hitCount + ')');
}

return true;
}

return false;
}

stryMutAct_9fa48 = isActive;
return isActive(id);
}

function loop(n, action) {
if (stryMutAct_9fa48("15")) {
{}
} else {
stryCov_9fa48("15");
let goOn = stryMutAct_9fa48("16") ? false : (stryCov_9fa48("16"), true);

while (stryMutAct_9fa48("17") ? false : (stryCov_9fa48("17"), goOn)) {
if (stryMutAct_9fa48("18")) {
{}
} else {
stryCov_9fa48("18");
action(n);
stryMutAct_9fa48("19") ? n++ : (stryCov_9fa48("19"), n--);
goOn = stryMutAct_9fa48("23") ? n <= 0 : stryMutAct_9fa48("22") ? n >= 0 : stryMutAct_9fa48("21") ? false : stryMutAct_9fa48("20") ? true : (stryCov_9fa48("20", "21", "22", "23"), n > 0);
}
}
}
}

module.exports = loop;
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"spec_dir": "spec",
"spec_files": [
"**/*[sS]pec.js"
],
"helpers": [
"helpers/**/*.js"
],
"stopSpecOnExpectationFailure": false,
"random": false
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
var loop = require('../../lib/infinite-loop');

it('should handle an infinite loop as a timeout', () => {
while (true);
});

it('should be able to recover and test others', () => {});

it('should be able to break out of an infinite loop with a hit counter', () => {
let total = 0;
loop(5, (n) => {
expect(n).not.toBe(0);
total += n;
});
expect(total).toBe(15);
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
function loop(n, action) {
let goOn = true;
while (goOn) {
action(n);
n--;
goOn = n > 0;
}
}

module.exports = loop;
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"spec_dir": "spec",
"spec_files": [
"**/*[sS]pec.js"
],
"helpers": [
"helpers/**/*.js"
],
"stopSpecOnExpectationFailure": false,
"random": false
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
var loop = require('../../lib/infinite-loop');

it('should handle an infinite loop as a timeout', () => {
while (true);
});

it('should be able to recover and test others', () => {});

it('should be able to break out of an infinite loop with a hit counter', () => {
let total = 0;
loop(5, (n) => {
expect(n).not.toBe(0);
total += n;
});
expect(total).toBe(15);
});
3 changes: 2 additions & 1 deletion tasks/instrument-test-resources.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@ async function main() {
}, '__stryker2__');
await instrument({
'./packages/jasmine-runner/testResources/jasmine-init/lib/jasmine_examples/Player.js': './packages/jasmine-runner/testResources/jasmine-init-instrumented/lib/jasmine_examples/Player.js',
'./packages/jasmine-runner/testResources/jasmine-init/lib/jasmine_examples/Song.js': './packages/jasmine-runner/testResources/jasmine-init-instrumented/lib/jasmine_examples/Song.js'
'./packages/jasmine-runner/testResources/jasmine-init/lib/jasmine_examples/Song.js': './packages/jasmine-runner/testResources/jasmine-init-instrumented/lib/jasmine_examples/Song.js',
'./packages/jasmine-runner/testResources/infinite-loop/lib/infinite-loop.js': './packages/jasmine-runner/testResources/infinite-loop-instrumented/lib/infinite-loop.js',
}, '__stryker2__');
await instrument({
'./packages/karma-runner/testResources/sampleProject/src/Add.js': './packages/karma-runner/testResources/instrumented/src/Add.js',
Expand Down

0 comments on commit bc792e0

Please sign in to comment.