5
* LICENSE file in the root directory of this source tree.
6
*/
7
8
-import watcher from "@parcel/watcher";
9
-import {
10
- COMPILER_PATH,
11
- FILTER_FILENAME,
12
- FILTER_PATH,
13
- FIXTURES_PATH,
14
- LOGGER_PATH,
15
- PARSE_CONFIG_PRAGMA_PATH,
16
- TestFilter,
17
- TestResult,
18
- TestResults,
19
- getFixtures,
20
- readTestFilter,
21
- report,
22
- update,
23
-} from "fixture-test-utils";
8
import { Worker } from "jest-worker";
25
-import path from "path";
9
import process from "process";
10
import * as readline from "readline";
11
import ts from "typescript";
12
import yargs from "yargs";
13
import { hideBin } from "yargs/helpers";
31
-import * as compiler from "./compiler-worker";
14
+import { FILTER_PATH } from "./constants";
15
+import { TestFilter, getFixtures, readTestFilter } from "./fixture-utils";
16
+import { TestResult, TestResults, report, update } from "./reporter";
17
+import {
18
+ RunnerAction,
19
+ RunnerState,
20
+ makeWatchRunner,
21
+ watchSrc,
22
+} from "./runner-watch";
23
+import * as runnerWorker from "./runner-worker";
24
33
-const WORKER_PATH = require.resolve("./compiler-worker.js");
25
+const WORKER_PATH = require.resolve("./runner-worker.js");
26
27
readline.emitKeypressEvents(process.stdin);
28
37
-process.stdin.on("keypress", function (chunk, key) {
38
- if (key && key.name === "c" && key.ctrl) {
39
- cleanup(-1);
40
- }
41
-});
42
-process.on("SIGINT", function () {
43
- // Parent process may send SIGINT
44
- cleanup(-1);
45
-});
46
-
47
-process.on("SIGTERM", function () {
48
- cleanup(-1);
49
-});
50
-
29
type RunnerOptions = {
30
sync: boolean;
31
workerThreads: boolean;
63
.strict()
64
.parseSync(hideBin(process.argv));
65
88
-/**
89
- * Cleanup / handle interrupts
90
- */
91
-const cleanupTasks: Array<() => void> = new Array();
92
-function pushCleanupTask(fn: () => void) {
93
- cleanupTasks.push(fn);
94
-}
95
-function cleanup(code: number) {
96
- for (const task of cleanupTasks) {
97
- task();
98
- }
99
- process.exit(code);
100
-}
101
-function clearConsole() {
102
- // console.clear() only works when stdout is connected to a TTY device.
103
- // we're currently piping stdout (see main.ts), so let's do a 'hack'
104
- console.log("\u001Bc");
105
-}
106
-
66
/**
67
* Do a test run and return the test results
68
*/
110
-async function run(
111
- worker: Worker & typeof compiler,
112
- opts: RunnerOptions,
69
+async function runFixtures(
70
+ worker: Worker & typeof runnerWorker,
71
filter: TestFilter | null,
72
compilerVersion: number
73
): Promise<TestResults> {
84
for (const [fixtureName, fixture] of fixtures) {
85
work.push(
86
worker
129
- .compile(
130
- COMPILER_PATH,
131
- LOGGER_PATH,
132
- PARSE_CONFIG_PRAGMA_PATH,
87
+ .transformFixture(
88
fixture,
89
compilerVersion,
135
- filter?.debug ?? false,
136
- isOnlyFixture
90
+ (filter?.debug ?? false) && isOnlyFixture,
91
+ true
92
)
93
.then((result) => [fixtureName, result])
94
);
98
} else {
99
entries = [];
100
for (const [fixtureName, fixture] of fixtures) {
146
- let output = await compiler.compile(
147
- COMPILER_PATH,
148
- LOGGER_PATH,
149
- PARSE_CONFIG_PRAGMA_PATH,
101
+ let output = await runnerWorker.transformFixture(
102
fixture,
103
compilerVersion,
152
- filter?.debug ?? false,
153
- isOnlyFixture
104
+ (filter?.debug ?? false) && isOnlyFixture,
105
+ true
106
);
107
entries.push([fixtureName, output]);
108
}
111
return new Map(entries);
112
}
113
162
-function watchSrc(
163
- onStart: () => void,
164
- onComplete: (isSuccess: boolean) => void
165
-): ts.WatchOfConfigFile<ts.SemanticDiagnosticsBuilderProgram> {
166
- const configPath = ts.findConfigFile(
167
- /*searchPath*/ "./",
168
- ts.sys.fileExists,
169
- "tsconfig.json"
170
- );
171
- if (!configPath) {
172
- throw new Error("Could not find a valid 'tsconfig.json'.");
173
- }
174
- const createProgram = ts.createSemanticDiagnosticsBuilderProgram;
175
- const host = ts.createWatchCompilerHost(
176
- configPath,
177
- {},
178
- ts.sys,
179
- createProgram,
180
- () => {}, // we manually report errors in afterProgramCreate
181
- () => {} // we manually report watch status
182
- );
183
-
184
- const origCreateProgram = host.createProgram;
185
- host.createProgram = (rootNames, options, host, oldProgram) => {
186
- onStart();
187
- return origCreateProgram(rootNames, options, host, oldProgram);
188
- };
189
- const origPostProgramCreate = host.afterProgramCreate;
190
- host.afterProgramCreate = (program) => {
191
- origPostProgramCreate!(program);
192
-
193
- // syntactic diagnostics refer to javascript syntax
194
- const errors = program
195
- .getSyntacticDiagnostics()
196
- .filter((diag) => diag.category === ts.DiagnosticCategory.Error);
197
- // semantic diagnostics refer to typescript semantics
198
- errors.push(
199
- ...program
200
- .getSemanticDiagnostics()
201
- .filter((diag) => diag.category === ts.DiagnosticCategory.Error)
114
+// Callback to re-run tests after some change
115
+async function onChange(
116
+ worker: Worker & typeof runnerWorker,
117
+ state: RunnerState
118
+) {
119
+ const { compilerVersion, isCompilerBuildValid, mode, filter } = state;
120
+ if (isCompilerBuildValid) {
121
+ const start = performance.now();
122
+
123
+ // console.clear() only works when stdout is connected to a TTY device.
124
+ // we're currently piping stdout (see main.ts), so let's do a 'hack'
125
+ console.log("\u001Bc");
126
+
127
+ // we don't clear console after this point, since
128
+ // it may contain debug console logging
129
+ const results = await runFixtures(
130
+ worker,
131
+ mode.filter ? filter : null,
132
+ compilerVersion
133
);
203
-
204
- if (errors.length > 0) {
205
- for (const diagnostic of errors) {
206
- let fileLoc: string;
207
- if (diagnostic.file) {
208
- // https://github.com/microsoft/TypeScript/blob/ddd5084659c423f4003d2176e12d879b6a5bcf30/src/compiler/program.ts#L663-L674
209
- const { line, character } = ts.getLineAndCharacterOfPosition(
210
- diagnostic.file,
211
- diagnostic.start!
212
- );
213
- const fileName = path.relative(
214
- ts.sys.getCurrentDirectory(),
215
- diagnostic.file.fileName
216
- );
217
- fileLoc = `${fileName}:${line + 1}:${character + 1} - `;
218
- } else {
219
- fileLoc = "";
220
- }
221
- console.error(
222
- `${fileLoc}error TS${diagnostic.code}:`,
223
- ts.flattenDiagnosticMessageText(diagnostic.messageText, "\n")
224
- );
225
- }
226
- console.error(
227
- `Compilation failed (${errors.length} ${
228
- errors.length > 1 ? "errors" : "error"
229
- }).\n`
230
- );
134
+ const end = performance.now();
135
+ if (mode.action === RunnerAction.Update) {
136
+ update(results);
137
+ state.lastUpdate = end;
138
+ } else {
139
+ report(results);
140
}
232
-
233
- const isSuccess = errors.length === 0;
234
- onComplete(isSuccess);
235
- };
236
-
237
- // `createWatchProgram` creates an initial program, watches files, and updates
238
- // the program over time.
239
- return ts.createWatchProgram(host);
240
-}
241
-
242
-enum Mode {
243
- Test = "Test",
244
- Update = "Update",
141
+ console.log(`Completed in ${Math.floor(end - start)} ms`);
142
+ } else {
143
+ console.error(
144
+ `${mode}: Found errors in Forget source code, skipping test fixtures.`
145
+ );
146
+ }
147
+ console.log(
148
+ "\n" +
149
+ (mode.filter
150
+ ? `Current mode = FILTER, filter test fixtures by "${FILTER_PATH}".`
151
+ : "Current mode = NORMAL, run all test fixtures.") +
152
+ "\nWaiting for input or file changes...\n" +
153
+ "u - update all fixtures\n" +
154
+ `f - toggle (turn ${mode.filter ? "off" : "on"}) filter mode\n` +
155
+ "q - quit\n" +
156
+ "[any] - rerun tests\n"
157
+ );
158
}
159
160
/**
161
* Runs the compiler in watch or single-execution mode
162
*/
163
export async function main(opts: RunnerOptions): Promise<void> {
251
- const worker: Worker & typeof compiler = new Worker(WORKER_PATH, {
164
+ const worker: Worker & typeof runnerWorker = new Worker(WORKER_PATH, {
165
enableWorkerThreads: opts.workerThreads,
166
}) as any;
167
worker.getStderr().pipe(process.stderr);
168
worker.getStdout().pipe(process.stdout);
256
- pushCleanupTask(() => {
257
- worker.end();
258
- });
169
170
if (opts.watch) {
261
- // Monotonically increasing integer to describe the 'version' of the compiler.
262
- // This is passed to `compile()` (from compiler-worker) when compiling, so
263
- // that the worker knows when it has to reset its module cache and when its
264
- // safe to use a cached compiler version
265
- let compilerVersion = 0;
266
- let isCompilerValid = false;
267
- let lastUpdate = -1;
268
- let filterMode: boolean = opts.filter;
269
- let testFilter: TestFilter | null;
270
-
271
- function isRealUpdate(): boolean {
272
- // Try to ignore changes that occurred as a result of our explicitly updating
273
- // fixtures in update().
274
- // Currently keeps a timestamp of last known changes, and ignore events that occurred
275
- // around that timestamp.
276
- return performance.now() - lastUpdate > 5000;
277
- }
278
-
279
- function onStart() {
280
- // Notify the user when compilation starts but don't clear the screen yet
281
- console.log("\nCompiling...");
282
- }
283
-
284
- // Callback to re-run tests after some change
285
- async function onChange({ mode }: { mode: Mode }) {
286
- if (isCompilerValid) {
287
- const start = performance.now();
288
- clearConsole();
289
- console.log("Running tests...");
290
- // we don't clear console after this point, since
291
- // it may contain debug console logging
292
- const results = await run(
293
- worker,
294
- opts,
295
- filterMode ? await readTestFilter() : null,
296
- compilerVersion
297
- );
298
- if (mode === Mode.Update) {
299
- update(results);
300
- } else {
301
- report(results);
302
- }
303
- const end = performance.now();
304
- if (mode === Mode.Update) {
305
- lastUpdate = end;
306
- }
307
- console.log(`Completed in ${Math.floor(end - start)} ms`);
308
- } else {
309
- console.error(
310
- `${mode}: Found errors in Forget source code, skipping test fixtures.`
311
- );
312
- }
313
- console.log(
314
- "\n" +
315
- (filterMode
316
- ? `Current mode = FILTER, filter test fixtures by "${FILTER_PATH}".`
317
- : "Current mode = NORMAL, run all test fixtures.") +
318
- "\nWaiting for input or file changes...\n" +
319
- "u - update all fixtures\n" +
320
- `f - toggle (turn ${filterMode ? "off" : "on"}) filter mode\n` +
321
- "q - quit\n" +
322
- "[any] - rerun tests\n"
323
- );
324
- }
325
-
326
- // Run TS in incremental watch mode
327
- const tsWatch = watchSrc(onStart, (isSuccess) => {
328
- // Bump the compiler version after a build finishes
329
- // and re-run tests
330
- if (isSuccess) {
331
- compilerVersion++;
332
- }
333
- isCompilerValid = isSuccess;
334
- onChange({ mode: Mode.Test });
335
- });
336
- pushCleanupTask(() => {
337
- tsWatch.close();
338
- });
339
-
340
- // Watch the fixtures directory for changes
341
- const fileSubscription = watcher.subscribe(
342
- FIXTURES_PATH,
343
- async (err, _events) => {
344
- if (err) {
345
- console.error(err);
346
- process.exit(1);
347
- }
348
- if (isRealUpdate()) {
349
- // Fixtures changed, re-run tests
350
- onChange({ mode: Mode.Test });
351
- }
352
- }
353
- );
354
-
355
- pushCleanupTask(() => {
356
- fileSubscription
357
- .then((subscription) => {
358
- subscription.unsubscribe();
359
- })
360
- .catch((err) => {
361
- console.log("error cleaning up file subscription", err);
362
- });
363
- });
364
-
365
- const filterSubscription = watcher.subscribe(
366
- process.cwd(),
367
- async (err, events) => {
368
- if (err) {
369
- console.error(err);
370
- process.exit(1);
371
- } else if (
372
- events.findIndex((event) => event.path.includes(FILTER_FILENAME)) !==
373
- -1
374
- ) {
375
- if (filterMode) {
376
- testFilter = await readTestFilter();
377
- onChange({ mode: Mode.Test });
378
- }
379
- }
380
- }
381
- );
382
- pushCleanupTask(() => {
383
- filterSubscription
384
- .then((subscription) => {
385
- subscription.unsubscribe();
386
- })
387
- .catch((err) => {
388
- console.log("error cleaning up filter subscription", err);
389
- });
390
- });
391
-
392
- // Basic key event handling
393
- process.stdin.on("keypress", (str, key) => {
394
- if (key.name === "u") {
395
- // u => update fixtures
396
- onChange({ mode: Mode.Update });
397
- } else if (key.name === "q") {
398
- process.exit(0);
399
- } else if (key.name === "f") {
400
- filterMode = !filterMode;
401
- onChange({ mode: Mode.Test });
402
- } else {
403
- // any other key re-runs tests
404
- onChange({ mode: Mode.Test });
405
- }
406
- });
171
+ makeWatchRunner((state) => onChange(worker, state), opts.filter);
172
} else {
173
// Non-watch mode. For simplicity we re-use the same watchSrc() function.
174
// After the first build completes run tests and exit
410
- let tsWatch: ts.WatchOfConfigFile<ts.SemanticDiagnosticsBuilderProgram> | null =
411
- null;
412
- tsWatch = watchSrc(
413
- () => {},
414
- async (compileSuccess: boolean) => {
415
- let isSuccess = compileSuccess;
416
- if (compileSuccess) {
417
- const testFilter = opts.filter ? await readTestFilter() : null;
418
- const results = await run(worker, opts, testFilter, 0);
419
- if (opts.update) {
420
- update(results);
175
+ const tsWatch: ts.WatchOfConfigFile<ts.SemanticDiagnosticsBuilderProgram> =
176
+ watchSrc(
177
+ () => {},
178
+ async (compileSuccess: boolean) => {
179
+ let isSuccess = compileSuccess;
180
+ if (compileSuccess) {
181
+ const testFilter = opts.filter ? await readTestFilter() : null;
182
+ const results = await runFixtures(worker, testFilter, 0);
183
+ if (opts.update) {
184
+ update(results);
185
+ } else {
186
+ const testSuccess = report(results);
187
+ isSuccess &&= testSuccess;
188
+ }
189
} else {
422
- const testSuccess = report(results);
423
- isSuccess &&= testSuccess;
190
+ console.error(
191
+ "Found errors in Forget source code, skipping test fixtures."
192
+ );
193
}
425
- } else {
426
- console.error(
427
- "Found errors in Forget source code, skipping test fixtures."
428
- );
429
- }
430
- if (tsWatch != null) {
194
tsWatch.close();
432
- tsWatch = null;
195
+ await worker.end();
196
+ process.exit(isSuccess ? 0 : 1);
197
}
434
- await worker.end();
435
- process.exit(isSuccess ? 0 : 1);
436
- }
437
- );
438
- pushCleanupTask(() => {
439
- tsWatch?.close();
440
- tsWatch = null;
441
- });
198
+ );
199
}
200
}
201
445
-// I couldn't figure out the right combination of settings to allow using `await` at the top-level,
446
-// but it's easy enough to use the promise API just here
202
main(opts).catch((error) => console.error(error));