229 lines
6.1 KiB
JavaScript
229 lines
6.1 KiB
JavaScript
|
|
#!/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');
|
||
|
|
const firstLine = lines[0].trim();
|
||
|
|
const itemMatch = firstLine.match(re.item);
|
||
|
|
|
||
|
|
if (!itemMatch) return null;
|
||
|
|
|
||
|
|
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 };
|