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