kythe.io@v0.0.68-0.20240422202219-7225dbc01741/kythe/typescript/test.ts (about)

     1  /*
     2   * Copyright 2017 The Kythe Authors. All rights reserved.
     3   *
     4   * Licensed under the Apache License, Version 2.0 (the "License");
     5   * you may not use this file except in compliance with the License.
     6   * You may obtain a copy of the License at
     7   *
     8   *   http://www.apache.org/licenses/LICENSE-2.0
     9   *
    10   * Unless required by applicable law or agreed to in writing, software
    11   * distributed under the License is distributed on an "AS IS" BASIS,
    12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13   * See the License for the specific language governing permissions and
    14   * limitations under the License.
    15   */
    16  
    17  // This program runs the Kythe verifier on test cases in the testdata/
    18  // directory.  It's written in TypeScript (rather than a plain shell
    19  // script) so it can reuse TypeScript data structures across test cases,
    20  // speeding up the test.
    21  //
    22  // If run with no arguments, runs all tests found in testdata/.
    23  // Otherwise run any files through the verifier by passing them:
    24  //   node test.js path/to/test1.ts testdata/test2.ts
    25  
    26  import * as assert from 'assert';
    27  import * as child_process from 'child_process';
    28  import * as fs from 'fs';
    29  import * as path from 'path';
    30  import * as ts from 'typescript';
    31  
    32  import * as indexer from './indexer';
    33  import * as kythe from './kythe';
    34  import {CompilationUnit, IndexerHost, Plugin} from './plugin_api';
    35  
    36  const KYTHE_PATH = process.env['KYTHE'] || '/opt/kythe';
    37  const RUNFILES = process.env['TEST_SRCDIR'];
    38  
    39  const ENTRYSTREAM = RUNFILES
    40    ? path.resolve('kythe/go/platform/tools/entrystream/entrystream')
    41    : path.resolve(KYTHE_PATH, 'tools/entrystream');
    42  const VERIFIER = RUNFILES
    43    ? path.resolve('kythe/cxx/verifier/verifier')
    44    : path.resolve(KYTHE_PATH, 'tools/verifier');
    45  const MARKEDSOURCE = RUNFILES
    46    ? path.resolve('kythe/go/util/tools/markedsource/markedsource')
    47    : path.resolve(KYTHE_PATH, 'tools/markedsource');
    48  
    49  /** Record representing a single test case to run. */
    50  interface TestCase {
    51    /** Name of the test case. Used for reporting. */
    52    readonly name: string;
    53    /**
    54     * List of files used for indexing. All these files passed together as source
    55     * files to the indexer.
    56     */
    57    readonly files: string[];
    58  }
    59  
    60  /**
    61   * createTestCompilerHost creates a ts.CompilerHost that caches the default
    62   * libraries.  This prevents re-parsing the (big) TypeScript standard library
    63   * across each test.
    64   */
    65  function createTestCompilerHost(options: ts.CompilerOptions): ts.CompilerHost {
    66    const compilerHost = ts.createCompilerHost(options);
    67  
    68    // Map of path to parsed SourceFile for all TS builtin libraries.
    69    const libs = new Map<string, ts.SourceFile | undefined>();
    70    const libDir = compilerHost.getDefaultLibLocation!();
    71  
    72    const hostGetSourceFile = compilerHost.getSourceFile;
    73    compilerHost.getSourceFile = (
    74      fileName: string,
    75      languageVersion: ts.ScriptTarget,
    76      onError?: (message: string) => void,
    77    ): ts.SourceFile | undefined => {
    78      let sourceFile = libs.get(fileName);
    79      if (!sourceFile) {
    80        sourceFile = hostGetSourceFile(fileName, languageVersion, onError);
    81        if (path.dirname(fileName) === libDir) libs.set(fileName, sourceFile);
    82      }
    83      return sourceFile;
    84    };
    85    return compilerHost;
    86  }
    87  
    88  function isTsFile(filename: string): boolean {
    89    return filename.endsWith('.ts') || filename.endsWith('.tsx');
    90  }
    91  
    92  /**
    93   * verify runs the indexer against a test case and passes it through the
    94   * Kythe verifier.  It returns a Promise because the node subprocess API must
    95   * be run async; if there's an error, it will reject the promise.
    96   */
    97  function verify(
    98    host: ts.CompilerHost,
    99    options: ts.CompilerOptions,
   100    testCase: TestCase,
   101    plugins?: Plugin[],
   102    emitRefCallOverIdentifier?: boolean,
   103    resolveCodeFacts?: boolean,
   104  ): Promise<void> {
   105    const rootVName: kythe.VName = {
   106      corpus: 'testcorpus',
   107      root: '',
   108      path: '',
   109      signature: '',
   110      language: '',
   111    };
   112    const testFiles = testCase.files;
   113  
   114    const verifier = child_process.spawn(
   115      `${ENTRYSTREAM} --read_format=json | ` +
   116        (resolveCodeFacts
   117          ? `${MARKEDSOURCE} --rewrite --render_callsite_signatures --render_qualified_names --render_signatures | `
   118          : '') +
   119        `${VERIFIER} --convert_marked_source ${testFiles.join(' ')}`,
   120      [],
   121      {
   122        stdio: ['pipe', process.stdout, process.stderr],
   123        shell: true,
   124      },
   125    );
   126    const fileVNames = new Map<string, kythe.VName>();
   127    for (const file of testFiles) {
   128      fileVNames.set(file, {...rootVName, path: file});
   129    }
   130  
   131    try {
   132      const compilationUnit: CompilationUnit = {
   133        rootVName,
   134        fileVNames,
   135        srcs: testFiles,
   136        rootFiles: testFiles,
   137      };
   138      indexer.index(compilationUnit, {
   139        compilerOptions: options,
   140        compilerHost: host,
   141        emit(obj: {}) {
   142          verifier.stdin.write(JSON.stringify(obj) + '\n');
   143        },
   144        plugins,
   145        emitRefCallOverIdentifier,
   146      });
   147    } finally {
   148      // Ensure we close stdin on the verifier even on crashes, or otherwise
   149      // we hang waiting for the verifier to complete.
   150      verifier.stdin.end();
   151    }
   152  
   153    return new Promise<void>((resolve, reject) => {
   154      verifier.on('close', (exitCode) => {
   155        if (exitCode === 0) {
   156          resolve();
   157        } else {
   158          reject(`process exited with code ${exitCode}`);
   159        }
   160      });
   161    });
   162  }
   163  
   164  function testLoadTsConfig() {
   165    const config = indexer.loadTsConfig(
   166      'testdata/tsconfig-files.for.tests.json',
   167      'testdata',
   168    );
   169    // We expect the paths that were loaded to be absolute.
   170    assert.deepEqual(config.fileNames, [path.resolve('testdata/alt.ts')]);
   171  }
   172  
   173  function collectTSFilesInDirectoryRecursively(dir: string, result: string[]) {
   174    for (const file of fs.readdirSync(dir, {withFileTypes: true})) {
   175      if (file.isDirectory()) {
   176        collectTSFilesInDirectoryRecursively(`${dir}/${file.name}`, result);
   177      } else if (isTsFile(file.name)) {
   178        result.push(path.resolve(`${dir}/${file.name}`));
   179      }
   180    }
   181  }
   182  
   183  /**
   184   * Returns list of files to test. Each group of files will be tested together:
   185   * all files from a group will be passed to indexer and verifier.
   186   *
   187   * The rules of constructing groups are the following:
   188   * 1. All individual files in testdata/ will be returned as group of one file.
   189   *    So they are tested separately.
   190   * 2. All files in subfolders ending with '_group' will be returned as one
   191   *    group. These are used when cross-file references need to be tested.
   192   * 3. All individual files in subfolders not ending with '_group' will be
   193   *    returned as group of one file. So they are tested separately.
   194   */
   195  function getTestCases(options: ts.CompilerOptions, dir: string): TestCase[] {
   196    const result = [];
   197    for (const file of fs.readdirSync(dir, {withFileTypes: true})) {
   198      const relativeName = `${dir}/${file.name}`;
   199      if (file.isDirectory()) {
   200        if (file.name.endsWith('_group')) {
   201          const files: string[] = [];
   202          collectTSFilesInDirectoryRecursively(relativeName, files);
   203          result.push({
   204            name: relativeName,
   205            files,
   206          });
   207        } else {
   208          result.push(...getTestCases(options, relativeName));
   209        }
   210      } else if (isTsFile(file.name)) {
   211        result.push({
   212          name: relativeName,
   213          files: [path.resolve(relativeName)],
   214        });
   215      }
   216    }
   217    return result;
   218  }
   219  
   220  /**
   221   * Given filters passed as command line arguments by user - returns only
   222   * groups that contain at least one file that matches at least one filter.
   223   * Matching is done by simply checking for substring.
   224   */
   225  function filterTestCases(testCases: TestCase[], filters: string[]): TestCase[] {
   226    return testCases.filter((testCase) => {
   227      for (const file of testCase.files) {
   228        if (filters.some((filter) => file.includes(filter))) {
   229          return true;
   230        }
   231      }
   232      return false;
   233    });
   234  }
   235  
   236  async function testIndexer(filters: string[], plugins?: Plugin[]) {
   237    const config = indexer.loadTsConfig(
   238      'testdata/tsconfig.for.tests.json',
   239      'testdata',
   240    );
   241    let testCases = getTestCases(config.options, 'testdata');
   242    if (filters.length !== 0) {
   243      testCases = filterTestCases(testCases, filters);
   244    }
   245  
   246    const host = createTestCompilerHost(config.options);
   247    for (const testCase of testCases) {
   248      if (testCase.name.endsWith('plugin.ts')) {
   249        // plugin.ts is tested by testPlugin() test.
   250        continue;
   251      }
   252      const emitRefCallOverIdentifier = testCase.name.endsWith('_id.ts');
   253      const resolveCodeFacts = testCase.name.startsWith(
   254        'testdata/marked_source/rendered/',
   255      );
   256      const start = new Date().valueOf();
   257      process.stdout.write(`${testCase.name}: `);
   258      try {
   259        await verify(
   260          host,
   261          config.options,
   262          testCase,
   263          plugins,
   264          emitRefCallOverIdentifier,
   265          resolveCodeFacts,
   266        );
   267      } catch (e) {
   268        console.log('FAIL');
   269        throw e;
   270      }
   271      const time = new Date().valueOf() - start;
   272      console.log('PASS', `${time}ms`);
   273    }
   274    return 0;
   275  }
   276  
   277  async function testPlugin() {
   278    const plugin: Plugin = {
   279      name: 'TestPlugin',
   280      index(context: IndexerHost) {
   281        for (const testPath of context.compilationUnit.srcs) {
   282          const pluginMod = {
   283            ...context.pathToVName(context.moduleName(testPath)),
   284            signature: 'plugin-module',
   285            language: 'plugin-language',
   286          };
   287          context.options.emit({
   288            source: pluginMod,
   289            fact_name: '/kythe/node/pluginKind' as kythe.FactName,
   290            fact_value: Buffer.from('pluginRecord').toString('base64'),
   291          });
   292        }
   293      },
   294    };
   295    return testIndexer(['testdata/plugin.ts'], [plugin]);
   296  }
   297  
   298  async function testMain(args: string[]) {
   299    if (RUNFILES) {
   300      process.chdir('kythe/typescript');
   301    }
   302    testLoadTsConfig();
   303    await testIndexer(args);
   304    await testPlugin();
   305  }
   306  
   307  testMain(process.argv.slice(2))
   308    .then(() => {
   309      process.exitCode = 0;
   310    })
   311    .catch((e) => {
   312      console.error(e);
   313      process.exitCode = 1;
   314    });