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