Skip to content

Commit

Permalink
fix(core): exit process on error (#2378)
Browse files Browse the repository at this point in the history
Exit with non-zero exit code when an error occurs. 

* Update typed-inject to v3 in order to dispose of the `rootInjector`
* Rename some e2e tests to better reflect what they're testing.
* Add an e2e test for a failing dry run. 

Fixes #2315
  • Loading branch information
nicojs committed Aug 13, 2020
1 parent 92f3bf5 commit af18a59
Show file tree
Hide file tree
Showing 50 changed files with 226 additions and 88 deletions.
3 changes: 3 additions & 0 deletions e2e/test/exit-prematurely-dry-run-fails/.mocharc.jsonc
@@ -0,0 +1,3 @@
{
"spec": ["test/unit/*.js"]
}
File renamed without changes.
14 changes: 14 additions & 0 deletions e2e/test/exit-prematurely-dry-run-fails/package.json
@@ -0,0 +1,14 @@
{
"name": "exit-prematurely-dry-run-fails",
"version": "0.0.0",
"private": true,
"description": "A module to perform an integration test",
"main": "index.js",
"scripts": {
"pretest": "rimraf reports .stryker-tmp stryker.log",
"test": "stryker run || node -p \"require('fs').appendFileSync('stryker.log', 'Exited with an error exit code')\"",
"posttest": "mocha --no-config --require ../../tasks/ts-node-register.js verify/*.ts"
},
"author": "",
"license": "ISC"
}
6 changes: 6 additions & 0 deletions e2e/test/exit-prematurely-dry-run-fails/src/add.js
@@ -0,0 +1,6 @@
module.exports.add = function(num1, num2) {
return num1 + num2;
};



11 changes: 11 additions & 0 deletions e2e/test/exit-prematurely-dry-run-fails/stryker.conf.json
@@ -0,0 +1,11 @@
{
"$schema": "../../node_modules/@stryker-mutator/core/schema/stryker-schema.json",
"testRunner": "mocha",
"concurrency": 2,
"coverageAnalysis": "off",
"fileLogLevel": "info",
"reporters": ["clear-text", "html", "event-recorder"],
"plugins": [
"@stryker-mutator/mocha-runner"
]
}
13 changes: 13 additions & 0 deletions e2e/test/exit-prematurely-dry-run-fails/test/unit/add.spec.ts.js
@@ -0,0 +1,13 @@
const { add } = require('../../src/add');
const { expect } = require('chai');

describe('add', () => {

it('2 + 3 = 5', () => {
expect(add(2, 3)).to.be.equal(5);
});

it('1 + 1 = 3... ? (this is the test that should fail)', () => {
expect(add(1, 1)).to.be.equal(3);
});
});
28 changes: 28 additions & 0 deletions e2e/test/exit-prematurely-dry-run-fails/verify/verify.ts
@@ -0,0 +1,28 @@
import { promises as fs } from 'fs';
import { expect } from 'chai';
import { it } from 'mocha';

describe('Verify stryker has handled dry run failure correctly', () => {

let strykerLog: string;

before(async () => {
strykerLog = await fs.readFile('./stryker.log', 'utf8');
});

it('should about failed tests in initial test run', async () => {
expect(strykerLog).contains('There were failed tests in the initial test run');
});

it('should log exactly which test failed and why', async () => {
expect(strykerLog)
.contains('add 1 + 1 = 3... ? (this is the test that should fail)')
.contains('expected 2 to equal 3');
});

it('should have exited with a non-zero exit code', async () => {
// This line is added in package.json script if the process exited in an error.
expect(strykerLog)
.contains('Exited with an error exit code');
});
});
File renamed without changes.
File renamed without changes.
@@ -1,5 +1,5 @@
{
"name": "exit-prematurely",
"name": "exit-prematurely-no-tests-executed",
"version": "1.0.0",
"private": true,
"description": "A module to test the alternative flow when Stryker should exit prematurely, see https://github.com/stryker-mutator/stryker/issues/1519",
Expand Down
File renamed without changes.
1 change: 1 addition & 0 deletions e2e/test/exit-prematurely-no-tests-executed/test/test.js
@@ -0,0 +1 @@
// Idle
File renamed without changes.
1 change: 0 additions & 1 deletion e2e/test/exit-prematurely/test/test.js

This file was deleted.

File renamed without changes.
@@ -1,5 +1,5 @@
{
"name": "jasmine-jasmine",
"name": "jasmine-javascript",
"version": "0.0.0",
"private": true,
"description": "A module to perform an integration test",
Expand Down
File renamed without changes.
File renamed without changes.
File renamed without changes.
5 changes: 5 additions & 0 deletions e2e/test/mocha-javascript/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

@@ -1,5 +1,5 @@
{
"name": "mocha-mocha",
"name": "mocha-javascript",
"version": "0.0.0",
"private": true,
"description": "A module to perform an integration test",
Expand Down
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
2 changes: 1 addition & 1 deletion packages/api/package.json
Expand Up @@ -44,6 +44,6 @@
},
"devDependencies": {
"@types/node": "^14.0.1",
"typed-inject": "~2.2.1"
"typed-inject": "~3.0.0"
}
}
2 changes: 1 addition & 1 deletion packages/core/package.json
Expand Up @@ -80,7 +80,7 @@
"surrial": "~2.0.2",
"tree-kill": "~1.2.0",
"tslib": "~2.0.0",
"typed-inject": "~2.2.1",
"typed-inject": "~3.0.0",
"typed-rest-client": "~1.7.1"
},
"devDependencies": {
Expand Down
48 changes: 33 additions & 15 deletions packages/core/src/Stryker.ts
@@ -1,10 +1,13 @@
import { PartialStrykerOptions } from '@stryker-mutator/api/core';
import { MutantResult } from '@stryker-mutator/api/report';
import { rootInjector } from 'typed-inject';
import { createInjector } from 'typed-inject';

import { commonTokens } from '@stryker-mutator/api/plugin';

import { LogConfigurator } from './logging';
import { PrepareExecutor, MutantInstrumenterExecutor, DryRunExecutor, MutationTestExecutor } from './process';
import { coreTokens } from './di';
import { coreTokens, provideLogger } from './di';
import { retrieveCause, ConfigError } from './errors';

/**
* The main Stryker class.
Expand All @@ -14,29 +17,44 @@ export default class Stryker {
/**
* @constructor
* @param cliOptions The cli options.
* @param injector The root injector, for testing purposes only
* @param injectorFactory The injector factory, for testing purposes only
*/
constructor(private readonly cliOptions: PartialStrykerOptions, private readonly injector = rootInjector) {}
constructor(private readonly cliOptions: PartialStrykerOptions, private readonly injectorFactory = createInjector) {}

public async runMutationTest(): Promise<MutantResult[]> {
// 1. Prepare. Load Stryker configuration, load the input files and starts the logging server
const prepareExecutor = this.injector.provideValue(coreTokens.cliOptions, this.cliOptions).injectClass(PrepareExecutor);
const mutantInstrumenterInjector = await prepareExecutor.execute();

// 2. Mutate and instrument the files and write to the sandbox.
const mutantInstrumenter = mutantInstrumenterInjector.injectClass(MutantInstrumenterExecutor);
const dryRunExecutorInjector = await mutantInstrumenter.execute();
const rootInjector = this.injectorFactory();
const loggerProvider = provideLogger(rootInjector);

// 3. Perform a 'dry run' (initial test run). Runs the tests without active mutants and collects coverage.
const dryRunExecutor = dryRunExecutorInjector.injectClass(DryRunExecutor);
const mutationRunExecutorInjector = await dryRunExecutor.execute();
try {
// 1. Prepare. Load Stryker configuration, load the input files and starts the logging server
const prepareExecutor = loggerProvider.provideValue(coreTokens.cliOptions, this.cliOptions).injectClass(PrepareExecutor);
const mutantInstrumenterInjector = await prepareExecutor.execute();

// 2. Mutate and instrument the files and write to the sandbox.
const mutantInstrumenter = mutantInstrumenterInjector.injectClass(MutantInstrumenterExecutor);
const dryRunExecutorInjector = await mutantInstrumenter.execute();

// 3. Perform a 'dry run' (initial test run). Runs the tests without active mutants and collects coverage.
const dryRunExecutor = dryRunExecutorInjector.injectClass(DryRunExecutor);
const mutationRunExecutorInjector = await dryRunExecutor.execute();
// 4. Actual mutation testing. Will check every mutant and if valid run it in an available test runner.
const mutationRunExecutor = mutationRunExecutorInjector.injectClass(MutationTestExecutor);
const mutantResults = await mutationRunExecutor.execute();
return mutantResults;
} catch (error) {
const log = loggerProvider.resolve(commonTokens.getLogger)(Stryker.name);
const cause = retrieveCause(error);
if (cause instanceof ConfigError) {
log.error(cause.message);
} else {
log.error('an error occurred', error);
if (!log.isTraceEnabled()) {
log.info('Trouble figuring out what went wrong? Try `npx stryker run --fileLogLevel trace --logLevel debug` to get some more info.');
}
}
throw cause;
} finally {
await mutationRunExecutorInjector.dispose();
await rootInjector.dispose();
await LogConfigurator.shutdown();
}
}
Expand Down
17 changes: 2 additions & 15 deletions packages/core/src/StrykerCli.ts
@@ -1,15 +1,12 @@
import * as commander from 'commander';
import { getLogger } from 'log4js';
import { DashboardOptions, ALL_REPORT_TYPES, PartialStrykerOptions } from '@stryker-mutator/api/core';
import { Logger } from '@stryker-mutator/api/logging';

import { MutantResult } from '@stryker-mutator/api/report';

import { initializerFactory } from './initializer';
import { LogConfigurator } from './logging';
import Stryker from './Stryker';
import { defaultOptions } from './config/OptionsValidator';
import { retrieveCause, ConfigError } from './errors';

/**
* Interpret a command line argument and add it to an object.
Expand Down Expand Up @@ -39,8 +36,7 @@ export default class StrykerCli {
constructor(
private readonly argv: string[],
private readonly program: commander.Command = new commander.Command(),
private readonly runMutationTest = async (options: PartialStrykerOptions) => new Stryker(options).runMutationTest(),
private readonly log: Logger = getLogger(StrykerCli.name)
private readonly runMutationTest = async (options: PartialStrykerOptions) => new Stryker(options).runMutationTest()
) {}

public run() {
Expand Down Expand Up @@ -156,19 +152,10 @@ export default class StrykerCli {
if (Object.keys(commands).includes(this.command)) {
const promise: Promise<void | MutantResult[]> = commands[this.command as keyof typeof commands]();
promise.catch((err) => {
const error = retrieveCause(err);
if (error instanceof ConfigError) {
this.log.error(error.message);
} else {
this.log.error('an error occurred', err);
if (!this.log.isTraceEnabled()) {
this.log.info('Trouble figuring out what went wrong? Try `npx stryker run --fileLogLevel trace --logLevel debug` to get some more info.');
}
}
process.exitCode = 1;
});
} else {
this.log.error('Unknown command: "%s", supported commands: [%s], or use `stryker --help`.', this.command, Object.keys(commands));
console.error('Unknown command: "%s", supported commands: [%s], or use `stryker --help`.', this.command, Object.keys(commands));
}
}
}
13 changes: 5 additions & 8 deletions packages/core/src/di/buildChildProcessInjector.ts
@@ -1,17 +1,14 @@
import { StrykerOptions } from '@stryker-mutator/api/core';
import { commonTokens, Injector, PluginContext, Scope, tokens } from '@stryker-mutator/api/plugin';
import { getLogger } from 'log4js';
import { rootInjector } from 'typed-inject';
import { commonTokens, Injector, PluginContext, tokens } from '@stryker-mutator/api/plugin';
import { createInjector } from 'typed-inject';

import { loggerFactory, mutatorDescriptorFactory, pluginResolverFactory } from './factoryMethods';
import { mutatorDescriptorFactory, pluginResolverFactory } from './factoryMethods';

import { coreTokens } from '.';
import { coreTokens, provideLogger } from '.';

export function buildChildProcessInjector(options: StrykerOptions): Injector<PluginContext> {
return rootInjector
return provideLogger(createInjector())
.provideValue(commonTokens.options, options)
.provideValue(commonTokens.getLogger, getLogger)
.provideFactory(commonTokens.logger, loggerFactory, Scope.Transient)
.provideFactory(coreTokens.pluginDescriptors, pluginDescriptorsFactory)
.provideFactory(commonTokens.pluginResolver, pluginResolverFactory)
.provideFactory(commonTokens.mutatorDescriptor, mutatorDescriptorFactory);
Expand Down
16 changes: 6 additions & 10 deletions packages/core/src/di/buildMainInjector.ts
@@ -1,16 +1,15 @@
import execa = require('execa');
import { StrykerOptions, strykerCoreSchema, PartialStrykerOptions } from '@stryker-mutator/api/core';
import { commonTokens, Injector, PluginContext, PluginKind, Scope, tokens } from '@stryker-mutator/api/plugin';
import { commonTokens, Injector, PluginContext, PluginKind, tokens } from '@stryker-mutator/api/plugin';
import { Reporter } from '@stryker-mutator/api/report';
import { getLogger } from 'log4js';

import { readConfig, buildSchemaWithPluginContributions, OptionsValidator, validateOptions, markUnknownOptions } from '../config';
import ConfigReader from '../config/ConfigReader';
import BroadcastReporter from '../reporters/BroadcastReporter';
import { TemporaryDirectory } from '../utils/TemporaryDirectory';
import Timer from '../utils/Timer';

import { loggerFactory, mutatorDescriptorFactory, pluginResolverFactory } from './factoryMethods';
import { mutatorDescriptorFactory, pluginResolverFactory } from './factoryMethods';

import { coreTokens, PluginCreator } from '.';

Expand All @@ -23,15 +22,12 @@ export interface MainContext extends PluginContext {
[coreTokens.execa]: typeof execa;
}

type BasicInjector = Injector<Pick<MainContext, 'logger' | 'getLogger'> & { [coreTokens.cliOptions]: PartialStrykerOptions }>;
type PluginResolverProvider = Injector<Pick<MainContext, 'logger' | 'getLogger' | 'options' | 'pluginResolver'>>;
export type CliOptionsProvider = Injector<{ [coreTokens.cliOptions]: PartialStrykerOptions }>;
type PluginResolverProvider = Injector<PluginContext>;
export type CliOptionsProvider = Injector<Pick<MainContext, 'logger' | 'getLogger'> & { [coreTokens.cliOptions]: PartialStrykerOptions }>;

buildMainInjector.inject = tokens(commonTokens.injector);
export function buildMainInjector(injector: CliOptionsProvider): Injector<MainContext> {
const pluginResolverProvider = createPluginResolverProvider(
injector.provideValue(commonTokens.getLogger, getLogger).provideFactory(commonTokens.logger, loggerFactory, Scope.Transient)
);
const pluginResolverProvider = createPluginResolverProvider(injector);
return pluginResolverProvider
.provideFactory(commonTokens.mutatorDescriptor, mutatorDescriptorFactory)
.provideFactory(coreTokens.pluginCreatorReporter, PluginCreator.createFactory(PluginKind.Reporter))
Expand All @@ -42,7 +38,7 @@ export function buildMainInjector(injector: CliOptionsProvider): Injector<MainCo
.provideValue(coreTokens.execa, execa);
}

export function createPluginResolverProvider(parent: BasicInjector): PluginResolverProvider {
export function createPluginResolverProvider(parent: CliOptionsProvider): PluginResolverProvider {
return parent
.provideValue(coreTokens.validationSchema, strykerCoreSchema)
.provideClass(coreTokens.optionsValidator, OptionsValidator)
Expand Down
8 changes: 1 addition & 7 deletions packages/core/src/di/factoryMethods.ts
@@ -1,5 +1,5 @@
import { MutatorDescriptor, StrykerOptions } from '@stryker-mutator/api/core';
import { Logger, LoggerFactoryMethod } from '@stryker-mutator/api/logging';
import { Logger } from '@stryker-mutator/api/logging';
import { commonTokens, Injector, PluginResolver, tokens } from '@stryker-mutator/api/plugin';

import { coreTokens, PluginLoader } from '.';
Expand All @@ -13,12 +13,6 @@ export function pluginResolverFactory(
}
pluginResolverFactory.inject = tokens(commonTokens.injector);

// eslint-disable-next-line @typescript-eslint/ban-types
export function loggerFactory(getLogger: LoggerFactoryMethod, target: Function | undefined) {
return getLogger(target ? target.name : 'UNKNOWN');
}
loggerFactory.inject = tokens(commonTokens.getLogger, commonTokens.target);

export function mutatorDescriptorFactory(options: StrykerOptions): MutatorDescriptor {
const defaults: MutatorDescriptor = {
plugins: null,
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/di/index.ts
Expand Up @@ -5,4 +5,5 @@ export * from './buildChildProcessInjector';
export * from './PluginCreator';
export * from './PluginLoader';
export * from './factoryMethods';
export * from './provideLogger';
export { coreTokens };
15 changes: 15 additions & 0 deletions packages/core/src/di/provideLogger.ts
@@ -0,0 +1,15 @@
import { Injector, Scope } from 'typed-inject';
import { commonTokens } from '@stryker-mutator/api/plugin';
import { LoggerFactoryMethod, Logger } from '@stryker-mutator/api/logging';
import { getLogger } from 'log4js';

export function provideLogger(injector: Injector): LoggerProvider {
return injector.provideValue(commonTokens.getLogger, getLogger).provideFactory(commonTokens.logger, loggerFactory, Scope.Transient);
}
export type LoggerProvider = Injector<{ [commonTokens.getLogger]: LoggerFactoryMethod; [commonTokens.logger]: Logger }>;

// eslint-disable-next-line @typescript-eslint/ban-types
function loggerFactory(getLogger: LoggerFactoryMethod, target: Function | undefined) {
return getLogger(target ? target.name : 'UNKNOWN');
}
loggerFactory.inject = [commonTokens.getLogger, commonTokens.target] as const;
10 changes: 3 additions & 7 deletions packages/core/src/initializer/index.ts
@@ -1,9 +1,7 @@
import { commonTokens } from '@stryker-mutator/api/plugin';
import { getLogger } from 'log4js';
import { rootInjector } from 'typed-inject';
import { createInjector } from 'typed-inject';
import { RestClient } from 'typed-rest-client';

import { loggerFactory } from '../di/factoryMethods';
import { provideLogger } from '../di';

import * as initializerTokens from './initializerTokens';
import NpmClient from './NpmClient';
Expand All @@ -17,9 +15,7 @@ const BASE_NPM_SEARCH = 'https://api.npms.io';
const BASE_NPM_PACKAGE = 'https://www.unpkg.com';

export function initializerFactory(): StrykerInitializer {
return rootInjector
.provideValue(commonTokens.getLogger, getLogger)
.provideFactory(commonTokens.logger, loggerFactory)
return provideLogger(createInjector())
.provideValue(initializerTokens.out, console.log)
.provideValue(initializerTokens.strykerPresets, strykerPresets)
.provideValue(initializerTokens.restClientNpmSearch, new RestClient('npmSearch', BASE_NPM_SEARCH))
Expand Down

0 comments on commit af18a59

Please sign in to comment.