#!/usr/bin/env node /** * Automated Test Conversion Script * Converts standalone Node.js test scripts to Mocha format * * Usage: * node tests/convert_to_mocha.js [--dry-run] [--pattern 'glob'] */ const fs = require('fs'); const path = require('path'); const glob = require('glob'); // Parse arguments const args = process.argv.slice(2); let dryRun = false; let pattern = 'tests/**/test_*.js'; for (let i = 0; i < args.length; i++) { if (args[i] === '--dry-run') { dryRun = true; } else if (args[i] === '--pattern' && args[i + 1]) { pattern = args[i + 1]; i++; } } console.log('╔════════════════════════════════════════════════════════════╗'); console.log('║ Mocha Test Conversion Script ║'); console.log('╚════════════════════════════════════════════════════════════╝\n'); console.log(`Pattern: ${pattern}`); console.log(`Dry Run: ${dryRun}\n`); /** * Convert a standalone test file to Mocha format */ function convertToMocha(filePath, content) { let converted = content; // Step 1: Remove shebang and convert header converted = converted.replace(/^#!\/usr\/bin\/env node\n/, ''); // Step 2: Remove environment loading boilerplate (replaced by tests/setup.js) const envLoadingPattern = /\/\/ Parse.*?const envPath.*?require\('dotenv'\)\.config\(\{ path: envPath \}\);?\n+/s; converted = converted.replace(envLoadingPattern, ''); // Step 3: Add Mocha/Chai imports if not present if (!converted.includes('require(\'chai\')')) { const firstRequire = converted.search(/^const |^require\(/m); if (firstRequire !== -1) { converted = converted.slice(0, firstRequire) + `const { expect } = require('chai');\n` + converted.slice(firstRequire); } else { converted = `const { expect } = require('chai');\n\n` + converted; } } // Step 4: Extract test name from file path const testName = path.basename(filePath, '.js') .replace(/^test_/, '') .replace(/_/g, ' ') .replace(/\b\w/g, l => l.toUpperCase()); // Step 5: Detect main async function pattern const mainFunctionPattern = /async function (main|test\w+)\(\) \{[\s\S]*?\}\s*(?:main\(\)|test\w+\(\)).*?\.catch/; const hasMainFunction = mainFunctionPattern.test(converted); if (hasMainFunction) { // Extract main function body const mainMatch = converted.match(/async function (main|test\w+)\(\) \{\s*([\s\S]*?)\s*\}\s*(?:main\(\)|test\w+\(\))/); if (mainMatch) { const functionBody = mainMatch[2]; // Wrap in describe block converted = converted.replace( mainFunctionPattern, `describe('${testName}', function() { this.timeout(60000); it('should complete successfully', async function() { ${functionBody.split('\n').map(line => ' ' + line).join('\n')} }); });` ); } } else { // For simple scripts without main function // Try to detect test sections and wrap them const lines = converted.split('\n'); let inTestSection = false; let testSections = []; let currentSection = []; for (const line of lines) { if (line.includes('console.log') && (line.includes('Test ') || line.includes('===') || line.includes('───'))) { if (currentSection.length > 0) { testSections.push(currentSection.join('\n')); currentSection = []; } inTestSection = true; } if (inTestSection) { currentSection.push(line); } } if (currentSection.length > 0) { testSections.push(currentSection.join('\n')); } } // Step 6: Convert console.log assertions to expect() // This is a simple heuristic - manual review still needed converted = converted.replace( /if \((.*?) === (.*?)\) \{\s*console\.log\('✓.*?'\);?\s*\} else \{\s*console\.log\('✗.*?'\);?\s*(?:process\.exit|return)\(1\);?\s*\}/g, 'expect($1).to.equal($2);' ); converted = converted.replace( /if \((.*?) !== (.*?)\) \{\s*console\.log\('✓.*?'\);?\s*\} else \{\s*console\.log\('✗.*?'\);?\s*(?:process\.exit|return)\(1\);?\s*\}/g, 'expect($1).to.not.equal($2);' ); // Step 7: Remove process.exit calls converted = converted.replace(/process\.exit\(\d+\);?/g, ''); converted = converted.replace(/\.then\(exitCode => process\.exit\(exitCode\)\)/g, ''); converted = converted.replace(/\.catch\(error => \{\s*console\.error.*?process\.exit\(1\);?\s*\}\);?/gs, ''); // Step 8: Handle try-catch blocks that return exit codes converted = converted.replace( /try \{([\s\S]*?)\s+return 0;\s*\} catch \(error\) \{[\s\S]*?return 1;\s*\}/g, 'try {$1\n } catch (error) {\n throw error;\n }' ); return converted; } /** * Find all test files matching pattern */ function findTestFiles() { const exclude = [ '**/node_modules/**', '**/run_all_tests.js', '**/organize_tests.js', '**/fix_paths.js', '**/setup.js', '**/convert_to_mocha.js', '**/*.spec.js', '**/manual_*.js' ]; return glob.sync(pattern, { ignore: exclude, absolute: true }); } /** * Main execution */ async function main() { const files = findTestFiles(); if (files.length === 0) { console.log('No test files found matching pattern.'); return; } console.log(`Found ${files.length} test files to convert:\n`); let converted = 0; let skipped = 0; let errors = 0; for (const filePath of files) { const relativePath = path.relative(process.cwd(), filePath); try { // Skip files that are already converted (contain describe/it) const content = fs.readFileSync(filePath, 'utf8'); if (content.includes('describe(') && content.includes('it(')) { console.log(`⊘ ${relativePath} - Already Mocha format`); skipped++; continue; } const convertedContent = convertToMocha(filePath, content); if (dryRun) { console.log(`✓ ${relativePath} - Would convert`); } else { // Backup original const backupPath = filePath + '.pre-mocha-backup'; fs.writeFileSync(backupPath, content, 'utf8'); // Write converted fs.writeFileSync(filePath, convertedContent, 'utf8'); console.log(`✓ ${relativePath} - Converted (backup: ${path.basename(backupPath)})`); } converted++; } catch (error) { console.error(`✗ ${relativePath} - Error: ${error.message}`); errors++; } } console.log('\n' + '═'.repeat(64)); console.log('Summary'); console.log('═'.repeat(64)); console.log(`Converted: ${converted}`); console.log(`Skipped: ${skipped} (already Mocha format)`); console.log(`Errors: ${errors}`); console.log(`Total: ${files.length}`); if (dryRun) { console.log('\n⚠ Dry run mode - no files were modified'); console.log('Run without --dry-run to apply changes'); } else { console.log('\n✓ Conversion complete'); console.log('⚠ IMPORTANT: Review and test converted files manually!'); console.log(' Automated conversion handles common patterns but may need adjustments.'); console.log(` Backups saved with .pre-mocha-backup extension`); } } main().catch(error => { console.error('Fatal error:', error); process.exit(1); });