13af6ab5fSopenharmony_ci/*
23af6ab5fSopenharmony_ci * Copyright (c) 2022-2024 Huawei Device Co., Ltd.
33af6ab5fSopenharmony_ci * Licensed under the Apache License, Version 2.0 (the "License");
43af6ab5fSopenharmony_ci * you may not use this file except in compliance with the License.
53af6ab5fSopenharmony_ci * You may obtain a copy of the License at
63af6ab5fSopenharmony_ci *
73af6ab5fSopenharmony_ci * http://www.apache.org/licenses/LICENSE-2.0
83af6ab5fSopenharmony_ci *
93af6ab5fSopenharmony_ci * Unless required by applicable law or agreed to in writing, software
103af6ab5fSopenharmony_ci * distributed under the License is distributed on an "AS IS" BASIS,
113af6ab5fSopenharmony_ci * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
123af6ab5fSopenharmony_ci * See the License for the specific language governing permissions and
133af6ab5fSopenharmony_ci * limitations under the License.
143af6ab5fSopenharmony_ci */
153af6ab5fSopenharmony_ci
163af6ab5fSopenharmony_ciimport { Logger } from '../lib/Logger';
173af6ab5fSopenharmony_ciimport { LoggerImpl } from './LoggerImpl';
183af6ab5fSopenharmony_ciLogger.init(new LoggerImpl());
193af6ab5fSopenharmony_ci
203af6ab5fSopenharmony_ciimport * as fs from 'node:fs';
213af6ab5fSopenharmony_ciimport * as path from 'node:path';
223af6ab5fSopenharmony_ciimport * as ts from 'typescript';
233af6ab5fSopenharmony_ciimport type { CommandLineOptions } from '../lib/CommandLineOptions';
243af6ab5fSopenharmony_ciimport { lint } from '../lib/LinterRunner';
253af6ab5fSopenharmony_ciimport { TypeScriptLinter } from '../lib/TypeScriptLinter';
263af6ab5fSopenharmony_ciimport type { Autofix } from '../lib/autofixes/Autofixer';
273af6ab5fSopenharmony_ciimport { parseCommandLine } from './CommandLineParser';
283af6ab5fSopenharmony_ciimport { compileLintOptions } from './Compiler';
293af6ab5fSopenharmony_ciimport { getEtsLoaderPath } from './LinterCLI';
303af6ab5fSopenharmony_ciimport { ProblemSeverity } from '../lib/ProblemSeverity';
313af6ab5fSopenharmony_ci
323af6ab5fSopenharmony_ciconst TEST_DIR = 'test';
333af6ab5fSopenharmony_ciconst TAB = '    ';
343af6ab5fSopenharmony_ci
353af6ab5fSopenharmony_ciinterface TestNodeInfo {
363af6ab5fSopenharmony_ci  line: number;
373af6ab5fSopenharmony_ci  column: number;
383af6ab5fSopenharmony_ci  endLine: number;
393af6ab5fSopenharmony_ci  endColumn: number;
403af6ab5fSopenharmony_ci  problem: string;
413af6ab5fSopenharmony_ci  autofix?: Autofix[];
423af6ab5fSopenharmony_ci  suggest?: string;
433af6ab5fSopenharmony_ci  rule?: string;
443af6ab5fSopenharmony_ci  severity?: string;
453af6ab5fSopenharmony_ci}
463af6ab5fSopenharmony_ci
473af6ab5fSopenharmony_cienum Mode {
483af6ab5fSopenharmony_ci  DEFAULT,
493af6ab5fSopenharmony_ci  AUTOFIX
503af6ab5fSopenharmony_ci}
513af6ab5fSopenharmony_ci
523af6ab5fSopenharmony_ciconst RESULT_EXT: string[] = [];
533af6ab5fSopenharmony_ciRESULT_EXT[Mode.DEFAULT] = '.json';
543af6ab5fSopenharmony_ciRESULT_EXT[Mode.AUTOFIX] = '.autofix.json';
553af6ab5fSopenharmony_ciconst AUTOFIX_SKIP_EXT = '.autofix.skip';
563af6ab5fSopenharmony_ciconst ARGS_CONFIG_EXT = '.args.json';
573af6ab5fSopenharmony_ciconst DIFF_EXT = '.diff';
583af6ab5fSopenharmony_ciconst testExtensionSts = '.sts';
593af6ab5fSopenharmony_ciconst testExtensionDSts = '.d.sts';
603af6ab5fSopenharmony_ci
613af6ab5fSopenharmony_cifunction runTests(testDirs: string[]): number {
623af6ab5fSopenharmony_ci
633af6ab5fSopenharmony_ci  /*
643af6ab5fSopenharmony_ci   * Set the IDE mode manually to enable storing information
653af6ab5fSopenharmony_ci   * about found bad nodes and also disable the log output.
663af6ab5fSopenharmony_ci   */
673af6ab5fSopenharmony_ci  TypeScriptLinter.ideMode = true;
683af6ab5fSopenharmony_ci  TypeScriptLinter.testMode = true;
693af6ab5fSopenharmony_ci
703af6ab5fSopenharmony_ci  let hasComparisonFailures = false;
713af6ab5fSopenharmony_ci  let passed = 0;
723af6ab5fSopenharmony_ci  let failed = 0;
733af6ab5fSopenharmony_ci  // Get tests from test directory
743af6ab5fSopenharmony_ci  if (!testDirs?.length) {
753af6ab5fSopenharmony_ci    // eslint-disable-next-line no-param-reassign
763af6ab5fSopenharmony_ci    testDirs = [TEST_DIR];
773af6ab5fSopenharmony_ci  }
783af6ab5fSopenharmony_ci  for (const testDir of testDirs) {
793af6ab5fSopenharmony_ci    const testFiles: string[] = fs.readdirSync(testDir).filter((x) => {
803af6ab5fSopenharmony_ci      return (
813af6ab5fSopenharmony_ci        x.trimEnd().endsWith(ts.Extension.Ts) && !x.trimEnd().endsWith(ts.Extension.Dts) ||
823af6ab5fSopenharmony_ci        x.trimEnd().endsWith(ts.Extension.Tsx) ||
833af6ab5fSopenharmony_ci        x.trimEnd().endsWith(ts.Extension.Ets) ||
843af6ab5fSopenharmony_ci        x.trimEnd().endsWith(testExtensionSts) && !x.trimEnd().endsWith(testExtensionDSts)
853af6ab5fSopenharmony_ci      );
863af6ab5fSopenharmony_ci    });
873af6ab5fSopenharmony_ci    Logger.info(`\nProcessing "${testDir}" directory:\n`);
883af6ab5fSopenharmony_ci    // Run each test in Default and Autofix mode:
893af6ab5fSopenharmony_ci    [passed, failed, hasComparisonFailures] = runTestFiles(testFiles, testDir);
903af6ab5fSopenharmony_ci  }
913af6ab5fSopenharmony_ci  Logger.info(`\nSUMMARY: ${passed + failed} total, ${passed} passed or skipped, ${failed} failed.`);
923af6ab5fSopenharmony_ci  Logger.info(failed > 0 ? '\nTEST FAILED' : '\nTEST SUCCESSFUL');
933af6ab5fSopenharmony_ci  process.exit(hasComparisonFailures ? -1 : 0);
943af6ab5fSopenharmony_ci}
953af6ab5fSopenharmony_ci
963af6ab5fSopenharmony_cifunction runTestFiles(testFiles: string[], testDir: string): [number, number, boolean] {
973af6ab5fSopenharmony_ci  let hasComparisonFailures = false;
983af6ab5fSopenharmony_ci  let passed = 0;
993af6ab5fSopenharmony_ci  let failed = 0;
1003af6ab5fSopenharmony_ci  for (const testFile of testFiles) {
1013af6ab5fSopenharmony_ci    let renamed = false;
1023af6ab5fSopenharmony_ci    let tsName = testFile;
1033af6ab5fSopenharmony_ci    if (testFile.includes(testExtensionSts)) {
1043af6ab5fSopenharmony_ci      renamed = true;
1053af6ab5fSopenharmony_ci      tsName = testFile.replace(testExtensionSts, ts.Extension.Ts);
1063af6ab5fSopenharmony_ci      fs.renameSync(path.join(testDir, testFile), path.join(testDir, tsName));
1073af6ab5fSopenharmony_ci    }
1083af6ab5fSopenharmony_ci    if (runTest(testDir, tsName, Mode.DEFAULT)) {
1093af6ab5fSopenharmony_ci      failed++;
1103af6ab5fSopenharmony_ci      hasComparisonFailures = true;
1113af6ab5fSopenharmony_ci    } else {
1123af6ab5fSopenharmony_ci      passed++;
1133af6ab5fSopenharmony_ci    }
1143af6ab5fSopenharmony_ci    if (runTest(testDir, tsName, Mode.AUTOFIX)) {
1153af6ab5fSopenharmony_ci      failed++;
1163af6ab5fSopenharmony_ci      hasComparisonFailures = true;
1173af6ab5fSopenharmony_ci    } else {
1183af6ab5fSopenharmony_ci      passed++;
1193af6ab5fSopenharmony_ci    }
1203af6ab5fSopenharmony_ci    if (renamed) {
1213af6ab5fSopenharmony_ci      fs.renameSync(path.join(testDir, tsName), path.join(testDir, testFile));
1223af6ab5fSopenharmony_ci    }
1233af6ab5fSopenharmony_ci  }
1243af6ab5fSopenharmony_ci  return [passed, failed, hasComparisonFailures];
1253af6ab5fSopenharmony_ci}
1263af6ab5fSopenharmony_ci
1273af6ab5fSopenharmony_cifunction parseArgs(testDir: string, testFile: string, mode: Mode): CommandLineOptions {
1283af6ab5fSopenharmony_ci  // Configure test parameters and run linter.
1293af6ab5fSopenharmony_ci  const cmdArgs: string[] = [path.join(testDir, testFile)];
1303af6ab5fSopenharmony_ci  const argsFileName = path.join(testDir, testFile + ARGS_CONFIG_EXT);
1313af6ab5fSopenharmony_ci
1323af6ab5fSopenharmony_ci  if (fs.existsSync(argsFileName)) {
1333af6ab5fSopenharmony_ci    const data = fs.readFileSync(argsFileName).toString();
1343af6ab5fSopenharmony_ci    const args = JSON.parse(data);
1353af6ab5fSopenharmony_ci    if (args.testMode !== undefined) {
1363af6ab5fSopenharmony_ci      TypeScriptLinter.testMode = args.testMode;
1373af6ab5fSopenharmony_ci    }
1383af6ab5fSopenharmony_ci    if (args.arkts2 === true) {
1393af6ab5fSopenharmony_ci      cmdArgs.push('--arkts-2');
1403af6ab5fSopenharmony_ci    }
1413af6ab5fSopenharmony_ci  }
1423af6ab5fSopenharmony_ci
1433af6ab5fSopenharmony_ci  if (mode === Mode.AUTOFIX) {
1443af6ab5fSopenharmony_ci    cmdArgs.push('--autofix');
1453af6ab5fSopenharmony_ci  }
1463af6ab5fSopenharmony_ci
1473af6ab5fSopenharmony_ci  return parseCommandLine(cmdArgs);
1483af6ab5fSopenharmony_ci}
1493af6ab5fSopenharmony_ci
1503af6ab5fSopenharmony_cifunction compareExpectedAndActual(testDir: string, testFile: string, mode: Mode, resultNodes: TestNodeInfo[]): string {
1513af6ab5fSopenharmony_ci  // Read file with expected test result.
1523af6ab5fSopenharmony_ci  let expectedResult: { nodes: TestNodeInfo[] };
1533af6ab5fSopenharmony_ci  let diff: string = '';
1543af6ab5fSopenharmony_ci  const resultExt = RESULT_EXT[mode];
1553af6ab5fSopenharmony_ci  const testResultFileName = testFile + resultExt;
1563af6ab5fSopenharmony_ci  try {
1573af6ab5fSopenharmony_ci    const expectedResultFile = fs.readFileSync(path.join(testDir, testResultFileName)).toString();
1583af6ab5fSopenharmony_ci    expectedResult = JSON.parse(expectedResultFile);
1593af6ab5fSopenharmony_ci
1603af6ab5fSopenharmony_ci    if (!expectedResult?.nodes || expectedResult.nodes.length !== resultNodes.length) {
1613af6ab5fSopenharmony_ci      const expectedResultCount = expectedResult?.nodes ? expectedResult.nodes.length : 0;
1623af6ab5fSopenharmony_ci      diff = `Expected count: ${expectedResultCount} vs actual count: ${resultNodes.length}`;
1633af6ab5fSopenharmony_ci      Logger.info(`${TAB}${diff}`);
1643af6ab5fSopenharmony_ci    } else {
1653af6ab5fSopenharmony_ci      diff = expectedAndActualMatch(expectedResult.nodes, resultNodes);
1663af6ab5fSopenharmony_ci    }
1673af6ab5fSopenharmony_ci
1683af6ab5fSopenharmony_ci    if (diff) {
1693af6ab5fSopenharmony_ci      Logger.info(`${TAB}Test failed. Expected and actual results differ.`);
1703af6ab5fSopenharmony_ci    }
1713af6ab5fSopenharmony_ci  } catch (error) {
1723af6ab5fSopenharmony_ci    Logger.info(`${TAB}Test failed. ` + error);
1733af6ab5fSopenharmony_ci  }
1743af6ab5fSopenharmony_ci
1753af6ab5fSopenharmony_ci  return diff;
1763af6ab5fSopenharmony_ci}
1773af6ab5fSopenharmony_ci
1783af6ab5fSopenharmony_cifunction runTest(testDir: string, testFile: string, mode: Mode): boolean {
1793af6ab5fSopenharmony_ci  if (mode === Mode.AUTOFIX && fs.existsSync(path.join(testDir, testFile + AUTOFIX_SKIP_EXT))) {
1803af6ab5fSopenharmony_ci    Logger.info(`Skipping test ${testFile} (${Mode[mode]} mode)`);
1813af6ab5fSopenharmony_ci    return false;
1823af6ab5fSopenharmony_ci  }
1833af6ab5fSopenharmony_ci  Logger.info(`Running test ${testFile} (${Mode[mode]} mode)`);
1843af6ab5fSopenharmony_ci
1853af6ab5fSopenharmony_ci  TypeScriptLinter.initGlobals();
1863af6ab5fSopenharmony_ci
1873af6ab5fSopenharmony_ci  const currentTestMode = TypeScriptLinter.testMode;
1883af6ab5fSopenharmony_ci
1893af6ab5fSopenharmony_ci  const cmdOptions = parseArgs(testDir, testFile, mode);
1903af6ab5fSopenharmony_ci  const lintOptions = compileLintOptions(cmdOptions);
1913af6ab5fSopenharmony_ci  lintOptions.compatibleSdkVersion = '12';
1923af6ab5fSopenharmony_ci  lintOptions.compatibleSdkVersionStage = 'beta3';
1933af6ab5fSopenharmony_ci  const result = lint(lintOptions, getEtsLoaderPath(lintOptions));
1943af6ab5fSopenharmony_ci  const fileProblems = result.problemsInfos.get(path.normalize(cmdOptions.inputFiles[0]));
1953af6ab5fSopenharmony_ci  if (fileProblems === undefined) {
1963af6ab5fSopenharmony_ci    return true;
1973af6ab5fSopenharmony_ci  }
1983af6ab5fSopenharmony_ci
1993af6ab5fSopenharmony_ci  TypeScriptLinter.testMode = currentTestMode;
2003af6ab5fSopenharmony_ci
2013af6ab5fSopenharmony_ci  // Get list of bad nodes from the current run.
2023af6ab5fSopenharmony_ci  const resultNodes: TestNodeInfo[] = fileProblems.map<TestNodeInfo>((x) => {
2033af6ab5fSopenharmony_ci    return {
2043af6ab5fSopenharmony_ci      line: x.line,
2053af6ab5fSopenharmony_ci      column: x.column,
2063af6ab5fSopenharmony_ci      endLine: x.endLine,
2073af6ab5fSopenharmony_ci      endColumn: x.endColumn,
2083af6ab5fSopenharmony_ci      problem: x.problem,
2093af6ab5fSopenharmony_ci      autofix: mode === Mode.AUTOFIX ? x.autofix : undefined,
2103af6ab5fSopenharmony_ci      suggest: x.suggest,
2113af6ab5fSopenharmony_ci      rule: x.rule,
2123af6ab5fSopenharmony_ci      severity: ProblemSeverity[x.severity]
2133af6ab5fSopenharmony_ci    };
2143af6ab5fSopenharmony_ci  });
2153af6ab5fSopenharmony_ci
2163af6ab5fSopenharmony_ci  // Read file with expected test result.
2173af6ab5fSopenharmony_ci  const testResult = compareExpectedAndActual(testDir, testFile, mode, resultNodes);
2183af6ab5fSopenharmony_ci
2193af6ab5fSopenharmony_ci  // Write file with actual test results.
2203af6ab5fSopenharmony_ci  writeActualResultFile(testDir, testFile, mode, resultNodes, testResult);
2213af6ab5fSopenharmony_ci
2223af6ab5fSopenharmony_ci  return !!testResult;
2233af6ab5fSopenharmony_ci}
2243af6ab5fSopenharmony_ci
2253af6ab5fSopenharmony_cifunction expectedAndActualMatch(expectedNodes: TestNodeInfo[], actualNodes: TestNodeInfo[]): string {
2263af6ab5fSopenharmony_ci  // Compare expected and actual results.
2273af6ab5fSopenharmony_ci  for (let i = 0; i < actualNodes.length; i++) {
2283af6ab5fSopenharmony_ci    const actual = actualNodes[i];
2293af6ab5fSopenharmony_ci    const expect = expectedNodes[i];
2303af6ab5fSopenharmony_ci    if (!locationMatch(expect, actual) || actual.problem !== expect.problem) {
2313af6ab5fSopenharmony_ci      return reportDiff(expect, actual);
2323af6ab5fSopenharmony_ci    }
2333af6ab5fSopenharmony_ci    if (!autofixArraysMatch(expect.autofix, actual.autofix)) {
2343af6ab5fSopenharmony_ci      return reportDiff(expect, actual);
2353af6ab5fSopenharmony_ci    }
2363af6ab5fSopenharmony_ci    if (expect.suggest && actual.suggest !== expect.suggest) {
2373af6ab5fSopenharmony_ci      return reportDiff(expect, actual);
2383af6ab5fSopenharmony_ci    }
2393af6ab5fSopenharmony_ci    if (expect.rule && actual.rule !== expect.rule) {
2403af6ab5fSopenharmony_ci      return reportDiff(expect, actual);
2413af6ab5fSopenharmony_ci    }
2423af6ab5fSopenharmony_ci    if (expect.severity && actual.severity !== expect.severity) {
2433af6ab5fSopenharmony_ci      return reportDiff(expect, actual);
2443af6ab5fSopenharmony_ci    }
2453af6ab5fSopenharmony_ci  }
2463af6ab5fSopenharmony_ci
2473af6ab5fSopenharmony_ci  return '';
2483af6ab5fSopenharmony_ci}
2493af6ab5fSopenharmony_ci
2503af6ab5fSopenharmony_cifunction locationMatch(expected: TestNodeInfo, actual: TestNodeInfo): boolean {
2513af6ab5fSopenharmony_ci  return (
2523af6ab5fSopenharmony_ci    actual.line === expected.line ||
2533af6ab5fSopenharmony_ci    actual.column === expected.column ||
2543af6ab5fSopenharmony_ci    !!(expected.endLine && actual.endLine === expected.endLine) ||
2553af6ab5fSopenharmony_ci    !!(expected.endColumn && actual.endColumn === expected.endColumn)
2563af6ab5fSopenharmony_ci  );
2573af6ab5fSopenharmony_ci}
2583af6ab5fSopenharmony_ci
2593af6ab5fSopenharmony_cifunction autofixArraysMatch(expected: Autofix[] | undefined, actual: Autofix[] | undefined): boolean {
2603af6ab5fSopenharmony_ci  if (!expected && !actual) {
2613af6ab5fSopenharmony_ci    return true;
2623af6ab5fSopenharmony_ci  }
2633af6ab5fSopenharmony_ci  if (!(expected && actual) || expected.length !== actual.length) {
2643af6ab5fSopenharmony_ci    return false;
2653af6ab5fSopenharmony_ci  }
2663af6ab5fSopenharmony_ci  for (let i = 0; i < actual.length; ++i) {
2673af6ab5fSopenharmony_ci    if (
2683af6ab5fSopenharmony_ci      actual[i].start !== expected[i].start ||
2693af6ab5fSopenharmony_ci      actual[i].end !== expected[i].end ||
2703af6ab5fSopenharmony_ci      actual[i].replacementText !== expected[i].replacementText
2713af6ab5fSopenharmony_ci    ) {
2723af6ab5fSopenharmony_ci      return false;
2733af6ab5fSopenharmony_ci    }
2743af6ab5fSopenharmony_ci  }
2753af6ab5fSopenharmony_ci  return true;
2763af6ab5fSopenharmony_ci}
2773af6ab5fSopenharmony_ci
2783af6ab5fSopenharmony_cifunction writeActualResultFile(
2793af6ab5fSopenharmony_ci  testDir: string,
2803af6ab5fSopenharmony_ci  testFile: string,
2813af6ab5fSopenharmony_ci  mode: Mode,
2823af6ab5fSopenharmony_ci  resultNodes: TestNodeInfo[],
2833af6ab5fSopenharmony_ci  diff: string
2843af6ab5fSopenharmony_ci): void {
2853af6ab5fSopenharmony_ci  const actualResultsDir = path.join(testDir, 'results');
2863af6ab5fSopenharmony_ci  const resultExt = RESULT_EXT[mode];
2873af6ab5fSopenharmony_ci  if (!fs.existsSync(actualResultsDir)) {
2883af6ab5fSopenharmony_ci    fs.mkdirSync(actualResultsDir);
2893af6ab5fSopenharmony_ci  }
2903af6ab5fSopenharmony_ci
2913af6ab5fSopenharmony_ci  const actualResultJSON = JSON.stringify({ nodes: resultNodes }, null, 4);
2923af6ab5fSopenharmony_ci  fs.writeFileSync(path.join(actualResultsDir, testFile + resultExt), actualResultJSON);
2933af6ab5fSopenharmony_ci
2943af6ab5fSopenharmony_ci  if (diff) {
2953af6ab5fSopenharmony_ci    fs.writeFileSync(path.join(actualResultsDir, testFile + resultExt + DIFF_EXT), diff);
2963af6ab5fSopenharmony_ci  }
2973af6ab5fSopenharmony_ci}
2983af6ab5fSopenharmony_ci
2993af6ab5fSopenharmony_cifunction reportDiff(expected: TestNodeInfo, actual: TestNodeInfo): string {
3003af6ab5fSopenharmony_ci  const expectedNode = JSON.stringify({ nodes: [expected] }, null, 4);
3013af6ab5fSopenharmony_ci  const actualNode = JSON.stringify({ nodes: [actual] }, null, 4);
3023af6ab5fSopenharmony_ci
3033af6ab5fSopenharmony_ci  const diff = `Expected:
3043af6ab5fSopenharmony_ci${expectedNode}
3053af6ab5fSopenharmony_ciActual:
3063af6ab5fSopenharmony_ci${actualNode}`;
3073af6ab5fSopenharmony_ci
3083af6ab5fSopenharmony_ci  Logger.info(diff);
3093af6ab5fSopenharmony_ci  return diff;
3103af6ab5fSopenharmony_ci}
3113af6ab5fSopenharmony_ci
3123af6ab5fSopenharmony_cirunTests(process.argv.slice(2));
313