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