UNPKG

4.64 kBPlain TextView Raw
1import { exec } from 'child_process';
2import os from 'os';
3
4import { StrykerOptions, CommandRunnerOptions, INSTRUMENTER_CONSTANTS } from '@stryker-mutator/api/core';
5import {
6 TestRunner,
7 TestStatus,
8 MutantRunOptions,
9 DryRunResult,
10 MutantRunResult,
11 DryRunStatus,
12 ErrorDryRunResult,
13 CompleteDryRunResult,
14 toMutantRunResult,
15 TestRunnerCapabilities,
16} from '@stryker-mutator/api/test-runner';
17import { errorToString } from '@stryker-mutator/util';
18
19import { objectUtils } from '../utils/object-utils.js';
20import { Timer } from '../utils/timer.js';
21
22/**
23 * A test runner that uses a (bash or cmd) command to execute the tests.
24 * Does not know hom many tests are executed or any code coverage results,
25 * instead, it mimics a simple test result based on the exit code.
26 * The command can be configured, but defaults to `npm test`.
27 */
28export class CommandTestRunner implements TestRunner {
29 /**
30 * "command"
31 */
32 public static readonly runnerName = CommandTestRunner.name.replace('TestRunner', '').toLowerCase();
33
34 /**
35 * Determines whether a given name is "command" (ignore case)
36 * @param name Maybe "command", maybe not
37 */
38 public static is(name: string): name is 'command' {
39 return this.runnerName === name.toLowerCase();
40 }
41
42 private readonly settings: CommandRunnerOptions;
43
44 private timeoutHandler: (() => Promise<void>) | undefined;
45
46 constructor(private readonly workingDir: string, options: StrykerOptions) {
47 this.settings = options.commandRunner;
48 }
49
50 public capabilities(): TestRunnerCapabilities {
51 // Can reload, because each call is a new process.
52 return { reloadEnvironment: true };
53 }
54
55 public async dryRun(): Promise<DryRunResult> {
56 return this.run({});
57 }
58
59 public async mutantRun({ activeMutant }: Pick<MutantRunOptions, 'activeMutant'>): Promise<MutantRunResult> {
60 const result = await this.run({ activeMutantId: activeMutant.id });
61 return toMutantRunResult(result);
62 }
63
64 private run({ activeMutantId }: { activeMutantId?: string }): Promise<DryRunResult> {
65 const timerInstance = new Timer();
66 return new Promise((res, rej) => {
67 const output: Array<Buffer | string> = [];
68 const env =
69 activeMutantId === undefined ? process.env : { ...process.env, [INSTRUMENTER_CONSTANTS.ACTIVE_MUTANT_ENV_VARIABLE]: activeMutantId };
70 const childProcess = exec(this.settings.command, { cwd: this.workingDir, env });
71 childProcess.on('error', (error) => {
72 objectUtils
73 .kill(childProcess.pid)
74 .then(() => handleResolve(errorResult(error)))
75 .catch(rej);
76 });
77 childProcess.on('exit', (code) => {
78 const result = completeResult(code, timerInstance);
79 handleResolve(result);
80 });
81 childProcess.stdout!.on('data', (chunk) => {
82 output.push(chunk as Buffer);
83 });
84 childProcess.stderr!.on('data', (chunk) => {
85 output.push(chunk as Buffer);
86 });
87
88 this.timeoutHandler = async () => {
89 handleResolve({ status: DryRunStatus.Timeout });
90 await objectUtils.kill(childProcess.pid);
91 };
92
93 const handleResolve = (runResult: DryRunResult) => {
94 removeAllListeners();
95 this.timeoutHandler = undefined;
96 res(runResult);
97 };
98
99 function removeAllListeners() {
100 childProcess.stderr!.removeAllListeners();
101 childProcess.stdout!.removeAllListeners();
102 childProcess.removeAllListeners();
103 }
104
105 function errorResult(error: Error): ErrorDryRunResult {
106 return {
107 errorMessage: errorToString(error),
108 status: DryRunStatus.Error,
109 };
110 }
111
112 function completeResult(exitCode: number | null, timer: Timer): CompleteDryRunResult {
113 const duration = timer.elapsedMs();
114 if (exitCode === 0) {
115 return {
116 status: DryRunStatus.Complete,
117 tests: [
118 {
119 id: 'all',
120 name: 'All tests',
121 status: TestStatus.Success,
122 timeSpentMs: duration,
123 },
124 ],
125 };
126 } else {
127 return {
128 status: DryRunStatus.Complete,
129 tests: [
130 {
131 id: 'all',
132 failureMessage: output.map((buf) => buf.toString()).join(os.EOL),
133 name: 'All tests',
134 status: TestStatus.Failed,
135 timeSpentMs: duration,
136 },
137 ],
138 };
139 }
140 }
141 });
142 }
143 public async dispose(): Promise<void> {
144 if (this.timeoutHandler) {
145 await this.timeoutHandler();
146 }
147 }
148}
149
\No newline at end of file