• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1/*
2 * Copyright (c) 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 type { Autofix } from '../lib/autofixes/Autofixer';
19import type { CommandLineOptions } from '../lib/CommandLineOptions';
20import type { LinterConfig } from '../lib/LinterConfig';
21import { lint } from '../lib/LinterRunner';
22import type { LintRunResult } from '../lib/LintRunResult';
23import { Logger } from '../lib/Logger';
24import { TimeRecorder } from '../lib/statistics/scan/TimeRecorder';
25import { compileLintOptions } from '../lib/ts-compiler/Compiler';
26import { DIFF_EXT, RESULTS_DIR, TAB } from './Consts';
27import type { CreateLintTestOptions } from './TestFactory';
28import type { TestModeProperties } from './TestMode';
29import { TestMode } from './TestMode';
30import type { TestProblemInfo, TestResult } from './TestResult';
31import { validateTestResult } from './TestResult';
32import { transformProblemInfos } from './TestUtil';
33
34export class LintTest {
35  readonly testDir: string;
36  readonly testFile: string;
37  readonly testModeProps: TestModeProperties;
38  readonly cmdOptions: CommandLineOptions;
39
40  constructor(createLintTestOpts: CreateLintTestOptions) {
41    this.testDir = createLintTestOpts.testDir;
42    this.testFile = createLintTestOpts.testFile;
43    this.testModeProps = createLintTestOpts.testModeProps;
44    this.cmdOptions = createLintTestOpts.cmdOptions;
45  }
46
47  run(): boolean {
48    Logger.info(`Running test ${this.testFile} (${TestMode[this.testModeProps.mode]} mode)`);
49
50    const linterConfig = this.compile();
51    const timeRecorder = new TimeRecorder();
52    const linterResult = lint(linterConfig, timeRecorder);
53    return this.validate(linterResult);
54  }
55
56  compile(): LinterConfig {
57    return compileLintOptions(this.cmdOptions);
58  }
59
60  validate(actualLinterResult: LintRunResult): boolean {
61    // Get actual test results.
62    const fileProblems = actualLinterResult.problemsInfos.get(path.normalize(this.cmdOptions.inputFiles[0]));
63    if (fileProblems === undefined) {
64      return true;
65    }
66    const actualResult = transformProblemInfos(fileProblems, this.testModeProps.mode);
67
68    // Read file with expected test result.
69    const resultDiff = this.compareLintResult(actualResult);
70
71    // Write file with actual test results.
72    this.writeLintResultFile(actualResult, resultDiff);
73    return !resultDiff;
74  }
75
76  private static readLintResultFile(testDir: string, testResultFileName: string): TestProblemInfo[] {
77    const filePath = path.join(testDir, testResultFileName);
78    try {
79      const testResult = fs.readFileSync(filePath).toString();
80      return validateTestResult(JSON.parse(testResult)).result;
81    } catch (error) {
82      throw new Error(`Failed to process ${filePath}: ${(error as Error).message}`);
83    }
84  }
85
86  private compareLintResult(actual: TestProblemInfo[]): string {
87    // Read file with expected test result.
88    let diff: string = '';
89    const testResultFileName = this.testFile + this.testModeProps.resultFileExt;
90    try {
91      let expected = LintTest.readLintResultFile(this.testDir, testResultFileName);
92
93      /**
94       * The exclusive field is added to identify whether the use case is exclusive to the RT or SDK
95       * RT means the RT exclusive
96       * SDK means the SDK exclusive
97       * undefined means shared
98       */
99      expected = expected.filter((x) => {
100        return !x?.exclusive || x.exclusive === (this.cmdOptions.linterOptions.useRtLogic ? 'RT' : 'SDK');
101      });
102
103      if (!expected || expected.length !== actual.length) {
104        const expectedResultCount = expected ? expected.length : 0;
105        diff = `Expected count: ${expectedResultCount} vs actual count: ${actual.length}`;
106      } else {
107        diff = LintTest.expectedAndActualMatch(expected, actual);
108      }
109
110      if (diff) {
111        Logger.info(`${TAB}Lint test failed. Expected and actual results differ:\n${diff}`);
112      }
113    } catch (error) {
114      // Write error message to diff, as non-empty diff indicates that test has failed.
115      diff = (error as Error).message;
116      Logger.info(`${TAB}Failed to compare expected and actual results. ` + diff);
117    }
118
119    return diff;
120  }
121
122  private static expectedAndActualMatch(expectedNodes: TestProblemInfo[], actualNodes: TestProblemInfo[]): string {
123    // Compare expected and actual results.
124    for (let i = 0; i < actualNodes.length; i++) {
125      const actual = actualNodes[i];
126      const expected = expectedNodes[i];
127      if (!LintTest.locationMatch(expected, actual) || actual.problem !== expected.problem) {
128        return LintTest.reportLintResultDiff(expected, actual);
129      }
130      if (!LintTest.autofixArraysMatch(expected.autofix, actual.autofix)) {
131        return LintTest.reportLintResultDiff(expected, actual);
132      }
133      if (expected.suggest && actual.suggest !== expected.suggest) {
134        return LintTest.reportLintResultDiff(expected, actual);
135      }
136      if (expected.rule && actual.rule !== expected.rule) {
137        return LintTest.reportLintResultDiff(expected, actual);
138      }
139      if (expected.severity && actual.severity !== expected.severity) {
140        return LintTest.reportLintResultDiff(expected, actual);
141      }
142    }
143
144    return '';
145  }
146
147  private static locationMatch(expected: TestProblemInfo, actual: TestProblemInfo): boolean {
148    return (
149      actual.line === expected.line &&
150      actual.column === expected.column &&
151      (!expected.endLine || actual.endLine === expected.endLine) &&
152      (!expected.endColumn || actual.endColumn === expected.endColumn)
153    );
154  }
155
156  private static autofixArraysMatch(expected: Autofix[] | undefined, actual: Autofix[] | undefined): boolean {
157    if (!expected && !actual) {
158      return true;
159    }
160    if (!(expected && actual) || expected.length !== actual.length) {
161      return false;
162    }
163    for (let i = 0; i < actual.length; ++i) {
164      if (
165        actual[i].start !== expected[i].start ||
166        actual[i].end !== expected[i].end ||
167        actual[i].replacementText !== expected[i].replacementText
168      ) {
169        return false;
170      }
171    }
172    return true;
173  }
174
175  private writeLintResultFile(actual: TestProblemInfo[], diff: string): void {
176    const actualResultsDir = path.join(this.testDir, RESULTS_DIR);
177    if (!fs.existsSync(actualResultsDir)) {
178      fs.mkdirSync(actualResultsDir);
179    }
180
181    const actualTestResult: TestResult = { result: actual };
182    const actualResultJSON = JSON.stringify(actualTestResult, null, 4);
183    fs.writeFileSync(path.join(actualResultsDir, this.testFile + this.testModeProps.resultFileExt), actualResultJSON);
184
185    if (diff) {
186      fs.writeFileSync(path.join(actualResultsDir, this.testFile + this.testModeProps.resultFileExt + DIFF_EXT), diff);
187    }
188  }
189
190  private static reportLintResultDiff(expected: TestProblemInfo, actual: TestProblemInfo): string {
191    const expectedNode = JSON.stringify({ nodes: [expected] }, null, 4);
192    const actualNode = JSON.stringify({ nodes: [actual] }, null, 4);
193
194    const diff = `Expected:
195${expectedNode}
196Actual:
197${actualNode}`;
198
199    return diff;
200  }
201}
202