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 });