diff --git a/e2e/package.json b/e2e/package.json index 2fc9ffde95..4731c78e34 100644 --- a/e2e/package.json +++ b/e2e/package.json @@ -45,6 +45,7 @@ "@stryker-mutator/util": "../packages/stryker-util", "stryker-typescript": "../packages/stryker-typescript", "stryker-vue-mutator": "../packages/stryker-vue-mutator", - "stryker-webpack-transpiler": "../packages/stryker-webpack-transpiler" + "stryker-webpack-transpiler": "../packages/stryker-webpack-transpiler", + "typed-inject": "../packages/typed-inject" } } diff --git a/e2e/test/angular-project/package-lock.json b/e2e/test/angular-project/package-lock.json index 5c88b6e738..d0a877b7f6 100644 --- a/e2e/test/angular-project/package-lock.json +++ b/e2e/test/angular-project/package-lock.json @@ -3625,12 +3625,14 @@ "balanced-match": { "version": "1.0.0", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "brace-expansion": { "version": "1.1.11", "bundled": true, "dev": true, + "optional": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -3645,17 +3647,20 @@ "code-point-at": { "version": "1.1.0", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "concat-map": { "version": "0.0.1", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "console-control-strings": { "version": "1.1.0", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "core-util-is": { "version": "1.0.2", @@ -3772,7 +3777,8 @@ "inherits": { "version": "2.0.3", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "ini": { "version": "1.3.5", @@ -3784,6 +3790,7 @@ "version": "1.0.0", "bundled": true, "dev": true, + "optional": true, "requires": { "number-is-nan": "^1.0.0" } @@ -3798,6 +3805,7 @@ "version": "3.0.4", "bundled": true, "dev": true, + "optional": true, "requires": { "brace-expansion": "^1.1.7" } @@ -3805,12 +3813,14 @@ "minimist": { "version": "0.0.8", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "minipass": { "version": "2.2.4", "bundled": true, "dev": true, + "optional": true, "requires": { "safe-buffer": "^5.1.1", "yallist": "^3.0.0" @@ -3829,6 +3839,7 @@ "version": "0.5.1", "bundled": true, "dev": true, + "optional": true, "requires": { "minimist": "0.0.8" } @@ -3909,7 +3920,8 @@ "number-is-nan": { "version": "1.0.1", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "object-assign": { "version": "4.1.1", @@ -3921,6 +3933,7 @@ "version": "1.4.0", "bundled": true, "dev": true, + "optional": true, "requires": { "wrappy": "1" } @@ -4042,6 +4055,7 @@ "version": "1.0.2", "bundled": true, "dev": true, + "optional": true, "requires": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", diff --git a/e2e/test/angular-project/package.json b/e2e/test/angular-project/package.json index 3969dec97c..1a70a22c9f 100644 --- a/e2e/test/angular-project/package.json +++ b/e2e/test/angular-project/package.json @@ -51,6 +51,7 @@ "stryker": "../../../packages/stryker", "stryker-karma-runner": "../../../packages/stryker-karma-runner", "stryker-typescript": "../../../packages/stryker-typescript", - "@stryker-mutator/util": "../../../packages/stryker-util" + "@stryker-mutator/util": "../../../packages/stryker-util", + "typed-inject": "../../../packages/typed-inject" } } diff --git a/e2e/test/jest-react/package.json b/e2e/test/jest-react/package.json index 4f5032ee1c..ece47819dd 100644 --- a/e2e/test/jest-react/package.json +++ b/e2e/test/jest-react/package.json @@ -88,6 +88,7 @@ "stryker": "../../../packages/stryker", "stryker-javascript-mutator": "../../../packages/stryker-javascript-mutator", "stryker-jest-runner": "../../../packages/stryker-jest-runner", - "@stryker-mutator/util": "../../../packages/stryker-util" + "@stryker-mutator/util": "../../../packages/stryker-util", + "typed-inject": "../../../packages/typed-inject" } } diff --git a/e2e/test/polymer-project/package.json b/e2e/test/polymer-project/package.json index 28053e6de1..686b8a9e53 100644 --- a/e2e/test/polymer-project/package.json +++ b/e2e/test/polymer-project/package.json @@ -29,7 +29,8 @@ "stryker": "../../../packages/stryker", "stryker-wct-runner": "../../../packages/stryker-wct-runner", "stryker-javascript-mutator": "../../../packages/stryker-javascript-mutator", - "@stryker-mutator/util": "../../../packages/stryker-util" + "@stryker-mutator/util": "../../../packages/stryker-util", + "typed-inject": "../../../packages/typed-inject" }, "scripts": { "prepare": "install-local", diff --git a/e2e/test/vue-javascript/package.json b/e2e/test/vue-javascript/package.json index 85f3d49152..146a5794c3 100644 --- a/e2e/test/vue-javascript/package.json +++ b/e2e/test/vue-javascript/package.json @@ -88,7 +88,8 @@ "stryker-javascript-mutator": "../../../packages/stryker-javascript-mutator", "stryker-karma-runner": "../../../packages/stryker-karma-runner", "stryker-vue-mutator": "../../../packages/stryker-vue-mutator", - "@stryker-mutator/util": "../../../packages/stryker-util" + "@stryker-mutator/util": "../../../packages/stryker-util", + "typed-inject": "../../../packages/typed-inject" }, "engines": { "node": ">= 6.0.0", diff --git a/packages/stryker-api/package.json b/packages/stryker-api/package.json index fc0e932094..c4ded32563 100644 --- a/packages/stryker-api/package.json +++ b/packages/stryker-api/package.json @@ -39,6 +39,7 @@ "tslib": "~1.9.3" }, "devDependencies": { - "surrial": "~0.1.1" + "surrial": "~0.1.1", + "typed-inject": "0.0.0" } } diff --git a/packages/stryker-api/plugin.ts b/packages/stryker-api/plugin.ts new file mode 100644 index 0000000000..7e10edc4b0 --- /dev/null +++ b/packages/stryker-api/plugin.ts @@ -0,0 +1,4 @@ +export * from './src/plugin/Contexts'; +export * from './src/plugin/Plugins'; +export * from './src/plugin/PluginKind'; +export * from './src/plugin/tokens'; diff --git a/packages/stryker-api/src/config/Config.ts b/packages/stryker-api/src/config/Config.ts index ba20018889..b7a6e9ad0a 100644 --- a/packages/stryker-api/src/config/Config.ts +++ b/packages/stryker-api/src/config/Config.ts @@ -34,7 +34,7 @@ export default class Config implements StrykerOptions { }; public allowConsoleColors: boolean = true; - public set(newConfig: StrykerOptions) { + public set(newConfig: Partial) { if (newConfig) { Object.keys(newConfig).forEach(key => { if (typeof newConfig[key] !== 'undefined') { diff --git a/packages/stryker-api/src/core/StrykerOptions.ts b/packages/stryker-api/src/core/StrykerOptions.ts index 1a986eb481..dca3439de9 100644 --- a/packages/stryker-api/src/core/StrykerOptions.ts +++ b/packages/stryker-api/src/core/StrykerOptions.ts @@ -9,7 +9,7 @@ interface StrykerOptions { /** * A list of globbing expression used for selecting the files that should be mutated. */ - mutate?: string[]; + mutate: string[]; /** * With `files` you can choose which files should be included in your test runner sandbox. @@ -32,7 +32,7 @@ interface StrykerOptions { * all the CPU cores of your machine. Default: infinity, Stryker will decide for you and tries to use * all CPUs in your machine optimally. */ - maxConcurrentTestRunners?: number; + maxConcurrentTestRunners: number; /** * A location to a config file. That file should export a function which accepts a "config" object which it uses to configure stryker @@ -45,9 +45,9 @@ interface StrykerOptions { testFramework?: string; /** - * The name of the test runner to use (default is the same name as the testFramework) + * The name of the test runner to use (default is 'command') */ - testRunner?: string; + testRunner: string; /** * The mutant generator to use to generate mutants based on your input file. @@ -61,7 +61,7 @@ interface StrykerOptions { * * The `excludedMutations` property is mandatory and contains the names of the specific mutation types to exclude from testing. * * The values must match the given names of the mutations. For example: 'BinaryExpression', 'BooleanSubstitution', etc. */ - mutator?: string | MutatorDescriptor; + mutator: string | MutatorDescriptor; /** * The names of the transpilers to use (in order). Default: []. @@ -80,12 +80,12 @@ interface StrykerOptions { * * Transpilers should ignore files marked with `transpiled = false`. See `files` array. */ - transpilers?: string[]; + transpilers: string[]; /** * Thresholds for mutation score. */ - thresholds?: Partial; + thresholds: MutationScoreThresholds; /** * Indicates which coverage analysis strategy to use. @@ -95,7 +95,7 @@ interface StrykerOptions { * 'all': Analyse the coverage for the entire test suite. * 'off': Don't use coverage analysis */ - coverageAnalysis?: 'perTest' | 'all' | 'off'; + coverageAnalysis: 'perTest' | 'all' | 'off'; /** * DEPRECATED PROPERTY. Please use the `reporters` property @@ -106,24 +106,24 @@ interface StrykerOptions { * Possible values: 'clear-text', 'progress'. * Load more plugins to be able to use more reporters */ - reporters?: string[]; + reporters: string[]; /** * The log level for logging to a file. If defined, stryker will output a log file called "stryker.log". * Default: "off" */ - fileLogLevel?: LogLevel; + fileLogLevel: LogLevel; /** * The log level for logging to the console. Default: "info". */ - logLevel?: LogLevel; + logLevel: LogLevel; /** * Indicates whether or not to symlink the node_modules folder inside the sandbox folder(s). * Default: true */ - symlinkNodeModules?: boolean; + symlinkNodeModules: boolean; /** * DEPRECATED PROPERTY. Please use the `timeoutMS` property @@ -132,17 +132,17 @@ interface StrykerOptions { /** * Amount of additional time, in milliseconds, the mutation test is allowed to run */ - timeoutMS?: number; + timeoutMS: number; /** * The factor is applied on top of the other timeouts when during mutation testing */ - timeoutFactor?: number; + timeoutFactor: number; /** * A list of plugins. These plugins will be imported ('required') by Stryker upon loading. */ - plugins?: string[]; + plugins: string[]; /** * DEPRECATED @@ -154,7 +154,7 @@ interface StrykerOptions { * Indicates whether or not to use colors in console. * Default: true */ - allowConsoleColors?: boolean; + allowConsoleColors: boolean; } export default StrykerOptions; diff --git a/packages/stryker-api/src/plugin/Contexts.ts b/packages/stryker-api/src/plugin/Contexts.ts new file mode 100644 index 0000000000..10e32f9101 --- /dev/null +++ b/packages/stryker-api/src/plugin/Contexts.ts @@ -0,0 +1,53 @@ +import { LoggerFactoryMethod, Logger } from '../../logging'; +import { StrykerOptions } from '../../core'; +import { PluginResolver } from './Plugins'; +import { Config } from '../../config'; +import { PluginKind } from './PluginKind'; +import { commonTokens } from './tokens'; + +/** + * The basic dependency injection context within Stryker + */ +export interface BaseContext { + [commonTokens.getLogger]: LoggerFactoryMethod; + [commonTokens.logger]: Logger; + [commonTokens.pluginResolver]: PluginResolver; +} + +/** + * The dependency injection context for most of Stryker's plugins. + * Can inject basic stuff as well as the Stryker options + */ +export interface OptionsContext extends BaseContext { + [commonTokens.options]: StrykerOptions; + /** + * @deprecated This is just here to migrate between old and new plugins. Don't use this! Use `options` instead + */ + [commonTokens.config]: Config; +} + +/** + * The dependency injection context for a `TranspilerPlugin` + */ +export interface TranspilerPluginContext extends OptionsContext { + [commonTokens.produceSourceMaps]: boolean; +} + +/** + * The dependency injection context for a `TestRunnerPlugin` + */ +export interface TestRunnerPluginContext extends OptionsContext { + [commonTokens.sandboxFileNames]: ReadonlyArray; +} + +/** + * Lookup type for plugin contexts by kind. + */ +export interface PluginContexts { + [PluginKind.ConfigEditor]: BaseContext; + [PluginKind.Mutator]: OptionsContext; + [PluginKind.Reporter]: OptionsContext; + [PluginKind.TestFramework]: OptionsContext; + [PluginKind.TestRunner]: TestRunnerPluginContext; + [PluginKind.Transpiler]: TranspilerPluginContext; +} diff --git a/packages/stryker-api/src/plugin/PluginKind.ts b/packages/stryker-api/src/plugin/PluginKind.ts new file mode 100644 index 0000000000..76201af0f2 --- /dev/null +++ b/packages/stryker-api/src/plugin/PluginKind.ts @@ -0,0 +1,11 @@ +/** + * The plugin kinds supported by Stryker + */ +export enum PluginKind { + ConfigEditor = 'ConfigEditor', + TestRunner = 'TestRunner', + TestFramework = 'TestFramework', + Transpiler = 'Transpiler', + Mutator = 'Mutator', + Reporter = 'Reporter' +} diff --git a/packages/stryker-api/src/plugin/Plugins.ts b/packages/stryker-api/src/plugin/Plugins.ts new file mode 100644 index 0000000000..10adc4aea6 --- /dev/null +++ b/packages/stryker-api/src/plugin/Plugins.ts @@ -0,0 +1,92 @@ +import { TestFramework } from '../../test_framework'; +import { TestRunner } from '../../test_runner'; +import { Reporter } from '../../report'; +import { Mutator } from '../../mutant'; +import { Transpiler } from '../../transpile'; +import { PluginContexts } from './Contexts'; +import { ConfigEditor } from '../../config'; +import { InjectionToken, InjectableClass, InjectableFunction } from 'typed-inject'; +import { PluginKind } from './PluginKind'; + +/** + * Represents a StrykerPlugin + */ +export type Plugin[]> = + FactoryPlugin | ClassPlugin; + +/** + * Represents a plugin that is created with a factory method + */ +export interface FactoryPlugin[]> { + readonly kind: TPluginKind; + readonly name: string; + /** + * The factory method used to create the plugin + */ + readonly factory: InjectableFunction; +} + +/** + * Represents a plugin that is created by instantiating a class. + */ +export interface ClassPlugin[]> { + readonly kind: TPluginKind; + readonly name: string; + readonly injectableClass: InjectableClass; +} + +/** + * Declare a class plugin. Use this method in order to type check the dependency graph of your plugin + * @param kind The plugin kind + * @param name The name of the plugin + * @param injectableClass The class to be instantiated for the plugin + */ +export function declareClassPlugin[]>(kind: TPluginKind, name: string, injectableClass: InjectableClass): + ClassPlugin { + return { + injectableClass, + kind, + name + }; +} + +/** + * Declare a factory plugin. Use this method in order to type check the dependency graph of your plugin, + * @param kind The plugin kind + * @param name The name of the plugin + * @param factory The factory used to instantiate the plugin + */ +export function declareFactoryPlugin[]>(kind: TPluginKind, name: string, factory: InjectableFunction): + FactoryPlugin { + return { + factory, + kind, + name + }; +} + +/** + * Lookup type for plugin interfaces by kind. + */ +export interface PluginInterfaces { + [PluginKind.ConfigEditor]: ConfigEditor; + [PluginKind.Mutator]: Mutator; + [PluginKind.Reporter]: Reporter; + [PluginKind.TestFramework]: TestFramework; + [PluginKind.TestRunner]: TestRunner; + [PluginKind.Transpiler]: Transpiler; +} + +/** + * Lookup type for plugins by kind. + */ +export type Plugins = { + [TPluginKind in keyof PluginInterfaces]: Plugin[]>; +}; + +/** + * Plugin resolver responsible to load plugins + */ +export interface PluginResolver { + resolve(kind: T, name: string): Plugins[T]; +} diff --git a/packages/stryker-api/src/plugin/tokens.ts b/packages/stryker-api/src/plugin/tokens.ts new file mode 100644 index 0000000000..75f17ae780 --- /dev/null +++ b/packages/stryker-api/src/plugin/tokens.ts @@ -0,0 +1,42 @@ + +/** + * Define a string literal. + * @param value Token literal + */ +function stringLiteral(value: T): T { + return value; +} + +const target: import('typed-inject').TargetToken = '$target'; +const injector: import('typed-inject').InjectorToken = '$injector'; + +/** + * Common tokens used for dependency injection (see typed-inject readme for more information) + */ +export const commonTokens = Object.freeze({ + /** + * @deprecated Use 'options' instead. This is just hear to support plugin migration + */ + config: stringLiteral('config'), + getLogger: stringLiteral('getLogger'), + injector, + logger: stringLiteral('logger'), + options: stringLiteral('options'), + pluginResolver: stringLiteral('pluginResolver'), + produceSourceMaps: stringLiteral('produceSourceMaps'), + sandboxFileNames: stringLiteral('sandboxFileNames'), + target +}); + +/** + * Helper method to create string literal tuple type. + * @example + * ```ts + * const inject = tokens('foo', 'bar'); + * const inject2: ['foo', 'bar'] = ['foo', 'bar']; + * ``` + * @param tokens The tokens as args + */ +export function tokens(...tokens: TS): TS { + return tokens; +} diff --git a/packages/stryker-api/test/integration/install-module/install-module.ts b/packages/stryker-api/test/integration/install-module/install-module.ts index 776cab352c..9925071d0b 100644 --- a/packages/stryker-api/test/integration/install-module/install-module.ts +++ b/packages/stryker-api/test/integration/install-module/install-module.ts @@ -37,7 +37,7 @@ describe('we have a module using stryker', () => { }); }); }; - arrangeActAndAssertModule('core', ['files', 'some', 'file']); + arrangeActAndAssertModule('core', ['files', 'file']); arrangeActAndAssertModule('config', ['plugins: [ \'stryker-*\' ]']); arrangeActAndAssertModule('test_framework', ['framework-1']); arrangeActAndAssertModule('mutant', ['mutatorName: \'foo\'']); diff --git a/packages/stryker-api/test/unit/config/ConfigSpec.ts b/packages/stryker-api/test/unit/config/ConfigSpec.ts index 8c1a644aab..4f37275091 100644 --- a/packages/stryker-api/test/unit/config/ConfigSpec.ts +++ b/packages/stryker-api/test/unit/config/ConfigSpec.ts @@ -24,8 +24,9 @@ describe('Config', () => { }); it('should override thresholds when assigned', () => { - sut.set({ thresholds: {} }); - expect(sut.thresholds).deep.eq({}); + const thresholds = Object.freeze({ break: 20, low: 30, high: 40 }); + sut.set({ thresholds }); + expect(sut.thresholds).deep.eq(thresholds); }); it('should never clear thresholds', () => { diff --git a/packages/stryker-api/test/unit/plugin/tokens.spec.ts b/packages/stryker-api/test/unit/plugin/tokens.spec.ts new file mode 100644 index 0000000000..e3ca0a8609 --- /dev/null +++ b/packages/stryker-api/test/unit/plugin/tokens.spec.ts @@ -0,0 +1,26 @@ +import { expect } from 'chai'; +import { tokens, commonTokens } from '../../../plugin'; + +describe('tokens', () => { + it('should return input as array', () => { + expect(tokens('a', 'b', 'c')).deep.eq(['a', 'b', 'c']); + }); + it('should return empty array if called without parameters', () => { + expect(tokens()).deep.eq([]); + }); +}); + +describe('commonTokens', () => { + function itShouldProvideToken(token: T) { + it(`should supply token "${token}" as "${token}"`, () => { + expect(commonTokens[token]).eq(token); + }); + } + itShouldProvideToken('config'); + itShouldProvideToken('options'); + itShouldProvideToken('logger'); + itShouldProvideToken('pluginResolver'); + itShouldProvideToken('produceSourceMaps'); + itShouldProvideToken('sandboxFileNames'); + itShouldProvideToken('getLogger'); +}); diff --git a/packages/stryker-api/testResources/module/package.json b/packages/stryker-api/testResources/module/package.json index 39d45ea4d0..830ae32b6c 100644 --- a/packages/stryker-api/testResources/module/package.json +++ b/packages/stryker-api/testResources/module/package.json @@ -19,9 +19,10 @@ "license": "ISC", "devDependencies": { "install-local": "^0.4.0", - "typescript": "^2.2.2" + "typescript": "^3.2.2" }, "localDependencies": { - "stryker-api": "../.." + "stryker-api": "../..", + "typed-inject": "../../../typed-inject" } } diff --git a/packages/stryker-api/testResources/module/useCore.ts b/packages/stryker-api/testResources/module/useCore.ts index 1bf6a517b2..5216f30a71 100644 --- a/packages/stryker-api/testResources/module/useCore.ts +++ b/packages/stryker-api/testResources/module/useCore.ts @@ -1,23 +1,41 @@ import { StrykerOptions, File, Position, Location, Range, LogLevel } from 'stryker-api/core'; -const options: StrykerOptions = {}; +const minimalOptions: StrykerOptions = { + allowConsoleColors: true, + coverageAnalysis: 'off', + fileLogLevel: LogLevel.Off, + logLevel: LogLevel.Information, + maxConcurrentTestRunners: 3, + mutate: [], + mutator: '', + plugins: [], + reporters: [], + symlinkNodeModules: true, + testRunner: 'command', + thresholds: { high: 80, low: 20, break: null}, + timeoutFactor: 1.5, + timeoutMS: 5000, + transpilers: [] +}; const optionsAllArgs: StrykerOptions = { - configFile: 'string', - files: ['some'], - logLevel: LogLevel.Fatal, - mutate: ['some'], - plugins: ['string'], - reporter: 'string', - repoters: ['reporter'], - testFramework: 'string', - testRunner: 'string', - thresholds: { - break: 60, - high: 80, - low: 70 - }, - timeoutFactor: 2, - timeoutMS: 1 + allowConsoleColors: true, + configFile: '', + coverageAnalysis: 'off', + fileLogLevel: LogLevel.Off, + files: [], + logLevel: LogLevel.Information, + maxConcurrentTestRunners: 3, + mutate: [], + mutator: '', + plugins: [], + reporters: [], + symlinkNodeModules: true, + testFramework: '', + testRunner: 'command', + thresholds: { high: 80, low: 20, break: null}, + timeoutFactor: 1.5, + timeoutMS: 5000, + transpilers: [] }; const textFile: File = new File('foo/bar.js', Buffer.from('foobar')); @@ -25,4 +43,4 @@ const range: Range = [1, 2]; const position: Position = { column: 2, line: 2 }; const location: Location = { start: position, end: position }; -console.log(range, position, location, textFile, optionsAllArgs, options); +console.log(range, position, location, textFile, optionsAllArgs, minimalOptions); diff --git a/packages/stryker-api/tsconfig.src.json b/packages/stryker-api/tsconfig.src.json index 908a3f39b1..7b87cff3f0 100644 --- a/packages/stryker-api/tsconfig.src.json +++ b/packages/stryker-api/tsconfig.src.json @@ -1,17 +1,23 @@ { - "extends": "../../tsconfig.settings.json", - "compilerOptions": { - "rootDir": "." - }, - "include": [ - "src", - "config.ts", - "core.ts", - "logging.ts", - "mutant.ts", - "report.ts", - "test_framework.ts", - "test_runner.ts", - "transpile.ts" - ] + "extends": "../../tsconfig.settings.json", + "compilerOptions": { + "rootDir": "." + }, + "include": [ + "src", + "config.ts", + "core.ts", + "logging.ts", + "mutant.ts", + "report.ts", + "test_framework.ts", + "test_runner.ts", + "transpile.ts", + "plugin.ts" + ], + "references": [ + { + "path": "../typed-inject/tsconfig.src.json" + } + ] } \ No newline at end of file diff --git a/packages/stryker-jasmine-runner/package.json b/packages/stryker-jasmine-runner/package.json index ccca01a341..e16d90e739 100644 --- a/packages/stryker-jasmine-runner/package.json +++ b/packages/stryker-jasmine-runner/package.json @@ -40,7 +40,8 @@ }, "devDependencies": { "stryker-api": "^0.23.0", - "stryker-jasmine": "^0.11.0" + "stryker-jasmine": "^0.11.0", + "@stryker-mutator/test-helpers": "0.0.0" }, "initStrykerConfig": { "jasmineConfigFile": "spec/support/jasmine.json" diff --git a/packages/stryker-jasmine-runner/test/integration/JasmineRunner.it.ts b/packages/stryker-jasmine-runner/test/integration/JasmineRunner.it.ts index 1d33251255..8dbb42e6d4 100644 --- a/packages/stryker-jasmine-runner/test/integration/JasmineRunner.it.ts +++ b/packages/stryker-jasmine-runner/test/integration/JasmineRunner.it.ts @@ -4,6 +4,7 @@ import JasmineTestRunner from '../../src/JasmineTestRunner'; import { TestResult, TestStatus, RunStatus } from 'stryker-api/test_runner'; import JasmineTestFramework from 'stryker-jasmine/src/JasmineTestFramework'; import { expectTestResultsToEqual } from '../helpers/assertions'; +import { factory } from '@stryker-mutator/test-helpers'; function wrapInClosure(codeFragment: string) { return ` @@ -52,7 +53,7 @@ describe('JasmineRunner integration', () => { path.resolve('spec', 'helpers', 'jasmine_examples', 'SpecHelper.js'), path.resolve('spec', 'jasmine_examples', 'PlayerSpec.js') ], - strykerOptions: { jasmineConfigFile: 'spec/support/jasmine.json' } + strykerOptions: factory.strykerOptions({ jasmineConfigFile: 'spec/support/jasmine.json' }) }); }); it('should run the specs', async () => { @@ -124,7 +125,7 @@ describe('JasmineRunner integration', () => { sut = new JasmineTestRunner({ fileNames: [path.resolve('lib', 'error.js'), path.resolve('spec', 'errorSpec.js') - ], strykerOptions: {} + ], strykerOptions: factory.strykerOptions() }); }); @@ -146,7 +147,7 @@ describe('JasmineRunner integration', () => { fileNames: [ path.resolve('lib', 'foo.js'), path.resolve('spec', 'fooSpec.js') - ], strykerOptions: {} + ], strykerOptions: factory.strykerOptions() }); }); diff --git a/packages/stryker-jasmine-runner/test/unit/JasmineTestRunnerSpec.ts b/packages/stryker-jasmine-runner/test/unit/JasmineTestRunnerSpec.ts index 54eab5d18a..02d4aec9e0 100644 --- a/packages/stryker-jasmine-runner/test/unit/JasmineTestRunnerSpec.ts +++ b/packages/stryker-jasmine-runner/test/unit/JasmineTestRunnerSpec.ts @@ -5,6 +5,7 @@ import Jasmine = require('jasmine'); import JasmineTestRunner from '../../src/JasmineTestRunner'; import { TestResult, TestStatus, RunStatus } from 'stryker-api/test_runner'; import { expectTestResultsToEqual } from '../helpers/assertions'; +import { factory } from '@stryker-mutator/test-helpers'; type SinonStubbedInstance = { [P in keyof TType]: TType[P] extends Function ? sinon.SinonStub : TType[P]; @@ -29,7 +30,7 @@ describe('JasmineTestRunner', () => { sandbox.stub(helpers, 'Jasmine').returns(jasmineStub); fileNames = ['foo.js', 'bar.js']; clock = sandbox.useFakeTimers(); - sut = new JasmineTestRunner({ fileNames, strykerOptions: { jasmineConfigFile: 'jasmineConfFile' } }); + sut = new JasmineTestRunner({ fileNames, strykerOptions: factory.strykerOptions({ jasmineConfigFile: 'jasmineConfFile' }) }); }); afterEach(() => { diff --git a/packages/stryker-jasmine-runner/tsconfig.test.json b/packages/stryker-jasmine-runner/tsconfig.test.json index d8528d3284..20c2c96560 100644 --- a/packages/stryker-jasmine-runner/tsconfig.test.json +++ b/packages/stryker-jasmine-runner/tsconfig.test.json @@ -13,6 +13,9 @@ "references": [ { "path": "./tsconfig.src.json" + }, + { + "path": "../stryker-test-helpers/tsconfig.src.json" } ] } \ No newline at end of file diff --git a/packages/stryker-karma-runner/package.json b/packages/stryker-karma-runner/package.json index 2ae7c28876..5f5161ef70 100644 --- a/packages/stryker-karma-runner/package.json +++ b/packages/stryker-karma-runner/package.json @@ -33,6 +33,7 @@ }, "devDependencies": { "@stryker-mutator/util": "0.0.3", + "@stryker-mutator/test-helpers": "0.0.0", "@types/decamelize": "^1.2.0", "@types/express": "~4.16.0", "@types/semver": "~5.5.0", diff --git a/packages/stryker-karma-runner/test/integration/KarmaTestRunner.it.ts b/packages/stryker-karma-runner/test/integration/KarmaTestRunner.it.ts index 20a0d34898..8c5c4a7219 100644 --- a/packages/stryker-karma-runner/test/integration/KarmaTestRunner.it.ts +++ b/packages/stryker-karma-runner/test/integration/KarmaTestRunner.it.ts @@ -6,6 +6,7 @@ import { expectTestResults } from '../helpers/assertions'; import http = require('http'); import { promisify } from '@stryker-mutator/util'; import { FilePattern } from 'karma'; +import { factory } from '@stryker-mutator/test-helpers'; function wrapInClosure(codeFragment: string) { return ` @@ -17,7 +18,7 @@ function wrapInClosure(codeFragment: string) { function createRunnerOptions(files: ReadonlyArray, coverageAnalysis: 'all' | 'perTest' | 'off' = 'off'): RunnerOptions { return { fileNames: [], - strykerOptions: { + strykerOptions: factory.strykerOptions({ coverageAnalysis, karma: { config: { @@ -26,7 +27,7 @@ function createRunnerOptions(files: ReadonlyArray, coverag reporters: [] } } - } + }) }; } diff --git a/packages/stryker-karma-runner/test/unit/KarmaTestRunnerSpec.ts b/packages/stryker-karma-runner/test/unit/KarmaTestRunnerSpec.ts index 4d9cf27d2c..c7e720afc9 100644 --- a/packages/stryker-karma-runner/test/unit/KarmaTestRunnerSpec.ts +++ b/packages/stryker-karma-runner/test/unit/KarmaTestRunnerSpec.ts @@ -19,6 +19,7 @@ import StrykerKarmaSetup, { } from '../../src/StrykerKarmaSetup'; import StrykerReporter from '../../src/StrykerReporter'; import TestHooksMiddleware from '../../src/TestHooksMiddleware'; +import { factory } from '@stryker-mutator/test-helpers'; describe('KarmaTestRunner', () => { let projectStarterMock: sinon.SinonStubbedInstance; @@ -31,7 +32,7 @@ describe('KarmaTestRunner', () => { beforeEach(() => { settings = { fileNames: ['foo.js', 'bar.js'], - strykerOptions: {} + strykerOptions: factory.strykerOptions() }; reporterMock = new EventEmitter(); projectStarterMock = sandbox.createStubInstance(ProjectStarter); diff --git a/packages/stryker-karma-runner/tsconfig.test.json b/packages/stryker-karma-runner/tsconfig.test.json index afcfd972e9..bb0b0077ce 100644 --- a/packages/stryker-karma-runner/tsconfig.test.json +++ b/packages/stryker-karma-runner/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-mocha-runner/package.json b/packages/stryker-mocha-runner/package.json index d0b8f0f9b5..d3da6ca4b7 100644 --- a/packages/stryker-mocha-runner/package.json +++ b/packages/stryker-mocha-runner/package.json @@ -39,7 +39,8 @@ "devDependencies": { "@types/multimatch": "~2.1.2", "stryker-api": "^0.23.0", - "stryker-mocha-framework": "^0.14.0" + "stryker-mocha-framework": "^0.14.0", + "@stryker-mutator/test-helpers": "0.0.0" }, "peerDependencies": { "mocha": ">= 2.3.3 < 6", diff --git a/packages/stryker-mocha-runner/test/helpers/mockHelpers.ts b/packages/stryker-mocha-runner/test/helpers/mockHelpers.ts index 4a9b01a183..2574d6fdcc 100644 --- a/packages/stryker-mocha-runner/test/helpers/mockHelpers.ts +++ b/packages/stryker-mocha-runner/test/helpers/mockHelpers.ts @@ -1,6 +1,7 @@ import * as sinon from 'sinon'; import { Logger } from 'stryker-api/logging'; import { RunnerOptions } from 'stryker-api/test_runner'; +import { factory } from '@stryker-mutator/test-helpers'; export type Mock = { [P in keyof T]: sinon.SinonStub; @@ -27,12 +28,12 @@ export function logger(): Mock { }; } -export const runnerOptions = factory(() => ({ +export const runnerOptions = factoryMethod(() => ({ fileNames: ['src/math.js', 'test/mathSpec.js'], - strykerOptions: { mochaOptions: {} } + strykerOptions: factory.strykerOptions({ mochaOptions: {} }) })); -function factory(defaults: () => T): (overrides?: Partial) => T { +function factoryMethod(defaults: () => T): (overrides?: Partial) => T { return (overrides?: Partial): T => { return Object.assign(defaults(), overrides); }; diff --git a/packages/stryker-mocha-runner/test/integration/MochaTestFrameworkIntegrationTestWorker.ts b/packages/stryker-mocha-runner/test/integration/MochaTestFrameworkIntegrationTestWorker.ts index 7e7dfab964..ad261dac9a 100644 --- a/packages/stryker-mocha-runner/test/integration/MochaTestFrameworkIntegrationTestWorker.ts +++ b/packages/stryker-mocha-runner/test/integration/MochaTestFrameworkIntegrationTestWorker.ts @@ -1,6 +1,7 @@ import MochaTestRunner from '../../src/MochaTestRunner'; import * as path from 'path'; import { RunResult } from 'stryker-api/test_runner'; +import { factory } from '../../../stryker-test-helpers/src'; export const AUTO_START_ARGUMENT = '2e164669-acf1-461c-9c05-2be139614de2'; @@ -20,13 +21,13 @@ export default class MochaTestFrameworkIntegrationTestWorker { path.resolve(__dirname, '..', '..', 'testResources', 'sampleProject', 'MyMath.js'), path.resolve(__dirname, '..', '..', 'testResources', 'sampleProject', 'MyMathSpec.js') ], - strykerOptions: { + strykerOptions: factory.strykerOptions({ mochaOptions: { files: [ path.resolve(__dirname, '..', '..', 'testResources', 'sampleProject', 'MyMathSpec.js') ] } - } + }) }); this.listenForParentProcess(); try { diff --git a/packages/stryker-mocha-runner/test/integration/QUnitSampleSpec.ts b/packages/stryker-mocha-runner/test/integration/QUnitSampleSpec.ts index 1f9493e107..1b03d30f0a 100644 --- a/packages/stryker-mocha-runner/test/integration/QUnitSampleSpec.ts +++ b/packages/stryker-mocha-runner/test/integration/QUnitSampleSpec.ts @@ -3,6 +3,7 @@ import { expect } from 'chai'; import { RunStatus } from 'stryker-api/test_runner'; import MochaTestRunner from '../../src/MochaTestRunner'; import { runnerOptions } from '../helpers/mockHelpers'; +import { factory } from '@stryker-mutator/test-helpers'; describe('QUnit sample', () => { @@ -14,7 +15,7 @@ describe('QUnit sample', () => { }; const sut = new MochaTestRunner(runnerOptions({ fileNames: mochaOptions.files, - strykerOptions: { mochaOptions } + strykerOptions: factory.strykerOptions({ mochaOptions }) })); await sut.init(); const actualResult = await sut.run({}); @@ -34,13 +35,13 @@ describe('QUnit sample', () => { resolve('./testResources/qunit-sample/MyMathSpec.js'), resolve('./testResources/qunit-sample/MyMath.js') ], - strykerOptions: { + strykerOptions: factory.strykerOptions({ mochaOptions: { files: [ resolve('./testResources/qunit-sample/MyMathSpec.js') ] } - } + }) }); await sut.init(); const actualResult = await sut.run({}); diff --git a/packages/stryker-mocha-runner/test/integration/SampleProjectSpec.ts b/packages/stryker-mocha-runner/test/integration/SampleProjectSpec.ts index 39091a069e..03f749eb09 100644 --- a/packages/stryker-mocha-runner/test/integration/SampleProjectSpec.ts +++ b/packages/stryker-mocha-runner/test/integration/SampleProjectSpec.ts @@ -4,6 +4,7 @@ import { TestResult, RunResult, TestStatus, RunStatus, RunnerOptions } from 'str import * as chaiAsPromised from 'chai-as-promised'; import * as path from 'path'; import MochaRunnerOptions from '../../src/MochaRunnerOptions'; +import { factory } from '../../../stryker-test-helpers/src'; chai.use(chaiAsPromised); const expect = chai.expect; @@ -32,9 +33,9 @@ describe('Running a sample project', () => { ]; const testRunnerOptions: RunnerOptions = { fileNames: files, - strykerOptions: { + strykerOptions: factory.strykerOptions({ mochaOptions: { files } - } + }) }; sut = new MochaTestRunner(testRunnerOptions); return sut.init(); @@ -67,7 +68,7 @@ describe('Running a sample project', () => { }; const options: RunnerOptions = { fileNames: files, - strykerOptions: { mochaOptions } + strykerOptions: factory.strykerOptions({ mochaOptions }) }; sut = new MochaTestRunner(options); return sut.init(); @@ -87,13 +88,13 @@ describe('Running a sample project', () => { resolve('testResources/sampleProject/MyMath.js'), resolve('testResources/sampleProject/MyMathFailedSpec.js') ], - strykerOptions: { + strykerOptions: factory.strykerOptions({ mochaOptions: { files: [ resolve('testResources/sampleProject/MyMath.js'), resolve('testResources/sampleProject/MyMathFailedSpec.js')], } - } + }) }); return sut.init(); }); @@ -110,9 +111,9 @@ describe('Running a sample project', () => { const files = [resolve('./testResources/sampleProject/MyMath.js')]; const testRunnerOptions = { fileNames: files, - strykerOptions: { + strykerOptions: factory.strykerOptions({ mochaOptions: { files } - } + }) }; sut = new MochaTestRunner(testRunnerOptions); return sut.init(); diff --git a/packages/stryker-mocha-runner/test/unit/MochaTestRunnerSpec.ts b/packages/stryker-mocha-runner/test/unit/MochaTestRunnerSpec.ts index ade0e17fb1..0ab3e06911 100644 --- a/packages/stryker-mocha-runner/test/unit/MochaTestRunnerSpec.ts +++ b/packages/stryker-mocha-runner/test/unit/MochaTestRunnerSpec.ts @@ -9,6 +9,7 @@ import LibWrapper from '../../src/LibWrapper'; import * as utils from '../../src/utils'; import { Mock, mock, logger, runnerOptions } from '../helpers/mockHelpers'; import MochaRunnerOptions from '../../src/MochaRunnerOptions'; +import { factory } from '../../../stryker-test-helpers/src'; describe('MochaTestRunner', () => { @@ -41,7 +42,7 @@ describe('MochaTestRunner', () => { it('should should add all mocha test files on run()', async () => { multimatchStub.returns(['foo.js', 'bar.js', 'foo2.js']); sut = new MochaTestRunner(runnerOptions({ - strykerOptions: { mochaOptions: {} } + strykerOptions: factory.strykerOptions({ mochaOptions: {} }) })); await sut.init(); await actRun(); @@ -79,7 +80,7 @@ describe('MochaTestRunner', () => { timeout: 2000, ui: 'assert' }; - sut = new MochaTestRunner(runnerOptions({ strykerOptions: { mochaOptions } })); + sut = new MochaTestRunner(runnerOptions({ strykerOptions: factory.strykerOptions({ mochaOptions }) })); await sut.init(); // Act @@ -94,7 +95,7 @@ describe('MochaTestRunner', () => { it('should pass require additional require options when constructed', () => { const mochaOptions: MochaRunnerOptions = { require: ['ts-node', 'babel-register'] }; - new MochaTestRunner(runnerOptions({ strykerOptions: { mochaOptions } })); + new MochaTestRunner(runnerOptions({ strykerOptions: factory.strykerOptions({ mochaOptions }) })); expect(requireStub).calledTwice; expect(requireStub).calledWith('ts-node'); expect(requireStub).calledWith('babel-register'); @@ -102,7 +103,7 @@ describe('MochaTestRunner', () => { it('should pass and resolve relative require options when constructed', () => { const mochaOptions: MochaRunnerOptions = { require: ['./setup.js', 'babel-register'] }; - new MochaTestRunner(runnerOptions({ strykerOptions: { mochaOptions } })); + new MochaTestRunner(runnerOptions({ strykerOptions: factory.strykerOptions({ mochaOptions }) })); const resolvedRequire = path.resolve('./setup.js'); expect(requireStub).calledTwice; expect(requireStub).calledWith(resolvedRequire); @@ -166,7 +167,7 @@ describe('MochaTestRunner', () => { multimatchStub.returns(['foo.js']); sut = new MochaTestRunner(runnerOptions({ fileNames: expectedFiles, - strykerOptions: { mochaOptions: { files: relativeGlobPatterns } } + strykerOptions: factory.strykerOptions({ mochaOptions: { files: relativeGlobPatterns } }) })); sut.init(); expect(multimatchStub).calledWith(expectedFiles, expectedGlobPatterns); diff --git a/packages/stryker-mocha-runner/tsconfig.json b/packages/stryker-mocha-runner/tsconfig.json index 2efdf04799..e98368432f 100644 --- a/packages/stryker-mocha-runner/tsconfig.json +++ b/packages/stryker-mocha-runner/tsconfig.json @@ -1,20 +1,11 @@ { - "extends": "../../tsconfig.settings.json", - "compilerOptions": { - "rootDir": "." - }, - "exclude": [ - "node_modules", - "src/**/*.d.ts", - "test/**/*.d.ts", - "testResources" - ], + "files": [], "references": [ { - "path": "../stryker-api/tsconfig.src.json" + "path": "./tsconfig.src.json" }, { - "path": "../stryker-mocha-framework/tsconfig.src.json" + "path": "./tsconfig.test.json" } ] } \ No newline at end of file diff --git a/packages/stryker-mocha-runner/tsconfig.src.json b/packages/stryker-mocha-runner/tsconfig.src.json new file mode 100644 index 0000000000..870e48bb28 --- /dev/null +++ b/packages/stryker-mocha-runner/tsconfig.src.json @@ -0,0 +1,17 @@ +{ + "extends": "../../tsconfig.settings.json", + "compilerOptions": { + "rootDir": "." + }, + "include": [ + "src" + ], + "references": [ + { + "path": "../stryker-api/tsconfig.src.json" + }, + { + "path": "../stryker-mocha-framework/tsconfig.src.json" + } + ] +} \ No newline at end of file diff --git a/packages/stryker-mocha-runner/tsconfig.test.json b/packages/stryker-mocha-runner/tsconfig.test.json new file mode 100644 index 0000000000..40f675e3a3 --- /dev/null +++ b/packages/stryker-mocha-runner/tsconfig.test.json @@ -0,0 +1,20 @@ +{ + "extends": "../../tsconfig.settings.json", + "compilerOptions": { + "rootDir": ".", + "types": [ + "mocha" + ] + }, + "include": [ + "test" + ], + "references": [ + { + "path": "./tsconfig.src.json" + }, + { + "path": "../stryker-test-helpers/tsconfig.src.json" + } + ] +} \ No newline at end of file diff --git a/packages/stryker-test-helpers/package.json b/packages/stryker-test-helpers/package.json new file mode 100644 index 0000000000..00e6b50951 --- /dev/null +++ b/packages/stryker-test-helpers/package.json @@ -0,0 +1,25 @@ +{ + "name": "@stryker-mutator/test-helpers", + "private": true, + "version": "0.0.0", + "description": "A helper package for testing", + "main": "src/index.js", + "typings": "src/index.d.ts", + "scripts": { + }, + "repository": { + "type": "git", + "url": "git+https://github.com/stryker-mutator/stryker.git" + }, + "keywords": [], + "author": "Nico Jansen ", + "bugs": { + "url": "https://github.com/stryker-mutator/stryker/issues" + }, + "homepage": "https://github.com/stryker-mutator/stryker/tree/master/packages/@stryker-mutator/test-helpers#readme", + "license": "ISC", + "devDependencies": { + "stryker-api": "^0.23.0", + "typed-inject": "0.0.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 new file mode 100644 index 0000000000..a0aa6033bb --- /dev/null +++ b/packages/stryker-test-helpers/src/TestInjector.ts @@ -0,0 +1,48 @@ +import { PluginResolver, OptionsContext, commonTokens } from 'stryker-api/plugin'; +import { StrykerOptions } from 'stryker-api/core'; +import { Logger } from 'stryker-api/logging'; +import * as factory from './factory'; +import * as sinon from 'sinon'; +import { rootInjector, Injector, Scope } from 'typed-inject'; +import { Config } from 'stryker-api/config'; + +class TestInjector { + + public providePluginResolver = (): PluginResolver => { + return this.pluginResolver; + } + public provideLogger = (): Logger => { + return this.logger; + } + public provideConfig = () => { + const config = new Config(); + config.set(this.provideOptions()); + return config; + } + + public provideOptions = () => { + return factory.strykerOptions(this.options); + } + + public pluginResolver: sinon.SinonStubbedInstance; + public options: Partial; + public logger: sinon.SinonStubbedInstance; + public injector: Injector = rootInjector + .provideValue(commonTokens.getLogger, this.provideLogger) + .provideFactory(commonTokens.logger, this.provideLogger, Scope.Transient) + .provideFactory(commonTokens.options, this.provideOptions, Scope.Transient) + .provideFactory(commonTokens.config, this.provideConfig, Scope.Transient) + .provideFactory(commonTokens.pluginResolver, this.providePluginResolver, Scope.Transient); + + public reset() { + this.options = {}; + this.logger = factory.logger(); + this.pluginResolver = { + resolve: sinon.stub() + }; + } +} + +const testInjector = new TestInjector(); +testInjector.reset(); +export default testInjector; diff --git a/packages/stryker-test-helpers/src/factory.ts b/packages/stryker-test-helpers/src/factory.ts new file mode 100644 index 0000000000..63441d0cd2 --- /dev/null +++ b/packages/stryker-test-helpers/src/factory.ts @@ -0,0 +1,174 @@ +import { TestResult, TestStatus, RunResult, RunStatus } from 'stryker-api/test_runner'; +import { Mutant } from 'stryker-api/mutant'; +import { Config } from 'stryker-api/config'; +import { Logger } from 'stryker-api/logging'; +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'; + +/** + * A 1x1 png base64 encoded + */ +export const PNG_BASE64_ENCODED = 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAAEnQAABJ0Ad5mH3gAAAAMSURBVBhXY/j//z8ABf4C/qc1gYQAAAAASUVORK5CYII='; + +/** + * Use this factory method to create deep test data + * @param defaults + */ +function factoryMethod(defaultsFactory: () => T) { + return (overrides?: Partial) => Object.assign({}, defaultsFactory(), overrides); +} + +export const location = factoryMethod(() => ({ start: { line: 0, column: 0 }, end: { line: 0, column: 0 } })); + +export const mutantResult = factoryMethod(() => ({ + id: '256', + location: location(), + mutatedLines: '', + mutatorName: '', + originalLines: '', + range: [0, 0], + replacement: '', + sourceFilePath: '', + status: MutantStatus.Killed, + testsRan: [''] +})); + +export const mutant = factoryMethod(() => ({ + fileName: 'file', + mutatorName: 'foobarMutator', + range: [0, 0], + replacement: 'replacement' +})); + +export function logger(): sinon.SinonStubbedInstance { + return { + debug: sinon.stub(), + error: sinon.stub(), + fatal: sinon.stub(), + info: sinon.stub(), + isDebugEnabled: sinon.stub(), + isErrorEnabled: sinon.stub(), + isFatalEnabled: sinon.stub(), + isInfoEnabled: sinon.stub(), + isTraceEnabled: sinon.stub(), + isWarnEnabled: sinon.stub(), + trace: sinon.stub(), + warn: sinon.stub() + }; +} + +export const testFramework = factoryMethod(() => ({ + beforeEach(codeFragment: string) { return `beforeEach(){ ${codeFragment}}`; }, + afterEach(codeFragment: string) { return `afterEach(){ ${codeFragment}}`; }, + filter(selections: TestSelection[]) { return `filter: ${selections}`; } +})); + +export const scoreResult = factoryMethod(() => ({ + childResults: [], + killed: 0, + mutationScore: 0, + mutationScoreBasedOnCoveredCode: 0, + name: 'name', + noCoverage: 0, + path: 'path', + representsFile: true, + runtimeErrors: 0, + survived: 0, + timedOut: 0, + totalCovered: 0, + totalDetected: 0, + totalInvalid: 0, + totalMutants: 0, + totalUndetected: 0, + totalValid: 0, + transpileErrors: 0 +})); + +export const testResult = factoryMethod(() => ({ + name: 'name', + status: TestStatus.Success, + timeSpentMs: 10 +})); + +export const runResult = factoryMethod(() => ({ + status: RunStatus.Complete, + tests: [testResult()] +})); + +export const mutationScoreThresholds = factoryMethod(() => ({ + break: null, + high: 80, + low: 60 +})); + +export const strykerOptions = factoryMethod(() => ({ + allowConsoleColors: true, + coverageAnalysis: 'off', + fileLogLevel: LogLevel.Off, + logLevel: LogLevel.Information, + maxConcurrentTestRunners: Infinity, + mutate: ['src/**/*.js'], + mutator: 'javascript', + plugins: [], + reporters: [], + symlinkNodeModules: true, + testRunner: 'command', + thresholds: { + break: 20, + high: 80, + low: 30 + }, + timeoutFactor: 1.5, + timeoutMS: 5000, + transpilers: [] +})); + +export const config = factoryMethod(() => new Config()); + +export const ALL_REPORTER_EVENTS: (keyof Reporter)[] = + ['onSourceFileRead', 'onAllSourceFilesRead', 'onAllMutantsMatchedWithTests', 'onMutantTested', 'onAllMutantsTested', 'onScoreCalculated', 'wrapUp']; + +export function reporter(name = 'fooReporter'): sinon.SinonStubbedInstance> { + const reporter = { name } as any; + ALL_REPORTER_EVENTS.forEach(event => reporter[event] = sinon.stub()); + return reporter; +} + +export function matchedMutant(numberOfTests: number, mutantId = numberOfTests.toString()): MatchedMutant { + const scopedTestIds: number[] = []; + for (let i = 0; i < numberOfTests; i++) { + scopedTestIds.push(1); + } + return { + fileName: '', + id: mutantId, + mutatorName: '', + replacement: '', + scopedTestIds, + timeSpentScopedTests: 0 + }; +} + +export function file() { + return new File('', ''); +} + +export function fileNotFoundError(): NodeJS.ErrnoException { + return createErrnoException('ENOENT'); +} + +export function fileAlreadyExistsError(): NodeJS.ErrnoException { + return createErrnoException('EEXIST'); +} + +export function createIsDirError(): NodeJS.ErrnoException { + return createErrnoException('EISDIR'); +} + +function createErrnoException(errorCode: string) { + const fileNotFoundError: NodeJS.ErrnoException = new Error(''); + fileNotFoundError.code = errorCode; + return fileNotFoundError; +} diff --git a/packages/stryker-test-helpers/src/index.ts b/packages/stryker-test-helpers/src/index.ts new file mode 100644 index 0000000000..0c10954d84 --- /dev/null +++ b/packages/stryker-test-helpers/src/index.ts @@ -0,0 +1,3 @@ +import * as factory from './factory'; +import testInjector from './TestInjector'; +export { factory, testInjector }; diff --git a/packages/stryker-test-helpers/tsconfig.json b/packages/stryker-test-helpers/tsconfig.json new file mode 100644 index 0000000000..050108a9da --- /dev/null +++ b/packages/stryker-test-helpers/tsconfig.json @@ -0,0 +1,8 @@ +{ + "files": [], + "references": [ + { + "path": "./tsconfig.src.json" + } + ] +} \ No newline at end of file diff --git a/packages/stryker-test-helpers/tsconfig.src.json b/packages/stryker-test-helpers/tsconfig.src.json new file mode 100644 index 0000000000..2048f74eca --- /dev/null +++ b/packages/stryker-test-helpers/tsconfig.src.json @@ -0,0 +1,21 @@ +{ + "extends": "../../tsconfig.settings.json", + "compilerOptions": { + "rootDir": ".", + "types": [ + "node", + "mocha" + ] + }, + "include": [ + "src" + ], + "references": [ + { + "path": "../stryker-api/tsconfig.src.json" + }, + { + "path": "../typed-inject/tsconfig.src.json" + } + ] +} \ No newline at end of file diff --git a/packages/stryker/src/utils/StrykerError.ts b/packages/stryker-util/src/StrykerError.ts similarity index 90% rename from packages/stryker/src/utils/StrykerError.ts rename to packages/stryker-util/src/StrykerError.ts index ff9ca1c419..e825e2f3b0 100644 --- a/packages/stryker/src/utils/StrykerError.ts +++ b/packages/stryker-util/src/StrykerError.ts @@ -1,4 +1,4 @@ -import { errorToString } from './objectUtils'; +import { errorToString } from './errors'; export default class StrykerError extends Error { constructor(message: string, readonly innerError?: Error) { diff --git a/packages/stryker-util/src/errors.ts b/packages/stryker-util/src/errors.ts new file mode 100644 index 0000000000..74e55a17e2 --- /dev/null +++ b/packages/stryker-util/src/errors.ts @@ -0,0 +1,21 @@ + +export function isErrnoException(error: Error): error is NodeJS.ErrnoException { + return typeof (error as NodeJS.ErrnoException).code === 'string'; +} + +export function errorToString(error: any) { + if (!error) { + return ''; + } else if (isErrnoException(error)) { + return `${error.name}: ${error.code} (${error.syscall}) ${error.stack}`; + } else if (error instanceof Error) { + const message = `${error.name}: ${error.message}`; + if (error.stack) { + return `${message}\n${error.stack.toString()}`; + } else { + return message; + } + } else { + return error.toString(); + } +} diff --git a/packages/stryker-util/src/index.ts b/packages/stryker-util/src/index.ts index 52dd2b3016..8d70683642 100644 --- a/packages/stryker-util/src/index.ts +++ b/packages/stryker-util/src/index.ts @@ -1,3 +1,5 @@ export { default as fsAsPromised } from './fsAsPromised'; export { default as childProcessAsPromised } from './childProcessAsPromised'; export { default as promisify } from './promisify'; +export { default as StrykerError } from './StrykerError'; +export * from './errors'; diff --git a/packages/stryker-util/test/unit/StrykerError.spec.ts b/packages/stryker-util/test/unit/StrykerError.spec.ts new file mode 100644 index 0000000000..79ae54c8da --- /dev/null +++ b/packages/stryker-util/test/unit/StrykerError.spec.ts @@ -0,0 +1,17 @@ +import StrykerError from '../../src/StrykerError'; +import { expect } from 'chai'; +import { errorToString } from '../../src/errors'; + +describe('StrykerError', () => { + it('should set inner error', () => { + const innerError = new Error(); + const sut = new StrykerError('some message', innerError); + expect(sut.innerError).eq(innerError); + }); + + it('should add inner error to the message', () => { + const innerError = new Error(); + const sut = new StrykerError('some message', innerError); + expect(sut.message).eq(`some message. Inner error: ${errorToString(innerError)}`); + }); +}); diff --git a/packages/stryker-util/test/unit/errors.spec.ts b/packages/stryker-util/test/unit/errors.spec.ts new file mode 100644 index 0000000000..10e8be8bd6 --- /dev/null +++ b/packages/stryker-util/test/unit/errors.spec.ts @@ -0,0 +1,34 @@ +import { errorToString } from '../../src/errors'; +import { expect } from 'chai'; + +describe('errors', () => { + describe('errorToString', () => { + it('should return empty string if error is undefined', () => { + expect(errorToString(undefined)).eq(''); + }); + + it('should convert a nodejs Errno error to string', () => { + const error: NodeJS.ErrnoException = { + code: 'foo', + errno: 20, + message: 'message', + name: 'name', + path: 'bar', + stack: 'qux', + syscall: 'baz' + }; + expect(errorToString(error)).eq('name: foo (baz) qux'); + }); + + it('should convert a regular error to string', () => { + const error = new Error('expected error'); + expect(errorToString(error)).eq(`Error: expected error\n${error.stack && error.stack.toString()}`); + }); + + it('should convert an error without a stack trace to string', () => { + const error = new Error('expected error'); + delete error.stack; + expect(errorToString(error)).eq('Error: expected error'); + }); + }); +}); diff --git a/packages/stryker-util/tsconfig.src.json b/packages/stryker-util/tsconfig.src.json index 85f1ef81b1..809734fd2b 100644 --- a/packages/stryker-util/tsconfig.src.json +++ b/packages/stryker-util/tsconfig.src.json @@ -5,5 +5,6 @@ }, "include": [ "src" - ] + ], + "references": [ ] } \ No newline at end of file diff --git a/packages/stryker-util/tsconfig.test.json b/packages/stryker-util/tsconfig.test.json index 2a8e9b0847..1bea525937 100644 --- a/packages/stryker-util/tsconfig.test.json +++ b/packages/stryker-util/tsconfig.test.json @@ -11,7 +11,10 @@ ], "references": [ { - "path": "tsconfig.src.json" + "path": "tsconfig.src.json", + }, + { + "path": "../stryker-test-helpers/tsconfig.src.json" } ] } \ No newline at end of file diff --git a/packages/stryker/package.json b/packages/stryker/package.json index 55545b0e18..2e7299ce5e 100644 --- a/packages/stryker/package.json +++ b/packages/stryker/package.json @@ -71,10 +71,12 @@ "source-map": "~0.6.1", "surrial": "~0.1.3", "tree-kill": "~1.2.0", + "typed-inject": "0.0.0", "tslib": "~1.9.3", "typed-rest-client": "~1.0.7" }, "devDependencies": { + "@stryker-mutator/test-helpers": "0.0.0", "@types/get-port": "~4.0.0", "@types/inquirer": "~0.0.42", "@types/istanbul-lib-instrument": "~1.7.0", diff --git a/packages/stryker/src/PluginLoader.ts b/packages/stryker/src/PluginLoader.ts deleted file mode 100644 index 431bb5a5ed..0000000000 --- a/packages/stryker/src/PluginLoader.ts +++ /dev/null @@ -1,66 +0,0 @@ -import * as path from 'path'; -import { getLogger } from 'stryker-api/logging'; -import * as _ from 'lodash'; -import { importModule } from './utils/fileUtils'; -import { fsAsPromised } from '@stryker-mutator/util'; - -const IGNORED_PACKAGES = ['stryker-cli', 'stryker-api']; - -export default class PluginLoader { - private readonly log = getLogger(PluginLoader.name); - constructor(private readonly plugins: string[]) { } - - public load() { - this.getModules().forEach(moduleName => this.requirePlugin(moduleName)); - } - - private getModules() { - const modules: string[] = []; - this.plugins.forEach(pluginExpression => { - if (_.isString(pluginExpression)) { - if (pluginExpression.indexOf('*') !== -1) { - - // Plugin directory is the node_modules folder of the module that installed stryker - // So if current __dirname is './stryker/src' than the plugin directory should be 2 directories above - const pluginDirectory = path.normalize(__dirname + '/../..'); - const regexp = new RegExp('^' + pluginExpression.replace('*', '.*')); - - this.log.debug('Loading %s from %s', pluginExpression, pluginDirectory); - const plugins = fsAsPromised.readdirSync(pluginDirectory) - .filter(pluginName => IGNORED_PACKAGES.indexOf(pluginName) === -1 && regexp.test(pluginName)) - .map(pluginName => pluginDirectory + '/' + pluginName); - if (plugins.length === 0) { - this.log.debug('Expression %s not resulted in plugins to load', pluginExpression); - } - plugins - .map(plugin => path.basename(plugin)) - .map(plugin => { - this.log.debug('Loading plugins %s (matched with expression %s)', plugin, pluginExpression); - return plugin; - }) - .forEach(p => modules.push(p)); - } else { - modules.push(pluginExpression); - } - } else { - this.log.warn('Ignoring plugin %s, as its not a string type', pluginExpression); - } - }); - - return modules; - } - - private requirePlugin(name: string) { - this.log.debug(`Loading plugins ${name}`); - try { - importModule(name); - } catch (e) { - if (e.code === 'MODULE_NOT_FOUND' && e.message.indexOf(name) !== -1) { - this.log.warn('Cannot find plugin "%s".\n Did you forget to install it ?\n' + - ' npm install %s --save-dev', name, name); - } else { - this.log.warn('Error during loading "%s" plugin:\n %s', name, e.message); - } - } - } -} diff --git a/packages/stryker/src/ReporterOrchestrator.ts b/packages/stryker/src/ReporterOrchestrator.ts deleted file mode 100644 index 0731fc694f..0000000000 --- a/packages/stryker/src/ReporterOrchestrator.ts +++ /dev/null @@ -1,80 +0,0 @@ -import { getLogger } from 'stryker-api/logging'; -import { ReporterFactory } from 'stryker-api/report'; -import BroadcastReporter, { - NamedReporter -} from './reporters/BroadcastReporter'; -import ClearTextReporter from './reporters/ClearTextReporter'; -import DashboardReporter from './reporters/DashboardReporter'; -import DotsReporter from './reporters/DotsReporter'; -import EventRecorderReporter from './reporters/EventRecorderReporter'; -import ProgressAppendOnlyReporter from './reporters/ProgressAppendOnlyReporter'; -import ProgressReporter from './reporters/ProgressReporter'; -import StrictReporter from './reporters/StrictReporter'; -import { Config } from 'stryker-api/config'; - -function registerDefaultReporters() { - ReporterFactory.instance().register( - 'progress-append-only', - ProgressAppendOnlyReporter - ); - ReporterFactory.instance().register('progress', ProgressReporter); - ReporterFactory.instance().register('dots', DotsReporter); - ReporterFactory.instance().register('clear-text', ClearTextReporter); - ReporterFactory.instance().register('event-recorder', EventRecorderReporter); - ReporterFactory.instance().register('dashboard', DashboardReporter); -} -registerDefaultReporters(); - -export default class ReporterOrchestrator { - private readonly log = getLogger(ReporterOrchestrator.name); - constructor(private readonly options: Config) {} - - public createBroadcastReporter(): StrictReporter { - const reporters: NamedReporter[] = []; - const reporterOption = this.options.reporters; - if (reporterOption && reporterOption.length) { - reporterOption.forEach(reporterName => - reporters.push(this.createReporter(reporterName)) - ); - } else { - this.log.warn( - `No reporter configured. Please configure one or more reporters in the (for example: reporters: ['progress'])` - ); - this.logPossibleReporters(); - } - return new BroadcastReporter(reporters); - } - - private createReporter(name: string) { - if (name === 'progress' && !process.stdout.isTTY) { - this.log.info( - 'Detected that current console does not support the "progress" reporter, downgrading to "progress-append-only" reporter' - ); - return { - name: 'progress-append-only', - reporter: ReporterFactory.instance().create( - 'progress-append-only', - this.options - ) - }; - } else { - return { - name, - reporter: ReporterFactory.instance().create(name, this.options) - }; - } - } - - private logPossibleReporters() { - let possibleReportersCsv = ''; - ReporterFactory.instance() - .knownNames() - .forEach(name => { - if (possibleReportersCsv.length) { - possibleReportersCsv += ', '; - } - possibleReportersCsv += name; - }); - this.log.warn(`Possible reporters: ${possibleReportersCsv}`); - } -} diff --git a/packages/stryker/src/Stryker.ts b/packages/stryker/src/Stryker.ts index d47455a302..a3865e41b4 100644 --- a/packages/stryker/src/Stryker.ts +++ b/packages/stryker/src/Stryker.ts @@ -1,51 +1,67 @@ -import { getLogger, Logger } from 'stryker-api/logging'; +import { getLogger } from 'stryker-api/logging'; import { Config, ConfigEditorFactory } 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 ReporterOrchestrator from './ReporterOrchestrator'; import TestFrameworkOrchestrator from './TestFrameworkOrchestrator'; import MutantTestMatcher from './MutantTestMatcher'; import InputFileResolver from './input/InputFileResolver'; import ConfigReader from './config/ConfigReader'; -import PluginLoader from './PluginLoader'; +import PluginLoader from './di/PluginLoader'; import ScoreResultCalculator from './ScoreResultCalculator'; import ConfigValidator from './config/ConfigValidator'; import { freezeRecursively, isPromise } from './utils/objectUtils'; import { TempFolder } from './utils/TempFolder'; import Timer from './utils/Timer'; -import StrictReporter from './reporters/StrictReporter'; 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'; export default class Stryker { public config: Config; private readonly timer = new Timer(); - private readonly reporter: StrictReporter; + private readonly reporter: BroadcastReporter; private readonly testFramework: TestFramework | null; - private readonly log: Logger; + private readonly log = getLogger(Stryker.name); + private readonly injector: Injector; /** * The Stryker mutation tester. * @constructor * @param {Object} [options] - Optional options. */ - constructor(options: StrykerOptions) { + constructor(options: Partial) { LogConfigurator.configureMainProcess(options.logLevel, options.fileLogLevel, options.allowConsoleColors); - this.log = getLogger(Stryker.name); const configReader = new ConfigReader(options); this.config = configReader.readConfig(); + // Log level may have changed LogConfigurator.configureMainProcess(this.config.logLevel, this.config.fileLogLevel, this.config.allowConsoleColors); // logLevel could be changed - this.loadPlugins(); - this.applyConfigEditors(); + 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 + this.applyConfigEditors(); this.freezeConfig(); - this.reporter = new ReporterOrchestrator(this.config).createBroadcastReporter(); + this.injector = rootInjector + .provideValue(commonTokens.getLogger, getLogger) + .provideFactory(commonTokens.logger, loggerFactory, Scope.Transient) + .provideValue(commonTokens.pluginResolver, pluginLoader as PluginResolver) + .provideValue(commonTokens.config, this.config) + .provideValue(commonTokens.options, this.config as StrykerOptions); + this.reporter = this.injector + .provideFactory(coreTokens.reporterPluginCreator, PluginCreator.createFactory(PluginKind.Reporter)) + .injectClass(BroadcastReporter); this.testFramework = new TestFrameworkOrchestrator(this.config).determineTestFramework(); new ConfigValidator(this.config, this.testFramework).validate(); } @@ -114,13 +130,11 @@ export default class Stryker { return mutants.filter(mutant => mutatorDescriptor.excludedMutations.indexOf(mutant.mutatorName) === -1); } } - - private loadPlugins() { - if (this.config.plugins) { - new PluginLoader(this.config.plugins).load(); - } + public addDefaultPlugins(): void { + this.config.plugins.push( + require.resolve('./reporters') + ); } - private wrapUpReporter(): Promise { const maybePromise = this.reporter.wrapUp(); if (isPromise(maybePromise)) { diff --git a/packages/stryker/src/child-proxy/ChildProcessCrashedError.ts b/packages/stryker/src/child-proxy/ChildProcessCrashedError.ts index 068dbfe8fc..779f976d66 100644 --- a/packages/stryker/src/child-proxy/ChildProcessCrashedError.ts +++ b/packages/stryker/src/child-proxy/ChildProcessCrashedError.ts @@ -1,4 +1,4 @@ -import StrykerError from '../utils/StrykerError'; +import { StrykerError } from '@stryker-mutator/util'; export default class ChildProcessCrashedError extends StrykerError { constructor( diff --git a/packages/stryker/src/child-proxy/ChildProcessProxy.ts b/packages/stryker/src/child-proxy/ChildProcessProxy.ts index cb97905bc1..9f57904a3d 100644 --- a/packages/stryker/src/child-proxy/ChildProcessProxy.ts +++ b/packages/stryker/src/child-proxy/ChildProcessProxy.ts @@ -3,10 +3,11 @@ import { fork, ChildProcess } from 'child_process'; import { File } from 'stryker-api/core'; import { getLogger } from 'stryker-api/logging'; import { WorkerMessage, WorkerMessageKind, ParentMessage, autoStart, ParentMessageKind } from './messageProtocol'; -import { serialize, deserialize, kill, isErrnoException, padLeft } from '../utils/objectUtils'; +import { serialize, deserialize, kill, padLeft } from '../utils/objectUtils'; import { Task, ExpirableTask } from '../utils/Task'; import LoggingClientContext from '../logging/LoggingClientContext'; import ChildProcessCrashedError from './ChildProcessCrashedError'; +import { isErrnoException } from '@stryker-mutator/util'; import OutOfMemoryError from './OutOfMemoryError'; import StringBuilder from '../utils/StringBuilder'; diff --git a/packages/stryker/src/child-proxy/ChildProcessProxyWorker.ts b/packages/stryker/src/child-proxy/ChildProcessProxyWorker.ts index 837762838e..54657cc677 100644 --- a/packages/stryker/src/child-proxy/ChildProcessProxyWorker.ts +++ b/packages/stryker/src/child-proxy/ChildProcessProxyWorker.ts @@ -1,9 +1,10 @@ import * as path from 'path'; import { getLogger, Logger } from 'stryker-api/logging'; import { File } from 'stryker-api/core'; -import { serialize, deserialize, errorToString } from '../utils/objectUtils'; +import { serialize, deserialize } from '../utils/objectUtils'; +import { errorToString } from '@stryker-mutator/util'; import { WorkerMessage, WorkerMessageKind, ParentMessage, autoStart, ParentMessageKind, CallMessage } from './messageProtocol'; -import PluginLoader from '../PluginLoader'; +import PluginLoader from '../di/PluginLoader'; import LogConfigurator from '../logging/LogConfigurator'; export default class ChildProcessProxyWorker { diff --git a/packages/stryker/src/config/ConfigReader.ts b/packages/stryker/src/config/ConfigReader.ts index 15e94a0b8d..e960164092 100644 --- a/packages/stryker/src/config/ConfigReader.ts +++ b/packages/stryker/src/config/ConfigReader.ts @@ -4,7 +4,7 @@ import * as path from 'path'; import { Config } from 'stryker-api/config'; import { StrykerOptions } from 'stryker-api/core'; import { getLogger } from 'stryker-api/logging'; -import StrykerError from '../utils/StrykerError'; +import { StrykerError } from '@stryker-mutator/util'; export const CONFIG_SYNTAX_HELP = ' module.exports = function(config) {\n' + ' config.set({\n' + @@ -18,7 +18,7 @@ export default class ConfigReader { private readonly log = getLogger(ConfigReader.name); - constructor(private readonly cliOptions: StrykerOptions) { } + constructor(private readonly cliOptions: Partial) { } public readConfig() { const configModule = this.loadConfigModule(); diff --git a/packages/stryker/src/config/ConfigValidator.ts b/packages/stryker/src/config/ConfigValidator.ts index 6a74398204..277173d51c 100644 --- a/packages/stryker/src/config/ConfigValidator.ts +++ b/packages/stryker/src/config/ConfigValidator.ts @@ -2,15 +2,14 @@ import { TestFramework } from 'stryker-api/test_framework'; import { MutatorDescriptor, MutationScoreThresholds, LogLevel, StrykerOptions } from 'stryker-api/core'; import { Config } from 'stryker-api/config'; import { getLogger } from 'stryker-api/logging'; -import StrykerError from '../utils/StrykerError'; +import { StrykerError } from '@stryker-mutator/util'; import { normalizeWhiteSpaces } from '../utils/objectUtils'; export default class ConfigValidator { private isValid = true; private readonly log = getLogger(ConfigValidator.name); - - constructor(private readonly strykerConfig: Config, private readonly testFramework: TestFramework | null) { } + constructor(private readonly strykerConfig: StrykerOptions, private readonly testFramework: TestFramework | null) { } public validate() { this.validateTestFramework(); diff --git a/packages/stryker/src/di/PluginCreator.ts b/packages/stryker/src/di/PluginCreator.ts new file mode 100644 index 0000000000..96430b7961 --- /dev/null +++ b/packages/stryker/src/di/PluginCreator.ts @@ -0,0 +1,40 @@ +import { Plugin, PluginKind, PluginContexts, PluginInterfaces, FactoryPlugin, ClassPlugin, PluginResolver, tokens, commonTokens } from 'stryker-api/plugin'; +import { Injector, InjectionToken, InjectableFunctionWithInject } from 'typed-inject'; + +export class PluginCreator { + + private constructor( + private readonly kind: TPluginKind, + private readonly pluginResolver: PluginResolver, + private readonly injector: Injector) { + } + + public create(name: string): PluginInterfaces[TPluginKind] { + const plugin = this.pluginResolver.resolve(this.kind, name); + if (this.isFactoryPlugin(plugin)) { + return this.injector.injectFunction(plugin.factory); + } else if (this.isClassPlugin(plugin)) { + return this.injector.injectClass(plugin.injectableClass); + } else { + throw new Error(`Plugin "${this.kind}:${name}" could not be created, missing "factory" or "injectableClass" property.`); + } + } + + private isFactoryPlugin(plugin: Plugin[]>): + plugin is FactoryPlugin[]> { + return !!(plugin as FactoryPlugin[]>).factory; + } + private isClassPlugin(plugin: Plugin[]>): + plugin is ClassPlugin[]> { + return !!(plugin as ClassPlugin[]>).injectableClass; + } + + 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); + return factory; + } +} diff --git a/packages/stryker/src/di/PluginLoader.ts b/packages/stryker/src/di/PluginLoader.ts new file mode 100644 index 0000000000..71a72670a8 --- /dev/null +++ b/packages/stryker/src/di/PluginLoader.ts @@ -0,0 +1,147 @@ +import * as path from 'path'; +import { getLogger } from 'stryker-api/logging'; +import * as _ from 'lodash'; +import { tokens, CorrespondingTypes, InjectionToken } from 'typed-inject'; +import { importModule } from '../utils/fileUtils'; +import { fsAsPromised } from '@stryker-mutator/util'; +import { Plugin, PluginKind, PluginResolver, Plugins, PluginContexts } from 'stryker-api/plugin'; +import { ConfigEditorFactory } from 'stryker-api/config'; +import { Factory } from 'stryker-api/core'; +import { ReporterFactory } from 'stryker-api/report'; +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'; + +const IGNORED_PACKAGES = ['stryker-cli', 'stryker-api']; + +interface PluginModule { + strykerPlugins: Plugin[]; +} + +export default class PluginLoader implements PluginResolver { + private readonly log = getLogger(PluginLoader.name); + private readonly pluginsByKind: Map[]> = new Map(); + + constructor(private readonly pluginDescriptors: string[]) { } + + public load() { + this.resolvePluginModules().forEach(moduleName => this.requirePlugin(moduleName)); + this.loadDeprecatedPlugins(); + } + + public resolve(kind: T, name: string): Plugins[T] { + const plugins = this.pluginsByKind.get(kind); + if (plugins) { + const plugin = plugins.find(plugin => plugin.name.toLowerCase() === name.toLowerCase()); + if (plugin) { + return plugin as any; + } else { + throw new Error(`Cannot load ${kind} plugin "${name}". Did you forget to install it? Loaded ${kind + } plugins were: ${plugins.map(p => p.name).join(', ')}`); + } + } else { + throw new Error(`Cannot load ${kind} plugin "${name}". In fact, no ${kind} plugins were loaded. Did you forget to install it?`); + } + } + + private loadDeprecatedPlugins() { + this.loadDeprecatedPluginsFor(PluginKind.ConfigEditor, ConfigEditorFactory.instance(), [], () => undefined); + this.loadDeprecatedPluginsFor(PluginKind.Reporter, ReporterFactory.instance(), tokens('config'), ([config]) => config); + this.loadDeprecatedPluginsFor(PluginKind.TestFramework, TestFrameworkFactory.instance(), tokens('options'), args => ({ options: args[0] })); + this.loadDeprecatedPluginsFor(PluginKind.Transpiler, TranspilerFactory.instance(), tokens('config', 'produceSourceMaps'), + ([config, produceSourceMaps]) => ({ config, produceSourceMaps })); + this.loadDeprecatedPluginsFor(PluginKind.Mutator, MutatorFactory.instance(), tokens('config'), ([config]) => config); + this.loadDeprecatedPluginsFor(PluginKind.TestRunner, TestRunnerFactory.instance(), tokens('options', 'sandboxFileNames'), + ([strykerOptions, fileNames]) => ({ strykerOptions, fileNames })); + } + + private loadDeprecatedPluginsFor[], TSettings>( + kind: TPlugin, + factory: Factory, + injectionTokens: Tokens, + settingsFactory: (args: CorrespondingTypes) => TSettings): void { + factory.knownNames().forEach(name => { + class ProxyPlugin { + constructor(...args: CorrespondingTypes) { + const realPlugin = factory.create(name, settingsFactory(args)); + for (const i in realPlugin) { + const method = (realPlugin as any)[i]; + if (typeof method === 'function' && method ) { + (this as any)[i] = method.bind(realPlugin); + } + } + } + public static inject = injectionTokens; + } + this.loadPlugin({ kind, name, injectableClass: ProxyPlugin }); + }); + } + + private resolvePluginModules() { + const modules: string[] = []; + this.pluginDescriptors.forEach(pluginExpression => { + if (_.isString(pluginExpression)) { + if (pluginExpression.indexOf('*') !== -1) { + + // Plugin directory is the node_modules folder of the module that installed stryker + // So if current __dirname is './stryker/src/di' so 3 directories above + const pluginDirectory = path.resolve(__dirname, '..', '..', '..'); + const regexp = new RegExp('^' + pluginExpression.replace('*', '.*')); + + this.log.debug('Loading %s from %s', pluginExpression, pluginDirectory); + const plugins = fsAsPromised.readdirSync(pluginDirectory) + .filter(pluginName => IGNORED_PACKAGES.indexOf(pluginName) === -1 && regexp.test(pluginName)) + .map(pluginName => pluginDirectory + '/' + pluginName); + if (plugins.length === 0) { + this.log.debug('Expression %s not resulted in plugins to load', pluginExpression); + } + plugins + .map(plugin => path.basename(plugin)) + .map(plugin => { + this.log.debug('Loading plugins %s (matched with expression %s)', plugin, pluginExpression); + return plugin; + }) + .forEach(p => modules.push(p)); + } else { + modules.push(pluginExpression); + } + } else { + this.log.warn('Ignoring plugin %s, as its not a string type', pluginExpression); + } + }); + + return modules; + } + + private requirePlugin(name: string) { + this.log.debug(`Loading plugins ${name}`); + try { + const module = importModule(name); + if (this.isPluginModule(module)) { + module.strykerPlugins.forEach(plugin => this.loadPlugin(plugin)); + } + } catch (e) { + if (e.code === 'MODULE_NOT_FOUND' && e.message.indexOf(name) !== -1) { + this.log.warn('Cannot find plugin "%s".\n Did you forget to install it ?\n' + + ' npm install %s --save-dev', name, name); + } else { + this.log.warn('Error during loading "%s" plugin:\n %s', name, e.message); + } + } + } + + private loadPlugin(plugin: Plugin) { + let plugins = this.pluginsByKind.get(plugin.kind); + if (!plugins) { + plugins = []; + this.pluginsByKind.set(plugin.kind, plugins); + } + plugins.push(plugin); + } + + private isPluginModule(module: unknown): module is PluginModule { + const pluginModule = (module as PluginModule); + return pluginModule && pluginModule.strykerPlugins && Array.isArray(pluginModule.strykerPlugins); + } +} diff --git a/packages/stryker/src/di/coreTokens.ts b/packages/stryker/src/di/coreTokens.ts new file mode 100644 index 0000000000..786d1c83ad --- /dev/null +++ b/packages/stryker/src/di/coreTokens.ts @@ -0,0 +1,2 @@ +export const pluginKind = 'pluginKind'; +export const reporterPluginCreator = 'reporterPluginCreator'; diff --git a/packages/stryker/src/di/loggerFactory.ts b/packages/stryker/src/di/loggerFactory.ts new file mode 100644 index 0000000000..52e55eb18f --- /dev/null +++ b/packages/stryker/src/di/loggerFactory.ts @@ -0,0 +1,8 @@ +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/initializer/NpmClient.ts b/packages/stryker/src/initializer/NpmClient.ts index 74535bd6de..378b9eba34 100644 --- a/packages/stryker/src/initializer/NpmClient.ts +++ b/packages/stryker/src/initializer/NpmClient.ts @@ -1,7 +1,7 @@ import { RestClient, IRestResponse } from 'typed-rest-client/RestClient'; import PromptOption from './PromptOption'; import { getLogger } from 'stryker-api/logging'; -import { errorToString } from '../utils/objectUtils'; +import { errorToString } from '@stryker-mutator/util'; interface NpmSearchPackageInfo { package: { diff --git a/packages/stryker/src/input/InputFileResolver.ts b/packages/stryker/src/input/InputFileResolver.ts index 887b78d178..e44d7fc4df 100644 --- a/packages/stryker/src/input/InputFileResolver.ts +++ b/packages/stryker/src/input/InputFileResolver.ts @@ -1,18 +1,14 @@ import * as path from 'path'; -import { fsAsPromised } from '@stryker-mutator/util'; +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 { glob } from '../utils/fileUtils'; import StrictReporter from '../reporters/StrictReporter'; import { SourceFile } from 'stryker-api/report'; -import StrykerError from '../utils/StrykerError'; +import { StrykerError } from '@stryker-mutator/util'; import InputFileCollection from './InputFileCollection'; -import { - normalizeWhiteSpaces, - filterEmpty, - isErrnoException -} from '../utils/objectUtils'; +import { normalizeWhiteSpaces, filterEmpty } from '../utils/objectUtils'; import { Config } from 'stryker-api/config'; function toReportSourceFile(file: File): SourceFile { diff --git a/packages/stryker/src/mutators/ES5Mutator.ts b/packages/stryker/src/mutators/ES5Mutator.ts index 5acf9b86e5..45f4cdaba0 100644 --- a/packages/stryker/src/mutators/ES5Mutator.ts +++ b/packages/stryker/src/mutators/ES5Mutator.ts @@ -1,7 +1,6 @@ import * as _ from 'lodash'; import { Logger, getLogger } from 'stryker-api/logging'; -import { Config } from 'stryker-api/config'; -import { File } from 'stryker-api/core'; +import { File, StrykerOptions } from 'stryker-api/core'; import { Mutator, Mutant } from 'stryker-api/mutant'; import * as parserUtils from '../utils/parserUtils'; import { copy } from '../utils/objectUtils'; @@ -19,7 +18,7 @@ export default class ES5Mutator implements Mutator { private readonly log: Logger; - constructor(_?: Config, private readonly mutators: NodeMutator[] = [ + constructor(_?: StrykerOptions, private readonly mutators: NodeMutator[] = [ new BinaryOperatorMutator(), new BlockStatementMutator(), new LogicalOperatorMutator(), diff --git a/packages/stryker/src/reporters/BroadcastReporter.ts b/packages/stryker/src/reporters/BroadcastReporter.ts index 161f88a6f1..8b78dd703f 100644 --- a/packages/stryker/src/reporters/BroadcastReporter.ts +++ b/packages/stryker/src/reporters/BroadcastReporter.ts @@ -1,34 +1,70 @@ import { Reporter, SourceFile, MutantResult, MatchedMutant, ScoreResult } from 'stryker-api/report'; -import { getLogger } from 'stryker-api/logging'; +import { Logger } from 'stryker-api/logging'; import { isPromise } from '../utils/objectUtils'; import StrictReporter from './StrictReporter'; - -export interface NamedReporter { - name: string; - reporter: Reporter; -} +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 { PluginCreator } from '../di/PluginCreator'; export default class BroadcastReporter implements StrictReporter { - private readonly log = getLogger(BroadcastReporter.name); - constructor(private readonly reporters: NamedReporter[]) { + public static readonly inject = tokens( + commonTokens.options, + coreTokens.reporterPluginCreator, + commonTokens.logger); + + public readonly reporters: { + [name: string]: Reporter; + }; + constructor( + private readonly options: StrykerOptions, + private readonly pluginCreator: PluginCreator, + private readonly log: Logger) { + this.reporters = {}; + this.options.reporters.forEach(reporterName => this.createReporter(reporterName)); + this.logAboutReporters(); + } + + private createReporter(reporterName: string): void { + if (reporterName === 'progress' && !process.stdout.isTTY) { + this.log.info( + 'Detected that current console does not support the "progress" reporter, downgrading to "progress-append-only" reporter' + ); + reporterName = 'progress-append-only'; + } + this.reporters[reporterName] = this.pluginCreator.create(reporterName); + } + + private logAboutReporters(): void { + const reporterNames = Object.keys(this.reporters); + if (reporterNames.length) { + if (this.log.isDebugEnabled()) { + this.log.debug(`Broadcasting to reporters ${JSON.stringify(reporterNames)}`); + } + } else { + this.log.warn('No reporter configured. Please configure one or more reporters in the (for example: reporters: [\'progress\'])'); + } } private broadcast(methodName: keyof Reporter, eventArgs: any): Promise | void { const allPromises: Promise[] = []; - this.reporters.forEach(namedReporter => { - if (typeof namedReporter.reporter[methodName] === 'function') { + Object.keys(this.reporters).forEach(reporterName => { + const reporter = this.reporters[reporterName]; + if (typeof reporter[methodName] === 'function') { try { - const maybePromise = (namedReporter.reporter[methodName] as any)(eventArgs); + const maybePromise = (reporter[methodName] as any)(eventArgs); if (isPromise(maybePromise)) { allPromises.push(maybePromise.catch(error => { - this.handleError(error, methodName, namedReporter.name); + this.handleError(error, methodName, reporterName); })); } } catch (error) { - this.handleError(error, methodName, namedReporter.name); + this.handleError(error, methodName, reporterName); } } + }); if (allPromises.length) { return Promise.all(allPromises); @@ -59,8 +95,8 @@ export default class BroadcastReporter implements StrictReporter { this.broadcast('onScoreCalculated', score); } - public wrapUp(): void | Promise { - return this.broadcast('wrapUp', undefined); + public async wrapUp(): Promise { + await this.broadcast('wrapUp', undefined); } private handleError(error: Error, methodName: string, reporterName: string) { diff --git a/packages/stryker/src/reporters/ClearTextReporter.ts b/packages/stryker/src/reporters/ClearTextReporter.ts index c566d76e44..5e0028bf6b 100644 --- a/packages/stryker/src/reporters/ClearTextReporter.ts +++ b/packages/stryker/src/reporters/ClearTextReporter.ts @@ -1,19 +1,21 @@ import chalk from 'chalk'; import { getLogger } from 'stryker-api/logging'; import { Reporter, MutantResult, MutantStatus, ScoreResult } from 'stryker-api/report'; -import { Config } from 'stryker-api/config'; -import { Position } from 'stryker-api/core'; +import { Position, StrykerOptions } from 'stryker-api/core'; import ClearTextScoreTable from './ClearTextScoreTable'; import * as os from 'os'; +import { tokens } from 'typed-inject'; export default class ClearTextReporter implements Reporter { private readonly log = getLogger(ClearTextReporter.name); - constructor(private readonly options: Config) { + constructor(private readonly options: StrykerOptions) { this.configConsoleColor(); } + public static readonly inject = tokens('options'); + private readonly out: NodeJS.WritableStream = process.stdout; private writeLine(output?: string) { diff --git a/packages/stryker/src/reporters/DashboardReporter.ts b/packages/stryker/src/reporters/DashboardReporter.ts index 29d739c846..758a8bd91e 100644 --- a/packages/stryker/src/reporters/DashboardReporter.ts +++ b/packages/stryker/src/reporters/DashboardReporter.ts @@ -3,14 +3,15 @@ import DashboardReporterClient from './dashboard-reporter/DashboardReporterClien import {getEnvironmentVariable} from '../utils/objectUtils'; import { getLogger } from 'stryker-api/logging'; import { determineCIProvider } from './ci/Provider'; -import { StrykerOptions } from 'stryker-api/core'; +import { tokens } from 'typed-inject'; export default class DashboardReporter implements Reporter { + public static readonly inject = tokens(); + private readonly log = getLogger(DashboardReporter.name); private readonly ciProvider = determineCIProvider(); constructor( - _setting: StrykerOptions, private readonly dashboardReporterClient: DashboardReporterClient = new DashboardReporterClient() ) { } diff --git a/packages/stryker/src/reporters/DotsReporter.ts b/packages/stryker/src/reporters/DotsReporter.ts index f3061127ef..a54ef2d301 100644 --- a/packages/stryker/src/reporters/DotsReporter.ts +++ b/packages/stryker/src/reporters/DotsReporter.ts @@ -3,6 +3,7 @@ import chalk from 'chalk'; import * as os from 'os'; export default class DotsReporter implements Reporter { + public onMutantTested(result: MutantResult) { let toLog: string; switch (result.status) { diff --git a/packages/stryker/src/reporters/EventRecorderReporter.ts b/packages/stryker/src/reporters/EventRecorderReporter.ts index 9ee54db908..7cc40764f6 100644 --- a/packages/stryker/src/reporters/EventRecorderReporter.ts +++ b/packages/stryker/src/reporters/EventRecorderReporter.ts @@ -5,10 +5,12 @@ import { SourceFile, MutantResult, MatchedMutant, Reporter, ScoreResult } from ' import { cleanFolder } from '../utils/fileUtils'; import StrictReporter from './StrictReporter'; import { fsAsPromised } from '@stryker-mutator/util'; +import { commonTokens } from 'stryker-api/plugin'; const DEFAULT_BASE_FOLDER = 'reports/mutation/events'; export default class EventRecorderReporter implements StrictReporter { + public static readonly inject = [commonTokens.options]; private readonly log = getLogger(EventRecorderReporter.name); private readonly allWork: Promise[] = []; diff --git a/packages/stryker/src/reporters/StrictReporter.ts b/packages/stryker/src/reporters/StrictReporter.ts index b6849f0715..4b645a1360 100644 --- a/packages/stryker/src/reporters/StrictReporter.ts +++ b/packages/stryker/src/reporters/StrictReporter.ts @@ -1,15 +1,5 @@ -import { Reporter, SourceFile, MutantResult, MatchedMutant, ScoreResult } from 'stryker-api/report'; +import { Reporter } from 'stryker-api/report'; + +type StrictReporter = Required; -/** - * Represents a Stryker reporter with all methods implemented - */ -interface StrictReporter extends Reporter { - onSourceFileRead(file: SourceFile): void; - onAllSourceFilesRead(files: SourceFile[]): void; - onAllMutantsMatchedWithTests(results: ReadonlyArray): void; - onMutantTested(result: MutantResult): void; - onAllMutantsTested(results: MutantResult[]): void; - onScoreCalculated(score: ScoreResult): void; - wrapUp(): void | Promise; -} export default StrictReporter; diff --git a/packages/stryker/src/reporters/dashboard-reporter/DashboardReporterClient.ts b/packages/stryker/src/reporters/dashboard-reporter/DashboardReporterClient.ts index 45325036d4..d434a3cd43 100644 --- a/packages/stryker/src/reporters/dashboard-reporter/DashboardReporterClient.ts +++ b/packages/stryker/src/reporters/dashboard-reporter/DashboardReporterClient.ts @@ -1,6 +1,6 @@ import { getLogger } from 'stryker-api/logging'; import { HttpClient } from 'typed-rest-client/HttpClient'; -import { errorToString } from '../../utils/objectUtils'; +import { errorToString } from '@stryker-mutator/util'; export interface StrykerDashboardReport { apiKey: string; diff --git a/packages/stryker/src/reporters/index.ts b/packages/stryker/src/reporters/index.ts new file mode 100644 index 0000000000..07389757ef --- /dev/null +++ b/packages/stryker/src/reporters/index.ts @@ -0,0 +1,16 @@ +import ClearTextReporter from './ClearTextReporter'; +import ProgressReporter from './ProgressReporter'; +import ProgressAppendOnlyReporter from './ProgressAppendOnlyReporter'; +import DotsReporter from './DotsReporter'; +import EventRecorderReporter from './EventRecorderReporter'; +import DashboardReporter from './DashboardReporter'; +import { PluginKind, declareClassPlugin } from 'stryker-api/plugin'; + +export const strykerPlugins = [ + declareClassPlugin(PluginKind.Reporter, 'clear-text', ClearTextReporter), + declareClassPlugin(PluginKind.Reporter, 'progress', ProgressReporter), + declareClassPlugin(PluginKind.Reporter, 'progress-append-only', ProgressAppendOnlyReporter), + declareClassPlugin(PluginKind.Reporter, 'dots', DotsReporter), + declareClassPlugin(PluginKind.Reporter, 'event-recorder', EventRecorderReporter), + declareClassPlugin(PluginKind.Reporter, 'dashboard', DashboardReporter) +]; diff --git a/packages/stryker/src/test-runner/ChildProcessTestRunnerWorker.ts b/packages/stryker/src/test-runner/ChildProcessTestRunnerWorker.ts index f65d8cb2bc..ce19a15f6b 100644 --- a/packages/stryker/src/test-runner/ChildProcessTestRunnerWorker.ts +++ b/packages/stryker/src/test-runner/ChildProcessTestRunnerWorker.ts @@ -1,5 +1,5 @@ import { TestRunner, TestRunnerFactory, RunnerOptions, RunOptions } from 'stryker-api/test_runner'; -import { errorToString } from '../utils/objectUtils'; +import { errorToString } from '@stryker-mutator/util'; export default class ChildProcessTestRunnerWorker implements TestRunner { diff --git a/packages/stryker/src/test-runner/CommandTestRunner.ts b/packages/stryker/src/test-runner/CommandTestRunner.ts index a43e2ded72..2ae75dfbed 100644 --- a/packages/stryker/src/test-runner/CommandTestRunner.ts +++ b/packages/stryker/src/test-runner/CommandTestRunner.ts @@ -1,8 +1,9 @@ import * as os from 'os'; import { TestRunner, RunResult, RunStatus, TestStatus, RunnerOptions } from 'stryker-api/test_runner'; import { exec } from 'child_process'; -import { errorToString, kill } from '../utils/objectUtils'; +import { kill } from '../utils/objectUtils'; import Timer from '../utils/Timer'; +import { errorToString } from '@stryker-mutator/util'; export interface CommandRunnerSettings { command: string; diff --git a/packages/stryker/src/test-runner/RetryDecorator.ts b/packages/stryker/src/test-runner/RetryDecorator.ts index 0e7ba74b76..0aa043c3df 100644 --- a/packages/stryker/src/test-runner/RetryDecorator.ts +++ b/packages/stryker/src/test-runner/RetryDecorator.ts @@ -1,8 +1,8 @@ import { RunOptions, RunResult, RunStatus } from 'stryker-api/test_runner'; import TestRunnerDecorator from './TestRunnerDecorator'; -import { errorToString } from '../utils/objectUtils'; import OutOfMemoryError from '../child-proxy/OutOfMemoryError'; import { getLogger } from 'stryker-api/logging'; +import { errorToString } from '@stryker-mutator/util'; const ERROR_MESSAGE = 'Test runner crashed. Tried twice to restart it without any luck. Last time the error message was: '; diff --git a/packages/stryker/src/transpiler/CoverageInstrumenterTranspiler.ts b/packages/stryker/src/transpiler/CoverageInstrumenterTranspiler.ts index c8ce155793..f5f85e0ab6 100644 --- a/packages/stryker/src/transpiler/CoverageInstrumenterTranspiler.ts +++ b/packages/stryker/src/transpiler/CoverageInstrumenterTranspiler.ts @@ -3,7 +3,7 @@ import { createInstrumenter, Instrumenter } from 'istanbul-lib-instrument'; import { StrykerOptions, File } from 'stryker-api/core'; import { FileCoverageData, Range } from 'istanbul-lib-coverage'; import { COVERAGE_CURRENT_TEST_VARIABLE_NAME } from './coverageHooks'; -import StrykerError from '../utils/StrykerError'; +import { StrykerError } from '@stryker-mutator/util'; export interface CoverageMaps { statementMap: { [key: string]: Range }; diff --git a/packages/stryker/src/transpiler/MutantTranspiler.ts b/packages/stryker/src/transpiler/MutantTranspiler.ts index 65fd3d02f9..e7d5d83879 100644 --- a/packages/stryker/src/transpiler/MutantTranspiler.ts +++ b/packages/stryker/src/transpiler/MutantTranspiler.ts @@ -8,8 +8,8 @@ import ChildProcessProxy, { Promisified } from '../child-proxy/ChildProcessProxy import { TranspilerOptions } from 'stryker-api/transpile'; import TranspiledMutant from '../TranspiledMutant'; import TranspileResult from './TranspileResult'; -import { errorToString } from '../utils/objectUtils'; import LoggingClientContext from '../logging/LoggingClientContext'; +import { errorToString } from '@stryker-mutator/util'; export default class MutantTranspiler { diff --git a/packages/stryker/src/transpiler/SourceMapper.ts b/packages/stryker/src/transpiler/SourceMapper.ts index a4a505d379..77464b10f9 100644 --- a/packages/stryker/src/transpiler/SourceMapper.ts +++ b/packages/stryker/src/transpiler/SourceMapper.ts @@ -4,7 +4,7 @@ import { File, Location, Position } from 'stryker-api/core'; import { Config } from 'stryker-api/config'; import { base64Decode } from '../utils/objectUtils'; import { getLogger } from 'stryker-api/logging'; -import StrykerError from '../utils/StrykerError'; +import { StrykerError } from '@stryker-mutator/util'; const SOURCE_MAP_URL_REGEX = /\/\/\s*#\s*sourceMappingURL=(.*)/g; diff --git a/packages/stryker/src/transpiler/TranspilerFacade.ts b/packages/stryker/src/transpiler/TranspilerFacade.ts index c23ef56b51..01cad28794 100644 --- a/packages/stryker/src/transpiler/TranspilerFacade.ts +++ b/packages/stryker/src/transpiler/TranspilerFacade.ts @@ -1,6 +1,6 @@ import { File } from 'stryker-api/core'; import { Transpiler, TranspilerOptions, TranspilerFactory } from 'stryker-api/transpile'; -import StrykerError from '../utils/StrykerError'; +import { StrykerError } from '@stryker-mutator/util'; class NamedTranspiler { constructor(public name: string, public transpiler: Transpiler) { } diff --git a/packages/stryker/src/utils/fileUtils.ts b/packages/stryker/src/utils/fileUtils.ts index 4387e74a00..55c32f1486 100644 --- a/packages/stryker/src/utils/fileUtils.ts +++ b/packages/stryker/src/utils/fileUtils.ts @@ -30,8 +30,8 @@ export async function cleanFolder(folderName: string) { /** * Wrapper around the 'require' function (for testability) */ -export function importModule(moduleName: string) { - require(moduleName); +export function importModule(moduleName: string): unknown { + return require(moduleName); } /** diff --git a/packages/stryker/src/utils/objectUtils.ts b/packages/stryker/src/utils/objectUtils.ts index 63f5c13c71..bf34a7fa6a 100644 --- a/packages/stryker/src/utils/objectUtils.ts +++ b/packages/stryker/src/utils/objectUtils.ts @@ -22,27 +22,6 @@ export function filterEmpty(input: (T | null | void)[]) { return input.filter(item => item !== undefined && item !== null) as T[]; } -export function isErrnoException(error: Error): error is NodeJS.ErrnoException { - return typeof (error as NodeJS.ErrnoException).code === 'string'; -} - -export function errorToString(error: any) { - if (!error) { - return ''; - } else if (isErrnoException(error)) { - return `${error.name}: ${error.code} (${error.syscall}) ${error.stack}`; - } else if (error instanceof Error) { - const message = `${error.name}: ${error.message}`; - if (error.stack) { - return `${message}\n${error.stack.toString()}`; - } else { - return message; - } - } else { - return error.toString(); - } -} - export function copy(obj: T, deep?: boolean) { if (deep) { return _.cloneDeep(obj); diff --git a/packages/stryker/test/helpers/TestRunnerMock.ts b/packages/stryker/test/helpers/TestRunnerMock.ts index 39562b6d27..8229095e34 100644 --- a/packages/stryker/test/helpers/TestRunnerMock.ts +++ b/packages/stryker/test/helpers/TestRunnerMock.ts @@ -1,5 +1,7 @@ +import * as sinon from 'sinon'; + export default class TestRunnerMock { - public init: sinon.SinonStub = sandbox.stub(); - public run: sinon.SinonStub = sandbox.stub(); - public dispose: sinon.SinonStub = sandbox.stub(); + public init: sinon.SinonStub = sinon.stub(); + public run: sinon.SinonStub = sinon.stub(); + public dispose: sinon.SinonStub = sinon.stub(); } diff --git a/packages/stryker/test/helpers/globals.ts b/packages/stryker/test/helpers/globals.ts deleted file mode 100644 index 8d26910fe6..0000000000 --- a/packages/stryker/test/helpers/globals.ts +++ /dev/null @@ -1,6 +0,0 @@ -declare const sandbox: sinon.SinonSandbox; -namespace NodeJS { - export interface Global { - sandbox: sinon.SinonSandbox; - } -} diff --git a/packages/stryker/test/helpers/initSinon.ts b/packages/stryker/test/helpers/initSinon.ts index 3f17ab7b90..00ee898357 100644 --- a/packages/stryker/test/helpers/initSinon.ts +++ b/packages/stryker/test/helpers/initSinon.ts @@ -1,9 +1,7 @@ import * as sinon from 'sinon'; - -beforeEach(() => { - global.sandbox = sinon.createSandbox(); -}); +import { testInjector } from '@stryker-mutator/test-helpers'; afterEach(() => { - global.sandbox.restore(); + sinon.restore(); + testInjector.reset(); }); diff --git a/packages/stryker/test/helpers/logMock.ts b/packages/stryker/test/helpers/logMock.ts index 41febf78d6..4e6a60c608 100644 --- a/packages/stryker/test/helpers/logMock.ts +++ b/packages/stryker/test/helpers/logMock.ts @@ -1,11 +1,12 @@ import * as logging from 'stryker-api/logging'; import { logger, Mock } from './producers'; +import * as sinon from 'sinon'; let log: Mock; beforeEach(() => { log = logger(); - sandbox.stub(logging, 'getLogger').returns(log); + sinon.stub(logging, 'getLogger').returns(log); }); export default function currentLogMock() { diff --git a/packages/stryker/test/helpers/producers.ts b/packages/stryker/test/helpers/producers.ts index 26b5c3d5d2..62f5f5dc6c 100644 --- a/packages/stryker/test/helpers/producers.ts +++ b/packages/stryker/test/helpers/producers.ts @@ -4,7 +4,7 @@ import { Config } from 'stryker-api/config'; import { Logger } from 'stryker-api/logging'; import { TestFramework, TestSelection } from 'stryker-api/test_framework'; import { MutantStatus, MatchedMutant, MutantResult, Reporter, ScoreResult } from 'stryker-api/report'; -import { MutationScoreThresholds, File, Location } from 'stryker-api/core'; +import { MutationScoreThresholds, File, Location, StrykerOptions, LogLevel } from 'stryker-api/core'; import TestableMutant from '../../src/TestableMutant'; import SourceFile from '../../src/SourceFile'; import TranspiledMutant from '../../src/TranspiledMutant'; @@ -12,11 +12,12 @@ import { FileCoverageData } from 'istanbul-lib-coverage'; import { CoverageMaps } from '../../src/transpiler/CoverageInstrumenterTranspiler'; import { MappedLocation } from '../../src/transpiler/SourceMapper'; import TranspileResult from '../../src/transpiler/TranspileResult'; +import * as sinon from 'sinon'; export type Mock = sinon.SinonStubbedInstance; export function mock(constructorFn: sinon.StubbableType): Mock { - return sandbox.createStubInstance(constructorFn); + return sinon.createStubInstance(constructorFn); } /** @@ -26,7 +27,7 @@ export function mock(constructorFn: sinon.StubbableType): Mock { function isPrimitive(value: any): boolean { return ['string', 'undefined', 'symbol', 'boolean'].indexOf(typeof value) > -1 || (typeof value === 'number' && !isNaN(value) - || value === null); + || value === null); } /** @@ -81,18 +82,18 @@ export const mutant = factoryMethod(() => ({ export const logger = (): Mock => { return { - debug: sandbox.stub(), - error: sandbox.stub(), - fatal: sandbox.stub(), - info: sandbox.stub(), - isDebugEnabled: sandbox.stub(), - isErrorEnabled: sandbox.stub(), - isFatalEnabled: sandbox.stub(), - isInfoEnabled: sandbox.stub(), - isTraceEnabled: sandbox.stub(), - isWarnEnabled: sandbox.stub(), - trace: sandbox.stub(), - warn: sandbox.stub() + debug: sinon.stub(), + error: sinon.stub(), + fatal: sinon.stub(), + info: sinon.stub(), + isDebugEnabled: sinon.stub(), + isErrorEnabled: sinon.stub(), + isFatalEnabled: sinon.stub(), + isInfoEnabled: sinon.stub(), + isTraceEnabled: sinon.stub(), + isWarnEnabled: sinon.stub(), + trace: sinon.stub(), + warn: sinon.stub() }; }; @@ -160,6 +161,28 @@ export const mutationScoreThresholds = factory({ low: 60 }); +export const strykerOptions = factoryMethod(() => ({ + allowConsoleColors: true, + coverageAnalysis: 'off', + fileLogLevel: LogLevel.Off, + logLevel: LogLevel.Information, + maxConcurrentTestRunners: Infinity, + mutate: ['src/**/*.js'], + mutator: 'javascript', + plugins: [], + reporters: [], + symlinkNodeModules: true, + testRunner: 'command', + thresholds: { + break: 20, + high: 80, + low: 30 + }, + timeoutFactor: 1.5, + timeoutMS: 5000, + transpilers: [] +})); + export const config = factoryMethod(() => new Config()); export const ALL_REPORTER_EVENTS: (keyof Reporter)[] = diff --git a/packages/stryker/test/integration/command-test-runner/CommandTestRunner.it.ts b/packages/stryker/test/integration/command-test-runner/CommandTestRunner.it.ts index 0c400aa545..38d075c3b5 100644 --- a/packages/stryker/test/integration/command-test-runner/CommandTestRunner.it.ts +++ b/packages/stryker/test/integration/command-test-runner/CommandTestRunner.it.ts @@ -4,6 +4,7 @@ import CommandTestRunner, { CommandRunnerSettings } from '../../../src/test-runn import { Config } from 'stryker-api/config'; import { RunStatus, TestStatus } from 'stryker-api/test_runner'; import * as objectUtils from '../../../src/utils/objectUtils'; +import * as sinon from 'sinon'; describe(`${CommandTestRunner.name} integration`, () => { @@ -33,7 +34,7 @@ describe(`${CommandTestRunner.name} integration`, () => { it('should kill the child process and timeout the run result if dispose is called', async () => { // Arrange - const killSpy = sandbox.spy(objectUtils, 'kill'); + const killSpy = sinon.spy(objectUtils, 'kill'); const sut = createSut({ command: 'npm run wait' }); const runPromise = sut.run(); diff --git a/packages/stryker/test/integration/config-reader/ConfigReaderSpec.ts b/packages/stryker/test/integration/config-reader/ConfigReaderSpec.ts index 905a0e1414..dd28f129b1 100644 --- a/packages/stryker/test/integration/config-reader/ConfigReaderSpec.ts +++ b/packages/stryker/test/integration/config-reader/ConfigReaderSpec.ts @@ -5,6 +5,7 @@ import * as logging from 'stryker-api/logging'; import ConfigReader from '../../../src/config/ConfigReader'; import currentLogMock from '../../helpers/logMock'; import { Mock } from '../../helpers/producers'; +import * as sinon from 'sinon'; describe('ConfigReader', () => { @@ -40,7 +41,7 @@ describe('ConfigReader', () => { describe('with a stryker.conf.js in the CWD', () => { it('should parse the config', () => { const mockCwd = process.cwd() + '/testResources/config-reader'; - sandbox.stub(process, 'cwd').returns(mockCwd); + sinon.stub(process, 'cwd').returns(mockCwd); sut = new ConfigReader({}); result = sut.readConfig(); @@ -55,7 +56,7 @@ describe('ConfigReader', () => { describe('without a stryker.conf.js in the CWD', () => { it('should return default config', () => { const mockCwd = process.cwd() + '/testResources/config-reader/no-config'; - sandbox.stub(process, 'cwd').returns(mockCwd); + sinon.stub(process, 'cwd').returns(mockCwd); sut = new ConfigReader({}); diff --git a/packages/stryker/test/integration/test-runner/ResilientTestRunnerFactory.it.ts b/packages/stryker/test/integration/test-runner/ResilientTestRunnerFactory.it.ts index beff8d27c4..902391dedc 100644 --- a/packages/stryker/test/integration/test-runner/ResilientTestRunnerFactory.it.ts +++ b/packages/stryker/test/integration/test-runner/ResilientTestRunnerFactory.it.ts @@ -10,6 +10,7 @@ import LoggingServer from '../../helpers/LoggingServer'; import LoggingClientContext from '../../../src/logging/LoggingClientContext'; import { toArray } from 'rxjs/operators'; import { sleep } from '../../helpers/testUtils'; +import { strykerOptions } from '../../helpers/producers'; describe('ResilientTestRunnerFactory integration', () => { @@ -28,12 +29,12 @@ describe('ResilientTestRunnerFactory integration', () => { loggingContext = { port, level: LogLevel.Trace }; options = { fileNames: [], - strykerOptions: { + strykerOptions: strykerOptions({ plugins: [require.resolve('./AdditionalTestRunners')], someRegex: /someRegex/, testFramework: 'jasmine', testRunner: 'karma' - } + }) }; alreadyDisposed = false; }); diff --git a/packages/stryker/test/unit/MutantTestMatcherSpec.ts b/packages/stryker/test/unit/MutantTestMatcherSpec.ts index 145d3cb760..d848371b01 100644 --- a/packages/stryker/test/unit/MutantTestMatcherSpec.ts +++ b/packages/stryker/test/unit/MutantTestMatcherSpec.ts @@ -7,12 +7,13 @@ import { StrykerOptions, File } from 'stryker-api/core'; import { MatchedMutant } from 'stryker-api/report'; import MutantTestMatcher from '../../src/MutantTestMatcher'; import currentLogMock from '../helpers/logMock'; -import { testResult, mutant, Mock, mock } from '../helpers/producers'; +import { testResult, mutant, Mock, mock, strykerOptions } from '../helpers/producers'; import TestableMutant, { TestSelectionResult } from '../../src/TestableMutant'; import SourceFile from '../../src/SourceFile'; import BroadcastReporter from '../../src/reporters/BroadcastReporter'; import { CoverageMapsByFile } from '../../src/transpiler/CoverageInstrumenterTranspiler'; import { PassThroughSourceMapper, MappedLocation } from '../../src/transpiler/SourceMapper'; +import * as sinon from 'sinon'; describe('MutantTestMatcher', () => { @@ -21,7 +22,7 @@ describe('MutantTestMatcher', () => { let mutants: Mutant[]; let runResult: RunResult; let fileCoverageDictionary: CoverageMapsByFile; - let strykerOptions: StrykerOptions; + let options: StrykerOptions; let reporter: Mock; let filesToMutate: ReadonlyArray; let sourceMapper: PassThroughSourceMapper; @@ -31,25 +32,25 @@ describe('MutantTestMatcher', () => { mutants = []; fileCoverageDictionary = Object.create(null); runResult = { tests: [], status: RunStatus.Complete }; - strykerOptions = {}; + options = strykerOptions(); reporter = mock(BroadcastReporter); filesToMutate = [new File('fileWithMutantOne', '\n\n\n\n12345'), new File('fileWithMutantTwo', '\n\n\n\n\n\n\n\n\n\n')]; sourceMapper = new PassThroughSourceMapper(); - sandbox.spy(sourceMapper, 'transpiledLocationFor'); + sinon.spy(sourceMapper, 'transpiledLocationFor'); sut = new MutantTestMatcher( mutants, filesToMutate, runResult, sourceMapper, fileCoverageDictionary, - strykerOptions, + options, reporter); }); describe('with coverageAnalysis: "perTest"', () => { beforeEach(() => { - strykerOptions.coverageAnalysis = 'perTest'; + options.coverageAnalysis = 'perTest'; }); describe('matchWithMutants()', () => { @@ -345,7 +346,7 @@ describe('MutantTestMatcher', () => { describe('with coverageAnalysis: "all"', () => { - beforeEach(() => strykerOptions.coverageAnalysis = 'all'); + beforeEach(() => options.coverageAnalysis = 'all'); it('should match all mutants to all tests and log a warning when there is no coverage data', () => { mutants.push(mutant({ fileName: 'fileWithMutantOne' }), mutant({ fileName: 'fileWithMutantTwo' })); @@ -416,7 +417,7 @@ describe('MutantTestMatcher', () => { describe('with coverageAnalysis: "off"', () => { - beforeEach(() => strykerOptions.coverageAnalysis = 'off'); + beforeEach(() => options.coverageAnalysis = 'off'); it('should match all mutants to all tests', () => { mutants.push(mutant({ fileName: 'fileWithMutantOne' }), mutant({ fileName: 'fileWithMutantTwo' })); diff --git a/packages/stryker/test/unit/MutatorFacadeSpec.ts b/packages/stryker/test/unit/MutatorFacadeSpec.ts index 4907749d3e..63b6b518f5 100644 --- a/packages/stryker/test/unit/MutatorFacadeSpec.ts +++ b/packages/stryker/test/unit/MutatorFacadeSpec.ts @@ -3,6 +3,7 @@ import { Mutator, MutatorFactory } from 'stryker-api/mutant'; import MutatorFacade from '../../src/MutatorFacade'; import { Config } from 'stryker-api/config'; import { Mock, file } from '../helpers/producers'; +import * as sinon from 'sinon'; describe('MutatorFacade', () => { @@ -10,10 +11,10 @@ describe('MutatorFacade', () => { beforeEach(() => { mutatorMock = { - mutate: sandbox.stub() + mutate: sinon.stub() }; mutatorMock.mutate.returns(['mutant']); - sandbox.stub(MutatorFactory.instance(), 'create').returns(mutatorMock); + sinon.stub(MutatorFactory.instance(), 'create').returns(mutatorMock); }); describe('mutate', () => { diff --git a/packages/stryker/test/unit/ReporterOrchestratorSpec.ts b/packages/stryker/test/unit/ReporterOrchestratorSpec.ts deleted file mode 100644 index 272ac3bfe7..0000000000 --- a/packages/stryker/test/unit/ReporterOrchestratorSpec.ts +++ /dev/null @@ -1,87 +0,0 @@ -import { expect } from 'chai'; -import * as sinon from 'sinon'; -import { ReporterFactory } from 'stryker-api/report'; -import ReporterOrchestrator from '../../src/ReporterOrchestrator'; -import * as broadcastReporter from '../../src/reporters/BroadcastReporter'; -import { Mock } from '../helpers/producers'; -import currentLogMock from '../helpers/logMock'; -import { Logger } from 'stryker-api/logging'; -import { Config } from 'stryker-api/config'; - -describe('ReporterOrchestrator', () => { - let sandbox: sinon.SinonSandbox; - let sut: ReporterOrchestrator; - let isTTY: boolean; - let broadcastReporterMock: sinon.SinonStub; - let log: Mock; - - beforeEach(() => { - sandbox = sinon.createSandbox(); - broadcastReporterMock = sandbox.stub(broadcastReporter, 'default'); - log = currentLogMock(); - captureTTY(); - }); - - afterEach(() => { - sandbox.restore(); - restoreTTY(); - }); - - it('should at least register the 5 default reporters', () => { - expect(ReporterFactory.instance().knownNames()).length.to.be.above(4); - }); - - describe('createBroadcastReporter()', () => { - // https://github.com/stryker-mutator/stryker/issues/212 - it('should create "progress-append-only" instead of "progress" reporter if process.stdout is not a tty', () => { - setTTY(false); - const config = new Config(); - config.set({ reporters: ['progress'] }); - sut = new ReporterOrchestrator(config); - sut.createBroadcastReporter(); - expect(broadcastReporterMock).to.have.been.calledWithNew; - expect(broadcastReporterMock).to.have.been.calledWith( - sinon.match( - (reporters: broadcastReporter.NamedReporter[]) => - reporters[0].name === 'progress-append-only' - ) - ); - }); - - it('should create the correct reporters', () => { - setTTY(true); - const config = new Config(); - config.set({ reporters: ['progress', 'progress-append-only'] }); - sut = new ReporterOrchestrator(config); - sut.createBroadcastReporter(); - expect(broadcastReporterMock).to.have.been.calledWith( - sinon.match( - (reporters: broadcastReporter.NamedReporter[]) => - reporters[0].name === 'progress' && - reporters[1].name === 'progress-append-only' - ) - ); - }); - - it('should warn if there is no reporter', () => { - setTTY(true); - const config = new Config(); - config.set({ reporters: [] }); - sut = new ReporterOrchestrator(config); - sut.createBroadcastReporter(); - expect(log.warn).to.have.been.calledTwice; - }); - }); - - function captureTTY() { - isTTY = (process.stdout as any).isTTY; - } - - function restoreTTY() { - (process.stdout as any).isTTY = isTTY; - } - - function setTTY(val: boolean) { - (process.stdout as any).isTTY = val; - } -}); diff --git a/packages/stryker/test/unit/SandboxPoolSpec.ts b/packages/stryker/test/unit/SandboxPoolSpec.ts index 6b442408a2..fd1759ba55 100644 --- a/packages/stryker/test/unit/SandboxPoolSpec.ts +++ b/packages/stryker/test/unit/SandboxPoolSpec.ts @@ -7,10 +7,10 @@ import { TestFramework } from 'stryker-api/test_framework'; import Sandbox from '../../src/Sandbox'; import SandboxPool from '../../src/SandboxPool'; import { Task } from '../../src/utils/Task'; -import '../helpers/globals'; import { Mock, config, file, mock, testFramework } from '../helpers/producers'; import LoggingClientContext from '../../src/logging/LoggingClientContext'; import { sleep } from '../helpers/testUtils'; +import * as sinon from 'sinon'; const OVERHEAD_TIME_MS = 42; const LOGGING_CONTEXT: LoggingClientContext = Object.freeze({ @@ -36,7 +36,7 @@ describe('SandboxPool', () => { secondSandbox.dispose.resolves(); const genericSandboxForAllSubsequentCallsToNewSandbox = mock(Sandbox as any); genericSandboxForAllSubsequentCallsToNewSandbox.dispose.resolves(); - createStub = global.sandbox.stub(Sandbox, 'create') + createStub = sinon.stub(Sandbox, 'create') .resolves(genericSandboxForAllSubsequentCallsToNewSandbox) .onCall(0).resolves(firstSandbox) .onCall(1).resolves(secondSandbox); @@ -54,7 +54,7 @@ describe('SandboxPool', () => { }); it('should use cpuCount when maxConcurrentTestRunners is set too high', async () => { - global.sandbox.stub(os, 'cpus').returns([1, 2, 3]); // stub 3 cpus + sinon.stub(os, 'cpus').returns([1, 2, 3]); // stub 3 cpus options.maxConcurrentTestRunners = 100; const actual = await sut.streamSandboxes().pipe(toArray()).toPromise(); expect(actual).lengthOf(3); @@ -63,7 +63,7 @@ describe('SandboxPool', () => { }); it('should use the cpuCount when maxConcurrentTestRunners is <= 0', async () => { - global.sandbox.stub(os, 'cpus').returns([1, 2, 3]); // stub 3 cpus + sinon.stub(os, 'cpus').returns([1, 2, 3]); // stub 3 cpus options.maxConcurrentTestRunners = 0; const actual = await sut.streamSandboxes().pipe(toArray()).toPromise(); expect(Sandbox.create).to.have.callCount(3); @@ -74,7 +74,7 @@ describe('SandboxPool', () => { it('should use the cpuCount - 1 when a transpiler is configured', async () => { options.transpilers = ['a transpiler']; options.maxConcurrentTestRunners = 2; - global.sandbox.stub(os, 'cpus').returns([1, 2]); // stub 2 cpus + sinon.stub(os, 'cpus').returns([1, 2]); // stub 2 cpus const actual = await sut.streamSandboxes().pipe(toArray()).toPromise(); expect(Sandbox.create).to.have.callCount(1); expect(actual).lengthOf(1); @@ -96,7 +96,7 @@ describe('SandboxPool', () => { it('should not resolve when there are still sandboxes being created (issue #713)', async () => { // Arrange - global.sandbox.stub(os, 'cpus').returns([1, 2, 3]); // stub 3 cpus + sinon.stub(os, 'cpus').returns([1, 2, 3]); // stub 3 cpus const task = new Task(); createStub.onCall(2).returns(task.promise); // promise is not yet resolved const registeredSandboxes: Sandbox[] = []; diff --git a/packages/stryker/test/unit/SandboxSpec.ts b/packages/stryker/test/unit/SandboxSpec.ts index 319b4a4925..4c08f73567 100644 --- a/packages/stryker/test/unit/SandboxSpec.ts +++ b/packages/stryker/test/unit/SandboxSpec.ts @@ -13,7 +13,6 @@ import ResilientTestRunnerFactory from '../../src/test-runner/ResilientTestRunne import TestableMutant, { TestSelectionResult } from '../../src/TestableMutant'; import { mutant as createMutant, testResult, Mock, createFileAlreadyExistsError } from '../helpers/producers'; import SourceFile from '../../src/SourceFile'; -import '../helpers/globals'; import TranspiledMutant from '../../src/TranspiledMutant'; import * as fileUtils from '../../src/utils/fileUtils'; import currentLogMock from '../helpers/logMock'; @@ -45,9 +44,9 @@ describe('Sandbox', () => { beforeEach(() => { options = { timeoutFactor: 23, timeoutMS: 1000, testRunner: 'sandboxUnitTestRunner', symlinkNodeModules: true } as any; - testRunner = { init: sandbox.stub(), run: sandbox.stub().resolves(), dispose: sandbox.stub() }; + testRunner = { init: sinon.stub(), run: sinon.stub().resolves(), dispose: sinon.stub() }; testFrameworkStub = { - filter: sandbox.stub() + filter: sinon.stub() }; expectedFileToMutate = new File(path.resolve('file1'), 'original code'); notMutatedFile = new File(path.resolve('file2'), 'to be mutated'); @@ -58,15 +57,15 @@ describe('Sandbox', () => { expectedFileToMutate, notMutatedFile, ]; - sandbox.stub(TempFolder.instance(), 'createRandomFolder').returns(sandboxDirectory); - writeFileStub = sandbox.stub(fileUtils, 'writeFile'); - symlinkJunctionStub = sandbox.stub(fileUtils, 'symlinkJunction'); - findNodeModulesStub = sandbox.stub(fileUtils, 'findNodeModules'); + sinon.stub(TempFolder.instance(), 'createRandomFolder').returns(sandboxDirectory); + writeFileStub = sinon.stub(fileUtils, 'writeFile'); + symlinkJunctionStub = sinon.stub(fileUtils, 'symlinkJunction'); + findNodeModulesStub = sinon.stub(fileUtils, 'findNodeModules'); symlinkJunctionStub.resolves(); findNodeModulesStub.resolves('node_modules'); writeFileStub.resolves(); - sandbox.stub(mkdirp, 'sync').returns(''); - sandbox.stub(ResilientTestRunnerFactory, 'create').returns(testRunner); + sinon.stub(mkdirp, 'sync').returns(''); + sinon.stub(ResilientTestRunnerFactory, 'create').returns(testRunner); log = currentLogMock(); }); diff --git a/packages/stryker/test/unit/ScoreResultCalculatorSpec.ts b/packages/stryker/test/unit/ScoreResultCalculatorSpec.ts index e8e0a73843..567763658e 100644 --- a/packages/stryker/test/unit/ScoreResultCalculatorSpec.ts +++ b/packages/stryker/test/unit/ScoreResultCalculatorSpec.ts @@ -178,7 +178,7 @@ describe('ScoreResult', () => { beforeEach(() => { sandbox = sinon.createSandbox(); - setExitCodeStub = sandbox.stub(objectUtils, 'setExitCode'); + setExitCodeStub = sinon.stub(objectUtils, 'setExitCode'); }); afterEach(() => { diff --git a/packages/stryker/test/unit/StrykerSpec.ts b/packages/stryker/test/unit/StrykerSpec.ts index da47ad3c4e..fcf73600d0 100644 --- a/packages/stryker/test/unit/StrykerSpec.ts +++ b/packages/stryker/test/unit/StrykerSpec.ts @@ -9,23 +9,23 @@ import { expect } from 'chai'; import InputFileResolver, * as inputFileResolver from '../../src/input/InputFileResolver'; import ConfigReader, * as configReader from '../../src/config/ConfigReader'; import TestFrameworkOrchestrator, * as testFrameworkOrchestrator from '../../src/TestFrameworkOrchestrator'; -import ReporterOrchestrator, * as reporterOrchestrator from '../../src/ReporterOrchestrator'; +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 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/PluginLoader'; +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 BroadcastReporter from '../../src/reporters/BroadcastReporter'; import TestableMutant from '../../src/TestableMutant'; -import '../helpers/globals'; import InputFileCollection from '../../src/input/InputFileCollection'; import LogConfigurator from '../../src/logging/LogConfigurator'; import LoggingClientContext from '../../src/logging/LoggingClientContext'; +import { OptionsContext } from 'stryker-api/plugin'; class FakeConfigEditor implements ConfigEditor { constructor() { } @@ -58,6 +58,7 @@ describe('Stryker', () => { let configureMainProcessStub: sinon.SinonStub; let configureLoggingServerStub: sinon.SinonStub; let shutdownLoggingStub: sinon.SinonStub; + let injectorMock: Mock>; beforeEach(() => { strykerConfig = config(); @@ -66,36 +67,47 @@ describe('Stryker', () => { configReaderMock = mock(ConfigReader); configReaderMock.readConfig.returns(strykerConfig); pluginLoaderMock = mock(PluginLoader); - const reporterOrchestratorMock = mock(ReporterOrchestrator); + + 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(); mutantRunResultMatcherMock = mock(MutantRunResultMatcher); mutatorMock = mock(MutatorFacade); - configureMainProcessStub = sandbox.stub(LogConfigurator, 'configureMainProcess'); - configureLoggingServerStub = sandbox.stub(LogConfigurator, 'configureLoggingServer'); - shutdownLoggingStub = sandbox.stub(LogConfigurator, 'shutdown'); + configureMainProcessStub = sinon.stub(LogConfigurator, 'configureMainProcess'); + configureLoggingServerStub = sinon.stub(LogConfigurator, 'configureLoggingServer'); + shutdownLoggingStub = sinon.stub(LogConfigurator, 'shutdown'); configureLoggingServerStub.resolves(LOGGING_CONTEXT); inputFileResolverMock = mock(InputFileResolver); - reporterOrchestratorMock.createBroadcastReporter.returns(reporter); + injectorMock.injectClass.returns(reporter); testFramework = testFrameworkMock(); initialTestExecutorMock = mock(InitialTestExecutor); mutationTestExecutorMock = mock(MutationTestExecutor); testFrameworkOrchestratorMock = mock(TestFrameworkOrchestrator); testFrameworkOrchestratorMock.determineTestFramework.returns(testFramework); - sandbox.stub(mutationTestExecutor, 'default').returns(mutationTestExecutorMock); - sandbox.stub(initialTestExecutor, 'default').returns(initialTestExecutorMock); - sandbox.stub(configValidator, 'default').returns(configValidatorMock); - sandbox.stub(testFrameworkOrchestrator, 'default').returns(testFrameworkOrchestratorMock); - sandbox.stub(reporterOrchestrator, 'default').returns(reporterOrchestratorMock); - sandbox.stub(mutatorFacade, 'default').returns(mutatorMock); - sandbox.stub(mutantRunResultMatcher, 'default').returns(mutantRunResultMatcherMock); - sandbox.stub(configReader, 'default').returns(configReaderMock); - sandbox.stub(pluginLoader, 'default').returns(pluginLoaderMock); - sandbox.stub(inputFileResolver, 'default').returns(inputFileResolverMock); + sinon.stub(mutationTestExecutor, 'default').returns(mutationTestExecutorMock); + sinon.stub(initialTestExecutor, 'default').returns(initialTestExecutorMock); + sinon.stub(configValidator, 'default').returns(configValidatorMock); + sinon.stub(testFrameworkOrchestrator, 'default').returns(testFrameworkOrchestratorMock); + 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); - sandbox.stub(TempFolder, 'instance').returns(tempFolderMock); + sinon.stub(TempFolder, 'instance').returns(tempFolderMock); tempFolderMock.clean.resolves(); scoreResultCalculator = new ScoreResultCalculator(); - sandbox.stub(scoreResultCalculator, 'determineExitCode').returns(sandbox.stub()); - sandbox.stub(scoreResultCalculatorModule, 'default').returns(scoreResultCalculator); + sinon.stub(scoreResultCalculator, 'determineExitCode').returns(sinon.stub()); + sinon.stub(scoreResultCalculatorModule, 'default').returns(scoreResultCalculator); }); describe('when constructed', () => { diff --git a/packages/stryker/test/unit/TestFrameworkOrchestratorSpec.ts b/packages/stryker/test/unit/TestFrameworkOrchestratorSpec.ts index 2e8a1be675..9d91ccd9ed 100644 --- a/packages/stryker/test/unit/TestFrameworkOrchestratorSpec.ts +++ b/packages/stryker/test/unit/TestFrameworkOrchestratorSpec.ts @@ -5,7 +5,7 @@ import * as sinon from 'sinon'; import { TestFrameworkFactory } from 'stryker-api/test_framework'; import { StrykerOptions } from 'stryker-api/core'; import currentLogMock from '../helpers/logMock'; -import { Mock } from '../helpers/producers'; +import { Mock, strykerOptions } from '../helpers/producers'; describe('TestFrameworkOrchestrator', () => { @@ -41,10 +41,10 @@ describe('TestFrameworkOrchestrator', () => { }; beforeEach(() => { - options = { coverageAnalysis: 'perTest' }; + options = strykerOptions({ coverageAnalysis: 'perTest' }); sandbox = sinon.createSandbox(); - sandbox.stub(TestFrameworkFactory.instance(), 'create').returns(testFramework); - sandbox.stub(TestFrameworkFactory.instance(), 'knownNames').returns(['awesomeFramework', 'unusedTestFramework']); + sinon.stub(TestFrameworkFactory.instance(), 'create').returns(testFramework); + sinon.stub(TestFrameworkFactory.instance(), 'knownNames').returns(['awesomeFramework', 'unusedTestFramework']); }); describe('when options contains a testFramework "awesomeFramework"', () => { diff --git a/packages/stryker/test/unit/child-proxy/ChildProcessProxySpec.ts b/packages/stryker/test/unit/child-proxy/ChildProcessProxySpec.ts index 7c1c5e7d6e..c211301347 100644 --- a/packages/stryker/test/unit/child-proxy/ChildProcessProxySpec.ts +++ b/packages/stryker/test/unit/child-proxy/ChildProcessProxySpec.ts @@ -12,6 +12,7 @@ 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'; const LOGGING_CONTEXT: LoggingClientContext = Object.freeze({ level: LogLevel.Fatal, @@ -19,7 +20,7 @@ const LOGGING_CONTEXT: LoggingClientContext = Object.freeze({ }); class ChildProcessMock extends EventEmitter { - public send = sandbox.stub(); + public send = sinon.stub(); public stderr = new EventEmitter(); public stdout = new EventEmitter(); public pid = 4648; @@ -35,10 +36,10 @@ describe('ChildProcessProxy', () => { let clock: sinon.SinonFakeTimers; beforeEach(() => { - clock = sandbox.useFakeTimers(); + clock = sinon.useFakeTimers(); childProcessMock = new ChildProcessMock(); - forkStub = sandbox.stub(childProcess, 'fork'); - killStub = sandbox.stub(objectUtils, 'kill'); + forkStub = sinon.stub(childProcess, 'fork'); + killStub = sinon.stub(objectUtils, 'kill'); forkStub.returns(childProcessMock); logMock = currentLogMock(); }); diff --git a/packages/stryker/test/unit/child-proxy/ChildProcessWorkerSpec.ts b/packages/stryker/test/unit/child-proxy/ChildProcessWorkerSpec.ts index fa039c6b12..e7a7e4e58a 100644 --- a/packages/stryker/test/unit/child-proxy/ChildProcessWorkerSpec.ts +++ b/packages/stryker/test/unit/child-proxy/ChildProcessWorkerSpec.ts @@ -3,7 +3,7 @@ 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/PluginLoader'; +import PluginLoader, * as pluginLoader from '../../../src/di/PluginLoader'; import { Mock, mock } from '../../helpers/producers'; import HelloClass from './HelloClass'; import LogConfigurator from '../../../src/logging/LogConfigurator'; @@ -11,6 +11,7 @@ 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'; const LOGGING_CONTEXT: LoggingClientContext = Object.freeze({ port: 4200, level: LogLevel.Fatal }); @@ -31,18 +32,18 @@ describe('ChildProcessProxyWorker', () => { beforeEach(() => { processes = []; logMock = currentLogMock(); - processOnStub = sandbox.stub(process, 'on'); - processListenersStub = sandbox.stub(process, 'listeners'); + processOnStub = sinon.stub(process, 'on'); + processListenersStub = sinon.stub(process, 'listeners'); processListenersStub.returns(processes); - processRemoveListenerStub = sandbox.stub(process, 'removeListener'); - processSendStub = sandbox.stub(); + processRemoveListenerStub = sinon.stub(process, 'removeListener'); + processSendStub = sinon.stub(); // process.send is normally undefined originalProcessSend = process.send; process.send = processSendStub; - processChdirStub = sandbox.stub(process, 'chdir'); - configureChildProcessStub = sandbox.stub(LogConfigurator, 'configureChildProcess'); + processChdirStub = sinon.stub(process, 'chdir'); + configureChildProcessStub = sinon.stub(LogConfigurator, 'configureChildProcess'); pluginLoaderMock = mock(PluginLoader); - sandbox.stub(pluginLoader, 'default').returns(pluginLoaderMock); + sinon.stub(pluginLoader, 'default').returns(pluginLoaderMock); }); afterEach(() => { diff --git a/packages/stryker/test/unit/di/PluginCreator.spec.ts b/packages/stryker/test/unit/di/PluginCreator.spec.ts new file mode 100644 index 0000000000..176ab4b56e --- /dev/null +++ b/packages/stryker/test/unit/di/PluginCreator.spec.ts @@ -0,0 +1,58 @@ +import { PluginKind, FactoryPlugin, ClassPlugin } from 'stryker-api/plugin'; +import { factory, testInjector } from '@stryker-mutator/test-helpers'; +import { expect } from 'chai'; +import { PluginCreator } from '../../../src/di/PluginCreator'; + +describe('PluginCreator', () => { + let sut: PluginCreator; + + beforeEach(() => { + sut = testInjector.injector + .injectFunction(PluginCreator.createFactory(PluginKind.Reporter)); + }); + + it('should create a FactoryPlugin using it\'s factory method', () => { + // Arrange + const expectedReporter = factory.reporter('fooReporter'); + const factoryPlugin: FactoryPlugin = { + kind: PluginKind.Reporter, + name: 'fooReporter', + factory() { + return expectedReporter; + } + }; + testInjector.pluginResolver.resolve.returns(factoryPlugin); + + // Act + const actualReporter = sut.create('fooReporter'); + + // Assert + expect(testInjector.pluginResolver.resolve).calledWith(PluginKind.Reporter, 'fooReporter'); + expect(actualReporter).eq(expectedReporter); + }); + + it('should create a ClassPlugin using it\'s constructor', () => { + // Arrange + class FooReporter { + } + const plugin: ClassPlugin = { + injectableClass: FooReporter, + kind: PluginKind.Reporter, + name: 'fooReporter' + }; + testInjector.pluginResolver.resolve.returns(plugin); + + // Act + const actualReporter = sut.create('fooReporter'); + + // Assert + expect(testInjector.pluginResolver.resolve).calledWith(PluginKind.Reporter, 'fooReporter'); + expect(actualReporter).instanceOf(FooReporter); + }); + + it('should throw if plugin is not recognized', () => { + testInjector.pluginResolver.resolve.returns({}); + expect(() => sut.create('foo')) + .throws('Plugin "Reporter:foo" could not be created, missing "factory" or "injectableClass" property.'); + }); +}); diff --git a/packages/stryker/test/unit/PluginLoaderSpec.ts b/packages/stryker/test/unit/di/PluginLoaderSpec.ts similarity index 85% rename from packages/stryker/test/unit/PluginLoaderSpec.ts rename to packages/stryker/test/unit/di/PluginLoaderSpec.ts index 7ef956eecd..241d9e409f 100644 --- a/packages/stryker/test/unit/PluginLoaderSpec.ts +++ b/packages/stryker/test/unit/di/PluginLoaderSpec.ts @@ -2,10 +2,10 @@ import * as path from 'path'; 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/PluginLoader'; -import currentLogMock from '../helpers/logMock'; -import { Mock } from '../helpers/producers'; +import * as fileUtils from '../../../src/utils/fileUtils'; +import PluginLoader from '../../../src/di/PluginLoader'; +import currentLogMock from '../../helpers/logMock'; +import { Mock } from '../../helpers/producers'; import { fsAsPromised } from '@stryker-mutator/util'; describe('PluginLoader', () => { @@ -19,8 +19,8 @@ describe('PluginLoader', () => { beforeEach(() => { log = currentLogMock(); sandbox = sinon.createSandbox(); - importModuleStub = sandbox.stub(fileUtils, 'importModule'); - pluginDirectoryReadMock = sandbox.stub(fsAsPromised, 'readdirSync'); + importModuleStub = sinon.stub(fileUtils, 'importModule'); + pluginDirectoryReadMock = sinon.stub(fsAsPromised, 'readdirSync'); }); describe('without wildcards', () => { @@ -70,7 +70,7 @@ describe('PluginLoader', () => { }); it('should read from a `node_modules` folder', () => { - expect(pluginDirectoryReadMock).to.have.been.calledWith(path.normalize(__dirname + '/../../..')); + expect(pluginDirectoryReadMock).to.have.been.calledWith(path.resolve(__dirname, '..', '..', '..', '..')); }); it('should load "stryker-jasmine" and "stryker-karma"', () => { diff --git a/packages/stryker/test/unit/initializer/PresetsSpec.ts b/packages/stryker/test/unit/initializer/PresetsSpec.ts index 4ee1e838c8..617a503bad 100644 --- a/packages/stryker/test/unit/initializer/PresetsSpec.ts +++ b/packages/stryker/test/unit/initializer/PresetsSpec.ts @@ -3,12 +3,13 @@ import { AngularPreset } from '../../../src/initializer/presets/AngularPreset'; import { ReactPreset } from '../../../src/initializer/presets/ReactPreset'; import * as inquirer from 'inquirer'; import { VueJsPreset } from '../../../src/initializer/presets/VueJsPreset'; +import * as sinon from 'sinon'; describe('Presets', () => { let inquirerPrompt: sinon.SinonStub; beforeEach(() => { - inquirerPrompt = sandbox.stub(inquirer, 'prompt'); + inquirerPrompt = sinon.stub(inquirer, 'prompt'); }); describe('AngularPreset', () => { let angularPreset: AngularPreset; diff --git a/packages/stryker/test/unit/initializer/StrykerInitializerSpec.ts b/packages/stryker/test/unit/initializer/StrykerInitializerSpec.ts index bcad7fd558..ed4a5cef8c 100644 --- a/packages/stryker/test/unit/initializer/StrykerInitializerSpec.ts +++ b/packages/stryker/test/unit/initializer/StrykerInitializerSpec.ts @@ -28,19 +28,19 @@ describe('StrykerInitializer', () => { beforeEach(() => { log = currentLogMock(); - out = sandbox.stub(); + out = sinon.stub(); presets = []; presetMock = { - createConfig: sandbox.stub(), + createConfig: sinon.stub(), name: 'awesome-preset' }; - inquirerPrompt = sandbox.stub(inquirer, 'prompt'); - childExecSync = sandbox.stub(child, 'execSync'); - fsWriteFile = sandbox.stub(fsAsPromised, 'writeFile'); - fsExistsSync = sandbox.stub(fsAsPromised, 'existsSync'); - restClientSearchGet = sandbox.stub(); - restClientPackageGet = sandbox.stub(); - sandbox.stub(restClient, 'RestClient') + inquirerPrompt = sinon.stub(inquirer, 'prompt'); + childExecSync = sinon.stub(child, 'execSync'); + fsWriteFile = sinon.stub(fsAsPromised, 'writeFile'); + fsExistsSync = sinon.stub(fsAsPromised, 'existsSync'); + restClientSearchGet = sinon.stub(); + restClientPackageGet = sinon.stub(); + sinon.stub(restClient, 'RestClient') .withArgs('npmSearch').returns({ get: restClientSearchGet }) @@ -51,7 +51,7 @@ describe('StrykerInitializer', () => { }); afterEach(() => { - sandbox.restore(); + sinon.restore(); }); describe('initialize()', () => { diff --git a/packages/stryker/test/unit/input/InputFileResolverSpec.ts b/packages/stryker/test/unit/input/InputFileResolverSpec.ts index 90f8843773..bb3c3fd30e 100644 --- a/packages/stryker/test/unit/input/InputFileResolverSpec.ts +++ b/packages/stryker/test/unit/input/InputFileResolverSpec.ts @@ -1,7 +1,7 @@ import os = require('os'); import * as path from 'path'; import { expect } from 'chai'; -import { childProcessAsPromised } from '@stryker-mutator/util'; +import { childProcessAsPromised, errorToString } from '@stryker-mutator/util'; import { Logger } from 'stryker-api/logging'; import { File } from 'stryker-api/core'; import { SourceFile } from 'stryker-api/report'; @@ -12,7 +12,7 @@ import * as fileUtils from '../../../src/utils/fileUtils'; import currentLogMock from '../../helpers/logMock'; import BroadcastReporter from '../../../src/reporters/BroadcastReporter'; import { Mock, mock, createFileNotFoundError, createIsDirError } from '../../helpers/producers'; -import { errorToString, normalizeWhiteSpaces } from '../../../src/utils/objectUtils'; +import { normalizeWhiteSpaces } from '../../../src/utils/objectUtils'; import { fsAsPromised } from '@stryker-mutator/util'; const files = (...namesWithContent: [string, string][]): File[] => @@ -32,8 +32,8 @@ describe('InputFileResolver', () => { beforeEach(() => { log = currentLogMock(); reporter = mock(BroadcastReporter); - globStub = sandbox.stub(fileUtils, 'glob'); - readFileStub = sandbox.stub(fsAsPromised, 'readFile') + globStub = sinon.stub(fileUtils, 'glob'); + readFileStub = sinon.stub(fsAsPromised, 'readFile') .withArgs(sinon.match.string).resolves(Buffer.from('')) // fallback .withArgs(sinon.match.string).resolves(Buffer.from('')) // fallback .withArgs(sinon.match('file1')).resolves(Buffer.from('file 1 content')) @@ -49,7 +49,7 @@ describe('InputFileResolver', () => { globStub.withArgs('file3').resolves(['/file3.js']); globStub.withArgs('file*').resolves(['/file1.js', '/file2.js', '/file3.js']); globStub.resolves([]); // default - childProcessExecStub = sandbox.stub(childProcessAsPromised, 'exec'); + childProcessExecStub = sinon.stub(childProcessAsPromised, 'exec'); }); it('should use git to identify files if files array is missing', async () => { diff --git a/packages/stryker/test/unit/logging/LogConfiguratorSpec.ts b/packages/stryker/test/unit/logging/LogConfiguratorSpec.ts index 7fab966cd8..b10228c458 100644 --- a/packages/stryker/test/unit/logging/LogConfiguratorSpec.ts +++ b/packages/stryker/test/unit/logging/LogConfiguratorSpec.ts @@ -4,6 +4,7 @@ import { LogLevel } from 'stryker-api/core'; import LogConfigurator from '../../../src/logging/LogConfigurator'; import * as netUtils from '../../../src/utils/netUtils'; import LoggingClientContext from '../../../src/logging/LoggingClientContext'; +import * as sinon from 'sinon'; describe('LogConfigurator', () => { @@ -13,9 +14,9 @@ describe('LogConfigurator', () => { let log4jsShutdown: sinon.SinonStub; beforeEach(() => { - getFreePortStub = sandbox.stub(netUtils, 'getFreePort'); - log4jsConfigure = sandbox.stub(log4js, 'configure'); - log4jsShutdown = sandbox.stub(log4js, 'shutdown'); + getFreePortStub = sinon.stub(netUtils, 'getFreePort'); + log4jsConfigure = sinon.stub(log4js, 'configure'); + log4jsShutdown = sinon.stub(log4js, 'shutdown'); }); describe('configureMainProcess', () => { diff --git a/packages/stryker/test/unit/mutators/ES5MutantGeneratorSpec.ts b/packages/stryker/test/unit/mutators/ES5MutantGeneratorSpec.ts index c77989b8dc..3df4d0e302 100644 --- a/packages/stryker/test/unit/mutators/ES5MutantGeneratorSpec.ts +++ b/packages/stryker/test/unit/mutators/ES5MutantGeneratorSpec.ts @@ -6,6 +6,7 @@ import ES5Mutator from '../../../src/mutators/ES5Mutator'; import NodeMutator from '../../../src/mutators/NodeMutator'; import { Identified, IdentifiedNode } from '../../../src/mutators/IdentifiedNode'; import { File } from 'stryker-api/core'; +import * as sinon from 'sinon'; describe('ES5Mutator', () => { let sut: ES5Mutator; @@ -15,7 +16,7 @@ describe('ES5Mutator', () => { }); afterEach(() => { - sandbox.restore(); + sinon.restore(); }); describe('with single input file with a one possible mutation', () => { diff --git a/packages/stryker/test/unit/process/InitialTestExecutorSpec.ts b/packages/stryker/test/unit/process/InitialTestExecutorSpec.ts index 753cf147c4..b427d8b2bc 100644 --- a/packages/stryker/test/unit/process/InitialTestExecutorSpec.ts +++ b/packages/stryker/test/unit/process/InitialTestExecutorSpec.ts @@ -18,6 +18,7 @@ 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'; const EXPECTED_INITIAL_TIMEOUT = 60 * 1000 * 5; const LOGGING_CONTEXT: LoggingClientContext = Object.freeze({ @@ -45,11 +46,11 @@ describe('InitialTestExecutor run', () => { strykerSandboxMock = producers.mock(StrykerSandbox as any); transpilerFacadeMock = producers.mock(TranspilerFacade); coverageInstrumenterTranspilerMock = producers.mock(CoverageInstrumenterTranspiler); - sandbox.stub(StrykerSandbox, 'create').resolves(strykerSandboxMock); - sandbox.stub(transpilerFacade, 'default').returns(transpilerFacadeMock); - sandbox.stub(coverageInstrumenterTranspiler, 'default').returns(coverageInstrumenterTranspilerMock); + sinon.stub(StrykerSandbox, 'create').resolves(strykerSandboxMock); + sinon.stub(transpilerFacade, 'default').returns(transpilerFacadeMock); + sinon.stub(coverageInstrumenterTranspiler, 'default').returns(coverageInstrumenterTranspilerMock); sourceMapperMock = producers.mock(PassThroughSourceMapper); - sandbox.stub(SourceMapper, 'create').returns(sourceMapperMock); + sinon.stub(SourceMapper, 'create').returns(sourceMapperMock); testFrameworkMock = producers.testFramework(); coverageAnnotatedFiles = [ new File('cov-annotated-transpiled-file-1.js', ''), @@ -195,7 +196,7 @@ describe('InitialTestExecutor run', () => { it('should also add a collectCoveragePerTest file when coverage analysis is "perTest" and there is a testFramework', async () => { options.coverageAnalysis = 'perTest'; - sandbox.stub(coverageHooks, 'coveragePerTestHooks').returns('test hook foobar'); + sinon.stub(coverageHooks, 'coveragePerTestHooks').returns('test hook foobar'); await sut.run(); expect(strykerSandboxMock.run).calledWith(EXPECTED_INITIAL_TIMEOUT, 'test hook foobar'); }); @@ -203,7 +204,7 @@ describe('InitialTestExecutor run', () => { 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); - sandbox.stub(coverageHooks, 'coveragePerTestHooks').returns('test hook foobar'); + sinon.stub(coverageHooks, 'coveragePerTestHooks').returns('test hook foobar'); 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.'); }); diff --git a/packages/stryker/test/unit/process/MutationTestExecutorSpec.ts b/packages/stryker/test/unit/process/MutationTestExecutorSpec.ts index c166178d69..2c0b503160 100644 --- a/packages/stryker/test/unit/process/MutationTestExecutorSpec.ts +++ b/packages/stryker/test/unit/process/MutationTestExecutorSpec.ts @@ -14,10 +14,10 @@ import TranspiledMutant from '../../../src/TranspiledMutant'; import MutantTestExecutor from '../../../src/process/MutationTestExecutor'; import BroadcastReporter from '../../../src/reporters/BroadcastReporter'; import MutantTranspiler, * as mutantTranspiler from '../../../src/transpiler/MutantTranspiler'; -import '../../helpers/globals'; import { Mock, config, file, mock, mutantResult, testFramework, testResult, testableMutant, transpiledMutant, runResult } from '../../helpers/producers'; import LoggingClientContext from '../../../src/logging/LoggingClientContext'; import currentLogMock from '../../helpers/logMock'; +import * as sinon from 'sinon'; const createTranspiledMutants = (...n: number[]) => { return n.map(n => { @@ -56,8 +56,8 @@ describe('MutationTestExecutor', () => { mutantTranspilerMock.initialize.resolves(initialTranspiledFiles); sandboxPoolMock.disposeAll.resolves(); testFrameworkMock = testFramework(); - sandbox.stub(sandboxPool, 'default').returns(sandboxPoolMock); - sandbox.stub(mutantTranspiler, 'default').returns(mutantTranspilerMock); + sinon.stub(sandboxPool, 'default').returns(sandboxPoolMock); + sinon.stub(mutantTranspiler, 'default').returns(mutantTranspilerMock); reporter = mock(BroadcastReporter); inputFiles = [new File('input.ts', '')]; expectedConfig = config(); diff --git a/packages/stryker/test/unit/reporters/BroadcastReporterSpec.ts b/packages/stryker/test/unit/reporters/BroadcastReporterSpec.ts index cf6ddcfbcf..7bba6b759f 100644 --- a/packages/stryker/test/unit/reporters/BroadcastReporterSpec.ts +++ b/packages/stryker/test/unit/reporters/BroadcastReporterSpec.ts @@ -1,97 +1,169 @@ -import { Logger } from 'stryker-api/logging'; import { expect } from 'chai'; -import currentLogMock from '../../helpers/logMock'; import BroadcastReporter from '../../../src/reporters/BroadcastReporter'; -import { ALL_REPORTER_EVENTS, Mock } from '../../helpers/producers'; +import { ALL_REPORTER_EVENTS } from '../../helpers/producers'; +import { PluginKind } from 'stryker-api/plugin'; +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'; describe('BroadcastReporter', () => { - let log: Mock; - let sut: any; - let reporter: any; - let reporter2: any; + let sut: BroadcastReporter; + let rep1: sinon.SinonStubbedInstance>; + let rep2: sinon.SinonStubbedInstance>; + let isTTY: boolean; + let pluginCreatorMock: sinon.SinonStubbedInstance>; beforeEach(() => { - log = currentLogMock(); - reporter = mockReporter(); - reporter2 = mockReporter(); - sut = new BroadcastReporter([{ name: 'rep1', reporter }, { name: 'rep2', reporter: reporter2 }]); + captureTTY(); + testInjector.options.reporters = ['rep1', 'rep2']; + rep1 = factory.reporter('rep1'); + rep2 = factory.reporter('rep2'); + pluginCreatorMock = sinon.createStubInstance(PluginCreator); + pluginCreatorMock.create + .withArgs('rep1').returns(rep1) + .withArgs('rep2').returns(rep2); }); - it('should forward all events', () => { - actArrangeAssertAllEvents(); + afterEach(() => { + restoreTTY(); }); - describe('when "wrapUp" returns promises', () => { - let wrapUpResolveFn: Function; - let wrapUpResolveFn2: Function; - let wrapUpRejectFn: Function; - let result: Promise; - let isResolved: boolean; + describe('when constructed', () => { + it('should create "progress-append-only" instead of "progress" reporter if process.stdout is not a tty', () => { + // Arrange + setTTY(false); + const expectedReporter = factory.reporter('progress-append-only'); + testInjector.options.reporters = ['progress']; + pluginCreatorMock.create.returns(expectedReporter); + // Act + sut = createSut(); + + // Assert + expect(sut.reporters).deep.eq({ 'progress-append-only': expectedReporter }); + expect(pluginCreatorMock.create).calledWith('progress-append-only'); + }); + + it('should create the correct reporters', () => { + // Arrange + setTTY(true); + testInjector.options.reporters = ['progress', 'rep2']; + const progress = factory.reporter('progress'); + pluginCreatorMock.create.withArgs('progress').returns(progress); + + // Act + sut = createSut(); + + // Assert + expect(sut.reporters).deep.eq({ + progress, + rep2 + }); + }); + + it('should warn if there is no reporter', () => { + testInjector.options.reporters = []; + sut = createSut(); + expect(testInjector.logger.warn).calledWith(sinon.match('No reporter configured')); + }); + }); + + describe('when created', () => { beforeEach(() => { - isResolved = false; - reporter.wrapUp.returns(new Promise((resolve, reject) => { - wrapUpResolveFn = resolve; - wrapUpRejectFn = reject; - })); - reporter2.wrapUp.returns(new Promise(resolve => wrapUpResolveFn2 = resolve)); - result = sut.wrapUp().then(() => isResolved = true); + sut = createSut(); }); - it('should forward a combined promise', () => { - expect(isResolved).to.be.eq(false); - wrapUpResolveFn(); - wrapUpResolveFn2(); - return result; + it('should forward all events', () => { + actArrangeAssertAllEvents(); }); - describe('and one of the promises results in a rejection', () => { + describe('when "wrapUp" returns promises', () => { + let wrapUpResolveFn: Function; + let wrapUpResolveFn2: Function; + let wrapUpRejectFn: Function; + let result: Promise; + let isResolved: boolean; + beforeEach(() => { - wrapUpRejectFn('some error'); + isResolved = false; + rep1.wrapUp.returns(new Promise((resolve, reject) => { + wrapUpResolveFn = resolve; + wrapUpRejectFn = reject; + })); + rep2.wrapUp.returns(new Promise(resolve => wrapUpResolveFn2 = resolve)); + result = sut.wrapUp().then(() => void (isResolved = true)); + }); + + it('should forward a combined promise', () => { + expect(isResolved).to.be.eq(false); + wrapUpResolveFn(); wrapUpResolveFn2(); return result; }); - it('should not result in a rejection', () => result); + describe('and one of the promises results in a rejection', () => { + beforeEach(() => { + wrapUpRejectFn('some error'); + wrapUpResolveFn2(); + return result; + }); + + it('should not result in a rejection', () => result); - it('should log the error', () => { - expect(log.error).to.have.been.calledWith(`An error occurred during 'wrapUp' on reporter 'rep1'. Error is: some error`); + it('should log the error', () => { + expect(testInjector.logger.error).calledWith(`An error occurred during 'wrapUp' on reporter 'rep1'. Error is: some error`); + }); }); }); - }); - describe('with one faulty reporter', () => { + describe('with one faulty reporter', () => { - beforeEach(() => { - ALL_REPORTER_EVENTS.forEach(eventName => reporter[eventName].throws('some error')); - }); + beforeEach(() => { + ALL_REPORTER_EVENTS.forEach(eventName => rep1[eventName].throws('some error')); + }); - it('should still broadcast to other reporters', () => { - actArrangeAssertAllEvents(); - }); + it('should still broadcast to other reporters', () => { + actArrangeAssertAllEvents(); + }); - it('should log each error', () => { - ALL_REPORTER_EVENTS.forEach(eventName => { - sut[eventName](); - expect(log.error).to.have.been.calledWith(`An error occurred during '${eventName}' on reporter 'rep1'. Error is: some error`); + it('should log each error', () => { + ALL_REPORTER_EVENTS.forEach(eventName => { + (sut as any)[eventName](); + expect(testInjector.logger.error).to.have.been.calledWith(`An error occurred during '${eventName}' on reporter 'rep1'. Error is: some error`); + }); }); + }); }); - function mockReporter() { - const reporter: any = {}; - ALL_REPORTER_EVENTS.forEach(event => reporter[event] = sandbox.stub()); - return reporter; + function createSut() { + return testInjector.injector + .provideValue(coreTokens.reporterPluginCreator, pluginCreatorMock as unknown as PluginCreator) + .injectClass(BroadcastReporter); } function actArrangeAssertAllEvents() { ALL_REPORTER_EVENTS.forEach(eventName => { const eventData = eventName === 'wrapUp' ? undefined : eventName; - sut[eventName](eventName); - expect(reporter[eventName]).to.have.been.calledWith(eventData); - expect(reporter2[eventName]).to.have.been.calledWith(eventData); + (sut as any)[eventName](eventName); + expect(rep1[eventName]).calledWith(eventData); + expect(rep2[eventName]).calledWith(eventData); }); } + + function captureTTY() { + isTTY = (process.stdout as any).isTTY; + } + + function restoreTTY() { + (process.stdout as any).isTTY = isTTY; + } + + function setTTY(val: boolean) { + (process.stdout as any).isTTY = val; + } }); diff --git a/packages/stryker/test/unit/reporters/ClearTextReporterSpec.ts b/packages/stryker/test/unit/reporters/ClearTextReporterSpec.ts index 3aaee66911..5b7d81f04a 100644 --- a/packages/stryker/test/unit/reporters/ClearTextReporterSpec.ts +++ b/packages/stryker/test/unit/reporters/ClearTextReporterSpec.ts @@ -22,7 +22,7 @@ describe('ClearTextReporter', () => { beforeEach(() => { sandbox = sinon.createSandbox(); - stdoutStub = sandbox.stub(process.stdout, 'write'); + stdoutStub = sinon.stub(process.stdout, 'write'); }); describe('onScoreCalculated', () => { diff --git a/packages/stryker/test/unit/reporters/DashboardReporterSpec.ts b/packages/stryker/test/unit/reporters/DashboardReporterSpec.ts index abeee88d31..69e0e46e1e 100644 --- a/packages/stryker/test/unit/reporters/DashboardReporterSpec.ts +++ b/packages/stryker/test/unit/reporters/DashboardReporterSpec.ts @@ -7,7 +7,6 @@ import StrykerDashboardClient, { StrykerDashboardReport } from '../../../src/rep import { scoreResult, mock, Mock } from '../../helpers/producers'; import { Logger } from 'stryker-api/logging'; import currentLogMock from '../../helpers/logMock'; -import { Config } from 'stryker-api/config'; describe('DashboardReporter', () => { let sut: DashboardReporter; @@ -19,8 +18,8 @@ describe('DashboardReporter', () => { beforeEach(() => { log = currentLogMock(); dashboardClientMock = mock(StrykerDashboardClient); - getEnvironmentVariables = sandbox.stub(environmentVariables, 'getEnvironmentVariable'); - determineCiProvider = sandbox.stub(ciProvider, 'determineCIProvider'); + getEnvironmentVariables = sinon.stub(environmentVariables, 'getEnvironmentVariable'); + determineCiProvider = sinon.stub(ciProvider, 'determineCIProvider'); }); function setupEnvironmentVariables(env?: { @@ -54,7 +53,7 @@ describe('DashboardReporter', () => { it('should report mutation score to report server', async () => { // Arrange setupEnvironmentVariables(); - sut = new DashboardReporter(new Config(), dashboardClientMock as any); + sut = new DashboardReporter(dashboardClientMock as any); // Act sut.onScoreCalculated(scoreResult({ mutationScore: 79.10 })); diff --git a/packages/stryker/test/unit/reporters/DotsReporterSpec.ts b/packages/stryker/test/unit/reporters/DotsReporterSpec.ts index b18a587fb1..46406fb370 100644 --- a/packages/stryker/test/unit/reporters/DotsReporterSpec.ts +++ b/packages/stryker/test/unit/reporters/DotsReporterSpec.ts @@ -14,7 +14,7 @@ describe('DotsReporter', () => { beforeEach(() => { sut = new DotsReporter(); sandbox = sinon.createSandbox(); - sandbox.stub(process.stdout, 'write'); + sinon.stub(process.stdout, 'write'); }); describe('onMutantTested()', () => { diff --git a/packages/stryker/test/unit/reporters/EventRecorderReporterSpec.ts b/packages/stryker/test/unit/reporters/EventRecorderReporterSpec.ts index bbbd898c44..281401cb0a 100644 --- a/packages/stryker/test/unit/reporters/EventRecorderReporterSpec.ts +++ b/packages/stryker/test/unit/reporters/EventRecorderReporterSpec.ts @@ -5,7 +5,7 @@ import EventRecorderReporter from '../../../src/reporters/EventRecorderReporter' import * as fileUtils from '../../../src/utils/fileUtils'; import currentLogMock from '../../helpers/logMock'; import StrictReporter from '../../../src/reporters/StrictReporter'; -import { ALL_REPORTER_EVENTS } from '../../helpers/producers'; +import { ALL_REPORTER_EVENTS, strykerOptions } from '../../helpers/producers'; import { fsAsPromised } from '@stryker-mutator/util'; describe('EventRecorderReporter', () => { @@ -17,8 +17,8 @@ describe('EventRecorderReporter', () => { beforeEach(() => { sandbox = sinon.createSandbox(); - cleanFolderStub = sandbox.stub(fileUtils, 'cleanFolder'); - writeFileStub = sandbox.stub(fsAsPromised, 'writeFile'); + cleanFolderStub = sinon.stub(fileUtils, 'cleanFolder'); + writeFileStub = sinon.stub(fsAsPromised, 'writeFile'); }); afterEach(() => { @@ -30,7 +30,7 @@ describe('EventRecorderReporter', () => { describe('and cleanFolder resolves correctly', () => { beforeEach(() => { cleanFolderStub.returns(Promise.resolve()); - sut = new EventRecorderReporter({}); + sut = new EventRecorderReporter(strykerOptions()); }); it('should log about the default baseFolder', () => { @@ -76,7 +76,7 @@ describe('EventRecorderReporter', () => { beforeEach(() => { expectedError = new Error('Some error 1'); cleanFolderStub.rejects(expectedError); - sut = new EventRecorderReporter({}); + sut = new EventRecorderReporter(strykerOptions()); }); it('should reject when `wrapUp()` is called', () => { diff --git a/packages/stryker/test/unit/reporters/ProgressAppendOnlyReporterSpec.ts b/packages/stryker/test/unit/reporters/ProgressAppendOnlyReporterSpec.ts index 83f1ae1330..bd90836955 100644 --- a/packages/stryker/test/unit/reporters/ProgressAppendOnlyReporterSpec.ts +++ b/packages/stryker/test/unit/reporters/ProgressAppendOnlyReporterSpec.ts @@ -12,17 +12,13 @@ const TEN_THOUSAND_SECONDS = SECOND * 10000; describe('ProgressAppendOnlyReporter', () => { let sut: ProgressAppendOnlyReporter; - let sandbox: sinon.SinonSandbox; beforeEach(() => { sut = new ProgressAppendOnlyReporter(); - sandbox = sinon.createSandbox(); - sandbox.useFakeTimers(); - sandbox.stub(process.stdout, 'write'); + sinon.useFakeTimers(); + sinon.stub(process.stdout, 'write'); }); - afterEach(() => sandbox.restore()); - describe('onAllMutantsMatchedWithTests() with 2 mutant to test', () => { beforeEach(() => { @@ -34,7 +30,7 @@ describe('ProgressAppendOnlyReporter', () => { }); it('should log zero progress after ten seconds without completed tests', () => { - sandbox.clock.tick(TEN_SECONDS); + sinon.clock.tick(TEN_SECONDS); expect(process.stdout.write).to.have.been.calledWith(`Mutation testing 0% (ETC n/a) ` + `0/2 tested (0 survived)${os.EOL}`); }); @@ -42,21 +38,21 @@ describe('ProgressAppendOnlyReporter', () => { it('should log 50% with 10s ETC after ten seconds with 1 completed test', () => { sut.onMutantTested(mutantResult({ status: MutantStatus.Killed })); expect(process.stdout.write).to.not.have.been.called; - sandbox.clock.tick(TEN_SECONDS); + sinon.clock.tick(TEN_SECONDS); expect(process.stdout.write).to.have.been.calledWith(`Mutation testing 50% (ETC 10s) 1/2 tested (0 survived)${os.EOL}`); }); it('should log 50% with "1m, 40s" ETC after hundred seconds with 1 completed test', () => { sut.onMutantTested(mutantResult({ status: MutantStatus.Killed })); expect(process.stdout.write).to.not.have.been.called; - sandbox.clock.tick(HUNDRED_SECONDS); + sinon.clock.tick(HUNDRED_SECONDS); expect(process.stdout.write).to.have.been.calledWith(`Mutation testing 50% (ETC 1m, 40s) 1/2 tested (0 survived)${os.EOL}`); }); it('should log 50% with "2h, 46m, 40s" ETC after ten tousand seconds with 1 completed test', () => { sut.onMutantTested(mutantResult({ status: MutantStatus.Killed })); expect(process.stdout.write).to.not.have.been.called; - sandbox.clock.tick(TEN_THOUSAND_SECONDS); + sinon.clock.tick(TEN_THOUSAND_SECONDS); expect(process.stdout.write).to.have.been.calledWith(`Mutation testing 50% (ETC 2h, 46m, 40s) 1/2 tested (0 survived)${os.EOL}`); }); }); diff --git a/packages/stryker/test/unit/reporters/ProgressReporterSpec.ts b/packages/stryker/test/unit/reporters/ProgressReporterSpec.ts index 64bdbdf5b9..6d2ff047fb 100644 --- a/packages/stryker/test/unit/reporters/ProgressReporterSpec.ts +++ b/packages/stryker/test/unit/reporters/ProgressReporterSpec.ts @@ -15,23 +15,17 @@ const ONE_HOUR = SECOND * 3600; describe('ProgressReporter', () => { let sut: ProgressReporter; - let sandbox: sinon.SinonSandbox; let matchedMutants: MatchedMutant[]; let progressBar: Mock; - const progressBarContent: string = `Mutation testing [:bar] :percent (ETC :etc) :tested/:total tested (:survived survived)`; + const progressBarContent = `Mutation testing [:bar] :percent (ETC :etc) :tested/:total tested (:survived survived)`; beforeEach(() => { - sandbox = sinon.createSandbox(); - sandbox.useFakeTimers(); + sinon.useFakeTimers(); sut = new ProgressReporter(); progressBar = mock(ProgressBar); - sandbox.stub(progressBarModule, 'default').returns(progressBar); - }); - - afterEach(() => { - sandbox.restore(); + sinon.stub(progressBarModule, 'default').returns(progressBar); }); describe('onAllMutantsMatchedWithTests()', () => { @@ -93,7 +87,7 @@ describe('ProgressReporter', () => { }); it('should show to an estimate of "10s" in the progressBar after ten seconds and 1 mutants tested', () => { - sandbox.clock.tick(TEN_SECONDS); + sinon.clock.tick(TEN_SECONDS); sut.onMutantTested(mutantResult({ status: MutantStatus.Killed })); @@ -101,7 +95,7 @@ describe('ProgressReporter', () => { }); it('should show to an estimate of "1m, 40s" in the progressBar after hundred seconds and 1 mutants tested', () => { - sandbox.clock.tick(HUNDRED_SECONDS); + sinon.clock.tick(HUNDRED_SECONDS); sut.onMutantTested(mutantResult({ status: MutantStatus.Killed })); @@ -109,7 +103,7 @@ describe('ProgressReporter', () => { }); it('should show to an estimate of "2h, 46m, 40s" in the progressBar after ten thousand seconds and 1 mutants tested', () => { - sandbox.clock.tick(TEN_THOUSAND_SECONDS); + sinon.clock.tick(TEN_THOUSAND_SECONDS); sut.onMutantTested(mutantResult({ status: MutantStatus.Killed })); @@ -117,7 +111,7 @@ describe('ProgressReporter', () => { }); it('should show to an estimate of "1h, 0m, 0s" in the progressBar after an hour and 1 mutants tested', () => { - sandbox.clock.tick(ONE_HOUR); + sinon.clock.tick(ONE_HOUR); sut.onMutantTested(mutantResult({ status: MutantStatus.Killed })); diff --git a/packages/stryker/test/unit/reporters/ci/CircleProviderSpec.ts b/packages/stryker/test/unit/reporters/ci/CircleProviderSpec.ts index 8a21df18ed..7936f32322 100644 --- a/packages/stryker/test/unit/reporters/ci/CircleProviderSpec.ts +++ b/packages/stryker/test/unit/reporters/ci/CircleProviderSpec.ts @@ -8,7 +8,7 @@ describe('CircleCI Provider', () => { let getEnvironmentVariables: sinon.SinonStub; beforeEach(() => { - getEnvironmentVariables = sandbox.stub(environmentVariables, 'getEnvironmentVariable'); + getEnvironmentVariables = sinon.stub(environmentVariables, 'getEnvironmentVariable'); }); describe('isPullRequest()', () => { diff --git a/packages/stryker/test/unit/reporters/ci/ProviderSpec.ts b/packages/stryker/test/unit/reporters/ci/ProviderSpec.ts index c611c834b7..723aee28b3 100644 --- a/packages/stryker/test/unit/reporters/ci/ProviderSpec.ts +++ b/packages/stryker/test/unit/reporters/ci/ProviderSpec.ts @@ -8,7 +8,7 @@ describe('determineCiProvider()', () => { let getEnvironmentVariables: sinon.SinonStub; beforeEach(() => { - getEnvironmentVariables = sandbox.stub(environmentVariables, 'getEnvironmentVariable'); + getEnvironmentVariables = sinon.stub(environmentVariables, 'getEnvironmentVariable'); }); describe('Without CI environment', () => { diff --git a/packages/stryker/test/unit/reporters/ci/TravisProviderSpec.ts b/packages/stryker/test/unit/reporters/ci/TravisProviderSpec.ts index cade374c0e..427ad0d21e 100644 --- a/packages/stryker/test/unit/reporters/ci/TravisProviderSpec.ts +++ b/packages/stryker/test/unit/reporters/ci/TravisProviderSpec.ts @@ -8,7 +8,7 @@ describe('Travis Provider', () => { let getEnvironmentVariables: sinon.SinonStub; beforeEach(() => { - getEnvironmentVariables = sandbox.stub(environmentVariables, 'getEnvironmentVariable'); + getEnvironmentVariables = sinon.stub(environmentVariables, 'getEnvironmentVariable'); }); describe('isPullRequest()', () => { diff --git a/packages/stryker/test/unit/test-runner/ChildProcessTestRunnerDecoratorSpec.ts b/packages/stryker/test/unit/test-runner/ChildProcessTestRunnerDecoratorSpec.ts index 56f1353ed7..43b35273c2 100644 --- a/packages/stryker/test/unit/test-runner/ChildProcessTestRunnerDecoratorSpec.ts +++ b/packages/stryker/test/unit/test-runner/ChildProcessTestRunnerDecoratorSpec.ts @@ -3,7 +3,7 @@ import { expect } from 'chai'; import { RunnerOptions, RunOptions } from 'stryker-api/test_runner'; import { LogLevel } from 'stryker-api/core'; import ChildProcessTestRunnerDecorator from '../../../src/test-runner/ChildProcessTestRunnerDecorator'; -import { Mock, mock } from '../../helpers/producers'; +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'; @@ -23,18 +23,18 @@ describe(ChildProcessTestRunnerDecorator.name, () => { let clock: sinon.SinonFakeTimers; beforeEach(() => { - clock = sandbox.useFakeTimers(); + clock = sinon.useFakeTimers(); childProcessProxyMock = { - dispose: sandbox.stub(), + dispose: sinon.stub(), proxy: mock(TestRunnerDecorator) }; - childProcessProxyCreateStub = sandbox.stub(ChildProcessProxy, 'create'); + childProcessProxyCreateStub = sinon.stub(ChildProcessProxy, 'create'); childProcessProxyCreateStub.returns(childProcessProxyMock); runnerOptions = { fileNames: [], - strykerOptions: { + strykerOptions: strykerOptions({ plugins: ['foo-plugin', 'bar-plugin'] - } + }) }; loggingContext = { port: 4200, level: LogLevel.Fatal }; sut = new ChildProcessTestRunnerDecorator('realRunner', runnerOptions, 'a working directory', loggingContext); diff --git a/packages/stryker/test/unit/test-runner/CommandTestRunnerSpec.ts b/packages/stryker/test/unit/test-runner/CommandTestRunnerSpec.ts index e2d2b2d577..0bf280bde2 100644 --- a/packages/stryker/test/unit/test-runner/CommandTestRunnerSpec.ts +++ b/packages/stryker/test/unit/test-runner/CommandTestRunnerSpec.ts @@ -8,6 +8,8 @@ import { Config } from 'stryker-api/config'; import { RunStatus, TestStatus, RunResult } from 'stryker-api/test_runner'; import Timer, * as timerModule from '../../../src/utils/Timer'; import { Mock, mock } from '../../helpers/producers'; +import * as sinon from 'sinon'; +import { errorToString } from '@stryker-mutator/util'; describe(CommandTestRunner.name, () => { @@ -17,10 +19,10 @@ describe(CommandTestRunner.name, () => { beforeEach(() => { childProcessMock = new ChildProcessMock(42); - sandbox.stub(childProcess, 'exec').returns(childProcessMock); - killStub = sandbox.stub(objectUtils, 'kill'); + sinon.stub(childProcess, 'exec').returns(childProcessMock); + killStub = sinon.stub(objectUtils, 'kill'); timerMock = mock(Timer); - sandbox.stub(timerModule, 'default').returns(timerMock); + sinon.stub(timerModule, 'default').returns(timerMock); }); describe('run', () => { @@ -73,7 +75,7 @@ describe(CommandTestRunner.name, () => { childProcessMock.emit('error', expectedError); const result = await resultPromise; const expectedResult: RunResult = { - errorMessages: [objectUtils.errorToString(expectedError)], + errorMessages: [errorToString(expectedError)], status: RunStatus.Error, tests: [] }; diff --git a/packages/stryker/test/unit/test-runner/RetryDecoratorSpec.ts b/packages/stryker/test/unit/test-runner/RetryDecoratorSpec.ts index f84917a4b7..5aafb73f6b 100644 --- a/packages/stryker/test/unit/test-runner/RetryDecoratorSpec.ts +++ b/packages/stryker/test/unit/test-runner/RetryDecoratorSpec.ts @@ -2,13 +2,13 @@ import { expect } from 'chai'; import { RunStatus } from 'stryker-api/test_runner'; import RetryDecorator from '../../../src/test-runner/RetryDecorator'; import TestRunnerMock from '../../helpers/TestRunnerMock'; -import { errorToString } from '../../../src/utils/objectUtils'; import TestRunnerDecorator from '../../../src/test-runner/TestRunnerDecorator'; import ChildProcessCrashedError from '../../../src/child-proxy/ChildProcessCrashedError'; import OutOfMemoryError from '../../../src/child-proxy/OutOfMemoryError'; import { Logger } from 'stryker-api/logging'; import { Mock } from '../../helpers/producers'; import currentLogMock from '../../helpers/logMock'; +import { errorToString } from '@stryker-mutator/util'; describe('RetryDecorator', () => { let sut: RetryDecorator; diff --git a/packages/stryker/test/unit/test-runner/TimeoutDecoratorSpec.ts b/packages/stryker/test/unit/test-runner/TimeoutDecoratorSpec.ts index 0100a3b844..17a01f5bcd 100644 --- a/packages/stryker/test/unit/test-runner/TimeoutDecoratorSpec.ts +++ b/packages/stryker/test/unit/test-runner/TimeoutDecoratorSpec.ts @@ -15,7 +15,7 @@ describe('TimeoutDecorator', () => { beforeEach(() => { sandbox = sinon.createSandbox(); - clock = sandbox.useFakeTimers(); + clock = sinon.useFakeTimers(); testRunner1 = new TestRunnerMock(); testRunner2 = new TestRunnerMock(); availableTestRunners = [testRunner1, testRunner2]; diff --git a/packages/stryker/test/unit/transpiler/MutantTranspilerSpec.ts b/packages/stryker/test/unit/transpiler/MutantTranspilerSpec.ts index 3719a5dacc..fe80f533b7 100644 --- a/packages/stryker/test/unit/transpiler/MutantTranspilerSpec.ts +++ b/packages/stryker/test/unit/transpiler/MutantTranspilerSpec.ts @@ -6,11 +6,11 @@ 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 { errorToString } from '../../../src/utils/objectUtils'; -import '../../helpers/globals'; 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'; const LOGGING_CONTEXT: LoggingClientContext = Object.freeze({ level: LogLevel.Fatal, @@ -26,9 +26,9 @@ describe('MutantTranspiler', () => { beforeEach(() => { transpilerFacadeMock = mock(TranspilerFacade); - childProcessProxyMock = { proxy: transpilerFacadeMock, dispose: sandbox.stub() }; - sandbox.stub(ChildProcessProxy, 'create').returns(childProcessProxyMock); - sandbox.stub(transpilerFacade, 'default').returns(transpilerFacadeMock); + 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 diff --git a/packages/stryker/test/unit/transpiler/SourceMapperSpec.ts b/packages/stryker/test/unit/transpiler/SourceMapperSpec.ts index 8f891f4126..a3e6120535 100644 --- a/packages/stryker/test/unit/transpiler/SourceMapperSpec.ts +++ b/packages/stryker/test/unit/transpiler/SourceMapperSpec.ts @@ -4,6 +4,7 @@ import { Config } from 'stryker-api/config'; import { File } from 'stryker-api/core'; import SourceMapper, { PassThroughSourceMapper, TranspiledSourceMapper, MappedLocation, SourceMapError } from '../../../src/transpiler/SourceMapper'; import { Mock, mock, config as configFactory, location as locationFactory, mappedLocation, PNG_BASE64_ENCODED } from '../../helpers/producers'; +import * as sinon from 'sinon'; const GREATEST_LOWER_BOUND = sourceMapModule.SourceMapConsumer.GREATEST_LOWER_BOUND; const LEAST_UPPER_BOUND = sourceMapModule.SourceMapConsumer.LEAST_UPPER_BOUND; @@ -25,12 +26,12 @@ describe('SourceMapper', () => { // For some reason, `generatedPositionFor` is not defined on the `SourceMapConsumer` prototype // Define it here by hand - sourceMapConsumerMock.generatedPositionFor = sandbox.stub(); + sourceMapConsumerMock.generatedPositionFor = sinon.stub(); sourceMapConsumerMock.generatedPositionFor.returns({ column: 2, line: 1 }); - sandbox.stub(sourceMapModule, 'SourceMapConsumer').returns(sourceMapConsumerMock); + sinon.stub(sourceMapModule, 'SourceMapConsumer').returns(sourceMapConsumerMock); // Restore the static values, removed by the stub sourceMapModule.SourceMapConsumer.LEAST_UPPER_BOUND = LEAST_UPPER_BOUND; diff --git a/packages/stryker/test/unit/transpiler/TranspilerFacadeSpec.ts b/packages/stryker/test/unit/transpiler/TranspilerFacadeSpec.ts index 69124945ca..e6692da596 100644 --- a/packages/stryker/test/unit/transpiler/TranspilerFacadeSpec.ts +++ b/packages/stryker/test/unit/transpiler/TranspilerFacadeSpec.ts @@ -4,13 +4,14 @@ import TranspilerFacade from '../../../src/transpiler/TranspilerFacade'; import { Transpiler, TranspilerFactory } from 'stryker-api/transpile'; import { mock, Mock } from '../../helpers/producers'; import { File } from 'stryker-api/core'; +import * as sinon from 'sinon'; describe('TranspilerFacade', () => { let createStub: sinon.SinonStub; let sut: TranspilerFacade; beforeEach(() => { - createStub = sandbox.stub(TranspilerFactory.instance(), 'create'); + createStub = sinon.stub(TranspilerFactory.instance(), 'create'); }); describe('when there are no transpilers', () => { diff --git a/packages/stryker/test/unit/utils/TempFolderSpec.ts b/packages/stryker/test/unit/utils/TempFolderSpec.ts index 8c19a44a67..6a172574aa 100644 --- a/packages/stryker/test/unit/utils/TempFolderSpec.ts +++ b/packages/stryker/test/unit/utils/TempFolderSpec.ts @@ -15,12 +15,12 @@ describe('TempFolder', () => { beforeEach(() => { sandbox = sinon.createSandbox(); - sandbox.stub(mkdirp, 'sync'); - sandbox.stub(fsAsPromised, 'writeFile'); - deleteDirStub = sandbox.stub(fileUtils, 'deleteDir'); - cwdStub = sandbox.stub(process, 'cwd'); + sinon.stub(mkdirp, 'sync'); + sinon.stub(fsAsPromised, 'writeFile'); + deleteDirStub = sinon.stub(fileUtils, 'deleteDir'); + cwdStub = sinon.stub(process, 'cwd'); cwdStub.returns(mockCwd); - randomStub = sandbox.stub(TempFolder.instance(), 'random'); + randomStub = sinon.stub(TempFolder.instance(), 'random'); randomStub.returns('rand'); TempFolder.instance().baseTempFolder = ''; diff --git a/packages/stryker/test/unit/utils/fileUtilsSpec.ts b/packages/stryker/test/unit/utils/fileUtilsSpec.ts index e0448a2f01..95691180a8 100644 --- a/packages/stryker/test/unit/utils/fileUtilsSpec.ts +++ b/packages/stryker/test/unit/utils/fileUtilsSpec.ts @@ -2,15 +2,16 @@ import * as path from 'path'; import { expect } from 'chai'; import { fsAsPromised } from '@stryker-mutator/util'; import * as fileUtils from '../../../src/utils/fileUtils'; +import * as sinon from 'sinon'; describe('fileUtils', () => { let existsStub: sinon.SinonStub; beforeEach(() => { - sandbox.stub(fsAsPromised, 'writeFile'); - sandbox.stub(fsAsPromised, 'symlink'); - existsStub = sandbox.stub(fsAsPromised, 'exists'); + sinon.stub(fsAsPromised, 'writeFile'); + sinon.stub(fsAsPromised, 'symlink'); + existsStub = sinon.stub(fsAsPromised, 'exists'); }); describe('writeFile', () => { diff --git a/packages/stryker/test/unit/utils/objectUtilsSpec.ts b/packages/stryker/test/unit/utils/objectUtilsSpec.ts index 1a3ab53b02..32e14d9348 100644 --- a/packages/stryker/test/unit/utils/objectUtilsSpec.ts +++ b/packages/stryker/test/unit/utils/objectUtilsSpec.ts @@ -2,6 +2,7 @@ import * as sut from '../../../src/utils/objectUtils'; import { expect } from 'chai'; import { match } from 'sinon'; import { Task } from '../../../src/utils/Task'; +import * as sinon from 'sinon'; describe('objectUtils', () => { describe('timeout', () => { @@ -15,8 +16,8 @@ describe('objectUtils', () => { it('should remove any nodejs timers when promise resolves', async () => { // Arrange const expectedTimer = 234; - const setTimeoutStub = sandbox.stub(global, 'setTimeout'); - const clearTimeoutStub = sandbox.stub(global, 'clearTimeout'); + const setTimeoutStub = sinon.stub(global, 'setTimeout'); + const clearTimeoutStub = sinon.stub(global, 'clearTimeout'); setTimeoutStub.returns(expectedTimer); const expectedResult = 'expectedResult'; const p = Promise.resolve(expectedResult); diff --git a/packages/stryker/tsconfig.src.json b/packages/stryker/tsconfig.src.json index 121f278a98..d3a6bf9266 100644 --- a/packages/stryker/tsconfig.src.json +++ b/packages/stryker/tsconfig.src.json @@ -13,6 +13,9 @@ }, { "path": "../stryker-util/tsconfig.src.json" + }, + { + "path": "../typed-inject/tsconfig.src.json" } ] } \ No newline at end of file diff --git a/packages/stryker/tsconfig.test.json b/packages/stryker/tsconfig.test.json index 46f9a425ff..40f675e3a3 100644 --- a/packages/stryker/tsconfig.test.json +++ b/packages/stryker/tsconfig.test.json @@ -12,6 +12,9 @@ "references": [ { "path": "./tsconfig.src.json" + }, + { + "path": "../stryker-test-helpers/tsconfig.src.json" } ] } \ No newline at end of file diff --git a/packages/typed-inject/.npmignore b/packages/typed-inject/.npmignore new file mode 100644 index 0000000000..86408d078c --- /dev/null +++ b/packages/typed-inject/.npmignore @@ -0,0 +1,11 @@ +**/* +!*.d.ts +!bin/** +!src/** +!*.js +src/**/*.map +src/**/*.ts +!src/**/*.d.ts +!readme.md +!LICENSE +!CHANGELOG.md \ No newline at end of file diff --git a/packages/typed-inject/.npmrc b/packages/typed-inject/.npmrc new file mode 100644 index 0000000000..9cf9495031 --- /dev/null +++ b/packages/typed-inject/.npmrc @@ -0,0 +1 @@ +package-lock=false \ No newline at end of file diff --git a/packages/typed-inject/.vscode/launch.json b/packages/typed-inject/.vscode/launch.json new file mode 100644 index 0000000000..a949b9d86f --- /dev/null +++ b/packages/typed-inject/.vscode/launch.json @@ -0,0 +1,48 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "type": "node", + "request": "launch", + "name": "Unit tests", + "program": "${workspaceFolder}/../../node_modules/mocha/bin/_mocha", + "args": [ + "-u", + "tdd", + "--timeout", + "999999", + "--colors", + "${workspaceFolder}/test/helpers/**/*.js", + "${workspaceFolder}/test/unit/**/*.js" + ], + "internalConsoleOptions": "openOnSessionStart", + "outFiles": [ + "${workspaceRoot}/test/**/*.js", + "${workspaceRoot}/src/**/*.js" + ] + }, + { + "type": "node", + "request": "launch", + "name": "Integration tests", + "program": "${workspaceFolder}/../../node_modules/mocha/bin/_mocha", + "args": [ + "-u", + "tdd", + "--timeout", + "999999", + "--colors", + "${workspaceFolder}/test/helpers/**/*.js", + "${workspaceFolder}/test/integration/**/*.js" + ], + "internalConsoleOptions": "openOnSessionStart", + "outFiles": [ + "${workspaceRoot}/test/**/*.js", + "${workspaceRoot}/src/**/*.js" + ] + } + ] +} \ No newline at end of file diff --git a/packages/typed-inject/README.md b/packages/typed-inject/README.md new file mode 100644 index 0000000000..edab9c2d56 --- /dev/null +++ b/packages/typed-inject/README.md @@ -0,0 +1,264 @@ +[![Build Status](https://travis-ci.org/stryker-mutator/stryker.svg?branch=master)](https://travis-ci.org/stryker-mutator/stryker) +[![NPM](https://img.shields.io/npm/dm/typed-inject.svg)](https://www.npmjs.com/package/typed-inject) +[![Node version](https://img.shields.io/node/v/typed-inject.svg)](https://img.shields.io/node/v/stryker-utils.svg) +[![Gitter](https://badges.gitter.im/stryker-mutator/stryker.svg)](https://gitter.im/stryker-mutator/stryker?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge) + +# Typed Inject + +> Type safe dependency injection for TypeScript + +A tiny, 100% type safe dependency injection framework for TypeScript. You can inject classes, interfaces or primitives. If your project compiles, you know for sure your dependencies are resolved at runtime and have their declared types. + +_If you are new to 'Dependency Injection'/'Inversion of control', please read up on it [in this blog article about it](https://medium.com/@samueleresca/inversion-of-control-and-dependency-injection-in-typescript-3040d568aabe)_ + +## πŸ—ΊοΈ Installation + +Install typed-inject locally within your project folder, like so: + +```shell +npm i typed-inject +``` + +Or with yarn: + +```shell +yarn add typed-inject +``` + +_Note: this package uses advanced TypeScript features. Only TS 3.0 and above is supported!_ + +## 🎁 Usage + +An example: + +```ts +import { rootInjector, tokens } from 'typed-inject'; + +interface Logger { + info(message: string): void; +} + +const logger: Logger = { + info(message: string) { + console.log(message); + } +}; + +class HttpClient { + constructor(private log: Logger) { } + public static inject = tokens('logger'); +} + +class MyService { + constructor(private http: HttpClient, private log: Logger) { } + public static inject = tokens('httpClient', 'logger'); +} + +const appInjector = rootInjector + .provideValue('logger', logger) + .provideClass('httpClient', HttpClient); + +const myService = appInjector.injectClass(MyService); +// Dependencies for MyService validated and injected +``` + +In this example: + +* The `logger` is injected into a new instance of `HttpClient` by value. +* The instance of `HttpClient` and the `logger` are injected into a new instance of `MyService`. + +Dependencies are resolved using the static `inject` property on their classes. They must match the names given to the dependencies when configuring the injector with `provideXXX` methods. + +Expect compiler errors when you mess up the order of tokens or forget it completely. + +```ts +import { rootInjector, tokens } from 'typed-inject'; + +// Same logger as before + +class HttpClient { + constructor(private log: Logger) { } + // ERROR! Property 'inject' is missing in type 'typeof HttpClient' but required +} + +class MyService { + constructor(private http: HttpClient, private log: Logger) { } + public static inject = tokens('logger', 'httpClient'); + // ERROR! Types of parameters 'http' and 'args_0' are incompatible +} + +const appInjector = rootInjector + .provideValue('logger', logger) + .provideClass('httpClient', HttpClient); + +const myService = appInjector.injectClass(MyService); +``` + +The error messages are a bit cryptic at times, but it sure is better than running into them at runtime. + +## ✨ Magic tokens + +Any `Injector` instance can always inject the following tokens: + +| Token name | Token value | Description | +| - | - | - | +| `INJECTOR_TOKEN` | `'$injector'` | Injects the current injector | +| `TARGET_TOKEN` | `'$target'` | The class or function in which the current values is injected, or `undefined` if resolved directly | + +An example: + +```ts +import { rootInjector, Injector, tokens, TARGET_TOKEN, INJECTOR_TOKEN } from 'typed-inject'; + +class Foo { + constructor(injector: Injector<{}>, target: Function | undefined) {} + static inject = tokens(INJECTOR_TOKEN, TARGET_TOKEN); +} + +const foo = rootInjector.inject(Foo); +``` + +## πŸ’­ Motivation + +JavaScript and TypeScript development already has a great dependency injection solution with [InversifyJS](https://github.com/inversify/InversifyJS). However, InversifyJS comes with 2 caveats. + +### InversifyJS uses Reflect-metadata + +InversifyJS works with a nice API using decorators. Decorators is in Stage 2 of ecma script proposal at the moment of writing this, so will most likely land in ESNext. However, it also is opinionated in that it requires you to use [reflect-metadata](https://rbuckton.github.io/reflect-metadata/), which [is supposed to be an ecma script proposal, but isn't yet (at the moment of writing this)](https://github.com/rbuckton/reflect-metadata/issues/96). It might take years for reflect-metadata to land in Ecma script, if it ever does. + +### InversifyJS is not type-safe + +InversifyJS is also _not_ type-safe. There is no check to see of the injected type is actually injectable or that the corresponding type adheres to the expected type. + +## πŸ—οΈ Type safe? How? + +Type safe dependency injection works by combining awesome TypeScript features. Some of those features are: + +* [Literal types](https://www.typescriptlang.org/docs/handbook/advanced-types.html#string-literal-types) +* [Intersection types](https://www.typescriptlang.org/docs/handbook/advanced-types.html#intersection-types) +* [Mapped types](https://www.typescriptlang.org/docs/handbook/advanced-types.html#mapped-types) +* [Conditional types](https://www.typescriptlang.org/docs/handbook/advanced-types.html#conditional-types) +* [Rest parameters with tuple types](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-0.html#rest-parameters-with-tuple-types) + +## πŸ“– API reference + +_Note: some generic parameters are omitted for clarity._ + +### `Injector` + +The `Injector` is the core interface of typed-inject. It provides the ability to inject your class or function with `injectClass` and `injectFunction` respectively. You can create new _child injectors_ from it using the `provideXXX` methods. + +The `TContext` generic arguments is a [lookup type](https://blog.mariusschulz.com/2017/01/06/typescript-2-1-keyof-and-lookup-types). The keys in this type are the tokens that can be injected, the values are the exact types of those tokens. For example, if `TContext extends { foo: string, bar: number }`, you can let a token `'foo'` be injected of type `string`, and a token `'bar'` of type `number`. + +Typed inject comes with only one implementation. The `rootInjector`. It implements `Injector<{}>` interface, meaning that it does not provide any tokens (except for [magic tokens](#magic-tokens)) Import it with `import { rootInjector } from 'typed-inject'`. From the `rootInjector`, you can create child injectors. + +Don't worry about reusing the `rootInjector` in your application. It is stateless and read-only, so safe for concurrent use. + +#### `injector.injectClass(injectable: InjectableClass)` + +This method creates a new instance of class `injectable` and returns it. +When there are any problems in the dependency graph, it gives a compiler error. + +```ts +class Foo { + constructor(bar: number) { } + static inject = tokens('bar'); +} +const foo /*: Foo*/ = injector.injectClass(Foo); +``` + +#### `injector.injectFunction(fn: InjectableFunction)` + +This methods injects the function with requested tokens and returns the return value of the function. +When there are any problems in the dependency graph, it gives a compiler error. + +```ts +function foo(bar: number) { + return bar + 1; +} +foo.inject = tokens('bar'); +const baz /*: number*/ = injector.injectFunction(Foo); +``` + +#### `injector.resolve(token: Token): CorrespondingType` + +The `resolve` method lets you resolve tokens by hand. + +```ts +const foo = injector.resolve('foo'); +// Equivalent to: +function retrieveFoo(foo: number){ + return foo; +} +retrieveFoo.inject = tokens('foo'); +const foo2 = injector.injectFunction(retrieveFoo); +``` + +#### `injector.provideValue(token: Token, value: R): Injector>` + +Create a child injector that can provide value `value` for token `'token'`. The new child injector can resolve all tokens the parent injector can as well as `'token'`. + +```ts +const fooInjector = injector.provideValue('foo', 42); +``` + +#### `injector.provideFactory(token: Token, factory: InjectableFunction, scope = Scope.Singleton): Injector>` + +Create a child injector that can provide a value using `factory` for token `'token'`. The new child injector can resolve all tokens the parent injector can, as well as the new `'token'`. + +With `scope` you can decide whether the value must be cached after the factory is invoked once. Use `Scope.Singleton` to enable caching (default), or `Scope.Transient` to disable caching. + +```ts +const fooInjector = injector.provideFactory('foo', () => 42); +function loggerFactory(target: Function | undefined) { + return new Logger((target && target.name) || ''); +} +loggerFactory.inject = tokens(TARGET_TOKEN); +const fooBarInjector = fooInjector.provideFactory('logger', loggerFactory, Scope.Transient) +``` + +#### `injector.provideFactory(token: Token, Class: InjectableClass, scope = Scope.Singleton): Injector>` + +Create a child injector that can provide a value using instances of `Class` for token `'token'`. The new child injector can resolve all tokens the parent injector can, as well as the new `'token'`. + +Scope is also supported here, for more info, see `provideFactory`. + +### `Scope` + +The `Scope` enum indicates the scope of a provided injectable (class or factory). Possible values: `Scope.Transient` (new injection per resolve) or `Scope.Singleton` (inject once, and reuse values). It generally defaults to `Singleton`. + +### `tokens` + +The `tokens` function is a simple helper method that makes sure that an `inject` array is filled with a [tuple type filled with literal strings](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-0.html#rest-parameters-with-tuple-types). + +```ts +const inject = tokens('foo', 'bar'); +// Equivalent to: +const inject: ['foo', 'bar'] = ['foo', 'bar']. +``` + +_Note: hopefully [TypeScript will introduce explicit tuple syntax](https://github.com/Microsoft/TypeScript/issues/16656), so this helper method can be removed_ + +### `InjectableClass[]>` + +The `InjectableClass` interface is used to identify the (static) interface of classes that can be injected. It is defined as follows: + +```ts +{ + new(...args: CorrespondingTypes): R; + readonly inject: Tokens; +} +``` + +In other words, it makes sure that the `inject` tokens is corresponding with the constructor types. + +### `InjectableFunction[]>` + +Comparable to `InjectableClass`, but for (non-constructor) functions. + +## 🀝 Commendation + +This entire framework would not be possible without the awesome guys working on TypeScript. Guys like [Ryan](https://github.com/RyanCavanaugh), [Anders](https://github.com/ahejlsberg) and the rest of the team: a heartfelt thanks! πŸ’– + +Inspiration for the API with static `inject` method comes from years long AngularJS development. Special thanks to the Angular team. + diff --git a/packages/typed-inject/package.json b/packages/typed-inject/package.json new file mode 100644 index 0000000000..d0ecde8408 --- /dev/null +++ b/packages/typed-inject/package.json @@ -0,0 +1,28 @@ +{ + "name": "typed-inject", + "version": "0.0.0", + "description": "Type safe dependency injection framework for TypeScript", + "main": "src/index.js", + "typings": "src/index.d.ts", + "scripts": { + "test": "nyc --check-coverage --reporter=html --report-dir=reports/coverage --lines 90 --functions 95 --branches 80 npm run mocha", + "mocha": "mocha \"test/helpers/**/*.js\" \"test/unit/**/*.js\" && mocha --timeout 20000 \"test/helpers/**/*.js\" \"test/integration/**/*.js\" " + }, + "repository": { + "type": "git", + "url": "git+https://github.com/stryker-mutator/stryker.git" + }, + "keywords": [ + "stryker", + "utils" + ], + "publishConfig": { + "access": "public" + }, + "author": "Nico Jansen ", + "license": "Apache-2.0", + "bugs": { + "url": "https://github.com/stryker-mutator/stryker/issues" + }, + "homepage": "https://github.com/stryker-mutator/stryker/tree/master/packages/typed-inject#readme" +} diff --git a/packages/typed-inject/src/Exception.ts b/packages/typed-inject/src/Exception.ts new file mode 100644 index 0000000000..5a1f88b478 --- /dev/null +++ b/packages/typed-inject/src/Exception.ts @@ -0,0 +1,9 @@ + +export default class Exception extends Error { + constructor(message: string, readonly innerError?: Error) { + super(`${message}${innerError ? `. Inner error: ${innerError.message}` : ''}`); + Error.captureStackTrace(this, Exception); + // TS recommendation: https://github.com/Microsoft/TypeScript/wiki/Breaking-Changes#extending-built-ins-like-error-array-and-map-may-no-longer-work + Object.setPrototypeOf(this, Exception.prototype); + } +} diff --git a/packages/typed-inject/src/InjectorImpl.ts b/packages/typed-inject/src/InjectorImpl.ts new file mode 100644 index 0000000000..b99e9e39c2 --- /dev/null +++ b/packages/typed-inject/src/InjectorImpl.ts @@ -0,0 +1,162 @@ +import { Scope } from './api/Scope'; +import { InjectionToken, INJECTOR_TOKEN, TARGET_TOKEN } from './api/InjectionToken'; +import { InjectableClass, InjectableFunction, Injectable } from './api/Injectable'; +import { CorrespondingType } from './api/CorrespondingType'; +import { Injector } from './api/Injector'; +import Exception from './Exception'; + +const DEFAULT_SCOPE = Scope.Singleton; + +/* + +# Composite design pattern: + + ┏━━━━━━━━━━━━━━━━━━┓ + ┃ AbstractInjector ┃ + ┗━━━━━━━━━━━━━━━━━━┛ + β–² + ┃ + ┏━━━━━━┻━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━┓ + ┃ ┃ ┃ + ┏━━━━━━━━┻━━━━━┓ ┏━━━━━━━━━━━━┻━━━━━━━━━━━┓ ┏━━━━━━━┻━━━━━━━┓ + ┃ RootInjector ┃ ┃ AbstractCachedInjector ┃ ┃ ValueInjector ┃ + ┗━━━━━━━━━━━━━━┛ ┗━━━━━━━━━━━━━━━━━━━━━━━━┛ ┗━━━━━━━━━━━━━━━┛ + β–² + ┃ + ┏━━━━━━━┻━━━━━━━━━━━━┓ + ┏━━━━━━━━┻━━━━━━━━┓ ┏━━━━━━━━┻━━━━━━┓ + ┃ FactoryInjector ┃ ┃ ClassInjector ┃ + ┗━━━━━━━━━━━━━━━━━┛ ┗━━━━━━━━━━━━━━━┛ +*/ + +abstract class AbstractInjector implements Injector { + public injectClass[]>(Class: InjectableClass): R { + try { + const args: any[] = this.resolveParametersToInject(Class); + return new Class(...args as any); + } catch (error) { + throw new Exception(`Could not inject "${Class.name}"`, error); + } + } + + public injectFunction[]>(fn: InjectableFunction, target?: Function): R { + try { + const args: any[] = this.resolveParametersToInject(fn, target); + return fn(...args as any); + } catch (error) { + throw new Exception(`Could not inject "${fn.name}"`, error); + } + } + + private resolveParametersToInject[]>(injectable: Injectable, target?: Function): any[] { + const tokens: InjectionToken[] = (injectable as any).inject || []; + return tokens.map(key => this.resolve(key, target)); + } + + public provideValue(token: Token, value: R): AbstractInjector<{ [k in Token]: R; } & TContext> { + return new ValueInjector(this, token, value); + } + + public provideClass[]>(token: Token, Class: InjectableClass, scope = DEFAULT_SCOPE) + : AbstractInjector<{ [k in Token]: R; } & TContext> { + return new ClassInjector(this, token, scope, Class); + } + public provideFactory[]>(token: Token, factory: InjectableFunction, scope = DEFAULT_SCOPE) + : AbstractInjector<{ [k in Token]: R; } & TContext> { + return new FactoryInjector(this, token, scope, factory); + } + + public resolve>(token: Token, target?: Function): CorrespondingType { + switch (token) { + case TARGET_TOKEN: + return target as any; + case INJECTOR_TOKEN: + return this as any; + default: + return this.resolveInternal(token); + } + } + + protected abstract resolveInternal>(token: Token, target?: Function): CorrespondingType; +} + +class RootInjector extends AbstractInjector<{}> { + public resolveInternal>(token: Token) + : CorrespondingType<{}, Token> { + throw new Error(`No provider found for "${token}"!.`); + } +} + +type ChildContext = TParentContext & { [k in Token]: R }; + +class ValueInjector extends AbstractInjector> { + + constructor(private readonly parent: AbstractInjector, private readonly token: Token, private readonly value: R) { + super(); + } + + protected resolveInternal>>(token: SearchToken, target: Function) + : CorrespondingType, SearchToken> { + if (token === this.token) { + return this.value as any; + } else { + return this.parent.resolve(token as any, target) as any; + } + } +} + +abstract class AbstractCachedInjector extends AbstractInjector> { + + private cached: { value?: any } | undefined; + + constructor(protected readonly parent: AbstractInjector, + protected readonly token: Token, + private readonly scope: Scope) { + super(); + } + + protected resolveInternal>>(token: SearchToken, target: Function | undefined) + : CorrespondingType, SearchToken> { + if (token === this.token) { + if (this.cached) { + return this.cached.value as any; + } else { + const value = this.result(target); + if (this.scope === Scope.Singleton) { + this.cached = { value }; + } + return value as any; + } + } else { + return this.parent.resolve(token as any, target) as any; + } + } + + protected abstract result(target: Function | undefined): R; +} + +class FactoryInjector[]> extends AbstractCachedInjector { + constructor(parent: AbstractInjector, + token: Token, + scope: Scope, + private readonly injectable: InjectableFunction) { + super(parent, token, scope); + } + protected result(target: Function): R { + return this.injectFunction(this.injectable as any, target); + } +} + +class ClassInjector[]> extends AbstractCachedInjector { + constructor(parent: AbstractInjector, + token: Token, + scope: Scope, + private readonly injectable: InjectableClass) { + super(parent, token, scope); + } + protected result(_target: Function): R { + return this.injectClass(this.injectable as any); + } +} + +export const rootInjector = new RootInjector() as Injector<{}>; diff --git a/packages/typed-inject/src/api/CorrespondingType.ts b/packages/typed-inject/src/api/CorrespondingType.ts new file mode 100644 index 0000000000..fe2a3dcbe5 --- /dev/null +++ b/packages/typed-inject/src/api/CorrespondingType.ts @@ -0,0 +1,11 @@ +import { InjectionToken, InjectorToken, TargetToken } from './InjectionToken'; +import { Injector } from './Injector'; + +export type CorrespondingType> = + T extends InjectorToken ? Injector + : T extends TargetToken ? Function | undefined + : T extends keyof TContext ? TContext[T] : never; + +export type CorrespondingTypes[]> = { + [K in keyof TS]: TS[K] extends InjectionToken ? CorrespondingType : never; +}; diff --git a/packages/typed-inject/src/api/Injectable.ts b/packages/typed-inject/src/api/Injectable.ts new file mode 100644 index 0000000000..9a9a57a82f --- /dev/null +++ b/packages/typed-inject/src/api/Injectable.ts @@ -0,0 +1,24 @@ +import { CorrespondingTypes } from './CorrespondingType'; +import { InjectionToken } from './InjectionToken'; + +export type InjectableClass[]> = + ClassWithInjections | ClassWithoutInjections; + +export interface ClassWithInjections[]> { + new(...args: CorrespondingTypes): R; + readonly inject: Tokens; +} + +export type ClassWithoutInjections = new () => R; + +export type InjectableFunction[]> = + InjectableFunctionWithInject | InjectableFunctionWithoutInject; + +export interface InjectableFunctionWithInject[]> { + (...args: CorrespondingTypes): R; + readonly inject: Tokens; +} + +export type InjectableFunctionWithoutInject = () => R; + +export type Injectable[]> = InjectableClass | InjectableFunction; diff --git a/packages/typed-inject/src/api/InjectionToken.ts b/packages/typed-inject/src/api/InjectionToken.ts new file mode 100644 index 0000000000..e068206d47 --- /dev/null +++ b/packages/typed-inject/src/api/InjectionToken.ts @@ -0,0 +1,5 @@ +export type InjectorToken = '$injector'; +export type TargetToken = '$target'; +export const INJECTOR_TOKEN: InjectorToken = '$injector'; +export const TARGET_TOKEN: TargetToken = '$target'; +export type InjectionToken = InjectorToken | TargetToken | keyof TContext; diff --git a/packages/typed-inject/src/api/Injector.ts b/packages/typed-inject/src/api/Injector.ts new file mode 100644 index 0000000000..848fdb547c --- /dev/null +++ b/packages/typed-inject/src/api/Injector.ts @@ -0,0 +1,15 @@ +import { InjectableClass, InjectableFunction } from './Injectable'; +import { CorrespondingType } from './CorrespondingType'; +import { InjectionToken } from './InjectionToken'; +import { Scope } from './Scope'; + +export interface Injector { + injectClass[]>(Class: InjectableClass): R; + injectFunction[]>(Class: InjectableFunction): R; + resolve>(token: Token): CorrespondingType; + provideValue(token: Token, value: R): Injector<{ [k in Token]: R } & TContext>; + provideClass[]>(token: Token, Class: InjectableClass, scope?: Scope) + : Injector<{ [k in Token]: R } & TContext>; + provideFactory[]>(token: Token, factory: InjectableFunction, scope?: Scope) + : Injector<{ [k in Token]: R } & TContext>; +} diff --git a/packages/typed-inject/src/api/Scope.ts b/packages/typed-inject/src/api/Scope.ts new file mode 100644 index 0000000000..65812f1e05 --- /dev/null +++ b/packages/typed-inject/src/api/Scope.ts @@ -0,0 +1,5 @@ + +export enum Scope { + Transient = 'transient', + Singleton = 'singleton' +} diff --git a/packages/typed-inject/src/index.ts b/packages/typed-inject/src/index.ts new file mode 100644 index 0000000000..0e6c995536 --- /dev/null +++ b/packages/typed-inject/src/index.ts @@ -0,0 +1,7 @@ +export * from './api/Injectable'; +export * from './api/CorrespondingType'; +export * from './api/InjectionToken'; +export * from './api/Injector'; +export * from './api/Scope'; +export * from './InjectorImpl'; +export * from './tokens'; diff --git a/packages/typed-inject/src/tokens.ts b/packages/typed-inject/src/tokens.ts new file mode 100644 index 0000000000..d8317928cf --- /dev/null +++ b/packages/typed-inject/src/tokens.ts @@ -0,0 +1,12 @@ +/** + * Helper method to create string literal tuple type. + * @example + * ```ts + * const inject = tokens('foo', 'bar'); + * const inject2: ['foo', 'bar'] = ['foo', 'bar']; + * ``` + * @param tokens The tokens as args + */ +export function tokens(...tokens: TS): TS { + return tokens; +} diff --git a/packages/typed-inject/test/helpers/initSourceMaps.ts b/packages/typed-inject/test/helpers/initSourceMaps.ts new file mode 100644 index 0000000000..fcfbfda16c --- /dev/null +++ b/packages/typed-inject/test/helpers/initSourceMaps.ts @@ -0,0 +1 @@ +import 'source-map-support/register'; diff --git a/packages/typed-inject/test/integration/typed-inject.it.spec.ts b/packages/typed-inject/test/integration/typed-inject.it.spec.ts new file mode 100644 index 0000000000..b19e01778f --- /dev/null +++ b/packages/typed-inject/test/integration/typed-inject.it.spec.ts @@ -0,0 +1,85 @@ +import fs = require('fs'); +import path = require('path'); +import ts = require('typescript'); +import { expect } from 'chai'; + +describe('typed-inject', () => { + + fs.readdirSync(testResource()) + .forEach(tsFile => { + it(path.basename(tsFile), async () => { + const fileName = testResource(tsFile); + const firstLine = await readFirstLine(fileName); + const expectedErrorMessage = parseExpectedError(firstLine); + const actualError = findActualError(fileName); + if (expectedErrorMessage) { + expect(actualError).contains(expectedErrorMessage); + } else { + expect(actualError).undefined; + } + }); + }); +}); + +let program: ts.Program | undefined; +function findActualError(fileName: string) { + program = ts.createProgram([fileName], { + module: ts.ModuleKind.ES2015, + strict: true, + target: ts.ScriptTarget.ESNext, + types: [ + 'node' + ] + }, undefined, program); + const diagnostics = ts.getPreEmitDiagnostics(program) + .map(diagnostic => ts.flattenDiagnosticMessageText(diagnostic.messageText, '\n')); + expect(diagnostics.length).lessThan(2, diagnostics.join(', ')); + return diagnostics[0]; +} + +function parseExpectedError(line: string): string | undefined { + const expectationRegex = /\/\/\s*error:\s*(.*)/; + const result = expectationRegex.exec(line); + if (result) { + const expectation: string = result[1]; + const error: unknown = JSON.parse(expectation); + if (!error) { + return undefined; + } else if (typeof error === 'string') { + return error; + } else { + expect.fail(`Unable to parse expectation: ${line}, use a JSON string or undefined`); + throw new Error(); + } + } else { + expect.fail(`Unable to parse expectation: ${line}, make sure file starts with '// error: "expected error"`); + throw new Error(); + } +} + +function readFile(fileName: string): Promise { + return new Promise((res, rej) => { + fs.readFile(fileName, 'utf8', (err, result) => { + if (err) { + rej(err); + } else { + res(result); + } + }); + }); +} + +function testResource(relativePath?: string) { + return path.resolve(__dirname, '..', '..', 'testResources', relativePath || '.'); +} + +async function readFirstLine(fileName: string) { + const file = await readFile(fileName); + const line = file.split('\n').shift(); + if (!line) { + expect.fail(`No content found in file: ${fileName}`); + throw new Error(); + } else { + return line; + } +} diff --git a/packages/typed-inject/test/unit/Injector.spec.ts b/packages/typed-inject/test/unit/Injector.spec.ts new file mode 100644 index 0000000000..b8343bf4b0 --- /dev/null +++ b/packages/typed-inject/test/unit/Injector.spec.ts @@ -0,0 +1,207 @@ +import { expect } from 'chai'; +import { Injector } from '../../src/api/Injector'; +import { tokens } from '../../src/tokens'; +import { rootInjector } from '../../src/InjectorImpl'; +import { TARGET_TOKEN, INJECTOR_TOKEN } from '../../src/api/InjectionToken'; +import Exception from '../../src/Exception'; +import { Scope } from '../../src/api/Scope'; + +describe('InjectorImpl', () => { + + describe('RootInjector', () => { + + it('should be able to inject injector and target in a class', () => { + // Arrange + class Injectable { + constructor( + public readonly target: Function | undefined, + public readonly injector: Injector<{}>) { + } + public static inject = tokens(TARGET_TOKEN, INJECTOR_TOKEN); + } + + // Act + const actual = rootInjector.injectClass(Injectable); + + // Assert + expect(actual.target).eq(undefined); + expect(actual.injector).eq(rootInjector); + }); + + it('should be able to inject injector and target in a function', () => { + // Arrange + let actualTarget: Function | undefined; + let actualInjector: Injector<{}> | undefined; + const expectedResult = { result: 42 }; + function injectable(t: Function | undefined, i: Injector<{}>) { + actualTarget = t; + actualInjector = i; + return expectedResult; + } + injectable.inject = tokens(TARGET_TOKEN, INJECTOR_TOKEN); + + // Act + const actualResult: { result: number } = rootInjector.injectFunction(injectable); + + // Assert + expect(actualTarget).eq(undefined); + expect(actualInjector).eq(rootInjector); + expect(actualResult).eq(expectedResult); + }); + + it('should throw when no provider was found', () => { + class FooInjectable { + constructor(public foo: string) { + } + public static inject = tokens('foo'); + } + expect(() => rootInjector.injectClass(FooInjectable as any)).throws(Exception, + 'Could not inject "FooInjectable". Inner error: No provider found for "foo"!'); + }); + }); + + describe('ValueInjector', () => { + it('should be able to provide a value', () => { + const sut = rootInjector.provideValue('foo', 42); + const actual = sut.injectClass(class { + constructor(public foo: number) { } + public static inject = tokens('foo'); + }); + expect(actual.foo).eq(42); + }); + it('should be able to provide a value from the parent injector', () => { + const sut = rootInjector + .provideValue('foo', 42) + .provideValue('bar', 'baz'); + expect(sut.resolve('bar')).eq('baz'); + }); + }); + + describe('FactoryInjector', () => { + it('should be able to provide the return value of the factoryMethod', () => { + const expectedValue = { foo: 'bar' }; + function foobar() { + return expectedValue; + } + + const actual = rootInjector + .provideFactory('foobar', foobar) + .injectClass(class { + constructor(public foobar: { foo: string }) { } + public static inject = tokens('foobar'); + }); + expect(actual.foobar).eq(expectedValue); + }); + + it('should be able still provide parent injector values', () => { + function truth() { + return true; + } + truth.inject = tokens(); + const factoryInjector = rootInjector.provideFactory('truth', truth); + const actual = factoryInjector.injectClass(class { + constructor(public injector: Injector<{ truth: boolean }>, public target: Function | undefined) { } + public static inject = tokens(INJECTOR_TOKEN, TARGET_TOKEN); + }); + expect(actual.injector).eq(factoryInjector); + expect(actual.target).undefined; + }); + + it('should cache the value if scope = Singleton', () => { + // Arrange + let n = 0; + function count() { + return n++; + } + count.inject = tokens(); + const countInjector = rootInjector.provideFactory('count', count); + class Injectable { + constructor(public count: number) { } + public static inject = tokens('count'); + } + + // Act + const first = countInjector.injectClass(Injectable); + const second = countInjector.injectClass(Injectable); + + // Assert + expect(first.count).eq(second.count); + }); + + it('should _not_ cache the value if scope = Transient', () => { + // Arrange + let n = 0; + function count() { + return n++; + } + count.inject = tokens(); + const countInjector = rootInjector.provideFactory('count', count, Scope.Transient); + class Injectable { + constructor(public count: number) { } + public static inject = tokens('count'); + } + + // Act + const first = countInjector.injectClass(Injectable); + const second = countInjector.injectClass(Injectable); + + // Assert + expect(first.count).eq(0); + expect(second.count).eq(1); + }); + }); + + describe('dependency tree', () => { + it('should be able to inject a dependency tree', () => { + // Arrange + class Logger { + public info(_msg: string) { + } + } + class GrandChild { + public baz = 'qux'; + constructor(public log: Logger) { + } + public static inject = tokens('logger'); + } + class Child1 { + public bar = 'foo'; + constructor(public log: Logger, public grandchild: GrandChild) { + } + public static inject = tokens('logger', 'grandChild'); + } + class Child2 { + public foo = 'bar'; + constructor(public log: Logger) { + } + public static inject = tokens('logger'); + } + class Parent { + constructor( + public readonly child: Child1, + public readonly child2: Child2, + public readonly log: Logger) { + } + public static inject = tokens('child1', 'child2', 'logger'); + } + const expectedLogger = new Logger(); + + // Act + const actual = rootInjector + .provideValue('logger', expectedLogger) + .provideClass('grandChild', GrandChild) + .provideClass('child1', Child1) + .provideClass('child2', Child2) + .injectClass(Parent); + + // Assert + expect(actual.child.bar).eq('foo'); + expect(actual.child2.foo).eq('bar'); + expect(actual.child.log).eq(expectedLogger); + expect(actual.child2.log).eq(expectedLogger); + expect(actual.child.grandchild.log).eq(expectedLogger); + expect(actual.child.grandchild.baz).eq('qux'); + expect(actual.log).eq(expectedLogger); + }); + }); +}); diff --git a/packages/typed-inject/testResources/dependency-graph.ts b/packages/typed-inject/testResources/dependency-graph.ts new file mode 100644 index 0000000000..d429a1061e --- /dev/null +++ b/packages/typed-inject/testResources/dependency-graph.ts @@ -0,0 +1,23 @@ +// error: false +import { rootInjector, tokens } from '../src/index'; + +class Baz { + public baz = 'baz'; +} + +function bar(baz: Baz) { + return { baz }; +} +bar.inject = tokens('baz'); + +class Foo { + constructor(public bar: { baz: Baz }, public baz: Baz, public qux: boolean) { } + public static inject = tokens('bar', 'baz', 'qux'); +} + +const fooInjector = rootInjector + .provideValue('qux', true) + .provideClass('baz', Baz) + .provideFactory('bar', bar); + +const foo: Foo = fooInjector.injectClass(Foo); diff --git a/packages/typed-inject/testResources/forgot-tokens-class.ts b/packages/typed-inject/testResources/forgot-tokens-class.ts new file mode 100644 index 0000000000..d2efebff15 --- /dev/null +++ b/packages/typed-inject/testResources/forgot-tokens-class.ts @@ -0,0 +1,6 @@ +// error: "Property 'inject' is missing in type 'typeof Foo'" +import { rootInjector, Injector } from '../src/index'; + +rootInjector.injectClass(class Foo { + constructor(public injector: Function | Injector<{}> | undefined) { } +}); diff --git a/packages/typed-inject/testResources/forgot-tokens-function.ts b/packages/typed-inject/testResources/forgot-tokens-function.ts new file mode 100644 index 0000000000..0a65e439aa --- /dev/null +++ b/packages/typed-inject/testResources/forgot-tokens-function.ts @@ -0,0 +1,4 @@ +// error: "Property 'inject' is missing in type '(injector: Function | Injector<{}> | undefined) => void' but required" +import { rootInjector, Injector } from '../src/index'; +function foo(injector: Function | Injector<{}> | undefined) { } +rootInjector.injectFunction(foo); diff --git a/packages/typed-inject/testResources/tokens-of-type-string.ts b/packages/typed-inject/testResources/tokens-of-type-string.ts new file mode 100644 index 0000000000..a93db81a21 --- /dev/null +++ b/packages/typed-inject/testResources/tokens-of-type-string.ts @@ -0,0 +1,11 @@ +// error: "Type 'string[]' is not assignable to type 'InjectionToken<{ bar: number; }>[]" + +import { rootInjector } from '../src/index'; + +class Foo { + constructor(bar: number) { } + public static inject = ['bar']; +} +const foo: Foo = rootInjector + .provideValue('bar', 42) + .injectClass(Foo); diff --git a/packages/typed-inject/testResources/unknown-token.ts b/packages/typed-inject/testResources/unknown-token.ts new file mode 100644 index 0000000000..feafc2c0fa --- /dev/null +++ b/packages/typed-inject/testResources/unknown-token.ts @@ -0,0 +1,9 @@ +// error: "Type '[\"not-exists\"]' is not assignable to type 'InjectionToken<{}>[]" + +import { rootInjector, tokens } from '../src/index'; + +function foo(bar: string) { } +foo.inject = tokens('not-exists'); + +rootInjector + .injectFunction(foo); diff --git a/packages/typed-inject/testResources/wrong-order-tokens.ts b/packages/typed-inject/testResources/wrong-order-tokens.ts new file mode 100644 index 0000000000..810fd97f9d --- /dev/null +++ b/packages/typed-inject/testResources/wrong-order-tokens.ts @@ -0,0 +1,12 @@ +// error: "Types of parameters 'bar' and 'args_0' are incompatible" +import { rootInjector, tokens } from '../src/index'; + +class Foo { + constructor(bar: string, baz: number) { } + public static inject = tokens('baz', 'bar'); +} + +const foo: Foo = rootInjector + .provideValue('bar', 'bar') + .provideValue('baz', 42) + .injectClass(Foo); diff --git a/packages/typed-inject/tsconfig.json b/packages/typed-inject/tsconfig.json new file mode 100644 index 0000000000..e98368432f --- /dev/null +++ b/packages/typed-inject/tsconfig.json @@ -0,0 +1,11 @@ +{ + "files": [], + "references": [ + { + "path": "./tsconfig.src.json" + }, + { + "path": "./tsconfig.test.json" + } + ] +} \ No newline at end of file diff --git a/packages/typed-inject/tsconfig.src.json b/packages/typed-inject/tsconfig.src.json new file mode 100644 index 0000000000..809734fd2b --- /dev/null +++ b/packages/typed-inject/tsconfig.src.json @@ -0,0 +1,10 @@ +{ + "extends": "../../tsconfig.settings.json", + "compilerOptions": { + "rootDir": "." + }, + "include": [ + "src" + ], + "references": [ ] +} \ No newline at end of file diff --git a/packages/typed-inject/tsconfig.test.json b/packages/typed-inject/tsconfig.test.json new file mode 100644 index 0000000000..0cf2f78c25 --- /dev/null +++ b/packages/typed-inject/tsconfig.test.json @@ -0,0 +1,18 @@ +{ + "extends": "../../tsconfig.settings.json", + "compilerOptions": { + "rootDir": ".", + "types": [ + "mocha", + "node" + ] + }, + "include": [ + "test" + ], + "references": [ + { + "path": "tsconfig.src.json", + }, + ] +} \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json index 2802ab8bb3..6b5f163b3e 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -17,6 +17,8 @@ { "path": "packages/stryker-typescript" }, { "path": "packages/stryker-vue-mutator" }, { "path": "packages/stryker-wct-runner" }, - { "path": "packages/stryker-webpack-transpiler" } + { "path": "packages/stryker-webpack-transpiler" }, + { "path": "packages/stryker-test-helpers" }, + { "path": "packages/typed-inject" } ] } diff --git a/workspace.code-workspace b/workspace.code-workspace index 7e58fe4425..4f0dd0cf14 100644 --- a/workspace.code-workspace +++ b/workspace.code-workspace @@ -54,9 +54,15 @@ { "path": "packages/stryker-util" }, + { + "path": "packages/stryker-test-helpers" + }, { "path": "e2e" }, + { + "path": "packages/typed-inject" + }, { "path": ".", "name": "stryker-parent"