@travetto/test 7.0.0-rc.0 → 7.0.0-rc.2
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 +6 -6
- package/package.json +7 -7
- package/src/assert/check.ts +46 -46
- package/src/assert/util.ts +30 -30
- package/src/consumer/registry-index.ts +4 -4
- package/src/consumer/types/cumulative.ts +10 -10
- package/src/consumer/types/delegating.ts +17 -17
- package/src/consumer/types/runnable.ts +3 -3
- package/src/consumer/types/summarizer.ts +10 -10
- package/src/consumer/types/tap-summary.ts +20 -20
- package/src/consumer/types/tap.ts +15 -15
- package/src/consumer/types/xunit.ts +15 -15
- package/src/decorator/suite.ts +2 -2
- package/src/decorator/test.ts +6 -4
- package/src/execute/barrier.ts +8 -8
- package/src/execute/console.ts +1 -1
- package/src/execute/executor.ts +11 -11
- package/src/execute/phase.ts +6 -6
- package/src/execute/runner.ts +4 -4
- package/src/execute/util.ts +11 -11
- package/src/execute/watcher.ts +16 -17
- package/src/fixture.ts +2 -2
- package/src/model/suite.ts +1 -1
- package/src/model/test.ts +1 -1
- package/src/registry/registry-adapter.ts +20 -20
- package/src/registry/registry-index.ts +16 -15
- package/src/worker/child.ts +10 -10
- package/src/worker/standard.ts +3 -3
- package/support/bin/run.ts +6 -6
- package/support/cli.test.ts +2 -2
- package/support/cli.test_digest.ts +5 -5
- package/support/cli.test_direct.ts +1 -1
- package/support/cli.test_watch.ts +2 -2
- package/support/transformer.assert.ts +12 -12
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#L14) 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#
|
|
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#L14) 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
|
|
|
@@ -35,14 +35,14 @@ import { Suite, Test } from '@travetto/test';
|
|
|
35
35
|
class SimpleTest {
|
|
36
36
|
|
|
37
37
|
#complexService: {
|
|
38
|
-
|
|
38
|
+
doLongOperation(): Promise<number>;
|
|
39
39
|
getText(): string;
|
|
40
40
|
};
|
|
41
41
|
|
|
42
42
|
@Test()
|
|
43
43
|
async test1() {
|
|
44
|
-
const
|
|
45
|
-
assert(
|
|
44
|
+
const value = await this.#complexService.doLongOperation();
|
|
45
|
+
assert(value === 5);
|
|
46
46
|
}
|
|
47
47
|
|
|
48
48
|
@Test()
|
|
@@ -212,8 +212,8 @@ class SimpleTest {
|
|
|
212
212
|
|
|
213
213
|
await assert.rejects(() => {
|
|
214
214
|
throw new Error('Big Error');
|
|
215
|
-
}, (
|
|
216
|
-
|
|
215
|
+
}, (error: Error) =>
|
|
216
|
+
error.message.startsWith('Big') && error.message.length > 4
|
|
217
217
|
);
|
|
218
218
|
}
|
|
219
219
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@travetto/test",
|
|
3
|
-
"version": "7.0.0-rc.
|
|
3
|
+
"version": "7.0.0-rc.2",
|
|
4
4
|
"description": "Declarative test framework",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"unit-testing",
|
|
@@ -27,15 +27,15 @@
|
|
|
27
27
|
"directory": "module/test"
|
|
28
28
|
},
|
|
29
29
|
"dependencies": {
|
|
30
|
-
"@travetto/registry": "^7.0.0-rc.
|
|
31
|
-
"@travetto/runtime": "^7.0.0-rc.
|
|
32
|
-
"@travetto/terminal": "^7.0.0-rc.
|
|
33
|
-
"@travetto/worker": "^7.0.0-rc.
|
|
30
|
+
"@travetto/registry": "^7.0.0-rc.2",
|
|
31
|
+
"@travetto/runtime": "^7.0.0-rc.2",
|
|
32
|
+
"@travetto/terminal": "^7.0.0-rc.2",
|
|
33
|
+
"@travetto/worker": "^7.0.0-rc.2",
|
|
34
34
|
"yaml": "^2.8.1"
|
|
35
35
|
},
|
|
36
36
|
"peerDependencies": {
|
|
37
|
-
"@travetto/cli": "^7.0.0-rc.
|
|
38
|
-
"@travetto/transformer": "^7.0.0-rc.
|
|
37
|
+
"@travetto/cli": "^7.0.0-rc.2",
|
|
38
|
+
"@travetto/transformer": "^7.0.0-rc.2"
|
|
39
39
|
},
|
|
40
40
|
"peerDependenciesMeta": {
|
|
41
41
|
"@travetto/transformer": {
|
package/src/assert/check.ts
CHANGED
|
@@ -13,7 +13,7 @@ type StringFields<T> = {
|
|
|
13
13
|
(T[K] extends string ? K : never)
|
|
14
14
|
}[Extract<keyof T, string>];
|
|
15
15
|
|
|
16
|
-
const isClass = (
|
|
16
|
+
const isClass = (input: unknown): input is Class => input === Error || input === AppError || Object.getPrototypeOf(input) !== Object.getPrototypeOf(Function);
|
|
17
17
|
|
|
18
18
|
/**
|
|
19
19
|
* Check assertion
|
|
@@ -35,7 +35,7 @@ export class AssertCheck {
|
|
|
35
35
|
};
|
|
36
36
|
|
|
37
37
|
// Invert check for negative
|
|
38
|
-
const assertFn = positive ? assert : (
|
|
38
|
+
const assertFn = positive ? assert : (value: unknown, msg?: string): unknown => assert(!value, msg);
|
|
39
39
|
|
|
40
40
|
// Check fn to call
|
|
41
41
|
if (fn === 'fail') {
|
|
@@ -102,36 +102,36 @@ export class AssertCheck {
|
|
|
102
102
|
|
|
103
103
|
// Pushing on not error
|
|
104
104
|
AssertCapture.add(assertion);
|
|
105
|
-
} catch (
|
|
105
|
+
} catch (error) {
|
|
106
106
|
// On error, produce the appropriate error message
|
|
107
|
-
if (
|
|
107
|
+
if (error instanceof assert.AssertionError) {
|
|
108
108
|
if (!assertion.message) {
|
|
109
109
|
assertion.message = (OP_MAPPING[fn] ?? '{state} be {expected}');
|
|
110
110
|
}
|
|
111
111
|
assertion.message = assertion.message
|
|
112
|
-
.replace(/[{]([A-Za-z]+)[}]/g, (a,
|
|
112
|
+
.replace(/[{]([A-Za-z]+)[}]/g, (a, key: StringFields<Assertion>) => common[key] || assertion[key]!)
|
|
113
113
|
.replace(/not not/g, ''); // Handle double negatives
|
|
114
|
-
assertion.error =
|
|
115
|
-
|
|
114
|
+
assertion.error = error;
|
|
115
|
+
error.message = assertion.message;
|
|
116
116
|
AssertCapture.add(assertion);
|
|
117
117
|
}
|
|
118
|
-
throw
|
|
118
|
+
throw error;
|
|
119
119
|
}
|
|
120
120
|
}
|
|
121
121
|
|
|
122
122
|
/**
|
|
123
123
|
* Check a given error
|
|
124
124
|
* @param shouldThrow Should the test throw anything
|
|
125
|
-
* @param
|
|
125
|
+
* @param error The provided error
|
|
126
126
|
*/
|
|
127
|
-
static checkError(shouldThrow: ThrowableError | undefined,
|
|
127
|
+
static checkError(shouldThrow: ThrowableError | undefined, error: Error | string | undefined): Error | undefined {
|
|
128
128
|
if (!shouldThrow) { // If we shouldn't be throwing anything, we are good
|
|
129
129
|
return;
|
|
130
|
-
} else if (!
|
|
130
|
+
} else if (!error) {
|
|
131
131
|
return new assert.AssertionError({ message: 'Expected to throw an error, but got nothing' });
|
|
132
132
|
} else if (typeof shouldThrow === 'string') {
|
|
133
|
-
if (!(
|
|
134
|
-
const actual =
|
|
133
|
+
if (!(error instanceof Error ? error.message : error).includes(shouldThrow)) {
|
|
134
|
+
const actual = error instanceof Error ? `'${error.message}'` : `'${error}'`;
|
|
135
135
|
return new assert.AssertionError({
|
|
136
136
|
message: `Expected error containing text '${shouldThrow}', but got ${actual}`,
|
|
137
137
|
actual,
|
|
@@ -139,8 +139,8 @@ export class AssertCheck {
|
|
|
139
139
|
});
|
|
140
140
|
}
|
|
141
141
|
} else if (shouldThrow instanceof RegExp) {
|
|
142
|
-
if (!shouldThrow.test(typeof
|
|
143
|
-
const actual =
|
|
142
|
+
if (!shouldThrow.test(typeof error === 'string' ? error : error.message)) {
|
|
143
|
+
const actual = error instanceof Error ? `'${error.message}'` : `'${error}'`;
|
|
144
144
|
return new assert.AssertionError({
|
|
145
145
|
message: `Expected error with message matching '${shouldThrow.source}', but got ${actual}`,
|
|
146
146
|
actual,
|
|
@@ -148,27 +148,27 @@ export class AssertCheck {
|
|
|
148
148
|
});
|
|
149
149
|
}
|
|
150
150
|
} else if (isClass(shouldThrow)) {
|
|
151
|
-
if (!(
|
|
151
|
+
if (!(error instanceof shouldThrow)) {
|
|
152
152
|
return new assert.AssertionError({
|
|
153
|
-
message: `Expected to throw ${shouldThrow.name}, but got ${
|
|
154
|
-
actual: (
|
|
153
|
+
message: `Expected to throw ${shouldThrow.name}, but got ${error}`,
|
|
154
|
+
actual: (error ?? 'nothing'),
|
|
155
155
|
expected: shouldThrow.name
|
|
156
156
|
});
|
|
157
157
|
}
|
|
158
158
|
} else if (typeof shouldThrow === 'function') {
|
|
159
159
|
const target = shouldThrow.name ? `("${shouldThrow.name}")` : '';
|
|
160
160
|
try {
|
|
161
|
-
const
|
|
162
|
-
if (
|
|
163
|
-
return new assert.AssertionError({ message: `Checking function ${target} indicated an invalid error`, actual:
|
|
164
|
-
} else if (typeof
|
|
165
|
-
return new assert.AssertionError({ message:
|
|
161
|
+
const result = shouldThrow(error);
|
|
162
|
+
if (result === false) {
|
|
163
|
+
return new assert.AssertionError({ message: `Checking function ${target} indicated an invalid error`, actual: error });
|
|
164
|
+
} else if (typeof result === 'string') {
|
|
165
|
+
return new assert.AssertionError({ message: result, actual: error });
|
|
166
166
|
}
|
|
167
|
-
} catch (
|
|
168
|
-
if (
|
|
169
|
-
return
|
|
167
|
+
} catch (checkError) {
|
|
168
|
+
if (checkError instanceof assert.AssertionError) {
|
|
169
|
+
return checkError;
|
|
170
170
|
} else {
|
|
171
|
-
return new assert.AssertionError({ message: `Checking function ${target} threw an error`, actual:
|
|
171
|
+
return new assert.AssertionError({ message: `Checking function ${target} threw an error`, actual: checkError });
|
|
172
172
|
}
|
|
173
173
|
}
|
|
174
174
|
}
|
|
@@ -177,26 +177,26 @@ export class AssertCheck {
|
|
|
177
177
|
static #onError(
|
|
178
178
|
positive: boolean,
|
|
179
179
|
message: string | undefined,
|
|
180
|
-
|
|
180
|
+
error: unknown,
|
|
181
181
|
missed: Error | undefined,
|
|
182
182
|
shouldThrow: ThrowableError | undefined,
|
|
183
183
|
assertion: CaptureAssert
|
|
184
184
|
): void {
|
|
185
|
-
if (!(
|
|
186
|
-
|
|
185
|
+
if (!(error instanceof Error)) {
|
|
186
|
+
error = new Error(`${error}`);
|
|
187
187
|
}
|
|
188
|
-
if (!(
|
|
189
|
-
throw
|
|
188
|
+
if (!(error instanceof Error)) {
|
|
189
|
+
throw error;
|
|
190
190
|
}
|
|
191
191
|
if (positive) {
|
|
192
192
|
missed = new assert.AssertionError({ message: 'Error thrown, but expected no errors' });
|
|
193
|
-
missed.stack =
|
|
193
|
+
missed.stack = error.stack;
|
|
194
194
|
}
|
|
195
195
|
|
|
196
|
-
const
|
|
197
|
-
if (
|
|
198
|
-
assertion.message = message || missed?.message ||
|
|
199
|
-
throw (assertion.error =
|
|
196
|
+
const resolvedError = (missed && error) ?? this.checkError(shouldThrow, error);
|
|
197
|
+
if (resolvedError) {
|
|
198
|
+
assertion.message = message || missed?.message || resolvedError.message;
|
|
199
|
+
throw (assertion.error = resolvedError);
|
|
200
200
|
}
|
|
201
201
|
}
|
|
202
202
|
|
|
@@ -225,8 +225,8 @@ export class AssertCheck {
|
|
|
225
225
|
}
|
|
226
226
|
throw (missed = new assert.AssertionError({ message: `No error thrown, but expected ${shouldThrow ?? 'an error'}`, expected: shouldThrow ?? 'an error' }));
|
|
227
227
|
}
|
|
228
|
-
} catch (
|
|
229
|
-
this.#onError(positive, message,
|
|
228
|
+
} catch (error) {
|
|
229
|
+
this.#onError(positive, message, error, missed, shouldThrow, assertion);
|
|
230
230
|
} finally {
|
|
231
231
|
AssertCapture.add(assertion);
|
|
232
232
|
}
|
|
@@ -261,8 +261,8 @@ export class AssertCheck {
|
|
|
261
261
|
}
|
|
262
262
|
throw (missed = new assert.AssertionError({ message: `No error thrown, but expected ${shouldThrow ?? 'an error'}`, expected: shouldThrow ?? 'an error' }));
|
|
263
263
|
}
|
|
264
|
-
} catch (
|
|
265
|
-
this.#onError(positive, message,
|
|
264
|
+
} catch (error) {
|
|
265
|
+
this.#onError(positive, message, error, missed, shouldThrow, assertion);
|
|
266
266
|
} finally {
|
|
267
267
|
AssertCapture.add(assertion);
|
|
268
268
|
}
|
|
@@ -271,8 +271,8 @@ export class AssertCheck {
|
|
|
271
271
|
/**
|
|
272
272
|
* Look for any unhandled exceptions
|
|
273
273
|
*/
|
|
274
|
-
static checkUnhandled(test: TestConfig,
|
|
275
|
-
let line = AssertUtil.getPositionOfError(
|
|
274
|
+
static checkUnhandled(test: TestConfig, error: Error | assert.AssertionError): void {
|
|
275
|
+
let line = AssertUtil.getPositionOfError(error, test.sourceImport ?? test.import).line;
|
|
276
276
|
if (line === 1) {
|
|
277
277
|
line = test.lineStart;
|
|
278
278
|
}
|
|
@@ -281,9 +281,9 @@ export class AssertCheck {
|
|
|
281
281
|
import: test.import,
|
|
282
282
|
line,
|
|
283
283
|
operator: 'throws',
|
|
284
|
-
error
|
|
285
|
-
message:
|
|
286
|
-
text: ('operator' in
|
|
284
|
+
error,
|
|
285
|
+
message: error.message,
|
|
286
|
+
text: ('operator' in error ? error.operator : '') || '(uncaught)'
|
|
287
287
|
});
|
|
288
288
|
}
|
|
289
289
|
}
|
package/src/assert/util.ts
CHANGED
|
@@ -15,53 +15,53 @@ export class AssertUtil {
|
|
|
15
15
|
/**
|
|
16
16
|
* Clean a value for displaying in the output
|
|
17
17
|
*/
|
|
18
|
-
static cleanValue(
|
|
19
|
-
switch (typeof
|
|
20
|
-
case 'number': case 'boolean': case 'bigint': case 'string': case 'undefined': return
|
|
18
|
+
static cleanValue(value: unknown): unknown {
|
|
19
|
+
switch (typeof value) {
|
|
20
|
+
case 'number': case 'boolean': case 'bigint': case 'string': case 'undefined': return value;
|
|
21
21
|
case 'object': {
|
|
22
|
-
if (isCleanable(
|
|
23
|
-
return
|
|
24
|
-
} else if (
|
|
25
|
-
return JSON.stringify(
|
|
22
|
+
if (isCleanable(value)) {
|
|
23
|
+
return value.toClean();
|
|
24
|
+
} else if (value === null || value.constructor === Object || Array.isArray(value) || value instanceof Date) {
|
|
25
|
+
return JSON.stringify(value);
|
|
26
26
|
}
|
|
27
27
|
break;
|
|
28
28
|
}
|
|
29
29
|
case 'function': {
|
|
30
|
-
if (
|
|
31
|
-
return
|
|
30
|
+
if (value.Ⲑid || !value.constructor) {
|
|
31
|
+
return value.name;
|
|
32
32
|
}
|
|
33
33
|
break;
|
|
34
34
|
}
|
|
35
35
|
}
|
|
36
|
-
return util.inspect(
|
|
36
|
+
return util.inspect(value, false, 1).replace(/\n/g, ' ');
|
|
37
37
|
}
|
|
38
38
|
|
|
39
39
|
/**
|
|
40
40
|
* Determine file location for a given error and the stack trace
|
|
41
41
|
*/
|
|
42
|
-
static getPositionOfError(
|
|
43
|
-
const
|
|
44
|
-
const lines = (
|
|
42
|
+
static getPositionOfError(error: Error, importLocation: string): { import: string, line: number } {
|
|
43
|
+
const workingDirectory = Runtime.mainSourcePath;
|
|
44
|
+
const lines = (error.stack ?? new Error().stack!)
|
|
45
45
|
.replace(/[\\/]/g, '/')
|
|
46
46
|
.split('\n')
|
|
47
47
|
// Exclude node_modules, target self
|
|
48
|
-
.filter(
|
|
48
|
+
.filter(lineText => lineText.includes(workingDirectory) && (!lineText.includes('node_modules') || lineText.includes('/support/')));
|
|
49
49
|
|
|
50
|
-
const filename = RuntimeIndex.getFromImport(
|
|
50
|
+
const filename = RuntimeIndex.getFromImport(importLocation)?.sourceFile!;
|
|
51
51
|
|
|
52
|
-
let best = lines.filter(
|
|
52
|
+
let best = lines.filter(lineText => lineText.includes(filename))[0];
|
|
53
53
|
|
|
54
54
|
if (!best) {
|
|
55
|
-
[best] = lines.filter(
|
|
55
|
+
[best] = lines.filter(lineText => lineText.includes(`${workingDirectory}/test`));
|
|
56
56
|
}
|
|
57
57
|
|
|
58
58
|
if (!best) {
|
|
59
|
-
return { import:
|
|
59
|
+
return { import: importLocation, line: 1 };
|
|
60
60
|
}
|
|
61
61
|
|
|
62
62
|
const pth = best.trim().split(/\s+/g).slice(1).pop()!;
|
|
63
63
|
if (!pth) {
|
|
64
|
-
return { import:
|
|
64
|
+
return { import: importLocation, line: 1 };
|
|
65
65
|
}
|
|
66
66
|
|
|
67
67
|
const [file, lineNo] = pth
|
|
@@ -74,21 +74,21 @@ export class AssertUtil {
|
|
|
74
74
|
line = -1;
|
|
75
75
|
}
|
|
76
76
|
|
|
77
|
-
const outFileParts = file.split(
|
|
77
|
+
const outFileParts = file.split(workingDirectory.replace(/^[A-Za-z]:/, ''));
|
|
78
78
|
|
|
79
79
|
const outFile = outFileParts.length > 1 ? outFileParts[1].replace(/^[\/]/, '') : filename;
|
|
80
80
|
|
|
81
|
-
const
|
|
81
|
+
const result = { import: RuntimeIndex.getFromSource(outFile)?.import!, line };
|
|
82
82
|
|
|
83
|
-
return
|
|
83
|
+
return result;
|
|
84
84
|
}
|
|
85
85
|
|
|
86
86
|
/**
|
|
87
87
|
* Generate a suite error given a suite config, and an error
|
|
88
88
|
*/
|
|
89
89
|
static generateSuiteFailure(suite: SuiteConfig, methodName: string, error: Error): SuiteFailure {
|
|
90
|
-
const { import: imp, ...
|
|
91
|
-
let line =
|
|
90
|
+
const { import: imp, ...rest } = this.getPositionOfError(error, suite.import);
|
|
91
|
+
let line = rest.line;
|
|
92
92
|
|
|
93
93
|
if (line === 1 && suite.lineStart) {
|
|
94
94
|
line = suite.lineStart;
|
|
@@ -118,13 +118,13 @@ export class AssertUtil {
|
|
|
118
118
|
/**
|
|
119
119
|
* Define import failure as a SuiteFailure object
|
|
120
120
|
*/
|
|
121
|
-
static gernerateImportFailure(
|
|
122
|
-
const name = path.basename(
|
|
123
|
-
const classId = `${RuntimeIndex.getFromImport(
|
|
121
|
+
static gernerateImportFailure(importLocation: string, error: Error): SuiteFailure {
|
|
122
|
+
const name = path.basename(importLocation);
|
|
123
|
+
const classId = `${RuntimeIndex.getFromImport(importLocation)?.id}#${name}`;
|
|
124
124
|
const suite = asFull<SuiteConfig & SuiteResult>({
|
|
125
|
-
class: asFull<Class>({ name }), classId, duration: 0, lineStart: 1, lineEnd: 1, import:
|
|
125
|
+
class: asFull<Class>({ name }), classId, duration: 0, lineStart: 1, lineEnd: 1, import: importLocation
|
|
126
126
|
});
|
|
127
|
-
|
|
128
|
-
return this.generateSuiteFailure(suite, 'require',
|
|
127
|
+
error.message = error.message.replaceAll(Runtime.mainSourcePath, '.');
|
|
128
|
+
return this.generateSuiteFailure(suite, 'require', error);
|
|
129
129
|
}
|
|
130
130
|
}
|
|
@@ -39,11 +39,11 @@ export class TestConsumerRegistryIndex implements RegistryIndex {
|
|
|
39
39
|
*/
|
|
40
40
|
async #init(): Promise<void> {
|
|
41
41
|
const allFiles = RuntimeIndex.find({
|
|
42
|
-
module:
|
|
43
|
-
file:
|
|
42
|
+
module: mod => mod.name === '@travetto/test',
|
|
43
|
+
file: file => file.relativeFile.startsWith('src/consumer/types/')
|
|
44
44
|
});
|
|
45
|
-
for (const
|
|
46
|
-
await import(
|
|
45
|
+
for (const file of allFiles) {
|
|
46
|
+
await import(file.outputFile);
|
|
47
47
|
}
|
|
48
48
|
for (const cls of this.store.getClasses()) {
|
|
49
49
|
this.store.finalize(cls);
|
|
@@ -30,7 +30,7 @@ export class CumulativeSummaryConsumer extends DelegatingConsumer {
|
|
|
30
30
|
// Was only loading to verify existence (TODO: double-check)
|
|
31
31
|
if (existsSync(RuntimeIndex.getFromImport(test.import)!.sourceFile)) {
|
|
32
32
|
(this.#state[test.classId] ??= {})[test.methodName] = test.status;
|
|
33
|
-
const SuiteCls = SuiteRegistryIndex.getClasses().find(
|
|
33
|
+
const SuiteCls = SuiteRegistryIndex.getClasses().find(cls => cls.Ⲑid === test.classId);
|
|
34
34
|
return SuiteCls ? this.computeTotal(SuiteCls) : this.removeClass(test.classId);
|
|
35
35
|
} else {
|
|
36
36
|
return this.removeClass(test.classId);
|
|
@@ -52,10 +52,10 @@ export class CumulativeSummaryConsumer extends DelegatingConsumer {
|
|
|
52
52
|
*/
|
|
53
53
|
computeTotal(cls: Class): SuiteResult {
|
|
54
54
|
const suite = SuiteRegistryIndex.getConfig(cls);
|
|
55
|
-
const total = Object.values(suite.tests).reduce((
|
|
56
|
-
const status = this.#state[
|
|
57
|
-
|
|
58
|
-
return
|
|
55
|
+
const total = Object.values(suite.tests).reduce((map, config) => {
|
|
56
|
+
const status = this.#state[config.classId][config.methodName] ?? 'unknown';
|
|
57
|
+
map[status] += 1;
|
|
58
|
+
return map;
|
|
59
59
|
}, { skipped: 0, passed: 0, failed: 0, unknown: 0 });
|
|
60
60
|
|
|
61
61
|
return {
|
|
@@ -76,17 +76,17 @@ export class CumulativeSummaryConsumer extends DelegatingConsumer {
|
|
|
76
76
|
* Listen for event, process the full event, and if the event is an after test,
|
|
77
77
|
* send a full suite summary
|
|
78
78
|
*/
|
|
79
|
-
onEventDone(
|
|
79
|
+
onEventDone(event: TestEvent): void {
|
|
80
80
|
try {
|
|
81
|
-
if (
|
|
81
|
+
if (event.type === 'test' && event.phase === 'after') {
|
|
82
82
|
this.onEvent({
|
|
83
83
|
type: 'suite',
|
|
84
84
|
phase: 'after',
|
|
85
|
-
suite: this.summarizeSuite(
|
|
85
|
+
suite: this.summarizeSuite(event.test),
|
|
86
86
|
});
|
|
87
87
|
}
|
|
88
|
-
} catch (
|
|
89
|
-
console.warn('Summarization Error', { error
|
|
88
|
+
} catch (error) {
|
|
89
|
+
console.warn('Summarization Error', { error });
|
|
90
90
|
}
|
|
91
91
|
}
|
|
92
92
|
}
|
|
@@ -6,53 +6,53 @@ import type { TestEvent } from '../../model/event.ts';
|
|
|
6
6
|
*/
|
|
7
7
|
export abstract class DelegatingConsumer implements TestConsumerShape {
|
|
8
8
|
#consumers: TestConsumerShape[];
|
|
9
|
-
#transformer?: (
|
|
10
|
-
#filter?: (
|
|
9
|
+
#transformer?: (event: TestEvent) => typeof event;
|
|
10
|
+
#filter?: (event: TestEvent) => boolean;
|
|
11
11
|
|
|
12
12
|
constructor(consumers: TestConsumerShape[]) {
|
|
13
13
|
this.#consumers = consumers;
|
|
14
|
-
for (const
|
|
15
|
-
|
|
14
|
+
for (const consumer of consumers) {
|
|
15
|
+
consumer.onEvent = consumer.onEvent.bind(consumer);
|
|
16
16
|
}
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
-
withTransformer(transformer: (
|
|
19
|
+
withTransformer(transformer: (event: TestEvent) => typeof event): this {
|
|
20
20
|
this.#transformer = transformer;
|
|
21
21
|
return this;
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
-
withFilter(filter: (
|
|
24
|
+
withFilter(filter: (event: TestEvent) => boolean): this {
|
|
25
25
|
this.#filter = filter;
|
|
26
26
|
return this;
|
|
27
27
|
}
|
|
28
28
|
|
|
29
29
|
async onStart(state: TestRunState): Promise<void> {
|
|
30
|
-
for (const
|
|
31
|
-
await
|
|
30
|
+
for (const consumer of this.#consumers) {
|
|
31
|
+
await consumer.onStart?.(state);
|
|
32
32
|
}
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
-
onEvent(
|
|
35
|
+
onEvent(event: TestEvent): void {
|
|
36
36
|
if (this.#transformer) {
|
|
37
|
-
|
|
37
|
+
event = this.#transformer(event);
|
|
38
38
|
}
|
|
39
|
-
if (this.#filter?.(
|
|
39
|
+
if (this.#filter?.(event) === false) {
|
|
40
40
|
return;
|
|
41
41
|
}
|
|
42
|
-
for (const
|
|
43
|
-
|
|
42
|
+
for (const consumer of this.#consumers) {
|
|
43
|
+
consumer.onEvent(event);
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
-
this.onEventDone?.(
|
|
46
|
+
this.onEventDone?.(event);
|
|
47
47
|
}
|
|
48
48
|
|
|
49
49
|
async summarize(summary?: SuitesSummary): Promise<void> {
|
|
50
50
|
if (summary) {
|
|
51
|
-
for (const
|
|
52
|
-
await
|
|
51
|
+
for (const consumer of this.#consumers) {
|
|
52
|
+
await consumer.onSummary?.(summary);
|
|
53
53
|
}
|
|
54
54
|
}
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
-
onEventDone?(
|
|
57
|
+
onEventDone?(event: TestEvent): void;
|
|
58
58
|
}
|
|
@@ -12,11 +12,11 @@ export class RunnableTestConsumer extends DelegatingConsumer {
|
|
|
12
12
|
|
|
13
13
|
constructor(...consumers: TestConsumerShape[]) {
|
|
14
14
|
super(consumers);
|
|
15
|
-
this.#results = consumers.find(
|
|
15
|
+
this.#results = consumers.find(consumer => !!consumer.onSummary) ? new TestResultsSummarizer() : undefined;
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
-
onEventDone(
|
|
19
|
-
this.#results?.onEvent(
|
|
18
|
+
onEventDone(event: TestEvent): void {
|
|
19
|
+
this.#results?.onEvent(event);
|
|
20
20
|
}
|
|
21
21
|
|
|
22
22
|
async summarizeAsBoolean(): Promise<boolean> {
|
|
@@ -17,21 +17,21 @@ export class TestResultsSummarizer implements TestConsumerShape {
|
|
|
17
17
|
errors: []
|
|
18
18
|
};
|
|
19
19
|
|
|
20
|
-
#merge(
|
|
21
|
-
this.summary.suites.push(
|
|
22
|
-
this.summary.failed +=
|
|
23
|
-
this.summary.passed +=
|
|
24
|
-
this.summary.skipped +=
|
|
25
|
-
this.summary.duration +=
|
|
26
|
-
this.summary.total += (
|
|
20
|
+
#merge(result: SuiteResult): void {
|
|
21
|
+
this.summary.suites.push(result);
|
|
22
|
+
this.summary.failed += result.failed;
|
|
23
|
+
this.summary.passed += result.passed;
|
|
24
|
+
this.summary.skipped += result.skipped;
|
|
25
|
+
this.summary.duration += result.duration;
|
|
26
|
+
this.summary.total += (result.failed + result.passed + result.skipped);
|
|
27
27
|
}
|
|
28
28
|
|
|
29
29
|
/**
|
|
30
30
|
* Merge all test results into a single Suite Result
|
|
31
31
|
*/
|
|
32
|
-
onEvent(
|
|
33
|
-
if (
|
|
34
|
-
this.#merge(
|
|
32
|
+
onEvent(event: TestEvent): void {
|
|
33
|
+
if (event.phase === 'after' && event.type === 'suite') {
|
|
34
|
+
this.#merge(event.suite);
|
|
35
35
|
}
|
|
36
36
|
}
|
|
37
37
|
}
|
|
@@ -56,7 +56,7 @@ export class TapSummaryEmitter implements TestConsumerShape {
|
|
|
56
56
|
const success = StyleUtil.getStyle({ text: '#e5e5e5', background: '#026020' }); // White on dark green
|
|
57
57
|
const fail = StyleUtil.getStyle({ text: '#e5e5e5', background: '#8b0000' }); // White on dark red
|
|
58
58
|
this.#progress = this.#terminal.streamToBottom(
|
|
59
|
-
Util.
|
|
59
|
+
Util.mapAsyncIterable(
|
|
60
60
|
this.#results,
|
|
61
61
|
(value) => {
|
|
62
62
|
failed += (value.status === 'failed' ? 1 : 0);
|
|
@@ -70,22 +70,22 @@ export class TapSummaryEmitter implements TestConsumerShape {
|
|
|
70
70
|
);
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
-
onEvent(
|
|
74
|
-
if (
|
|
75
|
-
const { test } =
|
|
73
|
+
onEvent(event: TestEvent): void {
|
|
74
|
+
if (event.type === 'test' && event.phase === 'after') {
|
|
75
|
+
const { test } = event;
|
|
76
76
|
this.#results.add(test);
|
|
77
77
|
if (test.status === 'failed') {
|
|
78
|
-
this.#consumer.onEvent(
|
|
78
|
+
this.#consumer.onEvent(event);
|
|
79
79
|
}
|
|
80
80
|
const tests = this.#timings.get('test')!;
|
|
81
|
-
tests.set(`${
|
|
82
|
-
key: `${
|
|
81
|
+
tests.set(`${event.test.classId}/${event.test.methodName}`, {
|
|
82
|
+
key: `${event.test.classId}/${event.test.methodName}`,
|
|
83
83
|
duration: test.duration,
|
|
84
84
|
tests: 1
|
|
85
85
|
});
|
|
86
|
-
} else if (
|
|
87
|
-
const [module] =
|
|
88
|
-
const [file] =
|
|
86
|
+
} else if (event.type === 'suite' && event.phase === 'after') {
|
|
87
|
+
const [module] = event.suite.classId.split(/:/);
|
|
88
|
+
const [file] = event.suite.classId.split(/#/);
|
|
89
89
|
|
|
90
90
|
const modules = this.#timings.get('module')!;
|
|
91
91
|
const files = this.#timings.get('file')!;
|
|
@@ -99,16 +99,16 @@ export class TapSummaryEmitter implements TestConsumerShape {
|
|
|
99
99
|
files.set(file, { key: file, duration: 0, tests: 0 });
|
|
100
100
|
}
|
|
101
101
|
|
|
102
|
-
suites.set(
|
|
103
|
-
key:
|
|
104
|
-
duration:
|
|
105
|
-
tests:
|
|
102
|
+
suites.set(event.suite.classId, {
|
|
103
|
+
key: event.suite.classId,
|
|
104
|
+
duration: event.suite.duration,
|
|
105
|
+
tests: event.suite.tests.length
|
|
106
106
|
});
|
|
107
107
|
|
|
108
|
-
files.get(file)!.duration +=
|
|
109
|
-
files.get(file)!.tests +=
|
|
110
|
-
modules.get(module)!.duration +=
|
|
111
|
-
modules.get(module)!.tests +=
|
|
108
|
+
files.get(file)!.duration += event.suite.duration;
|
|
109
|
+
files.get(file)!.tests += event.suite.tests.length;
|
|
110
|
+
modules.get(module)!.duration += event.suite.duration;
|
|
111
|
+
modules.get(module)!.tests += event.suite.tests.length;
|
|
112
112
|
}
|
|
113
113
|
}
|
|
114
114
|
|
|
@@ -127,8 +127,8 @@ export class TapSummaryEmitter implements TestConsumerShape {
|
|
|
127
127
|
await this.#consumer.log(`${this.#enhancer.suiteName(`Top ${count} slowest ${title}s`)}: `);
|
|
128
128
|
const top10 = [...results.values()].toSorted((a, b) => b.duration - a.duration).slice(0, count);
|
|
129
129
|
|
|
130
|
-
for (const
|
|
131
|
-
console.log(` * ${this.#enhancer.testName(
|
|
130
|
+
for (const result of top10) {
|
|
131
|
+
console.log(` * ${this.#enhancer.testName(result.key)} - ${this.#enhancer.total(result.duration)}ms / ${this.#enhancer.total(result.tests)} tests`);
|
|
132
132
|
}
|
|
133
133
|
await this.#consumer.log('');
|
|
134
134
|
}
|