-
Notifications
You must be signed in to change notification settings - Fork 82
Expand file tree
/
Copy pathSupervisor.js
More file actions
342 lines (297 loc) · 10.9 KB
/
Supervisor.js
File metadata and controls
342 lines (297 loc) · 10.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
// @flow
const chalk = require('./chalk');
const child_process = require('child_process');
const fs = require('fs');
const net = require('net');
const split = require('split');
const XmlBuilder = require('xmlbuilder');
const Report = require('./Report');
function run(
elmTestVersion /*: string */,
pipeFilename /*: string */,
report /*: typeof Report.Report */,
processes /*: number */,
dest /*: string */,
watch /*: boolean */
) /*: Promise<number> */ {
return new Promise(function (resolve) {
var nextResultToPrint = null;
var finishedWorkers = 0;
var closedWorkers = 0;
var results = new Map();
var failures = 0;
var todos = [];
var testsToRun = -1;
var initializedWorkers = -1;
var startingTime = Date.now();
var workers = [];
function printResult(result) {
switch (report) {
case 'console':
switch (result.type) {
case 'begin':
console.log(makeWindowsSafe(result.output));
break;
case 'complete':
switch (result.status) {
case 'pass':
// passed tests should be printed only if they contain distributionReport
if (result.distributionReport !== undefined) {
console.log(makeWindowsSafe(result.distributionReport));
}
break;
case 'todo':
// todos will be shown in the SUMMARY only.
break;
case 'fail':
console.log(makeWindowsSafe(result.failure));
break;
default:
throw new Error(`Unexpected result.status: ${result.status}`);
}
break;
case 'summary':
console.log(makeWindowsSafe(result.summary));
break;
default:
throw new Error(`Unexpected result.type: ${result.type}`);
}
break;
case 'json':
console.log(JSON.stringify(result));
break;
case 'junit':
// JUnit does everything at once in SUMMARY, elsewhere
break;
}
}
function flushResults() {
// Only print any results if we're ready - that is, nextResultToPrint
// is no longer null. (BEGIN changes it from null to 0.)
if (nextResultToPrint !== null) {
var result = results.get(nextResultToPrint);
while (
// If there are no more results to print, then we're done.
nextResultToPrint < testsToRun &&
// Otherwise, keep going until we have no result available to print.
typeof result !== 'undefined'
) {
printResult(result);
nextResultToPrint++;
result = results.get(nextResultToPrint);
}
}
}
function reportRuntimeException() {
console.error(
chalk.red(
'\n\nThere was an unexpected runtime exception while running tests\n\n'
)
);
}
function handleResults(response) {
// TODO print progress bar - e.g. "Running test 5 of 20" on a bar!
// -- yikes, be careful though...test the scenario where test
// authors put Debug.log in their tests - does that mess
// everything up re: the line feed? Seems like it would...
// ...so maybe a bar is not best. Can we do better? Hm.
// Maybe the answer is to print the thing, then Immediately
// backtrack the line feed, so that if someone else does more
// logging, it will overwrite our status update and that's ok?
Object.keys(response.results).forEach(function (index) {
var result = response.results[index];
results.set(parseInt(index), result);
switch (report) {
case 'console':
switch (result.status) {
case 'pass':
// It's a PASS; no need to take any action.
break;
case 'todo':
todos.push(result);
break;
case 'fail':
failures++;
break;
default:
throw new Error(`Unexpected result.status: ${result.status}`);
}
break;
case 'junit':
if (typeof result.failure !== 'undefined') {
failures++;
}
break;
case 'json':
if (result.status === 'fail') {
failures++;
} else if (result.status === 'todo') {
todos.push({ labels: result.labels, todo: result.failures[0] });
}
break;
}
});
flushResults();
}
function initWorker(socket) {
socket.setEncoding('utf8');
socket.setNoDelay(true);
// See the long note near client.write() in worker.js for why we do this.
// It fixes a nasty bug!
var stream = socket.pipe(split());
stream.on('data', function (data) {
// In watch mode, the socket is drained which results in an extraneous
// message being sent. If we receive no data, ignore it.
if (!data) {
return;
}
var response = JSON.parse(data);
switch (response.type) {
case 'FINISHED':
handleResults(response);
// This worker found no tests remaining to run; it's finished!
finishedWorkers++;
// If all the workers have finished, print the summmary.
if (finishedWorkers === workers.length) {
socket.write(
JSON.stringify({
type: 'SUMMARY',
duration: Date.now() - startingTime,
failures: failures,
todos: todos,
})
);
}
break;
case 'SUMMARY':
flushResults();
if (response.exitCode === 1) {
// The tests could not even run. At the time of this writing, the
// only case is “No exposed values of type Test found”. That
// _could_ have been caught at compile time, but the current
// architecture needs to actually run the JS to figure out which
// exposed values are of type Test. That’s why this type of
// response is handled differently than others.
console.error(response.message);
} else {
printResult(response.message);
if (report === 'junit') {
var xml = response.message;
var values = Array.from(results.values());
xml.testsuite.testcase = xml.testsuite.testcase.concat(values);
// The XmlBuilder by default does not remove characters that are
// invalid in XML, like backspaces. However, we can pass it an
// `invalidCharReplacement` option to tell it how to handle
// those characters, rather than crashing. In an attempt to
// retain useful information in the output, we try and output a
// hex-encoded unicode codepoint for the invalid character. For
// example, the start of a terminal escape (`\u{001B}` in Elm) will be output as a
// literal `\u{001B}`.
var invalidCharReplacement = function (char) {
return (
'\\u{' +
char.codePointAt(0).toString(16).padStart(4, '0') +
'}'
);
};
console.log(
XmlBuilder.create(xml, {
invalidCharReplacement: invalidCharReplacement,
}).end()
);
}
}
// Close all the workers.
workers.forEach(function (worker) {
worker.kill();
});
resolve(response.exitCode);
break;
case 'BEGIN':
testsToRun = response.testCount;
if (!Report.isMachineReadable(report)) {
var headline = 'elm-test ' + elmTestVersion;
var bar = '-'.repeat(headline.length);
console.log('\n' + headline + '\n' + bar + '\n');
}
printResult(response.message);
// Now we're ready to print results!
nextResultToPrint = 0;
flushResults();
break;
case 'RESULTS':
handleResults(response);
break;
case 'ERROR':
throw new Error(response.message);
default:
throw new Error(
'Unrecognized message from worker:' + response.type
);
}
});
socket.write(JSON.stringify({ type: 'TEST', index: initializedWorkers }));
initializedWorkers++;
}
var pendingException = false,
server = net.createServer(initWorker);
server.on('error', function (err) {
console.error(err.stack);
server.close();
});
server.on('listening', function () {
workers = Array.from({ length: processes }).map(() => {
var worker = child_process.fork(dest);
worker.on('close', function (code) {
// code can be null.
var hasNonZeroExitCode = typeof code === 'number' && code !== 0;
if (watch && !Report.isMachineReadable(report)) {
if (hasNonZeroExitCode) {
// Queue up complaining about an exception.
// Don't print it immediately, or else it might print N times
// where N is the number of cores.
pendingException = true;
}
closedWorkers++;
// If all the workers have closed, we're done! Continue watching.
if (closedWorkers === workers.length) {
if (pendingException) {
// If we had an exception pending, print it and clear pending flag.
reportRuntimeException();
pendingException = false;
}
resolve(1);
}
} else if (hasNonZeroExitCode) {
reportRuntimeException();
resolve(1);
}
});
return worker;
});
});
if (fs.existsSync(pipeFilename) && process.platform !== 'win32') {
// Never remove named pipes on Windows. The OS will clean them up when
// nothing has a handle to them anymore.
fs.unlinkSync(pipeFilename);
}
server.listen(pipeFilename);
});
}
function makeWindowsSafe(text) {
return process.platform === 'win32' ? windowsify(text) : text;
}
// Fix Windows Unicode problems. Credit to https://github.com/sindresorhus/figures for the Windows compat idea!
var windowsSubstitutions = [
[/[↓✗►]/g, '>'],
[/╵│╷╹┃╻/g, '|'],
[/═/g, '='],
[/▔/g, '-'],
[/✔/g, '√'],
];
function windowsify(str) {
return windowsSubstitutions.reduce(function (result /*: string */, sub) {
return result.replace(sub[0], sub[1]);
}, str);
}
module.exports = { run: run };