8
import watcher from "@parcel/watcher";
9
import chalk from "chalk";
10
import fs from "fs/promises";
11
+import invariant from "invariant";
12
import { diff } from "jest-diff";
13
import { Worker } from "jest-worker";
14
import path from "path";
19
import { hideBin } from "yargs/helpers";
20
import { TestResult } from "./compiler-worker";
21
import * as compiler from "./compiler-worker.js";
22
+import { exists } from "./utils";
23
+
24
+const WORKER_PATH = require.resolve("./compiler-worker.js");
25
+const COMPILER_PATH = path.join(
26
+ process.cwd(),
27
+ "dist",
28
+ "Babel",
29
+ "RunReactForgetBabelPlugin.js"
30
+);
31
+const FIXTURES_PATH = path.join(
32
+ process.cwd(),
33
+ "src",
34
+ "__tests__",
35
+ "fixtures",
36
+ "compiler"
37
+);
38
+const FILTER_FILENAME = "testfilter.txt";
39
+const FILTER_PATH = path.join(process.cwd(), FILTER_FILENAME);
40
41
readline.emitKeypressEvents(process.stdin);
42
58
type RunnerOptions = {
59
sync: boolean;
60
workerThreads: boolean;
42
- watch: boolean;
43
- update: boolean;
61
+ mode: "watch" | "update" | "filter" | null;
62
};
63
64
const opts: RunnerOptions = yargs
74
"Run compiler in worker threads (instead of subprocesses). Defaults to true."
75
)
76
.default("worker-threads", true)
59
- .boolean("watch")
60
- .describe("watch", "Run in watch mode. Defaults to false (single run).")
61
- .default("watch", false)
62
- .boolean("update") // Test mode by default, opt-in to update
77
.describe(
64
- "update",
65
- "Run in update mode. Update mode only affects the first run, subsequent runs (in watch mode) require typing `u` to update. Defaults to false."
78
+ "mode",
79
+ "Snap tester modes:\n" +
80
+ " [default] - test all test fixtures\n" +
81
+ ` filter - test filtered fixtures ("${FILTER_FILENAME}")\n` +
82
+ " update - update all test fixtures)\n" +
83
+ " watch - watch for changes"
84
)
67
- .default("update", false)
85
+ .choices("mode", ["watch", "update", "filter", null])
86
+ .default("mode", null)
87
.help("help")
88
.strict()
89
.parseSync(hideBin(process.argv));
90
72
-const WORKER_PATH = require.resolve("./compiler-worker.js");
73
-const COMPILER_PATH = path.join(
74
- process.cwd(),
75
- "dist",
76
- "Babel",
77
- "RunReactForgetBabelPlugin.js"
78
-);
79
-const FIXTURES_PATH = path.join(
80
- process.cwd(),
81
- "src",
82
- "__tests__",
83
- "fixtures",
84
- "compiler"
85
-);
86
-
91
/**
92
* Cleanup / handle interrupts
93
*/
113
async function run(
114
worker: Worker & typeof compiler,
115
opts: RunnerOptions,
116
+ filter: TestFilter | null,
117
compilerVersion: number
118
): Promise<Results> {
119
// We could in theory be fancy about tracking the contents of the fixtures
120
// directory via our file subscription, but it's simpler to just re-read
121
// the directory each time.
122
const files = await fs.readdir(FIXTURES_PATH);
118
- const fixtures = Array.from(
123
+ const allFixtures = Array.from(
124
new Set(
125
files.map((file) => {
126
return path.basename(path.basename(file, ".js"), ".expect.md");
128
)
129
).sort();
130
131
+ let fixtures;
132
+ if (filter) {
133
+ if (filter.kind === "only") {
134
+ fixtures = allFixtures.filter(
135
+ (name) => filter.paths.indexOf(name) !== -1
136
+ );
137
+ } else if (filter.kind === "skip") {
138
+ fixtures = allFixtures.filter(
139
+ (name) => filter.paths.indexOf(name) === -1
140
+ );
141
+ } else {
142
+ invariant(false, "Internal snap error.");
143
+ }
144
+ } else {
145
+ fixtures = allFixtures;
146
+ }
147
+
148
let entries: Array<[string, TestResult]>;
149
if (!opts.sync) {
150
// Note: promise.all to ensure parallelism when enabled
331
Update = "Update",
332
}
333
334
+type TestFilter =
335
+ | {
336
+ kind: "only";
337
+ paths: Array<string>;
338
+ }
339
+ | {
340
+ kind: "skip";
341
+ paths: Array<string>;
342
+ };
343
+
344
+async function readTestFilter(): Promise<TestFilter | null> {
345
+ const input = (await exists(FILTER_PATH))
346
+ ? await fs.readFile(FILTER_PATH, "utf8")
347
+ : null;
348
+ if (input === null) {
349
+ return null;
350
+ }
351
+
352
+ const lines = input.trim().split("\n");
353
+ if (lines.length < 2) {
354
+ console.warn("Misformed filter file. Expected at least two lines.");
355
+ return null;
356
+ }
357
+
358
+ let filter: "only" | "skip" | null = null;
359
+ if (lines[0]!.indexOf("@only") !== -1) {
360
+ filter = "only";
361
+ }
362
+ if (lines[0]!.indexOf("@skip") !== -1) {
363
+ filter = "skip";
364
+ }
365
+ if (filter === null) {
366
+ console.warn(
367
+ "Misformed filter file. Expected first line to contain @only or @skip"
368
+ );
369
+ return null;
370
+ }
371
+ lines.shift();
372
+ return {
373
+ kind: filter,
374
+ paths: lines,
375
+ };
376
+}
377
+
378
/**
379
* Runs the compiler in watch or single-execution mode
380
*/
388
worker.end();
389
});
390
325
- if (opts.watch) {
391
+ if (opts.mode === "watch") {
392
// Monotonically increasing integer to describe the 'version' of the compiler.
393
// This is passed to `compile()` (from compiler-worker) when compiling, so
394
// that the worker knows when it has to reset its module cache and when its
396
let compilerVersion = 0;
397
let isCompilerValid = false;
398
let lastUpdate = -1;
399
+ let filterMode: boolean = false;
400
+ let testFilter: TestFilter | null = await readTestFilter();
401
402
function isRealUpdate(): boolean {
403
// Try to ignore changes that occurred as a result of our explicitly updating
418
const start = performance.now();
419
clearConsole();
420
console.log("Running tests...");
353
- const results = await run(worker, opts, compilerVersion);
421
+ const results = await run(
422
+ worker,
423
+ opts,
424
+ filterMode ? testFilter : null,
425
+ compilerVersion
426
+ );
427
clearConsole();
428
if (mode === Mode.Update) {
429
update(results);
441
);
442
}
443
console.log(
371
- "\nWaiting for input or file changes...\n" +
372
- "u - update fixtures\n" +
444
+ "\n" +
445
+ (filterMode
446
+ ? `Current mode = FILTER, filter test fixtures by "${FILTER_FILENAME}".`
447
+ : "Current mode = NORMAL, run all test fixtures.") +
448
+ "\nWaiting for input or file changes...\n" +
449
+ "u - update all fixtures\n" +
450
+ `f - toggle (turn ${filterMode ? "off" : "on"}) filter mode\n` +
451
"q - quit\n" +
452
"[any] - rerun tests\n"
453
);
492
});
493
});
494
495
+ const filterSubscription = watcher.subscribe(
496
+ process.cwd(),
497
+ async (err, events) => {
498
+ if (err) {
499
+ console.error(err);
500
+ process.exit(1);
501
+ } else if (
502
+ events.findIndex((event) => event.path.includes(FILTER_FILENAME)) !==
503
+ -1
504
+ ) {
505
+ testFilter = await readTestFilter();
506
+ if (filterMode) {
507
+ onChange({ mode: Mode.Test });
508
+ }
509
+ }
510
+ }
511
+ );
512
+ pushCleanupTask(() => {
513
+ filterSubscription
514
+ .then((subscription) => {
515
+ subscription.unsubscribe();
516
+ })
517
+ .catch((err) => {
518
+ console.log("error cleaning up filter subscription", err);
519
+ });
520
+ });
521
+
522
// Basic key event handling
523
process.stdin.on("keypress", (str, key) => {
524
if (key.name === "u") {
526
onChange({ mode: Mode.Update });
527
} else if (key.name === "q") {
528
process.exit(0);
529
+ } else if (key.name === "f") {
530
+ filterMode = !filterMode;
531
+ onChange({ mode: Mode.Test });
532
} else {
533
// any other key re-runs tests
534
onChange({ mode: Mode.Test });
543
() => {},
544
async (isSuccess: boolean) => {
545
if (isSuccess) {
438
- const results = await run(worker, opts, 0);
439
- if (opts.update) {
546
+ const testFilter =
547
+ opts.mode === "filter" ? await readTestFilter() : null;
548
+ const results = await run(worker, opts, testFilter, 0);
549
+ if (opts.mode === "update") {
550
update(results);
551
} else {
552
report(results);