21
22
readline.emitKeypressEvents(process.stdin);
23
24
-if (process.stdin.isTTY) {
25
- process.stdin.setRawMode(true);
26
-}
24
+process.stdin.on("keypress", function (chunk, key) {
25
+ if (key && key.name === "c" && key.ctrl) {
26
+ cleanup(-1);
27
+ }
28
+});
29
+process.on("SIGINT", function () {
30
+ // Parent process may send SIGINT
31
+ cleanup(-1);
32
+});
33
+
34
+process.on("SIGTERM", function () {
35
+ cleanup(-1);
36
+});
37
28
-const argv: {
38
+type Results = Map<string, TestResult>;
39
+type RunnerOptions = {
40
sync: boolean;
41
workerThreads: boolean;
42
watch: boolean;
43
update: boolean;
33
-} = yargs
44
+};
45
+
46
+const opts: RunnerOptions = yargs
47
.boolean("sync")
48
.describe(
49
"sync",
69
.strict()
70
.parseSync(hideBin(process.argv));
71
59
-const PARALLEL = !argv.sync;
60
-const ENABLE_WORKER_THREADS = argv.workerThreads;
61
-const WATCH = argv.watch;
62
-const UPDATE = argv.update;
72
const WORKER_PATH = require.resolve("./compiler-worker.js");
73
const COMPILER_PATH = path.join(
74
process.cwd(),
84
"compiler"
85
);
86
78
-const worker: Worker & typeof compiler = new Worker(WORKER_PATH, {
79
- enableWorkerThreads: ENABLE_WORKER_THREADS,
80
-}) as any;
81
-worker.getStderr().pipe(process.stderr);
82
-worker.getStdout().pipe(process.stdout);
83
-
84
-type Results = Map<string, TestResult>;
87
+/**
88
+ * Cleanup / handle interrupts
89
+ */
90
+const cleanupTasks: Array<() => void> = new Array();
91
+function pushCleanupTask(fn: () => void) {
92
+ cleanupTasks.push(fn);
93
+}
94
+function cleanup(code: number) {
95
+ for (const task of cleanupTasks) {
96
+ task();
97
+ }
98
+ process.exit(code);
99
+}
100
+function clearConsole() {
101
+ // console.clear() only works when stdout is connected to a TTY device.
102
+ // we're currently piping stdout (see main.ts), so let's do a 'hack'
103
+ console.log("\u001Bc");
104
+}
105
106
/**
107
* Do a test run and return the test results
108
*/
89
-async function run(compilerVersion: number): Promise<Results> {
109
+async function run(
110
+ worker: Worker & typeof compiler,
111
+ opts: RunnerOptions,
112
+ compilerVersion: number
113
+): Promise<Results> {
114
// We could in theory be fancy about tracking the contents of the fixtures
115
// directory via our file subscription, but it's simpler to just re-read
116
// the directory each time.
124
).sort();
125
126
let entries: Array<[string, TestResult]>;
103
- if (PARALLEL) {
127
+ if (!opts.sync) {
128
// Note: promise.all to ensure parallelism when enabled
129
entries = await Promise.all(
130
fixtures.map(async (fixture) => {
312
/**
313
* Runs the compiler in watch or single-execution mode
314
*/
291
-async function main(): Promise<void> {
292
- if (WATCH) {
315
+export async function main(opts: RunnerOptions): Promise<void> {
316
+ const worker: Worker & typeof compiler = new Worker(WORKER_PATH, {
317
+ enableWorkerThreads: opts.workerThreads,
318
+ }) as any;
319
+ worker.getStderr().pipe(process.stderr);
320
+ worker.getStdout().pipe(process.stdout);
321
+ pushCleanupTask(() => {
322
+ worker.end();
323
+ });
324
+
325
+ if (opts.watch) {
326
// Monotonically increasing integer to describe the 'version' of the compiler.
327
// This is passed to `compile()` (from compiler-worker) when compiling, so
328
// that the worker knows when it has to reset its module cache and when its
339
async function onChange({ mode }: { mode: Mode }) {
340
if (isCompilerValid) {
341
const start = performance.now();
309
- console.clear();
342
+ clearConsole();
343
console.log("Running tests...");
311
- const results = await run(compilerVersion);
312
- console.clear();
344
+ const results = await run(worker, opts, compilerVersion);
345
+ clearConsole();
346
if (mode === Mode.Update) {
347
update(results);
348
} else {
364
}
365
366
// Run TS in incremental watch mode
334
- const _tsWatch = watchSrc(onStart, (isSuccess) => {
367
+ const tsWatch = watchSrc(onStart, (isSuccess) => {
368
// Bump the compiler version after a build finishes
369
// and re-run tests
370
if (isSuccess) {
373
isCompilerValid = isSuccess;
374
onChange({ mode: Mode.Test });
375
});
376
+ pushCleanupTask(() => {
377
+ tsWatch.close();
378
+ });
379
380
// Watch the fixtures directory for changes
381
// TODO: ignore changes that occurred as a result of our explicitly updating
382
// fixtures in update() - maybe keep a timestamp of last known changes, and
383
// ignore events that occurred prior to that timestamp.
348
- const _fileSubscription = watcher.subscribe(
384
+ const fileSubscription = watcher.subscribe(
385
FIXTURES_PATH,
386
async (err, _events) => {
387
if (err) {
393
}
394
);
395
396
+ pushCleanupTask(() => {
397
+ fileSubscription
398
+ .then((subscription) => {
399
+ subscription.unsubscribe();
400
+ })
401
+ .catch((err) => {
402
+ console.log("error cleaning up file subscription", err);
403
+ });
404
+ });
405
+
406
// Basic key event handling
407
process.stdin.on("keypress", (str, key) => {
408
if (key.name === "u") {
410
onChange({ mode: Mode.Update });
411
} else if (key.name === "q") {
412
process.exit(0);
367
- } else if (key.ctrl && key.name === "c") {
368
- process.exit(0);
413
} else {
414
// any other key re-runs tests
415
onChange({ mode: Mode.Test });
424
() => {},
425
async (isSuccess: boolean) => {
426
if (isSuccess) {
383
- const results = await run(0);
384
- if (UPDATE) {
427
+ const results = await run(worker, opts, 0);
428
+ if (opts.update) {
429
update(results);
430
} else {
431
report(results);
437
}
438
if (tsWatch != null) {
439
tsWatch.close();
440
+ tsWatch = null;
441
}
442
await worker.end();
443
process.exit(isSuccess ? 0 : -1);
444
}
445
);
446
+ pushCleanupTask(() => {
447
+ tsWatch?.close();
448
+ tsWatch = null;
449
+ });
450
}
451
}
452
453
// I couldn't figure out the right combination of settings to allow using `await` at the top-level,
454
// but it's easy enough to use the promise API just here
406
-main().catch((error) => console.error(error));
455
+main(opts).catch((error) => console.error(error));