Compare commits
6 Commits
Author | SHA1 | Date | |
---|---|---|---|
3c535a8a77 | |||
0954265095 | |||
e1d90589bc | |||
33f705d961 | |||
13b11ab1bf | |||
63280e4a9a |
23
changelog.md
23
changelog.md
@ -1,5 +1,28 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 2025-05-24 - 1.11.4 - fix(logging)
|
||||||
|
Improve warning logging and add permission settings file
|
||||||
|
|
||||||
|
- Replace multiple logger.error calls with logger.warning for tests running over 1 minute
|
||||||
|
- Add warning method in tstest logger to display warning messages consistently
|
||||||
|
- Introduce .claude/settings.local.json to configure allowed permissions
|
||||||
|
|
||||||
|
## 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)
|
## 2025-05-24 - 1.11.1 - fix(tstest)
|
||||||
Clear timeout identifiers after successful test execution and add local CLAUDE settings
|
Clear timeout identifiers after successful test execution and add local CLAUDE settings
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@git.zone/tstest",
|
"name": "@git.zone/tstest",
|
||||||
"version": "1.11.1",
|
"version": "1.11.4",
|
||||||
"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": {
|
||||||
|
@ -3,6 +3,6 @@
|
|||||||
*/
|
*/
|
||||||
export const commitinfo = {
|
export const commitinfo = {
|
||||||
name: '@git.zone/tstest',
|
name: '@git.zone/tstest',
|
||||||
version: '1.11.1',
|
version: '1.11.4',
|
||||||
description: 'a test utility to run tests that match test/**/*.ts'
|
description: 'a test utility to run tests that match test/**/*.ts'
|
||||||
}
|
}
|
||||||
|
@ -36,18 +36,20 @@ export class TapParser {
|
|||||||
* Handle test file timeout
|
* Handle test file timeout
|
||||||
*/
|
*/
|
||||||
public handleTimeout(timeoutSeconds: number) {
|
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
|
// Create a fake failing test result for timeout
|
||||||
this._getNewTapTestResult();
|
this._getNewTapTestResult();
|
||||||
this.activeTapTestResult.testOk = false;
|
this.activeTapTestResult.testOk = false;
|
||||||
this.activeTapTestResult.testSettled = true;
|
this.activeTapTestResult.testSettled = true;
|
||||||
this.testStore.push(this.activeTapTestResult);
|
this.testStore.push(this.activeTapTestResult);
|
||||||
|
|
||||||
// Set expected vs received to force failure
|
|
||||||
this.expectedTests = 1;
|
|
||||||
this.receivedTests = 0;
|
|
||||||
|
|
||||||
// Log the timeout error
|
// Log the timeout error
|
||||||
if (this.logger) {
|
if (this.logger) {
|
||||||
|
// First log the test result
|
||||||
this.logger.testResult(
|
this.logger.testResult(
|
||||||
`Test file timeout`,
|
`Test file timeout`,
|
||||||
false,
|
false,
|
||||||
@ -55,9 +57,9 @@ export class TapParser {
|
|||||||
`Error: Test file exceeded timeout of ${timeoutSeconds} seconds`
|
`Error: Test file exceeded timeout of ${timeoutSeconds} seconds`
|
||||||
);
|
);
|
||||||
this.logger.testErrorDetails(`Test execution was terminated after ${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() {
|
private _getNewTapTestResult() {
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
import * as plugins from './tstest.plugins.js';
|
import * as plugins from './tstest.plugins.js';
|
||||||
import * as paths from './tstest.paths.js';
|
import * as paths from './tstest.paths.js';
|
||||||
import * as logPrefixes from './tstest.logprefixes.js';
|
|
||||||
|
|
||||||
import { coloredString as cs } from '@push.rocks/consolecolor';
|
import { coloredString as cs } from '@push.rocks/consolecolor';
|
||||||
|
|
||||||
@ -19,6 +18,7 @@ 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,6 +45,15 @@ 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()];
|
||||||
|
|
||||||
@ -83,6 +92,12 @@ 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();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -156,8 +171,9 @@ export class TsTest {
|
|||||||
let timeoutId: NodeJS.Timeout;
|
let timeoutId: NodeJS.Timeout;
|
||||||
|
|
||||||
const timeoutPromise = new Promise<void>((_resolve, reject) => {
|
const timeoutPromise = new Promise<void>((_resolve, reject) => {
|
||||||
timeoutId = setTimeout(() => {
|
timeoutId = setTimeout(async () => {
|
||||||
execResultStreaming.childProcess.kill('SIGTERM');
|
// Use smartshell's terminate() to kill entire process tree
|
||||||
|
await execResultStreaming.terminate();
|
||||||
reject(new Error(`Test file timed out after ${this.timeoutSeconds} seconds`));
|
reject(new Error(`Test file timed out after ${this.timeoutSeconds} seconds`));
|
||||||
}, timeoutMs);
|
}, timeoutMs);
|
||||||
});
|
});
|
||||||
@ -172,6 +188,12 @@ export class TsTest {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Handle timeout error
|
// Handle timeout error
|
||||||
tapParser.handleTimeout(this.timeoutSeconds);
|
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 {
|
} else {
|
||||||
await tapParser.handleTapProcess(execResultStreaming.childProcess);
|
await tapParser.handleTapProcess(execResultStreaming.childProcess);
|
||||||
@ -321,13 +343,29 @@ export class TsTest {
|
|||||||
await evaluatePromise;
|
await evaluatePromise;
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.smartbrowserInstance.stop();
|
// Always clean up resources, even on timeout
|
||||||
await server.stop();
|
try {
|
||||||
wss.close();
|
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(
|
console.log(
|
||||||
`${cs('=> ', 'blue')} Stopped ${cs(fileNameArg, 'orange')} chromium instance and server.`
|
`${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();
|
await tapParser.evaluateFinalResult();
|
||||||
return tapParser;
|
return tapParser;
|
||||||
}
|
}
|
||||||
@ -351,7 +389,7 @@ export class TsTest {
|
|||||||
|
|
||||||
// Get all .log files in log directory (not in subdirectories)
|
// Get all .log files in log directory (not in subdirectories)
|
||||||
const files = await plugins.smartfile.fs.listFileTree(logDir, '*.log');
|
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) {
|
if (logFiles.length === 0) {
|
||||||
return;
|
return;
|
||||||
|
@ -443,6 +443,20 @@ export class TsTestLogger {
|
|||||||
this.log(this.format(`\n${status}`, statusColor));
|
this.log(this.format(`\n${status}`, statusColor));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Warning display
|
||||||
|
warning(message: string) {
|
||||||
|
if (this.options.json) {
|
||||||
|
this.logJson({ event: 'warning', message });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.options.quiet) {
|
||||||
|
console.log(`WARNING: ${message}`);
|
||||||
|
} else {
|
||||||
|
this.log(this.format(` ⚠️ ${message}`, 'orange'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Error display
|
// Error display
|
||||||
error(message: string, file?: string, stack?: string) {
|
error(message: string, file?: string, stack?: string) {
|
||||||
if (this.options.json) {
|
if (this.options.json) {
|
||||||
|
Reference in New Issue
Block a user