From 9a8b539fa0e5acce3e04befe6883a04de052db78 Mon Sep 17 00:00:00 2001 From: Nico Jansen Date: Tue, 5 Feb 2019 15:40:21 +0100 Subject: [PATCH] feat(transpilers): Remove side effects transpiler plugins (#1351) Remove side effects from all transpiler plugins. * stryker-webpack-transpiler * stryker-typescript * stryker-babel-transpiler --- .gitignore | 2 + package.json | 2 +- packages/stryker-api/package.json | 2 +- .../stryker-babel-transpiler/package.json | 10 +- .../src/BabelConfigReader.ts | 12 +- .../src/BabelTranspiler.ts | 18 +- .../stryker-babel-transpiler/src/index.ts | 6 +- .../test/integration/BabelProjects.it.ts | 12 +- .../test/unit/BabelTranspilerSpec.ts | 22 +-- .../tsconfig.src.json | 3 + packages/stryker-jest-runner/package.json | 82 ++++---- packages/stryker-mocha-runner/package.json | 82 ++++---- packages/stryker-mocha-runner/src/index.ts | 2 +- packages/stryker-test-helpers/package.json | 2 +- .../stryker-test-helpers/src/TestInjector.ts | 8 +- packages/stryker-test-helpers/src/factory.ts | 37 +++- .../src/TypescriptTranspiler.ts | 19 +- .../src/helpers/tsHelpers.ts | 11 +- packages/stryker-typescript/src/index.ts | 5 +- .../src/transpiler/TranspileFilter.ts | 6 +- .../test/integration/allowJS.it.ts | 2 +- .../test/integration/ownDogFoodSpec.ts | 6 +- .../test/integration/sampleSpec.ts | 6 +- .../test/integration/useHeaderFile.ts | 2 +- .../test/unit/TypescriptTranspilerSpec.ts | 9 +- .../stryker-webpack-transpiler/package.json | 5 +- .../src/WebpackTranspiler.ts | 17 +- .../src/compiler/ConfigLoader.ts | 26 ++- .../stryker-webpack-transpiler/src/index.ts | 16 +- .../src/pluginTokens.ts | 9 + .../test/helpers/globals.ts | 15 -- .../test/helpers/sinonInit.ts | 16 +- .../test/integration/transpiler.it.ts | 39 ++-- .../test/unit/WebpackTranspilerSpec.ts | 37 ++-- .../test/unit/compiler/ConfigLoaderSpec.ts | 28 +-- .../test/unit/compiler/WebpackCompilerSpec.ts | 5 +- .../test/unit/fs/InputFileSystemSpec.ts | 3 +- .../tsconfig.test.json | 3 + packages/stryker/.vscode/launch.json | 15 ++ packages/stryker/package.json | 4 +- packages/stryker/src/Sandbox.ts | 6 +- packages/stryker/src/Stryker.ts | 110 ++++------- .../stryker/src/TestFrameworkOrchestrator.ts | 2 +- .../src/child-proxy/ChildProcessProxy.ts | 27 ++- .../child-proxy/ChildProcessProxyWorker.ts | 17 +- .../src/child-proxy/messageProtocol.ts | 6 +- .../stryker/src/config/ConfigEditorApplier.ts | 2 +- packages/stryker/src/config/ConfigReader.ts | 5 +- .../stryker/src/config/ConfigValidator.ts | 5 +- packages/stryker/src/config/configFactory.ts | 17 ++ packages/stryker/src/config/index.ts | 2 + packages/stryker/src/di/PluginCreator.ts | 6 +- packages/stryker/src/di/PluginLoader.ts | 6 +- .../src/di/buildChildProcessInjector.ts | 21 ++ packages/stryker/src/di/buildMainInjector.ts | 50 +++++ packages/stryker/src/di/coreTokens.ts | 17 +- packages/stryker/src/di/createPlugin.ts | 22 --- packages/stryker/src/di/factoryMethods.ts | 27 +++ packages/stryker/src/di/index.ts | 6 + packages/stryker/src/di/loggerFactory.ts | 8 - .../stryker/src/input/InputFileResolver.ts | 8 +- .../src/process/InitialTestExecutor.ts | 53 +++-- .../src/reporters/BroadcastReporter.ts | 3 +- .../ChildProcessTestRunnerDecorator.ts | 9 +- .../ChildProcessTestRunnerWorker.ts | 6 +- .../ChildProcessTranspilerWorker.ts | 22 +++ .../src/transpiler/MutantTranspiler.ts | 36 ++-- .../stryker/src/transpiler/SourceMapper.ts | 7 +- .../src/transpiler/TranspilerFacade.ts | 19 +- .../child-proxy/ChildProcessProxy.it.ts | 8 +- .../test/integration/child-proxy/Echo.ts | 4 +- packages/stryker/test/unit/StrykerSpec.ts | 185 ++++++++---------- .../unit/TestFrameworkOrchestratorSpec.ts | 2 +- .../unit/child-proxy/ChildProcessProxySpec.ts | 26 +-- .../child-proxy/ChildProcessWorkerSpec.ts | 31 ++- .../test/unit/child-proxy/HelloClass.ts | 5 +- .../unit/config/ConfigEditorApplier.spec.ts | 2 +- .../stryker/test/unit/di/PluginLoaderSpec.ts | 2 +- .../test/unit/di/buildMainInjector.spec.ts | 110 +++++++++++ .../stryker/test/unit/di/createPlugin.spec.ts | 36 ---- .../test/unit/input/InputFileResolverSpec.ts | 86 +++++--- .../unit/process/InitialTestExecutorSpec.ts | 138 ++++++------- .../unit/reporters/BroadcastReporterSpec.ts | 2 +- .../ChildProcessTestRunnerDecoratorSpec.ts | 8 +- .../unit/transpiler/MutantTranspilerSpec.ts | 48 ++--- .../unit/transpiler/TranspilerFacadeSpec.ts | 42 ++-- 86 files changed, 1081 insertions(+), 785 deletions(-) create mode 100644 packages/stryker-webpack-transpiler/src/pluginTokens.ts delete mode 100644 packages/stryker-webpack-transpiler/test/helpers/globals.ts create mode 100644 packages/stryker/src/config/configFactory.ts create mode 100644 packages/stryker/src/config/index.ts create mode 100644 packages/stryker/src/di/buildChildProcessInjector.ts create mode 100644 packages/stryker/src/di/buildMainInjector.ts delete mode 100644 packages/stryker/src/di/createPlugin.ts create mode 100644 packages/stryker/src/di/factoryMethods.ts create mode 100644 packages/stryker/src/di/index.ts delete mode 100644 packages/stryker/src/di/loggerFactory.ts create mode 100644 packages/stryker/src/transpiler/ChildProcessTranspilerWorker.ts create mode 100644 packages/stryker/test/unit/di/buildMainInjector.spec.ts delete mode 100644 packages/stryker/test/unit/di/createPlugin.spec.ts diff --git a/.gitignore b/.gitignore index 6bf290c642..6862cb9897 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,8 @@ reports .tscache .stryker-tmp *.map +# Ignore heap dumps +packages/report.*.json e2e/module/*.js e2e/module/*.map diff --git a/package.json b/package.json index 42fb6eff94..1435c7706a 100644 --- a/package.json +++ b/package.json @@ -42,7 +42,7 @@ "sinon-chai": "^3.2.0", "source-map-support": "^0.5.6", "tslint": "~5.12.0", - "typescript": "3.2.2", + "typescript": "^3.3.1", "web-component-tester": "6.9.2" }, "prettier": { diff --git a/packages/stryker-api/package.json b/packages/stryker-api/package.json index 86c755db3c..6373840c42 100644 --- a/packages/stryker-api/package.json +++ b/packages/stryker-api/package.json @@ -34,6 +34,6 @@ }, "devDependencies": { "surrial": "~0.1.1", - "typed-inject": "^0.1.1" + "typed-inject": "^0.2.0" } } diff --git a/packages/stryker-babel-transpiler/package.json b/packages/stryker-babel-transpiler/package.json index 0021851e42..d600547b3f 100644 --- a/packages/stryker-babel-transpiler/package.json +++ b/packages/stryker-babel-transpiler/package.json @@ -43,21 +43,21 @@ "license": "Apache-2.0", "dependencies": { "babel-core": "6.26.3", - "minimatch": "~3.0.4" + "minimatch": "~3.0.4", + "stryker-api": "~0.23.0" }, "devDependencies": { + "@stryker-mutator/test-helpers": "0.0.0", "@types/babel-core": "~6.25.3", "@types/glob": "~7.1.0", "@types/minimatch": "~3.0.3", "babel-cli": "~6.26.0", "babel-plugin-transform-es2015-spread": "~6.22.0", "babel-preset-es2015": "~6.24.1", - "glob": "~7.1.2", - "stryker-api": "^0.23.0" + "glob": "~7.1.2" }, "peerDependencies": { - "babel-core": "^6.26.0 || ^7.0.0-bridge.0", - "stryker-api": ">=0.18.0 <0.24.0" + "babel-core": "^6.26.0 || ^7.0.0-bridge.0" }, "initStrykerConfig": { "babelrcFile": ".babelrc", diff --git a/packages/stryker-babel-transpiler/src/BabelConfigReader.ts b/packages/stryker-babel-transpiler/src/BabelConfigReader.ts index 6d7322c852..520f7ad6c6 100644 --- a/packages/stryker-babel-transpiler/src/BabelConfigReader.ts +++ b/packages/stryker-babel-transpiler/src/BabelConfigReader.ts @@ -1,21 +1,21 @@ import * as fs from 'fs'; import * as path from 'path'; -import { Config } from 'stryker-api/config'; import { CONFIG_KEY_FILE, CONFIG_KEY_OPTIONS } from './helpers/keys'; import { getLogger } from 'stryker-api/logging'; +import { StrykerOptions } from 'stryker-api/core'; export default class BabelConfigReader { private readonly log = getLogger(BabelConfigReader.name); - public readConfig(config: Config): babel.TransformOptions { - const babelConfig: babel.TransformOptions = config[CONFIG_KEY_OPTIONS] || this.getConfigFile(config) || {}; + public readConfig(options: StrykerOptions): babel.TransformOptions { + const babelConfig: babel.TransformOptions = options[CONFIG_KEY_OPTIONS] || this.getConfigFile(options) || {}; this.log.debug(`babel config is: ${JSON.stringify(babelConfig, null, 2)}`); return babelConfig; } - private getConfigFile(config: Config): babel.TransformOptions | null { - if (typeof config[CONFIG_KEY_FILE] === 'string') { - const babelrcPath = path.resolve(config[CONFIG_KEY_FILE]); + private getConfigFile(options: StrykerOptions): babel.TransformOptions | null { + if (typeof options[CONFIG_KEY_FILE] === 'string') { + const babelrcPath = path.resolve(options[CONFIG_KEY_FILE]); this.log.info(`Reading .babelrc file from path "${babelrcPath}"`); if (fs.existsSync(babelrcPath)) { try { diff --git a/packages/stryker-babel-transpiler/src/BabelTranspiler.ts b/packages/stryker-babel-transpiler/src/BabelTranspiler.ts index 811e40b31e..9593cc51fb 100644 --- a/packages/stryker-babel-transpiler/src/BabelTranspiler.ts +++ b/packages/stryker-babel-transpiler/src/BabelTranspiler.ts @@ -1,10 +1,11 @@ -import { Transpiler, TranspilerOptions } from 'stryker-api/transpile'; -import { File } from 'stryker-api/core'; +import { Transpiler } from 'stryker-api/transpile'; +import { File, StrykerOptions } from 'stryker-api/core'; import * as babel from 'babel-core'; import * as path from 'path'; import BabelConfigReader from './BabelConfigReader'; import { CONFIG_KEY_FILE } from './helpers/keys'; import { toJSFileName } from './helpers/helpers'; +import { tokens, commonTokens } from 'stryker-api/plugin'; const KNOWN_EXTENSIONS = Object.freeze([ '.es6', @@ -19,11 +20,12 @@ class BabelTranspiler implements Transpiler { private readonly babelOptions: babel.TransformOptions; private readonly projectRoot: string; - public constructor(options: TranspilerOptions) { - this.babelOptions = new BabelConfigReader().readConfig(options.config); + public static inject = tokens(commonTokens.options, commonTokens.produceSourceMaps); + public constructor(options: StrykerOptions, produceSourceMaps: boolean) { + this.babelOptions = new BabelConfigReader().readConfig(options); this.projectRoot = this.determineProjectRoot(options); - if (options.produceSourceMaps) { - throw new Error(`Invalid \`coverageAnalysis\` "${options.config.coverageAnalysis}" is not supported by the stryker-babel-transpiler. Not able to produce source maps yet. Please set it to "off".`); + if (produceSourceMaps) { + throw new Error(`Invalid \`coverageAnalysis\` "${options.coverageAnalysis}" is not supported by the stryker-babel-transpiler. Not able to produce source maps yet. Please set it to "off".`); } } @@ -61,8 +63,8 @@ class BabelTranspiler implements Transpiler { } } - private determineProjectRoot(options: TranspilerOptions): string { - const configFile = options.config[CONFIG_KEY_FILE]; + private determineProjectRoot(options: StrykerOptions): string { + const configFile = options[CONFIG_KEY_FILE]; if (configFile) { return path.dirname(configFile); } else { diff --git a/packages/stryker-babel-transpiler/src/index.ts b/packages/stryker-babel-transpiler/src/index.ts index b04f42ed63..8e597a81e8 100644 --- a/packages/stryker-babel-transpiler/src/index.ts +++ b/packages/stryker-babel-transpiler/src/index.ts @@ -1,4 +1,6 @@ -import { TranspilerFactory } from 'stryker-api/transpile'; import BabelTranspiler from './BabelTranspiler'; +import { declareClassPlugin, PluginKind } from 'stryker-api/plugin'; -TranspilerFactory.instance().register('babel', BabelTranspiler); +export const strykerPlugins = [ + declareClassPlugin(PluginKind.Transpiler, 'babel', BabelTranspiler) +]; diff --git a/packages/stryker-babel-transpiler/test/integration/BabelProjects.it.ts b/packages/stryker-babel-transpiler/test/integration/BabelProjects.it.ts index 9ae9e3f2a8..8e0c375f49 100644 --- a/packages/stryker-babel-transpiler/test/integration/BabelProjects.it.ts +++ b/packages/stryker-babel-transpiler/test/integration/BabelProjects.it.ts @@ -1,9 +1,9 @@ import * as path from 'path'; -import { File } from 'stryker-api/core'; -import { Config } from 'stryker-api/config'; +import { File, StrykerOptions } from 'stryker-api/core'; import { ProjectLoader } from '../helpers/projectLoader'; import BabelTranspiler from '../../src/BabelTranspiler'; import { expect } from 'chai'; +import { factory } from '@stryker-mutator/test-helpers'; function describeIntegrationTest(projectName: string) { @@ -11,14 +11,14 @@ function describeIntegrationTest(projectName: string) { let projectFiles: File[] = []; let resultFiles: File[] = []; let babelTranspiler: BabelTranspiler; - let config: Config; + let options: StrykerOptions; beforeEach(async () => { projectFiles = await ProjectLoader.getFiles(path.join(projectDir, 'source')); resultFiles = await ProjectLoader.getFiles(path.join(projectDir, 'expectedResult')); - config = new Config(); - config.set({ babelrcFile: path.join(projectDir, '.babelrc') }); - babelTranspiler = new BabelTranspiler({ config, produceSourceMaps: false }); + options = factory.strykerOptions(); + options.babelrcFile = path.join(projectDir, '.babelrc'); + babelTranspiler = new BabelTranspiler(options, /*produceSourceMaps:*/ false ); }); it('should be able to transpile the input files', async () => { diff --git a/packages/stryker-babel-transpiler/test/unit/BabelTranspilerSpec.ts b/packages/stryker-babel-transpiler/test/unit/BabelTranspilerSpec.ts index 402739e37d..58ead07f0e 100644 --- a/packages/stryker-babel-transpiler/test/unit/BabelTranspilerSpec.ts +++ b/packages/stryker-babel-transpiler/test/unit/BabelTranspilerSpec.ts @@ -1,19 +1,19 @@ import * as path from 'path'; import BabelTranspiler from '../../src/BabelTranspiler'; import { expect } from 'chai'; -import { File } from 'stryker-api/core'; -import { Config } from 'stryker-api/config'; +import { File, StrykerOptions } from 'stryker-api/core'; import * as sinon from 'sinon'; import * as babel from 'babel-core'; import BabelConfigReader, * as babelConfigReaderModule from '../../src/BabelConfigReader'; import { Mock, mock } from '../helpers/mock'; +import { factory } from '@stryker-mutator/test-helpers'; describe('BabelTranspiler', () => { let sut: BabelTranspiler; let sandbox: sinon.SinonSandbox; let files: File[]; let transformStub: sinon.SinonStub; - let config: Config; + let options: StrykerOptions; let babelConfigReaderMock: Mock; let babelOptions: any; @@ -23,7 +23,7 @@ describe('BabelTranspiler', () => { sandbox = sinon.createSandbox(); sandbox.stub(babelConfigReaderModule, 'default').returns(babelConfigReaderMock); transformStub = sandbox.stub(babel, 'transform'); - config = new Config(); + options = factory.strykerOptions(); files = [ new File(path.resolve('main.js'), 'const main = () => { sum(2); divide(2); }'), new File(path.resolve('sum.js'), 'const sum = (number) => number + number;'), @@ -37,18 +37,18 @@ describe('BabelTranspiler', () => { function arrangeHappyFlow() { babelConfigReaderMock.readConfig.returns(babelOptions); - sut = new BabelTranspiler({ config, produceSourceMaps: false }); + sut = new BabelTranspiler(options, /*produceSourceMaps:*/ false); } it('should read babel config using the BabelConfigReader', () => { arrangeHappyFlow(); expect(babelConfigReaderModule.default).calledWithNew; - expect(babelConfigReaderMock.readConfig).calledWith(config); + expect(babelConfigReaderMock.readConfig).calledWith(options); }); it('should throw if `produceSourceMaps` was true and coverage analysis is "perTest"', () => { - config.coverageAnalysis = 'perTest'; - expect(() => new BabelTranspiler({ produceSourceMaps: true, config })).throws('Invalid `coverageAnalysis` "perTest" is not supported by the stryker-babel-transpiler. Not able to produce source maps yet. Please set it to "off".'); + options.coverageAnalysis = 'perTest'; + expect(() => new BabelTranspiler(options, /*produceSourceMaps:*/ true)).throws('Invalid `coverageAnalysis` "perTest" is not supported by the stryker-babel-transpiler. Not able to produce source maps yet. Please set it to "off".'); }); }); @@ -56,7 +56,7 @@ describe('BabelTranspiler', () => { function arrangeHappyFlow(transformResult: babel.BabelFileResult & { ignored?: boolean } = { code: 'code' }) { babelConfigReaderMock.readConfig.returns(babelOptions); - sut = new BabelTranspiler({ config, produceSourceMaps: false }); + sut = new BabelTranspiler(options, /*produceSourceMaps:*/ false); transformStub.returns(transformResult); } @@ -77,7 +77,7 @@ describe('BabelTranspiler', () => { babelOptions.filename = 'override'; babelOptions.filenameRelative = 'override'; arrangeHappyFlow(); - sut = new BabelTranspiler({ config, produceSourceMaps: false }); + sut = new BabelTranspiler(options, /*produceSourceMaps:*/ false); await sut.transpile([files[0]]); expect(transformStub).calledWith(files[0].textContent, { filename: files[0].name, @@ -133,7 +133,7 @@ describe('BabelTranspiler', () => { it('should return with an error when the babel transform fails', async () => { const error = new Error('Syntax error'); transformStub.throws(error); - sut = new BabelTranspiler({ produceSourceMaps: false, config }); + sut = new BabelTranspiler(options, /*produceSourceMaps:*/ false); return expect(sut.transpile([new File('picture.js', 'S�L!##���XLDDDDDDDD\K�')])).rejectedWith(`Error while transpiling "picture.js": ${error.stack}`); }); }); diff --git a/packages/stryker-babel-transpiler/tsconfig.src.json b/packages/stryker-babel-transpiler/tsconfig.src.json index 4b7dbc31e7..717f97a580 100644 --- a/packages/stryker-babel-transpiler/tsconfig.src.json +++ b/packages/stryker-babel-transpiler/tsconfig.src.json @@ -9,6 +9,9 @@ "references": [ { "path": "../stryker-api/tsconfig.src.json" + }, + { + "path": "../stryker-test-helpers/tsconfig.src.json" } ] } \ No newline at end of file diff --git a/packages/stryker-jest-runner/package.json b/packages/stryker-jest-runner/package.json index 066f72c04b..eb74dbc185 100644 --- a/packages/stryker-jest-runner/package.json +++ b/packages/stryker-jest-runner/package.json @@ -1,43 +1,49 @@ { - "name": "stryker-jest-runner", - "version": "1.3.0", - "description": "A plugin to use the jest test runner and framework in Stryker, the JavaScript mutation testing framework", - "main": "src/index.js", - "repository": { - "type": "git", - "url": "https://github.com/stryker-mutator/stryker.git" - }, - "engines": { - "node": ">=6" - }, - "keywords": [ - "stryker", - "stryker-plugin", - "jest", - "stryker-test-runner" - ], - "author": "Sander koenders ", - "contributors": [ - "Maarten Mulders ", - "mshogren ", - "Nico Jansen ", - "Simon de Lang ", - "Philipp Weissenbacher ", - "Sander koenders " - ], - "license": "Apache-2.0", - "bugs": { - "url": "https://github.com/stryker-mutator/stryker/issues" - }, - "homepage": "https://github.com/stryker-mutator/stryker/tree/master/packages/stryker-jest-runner#readme", - "devDependencies": { + "name": "stryker-jest-runner", + "version": "1.3.0", + "description": "A plugin to use the jest test runner and framework in Stryker, the JavaScript mutation testing framework", + "main": "src/index.js", + "scripts": { + "start": "tsc -w", + "clean": "rimraf \"+(test|src)/**/*+(.d.ts|.js|.map)\" .nyc_output reports coverage", + "test": "nyc --reporter=html --report-dir=reports/coverage --lines 80 --functions 80 --branches 75 npm run mocha", + "mocha": "mocha \"test/helpers/**/*.js\" \"test/unit/**/*.js\" && mocha --timeout 30000 \"test/helpers/**/*.js\" \"test/integration/**/*.js\" --exit" + }, + "repository": { + "type": "git", + "url": "https://github.com/stryker-mutator/stryker.git" + }, + "engines": { + "node": ">=6" + }, + "keywords": [ + "stryker", + "stryker-plugin", + "jest", + "stryker-test-runner" + ], + "author": "Sander koenders ", + "contributors": [ + "Maarten Mulders ", + "mshogren ", + "Nico Jansen ", + "Simon de Lang ", + "Philipp Weissenbacher ", + "Sander koenders " + ], + "license": "Apache-2.0", + "bugs": { + "url": "https://github.com/stryker-mutator/stryker/issues" + }, + "homepage": "https://github.com/stryker-mutator/stryker/tree/master/packages/stryker-jest-runner#readme", + "devDependencies": { "@stryker-mutator/test-helpers": "0.0.0", - "@types/semver": "~5.5.0", - "jest": "~23.6.0", - "react": "~16.7.0", - "react-dom": "~16.7.0", - "react-scripts": "~2.1.0", - "react-scripts-ts": "~3.1.0", + "@types/semver": "~5.5.0", + "jest": "~23.6.0", + "react": "~16.7.0", + "react-dom": "~16.7.0", + "react-scripts": "~2.1.0", + "react-scripts-ts": "~3.1.0", "stryker-api": "^0.23.0" }, "peerDependencies": { diff --git a/packages/stryker-mocha-runner/package.json b/packages/stryker-mocha-runner/package.json index e4fa53e780..eac20649fc 100644 --- a/packages/stryker-mocha-runner/package.json +++ b/packages/stryker-mocha-runner/package.json @@ -1,43 +1,49 @@ { - "name": "stryker-mocha-runner", - "version": "0.16.0", - "description": "A plugin to use the mocha test runner in Stryker, the JavaScript mutation testing framework", - "main": "src/index.js", - "repository": { - "type": "git", - "url": "https://github.com/stryker-mutator/stryker" - }, - "engines": { - "node": ">=6" - }, - "keywords": [ - "stryker", - "stryker-plugin", - "mocha", - "stryker-test-runner" - ], - "author": "Simon de Lang ", - "contributors": [ - "Nico Jansen ", - "Simon de Lang " - ], - "license": "Apache-2.0", - "bugs": { - "url": "https://github.com/stryker-mutator/stryker/issues" - }, - "homepage": "https://github.com/stryker-mutator/stryker/tree/master/packages/stryker-mocha-runner#readme", - "dependencies": { - "multimatch": "~3.0.0", - "tslib": "~1.9.3" - }, - "devDependencies": { + "name": "stryker-mocha-runner", + "version": "0.16.0", + "description": "A plugin to use the mocha test runner in Stryker, the JavaScript mutation testing framework", + "main": "src/index.js", + "scripts": { + "start": "tsc -w", + "clean": "rimraf \"+(test|src)/**/*+(.d.ts|.js|.map)\" .nyc_output reports coverage", + "test": "nyc --reporter=html --report-dir=reports/coverage --lines 80 --functions 80 --branches 75 npm run mocha", + "mocha": "mocha \"test/helpers/**/*.js\" \"test/unit/**/*.js\" && mocha --timeout 10000 \"test/helpers/**/*.js\" \"test/integration/**/*.js\"" + }, + "repository": { + "type": "git", + "url": "https://github.com/stryker-mutator/stryker" + }, + "engines": { + "node": ">=6" + }, + "keywords": [ + "stryker", + "stryker-plugin", + "mocha", + "stryker-test-runner" + ], + "author": "Simon de Lang ", + "contributors": [ + "Nico Jansen ", + "Simon de Lang " + ], + "license": "Apache-2.0", + "bugs": { + "url": "https://github.com/stryker-mutator/stryker/issues" + }, + "homepage": "https://github.com/stryker-mutator/stryker/tree/master/packages/stryker-mocha-runner#readme", + "dependencies": { + "multimatch": "~3.0.0", + "tslib": "~1.9.3" + }, + "devDependencies": { "@stryker-mutator/test-helpers": "0.0.0", - "@types/multimatch": "~2.1.2", + "@types/multimatch": "~2.1.2", "stryker-api": "^0.23.0", "stryker-mocha-framework": "^0.14.0" - }, - "peerDependencies": { - "mocha": ">= 2.3.3 < 6", - "stryker-api": ">=0.18.0 <0.24.0" - } + }, + "peerDependencies": { + "mocha": ">= 2.3.3 < 6", + "stryker-api": ">=0.18.0 <0.24.0" + } } diff --git a/packages/stryker-mocha-runner/src/index.ts b/packages/stryker-mocha-runner/src/index.ts index 01bdbe5a53..8246a3b3ed 100644 --- a/packages/stryker-mocha-runner/src/index.ts +++ b/packages/stryker-mocha-runner/src/index.ts @@ -1,5 +1,5 @@ import { TestRunnerFactory } from 'stryker-api/test_runner'; -import { declareFactoryPlugin, Injector, PluginKind, BaseContext, tokens, commonTokens } from 'stryker-api/plugin'; +import { declareFactoryPlugin, PluginKind, BaseContext, tokens, commonTokens, Injector } from 'stryker-api/plugin'; import MochaTestRunner from './MochaTestRunner'; import MochaConfigEditor from './MochaConfigEditor'; diff --git a/packages/stryker-test-helpers/package.json b/packages/stryker-test-helpers/package.json index a055759b96..e1bf7aa5f5 100644 --- a/packages/stryker-test-helpers/package.json +++ b/packages/stryker-test-helpers/package.json @@ -20,6 +20,6 @@ "license": "ISC", "devDependencies": { "stryker-api": "^0.23.0", - "typed-inject": "^0.1.1" + "typed-inject": "^0.2.0" } } \ No newline at end of file diff --git a/packages/stryker-test-helpers/src/TestInjector.ts b/packages/stryker-test-helpers/src/TestInjector.ts index 83391aa54c..2c749937cf 100644 --- a/packages/stryker-test-helpers/src/TestInjector.ts +++ b/packages/stryker-test-helpers/src/TestInjector.ts @@ -8,19 +8,19 @@ import { Config } from 'stryker-api/config'; class TestInjector { - public providePluginResolver = (): PluginResolver => { + private readonly providePluginResolver = (): PluginResolver => { return this.pluginResolver; } - public provideLogger = (): Logger => { + private readonly provideLogger = (): Logger => { return this.logger; } - public provideConfig = () => { + private readonly provideConfig = () => { const config = new Config(); config.set(this.provideOptions()); return config; } - public provideOptions = () => { + private readonly provideOptions = () => { return factory.strykerOptions(this.options); } diff --git a/packages/stryker-test-helpers/src/factory.ts b/packages/stryker-test-helpers/src/factory.ts index 6abe582e3f..5738f2fafe 100644 --- a/packages/stryker-test-helpers/src/factory.ts +++ b/packages/stryker-test-helpers/src/factory.ts @@ -6,6 +6,8 @@ import { TestFramework, TestSelection } from 'stryker-api/test_framework'; import { MutantStatus, MatchedMutant, MutantResult, Reporter, ScoreResult } from 'stryker-api/report'; import { MutationScoreThresholds, File, Location, StrykerOptions, LogLevel } from 'stryker-api/core'; import * as sinon from 'sinon'; +import { Transpiler } from 'stryker-api/transpile'; +import { Injector } from 'typed-inject'; /** * A 1x1 png base64 encoded @@ -17,7 +19,7 @@ export const PNG_BASE64_ENCODED = 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeA * @param defaults */ function factoryMethod(defaultsFactory: () => T) { - return (overrides?: Partial) => Object.assign({}, defaultsFactory(), overrides); + return (overrides?: Partial): T => Object.assign({}, defaultsFactory(), overrides); } export const location = factoryMethod(() => ({ start: { line: 0, column: 0 }, end: { line: 0, column: 0 } })); @@ -59,11 +61,13 @@ export function logger(): sinon.SinonStubbedInstance { }; } -export const testFramework = factoryMethod(() => ({ - beforeEach(codeFragment: string) { return `beforeEach(){ ${codeFragment}}`; }, - afterEach(codeFragment: string) { return `afterEach(){ ${codeFragment}}`; }, - filter(selections: TestSelection[]) { return `filter: ${selections}`; } -})); +export function testFramework(): TestFramework { + return { + beforeEach(codeFragment: string) { return `beforeEach(){ ${codeFragment}}`; }, + afterEach(codeFragment: string) { return `afterEach(){ ${codeFragment}}`; }, + filter(selections: TestSelection[]) { return `filter: ${selections}`; } + }; +} export const scoreResult = factoryMethod(() => ({ childResults: [], @@ -142,6 +146,12 @@ export function configEditor(): sinon.SinonStubbedInstance { }; } +export function transpiler(): sinon.SinonStubbedInstance { + return { + transpile: sinon.stub() + }; +} + export function matchedMutant(numberOfTests: number, mutantId = numberOfTests.toString()): MatchedMutant { const scopedTestIds: number[] = []; for (let i = 0; i < numberOfTests; i++) { @@ -157,6 +167,21 @@ export function matchedMutant(numberOfTests: number, mutantId = numberOfTests.to }; } +export function injector(): sinon.SinonStubbedInstance { + const injectorMock: sinon.SinonStubbedInstance = { + injectClass: sinon.stub(), + injectFunction: sinon.stub(), + provideClass: sinon.stub(), + provideFactory: sinon.stub(), + provideValue: sinon.stub(), + resolve: sinon.stub() + }; + injectorMock.provideClass.returnsThis(); + injectorMock.provideFactory.returnsThis(); + injectorMock.provideValue.returnsThis(); + return injectorMock; +} + export function file() { return new File('', ''); } diff --git a/packages/stryker-typescript/src/TypescriptTranspiler.ts b/packages/stryker-typescript/src/TypescriptTranspiler.ts index 2d64c1d636..32813237b6 100644 --- a/packages/stryker-typescript/src/TypescriptTranspiler.ts +++ b/packages/stryker-typescript/src/TypescriptTranspiler.ts @@ -1,23 +1,20 @@ import flatMap = require('lodash.flatmap'); import * as ts from 'typescript'; -import { Config } from 'stryker-api/config'; -import { Transpiler, TranspilerOptions } from 'stryker-api/transpile'; -import { File } from 'stryker-api/core'; +import { Transpiler } from 'stryker-api/transpile'; +import { File, StrykerOptions } from 'stryker-api/core'; import { getTSConfig, getProjectDirectory, guardTypescriptVersion, isHeaderFile } from './helpers/tsHelpers'; import TranspilingLanguageService from './transpiler/TranspilingLanguageService'; import TranspileFilter from './transpiler/TranspileFilter'; +import { tokens, commonTokens } from 'stryker-api/plugin'; export default class TypescriptTranspiler implements Transpiler { private languageService: TranspilingLanguageService; - private readonly config: Config; - private readonly produceSourceMaps: boolean; private readonly filter: TranspileFilter; - constructor(options: TranspilerOptions) { + public static inject = tokens(commonTokens.options, commonTokens.produceSourceMaps); + constructor(private readonly options: StrykerOptions, private readonly produceSourceMaps: boolean) { guardTypescriptVersion(); - this.config = options.config; - this.produceSourceMaps = options.produceSourceMaps; - this.filter = TranspileFilter.create(this.config); + this.filter = TranspileFilter.create(this.options); } public transpile(files: ReadonlyArray): Promise> { @@ -41,10 +38,10 @@ export default class TypescriptTranspiler implements Transpiler { } private createLanguageService(typescriptFiles: ReadonlyArray) { - const tsConfig = getTSConfig(this.config); + const tsConfig = getTSConfig(this.options); const compilerOptions: ts.CompilerOptions = (tsConfig && tsConfig.options) || {}; return new TranspilingLanguageService( - compilerOptions, typescriptFiles, getProjectDirectory(this.config), this.produceSourceMaps); + compilerOptions, typescriptFiles, getProjectDirectory(this.options), this.produceSourceMaps); } private transpileFiles(files: ReadonlyArray) { diff --git a/packages/stryker-typescript/src/helpers/tsHelpers.ts b/packages/stryker-typescript/src/helpers/tsHelpers.ts index eba588ca9d..0190b04568 100644 --- a/packages/stryker-typescript/src/helpers/tsHelpers.ts +++ b/packages/stryker-typescript/src/helpers/tsHelpers.ts @@ -1,7 +1,6 @@ import * as os from 'os'; -import { File } from 'stryker-api/core'; +import { File, StrykerOptions } from 'stryker-api/core'; import { CONFIG_KEY, CONFIG_KEY_FILE } from './keys'; -import { Config } from 'stryker-api/config'; import * as ts from 'typescript'; import * as path from 'path'; import * as semver from 'semver'; @@ -30,12 +29,12 @@ export function normalizeFileFromTypescript(fileName: string) { return path.normalize(fileName); } -export function getTSConfig(config: Config): ts.ParsedCommandLine | undefined { - return config[CONFIG_KEY]; +export function getTSConfig(options: StrykerOptions): ts.ParsedCommandLine | undefined { + return options[CONFIG_KEY]; } -export function getProjectDirectory(config: Config) { - return path.dirname(config[CONFIG_KEY_FILE] || '.'); +export function getProjectDirectory(options: StrykerOptions) { + return path.dirname(options[CONFIG_KEY_FILE] || '.'); } /** diff --git a/packages/stryker-typescript/src/index.ts b/packages/stryker-typescript/src/index.ts index e34078abc2..32cd36cae6 100644 --- a/packages/stryker-typescript/src/index.ts +++ b/packages/stryker-typescript/src/index.ts @@ -1,12 +1,11 @@ import { MutatorFactory } from 'stryker-api/mutant'; -import { TranspilerFactory } from 'stryker-api/transpile'; import { declareClassPlugin, PluginKind } from 'stryker-api/plugin'; import TypescriptConfigEditor from './TypescriptConfigEditor'; import TypescriptMutator from './TypescriptMutator'; import TypescriptTranspiler from './TypescriptTranspiler'; export const strykerPlugins = [ - declareClassPlugin(PluginKind.ConfigEditor, 'typescript', TypescriptConfigEditor) + declareClassPlugin(PluginKind.ConfigEditor, 'typescript', TypescriptConfigEditor), + declareClassPlugin(PluginKind.Transpiler, 'typescript', TypescriptTranspiler) ]; MutatorFactory.instance().register('typescript', TypescriptMutator); -TranspilerFactory.instance().register('typescript', TypescriptTranspiler); diff --git a/packages/stryker-typescript/src/transpiler/TranspileFilter.ts b/packages/stryker-typescript/src/transpiler/TranspileFilter.ts index eda30dcd56..b4d2378f2e 100644 --- a/packages/stryker-typescript/src/transpiler/TranspileFilter.ts +++ b/packages/stryker-typescript/src/transpiler/TranspileFilter.ts @@ -1,5 +1,5 @@ -import { Config } from 'stryker-api/config'; import { normalizeFileFromTypescript, isTypescriptFile, getTSConfig } from '../helpers/tsHelpers'; +import { StrykerOptions } from 'stryker-api/core'; /** * Represents a transpile filter. This is the component that decides on which files needs to be transpiled. @@ -10,8 +10,8 @@ import { normalizeFileFromTypescript, isTypescriptFile, getTSConfig } from '../h export default abstract class TranspileFilter { public abstract isIncluded(fileName: string): boolean; - public static create(config: Config): TranspileFilter { - const parsedCommandLine = getTSConfig(config); + public static create(options: StrykerOptions): TranspileFilter { + const parsedCommandLine = getTSConfig(options); if (parsedCommandLine) { return new TSConfigFilter(parsedCommandLine); } else { diff --git a/packages/stryker-typescript/test/integration/allowJS.it.ts b/packages/stryker-typescript/test/integration/allowJS.it.ts index fc9eccebb6..76b534dbb3 100644 --- a/packages/stryker-typescript/test/integration/allowJS.it.ts +++ b/packages/stryker-typescript/test/integration/allowJS.it.ts @@ -24,7 +24,7 @@ describe('AllowJS integration', () => { }); it('should be able to transpile source code', async () => { - const transpiler = new TypescriptTranspiler({ config, produceSourceMaps: false }); + const transpiler = new TypescriptTranspiler(config, /*produceSourceMaps: */ false); const outputFiles = await transpiler.transpile(inputFiles); expect(outputFiles.length).to.eq(2); expect(outputFiles.map(f => f.name)).deep.eq([ diff --git a/packages/stryker-typescript/test/integration/ownDogFoodSpec.ts b/packages/stryker-typescript/test/integration/ownDogFoodSpec.ts index b1b3935ed5..7cbd2d385a 100644 --- a/packages/stryker-typescript/test/integration/ownDogFoodSpec.ts +++ b/packages/stryker-typescript/test/integration/ownDogFoodSpec.ts @@ -24,13 +24,13 @@ describe('stryker-typescript', () => { }); it('should be able to transpile itself', async () => { - const transpiler = new TypescriptTranspiler({ config, produceSourceMaps: true }); + const transpiler = new TypescriptTranspiler(config, /*produceSourceMaps: */ true); const outputFiles = await transpiler.transpile(inputFiles); expect(outputFiles.length).greaterThan(10); }); it('should result in an error if a variable is declared as any and noImplicitAny = true', async () => { - const transpiler = new TypescriptTranspiler({ config, produceSourceMaps: true }); + const transpiler = new TypescriptTranspiler(config, /*produceSourceMaps: */ true); inputFiles[0] = new File(inputFiles[0].name, inputFiles[0].textContent + 'function foo(bar) { return bar; } '); return expect(transpiler.transpile(inputFiles)).rejectedWith('error TS7006: Parameter \'bar\' implicitly has an \'any\' type'); }); @@ -38,7 +38,7 @@ describe('stryker-typescript', () => { it('should not result in an error if a variable is declared as any and noImplicitAny = false', async () => { config.tsconfig.noImplicitAny = false; inputFiles[0] = new File(inputFiles[0].name, inputFiles[0].textContent + 'const shouldResultInError = 3'); - const transpiler = new TypescriptTranspiler({ config, produceSourceMaps: true }); + const transpiler = new TypescriptTranspiler(config, /*produceSourceMaps: */ true); const outputFiles = await transpiler.transpile(inputFiles); expect(outputFiles).lengthOf.greaterThan(0); }); diff --git a/packages/stryker-typescript/test/integration/sampleSpec.ts b/packages/stryker-typescript/test/integration/sampleSpec.ts index a736766ede..d7d6e8aa0f 100644 --- a/packages/stryker-typescript/test/integration/sampleSpec.ts +++ b/packages/stryker-typescript/test/integration/sampleSpec.ts @@ -33,13 +33,13 @@ describe('Sample integration', () => { }); it('should be able to transpile source code', async () => { - const transpiler = new TypescriptTranspiler({ config, produceSourceMaps: false }); + const transpiler = new TypescriptTranspiler(config, /*produceSourceMaps: */ false); const outputFiles = await transpiler.transpile(inputFiles); expect(outputFiles.length).to.eq(2); }); it('should be able to produce source maps', async () => { - const transpiler = new TypescriptTranspiler({ config, produceSourceMaps: true }); + const transpiler = new TypescriptTranspiler(config, /*produceSourceMaps: */ true); const outputFiles = await transpiler.transpile(inputFiles); expect(outputFiles).lengthOf(4); const mapFiles = outputFiles.filter(file => file.name.endsWith('.map')); @@ -54,7 +54,7 @@ describe('Sample integration', () => { // Transpile mutants const mutator = new TypescriptMutator(config); const mutants = mutator.mutate(inputFiles); - const transpiler = new TypescriptTranspiler({ config, produceSourceMaps: false }); + const transpiler = new TypescriptTranspiler(config, /*produceSourceMaps: */ false); transpiler.transpile(inputFiles); const mathDotTS = inputFiles.filter(file => file.name.endsWith('math.ts'))[0]; const [firstBinaryMutant, stringSubtractMutant] = mutants.filter(m => m.mutatorName === 'BinaryExpression'); diff --git a/packages/stryker-typescript/test/integration/useHeaderFile.ts b/packages/stryker-typescript/test/integration/useHeaderFile.ts index 4b8ee02727..fc75ae5e4f 100644 --- a/packages/stryker-typescript/test/integration/useHeaderFile.ts +++ b/packages/stryker-typescript/test/integration/useHeaderFile.ts @@ -24,7 +24,7 @@ describe('Use header file integration', () => { }); it('should be able to transpile source code', async () => { - const transpiler = new TypescriptTranspiler({ config, produceSourceMaps: false }); + const transpiler = new TypescriptTranspiler(config, /*produceSourceMaps:*/ false); const outputFiles = await transpiler.transpile(inputFiles); expect(outputFiles.length).to.eq(2); }); diff --git a/packages/stryker-typescript/test/unit/TypescriptTranspilerSpec.ts b/packages/stryker-typescript/test/unit/TypescriptTranspilerSpec.ts index 9dcd9ca1ce..3f75e16dab 100644 --- a/packages/stryker-typescript/test/unit/TypescriptTranspilerSpec.ts +++ b/packages/stryker-typescript/test/unit/TypescriptTranspilerSpec.ts @@ -2,22 +2,21 @@ import TranspilingLanguageService, * as transpilingLanguageService from '../../s import { expect } from 'chai'; import { Mock, mock } from '../helpers/producers'; import TypescriptTranspiler from '../../src/TypescriptTranspiler'; -import { Config } from 'stryker-api/config'; import { File } from 'stryker-api/core'; import { EmitOutput } from '../../src/transpiler/TranspilingLanguageService'; import { serialize } from 'surrial'; import TranspileFilter from '../../src/transpiler/TranspileFilter'; import sinon = require('sinon'); +import { testInjector } from '@stryker-mutator/test-helpers'; +import { commonTokens } from 'stryker-api/plugin'; describe('TypescriptTranspiler', () => { let languageService: Mock; let sut: TypescriptTranspiler; - let config: Config; let transpileFilterMock: Mock; beforeEach(() => { - config = new Config(); languageService = mock(TranspilingLanguageService); transpileFilterMock = { // Cannot use `mock` as it is an abstract class @@ -31,7 +30,9 @@ describe('TypescriptTranspiler', () => { beforeEach(() => { languageService.getSemanticDiagnostics.returns([]); // no errors by default - sut = new TypescriptTranspiler({ config, produceSourceMaps: true }); + sut = testInjector.injector + .provideValue(commonTokens.produceSourceMaps, true) + .injectClass(TypescriptTranspiler); }); it('should transpile given files', async () => { diff --git a/packages/stryker-webpack-transpiler/package.json b/packages/stryker-webpack-transpiler/package.json index e1121bdca3..ae6e83acea 100644 --- a/packages/stryker-webpack-transpiler/package.json +++ b/packages/stryker-webpack-transpiler/package.json @@ -33,6 +33,7 @@ }, "homepage": "https://github.com/stryker-mutator/stryker/tree/master/packages/stryker-webpack-transpiler#readme", "devDependencies": { + "@stryker-mutator/test-helpers": "0.0.0", "@types/memory-fs": "~0.3.0", "@types/webpack": "~4.4.12", "raw-loader": "~1.0.0", @@ -40,13 +41,13 @@ "webpack": "~4.28.2" }, "peerDependencies": { - "stryker-api": ">=0.18.0 <0.24.0", "webpack": ">=2.0.0" }, "dependencies": { "enhanced-resolve": "~4.1.0", "lodash": "~4.17.4", - "memory-fs": "~0.4.1" + "memory-fs": "~0.4.1", + "stryker-api": "~0.23.0" }, "initStrykerConfig": { "webpack": { diff --git a/packages/stryker-webpack-transpiler/src/WebpackTranspiler.ts b/packages/stryker-webpack-transpiler/src/WebpackTranspiler.ts index b0b1ba51bf..8f28d24b6e 100644 --- a/packages/stryker-webpack-transpiler/src/WebpackTranspiler.ts +++ b/packages/stryker-webpack-transpiler/src/WebpackTranspiler.ts @@ -1,7 +1,9 @@ -import { TranspilerOptions, Transpiler } from 'stryker-api/transpile'; -import { File } from 'stryker-api/core'; +import { Transpiler } from 'stryker-api/transpile'; +import { File, StrykerOptions } from 'stryker-api/core'; import WebpackCompiler from './compiler/WebpackCompiler'; import ConfigLoader from './compiler/ConfigLoader'; +import { tokens, commonTokens } from 'stryker-api/plugin'; +import { pluginTokens } from './pluginTokens'; const DEFAULT_STRYKER_WEBPACK_CONFIG = Object.freeze({ configFile: undefined, silent: true, context: process.cwd() }); @@ -9,17 +11,18 @@ export default class WebpackTranspiler implements Transpiler { private readonly config: StrykerWebpackConfig; private webpackCompiler: WebpackCompiler; - public constructor(options: TranspilerOptions) { - if (options.produceSourceMaps) { - throw new Error(`Invalid \`coverageAnalysis\` "${options.config.coverageAnalysis}" is not supported by the stryker-webpack-transpiler (yet). It is not able to produce source maps yet. Please set it "coverageAnalysis" to "off".`); + public static inject = tokens(commonTokens.options, commonTokens.produceSourceMaps, pluginTokens.configLoader); + public constructor(options: StrykerOptions, produceSourceMaps: boolean, private readonly configLoader: ConfigLoader) { + if (produceSourceMaps) { + throw new Error(`Invalid \`coverageAnalysis\` "${options.coverageAnalysis}" is not supported by the stryker-webpack-transpiler (yet). It is not able to produce source maps yet. Please set it "coverageAnalysis" to "off".`); } - this.config = this.getStrykerWebpackConfig(options.config.webpack); + this.config = this.getStrykerWebpackConfig(options.webpack); } public async transpile(files: ReadonlyArray): Promise> { if (!this.webpackCompiler) { // Initialize the webpack compiler with the current directory (process.cwd) - const config = await new ConfigLoader().load(this.config); + const config = await this.configLoader.load(this.config); this.webpackCompiler = new WebpackCompiler(config); } diff --git a/packages/stryker-webpack-transpiler/src/compiler/ConfigLoader.ts b/packages/stryker-webpack-transpiler/src/compiler/ConfigLoader.ts index e7a170a979..4dd38ca822 100644 --- a/packages/stryker-webpack-transpiler/src/compiler/ConfigLoader.ts +++ b/packages/stryker-webpack-transpiler/src/compiler/ConfigLoader.ts @@ -2,18 +2,16 @@ import * as path from 'path'; import * as fs from 'fs'; import { Configuration } from 'webpack'; import { StrykerWebpackConfig } from '../WebpackTranspiler'; -import { getLogger, Logger } from 'stryker-api/logging'; import { isFunction } from 'lodash'; +import { tokens, commonTokens } from 'stryker-api/plugin'; +import { Logger } from 'stryker-api/logging'; +import { pluginTokens } from '../pluginTokens'; const PROGRESS_PLUGIN_NAME = 'ProgressPlugin'; export default class ConfigLoader { - private readonly log: Logger; - private readonly loader: NodeRequireFunction; - - public constructor(loader?: NodeRequireFunction) { - this.loader = loader || require; - this.log = getLogger(ConfigLoader.name); + public static inject = tokens(commonTokens.logger, pluginTokens.require); + public constructor(private readonly log: Logger, private readonly requireFn: NodeRequireFunction) { } public async load(config: StrykerWebpackConfig): Promise { @@ -35,15 +33,15 @@ export default class ConfigLoader { return webpackConfig; } -private loadWebpackConfigFromProjectRoot(configFileLocation: string) { - const resolvedName = path.resolve(configFileLocation); + private loadWebpackConfigFromProjectRoot(configFileLocation: string) { + const resolvedName = path.resolve(configFileLocation); - if (!fs.existsSync(resolvedName)) { - throw new Error(`Could not load webpack config at "${resolvedName}", file not found.`); - } + if (!fs.existsSync(resolvedName)) { + throw new Error(`Could not load webpack config at "${resolvedName}", file not found.`); + } - return this.loader(resolvedName); -} + return this.requireFn(resolvedName); + } private configureSilent(webpackConfig: Configuration) { if (webpackConfig.plugins) { diff --git a/packages/stryker-webpack-transpiler/src/index.ts b/packages/stryker-webpack-transpiler/src/index.ts index 66430db1a0..5b7e39f398 100644 --- a/packages/stryker-webpack-transpiler/src/index.ts +++ b/packages/stryker-webpack-transpiler/src/index.ts @@ -1,4 +1,16 @@ -import { TranspilerFactory } from 'stryker-api/transpile'; +import { PluginKind, Injector, TranspilerPluginContext, tokens, commonTokens, declareFactoryPlugin } from 'stryker-api/plugin'; import WebpackTranspiler from './WebpackTranspiler'; +import ConfigLoader from './compiler/ConfigLoader'; +import { pluginTokens } from './pluginTokens'; -TranspilerFactory.instance().register('webpack', WebpackTranspiler); +export const strykerPlugins = [ + declareFactoryPlugin(PluginKind.Transpiler, 'webpack', webpackTranspilerFactory) +]; + +function webpackTranspilerFactory(injector: Injector) { + return injector + .provideValue(pluginTokens.require, require) + .provideClass(pluginTokens.configLoader, ConfigLoader) + .injectClass(WebpackTranspiler); +} +webpackTranspilerFactory.inject = tokens(commonTokens.injector); diff --git a/packages/stryker-webpack-transpiler/src/pluginTokens.ts b/packages/stryker-webpack-transpiler/src/pluginTokens.ts new file mode 100644 index 0000000000..ad4bfcb12e --- /dev/null +++ b/packages/stryker-webpack-transpiler/src/pluginTokens.ts @@ -0,0 +1,9 @@ + +function stringLiteral(literal: T) { + return literal; +} + +export const pluginTokens = Object.freeze({ + configLoader: stringLiteral('configLoader'), + require: stringLiteral('require') +}); diff --git a/packages/stryker-webpack-transpiler/test/helpers/globals.ts b/packages/stryker-webpack-transpiler/test/helpers/globals.ts deleted file mode 100644 index a07f65a506..0000000000 --- a/packages/stryker-webpack-transpiler/test/helpers/globals.ts +++ /dev/null @@ -1,15 +0,0 @@ -interface LogMock { - debug: sinon.SinonStub; - info: sinon.SinonStub; - warn: sinon.SinonStub; - error: sinon.SinonStub; -} - -declare const sandbox: sinon.SinonSandbox; -declare const logMock: LogMock; -declare namespace NodeJS { - export interface Global { - sandbox: sinon.SinonSandbox; - logMock: LogMock; - } -} diff --git a/packages/stryker-webpack-transpiler/test/helpers/sinonInit.ts b/packages/stryker-webpack-transpiler/test/helpers/sinonInit.ts index 2bc6a37b6f..28212caff1 100644 --- a/packages/stryker-webpack-transpiler/test/helpers/sinonInit.ts +++ b/packages/stryker-webpack-transpiler/test/helpers/sinonInit.ts @@ -1,17 +1,7 @@ import * as sinon from 'sinon'; -import * as logging from 'stryker-api/logging'; - -beforeEach(() => { - global.sandbox = sinon.createSandbox(); - global.logMock = { - debug: sandbox.stub(), - error: sandbox.stub(), - info: sandbox.stub(), - warn: sandbox.stub() - }; - sandbox.stub(logging, 'getLogger').returns(global.logMock); -}); +import { testInjector } from '../../../stryker-test-helpers/src'; afterEach(() => { - global.sandbox.restore(); + sinon.restore(); + testInjector.reset(); }); diff --git a/packages/stryker-webpack-transpiler/test/integration/transpiler.it.ts b/packages/stryker-webpack-transpiler/test/integration/transpiler.it.ts index 09b7a77ff8..bb0c5ea511 100644 --- a/packages/stryker-webpack-transpiler/test/integration/transpiler.it.ts +++ b/packages/stryker-webpack-transpiler/test/integration/transpiler.it.ts @@ -1,29 +1,22 @@ import * as path from 'path'; import * as fs from 'fs'; import WebpackTranspiler from '../../src/WebpackTranspiler'; -import { Config } from 'stryker-api/config'; import { expect } from 'chai'; import { File } from 'stryker-api/core'; -import { TranspilerOptions } from 'stryker-api/transpile'; +import { testInjector } from '@stryker-mutator/test-helpers'; +import { commonTokens } from 'stryker-api/plugin'; +import ConfigLoader from '../../src/compiler/ConfigLoader'; +import { pluginTokens } from '../../src/pluginTokens'; describe('Webpack transpiler', () => { - let transpilerConfig: TranspilerOptions; - beforeEach(() => { - transpilerConfig = { produceSourceMaps: false, config: new Config() }; - transpilerConfig.config.set({ webpack: {} }); + testInjector.options.webpack = {}; }); - function readFiles(): File[] { - const dir = path.resolve(__dirname, '..', '..', 'testResources', 'gettingStarted', 'src'); - const files = fs.readdirSync(dir); - return files.map(fileName => new File(path.resolve(dir, fileName), fs.readFileSync(path.resolve(dir, fileName)))); - } - it('should be able to transpile the "gettingStarted" sample', async () => { - transpilerConfig.config.set({ webpack: { configFile: path.join(getProjectRoot('gettingStarted'), 'webpack.config.js') } }); - const sut = new WebpackTranspiler(transpilerConfig); + testInjector.options.webpack.configFile = path.join(getProjectRoot('gettingStarted'), 'webpack.config.js'); + const sut = createSut(); const files = readFiles(); const transpiledFiles = await sut.transpile(files); @@ -31,8 +24,8 @@ describe('Webpack transpiler', () => { }); it('should be able to transpile "zeroConfig" sample without a Webpack config file', async () => { - transpilerConfig.config.set({ webpack: { context: getProjectRoot('zeroConfig') } }); - const sut = new WebpackTranspiler(transpilerConfig); + testInjector.options.webpack.context = getProjectRoot('zeroConfig'); + const sut = createSut(); const files = readFiles(); const transpiledFiles = await sut.transpile(files); @@ -40,6 +33,20 @@ describe('Webpack transpiler', () => { }); }); +function createSut() { + return testInjector.injector + .provideValue(commonTokens.produceSourceMaps, false) + .provideValue(pluginTokens.require, require) + .provideClass(pluginTokens.configLoader, ConfigLoader) + .injectClass(WebpackTranspiler); +} + function getProjectRoot(testResourceProjectName: string) { return path.join(process.cwd(), 'testResources', testResourceProjectName); } + +function readFiles(): File[] { + const dir = path.resolve(__dirname, '..', '..', 'testResources', 'gettingStarted', 'src'); + const files = fs.readdirSync(dir); + return files.map(fileName => new File(path.resolve(dir, fileName), fs.readFileSync(path.resolve(dir, fileName)))); +} diff --git a/packages/stryker-webpack-transpiler/test/unit/WebpackTranspilerSpec.ts b/packages/stryker-webpack-transpiler/test/unit/WebpackTranspilerSpec.ts index ffe809ce14..902b743970 100644 --- a/packages/stryker-webpack-transpiler/test/unit/WebpackTranspilerSpec.ts +++ b/packages/stryker-webpack-transpiler/test/unit/WebpackTranspilerSpec.ts @@ -1,15 +1,16 @@ import WebpackTranspiler from '../../src/WebpackTranspiler'; -import ConfigLoader, * as configLoaderModule from '../../src/compiler/ConfigLoader'; +import ConfigLoader from '../../src/compiler/ConfigLoader'; import WebpackCompiler, * as webpackCompilerModule from '../../src/compiler/WebpackCompiler'; import { createTextFile, Mock, createMockInstance, createStrykerWebpackConfig } from '../helpers/producers'; -import { Config } from 'stryker-api/config'; import { File } from 'stryker-api/core'; import { expect } from 'chai'; import { Configuration } from 'webpack'; +import { testInjector, factory } from '@stryker-mutator/test-helpers'; +import { commonTokens } from 'stryker-api/plugin'; +import * as sinon from 'sinon'; describe('WebpackTranspiler', () => { let webpackTranspiler: WebpackTranspiler; - let config: Config; // Stubs let configLoaderStub: Mock; @@ -26,20 +27,16 @@ describe('WebpackTranspiler', () => { configLoaderStub = createMockInstance(ConfigLoader); configLoaderStub.load.returns(webpackConfig); - sandbox.stub(configLoaderModule, 'default').returns(configLoaderStub); - sandbox.stub(webpackCompilerModule, 'default').returns(webpackCompilerStub); + sinon.stub(webpackCompilerModule, 'default').returns(webpackCompilerStub); - config = new Config(); - config.set({ webpack: { context: '/path/to/project/root' } }); + testInjector.options.webpack = { context: '/path/to/project/root' }; }); it('should only create the compiler once', async () => { - webpackTranspiler = new WebpackTranspiler({ config, produceSourceMaps: false }); + webpackTranspiler = createSut(); await webpackTranspiler.transpile([]); await webpackTranspiler.transpile([]); - expect(configLoaderModule.default).calledOnce; - expect(configLoaderModule.default).calledWithNew; expect(webpackCompilerModule.default).calledOnce; expect(webpackCompilerModule.default).calledWithNew; expect(configLoaderStub.load).calledOnce; @@ -47,12 +44,13 @@ describe('WebpackTranspiler', () => { }); it('should throw an error if `produceSourceMaps` is `true`', () => { - config.coverageAnalysis = 'perTest'; - expect(() => new WebpackTranspiler({ config, produceSourceMaps: true })).throws('Invalid `coverageAnalysis` "perTest" is not supported by the stryker-webpack-transpiler (yet). It is not able to produce source maps yet. Please set it "coverageAnalysis" to "off"'); + testInjector.options.coverageAnalysis = 'perTest'; + expect(() => new WebpackTranspiler(factory.strykerOptions({ coverageAnalysis: 'perTest' }), true, configLoaderStub as unknown as ConfigLoader)) + .throws('Invalid `coverageAnalysis` "perTest" is not supported by the stryker-webpack-transpiler (yet). It is not able to produce source maps yet. Please set it "coverageAnalysis" to "off"'); }); it('should call the webpackCompiler.writeFilesToFs function with the given files', async () => { - webpackTranspiler = new WebpackTranspiler({ config, produceSourceMaps: false }); + webpackTranspiler = createSut(); const files = [createTextFile('main.js'), createTextFile('sum.js'), createTextFile('divide.js')]; await webpackTranspiler.transpile(files); @@ -61,7 +59,7 @@ describe('WebpackTranspiler', () => { }); it('should call the webpackCompiler.emit function to get the new bundled files', async () => { - webpackTranspiler = new WebpackTranspiler({ config, produceSourceMaps: false }); + webpackTranspiler = createSut(); await webpackTranspiler.transpile([]); expect(webpackCompilerStub.emit).called; @@ -69,16 +67,23 @@ describe('WebpackTranspiler', () => { }); it('should return all files (input and output) on success', async () => { - webpackTranspiler = new WebpackTranspiler({ config, produceSourceMaps: false }); + webpackTranspiler = createSut(); const input = new File('input.js', ''); const transpiledFiles = await webpackTranspiler.transpile([input]); expect(transpiledFiles).to.deep.equal([input, exampleBundleFile]); }); it('should return a error result when an error occurred', async () => { - webpackTranspiler = new WebpackTranspiler({ config, produceSourceMaps: false }); + webpackTranspiler = createSut(); const fakeError = new Error('compiler could not compile input files'); webpackCompilerStub.emit.throwsException(fakeError); expect(webpackTranspiler.transpile([])).rejectedWith(fakeError); }); + + function createSut() { + return testInjector.injector + .provideValue(commonTokens.produceSourceMaps, false) + .provideValue('configLoader', configLoaderStub as unknown as ConfigLoader) + .injectClass(WebpackTranspiler); + } }); diff --git a/packages/stryker-webpack-transpiler/test/unit/compiler/ConfigLoaderSpec.ts b/packages/stryker-webpack-transpiler/test/unit/compiler/ConfigLoaderSpec.ts index 5ae1b80530..c04b0bd8e5 100644 --- a/packages/stryker-webpack-transpiler/test/unit/compiler/ConfigLoaderSpec.ts +++ b/packages/stryker-webpack-transpiler/test/unit/compiler/ConfigLoaderSpec.ts @@ -1,9 +1,12 @@ import * as fs from 'fs'; import * as path from 'path'; -import { expect, assert } from 'chai'; +import * as sinon from 'sinon'; +import { expect } from 'chai'; import ConfigLoader from '../../../src/compiler/ConfigLoader'; import { Configuration, Plugin } from 'webpack'; import { createStrykerWebpackConfig } from '../../helpers/producers'; +import { testInjector } from '../../../../stryker-test-helpers/src'; +import { pluginTokens } from '../../../src/pluginTokens'; class FooPlugin implements Plugin { public foo = true; public apply() { } } class ProgressPlugin implements Plugin { public apply() { } } @@ -15,10 +18,12 @@ describe('ConfigLoader', () => { let existsSyncStub: sinon.SinonStub; beforeEach(() => { - requireStub = sandbox.stub(); - existsSyncStub = sandbox.stub(fs, 'existsSync'); + requireStub = sinon.stub(); + existsSyncStub = sinon.stub(fs, 'existsSync'); - sut = new ConfigLoader(requireStub); + sut = testInjector.injector + .provideValue(pluginTokens.require, requireStub) + .injectClass(ConfigLoader); }); it('should load webpack config from given location', async () => { @@ -31,7 +36,7 @@ describe('ConfigLoader', () => { }); it('should call function with configFileArgs if webpack config file exports a function', async () => { - const configFunctionStub = sandbox.stub(); + const configFunctionStub = sinon.stub(); configFunctionStub.returns('webpackconfig'); requireStub.returns(configFunctionStub); existsSyncStub.returns(true); @@ -58,7 +63,7 @@ describe('ConfigLoader', () => { // Assert expect(result.plugins).to.be.an('array').that.does.not.deep.include(new ProgressPlugin()); expect(result.plugins).to.be.an('array').that.deep.equals([new FooPlugin(), new BarPlugin(), bazPlugin]); - expect(logMock.debug).calledWith('Removing webpack plugin "%s" to keep webpack bundling silent. Set `webpack: { silent: false }` in your stryker.conf.js file to disable this feature.', 'ProgressPlugin'); + expect(testInjector.logger.debug).calledWith('Removing webpack plugin "%s" to keep webpack bundling silent. Set `webpack: { silent: false }` in your stryker.conf.js file to disable this feature.', 'ProgressPlugin'); }); it('should not remove "ProgressPlugin" if silent is `false`', async () => { @@ -88,13 +93,8 @@ describe('ConfigLoader', () => { existsSyncStub.returns(false); - try { - await sut.load(createStrykerWebpackConfig({ configFile })); - - expect.fail('WebpackConfigLoader should throw an error'); - } catch (e) { - expect(e.message).to.equal(`Could not load webpack config at "${path.resolve(configFile)}", file not found.`); - } + return expect(sut.load(createStrykerWebpackConfig({ configFile }))) + .rejectedWith(`Could not load webpack config at "${path.resolve(configFile)}", file not found.`); }); it('should log a debug message when the Webpack configuration is not found and it\'s trying webpack 4 zero config instead', async () => { @@ -104,7 +104,7 @@ describe('ConfigLoader', () => { await sut.load(createStrykerWebpackConfig({ context: contextPath })); - assert(logMock.debug.calledWith('Webpack config "%s" not found, trying Webpack 4 zero config')); + expect(testInjector.logger.debug).calledWith('Webpack config "%s" not found, trying Webpack 4 zero config'); }); it('should be able to load a webpack configuration asynchonously via a promise', async () => { diff --git a/packages/stryker-webpack-transpiler/test/unit/compiler/WebpackCompilerSpec.ts b/packages/stryker-webpack-transpiler/test/unit/compiler/WebpackCompilerSpec.ts index cb6ddb6551..a52c0711b8 100644 --- a/packages/stryker-webpack-transpiler/test/unit/compiler/WebpackCompilerSpec.ts +++ b/packages/stryker-webpack-transpiler/test/unit/compiler/WebpackCompilerSpec.ts @@ -7,6 +7,7 @@ import OutputFileSystem from '../../../src/fs/OutputFileSystem'; import WebpackCompiler from '../../../src/compiler/WebpackCompiler'; import * as webpack from '../../../src/compiler/Webpack'; import { Configuration } from 'webpack'; +import * as sinon from 'sinon'; describe('WebpackCompiler', () => { let sut: WebpackCompiler; @@ -20,7 +21,7 @@ describe('WebpackCompiler', () => { outputFileSystemMock = createMockInstance(OutputFileSystem); webpackCompilerMock = createWebpackMock(); fakeWebpackConfig = createFakeWebpackConfig(); - sandbox.stub(webpack, 'default').returns(webpackCompilerMock); + sinon.stub(webpack, 'default').returns(webpackCompilerMock); }); describe('writeFilesToFs', () => { @@ -44,7 +45,7 @@ describe('WebpackCompiler', () => { beforeEach(() => { sut = new WebpackCompiler(fakeWebpackConfig, inputFileSystemMock as any, outputFileSystemMock as any); - webpackRunStub = sandbox.stub(webpackCompilerMock, 'run').callsArgWith(0, null, { hasErrors: () => false }); + webpackRunStub = sinon.stub(webpackCompilerMock, 'run').callsArgWith(0, null, { hasErrors: () => false }); }); it('should call the run function on the webpack compiler', async () => { diff --git a/packages/stryker-webpack-transpiler/test/unit/fs/InputFileSystemSpec.ts b/packages/stryker-webpack-transpiler/test/unit/fs/InputFileSystemSpec.ts index 353db3bd63..5a6441e5f8 100644 --- a/packages/stryker-webpack-transpiler/test/unit/fs/InputFileSystemSpec.ts +++ b/packages/stryker-webpack-transpiler/test/unit/fs/InputFileSystemSpec.ts @@ -3,6 +3,7 @@ import MemoryFS, * as memoryFSModule from '../../../src/fs/MemoryFS'; import InputFileSystem from '../../../src/fs/InputFileSystem'; import { Mock, createMockInstance } from '../../helpers/producers'; import { CachedInputFileSystem } from 'enhanced-resolve'; +import * as sinon from 'sinon'; describe('InputFileSystem', () => { let sut: InputFileSystem; @@ -12,7 +13,7 @@ describe('InputFileSystem', () => { beforeEach(() => { memoryFSMock = createMockInstance(MemoryFS); innerFSMock = createMockInstance(CachedInputFileSystem); - sandbox.stub(memoryFSModule, 'default').returns(memoryFSMock); + sinon.stub(memoryFSModule, 'default').returns(memoryFSMock); sut = new InputFileSystem(innerFSMock); }); diff --git a/packages/stryker-webpack-transpiler/tsconfig.test.json b/packages/stryker-webpack-transpiler/tsconfig.test.json index 478c7ce29c..33819c23e0 100644 --- a/packages/stryker-webpack-transpiler/tsconfig.test.json +++ b/packages/stryker-webpack-transpiler/tsconfig.test.json @@ -14,6 +14,9 @@ "references": [ { "path": "tsconfig.src.json" + }, + { + "path": "../stryker-test-helpers/tsconfig.src.json" } ] } \ No newline at end of file diff --git a/packages/stryker/.vscode/launch.json b/packages/stryker/.vscode/launch.json index 5d87b722b7..5cedf66b6b 100644 --- a/packages/stryker/.vscode/launch.json +++ b/packages/stryker/.vscode/launch.json @@ -21,6 +21,21 @@ "${workspaceRoot}/src/**/*.js" ] }, + { + "type": "node", + "request": "launch", + "name": "e2e command runner", + "program": "${workspaceRoot}/../../e2e/node_modules/stryker/bin/stryker", + "args": [ + "run" + ], + "cwd": "${workspaceFolder}/../../e2e/test/command", + "internalConsoleOptions": "openOnSessionStart", + "outFiles": [ + "${workspaceRoot}/test/**/*.js", + "${workspaceRoot}/src/**/*.js" + ] + }, { "type": "node", "request": "launch", diff --git a/packages/stryker/package.json b/packages/stryker/package.json index dcd6bbc94a..bee632867d 100644 --- a/packages/stryker/package.json +++ b/packages/stryker/package.json @@ -69,10 +69,10 @@ "rimraf": "~2.6.1", "rxjs": "~6.3.0", "source-map": "~0.6.1", - "surrial": "~0.1.3", + "surrial": "^0.2.0", "tree-kill": "~1.2.0", "tslib": "~1.9.3", - "typed-inject": "^0.1.1", + "typed-inject": "^0.2.0", "typed-rest-client": "~1.0.7" }, "devDependencies": { diff --git a/packages/stryker/src/Sandbox.ts b/packages/stryker/src/Sandbox.ts index bb5584a2ed..d67b91221a 100644 --- a/packages/stryker/src/Sandbox.ts +++ b/packages/stryker/src/Sandbox.ts @@ -1,4 +1,4 @@ -import { Config } from 'stryker-api/config'; +import { StrykerOptions } from 'stryker-api/core'; import * as path from 'path'; import { getLogger } from 'stryker-api/logging'; import * as mkdirp from 'mkdirp'; @@ -26,7 +26,7 @@ export default class Sandbox { private readonly files: File[]; private readonly workingDirectory: string; - private constructor(private readonly options: Config, private readonly index: number, files: ReadonlyArray, private readonly testFramework: TestFramework | null, private readonly timeOverheadMS: number, private readonly loggingContext: LoggingClientContext) { + private constructor(private readonly options: StrykerOptions, private readonly index: number, files: ReadonlyArray, private readonly testFramework: TestFramework | null, private readonly timeOverheadMS: number, private readonly loggingContext: LoggingClientContext) { this.workingDirectory = TempFolder.instance().createRandomFolder('sandbox'); this.log.debug('Creating a sandbox for files in %s', this.workingDirectory); this.files = files.slice(); // Create a copy @@ -38,7 +38,7 @@ export default class Sandbox { return this.initializeTestRunner(); } - public static create(options: Config, index: number, files: ReadonlyArray, testFramework: TestFramework | null, timeoutOverheadMS: number, loggingContext: LoggingClientContext) + public static create(options: StrykerOptions, index: number, files: ReadonlyArray, testFramework: TestFramework | null, timeoutOverheadMS: number, loggingContext: LoggingClientContext) : Promise { const sandbox = new Sandbox(options, index, files, testFramework, timeoutOverheadMS, loggingContext); return sandbox.initialize().then(() => sandbox); diff --git a/packages/stryker/src/Stryker.ts b/packages/stryker/src/Stryker.ts index b6af029fe2..85178285cc 100644 --- a/packages/stryker/src/Stryker.ts +++ b/packages/stryker/src/Stryker.ts @@ -2,88 +2,75 @@ import { getLogger } from 'stryker-api/logging'; import { Config } from 'stryker-api/config'; import { StrykerOptions, MutatorDescriptor } from 'stryker-api/core'; import { MutantResult } from 'stryker-api/report'; -import { TestFramework } from 'stryker-api/test_framework'; import { Mutant } from 'stryker-api/mutant'; -import TestFrameworkOrchestrator from './TestFrameworkOrchestrator'; import MutantTestMatcher from './MutantTestMatcher'; import InputFileResolver from './input/InputFileResolver'; -import ConfigReader from './config/ConfigReader'; -import PluginLoader from './di/PluginLoader'; import ScoreResultCalculator from './ScoreResultCalculator'; -import ConfigValidator from './config/ConfigValidator'; -import { freezeRecursively, isPromise } from './utils/objectUtils'; +import { isPromise } from './utils/objectUtils'; import { TempFolder } from './utils/TempFolder'; -import Timer from './utils/Timer'; import MutatorFacade from './MutatorFacade'; import InitialTestExecutor, { InitialTestRunResult } from './process/InitialTestExecutor'; import MutationTestExecutor from './process/MutationTestExecutor'; import InputFileCollection from './input/InputFileCollection'; import LogConfigurator from './logging/LogConfigurator'; -import BroadcastReporter from './reporters/BroadcastReporter'; -import { commonTokens, OptionsContext, PluginResolver, PluginKind } from 'stryker-api/plugin'; -import * as coreTokens from './di/coreTokens'; -import { Injector, rootInjector, Scope } from 'typed-inject'; -import { loggerFactory } from './di/loggerFactory'; -import { PluginCreator } from './di/PluginCreator'; -import { ConfigEditorApplier } from './config/ConfigEditorApplier'; +import { Injector } from 'typed-inject'; +import { TranspilerFacade } from './transpiler/TranspilerFacade'; +import { coreTokens, MainContext, PluginCreator, buildMainInjector } from './di'; +import { commonTokens, PluginKind } from 'stryker-api/plugin'; export default class Stryker { - public config: Config; - private readonly timer = new Timer(); - private readonly reporter: BroadcastReporter; - private readonly testFramework: TestFramework | null; private readonly log = getLogger(Stryker.name); - private readonly injector: Injector; + private readonly injector: Injector; + + private get reporter() { + return this.injector.resolve(coreTokens.reporter); + } + + private get config(): Readonly { + return this.injector.resolve(commonTokens.config); + } + + private get timer() { + return this.injector.resolve(coreTokens.timer); + } /** * The Stryker mutation tester. * @constructor - * @param {Object} [options] - Optional options. + * @param {Object} [cliOptions] - Optional options. */ - constructor(options: Partial) { - LogConfigurator.configureMainProcess(options.logLevel, options.fileLogLevel, options.allowConsoleColors); - const configReader = new ConfigReader(options); - this.config = configReader.readConfig(); + constructor(cliOptions: Partial) { + LogConfigurator.configureMainProcess(cliOptions.logLevel, cliOptions.fileLogLevel, cliOptions.allowConsoleColors); + this.injector = buildMainInjector(cliOptions); // Log level may have changed - LogConfigurator.configureMainProcess(this.config.logLevel, this.config.fileLogLevel, this.config.allowConsoleColors); // logLevel could be changed - this.addDefaultPlugins(); - const pluginLoader = new PluginLoader(this.config.plugins); - pluginLoader.load(); - // Log level may have changed - LogConfigurator.configureMainProcess(this.config.logLevel, this.config.fileLogLevel, this.config.allowConsoleColors); // logLevel could be changed - const configEditorInjector = rootInjector - .provideValue(commonTokens.getLogger, getLogger) - .provideFactory(commonTokens.logger, loggerFactory, Scope.Transient) - .provideValue(commonTokens.pluginResolver, pluginLoader as PluginResolver); - configEditorInjector - .provideFactory(coreTokens.pluginCreatorConfigEditor, PluginCreator.createFactory(PluginKind.ConfigEditor)) - .injectClass(ConfigEditorApplier).edit(this.config); - this.freezeConfig(); - this.injector = configEditorInjector - .provideValue(commonTokens.config, this.config) - .provideValue(commonTokens.options, this.config as StrykerOptions); - this.testFramework = this.injector - .provideFactory(coreTokens.pluginCreatorTestFramework, PluginCreator.createFactory(PluginKind.TestFramework)) - .injectClass(TestFrameworkOrchestrator).determineTestFramework(); - this.reporter = this.injector - .provideFactory(coreTokens.pluginCreatorReporter, PluginCreator.createFactory(PluginKind.Reporter)) - .injectClass(BroadcastReporter); - new ConfigValidator(this.config, this.testFramework).validate(); + const options = this.config; + LogConfigurator.configureMainProcess(options.logLevel, options.fileLogLevel, options.allowConsoleColors); } public async runMutationTest(): Promise { const loggingContext = await LogConfigurator.configureLoggingServer(this.config.logLevel, this.config.fileLogLevel, this.config.allowConsoleColors); this.timer.reset(); - const inputFiles = await new InputFileResolver(this.config.mutate, this.config.files, this.reporter).resolve(); + const inputFiles = await this.injector.injectClass(InputFileResolver).resolve(); if (inputFiles.files.length) { TempFolder.instance().initialize(); - const initialTestRunProcess = new InitialTestExecutor(this.config, inputFiles, this.testFramework, this.timer, loggingContext); + const initialTestRunProcess = this.injector + .provideValue(coreTokens.inputFiles, inputFiles) + .provideValue(coreTokens.loggingContext, loggingContext) + .provideValue(commonTokens.produceSourceMaps, this.config.coverageAnalysis !== 'off') + .provideFactory(coreTokens.pluginCreatorTranspiler, PluginCreator.createFactory(PluginKind.Transpiler)) + .provideClass(coreTokens.transpiler, TranspilerFacade) + .injectClass(InitialTestExecutor); const initialTestRunResult = await initialTestRunProcess.run(); const testableMutants = await this.mutate(inputFiles, initialTestRunResult); if (initialTestRunResult.runResult.tests.length && testableMutants.length) { - const mutationTestExecutor = new MutationTestExecutor(this.config, inputFiles.files, this.testFramework, this.reporter, - initialTestRunResult.overheadTimeMS, loggingContext); + const mutationTestExecutor = new MutationTestExecutor( + this.config, + inputFiles.files, + this.injector.resolve(coreTokens.testFramework), + this.reporter, + initialTestRunResult.overheadTimeMS, + loggingContext); const mutantResults = await mutationTestExecutor.run(testableMutants); this.reportScore(mutantResults); await this.wrapUpReporter(); @@ -136,11 +123,6 @@ export default class Stryker { return mutants.filter(mutant => mutatorDescriptor.excludedMutations.indexOf(mutant.mutatorName) === -1); } } - public addDefaultPlugins(): void { - this.config.plugins.push( - require.resolve('./reporters') - ); - } private wrapUpReporter(): Promise { const maybePromise = this.reporter.wrapUp(); if (isPromise(maybePromise)) { @@ -150,20 +132,6 @@ export default class Stryker { } } - private freezeConfig() { - // A config class instance is not serializable using surrial. - // This is a temporary work around - // See https://github.com/stryker-mutator/stryker/issues/365 - const config: Config = {} as any; - for (const prop in this.config) { - config[prop] = this.config[prop]; - } - this.config = freezeRecursively(config); - if (this.log.isDebugEnabled()) { - this.log.debug(`Using config: ${JSON.stringify(this.config)}`); - } - } - private logDone() { this.log.info('Done in %s.', this.timer.humanReadableElapsed()); } diff --git a/packages/stryker/src/TestFrameworkOrchestrator.ts b/packages/stryker/src/TestFrameworkOrchestrator.ts index c5da3b4bf2..06b719ae14 100644 --- a/packages/stryker/src/TestFrameworkOrchestrator.ts +++ b/packages/stryker/src/TestFrameworkOrchestrator.ts @@ -2,7 +2,7 @@ import { TestFramework } from 'stryker-api/test_framework'; import { StrykerOptions } from 'stryker-api/core'; import { tokens, commonTokens, PluginKind } from 'stryker-api/plugin'; import { Logger } from 'stryker-api/logging'; -import * as coreTokens from './di/coreTokens'; +import { coreTokens } from './di'; import { PluginCreator } from './di/PluginCreator'; export default class TestFrameworkOrchestrator { diff --git a/packages/stryker/src/child-proxy/ChildProcessProxy.ts b/packages/stryker/src/child-proxy/ChildProcessProxy.ts index 9f57904a3d..3156443323 100644 --- a/packages/stryker/src/child-proxy/ChildProcessProxy.ts +++ b/packages/stryker/src/child-proxy/ChildProcessProxy.ts @@ -1,6 +1,6 @@ import * as os from 'os'; import { fork, ChildProcess } from 'child_process'; -import { File } from 'stryker-api/core'; +import { File, StrykerOptions } from 'stryker-api/core'; import { getLogger } from 'stryker-api/logging'; import { WorkerMessage, WorkerMessageKind, ParentMessage, autoStart, ParentMessageKind } from './messageProtocol'; import { serialize, deserialize, kill, padLeft } from '../utils/objectUtils'; @@ -10,13 +10,13 @@ import ChildProcessCrashedError from './ChildProcessCrashedError'; import { isErrnoException } from '@stryker-mutator/util'; import OutOfMemoryError from './OutOfMemoryError'; import StringBuilder from '../utils/StringBuilder'; +import { InjectionToken, InjectableClass } from 'typed-inject'; +import { OptionsContext } from 'stryker-api/plugin'; type Func = (...args: TS) => R; type PromisifiedFunc = (...args: TS) => Promise; -type Constructor = new (...args: TS) => T; - export type Promisified = { [K in keyof T]: T[K] extends PromisifiedFunc ? T[K] : T[K] extends Func ? PromisifiedFunc : () => Promise; }; @@ -37,15 +37,16 @@ export default class ChildProcessProxy { private readonly stdoutAndStderrBuilder = new StringBuilder(); private isDisposed = false; - private constructor(requirePath: string, loggingContext: LoggingClientContext, plugins: string[], workingDirectory: string, constructorParams: any[]) { + private constructor(requirePath: string, requireName: string, loggingContext: LoggingClientContext, options: StrykerOptions, additionalInjectableValues: unknown, workingDirectory: string) { this.worker = fork(require.resolve('./ChildProcessProxyWorker'), [autoStart], { silent: true, execArgv: [] }); this.initTask = new Task(); this.log.debug('Starting %s in child process %s', requirePath, this.worker.pid); this.send({ - constructorArgs: constructorParams, + additionalInjectableValues, kind: WorkerMessageKind.Init, loggingContext, - plugins, + options, + requireName, requirePath, workingDirectory }); @@ -62,13 +63,19 @@ export default class ChildProcessProxy { /** * @description Creates a proxy where each function of the object created using the constructorFunction arg is ran inside of a child process */ - public static create(requirePath: string, loggingContext: LoggingClientContext, plugins: string[], workingDirectory: string, _: Constructor, ...constructorArgs: TS): - ChildProcessProxy { - return new ChildProcessProxy(requirePath, loggingContext, plugins, workingDirectory, constructorArgs); + public static create[]>( + requirePath: string, + loggingContext: LoggingClientContext, + options: StrykerOptions, + additionalInjectableValues: TAdditionalContext, + workingDirectory: string, + InjectableClass: InjectableClass): + ChildProcessProxy { + return new ChildProcessProxy(requirePath, InjectableClass.name, loggingContext, options, additionalInjectableValues, workingDirectory); } private send(message: WorkerMessage) { - this.worker.send(serialize(message)); + this.worker.send(serialize(message, [File])); } private initProxy(): Promisified { diff --git a/packages/stryker/src/child-proxy/ChildProcessProxyWorker.ts b/packages/stryker/src/child-proxy/ChildProcessProxyWorker.ts index 54657cc677..61c62bedd8 100644 --- a/packages/stryker/src/child-proxy/ChildProcessProxyWorker.ts +++ b/packages/stryker/src/child-proxy/ChildProcessProxyWorker.ts @@ -4,8 +4,9 @@ import { File } from 'stryker-api/core'; import { serialize, deserialize } from '../utils/objectUtils'; import { errorToString } from '@stryker-mutator/util'; import { WorkerMessage, WorkerMessageKind, ParentMessage, autoStart, ParentMessageKind, CallMessage } from './messageProtocol'; -import PluginLoader from '../di/PluginLoader'; import LogConfigurator from '../logging/LogConfigurator'; +import { buildChildProcessInjector } from '../di'; +import { Config } from 'stryker-api/config'; export default class ChildProcessProxyWorker { @@ -21,10 +22,10 @@ export default class ChildProcessProxyWorker { private send(value: ParentMessage) { if (process.send) { - process.send(serialize(value)); + const str = serialize(value, [File]); + process.send(str); } } - private handleMessage(serializedMessage: string) { const message = deserialize(serializedMessage, [File]); switch (message.kind) { @@ -32,14 +33,18 @@ export default class ChildProcessProxyWorker { LogConfigurator.configureChildProcess(message.loggingContext); this.log = getLogger(ChildProcessProxyWorker.name); this.handlePromiseRejections(); - new PluginLoader(message.plugins).load(); - const RealSubjectClass = require(message.requirePath).default; + let injector = buildChildProcessInjector(message.options as unknown as Config); + const locals = message.additionalInjectableValues as any; + for (const token of Object.keys(locals)) { + injector = injector.provideValue(token, locals[token]); + } + const RealSubjectClass = require(message.requirePath)[message.requireName]; const workingDir = path.resolve(message.workingDirectory); if (process.cwd() !== workingDir) { this.log.debug(`Changing current working directory for this process to ${workingDir}`); process.chdir(workingDir); } - this.realSubject = new RealSubjectClass(...message.constructorArgs); + this.realSubject = injector.injectClass(RealSubjectClass); this.send({ kind: ParentMessageKind.Initialized }); this.removeAnyAdditionalMessageListeners(this.handleMessage); break; diff --git a/packages/stryker/src/child-proxy/messageProtocol.ts b/packages/stryker/src/child-proxy/messageProtocol.ts index ab1648c11f..9dbfeebf2e 100644 --- a/packages/stryker/src/child-proxy/messageProtocol.ts +++ b/packages/stryker/src/child-proxy/messageProtocol.ts @@ -1,4 +1,5 @@ import LoggingClientContext from '../logging/LoggingClientContext'; +import { StrykerOptions } from 'stryker-api/core'; export enum WorkerMessageKind { 'Init', @@ -23,10 +24,11 @@ export const autoStart = 'childProcessAutoStart12937129s7d'; export interface InitMessage { kind: WorkerMessageKind.Init; loggingContext: LoggingClientContext; - plugins: string[]; + options: StrykerOptions; workingDirectory: string; + requireName: string; requirePath: string; - constructorArgs: any[]; + additionalInjectableValues: unknown; } export interface DisposeMessage { kind: WorkerMessageKind.Dispose; } diff --git a/packages/stryker/src/config/ConfigEditorApplier.ts b/packages/stryker/src/config/ConfigEditorApplier.ts index 63fc820222..f9262c047a 100644 --- a/packages/stryker/src/config/ConfigEditorApplier.ts +++ b/packages/stryker/src/config/ConfigEditorApplier.ts @@ -2,7 +2,7 @@ import { Config, ConfigEditor } from 'stryker-api/config'; import { tokens } from 'typed-inject'; import { PluginResolver, PluginKind, commonTokens } from 'stryker-api/plugin'; import { PluginCreator } from '../di/PluginCreator'; -import * as coreTokens from '../di/coreTokens'; +import { coreTokens } from '../di'; /** * Class that applies all config editor plugins diff --git a/packages/stryker/src/config/ConfigReader.ts b/packages/stryker/src/config/ConfigReader.ts index e960164092..e39b115966 100644 --- a/packages/stryker/src/config/ConfigReader.ts +++ b/packages/stryker/src/config/ConfigReader.ts @@ -5,6 +5,8 @@ import { Config } from 'stryker-api/config'; import { StrykerOptions } from 'stryker-api/core'; import { getLogger } from 'stryker-api/logging'; import { StrykerError } from '@stryker-mutator/util'; +import { coreTokens } from '../di'; +import { tokens } from 'stryker-api/plugin'; export const CONFIG_SYNTAX_HELP = ' module.exports = function(config) {\n' + ' config.set({\n' + @@ -18,6 +20,7 @@ export default class ConfigReader { private readonly log = getLogger(ConfigReader.name); + public static inject = tokens(coreTokens.cliOptions); constructor(private readonly cliOptions: Partial) { } public readConfig() { @@ -51,7 +54,7 @@ export default class ConfigReader { private loadConfigModule(): Function { // Dummy module to be returned if no config file is loaded. - let configModule: Function = () => {}; + let configModule: Function = () => { }; if (!this.cliOptions.configFile) { try { diff --git a/packages/stryker/src/config/ConfigValidator.ts b/packages/stryker/src/config/ConfigValidator.ts index 277173d51c..bdd13d4bed 100644 --- a/packages/stryker/src/config/ConfigValidator.ts +++ b/packages/stryker/src/config/ConfigValidator.ts @@ -4,12 +4,15 @@ import { Config } from 'stryker-api/config'; import { getLogger } from 'stryker-api/logging'; import { StrykerError } from '@stryker-mutator/util'; import { normalizeWhiteSpaces } from '../utils/objectUtils'; +import { tokens, commonTokens } from 'stryker-api/plugin'; +import { coreTokens } from '../di'; export default class ConfigValidator { private isValid = true; private readonly log = getLogger(ConfigValidator.name); - constructor(private readonly strykerConfig: StrykerOptions, private readonly testFramework: TestFramework | null) { } + public static inject = tokens(commonTokens.options, coreTokens.testFramework); + constructor(private readonly strykerConfig: Readonly, private readonly testFramework: TestFramework | null) { } public validate() { this.validateTestFramework(); diff --git a/packages/stryker/src/config/configFactory.ts b/packages/stryker/src/config/configFactory.ts new file mode 100644 index 0000000000..a6d6d29ef0 --- /dev/null +++ b/packages/stryker/src/config/configFactory.ts @@ -0,0 +1,17 @@ +import { ConfigEditorApplier } from './ConfigEditorApplier'; +import ConfigReader from './ConfigReader'; +import { freezeRecursively } from '../utils/objectUtils'; +import { tokens } from 'stryker-api/plugin'; +import { coreTokens } from '../di'; +import { Config } from 'stryker-api/config'; + +export function readConfig(configReader: ConfigReader) { + return configReader.readConfig(); +} +readConfig.inject = tokens(coreTokens.configReader); + +export function configFactory(config: Config, configEditorApplier: ConfigEditorApplier) { + configEditorApplier.edit(config); + return freezeRecursively(config); +} +configFactory.inject = tokens(coreTokens.configReadFromConfigFile, coreTokens.configEditorApplier); diff --git a/packages/stryker/src/config/index.ts b/packages/stryker/src/config/index.ts new file mode 100644 index 0000000000..205d110b90 --- /dev/null +++ b/packages/stryker/src/config/index.ts @@ -0,0 +1,2 @@ +export { ConfigEditorApplier } from './ConfigEditorApplier'; +export * from './configFactory'; diff --git a/packages/stryker/src/di/PluginCreator.ts b/packages/stryker/src/di/PluginCreator.ts index 96430b7961..0c8cd623e0 100644 --- a/packages/stryker/src/di/PluginCreator.ts +++ b/packages/stryker/src/di/PluginCreator.ts @@ -29,9 +29,9 @@ export class PluginCreator { return !!(plugin as ClassPlugin[]>).injectableClass; } - public static createFactory(kind: TPluginKind) - : InjectableFunctionWithInject, [typeof commonTokens.pluginResolver, typeof commonTokens.injector]> { - function factory(pluginResolver: PluginResolver, injector: Injector): PluginCreator { + public static createFactory(kind: TPluginKind) + : InjectableFunctionWithInject, [typeof commonTokens.pluginResolver, typeof commonTokens.injector]> { + function factory(pluginResolver: PluginResolver, injector: Injector): PluginCreator { return new PluginCreator(kind, pluginResolver, injector); } factory.inject = tokens(commonTokens.pluginResolver, commonTokens.injector); diff --git a/packages/stryker/src/di/PluginLoader.ts b/packages/stryker/src/di/PluginLoader.ts index 50bd8b9958..a217b0eb0c 100644 --- a/packages/stryker/src/di/PluginLoader.ts +++ b/packages/stryker/src/di/PluginLoader.ts @@ -12,6 +12,7 @@ import { TestFrameworkFactory } from 'stryker-api/test_framework'; import { TestRunnerFactory } from 'stryker-api/test_runner'; import { TranspilerFactory } from 'stryker-api/transpile'; import { MutatorFactory } from 'stryker-api/mutant'; +import * as coreTokens from './coreTokens'; const IGNORED_PACKAGES = ['stryker-cli', 'stryker-api']; @@ -19,11 +20,12 @@ interface PluginModule { strykerPlugins: Plugin[]; } -export default class PluginLoader implements PluginResolver { +export class PluginLoader implements PluginResolver { private readonly log = getLogger(PluginLoader.name); private readonly pluginsByKind: Map[]> = new Map(); - constructor(private readonly pluginDescriptors: string[]) { } + public static inject = tokens(coreTokens.pluginDescriptors); + constructor(private readonly pluginDescriptors: ReadonlyArray) { } public load() { this.resolvePluginModules().forEach(moduleName => this.requirePlugin(moduleName)); diff --git a/packages/stryker/src/di/buildChildProcessInjector.ts b/packages/stryker/src/di/buildChildProcessInjector.ts new file mode 100644 index 0000000000..27d3178424 --- /dev/null +++ b/packages/stryker/src/di/buildChildProcessInjector.ts @@ -0,0 +1,21 @@ +import { Config } from 'stryker-api/config'; +import { commonTokens, Scope, Injector, OptionsContext, tokens } from 'stryker-api/plugin'; +import { optionsFactory, pluginResolverFactory, loggerFactory } from './factoryMethods'; +import { coreTokens } from '.'; +import { getLogger } from 'stryker-api/logging'; +import { rootInjector } from 'typed-inject'; + +export function buildChildProcessInjector(config: Config): Injector { + return rootInjector + .provideValue(commonTokens.config, config) + .provideFactory(commonTokens.options, optionsFactory) + .provideValue(commonTokens.getLogger, getLogger) + .provideFactory(commonTokens.logger, loggerFactory, Scope.Transient) + .provideFactory(coreTokens.pluginDescriptors, pluginDescriptorsFactory) + .provideFactory(commonTokens.pluginResolver, pluginResolverFactory); +} + +function pluginDescriptorsFactory(config: Config): ReadonlyArray { + return config.plugins; +} +pluginDescriptorsFactory.inject = tokens(commonTokens.config); diff --git a/packages/stryker/src/di/buildMainInjector.ts b/packages/stryker/src/di/buildMainInjector.ts new file mode 100644 index 0000000000..ac066dcc4e --- /dev/null +++ b/packages/stryker/src/di/buildMainInjector.ts @@ -0,0 +1,50 @@ +import { coreTokens, PluginCreator } from '.'; +import { commonTokens, Injector, OptionsContext, PluginKind, Scope, tokens } from 'stryker-api/plugin'; +import { StrykerOptions } from 'stryker-api/core'; +import { Reporter } from 'stryker-api/report'; +import { TestFramework } from 'stryker-api/test_framework'; +import { rootInjector } from 'typed-inject'; +import { getLogger } from 'stryker-api/logging'; +import { loggerFactory, pluginResolverFactory, optionsFactory, testFrameworkFactory } from './factoryMethods'; +import { ConfigEditorApplier, configFactory, readConfig } from '../config'; +import BroadcastReporter from '../reporters/BroadcastReporter'; +import { Config } from 'stryker-api/config'; +import ConfigReader from '../config/ConfigReader'; +import Timer from '../utils/Timer'; + +export interface MainContext extends OptionsContext { + [coreTokens.reporter]: Required; + [coreTokens.testFramework]: TestFramework | null; + [coreTokens.pluginCreatorReporter]: PluginCreator; + [coreTokens.pluginCreatorConfigEditor]: PluginCreator; + [coreTokens.pluginCreatorTestFramework]: PluginCreator; + [coreTokens.timer]: Timer; +} + +export function buildMainInjector(cliOptions: Partial): Injector { + return rootInjector + .provideValue(commonTokens.getLogger, getLogger) + .provideFactory(commonTokens.logger, loggerFactory, Scope.Transient) + .provideValue(coreTokens.cliOptions, cliOptions) + .provideClass(coreTokens.configReader, ConfigReader) + .provideFactory(coreTokens.configReadFromConfigFile, readConfig) + .provideFactory(coreTokens.pluginDescriptors, pluginDescriptorsFactory) + .provideFactory(commonTokens.pluginResolver, pluginResolverFactory) + .provideFactory(coreTokens.pluginCreatorConfigEditor, PluginCreator.createFactory(PluginKind.ConfigEditor)) + .provideClass(coreTokens.configEditorApplier, ConfigEditorApplier) + .provideFactory(commonTokens.config, configFactory) + .provideFactory(commonTokens.options, optionsFactory) + .provideFactory(coreTokens.pluginCreatorReporter, PluginCreator.createFactory(PluginKind.Reporter)) + .provideFactory(coreTokens.pluginCreatorTestFramework, PluginCreator.createFactory(PluginKind.TestFramework)) + .provideClass(coreTokens.reporter, BroadcastReporter) + .provideFactory(coreTokens.testFramework, testFrameworkFactory) + .provideClass(coreTokens.timer, Timer); +} + +function pluginDescriptorsFactory(config: Config): ReadonlyArray { + config.plugins.push( + require.resolve('../reporters') + ); + return config.plugins; +} +pluginDescriptorsFactory.inject = tokens(coreTokens.configReadFromConfigFile); diff --git a/packages/stryker/src/di/coreTokens.ts b/packages/stryker/src/di/coreTokens.ts index f36435b9a8..0893257383 100644 --- a/packages/stryker/src/di/coreTokens.ts +++ b/packages/stryker/src/di/coreTokens.ts @@ -1,4 +1,17 @@ +export const cliOptions = 'cliOptions'; +export const configReader = 'configReader'; +export const configReadFromConfigFile = 'configReadFromConfigFile'; +export const configEditorApplier = 'configEditorApplier'; +export const inputFiles = 'inputFiles'; +export const testFramework = 'testFramework'; +export const timer = 'timer'; +export const loggingContext = 'loggingContext'; +export const transpiler = 'transpiler'; +export const reporter = 'reporter'; export const pluginKind = 'pluginKind'; -export const pluginCreatorTestFramework = 'pluginCreatorTestFramework'; -export const pluginCreatorConfigEditor = 'pluginCreatorConfigEditor'; +export const pluginDescriptors = 'pluginDescriptors'; export const pluginCreatorReporter = 'pluginCreatorReporter'; +export const pluginCreatorConfigEditor = 'pluginCreatorConfigEditor'; +export const pluginCreatorTranspiler = 'pluginCreatorTranspiler'; +export const pluginCreatorTestRunner = 'pluginCreatorTestRunner'; +export const pluginCreatorTestFramework = 'pluginCreatorTestFramework'; diff --git a/packages/stryker/src/di/createPlugin.ts b/packages/stryker/src/di/createPlugin.ts deleted file mode 100644 index 53f8d62b05..0000000000 --- a/packages/stryker/src/di/createPlugin.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { Plugin, PluginKind, PluginContexts, Plugins, PluginInterfaces, FactoryPlugin, ClassPlugin } from 'stryker-api/plugin'; -import { Injector, InjectionToken } from 'typed-inject'; - -export function createPlugin(kind: TPluginKind, plugin: Plugins[TPluginKind], injector: Injector): - PluginInterfaces[TPluginKind] { - if (isFactoryPlugin(plugin)) { - return injector.injectFunction(plugin.factory); - } else if (isClassPlugin(plugin)) { - return injector.injectClass(plugin.injectableClass); - } else { - throw new Error(`Plugin "${kind}:${plugin.name}" could not be created, missing "factory" or "injectableClass" property.`); - } -} - -function isFactoryPlugin(plugin: Plugin[]>): - plugin is FactoryPlugin[]> { - return !!(plugin as FactoryPlugin[]>).factory; -} -function isClassPlugin(plugin: Plugin[]>): - plugin is ClassPlugin[]> { - return !!(plugin as ClassPlugin[]>).injectableClass; -} diff --git a/packages/stryker/src/di/factoryMethods.ts b/packages/stryker/src/di/factoryMethods.ts new file mode 100644 index 0000000000..d24b24b9ee --- /dev/null +++ b/packages/stryker/src/di/factoryMethods.ts @@ -0,0 +1,27 @@ +import { tokens, commonTokens, OptionsContext, Injector, PluginKind, PluginResolver } from 'stryker-api/plugin'; +import TestFrameworkOrchestrator from '../TestFrameworkOrchestrator'; +import { coreTokens, PluginCreator, PluginLoader } from '.'; +import { LoggerFactoryMethod } from 'stryker-api/logging'; +import { Config } from 'stryker-api/config'; + +export function pluginResolverFactory(injector: Injector<{ [coreTokens.pluginDescriptors]: ReadonlyArray }>): PluginResolver { + const pluginLoader = injector.injectClass(PluginLoader); + pluginLoader.load(); + return pluginLoader; +} +pluginResolverFactory.inject = tokens(commonTokens.injector); + +export function testFrameworkFactory(injector: Injector }>) { + return injector.injectClass(TestFrameworkOrchestrator).determineTestFramework(); +} +testFrameworkFactory.inject = tokens(commonTokens.injector); + +export function loggerFactory(getLogger: LoggerFactoryMethod, target: Function | undefined) { + return getLogger(target ? target.name : 'UNKNOWN'); +} +loggerFactory.inject = tokens(commonTokens.getLogger, commonTokens.target); + +export function optionsFactory(config: Config) { + return config; +} +optionsFactory.inject = tokens(commonTokens.config); diff --git a/packages/stryker/src/di/index.ts b/packages/stryker/src/di/index.ts new file mode 100644 index 0000000000..a595a548cb --- /dev/null +++ b/packages/stryker/src/di/index.ts @@ -0,0 +1,6 @@ +import * as coreTokens from './coreTokens'; +export * from './buildMainInjector'; +export * from './buildChildProcessInjector'; +export * from './PluginCreator'; +export * from './PluginLoader'; +export { coreTokens }; diff --git a/packages/stryker/src/di/loggerFactory.ts b/packages/stryker/src/di/loggerFactory.ts deleted file mode 100644 index 52e55eb18f..0000000000 --- a/packages/stryker/src/di/loggerFactory.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { LoggerFactoryMethod } from 'stryker-api/logging'; -import { TARGET_TOKEN, tokens } from 'typed-inject'; -import { commonTokens } from 'stryker-api/plugin'; - -export function loggerFactory(getLogger: LoggerFactoryMethod, target: Function | undefined) { - return getLogger(target ? target.name : 'UNKNOWN'); -} -loggerFactory.inject = tokens(commonTokens.getLogger, TARGET_TOKEN); diff --git a/packages/stryker/src/input/InputFileResolver.ts b/packages/stryker/src/input/InputFileResolver.ts index e44d7fc4df..a2ade6751d 100644 --- a/packages/stryker/src/input/InputFileResolver.ts +++ b/packages/stryker/src/input/InputFileResolver.ts @@ -2,7 +2,7 @@ import * as path from 'path'; import { fsAsPromised, isErrnoException } from '@stryker-mutator/util'; import { childProcessAsPromised } from '@stryker-mutator/util'; import { getLogger } from 'stryker-api/logging'; -import { File } from 'stryker-api/core'; +import { File, StrykerOptions } from 'stryker-api/core'; import { glob } from '../utils/fileUtils'; import StrictReporter from '../reporters/StrictReporter'; import { SourceFile } from 'stryker-api/report'; @@ -10,6 +10,8 @@ import { StrykerError } from '@stryker-mutator/util'; import InputFileCollection from './InputFileCollection'; import { normalizeWhiteSpaces, filterEmpty } from '../utils/objectUtils'; import { Config } from 'stryker-api/config'; +import { tokens, commonTokens } from 'stryker-api/plugin'; +import { coreTokens } from '../di'; function toReportSourceFile(file: File): SourceFile { return { @@ -25,9 +27,9 @@ export default class InputFileResolver { private readonly mutatePatterns: ReadonlyArray; private readonly filePatterns: ReadonlyArray | undefined; + public static inject = tokens(commonTokens.options, coreTokens.reporter); constructor( - mutate: string[], - files: string[] | undefined, + { mutate, files }: StrykerOptions, private readonly reporter: StrictReporter ) { this.mutatePatterns = this.normalize(mutate || []); diff --git a/packages/stryker/src/process/InitialTestExecutor.ts b/packages/stryker/src/process/InitialTestExecutor.ts index 068cd2cfb0..05a98a2c19 100644 --- a/packages/stryker/src/process/InitialTestExecutor.ts +++ b/packages/stryker/src/process/InitialTestExecutor.ts @@ -1,11 +1,9 @@ import { EOL } from 'os'; import { RunStatus, RunResult, TestResult, TestStatus } from 'stryker-api/test_runner'; import { TestFramework } from 'stryker-api/test_framework'; -import { Config } from 'stryker-api/config'; -import { TranspilerOptions, Transpiler } from 'stryker-api/transpile'; -import { File } from 'stryker-api/core'; -import TranspilerFacade from '../transpiler/TranspilerFacade'; -import { getLogger } from 'stryker-api/logging'; +import { Transpiler } from 'stryker-api/transpile'; +import { File, StrykerOptions } from 'stryker-api/core'; +import { Logger } from 'stryker-api/logging'; import Sandbox from '../Sandbox'; import Timer from '../utils/Timer'; import CoverageInstrumenterTranspiler, { CoverageMapsByFile } from '../transpiler/CoverageInstrumenterTranspiler'; @@ -13,6 +11,8 @@ import InputFileCollection from '../input/InputFileCollection'; import SourceMapper from '../transpiler/SourceMapper'; import { coveragePerTestHooks } from '../transpiler/coverageHooks'; import LoggingClientContext from '../logging/LoggingClientContext'; +import { tokens, commonTokens } from 'stryker-api/plugin'; +import { coreTokens } from '../di'; // The initial run might take a while. // For example: angular-bootstrap takes up to 45 seconds. @@ -44,17 +44,31 @@ interface Timing { export default class InitialTestExecutor { - private readonly log = getLogger(InitialTestExecutor.name); - - constructor(private readonly options: Config, private readonly inputFiles: InputFileCollection, private readonly testFramework: TestFramework | null, private readonly timer: Timer, private readonly loggingContext: LoggingClientContext) { } + public static inject = tokens( + commonTokens.options, + commonTokens.logger, + coreTokens.inputFiles, + coreTokens.testFramework, + coreTokens.timer, + coreTokens.loggingContext, + coreTokens.transpiler); + + constructor( + private readonly options: StrykerOptions, + private readonly log: Logger, + private readonly inputFiles: InputFileCollection, + private readonly testFramework: TestFramework | null, + private readonly timer: Timer, + private readonly loggingContext: LoggingClientContext, + private readonly transpiler: Transpiler) { } public async run(): Promise { - this.log.info(`Starting initial test run. This may take a while.`); + this.log.info('Starting initial test run. This may take a while.'); // Before we can run the tests we transpile the input files. // Files that are not transpiled should pass through without transpiling - const transpiledFiles = await this.transpileInputFiles(); + const transpiledFiles = await this.transpiler.transpile(this.inputFiles.files); // Now that we have the transpiled files, we create a source mapper so // we can figure out which files we need to annotate for code coverage @@ -85,11 +99,6 @@ export default class InitialTestExecutor { return { runResult, grossTimeMS }; } - private async transpileInputFiles(): Promise> { - const transpilerFacade = this.createTranspilerFacade(); - return transpilerFacade.transpile(this.inputFiles.files); - } - private async annotateForCodeCoverage(files: ReadonlyArray, sourceMapper: SourceMapper) : Promise<{ instrumentedFiles: ReadonlyArray, coverageMaps: CoverageMapsByFile }> { const filesToInstrument = this.inputFiles.filesToMutate.map(mutateFile => sourceMapper.transpiledFileNameFor(mutateFile.name)); @@ -139,20 +148,6 @@ export default class InitialTestExecutor { }; } - /** - * Creates a facade for the transpile pipeline. - * Also includes the coverage instrumenter transpiler, - * which is used to instrument for code coverage when needed. - */ - private createTranspilerFacade(): Transpiler { - // Let the transpiler produce source maps only if coverage analysis is enabled - const transpilerSettings: TranspilerOptions = { - config: this.options, - produceSourceMaps: this.options.coverageAnalysis !== 'off' - }; - return new TranspilerFacade(transpilerSettings); - } - private getCollectCoverageHooksIfNeeded(): string | undefined { if (this.options.coverageAnalysis === 'perTest') { if (this.testFramework) { diff --git a/packages/stryker/src/reporters/BroadcastReporter.ts b/packages/stryker/src/reporters/BroadcastReporter.ts index 3c0e2ed14c..abb547758d 100644 --- a/packages/stryker/src/reporters/BroadcastReporter.ts +++ b/packages/stryker/src/reporters/BroadcastReporter.ts @@ -5,7 +5,7 @@ import StrictReporter from './StrictReporter'; import { commonTokens, PluginKind } from 'stryker-api/plugin'; import { StrykerOptions } from 'stryker-api/core'; import { tokens } from 'typed-inject'; -import * as coreTokens from '../di/coreTokens'; +import { coreTokens } from '../di'; import { PluginCreator } from '../di/PluginCreator'; export default class BroadcastReporter implements StrictReporter { @@ -102,5 +102,4 @@ export default class BroadcastReporter implements StrictReporter { private handleError(error: Error, methodName: string, reporterName: string) { this.log.error(`An error occurred during '${methodName}' on reporter '${reporterName}'. Error is: ${error}`); } - } diff --git a/packages/stryker/src/test-runner/ChildProcessTestRunnerDecorator.ts b/packages/stryker/src/test-runner/ChildProcessTestRunnerDecorator.ts index d1758578e8..707352e1a6 100644 --- a/packages/stryker/src/test-runner/ChildProcessTestRunnerDecorator.ts +++ b/packages/stryker/src/test-runner/ChildProcessTestRunnerDecorator.ts @@ -1,7 +1,7 @@ import { TestRunner, RunResult, RunOptions, RunnerOptions } from 'stryker-api/test_runner'; import LoggingClientContext from '../logging/LoggingClientContext'; import ChildProcessProxy from '../child-proxy/ChildProcessProxy'; -import ChildProcessTestRunnerWorker from './ChildProcessTestRunnerWorker'; +import { ChildProcessTestRunnerWorker } from './ChildProcessTestRunnerWorker'; import { timeout } from '../utils/objectUtils'; import ChildProcessCrashedError from '../child-proxy/ChildProcessCrashedError'; @@ -21,11 +21,12 @@ export default class ChildProcessTestRunnerDecorator implements TestRunner { sandboxWorkingDirectory: string, loggingContext: LoggingClientContext) { this.worker = ChildProcessProxy.create( - require.resolve('./ChildProcessTestRunnerWorker.js'), + require.resolve(`./${ChildProcessTestRunnerWorker.name}`), loggingContext, - options.strykerOptions.plugins || [], + options.strykerOptions, + { realTestRunnerName, runnerOptions: options }, sandboxWorkingDirectory, - ChildProcessTestRunnerWorker, realTestRunnerName, options); + ChildProcessTestRunnerWorker); } public init(): Promise { diff --git a/packages/stryker/src/test-runner/ChildProcessTestRunnerWorker.ts b/packages/stryker/src/test-runner/ChildProcessTestRunnerWorker.ts index ce19a15f6b..83d932baba 100644 --- a/packages/stryker/src/test-runner/ChildProcessTestRunnerWorker.ts +++ b/packages/stryker/src/test-runner/ChildProcessTestRunnerWorker.ts @@ -1,11 +1,13 @@ import { TestRunner, TestRunnerFactory, RunnerOptions, RunOptions } from 'stryker-api/test_runner'; import { errorToString } from '@stryker-mutator/util'; +import { tokens, commonTokens, PluginResolver } from 'stryker-api/plugin'; -export default class ChildProcessTestRunnerWorker implements TestRunner { +export class ChildProcessTestRunnerWorker implements TestRunner { private readonly underlyingTestRunner: TestRunner; - constructor(realTestRunnerName: string, options: RunnerOptions) { + public static inject = tokens('realTestRunnerName', 'runnerOptions', commonTokens.pluginResolver); + constructor(realTestRunnerName: string, options: RunnerOptions, _: PluginResolver) { this.underlyingTestRunner = TestRunnerFactory.instance().create(realTestRunnerName, options); } diff --git a/packages/stryker/src/transpiler/ChildProcessTranspilerWorker.ts b/packages/stryker/src/transpiler/ChildProcessTranspilerWorker.ts new file mode 100644 index 0000000000..f28ecd8c94 --- /dev/null +++ b/packages/stryker/src/transpiler/ChildProcessTranspilerWorker.ts @@ -0,0 +1,22 @@ +import { Transpiler } from 'stryker-api/transpile'; +import { coreTokens } from '../di'; +import { commonTokens, PluginKind, TranspilerPluginContext } from 'stryker-api/plugin'; +import { File } from 'stryker-api/core'; +import { PluginCreator } from '../di/PluginCreator'; +import { TranspilerFacade } from './TranspilerFacade'; +import { Injector } from 'typed-inject'; + +export class ChildProcessTranspilerWorker implements Transpiler { + private readonly innerTranspiler: Transpiler; + + public static inject = [commonTokens.injector]; + constructor(injector: Injector) { + this.innerTranspiler = injector + .provideFactory(coreTokens.pluginCreatorTranspiler, PluginCreator.createFactory(PluginKind.Transpiler)) + .injectClass(TranspilerFacade); + } + + public transpile(files: ReadonlyArray): Promise> { + return this.innerTranspiler.transpile(files); + } +} diff --git a/packages/stryker/src/transpiler/MutantTranspiler.ts b/packages/stryker/src/transpiler/MutantTranspiler.ts index e7d5d83879..5b6bc03105 100644 --- a/packages/stryker/src/transpiler/MutantTranspiler.ts +++ b/packages/stryker/src/transpiler/MutantTranspiler.ts @@ -1,42 +1,48 @@ import { Observable } from 'rxjs'; -import { Config } from 'stryker-api/config'; -import TranspilerFacade from './TranspilerFacade'; import TestableMutant from '../TestableMutant'; -import { File } from 'stryker-api/core'; +import { File, StrykerOptions } from 'stryker-api/core'; import SourceFile from '../SourceFile'; -import ChildProcessProxy, { Promisified } from '../child-proxy/ChildProcessProxy'; -import { TranspilerOptions } from 'stryker-api/transpile'; +import ChildProcessProxy from '../child-proxy/ChildProcessProxy'; +import { Transpiler } from 'stryker-api/transpile'; import TranspiledMutant from '../TranspiledMutant'; import TranspileResult from './TranspileResult'; import LoggingClientContext from '../logging/LoggingClientContext'; import { errorToString } from '@stryker-mutator/util'; +import { ChildProcessTranspilerWorker } from './ChildProcessTranspilerWorker'; +import { tokens, commonTokens } from 'stryker-api/plugin'; +import { coreTokens } from '../di'; export default class MutantTranspiler { - private readonly transpilerChildProcess: ChildProcessProxy | undefined; - private readonly proxy: Promisified; + private readonly transpilerChildProcess: ChildProcessProxy | undefined; + private readonly proxy: Transpiler; private currentMutatedFile: SourceFile; private unMutatedFiles: ReadonlyArray; + public static inject = tokens(commonTokens.options, coreTokens.loggingContext); + /** * Creates the mutant transpiler in a child process if one is defined. * Otherwise will just forward input as output in same process. * @param config The Stryker config */ - constructor(config: Config, loggingContext: LoggingClientContext) { - const transpilerOptions: TranspilerOptions = { config, produceSourceMaps: false }; - if (config.transpilers.length) { + constructor(options: StrykerOptions, loggingContext: LoggingClientContext) { + if (options.transpilers.length) { this.transpilerChildProcess = ChildProcessProxy.create( - require.resolve('./TranspilerFacade'), + require.resolve(`./${ChildProcessTranspilerWorker.name}`), loggingContext, - config.plugins, + options, + { [commonTokens.produceSourceMaps]: false }, process.cwd(), - TranspilerFacade, - transpilerOptions + ChildProcessTranspilerWorker ); this.proxy = this.transpilerChildProcess.proxy; } else { - this.proxy = new TranspilerFacade(transpilerOptions); + this.proxy = { + transpile(input) { + return Promise.resolve(input); + } + }; } } diff --git a/packages/stryker/src/transpiler/SourceMapper.ts b/packages/stryker/src/transpiler/SourceMapper.ts index 77464b10f9..0398b1300a 100644 --- a/packages/stryker/src/transpiler/SourceMapper.ts +++ b/packages/stryker/src/transpiler/SourceMapper.ts @@ -1,7 +1,6 @@ import * as path from 'path'; import { SourceMapConsumer, RawSourceMap } from 'source-map'; -import { File, Location, Position } from 'stryker-api/core'; -import { Config } from 'stryker-api/config'; +import { File, Location, Position, StrykerOptions } from 'stryker-api/core'; import { base64Decode } from '../utils/objectUtils'; import { getLogger } from 'stryker-api/logging'; import { StrykerError } from '@stryker-mutator/util'; @@ -41,8 +40,8 @@ export default abstract class SourceMapper { public abstract transpiledFileNameFor(originalFileName: string): string; - public static create(transpiledFiles: ReadonlyArray, config: Config): SourceMapper { - if (config.transpilers.length && config.coverageAnalysis !== 'off') { + public static create(transpiledFiles: ReadonlyArray, options: StrykerOptions): SourceMapper { + if (options.transpilers.length && options.coverageAnalysis !== 'off') { return new TranspiledSourceMapper(transpiledFiles); } else { return new PassThroughSourceMapper(); diff --git a/packages/stryker/src/transpiler/TranspilerFacade.ts b/packages/stryker/src/transpiler/TranspilerFacade.ts index 01cad28794..bbb6732637 100644 --- a/packages/stryker/src/transpiler/TranspilerFacade.ts +++ b/packages/stryker/src/transpiler/TranspilerFacade.ts @@ -1,18 +1,25 @@ -import { File } from 'stryker-api/core'; -import { Transpiler, TranspilerOptions, TranspilerFactory } from 'stryker-api/transpile'; +import { File, StrykerOptions } from 'stryker-api/core'; +import { Transpiler } from 'stryker-api/transpile'; import { StrykerError } from '@stryker-mutator/util'; +import { tokens, commonTokens, PluginKind } from 'stryker-api/plugin'; +import { coreTokens } from '../di'; +import { PluginCreator } from '../di/PluginCreator'; class NamedTranspiler { constructor(public name: string, public transpiler: Transpiler) { } } -export default class TranspilerFacade implements Transpiler { +export class TranspilerFacade implements Transpiler { private readonly innerTranspilers: NamedTranspiler[]; - constructor(options: TranspilerOptions) { - this.innerTranspilers = options.config.transpilers - .map(transpilerName => new NamedTranspiler(transpilerName, TranspilerFactory.instance().create(transpilerName, options))); + public static inject = tokens( + commonTokens.options, + coreTokens.pluginCreatorTranspiler); + + constructor(options: StrykerOptions, pluginCreator: PluginCreator) { + this.innerTranspilers = options.transpilers + .map(transpilerName => new NamedTranspiler(transpilerName, pluginCreator.create(transpilerName))); } public transpile(files: ReadonlyArray): Promise> { diff --git a/packages/stryker/test/integration/child-proxy/ChildProcessProxy.it.ts b/packages/stryker/test/integration/child-proxy/ChildProcessProxy.it.ts index d24a03326a..e0f5f6edcf 100644 --- a/packages/stryker/test/integration/child-proxy/ChildProcessProxy.it.ts +++ b/packages/stryker/test/integration/child-proxy/ChildProcessProxy.it.ts @@ -4,7 +4,7 @@ import * as log4js from 'log4js'; import { expect } from 'chai'; import { File, LogLevel } from 'stryker-api/core'; import { Logger } from 'stryker-api/logging'; -import Echo from './Echo'; +import { Echo } from './Echo'; import ChildProcessProxy from '../../../src/child-proxy/ChildProcessProxy'; import { Task } from '../../../src/utils/Task'; import LoggingServer from '../../helpers/LoggingServer'; @@ -14,7 +14,8 @@ import currentLogMock from '../../helpers/logMock'; import { sleep } from '../../helpers/testUtils'; import OutOfMemoryError from '../../../src/child-proxy/OutOfMemoryError'; import ChildProcessCrashedError from '../../../src/child-proxy/ChildProcessCrashedError'; - +import { testInjector } from '@stryker-mutator/test-helpers'; +import { commonTokens } from 'stryker-api/plugin'; describe('ChildProcessProxy', () => { let sut: ChildProcessProxy; @@ -25,9 +26,10 @@ describe('ChildProcessProxy', () => { beforeEach(async () => { const port = await getPort(); + const options = testInjector.injector.resolve(commonTokens.options); log = currentLogMock(); loggingServer = new LoggingServer(port); - sut = ChildProcessProxy.create(require.resolve('./Echo'), { port, level: LogLevel.Debug }, [], workingDir, Echo, echoName); + sut = ChildProcessProxy.create(require.resolve('./Echo'), { port, level: LogLevel.Debug }, options, { name: echoName }, workingDir, Echo); }); afterEach(async () => { diff --git a/packages/stryker/test/integration/child-proxy/Echo.ts b/packages/stryker/test/integration/child-proxy/Echo.ts index 51b651187b..d0e0635922 100644 --- a/packages/stryker/test/integration/child-proxy/Echo.ts +++ b/packages/stryker/test/integration/child-proxy/Echo.ts @@ -1,10 +1,12 @@ import { File } from 'stryker-api/core'; import { getLogger } from 'stryker-api/logging'; +import { tokens } from 'stryker-api/plugin'; -export default class Echo { +export class Echo { private readonly logger = getLogger(Echo.name); + public static inject = tokens('name'); constructor(public name: string) { } public say(value: string) { diff --git a/packages/stryker/test/unit/StrykerSpec.ts b/packages/stryker/test/unit/StrykerSpec.ts index f7a4d772c7..2719d56c1e 100644 --- a/packages/stryker/test/unit/StrykerSpec.ts +++ b/packages/stryker/test/unit/StrykerSpec.ts @@ -3,109 +3,87 @@ import { MutantResult } from 'stryker-api/report'; import { File, LogLevel } from 'stryker-api/core'; import { RunResult } from 'stryker-api/test_runner'; import { TestFramework } from 'stryker-api/test_framework'; +import { factory } from '@stryker-mutator/test-helpers'; import Stryker from '../../src/Stryker'; import { Config } from 'stryker-api/config'; import { expect } from 'chai'; -import InputFileResolver, * as inputFileResolver from '../../src/input/InputFileResolver'; -import ConfigReader, * as configReader from '../../src/config/ConfigReader'; -import TestFrameworkOrchestrator from '../../src/TestFrameworkOrchestrator'; +import InputFileResolver from '../../src/input/InputFileResolver'; import * as typedInject from 'typed-inject'; import MutatorFacade, * as mutatorFacade from '../../src/MutatorFacade'; import MutantRunResultMatcher, * as mutantRunResultMatcher from '../../src/MutantTestMatcher'; -import InitialTestExecutor, * as initialTestExecutor from '../../src/process/InitialTestExecutor'; +import InitialTestExecutor from '../../src/process/InitialTestExecutor'; import MutationTestExecutor, * as mutationTestExecutor from '../../src/process/MutationTestExecutor'; -import ConfigValidator, * as configValidator from '../../src/config/ConfigValidator'; import ScoreResultCalculator, * as scoreResultCalculatorModule from '../../src/ScoreResultCalculator'; -import PluginLoader, * as pluginLoader from '../../src/di/PluginLoader'; import { TempFolder } from '../../src/utils/TempFolder'; import currentLogMock from '../helpers/logMock'; -import { mock, Mock, testFramework as testFrameworkMock, config, runResult, testableMutant, mutantResult } from '../helpers/producers'; +import { mock, Mock, testFramework, runResult, testableMutant, mutantResult } from '../helpers/producers'; import BroadcastReporter from '../../src/reporters/BroadcastReporter'; import TestableMutant from '../../src/TestableMutant'; import InputFileCollection from '../../src/input/InputFileCollection'; import LogConfigurator from '../../src/logging/LogConfigurator'; import LoggingClientContext from '../../src/logging/LoggingClientContext'; -import { OptionsContext } from 'stryker-api/plugin'; -import { ConfigEditorApplier } from '../../src/config/ConfigEditorApplier'; +import { commonTokens } from 'stryker-api/plugin'; +import { TranspilerFacade } from '../../src/transpiler/TranspilerFacade'; +import * as di from '../../src/di'; +import Timer from '../../src/utils/Timer'; const LOGGING_CONTEXT: LoggingClientContext = Object.freeze({ level: LogLevel.Debug, port: 4200 }); -describe('Stryker', () => { +describe(Stryker.name, () => { let sut: Stryker; - let testFramework: TestFramework; + let testFrameworkMock: TestFramework; let inputFileResolverMock: Mock; - let testFrameworkOrchestratorMock: Mock; - let configEditorApplierMock: Mock; - let configValidatorMock: Mock; - let configReaderMock: Mock; let initialTestExecutorMock: Mock; let mutationTestExecutorMock: Mock; let mutantRunResultMatcherMock: Mock; let mutatorMock: Mock; - let pluginLoaderMock: Mock; let strykerConfig: Config; - let reporter: Mock; + let reporterMock: Mock; let tempFolderMock: Mock; let scoreResultCalculator: ScoreResultCalculator; let configureMainProcessStub: sinon.SinonStub; let configureLoggingServerStub: sinon.SinonStub; let shutdownLoggingStub: sinon.SinonStub; - let injectorMock: Mock>; + let injectorMock: sinon.SinonStubbedInstance; + let timerMock: sinon.SinonStubbedInstance; beforeEach(() => { - strykerConfig = config(); - reporter = mock(BroadcastReporter); - configValidatorMock = mock(ConfigValidator); - configReaderMock = mock(ConfigReader); - configReaderMock.readConfig.returns(strykerConfig); - pluginLoaderMock = mock(PluginLoader); - - injectorMock = { - injectClass: sinon.stub(), - injectFunction: sinon.stub(), - provideClass: sinon.stub(), - provideFactory: sinon.stub(), - provideValue: sinon.stub(), - resolve: sinon.stub() - }; - injectorMock.provideClass.returnsThis(); - injectorMock.provideFactory.returnsThis(); - injectorMock.provideValue.returnsThis(); + strykerConfig = factory.config(); + reporterMock = mock(BroadcastReporter); + injectorMock = factory.injector(); mutantRunResultMatcherMock = mock(MutantRunResultMatcher); - configEditorApplierMock = mock(ConfigEditorApplier); mutatorMock = mock(MutatorFacade); configureMainProcessStub = sinon.stub(LogConfigurator, 'configureMainProcess'); configureLoggingServerStub = sinon.stub(LogConfigurator, 'configureLoggingServer'); shutdownLoggingStub = sinon.stub(LogConfigurator, 'shutdown'); configureLoggingServerStub.resolves(LOGGING_CONTEXT); inputFileResolverMock = mock(InputFileResolver); - testFramework = testFrameworkMock(); + testFrameworkMock = testFramework(); initialTestExecutorMock = mock(InitialTestExecutor); mutationTestExecutorMock = mock(MutationTestExecutor); - testFrameworkOrchestratorMock = mock(TestFrameworkOrchestrator); - testFrameworkOrchestratorMock.determineTestFramework.returns(testFramework); + timerMock = sinon.createStubInstance(Timer); + tempFolderMock = mock(TempFolder as any); + tempFolderMock.clean.resolves(); + scoreResultCalculator = new ScoreResultCalculator(); + sinon.stub(di, 'buildMainInjector').returns(injectorMock); sinon.stub(mutationTestExecutor, 'default').returns(mutationTestExecutorMock); - sinon.stub(initialTestExecutor, 'default').returns(initialTestExecutorMock); - sinon.stub(configValidator, 'default').returns(configValidatorMock); - sinon.stub(typedInject, 'rootInjector').value(injectorMock); sinon.stub(mutatorFacade, 'default').returns(mutatorMock); sinon.stub(mutantRunResultMatcher, 'default').returns(mutantRunResultMatcherMock); - sinon.stub(configReader, 'default').returns(configReaderMock); - sinon.stub(pluginLoader, 'default').returns(pluginLoaderMock); - sinon.stub(inputFileResolver, 'default').returns(inputFileResolverMock); - tempFolderMock = mock(TempFolder as any); sinon.stub(TempFolder, 'instance').returns(tempFolderMock); - tempFolderMock.clean.resolves(); - scoreResultCalculator = new ScoreResultCalculator(); sinon.stub(scoreResultCalculator, 'determineExitCode').returns(sinon.stub()); sinon.stub(scoreResultCalculatorModule, 'default').returns(scoreResultCalculator); injectorMock.injectClass - .withArgs(ConfigEditorApplier).returns(configEditorApplierMock) - .withArgs(BroadcastReporter).returns(reporter) - .withArgs(TestFrameworkOrchestrator).returns(testFrameworkOrchestratorMock); + .withArgs(BroadcastReporter).returns(reporterMock) + .withArgs(InitialTestExecutor).returns(initialTestExecutorMock) + .withArgs(InputFileResolver).returns(inputFileResolverMock); + injectorMock.resolve + .withArgs(commonTokens.config).returns(strykerConfig) + .withArgs(di.coreTokens.timer).returns(timerMock) + .withArgs(di.coreTokens.reporter).returns(reporterMock) + .withArgs(di.coreTokens.testFramework).returns(testFrameworkMock); }); describe('when constructed', () => { @@ -114,31 +92,8 @@ describe('Stryker', () => { sut = new Stryker({}); }); - it('should apply the config editors', () => { - expect(configEditorApplierMock.edit).calledWith(strykerConfig); - }); - it('should configure logging for master', () => { - expect(configureMainProcessStub).calledThrice; - }); - - it('should freeze the config', () => { - expect(Object.isFrozen(sut.config)).to.be.eq(true); - }); - - it('should load plugins', () => { - expect(pluginLoader.default).to.have.been.calledWith(strykerConfig.plugins); - expect(pluginLoaderMock.load).to.have.been.calledWith(); - }); - - it('should determine the testFramework', () => { - expect(testFrameworkOrchestratorMock.determineTestFramework).to.have.been.called; - }); - - it('should validate the config', () => { - expect(configValidator.default).calledWithNew; - expect(configValidator.default).calledWith(strykerConfig, testFramework); - expect(configValidatorMock.validate).called; + expect(configureMainProcessStub).calledTwice; }); }); @@ -231,66 +186,92 @@ describe('Stryker', () => { describe('happy flow', () => { - beforeEach(() => { + it('should configure the logging server', async () => { sut = new Stryker({}); - return sut.runMutationTest(); - }); - - it('should configure the logging server', () => { + await sut.runMutationTest(); expect(configureLoggingServerStub).calledWith(strykerConfig.logLevel, strykerConfig.fileLogLevel); }); - it('should report mutant score', () => { - expect(reporter.onScoreCalculated).to.have.been.called; + it('should report mutant score', async () => { + sut = new Stryker({}); + await sut.runMutationTest(); + expect(reporterMock.onScoreCalculated).called; }); - it('should determine the exit code', () => { + it('should determine the exit code', async () => { + sut = new Stryker({}); + await sut.runMutationTest(); expect(scoreResultCalculator.determineExitCode).called; }); - it('should determine the testFramework', () => { - expect(testFrameworkOrchestratorMock.determineTestFramework).to.have.been.called; - }); - - it('should create the InputFileResolver', () => { - expect(inputFileResolver.default).calledWithNew; - expect(inputFileResolver.default).calledWith(strykerConfig.mutate, strykerConfig.files, reporter); + it('should create the InputFileResolver', async () => { + sut = new Stryker({}); + await sut.runMutationTest(); expect(inputFileResolverMock.resolve).called; }); - it('should create the InitialTestExecutor', () => { - expect(initialTestExecutor.default).calledWithNew; - expect(initialTestExecutor.default).calledWith(strykerConfig, inputFiles, testFramework, sinon.match.any, LOGGING_CONTEXT); + it('should create the InitialTestExecutor', async () => { + sut = new Stryker({}); + await sut.runMutationTest(); + expect(injectorMock.injectClass).calledWith(InitialTestExecutor); expect(initialTestExecutorMock.run).called; }); - it('should create the mutator', () => { + it('should create the mutator', async () => { + sut = new Stryker({}); + await sut.runMutationTest(); expect(mutatorFacade.default).calledWithNew; expect(mutatorFacade.default).calledWith(strykerConfig); expect(mutatorMock.mutate).calledWith(inputFiles.filesToMutate); }); - it('should create the mutation test executor', () => { + it('should create the mutation test executor', async () => { + sut = new Stryker({}); + await sut.runMutationTest(); expect(mutationTestExecutor.default).calledWithNew; - expect(mutationTestExecutor.default).calledWith(strykerConfig, inputFiles.files, testFramework, reporter, undefined, LOGGING_CONTEXT); + expect(mutationTestExecutor.default).calledWith(strykerConfig, inputFiles.files, testFrameworkMock, reporterMock, undefined, LOGGING_CONTEXT); expect(mutationTestExecutorMock.run).calledWith(mutants); }); - it('should log the number of mutants generated', () => { + it('should log the number of mutants generated', async () => { + sut = new Stryker({}); + await sut.runMutationTest(); expect(currentLogMock().info).to.have.been.calledWith('3 Mutant(s) generated'); }); - it('should clean the stryker temp folder', () => { + it('should clean the stryker temp folder', async () => { + sut = new Stryker({}); + await sut.runMutationTest(); expect(tempFolderMock.clean).called; }); - it('should let the reporters wrapUp any async tasks', () => { - expect(reporter.wrapUp).to.have.been.called; + it('should let the reporters wrapUp any async tasks', async () => { + sut = new Stryker({}); + await sut.runMutationTest(); + expect(reporterMock.wrapUp).to.have.been.called; }); - it('should shutdown the log4js server', () => { + it('should shutdown the log4js server', async () => { + sut = new Stryker({}); + await sut.runMutationTest(); expect(shutdownLoggingStub).called; }); + + it('should create the transpiler with produceSourceMaps = true when coverage analysis is enabled', async () => { + strykerConfig.coverageAnalysis = 'all'; + sut = new Stryker({}); + await sut.runMutationTest(); + expect(injectorMock.provideValue).calledWith(commonTokens.produceSourceMaps, true); + expect(injectorMock.provideClass).calledWith(di.coreTokens.transpiler, TranspilerFacade); + }); + + it('should create the transpiler with produceSourceMaps = false when coverage analysis is "off"', async () => { + strykerConfig.coverageAnalysis = 'off'; + sut = new Stryker({}); + await sut.runMutationTest(); + expect(injectorMock.provideValue).calledWith(commonTokens.produceSourceMaps, false); + expect(injectorMock.provideClass).calledWith(di.coreTokens.transpiler, TranspilerFacade); + }); }); describe('with excluded mutants', () => { diff --git a/packages/stryker/test/unit/TestFrameworkOrchestratorSpec.ts b/packages/stryker/test/unit/TestFrameworkOrchestratorSpec.ts index e30268e995..2501f5bb33 100644 --- a/packages/stryker/test/unit/TestFrameworkOrchestratorSpec.ts +++ b/packages/stryker/test/unit/TestFrameworkOrchestratorSpec.ts @@ -3,7 +3,7 @@ import { expect } from 'chai'; import * as sinon from 'sinon'; import { testInjector, factory } from '@stryker-mutator/test-helpers'; import { PluginKind } from 'stryker-api/plugin'; -import * as coreTokens from '../../src/di/coreTokens'; +import { coreTokens } from '../../src/di'; import { PluginCreator } from '../../src/di/PluginCreator'; describe('TestFrameworkOrchestrator', () => { diff --git a/packages/stryker/test/unit/child-proxy/ChildProcessProxySpec.ts b/packages/stryker/test/unit/child-proxy/ChildProcessProxySpec.ts index c211301347..c3006abc96 100644 --- a/packages/stryker/test/unit/child-proxy/ChildProcessProxySpec.ts +++ b/packages/stryker/test/unit/child-proxy/ChildProcessProxySpec.ts @@ -4,15 +4,16 @@ import * as childProcess from 'child_process'; import ChildProcessProxy from '../../../src/child-proxy/ChildProcessProxy'; import { autoStart, InitMessage, WorkerMessageKind, ParentMessage, WorkerMessage, ParentMessageKind, DisposeMessage } from '../../../src/child-proxy/messageProtocol'; import { serialize } from '../../../src/utils/objectUtils'; -import HelloClass from './HelloClass'; +import { HelloClass } from './HelloClass'; import LoggingClientContext from '../../../src/logging/LoggingClientContext'; -import { LogLevel } from 'stryker-api/core'; +import { LogLevel, StrykerOptions } from 'stryker-api/core'; import * as objectUtils from '../../../src/utils/objectUtils'; import { EventEmitter } from 'events'; import { Logger } from 'stryker-api/logging'; import { Mock } from '../../helpers/producers'; import currentLogMock from '../../helpers/logMock'; import * as sinon from 'sinon'; +import { factory } from '@stryker-mutator/test-helpers'; const LOGGING_CONTEXT: LoggingClientContext = Object.freeze({ level: LogLevel.Fatal, @@ -59,24 +60,23 @@ describe('ChildProcessProxy', () => { it('should send init message to child process', () => { const expectedMessage: InitMessage = { - constructorArgs: ['something'], + additionalInjectableValues: { name: 'fooTest' }, kind: WorkerMessageKind.Init, loggingContext: LOGGING_CONTEXT, - plugins: ['examplePlugin', 'secondExamplePlugin'], + options: factory.strykerOptions(), + requireName: 'HelloClass', requirePath: 'foobar', workingDirectory: 'workingDirectory' }; // Act createSut({ - arg: expectedMessage.constructorArgs[0], loggingContext: LOGGING_CONTEXT, - plugins: expectedMessage.plugins, + name: (expectedMessage.additionalInjectableValues as { name: string }).name, + options: expectedMessage.options, requirePath: expectedMessage.requirePath, workingDir: expectedMessage.workingDirectory }); - ChildProcessProxy.create(expectedMessage.requirePath, LOGGING_CONTEXT, expectedMessage.plugins, - expectedMessage.workingDirectory, HelloClass, expectedMessage.constructorArgs[0]); // Assert expect(childProcessMock.send).calledWith(serialize(expectedMessage)); @@ -215,15 +215,15 @@ describe('ChildProcessProxy', () => { function createSut(overrides: { requirePath?: string; loggingContext?: LoggingClientContext; - plugins?: string[]; + options?: Partial; workingDir?: string; - arg?: string; + name?: string; } = {}): ChildProcessProxy { return ChildProcessProxy.create( overrides.requirePath || 'foobar', overrides.loggingContext || LOGGING_CONTEXT, - overrides.plugins || ['examplePlugin', 'secondExamplePlugin'], + factory.strykerOptions(overrides.options), + { name: overrides.name || 'someArg' }, overrides.workingDir || 'workingDir', - HelloClass, - overrides.arg || 'someArg'); + HelloClass); } diff --git a/packages/stryker/test/unit/child-proxy/ChildProcessWorkerSpec.ts b/packages/stryker/test/unit/child-proxy/ChildProcessWorkerSpec.ts index e7a7e4e58a..e9eccfab9a 100644 --- a/packages/stryker/test/unit/child-proxy/ChildProcessWorkerSpec.ts +++ b/packages/stryker/test/unit/child-proxy/ChildProcessWorkerSpec.ts @@ -3,15 +3,17 @@ import ChildProcessProxyWorker from '../../../src/child-proxy/ChildProcessProxyW import { expect } from 'chai'; import { serialize } from '../../../src/utils/objectUtils'; import { WorkerMessage, WorkerMessageKind, ParentMessage, WorkResult, CallMessage, ParentMessageKind, InitMessage } from '../../../src/child-proxy/messageProtocol'; -import PluginLoader, * as pluginLoader from '../../../src/di/PluginLoader'; -import { Mock, mock } from '../../helpers/producers'; -import HelloClass from './HelloClass'; +import { Mock } from '../../helpers/producers'; +import { HelloClass } from './HelloClass'; import LogConfigurator from '../../../src/logging/LogConfigurator'; import { LogLevel } from 'stryker-api/core'; import LoggingClientContext from '../../../src/logging/LoggingClientContext'; import { Logger } from 'stryker-api/logging'; import currentLogMock from '../../helpers/logMock'; import * as sinon from 'sinon'; +import { rootInjector } from 'typed-inject'; +import { factory } from '@stryker-mutator/test-helpers'; +import * as di from '../../../src/di'; const LOGGING_CONTEXT: LoggingClientContext = Object.freeze({ port: 4200, level: LogLevel.Fatal }); @@ -24,7 +26,6 @@ describe('ChildProcessProxyWorker', () => { let processRemoveListenerStub: sinon.SinonStub; let processChdirStub: sinon.SinonStub; let logMock: Mock; - let pluginLoaderMock: Mock; let originalProcessSend: undefined | NodeJS.MessageListener; let processes: NodeJS.MessageListener[]; const workingDir = 'working dir'; @@ -42,8 +43,7 @@ describe('ChildProcessProxyWorker', () => { process.send = processSendStub; processChdirStub = sinon.stub(process, 'chdir'); configureChildProcessStub = sinon.stub(LogConfigurator, 'configureChildProcess'); - pluginLoaderMock = mock(PluginLoader); - sinon.stub(pluginLoader, 'default').returns(pluginLoaderMock); + sinon.stub(di, 'buildChildProcessInjector').returns(rootInjector); }); afterEach(() => { @@ -62,11 +62,13 @@ describe('ChildProcessProxyWorker', () => { beforeEach(() => { sut = new ChildProcessProxyWorker(); + const options = factory.strykerOptions(); initMessage = { - constructorArgs: ['FooBarName'], + additionalInjectableValues: { name: 'FooBarName'}, kind: WorkerMessageKind.Init, loggingContext: LOGGING_CONTEXT, - plugins: ['fooPlugin', 'barPlugin'], + options, + requireName: HelloClass.name, requirePath: require.resolve('./HelloClass'), workingDirectory: workingDir }; @@ -118,13 +120,6 @@ describe('ChildProcessProxyWorker', () => { expect(configureChildProcessStub).calledWith(LOGGING_CONTEXT); }); - it('should load plugins', () => { - processOnMessage(initMessage); - expect(pluginLoader.default).calledWithNew; - expect(pluginLoader.default).calledWith(['fooPlugin', 'barPlugin']); - expect(pluginLoaderMock.load).called; - }); - it('should handle unhandledRejection events', () => { processOnMessage(initMessage); const error = new Error('foobar'); @@ -155,9 +150,9 @@ describe('ChildProcessProxyWorker', () => { processOnMessage(workerMessage); await tick(); // Assert - expect(processSendStub).calledWithMatch(`"correlationId": ${workerMessage.correlationId.toString()}`); - expect(processSendStub).calledWithMatch(`"kind": ${ParentMessageKind.Rejection.toString()}`); - expect(processSendStub).calledWithMatch(`"error": "Error: ${expectedError}`); + expect(processSendStub).calledWithMatch(`"correlationId":${workerMessage.correlationId.toString()}`); + expect(processSendStub).calledWithMatch(`"kind":${ParentMessageKind.Rejection.toString()}`); + expect(processSendStub).calledWithMatch(`"error":"Error: ${expectedError}`); } it('should send the result', async () => { diff --git a/packages/stryker/test/unit/child-proxy/HelloClass.ts b/packages/stryker/test/unit/child-proxy/HelloClass.ts index ba5eaebb52..17b5cc7d08 100644 --- a/packages/stryker/test/unit/child-proxy/HelloClass.ts +++ b/packages/stryker/test/unit/child-proxy/HelloClass.ts @@ -1,4 +1,7 @@ -export default class HelloClass { +import { tokens } from 'stryker-api/plugin'; + +export class HelloClass { + public static inject = tokens('name'); constructor(public name: string) { } public sayHello() { return `hello from ${this.name}`; diff --git a/packages/stryker/test/unit/config/ConfigEditorApplier.spec.ts b/packages/stryker/test/unit/config/ConfigEditorApplier.spec.ts index 8a25c28289..1842391423 100644 --- a/packages/stryker/test/unit/config/ConfigEditorApplier.spec.ts +++ b/packages/stryker/test/unit/config/ConfigEditorApplier.spec.ts @@ -3,7 +3,7 @@ import { testInjector, factory } from '@stryker-mutator/test-helpers'; import { PluginKind } from 'stryker-api/plugin'; import * as sinon from 'sinon'; import { expect } from 'chai'; -import * as coreTokens from '../../../src/di/coreTokens'; +import { coreTokens } from '../../../src/di'; import { PluginCreator } from '../../../src/di/PluginCreator'; describe('ConfigEditorApplier', () => { diff --git a/packages/stryker/test/unit/di/PluginLoaderSpec.ts b/packages/stryker/test/unit/di/PluginLoaderSpec.ts index 241d9e409f..e761b7e94e 100644 --- a/packages/stryker/test/unit/di/PluginLoaderSpec.ts +++ b/packages/stryker/test/unit/di/PluginLoaderSpec.ts @@ -3,7 +3,7 @@ import { Logger } from 'stryker-api/logging'; import * as sinon from 'sinon'; import { expect } from 'chai'; import * as fileUtils from '../../../src/utils/fileUtils'; -import PluginLoader from '../../../src/di/PluginLoader'; +import { PluginLoader } from '../../../src/di/PluginLoader'; import currentLogMock from '../../helpers/logMock'; import { Mock } from '../../helpers/producers'; import { fsAsPromised } from '@stryker-mutator/util'; diff --git a/packages/stryker/test/unit/di/buildMainInjector.spec.ts b/packages/stryker/test/unit/di/buildMainInjector.spec.ts new file mode 100644 index 0000000000..080cabdf15 --- /dev/null +++ b/packages/stryker/test/unit/di/buildMainInjector.spec.ts @@ -0,0 +1,110 @@ +import { buildMainInjector } from '../../../src/di/buildMainInjector'; +import * as di from '../../../src/di'; +import * as sinon from 'sinon'; +import { factory } from '@stryker-mutator/test-helpers'; +import { expect } from 'chai'; +import { commonTokens } from 'stryker-api/plugin'; +import { Config } from 'stryker-api/config'; +import { TestFramework } from 'stryker-api/test_framework'; +import TestFrameworkOrchestrator, * as testFrameworkOrchestratorModule from '../../../src/TestFrameworkOrchestrator'; +import { PluginCreator } from '../../../src/di'; +import ConfigReader, * as configReaderModule from '../../../src/config/ConfigReader'; +import * as configModule from '../../../src/config'; +import { Reporter } from 'stryker-api/report'; +import * as broadcastReporterModule from '../../../src/reporters/BroadcastReporter'; + +describe(buildMainInjector.name, () => { + + let testFrameworkOrchestratorMock: sinon.SinonStubbedInstance; + let pluginLoaderMock: sinon.SinonStubbedInstance; + let testFrameworkMock: TestFramework; + let configReaderMock: sinon.SinonStubbedInstance; + let pluginCreatorMock: sinon.SinonStubbedInstance>; + let configEditorApplierMock: sinon.SinonStubbedInstance; + let broadcastReporterMock: sinon.SinonStubbedInstance; + let expectedConfig: Config; + + beforeEach(() => { + configReaderMock = sinon.createStubInstance(ConfigReader); + pluginCreatorMock = sinon.createStubInstance(PluginCreator); + configEditorApplierMock = sinon.createStubInstance(configModule.ConfigEditorApplier); + testFrameworkMock = factory.testFramework(); + testFrameworkOrchestratorMock = sinon.createStubInstance(TestFrameworkOrchestrator); + testFrameworkOrchestratorMock.determineTestFramework.returns(testFrameworkMock); + pluginLoaderMock = sinon.createStubInstance(di.PluginLoader); + expectedConfig = new Config(); + broadcastReporterMock = factory.reporter('broadcast'); + configReaderMock.readConfig.returns(expectedConfig); + stubInjectable(PluginCreator, 'createFactory').returns(() => pluginCreatorMock); + stubInjectable(configModule, 'ConfigEditorApplier').returns(configEditorApplierMock); + stubInjectable(di, 'PluginLoader').returns(pluginLoaderMock); + stubInjectable(configReaderModule, 'default').returns(configReaderMock); + stubInjectable(broadcastReporterModule, 'default').returns(broadcastReporterMock); + stubInjectable(testFrameworkOrchestratorModule, 'default').returns(testFrameworkOrchestratorMock); + }); + + function stubInjectable(obj: T, method: keyof T) { + const inject = (obj[method] as any).inject; + const stub = sinon.stub(obj, method); + (stub as any).inject = inject; + return stub; + } + + describe('resolve config', () => { + + it('should supply readonly stryker options', () => { + const injector = buildMainInjector({}); + const actualConfig = injector.resolve(commonTokens.config); + const actualOptions = injector.resolve(commonTokens.options); + expect(actualConfig).eq(expectedConfig); + expect(actualConfig).frozen; + expect(actualOptions).eq(expectedConfig); + }); + + it('should load reporter plugins', () => { + buildMainInjector({}).resolve(commonTokens.config); + expect(di.PluginLoader).calledWithNew; + expect(di.PluginLoader).calledWith(['stryker-*', require.resolve('../../../src/reporters')]); + }); + + it('should load plugins', () => { + buildMainInjector({}).resolve(commonTokens.config); + expect(pluginLoaderMock.load).called; + }); + + it('should apply config editors', () => { + buildMainInjector({}).resolve(commonTokens.config); + expect(configEditorApplierMock.edit).called; + }); + + it('should cache the config', () => { + const injector = buildMainInjector({}); + injector.resolve(commonTokens.config); + injector.resolve(commonTokens.config); + expect(configReaderMock.readConfig).calledOnce; + }); + + it('should inject the `cliOptions` in the config reader', () => { + const expectedCliOptions = { foo: 'bar' }; + buildMainInjector(expectedCliOptions).resolve(commonTokens.config); + expect(configReaderModule.default).calledWith(expectedCliOptions); + }); + }); + + it('should be able to supply the test framework', () => { + const actualTestFramework = buildMainInjector({}).resolve(di.coreTokens.testFramework); + expect(testFrameworkMock).eq(actualTestFramework); + }); + + it('should be able to supply the reporter', () => { + const actualReporter = buildMainInjector({}).resolve(di.coreTokens.reporter); + expect(actualReporter).eq(broadcastReporterMock); + }); + + it('should supply pluginCreators for Reporter, ConfigEditor and TestFramework plugins', () => { + const injector = buildMainInjector({}); + expect(injector.resolve(di.coreTokens.pluginCreatorReporter)).eq(pluginCreatorMock); + expect(injector.resolve(di.coreTokens.pluginCreatorTestFramework)).eq(pluginCreatorMock); + expect(injector.resolve(di.coreTokens.pluginCreatorConfigEditor)).eq(pluginCreatorMock); + }); +}); diff --git a/packages/stryker/test/unit/di/createPlugin.spec.ts b/packages/stryker/test/unit/di/createPlugin.spec.ts deleted file mode 100644 index e86ddc2b96..0000000000 --- a/packages/stryker/test/unit/di/createPlugin.spec.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { createPlugin } from '../../../src/di/createPlugin'; -import { PluginKind, FactoryPlugin, ClassPlugin } from 'stryker-api/plugin'; -import { factory, testInjector } from '@stryker-mutator/test-helpers'; -import { expect } from 'chai'; - -describe('createPlugin', () => { - it('should create a FactoryPlugin using it\'s factory method', () => { - const expectedReporter = factory.reporter(); - const plugin: FactoryPlugin = { - kind: PluginKind.Reporter, - name: 'fooReporter', - factory() { - return expectedReporter; - } - }; - const actualReporter = createPlugin(PluginKind.Reporter, plugin, testInjector.injector); - expect(actualReporter).eq(expectedReporter); - }); - - it('should create a ClassPlugin using it\'s constructor', () => { - class FooReporter { - } - const plugin: ClassPlugin = { - injectableClass: FooReporter, - kind: PluginKind.Reporter, - name: 'fooReporter' - }; - const actualReporter = createPlugin(PluginKind.Reporter, plugin, testInjector.injector); - expect(actualReporter).instanceOf(FooReporter); - }); - - it('should throw if plugin is not recognized', () => { - expect(() => createPlugin(PluginKind.Reporter, { name: 'foo' } as any, testInjector.injector)) - .throws('Plugin "Reporter:foo" could not be created, missing "factory" or "injectableClass" property.'); - }); -}); diff --git a/packages/stryker/test/unit/input/InputFileResolverSpec.ts b/packages/stryker/test/unit/input/InputFileResolverSpec.ts index bb3c3fd30e..4e46a4b2ae 100644 --- a/packages/stryker/test/unit/input/InputFileResolverSpec.ts +++ b/packages/stryker/test/unit/input/InputFileResolverSpec.ts @@ -14,6 +14,8 @@ import BroadcastReporter from '../../../src/reporters/BroadcastReporter'; import { Mock, mock, createFileNotFoundError, createIsDirError } from '../../helpers/producers'; import { normalizeWhiteSpaces } from '../../../src/utils/objectUtils'; import { fsAsPromised } from '@stryker-mutator/util'; +import { testInjector } from '@stryker-mutator/test-helpers'; +import { coreTokens } from '../../../src/di'; const files = (...namesWithContent: [string, string][]): File[] => namesWithContent.map((nameAndContent): File => new File( @@ -25,13 +27,13 @@ describe('InputFileResolver', () => { let log: Mock; let globStub: sinon.SinonStub; let sut: InputFileResolver; - let reporter: Mock; + let reporterMock: Mock; let childProcessExecStub: sinon.SinonStub; let readFileStub: sinon.SinonStub; beforeEach(() => { log = currentLogMock(); - reporter = mock(BroadcastReporter); + reporterMock = mock(BroadcastReporter); globStub = sinon.stub(fileUtils, 'glob'); readFileStub = sinon.stub(fsAsPromised, 'readFile') .withArgs(sinon.match.string).resolves(Buffer.from('')) // fallback @@ -50,10 +52,11 @@ describe('InputFileResolver', () => { globStub.withArgs('file*').resolves(['/file1.js', '/file2.js', '/file3.js']); globStub.resolves([]); // default childProcessExecStub = sinon.stub(childProcessAsPromised, 'exec'); + testInjector.options.mutate = []; }); it('should use git to identify files if files array is missing', async () => { - sut = new InputFileResolver([], undefined, reporter); + sut = createSut(); childProcessExecStub.resolves({ stdout: Buffer.from(` file1.js @@ -69,7 +72,7 @@ describe('InputFileResolver', () => { it('should reject if there is no `files` array and `git ls-files` command fails', () => { const expectedError = new Error('fatal: Not a git repository (or any of the parent directories): .git'); childProcessExecStub.rejects(expectedError); - return expect(new InputFileResolver([], undefined, reporter).resolve()) + return expect(createSut().resolve()) .rejectedWith(`Cannot determine input files. Either specify a \`files\` array in your stryker configuration, or make sure "${process.cwd() }" is located inside a git repository. Inner error: ${ errorToString(expectedError) @@ -77,14 +80,15 @@ describe('InputFileResolver', () => { }); it('should log a warning if no files were resolved', async () => { - sut = new InputFileResolver([], [], reporter); + testInjector.options.files = []; + sut = createSut(); await sut.resolve(); expect(log.warn).calledWith(sinon.match(`No files selected. Please make sure you either${os.EOL} (1) Run Stryker inside a Git repository`) .and(sinon.match('(2) Specify the \`files\` property in your Stryker configuration'))); }); it('should be able to handle deleted files reported by `git ls-files`', async () => { - sut = new InputFileResolver([], undefined, reporter); + sut = createSut(); childProcessExecStub.resolves({ stdout: Buffer.from(` deleted/file.js @@ -97,7 +101,7 @@ describe('InputFileResolver', () => { }); it('should be able to handle directories reported by `git ls-files` (submodules)', async () => { - sut = new InputFileResolver([], undefined, reporter); + sut = createSut(); childProcessExecStub.resolves({ stdout: Buffer.from(` submoduleDir @@ -112,7 +116,9 @@ describe('InputFileResolver', () => { describe('with mutate file expressions', () => { it('should result in the expected mutate files', async () => { - sut = new InputFileResolver(['mute*'], ['file1', 'mute1', 'file2', 'mute2', 'file3'], reporter); + testInjector.options.mutate = ['mute*']; + testInjector.options.files = ['file1', 'mute1', 'file2', 'mute2', 'file3']; + sut = createSut(); const result = await sut.resolve(); expect(result.filesToMutate.map(_ => _.name)).to.deep.equal([ path.resolve('/mute1.js'), @@ -128,7 +134,9 @@ describe('InputFileResolver', () => { }); it('should only report a mutate file when it is included in the resolved files', async () => { - sut = new InputFileResolver(['mute*'], ['file1', 'mute1', 'file2', /*'mute2'*/ 'file3'], reporter); + testInjector.options.mutate = ['mute*']; + testInjector.options.files = ['file1', 'mute1', 'file2', /*'mute2'*/ 'file3']; + sut = createSut(); const result = await sut.resolve(); expect(result.filesToMutate.map(_ => _.name)).to.deep.equal([ path.resolve('/mute1.js') @@ -136,7 +144,9 @@ describe('InputFileResolver', () => { }); it('should report OnAllSourceFilesRead', async () => { - sut = new InputFileResolver(['mute*'], ['file1', 'mute1', 'file2', 'mute2', 'file3'], reporter); + testInjector.options.mutate = ['mute*']; + testInjector.options.files = ['file1', 'mute1', 'file2', 'mute2', 'file3']; + sut = createSut(); await sut.resolve(); const expected: SourceFile[] = [ { path: path.resolve('/file1.js'), content: 'file 1 content' }, @@ -145,11 +155,13 @@ describe('InputFileResolver', () => { { path: path.resolve('/mute2.js'), content: 'mutate 2 content' }, { path: path.resolve('/file3.js'), content: 'file 3 content' } ]; - expect(reporter.onAllSourceFilesRead).calledWith(expected); + expect(reporterMock.onAllSourceFilesRead).calledWith(expected); }); it('should report OnSourceFileRead', async () => { - sut = new InputFileResolver(['mute*'], ['file1', 'mute1', 'file2', 'mute2', 'file3'], reporter); + testInjector.options.mutate = ['mute*']; + testInjector.options.files = ['file1', 'mute1', 'file2', 'mute2', 'file3']; + sut = createSut(); await sut.resolve(); const expected: SourceFile[] = [ { path: path.resolve('/file1.js'), content: 'file 1 content' }, @@ -158,14 +170,15 @@ describe('InputFileResolver', () => { { path: path.resolve('/mute2.js'), content: 'mutate 2 content' }, { path: path.resolve('/file3.js'), content: 'file 3 content' } ]; - expected.forEach(sourceFile => expect(reporter.onSourceFileRead).calledWith(sourceFile)); + expected.forEach(sourceFile => expect(reporterMock.onSourceFileRead).calledWith(sourceFile)); }); }); describe('without mutate files', () => { beforeEach(() => { - sut = new InputFileResolver([], ['file1', 'mute1'], reporter); + testInjector.options.files = ['file1', 'mute1']; + sut = createSut(); }); it('should warn about dry-run', async () => { @@ -176,7 +189,8 @@ describe('InputFileResolver', () => { describe('with file expressions that resolve in different order', () => { beforeEach(() => { - sut = new InputFileResolver([], ['fileWhichResolvesLast', 'fileWhichResolvesFirst'], reporter); + testInjector.options.files = ['fileWhichResolvesLast', 'fileWhichResolvesFirst']; + sut = createSut(); globStub.withArgs('fileWhichResolvesLast').resolves(['file1']); globStub.withArgs('fileWhichResolvesFirst').resolves(['file2']); }); @@ -195,7 +209,8 @@ describe('InputFileResolver', () => { patternFile1 = { pattern: 'file1' }; patternFile3 = { pattern: 'file3' }; - sut = new InputFileResolver([], [patternFile1, 'file2', patternFile3], reporter); + testInjector.options.files = [patternFile1, 'file2', patternFile3]; + sut = createSut(); }); it('it should log a warning', async () => { @@ -226,14 +241,18 @@ describe('InputFileResolver', () => { describe('when a globbing expression does not result in a result', () => { it('should log a warning', async () => { - sut = new InputFileResolver(['file1'], ['file1', 'notExists'], reporter); + testInjector.options.files = ['file1', 'notExists']; + testInjector.options.mutate = ['file1']; + sut = createSut(); await sut.resolve(); expect(log.warn).to.have.been.calledWith('Globbing expression "notExists" did not result in any files.'); }); it('should not log a warning if the globbing expression was the default logging expression', async () => { const config = new Config(); - sut = new InputFileResolver(config.mutate, config.files, reporter); + testInjector.options.files = config.files; + testInjector.options.mutate = config.mutate; + sut = createSut(); childProcessExecStub.resolves({ stdout: Buffer.from(`src/foobar.js`) }); globStub.withArgs(config.mutate[0]).returns(['src/foobar.js']); await sut.resolve(); @@ -242,7 +261,9 @@ describe('InputFileResolver', () => { }); it('should reject when a globbing expression results in a reject', () => { - sut = new InputFileResolver(['file1'], ['fileError', 'fileError'], reporter); + testInjector.options.files = ['fileError', 'fileError']; + testInjector.options.mutate = ['file1']; + sut = createSut(); const expectedError = new Error('ERROR: something went wrong'); globStub.withArgs('fileError').rejects(expectedError); return expect(sut.resolve()).rejectedWith(expectedError); @@ -251,17 +272,23 @@ describe('InputFileResolver', () => { describe('when excluding files with "!"', () => { it('should exclude the files that were previously included', async () => { - const result = await new InputFileResolver([], ['file2', 'file1', '!file2'], reporter).resolve(); + testInjector.options.files = ['file2', 'file1', '!file2']; + const sut = createSut(); + const result = await sut.resolve(); assertFilesEqual(result.files, files(['/file1.js', 'file 1 content'])); }); it('should exclude the files that were previously with a wild card', async () => { - const result = await new InputFileResolver([], ['file*', '!file2'], reporter).resolve(); + testInjector.options.files = ['file*', '!file2']; + const sut = createSut(); + const result = await sut.resolve(); assertFilesEqual(result.files, files(['/file1.js', 'file 1 content'], ['/file3.js', 'file 3 content'])); }); it('should not exclude files when the globbing expression results in an empty array', async () => { - const result = await new InputFileResolver([], ['file2', '!does/not/exist'], reporter).resolve(); + testInjector.options.files = ['file2', '!does/not/exist']; + const sut = createSut(); + const result = await sut.resolve(); assertFilesEqual(result.files, files(['/file2.js', 'file 2 content'])); }); }); @@ -269,17 +296,20 @@ describe('InputFileResolver', () => { describe('when provided duplicate files', () => { it('should deduplicate files that occur more than once', async () => { - const result = await new InputFileResolver([], ['file2', 'file2'], reporter).resolve(); + testInjector.options.files = ['file2', 'file2']; + const result = await createSut().resolve(); assertFilesEqual(result.files, files(['/file2.js', 'file 2 content'])); }); it('should deduplicate files that previously occurred in a wildcard expression', async () => { - const result = await new InputFileResolver([], ['file*', 'file2'], reporter).resolve(); + testInjector.options.files = ['file*', 'file2']; + const result = await createSut().resolve(); assertFilesEqual(result.files, files(['/file1.js', 'file 1 content'], ['/file2.js', 'file 2 content'], ['/file3.js', 'file 3 content'])); }); it('should order files by expression order', async () => { - const result = await new InputFileResolver([], ['file2', 'file*'], reporter).resolve(); + testInjector.options.files = ['file2', 'file*']; + const result = await createSut().resolve(); assertFilesEqual(result.files, files(['/file2.js', 'file 2 content'], ['/file1.js', 'file 1 content'], ['/file3.js', 'file 3 content'])); }); }); @@ -291,4 +321,10 @@ describe('InputFileResolver', () => { expect(actual[index].textContent).eq(expected[index].textContent); } } + + function createSut() { + return testInjector.injector + .provideValue(coreTokens.reporter, reporterMock) + .injectClass(InputFileResolver); + } }); diff --git a/packages/stryker/test/unit/process/InitialTestExecutorSpec.ts b/packages/stryker/test/unit/process/InitialTestExecutorSpec.ts index b427d8b2bc..5f729d0840 100644 --- a/packages/stryker/test/unit/process/InitialTestExecutorSpec.ts +++ b/packages/stryker/test/unit/process/InitialTestExecutorSpec.ts @@ -1,24 +1,23 @@ import { EOL } from 'os'; import { expect } from 'chai'; -import { Logger } from 'stryker-api/logging'; -import { default as StrykerSandbox } from '../../../src/Sandbox'; +import Sandbox from '../../../src/Sandbox'; import InitialTestExecutor, { InitialTestRunResult } from '../../../src/process/InitialTestExecutor'; import { File, LogLevel } from 'stryker-api/core'; -import { Config } from 'stryker-api/config'; import * as producers from '../../helpers/producers'; import { TestFramework } from 'stryker-api/test_framework'; import CoverageInstrumenterTranspiler, * as coverageInstrumenterTranspiler from '../../../src/transpiler/CoverageInstrumenterTranspiler'; -import TranspilerFacade, * as transpilerFacade from '../../../src/transpiler/TranspilerFacade'; -import { TranspilerOptions } from 'stryker-api/transpile'; +import { Transpiler } from 'stryker-api/transpile'; import { RunStatus, RunResult, TestStatus } from 'stryker-api/test_runner'; -import currentLogMock from '../../helpers/logMock'; import Timer from '../../../src/utils/Timer'; -import { Mock, coverageMaps } from '../../helpers/producers'; +import { coverageMaps } from '../../helpers/producers'; import InputFileCollection from '../../../src/input/InputFileCollection'; import * as coverageHooks from '../../../src/transpiler/coverageHooks'; import SourceMapper, { PassThroughSourceMapper } from '../../../src/transpiler/SourceMapper'; import LoggingClientContext from '../../../src/logging/LoggingClientContext'; import * as sinon from 'sinon'; +import { testInjector, factory } from '@stryker-mutator/test-helpers'; +import { coreTokens } from '../../../src/di'; +import { commonTokens } from 'stryker-api/plugin'; const EXPECTED_INITIAL_TIMEOUT = 60 * 1000 * 5; const LOGGING_CONTEXT: LoggingClientContext = Object.freeze({ @@ -28,26 +27,34 @@ const LOGGING_CONTEXT: LoggingClientContext = Object.freeze({ describe('InitialTestExecutor run', () => { - let log: Mock; - let strykerSandboxMock: producers.Mock; + let strykerSandboxMock: producers.Mock; let sut: InitialTestExecutor; - let testFrameworkMock: TestFramework; + let testFrameworkMock: TestFramework | null; let coverageInstrumenterTranspilerMock: producers.Mock; - let options: Config; - let transpilerFacadeMock: producers.Mock; + let transpilerMock: producers.Mock; let transpiledFiles: File[]; let coverageAnnotatedFiles: File[]; let sourceMapperMock: producers.Mock; - let timer: producers.Mock; let expectedRunResult: RunResult; + let inputFiles: InputFileCollection; + let timerMock: sinon.SinonStubbedInstance; + + function createSut() { + return testInjector.injector + .provideValue(coreTokens.inputFiles, inputFiles) + .provideValue(coreTokens.loggingContext, LOGGING_CONTEXT) + .provideValue(coreTokens.testFramework, testFrameworkMock) + .provideValue(coreTokens.transpiler, transpilerMock as Transpiler) + .provideValue(coreTokens.timer, timerMock as unknown as Timer) + .injectClass(InitialTestExecutor); + } beforeEach(() => { - log = currentLogMock(); - strykerSandboxMock = producers.mock(StrykerSandbox as any); - transpilerFacadeMock = producers.mock(TranspilerFacade); + timerMock = sinon.createStubInstance(Timer); + strykerSandboxMock = producers.mock(Sandbox as any); + transpilerMock = factory.transpiler(); coverageInstrumenterTranspilerMock = producers.mock(CoverageInstrumenterTranspiler); - sinon.stub(StrykerSandbox, 'create').resolves(strykerSandboxMock); - sinon.stub(transpilerFacade, 'default').returns(transpilerFacadeMock); + sinon.stub(Sandbox, 'create').resolves(strykerSandboxMock); sinon.stub(coverageInstrumenterTranspiler, 'default').returns(coverageInstrumenterTranspilerMock); sourceMapperMock = producers.mock(PassThroughSourceMapper); sinon.stub(SourceMapper, 'create').returns(sourceMapperMock); @@ -61,49 +68,25 @@ describe('InitialTestExecutor run', () => { new File('transpiled-file-2.js', '') ]; coverageInstrumenterTranspilerMock.transpile.returns(coverageAnnotatedFiles); - transpilerFacadeMock.transpile.returns(transpiledFiles); - options = producers.config(); + transpilerMock.transpile.returns(transpiledFiles); expectedRunResult = producers.runResult(); strykerSandboxMock.run.resolves(expectedRunResult); - timer = producers.mock(Timer); }); describe('with input files', () => { - let inputFiles: InputFileCollection; - beforeEach(() => { inputFiles = new InputFileCollection([new File('mutate.js', ''), new File('mutate.spec.js', '')], ['mutate.js']); - sut = new InitialTestExecutor(options, inputFiles, testFrameworkMock, timer as any, LOGGING_CONTEXT); }); it('should create a sandbox with correct arguments', async () => { + sut = createSut(); await sut.run(); - expect(StrykerSandbox.create).calledWith(options, 0, coverageAnnotatedFiles, testFrameworkMock); - }); - - it('should create the transpiler with produceSourceMaps = true when coverage analysis is enabled', async () => { - options.coverageAnalysis = 'all'; - await sut.run(); - const expectedTranspilerOptions: TranspilerOptions = { - config: options, - produceSourceMaps: true - }; - expect(transpilerFacade.default).calledWithNew; - expect(transpilerFacade.default).calledWith(expectedTranspilerOptions); - }); - - it('should create the transpiler with produceSourceMaps = false when coverage analysis is "off"', async () => { - options.coverageAnalysis = 'off'; - await sut.run(); - const expectedTranspilerOptions: TranspilerOptions = { - config: options, - produceSourceMaps: false - }; - expect(transpilerFacade.default).calledWith(expectedTranspilerOptions); + expect(Sandbox.create).calledWith(testInjector.injector.resolve(commonTokens.options), 0, coverageAnnotatedFiles, testFrameworkMock); }); it('should initialize, run and dispose the sandbox', async () => { + sut = createSut(); await sut.run(); expect(strykerSandboxMock.run).to.have.been.calledWith(EXPECTED_INITIAL_TIMEOUT); expect(strykerSandboxMock.dispose).to.have.been.called; @@ -115,21 +98,23 @@ describe('InitialTestExecutor run', () => { expectedRunResult.tests[0].timeSpentMs = 10; expectedRunResult.tests.push(producers.testResult({ timeSpentMs: 2 })); expectedRunResult.tests.push(producers.testResult({ timeSpentMs: 6 })); - timer.elapsedMs.returns(100); + timerMock.elapsedMs.returns(100); + sut = createSut(); // Act const { overheadTimeMS } = await sut.run(); // Assert - expect(timer.mark).calledWith('Initial test run'); - expect(timer.elapsedMs).calledWith('Initial test run'); - expect(timer.mark).calledBefore(timer.elapsedMs); + expect(timerMock.mark).calledWith('Initial test run'); + expect(timerMock.elapsedMs).calledWith('Initial test run'); + expect(timerMock.mark).calledBefore(timerMock.elapsedMs); expect(overheadTimeMS).eq(expectedOverHeadTimeMs); }); it('should never calculate a negative overhead time', async () => { expectedRunResult.tests[0].timeSpentMs = 10; - timer.elapsedMs.returns(9); + timerMock.elapsedMs.returns(9); + sut = createSut(); const { overheadTimeMS } = await sut.run(); expect(overheadTimeMS).eq(0); }); @@ -137,7 +122,7 @@ describe('InitialTestExecutor run', () => { it('should pass through the result', async () => { const coverageData = coverageMaps(); coverageInstrumenterTranspilerMock.fileCoverageMaps = { someFile: coverageData } as any; - timer.elapsedMs.returns(42); + timerMock.elapsedMs.returns(42); const expectedResult: InitialTestRunResult = { coverageMaps: { someFile: coverageData @@ -146,67 +131,76 @@ describe('InitialTestExecutor run', () => { runResult: expectedRunResult, sourceMapper: sourceMapperMock }; + sut = createSut(); const actualRunResult = await sut.run(); expect(actualRunResult).deep.eq(expectedResult); }); it('should log the transpiled results if transpilers are specified', async () => { - options.transpilers.push('a transpiler'); - log.isDebugEnabled.returns(true); + testInjector.options.transpilers = ['a transpiler']; + testInjector.logger.isDebugEnabled.returns(true); + sut = createSut(); await sut.run(); - const actualLogMessage: string = log.debug.getCall(0).args[0]; + const actualLogMessage: string = testInjector.logger.debug.getCall(0).args[0]; const expectedLogMessage = `Transpiled files: ${JSON.stringify(coverageAnnotatedFiles.map(_ => _.name), null, 2)}`; expect(actualLogMessage).eq(expectedLogMessage); }); it('should not log the transpiled results if transpilers are not specified', async () => { - log.isDebugEnabled.returns(true); + testInjector.logger.isDebugEnabled.returns(true); await sut.run(); - expect(log.debug).not.calledWithMatch('Transpiled files'); + expect(testInjector.logger.debug).not.calledWithMatch('Transpiled files'); }); it('should have logged the amount of tests ran', async () => { expectedRunResult.tests.push(producers.testResult()); - timer.humanReadableElapsed.returns('2 seconds'); - timer.elapsedMs.returns(50); + timerMock.humanReadableElapsed.returns('2 seconds'); + timerMock.elapsedMs.returns(50); + sut = createSut(); await sut.run(); - expect(log.info).to.have.been.calledWith('Initial test run succeeded. Ran %s tests in %s (net %s ms, overhead %s ms).', + expect(testInjector.logger.info).to.have.been.calledWith('Initial test run succeeded. Ran %s tests in %s (net %s ms, overhead %s ms).', 2, '2 seconds', 20, 30); }); it('should log when there were no tests', async () => { while (expectedRunResult.tests.pop()); + sut = createSut(); await sut.run(); - expect(log.warn).to.have.been.calledWith('No tests were executed. Stryker will exit prematurely. Please check your configuration.'); + expect(testInjector.logger.warn).to.have.been.calledWith('No tests were executed. Stryker will exit prematurely. Please check your configuration.'); }); it('should pass through any rejections', async () => { const expectedError = new Error('expected error'); strykerSandboxMock.run.rejects(expectedError); + sut = createSut(); await expect(sut.run()).rejectedWith(expectedError); }); it('should create the coverage instrumenter transpiler with source-mapped files to mutate', async () => { sourceMapperMock.transpiledFileNameFor.returns('mutate.min.js'); + sut = createSut(); await sut.run(); expect(coverageInstrumenterTranspiler.default).calledWithNew; - expect(coverageInstrumenterTranspiler.default).calledWith(options, ['mutate.min.js']); + expect(coverageInstrumenterTranspiler.default).calledWith(testInjector.injector.resolve(commonTokens.options), ['mutate.min.js']); expect(sourceMapperMock.transpiledFileNameFor).calledWith('mutate.js'); }); it('should also add a collectCoveragePerTest file when coverage analysis is "perTest" and there is a testFramework', async () => { - options.coverageAnalysis = 'perTest'; + testInjector.options.coverageAnalysis = 'perTest'; sinon.stub(coverageHooks, 'coveragePerTestHooks').returns('test hook foobar'); + sut = createSut(); await sut.run(); expect(strykerSandboxMock.run).calledWith(EXPECTED_INITIAL_TIMEOUT, 'test hook foobar'); }); it('should result log a warning if coverage analysis is "perTest" and there is no testFramework', async () => { - options.coverageAnalysis = 'perTest'; - sut = new InitialTestExecutor(options, inputFiles, /* test framework */ null, timer as any, LOGGING_CONTEXT); + testInjector.options.coverageAnalysis = 'perTest'; + testFrameworkMock = null; + sut = createSut(); sinon.stub(coverageHooks, 'coveragePerTestHooks').returns('test hook foobar'); + sut = createSut(); await sut.run(); - expect(log.warn).calledWith('Cannot measure coverage results per test, there is no testFramework and thus no way of executing code right before and after each test.'); + expect(testInjector.logger.warn).calledWith('Cannot measure coverage results per test, there is no testFramework and thus no way of executing code right before and after each test.'); }); describe('and run has test failures', () => { @@ -219,10 +213,12 @@ describe('InitialTestExecutor run', () => { }); it('should have logged the errors', async () => { + sut = createSut(); await expect(sut.run()).rejected; - expect(log.error).calledWith(`One or more tests failed in the initial test run:${EOL}\texample test${EOL}\t\texpected error${EOL}\t2nd example test`); + expect(testInjector.logger.error).calledWith(`One or more tests failed in the initial test run:${EOL}\texample test${EOL}\t\texpected error${EOL}\t2nd example test`); }); it('should reject with correct message', async () => { + sut = createSut(); await expect(sut.run()).rejectedWith('There were failed tests in the initial test run.'); }); }); @@ -235,10 +231,12 @@ describe('InitialTestExecutor run', () => { }); it('should have logged the errors', async () => { + sut = createSut(); await expect(sut.run()).rejected; - expect(log.error).calledWith(`One or more tests resulted in an error:${EOL}\tfoobar${EOL}\texample`); + expect(testInjector.logger.error).calledWith(`One or more tests resulted in an error:${EOL}\tfoobar${EOL}\texample`); }); it('should reject with correct message', async () => { + sut = createSut(); await expect(sut.run()).rejectedWith('Something went wrong in the initial test run'); }); }); @@ -252,11 +250,13 @@ describe('InitialTestExecutor run', () => { }); it('should have logged the timeout', async () => { + sut = createSut(); await expect(sut.run()).rejected; - expect(log.error).calledWith(`Initial test run timed out! Ran following tests before timeout:${EOL}\tfoobar test (Success)${EOL}\texample test (Failed)`); + expect(testInjector.logger.error).calledWith(`Initial test run timed out! Ran following tests before timeout:${EOL}\tfoobar test (Success)${EOL}\texample test (Failed)`); }); it('should reject with correct message', async () => { + sut = createSut(); await expect(sut.run()).rejectedWith('Something went wrong in the initial test run'); }); }); diff --git a/packages/stryker/test/unit/reporters/BroadcastReporterSpec.ts b/packages/stryker/test/unit/reporters/BroadcastReporterSpec.ts index 499efb5281..75e910c511 100644 --- a/packages/stryker/test/unit/reporters/BroadcastReporterSpec.ts +++ b/packages/stryker/test/unit/reporters/BroadcastReporterSpec.ts @@ -6,7 +6,7 @@ import * as sinon from 'sinon'; import { testInjector, factory } from '@stryker-mutator/test-helpers'; import { Reporter } from 'stryker-api/report'; import { PluginCreator } from '../../../src/di/PluginCreator'; -import * as coreTokens from '../../../src/di/coreTokens'; +import { coreTokens } from '../../../src/di'; describe('BroadcastReporter', () => { diff --git a/packages/stryker/test/unit/test-runner/ChildProcessTestRunnerDecoratorSpec.ts b/packages/stryker/test/unit/test-runner/ChildProcessTestRunnerDecoratorSpec.ts index 43b35273c2..842ae528b4 100644 --- a/packages/stryker/test/unit/test-runner/ChildProcessTestRunnerDecoratorSpec.ts +++ b/packages/stryker/test/unit/test-runner/ChildProcessTestRunnerDecoratorSpec.ts @@ -6,7 +6,7 @@ import ChildProcessTestRunnerDecorator from '../../../src/test-runner/ChildProce import { Mock, mock, strykerOptions } from '../../helpers/producers'; import ChildProcessProxy from '../../../src/child-proxy/ChildProcessProxy'; import LoggingClientContext from '../../../src/logging/LoggingClientContext'; -import ChildProcessTestRunnerWorker from '../../../src/test-runner/ChildProcessTestRunnerWorker'; +import { ChildProcessTestRunnerWorker } from '../../../src/test-runner/ChildProcessTestRunnerWorker'; import TestRunnerDecorator from '../../../src/test-runner/TestRunnerDecorator'; import { Task } from '../../../src/utils/Task'; import ChildProcessCrashedError from '../../../src/child-proxy/ChildProcessCrashedError'; @@ -44,10 +44,10 @@ describe(ChildProcessTestRunnerDecorator.name, () => { expect(childProcessProxyCreateStub).calledWith( require.resolve('../../../src/test-runner/ChildProcessTestRunnerWorker.js'), loggingContext, - ['foo-plugin', 'bar-plugin'], + runnerOptions.strykerOptions, + { [ChildProcessTestRunnerWorker.inject[0]]: 'realRunner', [ChildProcessTestRunnerWorker.inject[1]]: runnerOptions }, 'a working directory', - ChildProcessTestRunnerWorker, - 'realRunner', runnerOptions + ChildProcessTestRunnerWorker ); }); diff --git a/packages/stryker/test/unit/transpiler/MutantTranspilerSpec.ts b/packages/stryker/test/unit/transpiler/MutantTranspilerSpec.ts index fe80f533b7..641405637e 100644 --- a/packages/stryker/test/unit/transpiler/MutantTranspilerSpec.ts +++ b/packages/stryker/test/unit/transpiler/MutantTranspilerSpec.ts @@ -5,12 +5,15 @@ import TranspiledMutant from '../../../src/TranspiledMutant'; import ChildProcessProxy from '../../../src/child-proxy/ChildProcessProxy'; import MutantTranspiler from '../../../src/transpiler/MutantTranspiler'; import TranspileResult from '../../../src/transpiler/TranspileResult'; -import TranspilerFacade, * as transpilerFacade from '../../../src/transpiler/TranspilerFacade'; import { Mock, config, file, mock, testableMutant } from '../../helpers/producers'; import LoggingClientContext from '../../../src/logging/LoggingClientContext'; import { sleep } from '../../helpers/testUtils'; import * as sinon from 'sinon'; import { errorToString } from '@stryker-mutator/util'; +import { Transpiler } from 'stryker-api/transpile'; +import { TranspilerFacade } from '../../../src/transpiler/TranspilerFacade'; +import { commonTokens } from 'stryker-api/plugin'; +import { ChildProcessTranspilerWorker } from '../../../src/transpiler/ChildProcessTranspilerWorker'; const LOGGING_CONTEXT: LoggingClientContext = Object.freeze({ level: LogLevel.Fatal, @@ -19,16 +22,14 @@ const LOGGING_CONTEXT: LoggingClientContext = Object.freeze({ describe('MutantTranspiler', () => { let sut: MutantTranspiler; - let transpilerFacadeMock: Mock; let transpiledFilesOne: File[]; let transpiledFilesTwo: File[]; - let childProcessProxyMock: { proxy: Mock, dispose: sinon.SinonStub }; + let childProcessProxyMock: { proxy: Mock, dispose: sinon.SinonStub }; beforeEach(() => { - transpilerFacadeMock = mock(TranspilerFacade); + const transpilerFacadeMock = mock(TranspilerFacade); childProcessProxyMock = { proxy: transpilerFacadeMock, dispose: sinon.stub() }; sinon.stub(ChildProcessProxy, 'create').returns(childProcessProxyMock); - sinon.stub(transpilerFacade, 'default').returns(transpilerFacadeMock); transpiledFilesOne = [new File('firstResult.js', 'first result')]; transpiledFilesTwo = [new File('secondResult.js', 'second result')]; transpilerFacadeMock.transpile @@ -42,12 +43,12 @@ describe('MutantTranspiler', () => { const expectedConfig = config({ transpilers: ['transpiler'], plugins: ['plugin1'] }); sut = new MutantTranspiler(expectedConfig, LOGGING_CONTEXT); expect(ChildProcessProxy.create).calledWith( - require.resolve('../../../src/transpiler/TranspilerFacade'), + require.resolve('../../../src/transpiler/ChildProcessTranspilerWorker'), LOGGING_CONTEXT, - ['plugin1'], + expectedConfig, + { [commonTokens.produceSourceMaps]: false }, process.cwd(), - TranspilerFacade, - { config: expectedConfig, produceSourceMaps: false } + ChildProcessTranspilerWorker ); }); @@ -57,7 +58,7 @@ describe('MutantTranspiler', () => { const expectedFiles = [file()]; sut = new MutantTranspiler(config({ transpilers: ['transpiler'] }), LOGGING_CONTEXT); const actualResult = sut.initialize(expectedFiles); - expect(transpilerFacadeMock.transpile).calledWith(expectedFiles); + expect(childProcessProxyMock.proxy.transpile).calledWith(expectedFiles); return expect(actualResult).eventually.eq(transpiledFilesOne); }); }); @@ -91,8 +92,8 @@ describe('MutantTranspiler', () => { it('should report rejected transpile attempts as errors', async () => { // Arrange const error = new Error('expected transpile error'); - transpilerFacadeMock.transpile.reset(); - transpilerFacadeMock.transpile.rejects(error); + childProcessProxyMock.proxy.transpile.reset(); + childProcessProxyMock.proxy.transpile.rejects(error); const mutant = testableMutant(); // Act @@ -109,8 +110,8 @@ describe('MutantTranspiler', () => { it('should set set the changedAnyTranspiledFiles boolean to false if transpiled output did not change', async () => { // Arrange - transpilerFacadeMock.transpile.reset(); - transpilerFacadeMock.transpile.resolves(transpiledFilesOne); + childProcessProxyMock.proxy.transpile.reset(); + childProcessProxyMock.proxy.transpile.resolves(transpiledFilesOne); const mutants = [testableMutant()]; const files = [file()]; await sut.initialize(files); @@ -131,8 +132,8 @@ describe('MutantTranspiler', () => { // Arrange let resolveFirst: (files: ReadonlyArray) => void = () => { }; let resolveSecond: (files: ReadonlyArray) => void = () => { }; - transpilerFacadeMock.transpile.resetBehavior(); - transpilerFacadeMock.transpile + childProcessProxyMock.proxy.transpile.resetBehavior(); + childProcessProxyMock.proxy.transpile .onFirstCall().returns(new Promise>(res => resolveFirst = res)) .onSecondCall().returns(new Promise>(res => resolveSecond = res)); const actualResults: TranspileResult[] = []; @@ -142,12 +143,12 @@ describe('MutantTranspiler', () => { .subscribe(transpiledMutant => actualResults.push(transpiledMutant.transpileResult)); // Assert: only first time called - expect(transpilerFacadeMock.transpile).calledOnce; + expect(childProcessProxyMock.proxy.transpile).calledOnce; expect(actualResults).lengthOf(0); resolveFirst(transpiledFilesOne); await nextTick(); // Assert: second one is called, first one is received - expect(transpilerFacadeMock.transpile).calledTwice; + expect(childProcessProxyMock.proxy.transpile).calledTwice; expect(actualResults).lengthOf(1); resolveSecond(transpiledFilesTwo); // Assert: all results are in @@ -166,7 +167,6 @@ describe('MutantTranspiler', () => { beforeEach(() => { sut = new MutantTranspiler(config(), LOGGING_CONTEXT); - }); it('should construct without a child process', () => { @@ -176,17 +176,19 @@ describe('MutantTranspiler', () => { it('should transpile the files when initialized', async () => { const expectedFiles = [file()]; const actualFiles = await sut.initialize(expectedFiles); - expect(transpilerFacadeMock.transpile).calledWith(expectedFiles); - expect(actualFiles).eq(transpiledFilesOne); + expect(actualFiles).eq(expectedFiles); }); it('should transpile the mutated files when transpileMutants is called', async () => { const actualMutants = [testableMutant('file1.ts'), testableMutant('file2.ts')]; const actualResult = await sut.transpileMutants(actualMutants).pipe(toArray()).toPromise(); expect(actualResult).lengthOf(2); - expect(actualResult[0].transpileResult.outputFiles).eq(transpiledFilesOne); + expect(actualResult[0].transpileResult.outputFiles).lengthOf(1); + expect(actualResult[0].transpileResult.outputFiles[0].textContent).eq('const a = 4 - 5'); expect(actualResult[0].mutant).eq(actualMutants[0]); - expect(actualResult[1].transpileResult.outputFiles).eq(transpiledFilesTwo); + expect(actualResult[1].transpileResult.outputFiles).lengthOf(2); + expect(actualResult[1].transpileResult.outputFiles[0].textContent).eq('const a = 4 + 5'); + expect(actualResult[1].transpileResult.outputFiles[1].textContent).eq('const a = 4 - 5'); expect(actualResult[1].mutant).eq(actualMutants[1]); }); diff --git a/packages/stryker/test/unit/transpiler/TranspilerFacadeSpec.ts b/packages/stryker/test/unit/transpiler/TranspilerFacadeSpec.ts index e6692da596..aa795a6ceb 100644 --- a/packages/stryker/test/unit/transpiler/TranspilerFacadeSpec.ts +++ b/packages/stryker/test/unit/transpiler/TranspilerFacadeSpec.ts @@ -1,29 +1,29 @@ import { expect } from 'chai'; -import { Config } from 'stryker-api/config'; -import TranspilerFacade from '../../../src/transpiler/TranspilerFacade'; -import { Transpiler, TranspilerFactory } from 'stryker-api/transpile'; +import { Transpiler } from 'stryker-api/transpile'; import { mock, Mock } from '../../helpers/producers'; import { File } from 'stryker-api/core'; +import { testInjector } from '@stryker-mutator/test-helpers'; +import { TranspilerFacade } from '../../../src/transpiler/TranspilerFacade'; import * as sinon from 'sinon'; +import { PluginCreator } from '../../../src/di/PluginCreator'; +import { PluginKind } from 'stryker-api/plugin'; +import { coreTokens } from '../../../src/di'; describe('TranspilerFacade', () => { - let createStub: sinon.SinonStub; let sut: TranspilerFacade; - - beforeEach(() => { - createStub = sinon.stub(TranspilerFactory.instance(), 'create'); - }); + let pluginCreatorMock: sinon.SinonStubbedInstance>; describe('when there are no transpilers', () => { beforeEach(() => { - sut = new TranspilerFacade({ config: new Config(), produceSourceMaps: true }); + pluginCreatorMock = sinon.createStubInstance(PluginCreator); + sut = createSut(); }); it('should return input when `transpile` is called', async () => { const input = [new File('input', '')]; const outputFiles = await sut.transpile(input); - expect(createStub).not.called; + expect(pluginCreatorMock.create).not.called; expect(outputFiles).eq(input); }); }); @@ -34,31 +34,27 @@ describe('TranspilerFacade', () => { let transpilerTwo: Mock; let resultFilesOne: ReadonlyArray; let resultFilesTwo: ReadonlyArray; - let config: Config; - beforeEach(() => { - config = new Config(); - config.transpilers.push('transpiler-one', 'transpiler-two'); + testInjector.options.transpilers = ['transpiler-one', 'transpiler-two']; transpilerOne = mock(TranspilerFacade); transpilerTwo = mock(TranspilerFacade); resultFilesOne = [new File('result-1', '')]; resultFilesTwo = [new File('result-2', '')]; - createStub + pluginCreatorMock.create .withArgs('transpiler-one').returns(transpilerOne) .withArgs('transpiler-two').returns(transpilerTwo); transpilerOne.transpile.resolves(resultFilesOne); transpilerTwo.transpile.resolves(resultFilesTwo); + sut = createSut(); }); it('should create two transpilers', () => { - sut = new TranspilerFacade({ config, produceSourceMaps: true }); - expect(createStub).calledTwice; - expect(createStub).calledWith('transpiler-one'); - expect(createStub).calledWith('transpiler-two'); + expect(pluginCreatorMock.create).calledTwice; + expect(pluginCreatorMock.create).calledWith('transpiler-one'); + expect(pluginCreatorMock.create).calledWith('transpiler-two'); }); it('should chain the transpilers when `transpile` is called', async () => { - sut = new TranspilerFacade({ config, produceSourceMaps: true }); const input = [new File('input', '')]; const outputFiles = await sut.transpile(input); expect(outputFiles).eq(resultFilesTwo); @@ -71,7 +67,6 @@ describe('TranspilerFacade', () => { transpilerOne.transpile.reset(); const expectedError = new Error('an error'); transpilerOne.transpile.rejects(expectedError); - sut = new TranspilerFacade({ config, produceSourceMaps: true }); const input = [new File('input', '')]; // Act @@ -86,4 +81,9 @@ describe('TranspilerFacade', () => { }); }); + function createSut() { + return testInjector.injector + .provideValue(coreTokens.pluginCreatorTranspiler, pluginCreatorMock as unknown as PluginCreator) + .injectClass(TranspilerFacade); + } });