Skip to content

Commit 6c68914

Browse files
feat: add use exec option
BREAKING CHANGE: default behavior is to use spawn
1 parent 1a03a08 commit 6c68914

File tree

3 files changed

+296
-7
lines changed

3 files changed

+296
-7
lines changed

__tests__/command1.test.ts

Lines changed: 236 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,236 @@
1+
/* eslint-disable no-magic-numbers */
2+
import {
3+
testChildProcess,
4+
setChildProcessParams,
5+
spyOnExec,
6+
execCalledWith,
7+
spyOnStdout,
8+
stdoutCalledWith,
9+
} from '@technote-space/github-action-test-helper';
10+
import { Logger, Command } from '../src';
11+
12+
describe('execAsync', () => {
13+
testChildProcess();
14+
beforeEach(() => {
15+
Logger.resetForTesting();
16+
});
17+
18+
const command = new Command(new Logger(), true);
19+
20+
it('should run command', async() => {
21+
const mockExec = spyOnExec();
22+
const mockStdout = spyOnStdout();
23+
24+
expect(await command.execAsync({command: 'test'})).toEqual({stdout: 'stdout', stderr: '', command: 'test'});
25+
26+
execCalledWith(mockExec, [
27+
'test',
28+
]);
29+
stdoutCalledWith(mockStdout, [
30+
'[command]test',
31+
' >> stdout',
32+
]);
33+
});
34+
35+
it('should run command with cwd, altCommand', async() => {
36+
setChildProcessParams({stderr: 'stderr'});
37+
const mockExec = spyOnExec();
38+
const mockStdout = spyOnStdout();
39+
40+
expect(await command.execAsync({command: 'test', cwd: 'dir', altCommand: 'alt'})).toEqual({stdout: 'stdout', stderr: 'stderr', command: 'alt'});
41+
42+
execCalledWith(mockExec, [
43+
['test', {'cwd': 'dir'}],
44+
]);
45+
stdoutCalledWith(mockStdout, [
46+
'[command]alt',
47+
' >> stdout',
48+
'::warning:: >> stderr',
49+
]);
50+
});
51+
52+
it('should run command with args', async() => {
53+
const mockExec = spyOnExec();
54+
const mockStdout = spyOnStdout();
55+
56+
expect(await command.execAsync({command: 'test', args: ['hello!', 'how are you doing $USER', '"double"', '\'single\'']})).toEqual({
57+
stdout: 'stdout',
58+
stderr: '',
59+
command: 'test \'hello!\' \'how are you doing $USER\' \'"double"\' \\\'\'single\'\\\'',
60+
});
61+
62+
execCalledWith(mockExec, [
63+
'test \'hello!\' \'how are you doing $USER\' \'"double"\' \\\'\'single\'\\\'',
64+
]);
65+
stdoutCalledWith(mockStdout, [
66+
'[command]test \'hello!\' \'how are you doing $USER\' \'"double"\' \\\'\'single\'\\\'',
67+
' >> stdout',
68+
]);
69+
});
70+
71+
it('should run command with args, altCommand', async() => {
72+
const mockExec = spyOnExec();
73+
const mockStdout = spyOnStdout();
74+
75+
expect(await command.execAsync({command: 'test', args: ['hello!', 'how are you doing $USER', '"double"', '\'single\''], altCommand: 'alt'})).toEqual({
76+
stdout: 'stdout',
77+
stderr: '',
78+
command: 'alt',
79+
});
80+
81+
execCalledWith(mockExec, [
82+
'test \'hello!\' \'how are you doing $USER\' \'"double"\' \\\'\'single\'\\\'',
83+
]);
84+
stdoutCalledWith(mockStdout, [
85+
'[command]alt',
86+
' >> stdout',
87+
]);
88+
});
89+
90+
it('should not output empty stdout', async() => {
91+
setChildProcessParams({stdout: ' \n\n \n'});
92+
const mockExec = spyOnExec();
93+
const mockStdout = spyOnStdout();
94+
95+
expect(await command.execAsync({command: 'test'})).toEqual({stdout: '', stderr: '', command: 'test'});
96+
97+
execCalledWith(mockExec, [
98+
'test',
99+
]);
100+
stdoutCalledWith(mockStdout, [
101+
'[command]test',
102+
]);
103+
});
104+
105+
it('should not output empty stderr', async() => {
106+
setChildProcessParams({stderr: ' \n\n \n'});
107+
const mockExec = spyOnExec();
108+
const mockStdout = spyOnStdout();
109+
110+
expect(await command.execAsync({command: 'test'})).toEqual({stdout: 'stdout', stderr: '', command: 'test'});
111+
112+
execCalledWith(mockExec, [
113+
'test',
114+
]);
115+
stdoutCalledWith(mockStdout, [
116+
'[command]test',
117+
' >> stdout',
118+
]);
119+
});
120+
121+
it('should catch error 1', async() => {
122+
const error = new Error('test message');
123+
error['code'] = 123;
124+
setChildProcessParams({error: error});
125+
126+
await expect(command.execAsync({
127+
command: 'test',
128+
})).rejects.toThrow('command [test] exited with code 123. message: test message');
129+
});
130+
131+
it('should catch error 2', async() => {
132+
const error = new Error('test message');
133+
error['code'] = 123;
134+
setChildProcessParams({error: error});
135+
136+
await expect(command.execAsync({
137+
command: 'test',
138+
altCommand: 'alt',
139+
})).rejects.toThrow('command [alt] exited with code 123. message: test message');
140+
});
141+
142+
it('should catch error 3', async() => {
143+
const error = new Error('test message');
144+
error['code'] = 123;
145+
setChildProcessParams({error: error});
146+
147+
await expect(command.execAsync({
148+
command: 'test',
149+
altCommand: 'alt',
150+
quiet: true,
151+
})).rejects.toThrow('command [alt] exited with code 123.');
152+
});
153+
154+
it('should catch error 4', async() => {
155+
const error = new Error('test message');
156+
error['code'] = 123;
157+
setChildProcessParams({error: error});
158+
159+
await expect(command.execAsync({
160+
command: 'test',
161+
quiet: true,
162+
})).rejects.toThrow('command exited with code 123.');
163+
});
164+
165+
it('should suppress stdout', async() => {
166+
const mockExec = spyOnExec();
167+
const mockStdout = spyOnStdout();
168+
169+
await command.execAsync({
170+
command: 'test',
171+
suppressOutput: true,
172+
});
173+
174+
execCalledWith(mockExec, [
175+
'test',
176+
]);
177+
stdoutCalledWith(mockStdout, [
178+
'[command]test',
179+
]);
180+
});
181+
182+
it('should output stdout instead of stderr', async() => {
183+
setChildProcessParams({stderr: 'stderr'});
184+
const mockExec = spyOnExec();
185+
const mockStdout = spyOnStdout();
186+
187+
await command.execAsync({
188+
command: 'test',
189+
stderrToStdout: true,
190+
});
191+
192+
execCalledWith(mockExec, [
193+
'test',
194+
]);
195+
stdoutCalledWith(mockStdout, [
196+
'[command]test',
197+
' >> stdout',
198+
' >> stderr',
199+
]);
200+
});
201+
202+
it('should not output stdout', async() => {
203+
setChildProcessParams({stdout: ''});
204+
const mockExec = spyOnExec();
205+
const mockStdout = spyOnStdout();
206+
207+
await command.execAsync({
208+
command: 'test',
209+
});
210+
211+
execCalledWith(mockExec, [
212+
'test',
213+
]);
214+
stdoutCalledWith(mockStdout, [
215+
'[command]test',
216+
]);
217+
});
218+
219+
it('should run suppress error command', async() => {
220+
const mockExec = spyOnExec();
221+
const mockStdout = spyOnStdout();
222+
223+
await command.execAsync({
224+
command: 'test',
225+
suppressError: true,
226+
});
227+
228+
execCalledWith(mockExec, [
229+
'test || :',
230+
]);
231+
stdoutCalledWith(mockStdout, [
232+
'[command]test',
233+
' >> stdout',
234+
]);
235+
});
236+
});
File renamed without changes.

src/command.ts

Lines changed: 60 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { spawn, ExecException } from 'child_process';
1+
import { exec, spawn, ExecException } from 'child_process';
22
import escape from 'shell-escape';
33
import { Logger } from './index';
44

@@ -9,8 +9,9 @@ export default class Command {
99

1010
/**
1111
* @param {Logger} logger logger
12+
* @param {boolean} useExec use exec?
1213
*/
13-
constructor(private logger: Logger) {
14+
constructor(private logger: Logger, private useExec = false) {
1415
}
1516

1617
/**
@@ -146,6 +147,48 @@ export default class Command {
146147
});
147148
};
148149

150+
/**
151+
* @param {string} command command
152+
* @param {string|undefined} altCommand alt command
153+
* @param {boolean} quiet quiet?
154+
* @param {boolean} suppressOutput suppress output?
155+
* @param {boolean} stderrToStdout output to stdout instead of stderr
156+
* @param {function} resolve resolve
157+
* @param {function} reject reject
158+
* @return {void} void
159+
*/
160+
private execCallback = (
161+
command: string,
162+
altCommand: string | undefined,
163+
quiet: boolean,
164+
suppressOutput: boolean,
165+
stderrToStdout: boolean,
166+
resolve: Function,
167+
reject: Function,
168+
): (error: ExecException | null, stdout: string, stderr: string) => void => (error: ExecException | null, stdout: string, stderr: string): void => {
169+
if (error) {
170+
reject(new Error(this.getRejectedErrorMessage(command, altCommand, quiet, error)));
171+
} else {
172+
let trimmedStdout = stdout.trim();
173+
let trimmedStderr = stderr.trim();
174+
if (!quiet && !suppressOutput) {
175+
if (trimmedStdout) {
176+
this.logger.displayStdout(trimmedStdout);
177+
}
178+
if (trimmedStderr) {
179+
if (stderrToStdout) {
180+
this.logger.displayStdout(trimmedStderr);
181+
trimmedStdout += `\n${trimmedStderr}`;
182+
trimmedStderr = '';
183+
} else {
184+
this.logger.displayStderr(trimmedStderr);
185+
}
186+
}
187+
}
188+
resolve({stdout: trimmedStdout, stderr: trimmedStderr, command: 'string' === typeof altCommand ? altCommand : command});
189+
}
190+
};
191+
149192
/**
150193
* @param {object} options options
151194
* @param {string} options.command command
@@ -180,11 +223,21 @@ export default class Command {
180223
this.logger.displayCommand(commandWithArgs);
181224
}
182225

183-
try {
184-
const {stdout, stderr} = await this.execCommand(this.getCommand(commandWithArgs, quiet, suppressError), quiet, suppressOutput, stderrToStdout, cwd);
185-
return this.getCommandResult(commandWithArgs, altCommand, stderrToStdout, stdout, stderr);
186-
} catch (error) {
187-
throw new Error(this.getRejectedErrorMessage(command, altCommand, quiet, error));
226+
if (this.useExec) {
227+
return new Promise((resolve, reject) => {
228+
if (typeof cwd === 'undefined') {
229+
exec(this.getCommand(commandWithArgs, quiet, suppressError), this.execCallback(commandWithArgs, altCommand, quiet, suppressOutput, stderrToStdout, resolve, reject));
230+
} else {
231+
exec(this.getCommand(commandWithArgs, quiet, suppressError), {cwd}, this.execCallback(commandWithArgs, altCommand, quiet, suppressOutput, stderrToStdout, resolve, reject));
232+
}
233+
});
234+
} else {
235+
try {
236+
const {stdout, stderr} = await this.execCommand(this.getCommand(commandWithArgs, quiet, suppressError), quiet, suppressOutput, stderrToStdout, cwd);
237+
return this.getCommandResult(commandWithArgs, altCommand, stderrToStdout, stdout, stderr);
238+
} catch (error) {
239+
throw new Error(this.getRejectedErrorMessage(command, altCommand, quiet, error));
240+
}
188241
}
189242
};
190243
}

0 commit comments

Comments
 (0)