Skip to content

Commit

Permalink
Update test structure and enhancements
Browse files Browse the repository at this point in the history
  • Loading branch information
RobAndrewHurst committed Nov 12, 2024
1 parent 36b71f9 commit abd002e
Show file tree
Hide file tree
Showing 8 changed files with 488 additions and 174 deletions.
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

32 changes: 32 additions & 0 deletions src/core/describe.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import chalk from 'chalk';
import { state } from '../state/TestState.js';

/**
* Create a test suite
* @async
* @function describe
* @param {string} description - Description of the test suite
* @param {Function} callback - Suite callback function
* @returns {Promise<void>}
*/
export async function describe(description, callback) {
const suite = {
description,
tests: [],
startTime: performance.now()
};

state.pushSuite(suite);
console.log(chalk.bold.cyan(`\n${description}`));

try {
await Promise.resolve(callback());
} catch (error) {
console.error(chalk.red(`Suite failed: ${description}`));
console.error(chalk.red(error.stack));
} finally {
suite.duration = performance.now() - suite.startTime;
state.testResults.push(suite);
state.popSuite();
}
}
39 changes: 39 additions & 0 deletions src/core/it.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import chalk from 'chalk';
import { state } from '../state/TestState.js';

/**
* Create a test case
* @async
* @function it
* @param {string} description - Test case description
* @param {Function} callback - Test callback function
* @returns {Promise<void>}
* @throws {Error} If called outside a describe block
*/
export async function it(description, callback) {
if (!state.currentSuite) {
throw new Error('Test case defined outside of describe block');
}

const test = {
description,
startTime: performance.now()
};

try {
await Promise.resolve(callback());
test.status = 'passed';
test.duration = performance.now() - test.startTime;
state.passedTests++;
console.log(chalk.green(` ✅ ${description} (${test.duration.toFixed(2)}ms)`));
} catch (error) {
test.status = 'failed';
test.error = error;
test.duration = performance.now() - test.startTime;
state.failedTests++;
console.error(chalk.red(` ⛔ ${description} (${test.duration.toFixed(2)}ms)`));
console.error(chalk.red(` ${error.message}`));
}

state.currentSuite.tests.push(test);
}
111 changes: 111 additions & 0 deletions src/runners/nodeRunner.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import fs from 'fs';
import path from 'path';
import chalk from 'chalk';
import { state } from '../state/TestState.js';
import { excludePattern } from '../util/regex.js';

/**
* Run a single test file
* @async
* @function runTestFile
* @param {string} testFile - Path to test file
* @returns {Promise<void>}
*/
async function runTestFile(testFile) {
try {
const fileUrl = path.isAbsolute(testFile)
? `file://${testFile}`
: `file://${path.resolve(testFile)}`;

await import(fileUrl);
} catch (error) {
console.error(chalk.red(`\nError running test file ${chalk.underline(testFile)}:`));
console.error(chalk.red(error.stack));
state.failedTests++;
}
}

/**
* Run tests in a directory
* @async
* @function runTests
* @param {string} testDirectory - Directory containing tests
* @param {boolean} [returnResults=false] - Whether to return results
* @param {object} [codiConfig={}] - Configuration object
* @returns {Promise<object|void>} Test results if returnResults is true
*/
export async function runTests(testDirectory, returnResults = false, codiConfig = {}) {
state.resetCounters();
state.startTimer();

let testFiles = fs.readdirSync(testDirectory, { recursive: true })
.filter(file => file.endsWith('.mjs'));

if (codiConfig.excludeDirectories) {
const matcher = excludePattern(codiConfig.excludeDirectories);
testFiles = testFiles.filter(file => !matcher(file));
}

console.log(chalk.bold.magenta(`\nRunning tests in directory: ${chalk.underline(testDirectory)}`));
console.log(chalk.bold.magenta(`Found ${testFiles.length} test file(s)\n`));

for (const file of testFiles) {
await runTestFile(path.join(testDirectory, file));
}

const summary = {
passedTests: state.passedTests,
failedTests: state.failedTests,
testResults: state.testResults,
executionTime: state.getExecutionTime()
};

console.log(chalk.bold.cyan('\nTest Summary:'));
console.log(chalk.green(` Passed: ${summary.passedTests}`));
console.log(chalk.red(` Failed: ${summary.failedTests}`));
console.log(chalk.blue(` Time: ${summary.executionTime}s`));

if (returnResults) return summary;

if (state.failedTests > 0) {
console.log(chalk.red('\nSome tests failed.'));
process.exit(1);
} else {
console.log(chalk.green('\nAll tests passed.'));
process.exit(0);
}
}

/**
* Run a single test function
* @async
* @function runTestFunction
* @param {Function} testFn - Test function to run
* @returns {Promise<object>} Test results
*/
export async function runTestFunction(testFn) {
const suite = {
description: `Function: ${testFn.name}`,
tests: [],
startTime: performance.now()
};

state.pushSuite(suite);

try {
await Promise.resolve(testFn());
} catch (error) {
console.error(`Error in test ${testFn.name}:`, error);
state.failedTests++;
} finally {
suite.duration = performance.now() - suite.startTime;
state.testResults.push(suite);
state.popSuite();
}

return {
passedTests: state.passedTests,
failedTests: state.failedTests,
testResults: state.testResults
};
}
175 changes: 175 additions & 0 deletions src/runners/webRunner.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
import { state } from '../state/TestState.js';
import chalk from 'chalk';

/**
* Run a web test file
* @async
* @function runWebTestFile
* @param {string} testFile - Path to test file
* @param {object} [options={}] - Options for running the test
* @returns {Promise<void>}
*/
export async function runWebTestFile(testFile, options = {}) {
const {
timeout = 5000,
silent = false
} = options;

try {
const startTime = performance.now();

const testPromise = import(testFile);
const timeoutPromise = new Promise((_, reject) => {
setTimeout(() => reject(new Error(`Test file ${testFile} timed out after ${timeout}ms`)), timeout);
});

await Promise.race([testPromise, timeoutPromise]);

const duration = performance.now() - startTime;
if (!silent) {
console.log(chalk.green(`✅ ${path.basename(testFile)} (${duration.toFixed(2)}ms)`));
}
} catch (error) {
console.error(`Error running test file ${testFile}:`);
console.error(error.stack);
state.failedTests++;
}
}

/**
* Run web tests
* @async
* @function runWebTests
* @param {string[]} testFiles - Array of test files
* @param {object} [options={}] - Options for running the tests
* @returns {Promise<object>} Test results
*/
export async function runWebTests(testFiles, options = {}) {
const {
parallel = false,
timeout = 5000,
silent = false,
batchSize = 5
} = options;

state.resetCounters();
state.startTimer();

if (!silent) {
console.log(chalk.bold.magenta(`\nRunning ${testFiles.length} web test file(s)`));
}

try {
if (parallel) {
if (batchSize > 0) {
// Run tests in batches
const batches = [];
for (let i = 0; i < testFiles.length; i += batchSize) {
batches.push(testFiles.slice(i, i + batchSize));
}

for (const [index, batch] of batches.entries()) {
if (!silent) {
console.log(chalk.blue(`\nBatch ${index + 1}/${batches.length}`));
}

await Promise.all(
batch.map(file => runWebTestFile(file, { timeout, silent }))
);
}
} else {
// Run all tests in parallel
await Promise.all(
testFiles.map(file => runWebTestFile(file, { timeout, silent }))
);
}
} else {
// Run tests sequentially
for (const file of testFiles) {
await runWebTestFile(file, { timeout, silent });
}
}
} catch (error) {
console.error(chalk.red('\nTest execution failed:'));
console.error(chalk.red(error.stack));
}

const summary = {
totalTests: state.passedTests + state.failedTests,
passedTests: state.passedTests,
failedTests: state.failedTests,
executionTime: state.getExecutionTime(),
testResults: state.testResults
};

if (!silent) {
console.log(chalk.bold.cyan('\nTest Summary:'));
console.log(chalk.blue(` Total: ${summary.totalTests}`));
console.log(chalk.green(` Passed: ${summary.passedTests}`));
console.log(chalk.red(` Failed: ${summary.failedTests}`));
console.log(chalk.blue(` Time: ${summary.executionTime}s`));
}

return summary;
}

/**
* Run a web test function
* @async
* @function runWebTestFunction
* @param {Function} testFn - Test function to run
* @param {object} [options={}] - Options for running the test
* @returns {Promise<object>} Test results
*/
export async function runWebTestFunction(testFn, options = {}) {
const {
timeout = 5000,
silent = false
} = options;

state.resetCounters();
state.startTimer();

const suite = {
description: `Function: ${testFn.name}`,
tests: [],
startTime: performance.now()
};

state.pushSuite(suite);

try {
const testPromise = Promise.resolve(testFn());
const timeoutPromise = new Promise((_, reject) => {
setTimeout(() => reject(new Error(`Test function ${testFn.name} timed out after ${timeout}ms`)), timeout);
});

await Promise.race([testPromise, timeoutPromise]);
} catch (error) {
console.error(chalk.red(`Error in test ${testFn.name}:`));
console.error(chalk.red(error.stack));
state.failedTests++;
} finally {
suite.duration = performance.now() - suite.startTime;
state.testResults.push(suite);
state.popSuite();
}

const summary = {
totalTests: state.passedTests + state.failedTests,
passedTests: state.passedTests,
failedTests: state.failedTests,
executionTime: state.getExecutionTime(),
testResults: state.testResults
};

if (!silent) {
console.log(chalk.bold.cyan('\nTest Summary:'));
console.log(chalk.blue(` Total: ${summary.totalTests}`));
console.log(chalk.green(` Passed: ${summary.passedTests}`));
console.log(chalk.red(` Failed: ${summary.failedTests}`));
console.log(chalk.blue(` Time: ${summary.executionTime}s`));
}

return summary;
}
Loading

0 comments on commit abd002e

Please sign in to comment.