Skip to content

Commit bae6fe5

Browse files
authored
Ensuring completion-listener.ts listen to a single close event emitter (#464)
1 parent 7207279 commit bae6fe5

3 files changed

Lines changed: 194 additions & 123 deletions

File tree

src/command.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ export interface CloseEvent {
5656
* The exit code or signal for the command.
5757
*/
5858
exitCode: string | number;
59+
5960
timings: {
6061
startDate: Date;
6162
endDate: Date;

src/completion-listener.spec.ts

Lines changed: 180 additions & 117 deletions
Original file line numberDiff line numberDiff line change
@@ -24,186 +24,249 @@ const createController = (successCondition?: SuccessCondition) =>
2424
const emitFakeCloseEvent = (command: FakeCommand, event?: Partial<CloseEvent>) =>
2525
command.close.next(createFakeCloseEvent({ ...event, command, index: command.index }));
2626

27-
describe('with default success condition set', () => {
28-
it('succeeds if all processes exited with code 0', () => {
29-
const result = createController().listen(commands);
27+
const flushPromises = () => new Promise((resolve) => setTimeout(resolve, 0));
28+
29+
describe('listen', () => {
30+
it('check for success once all commands have emitted at least a single close event', async () => {
31+
const finallyCallback = jest.fn();
32+
const result = createController().listen(commands).finally(finallyCallback);
33+
34+
// Emitting multiple close events to mimic calling command `kill/start` APIs.
35+
emitFakeCloseEvent(commands[0]);
36+
emitFakeCloseEvent(commands[0]);
37+
emitFakeCloseEvent(commands[0]);
38+
39+
scheduler.flush();
40+
// A broken implementantion will have called finallyCallback only after flushing promises
41+
await flushPromises();
42+
expect(finallyCallback).not.toHaveBeenCalled();
3043

31-
commands[0].close.next(createFakeCloseEvent({ exitCode: 0 }));
32-
commands[1].close.next(createFakeCloseEvent({ exitCode: 0 }));
33-
commands[2].close.next(createFakeCloseEvent({ exitCode: 0 }));
44+
emitFakeCloseEvent(commands[1]);
45+
emitFakeCloseEvent(commands[2]);
3446

3547
scheduler.flush();
3648

37-
return expect(result).resolves.toEqual(expect.anything());
49+
await expect(result).resolves.toEqual(expect.anything());
50+
expect(finallyCallback).toHaveBeenCalled();
3851
});
3952

40-
it('fails if one of the processes exited with non-0 code', () => {
53+
it('takes last event emitted from each command', async () => {
4154
const result = createController().listen(commands);
4255

43-
commands[0].close.next(createFakeCloseEvent({ exitCode: 0 }));
44-
commands[1].close.next(createFakeCloseEvent({ exitCode: 1 }));
45-
commands[2].close.next(createFakeCloseEvent({ exitCode: 0 }));
56+
emitFakeCloseEvent(commands[0], { exitCode: 0 });
57+
emitFakeCloseEvent(commands[0], { exitCode: 1 });
58+
emitFakeCloseEvent(commands[1], { exitCode: 0 });
59+
emitFakeCloseEvent(commands[2], { exitCode: 0 });
4660

4761
scheduler.flush();
4862

49-
return expect(result).rejects.toEqual(expect.anything());
63+
await expect(result).rejects.toEqual(expect.anything());
5064
});
51-
});
5265

53-
describe('with success condition set to first', () => {
54-
it('succeeds if first process to exit has code 0', () => {
55-
const result = createController('first').listen(commands);
66+
it('waits for manually restarted events to close', async () => {
67+
const finallyCallback = jest.fn();
68+
const result = createController().listen(commands).finally(finallyCallback);
5669

57-
commands[1].close.next(createFakeCloseEvent({ exitCode: 0 }));
58-
commands[0].close.next(createFakeCloseEvent({ exitCode: 1 }));
59-
commands[2].close.next(createFakeCloseEvent({ exitCode: 1 }));
70+
emitFakeCloseEvent(commands[0]);
71+
commands[0].state = 'started';
72+
emitFakeCloseEvent(commands[1]);
73+
emitFakeCloseEvent(commands[2]);
74+
75+
scheduler.flush();
76+
// A broken implementantion will have called finallyCallback only after flushing promises
77+
await flushPromises();
78+
expect(finallyCallback).not.toHaveBeenCalled();
6079

80+
commands[0].state = 'exited';
81+
emitFakeCloseEvent(commands[0]);
6182
scheduler.flush();
6283

63-
return expect(result).resolves.toEqual(expect.anything());
84+
await expect(result).resolves.toEqual(expect.anything());
85+
expect(finallyCallback).toHaveBeenCalled();
6486
});
87+
});
6588

66-
it('fails if first process to exit has non-0 code', () => {
67-
const result = createController('first').listen(commands);
89+
describe('Detect commands exit conditions', () => {
90+
describe('with default success condition set', () => {
91+
it('succeeds if all processes exited with code 0', () => {
92+
const result = createController().listen(commands);
6893

69-
commands[1].close.next(createFakeCloseEvent({ exitCode: 1 }));
70-
commands[0].close.next(createFakeCloseEvent({ exitCode: 0 }));
71-
commands[2].close.next(createFakeCloseEvent({ exitCode: 0 }));
94+
commands[0].close.next(createFakeCloseEvent({ exitCode: 0 }));
95+
commands[1].close.next(createFakeCloseEvent({ exitCode: 0 }));
96+
commands[2].close.next(createFakeCloseEvent({ exitCode: 0 }));
7297

73-
scheduler.flush();
98+
scheduler.flush();
7499

75-
return expect(result).rejects.toEqual(expect.anything());
76-
});
77-
});
100+
return expect(result).resolves.toEqual(expect.anything());
101+
});
78102

79-
describe('with success condition set to last', () => {
80-
it('succeeds if last process to exit has code 0', () => {
81-
const result = createController('last').listen(commands);
103+
it('fails if one of the processes exited with non-0 code', () => {
104+
const result = createController().listen(commands);
82105

83-
commands[1].close.next(createFakeCloseEvent({ exitCode: 1 }));
84-
commands[0].close.next(createFakeCloseEvent({ exitCode: 0 }));
85-
commands[2].close.next(createFakeCloseEvent({ exitCode: 0 }));
106+
commands[0].close.next(createFakeCloseEvent({ exitCode: 0 }));
107+
commands[1].close.next(createFakeCloseEvent({ exitCode: 1 }));
108+
commands[2].close.next(createFakeCloseEvent({ exitCode: 0 }));
86109

87-
scheduler.flush();
110+
scheduler.flush();
88111

89-
return expect(result).resolves.toEqual(expect.anything());
112+
return expect(result).rejects.toEqual(expect.anything());
113+
});
90114
});
91115

92-
it('fails if last process to exit has non-0 code', () => {
93-
const result = createController('last').listen(commands);
116+
describe('with success condition set to first', () => {
117+
it('succeeds if first process to exit has code 0', () => {
118+
const result = createController('first').listen(commands);
94119

95-
commands[1].close.next(createFakeCloseEvent({ exitCode: 0 }));
96-
commands[0].close.next(createFakeCloseEvent({ exitCode: 1 }));
97-
commands[2].close.next(createFakeCloseEvent({ exitCode: 1 }));
120+
commands[1].close.next(createFakeCloseEvent({ exitCode: 0 }));
121+
commands[0].close.next(createFakeCloseEvent({ exitCode: 1 }));
122+
commands[2].close.next(createFakeCloseEvent({ exitCode: 1 }));
98123

99-
scheduler.flush();
124+
scheduler.flush();
100125

101-
return expect(result).rejects.toEqual(expect.anything());
102-
});
103-
});
126+
return expect(result).resolves.toEqual(expect.anything());
127+
});
104128

105-
describe.each([
106-
// Use the middle command for both cases to make it more difficult to make a mess up
107-
// in the implementation cause false passes.
108-
['command-bar' as const, 'bar'],
109-
['command-1' as const, 1],
110-
])('with success condition set to %s', (condition, nameOrIndex) => {
111-
it(`succeeds if command ${nameOrIndex} exits with code 0`, () => {
112-
const result = createController(condition).listen(commands);
129+
it('fails if first process to exit has non-0 code', () => {
130+
const result = createController('first').listen(commands);
113131

114-
emitFakeCloseEvent(commands[0], { exitCode: 1 });
115-
emitFakeCloseEvent(commands[1], { exitCode: 0 });
116-
emitFakeCloseEvent(commands[2], { exitCode: 1 });
132+
commands[1].close.next(createFakeCloseEvent({ exitCode: 1 }));
133+
commands[0].close.next(createFakeCloseEvent({ exitCode: 0 }));
134+
commands[2].close.next(createFakeCloseEvent({ exitCode: 0 }));
117135

118-
scheduler.flush();
136+
scheduler.flush();
119137

120-
return expect(result).resolves.toEqual(expect.anything());
138+
return expect(result).rejects.toEqual(expect.anything());
139+
});
121140
});
122141

123-
it(`succeeds if all commands ${nameOrIndex} exit with code 0`, () => {
124-
commands = [commands[0], commands[1], commands[1]];
125-
const result = createController(condition).listen(commands);
142+
describe('with success condition set to last', () => {
143+
it('succeeds if last process to exit has code 0', () => {
144+
const result = createController('last').listen(commands);
126145

127-
emitFakeCloseEvent(commands[0], { exitCode: 1 });
128-
emitFakeCloseEvent(commands[1], { exitCode: 0 });
129-
emitFakeCloseEvent(commands[2], { exitCode: 0 });
146+
commands[1].close.next(createFakeCloseEvent({ exitCode: 1 }));
147+
commands[0].close.next(createFakeCloseEvent({ exitCode: 0 }));
148+
commands[2].close.next(createFakeCloseEvent({ exitCode: 0 }));
130149

131-
scheduler.flush();
150+
scheduler.flush();
132151

133-
return expect(result).resolves.toEqual(expect.anything());
134-
});
152+
return expect(result).resolves.toEqual(expect.anything());
153+
});
135154

136-
it(`fails if command ${nameOrIndex} exits with non-0 code`, () => {
137-
const result = createController(condition).listen(commands);
155+
it('fails if last process to exit has non-0 code', () => {
156+
const result = createController('last').listen(commands);
138157

139-
emitFakeCloseEvent(commands[0], { exitCode: 0 });
140-
emitFakeCloseEvent(commands[1], { exitCode: 1 });
141-
emitFakeCloseEvent(commands[2], { exitCode: 0 });
158+
commands[1].close.next(createFakeCloseEvent({ exitCode: 0 }));
159+
commands[0].close.next(createFakeCloseEvent({ exitCode: 1 }));
160+
commands[2].close.next(createFakeCloseEvent({ exitCode: 1 }));
142161

143-
scheduler.flush();
162+
scheduler.flush();
144163

145-
return expect(result).rejects.toEqual(expect.anything());
164+
return expect(result).rejects.toEqual(expect.anything());
165+
});
146166
});
147167

148-
it(`fails if some commands ${nameOrIndex} exit with non-0 code`, () => {
149-
commands = [commands[0], commands[1], commands[1]];
150-
const result = createController(condition).listen(commands);
168+
describe.each([
169+
// Use the middle command for both cases to make it more difficult to make a mess up
170+
// in the implementation cause false passes.
171+
['command-bar' as const, 'bar'],
172+
['command-1' as const, 1],
173+
])('with success condition set to %s', (condition, nameOrIndex) => {
174+
it(`succeeds if command ${nameOrIndex} exits with code 0`, () => {
175+
const result = createController(condition).listen(commands);
151176

152-
emitFakeCloseEvent(commands[0], { exitCode: 1 });
153-
emitFakeCloseEvent(commands[1], { exitCode: 0 });
154-
emitFakeCloseEvent(commands[2], { exitCode: 1 });
177+
emitFakeCloseEvent(commands[0], { exitCode: 1 });
178+
emitFakeCloseEvent(commands[1], { exitCode: 0 });
179+
emitFakeCloseEvent(commands[2], { exitCode: 1 });
155180

156-
scheduler.flush();
181+
scheduler.flush();
157182

158-
return expect(result).resolves.toEqual(expect.anything());
159-
});
183+
return expect(result).resolves.toEqual(expect.anything());
184+
});
160185

161-
it(`fails if command ${nameOrIndex} doesn't exist`, () => {
162-
const result = createController(condition).listen([commands[0]]);
186+
it(`succeeds if all commands ${nameOrIndex} exit with code 0`, () => {
187+
commands = [commands[0], commands[1], commands[1]];
188+
const result = createController(condition).listen(commands);
163189

164-
emitFakeCloseEvent(commands[0], { exitCode: 0 });
165-
scheduler.flush();
190+
emitFakeCloseEvent(commands[0], { exitCode: 1 });
191+
emitFakeCloseEvent(commands[1], { exitCode: 0 });
192+
emitFakeCloseEvent(commands[2], { exitCode: 0 });
166193

167-
return expect(result).rejects.toEqual(expect.anything());
168-
});
169-
});
194+
scheduler.flush();
170195

171-
describe.each([
172-
// Use the middle command for both cases to make it more difficult to make a mess up
173-
// in the implementation cause false passes.
174-
['!command-bar' as const, 'bar'],
175-
['!command-1' as const, 1],
176-
])('with success condition set to %s', (condition, nameOrIndex) => {
177-
it(`succeeds if all commands but ${nameOrIndex} exit with code 0`, () => {
178-
const result = createController(condition).listen(commands);
196+
return expect(result).resolves.toEqual(expect.anything());
197+
});
179198

180-
emitFakeCloseEvent(commands[0], { exitCode: 0 });
181-
emitFakeCloseEvent(commands[1], { exitCode: 1 });
182-
emitFakeCloseEvent(commands[2], { exitCode: 0 });
199+
it(`fails if command ${nameOrIndex} exits with non-0 code`, () => {
200+
const result = createController(condition).listen(commands);
183201

184-
scheduler.flush();
202+
emitFakeCloseEvent(commands[0], { exitCode: 0 });
203+
emitFakeCloseEvent(commands[1], { exitCode: 1 });
204+
emitFakeCloseEvent(commands[2], { exitCode: 0 });
185205

186-
return expect(result).resolves.toEqual(expect.anything());
187-
});
206+
scheduler.flush();
188207

189-
it(`fails if any commands but ${nameOrIndex} exit with non-0 code`, () => {
190-
const result = createController(condition).listen(commands);
208+
return expect(result).rejects.toEqual(expect.anything());
209+
});
191210

192-
emitFakeCloseEvent(commands[0], { exitCode: 1 });
193-
emitFakeCloseEvent(commands[1], { exitCode: 1 });
194-
emitFakeCloseEvent(commands[2], { exitCode: 0 });
211+
it(`fails if some commands ${nameOrIndex} exit with non-0 code`, () => {
212+
const result = createController(condition).listen(commands);
195213

196-
scheduler.flush();
214+
emitFakeCloseEvent(commands[0], { exitCode: 1 });
215+
emitFakeCloseEvent(commands[1], { exitCode: 0 });
216+
emitFakeCloseEvent(commands[2], { exitCode: 1 });
217+
218+
scheduler.flush();
219+
220+
return expect(result).resolves.toEqual(expect.anything());
221+
});
222+
223+
it(`fails if command ${nameOrIndex} doesn't exist`, () => {
224+
const result = createController(condition).listen([commands[0]]);
197225

198-
return expect(result).rejects.toEqual(expect.anything());
226+
emitFakeCloseEvent(commands[0], { exitCode: 0 });
227+
scheduler.flush();
228+
229+
return expect(result).rejects.toEqual(expect.anything());
230+
});
199231
});
200232

201-
it(`succeeds if command ${nameOrIndex} doesn't exist`, () => {
202-
const result = createController(condition).listen([commands[0]]);
233+
describe.each([
234+
// Use the middle command for both cases to make it more difficult to make a mess up
235+
// in the implementation cause false passes.
236+
['!command-bar' as const, 'bar'],
237+
['!command-1' as const, 1],
238+
])('with success condition set to %s', (condition, nameOrIndex) => {
239+
it(`succeeds if all commands but ${nameOrIndex} exit with code 0`, () => {
240+
const result = createController(condition).listen(commands);
203241

204-
emitFakeCloseEvent(commands[0], { exitCode: 0 });
205-
scheduler.flush();
242+
emitFakeCloseEvent(commands[0], { exitCode: 0 });
243+
emitFakeCloseEvent(commands[1], { exitCode: 1 });
244+
emitFakeCloseEvent(commands[2], { exitCode: 0 });
245+
246+
scheduler.flush();
247+
248+
return expect(result).resolves.toEqual(expect.anything());
249+
});
250+
251+
it(`fails if any commands but ${nameOrIndex} exit with non-0 code`, () => {
252+
const result = createController(condition).listen(commands);
253+
254+
emitFakeCloseEvent(commands[0], { exitCode: 1 });
255+
emitFakeCloseEvent(commands[1], { exitCode: 1 });
256+
emitFakeCloseEvent(commands[2], { exitCode: 0 });
257+
258+
scheduler.flush();
259+
260+
return expect(result).rejects.toEqual(expect.anything());
261+
});
262+
263+
it(`succeeds if command ${nameOrIndex} doesn't exist`, () => {
264+
const result = createController(condition).listen([commands[0]]);
265+
266+
emitFakeCloseEvent(commands[0], { exitCode: 0 });
267+
scheduler.flush();
206268

207-
return expect(result).resolves.toEqual(expect.anything());
269+
return expect(result).resolves.toEqual(expect.anything());
270+
});
208271
});
209272
});

0 commit comments

Comments
 (0)