@git.zone/tstest 4.1.1 → 5.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.
@@ -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
 
@@ -17,6 +17,7 @@ export interface TestFileResult {
17
17
  failed: number;
18
18
  total: number;
19
19
  duration: number;
20
+ executionErrors?: string[];
20
21
  tests: Array<{
21
22
  name: string;
22
23
  passed: boolean;
@@ -30,6 +31,7 @@ export interface TestSummary {
30
31
  totalTests: number;
31
32
  totalPassed: number;
32
33
  totalFailed: number;
34
+ failedFiles: number;
33
35
  totalSkipped: number;
34
36
  totalDuration: number;
35
37
  fileResults: TestFileResult[];
@@ -50,6 +52,13 @@ export class TsTestLogger {
50
52
  this.options = options;
51
53
  this.startTime = Date.now();
52
54
  }
55
+
56
+ /** Keep each concurrent file's counters and log buffers separate. */
57
+ public createTestFileLogger(): TsTestLogger {
58
+ const logger = new TsTestLogger(this.options);
59
+ logger.fileResults = this.fileResults;
60
+ return logger;
61
+ }
53
62
 
54
63
  private format(text: string, color?: string): string {
55
64
  if (this.options.noColor || !color) {
@@ -305,14 +314,20 @@ export class TsTestLogger {
305
314
  this.currentTestLogs = [];
306
315
  }
307
316
 
308
- testFileEnd(passed: number, failed: number, duration: number) {
317
+ testFileEnd(passed: number, failed: number, duration: number, executionErrors: readonly string[] = []) {
318
+ const fileFailed = failed > 0 || executionErrors.length > 0;
309
319
  if (this.currentFileResult) {
320
+ this.currentFileResult.passed = passed;
321
+ this.currentFileResult.failed = failed;
322
+ this.currentFileResult.total = passed + failed;
323
+ this.currentFileResult.duration = duration;
324
+ this.currentFileResult.executionErrors = [...executionErrors];
310
325
  this.fileResults.push(this.currentFileResult);
311
326
  this.currentFileResult = null;
312
327
  }
313
328
 
314
329
  if (this.options.json) {
315
- this.logJson({ event: 'fileEnd', passed, failed, duration });
330
+ this.logJson({ event: 'fileEnd', passed, failed, duration, executionErrors, fileFailed });
316
331
  return;
317
332
  }
318
333
 
@@ -320,10 +335,10 @@ export class TsTestLogger {
320
335
  const total = passed + failed;
321
336
  const durationStr = duration >= 1000 ? `${(duration / 1000).toFixed(1)}s` : `${duration}ms`;
322
337
 
323
- if (failed === 0) {
338
+ if (!fileFailed) {
324
339
  this.log(this.format(` Summary: ${passed}/${total} PASSED in ${durationStr}`, 'green'));
325
340
  } else {
326
- this.log(this.format(` Summary: ${passed} passed, ${failed} failed of ${total} tests in ${durationStr}`, 'red'));
341
+ this.log(this.format(` Summary: FAILED — ${passed} passed, ${failed} failed of ${total} tests; ${executionErrors.length} execution errors in ${durationStr}`, 'red'));
327
342
  }
328
343
  }
329
344
 
@@ -335,7 +350,7 @@ export class TsTestLogger {
335
350
  const logBasename = path.basename(this.currentTestLogFile);
336
351
 
337
352
  // Create error copy if there were failures
338
- if (failed > 0) {
353
+ if (fileFailed) {
339
354
  const errorDir = path.join(logDir, '00err');
340
355
  if (!fs.existsSync(errorDir)) {
341
356
  fs.mkdirSync(errorDir, { recursive: true });
@@ -509,6 +524,7 @@ export class TsTestLogger {
509
524
  totalTests: this.fileResults.reduce((sum, r) => sum + r.total, 0),
510
525
  totalPassed: this.fileResults.reduce((sum, r) => sum + r.passed, 0),
511
526
  totalFailed: this.fileResults.reduce((sum, r) => sum + r.failed, 0),
527
+ failedFiles: this.fileResults.filter((r) => r.failed > 0 || r.executionErrors?.length > 0).length,
512
528
  totalSkipped: skippedFiles.length,
513
529
  totalDuration,
514
530
  fileResults: this.fileResults,
@@ -521,13 +537,13 @@ export class TsTestLogger {
521
537
  }
522
538
 
523
539
  if (this.options.quiet) {
524
- const status = summary.totalFailed === 0 ? 'PASSED' : 'FAILED';
540
+ const status = summary.failedFiles === 0 ? 'PASSED' : 'FAILED';
525
541
  const durationStr = totalDuration >= 1000 ? `${(totalDuration / 1000).toFixed(1)}s` : `${totalDuration}ms`;
526
542
 
527
- if (summary.totalFailed === 0) {
543
+ if (summary.failedFiles === 0) {
528
544
  this.log(`\nSummary: ${summary.totalPassed}/${summary.totalTests} | ${durationStr} | ${status}`);
529
545
  } else {
530
- this.log(`\nSummary: ${summary.totalPassed} passed, ${summary.totalFailed} failed of ${summary.totalTests} tests | ${durationStr} | ${status}`);
546
+ this.log(`\nSummary: ${summary.totalPassed} passed, ${summary.totalFailed} failed of ${summary.totalTests} tests | ${summary.failedFiles} failed files | ${durationStr} | ${status}`);
531
547
  }
532
548
  return;
533
549
  }
@@ -539,6 +555,7 @@ export class TsTestLogger {
539
555
  this.log(this.format(`│ Total Tests: ${summary.totalTests.toString().padStart(14)} │`, 'white'));
540
556
  this.log(this.format(`│ Passed: ${summary.totalPassed.toString().padStart(14)} │`, 'green'));
541
557
  this.log(this.format(`│ Failed: ${summary.totalFailed.toString().padStart(14)} │`, summary.totalFailed > 0 ? 'red' : 'green'));
558
+ this.log(this.format(`│ Failed Files: ${summary.failedFiles.toString().padStart(14)} │`, summary.failedFiles > 0 ? 'red' : 'green'));
542
559
  if (summary.totalSkipped > 0) {
543
560
  this.log(this.format(`│ Skipped: ${summary.totalSkipped.toString().padStart(14)} │`, 'yellow'));
544
561
  }
@@ -547,11 +564,14 @@ export class TsTestLogger {
547
564
  this.log(this.format('└────────────────────────────────┘', 'dim'));
548
565
 
549
566
  // File results
550
- if (summary.totalFailed > 0) {
567
+ if (summary.failedFiles > 0) {
551
568
  this.log(this.format('\n❌ Failed Tests:', 'red'));
552
569
  this.fileResults.forEach(fileResult => {
553
- if (fileResult.failed > 0) {
570
+ if (fileResult.failed > 0 || fileResult.executionErrors?.length > 0) {
554
571
  this.log(this.format(`\n ${fileResult.file}`, 'yellow'));
572
+ for (const error of fileResult.executionErrors || []) {
573
+ this.log(this.format(` ❌ ${error}`, 'red'));
574
+ }
555
575
  fileResult.tests.filter(t => !t.passed).forEach(test => {
556
576
  this.log(this.format(` ❌ ${test.name}`, 'red'));
557
577
  if (test.error) {
@@ -585,8 +605,8 @@ export class TsTestLogger {
585
605
  }
586
606
 
587
607
  // Final status
588
- const status = summary.totalFailed === 0 ? 'ALL TESTS PASSED! 🎉' : 'SOME TESTS FAILED! ❌';
589
- const statusColor = summary.totalFailed === 0 ? 'green' : 'red';
608
+ const status = summary.failedFiles === 0 ? 'ALL TESTS PASSED! 🎉' : 'SOME TESTS FAILED! ❌';
609
+ const statusColor = summary.failedFiles === 0 ? 'green' : 'red';
590
610
  this.log(this.format(`\n${status}`, statusColor));
591
611
  }
592
612
 
@@ -710,4 +730,4 @@ export class TsTestLogger {
710
730
 
711
731
  this.log(this.format('\n\n👋 Stopping watch mode...', 'cyan'));
712
732
  }
713
- }
733
+ }
@@ -1,8 +1,9 @@
1
1
  // node native
2
2
  import * as fs from 'fs';
3
3
  import * as path from 'path';
4
+ import * as url from 'node:url';
4
5
 
5
- export { fs, path };
6
+ export { fs, path, url };
6
7
 
7
8
  // @push.rocks scope
8
9
  import * as consolecolor from '@push.rocks/consolecolor';