• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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