Skip to content

Commit c095c08

Browse files
authored
Fix command async tests (#369)
1 parent dc20842 commit c095c08

File tree

1 file changed

+97
-59
lines changed

1 file changed

+97
-59
lines changed

src/command.spec.ts

+97-59
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,19 @@
11
import { autoUnsubscribe, subscribeSpyTo } from '@hirez_io/observer-spy';
22
import { SpawnOptions } from 'child_process';
33
import { EventEmitter } from 'events';
4+
import * as Rx from 'rxjs';
45
import { Readable, Writable } from 'stream';
56

6-
import { ChildProcess, Command, CommandInfo, KillProcess, SpawnCommand } from './command';
7+
import {
8+
ChildProcess,
9+
CloseEvent,
10+
Command,
11+
CommandInfo,
12+
KillProcess,
13+
SpawnCommand,
14+
} from './command';
15+
16+
type CommandValues = { error: unknown; close: CloseEvent; timer: unknown[] };
717

818
let process: ChildProcess;
919
let spawn: jest.Mocked<SpawnCommand>;
@@ -34,94 +44,121 @@ beforeEach(() => {
3444
killProcess = jest.fn();
3545
});
3646

37-
const createCommand = (overrides?: Partial<CommandInfo>, spawnOpts?: SpawnOptions) =>
38-
new Command(
47+
const createCommand = (overrides?: Partial<CommandInfo>, spawnOpts?: SpawnOptions) => {
48+
const command = new Command(
3949
{ index: 0, name: '', command: 'echo foo', ...overrides },
4050
spawnOpts,
4151
spawn,
4252
killProcess
4353
);
4454

55+
let error: unknown;
56+
let close: CloseEvent;
57+
const timer = subscribeSpyTo(command.timer);
58+
const finished = subscribeSpyTo(
59+
new Rx.Observable((observer) => {
60+
// First event in both subjects means command has finished
61+
command.error.subscribe({
62+
next: (value) => {
63+
error = value;
64+
observer.complete();
65+
},
66+
});
67+
command.close.subscribe({
68+
next: (value) => {
69+
close = value;
70+
observer.complete();
71+
},
72+
});
73+
})
74+
);
75+
const values = async (): Promise<CommandValues> => {
76+
await finished.onComplete();
77+
return { error, close, timer: timer.getValues() };
78+
};
79+
80+
return { command, values };
81+
};
82+
4583
describe('#start()', () => {
4684
it('spawns process with given command and options', () => {
47-
const command = createCommand({}, { detached: true });
85+
const { command } = createCommand({}, { detached: true });
4886
command.start();
4987

5088
expect(spawn).toHaveBeenCalledTimes(1);
5189
expect(spawn).toHaveBeenCalledWith(command.command, { detached: true });
5290
});
5391

5492
it('sets stdin, process and PID', () => {
55-
const command = createCommand();
93+
const { command } = createCommand();
5694
command.start();
5795

5896
expect(command.process).toBe(process);
5997
expect(command.pid).toBe(process.pid);
6098
expect(command.stdin).toBe(process.stdin);
6199
});
62100

63-
it('shares errors to the error stream', () => {
64-
const command = createCommand();
65-
const observerSpy = subscribeSpyTo(command.error);
101+
it('shares errors to the error stream', async () => {
102+
const { command, values } = createCommand();
66103
command.start();
67104
process.emit('error', 'foo');
105+
const { error } = await values();
68106

69-
expect(observerSpy.getFirstValue()).toBe('foo');
107+
expect(error).toBe('foo');
70108
expect(command.process).toBeUndefined();
71109
});
72110

73-
it('shares start and close timing events to the timing stream', () => {
74-
const command = createCommand();
75-
const observerSpy = subscribeSpyTo(command.timer);
111+
it('shares start and close timing events to the timing stream', async () => {
112+
const { command, values } = createCommand();
76113
const startDate = new Date();
77114
const endDate = new Date(startDate.getTime() + 1000);
78115
jest.spyOn(Date, 'now')
79116
.mockReturnValueOnce(startDate.getTime())
80117
.mockReturnValueOnce(endDate.getTime());
81118
command.start();
82119
process.emit('close', 0, null);
120+
const { timer } = await values();
83121

84-
expect(observerSpy.getValueAt(0)).toEqual({ startDate, endDate: undefined });
85-
expect(observerSpy.getValueAt(1)).toEqual({ startDate, endDate });
122+
expect(timer[0]).toEqual({ startDate, endDate: undefined });
123+
expect(timer[1]).toEqual({ startDate, endDate });
86124
});
87125

88-
it('shares start and error timing events to the timing stream', () => {
89-
const command = createCommand();
90-
const observerSpy = subscribeSpyTo(command.timer);
126+
it('shares start and error timing events to the timing stream', async () => {
127+
const { command, values } = createCommand();
91128
const startDate = new Date();
92129
const endDate = new Date(startDate.getTime() + 1000);
93130
jest.spyOn(Date, 'now')
94131
.mockReturnValueOnce(startDate.getTime())
95132
.mockReturnValueOnce(endDate.getTime());
96133
command.start();
97134
process.emit('error', 0, null);
135+
const { timer } = await values();
98136

99-
expect(observerSpy.getValueAt(0)).toEqual({ startDate, endDate: undefined });
100-
expect(observerSpy.getValueAt(1)).toEqual({ startDate, endDate });
137+
expect(timer[0]).toEqual({ startDate, endDate: undefined });
138+
expect(timer[1]).toEqual({ startDate, endDate });
101139
});
102140

103-
it('shares closes to the close stream with exit code', () => {
104-
const command = createCommand();
105-
const observerSpy = subscribeSpyTo(command.close);
141+
it('shares closes to the close stream with exit code', async () => {
142+
const { command, values } = createCommand();
106143
command.start();
107144
process.emit('close', 0, null);
145+
const { close } = await values();
108146

109-
expect(observerSpy.getFirstValue()).toMatchObject({ exitCode: 0, killed: false });
147+
expect(close).toMatchObject({ exitCode: 0, killed: false });
110148
expect(command.process).toBeUndefined();
111149
});
112150

113-
it('shares closes to the close stream with signal', () => {
114-
const command = createCommand();
115-
const observerSpy = subscribeSpyTo(command.close);
151+
it('shares closes to the close stream with signal', async () => {
152+
const { command, values } = createCommand();
116153
command.start();
117154
process.emit('close', null, 'SIGKILL');
155+
const { close } = await values();
118156

119-
expect(observerSpy.getFirstValue()).toMatchObject({ exitCode: 'SIGKILL', killed: false });
157+
expect(close).toMatchObject({ exitCode: 'SIGKILL', killed: false });
120158
});
121159

122-
it('shares closes to the close stream with timing information', () => {
123-
const command = createCommand();
124-
const observerSpy = subscribeSpyTo(command.close);
160+
it('shares closes to the close stream with timing information', async () => {
161+
const { command, values } = createCommand();
125162
const startDate = new Date();
126163
const endDate = new Date(startDate.getTime() + 1000);
127164
jest.spyOn(Date, 'now')
@@ -132,85 +169,86 @@ describe('#start()', () => {
132169
.mockReturnValueOnce([1, 1e8]);
133170
command.start();
134171
process.emit('close', null, 'SIGKILL');
172+
const { close } = await values();
135173

136-
expect(observerSpy.getFirstValue().timings).toStrictEqual({
174+
expect(close.timings).toStrictEqual({
137175
startDate,
138176
endDate,
139177
durationSeconds: 1.1,
140178
});
141179
});
142180

143-
it('shares closes to the close stream with command info', () => {
181+
it('shares closes to the close stream with command info', async () => {
144182
const commandInfo = {
145183
command: 'cmd',
146184
name: 'name',
147185
prefixColor: 'green',
148186
env: { VAR: 'yes' },
149187
};
150-
const command = createCommand(commandInfo);
151-
const observerSpy = subscribeSpyTo(command.close);
188+
const { command, values } = createCommand(commandInfo);
152189
command.start();
153190
process.emit('close', 0, null);
191+
const { close } = await values();
154192

155-
expect(observerSpy.getFirstValue().command).toEqual(expect.objectContaining(commandInfo));
156-
expect(observerSpy.getFirstValue().killed).toBe(false);
193+
expect(close.command).toEqual(expect.objectContaining(commandInfo));
194+
expect(close.killed).toBe(false);
157195
});
158196

159-
it('shares stdout to the stdout stream', () => {
160-
const command = createCommand();
161-
const observerSpy = subscribeSpyTo(command.stdout);
197+
it('shares stdout to the stdout stream', async () => {
198+
const { command } = createCommand();
199+
const stdout = Rx.firstValueFrom(command.stdout);
162200
command.start();
163201
process.stdout.emit('data', Buffer.from('hello'));
164202

165-
expect(observerSpy.getFirstValue().toString()).toBe('hello');
203+
expect((await stdout).toString()).toBe('hello');
166204
});
167205

168-
it('shares stderr to the stdout stream', () => {
169-
const command = createCommand();
170-
const observerSpy = subscribeSpyTo(command.stderr);
206+
it('shares stderr to the stdout stream', async () => {
207+
const { command } = createCommand();
208+
const stderr = Rx.firstValueFrom(command.stderr);
171209
command.start();
172210
process.stderr.emit('data', Buffer.from('dang'));
173211

174-
expect(observerSpy.getFirstValue().toString()).toBe('dang');
212+
expect((await stderr).toString()).toBe('dang');
175213
});
176214
});
177215

178216
describe('#kill()', () => {
179-
let command: Command;
217+
let createdCommand: { command: Command; values: () => Promise<CommandValues> };
180218
beforeEach(() => {
181-
command = createCommand();
219+
createdCommand = createCommand();
182220
});
183221

184222
it('kills process', () => {
185-
command.start();
186-
command.kill();
223+
createdCommand.command.start();
224+
createdCommand.command.kill();
187225

188226
expect(killProcess).toHaveBeenCalledTimes(1);
189-
expect(killProcess).toHaveBeenCalledWith(command.pid, undefined);
227+
expect(killProcess).toHaveBeenCalledWith(createdCommand.command.pid, undefined);
190228
});
191229

192230
it('kills process with some signal', () => {
193-
command.start();
194-
command.kill('SIGKILL');
231+
createdCommand.command.start();
232+
createdCommand.command.kill('SIGKILL');
195233

196234
expect(killProcess).toHaveBeenCalledTimes(1);
197-
expect(killProcess).toHaveBeenCalledWith(command.pid, 'SIGKILL');
235+
expect(killProcess).toHaveBeenCalledWith(createdCommand.command.pid, 'SIGKILL');
198236
});
199237

200238
it('does not try to kill inexistent process', () => {
201-
command.start();
239+
createdCommand.command.start();
202240
process.emit('error');
203-
command.kill();
241+
createdCommand.command.kill();
204242

205243
expect(killProcess).not.toHaveBeenCalled();
206244
});
207245

208-
it('marks the command as killed', () => {
209-
command.start();
210-
const observerSpy = subscribeSpyTo(command.close);
211-
command.kill();
246+
it('marks the command as killed', async () => {
247+
createdCommand.command.start();
248+
createdCommand.command.kill();
212249
process.emit('close', 1, null);
250+
const { close } = await createdCommand.values();
213251

214-
expect(observerSpy.getFirstValue()).toMatchObject({ exitCode: 1, killed: true });
252+
expect(close).toMatchObject({ exitCode: 1, killed: true });
215253
});
216254
});

0 commit comments

Comments
 (0)