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