[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(() => {