Compare commits
6 Commits
Author | SHA1 | Date | |
---|---|---|---|
e1d90589bc | |||
33f705d961 | |||
13b11ab1bf | |||
63280e4a9a | |||
23addc2d2f | |||
3649114c8d |
22
changelog.md
22
changelog.md
@ -1,5 +1,27 @@
|
||||
# Changelog
|
||||
|
||||
## 2025-05-24 - 1.11.3 - fix(tstest)
|
||||
Add timeout warning for long-running tests and introduce local settings configuration
|
||||
|
||||
- Add .claude/settings.local.json with permission configuration for local development
|
||||
- Implement a timeout warning timer that notifies when tests run longer than 1 minute without an explicit timeout
|
||||
- Clear the timeout warning timer upon test completion
|
||||
- Remove unused import of logPrefixes in tstest.classes.tstest.ts
|
||||
|
||||
## 2025-05-24 - 1.11.2 - fix(tstest)
|
||||
Improve timeout and error handling in test execution along with TAP parser timeout logic improvements.
|
||||
|
||||
- In the TAP parser, ensure that expected tests are properly set when no tests are defined to avoid false negatives on timeout.
|
||||
- Use smartshell's terminate method and fallback kill to properly stop the entire process tree on timeout.
|
||||
- Clean up browser, server, and WebSocket instances reliably even when a timeout occurs.
|
||||
- Minor improvements in log file filtering and error logging for better clarity.
|
||||
|
||||
## 2025-05-24 - 1.11.1 - fix(tstest)
|
||||
Clear timeout identifiers after successful test execution and add local CLAUDE settings
|
||||
|
||||
- Ensure timeout IDs are cleared when tests complete to prevent lingering timeouts
|
||||
- Add .claude/settings.local.json with updated permission settings for CLI commands
|
||||
|
||||
## 2025-05-24 - 1.11.0 - feat(cli)
|
||||
Add new timeout and file range options with enhanced logfile diff logging
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@git.zone/tstest",
|
||||
"version": "1.11.0",
|
||||
"version": "1.11.3",
|
||||
"private": false,
|
||||
"description": "a test utility to run tests that match test/**/*.ts",
|
||||
"exports": {
|
||||
|
@ -3,6 +3,6 @@
|
||||
*/
|
||||
export const commitinfo = {
|
||||
name: '@git.zone/tstest',
|
||||
version: '1.11.0',
|
||||
version: '1.11.3',
|
||||
description: 'a test utility to run tests that match test/**/*.ts'
|
||||
}
|
||||
|
@ -36,18 +36,20 @@ export class TapParser {
|
||||
* Handle test file timeout
|
||||
*/
|
||||
public handleTimeout(timeoutSeconds: number) {
|
||||
// If no tests have been defined yet, set expected to 1
|
||||
if (this.expectedTests === 0) {
|
||||
this.expectedTests = 1;
|
||||
}
|
||||
|
||||
// Create a fake failing test result for timeout
|
||||
this._getNewTapTestResult();
|
||||
this.activeTapTestResult.testOk = false;
|
||||
this.activeTapTestResult.testSettled = true;
|
||||
this.testStore.push(this.activeTapTestResult);
|
||||
|
||||
// Set expected vs received to force failure
|
||||
this.expectedTests = 1;
|
||||
this.receivedTests = 0;
|
||||
|
||||
// Log the timeout error
|
||||
if (this.logger) {
|
||||
// First log the test result
|
||||
this.logger.testResult(
|
||||
`Test file timeout`,
|
||||
false,
|
||||
@ -55,9 +57,9 @@ export class TapParser {
|
||||
`Error: Test file exceeded timeout of ${timeoutSeconds} seconds`
|
||||
);
|
||||
this.logger.testErrorDetails(`Test execution was terminated after ${timeoutSeconds} seconds`);
|
||||
// Force file end with failure
|
||||
this.logger.testFileEnd(0, 1, timeoutSeconds * 1000);
|
||||
}
|
||||
|
||||
// Don't call evaluateFinalResult here, let the caller handle it
|
||||
}
|
||||
|
||||
private _getNewTapTestResult() {
|
||||
|
@ -1,6 +1,5 @@
|
||||
import * as plugins from './tstest.plugins.js';
|
||||
import * as paths from './tstest.paths.js';
|
||||
import * as logPrefixes from './tstest.logprefixes.js';
|
||||
|
||||
import { coloredString as cs } from '@push.rocks/consolecolor';
|
||||
|
||||
@ -19,6 +18,7 @@ export class TsTest {
|
||||
public startFromFile: number | null;
|
||||
public stopAtFile: number | null;
|
||||
public timeoutSeconds: number | null;
|
||||
private timeoutWarningTimer: NodeJS.Timeout | null = null;
|
||||
|
||||
public smartshellInstance = new plugins.smartshell.Smartshell({
|
||||
executor: 'bash',
|
||||
@ -45,6 +45,16 @@ export class TsTest {
|
||||
await this.movePreviousLogFiles();
|
||||
}
|
||||
|
||||
// Start timeout warning timer if no timeout was specified
|
||||
if (this.timeoutSeconds === null) {
|
||||
this.timeoutWarningTimer = setTimeout(() => {
|
||||
// Use the logger instead of console.error to ensure the warning is visible
|
||||
this.logger.error('Warning: Test is running for more than 1 minute.');
|
||||
this.logger.error('Consider using --timeout option to set a timeout for test files.');
|
||||
this.logger.error('Example: tstest test --timeout=300 (for 5 minutes)');
|
||||
}, 60000); // 1 minute
|
||||
}
|
||||
|
||||
const testGroups = await this.testDir.getTestFileGroups();
|
||||
const allFiles = [...testGroups.serial, ...Object.values(testGroups.parallelGroups).flat()];
|
||||
|
||||
@ -83,6 +93,12 @@ export class TsTest {
|
||||
}
|
||||
}
|
||||
|
||||
// Clear the timeout warning timer if it was set
|
||||
if (this.timeoutWarningTimer) {
|
||||
clearTimeout(this.timeoutWarningTimer);
|
||||
this.timeoutWarningTimer = null;
|
||||
}
|
||||
|
||||
tapCombinator.evaluate();
|
||||
}
|
||||
|
||||
@ -153,9 +169,12 @@ export class TsTest {
|
||||
// Handle timeout if specified
|
||||
if (this.timeoutSeconds !== null) {
|
||||
const timeoutMs = this.timeoutSeconds * 1000;
|
||||
let timeoutId: NodeJS.Timeout;
|
||||
|
||||
const timeoutPromise = new Promise<void>((_resolve, reject) => {
|
||||
setTimeout(() => {
|
||||
execResultStreaming.childProcess.kill('SIGTERM');
|
||||
timeoutId = setTimeout(async () => {
|
||||
// Use smartshell's terminate() to kill entire process tree
|
||||
await execResultStreaming.terminate();
|
||||
reject(new Error(`Test file timed out after ${this.timeoutSeconds} seconds`));
|
||||
}, timeoutMs);
|
||||
});
|
||||
@ -165,9 +184,17 @@ export class TsTest {
|
||||
tapParser.handleTapProcess(execResultStreaming.childProcess),
|
||||
timeoutPromise
|
||||
]);
|
||||
// Clear timeout if test completed successfully
|
||||
clearTimeout(timeoutId);
|
||||
} catch (error) {
|
||||
// Handle timeout error
|
||||
tapParser.handleTimeout(this.timeoutSeconds);
|
||||
// Ensure entire process tree is killed if still running
|
||||
try {
|
||||
await execResultStreaming.kill(); // This kills the entire process tree with SIGKILL
|
||||
} catch (killError) {
|
||||
// Process tree might already be dead
|
||||
}
|
||||
}
|
||||
} else {
|
||||
await tapParser.handleTapProcess(execResultStreaming.childProcess);
|
||||
@ -294,8 +321,10 @@ export class TsTest {
|
||||
// Handle timeout if specified
|
||||
if (this.timeoutSeconds !== null) {
|
||||
const timeoutMs = this.timeoutSeconds * 1000;
|
||||
let timeoutId: NodeJS.Timeout;
|
||||
|
||||
const timeoutPromise = new Promise<void>((_resolve, reject) => {
|
||||
setTimeout(() => {
|
||||
timeoutId = setTimeout(() => {
|
||||
reject(new Error(`Test file timed out after ${this.timeoutSeconds} seconds`));
|
||||
}, timeoutMs);
|
||||
});
|
||||
@ -305,6 +334,8 @@ export class TsTest {
|
||||
evaluatePromise,
|
||||
timeoutPromise
|
||||
]);
|
||||
// Clear timeout if test completed successfully
|
||||
clearTimeout(timeoutId);
|
||||
} catch (error) {
|
||||
// Handle timeout error
|
||||
tapParser.handleTimeout(this.timeoutSeconds);
|
||||
@ -313,13 +344,29 @@ export class TsTest {
|
||||
await evaluatePromise;
|
||||
}
|
||||
|
||||
await this.smartbrowserInstance.stop();
|
||||
await server.stop();
|
||||
wss.close();
|
||||
// Always clean up resources, even on timeout
|
||||
try {
|
||||
await this.smartbrowserInstance.stop();
|
||||
} catch (error) {
|
||||
// Browser might already be stopped
|
||||
}
|
||||
|
||||
try {
|
||||
await server.stop();
|
||||
} catch (error) {
|
||||
// Server might already be stopped
|
||||
}
|
||||
|
||||
try {
|
||||
wss.close();
|
||||
} catch (error) {
|
||||
// WebSocket server might already be closed
|
||||
}
|
||||
|
||||
console.log(
|
||||
`${cs('=> ', 'blue')} Stopped ${cs(fileNameArg, 'orange')} chromium instance and server.`
|
||||
);
|
||||
// lets create the tap parser
|
||||
// Always evaluate final result (handleTimeout just sets up the test state)
|
||||
await tapParser.evaluateFinalResult();
|
||||
return tapParser;
|
||||
}
|
||||
@ -343,7 +390,7 @@ export class TsTest {
|
||||
|
||||
// Get all .log files in log directory (not in subdirectories)
|
||||
const files = await plugins.smartfile.fs.listFileTree(logDir, '*.log');
|
||||
const logFiles = files.filter(file => !file.includes('/'));
|
||||
const logFiles = files.filter((file: string) => !file.includes('/'));
|
||||
|
||||
if (logFiles.length === 0) {
|
||||
return;
|
||||
|
Reference in New Issue
Block a user