1/* 2 * Copyright (c) 2022-2025 Huawei Device Co., Ltd. 3 * Licensed under the Apache License, Version 2.0 (the "License"); 4 * you may not use this file except in compliance with the License. 5 * You may obtain a copy of the License at 6 * 7 * http://www.apache.org/licenses/LICENSE-2.0 8 * 9 * Unless required by applicable law or agreed to in writing, software 10 * distributed under the License is distributed on an "AS IS" BASIS, 11 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 * See the License for the specific language governing permissions and 13 * limitations under the License. 14 */ 15 16import * as fs from 'node:fs'; 17import * as path from 'node:path'; 18import * as ts from 'typescript'; 19import { Logger } from '../lib/Logger'; 20import { LoggerImpl } from '../cli/LoggerImpl'; 21import { Command } from 'commander'; 22import { rimrafSync } from 'rimraf'; 23import { globSync } from 'glob'; 24import type { Path } from 'path-scurry'; 25import { PathScurry } from 'path-scurry'; 26import { createTests } from './TestFactory'; 27import type { LintTest } from './LintTest'; 28import type { TestRunnerOptions } from './TestRunnerOptions'; 29import type { RunTestFileOptions } from './RunTestFileOptions'; 30import { MIGRATE_RESULT_SUFFIX, RESULTS_DIR, TAB } from './Consts'; 31 32Logger.init(new LoggerImpl()); 33 34interface TestStatistics { 35 passed: number; 36 failed: number; 37} 38 39export function getSdkConfigOptions(configFile: string): ts.CompilerOptions { 40 // initial configuration 41 const options = ts.readConfigFile(configFile, ts.sys.readFile).config.compilerOptions; 42 43 const allPath = ['*']; 44 Object.assign(options, { 45 emitNodeModulesFiles: true, 46 importsNotUsedAsValues: ts.ImportsNotUsedAsValues.Preserve, 47 module: ts.ModuleKind.ES2020, 48 moduleResolution: ts.ModuleResolutionKind.NodeJs, 49 noEmit: true, 50 target: ts.ScriptTarget.ES2021, 51 baseUrl: '/', 52 paths: { 53 '*': allPath 54 }, 55 lib: ['lib.es2021.d.ts'], 56 types: [], 57 etsLoaderPath: 'null_sdkPath' 58 }); 59 60 return options; 61} 62 63function parseCommandLine(commandLineArgs: string[]): TestRunnerOptions { 64 const program = new Command(); 65 program. 66 name('testrunner'). 67 description('Test Runner for Linter tests'). 68 configureHelp({ helpWidth: 100 }). 69 version('1.0.0'); 70 program. 71 requiredOption( 72 '-d, --test-dir <test_dir>', 73 'Specifies a directory with test files. Multiple directories can be specified with a comma-separated list' 74 ). 75 option( 76 '-p, --test-pattern <pattern>', 77 'Specifies a glob pattern to filter the list of tests. The path from the pattern is resolved to test directory. ' + 78 'Note: Only tests in specified test directory will be run, and the sub-directories will be ignored.' 79 ). 80 option('--sdk', 'Use SDK check logic'). 81 option('--interop-mode', 'Run interop-mode check'); 82 83 // method parse() eats two first args, so make them dummy 84 const cmdArgs: string[] = ['dummy', 'dummy']; 85 cmdArgs.push(...commandLineArgs); 86 program.parse(cmdArgs); 87 const programOpts = program.opts(); 88 const testRunnerOpts: TestRunnerOptions = { 89 testDirs: processTestDirArg(programOpts.testDir), 90 linterOptions: {} 91 }; 92 if (programOpts.testPattern) { 93 testRunnerOpts.testPattern = programOpts.testPattern; 94 } 95 if (programOpts.interopMode) { 96 testRunnerOpts.linterOptions.interopCheckMode = true; 97 } 98 if (programOpts.sdk) { 99 testRunnerOpts.linterOptions.useRtLogic = false; 100 } 101 return testRunnerOpts; 102} 103 104function processTestDirArg(testDirs: string | undefined): string[] { 105 if (!testDirs) { 106 return []; 107 } 108 return testDirs. 109 split(','). 110 map((val) => { 111 return val.trim(); 112 }). 113 filter((val) => { 114 return val.length > 0; 115 }); 116} 117 118function runTests(): boolean { 119 const testRunnerOpts = parseCommandLine(process.argv.slice(2)); 120 const testStats: TestStatistics = { passed: 0, failed: 0 }; 121 // Get tests from test directory 122 for (const testDir of testRunnerOpts.testDirs) { 123 if (!fs.existsSync(testDir)) { 124 Logger.info(`\nThe "${testDir}" directory doesn't exist.\n`); 125 continue; 126 } 127 Logger.info(`\nProcessing "${testDir}" directory:\n`); 128 rimrafSync(path.join(testDir, RESULTS_DIR)); 129 let testFiles: string[] = testRunnerOpts.testPattern ? 130 collectTestFilesWithGlob(testDir, testRunnerOpts.testPattern) : 131 fs.readdirSync(testDir); 132 testFiles = testFiles.filter((x) => { 133 const file = x.trimEnd(); 134 return ( 135 (file.endsWith(ts.Extension.Ts) && !file.endsWith(ts.Extension.Dts) || 136 file.endsWith(ts.Extension.Tsx) || 137 file.endsWith(ts.Extension.Ets)) && 138 !file.endsWith(MIGRATE_RESULT_SUFFIX + path.extname(file)) 139 ); 140 }); 141 runTestFiles(testFiles, testDir, testRunnerOpts, testStats); 142 } 143 const { passed, failed } = testStats; 144 Logger.info(`\nSUMMARY: ${passed + failed} total, ${passed} passed, ${failed} failed.`); 145 Logger.info(failed > 0 ? '\nTEST FAILED' : '\nTEST SUCCESSFUL'); 146 147 const saveCoverageData = (): void => { 148 const coverageData = globalThis.__coverage__; 149 if (coverageData) { 150 const projectRoot = path.resolve(__dirname, '../../..'); 151 const coverageDir = path.join(projectRoot, 'coverage'); 152 fs.mkdirSync(coverageDir, { recursive: true }); 153 154 const coverageFile = path.join(coverageDir, 'coverage.json'); 155 fs.writeFileSync(coverageFile, JSON.stringify(coverageData, null, 4)); 156 } else { 157 console.log('no coverage data found'); 158 } 159 }; 160 saveCoverageData(); 161 process.exit(failed > 0 ? -1 : 0); 162} 163 164function collectTestFilesWithGlob(testDir: string, pattern: string): string[] { 165 const testDirPath = new PathScurry(testDir).cwd.fullpath(); 166 return globSync(pattern, { 167 cwd: testDir, 168 ignore: { 169 childrenIgnored(p: Path): boolean { 170 return p.fullpath() !== testDirPath; 171 } 172 } 173 }).sort(); 174} 175 176function runTestFiles( 177 testFiles: string[], 178 testDir: string, 179 testRunnerOpts: TestRunnerOptions, 180 testStats: TestStatistics 181): void { 182 for (const testFile of testFiles) { 183 try { 184 runTestFile({ testDir, testFile, testRunnerOpts }, testStats); 185 } catch (error) { 186 Logger.info(`Test ${testFile} failed:\n${TAB}` + (error as Error).message); 187 testStats.failed++; 188 } 189 } 190} 191 192function runTestFile(runTestFileOpts: RunTestFileOptions, testStats: TestStatistics): void { 193 const tests: LintTest[] = createTests(runTestFileOpts); 194 195 tests.forEach((test: LintTest) => { 196 if (test.run()) { 197 ++testStats.passed; 198 } else { 199 ++testStats.failed; 200 } 201 }); 202} 203 204runTests(); 205