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