From eb51b11d5df14e4bc1826bce868a4120b6996c1b Mon Sep 17 00:00:00 2001 From: Nico Jansen Date: Wed, 26 Dec 2018 18:20:09 +0100 Subject: [PATCH 01/36] Add dependency injection idea --- packages/stryker-api/src/di/ContainerValues.ts | 7 +++++++ packages/stryker-api/src/di/Injectable.ts | 9 +++++++++ packages/stryker-api/src/di/InjectableKey.ts | 4 ++++ packages/stryker-api/src/di/Injections.ts | 7 +++++++ 4 files changed, 27 insertions(+) create mode 100644 packages/stryker-api/src/di/ContainerValues.ts create mode 100644 packages/stryker-api/src/di/Injectable.ts create mode 100644 packages/stryker-api/src/di/InjectableKey.ts create mode 100644 packages/stryker-api/src/di/Injections.ts diff --git a/packages/stryker-api/src/di/ContainerValues.ts b/packages/stryker-api/src/di/ContainerValues.ts new file mode 100644 index 0000000000..a763880a02 --- /dev/null +++ b/packages/stryker-api/src/di/ContainerValues.ts @@ -0,0 +1,7 @@ +import { Injections } from './Injections'; + +type ContainerValues = { + [K in keyof TS]: TS[K] extends keyof Injections ? Injections[TS[K]] : never; +}; + +export default ContainerValues; diff --git a/packages/stryker-api/src/di/Injectable.ts b/packages/stryker-api/src/di/Injectable.ts new file mode 100644 index 0000000000..a2f22c0c95 --- /dev/null +++ b/packages/stryker-api/src/di/Injectable.ts @@ -0,0 +1,9 @@ +import InjectableKey from './InjectableKey'; +import ContainerValues from './ContainerValues'; + +interface Injectable { + new(...args: ContainerValues): T; + inject: TS; +} + +export default Injectable; diff --git a/packages/stryker-api/src/di/InjectableKey.ts b/packages/stryker-api/src/di/InjectableKey.ts new file mode 100644 index 0000000000..a7631704a5 --- /dev/null +++ b/packages/stryker-api/src/di/InjectableKey.ts @@ -0,0 +1,4 @@ +import { Injections } from './Injections'; + +type InjectableKey = keyof Injections; +export default InjectableKey; diff --git a/packages/stryker-api/src/di/Injections.ts b/packages/stryker-api/src/di/Injections.ts new file mode 100644 index 0000000000..57beaaea99 --- /dev/null +++ b/packages/stryker-api/src/di/Injections.ts @@ -0,0 +1,7 @@ +import { LoggerFactoryMethod } from '../../logging'; +import { TestFrameworkSettings } from '../../test_framework'; + +export interface Injections { + testFrameworkSettings: TestFrameworkSettings; + getLogger: LoggerFactoryMethod; +} From 9a532c72e1aa866d39be9b8bda032c756711acbd Mon Sep 17 00:00:00 2001 From: Nico Jansen Date: Sun, 30 Dec 2018 22:43:46 +0100 Subject: [PATCH 02/36] Started with implementation of new pugin mechanism. * Created `Injector` class, a readonly class that can instantiate `Injectable`s * Updated the `PluginLoader`, so it can load plugins with the new way of working as well as provide a Bridge plugin for all "old fashion" plugin * Started implenenting the `BroadcastReporter` and other Reporters in the new way. Couldn't help to make some quality of life improvements: * `StrykerOptions` now have the expected required/not required properties. * Created "test-helpers" package so we can share test helpers between packages. --- packages/stryker-api/config.ts | 1 + packages/stryker-api/di.ts | 10 ++ packages/stryker-api/mutant.ts | 1 + packages/stryker-api/report.ts | 1 + packages/stryker-api/src/config/Config.ts | 2 +- .../src/config/ConfigEditorPlugin.ts | 6 + .../stryker-api/src/core/StrykerOptions.ts | 32 ++-- packages/stryker-api/src/di/Container.ts | 19 ++ .../stryker-api/src/di/ContainerValues.ts | 7 +- packages/stryker-api/src/di/Inject.ts | 6 + packages/stryker-api/src/di/Injectable.ts | 8 +- packages/stryker-api/src/di/InjectableKey.ts | 4 - packages/stryker-api/src/di/Injections.ts | 7 - packages/stryker-api/src/di/InjectorKey.ts | 4 + packages/stryker-api/src/di/PluginKind.ts | 9 + packages/stryker-api/src/di/PluginResolver.ts | 7 + packages/stryker-api/src/di/StrykerPlugin.ts | 8 + packages/stryker-api/src/di/keys.ts | 5 + packages/stryker-api/src/di/plugins.ts | 10 ++ .../stryker-api/src/mutant/MutatorPlugin.ts | 6 + .../stryker-api/src/report/ReporterPlugin.ts | 6 + .../src/test_framework/TestFrameworkPlugin.ts | 6 + .../src/test_runner/TestRunnerPlugin.ts | 6 + .../src/transpile/TranspilerPlugin.ts | 6 + .../test/unit/config/ConfigSpec.ts | 5 +- packages/stryker-api/test_framework.ts | 1 + packages/stryker-api/test_runner.ts | 1 + packages/stryker-api/transpile.ts | 1 + packages/stryker-api/tsconfig.src.json | 3 +- .../stryker-html-reporter/src/HtmlReporter.ts | 9 +- packages/stryker-html-reporter/src/index.ts | 4 +- packages/stryker-jasmine-runner/package.json | 3 +- .../test/integration/JasmineRunner.it.ts | 7 +- .../test/unit/JasmineTestRunnerSpec.ts | 3 +- .../stryker-jasmine-runner/tsconfig.test.json | 3 + packages/stryker-karma-runner/package.json | 1 + .../test/integration/KarmaTestRunner.it.ts | 5 +- .../test/unit/KarmaTestRunnerSpec.ts | 3 +- .../stryker-karma-runner/tsconfig.test.json | 3 + packages/stryker-mocha-runner/package.json | 3 +- .../test/helpers/mockHelpers.ts | 7 +- ...MochaTestFrameworkIntegrationTestWorker.ts | 5 +- .../test/integration/QUnitSampleSpec.ts | 7 +- .../test/integration/SampleProjectSpec.ts | 15 +- .../test/unit/MochaTestRunnerSpec.ts | 11 +- packages/stryker-mocha-runner/tsconfig.json | 15 +- .../stryker-mocha-runner/tsconfig.src.json | 17 ++ .../stryker-mocha-runner/tsconfig.test.json | 20 +++ packages/stryker-test-helpers/package.json | 24 +++ packages/stryker-test-helpers/src/factory.ts | 168 ++++++++++++++++++ packages/stryker-test-helpers/src/index.ts | 2 + packages/stryker-test-helpers/tsconfig.json | 8 + .../stryker-test-helpers/tsconfig.src.json | 18 ++ packages/stryker-util/tsconfig.test.json | 5 +- packages/stryker/src/PluginLoader.ts | 66 ------- packages/stryker/src/ReporterOrchestrator.ts | 150 ++++++++-------- packages/stryker/src/Stryker.ts | 37 ++-- .../child-proxy/ChildProcessProxyWorker.ts | 2 +- packages/stryker/src/config/ConfigReader.ts | 2 +- packages/stryker/src/di/Injector.ts | 68 +++++++ packages/stryker/src/di/PluginLoader.ts | 151 ++++++++++++++++ packages/stryker/src/di/Providers.ts | 24 +++ packages/stryker/src/mutators/ES5Mutator.ts | 5 +- .../src/reporters/BroadcastReporter.ts | 27 +-- .../src/reporters/ClearTextReporter.ts | 10 +- .../src/reporters/DashboardReporter.ts | 5 + .../stryker/src/reporters/DotsReporter.ts | 5 + .../src/reporters/EventRecorderReporter.ts | 4 + .../stryker/src/reporters/ProgressReporter.ts | 4 + packages/stryker/src/reporters/index.ts | 14 ++ packages/stryker/src/utils/fileUtils.ts | 4 +- packages/stryker/stryker.conf.js | 8 +- .../stryker/test/helpers/TestRunnerMock.ts | 8 +- packages/stryker/test/helpers/globals.ts | 6 - packages/stryker/test/helpers/initSinon.ts | 6 +- packages/stryker/test/helpers/logMock.ts | 3 +- packages/stryker/test/helpers/producers.ts | 53 ++++-- .../CommandTestRunner.it.ts | 3 +- .../config-reader/ConfigReaderSpec.ts | 5 +- .../ResilientTestRunnerFactory.it.ts | 5 +- .../test/unit/MutantTestMatcherSpec.ts | 17 +- .../stryker/test/unit/MutatorFacadeSpec.ts | 5 +- .../test/unit/ReporterOrchestratorSpec.ts | 154 ++++++++-------- packages/stryker/test/unit/SandboxPoolSpec.ts | 12 +- packages/stryker/test/unit/SandboxSpec.ts | 17 +- .../test/unit/ScoreResultCalculatorSpec.ts | 2 +- packages/stryker/test/unit/StrykerSpec.ts | 43 ++--- .../unit/TestFrameworkOrchestratorSpec.ts | 8 +- .../unit/child-proxy/ChildProcessProxySpec.ts | 9 +- .../child-proxy/ChildProcessWorkerSpec.ts | 17 +- .../test/unit/{ => di}/PluginLoaderSpec.ts | 12 +- .../test/unit/initializer/PresetsSpec.ts | 3 +- .../initializer/StrykerInitializerSpec.ts | 20 +-- .../test/unit/input/InputFileResolverSpec.ts | 6 +- .../test/unit/logging/LogConfiguratorSpec.ts | 7 +- .../unit/mutators/ES5MutantGeneratorSpec.ts | 3 +- .../unit/process/InitialTestExecutorSpec.ts | 13 +- .../unit/process/MutationTestExecutorSpec.ts | 6 +- .../unit/reporters/BroadcastReporterSpec.ts | 121 ++++++++----- .../unit/reporters/ClearTextReporterSpec.ts | 2 +- .../unit/reporters/DashboardReporterSpec.ts | 4 +- .../test/unit/reporters/DotsReporterSpec.ts | 2 +- .../reporters/EventRecorderReporterSpec.ts | 10 +- .../ProgressAppendOnlyReporterSpec.ts | 16 +- .../unit/reporters/ProgressReporterSpec.ts | 19 +- .../unit/reporters/ci/CircleProviderSpec.ts | 2 +- .../test/unit/reporters/ci/ProviderSpec.ts | 2 +- .../unit/reporters/ci/TravisProviderSpec.ts | 2 +- .../ChildProcessTestRunnerDecoratorSpec.ts | 12 +- .../unit/test-runner/CommandTestRunnerSpec.ts | 7 +- .../unit/test-runner/TimeoutDecoratorSpec.ts | 2 +- .../unit/transpiler/MutantTranspilerSpec.ts | 8 +- .../test/unit/transpiler/SourceMapperSpec.ts | 5 +- .../unit/transpiler/TranspilerFacadeSpec.ts | 3 +- .../stryker/test/unit/utils/TempFolderSpec.ts | 10 +- .../stryker/test/unit/utils/fileUtilsSpec.ts | 7 +- .../test/unit/utils/objectUtilsSpec.ts | 5 +- tsconfig.json | 3 +- workspace.code-workspace | 3 + 119 files changed, 1248 insertions(+), 580 deletions(-) create mode 100644 packages/stryker-api/di.ts create mode 100644 packages/stryker-api/src/config/ConfigEditorPlugin.ts create mode 100644 packages/stryker-api/src/di/Container.ts create mode 100644 packages/stryker-api/src/di/Inject.ts delete mode 100644 packages/stryker-api/src/di/InjectableKey.ts delete mode 100644 packages/stryker-api/src/di/Injections.ts create mode 100644 packages/stryker-api/src/di/InjectorKey.ts create mode 100644 packages/stryker-api/src/di/PluginKind.ts create mode 100644 packages/stryker-api/src/di/PluginResolver.ts create mode 100644 packages/stryker-api/src/di/StrykerPlugin.ts create mode 100644 packages/stryker-api/src/di/keys.ts create mode 100644 packages/stryker-api/src/di/plugins.ts create mode 100644 packages/stryker-api/src/mutant/MutatorPlugin.ts create mode 100644 packages/stryker-api/src/report/ReporterPlugin.ts create mode 100644 packages/stryker-api/src/test_framework/TestFrameworkPlugin.ts create mode 100644 packages/stryker-api/src/test_runner/TestRunnerPlugin.ts create mode 100644 packages/stryker-api/src/transpile/TranspilerPlugin.ts create mode 100644 packages/stryker-mocha-runner/tsconfig.src.json create mode 100644 packages/stryker-mocha-runner/tsconfig.test.json create mode 100644 packages/stryker-test-helpers/package.json create mode 100644 packages/stryker-test-helpers/src/factory.ts create mode 100644 packages/stryker-test-helpers/src/index.ts create mode 100644 packages/stryker-test-helpers/tsconfig.json create mode 100644 packages/stryker-test-helpers/tsconfig.src.json delete mode 100644 packages/stryker/src/PluginLoader.ts create mode 100644 packages/stryker/src/di/Injector.ts create mode 100644 packages/stryker/src/di/PluginLoader.ts create mode 100644 packages/stryker/src/di/Providers.ts create mode 100644 packages/stryker/src/reporters/index.ts delete mode 100644 packages/stryker/test/helpers/globals.ts rename packages/stryker/test/unit/{ => di}/PluginLoaderSpec.ts (87%) diff --git a/packages/stryker-api/config.ts b/packages/stryker-api/config.ts index 3f6b39a7b4..df30f0fe6c 100644 --- a/packages/stryker-api/config.ts +++ b/packages/stryker-api/config.ts @@ -1,3 +1,4 @@ export { default as Config } from './src/config/Config'; export { default as ConfigEditor } from './src/config/ConfigEditor'; export { default as ConfigEditorFactory } from './src/config/ConfigEditorFactory'; +export { default as ConfigEditorPlugin } from './src/config/ConfigEditorPlugin'; diff --git a/packages/stryker-api/di.ts b/packages/stryker-api/di.ts new file mode 100644 index 0000000000..aeb85b2bf6 --- /dev/null +++ b/packages/stryker-api/di.ts @@ -0,0 +1,10 @@ +export { default as ContainerValues } from './src/di/ContainerValues'; +export { default as Injectable } from './src/di/Injectable'; +export { default as InjectorKey } from './src/di/InjectorKey'; +export { default as Container } from './src/di/Container'; +export { default as keys } from './src/di/keys'; +export { default as plugins } from './src/di/plugins'; +export { default as StrykerPlugin } from './src/di/StrykerPlugin'; +export { default as PluginKind } from './src/di/PluginKind'; +export { default as PluginResolver } from './src/di/PluginResolver'; +export { default as Inject } from './src/di/Inject'; diff --git a/packages/stryker-api/mutant.ts b/packages/stryker-api/mutant.ts index 81a9025925..737221c1cd 100644 --- a/packages/stryker-api/mutant.ts +++ b/packages/stryker-api/mutant.ts @@ -1,3 +1,4 @@ export { default as Mutant } from './src/mutant/Mutant'; export { default as Mutator } from './src/mutant/Mutator'; export { default as MutatorFactory } from './src/mutant/MutatorFactory'; +export { default as MutatorPlugin } from './src/mutant/MutatorPlugin'; diff --git a/packages/stryker-api/report.ts b/packages/stryker-api/report.ts index a486397752..a1e07f579a 100644 --- a/packages/stryker-api/report.ts +++ b/packages/stryker-api/report.ts @@ -2,6 +2,7 @@ export { default as Reporter } from './src/report/Reporter'; export { default as MutantResult } from './src/report/MutantResult'; export { default as MutantStatus } from './src/report/MutantStatus'; export { default as ReporterFactory } from './src/report/ReporterFactory'; +export { default as ReporterPlugin } from './src/report/ReporterPlugin'; export { default as SourceFile } from './src/report/SourceFile'; export { default as MatchedMutant } from './src/report/MatchedMutant'; export { default as ScoreResult } from './src/report/ScoreResult'; 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/config/ConfigEditorPlugin.ts b/packages/stryker-api/src/config/ConfigEditorPlugin.ts new file mode 100644 index 0000000000..761c52fad2 --- /dev/null +++ b/packages/stryker-api/src/config/ConfigEditorPlugin.ts @@ -0,0 +1,6 @@ +import ConfigEditor from './ConfigEditor'; +import { InjectorKey, StrykerPlugin, PluginKind } from '../../di'; + +export default interface ConfigEditorPlugin extends StrykerPlugin { + readonly kind: PluginKind.ConfigEditor; +} 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/di/Container.ts b/packages/stryker-api/src/di/Container.ts new file mode 100644 index 0000000000..43e49f080c --- /dev/null +++ b/packages/stryker-api/src/di/Container.ts @@ -0,0 +1,19 @@ +import { LoggerFactoryMethod, Logger } from '../../logging'; +import { StrykerOptions } from '../../core'; +import PluginResolver from './PluginResolver'; +import Inject from './Inject'; +import { Config } from '../../config'; + +export default interface Container { + getLogger: LoggerFactoryMethod; + logger: Logger; + options: StrykerOptions; + produceSourceMaps: boolean; + sandboxFileNames: string[]; + pluginResolver: PluginResolver; + inject: Inject; + /** + * @deprecated This is just here to migrate between old and new plugins. Don't use this! Use `options` instead + */ + config: Config; +} diff --git a/packages/stryker-api/src/di/ContainerValues.ts b/packages/stryker-api/src/di/ContainerValues.ts index a763880a02..110e5e25b1 100644 --- a/packages/stryker-api/src/di/ContainerValues.ts +++ b/packages/stryker-api/src/di/ContainerValues.ts @@ -1,7 +1,8 @@ -import { Injections } from './Injections'; +import Container from './Container'; +import InjectorKey from './InjectorKey'; -type ContainerValues = { - [K in keyof TS]: TS[K] extends keyof Injections ? Injections[TS[K]] : never; +type ContainerValues = { + [K in keyof TS]: TS[K] extends keyof Container ? Container[TS[K]] : never; }; export default ContainerValues; diff --git a/packages/stryker-api/src/di/Inject.ts b/packages/stryker-api/src/di/Inject.ts new file mode 100644 index 0000000000..7823305f43 --- /dev/null +++ b/packages/stryker-api/src/di/Inject.ts @@ -0,0 +1,6 @@ +import InjectorKey from './InjectorKey'; +import { Injectable } from '../../di'; + +type Inject = (Constructor: Injectable) => T; + +export default Inject; diff --git a/packages/stryker-api/src/di/Injectable.ts b/packages/stryker-api/src/di/Injectable.ts index a2f22c0c95..5d54a9736b 100644 --- a/packages/stryker-api/src/di/Injectable.ts +++ b/packages/stryker-api/src/di/Injectable.ts @@ -1,9 +1,9 @@ -import InjectableKey from './InjectableKey'; +import InjectableKey from './InjectorKey'; import ContainerValues from './ContainerValues'; -interface Injectable { - new(...args: ContainerValues): T; - inject: TS; +interface Injectable { + new(...args: ContainerValues): T; + readonly inject: TArgKeys; } export default Injectable; diff --git a/packages/stryker-api/src/di/InjectableKey.ts b/packages/stryker-api/src/di/InjectableKey.ts deleted file mode 100644 index a7631704a5..0000000000 --- a/packages/stryker-api/src/di/InjectableKey.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { Injections } from './Injections'; - -type InjectableKey = keyof Injections; -export default InjectableKey; diff --git a/packages/stryker-api/src/di/Injections.ts b/packages/stryker-api/src/di/Injections.ts deleted file mode 100644 index 57beaaea99..0000000000 --- a/packages/stryker-api/src/di/Injections.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { LoggerFactoryMethod } from '../../logging'; -import { TestFrameworkSettings } from '../../test_framework'; - -export interface Injections { - testFrameworkSettings: TestFrameworkSettings; - getLogger: LoggerFactoryMethod; -} diff --git a/packages/stryker-api/src/di/InjectorKey.ts b/packages/stryker-api/src/di/InjectorKey.ts new file mode 100644 index 0000000000..7ac5009021 --- /dev/null +++ b/packages/stryker-api/src/di/InjectorKey.ts @@ -0,0 +1,4 @@ +import Container from './Container'; + +type InjectorKey = keyof Container; +export default InjectorKey; diff --git a/packages/stryker-api/src/di/PluginKind.ts b/packages/stryker-api/src/di/PluginKind.ts new file mode 100644 index 0000000000..15290b24d7 --- /dev/null +++ b/packages/stryker-api/src/di/PluginKind.ts @@ -0,0 +1,9 @@ +enum PluginKind { + ConfigEditor = 'ConfigEditor', + TestRunner = 'TestRunner', + TestFramework = 'TestFramework', + Transpiler = 'Transpiler', + Mutator = 'Mutator', + Reporter = 'Reporter' +} +export default PluginKind; diff --git a/packages/stryker-api/src/di/PluginResolver.ts b/packages/stryker-api/src/di/PluginResolver.ts new file mode 100644 index 0000000000..2bf2f4efa3 --- /dev/null +++ b/packages/stryker-api/src/di/PluginResolver.ts @@ -0,0 +1,7 @@ +import StrykerPlugin from './StrykerPlugin'; +import PluginKind from './PluginKind'; +import InjectorKey from './InjectorKey'; + +export default interface PluginResolver { + resolve(kind: PluginKind, name: string): StrykerPlugin; +} diff --git a/packages/stryker-api/src/di/StrykerPlugin.ts b/packages/stryker-api/src/di/StrykerPlugin.ts new file mode 100644 index 0000000000..ae6a8b287d --- /dev/null +++ b/packages/stryker-api/src/di/StrykerPlugin.ts @@ -0,0 +1,8 @@ +import Injectable from './Injectable'; +import InjectorKey from './InjectorKey'; +import PluginKind from './PluginKind'; + +export default interface StrykerPlugin extends Injectable { + readonly pluginName: string; + readonly kind: PluginKind; +} diff --git a/packages/stryker-api/src/di/keys.ts b/packages/stryker-api/src/di/keys.ts new file mode 100644 index 0000000000..5d91e4e9fa --- /dev/null +++ b/packages/stryker-api/src/di/keys.ts @@ -0,0 +1,5 @@ +import InjectableKey from './InjectorKey'; + +export default function keys(...keys: TS): TS { + return keys; +} diff --git a/packages/stryker-api/src/di/plugins.ts b/packages/stryker-api/src/di/plugins.ts new file mode 100644 index 0000000000..4dc5d434b3 --- /dev/null +++ b/packages/stryker-api/src/di/plugins.ts @@ -0,0 +1,10 @@ +import { StrykerPlugin as P, InjectorKey as I } from '../../di'; + +export default function plugins(plugin: P): [P]; +export default function plugins(plugin: P, plugin2: P): [P, P]; +export default function plugins(plugin: P, plugin2: P, plugin3: P): [P, P, P]; +export default function plugins(plugin: P, plugin2: P, plugin3: P, plugin4: P): [P, P, P, P]; +export default function plugins(plugin: P, plugin2: P, plugin3: P, plugin4: P, plugin5: P): [P, P, P, P, P]; +export default function plugins(...plugins: P[]) { + return plugins; +} diff --git a/packages/stryker-api/src/mutant/MutatorPlugin.ts b/packages/stryker-api/src/mutant/MutatorPlugin.ts new file mode 100644 index 0000000000..5e300eb375 --- /dev/null +++ b/packages/stryker-api/src/mutant/MutatorPlugin.ts @@ -0,0 +1,6 @@ +import Mutator from './Mutator'; +import { InjectorKey, StrykerPlugin, PluginKind } from '../../di'; + +export default interface MutatorPlugin extends StrykerPlugin { + readonly kind: PluginKind.Mutator; +} diff --git a/packages/stryker-api/src/report/ReporterPlugin.ts b/packages/stryker-api/src/report/ReporterPlugin.ts new file mode 100644 index 0000000000..2cff674f4c --- /dev/null +++ b/packages/stryker-api/src/report/ReporterPlugin.ts @@ -0,0 +1,6 @@ +import Reporter from './Reporter'; +import { InjectorKey, StrykerPlugin, PluginKind } from '../../di'; + +export default interface ReporterPlugin extends StrykerPlugin { + readonly kind: PluginKind.Reporter; +} diff --git a/packages/stryker-api/src/test_framework/TestFrameworkPlugin.ts b/packages/stryker-api/src/test_framework/TestFrameworkPlugin.ts new file mode 100644 index 0000000000..d7a1b55f69 --- /dev/null +++ b/packages/stryker-api/src/test_framework/TestFrameworkPlugin.ts @@ -0,0 +1,6 @@ +import { InjectorKey, StrykerPlugin, PluginKind } from '../../di'; +import TestFramework from './TestFramework'; + +export default interface TestFrameworkPlugin extends StrykerPlugin { + readonly kind: PluginKind.TestFramework; +} diff --git a/packages/stryker-api/src/test_runner/TestRunnerPlugin.ts b/packages/stryker-api/src/test_runner/TestRunnerPlugin.ts new file mode 100644 index 0000000000..0ca30a2087 --- /dev/null +++ b/packages/stryker-api/src/test_runner/TestRunnerPlugin.ts @@ -0,0 +1,6 @@ +import { InjectorKey, StrykerPlugin, PluginKind } from '../../di'; +import TestRunner from './TestRunner'; + +export default interface TestRunnerPlugin extends StrykerPlugin { + readonly kind: PluginKind.TestRunner; +} diff --git a/packages/stryker-api/src/transpile/TranspilerPlugin.ts b/packages/stryker-api/src/transpile/TranspilerPlugin.ts new file mode 100644 index 0000000000..5e85df0f05 --- /dev/null +++ b/packages/stryker-api/src/transpile/TranspilerPlugin.ts @@ -0,0 +1,6 @@ +import Transpiler from './Transpiler'; +import { InjectorKey, StrykerPlugin, PluginKind } from '../../di'; + +export default interface TranspilerPlugin extends StrykerPlugin { + readonly kind: PluginKind.Transpiler; +} 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_framework.ts b/packages/stryker-api/test_framework.ts index f652538cf9..2b87a73b90 100644 --- a/packages/stryker-api/test_framework.ts +++ b/packages/stryker-api/test_framework.ts @@ -1,4 +1,5 @@ export { default as TestFramework } from './src/test_framework/TestFramework'; export { default as TestSelection } from './src/test_framework/TestSelection'; export { default as TestFrameworkFactory } from './src/test_framework/TestFrameworkFactory'; +export { default as TestFrameworkPlugin } from './src/test_framework/TestFrameworkPlugin'; export { default as TestFrameworkSettings } from './src/test_framework/TestFrameworkSettings'; diff --git a/packages/stryker-api/test_runner.ts b/packages/stryker-api/test_runner.ts index a1ac887779..c14081e035 100644 --- a/packages/stryker-api/test_runner.ts +++ b/packages/stryker-api/test_runner.ts @@ -7,3 +7,4 @@ export { default as RunResult } from './src/test_runner/RunResult'; export { default as RunOptions } from './src/test_runner/RunOptions'; export { default as RunStatus } from './src/test_runner/RunStatus'; export { default as TestRunnerFactory } from './src/test_runner/TestRunnerFactory'; +export { default as TestRunnerPlugin } from './src/test_runner/TestRunnerPlugin'; diff --git a/packages/stryker-api/transpile.ts b/packages/stryker-api/transpile.ts index 846df24e1b..19d730c5a8 100644 --- a/packages/stryker-api/transpile.ts +++ b/packages/stryker-api/transpile.ts @@ -1,3 +1,4 @@ export { default as Transpiler } from './src/transpile/Transpiler'; export { default as TranspilerFactory } from './src/transpile/TranspilerFactory'; +export { default as TranspilerPlugin } from './src/transpile/TranspilerPlugin'; export { default as TranspilerOptions } from './src/transpile/TranspilerOptions'; diff --git a/packages/stryker-api/tsconfig.src.json b/packages/stryker-api/tsconfig.src.json index 908a3f39b1..f2e3fb8fb0 100644 --- a/packages/stryker-api/tsconfig.src.json +++ b/packages/stryker-api/tsconfig.src.json @@ -12,6 +12,7 @@ "report.ts", "test_framework.ts", "test_runner.ts", - "transpile.ts" + "transpile.ts", + "di.ts" ] } \ No newline at end of file diff --git a/packages/stryker-html-reporter/src/HtmlReporter.ts b/packages/stryker-html-reporter/src/HtmlReporter.ts index 559e452efb..a231aa3e98 100644 --- a/packages/stryker-html-reporter/src/HtmlReporter.ts +++ b/packages/stryker-html-reporter/src/HtmlReporter.ts @@ -1,11 +1,12 @@ import { getLogger } from 'stryker-api/logging'; import fileUrl = require('file-url'); import * as path from 'path'; -import { Config } from 'stryker-api/config'; import { Reporter, MutantResult, SourceFile, ScoreResult } from 'stryker-api/report'; import * as util from './util'; import * as templates from './templates'; import Breadcrumb from './Breadcrumb'; +import { StrykerOptions } from 'stryker-api/core'; +import { keys, PluginKind } from 'stryker-api/di'; const DEFAULT_BASE_FOLDER = path.normalize('reports/mutation/html'); export const RESOURCES_DIR_NAME = 'strykerResources'; @@ -18,9 +19,13 @@ export default class HtmlReporter implements Reporter { private files: SourceFile[]; private scoreResult: ScoreResult; - constructor(private readonly options: Config) { + constructor(private readonly options: StrykerOptions) { } + public static readonly inject = keys('options'); + public static readonly kind = PluginKind.Reporter; + public static readonly pluginName = 'html'; + public onAllSourceFilesRead(files: SourceFile[]) { this.files = files; } diff --git a/packages/stryker-html-reporter/src/index.ts b/packages/stryker-html-reporter/src/index.ts index bf4152db08..8b48aaf54c 100644 --- a/packages/stryker-html-reporter/src/index.ts +++ b/packages/stryker-html-reporter/src/index.ts @@ -1,4 +1,4 @@ -import { ReporterFactory } from 'stryker-api/report'; import HtmlReporter from './HtmlReporter'; +import { plugins } from 'stryker-api/di'; -ReporterFactory.instance().register('html', HtmlReporter); +export const strykerPlugins = plugins(HtmlReporter); diff --git a/packages/stryker-jasmine-runner/package.json b/packages/stryker-jasmine-runner/package.json index ebcd365d0c..c969d5374a 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 96f6cc7b56..6e23085d12 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 a658074b16..83793e0e64 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..4ec7eddda9 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-test-helpers/src'; 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..bef36bd4fd --- /dev/null +++ b/packages/stryker-test-helpers/package.json @@ -0,0 +1,24 @@ +{ + "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" + } +} \ No newline at end of file diff --git a/packages/stryker-test-helpers/src/factory.ts b/packages/stryker-test-helpers/src/factory.ts new file mode 100644 index 0000000000..3d084bd489 --- /dev/null +++ b/packages/stryker-test-helpers/src/factory.ts @@ -0,0 +1,168 @@ +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 const 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 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..28b669de1f --- /dev/null +++ b/packages/stryker-test-helpers/src/index.ts @@ -0,0 +1,2 @@ +import * as factory from './factory'; +export { factory }; 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..be0b4588b8 --- /dev/null +++ b/packages/stryker-test-helpers/tsconfig.src.json @@ -0,0 +1,18 @@ +{ + "extends": "../../tsconfig.settings.json", + "compilerOptions": { + "rootDir": ".", + "types": [ + "node", + "mocha" + ] + }, + "include": [ + "src" + ], + "references": [ + { + "path": "../stryker-api/tsconfig.src.json" + } + ] +} \ 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/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 index 0731fc694f..81df7348a9 100644 --- a/packages/stryker/src/ReporterOrchestrator.ts +++ b/packages/stryker/src/ReporterOrchestrator.ts @@ -1,80 +1,80 @@ -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'; +// 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(); +// 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) {} +// 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); - } +// 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 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}`); - } -} +// 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..e947af0006 100644 --- a/packages/stryker/src/Stryker.ts +++ b/packages/stryker/src/Stryker.ts @@ -1,51 +1,56 @@ -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 Injector from './di/Injector'; +import BroadcastReporter from './reporters/BroadcastReporter'; 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 = Injector.create(pluginLoader, this.config); + this.reporter = this.injector.inject(BroadcastReporter); this.testFramework = new TestFrameworkOrchestrator(this.config).determineTestFramework(); new ConfigValidator(this.config, this.testFramework).validate(); } @@ -114,13 +119,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/ChildProcessProxyWorker.ts b/packages/stryker/src/child-proxy/ChildProcessProxyWorker.ts index 837762838e..60cd942634 100644 --- a/packages/stryker/src/child-proxy/ChildProcessProxyWorker.ts +++ b/packages/stryker/src/child-proxy/ChildProcessProxyWorker.ts @@ -3,7 +3,7 @@ import { getLogger, Logger } from 'stryker-api/logging'; import { File } from 'stryker-api/core'; import { serialize, deserialize, errorToString } from '../utils/objectUtils'; 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..856c51bf4c 100644 --- a/packages/stryker/src/config/ConfigReader.ts +++ b/packages/stryker/src/config/ConfigReader.ts @@ -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/di/Injector.ts b/packages/stryker/src/di/Injector.ts new file mode 100644 index 0000000000..0c9d8f6c00 --- /dev/null +++ b/packages/stryker/src/di/Injector.ts @@ -0,0 +1,68 @@ +import { Injectable, InjectorKey, Container, PluginResolver } from 'stryker-api/di'; +import Providers, { ProviderKind, Provider } from './Providers'; +import { getLogger } from 'stryker-api/logging'; +import { Config } from 'stryker-api/config'; + +abstract class Injector { + public inject(Constructor: Injectable): T { + const args: any[] = Constructor.inject.map(key => this.resolve(key, Constructor)); + return new Constructor(...args as any); + } + + public abstract resolve(key: T, target: Function): Container[T]; + + public createChildInjector(context: Partial): Injector { + return new ChildInjector(this, context); + } + + public static create(pluginResolver: PluginResolver, options: Config): Injector { + return new RootInjector() + .createChildInjector({ + // TODO: Remove `config` once old way of loading plugins is gone + config: { kind: ProviderKind.Value, value: options }, + getLogger: { kind: ProviderKind.Value, value: getLogger }, + logger: { kind: ProviderKind.Factory, factory: Constructor => getLogger(Constructor.name) }, + options: { kind: ProviderKind.Value, value: options }, + pluginResolver: { kind: ProviderKind.Value, value: pluginResolver } + }); + } +} + +export default Injector; + +class RootInjector extends Injector { + public resolve(key: T, target: Function): Container[T] { + throw new Error(`Cannot resolve ${key} to inject into ${target.name}.`); + } +} + +class ChildInjector extends Injector { + constructor(private readonly parent: Injector, private readonly context: Partial) { + super(); + } + + public resolve(key: TKey, target: Function): Container[TKey] { + if (key === 'inject') { + return this.inject.bind(this); + } else { + + const resolver: Provider | undefined = this.context[key]; + if (resolver) { + switch (resolver.kind) { + case ProviderKind.Value: + return resolver.value; + case ProviderKind.Factory: + return resolver.factory(target); + default: + throw resolverUnsupportedError(resolver); + } + } else { + return this.parent.resolve(key, target); + } + } + } +} + +function resolverUnsupportedError(resolver: never) { + return new Error(`Resolver ${resolver} is not supported`); +} diff --git a/packages/stryker/src/di/PluginLoader.ts b/packages/stryker/src/di/PluginLoader.ts new file mode 100644 index 0000000000..eba9fa19d7 --- /dev/null +++ b/packages/stryker/src/di/PluginLoader.ts @@ -0,0 +1,151 @@ +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'; +import { StrykerPlugin, PluginKind, keys, InjectorKey, ContainerValues, PluginResolver } from 'stryker-api/di'; +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/src/mutant/MutatorFactory'; + +const IGNORED_PACKAGES = ['stryker-cli', 'stryker-api']; + +interface PluginModule { + strykerPlugins: StrykerPlugin[]; +} + +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: PluginKind, name: string): StrykerPlugin { + return this.getPlugin(kind, name); + } + + public getPlugin(kind: PluginKind, name: string): StrykerPlugin { + const plugins = this.pluginsByKind.get(kind); + if (plugins) { + const plugin = plugins.find(plugin => plugin.pluginName.toLowerCase() === name.toLowerCase()); + if (plugin) { + return plugin; + } else { + throw new Error(`Cannot load ${kind} plugin "${name}". Did you forget to install it? Loaded ${kind} plugins were: ${plugins.map(p => p.pluginName).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(), keys('config'), ([config]) => config); + this.loadDeprecatedPluginsFor(PluginKind.TestFramework, TestFrameworkFactory.instance(), keys('options'), args => ({ options: args[0] })); + this.loadDeprecatedPluginsFor(PluginKind.Transpiler, TranspilerFactory.instance(), keys('config', 'produceSourceMaps'), + ([config, produceSourceMaps]) => ({ config, produceSourceMaps })); + this.loadDeprecatedPluginsFor(PluginKind.Mutator, MutatorFactory.instance(), keys('config'), ([config]) => config); + this.loadDeprecatedPluginsFor(PluginKind.TestRunner, TestRunnerFactory.instance(), keys('options', 'sandboxFileNames'), + ([strykerOptions, fileNames]) => ({ strykerOptions, fileNames })); + } + + private loadDeprecatedPluginsFor( + kind: PluginKind, + factory: Factory, + injectionKeys: TS, + settingsFactory: (args: ContainerValues) => TSettings): void { + factory.knownNames().forEach(name => { + class ProxyPlugin { + constructor(...args: ContainerValues) { + const realPlugin = factory.create(name, settingsFactory(args)); + for (const i in realPlugin) { + const method = (realPlugin as any)[i]; + if (method === 'function') { + (this as any)[i] = method.bind(realPlugin); + } + } + } + public static pluginName = name; + public static kind = kind; + public static inject = injectionKeys; + } + this.loadPlugin(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' than the plugin directory should be 2 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: StrykerPlugin) { + 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/Providers.ts b/packages/stryker/src/di/Providers.ts new file mode 100644 index 0000000000..ab994b8682 --- /dev/null +++ b/packages/stryker/src/di/Providers.ts @@ -0,0 +1,24 @@ +import { Container } from 'stryker-api/di'; + +type Providers = { + readonly [K in keyof Container]: Provider; +}; + +export type Provider = ValueProvider | FactoryProvider; ''; + +export enum ProviderKind { + 'Value', + 'Factory' +} + +interface ValueProvider { + kind: ProviderKind.Value; + value: T; +} + +interface FactoryProvider { + kind: ProviderKind.Factory; + factory(target: Function): T; +} + +export default Providers; 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..9788a1bebf 100644 --- a/packages/stryker/src/reporters/BroadcastReporter.ts +++ b/packages/stryker/src/reporters/BroadcastReporter.ts @@ -2,33 +2,38 @@ import { Reporter, SourceFile, MutantResult, MatchedMutant, ScoreResult } from ' import { getLogger } from 'stryker-api/logging'; import { isPromise } from '../utils/objectUtils'; import StrictReporter from './StrictReporter'; - -export interface NamedReporter { - name: string; - reporter: Reporter; -} +import { keys, PluginResolver, PluginKind, Inject } from 'stryker-api/di'; +import { StrykerOptions } from 'stryker-api/core'; export default class BroadcastReporter implements StrictReporter { + public static readonly inject = keys('options', 'pluginResolver', 'inject'); private readonly log = getLogger(BroadcastReporter.name); - constructor(private readonly reporters: NamedReporter[]) { + private readonly reporters: { + [name: string]: Reporter; + }; + constructor(private readonly options: StrykerOptions, pluginResolver: PluginResolver, inject: Inject) { + this.reporters = {}; + this.options.reporters.forEach(reporterName => this.reporters[reporterName] = inject(pluginResolver.resolve(PluginKind.Reporter, reporterName))); } 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); diff --git a/packages/stryker/src/reporters/ClearTextReporter.ts b/packages/stryker/src/reporters/ClearTextReporter.ts index c566d76e44..375dd1e607 100644 --- a/packages/stryker/src/reporters/ClearTextReporter.ts +++ b/packages/stryker/src/reporters/ClearTextReporter.ts @@ -1,19 +1,23 @@ 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 { keys, PluginKind } from 'stryker-api/di'; 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 = keys('options'); + public static readonly pluginName = 'clear-text'; + public static readonly kind = PluginKind.Reporter; + 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..4c1fbfbfae 100644 --- a/packages/stryker/src/reporters/DashboardReporter.ts +++ b/packages/stryker/src/reporters/DashboardReporter.ts @@ -4,8 +4,13 @@ import {getEnvironmentVariable} from '../utils/objectUtils'; import { getLogger } from 'stryker-api/logging'; import { determineCIProvider } from './ci/Provider'; import { StrykerOptions } from 'stryker-api/core'; +import { keys, PluginKind } from 'stryker-api/di'; export default class DashboardReporter implements Reporter { + public static readonly inject = keys('options'); + public static readonly pluginName = 'dashboard'; + public static readonly kind = PluginKind.Reporter; + private readonly log = getLogger(DashboardReporter.name); private readonly ciProvider = determineCIProvider(); diff --git a/packages/stryker/src/reporters/DotsReporter.ts b/packages/stryker/src/reporters/DotsReporter.ts index f3061127ef..3076942823 100644 --- a/packages/stryker/src/reporters/DotsReporter.ts +++ b/packages/stryker/src/reporters/DotsReporter.ts @@ -1,8 +1,13 @@ import {Reporter, MutantResult, MutantStatus} from 'stryker-api/report'; import chalk from 'chalk'; import * as os from 'os'; +import { PluginKind, keys } from 'stryker-api/di'; export default class DotsReporter implements Reporter { + public static readonly inject = keys(); + public static readonly pluginName = 'dots'; + public static readonly kind = PluginKind.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..473c92a462 100644 --- a/packages/stryker/src/reporters/EventRecorderReporter.ts +++ b/packages/stryker/src/reporters/EventRecorderReporter.ts @@ -5,10 +5,14 @@ import { SourceFile, MutantResult, MatchedMutant, Reporter, ScoreResult } from ' import { cleanFolder } from '../utils/fileUtils'; import StrictReporter from './StrictReporter'; import { fsAsPromised } from '@stryker-mutator/util'; +import { PluginKind, keys } from 'stryker-api/di'; const DEFAULT_BASE_FOLDER = 'reports/mutation/events'; export default class EventRecorderReporter implements StrictReporter { + public static readonly inject = keys('options'); + public static readonly pluginName = 'event-recorder'; + public static readonly kind = PluginKind.Reporter; private readonly log = getLogger(EventRecorderReporter.name); private readonly allWork: Promise[] = []; diff --git a/packages/stryker/src/reporters/ProgressReporter.ts b/packages/stryker/src/reporters/ProgressReporter.ts index dd26aabc8c..70c55b9479 100644 --- a/packages/stryker/src/reporters/ProgressReporter.ts +++ b/packages/stryker/src/reporters/ProgressReporter.ts @@ -1,9 +1,13 @@ import { MatchedMutant, MutantResult } from 'stryker-api/report'; import ProgressKeeper from './ProgressKeeper'; import ProgressBar from './ProgressBar'; +import { keys, PluginKind } from 'stryker-api/di'; export default class ProgressBarReporter extends ProgressKeeper { private progressBar: ProgressBar; + public static readonly inject = keys(); + public static readonly pluginName = 'progress'; + public static readonly kind = PluginKind.Reporter; public onAllMutantsMatchedWithTests(matchedMutants: ReadonlyArray): void { super.onAllMutantsMatchedWithTests(matchedMutants); diff --git a/packages/stryker/src/reporters/index.ts b/packages/stryker/src/reporters/index.ts new file mode 100644 index 0000000000..1b4b519012 --- /dev/null +++ b/packages/stryker/src/reporters/index.ts @@ -0,0 +1,14 @@ +import ClearTextReporter from './ClearTextReporter'; +import ProgressReporter from './ProgressReporter'; +import DotsReporter from './DotsReporter'; +import EventRecorderReporter from './EventRecorderReporter'; +import DashboardReporter from './DashboardReporter'; +import { plugins } from 'stryker-api/di'; + +export const strykerPlugins = plugins( + ClearTextReporter, + ProgressReporter, + DotsReporter, + EventRecorderReporter, + DashboardReporter +); 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/stryker.conf.js b/packages/stryker/stryker.conf.js index b1826a2c29..5cf2aebe99 100644 --- a/packages/stryker/stryker.conf.js +++ b/packages/stryker/stryker.conf.js @@ -51,11 +51,11 @@ module.exports = function (config) { fileLogLevel: 'trace', logLevel: 'info', plugins: [ - require.resolve('../stryker-mocha-runner/src/index'), - require.resolve('../stryker-mocha-framework/src/index'), + // require.resolve('../stryker-mocha-runner/src/index'), + // require.resolve('../stryker-mocha-framework/src/index'), require.resolve('../stryker-html-reporter/src/index'), - require.resolve('../stryker-typescript/src/index'), - require.resolve('../stryker-javascript-mutator/src/index') + // require.resolve('../stryker-typescript/src/index'), + // require.resolve('../stryker-javascript-mutator/src/index') ] }); }; \ No newline at end of file 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..b168594f22 100644 --- a/packages/stryker/test/helpers/initSinon.ts +++ b/packages/stryker/test/helpers/initSinon.ts @@ -1,9 +1,5 @@ import * as sinon from 'sinon'; -beforeEach(() => { - global.sandbox = sinon.createSandbox(); -}); - afterEach(() => { - global.sandbox.restore(); + sinon.restore(); }); 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 index 272ac3bfe7..6c95ab944d 100644 --- a/packages/stryker/test/unit/ReporterOrchestratorSpec.ts +++ b/packages/stryker/test/unit/ReporterOrchestratorSpec.ts @@ -1,87 +1,87 @@ -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'; +// 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; +// 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(); - }); +// beforeEach(() => { +// sandbox = sinon.createSandbox(); +// broadcastReporterMock = sinon.stub(broadcastReporter, 'default'); +// log = currentLogMock(); +// captureTTY(); +// }); - afterEach(() => { - sandbox.restore(); - restoreTTY(); - }); +// afterEach(() => { +// sandbox.restore(); +// restoreTTY(); +// }); - it('should at least register the 5 default reporters', () => { - expect(ReporterFactory.instance().knownNames()).length.to.be.above(4); - }); +// 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' - ) - ); - }); +// 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 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; - }); - }); +// 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 captureTTY() { +// isTTY = (process.stdout as any).isTTY; +// } - function restoreTTY() { - (process.stdout as any).isTTY = isTTY; - } +// function restoreTTY() { +// (process.stdout as any).isTTY = isTTY; +// } - function setTTY(val: boolean) { - (process.stdout as any).isTTY = val; - } -}); +// 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..30e8a582db 100644 --- a/packages/stryker/test/unit/StrykerSpec.ts +++ b/packages/stryker/test/unit/StrykerSpec.ts @@ -9,20 +9,19 @@ 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 Injector from '../../src/di/Injector'; 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'; @@ -58,6 +57,7 @@ describe('Stryker', () => { let configureMainProcessStub: sinon.SinonStub; let configureLoggingServerStub: sinon.SinonStub; let shutdownLoggingStub: sinon.SinonStub; + let injectorMock: Mock; beforeEach(() => { strykerConfig = config(); @@ -66,36 +66,37 @@ describe('Stryker', () => { configReaderMock = mock(ConfigReader); configReaderMock.readConfig.returns(strykerConfig); pluginLoaderMock = mock(PluginLoader); - const reporterOrchestratorMock = mock(ReporterOrchestrator); + + injectorMock = mock(Injector); 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.inject.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(Injector, 'create').returns(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/PluginLoaderSpec.ts b/packages/stryker/test/unit/di/PluginLoaderSpec.ts similarity index 87% rename from packages/stryker/test/unit/PluginLoaderSpec.ts rename to packages/stryker/test/unit/di/PluginLoaderSpec.ts index 7ef956eecd..853c36d91a 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', () => { 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..73b354bea8 100644 --- a/packages/stryker/test/unit/input/InputFileResolverSpec.ts +++ b/packages/stryker/test/unit/input/InputFileResolverSpec.ts @@ -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..7a880bcc38 100644 --- a/packages/stryker/test/unit/reporters/BroadcastReporterSpec.ts +++ b/packages/stryker/test/unit/reporters/BroadcastReporterSpec.ts @@ -2,7 +2,10 @@ 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, Mock, strykerOptions } from '../../helpers/producers'; +import { StrykerOptions } from 'stryker-api/core'; +import { PluginResolver, PluginKind, StrykerPlugin } from 'stryker-api/di'; +import * as sinon from 'sinon'; describe('BroadcastReporter', () => { @@ -10,79 +13,111 @@ describe('BroadcastReporter', () => { let sut: any; let reporter: any; let reporter2: any; + let options: StrykerOptions; + let pluginResolver: sinon.SinonStubbedInstance; + let inject: sinon.SinonStub; beforeEach(() => { log = currentLogMock(); reporter = mockReporter(); reporter2 = mockReporter(); - sut = new BroadcastReporter([{ name: 'rep1', reporter }, { name: 'rep2', reporter: reporter2 }]); - }); - - it('should forward all events', () => { - actArrangeAssertAllEvents(); + inject = sinon.stub(); + options = strykerOptions({ + reporters: ['rep1', 'rep2'] + }); + pluginResolver = { + resolve: sinon.stub() + }; + const reporterPlugin: StrykerPlugin = class { + public static readonly pluginName = 'rep1'; + public static readonly inject: [] = []; + public static readonly kind = PluginKind.Reporter; + }; + const reporterPlugin2: StrykerPlugin = class { + public static readonly pluginName = 'rep2'; + public static readonly inject: [] = []; + public static readonly kind = PluginKind.Reporter; + }; + pluginResolver.resolve + .withArgs(PluginKind.Reporter, 'rep1').returns(reporterPlugin) + .withArgs(PluginKind.Reporter, 'rep2').returns(reporterPlugin2); + inject + .withArgs(reporterPlugin).returns(reporter) + .withArgs(reporterPlugin2).returns(reporter2); }); - describe('when "wrapUp" returns promises', () => { - let wrapUpResolveFn: Function; - let wrapUpResolveFn2: Function; - let wrapUpRejectFn: Function; - let result: Promise; - let isResolved: boolean; - + 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 = new BroadcastReporter(options, pluginResolver, inject); }); - 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; + 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); + }); + + 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(log.error).to.have.been.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 => reporter[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[eventName](); + expect(log.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()); + ALL_REPORTER_EVENTS.forEach(event => reporter[event] = sinon.stub()); return reporter; } 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..0db73087b5 100644 --- a/packages/stryker/test/unit/reporters/DashboardReporterSpec.ts +++ b/packages/stryker/test/unit/reporters/DashboardReporterSpec.ts @@ -19,8 +19,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?: { 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..cc7c305797 100644 --- a/packages/stryker/test/unit/reporters/ProgressReporterSpec.ts +++ b/packages/stryker/test/unit/reporters/ProgressReporterSpec.ts @@ -15,24 +15,19 @@ 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); + sinon.stub(progressBarModule, 'default').returns(progressBar); }); - afterEach(() => { - sandbox.restore(); - }); describe('onAllMutantsMatchedWithTests()', () => { describe('when there are 3 MatchedMutants that all contain Tests', () => { @@ -93,7 +88,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 +96,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 +104,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 +112,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..c1cc1e0114 100644 --- a/packages/stryker/test/unit/test-runner/CommandTestRunnerSpec.ts +++ b/packages/stryker/test/unit/test-runner/CommandTestRunnerSpec.ts @@ -8,6 +8,7 @@ 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'; describe(CommandTestRunner.name, () => { @@ -17,10 +18,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', () => { 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..afc937cc67 100644 --- a/packages/stryker/test/unit/transpiler/MutantTranspilerSpec.ts +++ b/packages/stryker/test/unit/transpiler/MutantTranspilerSpec.ts @@ -7,10 +7,10 @@ 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'; 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/tsconfig.json b/tsconfig.json index 2802ab8bb3..56e2bcc3c9 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -17,6 +17,7 @@ { "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" } ] } diff --git a/workspace.code-workspace b/workspace.code-workspace index 7e58fe4425..12d961e4f2 100644 --- a/workspace.code-workspace +++ b/workspace.code-workspace @@ -54,6 +54,9 @@ { "path": "packages/stryker-util" }, + { + "path": "packages/stryker-test-helpers" + }, { "path": "e2e" }, From 4137cde4991282265b8c14153b4b8a77b94c5252 Mon Sep 17 00:00:00 2001 From: Nico Jansen Date: Sun, 30 Dec 2018 23:50:35 +0100 Subject: [PATCH 03/36] Finish up the broadcast reporter migration --- packages/stryker-api/src/di/plugins.ts | 1 + packages/stryker/src/ReporterOrchestrator.ts | 80 ------------- .../src/reporters/BroadcastReporter.ts | 42 +++++-- .../reporters/ProgressAppendOnlyReporter.ts | 4 + .../stryker/src/reporters/StrictReporter.ts | 16 +-- packages/stryker/src/reporters/index.ts | 2 + packages/stryker/stryker.conf.js | 8 +- .../test/unit/ReporterOrchestratorSpec.ts | 87 -------------- .../unit/reporters/BroadcastReporterSpec.ts | 113 ++++++++++++++---- .../unit/reporters/ProgressReporterSpec.ts | 1 - 10 files changed, 138 insertions(+), 216 deletions(-) delete mode 100644 packages/stryker/src/ReporterOrchestrator.ts delete mode 100644 packages/stryker/test/unit/ReporterOrchestratorSpec.ts diff --git a/packages/stryker-api/src/di/plugins.ts b/packages/stryker-api/src/di/plugins.ts index 4dc5d434b3..7588caff76 100644 --- a/packages/stryker-api/src/di/plugins.ts +++ b/packages/stryker-api/src/di/plugins.ts @@ -5,6 +5,7 @@ export default function plugins( export default function plugins(plugin: P, plugin2: P, plugin3: P): [P, P, P]; export default function plugins(plugin: P, plugin2: P, plugin3: P, plugin4: P): [P, P, P, P]; export default function plugins(plugin: P, plugin2: P, plugin3: P, plugin4: P, plugin5: P): [P, P, P, P, P]; +export default function plugins(plugin: P, plugin2: P, plugin3: P, plugin4: P, plugin5: P, plugin6: P): [P, P, P, P, P, P]; export default function plugins(...plugins: P[]) { return plugins; } diff --git a/packages/stryker/src/ReporterOrchestrator.ts b/packages/stryker/src/ReporterOrchestrator.ts deleted file mode 100644 index 81df7348a9..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/reporters/BroadcastReporter.ts b/packages/stryker/src/reporters/BroadcastReporter.ts index 9788a1bebf..6c359993f3 100644 --- a/packages/stryker/src/reporters/BroadcastReporter.ts +++ b/packages/stryker/src/reporters/BroadcastReporter.ts @@ -1,5 +1,5 @@ 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'; import { keys, PluginResolver, PluginKind, Inject } from 'stryker-api/di'; @@ -7,14 +7,40 @@ import { StrykerOptions } from 'stryker-api/core'; export default class BroadcastReporter implements StrictReporter { - public static readonly inject = keys('options', 'pluginResolver', 'inject'); - private readonly log = getLogger(BroadcastReporter.name); - private readonly reporters: { + public static readonly inject = keys('options', 'pluginResolver', 'inject', 'logger'); + public readonly reporters: { [name: string]: Reporter; }; - constructor(private readonly options: StrykerOptions, pluginResolver: PluginResolver, inject: Inject) { + constructor( + private readonly options: StrykerOptions, + private readonly pluginResolver: PluginResolver, + private readonly inject: Inject, + private readonly log: Logger) { this.reporters = {}; - this.options.reporters.forEach(reporterName => this.reporters[reporterName] = inject(pluginResolver.resolve(PluginKind.Reporter, reporterName))); + 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'; + } + const plugin = this.pluginResolver.resolve(PluginKind.Reporter, reporterName); + this.reporters[reporterName] = this.inject(plugin); + } + + 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 { @@ -64,8 +90,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/ProgressAppendOnlyReporter.ts b/packages/stryker/src/reporters/ProgressAppendOnlyReporter.ts index 687f54ace7..9c81497594 100644 --- a/packages/stryker/src/reporters/ProgressAppendOnlyReporter.ts +++ b/packages/stryker/src/reporters/ProgressAppendOnlyReporter.ts @@ -1,9 +1,13 @@ import { MatchedMutant } from 'stryker-api/report'; import * as os from 'os'; import ProgressKeeper from './ProgressKeeper'; +import { keys, PluginKind } from 'stryker-api/di'; export default class ProgressAppendOnlyReporter extends ProgressKeeper { private intervalReference: NodeJS.Timer; + public static readonly inject = keys(); + public static readonly pluginName = 'progress-append-only'; + public static readonly kind = PluginKind.Reporter; public onAllMutantsMatchedWithTests(matchedMutants: ReadonlyArray): void { super.onAllMutantsMatchedWithTests(matchedMutants); 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/index.ts b/packages/stryker/src/reporters/index.ts index 1b4b519012..1d283b4382 100644 --- a/packages/stryker/src/reporters/index.ts +++ b/packages/stryker/src/reporters/index.ts @@ -1,5 +1,6 @@ import ClearTextReporter from './ClearTextReporter'; import ProgressReporter from './ProgressReporter'; +import ProgressAppendOnlyReporter from './ProgressAppendOnlyReporter'; import DotsReporter from './DotsReporter'; import EventRecorderReporter from './EventRecorderReporter'; import DashboardReporter from './DashboardReporter'; @@ -8,6 +9,7 @@ import { plugins } from 'stryker-api/di'; export const strykerPlugins = plugins( ClearTextReporter, ProgressReporter, + ProgressAppendOnlyReporter, DotsReporter, EventRecorderReporter, DashboardReporter diff --git a/packages/stryker/stryker.conf.js b/packages/stryker/stryker.conf.js index 5cf2aebe99..b1826a2c29 100644 --- a/packages/stryker/stryker.conf.js +++ b/packages/stryker/stryker.conf.js @@ -51,11 +51,11 @@ module.exports = function (config) { fileLogLevel: 'trace', logLevel: 'info', plugins: [ - // require.resolve('../stryker-mocha-runner/src/index'), - // require.resolve('../stryker-mocha-framework/src/index'), + require.resolve('../stryker-mocha-runner/src/index'), + require.resolve('../stryker-mocha-framework/src/index'), require.resolve('../stryker-html-reporter/src/index'), - // require.resolve('../stryker-typescript/src/index'), - // require.resolve('../stryker-javascript-mutator/src/index') + require.resolve('../stryker-typescript/src/index'), + require.resolve('../stryker-javascript-mutator/src/index') ] }); }; \ No newline at end of file diff --git a/packages/stryker/test/unit/ReporterOrchestratorSpec.ts b/packages/stryker/test/unit/ReporterOrchestratorSpec.ts deleted file mode 100644 index 6c95ab944d..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 = sinon.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/reporters/BroadcastReporterSpec.ts b/packages/stryker/test/unit/reporters/BroadcastReporterSpec.ts index 7a880bcc38..f7807be832 100644 --- a/packages/stryker/test/unit/reporters/BroadcastReporterSpec.ts +++ b/packages/stryker/test/unit/reporters/BroadcastReporterSpec.ts @@ -6,49 +6,100 @@ import { ALL_REPORTER_EVENTS, Mock, strykerOptions } from '../../helpers/produce import { StrykerOptions } from 'stryker-api/core'; import { PluginResolver, PluginKind, StrykerPlugin } from 'stryker-api/di'; import * as sinon from 'sinon'; +import ProgressAppendOnlyReporter from '../../../src/reporters/ProgressAppendOnlyReporter'; +import ProgressReporter from '../../../src/reporters/ProgressReporter'; describe('BroadcastReporter', () => { let log: Mock; - let sut: any; - let reporter: any; - let reporter2: any; + let sut: BroadcastReporter; + let rep1: any; + let rep2: any; + let isTTY: boolean; let options: StrykerOptions; let pluginResolver: sinon.SinonStubbedInstance; let inject: sinon.SinonStub; beforeEach(() => { log = currentLogMock(); - reporter = mockReporter(); - reporter2 = mockReporter(); + rep1 = mockReporter('rep1'); + rep2 = mockReporter('rep2'); inject = sinon.stub(); + captureTTY(); options = strykerOptions({ reporters: ['rep1', 'rep2'] }); pluginResolver = { resolve: sinon.stub() }; - const reporterPlugin: StrykerPlugin = class { + const rep1Plugin: StrykerPlugin = class { public static readonly pluginName = 'rep1'; public static readonly inject: [] = []; public static readonly kind = PluginKind.Reporter; }; - const reporterPlugin2: StrykerPlugin = class { + const rep2Plugin: StrykerPlugin = class { public static readonly pluginName = 'rep2'; public static readonly inject: [] = []; public static readonly kind = PluginKind.Reporter; }; pluginResolver.resolve - .withArgs(PluginKind.Reporter, 'rep1').returns(reporterPlugin) - .withArgs(PluginKind.Reporter, 'rep2').returns(reporterPlugin2); + .withArgs(PluginKind.Reporter, rep1Plugin.pluginName).returns(rep1Plugin) + .withArgs(PluginKind.Reporter, rep2Plugin.pluginName).returns(rep2Plugin); inject - .withArgs(reporterPlugin).returns(reporter) - .withArgs(reporterPlugin2).returns(reporter2); + .withArgs(rep1Plugin).returns(rep1) + .withArgs(rep2Plugin).returns(rep2); + }); + + afterEach(() => { + restoreTTY(); + }); + + 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 = mockReporter('progress-append-only'); + options.reporters = ['progress']; + pluginResolver.resolve.withArgs(PluginKind.Reporter, 'progress-append-only') + .returns(ProgressAppendOnlyReporter); + inject.withArgs(ProgressAppendOnlyReporter).returns(expectedReporter); + + // Act + sut = createSut(); + + // Assert + expect(sut.reporters).deep.eq({ 'progress-append-only': expectedReporter }); + }); + + it('should create the correct reporters', () => { + // Arrange + setTTY(true); + const expectedReporter = mockReporter('progress'); + options.reporters = ['progress', 'rep2']; + pluginResolver.resolve.withArgs(PluginKind.Reporter, 'progress') + .returns(ProgressReporter); + inject.withArgs(ProgressReporter).returns(expectedReporter); + + // Act + sut = createSut(); + + // Assert + expect(sut.reporters).deep.eq({ + progress: expectedReporter, + rep2 + }); + }); + + it('should warn if there is no reporter', () => { + options.reporters = []; + sut = createSut(); + expect(log.warn).calledWith(sinon.match('No reporter configured')); + }); }); describe('when created', () => { beforeEach(() => { - sut = new BroadcastReporter(options, pluginResolver, inject); + sut = createSut(); }); it('should forward all events', () => { @@ -64,12 +115,12 @@ describe('BroadcastReporter', () => { beforeEach(() => { isResolved = false; - reporter.wrapUp.returns(new Promise((resolve, reject) => { + rep1.wrapUp.returns(new Promise((resolve, reject) => { wrapUpResolveFn = resolve; wrapUpRejectFn = reject; })); - reporter2.wrapUp.returns(new Promise(resolve => wrapUpResolveFn2 = resolve)); - result = sut.wrapUp().then(() => isResolved = true); + rep2.wrapUp.returns(new Promise(resolve => wrapUpResolveFn2 = resolve)); + result = sut.wrapUp().then(() => void (isResolved = true)); }); it('should forward a combined promise', () => { @@ -89,7 +140,7 @@ describe('BroadcastReporter', () => { 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`); + expect(log.error).calledWith(`An error occurred during 'wrapUp' on reporter 'rep1'. Error is: some error`); }); }); }); @@ -97,7 +148,7 @@ describe('BroadcastReporter', () => { describe('with one faulty reporter', () => { beforeEach(() => { - ALL_REPORTER_EVENTS.forEach(eventName => reporter[eventName].throws('some error')); + ALL_REPORTER_EVENTS.forEach(eventName => rep1[eventName].throws('some error')); }); it('should still broadcast to other reporters', () => { @@ -106,7 +157,7 @@ describe('BroadcastReporter', () => { it('should log each error', () => { ALL_REPORTER_EVENTS.forEach(eventName => { - sut[eventName](); + (sut as any)[eventName](); expect(log.error).to.have.been.calledWith(`An error occurred during '${eventName}' on reporter 'rep1'. Error is: some error`); }); }); @@ -115,8 +166,12 @@ describe('BroadcastReporter', () => { }); - function mockReporter() { - const reporter: any = {}; + function createSut() { + return new BroadcastReporter(options, pluginResolver, inject, log); + } + + function mockReporter(name: string) { + const reporter: any = { name }; ALL_REPORTER_EVENTS.forEach(event => reporter[event] = sinon.stub()); return reporter; } @@ -124,9 +179,21 @@ describe('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]).to.have.been.calledWith(eventData); + expect(rep2[eventName]).to.have.been.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/ProgressReporterSpec.ts b/packages/stryker/test/unit/reporters/ProgressReporterSpec.ts index cc7c305797..6d2ff047fb 100644 --- a/packages/stryker/test/unit/reporters/ProgressReporterSpec.ts +++ b/packages/stryker/test/unit/reporters/ProgressReporterSpec.ts @@ -28,7 +28,6 @@ describe('ProgressReporter', () => { sinon.stub(progressBarModule, 'default').returns(progressBar); }); - describe('onAllMutantsMatchedWithTests()', () => { describe('when there are 3 MatchedMutants that all contain Tests', () => { beforeEach(() => { From 78420528372c15d0eab5ba834d279a52d8e5d962 Mon Sep 17 00:00:00 2001 From: Nico Jansen Date: Mon, 31 Dec 2018 00:06:48 +0100 Subject: [PATCH 04/36] Let the logger be injected in the HtmlReporter --- packages/stryker-html-reporter/package.json | 1 + .../stryker-html-reporter/src/HtmlReporter.ts | 7 ++--- .../test/helpers/initSinon.ts | 5 ++++ .../test/helpers/loggingMock.ts | 30 ------------------- .../test/integration/MathReportSpec.ts | 9 ++++-- .../test/integration/StrykerReportSpec.ts | 3 +- .../integration/singleFileInFolderSpec.ts | 3 +- .../stryker-html-reporter/test/ui/hooks.ts | 3 +- .../test/unit/HtmlReporter.spec.ts | 16 ++++------ .../stryker-html-reporter/tsconfig.test.json | 3 ++ packages/stryker-test-helpers/src/factory.ts | 4 +-- 11 files changed, 32 insertions(+), 52 deletions(-) create mode 100644 packages/stryker-html-reporter/test/helpers/initSinon.ts delete mode 100644 packages/stryker-html-reporter/test/helpers/loggingMock.ts diff --git a/packages/stryker-html-reporter/package.json b/packages/stryker-html-reporter/package.json index fab6923841..b894dfc374 100644 --- a/packages/stryker-html-reporter/package.json +++ b/packages/stryker-html-reporter/package.json @@ -47,6 +47,7 @@ "stryker-api": ">=0.18.0 <0.24.0" }, "devDependencies": { + "@stryker-mutator/test-helpers": "0.0.0", "@types/file-url": "~2.0.0", "@types/jsdom": "~12.2.0", "@types/node": "^10.11.5", diff --git a/packages/stryker-html-reporter/src/HtmlReporter.ts b/packages/stryker-html-reporter/src/HtmlReporter.ts index a231aa3e98..8f95e7fc8c 100644 --- a/packages/stryker-html-reporter/src/HtmlReporter.ts +++ b/packages/stryker-html-reporter/src/HtmlReporter.ts @@ -1,4 +1,4 @@ -import { getLogger } from 'stryker-api/logging'; +import { Logger } from 'stryker-api/logging'; import fileUrl = require('file-url'); import * as path from 'path'; import { Reporter, MutantResult, SourceFile, ScoreResult } from 'stryker-api/report'; @@ -12,17 +12,16 @@ const DEFAULT_BASE_FOLDER = path.normalize('reports/mutation/html'); export const RESOURCES_DIR_NAME = 'strykerResources'; export default class HtmlReporter implements Reporter { - private readonly log = getLogger(HtmlReporter.name); private _baseDir: string; private mainPromise: Promise; private mutantResults: MutantResult[]; private files: SourceFile[]; private scoreResult: ScoreResult; - constructor(private readonly options: StrykerOptions) { + constructor(private readonly options: StrykerOptions, private readonly log: Logger) { } - public static readonly inject = keys('options'); + public static readonly inject = keys('options', 'logger'); public static readonly kind = PluginKind.Reporter; public static readonly pluginName = 'html'; diff --git a/packages/stryker-html-reporter/test/helpers/initSinon.ts b/packages/stryker-html-reporter/test/helpers/initSinon.ts new file mode 100644 index 0000000000..b168594f22 --- /dev/null +++ b/packages/stryker-html-reporter/test/helpers/initSinon.ts @@ -0,0 +1,5 @@ +import * as sinon from 'sinon'; + +afterEach(() => { + sinon.restore(); +}); diff --git a/packages/stryker-html-reporter/test/helpers/loggingMock.ts b/packages/stryker-html-reporter/test/helpers/loggingMock.ts deleted file mode 100644 index 7cec9895fb..0000000000 --- a/packages/stryker-html-reporter/test/helpers/loggingMock.ts +++ /dev/null @@ -1,30 +0,0 @@ -import * as logging from 'stryker-api/logging'; -import * as sinon from 'sinon'; - -const logger = { - 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() -}; - -sinon.stub(logging, 'getLogger').returns(logger); - -beforeEach(() => { - logger.trace.reset(); - logger.debug.reset(); - logger.info.reset(); - logger.warn.reset(); - logger.error.reset(); - logger.fatal.reset(); -}); - -export default logger; diff --git a/packages/stryker-html-reporter/test/integration/MathReportSpec.ts b/packages/stryker-html-reporter/test/integration/MathReportSpec.ts index def3a1cf4e..81ce738857 100644 --- a/packages/stryker-html-reporter/test/integration/MathReportSpec.ts +++ b/packages/stryker-html-reporter/test/integration/MathReportSpec.ts @@ -2,19 +2,22 @@ import * as fs from 'fs'; import * as path from 'path'; import { expect } from 'chai'; import { Config } from 'stryker-api/config'; -import logger from '../helpers/loggingMock'; import EventPlayer from '../helpers/EventPlayer'; import fileUrl = require('file-url'); import HtmlReporter from '../../src/HtmlReporter'; +import { factory } from '@stryker-mutator/test-helpers'; +import { Logger } from 'stryker-api/logging'; describe('HtmlReporter with example math project', () => { let sut: HtmlReporter; const baseDir = 'reports/mutation/math'; + let logger: sinon.SinonStubbedInstance; beforeEach(() => { const config = new Config(); + logger = factory.logger(); config.set({ htmlReporter: { baseDir } }); - sut = new HtmlReporter(config); + sut = new HtmlReporter(config, logger); return new EventPlayer(path.join('testResources', 'mathEvents')) .replay(sut) .then(() => sut.wrapUp()); @@ -34,7 +37,7 @@ describe('HtmlReporter with example math project', () => { beforeEach(() => { const config = new Config(); config.set({ htmlReporter: { baseDir } }); - sut = new HtmlReporter(config); + sut = new HtmlReporter(config, factory.logger()); sut.onAllSourceFilesRead([]); sut.onAllMutantsTested([]); return sut.wrapUp(); diff --git a/packages/stryker-html-reporter/test/integration/StrykerReportSpec.ts b/packages/stryker-html-reporter/test/integration/StrykerReportSpec.ts index 6fe00036e5..46875fc55f 100644 --- a/packages/stryker-html-reporter/test/integration/StrykerReportSpec.ts +++ b/packages/stryker-html-reporter/test/integration/StrykerReportSpec.ts @@ -3,6 +3,7 @@ import { Config } from 'stryker-api/config'; import HtmlReporter from '../../src/HtmlReporter'; import EventPlayer from '../helpers/EventPlayer'; import { readDirectoryTree } from '../helpers/fsHelpers'; +import { factory } from '@stryker-mutator/test-helpers'; const REPORT_DIR = 'reports/mutation/stryker'; @@ -12,7 +13,7 @@ describe('Html report of stryker', () => { beforeEach(() => { const config = new Config(); config.set({ htmlReporter: { baseDir: REPORT_DIR } }); - sut = new HtmlReporter(config); + sut = new HtmlReporter(config, factory.logger()); return new EventPlayer('testResources/strykerEvents') .replay(sut) .then(() => sut.wrapUp()); diff --git a/packages/stryker-html-reporter/test/integration/singleFileInFolderSpec.ts b/packages/stryker-html-reporter/test/integration/singleFileInFolderSpec.ts index 4d3d8fd9cd..4b3e7adc48 100644 --- a/packages/stryker-html-reporter/test/integration/singleFileInFolderSpec.ts +++ b/packages/stryker-html-reporter/test/integration/singleFileInFolderSpec.ts @@ -5,6 +5,7 @@ import EventPlayer from '../helpers/EventPlayer'; import HtmlReporter from '../../src/HtmlReporter'; import { readDirectoryTree } from '../helpers/fsHelpers'; import * as fs from 'fs'; +import { factory } from '@stryker-mutator/test-helpers'; const REPORT_DIR = 'reports/mutation/singleFileInFolder'; @@ -14,7 +15,7 @@ describe('HtmlReporter single file in a folder', () => { beforeEach(() => { const config = new Config(); config.set({ htmlReporter: { baseDir: REPORT_DIR } }); - sut = new HtmlReporter(config); + sut = new HtmlReporter(config, factory.logger()); return new EventPlayer(path.join('testResources', 'singleFileInFolder')) .replay(sut) .then(() => sut.wrapUp()); diff --git a/packages/stryker-html-reporter/test/ui/hooks.ts b/packages/stryker-html-reporter/test/ui/hooks.ts index 9edded3dd8..8dccdbdb54 100644 --- a/packages/stryker-html-reporter/test/ui/hooks.ts +++ b/packages/stryker-html-reporter/test/ui/hooks.ts @@ -3,6 +3,7 @@ import { browser } from 'protractor'; import { Config } from 'stryker-api/config'; import HtmlReporter from '../../src/HtmlReporter'; import EventPlayer from '../helpers/EventPlayer'; +import { factory } from '@stryker-mutator/test-helpers'; export const baseDir = path.join(__dirname, '../../reports/mutation/uiTest'); @@ -10,7 +11,7 @@ before(() => { browser.ignoreSynchronization = true; const config = new Config(); config.set({ htmlReporter: { baseDir } }); - const reporter = new HtmlReporter(config); + const reporter = new HtmlReporter(config, factory.logger()); return new EventPlayer('testResources/mathEvents') .replay(reporter) .then(() => reporter.wrapUp()); diff --git a/packages/stryker-html-reporter/test/unit/HtmlReporter.spec.ts b/packages/stryker-html-reporter/test/unit/HtmlReporter.spec.ts index c738455da2..1d536c35dd 100644 --- a/packages/stryker-html-reporter/test/unit/HtmlReporter.spec.ts +++ b/packages/stryker-html-reporter/test/unit/HtmlReporter.spec.ts @@ -1,13 +1,12 @@ import { normalize, join } from 'path'; import { expect } from 'chai'; import * as sinon from 'sinon'; -import { Config } from 'stryker-api/config'; import * as util from '../../src/util'; import HtmlReporter from '../../src/HtmlReporter'; import { sourceFile, mutantResult, scoreResult } from '../helpers/producers'; +import { factory } from '@stryker-mutator/test-helpers'; describe('HtmlReporter', () => { - let sandbox: sinon.SinonSandbox; let copyFolderStub: sinon.SinonStub; let writeFileStub: sinon.SinonStub; let mkdirStub: sinon.SinonStub; @@ -15,16 +14,13 @@ describe('HtmlReporter', () => { let sut: HtmlReporter; beforeEach(() => { - sandbox = sinon.createSandbox(); - copyFolderStub = sandbox.stub(util, 'copyFolder'); - writeFileStub = sandbox.stub(util, 'writeFile'); - deleteDirStub = sandbox.stub(util, 'deleteDir'); - mkdirStub = sandbox.stub(util, 'mkdir'); - sut = new HtmlReporter(new Config()); + copyFolderStub = sinon.stub(util, 'copyFolder'); + writeFileStub = sinon.stub(util, 'writeFile'); + deleteDirStub = sinon.stub(util, 'deleteDir'); + mkdirStub = sinon.stub(util, 'mkdir'); + sut = new HtmlReporter(factory.strykerOptions(), factory.logger()); }); - afterEach(() => sandbox.restore()); - describe('when in happy flow', () => { beforeEach(() => { diff --git a/packages/stryker-html-reporter/tsconfig.test.json b/packages/stryker-html-reporter/tsconfig.test.json index 9fb06c74c0..da121922f8 100644 --- a/packages/stryker-html-reporter/tsconfig.test.json +++ b/packages/stryker-html-reporter/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/stryker-test-helpers/src/factory.ts b/packages/stryker-test-helpers/src/factory.ts index 3d084bd489..f71ccc432f 100644 --- a/packages/stryker-test-helpers/src/factory.ts +++ b/packages/stryker-test-helpers/src/factory.ts @@ -42,7 +42,7 @@ export const mutant = factoryMethod(() => ({ replacement: 'replacement' })); -export const logger = (): sinon.SinonStubbedInstance => { +export function logger(): sinon.SinonStubbedInstance { return { debug: sinon.stub(), error: sinon.stub(), @@ -57,7 +57,7 @@ export const logger = (): sinon.SinonStubbedInstance => { trace: sinon.stub(), warn: sinon.stub() }; -}; +} export const testFramework = factoryMethod(() => ({ beforeEach(codeFragment: string) { return `beforeEach(){ ${codeFragment}}`; }, From c265402c6302a02559c99510aa2ab57d81cb5a79 Mon Sep 17 00:00:00 2001 From: Nico Jansen Date: Mon, 31 Dec 2018 12:35:14 +0100 Subject: [PATCH 05/36] test(TestInjector): Implement and use TestInjector The TestInjector is a new way of creating your System Under Test (sut) objects. You can configure what to inject and then call `TestInjector.inject(SutClass)` --- .../test/helpers/initSinon.ts | 2 + ...athReportSpec.ts => MathReport.it.spec.ts} | 14 ++--- ...ReportSpec.ts => StrykerReport.it.spec.ts} | 8 +-- ...rSpec.ts => singleFileInFolder.it.spec.ts} | 8 +-- .../test/unit/HtmlReporter.spec.ts | 4 +- .../stryker-test-helpers/src/TestInjector.ts | 50 +++++++++++++++++ packages/stryker-test-helpers/src/index.ts | 3 +- packages/stryker/package.json | 1 + packages/stryker/test/helpers/initSinon.ts | 2 + .../unit/reporters/BroadcastReporterSpec.ts | 55 +++++++------------ packages/stryker/tsconfig.test.json | 3 + 11 files changed, 93 insertions(+), 57 deletions(-) rename packages/stryker-html-reporter/test/integration/{MathReportSpec.ts => MathReport.it.spec.ts} (77%) rename packages/stryker-html-reporter/test/integration/{StrykerReportSpec.ts => StrykerReport.it.spec.ts} (95%) rename packages/stryker-html-reporter/test/integration/{singleFileInFolderSpec.ts => singleFileInFolder.it.spec.ts} (80%) create mode 100644 packages/stryker-test-helpers/src/TestInjector.ts diff --git a/packages/stryker-html-reporter/test/helpers/initSinon.ts b/packages/stryker-html-reporter/test/helpers/initSinon.ts index b168594f22..718bc2ab34 100644 --- a/packages/stryker-html-reporter/test/helpers/initSinon.ts +++ b/packages/stryker-html-reporter/test/helpers/initSinon.ts @@ -1,5 +1,7 @@ import * as sinon from 'sinon'; +import { TestInjector } from '@stryker-mutator/test-helpers'; afterEach(() => { sinon.restore(); + TestInjector.reset(); }); diff --git a/packages/stryker-html-reporter/test/integration/MathReportSpec.ts b/packages/stryker-html-reporter/test/integration/MathReport.it.spec.ts similarity index 77% rename from packages/stryker-html-reporter/test/integration/MathReportSpec.ts rename to packages/stryker-html-reporter/test/integration/MathReport.it.spec.ts index 81ce738857..bc8746f294 100644 --- a/packages/stryker-html-reporter/test/integration/MathReportSpec.ts +++ b/packages/stryker-html-reporter/test/integration/MathReport.it.spec.ts @@ -5,19 +5,15 @@ import { Config } from 'stryker-api/config'; import EventPlayer from '../helpers/EventPlayer'; import fileUrl = require('file-url'); import HtmlReporter from '../../src/HtmlReporter'; -import { factory } from '@stryker-mutator/test-helpers'; -import { Logger } from 'stryker-api/logging'; +import { TestInjector } from '@stryker-mutator/test-helpers'; describe('HtmlReporter with example math project', () => { let sut: HtmlReporter; const baseDir = 'reports/mutation/math'; - let logger: sinon.SinonStubbedInstance; beforeEach(() => { - const config = new Config(); - logger = factory.logger(); - config.set({ htmlReporter: { baseDir } }); - sut = new HtmlReporter(config, logger); + TestInjector.options.htmlReporter = { baseDir }; + sut = TestInjector.inject(HtmlReporter); return new EventPlayer(path.join('testResources', 'mathEvents')) .replay(sut) .then(() => sut.wrapUp()); @@ -30,14 +26,14 @@ describe('HtmlReporter with example math project', () => { }); it('should output a log message with a link to the HTML report', () => { - expect(logger.info).to.have.been.calledWith(`Your report can be found at: ${fileUrl(baseDir + '/index.html')}`); + expect(TestInjector.logger.info).to.have.been.calledWith(`Your report can be found at: ${fileUrl(baseDir + '/index.html')}`); }); describe('when initiated a second time with empty events', () => { beforeEach(() => { const config = new Config(); config.set({ htmlReporter: { baseDir } }); - sut = new HtmlReporter(config, factory.logger()); + sut = TestInjector.inject(HtmlReporter); sut.onAllSourceFilesRead([]); sut.onAllMutantsTested([]); return sut.wrapUp(); diff --git a/packages/stryker-html-reporter/test/integration/StrykerReportSpec.ts b/packages/stryker-html-reporter/test/integration/StrykerReport.it.spec.ts similarity index 95% rename from packages/stryker-html-reporter/test/integration/StrykerReportSpec.ts rename to packages/stryker-html-reporter/test/integration/StrykerReport.it.spec.ts index 46875fc55f..857eac0d01 100644 --- a/packages/stryker-html-reporter/test/integration/StrykerReportSpec.ts +++ b/packages/stryker-html-reporter/test/integration/StrykerReport.it.spec.ts @@ -1,9 +1,8 @@ import { expect } from 'chai'; -import { Config } from 'stryker-api/config'; import HtmlReporter from '../../src/HtmlReporter'; import EventPlayer from '../helpers/EventPlayer'; import { readDirectoryTree } from '../helpers/fsHelpers'; -import { factory } from '@stryker-mutator/test-helpers'; +import { TestInjector } from '@stryker-mutator/test-helpers'; const REPORT_DIR = 'reports/mutation/stryker'; @@ -11,9 +10,8 @@ describe('Html report of stryker', () => { let sut: HtmlReporter; beforeEach(() => { - const config = new Config(); - config.set({ htmlReporter: { baseDir: REPORT_DIR } }); - sut = new HtmlReporter(config, factory.logger()); + TestInjector.options.htmlReporter = { baseDir: REPORT_DIR }; + sut = TestInjector.inject(HtmlReporter); return new EventPlayer('testResources/strykerEvents') .replay(sut) .then(() => sut.wrapUp()); diff --git a/packages/stryker-html-reporter/test/integration/singleFileInFolderSpec.ts b/packages/stryker-html-reporter/test/integration/singleFileInFolder.it.spec.ts similarity index 80% rename from packages/stryker-html-reporter/test/integration/singleFileInFolderSpec.ts rename to packages/stryker-html-reporter/test/integration/singleFileInFolder.it.spec.ts index 4b3e7adc48..96cc40d681 100644 --- a/packages/stryker-html-reporter/test/integration/singleFileInFolderSpec.ts +++ b/packages/stryker-html-reporter/test/integration/singleFileInFolder.it.spec.ts @@ -1,11 +1,10 @@ import * as path from 'path'; import { expect } from 'chai'; -import { Config } from 'stryker-api/config'; import EventPlayer from '../helpers/EventPlayer'; import HtmlReporter from '../../src/HtmlReporter'; import { readDirectoryTree } from '../helpers/fsHelpers'; import * as fs from 'fs'; -import { factory } from '@stryker-mutator/test-helpers'; +import { TestInjector } from '@stryker-mutator/test-helpers'; const REPORT_DIR = 'reports/mutation/singleFileInFolder'; @@ -13,9 +12,8 @@ describe('HtmlReporter single file in a folder', () => { let sut: HtmlReporter; beforeEach(() => { - const config = new Config(); - config.set({ htmlReporter: { baseDir: REPORT_DIR } }); - sut = new HtmlReporter(config, factory.logger()); + TestInjector.options.htmlReporter = { baseDir: REPORT_DIR }; + sut = TestInjector.inject(HtmlReporter); return new EventPlayer(path.join('testResources', 'singleFileInFolder')) .replay(sut) .then(() => sut.wrapUp()); diff --git a/packages/stryker-html-reporter/test/unit/HtmlReporter.spec.ts b/packages/stryker-html-reporter/test/unit/HtmlReporter.spec.ts index 1d536c35dd..9ba0091814 100644 --- a/packages/stryker-html-reporter/test/unit/HtmlReporter.spec.ts +++ b/packages/stryker-html-reporter/test/unit/HtmlReporter.spec.ts @@ -4,7 +4,7 @@ import * as sinon from 'sinon'; import * as util from '../../src/util'; import HtmlReporter from '../../src/HtmlReporter'; import { sourceFile, mutantResult, scoreResult } from '../helpers/producers'; -import { factory } from '@stryker-mutator/test-helpers'; +import { TestInjector } from '@stryker-mutator/test-helpers'; describe('HtmlReporter', () => { let copyFolderStub: sinon.SinonStub; @@ -18,7 +18,7 @@ describe('HtmlReporter', () => { writeFileStub = sinon.stub(util, 'writeFile'); deleteDirStub = sinon.stub(util, 'deleteDir'); mkdirStub = sinon.stub(util, 'mkdir'); - sut = new HtmlReporter(factory.strykerOptions(), factory.logger()); + sut = TestInjector.inject(HtmlReporter); }); describe('when in happy flow', () => { diff --git a/packages/stryker-test-helpers/src/TestInjector.ts b/packages/stryker-test-helpers/src/TestInjector.ts new file mode 100644 index 0000000000..c3b1471d92 --- /dev/null +++ b/packages/stryker-test-helpers/src/TestInjector.ts @@ -0,0 +1,50 @@ +import { Injectable, InjectorKey, PluginResolver } from 'stryker-api/di'; +import { StrykerOptions } from 'stryker-api/core'; +import { Logger } from 'stryker-api/logging'; +import { logger, strykerOptions } from './factory'; +import * as sinon from 'sinon'; + +export default class TestInjector { + public static options: Partial = {}; + public static logger: sinon.SinonStubbedInstance; + public static pluginResolver: sinon.SinonStubbedInstance; + public static replacements: Map, any> = new Map(); + + public static reset() { + this.options = {}; + this.logger = logger(); + this.pluginResolver = { + resolve: sinon.stub() + }; + this.replacements.clear(); + } + + public static stub(Injectable: Injectable, replacement: T): typeof TestInjector { + this.replacements.set(Injectable, replacement); + return this; + } + + public static inject(Injectable: Injectable): T { + if (this.replacements.has(Injectable)) { + return this.replacements.get(Injectable); + } else { + const args: any = Injectable.inject.map(key => { + switch (key) { + case 'options': + return strykerOptions(this.options); + case 'logger': + return this.logger; + case 'pluginResolver': + return this.pluginResolver; + case 'inject': + return this.inject.bind(this); + default: + throw new Error(`Injecting "${key}" is not (yet) supported by the ${TestInjector.name}`); + } + }); + return new Injectable(...args); + } + } +} + +TestInjector.reset(); diff --git a/packages/stryker-test-helpers/src/index.ts b/packages/stryker-test-helpers/src/index.ts index 28b669de1f..662e92c82e 100644 --- a/packages/stryker-test-helpers/src/index.ts +++ b/packages/stryker-test-helpers/src/index.ts @@ -1,2 +1,3 @@ import * as factory from './factory'; -export { factory }; +import TestInjector from './TestInjector'; +export { factory, TestInjector }; diff --git a/packages/stryker/package.json b/packages/stryker/package.json index e450d48d9b..61d5a1d008 100644 --- a/packages/stryker/package.json +++ b/packages/stryker/package.json @@ -74,6 +74,7 @@ "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/test/helpers/initSinon.ts b/packages/stryker/test/helpers/initSinon.ts index b168594f22..c765bbb8bf 100644 --- a/packages/stryker/test/helpers/initSinon.ts +++ b/packages/stryker/test/helpers/initSinon.ts @@ -1,5 +1,7 @@ import * as sinon from 'sinon'; +import { TestInjector } from '../../../stryker-test-helpers/src'; afterEach(() => { sinon.restore(); + TestInjector.reset(); }); diff --git a/packages/stryker/test/unit/reporters/BroadcastReporterSpec.ts b/packages/stryker/test/unit/reporters/BroadcastReporterSpec.ts index f7807be832..75170bc54d 100644 --- a/packages/stryker/test/unit/reporters/BroadcastReporterSpec.ts +++ b/packages/stryker/test/unit/reporters/BroadcastReporterSpec.ts @@ -1,37 +1,24 @@ -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, strykerOptions } from '../../helpers/producers'; -import { StrykerOptions } from 'stryker-api/core'; -import { PluginResolver, PluginKind, StrykerPlugin } from 'stryker-api/di'; +import { ALL_REPORTER_EVENTS } from '../../helpers/producers'; +import { PluginKind, StrykerPlugin } from 'stryker-api/di'; import * as sinon from 'sinon'; import ProgressAppendOnlyReporter from '../../../src/reporters/ProgressAppendOnlyReporter'; import ProgressReporter from '../../../src/reporters/ProgressReporter'; +import { TestInjector } from '@stryker-mutator/test-helpers'; -describe('BroadcastReporter', () => { +describe.only('BroadcastReporter', () => { - let log: Mock; let sut: BroadcastReporter; let rep1: any; let rep2: any; let isTTY: boolean; - let options: StrykerOptions; - let pluginResolver: sinon.SinonStubbedInstance; - let inject: sinon.SinonStub; beforeEach(() => { - log = currentLogMock(); rep1 = mockReporter('rep1'); rep2 = mockReporter('rep2'); - inject = sinon.stub(); captureTTY(); - options = strykerOptions({ - reporters: ['rep1', 'rep2'] - }); - pluginResolver = { - resolve: sinon.stub() - }; + TestInjector.options.reporters = ['rep1', 'rep2']; const rep1Plugin: StrykerPlugin = class { public static readonly pluginName = 'rep1'; public static readonly inject: [] = []; @@ -42,12 +29,12 @@ describe('BroadcastReporter', () => { public static readonly inject: [] = []; public static readonly kind = PluginKind.Reporter; }; - pluginResolver.resolve + TestInjector.pluginResolver.resolve .withArgs(PluginKind.Reporter, rep1Plugin.pluginName).returns(rep1Plugin) .withArgs(PluginKind.Reporter, rep2Plugin.pluginName).returns(rep2Plugin); - inject - .withArgs(rep1Plugin).returns(rep1) - .withArgs(rep2Plugin).returns(rep2); + TestInjector + .stub(rep1Plugin, rep1) + .stub(rep2Plugin, rep2); }); afterEach(() => { @@ -59,10 +46,9 @@ describe('BroadcastReporter', () => { // Arrange setTTY(false); const expectedReporter = mockReporter('progress-append-only'); - options.reporters = ['progress']; - pluginResolver.resolve.withArgs(PluginKind.Reporter, 'progress-append-only') - .returns(ProgressAppendOnlyReporter); - inject.withArgs(ProgressAppendOnlyReporter).returns(expectedReporter); + TestInjector.options.reporters = ['progress']; + TestInjector.pluginResolver.resolve.withArgs(PluginKind.Reporter, 'progress-append-only').returns(ProgressAppendOnlyReporter); + TestInjector.stub(ProgressAppendOnlyReporter, expectedReporter); // Act sut = createSut(); @@ -75,10 +61,9 @@ describe('BroadcastReporter', () => { // Arrange setTTY(true); const expectedReporter = mockReporter('progress'); - options.reporters = ['progress', 'rep2']; - pluginResolver.resolve.withArgs(PluginKind.Reporter, 'progress') - .returns(ProgressReporter); - inject.withArgs(ProgressReporter).returns(expectedReporter); + TestInjector.options.reporters = ['progress', 'rep2']; + TestInjector.pluginResolver.resolve.withArgs(PluginKind.Reporter, 'progress').returns(ProgressReporter); + TestInjector.stub(ProgressReporter, expectedReporter); // Act sut = createSut(); @@ -91,9 +76,9 @@ describe('BroadcastReporter', () => { }); it('should warn if there is no reporter', () => { - options.reporters = []; + TestInjector.options.reporters = []; sut = createSut(); - expect(log.warn).calledWith(sinon.match('No reporter configured')); + expect(TestInjector.logger.warn).calledWith(sinon.match('No reporter configured')); }); }); @@ -140,7 +125,7 @@ describe('BroadcastReporter', () => { it('should not result in a rejection', () => result); it('should log the error', () => { - expect(log.error).calledWith(`An error occurred during 'wrapUp' on reporter 'rep1'. Error is: some error`); + expect(TestInjector.logger.error).calledWith(`An error occurred during 'wrapUp' on reporter 'rep1'. Error is: some error`); }); }); }); @@ -158,7 +143,7 @@ describe('BroadcastReporter', () => { it('should log each error', () => { ALL_REPORTER_EVENTS.forEach(eventName => { (sut as any)[eventName](); - expect(log.error).to.have.been.calledWith(`An error occurred during '${eventName}' on reporter 'rep1'. Error is: some error`); + expect(TestInjector.logger.error).to.have.been.calledWith(`An error occurred during '${eventName}' on reporter 'rep1'. Error is: some error`); }); }); @@ -167,7 +152,7 @@ describe('BroadcastReporter', () => { }); function createSut() { - return new BroadcastReporter(options, pluginResolver, inject, log); + return TestInjector.inject(BroadcastReporter); } function mockReporter(name: string) { 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 From f4cd5d8e6a0cea336d8a186341ca685e40be717e Mon Sep 17 00:00:00 2001 From: Nico Jansen Date: Tue, 1 Jan 2019 22:00:54 +0100 Subject: [PATCH 06/36] Add the ability to inject values into a tree of dependencies. Also added some unit tests for the Injector class. --- packages/stryker-api/di.ts | 8 +- .../src/config/ConfigEditorPlugin.ts | 4 +- packages/stryker-api/src/di/Container.ts | 2 +- .../stryker-api/src/di/ContainerValues.ts | 8 -- .../stryker-api/src/di/CorrespondingType.ts | 6 + .../stryker-api/src/di/CorrespondingTypes.ts | 8 ++ packages/stryker-api/src/di/Inject.ts | 4 +- packages/stryker-api/src/di/Injectable.ts | 10 +- packages/stryker-api/src/di/InjectionToken.ts | 9 ++ packages/stryker-api/src/di/InjectorKey.ts | 4 - packages/stryker-api/src/di/PluginResolver.ts | 4 +- packages/stryker-api/src/di/StrykerPlugin.ts | 4 +- packages/stryker-api/src/di/keys.ts | 5 - packages/stryker-api/src/di/plugins.ts | 2 +- packages/stryker-api/src/di/token.ts | 6 + packages/stryker-api/src/di/tokens.ts | 5 + .../stryker-api/src/mutant/MutatorPlugin.ts | 4 +- .../stryker-api/src/report/ReporterPlugin.ts | 4 +- .../src/test_framework/TestFrameworkPlugin.ts | 4 +- .../src/test_runner/TestRunnerPlugin.ts | 4 +- .../src/transpile/TranspilerPlugin.ts | 4 +- .../stryker-html-reporter/src/HtmlReporter.ts | 4 +- .../stryker-test-helpers/src/TestInjector.ts | 6 +- packages/stryker/src/di/Injector.ts | 60 ++++---- packages/stryker/src/di/PluginLoader.ts | 28 ++-- packages/stryker/src/di/Providers.ts | 17 +-- .../src/reporters/BroadcastReporter.ts | 4 +- .../src/reporters/ClearTextReporter.ts | 4 +- .../src/reporters/DashboardReporter.ts | 4 +- .../stryker/src/reporters/DotsReporter.ts | 4 +- .../src/reporters/EventRecorderReporter.ts | 4 +- .../reporters/ProgressAppendOnlyReporter.ts | 4 +- .../stryker/src/reporters/ProgressReporter.ts | 4 +- .../stryker/test/unit/di/Injector.spec.ts | 134 ++++++++++++++++++ .../unit/reporters/BroadcastReporterSpec.ts | 2 +- 35 files changed, 267 insertions(+), 121 deletions(-) delete mode 100644 packages/stryker-api/src/di/ContainerValues.ts create mode 100644 packages/stryker-api/src/di/CorrespondingType.ts create mode 100644 packages/stryker-api/src/di/CorrespondingTypes.ts create mode 100644 packages/stryker-api/src/di/InjectionToken.ts delete mode 100644 packages/stryker-api/src/di/InjectorKey.ts delete mode 100644 packages/stryker-api/src/di/keys.ts create mode 100644 packages/stryker-api/src/di/token.ts create mode 100644 packages/stryker-api/src/di/tokens.ts create mode 100644 packages/stryker/test/unit/di/Injector.spec.ts diff --git a/packages/stryker-api/di.ts b/packages/stryker-api/di.ts index aeb85b2bf6..6d5b6a1498 100644 --- a/packages/stryker-api/di.ts +++ b/packages/stryker-api/di.ts @@ -1,10 +1,12 @@ -export { default as ContainerValues } from './src/di/ContainerValues'; export { default as Injectable } from './src/di/Injectable'; -export { default as InjectorKey } from './src/di/InjectorKey'; +export { default as InjectionToken } from './src/di/InjectionToken'; export { default as Container } from './src/di/Container'; -export { default as keys } from './src/di/keys'; +export { default as tokens } from './src/di/tokens'; +export { default as token } from './src/di/token'; export { default as plugins } from './src/di/plugins'; export { default as StrykerPlugin } from './src/di/StrykerPlugin'; export { default as PluginKind } from './src/di/PluginKind'; export { default as PluginResolver } from './src/di/PluginResolver'; export { default as Inject } from './src/di/Inject'; +export { default as CorrespondingType } from './src/di/CorrespondingType'; +export { default as CorrespondingTypes } from './src/di/CorrespondingTypes'; diff --git a/packages/stryker-api/src/config/ConfigEditorPlugin.ts b/packages/stryker-api/src/config/ConfigEditorPlugin.ts index 761c52fad2..fc577428a6 100644 --- a/packages/stryker-api/src/config/ConfigEditorPlugin.ts +++ b/packages/stryker-api/src/config/ConfigEditorPlugin.ts @@ -1,6 +1,6 @@ import ConfigEditor from './ConfigEditor'; -import { InjectorKey, StrykerPlugin, PluginKind } from '../../di'; +import { InjectionToken, StrykerPlugin, PluginKind } from '../../di'; -export default interface ConfigEditorPlugin extends StrykerPlugin { +export default interface ConfigEditorPlugin extends StrykerPlugin { readonly kind: PluginKind.ConfigEditor; } diff --git a/packages/stryker-api/src/di/Container.ts b/packages/stryker-api/src/di/Container.ts index 43e49f080c..41c14dda91 100644 --- a/packages/stryker-api/src/di/Container.ts +++ b/packages/stryker-api/src/di/Container.ts @@ -9,7 +9,7 @@ export default interface Container { logger: Logger; options: StrykerOptions; produceSourceMaps: boolean; - sandboxFileNames: string[]; + sandboxFileNames: ReadonlyArray; pluginResolver: PluginResolver; inject: Inject; /** diff --git a/packages/stryker-api/src/di/ContainerValues.ts b/packages/stryker-api/src/di/ContainerValues.ts deleted file mode 100644 index 110e5e25b1..0000000000 --- a/packages/stryker-api/src/di/ContainerValues.ts +++ /dev/null @@ -1,8 +0,0 @@ -import Container from './Container'; -import InjectorKey from './InjectorKey'; - -type ContainerValues = { - [K in keyof TS]: TS[K] extends keyof Container ? Container[TS[K]] : never; -}; - -export default ContainerValues; diff --git a/packages/stryker-api/src/di/CorrespondingType.ts b/packages/stryker-api/src/di/CorrespondingType.ts new file mode 100644 index 0000000000..31f2390c95 --- /dev/null +++ b/packages/stryker-api/src/di/CorrespondingType.ts @@ -0,0 +1,6 @@ +import { InjectionToken, Container } from '../../di'; + +type CorrespondingType = + T extends keyof Container ? Container[T] : T extends new (...args: any[]) => any ? InstanceType : never; + +export default CorrespondingType; diff --git a/packages/stryker-api/src/di/CorrespondingTypes.ts b/packages/stryker-api/src/di/CorrespondingTypes.ts new file mode 100644 index 0000000000..721866e68a --- /dev/null +++ b/packages/stryker-api/src/di/CorrespondingTypes.ts @@ -0,0 +1,8 @@ +import InjectionToken from './InjectionToken'; +import CorrespondingType from './CorrespondingType'; + +type ContainerValues = { + [K in keyof TS]: TS[K] extends InjectionToken ? CorrespondingType : never; +}; + +export default ContainerValues; diff --git a/packages/stryker-api/src/di/Inject.ts b/packages/stryker-api/src/di/Inject.ts index 7823305f43..cd9c6a817d 100644 --- a/packages/stryker-api/src/di/Inject.ts +++ b/packages/stryker-api/src/di/Inject.ts @@ -1,6 +1,6 @@ -import InjectorKey from './InjectorKey'; +import InjectionToken from './InjectionToken'; import { Injectable } from '../../di'; -type Inject = (Constructor: Injectable) => T; +type Inject = (Constructor: Injectable) => T; export default Inject; diff --git a/packages/stryker-api/src/di/Injectable.ts b/packages/stryker-api/src/di/Injectable.ts index 5d54a9736b..f7ecf884bd 100644 --- a/packages/stryker-api/src/di/Injectable.ts +++ b/packages/stryker-api/src/di/Injectable.ts @@ -1,9 +1,9 @@ -import InjectableKey from './InjectorKey'; -import ContainerValues from './ContainerValues'; +import InjectionToken from './InjectionToken'; +import CorrespondingTypes from './CorrespondingTypes'; -interface Injectable { - new(...args: ContainerValues): T; - readonly inject: TArgKeys; +interface Injectable { + new(...args: CorrespondingTypes): T; + readonly inject: Tokens; } export default Injectable; diff --git a/packages/stryker-api/src/di/InjectionToken.ts b/packages/stryker-api/src/di/InjectionToken.ts new file mode 100644 index 0000000000..30c6ebd05c --- /dev/null +++ b/packages/stryker-api/src/di/InjectionToken.ts @@ -0,0 +1,9 @@ +import Container from './Container'; +import { Injectable } from '../../di'; + +type InjectionToken = keyof Container | Injectable; +export default InjectionToken; + +export function token(injectable: Injectable): InjectionToken { + return injectable; +} diff --git a/packages/stryker-api/src/di/InjectorKey.ts b/packages/stryker-api/src/di/InjectorKey.ts deleted file mode 100644 index 7ac5009021..0000000000 --- a/packages/stryker-api/src/di/InjectorKey.ts +++ /dev/null @@ -1,4 +0,0 @@ -import Container from './Container'; - -type InjectorKey = keyof Container; -export default InjectorKey; diff --git a/packages/stryker-api/src/di/PluginResolver.ts b/packages/stryker-api/src/di/PluginResolver.ts index 2bf2f4efa3..54dacdc24f 100644 --- a/packages/stryker-api/src/di/PluginResolver.ts +++ b/packages/stryker-api/src/di/PluginResolver.ts @@ -1,7 +1,7 @@ import StrykerPlugin from './StrykerPlugin'; import PluginKind from './PluginKind'; -import InjectorKey from './InjectorKey'; +import InjectionToken from './InjectionToken'; export default interface PluginResolver { - resolve(kind: PluginKind, name: string): StrykerPlugin; + resolve(kind: PluginKind, name: string): StrykerPlugin; } diff --git a/packages/stryker-api/src/di/StrykerPlugin.ts b/packages/stryker-api/src/di/StrykerPlugin.ts index ae6a8b287d..3da6081406 100644 --- a/packages/stryker-api/src/di/StrykerPlugin.ts +++ b/packages/stryker-api/src/di/StrykerPlugin.ts @@ -1,8 +1,8 @@ import Injectable from './Injectable'; -import InjectorKey from './InjectorKey'; +import InjectionToken from './InjectionToken'; import PluginKind from './PluginKind'; -export default interface StrykerPlugin extends Injectable { +export default interface StrykerPlugin extends Injectable { readonly pluginName: string; readonly kind: PluginKind; } diff --git a/packages/stryker-api/src/di/keys.ts b/packages/stryker-api/src/di/keys.ts deleted file mode 100644 index 5d91e4e9fa..0000000000 --- a/packages/stryker-api/src/di/keys.ts +++ /dev/null @@ -1,5 +0,0 @@ -import InjectableKey from './InjectorKey'; - -export default function keys(...keys: TS): TS { - return keys; -} diff --git a/packages/stryker-api/src/di/plugins.ts b/packages/stryker-api/src/di/plugins.ts index 7588caff76..06018f7043 100644 --- a/packages/stryker-api/src/di/plugins.ts +++ b/packages/stryker-api/src/di/plugins.ts @@ -1,4 +1,4 @@ -import { StrykerPlugin as P, InjectorKey as I } from '../../di'; +import { StrykerPlugin as P, InjectionToken as I } from '../../di'; export default function plugins(plugin: P): [P]; export default function plugins(plugin: P, plugin2: P): [P, P]; diff --git a/packages/stryker-api/src/di/token.ts b/packages/stryker-api/src/di/token.ts new file mode 100644 index 0000000000..93fb45bb1f --- /dev/null +++ b/packages/stryker-api/src/di/token.ts @@ -0,0 +1,6 @@ +import InjectionToken from './InjectionToken'; +import Injectable from './Injectable'; + +export default function token(injectable: Injectable): InjectionToken { + return injectable; +} diff --git a/packages/stryker-api/src/di/tokens.ts b/packages/stryker-api/src/di/tokens.ts new file mode 100644 index 0000000000..dd1c33075b --- /dev/null +++ b/packages/stryker-api/src/di/tokens.ts @@ -0,0 +1,5 @@ +import InjectionToken from './InjectionToken'; + +export default function tokens(...tokens: TS): TS { + return tokens; +} diff --git a/packages/stryker-api/src/mutant/MutatorPlugin.ts b/packages/stryker-api/src/mutant/MutatorPlugin.ts index 5e300eb375..3324ff9906 100644 --- a/packages/stryker-api/src/mutant/MutatorPlugin.ts +++ b/packages/stryker-api/src/mutant/MutatorPlugin.ts @@ -1,6 +1,6 @@ import Mutator from './Mutator'; -import { InjectorKey, StrykerPlugin, PluginKind } from '../../di'; +import { InjectionToken, StrykerPlugin, PluginKind } from '../../di'; -export default interface MutatorPlugin extends StrykerPlugin { +export default interface MutatorPlugin extends StrykerPlugin { readonly kind: PluginKind.Mutator; } diff --git a/packages/stryker-api/src/report/ReporterPlugin.ts b/packages/stryker-api/src/report/ReporterPlugin.ts index 2cff674f4c..51802e029d 100644 --- a/packages/stryker-api/src/report/ReporterPlugin.ts +++ b/packages/stryker-api/src/report/ReporterPlugin.ts @@ -1,6 +1,6 @@ import Reporter from './Reporter'; -import { InjectorKey, StrykerPlugin, PluginKind } from '../../di'; +import { InjectionToken, StrykerPlugin, PluginKind } from '../../di'; -export default interface ReporterPlugin extends StrykerPlugin { +export default interface ReporterPlugin extends StrykerPlugin { readonly kind: PluginKind.Reporter; } diff --git a/packages/stryker-api/src/test_framework/TestFrameworkPlugin.ts b/packages/stryker-api/src/test_framework/TestFrameworkPlugin.ts index d7a1b55f69..b7200af2be 100644 --- a/packages/stryker-api/src/test_framework/TestFrameworkPlugin.ts +++ b/packages/stryker-api/src/test_framework/TestFrameworkPlugin.ts @@ -1,6 +1,6 @@ -import { InjectorKey, StrykerPlugin, PluginKind } from '../../di'; +import { InjectionToken, StrykerPlugin, PluginKind } from '../../di'; import TestFramework from './TestFramework'; -export default interface TestFrameworkPlugin extends StrykerPlugin { +export default interface TestFrameworkPlugin extends StrykerPlugin { readonly kind: PluginKind.TestFramework; } diff --git a/packages/stryker-api/src/test_runner/TestRunnerPlugin.ts b/packages/stryker-api/src/test_runner/TestRunnerPlugin.ts index 0ca30a2087..b711805214 100644 --- a/packages/stryker-api/src/test_runner/TestRunnerPlugin.ts +++ b/packages/stryker-api/src/test_runner/TestRunnerPlugin.ts @@ -1,6 +1,6 @@ -import { InjectorKey, StrykerPlugin, PluginKind } from '../../di'; +import { InjectionToken, StrykerPlugin, PluginKind } from '../../di'; import TestRunner from './TestRunner'; -export default interface TestRunnerPlugin extends StrykerPlugin { +export default interface TestRunnerPlugin extends StrykerPlugin { readonly kind: PluginKind.TestRunner; } diff --git a/packages/stryker-api/src/transpile/TranspilerPlugin.ts b/packages/stryker-api/src/transpile/TranspilerPlugin.ts index 5e85df0f05..d49879dc8c 100644 --- a/packages/stryker-api/src/transpile/TranspilerPlugin.ts +++ b/packages/stryker-api/src/transpile/TranspilerPlugin.ts @@ -1,6 +1,6 @@ import Transpiler from './Transpiler'; -import { InjectorKey, StrykerPlugin, PluginKind } from '../../di'; +import { InjectionToken, StrykerPlugin, PluginKind } from '../../di'; -export default interface TranspilerPlugin extends StrykerPlugin { +export default interface TranspilerPlugin extends StrykerPlugin { readonly kind: PluginKind.Transpiler; } diff --git a/packages/stryker-html-reporter/src/HtmlReporter.ts b/packages/stryker-html-reporter/src/HtmlReporter.ts index 8f95e7fc8c..063cbdf256 100644 --- a/packages/stryker-html-reporter/src/HtmlReporter.ts +++ b/packages/stryker-html-reporter/src/HtmlReporter.ts @@ -6,7 +6,7 @@ import * as util from './util'; import * as templates from './templates'; import Breadcrumb from './Breadcrumb'; import { StrykerOptions } from 'stryker-api/core'; -import { keys, PluginKind } from 'stryker-api/di'; +import { tokens, PluginKind } from 'stryker-api/di'; const DEFAULT_BASE_FOLDER = path.normalize('reports/mutation/html'); export const RESOURCES_DIR_NAME = 'strykerResources'; @@ -21,7 +21,7 @@ export default class HtmlReporter implements Reporter { constructor(private readonly options: StrykerOptions, private readonly log: Logger) { } - public static readonly inject = keys('options', 'logger'); + public static readonly inject = tokens('options', 'logger'); public static readonly kind = PluginKind.Reporter; public static readonly pluginName = 'html'; diff --git a/packages/stryker-test-helpers/src/TestInjector.ts b/packages/stryker-test-helpers/src/TestInjector.ts index c3b1471d92..fc74db590a 100644 --- a/packages/stryker-test-helpers/src/TestInjector.ts +++ b/packages/stryker-test-helpers/src/TestInjector.ts @@ -1,4 +1,4 @@ -import { Injectable, InjectorKey, PluginResolver } from 'stryker-api/di'; +import { Injectable, InjectionToken, PluginResolver } from 'stryker-api/di'; import { StrykerOptions } from 'stryker-api/core'; import { Logger } from 'stryker-api/logging'; import { logger, strykerOptions } from './factory'; @@ -19,12 +19,12 @@ export default class TestInjector { this.replacements.clear(); } - public static stub(Injectable: Injectable, replacement: T): typeof TestInjector { + public static stub(Injectable: Injectable, replacement: T): typeof TestInjector { this.replacements.set(Injectable, replacement); return this; } - public static inject(Injectable: Injectable): T { + public static inject(Injectable: Injectable): T { if (this.replacements.has(Injectable)) { return this.replacements.get(Injectable); } else { diff --git a/packages/stryker/src/di/Injector.ts b/packages/stryker/src/di/Injector.ts index 0c9d8f6c00..b1f5651cae 100644 --- a/packages/stryker/src/di/Injector.ts +++ b/packages/stryker/src/di/Injector.ts @@ -1,15 +1,15 @@ -import { Injectable, InjectorKey, Container, PluginResolver } from 'stryker-api/di'; -import Providers, { ProviderKind, Provider } from './Providers'; +import { Injectable, InjectionToken, Container, PluginResolver, CorrespondingType } from 'stryker-api/di'; +import { Providers, Provider, FactoryProvider } from './Providers'; import { getLogger } from 'stryker-api/logging'; import { Config } from 'stryker-api/config'; abstract class Injector { - public inject(Constructor: Injectable): T { - const args: any[] = Constructor.inject.map(key => this.resolve(key, Constructor)); - return new Constructor(...args as any); + public inject(injectable: Injectable): T { + const args: any[] = injectable.inject.map(key => this.provide(key, injectable)); + return new injectable(...args as any); } - public abstract resolve(key: T, target: Function): Container[T]; + public abstract provide(key: T, target: Injectable): CorrespondingType; public createChildInjector(context: Partial): Injector { return new ChildInjector(this, context); @@ -19,11 +19,11 @@ abstract class Injector { return new RootInjector() .createChildInjector({ // TODO: Remove `config` once old way of loading plugins is gone - config: { kind: ProviderKind.Value, value: options }, - getLogger: { kind: ProviderKind.Value, value: getLogger }, - logger: { kind: ProviderKind.Factory, factory: Constructor => getLogger(Constructor.name) }, - options: { kind: ProviderKind.Value, value: options }, - pluginResolver: { kind: ProviderKind.Value, value: pluginResolver } + config: { value: options }, + getLogger: { value: getLogger }, + logger: { factory: Constructor => getLogger(Constructor.name) }, + options: { value: options }, + pluginResolver: { value: pluginResolver } }); } } @@ -31,8 +31,8 @@ abstract class Injector { export default Injector; class RootInjector extends Injector { - public resolve(key: T, target: Function): Container[T] { - throw new Error(`Cannot resolve ${key} to inject into ${target.name}.`); + public provide(key: T, target: Injectable): CorrespondingType { + throw new Error(`Can not inject "${target.name}". No provider found for "${key}".`); } } @@ -41,28 +41,30 @@ class ChildInjector extends Injector { super(); } - public resolve(key: TKey, target: Function): Container[TKey] { - if (key === 'inject') { + public provide(token: TToken, target: Injectable): CorrespondingType { + if (token === 'inject') { return this.inject.bind(this); + } else if (typeof token === 'string') { + return this.provideStringToken(token as any, target); } else { + return this.inject(token as any); + } + } - const resolver: Provider | undefined = this.context[key]; - if (resolver) { - switch (resolver.kind) { - case ProviderKind.Value: - return resolver.value; - case ProviderKind.Factory: - return resolver.factory(target); - default: - throw resolverUnsupportedError(resolver); - } + private provideStringToken(token: T, target: Injectable) { + const provider: Provider> | undefined = this.context[token] as any; + if (provider) { + if (this.isFactoryProvider(provider)) { + return provider.factory(target); } else { - return this.parent.resolve(key, target); + return provider.value; } + } else { + return this.parent.provide(token, target); } } -} -function resolverUnsupportedError(resolver: never) { - return new Error(`Resolver ${resolver} is not supported`); + private isFactoryProvider(provider: Provider): provider is FactoryProvider { + return !!(provider as FactoryProvider).factory; + } } diff --git a/packages/stryker/src/di/PluginLoader.ts b/packages/stryker/src/di/PluginLoader.ts index eba9fa19d7..92c14dedab 100644 --- a/packages/stryker/src/di/PluginLoader.ts +++ b/packages/stryker/src/di/PluginLoader.ts @@ -3,7 +3,7 @@ import { getLogger } from 'stryker-api/logging'; import * as _ from 'lodash'; import { importModule } from '../utils/fileUtils'; import { fsAsPromised } from '@stryker-mutator/util'; -import { StrykerPlugin, PluginKind, keys, InjectorKey, ContainerValues, PluginResolver } from 'stryker-api/di'; +import { StrykerPlugin, PluginKind, tokens as tokens, InjectionToken, PluginResolver, CorrespondingTypes } from 'stryker-api/di'; import { ConfigEditorFactory } from 'stryker-api/config'; import { Factory } from 'stryker-api/core'; import { ReporterFactory } from 'stryker-api/report'; @@ -29,11 +29,11 @@ export default class PluginLoader implements PluginResolver { this.loadDeprecatedPlugins(); } - public resolve(kind: PluginKind, name: string): StrykerPlugin { + public resolve(kind: PluginKind, name: string): StrykerPlugin { return this.getPlugin(kind, name); } - public getPlugin(kind: PluginKind, name: string): StrykerPlugin { + public getPlugin(kind: PluginKind, name: string): StrykerPlugin { const plugins = this.pluginsByKind.get(kind); if (plugins) { const plugin = plugins.find(plugin => plugin.pluginName.toLowerCase() === name.toLowerCase()); @@ -49,23 +49,23 @@ export default class PluginLoader implements PluginResolver { private loadDeprecatedPlugins() { this.loadDeprecatedPluginsFor(PluginKind.ConfigEditor, ConfigEditorFactory.instance(), [], () => undefined); - this.loadDeprecatedPluginsFor(PluginKind.Reporter, ReporterFactory.instance(), keys('config'), ([config]) => config); - this.loadDeprecatedPluginsFor(PluginKind.TestFramework, TestFrameworkFactory.instance(), keys('options'), args => ({ options: args[0] })); - this.loadDeprecatedPluginsFor(PluginKind.Transpiler, TranspilerFactory.instance(), keys('config', 'produceSourceMaps'), + 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(), keys('config'), ([config]) => config); - this.loadDeprecatedPluginsFor(PluginKind.TestRunner, TestRunnerFactory.instance(), keys('options', 'sandboxFileNames'), + 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( + private loadDeprecatedPluginsFor( kind: PluginKind, factory: Factory, - injectionKeys: TS, - settingsFactory: (args: ContainerValues) => TSettings): void { + injectionTokens: TS, + settingsFactory: (args: CorrespondingTypes) => TSettings): void { factory.knownNames().forEach(name => { class ProxyPlugin { - constructor(...args: ContainerValues) { + constructor(...args: CorrespondingTypes) { const realPlugin = factory.create(name, settingsFactory(args)); for (const i in realPlugin) { const method = (realPlugin as any)[i]; @@ -76,7 +76,7 @@ export default class PluginLoader implements PluginResolver { } public static pluginName = name; public static kind = kind; - public static inject = injectionKeys; + public static inject = injectionTokens; } this.loadPlugin(ProxyPlugin); }); @@ -135,7 +135,7 @@ export default class PluginLoader implements PluginResolver { } } - private loadPlugin(plugin: StrykerPlugin) { + private loadPlugin(plugin: StrykerPlugin) { let plugins = this.pluginsByKind.get(plugin.kind); if (!plugins) { plugins = []; diff --git a/packages/stryker/src/di/Providers.ts b/packages/stryker/src/di/Providers.ts index ab994b8682..64f3e8cd71 100644 --- a/packages/stryker/src/di/Providers.ts +++ b/packages/stryker/src/di/Providers.ts @@ -1,24 +1,15 @@ import { Container } from 'stryker-api/di'; -type Providers = { +export type Providers = { readonly [K in keyof Container]: Provider; }; -export type Provider = ValueProvider | FactoryProvider; ''; +export type Provider = ValueProvider | FactoryProvider; -export enum ProviderKind { - 'Value', - 'Factory' -} - -interface ValueProvider { - kind: ProviderKind.Value; +export interface ValueProvider { value: T; } -interface FactoryProvider { - kind: ProviderKind.Factory; +export interface FactoryProvider { factory(target: Function): T; } - -export default Providers; diff --git a/packages/stryker/src/reporters/BroadcastReporter.ts b/packages/stryker/src/reporters/BroadcastReporter.ts index 6c359993f3..c686a808c2 100644 --- a/packages/stryker/src/reporters/BroadcastReporter.ts +++ b/packages/stryker/src/reporters/BroadcastReporter.ts @@ -2,12 +2,12 @@ import { Reporter, SourceFile, MutantResult, MatchedMutant, ScoreResult } from ' import { Logger } from 'stryker-api/logging'; import { isPromise } from '../utils/objectUtils'; import StrictReporter from './StrictReporter'; -import { keys, PluginResolver, PluginKind, Inject } from 'stryker-api/di'; +import { tokens, PluginResolver, PluginKind, Inject } from 'stryker-api/di'; import { StrykerOptions } from 'stryker-api/core'; export default class BroadcastReporter implements StrictReporter { - public static readonly inject = keys('options', 'pluginResolver', 'inject', 'logger'); + public static readonly inject = tokens('options', 'pluginResolver', 'inject', 'logger'); public readonly reporters: { [name: string]: Reporter; }; diff --git a/packages/stryker/src/reporters/ClearTextReporter.ts b/packages/stryker/src/reporters/ClearTextReporter.ts index 375dd1e607..ad28d64058 100644 --- a/packages/stryker/src/reporters/ClearTextReporter.ts +++ b/packages/stryker/src/reporters/ClearTextReporter.ts @@ -4,7 +4,7 @@ import { Reporter, MutantResult, MutantStatus, ScoreResult } from 'stryker-api/r import { Position, StrykerOptions } from 'stryker-api/core'; import ClearTextScoreTable from './ClearTextScoreTable'; import * as os from 'os'; -import { keys, PluginKind } from 'stryker-api/di'; +import { tokens, PluginKind } from 'stryker-api/di'; export default class ClearTextReporter implements Reporter { @@ -14,7 +14,7 @@ export default class ClearTextReporter implements Reporter { this.configConsoleColor(); } - public static readonly inject = keys('options'); + public static readonly inject = tokens('options'); public static readonly pluginName = 'clear-text'; public static readonly kind = PluginKind.Reporter; diff --git a/packages/stryker/src/reporters/DashboardReporter.ts b/packages/stryker/src/reporters/DashboardReporter.ts index 4c1fbfbfae..b70acc7717 100644 --- a/packages/stryker/src/reporters/DashboardReporter.ts +++ b/packages/stryker/src/reporters/DashboardReporter.ts @@ -4,10 +4,10 @@ import {getEnvironmentVariable} from '../utils/objectUtils'; import { getLogger } from 'stryker-api/logging'; import { determineCIProvider } from './ci/Provider'; import { StrykerOptions } from 'stryker-api/core'; -import { keys, PluginKind } from 'stryker-api/di'; +import { tokens, PluginKind } from 'stryker-api/di'; export default class DashboardReporter implements Reporter { - public static readonly inject = keys('options'); + public static readonly inject = tokens('options'); public static readonly pluginName = 'dashboard'; public static readonly kind = PluginKind.Reporter; diff --git a/packages/stryker/src/reporters/DotsReporter.ts b/packages/stryker/src/reporters/DotsReporter.ts index 3076942823..d7205074b5 100644 --- a/packages/stryker/src/reporters/DotsReporter.ts +++ b/packages/stryker/src/reporters/DotsReporter.ts @@ -1,10 +1,10 @@ import {Reporter, MutantResult, MutantStatus} from 'stryker-api/report'; import chalk from 'chalk'; import * as os from 'os'; -import { PluginKind, keys } from 'stryker-api/di'; +import { PluginKind, tokens } from 'stryker-api/di'; export default class DotsReporter implements Reporter { - public static readonly inject = keys(); + public static readonly inject = tokens(); public static readonly pluginName = 'dots'; public static readonly kind = PluginKind.Reporter; diff --git a/packages/stryker/src/reporters/EventRecorderReporter.ts b/packages/stryker/src/reporters/EventRecorderReporter.ts index 473c92a462..7a34cfcd96 100644 --- a/packages/stryker/src/reporters/EventRecorderReporter.ts +++ b/packages/stryker/src/reporters/EventRecorderReporter.ts @@ -5,12 +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 { PluginKind, keys } from 'stryker-api/di'; +import { PluginKind, tokens } from 'stryker-api/di'; const DEFAULT_BASE_FOLDER = 'reports/mutation/events'; export default class EventRecorderReporter implements StrictReporter { - public static readonly inject = keys('options'); + public static readonly inject = tokens('options'); public static readonly pluginName = 'event-recorder'; public static readonly kind = PluginKind.Reporter; diff --git a/packages/stryker/src/reporters/ProgressAppendOnlyReporter.ts b/packages/stryker/src/reporters/ProgressAppendOnlyReporter.ts index 9c81497594..d2c9955a27 100644 --- a/packages/stryker/src/reporters/ProgressAppendOnlyReporter.ts +++ b/packages/stryker/src/reporters/ProgressAppendOnlyReporter.ts @@ -1,11 +1,11 @@ import { MatchedMutant } from 'stryker-api/report'; import * as os from 'os'; import ProgressKeeper from './ProgressKeeper'; -import { keys, PluginKind } from 'stryker-api/di'; +import { tokens, PluginKind } from 'stryker-api/di'; export default class ProgressAppendOnlyReporter extends ProgressKeeper { private intervalReference: NodeJS.Timer; - public static readonly inject = keys(); + public static readonly inject = tokens(); public static readonly pluginName = 'progress-append-only'; public static readonly kind = PluginKind.Reporter; diff --git a/packages/stryker/src/reporters/ProgressReporter.ts b/packages/stryker/src/reporters/ProgressReporter.ts index 70c55b9479..4e0e2be0ec 100644 --- a/packages/stryker/src/reporters/ProgressReporter.ts +++ b/packages/stryker/src/reporters/ProgressReporter.ts @@ -1,11 +1,11 @@ import { MatchedMutant, MutantResult } from 'stryker-api/report'; import ProgressKeeper from './ProgressKeeper'; import ProgressBar from './ProgressBar'; -import { keys, PluginKind } from 'stryker-api/di'; +import { tokens, PluginKind } from 'stryker-api/di'; export default class ProgressBarReporter extends ProgressKeeper { private progressBar: ProgressBar; - public static readonly inject = keys(); + public static readonly inject = tokens(); public static readonly pluginName = 'progress'; public static readonly kind = PluginKind.Reporter; diff --git a/packages/stryker/test/unit/di/Injector.spec.ts b/packages/stryker/test/unit/di/Injector.spec.ts new file mode 100644 index 0000000000..7f5b956fdf --- /dev/null +++ b/packages/stryker/test/unit/di/Injector.spec.ts @@ -0,0 +1,134 @@ +import { tokens, PluginResolver, token } from 'stryker-api/di'; +import { Config } from 'stryker-api/config'; +import { Logger, LoggerFactoryMethod, getLogger } from 'stryker-api/logging'; +import Injector from '../../../src/di/Injector'; +import { StrykerOptions } from 'stryker-api/core'; +import * as sinon from 'sinon'; +import { expect } from 'chai'; +import currentLogMock from '../../helpers/logMock'; +import { strykerOptions } from '../../helpers/producers'; + +describe('Injector', () => { + + let pluginResolver: PluginResolver; + let config: Config; + let sut: Injector; + + beforeEach(() => { + pluginResolver = { + resolve: sinon.stub() + }; + config = new Config(); + sut = Injector.create(pluginResolver, config); + }); + + describe('RootInjector', () => { + + it('should be able to inject config, log, getLogger, options and pluginResolver class', () => { + // Arrange + class Injectable { + constructor( + public readonly config: Config, + public readonly log: Logger, + public readonly getLogger: LoggerFactoryMethod, + public readonly options: StrykerOptions, + public readonly pluginResolver: PluginResolver) { + } + public static inject = tokens('config', 'logger', 'getLogger', 'options', 'pluginResolver'); + } + + // Act + const actual = sut.inject(Injectable); + + // Assert + expect(actual.config).eq(config); + expect(actual.options).eq(config); + expect(actual.log).eq(currentLogMock()); + expect(actual.getLogger).eq(getLogger); + expect(actual.pluginResolver).eq(pluginResolver); + }); + + it('should throw when no provider was found', () => { + expect(() => sut.inject(class FileNamesInjectable { + constructor(public fileNames: ReadonlyArray) { + } + public static inject = tokens('sandboxFileNames'); + })).throws('Can not inject "FileNamesInjectable". No provider found for "sandboxFileNames".'); + }); + }); + + describe('ChildInjector', () => { + + it('should be able to inject testFileNames', () => { + const expectedFileNames = Object.freeze(['foo.js', 'bar.js']); + const child = sut.createChildInjector({ + sandboxFileNames: { + value: expectedFileNames + } + }); + const actual = child.inject(class { + constructor(public sandboxFileNames: ReadonlyArray) { } + public static inject = tokens('sandboxFileNames'); + }); + expect(actual.sandboxFileNames).eq(expectedFileNames); + }); + + it('should be able still provide parent injector values', () => { + const expectedOptions = strykerOptions(); + const child = sut.createChildInjector({ + options: { + value: expectedOptions + } + }); + const actual = child.inject(class { + constructor(public options: StrykerOptions, public getLogger: LoggerFactoryMethod) { } + public static inject = tokens('options', 'getLogger'); + }); + expect(actual.options).eq(expectedOptions); + expect(actual.getLogger).eq(getLogger); + }); + }); + + it('should be able to inject a dependency tree', () => { + // Arrange + 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', token(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(token(Child1), token(Child2), 'logger'); + } + + // Act + const actual = sut.inject(Parent); + + // Assert + expect(actual.child.bar).eq('foo'); + expect(actual.child2.foo).eq('bar'); + expect(actual.child.log).eq(currentLogMock()); + expect(actual.child2.log).eq(currentLogMock()); + expect(actual.child.grandchild.log).eq(currentLogMock()); + expect(actual.child.grandchild.baz).eq('qux'); + expect(actual.log).eq(currentLogMock()); + }); + +}); diff --git a/packages/stryker/test/unit/reporters/BroadcastReporterSpec.ts b/packages/stryker/test/unit/reporters/BroadcastReporterSpec.ts index 75170bc54d..8838b269c2 100644 --- a/packages/stryker/test/unit/reporters/BroadcastReporterSpec.ts +++ b/packages/stryker/test/unit/reporters/BroadcastReporterSpec.ts @@ -7,7 +7,7 @@ import ProgressAppendOnlyReporter from '../../../src/reporters/ProgressAppendOnl import ProgressReporter from '../../../src/reporters/ProgressReporter'; import { TestInjector } from '@stryker-mutator/test-helpers'; -describe.only('BroadcastReporter', () => { +describe('BroadcastReporter', () => { let sut: BroadcastReporter; let rep1: any; From 50a01a67ca2bbd30bdd0a81253f5f0975d7a52ec Mon Sep 17 00:00:00 2001 From: Nico Jansen Date: Wed, 2 Jan 2019 17:19:36 +0100 Subject: [PATCH 07/36] Improve HTML reporter typings --- .../stryker-html-reporter/src/HtmlReporter.ts | 15 ++++++++------- .../stryker-html-reporter/tsconfig.settings.json | 2 ++ 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/packages/stryker-html-reporter/src/HtmlReporter.ts b/packages/stryker-html-reporter/src/HtmlReporter.ts index 063cbdf256..c008d9017d 100644 --- a/packages/stryker-html-reporter/src/HtmlReporter.ts +++ b/packages/stryker-html-reporter/src/HtmlReporter.ts @@ -12,16 +12,17 @@ const DEFAULT_BASE_FOLDER = path.normalize('reports/mutation/html'); export const RESOURCES_DIR_NAME = 'strykerResources'; export default class HtmlReporter implements Reporter { - private _baseDir: string; - private mainPromise: Promise; - private mutantResults: MutantResult[]; - private files: SourceFile[]; - private scoreResult: ScoreResult; + private _baseDir!: string; + private mainPromise!: Promise; + private mutantResults!: MutantResult[]; + private files!: SourceFile[]; + private scoreResult!: ScoreResult; constructor(private readonly options: StrykerOptions, private readonly log: Logger) { } public static readonly inject = tokens('options', 'logger'); + public static readonly kind = PluginKind.Reporter; public static readonly pluginName = 'html'; @@ -69,7 +70,7 @@ export default class HtmlReporter implements Reporter { if (child.representsFile) { return this.writeReportFile(child, currentDirectory, breadcrumb.add(child.name, util.countPathSep(child.name))); } else { - return this.writeReportDirectory(child, path.join(currentDirectory, child.name), breadcrumb.add(child.name, util.countPathSep(child.name) + 1)) + return this.writeReportDirectory(child, path.join(currentDirectory, child.name), breadcrumb.add(child.name, util.countPathSep(child.name) + 1)) .then(_ => void 0); } })); @@ -96,7 +97,7 @@ export default class HtmlReporter implements Reporter { return path.join(this.baseDir, RESOURCES_DIR_NAME); } - private get baseDir() { + private get baseDir(): string { if (!this._baseDir) { if (this.options.htmlReporter && this.options.htmlReporter.baseDir) { this._baseDir = this.options.htmlReporter.baseDir; diff --git a/packages/stryker-html-reporter/tsconfig.settings.json b/packages/stryker-html-reporter/tsconfig.settings.json index 84b63e5ffc..dbf001740d 100644 --- a/packages/stryker-html-reporter/tsconfig.settings.json +++ b/packages/stryker-html-reporter/tsconfig.settings.json @@ -10,6 +10,8 @@ "es2015.symbol.wellknown" ], "noImplicitReturns": false, + "strictPropertyInitialization": true, + "strictNullChecks": true, "jsx": "react", "jsxFactory": "typedHtml.createElement" } From 964748462d3c07626fbc0f101f904dabad12ab6a Mon Sep 17 00:00:00 2001 From: Nico Jansen Date: Wed, 2 Jan 2019 17:19:47 +0100 Subject: [PATCH 08/36] Rename CorrespondingTypes --- packages/stryker-api/src/di/CorrespondingTypes.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/stryker-api/src/di/CorrespondingTypes.ts b/packages/stryker-api/src/di/CorrespondingTypes.ts index 721866e68a..9e2b3f1a8d 100644 --- a/packages/stryker-api/src/di/CorrespondingTypes.ts +++ b/packages/stryker-api/src/di/CorrespondingTypes.ts @@ -1,8 +1,8 @@ import InjectionToken from './InjectionToken'; import CorrespondingType from './CorrespondingType'; -type ContainerValues = { +type CorrespondingTypes = { [K in keyof TS]: TS[K] extends InjectionToken ? CorrespondingType : never; }; -export default ContainerValues; +export default CorrespondingTypes; From 3bc17857b4a1323c5913b1e8e9b003eaf5519269 Mon Sep 17 00:00:00 2001 From: Nico Jansen Date: Mon, 7 Jan 2019 10:03:29 +0100 Subject: [PATCH 09/36] Refactor all the things. * Made the context of the Injector generic. * Made injector readonly and type safe * Moved all injector related stuff to its own package: 'typed-inject' * Updated TestInjector class to now use the new typed-inject * Updated Stryker to now use the new typed-inject * Updated Stryker-api to now use types from typed-inject --- packages/stryker-api/config.ts | 2 +- packages/stryker-api/di.ts | 15 +- packages/stryker-api/mutant.ts | 2 +- packages/stryker-api/package.json | 3 +- packages/stryker-api/report.ts | 2 +- .../src/config/ConfigEditorPlugin.ts | 10 +- packages/stryker-api/src/di/Container.ts | 19 -- packages/stryker-api/src/di/Contexts.ts | 33 +++ .../stryker-api/src/di/CorrespondingType.ts | 6 - .../stryker-api/src/di/CorrespondingTypes.ts | 8 - packages/stryker-api/src/di/Inject.ts | 6 - packages/stryker-api/src/di/Injectable.ts | 9 - packages/stryker-api/src/di/InjectionToken.ts | 9 - packages/stryker-api/src/di/PluginKind.ts | 9 - packages/stryker-api/src/di/PluginResolver.ts | 7 - packages/stryker-api/src/di/StrykerPlugin.ts | 8 - packages/stryker-api/src/di/plugins.ts | 54 ++++- packages/stryker-api/src/di/token.ts | 6 - packages/stryker-api/src/di/tokens.ts | 3 +- .../stryker-api/src/mutant/MutatorPlugin.ts | 10 +- .../stryker-api/src/report/ReporterPlugin.ts | 9 +- .../src/test_framework/TestFrameworkPlugin.ts | 9 +- .../src/test_runner/TestRunnerPlugin.ts | 13 +- .../src/transpile/TranspilerPlugin.ts | 14 +- packages/stryker-api/test_framework.ts | 2 +- packages/stryker-api/test_runner.ts | 2 +- packages/stryker-api/transpile.ts | 2 +- packages/stryker-api/tsconfig.src.json | 37 ++-- .../stryker-html-reporter/src/HtmlReporter.ts | 5 +- packages/stryker-html-reporter/src/index.ts | 9 +- .../test/helpers/initSinon.ts | 4 +- .../test/integration/MathReport.it.spec.ts | 10 +- .../test/integration/StrykerReport.it.spec.ts | 6 +- .../integration/singleFileInFolder.it.spec.ts | 6 +- .../test/unit/HtmlReporter.spec.ts | 4 +- packages/stryker-test-helpers/package.json | 3 +- .../stryker-test-helpers/src/TestInjector.ts | 74 ++++--- packages/stryker-test-helpers/src/factory.ts | 6 + packages/stryker-test-helpers/src/index.ts | 4 +- .../stryker-test-helpers/tsconfig.src.json | 3 + packages/stryker-util/package.json | 5 +- .../src}/StrykerError.ts | 2 +- packages/stryker-util/src/errors.ts | 21 ++ packages/stryker-util/src/index.ts | 2 + packages/stryker-util/tsconfig.src.json | 5 + packages/stryker/package.json | 1 + packages/stryker/src/Stryker.ts | 15 +- .../stryker/src/TestFrameworkOrchestrator.ts | 2 + .../child-proxy/ChildProcessCrashedError.ts | 2 +- .../src/child-proxy/ChildProcessProxy.ts | 3 +- .../child-proxy/ChildProcessProxyWorker.ts | 3 +- packages/stryker/src/config/ConfigReader.ts | 2 +- .../stryker/src/config/ConfigValidator.ts | 7 +- packages/stryker/src/di/Injector.ts | 70 ------ packages/stryker/src/di/PluginLoader.ts | 38 ++-- packages/stryker/src/di/Providers.ts | 15 -- packages/stryker/src/di/loggerFactory.ts | 8 + packages/stryker/src/initializer/NpmClient.ts | 2 +- .../stryker/src/input/InputFileResolver.ts | 10 +- .../src/reporters/BroadcastReporter.ts | 10 +- .../src/reporters/ClearTextReporter.ts | 4 +- .../src/reporters/DashboardReporter.ts | 8 +- .../stryker/src/reporters/DotsReporter.ts | 4 +- .../src/reporters/EventRecorderReporter.ts | 4 +- .../reporters/ProgressAppendOnlyReporter.ts | 4 +- .../stryker/src/reporters/ProgressReporter.ts | 4 +- .../DashboardReporterClient.ts | 2 +- packages/stryker/src/reporters/index.ts | 19 +- .../ChildProcessTestRunnerWorker.ts | 2 +- .../src/test-runner/CommandTestRunner.ts | 3 +- .../stryker/src/test-runner/RetryDecorator.ts | 2 +- .../CoverageInstrumenterTranspiler.ts | 2 +- .../src/transpiler/MutantTranspiler.ts | 2 +- .../stryker/src/transpiler/SourceMapper.ts | 2 +- .../src/transpiler/TranspilerFacade.ts | 2 +- packages/stryker/src/utils/objectUtils.ts | 21 -- packages/stryker/test/helpers/initSinon.ts | 4 +- packages/stryker/test/unit/StrykerSpec.ts | 21 +- .../stryker/test/unit/di/Injector.spec.ts | 134 ------------ .../test/unit/input/InputFileResolverSpec.ts | 4 +- .../unit/reporters/BroadcastReporterSpec.ts | 94 ++++---- .../unit/reporters/DashboardReporterSpec.ts | 3 +- .../unit/test-runner/CommandTestRunnerSpec.ts | 3 +- .../unit/test-runner/RetryDecoratorSpec.ts | 2 +- .../unit/transpiler/MutantTranspilerSpec.ts | 2 +- packages/stryker/tsconfig.src.json | 3 + packages/typed-inject/.vscode/launch.json | 48 +++++ packages/typed-inject/package.json | 28 +++ packages/typed-inject/src/Exception.ts | 9 + packages/typed-inject/src/InjectorImpl.ts | 161 ++++++++++++++ .../typed-inject/src/api/CorrespondingType.ts | 9 + packages/typed-inject/src/api/Injectable.ts | 14 ++ .../typed-inject/src/api/InjectionToken.ts | 5 + packages/typed-inject/src/api/Injector.ts | 15 ++ packages/typed-inject/src/api/Scope.ts | 5 + packages/typed-inject/src/index.ts | 7 + packages/typed-inject/src/tokens.ts | 4 + .../typed-inject/test/unit/Injector.spec.ts | 201 ++++++++++++++++++ packages/typed-inject/tsconfig.json | 11 + packages/typed-inject/tsconfig.src.json | 10 + packages/typed-inject/tsconfig.test.json | 17 ++ tsconfig.json | 3 +- workspace.code-workspace | 3 + 103 files changed, 974 insertions(+), 597 deletions(-) delete mode 100644 packages/stryker-api/src/di/Container.ts create mode 100644 packages/stryker-api/src/di/Contexts.ts delete mode 100644 packages/stryker-api/src/di/CorrespondingType.ts delete mode 100644 packages/stryker-api/src/di/CorrespondingTypes.ts delete mode 100644 packages/stryker-api/src/di/Inject.ts delete mode 100644 packages/stryker-api/src/di/Injectable.ts delete mode 100644 packages/stryker-api/src/di/InjectionToken.ts delete mode 100644 packages/stryker-api/src/di/PluginKind.ts delete mode 100644 packages/stryker-api/src/di/PluginResolver.ts delete mode 100644 packages/stryker-api/src/di/StrykerPlugin.ts delete mode 100644 packages/stryker-api/src/di/token.ts rename packages/{stryker/src/utils => stryker-util/src}/StrykerError.ts (90%) create mode 100644 packages/stryker-util/src/errors.ts delete mode 100644 packages/stryker/src/di/Injector.ts delete mode 100644 packages/stryker/src/di/Providers.ts create mode 100644 packages/stryker/src/di/loggerFactory.ts delete mode 100644 packages/stryker/test/unit/di/Injector.spec.ts create mode 100644 packages/typed-inject/.vscode/launch.json create mode 100644 packages/typed-inject/package.json create mode 100644 packages/typed-inject/src/Exception.ts create mode 100644 packages/typed-inject/src/InjectorImpl.ts create mode 100644 packages/typed-inject/src/api/CorrespondingType.ts create mode 100644 packages/typed-inject/src/api/Injectable.ts create mode 100644 packages/typed-inject/src/api/InjectionToken.ts create mode 100644 packages/typed-inject/src/api/Injector.ts create mode 100644 packages/typed-inject/src/api/Scope.ts create mode 100644 packages/typed-inject/src/index.ts create mode 100644 packages/typed-inject/src/tokens.ts create mode 100644 packages/typed-inject/test/unit/Injector.spec.ts create mode 100644 packages/typed-inject/tsconfig.json create mode 100644 packages/typed-inject/tsconfig.src.json create mode 100644 packages/typed-inject/tsconfig.test.json diff --git a/packages/stryker-api/config.ts b/packages/stryker-api/config.ts index df30f0fe6c..328d182ac9 100644 --- a/packages/stryker-api/config.ts +++ b/packages/stryker-api/config.ts @@ -1,4 +1,4 @@ export { default as Config } from './src/config/Config'; export { default as ConfigEditor } from './src/config/ConfigEditor'; export { default as ConfigEditorFactory } from './src/config/ConfigEditorFactory'; -export { default as ConfigEditorPlugin } from './src/config/ConfigEditorPlugin'; +export * from './src/config/ConfigEditorPlugin'; diff --git a/packages/stryker-api/di.ts b/packages/stryker-api/di.ts index 6d5b6a1498..157f41dbe6 100644 --- a/packages/stryker-api/di.ts +++ b/packages/stryker-api/di.ts @@ -1,12 +1,3 @@ -export { default as Injectable } from './src/di/Injectable'; -export { default as InjectionToken } from './src/di/InjectionToken'; -export { default as Container } from './src/di/Container'; -export { default as tokens } from './src/di/tokens'; -export { default as token } from './src/di/token'; -export { default as plugins } from './src/di/plugins'; -export { default as StrykerPlugin } from './src/di/StrykerPlugin'; -export { default as PluginKind } from './src/di/PluginKind'; -export { default as PluginResolver } from './src/di/PluginResolver'; -export { default as Inject } from './src/di/Inject'; -export { default as CorrespondingType } from './src/di/CorrespondingType'; -export { default as CorrespondingTypes } from './src/di/CorrespondingTypes'; +export * from './src/di/Contexts'; +export * from './src/di/Plugins'; +export * from './src/di/tokens'; diff --git a/packages/stryker-api/mutant.ts b/packages/stryker-api/mutant.ts index 737221c1cd..792ff787a5 100644 --- a/packages/stryker-api/mutant.ts +++ b/packages/stryker-api/mutant.ts @@ -1,4 +1,4 @@ export { default as Mutant } from './src/mutant/Mutant'; export { default as Mutator } from './src/mutant/Mutator'; export { default as MutatorFactory } from './src/mutant/MutatorFactory'; -export { default as MutatorPlugin } from './src/mutant/MutatorPlugin'; +export * from './src/mutant/MutatorPlugin'; diff --git a/packages/stryker-api/package.json b/packages/stryker-api/package.json index 201c228dc1..42bd319ebd 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/report.ts b/packages/stryker-api/report.ts index a1e07f579a..47897d9b30 100644 --- a/packages/stryker-api/report.ts +++ b/packages/stryker-api/report.ts @@ -2,7 +2,7 @@ export { default as Reporter } from './src/report/Reporter'; export { default as MutantResult } from './src/report/MutantResult'; export { default as MutantStatus } from './src/report/MutantStatus'; export { default as ReporterFactory } from './src/report/ReporterFactory'; -export { default as ReporterPlugin } from './src/report/ReporterPlugin'; +export * from './src/report/ReporterPlugin'; export { default as SourceFile } from './src/report/SourceFile'; export { default as MatchedMutant } from './src/report/MatchedMutant'; export { default as ScoreResult } from './src/report/ScoreResult'; diff --git a/packages/stryker-api/src/config/ConfigEditorPlugin.ts b/packages/stryker-api/src/config/ConfigEditorPlugin.ts index fc577428a6..4af9b151b8 100644 --- a/packages/stryker-api/src/config/ConfigEditorPlugin.ts +++ b/packages/stryker-api/src/config/ConfigEditorPlugin.ts @@ -1,6 +1,12 @@ import ConfigEditor from './ConfigEditor'; -import { InjectionToken, StrykerPlugin, PluginKind } from '../../di'; +import { StrykerPlugin, PluginKind, StrykerContext } from '../../di'; +import { InjectionToken } from 'typed-inject'; -export default interface ConfigEditorPlugin extends StrykerPlugin { +export interface ConfigEditorPlugin[]> + extends StrykerPlugin { readonly kind: PluginKind.ConfigEditor; } + +export function configEditorPlugin[]>(reporterPlugin: ConfigEditorPlugin) { + return reporterPlugin; +} diff --git a/packages/stryker-api/src/di/Container.ts b/packages/stryker-api/src/di/Container.ts deleted file mode 100644 index 41c14dda91..0000000000 --- a/packages/stryker-api/src/di/Container.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { LoggerFactoryMethod, Logger } from '../../logging'; -import { StrykerOptions } from '../../core'; -import PluginResolver from './PluginResolver'; -import Inject from './Inject'; -import { Config } from '../../config'; - -export default interface Container { - getLogger: LoggerFactoryMethod; - logger: Logger; - options: StrykerOptions; - produceSourceMaps: boolean; - sandboxFileNames: ReadonlyArray; - pluginResolver: PluginResolver; - inject: Inject; - /** - * @deprecated This is just here to migrate between old and new plugins. Don't use this! Use `options` instead - */ - config: Config; -} diff --git a/packages/stryker-api/src/di/Contexts.ts b/packages/stryker-api/src/di/Contexts.ts new file mode 100644 index 0000000000..ee640d45fc --- /dev/null +++ b/packages/stryker-api/src/di/Contexts.ts @@ -0,0 +1,33 @@ +import { LoggerFactoryMethod, Logger } from '../../logging'; +import { StrykerOptions } from '../../core'; +import { PluginResolver } from './Plugins'; +import { Config } from '../../config'; + +function token(value: T): T { + return value; +} + +export const commonTokens = Object.freeze({ + /** + * @deprecated Use 'options' instead. This is just hear to support plugin migration + */ + config: token('config'), + getLogger: token('getLogger'), + logger: token('logger'), + options: token('options'), + pluginResolver: token('pluginResolver') +}); + +export interface StrykerContext { + [commonTokens.getLogger]: LoggerFactoryMethod; + [commonTokens.logger]: Logger; + [commonTokens.pluginResolver]: PluginResolver; +} + +export interface PluginContext extends StrykerContext { + [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; +} diff --git a/packages/stryker-api/src/di/CorrespondingType.ts b/packages/stryker-api/src/di/CorrespondingType.ts deleted file mode 100644 index 31f2390c95..0000000000 --- a/packages/stryker-api/src/di/CorrespondingType.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { InjectionToken, Container } from '../../di'; - -type CorrespondingType = - T extends keyof Container ? Container[T] : T extends new (...args: any[]) => any ? InstanceType : never; - -export default CorrespondingType; diff --git a/packages/stryker-api/src/di/CorrespondingTypes.ts b/packages/stryker-api/src/di/CorrespondingTypes.ts deleted file mode 100644 index 9e2b3f1a8d..0000000000 --- a/packages/stryker-api/src/di/CorrespondingTypes.ts +++ /dev/null @@ -1,8 +0,0 @@ -import InjectionToken from './InjectionToken'; -import CorrespondingType from './CorrespondingType'; - -type CorrespondingTypes = { - [K in keyof TS]: TS[K] extends InjectionToken ? CorrespondingType : never; -}; - -export default CorrespondingTypes; diff --git a/packages/stryker-api/src/di/Inject.ts b/packages/stryker-api/src/di/Inject.ts deleted file mode 100644 index cd9c6a817d..0000000000 --- a/packages/stryker-api/src/di/Inject.ts +++ /dev/null @@ -1,6 +0,0 @@ -import InjectionToken from './InjectionToken'; -import { Injectable } from '../../di'; - -type Inject = (Constructor: Injectable) => T; - -export default Inject; diff --git a/packages/stryker-api/src/di/Injectable.ts b/packages/stryker-api/src/di/Injectable.ts deleted file mode 100644 index f7ecf884bd..0000000000 --- a/packages/stryker-api/src/di/Injectable.ts +++ /dev/null @@ -1,9 +0,0 @@ -import InjectionToken from './InjectionToken'; -import CorrespondingTypes from './CorrespondingTypes'; - -interface Injectable { - new(...args: CorrespondingTypes): T; - readonly inject: Tokens; -} - -export default Injectable; diff --git a/packages/stryker-api/src/di/InjectionToken.ts b/packages/stryker-api/src/di/InjectionToken.ts deleted file mode 100644 index 30c6ebd05c..0000000000 --- a/packages/stryker-api/src/di/InjectionToken.ts +++ /dev/null @@ -1,9 +0,0 @@ -import Container from './Container'; -import { Injectable } from '../../di'; - -type InjectionToken = keyof Container | Injectable; -export default InjectionToken; - -export function token(injectable: Injectable): InjectionToken { - return injectable; -} diff --git a/packages/stryker-api/src/di/PluginKind.ts b/packages/stryker-api/src/di/PluginKind.ts deleted file mode 100644 index 15290b24d7..0000000000 --- a/packages/stryker-api/src/di/PluginKind.ts +++ /dev/null @@ -1,9 +0,0 @@ -enum PluginKind { - ConfigEditor = 'ConfigEditor', - TestRunner = 'TestRunner', - TestFramework = 'TestFramework', - Transpiler = 'Transpiler', - Mutator = 'Mutator', - Reporter = 'Reporter' -} -export default PluginKind; diff --git a/packages/stryker-api/src/di/PluginResolver.ts b/packages/stryker-api/src/di/PluginResolver.ts deleted file mode 100644 index 54dacdc24f..0000000000 --- a/packages/stryker-api/src/di/PluginResolver.ts +++ /dev/null @@ -1,7 +0,0 @@ -import StrykerPlugin from './StrykerPlugin'; -import PluginKind from './PluginKind'; -import InjectionToken from './InjectionToken'; - -export default interface PluginResolver { - resolve(kind: PluginKind, name: string): StrykerPlugin; -} diff --git a/packages/stryker-api/src/di/StrykerPlugin.ts b/packages/stryker-api/src/di/StrykerPlugin.ts deleted file mode 100644 index 3da6081406..0000000000 --- a/packages/stryker-api/src/di/StrykerPlugin.ts +++ /dev/null @@ -1,8 +0,0 @@ -import Injectable from './Injectable'; -import InjectionToken from './InjectionToken'; -import PluginKind from './PluginKind'; - -export default interface StrykerPlugin extends Injectable { - readonly pluginName: string; - readonly kind: PluginKind; -} diff --git a/packages/stryker-api/src/di/plugins.ts b/packages/stryker-api/src/di/plugins.ts index 06018f7043..b0c13fa90f 100644 --- a/packages/stryker-api/src/di/plugins.ts +++ b/packages/stryker-api/src/di/plugins.ts @@ -1,11 +1,45 @@ -import { StrykerPlugin as P, InjectionToken as I } from '../../di'; - -export default function plugins(plugin: P): [P]; -export default function plugins(plugin: P, plugin2: P): [P, P]; -export default function plugins(plugin: P, plugin2: P, plugin3: P): [P, P, P]; -export default function plugins(plugin: P, plugin2: P, plugin3: P, plugin4: P): [P, P, P, P]; -export default function plugins(plugin: P, plugin2: P, plugin3: P, plugin4: P, plugin5: P): [P, P, P, P, P]; -export default function plugins(plugin: P, plugin2: P, plugin3: P, plugin4: P, plugin5: P, plugin6: P): [P, P, P, P, P, P]; -export default function plugins(...plugins: P[]) { - return plugins; +import { TestFrameworkPlugin } from '../../test_framework'; +import { TestRunnerPluginContext, TestRunnerPlugin } from '../../test_runner'; +import { ReporterPlugin } from '../../report'; +import { MutatorPlugin } from '../../mutant'; +import { TranspilerPlugin, TranspilerPluginContext } from '../../transpile'; +import { PluginContext, StrykerContext } from './Contexts'; +import { ConfigEditorPlugin } from '../../config'; +import { InjectionToken, InjectableClass } from 'typed-inject'; + +export interface StrykerPlugin[]> { + readonly name: string; + readonly kind: PluginKind; + readonly injectable: InjectableClass; +} + +export enum PluginKind { + ConfigEditor = 'ConfigEditor', + TestRunner = 'TestRunner', + TestFramework = 'TestFramework', + Transpiler = 'Transpiler', + Mutator = 'Mutator', + Reporter = 'Reporter' +} + +export interface Plugins { + [PluginKind.ConfigEditor]: ConfigEditorPlugin[]>; + [PluginKind.Mutator]: MutatorPlugin[]>; + [PluginKind.Reporter]: ReporterPlugin[]>; + [PluginKind.TestFramework]: TestFrameworkPlugin[]>; + [PluginKind.TestRunner]: TestRunnerPlugin[]>; + [PluginKind.Transpiler]: TranspilerPlugin[]>; +} + +export interface PluginContexts { + [PluginKind.ConfigEditor]: StrykerContext; + [PluginKind.Mutator]: PluginContext; + [PluginKind.Reporter]: PluginContext; + [PluginKind.TestFramework]: PluginContext; + [PluginKind.TestRunner]: TestRunnerPluginContext; + [PluginKind.Transpiler]: TranspilerPluginContext; +} + +export interface PluginResolver { + resolve(kind: T, name: string): Plugins[T]; } diff --git a/packages/stryker-api/src/di/token.ts b/packages/stryker-api/src/di/token.ts deleted file mode 100644 index 93fb45bb1f..0000000000 --- a/packages/stryker-api/src/di/token.ts +++ /dev/null @@ -1,6 +0,0 @@ -import InjectionToken from './InjectionToken'; -import Injectable from './Injectable'; - -export default function token(injectable: Injectable): InjectionToken { - return injectable; -} diff --git a/packages/stryker-api/src/di/tokens.ts b/packages/stryker-api/src/di/tokens.ts index dd1c33075b..6fa2601577 100644 --- a/packages/stryker-api/src/di/tokens.ts +++ b/packages/stryker-api/src/di/tokens.ts @@ -1,5 +1,4 @@ -import InjectionToken from './InjectionToken'; -export default function tokens(...tokens: TS): TS { +export function tokens(...tokens: TS): TS { return tokens; } diff --git a/packages/stryker-api/src/mutant/MutatorPlugin.ts b/packages/stryker-api/src/mutant/MutatorPlugin.ts index 3324ff9906..51ab96a138 100644 --- a/packages/stryker-api/src/mutant/MutatorPlugin.ts +++ b/packages/stryker-api/src/mutant/MutatorPlugin.ts @@ -1,6 +1,12 @@ import Mutator from './Mutator'; -import { InjectionToken, StrykerPlugin, PluginKind } from '../../di'; +import { StrykerPlugin, PluginKind, PluginContext } from '../../di'; +import { InjectionToken } from 'typed-inject'; -export default interface MutatorPlugin extends StrykerPlugin { +export interface MutatorPlugin[]> + extends StrykerPlugin { readonly kind: PluginKind.Mutator; } + +export function mutatorPlugin[]>(mutatorPlugin: MutatorPlugin) { + return mutatorPlugin; +} diff --git a/packages/stryker-api/src/report/ReporterPlugin.ts b/packages/stryker-api/src/report/ReporterPlugin.ts index 51802e029d..dd0959e0a1 100644 --- a/packages/stryker-api/src/report/ReporterPlugin.ts +++ b/packages/stryker-api/src/report/ReporterPlugin.ts @@ -1,6 +1,11 @@ import Reporter from './Reporter'; -import { InjectionToken, StrykerPlugin, PluginKind } from '../../di'; +import { StrykerPlugin, PluginKind, PluginContext } from '../../di'; +import { InjectionToken } from 'typed-inject'; -export default interface ReporterPlugin extends StrykerPlugin { +export interface ReporterPlugin[]> extends StrykerPlugin { readonly kind: PluginKind.Reporter; } + +export function reporterPlugin[]>(reporterPlugin: ReporterPlugin) { + return reporterPlugin; +} diff --git a/packages/stryker-api/src/test_framework/TestFrameworkPlugin.ts b/packages/stryker-api/src/test_framework/TestFrameworkPlugin.ts index b7200af2be..2d8c69fbb8 100644 --- a/packages/stryker-api/src/test_framework/TestFrameworkPlugin.ts +++ b/packages/stryker-api/src/test_framework/TestFrameworkPlugin.ts @@ -1,6 +1,11 @@ -import { InjectionToken, StrykerPlugin, PluginKind } from '../../di'; import TestFramework from './TestFramework'; +import { StrykerPlugin, PluginKind, PluginContext } from '../../di'; +import { InjectionToken } from 'typed-inject'; -export default interface TestFrameworkPlugin extends StrykerPlugin { +export interface TestFrameworkPlugin[]> extends StrykerPlugin { readonly kind: PluginKind.TestFramework; } + +export function testFrameworkPlugin[]>(testFrameworkPlugin: TestFrameworkPlugin) { + return testFrameworkPlugin; +} diff --git a/packages/stryker-api/src/test_runner/TestRunnerPlugin.ts b/packages/stryker-api/src/test_runner/TestRunnerPlugin.ts index b711805214..19045fef1b 100644 --- a/packages/stryker-api/src/test_runner/TestRunnerPlugin.ts +++ b/packages/stryker-api/src/test_runner/TestRunnerPlugin.ts @@ -1,6 +1,15 @@ -import { InjectionToken, StrykerPlugin, PluginKind } from '../../di'; import TestRunner from './TestRunner'; +import { StrykerPlugin, PluginKind, PluginContext } from '../../di'; +import { InjectionToken } from 'typed-inject'; -export default interface TestRunnerPlugin extends StrykerPlugin { +export interface TestRunnerPluginContext extends PluginContext { + sandboxFileNames: ReadonlyArray; +} + +export interface TestRunnerPlugin[]> extends StrykerPlugin { readonly kind: PluginKind.TestRunner; } + +export function testRunnerPlugin[]>(testRunnerPlugin: TestRunnerPlugin) { + return testRunnerPlugin; +} diff --git a/packages/stryker-api/src/transpile/TranspilerPlugin.ts b/packages/stryker-api/src/transpile/TranspilerPlugin.ts index d49879dc8c..68e6ee364e 100644 --- a/packages/stryker-api/src/transpile/TranspilerPlugin.ts +++ b/packages/stryker-api/src/transpile/TranspilerPlugin.ts @@ -1,6 +1,16 @@ +import { PluginContext, PluginKind, StrykerPlugin } from '../../di'; import Transpiler from './Transpiler'; -import { InjectionToken, StrykerPlugin, PluginKind } from '../../di'; +import { InjectionToken } from 'typed-inject'; -export default interface TranspilerPlugin extends StrykerPlugin { +export interface TranspilerPluginContext extends PluginContext { + produceSourceMaps: boolean; +} + +export interface TranspilerPlugin[]> + extends StrykerPlugin { readonly kind: PluginKind.Transpiler; } + +export function transpilerPlugin[]>(transpilerPlugin: TranspilerPlugin) { + return transpilerPlugin; +} diff --git a/packages/stryker-api/test_framework.ts b/packages/stryker-api/test_framework.ts index 2b87a73b90..dd18f250bd 100644 --- a/packages/stryker-api/test_framework.ts +++ b/packages/stryker-api/test_framework.ts @@ -1,5 +1,5 @@ export { default as TestFramework } from './src/test_framework/TestFramework'; export { default as TestSelection } from './src/test_framework/TestSelection'; export { default as TestFrameworkFactory } from './src/test_framework/TestFrameworkFactory'; -export { default as TestFrameworkPlugin } from './src/test_framework/TestFrameworkPlugin'; +export * from './src/test_framework/TestFrameworkPlugin'; export { default as TestFrameworkSettings } from './src/test_framework/TestFrameworkSettings'; diff --git a/packages/stryker-api/test_runner.ts b/packages/stryker-api/test_runner.ts index c14081e035..7e48088265 100644 --- a/packages/stryker-api/test_runner.ts +++ b/packages/stryker-api/test_runner.ts @@ -7,4 +7,4 @@ export { default as RunResult } from './src/test_runner/RunResult'; export { default as RunOptions } from './src/test_runner/RunOptions'; export { default as RunStatus } from './src/test_runner/RunStatus'; export { default as TestRunnerFactory } from './src/test_runner/TestRunnerFactory'; -export { default as TestRunnerPlugin } from './src/test_runner/TestRunnerPlugin'; +export * from './src/test_runner/TestRunnerPlugin'; diff --git a/packages/stryker-api/transpile.ts b/packages/stryker-api/transpile.ts index 19d730c5a8..c89707ec90 100644 --- a/packages/stryker-api/transpile.ts +++ b/packages/stryker-api/transpile.ts @@ -1,4 +1,4 @@ export { default as Transpiler } from './src/transpile/Transpiler'; export { default as TranspilerFactory } from './src/transpile/TranspilerFactory'; -export { default as TranspilerPlugin } from './src/transpile/TranspilerPlugin'; +export * from './src/transpile/TranspilerPlugin'; export { default as TranspilerOptions } from './src/transpile/TranspilerOptions'; diff --git a/packages/stryker-api/tsconfig.src.json b/packages/stryker-api/tsconfig.src.json index f2e3fb8fb0..aaf4743289 100644 --- a/packages/stryker-api/tsconfig.src.json +++ b/packages/stryker-api/tsconfig.src.json @@ -1,18 +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", - "di.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", + "di.ts" + ], + "references": [ + { + "path": "../typed-inject/tsconfig.src.json" + } + ] } \ No newline at end of file diff --git a/packages/stryker-html-reporter/src/HtmlReporter.ts b/packages/stryker-html-reporter/src/HtmlReporter.ts index c008d9017d..1eeb185730 100644 --- a/packages/stryker-html-reporter/src/HtmlReporter.ts +++ b/packages/stryker-html-reporter/src/HtmlReporter.ts @@ -6,7 +6,7 @@ import * as util from './util'; import * as templates from './templates'; import Breadcrumb from './Breadcrumb'; import { StrykerOptions } from 'stryker-api/core'; -import { tokens, PluginKind } from 'stryker-api/di'; +import { tokens } from 'stryker-api/di'; const DEFAULT_BASE_FOLDER = path.normalize('reports/mutation/html'); export const RESOURCES_DIR_NAME = 'strykerResources'; @@ -23,9 +23,6 @@ export default class HtmlReporter implements Reporter { public static readonly inject = tokens('options', 'logger'); - public static readonly kind = PluginKind.Reporter; - public static readonly pluginName = 'html'; - public onAllSourceFilesRead(files: SourceFile[]) { this.files = files; } diff --git a/packages/stryker-html-reporter/src/index.ts b/packages/stryker-html-reporter/src/index.ts index 8b48aaf54c..cee77f7379 100644 --- a/packages/stryker-html-reporter/src/index.ts +++ b/packages/stryker-html-reporter/src/index.ts @@ -1,4 +1,9 @@ import HtmlReporter from './HtmlReporter'; -import { plugins } from 'stryker-api/di'; +import { PluginKind } from 'stryker-api/di'; +import { reporterPlugin } from 'stryker-api/report'; -export const strykerPlugins = plugins(HtmlReporter); +export const strykerPlugins = [reporterPlugin({ + injectable: HtmlReporter, + kind: PluginKind.Reporter, + name: 'html' +})]; diff --git a/packages/stryker-html-reporter/test/helpers/initSinon.ts b/packages/stryker-html-reporter/test/helpers/initSinon.ts index 718bc2ab34..00ee898357 100644 --- a/packages/stryker-html-reporter/test/helpers/initSinon.ts +++ b/packages/stryker-html-reporter/test/helpers/initSinon.ts @@ -1,7 +1,7 @@ import * as sinon from 'sinon'; -import { TestInjector } from '@stryker-mutator/test-helpers'; +import { testInjector } from '@stryker-mutator/test-helpers'; afterEach(() => { sinon.restore(); - TestInjector.reset(); + testInjector.reset(); }); diff --git a/packages/stryker-html-reporter/test/integration/MathReport.it.spec.ts b/packages/stryker-html-reporter/test/integration/MathReport.it.spec.ts index bc8746f294..3786177495 100644 --- a/packages/stryker-html-reporter/test/integration/MathReport.it.spec.ts +++ b/packages/stryker-html-reporter/test/integration/MathReport.it.spec.ts @@ -5,15 +5,15 @@ import { Config } from 'stryker-api/config'; import EventPlayer from '../helpers/EventPlayer'; import fileUrl = require('file-url'); import HtmlReporter from '../../src/HtmlReporter'; -import { TestInjector } from '@stryker-mutator/test-helpers'; +import { testInjector } from '@stryker-mutator/test-helpers'; describe('HtmlReporter with example math project', () => { let sut: HtmlReporter; const baseDir = 'reports/mutation/math'; beforeEach(() => { - TestInjector.options.htmlReporter = { baseDir }; - sut = TestInjector.inject(HtmlReporter); + testInjector.options.htmlReporter = { baseDir }; + sut = testInjector.injector.injectClass(HtmlReporter); return new EventPlayer(path.join('testResources', 'mathEvents')) .replay(sut) .then(() => sut.wrapUp()); @@ -26,14 +26,14 @@ describe('HtmlReporter with example math project', () => { }); it('should output a log message with a link to the HTML report', () => { - expect(TestInjector.logger.info).to.have.been.calledWith(`Your report can be found at: ${fileUrl(baseDir + '/index.html')}`); + expect(testInjector.logger.info).to.have.been.calledWith(`Your report can be found at: ${fileUrl(baseDir + '/index.html')}`); }); describe('when initiated a second time with empty events', () => { beforeEach(() => { const config = new Config(); config.set({ htmlReporter: { baseDir } }); - sut = TestInjector.inject(HtmlReporter); + sut = testInjector.injector.injectClass(HtmlReporter); sut.onAllSourceFilesRead([]); sut.onAllMutantsTested([]); return sut.wrapUp(); diff --git a/packages/stryker-html-reporter/test/integration/StrykerReport.it.spec.ts b/packages/stryker-html-reporter/test/integration/StrykerReport.it.spec.ts index 857eac0d01..6011ed0b4a 100644 --- a/packages/stryker-html-reporter/test/integration/StrykerReport.it.spec.ts +++ b/packages/stryker-html-reporter/test/integration/StrykerReport.it.spec.ts @@ -2,7 +2,7 @@ import { expect } from 'chai'; import HtmlReporter from '../../src/HtmlReporter'; import EventPlayer from '../helpers/EventPlayer'; import { readDirectoryTree } from '../helpers/fsHelpers'; -import { TestInjector } from '@stryker-mutator/test-helpers'; +import { testInjector } from '@stryker-mutator/test-helpers'; const REPORT_DIR = 'reports/mutation/stryker'; @@ -10,8 +10,8 @@ describe('Html report of stryker', () => { let sut: HtmlReporter; beforeEach(() => { - TestInjector.options.htmlReporter = { baseDir: REPORT_DIR }; - sut = TestInjector.inject(HtmlReporter); + testInjector.options.htmlReporter = { baseDir: REPORT_DIR }; + sut = testInjector.injector.injectClass(HtmlReporter); return new EventPlayer('testResources/strykerEvents') .replay(sut) .then(() => sut.wrapUp()); diff --git a/packages/stryker-html-reporter/test/integration/singleFileInFolder.it.spec.ts b/packages/stryker-html-reporter/test/integration/singleFileInFolder.it.spec.ts index 96cc40d681..bc61980d8c 100644 --- a/packages/stryker-html-reporter/test/integration/singleFileInFolder.it.spec.ts +++ b/packages/stryker-html-reporter/test/integration/singleFileInFolder.it.spec.ts @@ -4,7 +4,7 @@ import EventPlayer from '../helpers/EventPlayer'; import HtmlReporter from '../../src/HtmlReporter'; import { readDirectoryTree } from '../helpers/fsHelpers'; import * as fs from 'fs'; -import { TestInjector } from '@stryker-mutator/test-helpers'; +import { testInjector } from '@stryker-mutator/test-helpers'; const REPORT_DIR = 'reports/mutation/singleFileInFolder'; @@ -12,8 +12,8 @@ describe('HtmlReporter single file in a folder', () => { let sut: HtmlReporter; beforeEach(() => { - TestInjector.options.htmlReporter = { baseDir: REPORT_DIR }; - sut = TestInjector.inject(HtmlReporter); + testInjector.options.htmlReporter = { baseDir: REPORT_DIR }; + sut = testInjector.injector.injectClass(HtmlReporter); return new EventPlayer(path.join('testResources', 'singleFileInFolder')) .replay(sut) .then(() => sut.wrapUp()); diff --git a/packages/stryker-html-reporter/test/unit/HtmlReporter.spec.ts b/packages/stryker-html-reporter/test/unit/HtmlReporter.spec.ts index 9ba0091814..c571392c14 100644 --- a/packages/stryker-html-reporter/test/unit/HtmlReporter.spec.ts +++ b/packages/stryker-html-reporter/test/unit/HtmlReporter.spec.ts @@ -4,7 +4,7 @@ import * as sinon from 'sinon'; import * as util from '../../src/util'; import HtmlReporter from '../../src/HtmlReporter'; import { sourceFile, mutantResult, scoreResult } from '../helpers/producers'; -import { TestInjector } from '@stryker-mutator/test-helpers'; +import { testInjector } from '@stryker-mutator/test-helpers'; describe('HtmlReporter', () => { let copyFolderStub: sinon.SinonStub; @@ -18,7 +18,7 @@ describe('HtmlReporter', () => { writeFileStub = sinon.stub(util, 'writeFile'); deleteDirStub = sinon.stub(util, 'deleteDir'); mkdirStub = sinon.stub(util, 'mkdir'); - sut = TestInjector.inject(HtmlReporter); + sut = testInjector.injector.injectClass(HtmlReporter); }); describe('when in happy flow', () => { diff --git a/packages/stryker-test-helpers/package.json b/packages/stryker-test-helpers/package.json index bef36bd4fd..00e6b50951 100644 --- a/packages/stryker-test-helpers/package.json +++ b/packages/stryker-test-helpers/package.json @@ -19,6 +19,7 @@ "homepage": "https://github.com/stryker-mutator/stryker/tree/master/packages/@stryker-mutator/test-helpers#readme", "license": "ISC", "devDependencies": { - "stryker-api": "^0.23.0" + "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 index fc74db590a..cc560c5a65 100644 --- a/packages/stryker-test-helpers/src/TestInjector.ts +++ b/packages/stryker-test-helpers/src/TestInjector.ts @@ -1,50 +1,48 @@ -import { Injectable, InjectionToken, PluginResolver } from 'stryker-api/di'; +import { PluginResolver, PluginContext } from 'stryker-api/di'; import { StrykerOptions } from 'stryker-api/core'; import { Logger } from 'stryker-api/logging'; -import { logger, strykerOptions } from './factory'; +import * as factory from './factory'; import * as sinon from 'sinon'; +import { rootInjector, Injector, Scope } from 'typed-inject'; +import { Config } from 'stryker-api/config'; -export default class TestInjector { - public static options: Partial = {}; - public static logger: sinon.SinonStubbedInstance; - public static pluginResolver: sinon.SinonStubbedInstance; - public static replacements: Map, any> = new Map(); +class TestInjector { - public static reset() { - this.options = {}; - this.logger = logger(); - this.pluginResolver = { - resolve: sinon.stub() - }; - this.replacements.clear(); + 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 static stub(Injectable: Injectable, replacement: T): typeof TestInjector { - this.replacements.set(Injectable, replacement); - return this; + public provideOptions = () => { + return factory.strykerOptions(this.options); } - public static inject(Injectable: Injectable): T { - if (this.replacements.has(Injectable)) { - return this.replacements.get(Injectable); - } else { - const args: any = Injectable.inject.map(key => { - switch (key) { - case 'options': - return strykerOptions(this.options); - case 'logger': - return this.logger; - case 'pluginResolver': - return this.pluginResolver; - case 'inject': - return this.inject.bind(this); - default: - throw new Error(`Injecting "${key}" is not (yet) supported by the ${TestInjector.name}`); - } - }); - return new Injectable(...args); - } + public pluginResolver: sinon.SinonStubbedInstance; + public options: Partial; + public logger: sinon.SinonStubbedInstance; + public injector: Injector = rootInjector + .provideValue('getLogger', this.provideLogger) + .provideFactory('logger', this.provideLogger, Scope.Transient) + .provideFactory('options', this.provideOptions, Scope.Transient) + .provideFactory('config', this.provideConfig, Scope.Transient) + .provideFactory('pluginResolver', this.providePluginResolver, Scope.Transient); + + public reset() { + this.options = {}; + this.logger = factory.logger(); + this.pluginResolver = { + resolve: sinon.stub() + }; } } -TestInjector.reset(); +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 index f71ccc432f..7e264eb754 100644 --- a/packages/stryker-test-helpers/src/factory.ts +++ b/packages/stryker-test-helpers/src/factory.ts @@ -130,6 +130,12 @@ export const config = factoryMethod(() => new Config()); export const ALL_REPORTER_EVENTS: (keyof Reporter)[] = ['onSourceFileRead', 'onAllSourceFilesRead', 'onAllMutantsMatchedWithTests', 'onMutantTested', 'onAllMutantsTested', 'onScoreCalculated', 'wrapUp']; +export function reporter(): sinon.SinonStubbedInstance> { + const reporter = {} 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++) { diff --git a/packages/stryker-test-helpers/src/index.ts b/packages/stryker-test-helpers/src/index.ts index 662e92c82e..0c10954d84 100644 --- a/packages/stryker-test-helpers/src/index.ts +++ b/packages/stryker-test-helpers/src/index.ts @@ -1,3 +1,3 @@ import * as factory from './factory'; -import TestInjector from './TestInjector'; -export { factory, TestInjector }; +import testInjector from './TestInjector'; +export { factory, testInjector }; diff --git a/packages/stryker-test-helpers/tsconfig.src.json b/packages/stryker-test-helpers/tsconfig.src.json index be0b4588b8..2048f74eca 100644 --- a/packages/stryker-test-helpers/tsconfig.src.json +++ b/packages/stryker-test-helpers/tsconfig.src.json @@ -13,6 +13,9 @@ "references": [ { "path": "../stryker-api/tsconfig.src.json" + }, + { + "path": "../typed-inject/tsconfig.src.json" } ] } \ No newline at end of file diff --git a/packages/stryker-util/package.json b/packages/stryker-util/package.json index a38b058d75..d60ea67551 100644 --- a/packages/stryker-util/package.json +++ b/packages/stryker-util/package.json @@ -25,5 +25,8 @@ "bugs": { "url": "https://github.com/stryker-mutator/stryker/issues" }, - "homepage": "https://github.com/stryker-mutator/stryker/tree/master/packages/@stryker-mutator/util#readme" + "homepage": "https://github.com/stryker-mutator/stryker/tree/master/packages/@stryker-mutator/util#readme", + "dependencies": { + "stryker-api": "^0.23.0" + } } 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/tsconfig.src.json b/packages/stryker-util/tsconfig.src.json index 85f1ef81b1..4b7dbc31e7 100644 --- a/packages/stryker-util/tsconfig.src.json +++ b/packages/stryker-util/tsconfig.src.json @@ -5,5 +5,10 @@ }, "include": [ "src" + ], + "references": [ + { + "path": "../stryker-api/tsconfig.src.json" + } ] } \ No newline at end of file diff --git a/packages/stryker/package.json b/packages/stryker/package.json index 61d5a1d008..b372ee3af4 100644 --- a/packages/stryker/package.json +++ b/packages/stryker/package.json @@ -70,6 +70,7 @@ "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" }, diff --git a/packages/stryker/src/Stryker.ts b/packages/stryker/src/Stryker.ts index e947af0006..180a643497 100644 --- a/packages/stryker/src/Stryker.ts +++ b/packages/stryker/src/Stryker.ts @@ -19,8 +19,10 @@ import InitialTestExecutor, { InitialTestRunResult } from './process/InitialTest import MutationTestExecutor from './process/MutationTestExecutor'; import InputFileCollection from './input/InputFileCollection'; import LogConfigurator from './logging/LogConfigurator'; -import Injector from './di/Injector'; import BroadcastReporter from './reporters/BroadcastReporter'; +import { PluginContext, commonTokens, PluginResolver } from 'stryker-api/di'; +import { Injector, rootInjector, Scope } from 'typed-inject'; +import { loggerFactory } from './di/loggerFactory'; export default class Stryker { @@ -29,7 +31,7 @@ export default class Stryker { private readonly reporter: BroadcastReporter; private readonly testFramework: TestFramework | null; private readonly log = getLogger(Stryker.name); - private readonly injector: Injector; + private readonly injector: Injector; /** * The Stryker mutation tester. @@ -49,8 +51,13 @@ export default class Stryker { LogConfigurator.configureMainProcess(this.config.logLevel, this.config.fileLogLevel, this.config.allowConsoleColors); // logLevel could be changed this.applyConfigEditors(); this.freezeConfig(); - this.injector = Injector.create(pluginLoader, this.config); - this.reporter = this.injector.inject(BroadcastReporter); + 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.injectClass(BroadcastReporter); this.testFramework = new TestFrameworkOrchestrator(this.config).determineTestFramework(); new ConfigValidator(this.config, this.testFramework).validate(); } diff --git a/packages/stryker/src/TestFrameworkOrchestrator.ts b/packages/stryker/src/TestFrameworkOrchestrator.ts index 5cd0e39422..804f2b6292 100644 --- a/packages/stryker/src/TestFrameworkOrchestrator.ts +++ b/packages/stryker/src/TestFrameworkOrchestrator.ts @@ -1,10 +1,12 @@ import { TestFrameworkFactory, TestFramework } from 'stryker-api/test_framework'; import { StrykerOptions } from 'stryker-api/core'; import { getLogger } from 'stryker-api/logging'; +import { tokens } from 'stryker-api/di'; export default class TestFrameworkOrchestrator { private readonly log = getLogger(TestFrameworkOrchestrator.name); + public static inject = tokens('options'); constructor(private readonly options: StrykerOptions) { } public determineTestFramework(): TestFramework | null { 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 60cd942634..54657cc677 100644 --- a/packages/stryker/src/child-proxy/ChildProcessProxyWorker.ts +++ b/packages/stryker/src/child-proxy/ChildProcessProxyWorker.ts @@ -1,7 +1,8 @@ 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 '../di/PluginLoader'; import LogConfigurator from '../logging/LogConfigurator'; diff --git a/packages/stryker/src/config/ConfigReader.ts b/packages/stryker/src/config/ConfigReader.ts index 856c51bf4c..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' + diff --git a/packages/stryker/src/config/ConfigValidator.ts b/packages/stryker/src/config/ConfigValidator.ts index 6a74398204..384c01b205 100644 --- a/packages/stryker/src/config/ConfigValidator.ts +++ b/packages/stryker/src/config/ConfigValidator.ts @@ -2,15 +2,16 @@ 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'; +import { tokens } from 'stryker-api/di'; export default class ConfigValidator { private isValid = true; private readonly log = getLogger(ConfigValidator.name); - - constructor(private readonly strykerConfig: Config, private readonly testFramework: TestFramework | null) { } + public static inject = tokens('options', 'testFramework'); + constructor(private readonly strykerConfig: StrykerOptions, private readonly testFramework: TestFramework | null) { } public validate() { this.validateTestFramework(); diff --git a/packages/stryker/src/di/Injector.ts b/packages/stryker/src/di/Injector.ts deleted file mode 100644 index b1f5651cae..0000000000 --- a/packages/stryker/src/di/Injector.ts +++ /dev/null @@ -1,70 +0,0 @@ -import { Injectable, InjectionToken, Container, PluginResolver, CorrespondingType } from 'stryker-api/di'; -import { Providers, Provider, FactoryProvider } from './Providers'; -import { getLogger } from 'stryker-api/logging'; -import { Config } from 'stryker-api/config'; - -abstract class Injector { - public inject(injectable: Injectable): T { - const args: any[] = injectable.inject.map(key => this.provide(key, injectable)); - return new injectable(...args as any); - } - - public abstract provide(key: T, target: Injectable): CorrespondingType; - - public createChildInjector(context: Partial): Injector { - return new ChildInjector(this, context); - } - - public static create(pluginResolver: PluginResolver, options: Config): Injector { - return new RootInjector() - .createChildInjector({ - // TODO: Remove `config` once old way of loading plugins is gone - config: { value: options }, - getLogger: { value: getLogger }, - logger: { factory: Constructor => getLogger(Constructor.name) }, - options: { value: options }, - pluginResolver: { value: pluginResolver } - }); - } -} - -export default Injector; - -class RootInjector extends Injector { - public provide(key: T, target: Injectable): CorrespondingType { - throw new Error(`Can not inject "${target.name}". No provider found for "${key}".`); - } -} - -class ChildInjector extends Injector { - constructor(private readonly parent: Injector, private readonly context: Partial) { - super(); - } - - public provide(token: TToken, target: Injectable): CorrespondingType { - if (token === 'inject') { - return this.inject.bind(this); - } else if (typeof token === 'string') { - return this.provideStringToken(token as any, target); - } else { - return this.inject(token as any); - } - } - - private provideStringToken(token: T, target: Injectable) { - const provider: Provider> | undefined = this.context[token] as any; - if (provider) { - if (this.isFactoryProvider(provider)) { - return provider.factory(target); - } else { - return provider.value; - } - } else { - return this.parent.provide(token, target); - } - } - - private isFactoryProvider(provider: Provider): provider is FactoryProvider { - return !!(provider as FactoryProvider).factory; - } -} diff --git a/packages/stryker/src/di/PluginLoader.ts b/packages/stryker/src/di/PluginLoader.ts index 92c14dedab..cdf3fd7dd2 100644 --- a/packages/stryker/src/di/PluginLoader.ts +++ b/packages/stryker/src/di/PluginLoader.ts @@ -1,26 +1,27 @@ 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 { StrykerPlugin, PluginKind, tokens as tokens, InjectionToken, PluginResolver, CorrespondingTypes } from 'stryker-api/di'; +import { StrykerPlugin, PluginKind, PluginResolver, Plugins, PluginContexts } from 'stryker-api/di'; 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/src/mutant/MutatorFactory'; +import { MutatorFactory } from 'stryker-api/mutant'; const IGNORED_PACKAGES = ['stryker-cli', 'stryker-api']; interface PluginModule { - strykerPlugins: StrykerPlugin[]; + strykerPlugins: StrykerPlugin[]; } export default class PluginLoader implements PluginResolver { private readonly log = getLogger(PluginLoader.name); - private readonly pluginsByKind: Map[]> = new Map(); + private readonly pluginsByKind: Map[]> = new Map(); constructor(private readonly pluginDescriptors: string[]) { } @@ -29,18 +30,15 @@ export default class PluginLoader implements PluginResolver { this.loadDeprecatedPlugins(); } - public resolve(kind: PluginKind, name: string): StrykerPlugin { - return this.getPlugin(kind, name); - } - - public getPlugin(kind: PluginKind, name: string): StrykerPlugin { + public resolve(kind: T, name: string): Plugins[T] { const plugins = this.pluginsByKind.get(kind); if (plugins) { - const plugin = plugins.find(plugin => plugin.pluginName.toLowerCase() === name.toLowerCase()); + const plugin = plugins.find(plugin => plugin.name.toLowerCase() === name.toLowerCase()); if (plugin) { - return 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.pluginName).join(', ')}`); + 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?`); @@ -58,14 +56,14 @@ export default class PluginLoader implements PluginResolver { ([strykerOptions, fileNames]) => ({ strykerOptions, fileNames })); } - private loadDeprecatedPluginsFor( - kind: PluginKind, + private loadDeprecatedPluginsFor[], TSettings>( + kind: TPlugin, factory: Factory, - injectionTokens: TS, - settingsFactory: (args: CorrespondingTypes) => TSettings): void { + injectionTokens: Tokens, + settingsFactory: (args: CorrespondingTypes) => TSettings): void { factory.knownNames().forEach(name => { class ProxyPlugin { - constructor(...args: CorrespondingTypes) { + constructor(...args: CorrespondingTypes) { const realPlugin = factory.create(name, settingsFactory(args)); for (const i in realPlugin) { const method = (realPlugin as any)[i]; @@ -74,11 +72,9 @@ export default class PluginLoader implements PluginResolver { } } } - public static pluginName = name; - public static kind = kind; public static inject = injectionTokens; } - this.loadPlugin(ProxyPlugin); + this.loadPlugin({ kind, name, injectable: ProxyPlugin }); }); } @@ -135,7 +131,7 @@ export default class PluginLoader implements PluginResolver { } } - private loadPlugin(plugin: StrykerPlugin) { + private loadPlugin(plugin: StrykerPlugin) { let plugins = this.pluginsByKind.get(plugin.kind); if (!plugins) { plugins = []; diff --git a/packages/stryker/src/di/Providers.ts b/packages/stryker/src/di/Providers.ts deleted file mode 100644 index 64f3e8cd71..0000000000 --- a/packages/stryker/src/di/Providers.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { Container } from 'stryker-api/di'; - -export type Providers = { - readonly [K in keyof Container]: Provider; -}; - -export type Provider = ValueProvider | FactoryProvider; - -export interface ValueProvider { - value: T; -} - -export interface FactoryProvider { - factory(target: Function): T; -} diff --git a/packages/stryker/src/di/loggerFactory.ts b/packages/stryker/src/di/loggerFactory.ts new file mode 100644 index 0000000000..a6887ad6fa --- /dev/null +++ b/packages/stryker/src/di/loggerFactory.ts @@ -0,0 +1,8 @@ +import { LoggerFactoryMethod } from 'stryker-api/logging'; +import { tokens, commonTokens } from 'stryker-api/di'; +import { TARGET_TOKEN } from 'typed-inject'; + +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/reporters/BroadcastReporter.ts b/packages/stryker/src/reporters/BroadcastReporter.ts index c686a808c2..fa16f5fcdf 100644 --- a/packages/stryker/src/reporters/BroadcastReporter.ts +++ b/packages/stryker/src/reporters/BroadcastReporter.ts @@ -2,19 +2,21 @@ import { Reporter, SourceFile, MutantResult, MatchedMutant, ScoreResult } from ' import { Logger } from 'stryker-api/logging'; import { isPromise } from '../utils/objectUtils'; import StrictReporter from './StrictReporter'; -import { tokens, PluginResolver, PluginKind, Inject } from 'stryker-api/di'; +import { tokens, PluginResolver, PluginKind, PluginContext, commonTokens } from 'stryker-api/di'; import { StrykerOptions } from 'stryker-api/core'; +import { Injector, INJECTOR_TOKEN } from 'typed-inject'; export default class BroadcastReporter implements StrictReporter { - public static readonly inject = tokens('options', 'pluginResolver', 'inject', 'logger'); + public static readonly inject = tokens(commonTokens.options, commonTokens.pluginResolver, INJECTOR_TOKEN, commonTokens.logger); + public readonly reporters: { [name: string]: Reporter; }; constructor( private readonly options: StrykerOptions, private readonly pluginResolver: PluginResolver, - private readonly inject: Inject, + private readonly injector: Injector, private readonly log: Logger) { this.reporters = {}; this.options.reporters.forEach(reporterName => this.createReporter(reporterName)); @@ -29,7 +31,7 @@ export default class BroadcastReporter implements StrictReporter { reporterName = 'progress-append-only'; } const plugin = this.pluginResolver.resolve(PluginKind.Reporter, reporterName); - this.reporters[reporterName] = this.inject(plugin); + this.reporters[reporterName] = this.injector.injectClass(plugin.injectable); } private logAboutReporters(): void { diff --git a/packages/stryker/src/reporters/ClearTextReporter.ts b/packages/stryker/src/reporters/ClearTextReporter.ts index ad28d64058..a7b562b043 100644 --- a/packages/stryker/src/reporters/ClearTextReporter.ts +++ b/packages/stryker/src/reporters/ClearTextReporter.ts @@ -4,7 +4,7 @@ import { Reporter, MutantResult, MutantStatus, ScoreResult } from 'stryker-api/r import { Position, StrykerOptions } from 'stryker-api/core'; import ClearTextScoreTable from './ClearTextScoreTable'; import * as os from 'os'; -import { tokens, PluginKind } from 'stryker-api/di'; +import { tokens } from 'stryker-api/di'; export default class ClearTextReporter implements Reporter { @@ -15,8 +15,6 @@ export default class ClearTextReporter implements Reporter { } public static readonly inject = tokens('options'); - public static readonly pluginName = 'clear-text'; - public static readonly kind = PluginKind.Reporter; private readonly out: NodeJS.WritableStream = process.stdout; diff --git a/packages/stryker/src/reporters/DashboardReporter.ts b/packages/stryker/src/reporters/DashboardReporter.ts index b70acc7717..9471b38090 100644 --- a/packages/stryker/src/reporters/DashboardReporter.ts +++ b/packages/stryker/src/reporters/DashboardReporter.ts @@ -3,19 +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, PluginKind } from 'stryker-api/di'; +import { tokens } from 'stryker-api/di'; export default class DashboardReporter implements Reporter { - public static readonly inject = tokens('options'); - public static readonly pluginName = 'dashboard'; - public static readonly kind = PluginKind.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 d7205074b5..9ec9fc924c 100644 --- a/packages/stryker/src/reporters/DotsReporter.ts +++ b/packages/stryker/src/reporters/DotsReporter.ts @@ -1,12 +1,10 @@ import {Reporter, MutantResult, MutantStatus} from 'stryker-api/report'; import chalk from 'chalk'; import * as os from 'os'; -import { PluginKind, tokens } from 'stryker-api/di'; +import { tokens } from 'stryker-api/di'; export default class DotsReporter implements Reporter { public static readonly inject = tokens(); - public static readonly pluginName = 'dots'; - public static readonly kind = PluginKind.Reporter; public onMutantTested(result: MutantResult) { let toLog: string; diff --git a/packages/stryker/src/reporters/EventRecorderReporter.ts b/packages/stryker/src/reporters/EventRecorderReporter.ts index 7a34cfcd96..48588461b9 100644 --- a/packages/stryker/src/reporters/EventRecorderReporter.ts +++ b/packages/stryker/src/reporters/EventRecorderReporter.ts @@ -5,14 +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 { PluginKind, tokens } from 'stryker-api/di'; +import { tokens } from 'stryker-api/di'; const DEFAULT_BASE_FOLDER = 'reports/mutation/events'; export default class EventRecorderReporter implements StrictReporter { public static readonly inject = tokens('options'); - public static readonly pluginName = 'event-recorder'; - public static readonly kind = PluginKind.Reporter; private readonly log = getLogger(EventRecorderReporter.name); private readonly allWork: Promise[] = []; diff --git a/packages/stryker/src/reporters/ProgressAppendOnlyReporter.ts b/packages/stryker/src/reporters/ProgressAppendOnlyReporter.ts index d2c9955a27..253009c848 100644 --- a/packages/stryker/src/reporters/ProgressAppendOnlyReporter.ts +++ b/packages/stryker/src/reporters/ProgressAppendOnlyReporter.ts @@ -1,13 +1,11 @@ import { MatchedMutant } from 'stryker-api/report'; import * as os from 'os'; import ProgressKeeper from './ProgressKeeper'; -import { tokens, PluginKind } from 'stryker-api/di'; +import { tokens } from 'stryker-api/di'; export default class ProgressAppendOnlyReporter extends ProgressKeeper { private intervalReference: NodeJS.Timer; public static readonly inject = tokens(); - public static readonly pluginName = 'progress-append-only'; - public static readonly kind = PluginKind.Reporter; public onAllMutantsMatchedWithTests(matchedMutants: ReadonlyArray): void { super.onAllMutantsMatchedWithTests(matchedMutants); diff --git a/packages/stryker/src/reporters/ProgressReporter.ts b/packages/stryker/src/reporters/ProgressReporter.ts index 4e0e2be0ec..f25930dd13 100644 --- a/packages/stryker/src/reporters/ProgressReporter.ts +++ b/packages/stryker/src/reporters/ProgressReporter.ts @@ -1,13 +1,11 @@ import { MatchedMutant, MutantResult } from 'stryker-api/report'; import ProgressKeeper from './ProgressKeeper'; import ProgressBar from './ProgressBar'; -import { tokens, PluginKind } from 'stryker-api/di'; +import { tokens } from 'stryker-api/di'; export default class ProgressBarReporter extends ProgressKeeper { private progressBar: ProgressBar; public static readonly inject = tokens(); - public static readonly pluginName = 'progress'; - public static readonly kind = PluginKind.Reporter; public onAllMutantsMatchedWithTests(matchedMutants: ReadonlyArray): void { super.onAllMutantsMatchedWithTests(matchedMutants); 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 index 1d283b4382..883f83afdb 100644 --- a/packages/stryker/src/reporters/index.ts +++ b/packages/stryker/src/reporters/index.ts @@ -4,13 +4,14 @@ import ProgressAppendOnlyReporter from './ProgressAppendOnlyReporter'; import DotsReporter from './DotsReporter'; import EventRecorderReporter from './EventRecorderReporter'; import DashboardReporter from './DashboardReporter'; -import { plugins } from 'stryker-api/di'; +import { reporterPlugin } from 'stryker-api/report'; +import { PluginKind } from 'stryker-api/di'; -export const strykerPlugins = plugins( - ClearTextReporter, - ProgressReporter, - ProgressAppendOnlyReporter, - DotsReporter, - EventRecorderReporter, - DashboardReporter -); +export const strykerPlugins = [ + reporterPlugin({ name: 'clear-text', kind: PluginKind.Reporter, injectable: ClearTextReporter }), + reporterPlugin({ name: 'progress', kind: PluginKind.Reporter, injectable: ProgressReporter }), + reporterPlugin({ name: 'progress-append-only', kind: PluginKind.Reporter, injectable: ProgressAppendOnlyReporter }), + reporterPlugin({ name: 'dots', kind: PluginKind.Reporter, injectable: DotsReporter }), + reporterPlugin({ name: 'event-recorder', kind: PluginKind.Reporter, injectable: EventRecorderReporter }), + reporterPlugin({ name: 'dashboard', kind: PluginKind.Reporter, injectable: 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/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/initSinon.ts b/packages/stryker/test/helpers/initSinon.ts index c765bbb8bf..00ee898357 100644 --- a/packages/stryker/test/helpers/initSinon.ts +++ b/packages/stryker/test/helpers/initSinon.ts @@ -1,7 +1,7 @@ import * as sinon from 'sinon'; -import { TestInjector } from '../../../stryker-test-helpers/src'; +import { testInjector } from '@stryker-mutator/test-helpers'; afterEach(() => { sinon.restore(); - TestInjector.reset(); + testInjector.reset(); }); diff --git a/packages/stryker/test/unit/StrykerSpec.ts b/packages/stryker/test/unit/StrykerSpec.ts index 30e8a582db..eb34bbb986 100644 --- a/packages/stryker/test/unit/StrykerSpec.ts +++ b/packages/stryker/test/unit/StrykerSpec.ts @@ -9,7 +9,7 @@ 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 Injector from '../../src/di/Injector'; +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'; @@ -25,6 +25,7 @@ import TestableMutant from '../../src/TestableMutant'; import InputFileCollection from '../../src/input/InputFileCollection'; import LogConfigurator from '../../src/logging/LogConfigurator'; import LoggingClientContext from '../../src/logging/LoggingClientContext'; +import { PluginContext } from 'stryker-api/di'; class FakeConfigEditor implements ConfigEditor { constructor() { } @@ -57,7 +58,7 @@ describe('Stryker', () => { let configureMainProcessStub: sinon.SinonStub; let configureLoggingServerStub: sinon.SinonStub; let shutdownLoggingStub: sinon.SinonStub; - let injectorMock: Mock; + let injectorMock: Mock>; beforeEach(() => { strykerConfig = config(); @@ -67,7 +68,17 @@ describe('Stryker', () => { configReaderMock.readConfig.returns(strykerConfig); pluginLoaderMock = mock(PluginLoader); - injectorMock = mock(Injector); + 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 = sinon.stub(LogConfigurator, 'configureMainProcess'); @@ -75,7 +86,7 @@ describe('Stryker', () => { shutdownLoggingStub = sinon.stub(LogConfigurator, 'shutdown'); configureLoggingServerStub.resolves(LOGGING_CONTEXT); inputFileResolverMock = mock(InputFileResolver); - injectorMock.inject.returns(reporter); + injectorMock.injectClass.returns(reporter); testFramework = testFrameworkMock(); initialTestExecutorMock = mock(InitialTestExecutor); mutationTestExecutorMock = mock(MutationTestExecutor); @@ -85,7 +96,7 @@ describe('Stryker', () => { sinon.stub(initialTestExecutor, 'default').returns(initialTestExecutorMock); sinon.stub(configValidator, 'default').returns(configValidatorMock); sinon.stub(testFrameworkOrchestrator, 'default').returns(testFrameworkOrchestratorMock); - sinon.stub(Injector, 'create').returns(injectorMock); + sinon.stub(typedInject, 'rootInjector').value(injectorMock); sinon.stub(mutatorFacade, 'default').returns(mutatorMock); sinon.stub(mutantRunResultMatcher, 'default').returns(mutantRunResultMatcherMock); sinon.stub(configReader, 'default').returns(configReaderMock); diff --git a/packages/stryker/test/unit/di/Injector.spec.ts b/packages/stryker/test/unit/di/Injector.spec.ts deleted file mode 100644 index 7f5b956fdf..0000000000 --- a/packages/stryker/test/unit/di/Injector.spec.ts +++ /dev/null @@ -1,134 +0,0 @@ -import { tokens, PluginResolver, token } from 'stryker-api/di'; -import { Config } from 'stryker-api/config'; -import { Logger, LoggerFactoryMethod, getLogger } from 'stryker-api/logging'; -import Injector from '../../../src/di/Injector'; -import { StrykerOptions } from 'stryker-api/core'; -import * as sinon from 'sinon'; -import { expect } from 'chai'; -import currentLogMock from '../../helpers/logMock'; -import { strykerOptions } from '../../helpers/producers'; - -describe('Injector', () => { - - let pluginResolver: PluginResolver; - let config: Config; - let sut: Injector; - - beforeEach(() => { - pluginResolver = { - resolve: sinon.stub() - }; - config = new Config(); - sut = Injector.create(pluginResolver, config); - }); - - describe('RootInjector', () => { - - it('should be able to inject config, log, getLogger, options and pluginResolver class', () => { - // Arrange - class Injectable { - constructor( - public readonly config: Config, - public readonly log: Logger, - public readonly getLogger: LoggerFactoryMethod, - public readonly options: StrykerOptions, - public readonly pluginResolver: PluginResolver) { - } - public static inject = tokens('config', 'logger', 'getLogger', 'options', 'pluginResolver'); - } - - // Act - const actual = sut.inject(Injectable); - - // Assert - expect(actual.config).eq(config); - expect(actual.options).eq(config); - expect(actual.log).eq(currentLogMock()); - expect(actual.getLogger).eq(getLogger); - expect(actual.pluginResolver).eq(pluginResolver); - }); - - it('should throw when no provider was found', () => { - expect(() => sut.inject(class FileNamesInjectable { - constructor(public fileNames: ReadonlyArray) { - } - public static inject = tokens('sandboxFileNames'); - })).throws('Can not inject "FileNamesInjectable". No provider found for "sandboxFileNames".'); - }); - }); - - describe('ChildInjector', () => { - - it('should be able to inject testFileNames', () => { - const expectedFileNames = Object.freeze(['foo.js', 'bar.js']); - const child = sut.createChildInjector({ - sandboxFileNames: { - value: expectedFileNames - } - }); - const actual = child.inject(class { - constructor(public sandboxFileNames: ReadonlyArray) { } - public static inject = tokens('sandboxFileNames'); - }); - expect(actual.sandboxFileNames).eq(expectedFileNames); - }); - - it('should be able still provide parent injector values', () => { - const expectedOptions = strykerOptions(); - const child = sut.createChildInjector({ - options: { - value: expectedOptions - } - }); - const actual = child.inject(class { - constructor(public options: StrykerOptions, public getLogger: LoggerFactoryMethod) { } - public static inject = tokens('options', 'getLogger'); - }); - expect(actual.options).eq(expectedOptions); - expect(actual.getLogger).eq(getLogger); - }); - }); - - it('should be able to inject a dependency tree', () => { - // Arrange - 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', token(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(token(Child1), token(Child2), 'logger'); - } - - // Act - const actual = sut.inject(Parent); - - // Assert - expect(actual.child.bar).eq('foo'); - expect(actual.child2.foo).eq('bar'); - expect(actual.child.log).eq(currentLogMock()); - expect(actual.child2.log).eq(currentLogMock()); - expect(actual.child.grandchild.log).eq(currentLogMock()); - expect(actual.child.grandchild.baz).eq('qux'); - expect(actual.log).eq(currentLogMock()); - }); - -}); diff --git a/packages/stryker/test/unit/input/InputFileResolverSpec.ts b/packages/stryker/test/unit/input/InputFileResolverSpec.ts index 73b354bea8..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[] => diff --git a/packages/stryker/test/unit/reporters/BroadcastReporterSpec.ts b/packages/stryker/test/unit/reporters/BroadcastReporterSpec.ts index 8838b269c2..b1854ae768 100644 --- a/packages/stryker/test/unit/reporters/BroadcastReporterSpec.ts +++ b/packages/stryker/test/unit/reporters/BroadcastReporterSpec.ts @@ -1,40 +1,23 @@ import { expect } from 'chai'; import BroadcastReporter from '../../../src/reporters/BroadcastReporter'; import { ALL_REPORTER_EVENTS } from '../../helpers/producers'; -import { PluginKind, StrykerPlugin } from 'stryker-api/di'; +import { PluginKind, tokens } from 'stryker-api/di'; import * as sinon from 'sinon'; -import ProgressAppendOnlyReporter from '../../../src/reporters/ProgressAppendOnlyReporter'; -import ProgressReporter from '../../../src/reporters/ProgressReporter'; -import { TestInjector } from '@stryker-mutator/test-helpers'; +import { testInjector, factory } from '@stryker-mutator/test-helpers'; +import { Reporter, ReporterPlugin } from 'stryker-api/report'; describe('BroadcastReporter', () => { let sut: BroadcastReporter; - let rep1: any; - let rep2: any; + let rep1Plugin: MockedReporterPlugin; + let rep2Plugin: MockedReporterPlugin; let isTTY: boolean; beforeEach(() => { - rep1 = mockReporter('rep1'); - rep2 = mockReporter('rep2'); captureTTY(); - TestInjector.options.reporters = ['rep1', 'rep2']; - const rep1Plugin: StrykerPlugin = class { - public static readonly pluginName = 'rep1'; - public static readonly inject: [] = []; - public static readonly kind = PluginKind.Reporter; - }; - const rep2Plugin: StrykerPlugin = class { - public static readonly pluginName = 'rep2'; - public static readonly inject: [] = []; - public static readonly kind = PluginKind.Reporter; - }; - TestInjector.pluginResolver.resolve - .withArgs(PluginKind.Reporter, rep1Plugin.pluginName).returns(rep1Plugin) - .withArgs(PluginKind.Reporter, rep2Plugin.pluginName).returns(rep2Plugin); - TestInjector - .stub(rep1Plugin, rep1) - .stub(rep2Plugin, rep2); + testInjector.options.reporters = ['rep1', 'rep2']; + rep1Plugin = mockReporterPlugin('rep1'); + rep2Plugin = mockReporterPlugin('rep2'); }); afterEach(() => { @@ -45,40 +28,36 @@ describe('BroadcastReporter', () => { it('should create "progress-append-only" instead of "progress" reporter if process.stdout is not a tty', () => { // Arrange setTTY(false); - const expectedReporter = mockReporter('progress-append-only'); - TestInjector.options.reporters = ['progress']; - TestInjector.pluginResolver.resolve.withArgs(PluginKind.Reporter, 'progress-append-only').returns(ProgressAppendOnlyReporter); - TestInjector.stub(ProgressAppendOnlyReporter, expectedReporter); + const expectedReporterPlugin = mockReporterPlugin('progress-append-only'); + testInjector.options.reporters = ['progress']; // Act sut = createSut(); // Assert - expect(sut.reporters).deep.eq({ 'progress-append-only': expectedReporter }); + expect(sut.reporters).deep.eq({ 'progress-append-only': new expectedReporterPlugin.injectable() }); }); it('should create the correct reporters', () => { // Arrange setTTY(true); - const expectedReporter = mockReporter('progress'); - TestInjector.options.reporters = ['progress', 'rep2']; - TestInjector.pluginResolver.resolve.withArgs(PluginKind.Reporter, 'progress').returns(ProgressReporter); - TestInjector.stub(ProgressReporter, expectedReporter); + testInjector.options.reporters = ['progress', 'rep2']; + const progressReporterPlugin = mockReporterPlugin('progress'); // Act sut = createSut(); // Assert expect(sut.reporters).deep.eq({ - progress: expectedReporter, - rep2 + progress: progressReporterPlugin.reporterStub, + rep2: rep2Plugin.reporterStub }); }); it('should warn if there is no reporter', () => { - TestInjector.options.reporters = []; + testInjector.options.reporters = []; sut = createSut(); - expect(TestInjector.logger.warn).calledWith(sinon.match('No reporter configured')); + expect(testInjector.logger.warn).calledWith(sinon.match('No reporter configured')); }); }); @@ -100,11 +79,11 @@ describe('BroadcastReporter', () => { beforeEach(() => { isResolved = false; - rep1.wrapUp.returns(new Promise((resolve, reject) => { + rep1Plugin.reporterStub.wrapUp.returns(new Promise((resolve, reject) => { wrapUpResolveFn = resolve; wrapUpRejectFn = reject; })); - rep2.wrapUp.returns(new Promise(resolve => wrapUpResolveFn2 = resolve)); + rep2Plugin.reporterStub.wrapUp.returns(new Promise(resolve => wrapUpResolveFn2 = resolve)); result = sut.wrapUp().then(() => void (isResolved = true)); }); @@ -125,7 +104,7 @@ describe('BroadcastReporter', () => { it('should not result in a rejection', () => result); it('should log the error', () => { - expect(TestInjector.logger.error).calledWith(`An error occurred during 'wrapUp' on reporter 'rep1'. Error is: some error`); + expect(testInjector.logger.error).calledWith(`An error occurred during 'wrapUp' on reporter 'rep1'. Error is: some error`); }); }); }); @@ -133,7 +112,7 @@ describe('BroadcastReporter', () => { describe('with one faulty reporter', () => { beforeEach(() => { - ALL_REPORTER_EVENTS.forEach(eventName => rep1[eventName].throws('some error')); + ALL_REPORTER_EVENTS.forEach(eventName => rep1Plugin.reporterStub[eventName].throws('some error')); }); it('should still broadcast to other reporters', () => { @@ -143,7 +122,7 @@ describe('BroadcastReporter', () => { 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`); + expect(testInjector.logger.error).to.have.been.calledWith(`An error occurred during '${eventName}' on reporter 'rep1'. Error is: some error`); }); }); @@ -152,21 +131,36 @@ describe('BroadcastReporter', () => { }); function createSut() { - return TestInjector.inject(BroadcastReporter); + return testInjector.injector.injectClass(BroadcastReporter); } - function mockReporter(name: string) { - const reporter: any = { name }; - ALL_REPORTER_EVENTS.forEach(event => reporter[event] = sinon.stub()); - return reporter; + type MockedReporterPlugin = ReporterPlugin<[]> & { reporterStub: sinon.SinonStubbedInstance>} ; + + function mockReporterPlugin(name: string): MockedReporterPlugin { + const reporterStub = factory.reporter(); + (reporterStub as any)[name] = name; + const reporterPlugin: MockedReporterPlugin = { + injectable: class MockReporterPlugin { + constructor() { + return reporterStub; + } + public static inject = tokens(); + }, + kind: PluginKind.Reporter, + name, + reporterStub + }; + testInjector.pluginResolver.resolve + .withArgs(PluginKind.Reporter, reporterPlugin.name).returns(reporterPlugin); + return reporterPlugin; } function actArrangeAssertAllEvents() { ALL_REPORTER_EVENTS.forEach(eventName => { const eventData = eventName === 'wrapUp' ? undefined : eventName; (sut as any)[eventName](eventName); - expect(rep1[eventName]).to.have.been.calledWith(eventData); - expect(rep2[eventName]).to.have.been.calledWith(eventData); + expect(rep1Plugin.reporterStub[eventName]).to.have.been.calledWith(eventData); + expect(rep2Plugin.reporterStub[eventName]).to.have.been.calledWith(eventData); }); } diff --git a/packages/stryker/test/unit/reporters/DashboardReporterSpec.ts b/packages/stryker/test/unit/reporters/DashboardReporterSpec.ts index 0db73087b5..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; @@ -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/test-runner/CommandTestRunnerSpec.ts b/packages/stryker/test/unit/test-runner/CommandTestRunnerSpec.ts index c1cc1e0114..0bf280bde2 100644 --- a/packages/stryker/test/unit/test-runner/CommandTestRunnerSpec.ts +++ b/packages/stryker/test/unit/test-runner/CommandTestRunnerSpec.ts @@ -9,6 +9,7 @@ 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, () => { @@ -74,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/transpiler/MutantTranspilerSpec.ts b/packages/stryker/test/unit/transpiler/MutantTranspilerSpec.ts index afc937cc67..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 { 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, 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/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/package.json b/packages/typed-inject/package.json new file mode 100644 index 0000000000..6490a854ba --- /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 85 npm run mocha", + "mocha": "mocha \"test/helpers/**/*.js\" \"test/unit/**/*.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..2c36394d22 --- /dev/null +++ b/packages/typed-inject/src/InjectorImpl.ts @@ -0,0 +1,161 @@ +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[] { + return (injectable.inject || [] as InjectionToken[]).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..0d262d50ef --- /dev/null +++ b/packages/typed-inject/src/api/CorrespondingType.ts @@ -0,0 +1,9 @@ +import { InjectionToken, InjectorToken, TargetToken } from './InjectionToken'; +import { Injector } from './Injector'; + +export type CorrespondingType> = + T extends keyof TContext ? TContext[T] : T extends InjectorToken ? Injector : T extends TargetToken ? Function | undefined : 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..9d59ff97ca --- /dev/null +++ b/packages/typed-inject/src/api/Injectable.ts @@ -0,0 +1,14 @@ +import { CorrespondingTypes } from './CorrespondingType'; +import { InjectionToken } from './InjectionToken'; + +export interface InjectableClass[]> { + new(...args: CorrespondingTypes): R; + readonly inject?: Tokens; +} + +export interface InjectableFunction[]> { + (...args: CorrespondingTypes): R; + readonly inject?: Tokens; +} + +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..be0876647e --- /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 = keyof TContext | InjectorToken | TargetToken; 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..6fa2601577 --- /dev/null +++ b/packages/typed-inject/src/tokens.ts @@ -0,0 +1,4 @@ + +export function tokens(...tokens: TS): TS { + return tokens; +} 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..b2ae51cd4d --- /dev/null +++ b/packages/typed-inject/test/unit/Injector.spec.ts @@ -0,0 +1,201 @@ +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); + }); + }); + + 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/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..16f2e5e5b3 --- /dev/null +++ b/packages/typed-inject/tsconfig.test.json @@ -0,0 +1,17 @@ +{ + "extends": "../../tsconfig.settings.json", + "compilerOptions": { + "rootDir": ".", + "types": [ + "mocha" + ] + }, + "include": [ + "test" + ], + "references": [ + { + "path": "tsconfig.src.json", + }, + ] +} \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json index 56e2bcc3c9..6b5f163b3e 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -18,6 +18,7 @@ { "path": "packages/stryker-vue-mutator" }, { "path": "packages/stryker-wct-runner" }, { "path": "packages/stryker-webpack-transpiler" }, - { "path": "packages/stryker-test-helpers" } + { "path": "packages/stryker-test-helpers" }, + { "path": "packages/typed-inject" } ] } diff --git a/workspace.code-workspace b/workspace.code-workspace index 12d961e4f2..4f0dd0cf14 100644 --- a/workspace.code-workspace +++ b/workspace.code-workspace @@ -60,6 +60,9 @@ { "path": "e2e" }, + { + "path": "packages/typed-inject" + }, { "path": ".", "name": "stryker-parent" From 22c1902f284c47a8d7901da52698a63cf991e65c Mon Sep 17 00:00:00 2001 From: Nico Jansen Date: Mon, 7 Jan 2019 12:59:32 +0100 Subject: [PATCH 10/36] Test: Add unit tests for `StrykerError` and `errorToString` --- .../test/unit/StrykerError.spec.ts | 17 ++++++++++ .../stryker-util/test/unit/errors.spec.ts | 34 +++++++++++++++++++ 2 files changed, 51 insertions(+) create mode 100644 packages/stryker-util/test/unit/StrykerError.spec.ts create mode 100644 packages/stryker-util/test/unit/errors.spec.ts 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'); + }); + }); +}); From 48acafae617b4430168f8eb546df41aa595a3743 Mon Sep 17 00:00:00 2001 From: Nico Jansen Date: Mon, 7 Jan 2019 12:59:53 +0100 Subject: [PATCH 11/36] Test: Update tests for stryker-api --- .../install-module/install-module.ts | 2 +- .../testResources/module/package.json | 5 +- .../testResources/module/useCore.ts | 54 ++++++++++++------- 3 files changed, 40 insertions(+), 21 deletions(-) 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/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); From 014a1675c974581774ec11cd9f81a9e8a4ea477a Mon Sep 17 00:00:00 2001 From: Nico Jansen Date: Mon, 7 Jan 2019 13:00:18 +0100 Subject: [PATCH 12/36] Add npmignore and npmrc to typed-inject --- packages/typed-inject/.npmignore | 11 +++++++++++ packages/typed-inject/.npmrc | 1 + packages/typed-inject/package.json | 2 +- 3 files changed, 13 insertions(+), 1 deletion(-) create mode 100644 packages/typed-inject/.npmignore create mode 100644 packages/typed-inject/.npmrc 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/package.json b/packages/typed-inject/package.json index 6490a854ba..8916aa0d66 100644 --- a/packages/typed-inject/package.json +++ b/packages/typed-inject/package.json @@ -5,7 +5,7 @@ "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 85 npm run mocha", + "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\" \"test/integration/**/*.js\"" }, "repository": { From e5d230b05fb44491438d73411f8072ac35bcc94a Mon Sep 17 00:00:00 2001 From: Nico Jansen Date: Mon, 7 Jan 2019 16:04:39 +0100 Subject: [PATCH 13/36] Rename - plugins -> Plugins part 1 --- packages/stryker-api/src/di/{plugins.ts => Pluginss.ts} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename packages/stryker-api/src/di/{plugins.ts => Pluginss.ts} (100%) diff --git a/packages/stryker-api/src/di/plugins.ts b/packages/stryker-api/src/di/Pluginss.ts similarity index 100% rename from packages/stryker-api/src/di/plugins.ts rename to packages/stryker-api/src/di/Pluginss.ts From 4fac5eea42d6bec3bff79088bc4e1b7b6fdf21c5 Mon Sep 17 00:00:00 2001 From: Nico Jansen Date: Mon, 7 Jan 2019 16:05:01 +0100 Subject: [PATCH 14/36] Rename - plugins -> Plugins part 2 --- packages/stryker-api/src/di/{Pluginss.ts => Plugins.ts} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename packages/stryker-api/src/di/{Pluginss.ts => Plugins.ts} (100%) diff --git a/packages/stryker-api/src/di/Pluginss.ts b/packages/stryker-api/src/di/Plugins.ts similarity index 100% rename from packages/stryker-api/src/di/Pluginss.ts rename to packages/stryker-api/src/di/Plugins.ts From a609213c7bf2a15c902f56596a568bd1a8ef815e Mon Sep 17 00:00:00 2001 From: Nico Jansen Date: Mon, 7 Jan 2019 16:50:47 +0100 Subject: [PATCH 15/36] Update plugin loader to load from correct node_modules directory --- packages/stryker/src/di/PluginLoader.ts | 4 ++-- packages/stryker/test/unit/di/PluginLoaderSpec.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/stryker/src/di/PluginLoader.ts b/packages/stryker/src/di/PluginLoader.ts index cdf3fd7dd2..36a2bb95e1 100644 --- a/packages/stryker/src/di/PluginLoader.ts +++ b/packages/stryker/src/di/PluginLoader.ts @@ -85,8 +85,8 @@ export default class PluginLoader implements PluginResolver { 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.resolve(__dirname, '..', '..'); + // 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); diff --git a/packages/stryker/test/unit/di/PluginLoaderSpec.ts b/packages/stryker/test/unit/di/PluginLoaderSpec.ts index 853c36d91a..241d9e409f 100644 --- a/packages/stryker/test/unit/di/PluginLoaderSpec.ts +++ b/packages/stryker/test/unit/di/PluginLoaderSpec.ts @@ -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"', () => { From 8f66a3960f0af6e1effbe85e6e9e9646db9f7aa0 Mon Sep 17 00:00:00 2001 From: Nico Jansen Date: Mon, 7 Jan 2019 16:51:18 +0100 Subject: [PATCH 16/36] Add typed-inject to local install of e2e tests --- e2e/package.json | 3 ++- e2e/test/angular-project/package.json | 3 ++- e2e/test/jest-react/package.json | 3 ++- e2e/test/polymer-project/package.json | 3 ++- e2e/test/vue-javascript/package.json | 3 ++- 5 files changed, 10 insertions(+), 5 deletions(-) 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.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", From 46711ed758997a38be23ae927bc351628f9689d2 Mon Sep 17 00:00:00 2001 From: Nico Jansen Date: Mon, 7 Jan 2019 18:53:26 +0100 Subject: [PATCH 17/36] Add missing dependency to `polymer-project` e2e test --- e2e/test/polymer-project/package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/e2e/test/polymer-project/package.json b/e2e/test/polymer-project/package.json index 686b8a9e53..7c9d55cae9 100644 --- a/e2e/test/polymer-project/package.json +++ b/e2e/test/polymer-project/package.json @@ -27,6 +27,7 @@ }, "localDependencies": { "stryker": "../../../packages/stryker", + "stryker-api": "../../../packages/stryker-api", "stryker-wct-runner": "../../../packages/stryker-wct-runner", "stryker-javascript-mutator": "../../../packages/stryker-javascript-mutator", "@stryker-mutator/util": "../../../packages/stryker-util", From a781d7c8216391df39256a8b8fbd79fd50cabdb5 Mon Sep 17 00:00:00 2001 From: Nico Jansen Date: Mon, 7 Jan 2019 19:14:30 +0100 Subject: [PATCH 18/36] Add missing dependency to `jest-react` e2e test --- e2e/test/jest-react/package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/e2e/test/jest-react/package.json b/e2e/test/jest-react/package.json index ece47819dd..3a73ee4eff 100644 --- a/e2e/test/jest-react/package.json +++ b/e2e/test/jest-react/package.json @@ -86,6 +86,7 @@ }, "localDependencies": { "stryker": "../../../packages/stryker", + "stryker-api": "../../../packages/stryker-api", "stryker-javascript-mutator": "../../../packages/stryker-javascript-mutator", "stryker-jest-runner": "../../../packages/stryker-jest-runner", "@stryker-mutator/util": "../../../packages/stryker-util", From b22365d1e426bc1ad9d33ae5d4efc2fd01371659 Mon Sep 17 00:00:00 2001 From: Nico Jansen Date: Mon, 7 Jan 2019 22:35:52 +0100 Subject: [PATCH 19/36] Remove stryker-api dependency from stryker-util --- e2e/test/angular-project/package-lock.json | 28 ++++++++++++++++------ e2e/test/jest-react/package.json | 1 - e2e/test/polymer-project/package.json | 1 - packages/stryker-util/package.json | 5 +--- packages/stryker-util/tsconfig.src.json | 6 +---- 5 files changed, 23 insertions(+), 18 deletions(-) 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/jest-react/package.json b/e2e/test/jest-react/package.json index 3a73ee4eff..ece47819dd 100644 --- a/e2e/test/jest-react/package.json +++ b/e2e/test/jest-react/package.json @@ -86,7 +86,6 @@ }, "localDependencies": { "stryker": "../../../packages/stryker", - "stryker-api": "../../../packages/stryker-api", "stryker-javascript-mutator": "../../../packages/stryker-javascript-mutator", "stryker-jest-runner": "../../../packages/stryker-jest-runner", "@stryker-mutator/util": "../../../packages/stryker-util", diff --git a/e2e/test/polymer-project/package.json b/e2e/test/polymer-project/package.json index 7c9d55cae9..686b8a9e53 100644 --- a/e2e/test/polymer-project/package.json +++ b/e2e/test/polymer-project/package.json @@ -27,7 +27,6 @@ }, "localDependencies": { "stryker": "../../../packages/stryker", - "stryker-api": "../../../packages/stryker-api", "stryker-wct-runner": "../../../packages/stryker-wct-runner", "stryker-javascript-mutator": "../../../packages/stryker-javascript-mutator", "@stryker-mutator/util": "../../../packages/stryker-util", diff --git a/packages/stryker-util/package.json b/packages/stryker-util/package.json index d60ea67551..a38b058d75 100644 --- a/packages/stryker-util/package.json +++ b/packages/stryker-util/package.json @@ -25,8 +25,5 @@ "bugs": { "url": "https://github.com/stryker-mutator/stryker/issues" }, - "homepage": "https://github.com/stryker-mutator/stryker/tree/master/packages/@stryker-mutator/util#readme", - "dependencies": { - "stryker-api": "^0.23.0" - } + "homepage": "https://github.com/stryker-mutator/stryker/tree/master/packages/@stryker-mutator/util#readme" } diff --git a/packages/stryker-util/tsconfig.src.json b/packages/stryker-util/tsconfig.src.json index 4b7dbc31e7..809734fd2b 100644 --- a/packages/stryker-util/tsconfig.src.json +++ b/packages/stryker-util/tsconfig.src.json @@ -6,9 +6,5 @@ "include": [ "src" ], - "references": [ - { - "path": "../stryker-api/tsconfig.src.json" - } - ] + "references": [ ] } \ No newline at end of file From 90f0fcb664ae410efc45888432ea3013f2d3e659 Mon Sep 17 00:00:00 2001 From: Nico Jansen Date: Tue, 8 Jan 2019 12:30:09 +0100 Subject: [PATCH 20/36] docs(typed-inject): Add README --- packages/typed-inject/README.md | 242 ++++++++++++++++++++++++++++++++ 1 file changed, 242 insertions(+) create mode 100644 packages/typed-inject/README.md diff --git a/packages/typed-inject/README.md b/packages/typed-inject/README.md new file mode 100644 index 0000000000..b73fbf55f4 --- /dev/null +++ b/packages/typed-inject/README.md @@ -0,0 +1,242 @@ +[![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'/'In version 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 './src'; + +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` is 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 './src'; + +// 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. + +## πŸ’­ 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 heart felled thanks! πŸ’– + +Inspiration for the API with static `inject` method comes from years long AngularJS development. Special thanks to the Angular team. + From 8be835803e9c47de7d0161fec6e700ad0b7a2def Mon Sep 17 00:00:00 2001 From: Nico Jansen Date: Tue, 8 Jan 2019 15:07:19 +0100 Subject: [PATCH 21/36] docs(typed-inject): Add "magic tokens" section to readme --- packages/typed-inject/README.md | 26 ++++++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/packages/typed-inject/README.md b/packages/typed-inject/README.md index b73fbf55f4..3483709517 100644 --- a/packages/typed-inject/README.md +++ b/packages/typed-inject/README.md @@ -32,7 +32,7 @@ _Note: this package uses advanced TypeScript features. Only TS 3.0 and above is An example: ```ts -import { rootInjector, tokens } from './src'; +import { rootInjector, tokens } from 'typed-inject'; interface Logger { info(message: string): void; @@ -72,7 +72,7 @@ Dependencies are resolved using the static `inject` property on their classes. T Expect compiler errors when you mess up the order of tokens or forget it completely. ```ts -import { rootInjector, tokens } from './src'; +import { rootInjector, tokens } from 'typed-inject'; // Same logger as before @@ -96,6 +96,28 @@ 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 +----- +| `TARGET_TOKEN` | `'$target'` | The class or function in which the current values is injected, or `undefined` if resolved directly | +| `INJECTOR_TOKEN` | `'$injector'` | Injects the current injector | + +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. From dfeb4b0fa2b3f2bd69305b79863ba0de66ad8836 Mon Sep 17 00:00:00 2001 From: Nico Jansen Date: Tue, 8 Jan 2019 15:11:09 +0100 Subject: [PATCH 22/36] docs(typed-html): add gh markdown table --- packages/typed-inject/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/typed-inject/README.md b/packages/typed-inject/README.md index 3483709517..fc71777fbe 100644 --- a/packages/typed-inject/README.md +++ b/packages/typed-inject/README.md @@ -100,8 +100,8 @@ The error messages are a bit cryptic at times, but it sure is better than runnin Any `Injector` instance can always inject the following tokens: -| Token name | Token value | Description ------ +| Token name | Token value | Description | +| - | - | - | | `TARGET_TOKEN` | `'$target'` | The class or function in which the current values is injected, or `undefined` if resolved directly | | `INJECTOR_TOKEN` | `'$injector'` | Injects the current injector | From dabc80ae376b5dc659ea6d87730732550df08514 Mon Sep 17 00:00:00 2001 From: Nico Jansen Date: Tue, 8 Jan 2019 21:06:02 +0100 Subject: [PATCH 23/36] test(typed-inject): Add integration tests --- packages/typed-inject/package.json | 2 +- packages/typed-inject/src/InjectorImpl.ts | 3 +- packages/typed-inject/src/api/Injectable.ts | 18 +++- .../test/helpers/initSourceMaps.ts | 1 + .../test/integration/typed-inject.it.spec.ts | 85 +++++++++++++++++++ .../testResources/dependency-graph.ts | 23 +++++ .../testResources/forgot-tokens-class.ts | 6 ++ .../testResources/forgot-tokens-function.ts | 4 + packages/typed-inject/tsconfig.test.json | 3 +- 9 files changed, 138 insertions(+), 7 deletions(-) create mode 100644 packages/typed-inject/test/helpers/initSourceMaps.ts create mode 100644 packages/typed-inject/test/integration/typed-inject.it.spec.ts create mode 100644 packages/typed-inject/testResources/dependency-graph.ts create mode 100644 packages/typed-inject/testResources/forgot-tokens-class.ts create mode 100644 packages/typed-inject/testResources/forgot-tokens-function.ts diff --git a/packages/typed-inject/package.json b/packages/typed-inject/package.json index 8916aa0d66..d0ecde8408 100644 --- a/packages/typed-inject/package.json +++ b/packages/typed-inject/package.json @@ -6,7 +6,7 @@ "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\" \"test/integration/**/*.js\"" + "mocha": "mocha \"test/helpers/**/*.js\" \"test/unit/**/*.js\" && mocha --timeout 20000 \"test/helpers/**/*.js\" \"test/integration/**/*.js\" " }, "repository": { "type": "git", diff --git a/packages/typed-inject/src/InjectorImpl.ts b/packages/typed-inject/src/InjectorImpl.ts index 2c36394d22..b99e9e39c2 100644 --- a/packages/typed-inject/src/InjectorImpl.ts +++ b/packages/typed-inject/src/InjectorImpl.ts @@ -49,7 +49,8 @@ abstract class AbstractInjector implements Injector { } private resolveParametersToInject[]>(injectable: Injectable, target?: Function): any[] { - return (injectable.inject || [] as InjectionToken[]).map(key => this.resolve(key, target)); + 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> { diff --git a/packages/typed-inject/src/api/Injectable.ts b/packages/typed-inject/src/api/Injectable.ts index 9d59ff97ca..9a9a57a82f 100644 --- a/packages/typed-inject/src/api/Injectable.ts +++ b/packages/typed-inject/src/api/Injectable.ts @@ -1,14 +1,24 @@ import { CorrespondingTypes } from './CorrespondingType'; import { InjectionToken } from './InjectionToken'; -export interface InjectableClass[]> { +export type InjectableClass[]> = + ClassWithInjections | ClassWithoutInjections; + +export interface ClassWithInjections[]> { new(...args: CorrespondingTypes): R; - readonly inject?: Tokens; + readonly inject: Tokens; } -export interface InjectableFunction[]> { +export type ClassWithoutInjections = new () => R; + +export type InjectableFunction[]> = + InjectableFunctionWithInject | InjectableFunctionWithoutInject; + +export interface InjectableFunctionWithInject[]> { (...args: CorrespondingTypes): R; - readonly inject?: Tokens; + readonly inject: Tokens; } +export type InjectableFunctionWithoutInject = () => R; + export type Injectable[]> = InjectableClass | InjectableFunction; 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/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/tsconfig.test.json b/packages/typed-inject/tsconfig.test.json index 16f2e5e5b3..0cf2f78c25 100644 --- a/packages/typed-inject/tsconfig.test.json +++ b/packages/typed-inject/tsconfig.test.json @@ -3,7 +3,8 @@ "compilerOptions": { "rootDir": ".", "types": [ - "mocha" + "mocha", + "node" ] }, "include": [ From f9dfc625e81abf59e54cf4ee1e50de94cdd21404 Mon Sep 17 00:00:00 2001 From: Nico Jansen Date: Tue, 8 Jan 2019 22:25:24 +0100 Subject: [PATCH 24/36] test(typed-inject): Add more unit and integration tests --- packages/typed-inject/test/unit/Injector.spec.ts | 6 ++++++ .../testResources/tokens-of-type-string.ts | 11 +++++++++++ packages/typed-inject/testResources/unknown-token.ts | 9 +++++++++ .../typed-inject/testResources/wrong-order-tokens.ts | 12 ++++++++++++ 4 files changed, 38 insertions(+) create mode 100644 packages/typed-inject/testResources/tokens-of-type-string.ts create mode 100644 packages/typed-inject/testResources/unknown-token.ts create mode 100644 packages/typed-inject/testResources/wrong-order-tokens.ts diff --git a/packages/typed-inject/test/unit/Injector.spec.ts b/packages/typed-inject/test/unit/Injector.spec.ts index b2ae51cd4d..b8343bf4b0 100644 --- a/packages/typed-inject/test/unit/Injector.spec.ts +++ b/packages/typed-inject/test/unit/Injector.spec.ts @@ -69,6 +69,12 @@ describe('InjectorImpl', () => { }); 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', () => { 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); From bb1ff55ab70a5f20fa7e7657a8e7837430bbc8b8 Mon Sep 17 00:00:00 2001 From: Nico Jansen Date: Wed, 9 Jan 2019 09:19:34 +0100 Subject: [PATCH 25/36] refactor(stryker-api): Rename stryker-api/di to stryker-api/plugin and move implementation of functions to stryker-util --- packages/stryker-api/config.ts | 1 - packages/stryker-api/di.ts | 3 -- packages/stryker-api/mutant.ts | 1 - packages/stryker-api/plugin.ts | 3 ++ packages/stryker-api/report.ts | 1 - .../src/config/ConfigEditorPlugin.ts | 12 ----- packages/stryker-api/src/di/Contexts.ts | 33 -------------- packages/stryker-api/src/di/Plugins.ts | 45 ------------------- packages/stryker-api/src/di/tokens.ts | 4 -- .../stryker-api/src/mutant/MutatorPlugin.ts | 12 ----- packages/stryker-api/src/plugin/Contexts.ts | 36 +++++++++++++++ packages/stryker-api/src/plugin/PluginKind.ts | 8 ++++ packages/stryker-api/src/plugin/Plugins.ts | 39 ++++++++++++++++ .../stryker-api/src/report/ReporterPlugin.ts | 11 ----- .../src/test_framework/TestFrameworkPlugin.ts | 11 ----- .../src/test_runner/TestRunnerPlugin.ts | 15 ------- .../src/transpile/TranspilerPlugin.ts | 16 ------- packages/stryker-api/test_framework.ts | 1 - packages/stryker-api/test_runner.ts | 1 - packages/stryker-api/transpile.ts | 1 - packages/stryker-api/tsconfig.src.json | 2 +- .../stryker-html-reporter/src/HtmlReporter.ts | 4 +- packages/stryker-html-reporter/src/index.ts | 15 ++++--- .../test/integration/QUnitSampleSpec.ts | 2 +- .../stryker-test-helpers/src/TestInjector.ts | 2 +- packages/stryker-util/src/index.ts | 1 + packages/stryker-util/src/tokens.ts | 21 +++++++++ .../stryker-util/test/unit/tokens.spec.ts | 26 +++++++++++ packages/stryker/src/Stryker.ts | 3 +- .../stryker/src/TestFrameworkOrchestrator.ts | 5 ++- .../stryker/src/config/ConfigValidator.ts | 2 - packages/stryker/src/di/PluginLoader.ts | 8 ++-- packages/stryker/src/di/loggerFactory.ts | 4 +- .../src/reporters/BroadcastReporter.ts | 5 ++- .../src/reporters/ClearTextReporter.ts | 2 +- .../src/reporters/DashboardReporter.ts | 2 +- .../stryker/src/reporters/DotsReporter.ts | 2 - .../src/reporters/EventRecorderReporter.ts | 5 +-- .../reporters/ProgressAppendOnlyReporter.ts | 2 - .../stryker/src/reporters/ProgressReporter.ts | 2 - packages/stryker/src/reporters/index.ts | 3 +- packages/stryker/test/unit/StrykerSpec.ts | 2 +- .../unit/reporters/BroadcastReporterSpec.ts | 9 ++-- 43 files changed, 171 insertions(+), 212 deletions(-) delete mode 100644 packages/stryker-api/di.ts create mode 100644 packages/stryker-api/plugin.ts delete mode 100644 packages/stryker-api/src/config/ConfigEditorPlugin.ts delete mode 100644 packages/stryker-api/src/di/Contexts.ts delete mode 100644 packages/stryker-api/src/di/Plugins.ts delete mode 100644 packages/stryker-api/src/di/tokens.ts delete mode 100644 packages/stryker-api/src/mutant/MutatorPlugin.ts create mode 100644 packages/stryker-api/src/plugin/Contexts.ts create mode 100644 packages/stryker-api/src/plugin/PluginKind.ts create mode 100644 packages/stryker-api/src/plugin/Plugins.ts delete mode 100644 packages/stryker-api/src/report/ReporterPlugin.ts delete mode 100644 packages/stryker-api/src/test_framework/TestFrameworkPlugin.ts delete mode 100644 packages/stryker-api/src/test_runner/TestRunnerPlugin.ts delete mode 100644 packages/stryker-api/src/transpile/TranspilerPlugin.ts create mode 100644 packages/stryker-util/src/tokens.ts create mode 100644 packages/stryker-util/test/unit/tokens.spec.ts diff --git a/packages/stryker-api/config.ts b/packages/stryker-api/config.ts index 328d182ac9..3f6b39a7b4 100644 --- a/packages/stryker-api/config.ts +++ b/packages/stryker-api/config.ts @@ -1,4 +1,3 @@ export { default as Config } from './src/config/Config'; export { default as ConfigEditor } from './src/config/ConfigEditor'; export { default as ConfigEditorFactory } from './src/config/ConfigEditorFactory'; -export * from './src/config/ConfigEditorPlugin'; diff --git a/packages/stryker-api/di.ts b/packages/stryker-api/di.ts deleted file mode 100644 index 157f41dbe6..0000000000 --- a/packages/stryker-api/di.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from './src/di/Contexts'; -export * from './src/di/Plugins'; -export * from './src/di/tokens'; diff --git a/packages/stryker-api/mutant.ts b/packages/stryker-api/mutant.ts index 792ff787a5..81a9025925 100644 --- a/packages/stryker-api/mutant.ts +++ b/packages/stryker-api/mutant.ts @@ -1,4 +1,3 @@ export { default as Mutant } from './src/mutant/Mutant'; export { default as Mutator } from './src/mutant/Mutator'; export { default as MutatorFactory } from './src/mutant/MutatorFactory'; -export * from './src/mutant/MutatorPlugin'; diff --git a/packages/stryker-api/plugin.ts b/packages/stryker-api/plugin.ts new file mode 100644 index 0000000000..9e0788c070 --- /dev/null +++ b/packages/stryker-api/plugin.ts @@ -0,0 +1,3 @@ +export * from './src/plugin/Contexts'; +export * from './src/plugin/Plugins'; +export * from './src/plugin/PluginKind'; diff --git a/packages/stryker-api/report.ts b/packages/stryker-api/report.ts index 47897d9b30..a486397752 100644 --- a/packages/stryker-api/report.ts +++ b/packages/stryker-api/report.ts @@ -2,7 +2,6 @@ export { default as Reporter } from './src/report/Reporter'; export { default as MutantResult } from './src/report/MutantResult'; export { default as MutantStatus } from './src/report/MutantStatus'; export { default as ReporterFactory } from './src/report/ReporterFactory'; -export * from './src/report/ReporterPlugin'; export { default as SourceFile } from './src/report/SourceFile'; export { default as MatchedMutant } from './src/report/MatchedMutant'; export { default as ScoreResult } from './src/report/ScoreResult'; diff --git a/packages/stryker-api/src/config/ConfigEditorPlugin.ts b/packages/stryker-api/src/config/ConfigEditorPlugin.ts deleted file mode 100644 index 4af9b151b8..0000000000 --- a/packages/stryker-api/src/config/ConfigEditorPlugin.ts +++ /dev/null @@ -1,12 +0,0 @@ -import ConfigEditor from './ConfigEditor'; -import { StrykerPlugin, PluginKind, StrykerContext } from '../../di'; -import { InjectionToken } from 'typed-inject'; - -export interface ConfigEditorPlugin[]> - extends StrykerPlugin { - readonly kind: PluginKind.ConfigEditor; -} - -export function configEditorPlugin[]>(reporterPlugin: ConfigEditorPlugin) { - return reporterPlugin; -} diff --git a/packages/stryker-api/src/di/Contexts.ts b/packages/stryker-api/src/di/Contexts.ts deleted file mode 100644 index ee640d45fc..0000000000 --- a/packages/stryker-api/src/di/Contexts.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { LoggerFactoryMethod, Logger } from '../../logging'; -import { StrykerOptions } from '../../core'; -import { PluginResolver } from './Plugins'; -import { Config } from '../../config'; - -function token(value: T): T { - return value; -} - -export const commonTokens = Object.freeze({ - /** - * @deprecated Use 'options' instead. This is just hear to support plugin migration - */ - config: token('config'), - getLogger: token('getLogger'), - logger: token('logger'), - options: token('options'), - pluginResolver: token('pluginResolver') -}); - -export interface StrykerContext { - [commonTokens.getLogger]: LoggerFactoryMethod; - [commonTokens.logger]: Logger; - [commonTokens.pluginResolver]: PluginResolver; -} - -export interface PluginContext extends StrykerContext { - [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; -} diff --git a/packages/stryker-api/src/di/Plugins.ts b/packages/stryker-api/src/di/Plugins.ts deleted file mode 100644 index b0c13fa90f..0000000000 --- a/packages/stryker-api/src/di/Plugins.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { TestFrameworkPlugin } from '../../test_framework'; -import { TestRunnerPluginContext, TestRunnerPlugin } from '../../test_runner'; -import { ReporterPlugin } from '../../report'; -import { MutatorPlugin } from '../../mutant'; -import { TranspilerPlugin, TranspilerPluginContext } from '../../transpile'; -import { PluginContext, StrykerContext } from './Contexts'; -import { ConfigEditorPlugin } from '../../config'; -import { InjectionToken, InjectableClass } from 'typed-inject'; - -export interface StrykerPlugin[]> { - readonly name: string; - readonly kind: PluginKind; - readonly injectable: InjectableClass; -} - -export enum PluginKind { - ConfigEditor = 'ConfigEditor', - TestRunner = 'TestRunner', - TestFramework = 'TestFramework', - Transpiler = 'Transpiler', - Mutator = 'Mutator', - Reporter = 'Reporter' -} - -export interface Plugins { - [PluginKind.ConfigEditor]: ConfigEditorPlugin[]>; - [PluginKind.Mutator]: MutatorPlugin[]>; - [PluginKind.Reporter]: ReporterPlugin[]>; - [PluginKind.TestFramework]: TestFrameworkPlugin[]>; - [PluginKind.TestRunner]: TestRunnerPlugin[]>; - [PluginKind.Transpiler]: TranspilerPlugin[]>; -} - -export interface PluginContexts { - [PluginKind.ConfigEditor]: StrykerContext; - [PluginKind.Mutator]: PluginContext; - [PluginKind.Reporter]: PluginContext; - [PluginKind.TestFramework]: PluginContext; - [PluginKind.TestRunner]: TestRunnerPluginContext; - [PluginKind.Transpiler]: TranspilerPluginContext; -} - -export interface PluginResolver { - resolve(kind: T, name: string): Plugins[T]; -} diff --git a/packages/stryker-api/src/di/tokens.ts b/packages/stryker-api/src/di/tokens.ts deleted file mode 100644 index 6fa2601577..0000000000 --- a/packages/stryker-api/src/di/tokens.ts +++ /dev/null @@ -1,4 +0,0 @@ - -export function tokens(...tokens: TS): TS { - return tokens; -} diff --git a/packages/stryker-api/src/mutant/MutatorPlugin.ts b/packages/stryker-api/src/mutant/MutatorPlugin.ts deleted file mode 100644 index 51ab96a138..0000000000 --- a/packages/stryker-api/src/mutant/MutatorPlugin.ts +++ /dev/null @@ -1,12 +0,0 @@ -import Mutator from './Mutator'; -import { StrykerPlugin, PluginKind, PluginContext } from '../../di'; -import { InjectionToken } from 'typed-inject'; - -export interface MutatorPlugin[]> - extends StrykerPlugin { - readonly kind: PluginKind.Mutator; -} - -export function mutatorPlugin[]>(mutatorPlugin: MutatorPlugin) { - return mutatorPlugin; -} diff --git a/packages/stryker-api/src/plugin/Contexts.ts b/packages/stryker-api/src/plugin/Contexts.ts new file mode 100644 index 0000000000..ac6cf22451 --- /dev/null +++ b/packages/stryker-api/src/plugin/Contexts.ts @@ -0,0 +1,36 @@ +import { LoggerFactoryMethod, Logger } from '../../logging'; +import { StrykerOptions } from '../../core'; +import { PluginResolver } from './Plugins'; +import { Config } from '../../config'; +import { PluginKind } from './PluginKind'; + +export interface StrykerContext { + getLogger: LoggerFactoryMethod; + logger: Logger; + pluginResolver: PluginResolver; +} + +export interface TranspilerPluginContext extends PluginContext { + produceSourceMaps: boolean; +} + +export interface TestRunnerPluginContext extends PluginContext { + sandboxFileNames: ReadonlyArray; +} + +export interface PluginContext extends StrykerContext { + options: StrykerOptions; + /** + * @deprecated This is just here to migrate between old and new plugins. Don't use this! Use `options` instead + */ + config: Config; +} + +export interface PluginContexts { + [PluginKind.ConfigEditor]: StrykerContext; + [PluginKind.Mutator]: PluginContext; + [PluginKind.Reporter]: PluginContext; + [PluginKind.TestFramework]: PluginContext; + [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..dbb1fcbb9f --- /dev/null +++ b/packages/stryker-api/src/plugin/PluginKind.ts @@ -0,0 +1,8 @@ +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..0b25732aa8 --- /dev/null +++ b/packages/stryker-api/src/plugin/Plugins.ts @@ -0,0 +1,39 @@ +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 } from 'typed-inject'; +import { PluginKind } from './PluginKind'; + +export interface BasePlugin[]> { + readonly kind: TPlugin; + readonly name: string; + readonly injectable: InjectableClass; +} + +export function reporterPlugin[]>(p: ReporterPlugin) { + return p; +} + +export interface ReporterPlugin[]> extends BasePlugin { } +export interface ConfigEditorPlugin[]> extends BasePlugin { } +export interface MutatorPlugin[]> extends BasePlugin { } +export interface TestFrameworkPlugin[]> extends BasePlugin { } +export interface TestRunnerPlugin[]> extends BasePlugin { } +export interface TranspilerPlugin[]> extends BasePlugin { } + +export interface Plugins { + [PluginKind.ConfigEditor]: ConfigEditorPlugin[]>; + [PluginKind.Mutator]: MutatorPlugin[]>; + [PluginKind.Reporter]: ReporterPlugin[]>; + [PluginKind.TestFramework]: TestFrameworkPlugin[]>; + [PluginKind.TestRunner]: TestRunnerPlugin[]>; + [PluginKind.Transpiler]: TranspilerPlugin[]>; +} + +export interface PluginResolver { + resolve(kind: T, name: string): Plugins[T]; +} diff --git a/packages/stryker-api/src/report/ReporterPlugin.ts b/packages/stryker-api/src/report/ReporterPlugin.ts deleted file mode 100644 index dd0959e0a1..0000000000 --- a/packages/stryker-api/src/report/ReporterPlugin.ts +++ /dev/null @@ -1,11 +0,0 @@ -import Reporter from './Reporter'; -import { StrykerPlugin, PluginKind, PluginContext } from '../../di'; -import { InjectionToken } from 'typed-inject'; - -export interface ReporterPlugin[]> extends StrykerPlugin { - readonly kind: PluginKind.Reporter; -} - -export function reporterPlugin[]>(reporterPlugin: ReporterPlugin) { - return reporterPlugin; -} diff --git a/packages/stryker-api/src/test_framework/TestFrameworkPlugin.ts b/packages/stryker-api/src/test_framework/TestFrameworkPlugin.ts deleted file mode 100644 index 2d8c69fbb8..0000000000 --- a/packages/stryker-api/src/test_framework/TestFrameworkPlugin.ts +++ /dev/null @@ -1,11 +0,0 @@ -import TestFramework from './TestFramework'; -import { StrykerPlugin, PluginKind, PluginContext } from '../../di'; -import { InjectionToken } from 'typed-inject'; - -export interface TestFrameworkPlugin[]> extends StrykerPlugin { - readonly kind: PluginKind.TestFramework; -} - -export function testFrameworkPlugin[]>(testFrameworkPlugin: TestFrameworkPlugin) { - return testFrameworkPlugin; -} diff --git a/packages/stryker-api/src/test_runner/TestRunnerPlugin.ts b/packages/stryker-api/src/test_runner/TestRunnerPlugin.ts deleted file mode 100644 index 19045fef1b..0000000000 --- a/packages/stryker-api/src/test_runner/TestRunnerPlugin.ts +++ /dev/null @@ -1,15 +0,0 @@ -import TestRunner from './TestRunner'; -import { StrykerPlugin, PluginKind, PluginContext } from '../../di'; -import { InjectionToken } from 'typed-inject'; - -export interface TestRunnerPluginContext extends PluginContext { - sandboxFileNames: ReadonlyArray; -} - -export interface TestRunnerPlugin[]> extends StrykerPlugin { - readonly kind: PluginKind.TestRunner; -} - -export function testRunnerPlugin[]>(testRunnerPlugin: TestRunnerPlugin) { - return testRunnerPlugin; -} diff --git a/packages/stryker-api/src/transpile/TranspilerPlugin.ts b/packages/stryker-api/src/transpile/TranspilerPlugin.ts deleted file mode 100644 index 68e6ee364e..0000000000 --- a/packages/stryker-api/src/transpile/TranspilerPlugin.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { PluginContext, PluginKind, StrykerPlugin } from '../../di'; -import Transpiler from './Transpiler'; -import { InjectionToken } from 'typed-inject'; - -export interface TranspilerPluginContext extends PluginContext { - produceSourceMaps: boolean; -} - -export interface TranspilerPlugin[]> - extends StrykerPlugin { - readonly kind: PluginKind.Transpiler; -} - -export function transpilerPlugin[]>(transpilerPlugin: TranspilerPlugin) { - return transpilerPlugin; -} diff --git a/packages/stryker-api/test_framework.ts b/packages/stryker-api/test_framework.ts index dd18f250bd..f652538cf9 100644 --- a/packages/stryker-api/test_framework.ts +++ b/packages/stryker-api/test_framework.ts @@ -1,5 +1,4 @@ export { default as TestFramework } from './src/test_framework/TestFramework'; export { default as TestSelection } from './src/test_framework/TestSelection'; export { default as TestFrameworkFactory } from './src/test_framework/TestFrameworkFactory'; -export * from './src/test_framework/TestFrameworkPlugin'; export { default as TestFrameworkSettings } from './src/test_framework/TestFrameworkSettings'; diff --git a/packages/stryker-api/test_runner.ts b/packages/stryker-api/test_runner.ts index 7e48088265..a1ac887779 100644 --- a/packages/stryker-api/test_runner.ts +++ b/packages/stryker-api/test_runner.ts @@ -7,4 +7,3 @@ export { default as RunResult } from './src/test_runner/RunResult'; export { default as RunOptions } from './src/test_runner/RunOptions'; export { default as RunStatus } from './src/test_runner/RunStatus'; export { default as TestRunnerFactory } from './src/test_runner/TestRunnerFactory'; -export * from './src/test_runner/TestRunnerPlugin'; diff --git a/packages/stryker-api/transpile.ts b/packages/stryker-api/transpile.ts index c89707ec90..846df24e1b 100644 --- a/packages/stryker-api/transpile.ts +++ b/packages/stryker-api/transpile.ts @@ -1,4 +1,3 @@ export { default as Transpiler } from './src/transpile/Transpiler'; export { default as TranspilerFactory } from './src/transpile/TranspilerFactory'; -export * from './src/transpile/TranspilerPlugin'; export { default as TranspilerOptions } from './src/transpile/TranspilerOptions'; diff --git a/packages/stryker-api/tsconfig.src.json b/packages/stryker-api/tsconfig.src.json index aaf4743289..7b87cff3f0 100644 --- a/packages/stryker-api/tsconfig.src.json +++ b/packages/stryker-api/tsconfig.src.json @@ -13,7 +13,7 @@ "test_framework.ts", "test_runner.ts", "transpile.ts", - "di.ts" + "plugin.ts" ], "references": [ { diff --git a/packages/stryker-html-reporter/src/HtmlReporter.ts b/packages/stryker-html-reporter/src/HtmlReporter.ts index 1eeb185730..29ff544699 100644 --- a/packages/stryker-html-reporter/src/HtmlReporter.ts +++ b/packages/stryker-html-reporter/src/HtmlReporter.ts @@ -6,7 +6,7 @@ import * as util from './util'; import * as templates from './templates'; import Breadcrumb from './Breadcrumb'; import { StrykerOptions } from 'stryker-api/core'; -import { tokens } from 'stryker-api/di'; +import { commonTokens, tokens } from '@stryker-mutator/util'; const DEFAULT_BASE_FOLDER = path.normalize('reports/mutation/html'); export const RESOURCES_DIR_NAME = 'strykerResources'; @@ -21,7 +21,7 @@ export default class HtmlReporter implements Reporter { constructor(private readonly options: StrykerOptions, private readonly log: Logger) { } - public static readonly inject = tokens('options', 'logger'); + public static readonly inject = tokens(commonTokens.options, commonTokens.logger); public onAllSourceFilesRead(files: SourceFile[]) { this.files = files; diff --git a/packages/stryker-html-reporter/src/index.ts b/packages/stryker-html-reporter/src/index.ts index cee77f7379..34fabcd5d0 100644 --- a/packages/stryker-html-reporter/src/index.ts +++ b/packages/stryker-html-reporter/src/index.ts @@ -1,9 +1,10 @@ import HtmlReporter from './HtmlReporter'; -import { PluginKind } from 'stryker-api/di'; -import { reporterPlugin } from 'stryker-api/report'; +import { PluginKind, reporterPlugin } from 'stryker-api/plugin'; -export const strykerPlugins = [reporterPlugin({ - injectable: HtmlReporter, - kind: PluginKind.Reporter, - name: 'html' -})]; +export const strykerPlugins = [ + reporterPlugin({ + injectable: HtmlReporter, + kind: PluginKind.Reporter, + name: 'html' + }) +]; diff --git a/packages/stryker-mocha-runner/test/integration/QUnitSampleSpec.ts b/packages/stryker-mocha-runner/test/integration/QUnitSampleSpec.ts index 4ec7eddda9..1b03d30f0a 100644 --- a/packages/stryker-mocha-runner/test/integration/QUnitSampleSpec.ts +++ b/packages/stryker-mocha-runner/test/integration/QUnitSampleSpec.ts @@ -3,7 +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-test-helpers/src'; +import { factory } from '@stryker-mutator/test-helpers'; describe('QUnit sample', () => { diff --git a/packages/stryker-test-helpers/src/TestInjector.ts b/packages/stryker-test-helpers/src/TestInjector.ts index cc560c5a65..8a67358a60 100644 --- a/packages/stryker-test-helpers/src/TestInjector.ts +++ b/packages/stryker-test-helpers/src/TestInjector.ts @@ -1,4 +1,4 @@ -import { PluginResolver, PluginContext } from 'stryker-api/di'; +import { PluginResolver, PluginContext } from 'stryker-api/plugin'; import { StrykerOptions } from 'stryker-api/core'; import { Logger } from 'stryker-api/logging'; import * as factory from './factory'; diff --git a/packages/stryker-util/src/index.ts b/packages/stryker-util/src/index.ts index 8d70683642..8f0b9bcd64 100644 --- a/packages/stryker-util/src/index.ts +++ b/packages/stryker-util/src/index.ts @@ -3,3 +3,4 @@ export { default as childProcessAsPromised } from './childProcessAsPromised'; export { default as promisify } from './promisify'; export { default as StrykerError } from './StrykerError'; export * from './errors'; +export * from './tokens'; diff --git a/packages/stryker-util/src/tokens.ts b/packages/stryker-util/src/tokens.ts new file mode 100644 index 0000000000..fa727db5ee --- /dev/null +++ b/packages/stryker-util/src/tokens.ts @@ -0,0 +1,21 @@ + +function token(value: T): T { + return value; +} + +export const commonTokens = Object.freeze({ + /** + * @deprecated Use 'options' instead. This is just hear to support plugin migration + */ + config: token('config'), + getLogger: token('getLogger'), + logger: token('logger'), + options: token('options'), + pluginResolver: token('pluginResolver'), + produceSourceMaps: token('produceSourceMaps'), + sandboxFileNames: token('sandboxFileNames') +}); + +export function tokens(...tokens: TS): TS { + return tokens; +} diff --git a/packages/stryker-util/test/unit/tokens.spec.ts b/packages/stryker-util/test/unit/tokens.spec.ts new file mode 100644 index 0000000000..571fdb44c3 --- /dev/null +++ b/packages/stryker-util/test/unit/tokens.spec.ts @@ -0,0 +1,26 @@ +import { expect } from 'chai'; +import { tokens, commonTokens } from '../../src'; + +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/src/Stryker.ts b/packages/stryker/src/Stryker.ts index 180a643497..cfee0154f3 100644 --- a/packages/stryker/src/Stryker.ts +++ b/packages/stryker/src/Stryker.ts @@ -20,9 +20,10 @@ import MutationTestExecutor from './process/MutationTestExecutor'; import InputFileCollection from './input/InputFileCollection'; import LogConfigurator from './logging/LogConfigurator'; import BroadcastReporter from './reporters/BroadcastReporter'; -import { PluginContext, commonTokens, PluginResolver } from 'stryker-api/di'; +import { PluginContext, PluginResolver } from 'stryker-api/plugin'; import { Injector, rootInjector, Scope } from 'typed-inject'; import { loggerFactory } from './di/loggerFactory'; +import { commonTokens } from '@stryker-mutator/util'; export default class Stryker { diff --git a/packages/stryker/src/TestFrameworkOrchestrator.ts b/packages/stryker/src/TestFrameworkOrchestrator.ts index 804f2b6292..9db0fab320 100644 --- a/packages/stryker/src/TestFrameworkOrchestrator.ts +++ b/packages/stryker/src/TestFrameworkOrchestrator.ts @@ -1,12 +1,13 @@ import { TestFrameworkFactory, TestFramework } from 'stryker-api/test_framework'; import { StrykerOptions } from 'stryker-api/core'; import { getLogger } from 'stryker-api/logging'; -import { tokens } from 'stryker-api/di'; +import { commonTokens } from '@stryker-mutator/util'; +import { tokens } from 'typed-inject'; export default class TestFrameworkOrchestrator { private readonly log = getLogger(TestFrameworkOrchestrator.name); - public static inject = tokens('options'); + public static inject = tokens(commonTokens.options); constructor(private readonly options: StrykerOptions) { } public determineTestFramework(): TestFramework | null { diff --git a/packages/stryker/src/config/ConfigValidator.ts b/packages/stryker/src/config/ConfigValidator.ts index 384c01b205..277173d51c 100644 --- a/packages/stryker/src/config/ConfigValidator.ts +++ b/packages/stryker/src/config/ConfigValidator.ts @@ -4,13 +4,11 @@ import { Config } from 'stryker-api/config'; import { getLogger } from 'stryker-api/logging'; import { StrykerError } from '@stryker-mutator/util'; import { normalizeWhiteSpaces } from '../utils/objectUtils'; -import { tokens } from 'stryker-api/di'; export default class ConfigValidator { private isValid = true; private readonly log = getLogger(ConfigValidator.name); - public static inject = tokens('options', 'testFramework'); constructor(private readonly strykerConfig: StrykerOptions, private readonly testFramework: TestFramework | null) { } public validate() { diff --git a/packages/stryker/src/di/PluginLoader.ts b/packages/stryker/src/di/PluginLoader.ts index 36a2bb95e1..a4043fff8f 100644 --- a/packages/stryker/src/di/PluginLoader.ts +++ b/packages/stryker/src/di/PluginLoader.ts @@ -4,7 +4,7 @@ import * as _ from 'lodash'; import { tokens, CorrespondingTypes, InjectionToken } from 'typed-inject'; import { importModule } from '../utils/fileUtils'; import { fsAsPromised } from '@stryker-mutator/util'; -import { StrykerPlugin, PluginKind, PluginResolver, Plugins, PluginContexts } from 'stryker-api/di'; +import { PluginKind, PluginResolver, Plugins, PluginContexts, BasePlugin } from 'stryker-api/plugin'; import { ConfigEditorFactory } from 'stryker-api/config'; import { Factory } from 'stryker-api/core'; import { ReporterFactory } from 'stryker-api/report'; @@ -16,12 +16,12 @@ import { MutatorFactory } from 'stryker-api/mutant'; const IGNORED_PACKAGES = ['stryker-cli', 'stryker-api']; interface PluginModule { - strykerPlugins: StrykerPlugin[]; + strykerPlugins: BasePlugin[]; } export default class PluginLoader implements PluginResolver { private readonly log = getLogger(PluginLoader.name); - private readonly pluginsByKind: Map[]> = new Map(); + private readonly pluginsByKind: Map[]> = new Map(); constructor(private readonly pluginDescriptors: string[]) { } @@ -131,7 +131,7 @@ export default class PluginLoader implements PluginResolver { } } - private loadPlugin(plugin: StrykerPlugin) { + private loadPlugin(plugin: BasePlugin) { let plugins = this.pluginsByKind.get(plugin.kind); if (!plugins) { plugins = []; diff --git a/packages/stryker/src/di/loggerFactory.ts b/packages/stryker/src/di/loggerFactory.ts index a6887ad6fa..5acb72996c 100644 --- a/packages/stryker/src/di/loggerFactory.ts +++ b/packages/stryker/src/di/loggerFactory.ts @@ -1,6 +1,6 @@ import { LoggerFactoryMethod } from 'stryker-api/logging'; -import { tokens, commonTokens } from 'stryker-api/di'; -import { TARGET_TOKEN } from 'typed-inject'; +import { TARGET_TOKEN, tokens } from 'typed-inject'; +import { commonTokens } from '@stryker-mutator/util'; export function loggerFactory(getLogger: LoggerFactoryMethod, target: Function | undefined) { return getLogger(target ? target.name : 'UNKNOWN'); diff --git a/packages/stryker/src/reporters/BroadcastReporter.ts b/packages/stryker/src/reporters/BroadcastReporter.ts index fa16f5fcdf..d18599b0f2 100644 --- a/packages/stryker/src/reporters/BroadcastReporter.ts +++ b/packages/stryker/src/reporters/BroadcastReporter.ts @@ -2,9 +2,10 @@ import { Reporter, SourceFile, MutantResult, MatchedMutant, ScoreResult } from ' import { Logger } from 'stryker-api/logging'; import { isPromise } from '../utils/objectUtils'; import StrictReporter from './StrictReporter'; -import { tokens, PluginResolver, PluginKind, PluginContext, commonTokens } from 'stryker-api/di'; +import { PluginResolver, PluginKind, PluginContext } from 'stryker-api/plugin'; import { StrykerOptions } from 'stryker-api/core'; -import { Injector, INJECTOR_TOKEN } from 'typed-inject'; +import { Injector, INJECTOR_TOKEN, tokens } from 'typed-inject'; +import { commonTokens } from '@stryker-mutator/util'; export default class BroadcastReporter implements StrictReporter { diff --git a/packages/stryker/src/reporters/ClearTextReporter.ts b/packages/stryker/src/reporters/ClearTextReporter.ts index a7b562b043..5e0028bf6b 100644 --- a/packages/stryker/src/reporters/ClearTextReporter.ts +++ b/packages/stryker/src/reporters/ClearTextReporter.ts @@ -4,7 +4,7 @@ import { Reporter, MutantResult, MutantStatus, ScoreResult } from 'stryker-api/r import { Position, StrykerOptions } from 'stryker-api/core'; import ClearTextScoreTable from './ClearTextScoreTable'; import * as os from 'os'; -import { tokens } from 'stryker-api/di'; +import { tokens } from 'typed-inject'; export default class ClearTextReporter implements Reporter { diff --git a/packages/stryker/src/reporters/DashboardReporter.ts b/packages/stryker/src/reporters/DashboardReporter.ts index 9471b38090..758a8bd91e 100644 --- a/packages/stryker/src/reporters/DashboardReporter.ts +++ b/packages/stryker/src/reporters/DashboardReporter.ts @@ -3,7 +3,7 @@ import DashboardReporterClient from './dashboard-reporter/DashboardReporterClien import {getEnvironmentVariable} from '../utils/objectUtils'; import { getLogger } from 'stryker-api/logging'; import { determineCIProvider } from './ci/Provider'; -import { tokens } from 'stryker-api/di'; +import { tokens } from 'typed-inject'; export default class DashboardReporter implements Reporter { public static readonly inject = tokens(); diff --git a/packages/stryker/src/reporters/DotsReporter.ts b/packages/stryker/src/reporters/DotsReporter.ts index 9ec9fc924c..a54ef2d301 100644 --- a/packages/stryker/src/reporters/DotsReporter.ts +++ b/packages/stryker/src/reporters/DotsReporter.ts @@ -1,10 +1,8 @@ import {Reporter, MutantResult, MutantStatus} from 'stryker-api/report'; import chalk from 'chalk'; import * as os from 'os'; -import { tokens } from 'stryker-api/di'; export default class DotsReporter implements Reporter { - public static readonly inject = tokens(); public onMutantTested(result: MutantResult) { let toLog: string; diff --git a/packages/stryker/src/reporters/EventRecorderReporter.ts b/packages/stryker/src/reporters/EventRecorderReporter.ts index 48588461b9..0874d71703 100644 --- a/packages/stryker/src/reporters/EventRecorderReporter.ts +++ b/packages/stryker/src/reporters/EventRecorderReporter.ts @@ -4,13 +4,12 @@ import { StrykerOptions } from 'stryker-api/core'; import { SourceFile, MutantResult, MatchedMutant, Reporter, ScoreResult } from 'stryker-api/report'; import { cleanFolder } from '../utils/fileUtils'; import StrictReporter from './StrictReporter'; -import { fsAsPromised } from '@stryker-mutator/util'; -import { tokens } from 'stryker-api/di'; +import { fsAsPromised, commonTokens } from '@stryker-mutator/util'; const DEFAULT_BASE_FOLDER = 'reports/mutation/events'; export default class EventRecorderReporter implements StrictReporter { - public static readonly inject = tokens('options'); + public static readonly inject = [commonTokens.options]; private readonly log = getLogger(EventRecorderReporter.name); private readonly allWork: Promise[] = []; diff --git a/packages/stryker/src/reporters/ProgressAppendOnlyReporter.ts b/packages/stryker/src/reporters/ProgressAppendOnlyReporter.ts index 253009c848..687f54ace7 100644 --- a/packages/stryker/src/reporters/ProgressAppendOnlyReporter.ts +++ b/packages/stryker/src/reporters/ProgressAppendOnlyReporter.ts @@ -1,11 +1,9 @@ import { MatchedMutant } from 'stryker-api/report'; import * as os from 'os'; import ProgressKeeper from './ProgressKeeper'; -import { tokens } from 'stryker-api/di'; export default class ProgressAppendOnlyReporter extends ProgressKeeper { private intervalReference: NodeJS.Timer; - public static readonly inject = tokens(); public onAllMutantsMatchedWithTests(matchedMutants: ReadonlyArray): void { super.onAllMutantsMatchedWithTests(matchedMutants); diff --git a/packages/stryker/src/reporters/ProgressReporter.ts b/packages/stryker/src/reporters/ProgressReporter.ts index f25930dd13..dd26aabc8c 100644 --- a/packages/stryker/src/reporters/ProgressReporter.ts +++ b/packages/stryker/src/reporters/ProgressReporter.ts @@ -1,11 +1,9 @@ import { MatchedMutant, MutantResult } from 'stryker-api/report'; import ProgressKeeper from './ProgressKeeper'; import ProgressBar from './ProgressBar'; -import { tokens } from 'stryker-api/di'; export default class ProgressBarReporter extends ProgressKeeper { private progressBar: ProgressBar; - public static readonly inject = tokens(); public onAllMutantsMatchedWithTests(matchedMutants: ReadonlyArray): void { super.onAllMutantsMatchedWithTests(matchedMutants); diff --git a/packages/stryker/src/reporters/index.ts b/packages/stryker/src/reporters/index.ts index 883f83afdb..40413eb889 100644 --- a/packages/stryker/src/reporters/index.ts +++ b/packages/stryker/src/reporters/index.ts @@ -4,8 +4,7 @@ import ProgressAppendOnlyReporter from './ProgressAppendOnlyReporter'; import DotsReporter from './DotsReporter'; import EventRecorderReporter from './EventRecorderReporter'; import DashboardReporter from './DashboardReporter'; -import { reporterPlugin } from 'stryker-api/report'; -import { PluginKind } from 'stryker-api/di'; +import { PluginKind, reporterPlugin } from 'stryker-api/plugin'; export const strykerPlugins = [ reporterPlugin({ name: 'clear-text', kind: PluginKind.Reporter, injectable: ClearTextReporter }), diff --git a/packages/stryker/test/unit/StrykerSpec.ts b/packages/stryker/test/unit/StrykerSpec.ts index eb34bbb986..2f008822ec 100644 --- a/packages/stryker/test/unit/StrykerSpec.ts +++ b/packages/stryker/test/unit/StrykerSpec.ts @@ -25,7 +25,7 @@ import TestableMutant from '../../src/TestableMutant'; import InputFileCollection from '../../src/input/InputFileCollection'; import LogConfigurator from '../../src/logging/LogConfigurator'; import LoggingClientContext from '../../src/logging/LoggingClientContext'; -import { PluginContext } from 'stryker-api/di'; +import { PluginContext } from 'stryker-api/plugin'; class FakeConfigEditor implements ConfigEditor { constructor() { } diff --git a/packages/stryker/test/unit/reporters/BroadcastReporterSpec.ts b/packages/stryker/test/unit/reporters/BroadcastReporterSpec.ts index b1854ae768..1000685d5e 100644 --- a/packages/stryker/test/unit/reporters/BroadcastReporterSpec.ts +++ b/packages/stryker/test/unit/reporters/BroadcastReporterSpec.ts @@ -1,10 +1,10 @@ import { expect } from 'chai'; import BroadcastReporter from '../../../src/reporters/BroadcastReporter'; import { ALL_REPORTER_EVENTS } from '../../helpers/producers'; -import { PluginKind, tokens } from 'stryker-api/di'; +import { PluginKind, ReporterPlugin } from 'stryker-api/plugin'; import * as sinon from 'sinon'; import { testInjector, factory } from '@stryker-mutator/test-helpers'; -import { Reporter, ReporterPlugin } from 'stryker-api/report'; +import { Reporter } from 'stryker-api/report'; describe('BroadcastReporter', () => { @@ -49,7 +49,7 @@ describe('BroadcastReporter', () => { // Assert expect(sut.reporters).deep.eq({ - progress: progressReporterPlugin.reporterStub, + progress: progressReporterPlugin.reporterStub, rep2: rep2Plugin.reporterStub }); }); @@ -134,7 +134,7 @@ describe('BroadcastReporter', () => { return testInjector.injector.injectClass(BroadcastReporter); } - type MockedReporterPlugin = ReporterPlugin<[]> & { reporterStub: sinon.SinonStubbedInstance>} ; + type MockedReporterPlugin = ReporterPlugin<[]> & { reporterStub: sinon.SinonStubbedInstance> }; function mockReporterPlugin(name: string): MockedReporterPlugin { const reporterStub = factory.reporter(); @@ -144,7 +144,6 @@ describe('BroadcastReporter', () => { constructor() { return reporterStub; } - public static inject = tokens(); }, kind: PluginKind.Reporter, name, From a1598976df4be63a6806fbb7307307f1cd059a7e Mon Sep 17 00:00:00 2001 From: Nico Jansen Date: Wed, 9 Jan 2019 09:23:43 +0100 Subject: [PATCH 26/36] refactor(html-reporter): Revert back html reporter in order to present it in its own PR --- packages/stryker-html-reporter/package.json | 1 - .../stryker-html-reporter/src/HtmlReporter.ts | 26 ++++++++-------- packages/stryker-html-reporter/src/index.ts | 10 ++----- .../test/helpers/initSinon.ts | 7 ----- .../test/helpers/loggingMock.ts | 30 +++++++++++++++++++ ...athReport.it.spec.ts => MathReportSpec.ts} | 11 +++---- ...Report.it.spec.ts => StrykerReportSpec.ts} | 7 +++-- ...r.it.spec.ts => singleFileInFolderSpec.ts} | 7 +++-- .../stryker-html-reporter/test/ui/hooks.ts | 3 +- .../test/unit/HtmlReporter.spec.ts | 16 ++++++---- .../tsconfig.settings.json | 2 -- .../stryker-html-reporter/tsconfig.test.json | 3 -- 12 files changed, 69 insertions(+), 54 deletions(-) delete mode 100644 packages/stryker-html-reporter/test/helpers/initSinon.ts create mode 100644 packages/stryker-html-reporter/test/helpers/loggingMock.ts rename packages/stryker-html-reporter/test/integration/{MathReport.it.spec.ts => MathReportSpec.ts} (82%) rename packages/stryker-html-reporter/test/integration/{StrykerReport.it.spec.ts => StrykerReportSpec.ts} (96%) rename packages/stryker-html-reporter/test/integration/{singleFileInFolder.it.spec.ts => singleFileInFolderSpec.ts} (84%) diff --git a/packages/stryker-html-reporter/package.json b/packages/stryker-html-reporter/package.json index b894dfc374..fab6923841 100644 --- a/packages/stryker-html-reporter/package.json +++ b/packages/stryker-html-reporter/package.json @@ -47,7 +47,6 @@ "stryker-api": ">=0.18.0 <0.24.0" }, "devDependencies": { - "@stryker-mutator/test-helpers": "0.0.0", "@types/file-url": "~2.0.0", "@types/jsdom": "~12.2.0", "@types/node": "^10.11.5", diff --git a/packages/stryker-html-reporter/src/HtmlReporter.ts b/packages/stryker-html-reporter/src/HtmlReporter.ts index 29ff544699..559e452efb 100644 --- a/packages/stryker-html-reporter/src/HtmlReporter.ts +++ b/packages/stryker-html-reporter/src/HtmlReporter.ts @@ -1,28 +1,26 @@ -import { Logger } from 'stryker-api/logging'; +import { getLogger } from 'stryker-api/logging'; import fileUrl = require('file-url'); import * as path from 'path'; +import { Config } from 'stryker-api/config'; import { Reporter, MutantResult, SourceFile, ScoreResult } from 'stryker-api/report'; import * as util from './util'; import * as templates from './templates'; import Breadcrumb from './Breadcrumb'; -import { StrykerOptions } from 'stryker-api/core'; -import { commonTokens, tokens } from '@stryker-mutator/util'; const DEFAULT_BASE_FOLDER = path.normalize('reports/mutation/html'); export const RESOURCES_DIR_NAME = 'strykerResources'; export default class HtmlReporter implements Reporter { - private _baseDir!: string; - private mainPromise!: Promise; - private mutantResults!: MutantResult[]; - private files!: SourceFile[]; - private scoreResult!: ScoreResult; - - constructor(private readonly options: StrykerOptions, private readonly log: Logger) { + private readonly log = getLogger(HtmlReporter.name); + private _baseDir: string; + private mainPromise: Promise; + private mutantResults: MutantResult[]; + private files: SourceFile[]; + private scoreResult: ScoreResult; + + constructor(private readonly options: Config) { } - public static readonly inject = tokens(commonTokens.options, commonTokens.logger); - public onAllSourceFilesRead(files: SourceFile[]) { this.files = files; } @@ -67,7 +65,7 @@ export default class HtmlReporter implements Reporter { if (child.representsFile) { return this.writeReportFile(child, currentDirectory, breadcrumb.add(child.name, util.countPathSep(child.name))); } else { - return this.writeReportDirectory(child, path.join(currentDirectory, child.name), breadcrumb.add(child.name, util.countPathSep(child.name) + 1)) + return this.writeReportDirectory(child, path.join(currentDirectory, child.name), breadcrumb.add(child.name, util.countPathSep(child.name) + 1)) .then(_ => void 0); } })); @@ -94,7 +92,7 @@ export default class HtmlReporter implements Reporter { return path.join(this.baseDir, RESOURCES_DIR_NAME); } - private get baseDir(): string { + private get baseDir() { if (!this._baseDir) { if (this.options.htmlReporter && this.options.htmlReporter.baseDir) { this._baseDir = this.options.htmlReporter.baseDir; diff --git a/packages/stryker-html-reporter/src/index.ts b/packages/stryker-html-reporter/src/index.ts index 34fabcd5d0..bf4152db08 100644 --- a/packages/stryker-html-reporter/src/index.ts +++ b/packages/stryker-html-reporter/src/index.ts @@ -1,10 +1,4 @@ +import { ReporterFactory } from 'stryker-api/report'; import HtmlReporter from './HtmlReporter'; -import { PluginKind, reporterPlugin } from 'stryker-api/plugin'; -export const strykerPlugins = [ - reporterPlugin({ - injectable: HtmlReporter, - kind: PluginKind.Reporter, - name: 'html' - }) -]; +ReporterFactory.instance().register('html', HtmlReporter); diff --git a/packages/stryker-html-reporter/test/helpers/initSinon.ts b/packages/stryker-html-reporter/test/helpers/initSinon.ts deleted file mode 100644 index 00ee898357..0000000000 --- a/packages/stryker-html-reporter/test/helpers/initSinon.ts +++ /dev/null @@ -1,7 +0,0 @@ -import * as sinon from 'sinon'; -import { testInjector } from '@stryker-mutator/test-helpers'; - -afterEach(() => { - sinon.restore(); - testInjector.reset(); -}); diff --git a/packages/stryker-html-reporter/test/helpers/loggingMock.ts b/packages/stryker-html-reporter/test/helpers/loggingMock.ts new file mode 100644 index 0000000000..7cec9895fb --- /dev/null +++ b/packages/stryker-html-reporter/test/helpers/loggingMock.ts @@ -0,0 +1,30 @@ +import * as logging from 'stryker-api/logging'; +import * as sinon from 'sinon'; + +const logger = { + 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() +}; + +sinon.stub(logging, 'getLogger').returns(logger); + +beforeEach(() => { + logger.trace.reset(); + logger.debug.reset(); + logger.info.reset(); + logger.warn.reset(); + logger.error.reset(); + logger.fatal.reset(); +}); + +export default logger; diff --git a/packages/stryker-html-reporter/test/integration/MathReport.it.spec.ts b/packages/stryker-html-reporter/test/integration/MathReportSpec.ts similarity index 82% rename from packages/stryker-html-reporter/test/integration/MathReport.it.spec.ts rename to packages/stryker-html-reporter/test/integration/MathReportSpec.ts index 3786177495..def3a1cf4e 100644 --- a/packages/stryker-html-reporter/test/integration/MathReport.it.spec.ts +++ b/packages/stryker-html-reporter/test/integration/MathReportSpec.ts @@ -2,18 +2,19 @@ import * as fs from 'fs'; import * as path from 'path'; import { expect } from 'chai'; import { Config } from 'stryker-api/config'; +import logger from '../helpers/loggingMock'; import EventPlayer from '../helpers/EventPlayer'; import fileUrl = require('file-url'); import HtmlReporter from '../../src/HtmlReporter'; -import { testInjector } from '@stryker-mutator/test-helpers'; describe('HtmlReporter with example math project', () => { let sut: HtmlReporter; const baseDir = 'reports/mutation/math'; beforeEach(() => { - testInjector.options.htmlReporter = { baseDir }; - sut = testInjector.injector.injectClass(HtmlReporter); + const config = new Config(); + config.set({ htmlReporter: { baseDir } }); + sut = new HtmlReporter(config); return new EventPlayer(path.join('testResources', 'mathEvents')) .replay(sut) .then(() => sut.wrapUp()); @@ -26,14 +27,14 @@ describe('HtmlReporter with example math project', () => { }); it('should output a log message with a link to the HTML report', () => { - expect(testInjector.logger.info).to.have.been.calledWith(`Your report can be found at: ${fileUrl(baseDir + '/index.html')}`); + expect(logger.info).to.have.been.calledWith(`Your report can be found at: ${fileUrl(baseDir + '/index.html')}`); }); describe('when initiated a second time with empty events', () => { beforeEach(() => { const config = new Config(); config.set({ htmlReporter: { baseDir } }); - sut = testInjector.injector.injectClass(HtmlReporter); + sut = new HtmlReporter(config); sut.onAllSourceFilesRead([]); sut.onAllMutantsTested([]); return sut.wrapUp(); diff --git a/packages/stryker-html-reporter/test/integration/StrykerReport.it.spec.ts b/packages/stryker-html-reporter/test/integration/StrykerReportSpec.ts similarity index 96% rename from packages/stryker-html-reporter/test/integration/StrykerReport.it.spec.ts rename to packages/stryker-html-reporter/test/integration/StrykerReportSpec.ts index 6011ed0b4a..6fe00036e5 100644 --- a/packages/stryker-html-reporter/test/integration/StrykerReport.it.spec.ts +++ b/packages/stryker-html-reporter/test/integration/StrykerReportSpec.ts @@ -1,8 +1,8 @@ import { expect } from 'chai'; +import { Config } from 'stryker-api/config'; import HtmlReporter from '../../src/HtmlReporter'; import EventPlayer from '../helpers/EventPlayer'; import { readDirectoryTree } from '../helpers/fsHelpers'; -import { testInjector } from '@stryker-mutator/test-helpers'; const REPORT_DIR = 'reports/mutation/stryker'; @@ -10,8 +10,9 @@ describe('Html report of stryker', () => { let sut: HtmlReporter; beforeEach(() => { - testInjector.options.htmlReporter = { baseDir: REPORT_DIR }; - sut = testInjector.injector.injectClass(HtmlReporter); + const config = new Config(); + config.set({ htmlReporter: { baseDir: REPORT_DIR } }); + sut = new HtmlReporter(config); return new EventPlayer('testResources/strykerEvents') .replay(sut) .then(() => sut.wrapUp()); diff --git a/packages/stryker-html-reporter/test/integration/singleFileInFolder.it.spec.ts b/packages/stryker-html-reporter/test/integration/singleFileInFolderSpec.ts similarity index 84% rename from packages/stryker-html-reporter/test/integration/singleFileInFolder.it.spec.ts rename to packages/stryker-html-reporter/test/integration/singleFileInFolderSpec.ts index bc61980d8c..4d3d8fd9cd 100644 --- a/packages/stryker-html-reporter/test/integration/singleFileInFolder.it.spec.ts +++ b/packages/stryker-html-reporter/test/integration/singleFileInFolderSpec.ts @@ -1,10 +1,10 @@ import * as path from 'path'; import { expect } from 'chai'; +import { Config } from 'stryker-api/config'; import EventPlayer from '../helpers/EventPlayer'; import HtmlReporter from '../../src/HtmlReporter'; import { readDirectoryTree } from '../helpers/fsHelpers'; import * as fs from 'fs'; -import { testInjector } from '@stryker-mutator/test-helpers'; const REPORT_DIR = 'reports/mutation/singleFileInFolder'; @@ -12,8 +12,9 @@ describe('HtmlReporter single file in a folder', () => { let sut: HtmlReporter; beforeEach(() => { - testInjector.options.htmlReporter = { baseDir: REPORT_DIR }; - sut = testInjector.injector.injectClass(HtmlReporter); + const config = new Config(); + config.set({ htmlReporter: { baseDir: REPORT_DIR } }); + sut = new HtmlReporter(config); return new EventPlayer(path.join('testResources', 'singleFileInFolder')) .replay(sut) .then(() => sut.wrapUp()); diff --git a/packages/stryker-html-reporter/test/ui/hooks.ts b/packages/stryker-html-reporter/test/ui/hooks.ts index 8dccdbdb54..9edded3dd8 100644 --- a/packages/stryker-html-reporter/test/ui/hooks.ts +++ b/packages/stryker-html-reporter/test/ui/hooks.ts @@ -3,7 +3,6 @@ import { browser } from 'protractor'; import { Config } from 'stryker-api/config'; import HtmlReporter from '../../src/HtmlReporter'; import EventPlayer from '../helpers/EventPlayer'; -import { factory } from '@stryker-mutator/test-helpers'; export const baseDir = path.join(__dirname, '../../reports/mutation/uiTest'); @@ -11,7 +10,7 @@ before(() => { browser.ignoreSynchronization = true; const config = new Config(); config.set({ htmlReporter: { baseDir } }); - const reporter = new HtmlReporter(config, factory.logger()); + const reporter = new HtmlReporter(config); return new EventPlayer('testResources/mathEvents') .replay(reporter) .then(() => reporter.wrapUp()); diff --git a/packages/stryker-html-reporter/test/unit/HtmlReporter.spec.ts b/packages/stryker-html-reporter/test/unit/HtmlReporter.spec.ts index c571392c14..c738455da2 100644 --- a/packages/stryker-html-reporter/test/unit/HtmlReporter.spec.ts +++ b/packages/stryker-html-reporter/test/unit/HtmlReporter.spec.ts @@ -1,12 +1,13 @@ import { normalize, join } from 'path'; import { expect } from 'chai'; import * as sinon from 'sinon'; +import { Config } from 'stryker-api/config'; import * as util from '../../src/util'; import HtmlReporter from '../../src/HtmlReporter'; import { sourceFile, mutantResult, scoreResult } from '../helpers/producers'; -import { testInjector } from '@stryker-mutator/test-helpers'; describe('HtmlReporter', () => { + let sandbox: sinon.SinonSandbox; let copyFolderStub: sinon.SinonStub; let writeFileStub: sinon.SinonStub; let mkdirStub: sinon.SinonStub; @@ -14,13 +15,16 @@ describe('HtmlReporter', () => { let sut: HtmlReporter; beforeEach(() => { - copyFolderStub = sinon.stub(util, 'copyFolder'); - writeFileStub = sinon.stub(util, 'writeFile'); - deleteDirStub = sinon.stub(util, 'deleteDir'); - mkdirStub = sinon.stub(util, 'mkdir'); - sut = testInjector.injector.injectClass(HtmlReporter); + sandbox = sinon.createSandbox(); + copyFolderStub = sandbox.stub(util, 'copyFolder'); + writeFileStub = sandbox.stub(util, 'writeFile'); + deleteDirStub = sandbox.stub(util, 'deleteDir'); + mkdirStub = sandbox.stub(util, 'mkdir'); + sut = new HtmlReporter(new Config()); }); + afterEach(() => sandbox.restore()); + describe('when in happy flow', () => { beforeEach(() => { diff --git a/packages/stryker-html-reporter/tsconfig.settings.json b/packages/stryker-html-reporter/tsconfig.settings.json index dbf001740d..84b63e5ffc 100644 --- a/packages/stryker-html-reporter/tsconfig.settings.json +++ b/packages/stryker-html-reporter/tsconfig.settings.json @@ -10,8 +10,6 @@ "es2015.symbol.wellknown" ], "noImplicitReturns": false, - "strictPropertyInitialization": true, - "strictNullChecks": true, "jsx": "react", "jsxFactory": "typedHtml.createElement" } diff --git a/packages/stryker-html-reporter/tsconfig.test.json b/packages/stryker-html-reporter/tsconfig.test.json index da121922f8..9fb06c74c0 100644 --- a/packages/stryker-html-reporter/tsconfig.test.json +++ b/packages/stryker-html-reporter/tsconfig.test.json @@ -12,9 +12,6 @@ "references": [ { "path": "./tsconfig.src.json" - }, - { - "path": "../stryker-test-helpers/tsconfig.src.json" } ] } \ No newline at end of file From ce338cc6409fa8e60ead6eccc83e3516570e62bd Mon Sep 17 00:00:00 2001 From: Nico Jansen Date: Wed, 9 Jan 2019 10:30:42 +0100 Subject: [PATCH 27/36] fix(plugin-loader): Fix dynamicly loading of deprecated plugins --- packages/stryker/src/di/PluginLoader.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/stryker/src/di/PluginLoader.ts b/packages/stryker/src/di/PluginLoader.ts index a4043fff8f..62caeb2b5d 100644 --- a/packages/stryker/src/di/PluginLoader.ts +++ b/packages/stryker/src/di/PluginLoader.ts @@ -67,7 +67,7 @@ export default class PluginLoader implements PluginResolver { const realPlugin = factory.create(name, settingsFactory(args)); for (const i in realPlugin) { const method = (realPlugin as any)[i]; - if (method === 'function') { + if (typeof method === 'function' && method ) { (this as any)[i] = method.bind(realPlugin); } } From 266c1b6f60e52029518d41912d5e83c8f1b8c582 Mon Sep 17 00:00:00 2001 From: Nico Jansen Date: Thu, 10 Jan 2019 10:34:25 +0100 Subject: [PATCH 28/36] feat(plugins): Make sure you can also create a plugin with a factory method. --- packages/stryker-api/src/plugin/Plugins.ts | 57 ++++++++++++------- packages/stryker/src/di/PluginLoader.ts | 10 ++-- packages/stryker/src/di/createPlugin.ts | 22 +++++++ .../src/reporters/BroadcastReporter.ts | 3 +- packages/stryker/src/reporters/index.ts | 14 ++--- .../stryker/test/unit/di/createPlugin.spec.ts | 36 ++++++++++++ .../unit/reporters/BroadcastReporterSpec.ts | 12 ++-- 7 files changed, 115 insertions(+), 39 deletions(-) create mode 100644 packages/stryker/src/di/createPlugin.ts create mode 100644 packages/stryker/test/unit/di/createPlugin.spec.ts diff --git a/packages/stryker-api/src/plugin/Plugins.ts b/packages/stryker-api/src/plugin/Plugins.ts index 0b25732aa8..a42ced0d93 100644 --- a/packages/stryker-api/src/plugin/Plugins.ts +++ b/packages/stryker-api/src/plugin/Plugins.ts @@ -5,35 +5,54 @@ import { Mutator } from '../../mutant'; import { Transpiler } from '../../transpile'; import { PluginContexts } from './Contexts'; import { ConfigEditor } from '../../config'; -import { InjectionToken, InjectableClass } from 'typed-inject'; +import { InjectionToken, InjectableClass, InjectableFunction } from 'typed-inject'; import { PluginKind } from './PluginKind'; -export interface BasePlugin[]> { - readonly kind: TPlugin; +export type Plugin[]> = + FactoryPlugin | ClassPlugin; + +export interface FactoryPlugin[]> { + readonly kind: TPluginKind; + readonly name: string; + readonly factory: InjectableFunction; +} +export interface ClassPlugin[]> { + readonly kind: TPluginKind; readonly name: string; - readonly injectable: InjectableClass; + readonly injectableClass: InjectableClass; } -export function reporterPlugin[]>(p: ReporterPlugin) { - return p; +export function pluginClass[]>(kind: TPluginKind, name: string, injectableClass: InjectableClass): + ClassPlugin { + return { + injectableClass, + kind, + name + }; } -export interface ReporterPlugin[]> extends BasePlugin { } -export interface ConfigEditorPlugin[]> extends BasePlugin { } -export interface MutatorPlugin[]> extends BasePlugin { } -export interface TestFrameworkPlugin[]> extends BasePlugin { } -export interface TestRunnerPlugin[]> extends BasePlugin { } -export interface TranspilerPlugin[]> extends BasePlugin { } +export function pluginFactory[]>(kind: TPluginKind, name: string, factory: InjectableFunction): + FactoryPlugin { + return { + factory, + kind, + name + }; +} -export interface Plugins { - [PluginKind.ConfigEditor]: ConfigEditorPlugin[]>; - [PluginKind.Mutator]: MutatorPlugin[]>; - [PluginKind.Reporter]: ReporterPlugin[]>; - [PluginKind.TestFramework]: TestFrameworkPlugin[]>; - [PluginKind.TestRunner]: TestRunnerPlugin[]>; - [PluginKind.Transpiler]: TranspilerPlugin[]>; +export interface PluginKinds { + [PluginKind.ConfigEditor]: ConfigEditor; + [PluginKind.Mutator]: Mutator; + [PluginKind.Reporter]: Reporter; + [PluginKind.TestFramework]: TestFramework; + [PluginKind.TestRunner]: TestRunner; + [PluginKind.Transpiler]: Transpiler; } +export type Plugins = { + [TPluginKind in keyof PluginKinds]: Plugin[]>; +}; + export interface PluginResolver { resolve(kind: T, name: string): Plugins[T]; } diff --git a/packages/stryker/src/di/PluginLoader.ts b/packages/stryker/src/di/PluginLoader.ts index 62caeb2b5d..71a72670a8 100644 --- a/packages/stryker/src/di/PluginLoader.ts +++ b/packages/stryker/src/di/PluginLoader.ts @@ -4,7 +4,7 @@ import * as _ from 'lodash'; import { tokens, CorrespondingTypes, InjectionToken } from 'typed-inject'; import { importModule } from '../utils/fileUtils'; import { fsAsPromised } from '@stryker-mutator/util'; -import { PluginKind, PluginResolver, Plugins, PluginContexts, BasePlugin } from 'stryker-api/plugin'; +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'; @@ -16,12 +16,12 @@ import { MutatorFactory } from 'stryker-api/mutant'; const IGNORED_PACKAGES = ['stryker-cli', 'stryker-api']; interface PluginModule { - strykerPlugins: BasePlugin[]; + strykerPlugins: Plugin[]; } export default class PluginLoader implements PluginResolver { private readonly log = getLogger(PluginLoader.name); - private readonly pluginsByKind: Map[]> = new Map(); + private readonly pluginsByKind: Map[]> = new Map(); constructor(private readonly pluginDescriptors: string[]) { } @@ -74,7 +74,7 @@ export default class PluginLoader implements PluginResolver { } public static inject = injectionTokens; } - this.loadPlugin({ kind, name, injectable: ProxyPlugin }); + this.loadPlugin({ kind, name, injectableClass: ProxyPlugin }); }); } @@ -131,7 +131,7 @@ export default class PluginLoader implements PluginResolver { } } - private loadPlugin(plugin: BasePlugin) { + private loadPlugin(plugin: Plugin) { let plugins = this.pluginsByKind.get(plugin.kind); if (!plugins) { plugins = []; diff --git a/packages/stryker/src/di/createPlugin.ts b/packages/stryker/src/di/createPlugin.ts new file mode 100644 index 0000000000..a80deeeef9 --- /dev/null +++ b/packages/stryker/src/di/createPlugin.ts @@ -0,0 +1,22 @@ +import { Plugin, PluginKind, PluginContexts, Plugins, PluginKinds, FactoryPlugin, ClassPlugin } from 'stryker-api/plugin'; +import { Injector, InjectionToken } from 'typed-inject'; + +export function createPlugin(kind: TPluginKind, plugin: Plugins[TPluginKind], injector: Injector): + PluginKinds[TPluginKind] { + if (isFactoryPlugin(plugin)) { + return injector.injectFunction(plugin.factory); + } else if (isClassPlugin(plugin)) { + return injector.injectClass(plugin.injectableClass); + } else { + throw new Error(`Plugin "${kind}:${plugin.name}" could not be created, missing "factory" or "injectableClass" property.`); + } +} + +function isFactoryPlugin(plugin: Plugin[]>): + plugin is FactoryPlugin[]> { + return !!(plugin as FactoryPlugin[]>).factory; +} +function isClassPlugin(plugin: Plugin[]>): + plugin is ClassPlugin[]> { + return !!(plugin as ClassPlugin[]>).injectableClass; +} diff --git a/packages/stryker/src/reporters/BroadcastReporter.ts b/packages/stryker/src/reporters/BroadcastReporter.ts index d18599b0f2..de0d1acd4b 100644 --- a/packages/stryker/src/reporters/BroadcastReporter.ts +++ b/packages/stryker/src/reporters/BroadcastReporter.ts @@ -6,6 +6,7 @@ import { PluginResolver, PluginKind, PluginContext } from 'stryker-api/plugin'; import { StrykerOptions } from 'stryker-api/core'; import { Injector, INJECTOR_TOKEN, tokens } from 'typed-inject'; import { commonTokens } from '@stryker-mutator/util'; +import { createPlugin } from '../di/createPlugin'; export default class BroadcastReporter implements StrictReporter { @@ -32,7 +33,7 @@ export default class BroadcastReporter implements StrictReporter { reporterName = 'progress-append-only'; } const plugin = this.pluginResolver.resolve(PluginKind.Reporter, reporterName); - this.reporters[reporterName] = this.injector.injectClass(plugin.injectable); + this.reporters[reporterName] = createPlugin(PluginKind.Reporter, plugin, this.injector); } private logAboutReporters(): void { diff --git a/packages/stryker/src/reporters/index.ts b/packages/stryker/src/reporters/index.ts index 40413eb889..ba81ad0a98 100644 --- a/packages/stryker/src/reporters/index.ts +++ b/packages/stryker/src/reporters/index.ts @@ -4,13 +4,13 @@ import ProgressAppendOnlyReporter from './ProgressAppendOnlyReporter'; import DotsReporter from './DotsReporter'; import EventRecorderReporter from './EventRecorderReporter'; import DashboardReporter from './DashboardReporter'; -import { PluginKind, reporterPlugin } from 'stryker-api/plugin'; +import { PluginKind, pluginClass } from 'stryker-api/plugin'; export const strykerPlugins = [ - reporterPlugin({ name: 'clear-text', kind: PluginKind.Reporter, injectable: ClearTextReporter }), - reporterPlugin({ name: 'progress', kind: PluginKind.Reporter, injectable: ProgressReporter }), - reporterPlugin({ name: 'progress-append-only', kind: PluginKind.Reporter, injectable: ProgressAppendOnlyReporter }), - reporterPlugin({ name: 'dots', kind: PluginKind.Reporter, injectable: DotsReporter }), - reporterPlugin({ name: 'event-recorder', kind: PluginKind.Reporter, injectable: EventRecorderReporter }), - reporterPlugin({ name: 'dashboard', kind: PluginKind.Reporter, injectable: DashboardReporter }) + pluginClass(PluginKind.Reporter, 'clear-text', ClearTextReporter), + pluginClass(PluginKind.Reporter, 'progress', ProgressReporter), + pluginClass(PluginKind.Reporter, 'progress-append-only', ProgressAppendOnlyReporter), + pluginClass(PluginKind.Reporter, 'dots', DotsReporter), + pluginClass(PluginKind.Reporter, 'event-recorder', EventRecorderReporter), + pluginClass(PluginKind.Reporter, 'dashboard', DashboardReporter) ]; diff --git a/packages/stryker/test/unit/di/createPlugin.spec.ts b/packages/stryker/test/unit/di/createPlugin.spec.ts new file mode 100644 index 0000000000..e86ddc2b96 --- /dev/null +++ b/packages/stryker/test/unit/di/createPlugin.spec.ts @@ -0,0 +1,36 @@ +import { createPlugin } from '../../../src/di/createPlugin'; +import { PluginKind, FactoryPlugin, ClassPlugin } from 'stryker-api/plugin'; +import { factory, testInjector } from '@stryker-mutator/test-helpers'; +import { expect } from 'chai'; + +describe('createPlugin', () => { + it('should create a FactoryPlugin using it\'s factory method', () => { + const expectedReporter = factory.reporter(); + const plugin: FactoryPlugin = { + kind: PluginKind.Reporter, + name: 'fooReporter', + factory() { + return expectedReporter; + } + }; + const actualReporter = createPlugin(PluginKind.Reporter, plugin, testInjector.injector); + expect(actualReporter).eq(expectedReporter); + }); + + it('should create a ClassPlugin using it\'s constructor', () => { + class FooReporter { + } + const plugin: ClassPlugin = { + injectableClass: FooReporter, + kind: PluginKind.Reporter, + name: 'fooReporter' + }; + const actualReporter = createPlugin(PluginKind.Reporter, plugin, testInjector.injector); + expect(actualReporter).instanceOf(FooReporter); + }); + + it('should throw if plugin is not recognized', () => { + expect(() => createPlugin(PluginKind.Reporter, { name: 'foo' } as any, testInjector.injector)) + .throws('Plugin "Reporter:foo" could not be created, missing "factory" or "injectableClass" property.'); + }); +}); diff --git a/packages/stryker/test/unit/reporters/BroadcastReporterSpec.ts b/packages/stryker/test/unit/reporters/BroadcastReporterSpec.ts index 1000685d5e..fe67bb248b 100644 --- a/packages/stryker/test/unit/reporters/BroadcastReporterSpec.ts +++ b/packages/stryker/test/unit/reporters/BroadcastReporterSpec.ts @@ -1,7 +1,7 @@ import { expect } from 'chai'; import BroadcastReporter from '../../../src/reporters/BroadcastReporter'; import { ALL_REPORTER_EVENTS } from '../../helpers/producers'; -import { PluginKind, ReporterPlugin } from 'stryker-api/plugin'; +import { PluginKind, FactoryPlugin } from 'stryker-api/plugin'; import * as sinon from 'sinon'; import { testInjector, factory } from '@stryker-mutator/test-helpers'; import { Reporter } from 'stryker-api/report'; @@ -35,7 +35,7 @@ describe('BroadcastReporter', () => { sut = createSut(); // Assert - expect(sut.reporters).deep.eq({ 'progress-append-only': new expectedReporterPlugin.injectable() }); + expect(sut.reporters).deep.eq({ 'progress-append-only': expectedReporterPlugin.reporterStub }); }); it('should create the correct reporters', () => { @@ -134,16 +134,14 @@ describe('BroadcastReporter', () => { return testInjector.injector.injectClass(BroadcastReporter); } - type MockedReporterPlugin = ReporterPlugin<[]> & { reporterStub: sinon.SinonStubbedInstance> }; + type MockedReporterPlugin = FactoryPlugin & { reporterStub: sinon.SinonStubbedInstance> }; function mockReporterPlugin(name: string): MockedReporterPlugin { const reporterStub = factory.reporter(); (reporterStub as any)[name] = name; const reporterPlugin: MockedReporterPlugin = { - injectable: class MockReporterPlugin { - constructor() { - return reporterStub; - } + factory() { + return reporterStub; }, kind: PluginKind.Reporter, name, From 37a2e6816db745d690077a4012fc015399b399af Mon Sep 17 00:00:00 2001 From: Nico Jansen Date: Fri, 11 Jan 2019 12:54:50 +0100 Subject: [PATCH 29/36] refactor(plugins): Move all stryker plugin stuff back to stryker-api Move all stryker plugin stuff back to the api. Talked about it with Simon. We'll allow small pieces of implentation code in the api, as long as it doesn't have any side effects --- packages/stryker-api/plugin.ts | 1 + packages/stryker-api/src/plugin/Contexts.ts | 15 ++++++++------- .../src => stryker-api/src/plugin}/tokens.ts | 7 ++++++- .../test/unit/plugin}/tokens.spec.ts | 2 +- packages/stryker-test-helpers/src/TestInjector.ts | 12 ++++++------ packages/stryker-util/src/index.ts | 1 - packages/stryker/src/Stryker.ts | 3 +-- packages/stryker/src/TestFrameworkOrchestrator.ts | 3 --- packages/stryker/src/di/loggerFactory.ts | 2 +- .../stryker/src/reporters/BroadcastReporter.ts | 3 +-- .../src/reporters/EventRecorderReporter.ts | 3 ++- 11 files changed, 27 insertions(+), 25 deletions(-) rename packages/{stryker-util/src => stryker-api/src/plugin}/tokens.ts (71%) rename packages/{stryker-util/test/unit => stryker-api/test/unit/plugin}/tokens.spec.ts (93%) diff --git a/packages/stryker-api/plugin.ts b/packages/stryker-api/plugin.ts index 9e0788c070..7e10edc4b0 100644 --- a/packages/stryker-api/plugin.ts +++ b/packages/stryker-api/plugin.ts @@ -1,3 +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/plugin/Contexts.ts b/packages/stryker-api/src/plugin/Contexts.ts index ac6cf22451..655fa7d4b1 100644 --- a/packages/stryker-api/src/plugin/Contexts.ts +++ b/packages/stryker-api/src/plugin/Contexts.ts @@ -3,27 +3,28 @@ import { StrykerOptions } from '../../core'; import { PluginResolver } from './Plugins'; import { Config } from '../../config'; import { PluginKind } from './PluginKind'; +import { commonTokens } from './tokens'; export interface StrykerContext { - getLogger: LoggerFactoryMethod; - logger: Logger; - pluginResolver: PluginResolver; + [commonTokens.getLogger]: LoggerFactoryMethod; + [commonTokens.logger]: Logger; + [commonTokens.pluginResolver]: PluginResolver; } export interface TranspilerPluginContext extends PluginContext { - produceSourceMaps: boolean; + [commonTokens.produceSourceMaps]: boolean; } export interface TestRunnerPluginContext extends PluginContext { - sandboxFileNames: ReadonlyArray; + [commonTokens.sandboxFileNames]: ReadonlyArray; } export interface PluginContext extends StrykerContext { - options: StrykerOptions; + [commonTokens.options]: StrykerOptions; /** * @deprecated This is just here to migrate between old and new plugins. Don't use this! Use `options` instead */ - config: Config; + [commonTokens.config]: Config; } export interface PluginContexts { diff --git a/packages/stryker-util/src/tokens.ts b/packages/stryker-api/src/plugin/tokens.ts similarity index 71% rename from packages/stryker-util/src/tokens.ts rename to packages/stryker-api/src/plugin/tokens.ts index fa727db5ee..5c0fdef6e1 100644 --- a/packages/stryker-util/src/tokens.ts +++ b/packages/stryker-api/src/plugin/tokens.ts @@ -3,17 +3,22 @@ function token(value: T): T { return value; } +const target: import('typed-inject').TargetToken = '$target'; +const injector: import('typed-inject').InjectorToken = '$injector'; + export const commonTokens = Object.freeze({ /** * @deprecated Use 'options' instead. This is just hear to support plugin migration */ config: token('config'), getLogger: token('getLogger'), + injector, logger: token('logger'), options: token('options'), pluginResolver: token('pluginResolver'), produceSourceMaps: token('produceSourceMaps'), - sandboxFileNames: token('sandboxFileNames') + sandboxFileNames: token('sandboxFileNames'), + target }); export function tokens(...tokens: TS): TS { diff --git a/packages/stryker-util/test/unit/tokens.spec.ts b/packages/stryker-api/test/unit/plugin/tokens.spec.ts similarity index 93% rename from packages/stryker-util/test/unit/tokens.spec.ts rename to packages/stryker-api/test/unit/plugin/tokens.spec.ts index 571fdb44c3..e3ca0a8609 100644 --- a/packages/stryker-util/test/unit/tokens.spec.ts +++ b/packages/stryker-api/test/unit/plugin/tokens.spec.ts @@ -1,5 +1,5 @@ import { expect } from 'chai'; -import { tokens, commonTokens } from '../../src'; +import { tokens, commonTokens } from '../../../plugin'; describe('tokens', () => { it('should return input as array', () => { diff --git a/packages/stryker-test-helpers/src/TestInjector.ts b/packages/stryker-test-helpers/src/TestInjector.ts index 8a67358a60..c3841555dd 100644 --- a/packages/stryker-test-helpers/src/TestInjector.ts +++ b/packages/stryker-test-helpers/src/TestInjector.ts @@ -1,4 +1,4 @@ -import { PluginResolver, PluginContext } from 'stryker-api/plugin'; +import { PluginResolver, PluginContext, commonTokens } from 'stryker-api/plugin'; import { StrykerOptions } from 'stryker-api/core'; import { Logger } from 'stryker-api/logging'; import * as factory from './factory'; @@ -28,11 +28,11 @@ class TestInjector { public options: Partial; public logger: sinon.SinonStubbedInstance; public injector: Injector = rootInjector - .provideValue('getLogger', this.provideLogger) - .provideFactory('logger', this.provideLogger, Scope.Transient) - .provideFactory('options', this.provideOptions, Scope.Transient) - .provideFactory('config', this.provideConfig, Scope.Transient) - .provideFactory('pluginResolver', this.providePluginResolver, Scope.Transient); + .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 = {}; diff --git a/packages/stryker-util/src/index.ts b/packages/stryker-util/src/index.ts index 8f0b9bcd64..8d70683642 100644 --- a/packages/stryker-util/src/index.ts +++ b/packages/stryker-util/src/index.ts @@ -3,4 +3,3 @@ export { default as childProcessAsPromised } from './childProcessAsPromised'; export { default as promisify } from './promisify'; export { default as StrykerError } from './StrykerError'; export * from './errors'; -export * from './tokens'; diff --git a/packages/stryker/src/Stryker.ts b/packages/stryker/src/Stryker.ts index cfee0154f3..7aeb08d656 100644 --- a/packages/stryker/src/Stryker.ts +++ b/packages/stryker/src/Stryker.ts @@ -20,10 +20,9 @@ import MutationTestExecutor from './process/MutationTestExecutor'; import InputFileCollection from './input/InputFileCollection'; import LogConfigurator from './logging/LogConfigurator'; import BroadcastReporter from './reporters/BroadcastReporter'; -import { PluginContext, PluginResolver } from 'stryker-api/plugin'; +import { commonTokens, PluginContext, PluginResolver } from 'stryker-api/plugin'; import { Injector, rootInjector, Scope } from 'typed-inject'; import { loggerFactory } from './di/loggerFactory'; -import { commonTokens } from '@stryker-mutator/util'; export default class Stryker { diff --git a/packages/stryker/src/TestFrameworkOrchestrator.ts b/packages/stryker/src/TestFrameworkOrchestrator.ts index 9db0fab320..5cd0e39422 100644 --- a/packages/stryker/src/TestFrameworkOrchestrator.ts +++ b/packages/stryker/src/TestFrameworkOrchestrator.ts @@ -1,13 +1,10 @@ import { TestFrameworkFactory, TestFramework } from 'stryker-api/test_framework'; import { StrykerOptions } from 'stryker-api/core'; import { getLogger } from 'stryker-api/logging'; -import { commonTokens } from '@stryker-mutator/util'; -import { tokens } from 'typed-inject'; export default class TestFrameworkOrchestrator { private readonly log = getLogger(TestFrameworkOrchestrator.name); - public static inject = tokens(commonTokens.options); constructor(private readonly options: StrykerOptions) { } public determineTestFramework(): TestFramework | null { diff --git a/packages/stryker/src/di/loggerFactory.ts b/packages/stryker/src/di/loggerFactory.ts index 5acb72996c..52e55eb18f 100644 --- a/packages/stryker/src/di/loggerFactory.ts +++ b/packages/stryker/src/di/loggerFactory.ts @@ -1,6 +1,6 @@ import { LoggerFactoryMethod } from 'stryker-api/logging'; import { TARGET_TOKEN, tokens } from 'typed-inject'; -import { commonTokens } from '@stryker-mutator/util'; +import { commonTokens } from 'stryker-api/plugin'; export function loggerFactory(getLogger: LoggerFactoryMethod, target: Function | undefined) { return getLogger(target ? target.name : 'UNKNOWN'); diff --git a/packages/stryker/src/reporters/BroadcastReporter.ts b/packages/stryker/src/reporters/BroadcastReporter.ts index de0d1acd4b..5e1c7bb9b6 100644 --- a/packages/stryker/src/reporters/BroadcastReporter.ts +++ b/packages/stryker/src/reporters/BroadcastReporter.ts @@ -2,10 +2,9 @@ import { Reporter, SourceFile, MutantResult, MatchedMutant, ScoreResult } from ' import { Logger } from 'stryker-api/logging'; import { isPromise } from '../utils/objectUtils'; import StrictReporter from './StrictReporter'; -import { PluginResolver, PluginKind, PluginContext } from 'stryker-api/plugin'; +import { commonTokens, PluginResolver, PluginKind, PluginContext } from 'stryker-api/plugin'; import { StrykerOptions } from 'stryker-api/core'; import { Injector, INJECTOR_TOKEN, tokens } from 'typed-inject'; -import { commonTokens } from '@stryker-mutator/util'; import { createPlugin } from '../di/createPlugin'; export default class BroadcastReporter implements StrictReporter { diff --git a/packages/stryker/src/reporters/EventRecorderReporter.ts b/packages/stryker/src/reporters/EventRecorderReporter.ts index 0874d71703..7cc40764f6 100644 --- a/packages/stryker/src/reporters/EventRecorderReporter.ts +++ b/packages/stryker/src/reporters/EventRecorderReporter.ts @@ -4,7 +4,8 @@ import { StrykerOptions } from 'stryker-api/core'; import { SourceFile, MutantResult, MatchedMutant, Reporter, ScoreResult } from 'stryker-api/report'; import { cleanFolder } from '../utils/fileUtils'; import StrictReporter from './StrictReporter'; -import { fsAsPromised, commonTokens } from '@stryker-mutator/util'; +import { fsAsPromised } from '@stryker-mutator/util'; +import { commonTokens } from 'stryker-api/plugin'; const DEFAULT_BASE_FOLDER = 'reports/mutation/events'; From 2dc8ed67ea5cde6073921c14fedd31c1975505b7 Mon Sep 17 00:00:00 2001 From: Nico Jansen Date: Fri, 11 Jan 2019 20:03:27 +0100 Subject: [PATCH 30/36] docs(api): Document the plugin api --- packages/stryker-api/src/plugin/Contexts.ts | 44 ++++++++++++------ packages/stryker-api/src/plugin/PluginKind.ts | 3 ++ packages/stryker-api/src/plugin/Plugins.ts | 46 ++++++++++++++++--- packages/stryker-api/src/plugin/tokens.ts | 12 +++++ packages/typed-inject/src/tokens.ts | 10 +++- 5 files changed, 94 insertions(+), 21 deletions(-) diff --git a/packages/stryker-api/src/plugin/Contexts.ts b/packages/stryker-api/src/plugin/Contexts.ts index 655fa7d4b1..10e32f9101 100644 --- a/packages/stryker-api/src/plugin/Contexts.ts +++ b/packages/stryker-api/src/plugin/Contexts.ts @@ -5,21 +5,20 @@ import { Config } from '../../config'; import { PluginKind } from './PluginKind'; import { commonTokens } from './tokens'; -export interface StrykerContext { +/** + * The basic dependency injection context within Stryker + */ +export interface BaseContext { [commonTokens.getLogger]: LoggerFactoryMethod; [commonTokens.logger]: Logger; [commonTokens.pluginResolver]: PluginResolver; } -export interface TranspilerPluginContext extends PluginContext { - [commonTokens.produceSourceMaps]: boolean; -} - -export interface TestRunnerPluginContext extends PluginContext { - [commonTokens.sandboxFileNames]: ReadonlyArray; -} - -export interface PluginContext extends StrykerContext { +/** + * 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 @@ -27,11 +26,28 @@ export interface PluginContext extends StrykerContext { [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]: StrykerContext; - [PluginKind.Mutator]: PluginContext; - [PluginKind.Reporter]: PluginContext; - [PluginKind.TestFramework]: PluginContext; + [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 index dbb1fcbb9f..76201af0f2 100644 --- a/packages/stryker-api/src/plugin/PluginKind.ts +++ b/packages/stryker-api/src/plugin/PluginKind.ts @@ -1,3 +1,6 @@ +/** + * The plugin kinds supported by Stryker + */ export enum PluginKind { ConfigEditor = 'ConfigEditor', TestRunner = 'TestRunner', diff --git a/packages/stryker-api/src/plugin/Plugins.ts b/packages/stryker-api/src/plugin/Plugins.ts index a42ced0d93..10adc4aea6 100644 --- a/packages/stryker-api/src/plugin/Plugins.ts +++ b/packages/stryker-api/src/plugin/Plugins.ts @@ -8,21 +8,40 @@ 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; - readonly factory: InjectableFunction; + /** + * 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; + readonly injectableClass: InjectableClass; } -export function pluginClass[]>(kind: TPluginKind, name: string, 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, @@ -31,7 +50,13 @@ export function pluginClass[]>(kind: TPluginKind, name: string, factory: InjectableFunction): +/** + * 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, @@ -40,7 +65,10 @@ export function pluginFactory[]>; + [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 index 5c0fdef6e1..66f093c01f 100644 --- a/packages/stryker-api/src/plugin/tokens.ts +++ b/packages/stryker-api/src/plugin/tokens.ts @@ -6,6 +6,9 @@ function token(value: T): T { 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 @@ -21,6 +24,15 @@ export const commonTokens = Object.freeze({ 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/typed-inject/src/tokens.ts b/packages/typed-inject/src/tokens.ts index 6fa2601577..d8317928cf 100644 --- a/packages/typed-inject/src/tokens.ts +++ b/packages/typed-inject/src/tokens.ts @@ -1,4 +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; } From 416fadfb5f20a7f14492a2e58dafe6f460c3f8b8 Mon Sep 17 00:00:00 2001 From: Nico Jansen Date: Fri, 11 Jan 2019 20:10:40 +0100 Subject: [PATCH 31/36] refactor: Change naming of plugin contexts and methods for declaring plugins --- packages/stryker-test-helpers/src/TestInjector.ts | 4 ++-- packages/stryker/src/Stryker.ts | 4 ++-- packages/stryker/src/di/createPlugin.ts | 4 ++-- .../stryker/src/reporters/BroadcastReporter.ts | 4 ++-- packages/stryker/src/reporters/index.ts | 14 +++++++------- packages/stryker/test/unit/StrykerSpec.ts | 4 ++-- 6 files changed, 17 insertions(+), 17 deletions(-) diff --git a/packages/stryker-test-helpers/src/TestInjector.ts b/packages/stryker-test-helpers/src/TestInjector.ts index c3841555dd..a0aa6033bb 100644 --- a/packages/stryker-test-helpers/src/TestInjector.ts +++ b/packages/stryker-test-helpers/src/TestInjector.ts @@ -1,4 +1,4 @@ -import { PluginResolver, PluginContext, commonTokens } from 'stryker-api/plugin'; +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'; @@ -27,7 +27,7 @@ class TestInjector { public pluginResolver: sinon.SinonStubbedInstance; public options: Partial; public logger: sinon.SinonStubbedInstance; - public injector: Injector = rootInjector + public injector: Injector = rootInjector .provideValue(commonTokens.getLogger, this.provideLogger) .provideFactory(commonTokens.logger, this.provideLogger, Scope.Transient) .provideFactory(commonTokens.options, this.provideOptions, Scope.Transient) diff --git a/packages/stryker/src/Stryker.ts b/packages/stryker/src/Stryker.ts index 7aeb08d656..f166be9b03 100644 --- a/packages/stryker/src/Stryker.ts +++ b/packages/stryker/src/Stryker.ts @@ -20,7 +20,7 @@ import MutationTestExecutor from './process/MutationTestExecutor'; import InputFileCollection from './input/InputFileCollection'; import LogConfigurator from './logging/LogConfigurator'; import BroadcastReporter from './reporters/BroadcastReporter'; -import { commonTokens, PluginContext, PluginResolver } from 'stryker-api/plugin'; +import { commonTokens, OptionsContext, PluginResolver } from 'stryker-api/plugin'; import { Injector, rootInjector, Scope } from 'typed-inject'; import { loggerFactory } from './di/loggerFactory'; @@ -31,7 +31,7 @@ export default class Stryker { private readonly reporter: BroadcastReporter; private readonly testFramework: TestFramework | null; private readonly log = getLogger(Stryker.name); - private readonly injector: Injector; + private readonly injector: Injector; /** * The Stryker mutation tester. diff --git a/packages/stryker/src/di/createPlugin.ts b/packages/stryker/src/di/createPlugin.ts index a80deeeef9..53f8d62b05 100644 --- a/packages/stryker/src/di/createPlugin.ts +++ b/packages/stryker/src/di/createPlugin.ts @@ -1,8 +1,8 @@ -import { Plugin, PluginKind, PluginContexts, Plugins, PluginKinds, FactoryPlugin, ClassPlugin } from 'stryker-api/plugin'; +import { Plugin, PluginKind, PluginContexts, Plugins, PluginInterfaces, FactoryPlugin, ClassPlugin } from 'stryker-api/plugin'; import { Injector, InjectionToken } from 'typed-inject'; export function createPlugin(kind: TPluginKind, plugin: Plugins[TPluginKind], injector: Injector): - PluginKinds[TPluginKind] { + PluginInterfaces[TPluginKind] { if (isFactoryPlugin(plugin)) { return injector.injectFunction(plugin.factory); } else if (isClassPlugin(plugin)) { diff --git a/packages/stryker/src/reporters/BroadcastReporter.ts b/packages/stryker/src/reporters/BroadcastReporter.ts index 5e1c7bb9b6..c3a5596bf2 100644 --- a/packages/stryker/src/reporters/BroadcastReporter.ts +++ b/packages/stryker/src/reporters/BroadcastReporter.ts @@ -2,7 +2,7 @@ import { Reporter, SourceFile, MutantResult, MatchedMutant, ScoreResult } from ' import { Logger } from 'stryker-api/logging'; import { isPromise } from '../utils/objectUtils'; import StrictReporter from './StrictReporter'; -import { commonTokens, PluginResolver, PluginKind, PluginContext } from 'stryker-api/plugin'; +import { commonTokens, PluginResolver, PluginKind, OptionsContext } from 'stryker-api/plugin'; import { StrykerOptions } from 'stryker-api/core'; import { Injector, INJECTOR_TOKEN, tokens } from 'typed-inject'; import { createPlugin } from '../di/createPlugin'; @@ -17,7 +17,7 @@ export default class BroadcastReporter implements StrictReporter { constructor( private readonly options: StrykerOptions, private readonly pluginResolver: PluginResolver, - private readonly injector: Injector, + private readonly injector: Injector, private readonly log: Logger) { this.reporters = {}; this.options.reporters.forEach(reporterName => this.createReporter(reporterName)); diff --git a/packages/stryker/src/reporters/index.ts b/packages/stryker/src/reporters/index.ts index ba81ad0a98..07389757ef 100644 --- a/packages/stryker/src/reporters/index.ts +++ b/packages/stryker/src/reporters/index.ts @@ -4,13 +4,13 @@ import ProgressAppendOnlyReporter from './ProgressAppendOnlyReporter'; import DotsReporter from './DotsReporter'; import EventRecorderReporter from './EventRecorderReporter'; import DashboardReporter from './DashboardReporter'; -import { PluginKind, pluginClass } from 'stryker-api/plugin'; +import { PluginKind, declareClassPlugin } from 'stryker-api/plugin'; export const strykerPlugins = [ - pluginClass(PluginKind.Reporter, 'clear-text', ClearTextReporter), - pluginClass(PluginKind.Reporter, 'progress', ProgressReporter), - pluginClass(PluginKind.Reporter, 'progress-append-only', ProgressAppendOnlyReporter), - pluginClass(PluginKind.Reporter, 'dots', DotsReporter), - pluginClass(PluginKind.Reporter, 'event-recorder', EventRecorderReporter), - pluginClass(PluginKind.Reporter, 'dashboard', DashboardReporter) + 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/test/unit/StrykerSpec.ts b/packages/stryker/test/unit/StrykerSpec.ts index 2f008822ec..fcf73600d0 100644 --- a/packages/stryker/test/unit/StrykerSpec.ts +++ b/packages/stryker/test/unit/StrykerSpec.ts @@ -25,7 +25,7 @@ import TestableMutant from '../../src/TestableMutant'; import InputFileCollection from '../../src/input/InputFileCollection'; import LogConfigurator from '../../src/logging/LogConfigurator'; import LoggingClientContext from '../../src/logging/LoggingClientContext'; -import { PluginContext } from 'stryker-api/plugin'; +import { OptionsContext } from 'stryker-api/plugin'; class FakeConfigEditor implements ConfigEditor { constructor() { } @@ -58,7 +58,7 @@ describe('Stryker', () => { let configureMainProcessStub: sinon.SinonStub; let configureLoggingServerStub: sinon.SinonStub; let shutdownLoggingStub: sinon.SinonStub; - let injectorMock: Mock>; + let injectorMock: Mock>; beforeEach(() => { strykerConfig = config(); From 74608ae1bf48df30bfba30975d4cd003eda50ad5 Mon Sep 17 00:00:00 2001 From: Nico Jansen Date: Thu, 17 Jan 2019 18:03:33 +0100 Subject: [PATCH 32/36] feat(di): Create a `PluginCreator` that can be reused for all plugin creators --- packages/stryker-test-helpers/src/factory.ts | 4 +- packages/stryker/src/Stryker.ts | 8 ++- packages/stryker/src/di/PluginCreator.ts | 40 ++++++++++++ packages/stryker/src/di/coreTokens.ts | 2 + packages/stryker/src/di/createPlugin.ts | 22 ------- .../src/reporters/BroadcastReporter.ts | 18 +++--- .../test/unit/di/PluginCreator.spec.ts | 58 +++++++++++++++++ .../stryker/test/unit/di/createPlugin.spec.ts | 36 ----------- .../unit/reporters/BroadcastReporterSpec.ts | 62 +++++++++---------- .../typed-inject/src/api/CorrespondingType.ts | 4 +- .../typed-inject/src/api/InjectionToken.ts | 2 +- 11 files changed, 150 insertions(+), 106 deletions(-) create mode 100644 packages/stryker/src/di/PluginCreator.ts create mode 100644 packages/stryker/src/di/coreTokens.ts delete mode 100644 packages/stryker/src/di/createPlugin.ts create mode 100644 packages/stryker/test/unit/di/PluginCreator.spec.ts delete mode 100644 packages/stryker/test/unit/di/createPlugin.spec.ts diff --git a/packages/stryker-test-helpers/src/factory.ts b/packages/stryker-test-helpers/src/factory.ts index 7e264eb754..63441d0cd2 100644 --- a/packages/stryker-test-helpers/src/factory.ts +++ b/packages/stryker-test-helpers/src/factory.ts @@ -130,8 +130,8 @@ export const config = factoryMethod(() => new Config()); export const ALL_REPORTER_EVENTS: (keyof Reporter)[] = ['onSourceFileRead', 'onAllSourceFilesRead', 'onAllMutantsMatchedWithTests', 'onMutantTested', 'onAllMutantsTested', 'onScoreCalculated', 'wrapUp']; -export function reporter(): sinon.SinonStubbedInstance> { - const reporter = {} as any; +export function reporter(name = 'fooReporter'): sinon.SinonStubbedInstance> { + const reporter = { name } as any; ALL_REPORTER_EVENTS.forEach(event => reporter[event] = sinon.stub()); return reporter; } diff --git a/packages/stryker/src/Stryker.ts b/packages/stryker/src/Stryker.ts index f166be9b03..a3865e41b4 100644 --- a/packages/stryker/src/Stryker.ts +++ b/packages/stryker/src/Stryker.ts @@ -20,9 +20,11 @@ import MutationTestExecutor from './process/MutationTestExecutor'; import InputFileCollection from './input/InputFileCollection'; import LogConfigurator from './logging/LogConfigurator'; import BroadcastReporter from './reporters/BroadcastReporter'; -import { commonTokens, OptionsContext, PluginResolver } from 'stryker-api/plugin'; +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 { @@ -57,7 +59,9 @@ export default class Stryker { .provideValue(commonTokens.pluginResolver, pluginLoader as PluginResolver) .provideValue(commonTokens.config, this.config) .provideValue(commonTokens.options, this.config as StrykerOptions); - this.reporter = this.injector.injectClass(BroadcastReporter); + 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(); } 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/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/createPlugin.ts b/packages/stryker/src/di/createPlugin.ts deleted file mode 100644 index 53f8d62b05..0000000000 --- a/packages/stryker/src/di/createPlugin.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { Plugin, PluginKind, PluginContexts, Plugins, PluginInterfaces, FactoryPlugin, ClassPlugin } from 'stryker-api/plugin'; -import { Injector, InjectionToken } from 'typed-inject'; - -export function createPlugin(kind: TPluginKind, plugin: Plugins[TPluginKind], injector: Injector): - PluginInterfaces[TPluginKind] { - if (isFactoryPlugin(plugin)) { - return injector.injectFunction(plugin.factory); - } else if (isClassPlugin(plugin)) { - return injector.injectClass(plugin.injectableClass); - } else { - throw new Error(`Plugin "${kind}:${plugin.name}" could not be created, missing "factory" or "injectableClass" property.`); - } -} - -function isFactoryPlugin(plugin: Plugin[]>): - plugin is FactoryPlugin[]> { - return !!(plugin as FactoryPlugin[]>).factory; -} -function isClassPlugin(plugin: Plugin[]>): - plugin is ClassPlugin[]> { - return !!(plugin as ClassPlugin[]>).injectableClass; -} diff --git a/packages/stryker/src/reporters/BroadcastReporter.ts b/packages/stryker/src/reporters/BroadcastReporter.ts index c3a5596bf2..8b78dd703f 100644 --- a/packages/stryker/src/reporters/BroadcastReporter.ts +++ b/packages/stryker/src/reporters/BroadcastReporter.ts @@ -2,22 +2,25 @@ import { Reporter, SourceFile, MutantResult, MatchedMutant, ScoreResult } from ' import { Logger } from 'stryker-api/logging'; import { isPromise } from '../utils/objectUtils'; import StrictReporter from './StrictReporter'; -import { commonTokens, PluginResolver, PluginKind, OptionsContext } from 'stryker-api/plugin'; +import { commonTokens, PluginKind } from 'stryker-api/plugin'; import { StrykerOptions } from 'stryker-api/core'; -import { Injector, INJECTOR_TOKEN, tokens } from 'typed-inject'; -import { createPlugin } from '../di/createPlugin'; +import { tokens } from 'typed-inject'; +import * as coreTokens from '../di/coreTokens'; +import { PluginCreator } from '../di/PluginCreator'; export default class BroadcastReporter implements StrictReporter { - public static readonly inject = tokens(commonTokens.options, commonTokens.pluginResolver, INJECTOR_TOKEN, commonTokens.logger); + public static readonly inject = tokens( + commonTokens.options, + coreTokens.reporterPluginCreator, + commonTokens.logger); public readonly reporters: { [name: string]: Reporter; }; constructor( private readonly options: StrykerOptions, - private readonly pluginResolver: PluginResolver, - private readonly injector: Injector, + private readonly pluginCreator: PluginCreator, private readonly log: Logger) { this.reporters = {}; this.options.reporters.forEach(reporterName => this.createReporter(reporterName)); @@ -31,8 +34,7 @@ export default class BroadcastReporter implements StrictReporter { ); reporterName = 'progress-append-only'; } - const plugin = this.pluginResolver.resolve(PluginKind.Reporter, reporterName); - this.reporters[reporterName] = createPlugin(PluginKind.Reporter, plugin, this.injector); + this.reporters[reporterName] = this.pluginCreator.create(reporterName); } private logAboutReporters(): void { 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/di/createPlugin.spec.ts b/packages/stryker/test/unit/di/createPlugin.spec.ts deleted file mode 100644 index e86ddc2b96..0000000000 --- a/packages/stryker/test/unit/di/createPlugin.spec.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { createPlugin } from '../../../src/di/createPlugin'; -import { PluginKind, FactoryPlugin, ClassPlugin } from 'stryker-api/plugin'; -import { factory, testInjector } from '@stryker-mutator/test-helpers'; -import { expect } from 'chai'; - -describe('createPlugin', () => { - it('should create a FactoryPlugin using it\'s factory method', () => { - const expectedReporter = factory.reporter(); - const plugin: FactoryPlugin = { - kind: PluginKind.Reporter, - name: 'fooReporter', - factory() { - return expectedReporter; - } - }; - const actualReporter = createPlugin(PluginKind.Reporter, plugin, testInjector.injector); - expect(actualReporter).eq(expectedReporter); - }); - - it('should create a ClassPlugin using it\'s constructor', () => { - class FooReporter { - } - const plugin: ClassPlugin = { - injectableClass: FooReporter, - kind: PluginKind.Reporter, - name: 'fooReporter' - }; - const actualReporter = createPlugin(PluginKind.Reporter, plugin, testInjector.injector); - expect(actualReporter).instanceOf(FooReporter); - }); - - it('should throw if plugin is not recognized', () => { - expect(() => createPlugin(PluginKind.Reporter, { name: 'foo' } as any, testInjector.injector)) - .throws('Plugin "Reporter:foo" could not be created, missing "factory" or "injectableClass" property.'); - }); -}); diff --git a/packages/stryker/test/unit/reporters/BroadcastReporterSpec.ts b/packages/stryker/test/unit/reporters/BroadcastReporterSpec.ts index fe67bb248b..7bba6b759f 100644 --- a/packages/stryker/test/unit/reporters/BroadcastReporterSpec.ts +++ b/packages/stryker/test/unit/reporters/BroadcastReporterSpec.ts @@ -1,23 +1,30 @@ import { expect } from 'chai'; import BroadcastReporter from '../../../src/reporters/BroadcastReporter'; import { ALL_REPORTER_EVENTS } from '../../helpers/producers'; -import { PluginKind, FactoryPlugin } from 'stryker-api/plugin'; +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 sut: BroadcastReporter; - let rep1Plugin: MockedReporterPlugin; - let rep2Plugin: MockedReporterPlugin; + let rep1: sinon.SinonStubbedInstance>; + let rep2: sinon.SinonStubbedInstance>; let isTTY: boolean; + let pluginCreatorMock: sinon.SinonStubbedInstance>; beforeEach(() => { captureTTY(); testInjector.options.reporters = ['rep1', 'rep2']; - rep1Plugin = mockReporterPlugin('rep1'); - rep2Plugin = mockReporterPlugin('rep2'); + rep1 = factory.reporter('rep1'); + rep2 = factory.reporter('rep2'); + pluginCreatorMock = sinon.createStubInstance(PluginCreator); + pluginCreatorMock.create + .withArgs('rep1').returns(rep1) + .withArgs('rep2').returns(rep2); }); afterEach(() => { @@ -28,29 +35,32 @@ describe('BroadcastReporter', () => { it('should create "progress-append-only" instead of "progress" reporter if process.stdout is not a tty', () => { // Arrange setTTY(false); - const expectedReporterPlugin = mockReporterPlugin('progress-append-only'); + 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': expectedReporterPlugin.reporterStub }); + 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 progressReporterPlugin = mockReporterPlugin('progress'); + const progress = factory.reporter('progress'); + pluginCreatorMock.create.withArgs('progress').returns(progress); // Act sut = createSut(); // Assert expect(sut.reporters).deep.eq({ - progress: progressReporterPlugin.reporterStub, - rep2: rep2Plugin.reporterStub + progress, + rep2 }); }); @@ -79,11 +89,11 @@ describe('BroadcastReporter', () => { beforeEach(() => { isResolved = false; - rep1Plugin.reporterStub.wrapUp.returns(new Promise((resolve, reject) => { + rep1.wrapUp.returns(new Promise((resolve, reject) => { wrapUpResolveFn = resolve; wrapUpRejectFn = reject; })); - rep2Plugin.reporterStub.wrapUp.returns(new Promise(resolve => wrapUpResolveFn2 = resolve)); + rep2.wrapUp.returns(new Promise(resolve => wrapUpResolveFn2 = resolve)); result = sut.wrapUp().then(() => void (isResolved = true)); }); @@ -112,7 +122,7 @@ describe('BroadcastReporter', () => { describe('with one faulty reporter', () => { beforeEach(() => { - ALL_REPORTER_EVENTS.forEach(eventName => rep1Plugin.reporterStub[eventName].throws('some error')); + ALL_REPORTER_EVENTS.forEach(eventName => rep1[eventName].throws('some error')); }); it('should still broadcast to other reporters', () => { @@ -131,33 +141,17 @@ describe('BroadcastReporter', () => { }); function createSut() { - return testInjector.injector.injectClass(BroadcastReporter); - } - - type MockedReporterPlugin = FactoryPlugin & { reporterStub: sinon.SinonStubbedInstance> }; - - function mockReporterPlugin(name: string): MockedReporterPlugin { - const reporterStub = factory.reporter(); - (reporterStub as any)[name] = name; - const reporterPlugin: MockedReporterPlugin = { - factory() { - return reporterStub; - }, - kind: PluginKind.Reporter, - name, - reporterStub - }; - testInjector.pluginResolver.resolve - .withArgs(PluginKind.Reporter, reporterPlugin.name).returns(reporterPlugin); - return reporterPlugin; + 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 as any)[eventName](eventName); - expect(rep1Plugin.reporterStub[eventName]).to.have.been.calledWith(eventData); - expect(rep2Plugin.reporterStub[eventName]).to.have.been.calledWith(eventData); + expect(rep1[eventName]).calledWith(eventData); + expect(rep2[eventName]).calledWith(eventData); }); } diff --git a/packages/typed-inject/src/api/CorrespondingType.ts b/packages/typed-inject/src/api/CorrespondingType.ts index 0d262d50ef..fe2a3dcbe5 100644 --- a/packages/typed-inject/src/api/CorrespondingType.ts +++ b/packages/typed-inject/src/api/CorrespondingType.ts @@ -2,7 +2,9 @@ import { InjectionToken, InjectorToken, TargetToken } from './InjectionToken'; import { Injector } from './Injector'; export type CorrespondingType> = - T extends keyof TContext ? TContext[T] : T extends InjectorToken ? Injector : T extends TargetToken ? Function | undefined : never; + 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/InjectionToken.ts b/packages/typed-inject/src/api/InjectionToken.ts index be0876647e..e068206d47 100644 --- a/packages/typed-inject/src/api/InjectionToken.ts +++ b/packages/typed-inject/src/api/InjectionToken.ts @@ -2,4 +2,4 @@ export type InjectorToken = '$injector'; export type TargetToken = '$target'; export const INJECTOR_TOKEN: InjectorToken = '$injector'; export const TARGET_TOKEN: TargetToken = '$target'; -export type InjectionToken = keyof TContext | InjectorToken | TargetToken; +export type InjectionToken = InjectorToken | TargetToken | keyof TContext; From 6da93951a8dc021f6f36fbac6ee83c74e7584502 Mon Sep 17 00:00:00 2001 From: Hugo van Rijswijk Date: Thu, 17 Jan 2019 20:12:10 +0100 Subject: [PATCH 33/36] Fix typo in typed-inject readme (#1321) --- packages/typed-inject/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/typed-inject/README.md b/packages/typed-inject/README.md index fc71777fbe..413bc38ff7 100644 --- a/packages/typed-inject/README.md +++ b/packages/typed-inject/README.md @@ -258,7 +258,7 @@ 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 heart felled thanks! πŸ’– +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. From 3f62298532b254655e4850cd9c0ff0a4b50b7f64 Mon Sep 17 00:00:00 2001 From: Nico Jansen Date: Tue, 22 Jan 2019 15:16:32 +0100 Subject: [PATCH 34/36] docs(review-comments): Implement review comments --- packages/stryker-api/src/plugin/tokens.ts | 20 ++++++++++++-------- packages/typed-inject/README.md | 6 +++--- 2 files changed, 15 insertions(+), 11 deletions(-) diff --git a/packages/stryker-api/src/plugin/tokens.ts b/packages/stryker-api/src/plugin/tokens.ts index 66f093c01f..75f17ae780 100644 --- a/packages/stryker-api/src/plugin/tokens.ts +++ b/packages/stryker-api/src/plugin/tokens.ts @@ -1,5 +1,9 @@ -function token(value: T): T { +/** + * Define a string literal. + * @param value Token literal + */ +function stringLiteral(value: T): T { return value; } @@ -13,14 +17,14 @@ export const commonTokens = Object.freeze({ /** * @deprecated Use 'options' instead. This is just hear to support plugin migration */ - config: token('config'), - getLogger: token('getLogger'), + config: stringLiteral('config'), + getLogger: stringLiteral('getLogger'), injector, - logger: token('logger'), - options: token('options'), - pluginResolver: token('pluginResolver'), - produceSourceMaps: token('produceSourceMaps'), - sandboxFileNames: token('sandboxFileNames'), + logger: stringLiteral('logger'), + options: stringLiteral('options'), + pluginResolver: stringLiteral('pluginResolver'), + produceSourceMaps: stringLiteral('produceSourceMaps'), + sandboxFileNames: stringLiteral('sandboxFileNames'), target }); diff --git a/packages/typed-inject/README.md b/packages/typed-inject/README.md index 413bc38ff7..16de139615 100644 --- a/packages/typed-inject/README.md +++ b/packages/typed-inject/README.md @@ -65,7 +65,7 @@ const myService = appInjector.injectClass(MyService); In this example: * the `logger` is injected into a new instance of `HttpClient` by value. -* The instance of `HttpClient` and the `logger` is injected into a new instance of `MyService`. +* 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. @@ -102,8 +102,8 @@ Any `Injector` instance can always inject the following tokens: | Token name | Token value | Description | | - | - | - | -| `TARGET_TOKEN` | `'$target'` | The class or function in which the current values is injected, or `undefined` if resolved directly | | `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: @@ -164,7 +164,7 @@ class Foo { constructor(bar: number) { } static inject = tokens('bar'); } -const foo /*: Foo*/ injector.injectClass(Foo); +const foo /*: Foo*/ = injector.injectClass(Foo); ``` #### `injector.injectFunction(fn: InjectableFunction)` From 52a976c8740894f7b97f96c2c81383bc7a62526b Mon Sep 17 00:00:00 2001 From: Nico Jansen Date: Tue, 22 Jan 2019 15:23:10 +0100 Subject: [PATCH 35/36] docs(typed-inject): Fix typo in readme --- packages/typed-inject/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/typed-inject/README.md b/packages/typed-inject/README.md index 16de139615..0533645b2a 100644 --- a/packages/typed-inject/README.md +++ b/packages/typed-inject/README.md @@ -177,7 +177,7 @@ function foo(bar: number) { return bar + 1; } foo.inject = tokens('bar'); -const baz /*: number*/ injector.injectFunction(Foo); +const baz /*: number*/ = injector.injectFunction(Foo); ``` #### `injector.resolve(token: Token): CorrespondingType` From a7abc075c7fdb15f99152ed951437c98e3ff14a0 Mon Sep 17 00:00:00 2001 From: Nico Jansen Date: Tue, 22 Jan 2019 15:24:54 +0100 Subject: [PATCH 36/36] docs(readme): fix typo's --- packages/typed-inject/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/typed-inject/README.md b/packages/typed-inject/README.md index 0533645b2a..edab9c2d56 100644 --- a/packages/typed-inject/README.md +++ b/packages/typed-inject/README.md @@ -9,7 +9,7 @@ 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'/'In version 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)_ +_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 @@ -64,7 +64,7 @@ const myService = appInjector.injectClass(MyService); In this example: -* the `logger` is injected into a new instance of `HttpClient` by value. +* 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.