• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1/*
2 * Copyright (c) 2022-2024 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 { Logger } from '../lib/Logger';
17import { LoggerImpl } from './LoggerImpl';
18Logger.init(new LoggerImpl());
19
20import * as fs from 'node:fs';
21import * as path from 'node:path';
22import * as ts from 'typescript';
23import type { CommandLineOptions } from '../lib/CommandLineOptions';
24import { lint } from '../lib/LinterRunner';
25import { TypeScriptLinter } from '../lib/TypeScriptLinter';
26import type { Autofix } from '../lib/autofixes/Autofixer';
27import { parseCommandLine } from './CommandLineParser';
28import { compileLintOptions } from './Compiler';
29
30const TEST_DIR = 'test';
31const TAB = '    ';
32
33interface TestNodeInfo {
34  line: number;
35  column: number;
36  endLine: number;
37  endColumn: number;
38  problem: string;
39  autofix?: Autofix[];
40  suggest?: string;
41  rule?: string;
42}
43
44enum Mode {
45  DEFAULT,
46  AUTOFIX
47}
48
49const RESULT_EXT: string[] = [];
50RESULT_EXT[Mode.DEFAULT] = '.json';
51RESULT_EXT[Mode.AUTOFIX] = '.autofix.json';
52const AUTOFIX_SKIP_EXT = '.autofix.skip';
53const ARGS_CONFIG_EXT = '.args.json';
54const DIFF_EXT = '.diff';
55const testExtensionSts = '.sts';
56const testExtensionDSts = '.d.sts';
57
58function runTests(testDirs: string[]): number {
59
60  /*
61   * Set the IDE mode manually to enable storing information
62   * about found bad nodes and also disable the log output.
63   */
64  TypeScriptLinter.ideMode = true;
65  TypeScriptLinter.testMode = true;
66
67  let hasComparisonFailures = false;
68  let passed = 0;
69  let failed = 0;
70  // Get tests from test directory
71  if (!testDirs?.length) {
72    // eslint-disable-next-line no-param-reassign
73    testDirs = [TEST_DIR];
74  }
75  for (const testDir of testDirs) {
76    const testFiles: string[] = fs.readdirSync(testDir).filter((x) => {
77      return (
78        x.trimEnd().endsWith(ts.Extension.Ts) && !x.trimEnd().endsWith(ts.Extension.Dts) ||
79        x.trimEnd().endsWith(ts.Extension.Tsx) ||
80        x.trimEnd().endsWith(ts.Extension.Ets) ||
81        x.trimEnd().endsWith(testExtensionSts) && !x.trimEnd().endsWith(testExtensionDSts)
82      );
83    });
84    Logger.info(`\nProcessing "${testDir}" directory:\n`);
85    // Run each test in Default and Autofix mode:
86    [passed, failed, hasComparisonFailures] = runTestFiles(testFiles, testDir);
87  }
88  Logger.info(`\nSUMMARY: ${passed + failed} total, ${passed} passed or skipped, ${failed} failed.`);
89  Logger.info(failed > 0 ? '\nTEST FAILED' : '\nTEST SUCCESSFUL');
90  process.exit(hasComparisonFailures ? -1 : 0);
91}
92
93function runTestFiles(testFiles: string[], testDir: string): [number, number, boolean] {
94  let hasComparisonFailures = false;
95  let passed = 0;
96  let failed = 0;
97  for (const testFile of testFiles) {
98    let renamed = false;
99    let tsName = testFile;
100    if (testFile.includes(testExtensionSts)) {
101      renamed = true;
102      tsName = testFile.replace(testExtensionSts, ts.Extension.Ts);
103      fs.renameSync(path.join(testDir, testFile), path.join(testDir, tsName));
104    }
105    if (runTest(testDir, tsName, Mode.DEFAULT)) {
106      failed++;
107      hasComparisonFailures = true;
108    } else {
109      passed++;
110    }
111    if (runTest(testDir, tsName, Mode.AUTOFIX)) {
112      failed++;
113      hasComparisonFailures = true;
114    } else {
115      passed++;
116    }
117    if (renamed) {
118      fs.renameSync(path.join(testDir, tsName), path.join(testDir, testFile));
119    }
120  }
121  return [passed, failed, hasComparisonFailures];
122}
123
124function parseArgs(testDir: string, testFile: string, mode: Mode): CommandLineOptions {
125  // Configure test parameters and run linter.
126  const args: string[] = [path.join(testDir, testFile)];
127  const argsFileName = path.join(testDir, testFile + ARGS_CONFIG_EXT);
128
129  if (fs.existsSync(argsFileName)) {
130    const data = fs.readFileSync(argsFileName).toString();
131    const args = JSON.parse(data);
132    if (args.testMode !== undefined) {
133      TypeScriptLinter.testMode = args.testMode;
134    }
135  }
136
137  if (mode === Mode.AUTOFIX) {
138    args.push('--autofix');
139  }
140
141  return parseCommandLine(args);
142}
143
144function compareExpectedAndActual(testDir: string, testFile: string, mode: Mode, resultNodes: TestNodeInfo[]): string {
145  // Read file with expected test result.
146  let expectedResult: { nodes: TestNodeInfo[] };
147  let diff: string = '';
148  const resultExt = RESULT_EXT[mode];
149  const testResultFileName = testFile + resultExt;
150  try {
151    const expectedResultFile = fs.readFileSync(path.join(testDir, testResultFileName)).toString();
152    expectedResult = JSON.parse(expectedResultFile);
153
154    if (!expectedResult?.nodes || expectedResult.nodes.length !== resultNodes.length) {
155      const expectedResultCount = expectedResult?.nodes ? expectedResult.nodes.length : 0;
156      diff = `Expected count: ${expectedResultCount} vs actual count: ${resultNodes.length}`;
157      Logger.info(`${TAB}${diff}`);
158    } else {
159      diff = expectedAndActualMatch(expectedResult.nodes, resultNodes);
160    }
161
162    if (diff) {
163      Logger.info(`${TAB}Test failed. Expected and actual results differ.`);
164    }
165  } catch (error) {
166    Logger.info(`${TAB}Test failed. ` + error);
167  }
168
169  return diff;
170}
171
172function runTest(testDir: string, testFile: string, mode: Mode): boolean {
173  if (mode === Mode.AUTOFIX && fs.existsSync(path.join(testDir, testFile + AUTOFIX_SKIP_EXT))) {
174    Logger.info(`Skipping test ${testFile} (${Mode[mode]} mode)`);
175    return false;
176  }
177  Logger.info(`Running test ${testFile} (${Mode[mode]} mode)`);
178
179  TypeScriptLinter.initGlobals();
180
181  const currentTestMode = TypeScriptLinter.testMode;
182
183  const cmdOptions = parseArgs(testDir, testFile, mode);
184  const result = lint(compileLintOptions(cmdOptions));
185  const fileProblems = result.problemsInfos.get(path.normalize(cmdOptions.inputFiles[0]));
186  if (fileProblems === undefined) {
187    return true;
188  }
189
190  TypeScriptLinter.testMode = currentTestMode;
191
192  // Get list of bad nodes from the current run.
193  const resultNodes: TestNodeInfo[] = fileProblems.map<TestNodeInfo>((x) => {
194    return {
195      line: x.line,
196      column: x.column,
197      endLine: x.endLine,
198      endColumn: x.endColumn,
199      problem: x.problem,
200      autofix: mode === Mode.AUTOFIX ? x.autofix : undefined,
201      suggest: x.suggest,
202      rule: x.rule
203    };
204  });
205
206  // Read file with expected test result.
207  const testResult = compareExpectedAndActual(testDir, testFile, mode, resultNodes);
208
209  // Write file with actual test results.
210  writeActualResultFile(testDir, testFile, mode, resultNodes, testResult);
211
212  return !!testResult;
213}
214
215function expectedAndActualMatch(expectedNodes: TestNodeInfo[], actualNodes: TestNodeInfo[]): string {
216  // Compare expected and actual results.
217  for (let i = 0; i < actualNodes.length; i++) {
218    const actual = actualNodes[i];
219    const expect = expectedNodes[i];
220    if (actual.line !== expect.line || actual.column !== expect.column || actual.problem !== expect.problem) {
221      return reportDiff(expect, actual);
222    }
223    if (
224      expect.endLine && actual.endLine !== expect.endLine ||
225      expect.endColumn && actual.endColumn !== expect.endColumn
226    ) {
227      return reportDiff(expect, actual);
228    }
229    if (!autofixArraysMatch(expect.autofix, actual.autofix)) {
230      return reportDiff(expect, actual);
231    }
232    if (expect.suggest && actual.suggest !== expect.suggest) {
233      return reportDiff(expect, actual);
234    }
235    if (expect.rule && actual.rule !== expect.rule) {
236      return reportDiff(expect, actual);
237    }
238  }
239
240  return '';
241}
242
243function autofixArraysMatch(expected: Autofix[] | undefined, actual: Autofix[] | undefined): boolean {
244  if (!expected && !actual) {
245    return true;
246  }
247  if (!(expected && actual) || expected.length !== actual.length) {
248    return false;
249  }
250  for (let i = 0; i < actual.length; ++i) {
251    if (
252      actual[i].start !== expected[i].start ||
253      actual[i].end !== expected[i].end ||
254      actual[i].replacementText !== expected[i].replacementText
255    ) {
256      return false;
257    }
258  }
259  return true;
260}
261
262function writeActualResultFile(
263  testDir: string,
264  testFile: string,
265  mode: Mode,
266  resultNodes: TestNodeInfo[],
267  diff: string
268): void {
269  const actualResultsDir = path.join(testDir, 'results');
270  const resultExt = RESULT_EXT[mode];
271  if (!fs.existsSync(actualResultsDir)) {
272    fs.mkdirSync(actualResultsDir);
273  }
274
275  const actualResultJSON = JSON.stringify({ nodes: resultNodes }, null, 4);
276  fs.writeFileSync(path.join(actualResultsDir, testFile + resultExt), actualResultJSON);
277
278  if (diff) {
279    fs.writeFileSync(path.join(actualResultsDir, testFile + resultExt + DIFF_EXT), diff);
280  }
281}
282
283function reportDiff(expected: TestNodeInfo, actual: TestNodeInfo): string {
284  const expectedNode = JSON.stringify({ nodes: [expected] }, null, 4);
285  const actualNode = JSON.stringify({ nodes: [actual] }, null, 4);
286
287  const diff = `Expected:
288${expectedNode}
289Actual:
290${actualNode}`;
291
292  Logger.info(diff);
293  return diff;
294}
295
296runTests(process.argv.slice(2));
297