diff --git a/changelog.md b/changelog.md index ca60574..7939e31 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,13 @@ # Changelog +## 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 diff --git a/ts/00_commitinfo_data.ts b/ts/00_commitinfo_data.ts index 96d5b71..825d788 100644 --- a/ts/00_commitinfo_data.ts +++ b/ts/00_commitinfo_data.ts @@ -3,6 +3,6 @@ */ export const commitinfo = { name: '@git.zone/tstest', - version: '1.11.1', + version: '1.11.2', description: 'a test utility to run tests that match test/**/*.ts' } diff --git a/ts/tstest.classes.tap.parser.ts b/ts/tstest.classes.tap.parser.ts index 86825e9..edeb4ca 100644 --- a/ts/tstest.classes.tap.parser.ts +++ b/ts/tstest.classes.tap.parser.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() { diff --git a/ts/tstest.classes.tstest.ts b/ts/tstest.classes.tstest.ts index 97f73b8..936b4bc 100644 --- a/ts/tstest.classes.tstest.ts +++ b/ts/tstest.classes.tstest.ts @@ -156,8 +156,9 @@ export class TsTest { let timeoutId: NodeJS.Timeout; const timeoutPromise = new Promise((_resolve, reject) => { - timeoutId = 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); }); @@ -172,6 +173,12 @@ export class TsTest { } 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); @@ -296,6 +303,7 @@ export class TsTest { ); // Handle timeout if specified + let hasTimedOut = false; if (this.timeoutSeconds !== null) { const timeoutMs = this.timeoutSeconds * 1000; let timeoutId: NodeJS.Timeout; @@ -315,19 +323,36 @@ export class TsTest { clearTimeout(timeoutId); } catch (error) { // Handle timeout error + hasTimedOut = true; tapParser.handleTimeout(this.timeoutSeconds); } } else { 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; } @@ -351,7 +376,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;