@samitouri / QOS-React-2 / commits / 5ed297fa74

[snap tester] Support @only / @skip with special file

--- Changes: - Added `testfilter.txt` ``` // @only call capture-param-mutate jsx-spread ``` or ``` // @skip call error.todo-kitchensink ``` - grouped all commands under `--mode` ```js // runs all tests yarn snap // runs all tests and updates fixtures yarn snap --mode update // runs only tests that pass `testfilter.txt` yarn snap --mode filter // run in watch mode yarn snap --mode watch ``` - in watch mode, toggle between running all tests or filtered tests ``` 386 Tests, 386 Passed, 0 Failed Completed in 4994 ms Current mode = NORMAL, run all test fixtures. Waiting for input or file changes... u - update all fixtures f - toggle (turn on) filter mode q - quit [any] - rerun tests > f PASS call PASS capture_mutate-across-fns PASS timers 3 Tests, 3 Passed, 0 Failed Completed in 39 ms Current mode = FILTER, filter test fixtures by "testfilter.txt" Waiting for input or file changes... u - update all fixtures f - toggle (turn off) filter mode q - quit [any] - rerun tests ``` --- - `runner.ts` is pretty large now, happy to split it up into multiple files - I'd also like to refactor `watch` to make its shared state and control flow explicit

Mofei Zhang committed Apr 25, 2023 at 14:36 UTC 5ed297fa74fb75d4b25d8b3e9dfa0608c23872aa
2 files changed +142 -31
compiler/forget/.gitignore
+1
@@ -6,3 +6,4 @@ package-lock.json
6 .vscode
7 !packages/playground/.vscode
8 .spr.yml
9 +testfilter.txt
compiler/forget/packages/snap/src/runner.ts
+141 -31
@@ -8,6 +8,7 @@
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";
@@ -18,6 +19,24 @@ import yargs from "yargs";
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
@@ -39,8 +58,7 @@ type Results = Map<string, TestResult>;
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
@@ -56,34 +74,20 @@ 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 */
@@ -109,13 +113,14 @@ function clearConsole() {
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");
@@ -123,6 +128,23 @@ async function run(
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
@@ -309,6 +331,50 @@ enum Mode {
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 */
@@ -322,7 +388,7 @@ export async function main(opts: RunnerOptions): Promise<void> {
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
@@ -330,6 +396,8 @@ export async function main(opts: RunnerOptions): Promise<void> {
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
@@ -350,7 +418,12 @@ export async function main(opts: RunnerOptions): Promise<void> {
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);
@@ -368,8 +441,13 @@ export async function main(opts: RunnerOptions): Promise<void> {
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 );
@@ -414,6 +492,33 @@ export async function main(opts: RunnerOptions): Promise<void> {
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") {
@@ -421,6 +526,9 @@ export async function main(opts: RunnerOptions): Promise<void> {
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 });
@@ -435,8 +543,10 @@ export async function main(opts: RunnerOptions): Promise<void> {
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);