@mutineerjs/mutineer 0.8.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -1
- package/dist/bin/__tests__/mutineer.spec.js +8 -0
- package/dist/bin/mutineer.d.ts +1 -1
- package/dist/bin/mutineer.js +2 -1
- package/dist/runner/__tests__/args.spec.js +8 -0
- package/dist/runner/__tests__/coverage-resolver.spec.js +1 -0
- package/dist/runner/args.d.ts +1 -0
- package/dist/runner/args.js +2 -0
- package/dist/runner/jest/__tests__/adapter.spec.js +49 -0
- package/dist/runner/jest/adapter.js +30 -2
- package/dist/runner/orchestrator.js +18 -5
- package/dist/runner/vitest/__tests__/adapter.spec.js +70 -0
- package/dist/runner/vitest/adapter.js +12 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -100,6 +100,7 @@ npm run mutineer
|
|
|
100
100
|
| `--concurrency <n>` | Parallel workers (min 1) | CPUs - 1 |
|
|
101
101
|
| `--changed` | Only mutate files changed vs base branch | -- |
|
|
102
102
|
| `--changed-with-deps` | Include dependents of changed files | -- |
|
|
103
|
+
| `--full` | Mutate full codebase, skipping confirmation prompt | -- |
|
|
103
104
|
| `--only-covered-lines` | Skip mutations on uncovered lines | -- |
|
|
104
105
|
| `--per-test-coverage` | Run only tests that cover the mutated line | -- |
|
|
105
106
|
| `--coverage-file <path>` | Path to Istanbul coverage JSON | auto-detected |
|
|
@@ -170,7 +171,7 @@ export default defineMutineerConfig({
|
|
|
170
171
|
|
|
171
172
|
Large repos can generate thousands of mutations. These strategies keep runs fast and incremental.
|
|
172
173
|
|
|
173
|
-
When you run `mutineer run` without `--changed
|
|
174
|
+
When you run `mutineer run` without `--changed`, `--changed-with-deps`, or `--full` on an interactive terminal, mutineer warns you and lets you narrow scope before starting:
|
|
174
175
|
|
|
175
176
|
```
|
|
176
177
|
Warning: Running on the full codebase may take a while.
|
|
@@ -11,6 +11,7 @@ describe('HELP_TEXT', () => {
|
|
|
11
11
|
'--progress',
|
|
12
12
|
'--changed',
|
|
13
13
|
'--changed-with-deps',
|
|
14
|
+
'--full',
|
|
14
15
|
'--only-covered-lines',
|
|
15
16
|
'--per-test-coverage',
|
|
16
17
|
'--coverage-file',
|
|
@@ -61,6 +62,13 @@ describe('confirmFullRun()', () => {
|
|
|
61
62
|
const args = ['--changed-with-deps'];
|
|
62
63
|
expect(await confirmFullRun(args)).toBe(args);
|
|
63
64
|
});
|
|
65
|
+
it('returns args unchanged when --full is present, skipping prompt', async () => {
|
|
66
|
+
mockTTY(true);
|
|
67
|
+
const createSpy = vi.spyOn(readline, 'createInterface');
|
|
68
|
+
const args = ['--full'];
|
|
69
|
+
expect(await confirmFullRun(args)).toBe(args);
|
|
70
|
+
expect(createSpy).not.toHaveBeenCalled();
|
|
71
|
+
});
|
|
64
72
|
it('skips prompt and returns args unchanged when stdin is not a TTY', async () => {
|
|
65
73
|
mockTTY(false);
|
|
66
74
|
const createSpy = vi.spyOn(readline, 'createInterface');
|
package/dist/bin/mutineer.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
export declare const HELP_TEXT = "Usage: mutineer <command> [options]\n\nCommands:\n init Create a mutineer.config.ts template\n run Run mutation testing\n clean Remove __mutineer__ temp directories\n\nOptions (run):\n --config, -c <path> Config file path\n --concurrency <n> Worker count (default: CPU count - 1)\n --runner <vitest|jest> Test runner (default: vitest)\n --progress <bar|list|quiet> Progress display (default: bar)\n --changed Mutate only git-changed files\n --changed-with-deps Mutate changed files + their local dependencies\n --only-covered-lines Mutate only lines covered by tests\n --per-test-coverage Collect per-test coverage data\n --coverage-file <path> Path to coverage JSON\n --min-kill-percent <n> Minimum kill % threshold (0\u2013100)\n --timeout <ms> Per-mutant test timeout in ms (default: 30000)\n --report <text|json> Output format: text (default) or json (writes mutineer-report.json)\n --shard <n>/<total> Run a shard of mutants (e.g. --shard 1/4)\n --skip-baseline Skip the baseline test run\n\n --help, -h Show this help\n --version, -V Show version\n";
|
|
2
|
+
export declare const HELP_TEXT = "Usage: mutineer <command> [options]\n\nCommands:\n init Create a mutineer.config.ts template\n run Run mutation testing\n clean Remove __mutineer__ temp directories\n\nOptions (run):\n --config, -c <path> Config file path\n --concurrency <n> Worker count (default: CPU count - 1)\n --runner <vitest|jest> Test runner (default: vitest)\n --progress <bar|list|quiet> Progress display (default: bar)\n --changed Mutate only git-changed files\n --changed-with-deps Mutate changed files + their local dependencies\n --full Mutate full codebase, skipping confirmation prompt\n --only-covered-lines Mutate only lines covered by tests\n --per-test-coverage Collect per-test coverage data\n --coverage-file <path> Path to coverage JSON\n --min-kill-percent <n> Minimum kill % threshold (0\u2013100)\n --timeout <ms> Per-mutant test timeout in ms (default: 30000)\n --report <text|json> Output format: text (default) or json (writes mutineer-report.json)\n --shard <n>/<total> Run a shard of mutants (e.g. --shard 1/4)\n --skip-baseline Skip the baseline test run\n\n --help, -h Show this help\n --version, -V Show version\n";
|
|
3
3
|
export declare function getVersion(): string;
|
|
4
4
|
/**
|
|
5
5
|
* When running in full-codebase mode on an interactive TTY, warn the user and
|
package/dist/bin/mutineer.js
CHANGED
|
@@ -24,6 +24,7 @@ Options (run):
|
|
|
24
24
|
--progress <bar|list|quiet> Progress display (default: bar)
|
|
25
25
|
--changed Mutate only git-changed files
|
|
26
26
|
--changed-with-deps Mutate changed files + their local dependencies
|
|
27
|
+
--full Mutate full codebase, skipping confirmation prompt
|
|
27
28
|
--only-covered-lines Mutate only lines covered by tests
|
|
28
29
|
--per-test-coverage Collect per-test coverage data
|
|
29
30
|
--coverage-file <path> Path to coverage JSON
|
|
@@ -62,7 +63,7 @@ Warning: Running on the full codebase may take a while.
|
|
|
62
63
|
*/
|
|
63
64
|
export async function confirmFullRun(args) {
|
|
64
65
|
const isFullRun = !args.includes('--changed') && !args.includes('--changed-with-deps');
|
|
65
|
-
if (!isFullRun || !process.stdin.isTTY)
|
|
66
|
+
if (!isFullRun || !process.stdin.isTTY || args.includes('--full'))
|
|
66
67
|
return args;
|
|
67
68
|
process.stdout.write(FULL_RUN_WARNING);
|
|
68
69
|
const rl = readline.createInterface({
|
|
@@ -131,6 +131,14 @@ describe('parseCliOptions', () => {
|
|
|
131
131
|
const opts = parseCliOptions(['--changed-with-deps'], emptyCfg);
|
|
132
132
|
expect(opts.wantsChangedWithDeps).toBe(true);
|
|
133
133
|
});
|
|
134
|
+
it('parses --full flag', () => {
|
|
135
|
+
const opts = parseCliOptions(['--full'], emptyCfg);
|
|
136
|
+
expect(opts.wantsFull).toBe(true);
|
|
137
|
+
});
|
|
138
|
+
it('wantsFull defaults to false', () => {
|
|
139
|
+
const opts = parseCliOptions([], emptyCfg);
|
|
140
|
+
expect(opts.wantsFull).toBe(false);
|
|
141
|
+
});
|
|
134
142
|
it('parses --only-covered-lines flag', () => {
|
|
135
143
|
const opts = parseCliOptions(['--only-covered-lines'], emptyCfg);
|
|
136
144
|
expect(opts.wantsOnlyCoveredLines).toBe(true);
|
package/dist/runner/args.d.ts
CHANGED
|
@@ -12,6 +12,7 @@ export interface ParsedCliOptions {
|
|
|
12
12
|
readonly configPath: string | undefined;
|
|
13
13
|
readonly wantsChanged: boolean;
|
|
14
14
|
readonly wantsChangedWithDeps: boolean;
|
|
15
|
+
readonly wantsFull: boolean;
|
|
15
16
|
readonly wantsOnlyCoveredLines: boolean;
|
|
16
17
|
readonly wantsPerTestCoverage: boolean;
|
|
17
18
|
readonly coverageFilePath: string | undefined;
|
package/dist/runner/args.js
CHANGED
|
@@ -134,6 +134,7 @@ export function parseCliOptions(args, cfg) {
|
|
|
134
134
|
const configPath = readStringFlag(args, '--config', '-c');
|
|
135
135
|
const wantsChanged = args.includes('--changed');
|
|
136
136
|
const wantsChangedWithDeps = args.includes('--changed-with-deps');
|
|
137
|
+
const wantsFull = args.includes('--full');
|
|
137
138
|
const wantsOnlyCoveredLines = args.includes('--only-covered-lines') || cfg.onlyCoveredLines === true;
|
|
138
139
|
const wantsPerTestCoverage = args.includes('--per-test-coverage') || cfg.perTestCoverage === true;
|
|
139
140
|
const coverageFilePath = readStringFlag(args, '--coverage-file') ?? cfg.coverageFile;
|
|
@@ -163,6 +164,7 @@ export function parseCliOptions(args, cfg) {
|
|
|
163
164
|
configPath,
|
|
164
165
|
wantsChanged,
|
|
165
166
|
wantsChangedWithDeps,
|
|
167
|
+
wantsFull,
|
|
166
168
|
wantsOnlyCoveredLines,
|
|
167
169
|
wantsPerTestCoverage,
|
|
168
170
|
coverageFilePath,
|
|
@@ -61,6 +61,55 @@ describe('Jest adapter', () => {
|
|
|
61
61
|
expect(args.coverageProvider).toBe('v8');
|
|
62
62
|
expect(args.config).toBe('jest.config.ts');
|
|
63
63
|
});
|
|
64
|
+
it('does not write captured output to stdout/stderr on success', async () => {
|
|
65
|
+
const adapter = makeAdapter();
|
|
66
|
+
const stdoutWrite = vi.spyOn(process.stdout, 'write').mockReturnValue(true);
|
|
67
|
+
const stderrWrite = vi.spyOn(process.stderr, 'write').mockReturnValue(true);
|
|
68
|
+
runCLIMock.mockResolvedValueOnce({ results: { success: true } });
|
|
69
|
+
const ok = await adapter.runBaseline(['test-a'], {
|
|
70
|
+
collectCoverage: false,
|
|
71
|
+
perTestCoverage: false,
|
|
72
|
+
});
|
|
73
|
+
expect(ok).toBe(true);
|
|
74
|
+
// No output should be replayed on success
|
|
75
|
+
expect(stdoutWrite).not.toHaveBeenCalled();
|
|
76
|
+
expect(stderrWrite).not.toHaveBeenCalled();
|
|
77
|
+
stdoutWrite.mockRestore();
|
|
78
|
+
stderrWrite.mockRestore();
|
|
79
|
+
});
|
|
80
|
+
it('replays captured output to stdout/stderr on failure', async () => {
|
|
81
|
+
const adapter = makeAdapter();
|
|
82
|
+
// Spy before runBaseline so adapter's origStdoutWrite/origStderrWrite bind to the spy.
|
|
83
|
+
// runCLI writes during capture are suppressed; the spy sees only the replay after restore.
|
|
84
|
+
const writtenStdout = [];
|
|
85
|
+
const writtenStderr = [];
|
|
86
|
+
const stdoutSpy = vi
|
|
87
|
+
.spyOn(process.stdout, 'write')
|
|
88
|
+
.mockImplementation((chunk) => {
|
|
89
|
+
writtenStdout.push(Buffer.isBuffer(chunk) ? chunk.toString() : String(chunk));
|
|
90
|
+
return true;
|
|
91
|
+
});
|
|
92
|
+
const stderrSpy = vi
|
|
93
|
+
.spyOn(process.stderr, 'write')
|
|
94
|
+
.mockImplementation((chunk) => {
|
|
95
|
+
writtenStderr.push(Buffer.isBuffer(chunk) ? chunk.toString() : String(chunk));
|
|
96
|
+
return true;
|
|
97
|
+
});
|
|
98
|
+
runCLIMock.mockImplementationOnce(async () => {
|
|
99
|
+
process.stdout.write('jest stdout output');
|
|
100
|
+
process.stderr.write('jest stderr output');
|
|
101
|
+
return { results: { success: false } };
|
|
102
|
+
});
|
|
103
|
+
const ok = await adapter.runBaseline(['test-a'], {
|
|
104
|
+
collectCoverage: false,
|
|
105
|
+
perTestCoverage: false,
|
|
106
|
+
});
|
|
107
|
+
expect(ok).toBe(false);
|
|
108
|
+
expect(writtenStdout.join('')).toContain('jest stdout output');
|
|
109
|
+
expect(writtenStderr.join('')).toContain('jest stderr output');
|
|
110
|
+
stdoutSpy.mockRestore();
|
|
111
|
+
stderrSpy.mockRestore();
|
|
112
|
+
});
|
|
64
113
|
it('maps pool result to mutant status', async () => {
|
|
65
114
|
const adapter = makeAdapter();
|
|
66
115
|
await adapter.init();
|
|
@@ -33,6 +33,7 @@ function stripMutineerArgs(args) {
|
|
|
33
33
|
'--mutate',
|
|
34
34
|
'--changed',
|
|
35
35
|
'--changed-with-deps',
|
|
36
|
+
'--full',
|
|
36
37
|
'--only-covered-lines',
|
|
37
38
|
'--per-test-coverage',
|
|
38
39
|
'--perTestCoverage',
|
|
@@ -108,16 +109,43 @@ export class JestAdapter {
|
|
|
108
109
|
? 'baseline-with-coverage'
|
|
109
110
|
: 'baseline';
|
|
110
111
|
const cliOptions = buildJestCliOptions(tests, mode, this.jestConfigPath);
|
|
112
|
+
const stdoutChunks = [];
|
|
113
|
+
const stderrChunks = [];
|
|
114
|
+
const origStdoutWrite = process.stdout.write.bind(process.stdout);
|
|
115
|
+
const origStderrWrite = process.stderr.write.bind(process.stderr);
|
|
116
|
+
// Temporarily capture stdout/stderr so Jest output is suppressed during baseline;
|
|
117
|
+
// captured output is replayed below on failure.
|
|
118
|
+
const makeCapture = (chunks) => function (chunk, encodingOrCb, cb) {
|
|
119
|
+
chunks.push(Buffer.isBuffer(chunk)
|
|
120
|
+
? Buffer.from(chunk)
|
|
121
|
+
: Buffer.from(chunk));
|
|
122
|
+
const callback = (typeof encodingOrCb === 'function' ? encodingOrCb : cb);
|
|
123
|
+
callback?.();
|
|
124
|
+
return true;
|
|
125
|
+
};
|
|
126
|
+
process.stdout.write = makeCapture(stdoutChunks);
|
|
127
|
+
process.stderr.write = makeCapture(stderrChunks);
|
|
128
|
+
let success = false;
|
|
111
129
|
try {
|
|
112
130
|
const { runCLI } = await loadRunCLI(this.requireFromCwd);
|
|
113
131
|
const { results } = await runCLI(cliOptions, [this.options.cwd]);
|
|
114
|
-
|
|
132
|
+
success = results.success;
|
|
115
133
|
}
|
|
116
134
|
catch (err) {
|
|
117
135
|
log.debug('Failed to run Jest baseline: ' +
|
|
118
136
|
(err instanceof Error ? err.message : String(err)));
|
|
119
|
-
return false;
|
|
120
137
|
}
|
|
138
|
+
finally {
|
|
139
|
+
process.stdout.write = origStdoutWrite;
|
|
140
|
+
process.stderr.write = origStderrWrite;
|
|
141
|
+
}
|
|
142
|
+
if (!success) {
|
|
143
|
+
if (stdoutChunks.length)
|
|
144
|
+
process.stdout.write(Buffer.concat(stdoutChunks));
|
|
145
|
+
if (stderrChunks.length)
|
|
146
|
+
process.stderr.write(Buffer.concat(stderrChunks));
|
|
147
|
+
}
|
|
148
|
+
return success;
|
|
121
149
|
}
|
|
122
150
|
async runMutant(mutant, tests) {
|
|
123
151
|
if (!this.pool) {
|
|
@@ -124,11 +124,24 @@ export async function runOrchestrator(cliArgs, cwd) {
|
|
|
124
124
|
log.info('Skipping baseline tests (--skip-baseline)');
|
|
125
125
|
}
|
|
126
126
|
else {
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
}
|
|
127
|
+
const baselineMsg = `Running ${baselineTests.length} baseline tests${coverage.enableCoverageForBaseline ? ' (collecting coverage)' : ''}\u2026`;
|
|
128
|
+
let baselineSpinner = null;
|
|
129
|
+
if (process.stderr.isTTY) {
|
|
130
|
+
baselineSpinner = render(createElement(PoolSpinner, { message: baselineMsg }), { stdout: process.stderr, stderr: process.stderr });
|
|
131
|
+
}
|
|
132
|
+
else {
|
|
133
|
+
log.info(baselineMsg);
|
|
134
|
+
}
|
|
135
|
+
let baselineOk;
|
|
136
|
+
try {
|
|
137
|
+
baselineOk = await adapter.runBaseline(baselineTests, {
|
|
138
|
+
collectCoverage: coverage.enableCoverageForBaseline,
|
|
139
|
+
perTestCoverage: coverage.wantsPerTestCoverage,
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
finally {
|
|
143
|
+
baselineSpinner?.unmount();
|
|
144
|
+
}
|
|
132
145
|
if (!baselineOk) {
|
|
133
146
|
process.exitCode = 1;
|
|
134
147
|
return;
|
|
@@ -137,6 +137,76 @@ describe('Vitest adapter', () => {
|
|
|
137
137
|
const args = spawnMock.mock.calls[0][1];
|
|
138
138
|
expect(args.join(' ')).not.toContain('--shard');
|
|
139
139
|
});
|
|
140
|
+
it('does not write captured output to stdout/stderr on success', async () => {
|
|
141
|
+
const adapter = makeAdapter({ cliArgs: [] });
|
|
142
|
+
const stdoutWrite = vi.spyOn(process.stdout, 'write').mockReturnValue(true);
|
|
143
|
+
const stderrWrite = vi.spyOn(process.stderr, 'write').mockReturnValue(true);
|
|
144
|
+
const listeners = {};
|
|
145
|
+
spawnMock.mockImplementationOnce(() => ({
|
|
146
|
+
stdout: {
|
|
147
|
+
on: (evt, cb) => {
|
|
148
|
+
;
|
|
149
|
+
(listeners[`stdout:${evt}`] ??= []).push(cb);
|
|
150
|
+
},
|
|
151
|
+
},
|
|
152
|
+
stderr: {
|
|
153
|
+
on: (evt, cb) => {
|
|
154
|
+
;
|
|
155
|
+
(listeners[`stderr:${evt}`] ??= []).push(cb);
|
|
156
|
+
},
|
|
157
|
+
},
|
|
158
|
+
on: (evt, cb) => {
|
|
159
|
+
if (evt === 'exit')
|
|
160
|
+
cb(0);
|
|
161
|
+
},
|
|
162
|
+
}));
|
|
163
|
+
const ok = await adapter.runBaseline(['test-a'], {
|
|
164
|
+
collectCoverage: false,
|
|
165
|
+
perTestCoverage: false,
|
|
166
|
+
});
|
|
167
|
+
expect(ok).toBe(true);
|
|
168
|
+
expect(stdoutWrite).not.toHaveBeenCalled();
|
|
169
|
+
expect(stderrWrite).not.toHaveBeenCalled();
|
|
170
|
+
stdoutWrite.mockRestore();
|
|
171
|
+
stderrWrite.mockRestore();
|
|
172
|
+
});
|
|
173
|
+
it('replays captured output to stdout/stderr on failure', async () => {
|
|
174
|
+
const adapter = makeAdapter({ cliArgs: [] });
|
|
175
|
+
const stdoutWrite = vi.spyOn(process.stdout, 'write').mockReturnValue(true);
|
|
176
|
+
const stderrWrite = vi.spyOn(process.stderr, 'write').mockReturnValue(true);
|
|
177
|
+
const stdoutListeners = [];
|
|
178
|
+
const stderrListeners = [];
|
|
179
|
+
spawnMock.mockImplementationOnce(() => ({
|
|
180
|
+
stdout: {
|
|
181
|
+
on: (evt, cb) => {
|
|
182
|
+
if (evt === 'data')
|
|
183
|
+
stdoutListeners.push(cb);
|
|
184
|
+
},
|
|
185
|
+
},
|
|
186
|
+
stderr: {
|
|
187
|
+
on: (evt, cb) => {
|
|
188
|
+
if (evt === 'data')
|
|
189
|
+
stderrListeners.push(cb);
|
|
190
|
+
},
|
|
191
|
+
},
|
|
192
|
+
on: (evt, cb) => {
|
|
193
|
+
if (evt === 'exit') {
|
|
194
|
+
stdoutListeners.forEach((l) => l(Buffer.from('stdout output')));
|
|
195
|
+
stderrListeners.forEach((l) => l(Buffer.from('stderr output')));
|
|
196
|
+
cb(1);
|
|
197
|
+
}
|
|
198
|
+
},
|
|
199
|
+
}));
|
|
200
|
+
const ok = await adapter.runBaseline(['test-a'], {
|
|
201
|
+
collectCoverage: false,
|
|
202
|
+
perTestCoverage: false,
|
|
203
|
+
});
|
|
204
|
+
expect(ok).toBe(false);
|
|
205
|
+
expect(stdoutWrite).toHaveBeenCalledWith(Buffer.from('stdout output'));
|
|
206
|
+
expect(stderrWrite).toHaveBeenCalledWith(Buffer.from('stderr output'));
|
|
207
|
+
stdoutWrite.mockRestore();
|
|
208
|
+
stderrWrite.mockRestore();
|
|
209
|
+
});
|
|
140
210
|
it('detects coverage config from vitest config file', async () => {
|
|
141
211
|
const tmp = await fs.mkdtemp(path.join(os.tmpdir(), 'mutineer-vitest-'));
|
|
142
212
|
const cfgPath = path.join(tmp, 'vitest.config.ts');
|
|
@@ -44,6 +44,7 @@ function stripMutineerArgs(args) {
|
|
|
44
44
|
'--mutate',
|
|
45
45
|
'--changed',
|
|
46
46
|
'--changed-with-deps',
|
|
47
|
+
'--full',
|
|
47
48
|
'--only-covered-lines',
|
|
48
49
|
'--per-test-coverage',
|
|
49
50
|
'--perTestCoverage',
|
|
@@ -147,14 +148,24 @@ export class VitestAdapter {
|
|
|
147
148
|
env.CI = '1';
|
|
148
149
|
const child = spawn(process.execPath, [this.vitestPath, ...args, ...tests], {
|
|
149
150
|
cwd: this.options.cwd,
|
|
150
|
-
stdio: ['ignore', '
|
|
151
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
151
152
|
env,
|
|
152
153
|
});
|
|
154
|
+
const stdoutChunks = [];
|
|
155
|
+
const stderrChunks = [];
|
|
156
|
+
child.stdout?.on('data', (chunk) => stdoutChunks.push(chunk));
|
|
157
|
+
child.stderr?.on('data', (chunk) => stderrChunks.push(chunk));
|
|
153
158
|
child.on('error', (err) => {
|
|
154
159
|
log.debug('Failed to spawn vitest process: ' + err.message);
|
|
155
160
|
resolve(false);
|
|
156
161
|
});
|
|
157
162
|
child.on('exit', (code) => {
|
|
163
|
+
if (code !== 0) {
|
|
164
|
+
if (stdoutChunks.length)
|
|
165
|
+
process.stdout.write(Buffer.concat(stdoutChunks));
|
|
166
|
+
if (stderrChunks.length)
|
|
167
|
+
process.stderr.write(Buffer.concat(stderrChunks));
|
|
168
|
+
}
|
|
158
169
|
resolve(code === 0);
|
|
159
170
|
});
|
|
160
171
|
});
|