• 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 * as fs from 'node:fs';
17import * as path from 'node:path';
18import type { LinterConfig } from '../lib/LinterConfig';
19import { Logger } from '../lib/Logger';
20import { compileLintOptions } from '../lib/ts-compiler/Compiler';
21import type { LintRunResult } from '../lib/LintRunResult';
22import { DIFF_EXT, MIGRATE_RESULT_SUFFIX, RESULTS_DIR, TAB } from './Consts';
23import { LintTest } from './LintTest';
24import type { CreateLintTestOptions } from './TestFactory';
25import { readLines } from './FileUtil';
26
27export class MigrateTest extends LintTest {
28  constructor(createLintTestOpts: CreateLintTestOptions) {
29    super(createLintTestOpts);
30  }
31
32  compile(): LinterConfig {
33    this.cmdOptions.linterOptions.migratorMode = true;
34
35    // Set filepath mapping to write migration result at 'results' dir instead of modifying original test file
36    const filePathMap = new Map();
37    filePathMap.set(
38      path.normalize(path.join(this.testDir, this.testFile)),
39      path.normalize(this.getActualMigrateResultsFilePath())
40    );
41    this.cmdOptions.linterOptions.migrationFilePathMap = filePathMap;
42
43    return compileLintOptions(this.cmdOptions);
44  }
45
46  validate(actualLinterResult: LintRunResult): boolean {
47    const validateBase = super.validate(actualLinterResult);
48    const validateMigrateResult = this.validateMigrateResult();
49    return validateBase && validateMigrateResult;
50  }
51
52  private validateMigrateResult(): boolean {
53    this.writeMigrationResultForUnchangedFiles();
54    const resultDiff = this.compareMigrateResult();
55    if (resultDiff) {
56      this.writeMigrateDiff(resultDiff);
57    }
58    return !resultDiff;
59  }
60
61  private compareMigrateResult(): string {
62    let diff: string = '';
63    try {
64      const expectedResult = readFileLines(this.getExpectedMigrateResultsFilePath());
65      const actualResult = readFileLines(this.getActualMigrateResultsFilePath());
66
67      if (expectedResult.length !== actualResult.length) {
68        diff = `Expected lines: ${expectedResult.length} vs actual lines: ${actualResult.length}`;
69      } else {
70        diff = MigrateTest.compareTextLines(expectedResult, actualResult);
71      }
72
73      if (diff) {
74        Logger.info(`${TAB}Migration test failed. Expected and actual results differ:\n${diff}`);
75      }
76    } catch (error) {
77      // Write error message to diff, as non-empty diff indicates that test has failed.
78      diff = (error as Error).message;
79      Logger.info(`${TAB}Failed to compare expected and actual results. ` + diff);
80    }
81
82    return diff;
83  }
84
85  private static compareTextLines(expected: string[], actual: string[]): string {
86    for (let i = 0; i < expected.length && i < actual.length; i++) {
87      if (expected[i] !== actual[i]) {
88        const diff = `Difference at line ${i + 1}
89Expected:
90${expected[i]}
91Actual:
92${actual[i]}`;
93        return diff;
94      }
95    }
96    return '';
97  }
98
99  writeMigrateDiff(diff: string): void {
100    fs.writeFileSync(this.getActualMigrateResultsFilePath() + DIFF_EXT, diff);
101  }
102
103  writeMigrationResultForUnchangedFiles(): void {
104
105    /*
106     * If test file doesn't produce any autofix, the migration result won't be created.
107     * In such case, use original text of the test file as migration result.
108     */
109    const filePathMap = this.cmdOptions.linterOptions.migrationFilePathMap;
110    if (!filePathMap) {
111      return;
112    }
113    filePathMap.forEach((targetFile, srcFile) => {
114      if (fs.existsSync(targetFile)) {
115        return;
116      }
117      fs.copyFileSync(srcFile, targetFile);
118    });
119  }
120
121  private getMigrateResultFileName(): string {
122    return this.testFile + MIGRATE_RESULT_SUFFIX + path.extname(this.testFile);
123  }
124
125  private getExpectedMigrateResultsFilePath(): string {
126    return path.join(this.testDir, this.getMigrateResultFileName());
127  }
128
129  private getActualMigrateResultsFilePath(): string {
130    return path.join(this.testDir, RESULTS_DIR, this.getMigrateResultFileName());
131  }
132}
133
134function readFileLines(filePath: string): string[] {
135  try {
136    return readLines(filePath);
137  } catch (error) {
138    throw new Error(`Failed to process ${filePath}: ${(error as Error).message}`);
139  }
140}
141