@samitouri / QOS-React-2 / commits / 8b38651986

[snap] Add jest test capabilities to snap

- delete output files when we detect input files are deleted - enable test fixtures in nested directories - exit with error code when we detect failures Note that the test failure on this PR is expected and will be fixed by #1608 (or happy to abandon that PR and fold the changes)

Mofei Zhang committed May 17, 2023 at 14:39 UTC 8b3865198658e791c3675c5356ec14bcc4a0d826
2 files changed +109 -63
compiler/forget/packages/snap/src/compiler-worker.ts
+13 -10
@@ -6,7 +6,6 @@
6 */
7
8 import fs from "fs/promises";
9 -import path from "path";
9 import { exists } from "./utils";
10
11 const originalConsoleError = console.error;
@@ -32,11 +31,16 @@ export type TestResult = {
31 unexpectedError: string | null;
32 };
33
34 +export type TestFixture = {
35 + basename: string;
36 + inputPath: string;
37 + outputPath: string;
38 +};
39 +
40 export async function compile(
41 compilerPath: string,
42 loggerPath: string,
38 - fixturesDir: string,
39 - fixture: string,
43 + fixture: TestFixture,
44 compilerVersion: number,
45 isOnlyFixture: boolean
46 ): Promise<TestResult> {
@@ -48,11 +52,10 @@ export async function compile(
52 clearRequireCache();
53 }
54 version = compilerVersion;
51 - const inputPath = path.join(fixturesDir, `${fixture}.js`);
55 + const { inputPath, outputPath, basename } = fixture;
56 const input = (await exists(inputPath))
57 ? await fs.readFile(inputPath, "utf8")
58 : null;
55 - const outputPath = path.join(fixturesDir, `${fixture}.expect.md`);
59 const expected = (await exists(outputPath))
60 ? await fs.readFile(outputPath, "utf8")
61 : null;
@@ -119,7 +122,7 @@ export async function compile(
122
123 const language = parseLanguage(firstLine);
124
122 - code = runReactForgetBabelPlugin(input, fixture, language, {
125 + code = runReactForgetBabelPlugin(input, basename, language, {
126 enableOnlyOnUseForgetDirective,
127 environment: {
128 customHooks: new Map([
@@ -157,7 +160,7 @@ export async function compile(
160 }
161
162 let output: string;
160 - const expectError = fixture.startsWith("error.");
163 + const expectError = basename.startsWith("error.");
164 if (expectError) {
165 if (error === null) {
166 return {
@@ -165,7 +168,7 @@ export async function compile(
168 outputPath,
169 actual: code,
170 expected,
168 - unexpectedError: `Expected an error to be thrown for fixture: '${fixture}', remove the 'error.' prefix if an error is not expected.`,
171 + unexpectedError: `Expected an error to be thrown for fixture: '${basename}', remove the 'error.' prefix if an error is not expected.`,
172 };
173 } else if (code != null) {
174 output = `${formatOutput(code)}\n${formatErrorOutput(error)}`;
@@ -179,7 +182,7 @@ export async function compile(
182 outputPath,
183 actual: code,
184 expected,
182 - unexpectedError: `Expected fixture '${fixture}' to succeed but it failed with error:\n\n${error.message}`,
185 + unexpectedError: `Expected fixture '${basename}' to succeed but it failed with error:\n\n${error.message}`,
186 };
187 }
188 if (code == null || code.length === 0) {
@@ -188,7 +191,7 @@ export async function compile(
191 outputPath,
192 actual: code,
193 expected,
191 - unexpectedError: `Expected output for fixture '${fixture}'.`,
194 + unexpectedError: `Expected output for fixture '${basename}'.`,
195 };
196 }
197 output = formatOutput(code);
compiler/forget/packages/snap/src/runner.ts
+96 -53
@@ -8,6 +8,7 @@
8 import watcher from "@parcel/watcher";
9 import chalk from "chalk";
10 import fs from "fs/promises";
11 +import glob from "glob";
12 import invariant from "invariant";
13 import { diff } from "jest-diff";
14 import { Worker } from "jest-worker";
@@ -108,6 +109,49 @@ function clearConsole() {
109 console.log("\u001Bc");
110 }
111
112 +function getFixtures(
113 + filter: TestFilter | null
114 +): Map<string, compiler.TestFixture> {
115 + // search for fixtures within nested directories
116 + const files = glob.sync(`**/*.{js,md}`, {
117 + cwd: FIXTURES_PATH,
118 + });
119 + const fixtures = new Map();
120 +
121 + for (const filePath of files) {
122 + const basename = path.basename(
123 + path.basename(filePath, ".js"),
124 + ".expect.md"
125 + );
126 + // "partial" paths do not include suffixes
127 + const partialRelativePath = path.join(path.dirname(filePath), basename);
128 + const partialAbsolutePath = path.join(FIXTURES_PATH, partialRelativePath);
129 + // Replicate jest test behavior
130 + if (basename.startsWith("todo.")) {
131 + continue;
132 + }
133 + if (filter) {
134 + if (
135 + filter.kind === "only" &&
136 + filter.paths.indexOf(partialRelativePath) === -1
137 + ) {
138 + continue;
139 + } else if (
140 + filter.kind === "skip" &&
141 + filter.paths.indexOf(partialRelativePath) !== -1
142 + ) {
143 + continue;
144 + }
145 + }
146 + fixtures.set(partialRelativePath, {
147 + basename,
148 + inputPath: `${partialAbsolutePath}.js`,
149 + outputPath: `${partialAbsolutePath}.expect.md`,
150 + });
151 + }
152 + // console.log("fixtures!", JSON.stringify(Array.from(fixtures.keys())));
153 + return fixtures;
154 +}
155 /**
156 * Do a test run and return the test results
157 */
@@ -120,61 +164,39 @@ async function run(
164 // We could in theory be fancy about tracking the contents of the fixtures
165 // directory via our file subscription, but it's simpler to just re-read
166 // the directory each time.
123 - const files = await fs.readdir(FIXTURES_PATH);
124 - const allFixtures = Array.from(
125 - new Set(
126 - files.map((file) => {
127 - return path.basename(path.basename(file, ".js"), ".expect.md");
128 - })
129 - )
130 - ).sort();
131 -
132 - let fixtures;
133 - if (filter) {
134 - if (filter.kind === "only") {
135 - fixtures = allFixtures.filter(
136 - (name) => filter.paths.indexOf(name) !== -1
137 - );
138 - } else if (filter.kind === "skip") {
139 - fixtures = allFixtures.filter(
140 - (name) => filter.paths.indexOf(name) === -1
141 - );
142 - } else {
143 - invariant(false, "Internal snap error.");
144 - }
145 - } else {
146 - fixtures = allFixtures;
147 - }
148 - const isOnlyFixture = filter !== null && fixtures.length === 1;
167 + const fixtures = getFixtures(filter);
168 + const isOnlyFixture = filter !== null && fixtures.size === 1;
169
170 let entries: Array<[string, TestResult]>;
171 if (!opts.sync) {
172 // Note: promise.all to ensure parallelism when enabled
153 - entries = await Promise.all(
154 - fixtures.map(async (fixture) => {
155 - let output = await worker.compile(
156 - COMPILER_PATH,
157 - LOGGER_PATH,
158 - FIXTURES_PATH,
159 - fixture,
160 - compilerVersion,
161 - isOnlyFixture
162 - );
163 - return [fixture, output];
164 - })
165 - );
173 + const work: Array<Promise<[string, TestResult]>> = [];
174 + for (const [fixtureName, fixture] of fixtures) {
175 + work.push(
176 + worker
177 + .compile(
178 + COMPILER_PATH,
179 + LOGGER_PATH,
180 + fixture,
181 + compilerVersion,
182 + isOnlyFixture
183 + )
184 + .then((result) => [fixtureName, result])
185 + );
186 + }
187 +
188 + entries = await Promise.all(work);
189 } else {
190 entries = [];
168 - for (const fixture of fixtures) {
191 + for (const [fixtureName, fixture] of fixtures) {
192 let output = await compiler.compile(
193 COMPILER_PATH,
194 LOGGER_PATH,
172 - FIXTURES_PATH,
195 fixture,
196 compilerVersion,
197 isOnlyFixture
198 );
177 - entries.push([fixture, output]);
199 + entries.push([fixtureName, output]);
200 }
201 }
202
@@ -183,8 +205,9 @@ async function run(
205
206 /**
207 * Report test results to the user
208 + * @returns boolean indicatig whether all tests passed
209 */
187 -function report(results: Results): void {
210 +function report(results: Results): boolean {
211 const failures: Array<[string, TestResult]> = [];
212 for (const [basename, result] of results) {
213 if (result.actual === result.expected && result.unexpectedError == null) {
@@ -207,7 +230,19 @@ function report(results: Results): void {
230 ` >> Unexpected error during test: \n${result.unexpectedError}`
231 );
232 } else {
210 - console.log(diff(result.expected, result.actual) + "\n");
233 + if (result.expected == null) {
234 + invariant(result.actual != null, "[Snap tester] Internal failure.");
235 + console.log(
236 + chalk.red("[ expected fixture output is absent ]") + "\n"
237 + );
238 + } else if (result.actual == null) {
239 + invariant(result.expected != null, "[Snap tester] Internal failure.");
240 + console.log(
241 + chalk.red("[ fixture input (test.js) is absent ]") + "\n"
242 + );
243 + } else {
244 + console.log(diff(result.expected, result.actual) + "\n");
245 + }
246 }
247 }
248 }
@@ -217,6 +252,7 @@ function report(results: Results): void {
252 failures.length
253 } Failed`
254 );
255 + return failures.length === 0;
256 }
257
258 /**
@@ -238,11 +274,16 @@ async function update(results: Results): Promise<void> {
274 console.log(
275 chalk.red.inverse.bold(" REMOVE ") + " " + chalk.dim(basename)
276 );
241 - // await fs.unlink(result.inputPath);
242 - // await fs.unlink(result.outputPath);
243 - console.log(" remove " + result.inputPath);
244 - console.log(" remove " + result.outputPath);
245 - deleted++;
277 + try {
278 + await fs.unlink(result.outputPath);
279 + console.log(" remove " + result.outputPath);
280 + deleted++;
281 + } catch (e) {
282 + console.error(
283 + "[Snap tester error]: failed to remove " + result.outputPath
284 + );
285 + failed.push([basename, result.unexpectedError]);
286 + }
287 } else if (result.actual !== result.expected) {
288 // Expected output has changed
289 console.log(
@@ -565,15 +606,17 @@ export async function main(opts: RunnerOptions): Promise<void> {
606 null;
607 tsWatch = watchSrc(
608 () => {},
568 - async (isSuccess: boolean) => {
569 - if (isSuccess) {
609 + async (compileSuccess: boolean) => {
610 + let isSuccess = compileSuccess;
611 + if (compileSuccess) {
612 const testFilter =
613 opts.mode === "filter" ? await readTestFilter() : null;
614 const results = await run(worker, opts, testFilter, 0);
615 if (opts.mode === "update") {
616 update(results);
617 } else {
576 - report(results);
618 + const testSuccess = report(results);
619 + isSuccess &&= testSuccess;
620 }
621 } else {
622 console.error(
@@ -585,7 +628,7 @@ export async function main(opts: RunnerOptions): Promise<void> {
628 tsWatch = null;
629 }
630 await worker.end();
588 - process.exit(isSuccess ? 0 : -1);
631 + process.exit(isSuccess ? 0 : 1);
632 }
633 );
634 pushCleanupTask(() => {