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 `--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. Example: --ignorePatterns dist. Note that `node_modules`, `.git` and others are always ignored. Note: this cannot be combined with "files".',
|
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 | '-m, --mutate <filesToMutate>',
|
78 | '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',
|
79 | list
|
80 | )
|
81 | .option(
|
82 | '-b, --buildCommand <command>',
|
83 | '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.' +
|
84 | " 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`."
|
85 | )
|
86 | .option(
|
87 | '--checkers <listOfCheckersOrEmptyString>',
|
88 | 'A comma separated list of checkers to use, for example --checkers typescript',
|
89 | createSplitter(',')
|
90 | )
|
91 | .option('--checkerNodeArgs <listOfNodeArgs>', 'A list of node args to be passed to checker child processes.', createSplitter(' '))
|
92 | .option(
|
93 | `--coverageAnalysis <perTest|all|off>', 'The coverage analysis strategy you want to use. Default value: "${defaultOptions.coverageAnalysis}"`
|
94 | )
|
95 | .option('--testRunner <name>', 'The name of the test runner you want to use')
|
96 | .option(
|
97 | '--testRunnerNodeArgs <listOfNodeArgs>',
|
98 | 'A comma separated list of node args to be passed to test runner child processes.',
|
99 | createSplitter(' ')
|
100 | )
|
101 | .option('--reporters <name>', 'A comma separated list of the names of the reporter(s) you want to use', list)
|
102 | .option('--plugins <listOfPlugins>', 'A list of plugins you want stryker to load (`require`).', list)
|
103 | .option(
|
104 | '--appendPlugins <listOfPlugions>',
|
105 | 'A list of additional plugins you want Stryker to load (`require`) without overwriting the (default) `plugins`.',
|
106 | list
|
107 | )
|
108 | .option('--timeoutMS <number>', 'Tweak the absolute timeout used to wait for a test runner to complete', parseInt)
|
109 | .option('--timeoutFactor <number>', 'Tweak the standard deviation relative to the normal test run of a mutated test', parseFloat)
|
110 | .option('--dryRunTimeoutMinutes <number>', 'Configure an absolute timeout for the initial test run. (It can take a while.)', parseFloat)
|
111 | .option('--maxConcurrentTestRunners <n>', 'Set the number of max concurrent test runner to spawn (default: cpuCount)', parseInt)
|
112 | .option(
|
113 | '-c, --concurrency <n>',
|
114 | 'Set the concurrency of workers. Stryker will always run checkers and test runners in parallel by creating worker processes (default: cpuCount - 1)',
|
115 | parseInt
|
116 | )
|
117 | .option('--disableBail', 'Force the test runner to keep running tests, even when a mutant is already killed.')
|
118 | .option(
|
119 | '--maxTestRunnerReuse <n>',
|
120 | '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.',
|
121 | parseInt
|
122 | )
|
123 | .option(
|
124 | '--logLevel <level>',
|
125 | `Set the log level for the console. Possible values: fatal, error, warn, info, debug, trace and off. Default is "${defaultOptions.logLevel}"`
|
126 | )
|
127 | .option(
|
128 | '--fileLogLevel <level>',
|
129 | `Set the log4js log level for the "stryker.log" file. Possible values: fatal, error, warn, info, debug, trace and off. Default is "${defaultOptions.fileLogLevel}"`
|
130 | )
|
131 | .option('--allowConsoleColors <true/false>', 'Indicates whether or not Stryker should use colors in console.', parseBoolean)
|
132 | .option(
|
133 | '--dashboard.project <name>',
|
134 | '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.',
|
135 | deepOption(dashboard, 'project')
|
136 | )
|
137 | .option(
|
138 | '--dashboard.version <version>',
|
139 | '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.',
|
140 | deepOption(dashboard, 'version')
|
141 | )
|
142 | .option(
|
143 | '--dashboard.module <name>',
|
144 | 'Indicates which module name to use if the "dashboard" reporter is enabled.',
|
145 | deepOption(dashboard, 'module')
|
146 | )
|
147 | .option(
|
148 | '--dashboard.baseUrl <url>',
|
149 | `Indicates which baseUrl to use when reporting to the stryker dashboard. Default: "${defaultOptions.dashboard.baseUrl}"`,
|
150 | deepOption(dashboard, 'baseUrl')
|
151 | )
|
152 | .option(
|
153 | `--dashboard.reportType <${ALL_REPORT_TYPES.join('|')}>`,
|
154 | `Send a full report (inc. source code and mutant results) or only the mutation score. Default: ${defaultOptions.dashboard.reportType}`,
|
155 | deepOption(dashboard, 'reportType')
|
156 | )
|
157 | .option(
|
158 | '--inPlace',
|
159 | '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.'
|
160 | )
|
161 | .option(
|
162 | '--tempDirName <name>',
|
163 | 'Set the name of the directory that is used by Stryker as a working directory. This directory will be cleaned after a successful run'
|
164 | )
|
165 | .option(
|
166 | '--cleanTempDir <true/false>',
|
167 | `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).`,
|
168 | parseBoolean
|
169 | )
|
170 | .showSuggestionAfterError()
|
171 | .parse(this.argv);
|
172 |
|
173 |
|
174 | const options: PartialStrykerOptions = this.program.opts();
|
175 | LogConfigurator.configureMainProcess(options.logLevel);
|
176 |
|
177 |
|
178 | delete options.version;
|
179 | Object.keys(options)
|
180 | .filter((key) => key.startsWith('dashboard.'))
|
181 | .forEach((key) => delete options[key]);
|
182 |
|
183 | if (this.strykerConfig) {
|
184 | options.configFile = this.strykerConfig;
|
185 | }
|
186 | if (Object.keys(dashboard).length > 0) {
|
187 | options.dashboard = dashboard;
|
188 | }
|
189 |
|
190 | const commands = {
|
191 | init: () => initializerFactory().initialize(),
|
192 | run: () => this.runMutationTest(options),
|
193 | };
|
194 |
|
195 | if (Object.keys(commands).includes(this.command)) {
|
196 | const promise: Promise<MutantResult[] | void> = commands[this.command as keyof typeof commands]();
|
197 | promise.catch(() => {
|
198 | process.exitCode = 1;
|
199 | });
|
200 | } else {
|
201 | console.error('Unknown command: "%s", supported commands: [%s], or use `stryker --help`.', this.command, Object.keys(commands));
|
202 | }
|
203 | }
|
204 | }
|
205 |
|
206 | export function guardMinimalNodeVersion(processVersion = process.version): void {
|
207 |
|
208 | if (!semver.satisfies(processVersion, strykerEngines.node)) {
|
209 | throw new Error(
|
210 | `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`
|
211 | );
|
212 | }
|
213 | }
|