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 path from 'path'; 17import fs from 'fs'; 18import { 19 Lsp, 20 LspDefinitionData, 21 LspCompletionInfo, 22 LspDiagsNode, 23 ModuleDescriptor, 24 generateArkTsConfigByModules 25} from '../src/index'; 26import { testCases } from './cases'; 27import { LspCompletionEntry } from '../src/lspNode'; 28 29interface ComparisonOptions { 30 subMatch?: boolean; 31} 32 33interface ComparisonOutcome { 34 passed: boolean; 35 expectedJSON?: string; 36 actualJSON?: string; 37} 38 39let updateMode = false; 40 41function checkEnvironment(testDir: string): void { 42 const testCasesFilePath = path.join(testDir, 'testcases', 'cases.json'); 43 if (!fs.existsSync(testCasesFilePath)) { 44 console.error(`Test cases file not found: ${testCasesFilePath}`); 45 process.exit(1); 46 } 47} 48 49function getModules(projectRoot: string): ModuleDescriptor[] { 50 const testCases = JSON.parse(fs.readFileSync(path.join(projectRoot, 'cases.json'), 'utf-8')) as TestCases; 51 return Object.keys(testCases).map((name) => { 52 const modulePath = path.join(projectRoot, name); 53 return { 54 arktsversion: '1.2', 55 name, 56 moduleType: 'har', 57 srcPath: modulePath 58 } as ModuleDescriptor; 59 }); 60} 61 62// CC-OFFNXT(no_explicit_any) project code style 63function getExpectedResult(filePath: string): any { 64 try { 65 return JSON.parse(fs.readFileSync(filePath, 'utf-8')); 66 } catch (err) { 67 console.error(`Failed to read expected result from ${filePath}: ${err}`); 68 return null; 69 } 70} 71 72function sortCompletions(completionResult: LspCompletionInfo): LspCompletionInfo { 73 if (!completionResult || !completionResult.entries || !Array.isArray(completionResult.entries)) { 74 return completionResult; 75 } 76 77 // Sort entries by name 78 completionResult.entries.sort((a, b) => { 79 const nameA = a.name.toString().toLowerCase(); 80 const nameB = b.name.toString().toLowerCase(); 81 return nameA.localeCompare(nameB); 82 }); 83 84 return completionResult; 85} 86 87function sortDiagnostics(diags: LspDiagsNode): LspDiagsNode { 88 if (!diags || !diags.diagnostics || !Array.isArray(diags.diagnostics)) { 89 return diags; 90 } 91 92 diags.diagnostics.sort((a, b) => { 93 if (a.range.start.line !== b.range.start.line) { 94 return a.range.start.line - b.range.start.line; 95 } 96 97 if (a.range.start.character !== b.range.start.character) { 98 return a.range.start.character - b.range.start.character; 99 } 100 101 if (a.range.end.line !== b.range.end.line) { 102 return a.range.end.line - b.range.end.line; 103 } 104 105 return a.range.end.character - b.range.end.character; 106 }); 107 108 return diags; 109} 110 111// CC-OFFNXT(no_explicit_any) project code style 112function sortActualResult(testName: string, res: any): any { 113 if (testName === 'getCompletionAtPosition') { 114 return sortCompletions(res as LspCompletionInfo); 115 } 116 if (testName === 'getSuggestionDiagnostics') { 117 return sortDiagnostics(res as LspDiagsNode); 118 } 119 return res; 120} 121 122// CC-OFFNXT(no_explicit_any) project code style 123function normalizeData(obj: any): any { 124 if (Array.isArray(obj)) { 125 return obj.map(normalizeData); 126 } else if (obj && typeof obj === 'object') { 127 const newObj = { ...obj }; 128 if ('peer' in newObj) { 129 delete newObj.peer; // do not compare peer 130 } 131 if (newObj.fileName) { 132 newObj.fileName = path.basename(newObj.fileName); 133 } 134 for (const key of Object.keys(newObj)) { 135 newObj[key] = normalizeData(newObj[key]); 136 } 137 return newObj; 138 } 139 return obj; 140} 141 142// CC-OFFNXT(no_explicit_any) project code style 143function isSubObject(actual: any, expected: any): boolean { 144 if (typeof expected !== 'object' || expected === null) { 145 return actual === expected; 146 } 147 148 if (typeof actual !== 'object' || actual === null) { 149 return false; 150 } 151 152 if (Array.isArray(expected)) { 153 if (!Array.isArray(actual)) { 154 return false; 155 } 156 return expected.every((expectedItem) => actual.some((actualItem) => isSubObject(actualItem, expectedItem))); 157 } 158 159 for (const key in expected) { 160 if (Object.prototype.hasOwnProperty.call(expected, key)) { 161 if (!Object.prototype.hasOwnProperty.call(actual, key)) { 162 return false; 163 } 164 if (!isSubObject(actual[key], expected[key])) { 165 return false; 166 } 167 } 168 } 169 170 return true; 171} 172 173function performComparison( 174 normalizedActual: unknown, 175 expected: unknown, 176 options: ComparisonOptions = {} 177): ComparisonOutcome { 178 const { subMatch = false } = options; 179 if (subMatch) { 180 if (isSubObject(normalizedActual, expected)) { 181 return { passed: true }; 182 } 183 return { 184 passed: false, 185 expectedJSON: JSON.stringify(expected, null, 2), 186 actualJSON: JSON.stringify(normalizedActual, null, 2) 187 }; 188 } 189 190 const actualJSON = JSON.stringify(normalizedActual, null, 2); 191 const expectedJSON = JSON.stringify(expected, null, 2); 192 193 if (actualJSON === expectedJSON) { 194 return { passed: true }; 195 } 196 197 return { 198 passed: false, 199 expectedJSON: expectedJSON, 200 actualJSON: actualJSON 201 }; 202} 203 204function compareResultsHelper( 205 testName: string, 206 normalizedActual: unknown, 207 expected: unknown, 208 options: ComparisonOptions = {} 209): boolean { 210 const comparison = performComparison(normalizedActual, expected, options); 211 212 if (comparison.passed) { 213 console.log(`[${testName}] ✅ Passed`); 214 return true; 215 } 216 217 console.log(`[${testName}] ❌ Failed`); 218 console.log(`Expected: ${comparison.expectedJSON}`); 219 console.log(`Actual: ${comparison.actualJSON}`); 220 return false; 221} 222 223function compareGetCompletionResult(testName: string, actual: unknown, expected: unknown): boolean { 224 const completionResult = actual as LspCompletionInfo; 225 const actualEntries = completionResult.entries as LspCompletionEntry[]; 226 const expectedEntries = expected as { 227 name: string; 228 sortText: string; 229 insertText: string; 230 kind: number; 231 data: null; 232 }[]; 233 234 return compareResultsHelper(testName, normalizeData(actualEntries), expectedEntries, { 235 subMatch: true 236 } as ComparisonOptions); 237} 238 239function findTextDefinitionPosition(sourceCode: string): number { 240 const textDefinitionPattern = /export\s+declare\s+function\s+Text\(/; 241 const match = textDefinitionPattern.exec(sourceCode); 242 if (match) { 243 const functionTextPattern = /function\s+Text\(/; 244 const subMatch = functionTextPattern.exec(sourceCode.substring(match.index)); 245 if (subMatch) { 246 const positionOfT = match.index + subMatch.index + 'function '.length; 247 return positionOfT; 248 } 249 } 250 throw new Error('Could not find Text definition in source code'); 251} 252 253// CC-OFFNXT(huge_cyclomatic_complexity, huge_depth, huge_method) false positive 254function findTaskDefinitionPosition(sourceCode: string): number { 255 const taskDefinitionPattern = /export\s+class\s+Task\s+{/; 256 const match = taskDefinitionPattern.exec(sourceCode); 257 if (match) { 258 const classTaskPattern = /class\s+Task\s+{/; 259 const subMatch = classTaskPattern.exec(sourceCode.substring(match.index)); 260 if (subMatch) { 261 const positionOfT = match.index + subMatch.index + 'class '.length; 262 return positionOfT; 263 } 264 } 265 throw new Error('Could not find Task definition in source code'); 266} 267 268function compareGetDefinitionResult(testName: string, actual: any, expected: Record<string, string | number>): boolean { 269 // This is the definition info for the UI component. 270 // File in the SDK might changed, so the offset needs to be checked dynamically. 271 if (expected['fileName'] === 'text.d.ets') { 272 const actualDef = actual as LspDefinitionData; 273 const fileName = actualDef.fileName as string; 274 const fileContent = fs.readFileSync(fileName, 'utf8'); 275 const expectedStart = findTextDefinitionPosition(fileContent); 276 const expectedResult = { 277 ...expected, 278 start: expectedStart 279 }; 280 return compareResultsHelper(testName, normalizeData(actual), expectedResult); 281 } 282 // This is the definition info for the class in std library. 283 // File in the SDK might changed, so the offset needs to be checked dynamically. 284 if (expected['fileName'] === 'taskpool.ets') { 285 const actualDef = actual as LspDefinitionData; 286 const fileName = actualDef.fileName as string; 287 const fileContent = fs.readFileSync(fileName, 'utf8'); 288 const expectedStart = findTaskDefinitionPosition(fileContent); 289 const expectedResult = { 290 ...expected, 291 start: expectedStart 292 }; 293 return compareResultsHelper(testName, normalizeData(actual), expectedResult); 294 } 295 return compareResultsHelper(testName, normalizeData(actual), expected); 296} 297 298// CC-OFFNXT(no_explicit_any) project code style 299function compareResults(testName: string, index: string, actual: unknown, expected: unknown): boolean { 300 const name = `${testName}:${index}`; 301 if (testName === 'getDefinitionAtPosition') { 302 return compareGetDefinitionResult(name, actual, expected as Record<string, string | number>); 303 } 304 if (testName === 'getCompletionAtPosition') { 305 return compareGetCompletionResult(name, actual, expected); 306 } 307 308 return compareResultsHelper(name, normalizeData(actual), expected); 309} 310 311function runTests(testDir: string, lsp: Lsp) { 312 console.log('Running tests...'); 313 const testCases = JSON.parse(fs.readFileSync(path.join(testDir, 'testcases', 'cases.json'), 'utf-8')) as TestCases; 314 if (!testCases) { 315 console.error('Failed to load test cases'); 316 return; 317 } 318 319 let failedList: string[] = []; 320 for (const [testName, testConfig] of Object.entries(testCases)) { 321 const { expectedFilePath, ...testCaseVariants } = testConfig; 322 const expectedResult = getExpectedResult(expectedFilePath); 323 if (expectedResult === null) { 324 console.error(`[${testName}] Skipped (expected result not found)`); 325 continue; 326 } 327 // CC-OFFNXT(no_explicit_any) project code style 328 if (typeof (lsp as any)[testName] !== 'function') { 329 console.error(`[${testName}] ❌ Error: Method "${testName}" not found on Lsp object`); 330 continue; 331 } 332 333 for (const [index, params] of Object.entries(testCaseVariants)) { 334 let pass = false; 335 let actualResult = null; 336 try { 337 // CC-OFFNXT(no_explicit_any) project code style 338 actualResult = (lsp as any)[testName](...params); 339 actualResult = sortActualResult(testName, actualResult); 340 pass = compareResults(testName, index, actualResult, expectedResult[index]); 341 } catch (error) { 342 console.error(`[${testName}:${index}] ❌ Error: ${error}`); 343 } 344 if (!pass) { 345 failedList.push(`${testName}:${index}`); 346 } 347 if (!pass && updateMode) { 348 console.log(`Updating expected result for ${testName}:${index}`); 349 expectedResult[index] = normalizeData(actualResult); 350 } 351 } 352 if (updateMode) { 353 fs.writeFileSync(expectedFilePath, JSON.stringify(expectedResult, null, 2)); 354 } 355 console.log(`Finished test: ${testName}`); 356 console.log('-----------------------------------'); 357 } 358 console.log('Tests completed.'); 359 if (failedList.length > 0) { 360 console.log('❌ Failed tests:'); 361 failedList.forEach((failedCase) => { 362 console.log(`- ${failedCase}`); 363 }); 364 } 365} 366 367if (require.main === module) { 368 if (process.argv.length < 3) { 369 console.error('Usage: node run_tests.js <test_directory>'); 370 process.exit(1); 371 } 372 // If update flag is passed, update the expected result files 373 if (process.argv[3] && process.argv[3] === '--update') { 374 updateMode = true; 375 } 376 const testDir = path.resolve(process.argv[2]); 377 checkEnvironment(testDir); 378 const buildSdkPath = path.join(testDir, 'ets', 'ets1.2'); 379 const projectRoot = path.join(testDir, 'testcases'); 380 const modules = getModules(projectRoot); 381 382 generateArkTsConfigByModules(buildSdkPath, projectRoot, modules); 383 const lsp = new Lsp(projectRoot); 384 385 process.env.BINDINGS_PATH = path.join(buildSdkPath, 'build-tools', 'bindings'); 386 runTests(testDir, lsp); 387} 388