oxjs/scripts/extract-tests.js

245 lines
6.6 KiB
JavaScript
Raw Normal View History

#!/usr/bin/env node
/**
* Extract inline tests from OxJS documentation comments
* and convert them to Vitest test files
*/
const fs = require('fs');
const path = require('path');
const { glob } = require('glob');
// Regular expressions from Ox.doc
const re = {
multiline: /\/\*@([\w\W]+?)@?\*\//g,
singleline: /\/\/@\s*(.*?)\s*$/gm,
test: /^\s*>\s+(.+)$/,
expected: /^\s*([^>].*)$/,
item: /^(.+?)\s+<(.+?)>\s+(.+?)$/,
};
/**
* Parse documentation comments from source code
*/
function parseDocComments(source, filename) {
const docs = [];
let match;
// Parse multiline comments
while ((match = re.multiline.exec(source)) !== null) {
const content = match[1];
const doc = parseDocContent(content, filename);
if (doc) docs.push(doc);
}
// Parse single line comments
source.replace(re.singleline, (match, content) => {
const doc = parseDocContent(content, filename);
if (doc) docs.push(doc);
return match;
});
return docs;
}
/**
* Parse documentation content
*/
function parseDocContent(content, filename) {
const lines = content.split('\n');
// Find the first non-empty line that matches the item pattern
let itemMatch = null;
let itemName = 'Unknown';
for (const line of lines) {
const trimmed = line.trim();
if (trimmed) {
itemMatch = trimmed.match(re.item);
if (itemMatch) {
break;
}
}
}
if (!itemMatch) {
// If no item match, still try to extract tests with a generic name
// This handles cases where tests are in script blocks or without proper headers
itemMatch = ['', filename.replace(/.*\//, '').replace('.js', ''), 'tests', ''];
}
const doc = {
name: itemMatch[1],
type: itemMatch[2],
summary: itemMatch[3],
file: filename,
tests: []
};
// Extract tests
let inTest = false;
let currentTest = null;
for (let i = 1; i < lines.length; i++) {
const line = lines[i];
const testMatch = line.match(re.test);
const expectedMatch = line.match(re.expected);
if (testMatch) {
if (currentTest) {
doc.tests.push(currentTest);
}
currentTest = {
statement: testMatch[1],
expected: null
};
inTest = true;
} else if (inTest && expectedMatch) {
if (currentTest) {
currentTest.expected = expectedMatch[1].trim();
doc.tests.push(currentTest);
currentTest = null;
inTest = false;
}
}
}
if (currentTest) {
doc.tests.push(currentTest);
}
return doc.tests.length > 0 ? doc : null;
}
/**
* Convert extracted tests to Vitest format
*/
function generateVitestTest(docs, sourceFile) {
if (docs.length === 0) return null;
const testName = path.basename(sourceFile, '.js');
const tests = [];
tests.push(`import { describe, it, expect, beforeAll } from 'vitest';`);
tests.push(`import '../test-setup.js';\n`);
tests.push(`// Tests extracted from ${sourceFile}\n`);
tests.push(`describe('${testName}', () => {`);
for (const doc of docs) {
if (doc.tests.length === 0) continue;
tests.push(` describe('${doc.name}', () => {`);
for (const test of doc.tests) {
if (!test.expected) continue;
// Escape the test statement for use in test name
const testName = test.statement.replace(/'/g, "\\'").substring(0, 60);
tests.push(` it('${testName}...', async () => {`);
tests.push(` const actual = await evaluateInContext(\`${test.statement.replace(/`/g, '\\`')}\`);`);
tests.push(` const expected = ${test.expected};`);
tests.push(` expect(actual).toEqual(expected);`);
tests.push(` });\n`);
}
tests.push(` });\n`);
}
tests.push(`});`);
return tests.join('\n');
}
/**
* Process a single source file
*/
async function processFile(filePath) {
const source = fs.readFileSync(filePath, 'utf-8');
const docs = parseDocComments(source, filePath);
if (docs.length === 0) return null;
const testContent = generateVitestTest(docs, filePath);
if (!testContent) return null;
// Create test file path
const relativePath = path.relative('source', filePath);
const testPath = path.join('test/extracted', relativePath.replace('.js', '.test.js'));
// Ensure directory exists
fs.mkdirSync(path.dirname(testPath), { recursive: true });
// Write test file
fs.writeFileSync(testPath, testContent);
console.log(`✓ Extracted tests from ${relativePath} -> ${testPath}`);
return testPath;
}
/**
* Create test setup file
*/
function createTestSetup() {
const setupContent = `/**
* Test setup for running extracted OxJS inline tests
*/
// Load OxJS in test environment
import '../source/Ox.js';
// Helper function to evaluate test statements in context
global.evaluateInContext = async function(statement) {
try {
// This will need to be enhanced to handle async tests
// For now, we'll use eval which isn't ideal but matches the original test system
return eval(statement);
} catch (error) {
console.error('Error evaluating:', statement, error);
throw error;
}
};
// Make Ox available globally for tests
global.Ox = window.Ox || global.Ox;
`;
fs.mkdirSync('test', { recursive: true });
fs.writeFileSync('test/test-setup.js', setupContent);
console.log('✓ Created test/test-setup.js');
}
/**
* Main function
*/
async function main() {
console.log('Extracting inline tests from OxJS source files...\n');
// Create test setup
createTestSetup();
// Find all JavaScript files in source
const files = await glob('source/**/*.js', {
ignore: ['**/node_modules/**', '**/min/**', '**/dev/**']
});
console.log(`Found ${files.length} source files to process\n`);
const testFiles = [];
for (const file of files) {
const testFile = await processFile(file);
if (testFile) {
testFiles.push(testFile);
}
}
console.log(`\n✅ Extracted tests from ${testFiles.length} files`);
console.log('\nRun "npm test" to execute the extracted tests');
}
// Run if called directly
if (require.main === module) {
main().catch(console.error);
}
module.exports = { parseDocComments, generateVitestTest };