Compare commits

...

4 Commits

6 changed files with 120 additions and 24 deletions

View File

@ -1,5 +1,20 @@
# Changelog # Changelog
## 2025-05-26 - 2.3.0 - feat(cli)
Add '--version' option and warn against global tstest usage in the tstest project
- Introduced a new '--version' CLI flag that prints the version from package.json
- Added logic in ts/index.ts to detect if tstest is run globally within its own project and issue a warning
- Added .claude/settings.local.json to configure allowed permissions for various commands
## 2025-05-26 - 2.2.6 - fix(tstest)
Improve timeout warning timer management and summary output formatting in the test runner.
- Removed the global timeoutWarningTimer and replaced it with local warning timers in runInNode and runInChrome methods.
- Added warnings when test files run for over one minute if no timeout is specified.
- Ensured proper clearing of warning timers on successful completion or timeout.
- Enhanced quiet mode summary output to clearly display passed and failed test counts.
## 2025-05-26 - 2.2.5 - fix(protocol) ## 2025-05-26 - 2.2.5 - fix(protocol)
Fix inline timing metadata parsing and enhance test coverage for performance metrics and timing edge cases Fix inline timing metadata parsing and enhance test coverage for performance metrics and timing edge cases

View File

@ -1,6 +1,6 @@
{ {
"name": "@git.zone/tstest", "name": "@git.zone/tstest",
"version": "2.2.5", "version": "2.3.0",
"private": false, "private": false,
"description": "a test utility to run tests that match test/**/*.ts", "description": "a test utility to run tests that match test/**/*.ts",
"exports": { "exports": {

View File

@ -3,6 +3,6 @@
*/ */
export const commitinfo = { export const commitinfo = {
name: '@git.zone/tstest', name: '@git.zone/tstest',
version: '2.2.5', version: '2.3.0',
description: 'a test utility to run tests that match test/**/*.ts' description: 'a test utility to run tests that match test/**/*.ts'
} }

View File

@ -8,6 +8,40 @@ export enum TestExecutionMode {
} }
export const runCli = async () => { export const runCli = async () => {
// Check if we're using global tstest in the tstest project itself
try {
const packageJsonPath = `${process.cwd()}/package.json`;
const fs = await import('fs');
if (fs.existsSync(packageJsonPath)) {
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
if (packageJson.name === '@git.zone/tstest') {
// Check if we're running from a global installation
const execPath = process.argv[1];
// Debug: log the paths (uncomment for debugging)
// console.log('DEBUG: Checking global tstest usage...');
// console.log('execPath:', execPath);
// console.log('cwd:', process.cwd());
// console.log('process.argv:', process.argv);
// Check if this is running from global installation
const isLocalCli = execPath.includes(process.cwd());
const isGlobalPnpm = process.argv.some(arg => arg.includes('.pnpm') && !arg.includes(process.cwd()));
const isGlobalNpm = process.argv.some(arg => arg.includes('npm/node_modules') && !arg.includes(process.cwd()));
if (!isLocalCli && (isGlobalPnpm || isGlobalNpm || !execPath.includes('node_modules'))) {
console.error('\n⚠ WARNING: You are using a globally installed tstest in the tstest project itself!');
console.error(' This means you are NOT testing your local changes.');
console.error(' Please use one of these commands instead:');
console.error(' • node cli.js <test-path>');
console.error(' • pnpm test <test-path>');
console.error(' • ./cli.js <test-path> (if executable)\n');
}
}
}
} catch (error) {
// Silently ignore any errors in this check
}
// Parse command line arguments // Parse command line arguments
const args = process.argv.slice(2); const args = process.argv.slice(2);
const logOptions: LogOptions = {}; const logOptions: LogOptions = {};
@ -24,6 +58,18 @@ export const runCli = async () => {
const arg = args[i]; const arg = args[i];
switch (arg) { switch (arg) {
case '--version':
// Get version from package.json
try {
const fs = await import('fs');
const packagePath = new URL('../package.json', import.meta.url).pathname;
const packageData = JSON.parse(await fs.promises.readFile(packagePath, 'utf8'));
console.log(`tstest version ${packageData.version}`);
} catch (error) {
console.log('tstest version unknown');
}
process.exit(0);
break;
case '--quiet': case '--quiet':
case '-q': case '-q':
logOptions.quiet = true; logOptions.quiet = true;
@ -115,6 +161,7 @@ export const runCli = async () => {
console.error('You must specify a test directory/file/pattern as argument. Please try again.'); console.error('You must specify a test directory/file/pattern as argument. Please try again.');
console.error('\nUsage: tstest <path> [options]'); console.error('\nUsage: tstest <path> [options]');
console.error('\nOptions:'); console.error('\nOptions:');
console.error(' --version Show version information');
console.error(' --quiet, -q Minimal output'); console.error(' --quiet, -q Minimal output');
console.error(' --verbose, -v Verbose output'); console.error(' --verbose, -v Verbose output');
console.error(' --no-color Disable colored output'); console.error(' --no-color Disable colored output');

View File

@ -18,7 +18,6 @@ export class TsTest {
public startFromFile: number | null; public startFromFile: number | null;
public stopAtFile: number | null; public stopAtFile: number | null;
public timeoutSeconds: number | null; public timeoutSeconds: number | null;
private timeoutWarningTimer: NodeJS.Timeout | null = null;
public smartshellInstance = new plugins.smartshell.Smartshell({ public smartshellInstance = new plugins.smartshell.Smartshell({
executor: 'bash', executor: 'bash',
@ -45,15 +44,6 @@ export class TsTest {
await this.movePreviousLogFiles(); await this.movePreviousLogFiles();
} }
// Start timeout warning timer if no timeout was specified
if (this.timeoutSeconds === null) {
this.timeoutWarningTimer = setTimeout(() => {
this.logger.warning('Test is running for more than 1 minute.');
this.logger.warning('Consider using --timeout option to set a timeout for test files.');
this.logger.warning('Example: tstest test --timeout=300 (for 5 minutes)');
}, 60000); // 1 minute
}
const testGroups = await this.testDir.getTestFileGroups(); const testGroups = await this.testDir.getTestFileGroups();
const allFiles = [...testGroups.serial, ...Object.values(testGroups.parallelGroups).flat()]; const allFiles = [...testGroups.serial, ...Object.values(testGroups.parallelGroups).flat()];
@ -92,12 +82,6 @@ export class TsTest {
} }
} }
// Clear the timeout warning timer if it was set
if (this.timeoutWarningTimer) {
clearTimeout(this.timeoutWarningTimer);
this.timeoutWarningTimer = null;
}
tapCombinator.evaluate(); tapCombinator.evaluate();
} }
@ -272,6 +256,19 @@ import '${absoluteTestFile.replace(/\\/g, '/')}';
execResultStreaming.childProcess.on('error', cleanup); execResultStreaming.childProcess.on('error', cleanup);
} }
// Start warning timer if no timeout was specified
let warningTimer: NodeJS.Timeout | null = null;
if (this.timeoutSeconds === null) {
warningTimer = setTimeout(() => {
console.error('');
console.error(cs('⚠️ WARNING: Test file is running for more than 1 minute', 'orange'));
console.error(cs(` File: ${fileNameArg}`, 'orange'));
console.error(cs(' Consider using --timeout option to set a timeout for test files.', 'orange'));
console.error(cs(' Example: tstest test --timeout=300 (for 5 minutes)', 'orange'));
console.error('');
}, 60000); // 1 minute
}
// Handle timeout if specified // Handle timeout if specified
if (this.timeoutSeconds !== null) { if (this.timeoutSeconds !== null) {
const timeoutMs = this.timeoutSeconds * 1000; const timeoutMs = this.timeoutSeconds * 1000;
@ -293,6 +290,10 @@ import '${absoluteTestFile.replace(/\\/g, '/')}';
// Clear timeout if test completed successfully // Clear timeout if test completed successfully
clearTimeout(timeoutId); clearTimeout(timeoutId);
} catch (error) { } catch (error) {
// Clear warning timer if it was set
if (warningTimer) {
clearTimeout(warningTimer);
}
// Handle timeout error // Handle timeout error
tapParser.handleTimeout(this.timeoutSeconds); tapParser.handleTimeout(this.timeoutSeconds);
// Ensure entire process tree is killed if still running // Ensure entire process tree is killed if still running
@ -307,6 +308,11 @@ import '${absoluteTestFile.replace(/\\/g, '/')}';
await tapParser.handleTapProcess(execResultStreaming.childProcess); await tapParser.handleTapProcess(execResultStreaming.childProcess);
} }
// Clear warning timer if it was set
if (warningTimer) {
clearTimeout(warningTimer);
}
return tapParser; return tapParser;
} }
@ -425,6 +431,19 @@ import '${absoluteTestFile.replace(/\\/g, '/')}';
} }
); );
// Start warning timer if no timeout was specified
let warningTimer: NodeJS.Timeout | null = null;
if (this.timeoutSeconds === null) {
warningTimer = setTimeout(() => {
console.error('');
console.error(cs('⚠️ WARNING: Test file is running for more than 1 minute', 'orange'));
console.error(cs(` File: ${fileNameArg}`, 'orange'));
console.error(cs(' Consider using --timeout option to set a timeout for test files.', 'orange'));
console.error(cs(' Example: tstest test --timeout=300 (for 5 minutes)', 'orange'));
console.error('');
}, 60000); // 1 minute
}
// Handle timeout if specified // Handle timeout if specified
if (this.timeoutSeconds !== null) { if (this.timeoutSeconds !== null) {
const timeoutMs = this.timeoutSeconds * 1000; const timeoutMs = this.timeoutSeconds * 1000;
@ -444,6 +463,10 @@ import '${absoluteTestFile.replace(/\\/g, '/')}';
// Clear timeout if test completed successfully // Clear timeout if test completed successfully
clearTimeout(timeoutId); clearTimeout(timeoutId);
} catch (error) { } catch (error) {
// Clear warning timer if it was set
if (warningTimer) {
clearTimeout(warningTimer);
}
// Handle timeout error // Handle timeout error
tapParser.handleTimeout(this.timeoutSeconds); tapParser.handleTimeout(this.timeoutSeconds);
} }
@ -451,6 +474,11 @@ import '${absoluteTestFile.replace(/\\/g, '/')}';
await evaluatePromise; await evaluatePromise;
} }
// Clear warning timer if it was set
if (warningTimer) {
clearTimeout(warningTimer);
}
// Always clean up resources, even on timeout // Always clean up resources, even on timeout
try { try {
await this.smartbrowserInstance.stop(); await this.smartbrowserInstance.stop();
@ -488,10 +516,10 @@ import '${absoluteTestFile.replace(/\\/g, '/')}';
try { try {
// Delete 00err and 00diff directories if they exist // Delete 00err and 00diff directories if they exist
if (await plugins.smartfile.fs.isDirectory(errDir)) { if (plugins.smartfile.fs.isDirectorySync(errDir)) {
plugins.smartfile.fs.removeSync(errDir); plugins.smartfile.fs.removeSync(errDir);
} }
if (await plugins.smartfile.fs.isDirectory(diffDir)) { if (plugins.smartfile.fs.isDirectorySync(diffDir)) {
plugins.smartfile.fs.removeSync(diffDir); plugins.smartfile.fs.removeSync(diffDir);
} }

View File

@ -242,9 +242,11 @@ export class TsTestLogger {
if (!this.options.quiet) { if (!this.options.quiet) {
const total = passed + failed; const total = passed + failed;
const status = failed === 0 ? 'PASSED' : 'FAILED'; if (failed === 0) {
const color = failed === 0 ? 'green' : 'red'; this.log(this.format(` Summary: ${passed}/${total} PASSED`, 'green'));
this.log(this.format(` Summary: ${passed}/${total} ${status}`, color)); } else {
this.log(this.format(` Summary: ${passed} passed, ${failed} failed of ${total} tests`, 'red'));
}
} }
// If using --logfile, handle error copy and diff detection // If using --logfile, handle error copy and diff detection
@ -390,7 +392,11 @@ export class TsTestLogger {
if (this.options.quiet) { if (this.options.quiet) {
const status = summary.totalFailed === 0 ? 'PASSED' : 'FAILED'; const status = summary.totalFailed === 0 ? 'PASSED' : 'FAILED';
this.log(`\nSummary: ${summary.totalPassed}/${summary.totalTests} | ${totalDuration}ms | ${status}`); if (summary.totalFailed === 0) {
this.log(`\nSummary: ${summary.totalPassed}/${summary.totalTests} | ${totalDuration}ms | ${status}`);
} else {
this.log(`\nSummary: ${summary.totalPassed} passed, ${summary.totalFailed} failed of ${summary.totalTests} tests | ${totalDuration}ms | ${status}`);
}
return; return;
} }