1/**2 * Copyright (c) Meta Platforms, Inc. and affiliates.3 *4 * This source code is licensed under the MIT license found in the5 * LICENSE file in the root directory of this source tree.6 */78import chalk from 'chalk';9import fs from 'fs';10import invariant from 'invariant';11import {diff} from 'jest-diff';12import path from 'path';1314function wrapWithTripleBackticks(s: string, ext: string | null = null): string {15 return `\`\`\`${ext ?? ''}16${s}17\`\`\``;18}19const SPROUT_SEPARATOR = '\n### Eval output\n';2021/**22 * Normalize blank lines in the ## Code section of a snapshot.23 * Strips blank lines that appear inside code blocks so that24 * whitespace-only differences don't cause test failures.25 */26export function normalizeCodeBlankLines(snapshot: string): string {27 const codeStart = snapshot.indexOf('## Code\n');28 if (codeStart === -1) return snapshot;29 const codeBlockStart = snapshot.indexOf('```javascript\n', codeStart);30 if (codeBlockStart === -1) return snapshot;31 const contentStart = codeBlockStart + '```javascript\n'.length;32 const codeBlockEnd = snapshot.indexOf('\n```', contentStart);33 if (codeBlockEnd === -1) return snapshot;3435 const before = snapshot.slice(0, contentStart);36 const code = snapshot.slice(contentStart, codeBlockEnd);37 const after = snapshot.slice(codeBlockEnd);3839 const lines = code.split('\n');40 const filtered = lines.filter(line => {41 if (line.trim() === '') return false;42 // Strip unused var declarations from babel-plugin-idx (e.g., `var _ref2;`)43 // These are generated by generateUidIdentifier and scope.push() in44 // babel-plugin-idx, but the actual references may use a different _refN.45 // The TS and Rust compilers interact differently with Babel's scope UID46 // counter, producing different _refN numbering for unused declarations.47 const match = line.match(/^\s*var (_ref\d*);$/);48 if (match) {49 const varName = match[1];50 // Check if this identifier is used anywhere else in the code51 const regex = new RegExp('\\b' + varName + '\\b');52 const otherLines = lines.filter(l => l !== line);53 const isUsed = otherLines.some(l => regex.test(l));54 if (!isUsed) return false;55 }56 return true;57 });58 const normalized = filtered.join('\n');5960 return before + normalized + after;61}6263export function writeOutputToString(64 input: string,65 compilerOutput: string | null,66 evaluatorOutput: string | null,67 logs: string | null,68 errorMessage: string | null,69) {70 // leading newline intentional71 let result = `72## Input7374${wrapWithTripleBackticks(input, 'javascript')}75`; // trailing newline + space internional7677 if (compilerOutput != null) {78 result += `79## Code8081${wrapWithTripleBackticks(compilerOutput, 'javascript')}82`;83 } else {84 result += '\n';85 }8687 if (logs != null) {88 result += `89## Logs9091${wrapWithTripleBackticks(logs, null)}92`;93 }9495 if (errorMessage != null) {96 result += `97## Error9899${wrapWithTripleBackticks(errorMessage.replace(/^\/.*?:\s/, ''))}100 \n`;101 }102 result += ` `;103 if (evaluatorOutput != null) {104 result += SPROUT_SEPARATOR + evaluatorOutput;105 }106 return result;107}108109export type TestResult = {110 actual: string | null; // null == input did not exist111 expected: string | null; // null == output did not exist112 outputPath: string;113 unexpectedError: string | null;114};115export type TestResults = Map<string, TestResult>;116117/**118 * Update the fixtures directory given the compilation results119 */120export async function update(results: TestResults): Promise<void> {121 let deleted = 0;122 let updated = 0;123 let created = 0;124 const failed = [];125 for (const [basename, result] of results) {126 if (result.unexpectedError != null) {127 console.log(128 chalk.red.inverse.bold(' FAILED ') + ' ' + chalk.dim(basename),129 );130 failed.push([basename, result.unexpectedError]);131 } else if (result.actual == null) {132 // Input was deleted but the expect file still existed, remove it133 console.log(134 chalk.red.inverse.bold(' REMOVE ') + ' ' + chalk.dim(basename),135 );136 try {137 fs.unlinkSync(result.outputPath);138 console.log(' remove ' + result.outputPath);139 deleted++;140 } catch (e) {141 console.error(142 '[Snap tester error]: failed to remove ' + result.outputPath,143 );144 failed.push([basename, result.unexpectedError]);145 }146 } else if (result.actual !== result.expected) {147 // Expected output has changed148 console.log(149 chalk.blue.inverse.bold(' UPDATE ') + ' ' + chalk.dim(basename),150 );151 try {152 fs.writeFileSync(result.outputPath, result.actual, 'utf8');153 } catch (e) {154 if (e?.code === 'ENOENT') {155 // May have failed to create nested dir, so make a directory and retry156 fs.mkdirSync(path.dirname(result.outputPath), {recursive: true});157 fs.writeFileSync(result.outputPath, result.actual, 'utf8');158 }159 }160 if (result.expected == null) {161 created++;162 } else {163 updated++;164 }165 } else {166 // Expected output is current167 console.log(168 chalk.green.inverse.bold(' OKAY ') + ' ' + chalk.dim(basename),169 );170 }171 }172 console.log(173 `${deleted} Deleted, ${created} Created, ${updated} Updated, ${failed.length} Failed`,174 );175 for (const [basename, errorMsg] of failed) {176 console.log(`${chalk.red.bold('Fail:')} ${basename}\n${errorMsg}`);177 }178}179180/**181 * Report test results to the user182 * @returns boolean indicatig whether all tests passed183 */184// Fixtures where TS and Rust produce different output. Snapshots reflect Rust185// output (source of truth). Skipped when running the TS compiler.186const TS_SKIP_FIXTURES: Set<string> = new Set([187 // Rust compiles successfully, TS would error. Renamed from error.todo-/error.bug-.188 'todo-hoist-type-alias-before-declaration',189 // Error message/format divergences190 'fbt/error.todo-locally-require-fbt',191 // Minor output difference (TS adds unused runtime import)192 'use-no-forget-multiple-with-eslint-suppression',193 // Cosmetic blank-line/unused-var differences between Rust and TS codegen194 'debugger',195 'debugger-memoized',196 'idx-no-outlining',197 'optional-call-with-independently-memoizable-arg',198]);199export function report(200 results: TestResults,201 verbose: boolean = false,202 rust: boolean = false,203): boolean {204 const failures: Array<[string, TestResult]> = [];205 for (const [basename, result] of results) {206 if (!rust && TS_SKIP_FIXTURES.has(basename)) {207 continue;208 }209 const actual =210 rust && result.actual211 ? normalizeCodeBlankLines(result.actual)212 : result.actual;213 const expected =214 rust && result.expected215 ? normalizeCodeBlankLines(result.expected)216 : result.expected;217 if (actual === expected && result.unexpectedError == null) {218 if (verbose) {219 console.log(220 chalk.green.inverse.bold(' PASS ') + ' ' + chalk.dim(basename),221 );222 }223 } else {224 if (verbose) {225 console.log(226 chalk.red.inverse.bold(' FAIL ') + ' ' + chalk.dim(basename),227 );228 }229 failures.push([basename, result]);230 }231 }232233 if (failures.length !== 0) {234 console.log('\n' + chalk.red.bold('Failures:') + '\n');235236 for (const [basename, result] of failures) {237 console.log(chalk.red.bold('FAIL:') + ' ' + basename);238 if (result.unexpectedError != null) {239 console.log(240 ` >> Unexpected error during test: \n${result.unexpectedError}`,241 );242 } else {243 const actual =244 rust && result.actual245 ? normalizeCodeBlankLines(result.actual)246 : result.actual;247 const expected =248 rust && result.expected249 ? normalizeCodeBlankLines(result.expected)250 : result.expected;251 if (expected == null) {252 invariant(actual != null, '[Tester] Internal failure.');253 console.log(254 chalk.red('[ expected fixture output is absent ]') + '\n',255 );256 } else if (actual == null) {257 invariant(expected != null, '[Tester] Internal failure.');258 console.log(259 chalk.red(`[ fixture input for ${result.outputPath} is absent ]`) +260 '\n',261 );262 } else {263 console.log(diff(expected, actual) + '\n');264 }265 }266 }267 }268269 console.log(270 `${results.size} Tests, ${results.size - failures.length} Passed, ${271 failures.length272 } Failed`,273 );274 return failures.length === 0;275}
Findings
✓ No findings reported for this file.