1 | import semver from 'semver';
|
2 |
|
3 | guardMinimalNodeVersion();
|
4 |
|
5 | import { Command } from 'commander';
|
6 | import { MutantResult, DashboardOptions, ALL_REPORT_TYPES, PartialStrykerOptions } from '@stryker-mutator/api/core';
|
7 |
|
8 | import { initializerFactory } from './initializer/index.js';
|
9 | import { LogConfigurator } from './logging/index.js';
|
10 | import { Stryker } from './stryker.js';
|
11 | import { defaultOptions } from './config/index.js';
|
12 | import { strykerEngines, strykerVersion } from './stryker-package.js';
|
13 |
|
14 |
|
15 |
|
16 |
|
17 |
|
18 |
|
19 | function deepOption<T extends string, R>(object: { [K in T]?: R }, key: T) {
|
20 | return (value: R) => {
|
21 | object[key] = value;
|
22 | return undefined;
|
23 | };
|
24 | }
|
25 |
|
26 | const list = createSplitter(',');
|
27 |
|
28 | function createSplitter(sep: string) {
|
29 | return (val: string) => val.split(sep).filter(Boolean);
|
30 | }
|
31 |
|
32 | function parseBoolean(val: string) {
|
33 | const v = val.toLocaleLowerCase();
|
34 | return v !== 'false' && v !== '0';
|
35 | }
|
36 |
|
37 | export class StrykerCli {
|
38 | private command = '';
|
39 | private strykerConfig: string | null = null;
|
40 |
|
41 | constructor(
|
42 | private readonly argv: string[],
|
43 | private readonly program: Command = new Command(),
|
44 | private readonly runMutationTest = async (options: PartialStrykerOptions) => new Stryker(options).runMutationTest()
|
45 | ) {}
|
46 |
|
47 | public run(): void {
|
48 | const dashboard: Partial<DashboardOptions> = {};
|
49 | this.program
|
50 |
|
51 | .version(strykerVersion)
|
52 | .usage('<command> [options] [configFile]')
|
53 | .description(
|
54 | `Possible commands:
|
55 | run: Run mutation testing
|
56 | init: Initialize Stryker for your project
|
57 |
|
58 | Optional location to a JSON or JavaScript config file as the last argument. If it's a JavaScript file, that file should export the config directly.`
|
59 | )
|
60 | .arguments('<command> [configFile]')
|
61 | .action((cmd: string, config: string) => {
|
62 | this.command = cmd;
|
63 | this.strykerConfig = config;
|
64 | })
|
65 | .option(
|
66 | '-f, --files <allFiles>',
|
67 | '[DEPRECATED, please use the inverse option `--ignorePatterns` instead] A comma separated list of patterns used for selecting all files needed to run the tests. For a more detailed way of selecting input files, please use a configFile. Example: src/**/*.js,!src/index.js,a.js,test/**/*.js.',
|
68 | list
|
69 | )
|
70 | .option(
|
71 | '--ignorePatterns <filesToIgnore>',
|
72 | 'A comma separated list of patterns used for specifying which files need to be ignored. This should only be used in cases where you experience a slow Stryker startup, because too many (or too large) files are copied to the sandbox that are not needed to run the tests. For example, image or movie directories. Note: This option will have no effect when using the --inPlace option. The directories `node_modules`, `.git` and some others are always ignored. Example: --ignorePatterns dist',
|
73 | list
|
74 | )
|
75 | .option('--ignoreStatic', 'Ignore static mutants. Static mutants are mutants which are only executed during the loading of a file.')
|
76 | .option(
|
77 | '--incremental',
|
78 | "Enable 'incremental mode'. Stryker will store results in a file and use that file to speed up the next --incremental run"
|
79 | )
|
80 | .option('--incrementalFile <file>', 'Specify the file to use for incremental mode.')
|
81 | .option(
|
82 | '--force',
|
83 | 'Run all mutants, even if --incremental is provided and an incremental file exists. Can be used to force a rebuild of the incremental file.'
|
84 | )
|
85 | .option(
|
86 | '-m, --mutate <filesToMutate>',
|
87 | 'A comma separated list of globbing expression used for selecting the files that should be mutated. Example: src/**/*.js,a.js. You can also specify specific lines and columns to mutate by adding :startLine[:startColumn]-endLine[:endColumn]. This will execute all mutants inside that range. It cannot be combined with glob patterns. Example: src/index.js:1:3-1:5',
|
88 | list
|
89 | )
|
90 | .option(
|
91 | '-b, --buildCommand <command>',
|
92 | 'Configure a build command to run after mutating the code, but before mutants are tested. This is generally used to transpile your code before testing.' +
|
93 | " Only configure this if your test runner doesn't take care of this already and you're not using just-in-time transpiler like `babel/register` or `ts-node`."
|
94 | )
|
95 | .option(
|
96 | '--dryRunOnly',
|
97 | 'Execute the initial test run only, without doing actual mutation testing. Doing a dry run only can be used to test that StrykerJS can run your test setup, for example, in CI pipelines.'
|
98 | )
|
99 | .option(
|
100 | '--checkers <listOfCheckersOrEmptyString>',
|
101 | 'A comma separated list of checkers to use, for example --checkers typescript',
|
102 | createSplitter(',')
|
103 | )
|
104 | .option('--checkerNodeArgs <listOfNodeArgs>', 'A list of node args to be passed to checker child processes.', createSplitter(' '))
|
105 | .option(
|
106 | '--coverageAnalysis <perTest|all|off>',
|
107 | `The coverage analysis strategy you want to use. Default value: "${defaultOptions.coverageAnalysis}"`
|
108 | )
|
109 | .option('--testRunner <name>', 'The name of the test runner you want to use')
|
110 | .option(
|
111 | '--testRunnerNodeArgs <listOfNodeArgs>',
|
112 | 'A comma separated list of node args to be passed to test runner child processes.',
|
113 | createSplitter(' ')
|
114 | )
|
115 | .option('--reporters <name>', 'A comma separated list of the names of the reporter(s) you want to use', list)
|
116 | .option('--plugins <listOfPlugins>', 'A list of plugins you want stryker to load (`require`).', list)
|
117 | .option(
|
118 | '--appendPlugins <listOfPlugins>',
|
119 | 'A list of additional plugins you want Stryker to load (`require`) without overwriting the (default) `plugins`.',
|
120 | list
|
121 | )
|
122 | .option('--timeoutMS <number>', 'Tweak the absolute timeout used to wait for a test runner to complete', parseInt)
|
123 | .option('--timeoutFactor <number>', 'Tweak the standard deviation relative to the normal test run of a mutated test', parseFloat)
|
124 | .option('--dryRunTimeoutMinutes <number>', 'Configure an absolute timeout for the initial test run. (It can take a while.)', parseFloat)
|
125 | .option('--maxConcurrentTestRunners <n>', 'Set the number of max concurrent test runner to spawn (default: cpuCount)', parseInt)
|
126 | .option(
|
127 | '-c, --concurrency <n>',
|
128 | 'Set the concurrency of workers. Stryker will always run checkers and test runners in parallel by creating worker processes (default: cpuCount - 1)',
|
129 | parseInt
|
130 | )
|
131 | .option('--disableBail', 'Force the test runner to keep running tests, even when a mutant is already killed.')
|
132 | .option(
|
133 | '--maxTestRunnerReuse <n>',
|
134 | 'Restart each test runner worker process after `n` runs. Not recommended unless you are experiencing memory leaks that you are unable to resolve. Configuring `0` here means infinite reuse.',
|
135 | parseInt
|
136 | )
|
137 | .option(
|
138 | '--logLevel <level>',
|
139 | `Set the log level for the console. Possible values: fatal, error, warn, info, debug, trace and off. Default is "${defaultOptions.logLevel}"`
|
140 | )
|
141 | .option(
|
142 | '--fileLogLevel <level>',
|
143 | `Set the log4js log level for the "stryker.log" file. Possible values: fatal, error, warn, info, debug, trace and off. Default is "${defaultOptions.fileLogLevel}"`
|
144 | )
|
145 | .option('--allowConsoleColors <true/false>', 'Indicates whether or not Stryker should use colors in console.', parseBoolean)
|
146 | .option(
|
147 | '--dashboard.project <name>',
|
148 | 'Indicates which project name to use if the "dashboard" reporter is enabled. Defaults to the git url configured in the environment of your CI server.',
|
149 | deepOption(dashboard, 'project')
|
150 | )
|
151 | .option(
|
152 | '--dashboard.version <version>',
|
153 | 'Indicates which version to use if the "dashboard" reporter is enabled. Defaults to the branch name or tag name configured in the environment of your CI server.',
|
154 | deepOption(dashboard, 'version')
|
155 | )
|
156 | .option(
|
157 | '--dashboard.module <name>',
|
158 | 'Indicates which module name to use if the "dashboard" reporter is enabled.',
|
159 | deepOption(dashboard, 'module')
|
160 | )
|
161 | .option(
|
162 | '--dashboard.baseUrl <url>',
|
163 | `Indicates which baseUrl to use when reporting to the stryker dashboard. Default: "${defaultOptions.dashboard.baseUrl}"`,
|
164 | deepOption(dashboard, 'baseUrl')
|
165 | )
|
166 | .option(
|
167 | `--dashboard.reportType <${ALL_REPORT_TYPES.join('|')}>`,
|
168 | `Send a full report (inc. source code and mutant results) or only the mutation score. Default: ${defaultOptions.dashboard.reportType}`,
|
169 | deepOption(dashboard, 'reportType')
|
170 | )
|
171 | .option(
|
172 | '--inPlace',
|
173 | 'Enable Stryker to mutate your files in place and put back the originals after its done. Note: mutating your files in place is generally not needed for mutation testing.'
|
174 | )
|
175 | .option(
|
176 | '--tempDirName <name>',
|
177 | 'Set the name of the directory that is used by Stryker as a working directory. This directory will be cleaned after a successful run'
|
178 | )
|
179 | .option(
|
180 | '--cleanTempDir <true/false>',
|
181 | `Choose whether or not to clean the temp dir (which is "${defaultOptions.tempDirName}" inside the current working directory by default) after a successful run. The temp dir will never be removed when the run failed for some reason (for debugging purposes).`,
|
182 | parseBoolean
|
183 | )
|
184 | .showSuggestionAfterError()
|
185 | .parse(this.argv);
|
186 |
|
187 |
|
188 | const options: PartialStrykerOptions = this.program.opts();
|
189 | LogConfigurator.configureMainProcess(options.logLevel);
|
190 |
|
191 |
|
192 | delete options.version;
|
193 | Object.keys(options)
|
194 | .filter((key) => key.startsWith('dashboard.'))
|
195 | .forEach((key) => delete options[key]);
|
196 |
|
197 | if (this.strykerConfig) {
|
198 | options.configFile = this.strykerConfig;
|
199 | }
|
200 | if (Object.keys(dashboard).length > 0) {
|
201 | options.dashboard = dashboard;
|
202 | }
|
203 |
|
204 | const commands = {
|
205 | init: () => initializerFactory().initialize(),
|
206 | run: () => this.runMutationTest(options),
|
207 | };
|
208 |
|
209 | if (Object.keys(commands).includes(this.command)) {
|
210 | const promise: Promise<MutantResult[] | void> = commands[this.command as keyof typeof commands]();
|
211 | promise.catch(() => {
|
212 | process.exitCode = 1;
|
213 | });
|
214 | } else {
|
215 | console.error('Unknown command: "%s", supported commands: [%s], or use `stryker --help`.', this.command, Object.keys(commands));
|
216 | }
|
217 | }
|
218 | }
|
219 |
|
220 | export function guardMinimalNodeVersion(processVersion = process.version): void {
|
221 |
|
222 | if (!semver.satisfies(processVersion, strykerEngines.node)) {
|
223 | throw new Error(
|
224 | `Node.js version ${processVersion} detected. StrykerJS requires version to match ${strykerEngines.node}. Please update your Node.js version or visit https://nodejs.org/ for additional instructions`
|
225 | );
|
226 | }
|
227 | }
|