• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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