@travetto/test 7.1.4 → 8.0.0-alpha.1
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/README.md +1 -1
- package/__index__.ts +2 -1
- package/package.json +7 -7
- package/src/assert/capture.ts +1 -0
- package/src/assert/check.ts +26 -9
- package/src/assert/util.ts +73 -73
- package/src/consumer/types/cumulative.ts +9 -1
- package/src/consumer/types/event.ts +3 -2
- package/src/consumer/types/exec.ts +2 -2
- package/src/consumer/types/json.ts +3 -1
- package/src/consumer/types/summarizer.ts +2 -0
- package/src/consumer/types/tap-summary.ts +59 -56
- package/src/consumer/types/tap.ts +35 -17
- package/src/consumer/types/xunit.ts +12 -10
- package/src/decorator/suite.ts +13 -9
- package/src/execute/barrier.ts +3 -3
- package/src/execute/executor.ts +80 -64
- package/src/execute/phase.ts +26 -41
- package/src/execute/run.ts +6 -7
- package/src/execute/watcher.ts +1 -1
- package/src/model/common.ts +2 -2
- package/src/model/error.ts +11 -0
- package/src/model/suite.ts +10 -27
- package/src/model/test.ts +5 -1
- package/src/model/util.ts +7 -1
- package/src/registry/registry-adapter.ts +28 -24
- package/src/registry/registry-index.ts +3 -3
- package/src/worker/child.ts +2 -3
- package/src/worker/standard.ts +8 -9
- package/support/bin/run.ts +1 -18
- package/support/cli.test.ts +4 -7
- package/support/cli.test_diff.ts +6 -7
- package/support/cli.test_digest.ts +3 -3
- package/support/cli.test_direct.ts +3 -6
- package/support/cli.test_watch.ts +4 -6
- package/src/communication.ts +0 -66
- package/src/execute/error.ts +0 -11
package/README.md
CHANGED
|
@@ -21,7 +21,7 @@ This module provides unit testing functionality that integrates with the framewo
|
|
|
21
21
|
**Note**: All tests should be under the `**/*` folders. The pattern for tests is defined as as a standard glob using [Node](https://nodejs.org)'s built in globbing support.
|
|
22
22
|
|
|
23
23
|
## Definition
|
|
24
|
-
A test suite is a collection of individual tests. All test suites are classes with the [@Suite](https://github.com/travetto/travetto/tree/main/module/test/src/decorator/suite.ts#
|
|
24
|
+
A test suite is a collection of individual tests. All test suites are classes with the [@Suite](https://github.com/travetto/travetto/tree/main/module/test/src/decorator/suite.ts#L13) decorator. Tests are defined as methods on the suite class, using the [@Test](https://github.com/travetto/travetto/tree/main/module/test/src/decorator/test.ts#L25) decorator. All tests intrinsically support `async`/`await`.
|
|
25
25
|
|
|
26
26
|
A simple example would be:
|
|
27
27
|
|
package/__index__.ts
CHANGED
|
@@ -2,6 +2,7 @@ import type { } from './src/trv.d.ts';
|
|
|
2
2
|
export * from './src/decorator/suite.ts';
|
|
3
3
|
export * from './src/decorator/test.ts';
|
|
4
4
|
export * from './src/model/suite.ts';
|
|
5
|
+
export * from './src/model/error.ts';
|
|
5
6
|
export * from './src/model/test.ts';
|
|
6
7
|
export * from './src/model/event.ts';
|
|
7
8
|
export * from './src/model/util.ts';
|
|
@@ -10,5 +11,5 @@ export * from './src/registry/registry-adapter.ts';
|
|
|
10
11
|
export * from './src/fixture.ts';
|
|
11
12
|
export * from './src/consumer/types.ts';
|
|
12
13
|
export * from './src/consumer/registry-index.ts';
|
|
13
|
-
|
|
14
|
+
|
|
14
15
|
export { TestWatchEvent } from './src/worker/types.ts';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@travetto/test",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "8.0.0-alpha.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Declarative test framework",
|
|
6
6
|
"keywords": [
|
|
@@ -28,15 +28,15 @@
|
|
|
28
28
|
"directory": "module/test"
|
|
29
29
|
},
|
|
30
30
|
"dependencies": {
|
|
31
|
-
"@travetto/registry": "^
|
|
32
|
-
"@travetto/runtime": "^
|
|
33
|
-
"@travetto/terminal": "^
|
|
34
|
-
"@travetto/worker": "^
|
|
31
|
+
"@travetto/registry": "^8.0.0-alpha.1",
|
|
32
|
+
"@travetto/runtime": "^8.0.0-alpha.1",
|
|
33
|
+
"@travetto/terminal": "^8.0.0-alpha.1",
|
|
34
|
+
"@travetto/worker": "^8.0.0-alpha.1",
|
|
35
35
|
"yaml": "^2.8.2"
|
|
36
36
|
},
|
|
37
37
|
"peerDependencies": {
|
|
38
|
-
"@travetto/cli": "^
|
|
39
|
-
"@travetto/transformer": "^
|
|
38
|
+
"@travetto/cli": "^8.0.0-alpha.1",
|
|
39
|
+
"@travetto/transformer": "^8.0.0-alpha.1"
|
|
40
40
|
},
|
|
41
41
|
"peerDependenciesMeta": {
|
|
42
42
|
"@travetto/transformer": {
|
package/src/assert/capture.ts
CHANGED
package/src/assert/check.ts
CHANGED
|
@@ -1,19 +1,20 @@
|
|
|
1
1
|
import assert from 'node:assert';
|
|
2
2
|
import { isPromise } from 'node:util/types';
|
|
3
3
|
|
|
4
|
-
import {
|
|
4
|
+
import { RuntimeError, type Class, castTo, castKey, asConstructable } from '@travetto/runtime';
|
|
5
5
|
|
|
6
|
-
import type { ThrowableError, TestConfig, Assertion } from '../model/test.ts';
|
|
6
|
+
import type { ThrowableError, TestConfig, Assertion, TestStatus } from '../model/test.ts';
|
|
7
7
|
import { AssertCapture, type CapturedAssertion } from './capture.ts';
|
|
8
8
|
import { AssertUtil } from './util.ts';
|
|
9
9
|
import { ASSERT_FN_OPERATOR, OP_MAPPING } from './types.ts';
|
|
10
|
+
import { TestExecutionError } from '../model/error.ts';
|
|
10
11
|
|
|
11
12
|
type StringFields<T> = {
|
|
12
13
|
[K in Extract<keyof T, string>]:
|
|
13
14
|
(T[K] extends string ? K : never)
|
|
14
15
|
}[Extract<keyof T, string>];
|
|
15
16
|
|
|
16
|
-
const isClass = (input: unknown): input is Class => input === Error || input ===
|
|
17
|
+
const isClass = (input: unknown): input is Class => input === Error || input === RuntimeError || Object.getPrototypeOf(input) !== Object.getPrototypeOf(Function);
|
|
17
18
|
|
|
18
19
|
/**
|
|
19
20
|
* Check assertion
|
|
@@ -272,18 +273,34 @@ export class AssertCheck {
|
|
|
272
273
|
* Look for any unhandled exceptions
|
|
273
274
|
*/
|
|
274
275
|
static checkUnhandled(test: TestConfig, error: Error | assert.AssertionError): void {
|
|
275
|
-
|
|
276
|
-
if (line === 1) {
|
|
277
|
-
line = test.lineStart;
|
|
278
|
-
}
|
|
276
|
+
const { line } = AssertUtil.getPositionOfError(error) ?? {};
|
|
279
277
|
|
|
280
278
|
AssertCapture.add({
|
|
281
|
-
import: test.import,
|
|
282
|
-
line,
|
|
279
|
+
import: test.declarationImport ?? test.import,
|
|
280
|
+
line: line ?? test.lineStart,
|
|
283
281
|
operator: 'throws',
|
|
284
282
|
error,
|
|
283
|
+
unexpected: true,
|
|
285
284
|
message: error.message,
|
|
286
285
|
text: ('operator' in error ? error.operator : '') || '(uncaught)'
|
|
287
286
|
});
|
|
288
287
|
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Validate the test result based on the error and test configuration
|
|
291
|
+
*/
|
|
292
|
+
static validateTestResultError(test: TestConfig, error: Error | undefined): [TestStatus, Error | undefined] {
|
|
293
|
+
if (error instanceof assert.AssertionError) {
|
|
294
|
+
return ['failed', error];
|
|
295
|
+
} else if (error instanceof TestExecutionError) {
|
|
296
|
+
this.checkUnhandled(test, error);
|
|
297
|
+
return ['errored', error];
|
|
298
|
+
} else if (error === undefined || test.shouldThrow) {
|
|
299
|
+
error = this.checkError(test.shouldThrow, error); // Rewrite error
|
|
300
|
+
return [error ? 'failed' : 'passed', error];
|
|
301
|
+
} else {
|
|
302
|
+
this.checkUnhandled(test, error);
|
|
303
|
+
return ['errored', error];
|
|
304
|
+
}
|
|
305
|
+
}
|
|
289
306
|
}
|
package/src/assert/util.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import util from 'node:util';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
|
|
4
|
-
import { asFull, type Class, hasFunction, Runtime, RuntimeIndex } from '@travetto/runtime';
|
|
4
|
+
import { asFull, type Class, JSONUtil, hasFunction, Runtime, RuntimeIndex, Util } from '@travetto/runtime';
|
|
5
5
|
|
|
6
|
-
import type { TestConfig,
|
|
7
|
-
import type { SuiteConfig,
|
|
6
|
+
import type { TestConfig, TestResult } from '../model/test.ts';
|
|
7
|
+
import type { SuiteConfig, SuiteResult } from '../model/suite.ts';
|
|
8
8
|
|
|
9
9
|
const isCleanable = hasFunction<{ toClean(): unknown }>('toClean');
|
|
10
10
|
|
|
@@ -22,7 +22,7 @@ export class AssertUtil {
|
|
|
22
22
|
if (isCleanable(value)) {
|
|
23
23
|
return value.toClean();
|
|
24
24
|
} else if (value === null || value.constructor === Object || Array.isArray(value) || value instanceof Date) {
|
|
25
|
-
return
|
|
25
|
+
return JSONUtil.toUTF8(value);
|
|
26
26
|
}
|
|
27
27
|
break;
|
|
28
28
|
}
|
|
@@ -39,92 +39,92 @@ export class AssertUtil {
|
|
|
39
39
|
/**
|
|
40
40
|
* Determine file location for a given error and the stack trace
|
|
41
41
|
*/
|
|
42
|
-
static getPositionOfError(error: Error
|
|
43
|
-
const
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
const filename = RuntimeIndex.getFromImport(importLocation)?.sourceFile!;
|
|
51
|
-
|
|
52
|
-
let best = lines.filter(lineText => lineText.includes(filename))[0];
|
|
53
|
-
|
|
54
|
-
if (!best) {
|
|
55
|
-
[best] = lines.filter(lineText => lineText.includes(`${workingDirectory}/test`));
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
if (!best) {
|
|
59
|
-
return { import: importLocation, line: 1 };
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
const pth = best.trim().split(/\s+/g).slice(1).pop()!;
|
|
63
|
-
if (!pth) {
|
|
64
|
-
return { import: importLocation, line: 1 };
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
const [file, lineNo] = pth
|
|
68
|
-
.replace(/[()]/g, '')
|
|
69
|
-
.replace(/^[A-Za-z]:/, '')
|
|
70
|
-
.split(':');
|
|
71
|
-
|
|
72
|
-
let line = parseInt(lineNo, 10);
|
|
73
|
-
if (Number.isNaN(line)) {
|
|
74
|
-
line = -1;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
const outFileParts = file.split(workingDirectory.replace(/^[A-Za-z]:/, ''));
|
|
78
|
-
|
|
79
|
-
const outFile = outFileParts.length > 1 ? outFileParts[1].replace(/^[\/]/, '') : filename;
|
|
80
|
-
|
|
81
|
-
const result = { import: RuntimeIndex.getFromSource(outFile)?.import!, line };
|
|
82
|
-
|
|
83
|
-
return result;
|
|
42
|
+
static getPositionOfError(error: Error): { import: string, line: number } | undefined {
|
|
43
|
+
const frames = Util.stackTraceToParts(error.stack ?? new Error().stack!)
|
|
44
|
+
.map(frame => {
|
|
45
|
+
const entry = RuntimeIndex.getEntry(frame.filename);
|
|
46
|
+
return { ...frame, import: entry?.import!, line: entry?.type === 'ts' ? frame.line : 1 };
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
return frames.find(frame => frame.import);
|
|
84
50
|
}
|
|
85
51
|
|
|
86
52
|
/**
|
|
87
53
|
* Generate a suite error given a suite config, and an error
|
|
88
54
|
*/
|
|
89
|
-
static
|
|
90
|
-
const {
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
line = suite.lineStart;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
const msg = error.message.split(/\n/)[0];
|
|
98
|
-
|
|
99
|
-
const core = { import: imp, classId: suite.classId, methodName, sourceHash: suite.sourceHash };
|
|
100
|
-
const coreAll = { ...core, description: msg, lineStart: line, lineEnd: line, lineBodyStart: line };
|
|
101
|
-
|
|
102
|
-
const assert: Assertion = {
|
|
103
|
-
...core,
|
|
104
|
-
operator: 'throw', error, line, message: msg, text: methodName
|
|
105
|
-
};
|
|
55
|
+
static generateSuiteTestFailure(config: { suite: SuiteConfig, test: TestConfig, error: Error, importLocation?: string }): TestResult {
|
|
56
|
+
const { suite, test, error, importLocation } = config;
|
|
57
|
+
const testImport = importLocation ?? test.import;
|
|
58
|
+
const position = this.getPositionOfError(error);
|
|
59
|
+
const line = position?.line ?? (testImport === suite.import ? suite.lineStart : 1);
|
|
106
60
|
const testResult: TestResult = {
|
|
107
|
-
...
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
61
|
+
...suite.tests[test.methodName],
|
|
62
|
+
suiteLineStart: suite.lineStart,
|
|
63
|
+
status: 'errored',
|
|
64
|
+
error,
|
|
65
|
+
duration: 0,
|
|
66
|
+
durationTotal: 0,
|
|
67
|
+
output: [],
|
|
68
|
+
assertions: [{
|
|
69
|
+
import: position?.import ?? testImport,
|
|
70
|
+
methodName: test.methodName,
|
|
71
|
+
classId: suite.classId,
|
|
72
|
+
operator: 'throw',
|
|
73
|
+
error,
|
|
74
|
+
line,
|
|
75
|
+
message: error.message.split(/\n/)[0],
|
|
76
|
+
text: test.methodName
|
|
77
|
+
}],
|
|
113
78
|
};
|
|
114
79
|
|
|
115
|
-
return
|
|
80
|
+
return testResult;
|
|
116
81
|
}
|
|
117
82
|
|
|
118
83
|
/**
|
|
119
|
-
*
|
|
84
|
+
* Generate suite failure
|
|
120
85
|
*/
|
|
121
|
-
static
|
|
86
|
+
static generateSuiteTestFailures(suite: SuiteConfig, error: Error): TestResult[] {
|
|
87
|
+
const finalError = error.cause instanceof Error ? error.cause : error;
|
|
88
|
+
return Object.values(suite.tests).map(test => this.generateSuiteTestFailure({ suite, test, error: finalError }));
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Define import failure as a TestResult
|
|
93
|
+
*/
|
|
94
|
+
static gernerateImportFailure(importLocation: string, error: Error): { result: TestResult, test: TestConfig, suite: SuiteResult & SuiteConfig } {
|
|
122
95
|
const name = path.basename(importLocation);
|
|
123
96
|
const classId = `${RuntimeIndex.getFromImport(importLocation)?.id}#${name}`;
|
|
124
97
|
const suite = asFull<SuiteConfig & SuiteResult>({
|
|
125
98
|
class: asFull<Class>({ name }), classId, duration: 0, lineStart: 1, lineEnd: 1, import: importLocation
|
|
126
99
|
});
|
|
127
100
|
error.message = error.message.replaceAll(Runtime.mainSourcePath, '.');
|
|
128
|
-
|
|
101
|
+
const result = this.generateSuiteTestFailure({
|
|
102
|
+
suite,
|
|
103
|
+
test: {
|
|
104
|
+
methodName: 'require',
|
|
105
|
+
classId,
|
|
106
|
+
import: importLocation,
|
|
107
|
+
class: suite.class,
|
|
108
|
+
lineBodyStart: 1,
|
|
109
|
+
lineStart: 1,
|
|
110
|
+
lineEnd: 1,
|
|
111
|
+
skip: false
|
|
112
|
+
},
|
|
113
|
+
error
|
|
114
|
+
});
|
|
115
|
+
const test: TestConfig = {
|
|
116
|
+
methodName: 'import',
|
|
117
|
+
classId,
|
|
118
|
+
import: importLocation,
|
|
119
|
+
declarationImport: importLocation,
|
|
120
|
+
lineStart: 0,
|
|
121
|
+
lineEnd: 0,
|
|
122
|
+
lineBodyStart: 0,
|
|
123
|
+
tags: [],
|
|
124
|
+
description: 'Import Failure',
|
|
125
|
+
skip: false,
|
|
126
|
+
class: undefined!
|
|
127
|
+
};
|
|
128
|
+
return { result, test, suite };
|
|
129
129
|
}
|
|
130
130
|
}
|
|
@@ -56,7 +56,15 @@ export class CumulativeSummaryConsumer extends DelegatingConsumer {
|
|
|
56
56
|
onSuiteAfter(result: SuiteResult): SuiteResult {
|
|
57
57
|
// Reset counts
|
|
58
58
|
const suite = this.getSuite(result);
|
|
59
|
-
const totals: Counts & { duration: number } = {
|
|
59
|
+
const totals: Counts & { duration: number } = {
|
|
60
|
+
passed: 0,
|
|
61
|
+
failed: 0,
|
|
62
|
+
skipped: 0,
|
|
63
|
+
errored: 0,
|
|
64
|
+
unknown: 0,
|
|
65
|
+
total: 0,
|
|
66
|
+
duration: 0
|
|
67
|
+
};
|
|
60
68
|
for (const test of Object.values(suite.tests)) {
|
|
61
69
|
totals[test.status] += 1;
|
|
62
70
|
totals.total += 1;
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import type { Writable } from 'node:stream';
|
|
2
2
|
|
|
3
|
+
import { JSONUtil } from '@travetto/runtime';
|
|
4
|
+
|
|
3
5
|
import type { TestEvent, TestRemoveEvent } from '../../model/event.ts';
|
|
4
6
|
import type { TestConsumerShape } from '../types.ts';
|
|
5
7
|
import { TestConsumer } from '../decorator.ts';
|
|
6
|
-
import { CommunicationUtil } from '../../communication.ts';
|
|
7
8
|
|
|
8
9
|
/**
|
|
9
10
|
* Streams all test events a JSON payload, in an nd-json format
|
|
@@ -17,7 +18,7 @@ export class EventStreamer implements TestConsumerShape {
|
|
|
17
18
|
}
|
|
18
19
|
|
|
19
20
|
sendPayload(payload: unknown): void {
|
|
20
|
-
this.#stream.write(`${
|
|
21
|
+
this.#stream.write(`${JSONUtil.toUTF8(JSONUtil.cloneForTransmit(payload))}\n`);
|
|
21
22
|
}
|
|
22
23
|
|
|
23
24
|
onEvent(event: TestEvent): void {
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { IpcChannel } from '@travetto/worker';
|
|
2
|
+
import { JSONUtil } from '@travetto/runtime';
|
|
2
3
|
|
|
3
4
|
import type { TestEvent, TestRemoveEvent } from '../../model/event.ts';
|
|
4
5
|
import type { TestConsumerShape } from '../types.ts';
|
|
5
6
|
import { TestConsumer } from '../decorator.ts';
|
|
6
|
-
import { CommunicationUtil } from '../../communication.ts';
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
9
|
* Triggers each event as an IPC command to a parent process
|
|
@@ -12,7 +12,7 @@ import { CommunicationUtil } from '../../communication.ts';
|
|
|
12
12
|
export class ExecutionEmitter extends IpcChannel<TestEvent> implements TestConsumerShape {
|
|
13
13
|
|
|
14
14
|
sendPayload(payload: unknown & { type: string }): void {
|
|
15
|
-
this.send(payload.type,
|
|
15
|
+
this.send(payload.type, JSONUtil.cloneForTransmit(payload));
|
|
16
16
|
}
|
|
17
17
|
|
|
18
18
|
onEvent(event: TestEvent): void {
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import type { Writable } from 'node:stream';
|
|
2
2
|
|
|
3
|
+
import { JSONUtil } from '@travetto/runtime';
|
|
4
|
+
|
|
3
5
|
import type { SuitesSummary } from '../types.ts';
|
|
4
6
|
import { TestConsumer } from '../decorator.ts';
|
|
5
7
|
|
|
@@ -18,6 +20,6 @@ export class JSONEmitter {
|
|
|
18
20
|
onEvent(): void { }
|
|
19
21
|
|
|
20
22
|
onSummary(summary: SuitesSummary): void {
|
|
21
|
-
this.#stream.write(
|
|
23
|
+
this.#stream.write(JSONUtil.toUTF8Pretty(summary));
|
|
22
24
|
}
|
|
23
25
|
}
|
|
@@ -10,6 +10,7 @@ export class TestResultsSummarizer implements TestConsumerShape {
|
|
|
10
10
|
summary: SuitesSummary = {
|
|
11
11
|
passed: 0,
|
|
12
12
|
failed: 0,
|
|
13
|
+
errored: 0,
|
|
13
14
|
skipped: 0,
|
|
14
15
|
unknown: 0,
|
|
15
16
|
total: 0,
|
|
@@ -21,6 +22,7 @@ export class TestResultsSummarizer implements TestConsumerShape {
|
|
|
21
22
|
#merge(result: SuiteResult): void {
|
|
22
23
|
this.summary.suites.push(result);
|
|
23
24
|
this.summary.failed += result.failed;
|
|
25
|
+
this.summary.errored += result.errored;
|
|
24
26
|
this.summary.passed += result.passed;
|
|
25
27
|
this.summary.unknown += result.unknown;
|
|
26
28
|
this.summary.skipped += result.skipped;
|
|
@@ -2,13 +2,14 @@ import { Util, AsyncQueue } from '@travetto/runtime';
|
|
|
2
2
|
import { StyleUtil, Terminal, TerminalUtil } from '@travetto/terminal';
|
|
3
3
|
|
|
4
4
|
import type { TestEvent } from '../../model/event.ts';
|
|
5
|
-
import type { TestResult } from '../../model/test.ts';
|
|
5
|
+
import type { TestResult, TestStatus } from '../../model/test.ts';
|
|
6
6
|
|
|
7
7
|
import type { SuitesSummary, TestConsumerShape, TestRunState } from '../types.ts';
|
|
8
8
|
import { TestConsumer } from '../decorator.ts';
|
|
9
9
|
|
|
10
10
|
import { TapEmitter } from './tap.ts';
|
|
11
11
|
import { CONSOLE_ENHANCER, type TestResultsEnhancer } from '../enhancer.ts';
|
|
12
|
+
import type { SuiteResult } from '../../model/suite.ts';
|
|
12
13
|
|
|
13
14
|
type Result = {
|
|
14
15
|
key: string;
|
|
@@ -22,12 +23,7 @@ type Result = {
|
|
|
22
23
|
@TestConsumer()
|
|
23
24
|
export class TapSummaryEmitter implements TestConsumerShape {
|
|
24
25
|
|
|
25
|
-
#timings = new Map(
|
|
26
|
-
['file', new Map<string, Result>()],
|
|
27
|
-
['module', new Map<string, Result>()],
|
|
28
|
-
['suite', new Map<string, Result>()],
|
|
29
|
-
['test', new Map<string, Result>()],
|
|
30
|
-
] as const);
|
|
26
|
+
#timings = new Map<'test' | 'module' | 'file' | 'suite', Map<string, Result>>();
|
|
31
27
|
|
|
32
28
|
#terminal: Terminal;
|
|
33
29
|
#results = new AsyncQueue<TestResult>();
|
|
@@ -42,6 +38,50 @@ export class TapSummaryEmitter implements TestConsumerShape {
|
|
|
42
38
|
this.#consumer = new TapEmitter(this.#terminal, this.#enhancer);
|
|
43
39
|
}
|
|
44
40
|
|
|
41
|
+
#computeTimings(suite: SuiteResult): void {
|
|
42
|
+
const [module] = suite.classId.split(/:/);
|
|
43
|
+
const [file] = suite.classId.split(/#/);
|
|
44
|
+
|
|
45
|
+
const testCount = Object.keys(suite.tests).length;
|
|
46
|
+
|
|
47
|
+
const foundModule = this.#timings
|
|
48
|
+
.getOrInsert('module', new Map<string, Result>())
|
|
49
|
+
.getOrInsert(module, { key: module, duration: 0, tests: 0 });
|
|
50
|
+
|
|
51
|
+
foundModule.duration += suite.duration;
|
|
52
|
+
foundModule.tests += testCount;
|
|
53
|
+
|
|
54
|
+
const foundFile = this.#timings
|
|
55
|
+
.getOrInsert('file', new Map<string, Result>())
|
|
56
|
+
.getOrInsert(file, { key: file, duration: 0, tests: 0 });
|
|
57
|
+
|
|
58
|
+
foundFile.duration += suite.duration;
|
|
59
|
+
foundFile.tests += testCount;
|
|
60
|
+
|
|
61
|
+
this.#timings
|
|
62
|
+
.getOrInsert('suite', new Map<string, Result>())
|
|
63
|
+
.set(suite.classId, {
|
|
64
|
+
key: suite.classId,
|
|
65
|
+
duration: suite.duration,
|
|
66
|
+
tests: testCount
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
#renderTimings(): void {
|
|
71
|
+
const count = +(this.#options?.count ?? 5);
|
|
72
|
+
this.#consumer.log('\n---');
|
|
73
|
+
for (const [title, results] of [...this.#timings.entries()].toSorted((a, b) => a[0].localeCompare(b[0]))) {
|
|
74
|
+
this.#consumer.log(`${this.#enhancer.suiteName(`Top ${count} slowest ${title}s`)}: `);
|
|
75
|
+
const top10 = [...results.values()].toSorted((a, b) => b.duration - a.duration).slice(0, count);
|
|
76
|
+
|
|
77
|
+
for (const result of top10) {
|
|
78
|
+
console.log(` * ${this.#enhancer.testName(result.key)} - ${this.#enhancer.total(result.duration)}ms / ${this.#enhancer.total(result.tests)} tests`);
|
|
79
|
+
}
|
|
80
|
+
this.#consumer.log('');
|
|
81
|
+
}
|
|
82
|
+
this.#consumer.log('...');
|
|
83
|
+
}
|
|
84
|
+
|
|
45
85
|
setOptions(options?: Record<string, unknown>): Promise<void> | void {
|
|
46
86
|
this.#options = options;
|
|
47
87
|
this.#consumer.setOptions(options);
|
|
@@ -49,22 +89,20 @@ export class TapSummaryEmitter implements TestConsumerShape {
|
|
|
49
89
|
|
|
50
90
|
async onStart(state: TestRunState): Promise<void> {
|
|
51
91
|
this.#consumer.onStart();
|
|
52
|
-
|
|
53
|
-
let failed = 0;
|
|
54
|
-
let skipped = 0;
|
|
55
|
-
let completed = 0;
|
|
92
|
+
const total: Record<TestStatus | 'count', number> = { errored: 0, failed: 0, passed: 0, skipped: 0, unknown: 0, count: 0 };
|
|
56
93
|
const success = StyleUtil.getStyle({ text: '#e5e5e5', background: '#026020' }); // White on dark green
|
|
57
94
|
const fail = StyleUtil.getStyle({ text: '#e5e5e5', background: '#8b0000' }); // White on dark red
|
|
58
95
|
this.#progress = this.#terminal.streamToBottom(
|
|
59
96
|
Util.mapAsyncIterable(
|
|
60
97
|
this.#results,
|
|
61
98
|
(value) => {
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
return { value: `Tests %idx/%total [${
|
|
99
|
+
total[value.status] += 1;
|
|
100
|
+
total.count += 1;
|
|
101
|
+
const statusLine = `${total.failed} failed, ${total.errored} errored, ${total.skipped} skipped`;
|
|
102
|
+
return { value: `Tests %idx/%total [${statusLine}] -- ${value.classId}`, total: state.testCount, idx: total.passed };
|
|
103
|
+
|
|
66
104
|
},
|
|
67
|
-
TerminalUtil.progressBarUpdater(this.#terminal, { style: () => ({ complete: failed ? fail : success }) })
|
|
105
|
+
TerminalUtil.progressBarUpdater(this.#terminal, { style: () => ({ complete: (total.failed || total.errored) ? fail : success }) })
|
|
68
106
|
),
|
|
69
107
|
{ minDelay: 100 }
|
|
70
108
|
);
|
|
@@ -77,40 +115,16 @@ export class TapSummaryEmitter implements TestConsumerShape {
|
|
|
77
115
|
if (test.status === 'failed') {
|
|
78
116
|
this.#consumer.onEvent(event);
|
|
79
117
|
}
|
|
80
|
-
const tests = this.#timings.
|
|
118
|
+
const tests = this.#timings.getOrInsert('test', new Map<string, Result>());
|
|
81
119
|
tests.set(`${event.test.classId}/${event.test.methodName}`, {
|
|
82
120
|
key: `${event.test.classId}/${event.test.methodName}`,
|
|
83
121
|
duration: test.duration,
|
|
84
122
|
tests: 1
|
|
85
123
|
});
|
|
86
124
|
} else if (event.type === 'suite' && event.phase === 'after') {
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
const modules = this.#timings.get('module')!;
|
|
91
|
-
const files = this.#timings.get('file')!;
|
|
92
|
-
const suites = this.#timings.get('suite')!;
|
|
93
|
-
|
|
94
|
-
if (!modules!.has(module)) {
|
|
95
|
-
modules.set(module, { key: module, duration: 0, tests: 0 });
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
if (!files.has(file)) {
|
|
99
|
-
files.set(file, { key: file, duration: 0, tests: 0 });
|
|
125
|
+
if (this.#options?.timings) {
|
|
126
|
+
this.#computeTimings(event.suite);
|
|
100
127
|
}
|
|
101
|
-
|
|
102
|
-
const testCount = Object.keys(event.suite.tests).length;
|
|
103
|
-
|
|
104
|
-
suites.set(event.suite.classId, {
|
|
105
|
-
key: event.suite.classId,
|
|
106
|
-
duration: event.suite.duration,
|
|
107
|
-
tests: testCount
|
|
108
|
-
});
|
|
109
|
-
|
|
110
|
-
files.get(file)!.duration += event.suite.duration;
|
|
111
|
-
files.get(file)!.tests += testCount;
|
|
112
|
-
modules.get(module)!.duration += event.suite.duration;
|
|
113
|
-
modules.get(module)!.tests += testCount;
|
|
114
128
|
}
|
|
115
129
|
}
|
|
116
130
|
|
|
@@ -120,21 +134,10 @@ export class TapSummaryEmitter implements TestConsumerShape {
|
|
|
120
134
|
async onSummary(summary: SuitesSummary): Promise<void> {
|
|
121
135
|
this.#results.close();
|
|
122
136
|
await this.#progress;
|
|
123
|
-
|
|
137
|
+
this.#consumer.onSummary?.(summary);
|
|
124
138
|
|
|
125
139
|
if (this.#options?.timings) {
|
|
126
|
-
|
|
127
|
-
await this.#consumer.log('\n---');
|
|
128
|
-
for (const [title, results] of [...this.#timings.entries()].toSorted((a, b) => a[0].localeCompare(b[0]))) {
|
|
129
|
-
await this.#consumer.log(`${this.#enhancer.suiteName(`Top ${count} slowest ${title}s`)}: `);
|
|
130
|
-
const top10 = [...results.values()].toSorted((a, b) => b.duration - a.duration).slice(0, count);
|
|
131
|
-
|
|
132
|
-
for (const result of top10) {
|
|
133
|
-
console.log(` * ${this.#enhancer.testName(result.key)} - ${this.#enhancer.total(result.duration)}ms / ${this.#enhancer.total(result.tests)} tests`);
|
|
134
|
-
}
|
|
135
|
-
await this.#consumer.log('');
|
|
136
|
-
}
|
|
137
|
-
await this.#consumer.log('...');
|
|
140
|
+
this.#renderTimings();
|
|
138
141
|
}
|
|
139
142
|
}
|
|
140
143
|
}
|