compiler/packages/snap/src/reporter.ts TYPESCRIPT 276 lines View on github.com → Search inside
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.

Get this view in your editor

Same data, no extra tab — call code_get_file + code_get_findings over MCP from Claude/Cursor/Copilot.