@git.zone/tstest 4.1.1 → 6.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/.smartconfig.json +5 -1
  2. package/dist_ts/00_commitinfo_data.js +1 -1
  3. package/dist_ts/tstest.classes.runtime.bun.js +4 -3
  4. package/dist_ts/tstest.classes.runtime.chromium.js +5 -4
  5. package/dist_ts/tstest.classes.runtime.deno.js +49 -117
  6. package/dist_ts/tstest.classes.runtime.docker.js +9 -10
  7. package/dist_ts/tstest.classes.runtime.node.js +8 -3
  8. package/dist_ts/tstest.classes.tap.combinator.js +2 -4
  9. package/dist_ts/tstest.classes.tap.parser.d.ts +5 -1
  10. package/dist_ts/tstest.classes.tap.parser.js +47 -68
  11. package/dist_ts/tstest.classes.testfile.directives.js +2 -2
  12. package/dist_ts/tstest.classes.tstest.d.ts +2 -0
  13. package/dist_ts/tstest.classes.tstest.js +24 -10
  14. package/dist_ts/tstest.logging.d.ts +5 -1
  15. package/dist_ts/tstest.logging.js +30 -13
  16. package/dist_ts/tstest.plugins.d.ts +2 -1
  17. package/dist_ts/tstest.plugins.js +3 -2
  18. package/dist_ts_tapbundle_serverside/classes.tapnodetools.d.ts +2 -2
  19. package/dist_ts_tapbundle_serverside/classes.tapnodetools.js +10 -10
  20. package/dist_ts_tapbundle_serverside/plugins.d.ts +1 -0
  21. package/dist_ts_tapbundle_serverside/plugins.js +3 -1
  22. package/package.json +4 -4
  23. package/readme.md +17 -6
  24. package/ts/00_commitinfo_data.ts +1 -1
  25. package/ts/tstest.classes.runtime.bun.ts +3 -2
  26. package/ts/tstest.classes.runtime.chromium.ts +4 -3
  27. package/ts/tstest.classes.runtime.deno.ts +47 -131
  28. package/ts/tstest.classes.runtime.docker.ts +8 -9
  29. package/ts/tstest.classes.runtime.node.ts +7 -2
  30. package/ts/tstest.classes.tap.combinator.ts +1 -3
  31. package/ts/tstest.classes.tap.parser.ts +46 -75
  32. package/ts/tstest.classes.testfile.directives.ts +1 -1
  33. package/ts/tstest.classes.tstest.ts +33 -10
  34. package/ts/tstest.logging.ts +33 -13
  35. package/ts/tstest.plugins.ts +2 -1
@@ -20,7 +20,6 @@ export const DENO_DEFAULT_PERMISSIONS = [
20
20
  '--allow-write',
21
21
  '--allow-sys',
22
22
  '--allow-import',
23
- '--node-modules-dir',
24
23
  '--sloppy-imports',
25
24
  ];
26
25
 
@@ -45,20 +44,9 @@ export class DenoRuntimeAdapter extends RuntimeAdapter {
45
44
  * Get default Deno options
46
45
  */
47
46
  protected getDefaultOptions(): DenoOptions {
48
- // Auto-detect deno.json or deno.jsonc config file for TypeScript decorator support
49
- let configPath: string | undefined;
50
- const denoJsonPath = plugins.path.join(process.cwd(), 'deno.json');
51
- const denoJsoncPath = plugins.path.join(process.cwd(), 'deno.jsonc');
52
-
53
- if (plugins.fs.existsSync(denoJsonPath)) {
54
- configPath = denoJsonPath;
55
- } else if (plugins.fs.existsSync(denoJsoncPath)) {
56
- configPath = denoJsoncPath;
57
- }
58
-
47
+ // Deno discovers the project's config and node_modules mode itself.
59
48
  return {
60
49
  ...super.getDefaultOptions(),
61
- configPath,
62
50
  permissions: [...DENO_DEFAULT_PERMISSIONS],
63
51
  };
64
52
  }
@@ -152,130 +140,58 @@ export class DenoRuntimeAdapter extends RuntimeAdapter {
152
140
  total: number,
153
141
  options?: DenoOptions
154
142
  ): Promise<TapParser> {
155
- this.logger.testFileStart(testFile, this.displayName, index, total);
156
- const tapParser = new TapParser(testFile + ':deno', this.logger);
157
-
158
- const mergedOptions = this.mergeOptions(options) as DenoOptions;
159
-
160
- // Build Deno command
161
- const command = this.createCommand(testFile, mergedOptions);
162
- const fullCommand = `${command.command} ${command.args.join(' ')}`;
163
-
164
- // Set filter tags as environment variable
165
- if (this.filterTags.length > 0) {
166
- process.env.TSTEST_FILTER_TAGS = this.filterTags.join(',');
167
- }
168
-
169
- // Check for 00init.ts file in test directory
170
- const testDir = plugins.path.dirname(testFile);
171
- const initFile = plugins.path.join(testDir, '00init.ts');
172
- const initFileExists = await plugins.smartfsInstance.file(initFile).exists();
173
-
174
- let runCommand = fullCommand;
175
- let loaderPath: string | null = null;
176
-
177
- // If 00init.ts exists, create a loader file
178
- if (initFileExists) {
179
- const absoluteInitFile = plugins.path.resolve(initFile);
180
- const absoluteTestFile = plugins.path.resolve(testFile);
181
- const loaderContent = `
182
- import '${absoluteInitFile.replace(/\\/g, '/')}';
183
- import '${absoluteTestFile.replace(/\\/g, '/')}';
184
- `;
185
- loaderPath = plugins.path.join(testDir, `.loader_${plugins.path.basename(testFile)}`);
186
- await plugins.smartfsInstance.file(loaderPath).write(loaderContent);
187
-
188
- // Rebuild command with loader file
189
- const loaderCommand = this.createCommand(loaderPath, mergedOptions);
190
- runCommand = `${loaderCommand.command} ${loaderCommand.args.join(' ')}`;
191
- }
192
-
193
- // Pre-resolve dependencies for the Deno test entrypoint
194
- const installTarget = loaderPath || testFile;
195
- const installArgs = ['install', '--entrypoint', installTarget];
196
- if (mergedOptions.configPath) {
197
- installArgs.push('--config', mergedOptions.configPath);
198
- }
199
- const installCommand = `deno ${installArgs.join(' ')}`;
200
- console.log(cs(` ⏳ Resolving Deno dependencies for ${plugins.path.basename(testFile)}...`, 'blue'));
201
- await this.smartshellInstance.execSilent(installCommand, { cwd: process.cwd() });
202
- console.log(cs(` ✓ Deno dependencies resolved`, 'green'));
203
-
204
- const execResultStreaming = await this.smartshellInstance.execStreamingSilent(runCommand);
143
+ const logger = this.logger.createTestFileLogger();
144
+ logger.testFileStart(testFile, this.displayName, index, total);
145
+ const tapParser = new TapParser(testFile + ':deno', logger);
205
146
 
206
- // If we created a loader file, clean it up after test execution
207
- if (loaderPath) {
208
- const cleanup = () => {
209
- try {
210
- if (plugins.fs.existsSync(loaderPath)) {
211
- plugins.fs.rmSync(loaderPath, { force: true });
212
- }
213
- } catch (e) {
214
- // Ignore cleanup errors
215
- }
216
- };
217
-
218
- execResultStreaming.childProcess.on('exit', cleanup);
219
- execResultStreaming.childProcess.on('error', cleanup);
220
- }
221
-
222
- // Start warning timer if no timeout was specified
223
- let warningTimer: NodeJS.Timeout | null = null;
224
- if (this.timeoutSeconds === null) {
225
- warningTimer = setTimeout(() => {
226
- console.error('');
227
- console.error(cs('⚠️ WARNING: Test file is running for more than 1 minute', 'orange'));
228
- console.error(cs(` File: ${testFile}`, 'orange'));
229
- console.error(cs(' Consider using --timeout option to set a timeout for test files.', 'orange'));
230
- console.error(cs(' Example: tstest test --timeout=300 (for 5 minutes)', 'orange'));
231
- console.error('');
232
- }, 60000); // 1 minute
233
- }
234
-
235
- // Handle timeout if specified
236
- if (this.timeoutSeconds !== null) {
237
- const timeoutMs = this.timeoutSeconds * 1000;
238
- let timeoutId: NodeJS.Timeout;
239
-
240
- const timeoutPromise = new Promise<void>((_resolve, reject) => {
241
- timeoutId = setTimeout(async () => {
242
- // Use smartshell's terminate() to kill entire process tree
243
- await execResultStreaming.terminate();
244
- reject(new Error(`Test file timed out after ${this.timeoutSeconds} seconds`));
245
- }, timeoutMs);
147
+ let loaderPath: string | undefined;
148
+ let timer: NodeJS.Timeout | undefined;
149
+ try {
150
+ const mergedOptions = this.mergeOptions(options) as DenoOptions;
151
+ const absoluteTestFile = plugins.path.resolve(mergedOptions.cwd || process.cwd(), testFile);
152
+ const testDir = plugins.path.dirname(absoluteTestFile);
153
+ const initFile = plugins.path.join(testDir, '00init.ts');
154
+ if (await plugins.smartfsInstance.file(initFile).exists()) {
155
+ loaderPath = plugins.path.join(testDir,
156
+ `.${plugins.path.basename(testFile)}.${process.pid}.${Date.now()}.loader.ts`);
157
+ const loaderContent = [initFile, absoluteTestFile].map((fileArg) =>
158
+ `import ${JSON.stringify(plugins.url.pathToFileURL(fileArg).href)};`).join('\n');
159
+ await plugins.smartfsInstance.file(loaderPath).write(loaderContent);
160
+ }
161
+ const command = this.createCommand(loaderPath || absoluteTestFile, mergedOptions);
162
+ const quote = (valueArg: string) => "'" + valueArg.replaceAll("'", "'\\''") + "'";
163
+ const runCommand = [command.command, ...command.args].map(quote).join(' ');
164
+ // One Deno invocation honors project defaults and explicit manual/auto flags.
165
+ const execution = await this.smartshellInstance.execStreamingSilent(runCommand, {
166
+ cwd: command.cwd,
167
+ env: { ...process.env, ...command.env },
246
168
  });
247
-
248
- try {
249
- await Promise.race([
250
- tapParser.handleTapProcess(execResultStreaming.childProcess),
251
- timeoutPromise
252
- ]);
253
- // Clear timeout if test completed successfully
254
- clearTimeout(timeoutId);
255
- } catch (error) {
256
- // Clear warning timer if it was set
257
- if (warningTimer) {
258
- clearTimeout(warningTimer);
259
- }
260
- // Handle timeout error
261
- tapParser.handleTimeout(this.timeoutSeconds);
262
- // Ensure entire process tree is killed if still running
169
+ if (this.timeoutSeconds !== null) {
170
+ timer = setTimeout(() => {
171
+ tapParser.handleTimeout(this.timeoutSeconds);
172
+ void execution.kill().catch((errorArg: Error) => {
173
+ tapParser.handleExecutionError(`Unable to terminate timed out test: ${errorArg.message}`);
174
+ });
175
+ }, this.timeoutSeconds * 1000);
176
+ } else {
177
+ timer = setTimeout(() => {
178
+ logger.warning(`Test file ${testFile} has run for more than one minute; use --timeout to limit execution.`);
179
+ }, 60000);
180
+ }
181
+ await tapParser.handleTapProcess(execution.childProcess, false);
182
+ } catch (errorArg) {
183
+ tapParser.handleExecutionError(errorArg instanceof Error ? errorArg.message : String(errorArg));
184
+ } finally {
185
+ if (timer) clearTimeout(timer);
186
+ if (loaderPath) {
263
187
  try {
264
- await execResultStreaming.kill(); // This kills the entire process tree with SIGKILL
265
- } catch (killError) {
266
- // Process tree might already be dead
188
+ await plugins.fs.promises.rm(loaderPath, { force: true });
189
+ } catch (errorArg) {
190
+ tapParser.handleExecutionError(`Failed to remove Deno loader: ${String(errorArg)}`);
267
191
  }
268
- await tapParser.evaluateFinalResult();
269
192
  }
270
- } else {
271
- await tapParser.handleTapProcess(execResultStreaming.childProcess);
272
- }
273
-
274
- // Clear warning timer if it was set
275
- if (warningTimer) {
276
- clearTimeout(warningTimer);
193
+ await tapParser.evaluateFinalResult();
277
194
  }
278
-
279
195
  return tapParser;
280
196
  }
281
197
  }
@@ -139,7 +139,8 @@ export class DockerRuntimeAdapter extends RuntimeAdapter {
139
139
  total: number,
140
140
  options?: RuntimeOptions
141
141
  ): Promise<TapParser> {
142
- this.logger.testFileStart(testFile, this.displayName, index, total);
142
+ const logger = this.logger.createTestFileLogger();
143
+ logger.testFileStart(testFile, this.displayName, index, total);
143
144
 
144
145
  // Parse the Docker test filename
145
146
  const parsed = parseDockerTestFilename(testFile);
@@ -158,7 +159,7 @@ export class DockerRuntimeAdapter extends RuntimeAdapter {
158
159
  const relativeTestPath = plugins.path.relative(this.cwd, absoluteTestPath);
159
160
 
160
161
  // Create TAP parser
161
- const tapParser = new TapParser(testFile + ':docker', this.logger);
162
+ const tapParser = new TapParser(testFile + ':docker', logger);
162
163
 
163
164
  try {
164
165
  // Build docker run command
@@ -172,7 +173,7 @@ export class DockerRuntimeAdapter extends RuntimeAdapter {
172
173
  `/test/${plugins.path.basename(testFile)}`
173
174
  ];
174
175
 
175
- this.logger.tapOutput(`Executing: docker ${dockerArgs.join(' ')}`);
176
+ logger.tapOutput(`Executing: docker ${dockerArgs.join(' ')}`);
176
177
 
177
178
  // Execute the Docker container
178
179
  const execPromise = this.smartshellInstance.execStreaming(
@@ -186,7 +187,7 @@ export class DockerRuntimeAdapter extends RuntimeAdapter {
186
187
  let timeoutHandle: NodeJS.Timeout | null = null;
187
188
  if (this.timeoutSeconds) {
188
189
  timeoutHandle = setTimeout(() => {
189
- this.logger.tapOutput(`⏱️ Test timeout (${this.timeoutSeconds}s) - killing container`);
190
+ logger.tapOutput(`⏱️ Test timeout (${this.timeoutSeconds}s) - killing container`);
190
191
  // Try to kill any running containers with this image
191
192
  this.smartshellInstance.exec(`docker ps -q --filter ancestor=${imageName} | xargs -r docker kill`);
192
193
  }, this.timeoutSeconds * 1000);
@@ -205,7 +206,7 @@ export class DockerRuntimeAdapter extends RuntimeAdapter {
205
206
 
206
207
  execPromise.childProcess.stderr.on('data', (data: Buffer) => {
207
208
  const output = data.toString();
208
- this.logger.tapOutput(cs(`[stderr] ${output}`, 'orange'));
209
+ logger.tapOutput(cs(`[stderr] ${output}`, 'orange'));
209
210
  });
210
211
 
211
212
  // Wait for completion
@@ -217,16 +218,14 @@ export class DockerRuntimeAdapter extends RuntimeAdapter {
217
218
  }
218
219
 
219
220
  if (result.exitCode !== 0) {
220
- this.logger.tapOutput(cs(`❌ Docker test failed with exit code ${result.exitCode}`, 'red'));
221
+ tapParser.handleExecutionError(`Docker test failed with exit code ${result.exitCode}`);
221
222
  }
222
223
 
223
224
  // Evaluate final result
224
225
  await tapParser.evaluateFinalResult();
225
226
 
226
227
  } catch (error) {
227
- this.logger.tapOutput(cs(`❌ Error running Docker test: ${error.message}`, 'red'));
228
- // Add a failing test result to the parser
229
- tapParser.handleTapLog('not ok 1 - Docker test execution failed');
228
+ tapParser.handleExecutionError(`Error running Docker test: ${error.message}`);
230
229
  await tapParser.evaluateFinalResult();
231
230
  }
232
231
 
@@ -97,8 +97,9 @@ export class NodeRuntimeAdapter extends RuntimeAdapter {
97
97
  total: number,
98
98
  options?: RuntimeOptions
99
99
  ): Promise<TapParser> {
100
- this.logger.testFileStart(testFile, this.displayName, index, total);
101
- const tapParser = new TapParser(testFile + ':node', this.logger);
100
+ const logger = this.logger.createTestFileLogger();
101
+ logger.testFileStart(testFile, this.displayName, index, total);
102
+ const tapParser = new TapParser(testFile + ':node', logger);
102
103
 
103
104
  const mergedOptions = this.mergeOptions(options);
104
105
 
@@ -145,6 +146,10 @@ import '${absoluteTestFile.replace(/\\/g, '/')}';
145
146
  // Spawn the test process using tsrun's spawnPath API
146
147
  // Pass undefined for fromFileUrl since fileToRun is already an absolute path
147
148
  const tsrunProcess = plugins.tsrun.spawnPath(fileToRun, undefined, spawnOptions);
149
+ // The parser owns the outcome; also observe tsrun's rejected spawn/signal promise.
150
+ void tsrunProcess.exitCode.catch((errorArg: Error) => {
151
+ tapParser.handleExecutionError(errorArg.message);
152
+ });
148
153
 
149
154
  // If we created a loader file, clean it up after test execution
150
155
  if (loaderPath) {
@@ -32,9 +32,7 @@ export class TapCombinator {
32
32
  // Check for failures
33
33
  let failGlobal = false;
34
34
  for (const tapParser of this.tapParserStore) {
35
- if (!tapParser.expectedTests ||
36
- tapParser.expectedTests !== tapParser.receivedTests ||
37
- tapParser.getErrorTests().length > 0) {
35
+ if (tapParser.failed) {
38
36
  failGlobal = true;
39
37
  break;
40
38
  }
@@ -16,6 +16,17 @@ export class TapParser {
16
16
 
17
17
  expectedTests: number = 0;
18
18
  receivedTests: number = 0;
19
+ public readonly executionErrors: string[] = [];
20
+ private finalized = false;
21
+
22
+ public get failed(): boolean {
23
+ return this.executionErrors.length > 0 || !this.expectedTests ||
24
+ this.expectedTests !== this.receivedTests || this.getErrorTests().length > 0;
25
+ }
26
+
27
+ public handleExecutionError(messageArg: string): void {
28
+ if (!this.executionErrors.includes(messageArg)) this.executionErrors.push(messageArg);
29
+ }
19
30
 
20
31
  activeTapTestResult: TapTestResult;
21
32
 
@@ -38,30 +49,7 @@ export class TapParser {
38
49
  * Handle test file timeout
39
50
  */
40
51
  public handleTimeout(timeoutSeconds: number) {
41
- // If no tests have been defined yet, set expected to 1
42
- if (this.expectedTests === 0) {
43
- this.expectedTests = 1;
44
- }
45
-
46
- // Create a fake failing test result for timeout
47
- this._getNewTapTestResult();
48
- this.activeTapTestResult.testOk = false;
49
- this.activeTapTestResult.testSettled = true;
50
- this.testStore.push(this.activeTapTestResult);
51
-
52
- // Log the timeout error
53
- if (this.logger) {
54
- // First log the test result
55
- this.logger.testResult(
56
- `Test file timeout`,
57
- false,
58
- timeoutSeconds * 1000,
59
- `Error: Test file exceeded timeout of ${timeoutSeconds} seconds`
60
- );
61
- this.logger.testErrorDetails(`Test execution was terminated after ${timeoutSeconds} seconds`);
62
- }
63
-
64
- // Don't call evaluateFinalResult here, let the caller handle it
52
+ this.handleExecutionError(`Test file exceeded timeout of ${timeoutSeconds} seconds`);
65
53
  }
66
54
 
67
55
  private _getNewTapTestResult() {
@@ -259,9 +247,7 @@ export class TapParser {
259
247
  break;
260
248
 
261
249
  case 'bailout':
262
- if (this.logger) {
263
- this.logger.error(`Bail out! ${message.content}`);
264
- }
250
+ this.handleExecutionError(`Bail out! ${message.content}`);
265
251
  break;
266
252
 
267
253
  case 'error':
@@ -466,24 +452,32 @@ export class TapParser {
466
452
  * handles a tap process
467
453
  * @param childProcessArg
468
454
  */
469
- public async handleTapProcess(childProcessArg: ChildProcess) {
470
- const done = plugins.smartpromise.defer();
471
- childProcessArg.stdout.on('data', (data) => {
472
- this._processLog(data);
473
- });
474
- childProcessArg.stderr.on('data', (data) => {
475
- this._processLog(data);
476
- });
477
- childProcessArg.on('exit', async () => {
478
- // Flush any remaining buffered content
479
- if (this.lineBuffer) {
480
- this._handleConsoleOutput(this.lineBuffer, false);
481
- this.lineBuffer = '';
482
- }
483
- await this.evaluateFinalResult();
484
- done.resolve();
455
+ public async handleTapProcess(childProcessArg: ChildProcess, finalizeArg = true) {
456
+ await new Promise<void>((resolve) => {
457
+ const onData = (dataArg: Buffer) => this._processLog(dataArg);
458
+ const onError = (errorArg: Error) => this.handleExecutionError(errorArg.message);
459
+ childProcessArg.stdout?.on('data', onData);
460
+ childProcessArg.stderr?.on('data', onData);
461
+ childProcessArg.on('error', onError);
462
+ // close follows both process exit and pipe drainage, including failed spawn.
463
+ childProcessArg.once('close', (codeArg, signalArg) => {
464
+ childProcessArg.stdout?.off('data', onData);
465
+ childProcessArg.stderr?.off('data', onData);
466
+ childProcessArg.off('error', onError);
467
+ if (codeArg !== 0) {
468
+ this.handleExecutionError(signalArg
469
+ ? `Test process terminated by ${signalArg}`
470
+ : `Test process exited with code ${codeArg}`);
471
+ }
472
+ if (this.lineBuffer) {
473
+ const remaining = this.lineBuffer;
474
+ this.lineBuffer = '';
475
+ this._processLog(remaining + '\n');
476
+ }
477
+ resolve();
478
+ });
485
479
  });
486
- await done.promise;
480
+ if (finalizeArg) await this.evaluateFinalResult();
487
481
  }
488
482
 
489
483
  public async handleTapLog(tapLog: string) {
@@ -544,40 +538,17 @@ export class TapParser {
544
538
  }
545
539
 
546
540
  public async evaluateFinalResult() {
541
+ if (this.finalized) return;
542
+ this.finalized = true;
547
543
  this.receivedTests = this.testStore.length;
548
544
  const duration = Date.now() - this.startTime;
549
-
550
- // check wether all tests ran
551
- if (this.expectedTests === this.receivedTests) {
552
- if (this.logger) {
553
- this.logger.tapOutput(`${this.receivedTests} out of ${this.expectedTests} Tests completed!`);
554
- }
555
- } else {
556
- if (this.logger) {
557
- this.logger.error(`Only ${this.receivedTests} out of ${this.expectedTests} completed!`);
558
- }
559
- }
560
545
  if (!this.expectedTests && this.receivedTests === 0) {
561
- if (this.logger) {
562
- this.logger.error('No tests were defined. Therefore the testfile failed!');
563
- this.logger.testFileEnd(0, 1, duration); // Count as 1 failure
564
- }
546
+ this.handleExecutionError('No tests were defined.');
565
547
  } else if (this.expectedTests !== this.receivedTests) {
566
- if (this.logger) {
567
- this.logger.error('The amount of received tests and expectedTests is unequal! Therefore the testfile failed');
568
- const errorCount = this.getErrorTests().length || 1; // At least 1 error
569
- this.logger.testFileEnd(this.receivedTests - errorCount, errorCount, duration);
570
- }
571
- } else if (this.getErrorTests().length === 0) {
572
- if (this.logger) {
573
- this.logger.tapOutput('All tests are successfull!!!');
574
- this.logger.testFileEnd(this.receivedTests, 0, duration);
575
- }
576
- } else {
577
- if (this.logger) {
578
- this.logger.tapOutput(`${this.getErrorTests().length} tests threw an error!!!`, true);
579
- this.logger.testFileEnd(this.receivedTests - this.getErrorTests().length, this.getErrorTests().length, duration);
580
- }
548
+ this.handleExecutionError(`Only ${this.receivedTests} out of ${this.expectedTests} tests completed.`);
581
549
  }
550
+ for (const error of this.executionErrors) this.logger?.testErrorDetails(error);
551
+ const failedTests = this.getErrorTests().length;
552
+ this.logger?.testFileEnd(this.receivedTests - failedTests, failedTests, duration, this.executionErrors);
582
553
  }
583
- }
554
+ }
@@ -165,7 +165,7 @@ function directivesToDenoOptions(directives: IParsedDirectives): DenoOptions | u
165
165
 
166
166
  if (useAllowAll) {
167
167
  // --allow-all replaces individual permissions, but keep compatibility flags
168
- options.permissions = ['--allow-all', '--node-modules-dir', '--sloppy-imports'];
168
+ options.permissions = ['--allow-all', '--sloppy-imports'];
169
169
  } else if (extraPermissions.length > 0) {
170
170
  // Start with defaults and add extra permissions (deduplicated)
171
171
  const allPermissions = [...DENO_DEFAULT_PERMISSIONS];
@@ -12,7 +12,7 @@ import type { LogOptions } from './tstest.logging.js';
12
12
 
13
13
  // Runtime adapters
14
14
  import { parseTestFilename, isDockerTestFile, parseDockerTestFilename } from './tstest.classes.runtime.parser.js';
15
- import { RuntimeAdapterRegistry } from './tstest.classes.runtime.adapter.js';
15
+ import { RuntimeAdapterRegistry, type RuntimeAdapter, type RuntimeOptions } from './tstest.classes.runtime.adapter.js';
16
16
  import { NodeRuntimeAdapter } from './tstest.classes.runtime.node.js';
17
17
  import { ChromiumRuntimeAdapter } from './tstest.classes.runtime.chromium.js';
18
18
  import { DenoRuntimeAdapter } from './tstest.classes.runtime.deno.js';
@@ -318,14 +318,14 @@ export class TsTest {
318
318
  { TSTEST_FILE: fileNameArg, TSTEST_RUNTIME: adapter.id },
319
319
  );
320
320
  if (!success) {
321
- this.logger.error(`test:before:testfile failed for ${fileName}. Skipping test file.`);
321
+ await this.recordRuntimeFailure(fileNameArg, adapter.displayName, fileIndex, totalFiles,
322
+ tapCombinator, 'test:before:testfile failed.');
322
323
  return;
323
324
  }
324
325
  }
325
326
 
326
327
  const options = hasDirectives(directives) ? directivesToRuntimeOptions(directives, adapter.id) : undefined;
327
- const tapParser = await adapter.run(fileNameArg, fileIndex, totalFiles, options);
328
- tapCombinator.addTapParser(tapParser);
328
+ await this.runAdapter(adapter, fileNameArg, fileIndex, totalFiles, tapCombinator, options);
329
329
  } else {
330
330
  // Multiple runtimes - use sections
331
331
  for (let i = 0; i < adapters.length; i++) {
@@ -342,20 +342,40 @@ export class TsTest {
342
342
  { TSTEST_FILE: fileNameArg, TSTEST_RUNTIME: adapter.id },
343
343
  );
344
344
  if (!success) {
345
- this.logger.error(`test:before:testfile failed for ${fileName} on ${adapter.displayName}. Skipping.`);
345
+ await this.recordRuntimeFailure(fileNameArg, adapter.displayName, fileIndex, totalFiles,
346
+ tapCombinator, 'test:before:testfile failed.');
346
347
  this.logger.sectionEnd();
347
348
  continue;
348
349
  }
349
350
  }
350
351
 
351
352
  const options = hasDirectives(directives) ? directivesToRuntimeOptions(directives, adapter.id) : undefined;
352
- const tapParser = await adapter.run(fileNameArg, fileIndex, totalFiles, options);
353
- tapCombinator.addTapParser(tapParser);
353
+ await this.runAdapter(adapter, fileNameArg, fileIndex, totalFiles, tapCombinator, options);
354
354
  this.logger.sectionEnd();
355
355
  }
356
356
  }
357
357
  }
358
358
 
359
+ private async recordRuntimeFailure(fileArg: string, runtimeArg: string, indexArg: number,
360
+ totalArg: number, combinatorArg: TapCombinator, errorArg: unknown): Promise<void> {
361
+ const logger = this.logger.createTestFileLogger();
362
+ logger.testFileStart(fileArg, runtimeArg, indexArg, totalArg);
363
+ const parser = new TapParser(`${fileArg}:${runtimeArg}`, logger);
364
+ parser.handleExecutionError(errorArg instanceof Error ? errorArg.message : String(errorArg));
365
+ await parser.evaluateFinalResult();
366
+ combinatorArg.addTapParser(parser);
367
+ }
368
+
369
+ private async runAdapter(adapterArg: RuntimeAdapter, fileArg: string, indexArg: number,
370
+ totalArg: number, combinatorArg: TapCombinator, optionsArg?: RuntimeOptions): Promise<void> {
371
+ try {
372
+ combinatorArg.addTapParser(await adapterArg.run(fileArg, indexArg, totalArg, optionsArg));
373
+ } catch (errorArg) {
374
+ await this.recordRuntimeFailure(fileArg, adapterArg.displayName, indexArg, totalArg,
375
+ combinatorArg, errorArg);
376
+ }
377
+ }
378
+
359
379
  /**
360
380
  * Execute a Docker test file
361
381
  */
@@ -366,7 +386,8 @@ export class TsTest {
366
386
  tapCombinator: TapCombinator
367
387
  ): Promise<void> {
368
388
  if (!this.dockerAdapter) {
369
- this.logger.tapOutput(cs('Docker adapter not initialized', 'red'));
389
+ await this.recordRuntimeFailure(fileNameArg, 'Docker', fileIndex, totalFiles,
390
+ tapCombinator, 'Docker adapter not initialized.');
370
391
  return;
371
392
  }
372
393
 
@@ -380,7 +401,8 @@ export class TsTest {
380
401
  { TSTEST_FILE: fileNameArg, TSTEST_RUNTIME: 'docker' },
381
402
  );
382
403
  if (!success) {
383
- this.logger.error(`test:before:testfile failed for ${fileNameArg}. Skipping.`);
404
+ await this.recordRuntimeFailure(fileNameArg, 'Docker', fileIndex, totalFiles,
405
+ tapCombinator, 'test:before:testfile failed.');
384
406
  return;
385
407
  }
386
408
  }
@@ -389,7 +411,8 @@ export class TsTest {
389
411
  const tapParser = await this.dockerAdapter.run(fileNameArg, fileIndex, totalFiles);
390
412
  tapCombinator.addTapParser(tapParser);
391
413
  } catch (error) {
392
- this.logger.tapOutput(cs(`❌ Docker test failed: ${error.message}`, 'red'));
414
+ await this.recordRuntimeFailure(fileNameArg, 'Docker', fileIndex, totalFiles,
415
+ tapCombinator, error);
393
416
  }
394
417
  }
395
418