feat(runtime): Add runtime adapters, filename runtime parser and migration tool; integrate runtime selection into TsTest and add tests
This commit is contained in:
316
ts/tstest.classes.migration.ts
Normal file
316
ts/tstest.classes.migration.ts
Normal file
@@ -0,0 +1,316 @@
|
||||
import * as plugins from './tstest.plugins.js';
|
||||
import { coloredString as cs } from '@push.rocks/consolecolor';
|
||||
import { parseTestFilename, getLegacyMigrationTarget, isLegacyFilename } from './tstest.classes.runtime.parser.js';
|
||||
|
||||
/**
|
||||
* Migration result for a single file
|
||||
*/
|
||||
export interface MigrationResult {
|
||||
/**
|
||||
* Original file path
|
||||
*/
|
||||
oldPath: string;
|
||||
|
||||
/**
|
||||
* New file path after migration
|
||||
*/
|
||||
newPath: string;
|
||||
|
||||
/**
|
||||
* Whether the migration was performed
|
||||
*/
|
||||
migrated: boolean;
|
||||
|
||||
/**
|
||||
* Error message if migration failed
|
||||
*/
|
||||
error?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Migration summary
|
||||
*/
|
||||
export interface MigrationSummary {
|
||||
/**
|
||||
* Total number of legacy files found
|
||||
*/
|
||||
totalLegacyFiles: number;
|
||||
|
||||
/**
|
||||
* Number of files successfully migrated
|
||||
*/
|
||||
migratedCount: number;
|
||||
|
||||
/**
|
||||
* Number of files that failed to migrate
|
||||
*/
|
||||
errorCount: number;
|
||||
|
||||
/**
|
||||
* Individual migration results
|
||||
*/
|
||||
results: MigrationResult[];
|
||||
|
||||
/**
|
||||
* Whether this was a dry run
|
||||
*/
|
||||
dryRun: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Migration options
|
||||
*/
|
||||
export interface MigrationOptions {
|
||||
/**
|
||||
* Base directory to search for test files
|
||||
* Default: process.cwd()
|
||||
*/
|
||||
baseDir?: string;
|
||||
|
||||
/**
|
||||
* Glob pattern for finding test files
|
||||
* Default: '** /*test*.ts' (without space)
|
||||
*/
|
||||
pattern?: string;
|
||||
|
||||
/**
|
||||
* Dry run mode - don't actually rename files
|
||||
* Default: true
|
||||
*/
|
||||
dryRun?: boolean;
|
||||
|
||||
/**
|
||||
* Verbose output
|
||||
* Default: false
|
||||
*/
|
||||
verbose?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Migration class for renaming legacy test files to new naming convention
|
||||
*
|
||||
* Migrations:
|
||||
* - .browser.ts → .chromium.ts
|
||||
* - .both.ts → .node+chromium.ts
|
||||
* - .both.nonci.ts → .node+chromium.nonci.ts
|
||||
* - .browser.nonci.ts → .chromium.nonci.ts
|
||||
*/
|
||||
export class Migration {
|
||||
private options: Required<MigrationOptions>;
|
||||
|
||||
constructor(options: MigrationOptions = {}) {
|
||||
this.options = {
|
||||
baseDir: options.baseDir || process.cwd(),
|
||||
pattern: options.pattern || '**/test*.ts',
|
||||
dryRun: options.dryRun !== undefined ? options.dryRun : true,
|
||||
verbose: options.verbose || false,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Find all legacy test files in the base directory
|
||||
*/
|
||||
async findLegacyFiles(): Promise<string[]> {
|
||||
const files = await plugins.smartfile.fs.listFileTree(
|
||||
this.options.baseDir,
|
||||
this.options.pattern
|
||||
);
|
||||
|
||||
const legacyFiles: string[] = [];
|
||||
|
||||
for (const file of files) {
|
||||
const fileName = plugins.path.basename(file);
|
||||
if (isLegacyFilename(fileName)) {
|
||||
const absolutePath = plugins.path.isAbsolute(file)
|
||||
? file
|
||||
: plugins.path.join(this.options.baseDir, file);
|
||||
legacyFiles.push(absolutePath);
|
||||
}
|
||||
}
|
||||
|
||||
return legacyFiles;
|
||||
}
|
||||
|
||||
/**
|
||||
* Migrate a single file
|
||||
*/
|
||||
private async migrateFile(filePath: string): Promise<MigrationResult> {
|
||||
const fileName = plugins.path.basename(filePath);
|
||||
const dirName = plugins.path.dirname(filePath);
|
||||
|
||||
try {
|
||||
// Get the new filename
|
||||
const newFileName = getLegacyMigrationTarget(fileName);
|
||||
|
||||
if (!newFileName) {
|
||||
return {
|
||||
oldPath: filePath,
|
||||
newPath: filePath,
|
||||
migrated: false,
|
||||
error: 'File is not a legacy file',
|
||||
};
|
||||
}
|
||||
|
||||
const newPath = plugins.path.join(dirName, newFileName);
|
||||
|
||||
// Check if target file already exists
|
||||
if (await plugins.smartfile.fs.fileExists(newPath)) {
|
||||
return {
|
||||
oldPath: filePath,
|
||||
newPath,
|
||||
migrated: false,
|
||||
error: `Target file already exists: ${newPath}`,
|
||||
};
|
||||
}
|
||||
|
||||
if (!this.options.dryRun) {
|
||||
// Check if we're in a git repository
|
||||
const isGitRepo = await this.isGitRepository(this.options.baseDir);
|
||||
|
||||
if (isGitRepo) {
|
||||
// Use git mv to preserve history
|
||||
const smartshell = new plugins.smartshell.Smartshell({
|
||||
executor: 'bash',
|
||||
pathDirectories: [],
|
||||
});
|
||||
const gitCommand = `cd "${this.options.baseDir}" && git mv "${filePath}" "${newPath}"`;
|
||||
const result = await smartshell.exec(gitCommand);
|
||||
|
||||
if (result.exitCode !== 0) {
|
||||
throw new Error(`git mv failed: ${result.stderr}`);
|
||||
}
|
||||
} else {
|
||||
// Not a git repository - cannot migrate without git
|
||||
throw new Error('Migration requires a git repository. We have git!');
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
oldPath: filePath,
|
||||
newPath,
|
||||
migrated: true,
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
oldPath: filePath,
|
||||
newPath: filePath,
|
||||
migrated: false,
|
||||
error: error.message,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a directory is a git repository
|
||||
*/
|
||||
private async isGitRepository(dir: string): Promise<boolean> {
|
||||
try {
|
||||
const gitDir = plugins.path.join(dir, '.git');
|
||||
return await plugins.smartfile.fs.isDirectory(gitDir);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Run the migration
|
||||
*/
|
||||
async run(): Promise<MigrationSummary> {
|
||||
const legacyFiles = await this.findLegacyFiles();
|
||||
|
||||
console.log('');
|
||||
console.log(cs('='.repeat(60), 'blue'));
|
||||
console.log(cs('Test File Migration Tool', 'blue'));
|
||||
console.log(cs('='.repeat(60), 'blue'));
|
||||
console.log('');
|
||||
|
||||
if (this.options.dryRun) {
|
||||
console.log(cs('🔍 DRY RUN MODE - No files will be modified', 'orange'));
|
||||
console.log('');
|
||||
}
|
||||
|
||||
console.log(`Found ${legacyFiles.length} legacy test file(s)`);
|
||||
console.log('');
|
||||
|
||||
const results: MigrationResult[] = [];
|
||||
let migratedCount = 0;
|
||||
let errorCount = 0;
|
||||
|
||||
for (const file of legacyFiles) {
|
||||
const result = await this.migrateFile(file);
|
||||
results.push(result);
|
||||
|
||||
if (result.migrated) {
|
||||
migratedCount++;
|
||||
const oldName = plugins.path.basename(result.oldPath);
|
||||
const newName = plugins.path.basename(result.newPath);
|
||||
|
||||
if (this.options.dryRun) {
|
||||
console.log(cs(` Would migrate:`, 'cyan'));
|
||||
} else {
|
||||
console.log(cs(` ✓ Migrated:`, 'green'));
|
||||
}
|
||||
console.log(` ${oldName}`);
|
||||
console.log(cs(` → ${newName}`, 'green'));
|
||||
console.log('');
|
||||
} else if (result.error) {
|
||||
errorCount++;
|
||||
console.log(cs(` ✗ Failed: ${plugins.path.basename(result.oldPath)}`, 'red'));
|
||||
console.log(cs(` ${result.error}`, 'red'));
|
||||
console.log('');
|
||||
}
|
||||
}
|
||||
|
||||
console.log(cs('='.repeat(60), 'blue'));
|
||||
console.log(`Summary:`);
|
||||
console.log(` Total legacy files: ${legacyFiles.length}`);
|
||||
console.log(` Successfully migrated: ${migratedCount}`);
|
||||
console.log(` Errors: ${errorCount}`);
|
||||
console.log(cs('='.repeat(60), 'blue'));
|
||||
|
||||
if (this.options.dryRun && legacyFiles.length > 0) {
|
||||
console.log('');
|
||||
console.log(cs('To apply these changes, run:', 'orange'));
|
||||
console.log(cs(' tstest migrate --write', 'orange'));
|
||||
}
|
||||
|
||||
console.log('');
|
||||
|
||||
return {
|
||||
totalLegacyFiles: legacyFiles.length,
|
||||
migratedCount,
|
||||
errorCount,
|
||||
results,
|
||||
dryRun: this.options.dryRun,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a migration report without performing the migration
|
||||
*/
|
||||
async generateReport(): Promise<string> {
|
||||
const legacyFiles = await this.findLegacyFiles();
|
||||
|
||||
let report = '';
|
||||
report += 'Test File Migration Report\n';
|
||||
report += '='.repeat(60) + '\n';
|
||||
report += '\n';
|
||||
report += `Found ${legacyFiles.length} legacy test file(s)\n`;
|
||||
report += '\n';
|
||||
|
||||
for (const file of legacyFiles) {
|
||||
const fileName = plugins.path.basename(file);
|
||||
const newFileName = getLegacyMigrationTarget(fileName);
|
||||
|
||||
if (newFileName) {
|
||||
report += `${fileName}\n`;
|
||||
report += ` → ${newFileName}\n`;
|
||||
report += '\n';
|
||||
}
|
||||
}
|
||||
|
||||
report += '='.repeat(60) + '\n';
|
||||
|
||||
return report;
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user