@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.
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/tstest.classes.runtime.bun.js +4 -3
- package/dist_ts/tstest.classes.runtime.chromium.js +5 -4
- package/dist_ts/tstest.classes.runtime.deno.js +49 -117
- package/dist_ts/tstest.classes.runtime.docker.js +9 -10
- package/dist_ts/tstest.classes.runtime.node.js +8 -3
- package/dist_ts/tstest.classes.tap.combinator.js +2 -4
- package/dist_ts/tstest.classes.tap.parser.d.ts +5 -1
- package/dist_ts/tstest.classes.tap.parser.js +47 -68
- package/dist_ts/tstest.classes.testfile.directives.js +2 -2
- package/dist_ts/tstest.classes.tstest.d.ts +2 -0
- package/dist_ts/tstest.classes.tstest.js +24 -10
- package/dist_ts/tstest.logging.d.ts +5 -1
- package/dist_ts/tstest.logging.js +30 -13
- package/dist_ts/tstest.plugins.d.ts +2 -1
- package/dist_ts/tstest.plugins.js +3 -2
- package/package.json +1 -1
- package/readme.md +5 -2
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/tstest.classes.runtime.bun.ts +3 -2
- package/ts/tstest.classes.runtime.chromium.ts +4 -3
- package/ts/tstest.classes.runtime.deno.ts +47 -131
- package/ts/tstest.classes.runtime.docker.ts +8 -9
- package/ts/tstest.classes.runtime.node.ts +7 -2
- package/ts/tstest.classes.tap.combinator.ts +1 -3
- package/ts/tstest.classes.tap.parser.ts +46 -75
- package/ts/tstest.classes.testfile.directives.ts +1 -1
- package/ts/tstest.classes.tstest.ts +33 -10
- package/ts/tstest.logging.ts +33 -13
- package/ts/tstest.plugins.ts +2 -1
|
@@ -139,7 +139,8 @@ export class DockerRuntimeAdapter extends RuntimeAdapter {
|
|
|
139
139
|
total: number,
|
|
140
140
|
options?: RuntimeOptions
|
|
141
141
|
): Promise<TapParser> {
|
|
142
|
-
this.logger.
|
|
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',
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
101
|
-
|
|
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 (
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
471
|
-
|
|
472
|
-
this.
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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', '--
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
414
|
+
await this.recordRuntimeFailure(fileNameArg, 'Docker', fileIndex, totalFiles,
|
|
415
|
+
tapCombinator, error);
|
|
393
416
|
}
|
|
394
417
|
}
|
|
395
418
|
|
package/ts/tstest.logging.ts
CHANGED
|
@@ -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 (
|
|
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 (
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
589
|
-
const statusColor = summary.
|
|
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
|
+
}
|
package/ts/tstest.plugins.ts
CHANGED