@samitouri / QOS-React-2 / commits / fc13557d30

[tests] remove jest fixture tests

--- Remove jest fixture tests in favor of snap runner. Main reasons: - maintaining feature flags and compatible behavior required syncing all changes to 3 files (`generateTestsFromFixtures`, `compiler-test`, and `compiler-worker`) - jest snapshot test file causes rebase conflicts on most rebases - speed 🙌 $ time yarn test compiler-test (the extra test here is `has a consistent extension for input fixtures`) ``` Test Suites: 1 passed, 1 total Tests: 37 skipped, 480 passed, 517 total Snapshots: 479 passed, 479 total Time: 27.668 s Ran all test suites matching /compiler-test/i. ✨ Done in 43.18s. yarn test compiler-test 57.05s user 3.85s system 139% cpu 43.546 total ``` $ time yarn snap ``` 478 Tests, 478 Passed, 0 Failed ✨ Done in 13.12s. yarn snap 53.96s user 9.35s system 468% cpu 13.518 total ``` Jest and snap should have the same set of features: - report test failures via exit status (used by Git Actions) - watch mode - breakpoints + `debugger` statements - note that `--sync` is not required for this - skip `todo.` prefixed fixtures - fixtures in nested directories e.g. `rules-of-hooks/testname.js` - filter mode (via editing `testfilter.txt`) - filter + debug mode (1) edit `testfilter.txt` to filter out all but one test (2) add `@debug` pragma to the first line of the test testfilter.txt ```js // @only testfixture_basename1 testfixture_basename2 ```

Mofei Zhang committed May 26, 2023 at 13:18 UTC fc13557d30c50517f5f1d369ce2ee02f506bb904
2 files changed -430
compiler/forget/src/__tests__/compiler-test.ts deleted
-138
@@ -1,138 +0,0 @@
1 -/**
2 - * Copyright (c) Meta Platforms, Inc. and affiliates.
3 - *
4 - * This source code is licensed under the MIT license found in the
5 - * LICENSE file in the root directory of this source tree.
6 - */
7 -
8 -"use strict";
9 -
10 -import path from "path";
11 -import { runReactForgetBabelPlugin } from "../Babel/RunReactForgetBabelPlugin";
12 -import { toggleLogging } from "../Utils/logger";
13 -import { Effect, ValueKind } from "../index";
14 -import generateTestsFromFixtures from "./test-utils/generateTestsFromFixtures";
15 -
16 -type TestOutput = {
17 - js: string;
18 -};
19 -
20 -function wrapWithTripleBackticks(s: string, ext?: string) {
21 - return `\`\`\`${ext ?? ""}
22 -${s}
23 -\`\`\``;
24 -}
25 -
26 -describe("React Forget", () => {
27 - const originalConsoleError = console.error;
28 - generateTestsFromFixtures(
29 - path.join(__dirname, "fixtures", "compiler"),
30 - (input, file, options) => {
31 - const seenConsoleErrors: Array<string> = [];
32 - let items: Array<TestOutput> = [];
33 - let error: Error | null = null;
34 - if (options.debug) {
35 - toggleLogging(options.debug);
36 - }
37 - // Mock console.error so we can record it in test output
38 - console.error = jest.fn((...messages: Array<string>) => {
39 - seenConsoleErrors.push(...messages);
40 - });
41 - try {
42 - items.push({
43 - js: runReactForgetBabelPlugin(input, file, options.language, {
44 - enableOnlyOnUseForgetDirective:
45 - options.enableOnlyOnUseForgetDirective,
46 - environment: {
47 - memoizeJsxElements:
48 - options.environment?.memoizeJsxElements ?? true,
49 - customHooks: new Map([
50 - [
51 - "useFreeze",
52 - {
53 - name: "useFreeze",
54 - kind: "Custom",
55 - valueKind: ValueKind.Frozen,
56 - effectKind: Effect.Freeze,
57 - },
58 - ],
59 - ]),
60 - validateHooksUsage: true,
61 - inlineUseMemo: options.environment?.inlineUseMemo ?? false,
62 - enableFunctionCallSignatureOptimizations: true,
63 - enableAssumeHooksFollowRulesOfReact:
64 - options.environment?.enableAssumeHooksFollowRulesOfReact ??
65 - false,
66 - },
67 - logger: null,
68 - gating: options.gating,
69 - instrumentForget: options.instrumentForget,
70 - panicOnBailout: options.panicOnBailout,
71 - isDev: true,
72 - }).code,
73 - });
74 - } catch (e) {
75 - error = e;
76 - }
77 -
78 - // Promote console errors so they can be recorded in fixture output
79 - for (const consoleError of seenConsoleErrors) {
80 - if (error != null) {
81 - error.message = `${error.message}\n\n${consoleError}`;
82 - } else {
83 - error = new Error(consoleError);
84 - error.name = "ConsoleError";
85 - }
86 - }
87 -
88 - let outputs: Array<string>;
89 -
90 - const expectError = file.startsWith("error.");
91 - if (expectError) {
92 - if (error === null) {
93 - throw new Error(
94 - `Expected an error to be thrown for fixture: '${file}', remove the 'error.' prefix if an error is not expected.`
95 - );
96 - } else {
97 - outputs = [...formatOutput(items), formatErrorOutput(error)];
98 - }
99 - } else {
100 - if (error !== null) {
101 - error.message = `Expected fixture '${file}' to succeed but it failed with error:\n\n${error.message}`;
102 - throw error;
103 - }
104 - if (items === null || items.length === 0) {
105 - throw new Error(`Expected at least one output for file '${file}'.`);
106 - }
107 - outputs = formatOutput(items);
108 - }
109 - return `
110 -## Input
111 -
112 -${wrapWithTripleBackticks(input, "javascript")}
113 -
114 -${outputs.join("\n")}
115 - `;
116 - }
117 - );
118 - console.error = originalConsoleError;
119 -});
120 -
121 -function formatErrorOutput(error: Error): string {
122 - error.message = error.message.replace(/^\/.*?:\s/, "");
123 - return `
124 -## Error
125 -
126 -${wrapWithTripleBackticks(error.message)}
127 - `;
128 -}
129 -
130 -function formatOutput(items: Array<TestOutput>): Array<string> {
131 - return items.map(({ js }) => {
132 - return `
133 -## Code
134 -
135 -${wrapWithTripleBackticks(js, "javascript")}
136 - `.trim();
137 - });
138 -}
compiler/forget/src/__tests__/test-utils/generateTestsFromFixtures.ts deleted
-292
@@ -1,292 +0,0 @@
1 -/**
2 - * Copyright (c) Meta Platforms, Inc. and affiliates.
3 - *
4 - * This source code is licensed under the MIT license found in the
5 - * LICENSE file in the root directory of this source tree.
6 - */
7 -
8 -"use strict";
9 -
10 -/* global expect,test */
11 -
12 -import fs from "fs";
13 -import glob from "glob";
14 -import path from "path";
15 -import {
16 - ExternalFunction,
17 - InstrumentForgetOptions,
18 - PluginOptions,
19 -} from "../../Babel/PluginOptions";
20 -
21 -const EXPECT_SUFFIX = ".expect.md";
22 -
23 -const PROJECT_ROOT = path.dirname(path.dirname(__dirname));
24 -
25 -expect.extend({
26 - accessSnapshotState(_anything, cb) {
27 - cb(this["snapshotState"]._updateSnapshot);
28 - return {
29 - pass: true,
30 - message: () => "",
31 - };
32 - },
33 - toHaveNoUnmatchedSnapshots(unmatchedSnapshots, fixturesPath) {
34 - return {
35 - pass: unmatchedSnapshots.length === 0,
36 - message: () => {
37 - const unmatchedSnapshotsText = unmatchedSnapshots
38 - .map((file: string) => path.join(fixturesPath, file))
39 - .join("\n * ");
40 - return (
41 - `Found ${EXPECT_SUFFIX} files without corresponding inputs:\n* ${unmatchedSnapshotsText}` +
42 - `\n\nRun 'npm test -- -u' to remove these extra ${EXPECT_SUFFIX} files`
43 - );
44 - },
45 - };
46 - },
47 -});
48 -
49 -type FixtureTestOptions = {
50 - debug: boolean;
51 - language: "flow" | "typescript";
52 -};
53 -export default function generateTestsFromFixtures(
54 - fixturesPath: string,
55 - transform: (
56 - input: string,
57 - file: any,
58 - options: FixtureTestOptions & PluginOptions
59 - ) => string
60 -) {
61 - let files: Array<string>;
62 - try {
63 - files = glob.sync("**/*.{js,md}", {
64 - cwd: fixturesPath,
65 - });
66 - } catch (e) {
67 - if (e.code === "ENOENT") {
68 - files = [];
69 - } else {
70 - throw e;
71 - }
72 - }
73 - const fixtures = matchInputOutputFixtures(files, fixturesPath);
74 -
75 - const relativeFixturesPath = path.relative(PROJECT_ROOT, fixturesPath);
76 - describe(relativeFixturesPath, () => {
77 - test("has a consistent extension for input fixtures", () => {
78 - const extensions = Array.from(
79 - new Set(
80 - Array.from(fixtures.values())
81 - .map((entry) =>
82 - entry.input != null ? path.extname(entry.input) : null
83 - )
84 - .filter(Boolean)
85 - )
86 - );
87 - expect(extensions).toEqual(extensions.slice(0, 1));
88 - });
89 -
90 - describe("fixtures", () => {
91 - for (const {
92 - basename,
93 - input: inputFile,
94 - output: outputFile,
95 - } of Array.from(fixtures.values())) {
96 - let testCommand = test;
97 - let input: string | null = null;
98 - let debug = false;
99 - let enableOnlyOnUseForgetDirective = false;
100 - let gating: ExternalFunction | null = null;
101 - let instrumentForget: InstrumentForgetOptions | null = null;
102 - let inlineUseMemo = true;
103 - let panicOnBailout = true;
104 - let memoizeJsxElements = true;
105 - let enableAssumeHooksFollowRulesOfReact = false;
106 - let enableTreatHooksAsFunctions = true;
107 -
108 - if (inputFile != null) {
109 - input = fs.readFileSync(inputFile, "utf8");
110 - const lines = input.split("\n");
111 - if (lines[0]!.indexOf("@only") !== -1) {
112 - testCommand = test.only;
113 - }
114 - if (lines[0]!.indexOf("@skip") !== -1) {
115 - testCommand = test.skip;
116 - }
117 - if (lines[0]!.indexOf("@debug") !== -1) {
118 - debug = true;
119 - }
120 - if (lines[0]!.indexOf("@forgetDirective") !== -1) {
121 - enableOnlyOnUseForgetDirective = true;
122 - }
123 - if (lines[0]!.indexOf("@gating") !== -1) {
124 - gating = {
125 - source: "ReactForgetFeatureFlag",
126 - importSpecifierName: "isForgetEnabled_Fixtures",
127 - };
128 - }
129 - if (lines[0]!.indexOf("@instrumentForget") !== -1) {
130 - instrumentForget = {
131 - gating: {
132 - source: "ReactInstrumentForgetFeatureFlag",
133 - importSpecifierName: "isInstrumentForgetEnabled_Fixtures",
134 - },
135 - instrumentFn: {
136 - source: "react-forget-runtime",
137 - importSpecifierName: "useRenderCounter",
138 - },
139 - };
140 - }
141 - if (lines[0]!.indexOf("@inlineUseMemo false") !== -1) {
142 - inlineUseMemo = false;
143 - }
144 - if (lines[0]!.indexOf("@panicOnBailout false") !== -1) {
145 - panicOnBailout = false;
146 - }
147 - if (lines[0]!.indexOf("@memoizeJsxElements false") !== -1) {
148 - memoizeJsxElements = false;
149 - }
150 - if (
151 - lines[0]!.indexOf("@enableAssumeHooksFollowRulesOfReact true") !==
152 - -1
153 - ) {
154 - enableAssumeHooksFollowRulesOfReact = true;
155 - }
156 - if (lines[0]!.indexOf("@enableTreatHooksAsFunctions false") !== -1) {
157 - enableTreatHooksAsFunctions = false;
158 - }
159 - }
160 -
161 - testCommand(basename, () => {
162 - let receivedOutput;
163 - if (input !== null) {
164 - receivedOutput = transform(input, basename, {
165 - environment: {
166 - inlineUseMemo,
167 - memoizeJsxElements,
168 - enableAssumeHooksFollowRulesOfReact,
169 - enableTreatHooksAsFunctions,
170 - },
171 - logger: null,
172 - debug,
173 - enableOnlyOnUseForgetDirective,
174 - gating,
175 - instrumentForget,
176 - language: parseLanguage(input),
177 - panicOnBailout,
178 - isDev: true,
179 - });
180 - } else {
181 - receivedOutput = "<<input deleted>>";
182 - }
183 -
184 - // Use a standard snapshot for the expected output so that the snapshot fails unless the
185 - // value matches
186 - expect(receivedOutput).toMatchSnapshot();
187 -
188 - // Determine whether the snapshot is in update mode or only creating snapshots for new inputs
189 - // to update the .expect file in parallel with updating the snapshot itself.
190 - const snapshotUpdateMode = determineSnapshotMode();
191 - if (outputFile != null) {
192 - const outputExists = fs.existsSync(outputFile);
193 - if (
194 - snapshotUpdateMode === "all" ||
195 - (snapshotUpdateMode === "new" && !outputExists)
196 - ) {
197 - if (inputFile != null) {
198 - fs.writeFileSync(outputFile, receivedOutput, "utf8");
199 - } else {
200 - fs.unlinkSync(outputFile);
201 - }
202 - } else {
203 - // As a sanity check, make sure that the current output matches the .expect file
204 - const actualOutput = fs.readFileSync(outputFile, "utf8");
205 - expect(receivedOutput).toEqual(actualOutput);
206 - }
207 - }
208 - });
209 - }
210 - });
211 - });
212 -}
213 -
214 -const FlowPragmas = [/\/\/\s@flow$/gm, /\*\s@flow$/gm];
215 -function parseLanguage(source: string): "flow" | "typescript" {
216 - let useFlow: boolean = false;
217 - for (const flowPragma of FlowPragmas) {
218 - useFlow ||= !!source.match(flowPragma);
219 - }
220 - return useFlow ? "flow" : "typescript";
221 -}
222 -
223 -function determineSnapshotMode() {
224 - // Determine which snapshot mode we're in: ignoring snapshots,
225 - // updating new files only, or updating all files
226 - let updateSnapshots = "none";
227 - // @ts-ignore
228 - expect(null).accessSnapshotState((_updateSnapshots) => {
229 - updateSnapshots = _updateSnapshots;
230 - });
231 - const updateSnapshotEnvVariable = process.env["UPDATE_SNAPSHOTS"];
232 - if (
233 - updateSnapshotEnvVariable === "1" ||
234 - updateSnapshotEnvVariable === "all"
235 - ) {
236 - console.log(
237 - "Updating all snapshots due to UPDATE_SNAPSHOTS environment variable being set"
238 - );
239 - updateSnapshots = "all";
240 - } else {
241 - // @ts-ignore
242 - expect(updateSnapshotEnvVariable).toEqual();
243 - }
244 - expect(updateSnapshots).toEqual(expect.stringMatching(/none|new|all/));
245 - return updateSnapshots;
246 -}
247 -
248 -function matchInputOutputFixtures(files: string[], fixturesPath: string) {
249 - const fixtures: Map<
250 - string,
251 - { basename: string; input: string | null; output: string | null }
252 - > = new Map();
253 - for (const file of files) {
254 - const isOutput = file.endsWith(EXPECT_SUFFIX);
255 - const basename = path.basename(
256 - file,
257 - isOutput ? EXPECT_SUFFIX : path.extname(file)
258 - );
259 - let entry = fixtures.get(basename);
260 - if (entry === undefined) {
261 - entry = { basename, input: null, output: null };
262 - fixtures.set(basename, entry);
263 - }
264 - const resolvedPath = path.format({
265 - dir: fixturesPath,
266 - name: file,
267 - });
268 - if (isOutput) {
269 - entry.output = resolvedPath;
270 - } else {
271 - if (entry.input !== null) {
272 - throw new Error(
273 - "Found multiple inputs with the basename '" +
274 - basename +
275 - "': " +
276 - entry.input +
277 - " and " +
278 - resolvedPath
279 - );
280 - }
281 - entry.input = resolvedPath;
282 - const outputName = `${basename}${EXPECT_SUFFIX}`;
283 - const outputFile = path.join(
284 - fixturesPath,
285 - path.dirname(file),
286 - outputName
287 - );
288 - entry.output = outputFile;
289 - }
290 - }
291 - return fixtures;
292 -}