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