@travetto/test 5.0.0-rc.0 → 5.0.0-rc.10
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 +12 -13
- package/package.json +8 -8
- package/src/assert/capture.ts +5 -4
- package/src/assert/check.ts +24 -33
- package/src/assert/util.ts +32 -17
- package/src/consumer/registry.ts +2 -3
- package/src/consumer/{error.ts → serialize.ts} +13 -22
- package/src/consumer/types/cumulative.ts +15 -22
- package/src/consumer/types/delegating.ts +58 -0
- package/src/consumer/types/event.ts +2 -4
- package/src/consumer/types/execution.ts +2 -4
- package/src/consumer/types/runnable.ts +12 -41
- package/src/consumer/types/tap-streamed.ts +9 -6
- package/src/consumer/types/tap.ts +5 -5
- package/src/consumer/types/xunit.ts +4 -2
- package/src/decorator/suite.ts +5 -7
- package/src/decorator/test.ts +2 -1
- package/src/execute/console.ts +1 -1
- package/src/execute/executor.ts +84 -104
- package/src/execute/phase.ts +20 -30
- package/src/execute/promise.ts +4 -4
- package/src/execute/runner.ts +34 -24
- package/src/execute/types.ts +12 -10
- package/src/execute/util.ts +61 -34
- package/src/execute/watcher.ts +34 -36
- package/src/fixture.ts +7 -2
- package/src/model/common.ts +11 -7
- package/src/model/event.ts +9 -5
- package/src/model/suite.ts +14 -4
- package/src/model/test.ts +30 -4
- package/src/registry/suite.ts +42 -39
- package/src/trv.d.ts +3 -3
- package/src/worker/child.ts +11 -18
- package/src/worker/standard.ts +18 -21
- package/src/worker/types.ts +13 -10
- package/support/cli.test.ts +20 -6
- package/support/cli.test_child.ts +1 -1
- package/support/cli.test_digest.ts +43 -0
- package/support/cli.test_direct.ts +10 -3
- package/support/cli.test_watch.ts +1 -1
- package/support/transformer.assert.ts +12 -12
- package/support/cli.test_count.ts +0 -39
- package/support/transformer.annotate.ts +0 -103
package/README.md
CHANGED
|
@@ -18,10 +18,10 @@ This module provides unit testing functionality that integrates with the framewo
|
|
|
18
18
|
* [JSON](https://www.json.org), best for integrating with at a code level
|
|
19
19
|
* [xUnit](https://en.wikipedia.org/wiki/XUnit), standard format for CI/CD systems e.g. Jenkins, Bamboo, etc.
|
|
20
20
|
|
|
21
|
-
**Note**: All tests should be under the
|
|
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#L20) decorator. All tests intrinsically support `async`/`await`.
|
|
25
25
|
|
|
26
26
|
A simple example would be:
|
|
27
27
|
|
|
@@ -79,27 +79,25 @@ would translate to:
|
|
|
79
79
|
"use strict";
|
|
80
80
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
81
81
|
const tslib_1 = require("tslib");
|
|
82
|
-
const Ⲑ
|
|
82
|
+
const Ⲑ_debug_1 = tslib_1.__importStar(require("@travetto/runtime/src/debug.js"));
|
|
83
83
|
const Ⲑ_check_1 = tslib_1.__importStar(require("@travetto/test/src/assert/check.js"));
|
|
84
|
-
const Ⲑ
|
|
85
|
-
|
|
86
|
-
var ᚕf = "@travetto/test/doc/assert-example.js";
|
|
84
|
+
const Ⲑ_function_1 = tslib_1.__importStar(require("@travetto/runtime/src/function.js"));
|
|
85
|
+
var ᚕm = ["@travetto/test", "doc/assert-example.ts"];
|
|
87
86
|
const node_assert_1 = tslib_1.__importDefault(require("node:assert"));
|
|
88
87
|
const test_1 = require("@travetto/test");
|
|
89
88
|
let SimpleTest = class SimpleTest {
|
|
90
|
-
static Ⲑinit = Ⲑ
|
|
89
|
+
static Ⲑinit = Ⲑ_function_1.registerFunction(SimpleTest, ᚕm, { hash: 1887908328, lines: [5, 12] }, { test: { hash: 102834457, lines: [8, 11, 10] } }, false, false);
|
|
91
90
|
async test() {
|
|
92
|
-
if (Ⲑ
|
|
91
|
+
if (Ⲑ_debug_1.tryDebugger)
|
|
93
92
|
debugger;
|
|
94
|
-
Ⲑ_check_1.AssertCheck.check({
|
|
93
|
+
Ⲑ_check_1.AssertCheck.check({ module: ᚕm, line: 10, text: "{ size: 20, address: { state: 'VA' } }", operator: "deepStrictEqual" }, true, { size: 20, address: { state: 'VA' } }, {});
|
|
95
94
|
}
|
|
96
95
|
};
|
|
97
96
|
tslib_1.__decorate([
|
|
98
|
-
(0, test_1.Test)(
|
|
97
|
+
(0, test_1.Test)()
|
|
99
98
|
], SimpleTest.prototype, "test", null);
|
|
100
99
|
SimpleTest = tslib_1.__decorate([
|
|
101
|
-
|
|
102
|
-
(0, test_1.Suite)({ ident: "@Suite()" })
|
|
100
|
+
(0, test_1.Suite)()
|
|
103
101
|
], SimpleTest);
|
|
104
102
|
```
|
|
105
103
|
|
|
@@ -223,12 +221,13 @@ To run the tests you can either call the [Command Line Interface](https://github
|
|
|
223
221
|
```bash
|
|
224
222
|
$ trv test --help
|
|
225
223
|
|
|
226
|
-
Usage: test [options] [first:string] [
|
|
224
|
+
Usage: test [options] [first:string] [globs...:string]
|
|
227
225
|
|
|
228
226
|
Options:
|
|
229
227
|
-f, --format <string> Output format for test results (default: "tap")
|
|
230
228
|
-c, --concurrency <number> Number of tests to run concurrently (default: 4)
|
|
231
229
|
-m, --mode <single|standard> Test run mode (default: "standard")
|
|
230
|
+
-t, --tags <string> Tags to target or exclude
|
|
232
231
|
-h, --help display help for command
|
|
233
232
|
```
|
|
234
233
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@travetto/test",
|
|
3
|
-
"version": "5.0.0-rc.
|
|
3
|
+
"version": "5.0.0-rc.10",
|
|
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/
|
|
31
|
-
"@travetto/
|
|
32
|
-
"@travetto/terminal": "^5.0.0-rc.
|
|
33
|
-
"@travetto/worker": "^5.0.0-rc.
|
|
34
|
-
"yaml": "^2.
|
|
30
|
+
"@travetto/registry": "^5.0.0-rc.10",
|
|
31
|
+
"@travetto/runtime": "^5.0.0-rc.10",
|
|
32
|
+
"@travetto/terminal": "^5.0.0-rc.10",
|
|
33
|
+
"@travetto/worker": "^5.0.0-rc.10",
|
|
34
|
+
"yaml": "^2.5.0"
|
|
35
35
|
},
|
|
36
36
|
"peerDependencies": {
|
|
37
|
-
"@travetto/cli": "^5.0.0-rc.
|
|
38
|
-
"@travetto/transformer": "^5.0.0-rc.
|
|
37
|
+
"@travetto/cli": "^5.0.0-rc.11",
|
|
38
|
+
"@travetto/transformer": "^5.0.0-rc.7"
|
|
39
39
|
},
|
|
40
40
|
"peerDependenciesMeta": {
|
|
41
41
|
"@travetto/transformer": {
|
package/src/assert/capture.ts
CHANGED
|
@@ -3,7 +3,7 @@ import { EventEmitter } from 'node:events';
|
|
|
3
3
|
import { Assertion, TestConfig } from '../model/test';
|
|
4
4
|
|
|
5
5
|
export interface CaptureAssert extends Partial<Assertion> {
|
|
6
|
-
|
|
6
|
+
module?: [string, string];
|
|
7
7
|
line: number;
|
|
8
8
|
text: string;
|
|
9
9
|
operator: string;
|
|
@@ -26,14 +26,15 @@ class $AssertCapture {
|
|
|
26
26
|
|
|
27
27
|
// Emit and collect, every assertion as it occurs
|
|
28
28
|
const handler = (a: CaptureAssert): void => {
|
|
29
|
-
const
|
|
29
|
+
const asrt: Assertion = {
|
|
30
30
|
...a,
|
|
31
|
+
import: a.import ?? a.module!.join('/'),
|
|
31
32
|
classId: test.classId,
|
|
32
33
|
methodName: test.methodName
|
|
33
34
|
};
|
|
34
|
-
assertions.push(
|
|
35
|
+
assertions.push(asrt);
|
|
35
36
|
if (listener) {
|
|
36
|
-
listener(
|
|
37
|
+
listener(asrt);
|
|
37
38
|
}
|
|
38
39
|
};
|
|
39
40
|
|
package/src/assert/check.ts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import assert from 'node:assert';
|
|
2
2
|
|
|
3
|
-
import {
|
|
4
|
-
import { AppError, ClassInstance, Class } from '@travetto/base';
|
|
3
|
+
import { AppError, Class, castTo, castKey, asConstructable } from '@travetto/runtime';
|
|
5
4
|
|
|
6
5
|
import { ThrowableError, TestConfig, Assertion } from '../model/test';
|
|
7
6
|
import { AssertCapture, CaptureAssert } from './capture';
|
|
@@ -24,9 +23,6 @@ export class AssertCheck {
|
|
|
24
23
|
* @param args The arguments passed in
|
|
25
24
|
*/
|
|
26
25
|
static check(assertion: CaptureAssert, positive: boolean, ...args: unknown[]): void {
|
|
27
|
-
/* eslint-disable @typescript-eslint/consistent-type-assertions */
|
|
28
|
-
assertion.file = RuntimeIndex.getSourceFile(assertion.file);
|
|
29
|
-
|
|
30
26
|
let fn = assertion.operator;
|
|
31
27
|
assertion.operator = ASSERT_FN_OPERATOR[fn];
|
|
32
28
|
|
|
@@ -41,32 +37,32 @@ export class AssertCheck {
|
|
|
41
37
|
// Check fn to call
|
|
42
38
|
if (fn === 'fail') {
|
|
43
39
|
if (args.length > 1) {
|
|
44
|
-
[assertion.actual, assertion.expected, assertion.message, assertion.operator] = args
|
|
40
|
+
[assertion.actual, assertion.expected, assertion.message, assertion.operator] = castTo(args);
|
|
45
41
|
} else {
|
|
46
|
-
[assertion.message] = args
|
|
42
|
+
[assertion.message] = castTo(args);
|
|
47
43
|
}
|
|
48
44
|
} else if (/throw|reject/i.test(fn)) {
|
|
49
45
|
assertion.operator = fn;
|
|
50
46
|
if (typeof args[1] !== 'string') {
|
|
51
|
-
[, assertion.expected, assertion.message] = args
|
|
47
|
+
[, assertion.expected, assertion.message] = castTo(args);
|
|
52
48
|
} else {
|
|
53
|
-
[, assertion.message] = args
|
|
49
|
+
[, assertion.message] = castTo(args);
|
|
54
50
|
}
|
|
55
51
|
} else if (fn === 'ok' || fn === 'assert') {
|
|
56
52
|
fn = assertion.operator = 'ok';
|
|
57
|
-
[assertion.actual, assertion.message] = args
|
|
53
|
+
[assertion.actual, assertion.message] = castTo(args);
|
|
58
54
|
assertion.expected = { toClean: (): string => positive ? 'truthy' : 'falsy' };
|
|
59
55
|
common.state = 'should be';
|
|
60
56
|
} else if (fn === 'includes') {
|
|
61
57
|
assertion.operator = fn;
|
|
62
|
-
[assertion.actual, assertion.expected, assertion.message] = args
|
|
58
|
+
[assertion.actual, assertion.expected, assertion.message] = castTo(args);
|
|
63
59
|
} else if (fn === 'instanceof') {
|
|
64
60
|
assertion.operator = fn;
|
|
65
|
-
[assertion.actual, assertion.expected, assertion.message] = args
|
|
66
|
-
assertion.actual = (assertion.actual
|
|
61
|
+
[assertion.actual, assertion.expected, assertion.message] = castTo(args);
|
|
62
|
+
assertion.actual = asConstructable(assertion.actual)?.constructor;
|
|
67
63
|
} else { // Handle unknown
|
|
68
64
|
assertion.operator = fn ?? '';
|
|
69
|
-
[assertion.actual, assertion.expected, assertion.message] = args
|
|
65
|
+
[assertion.actual, assertion.expected, assertion.message] = castTo(args);
|
|
70
66
|
}
|
|
71
67
|
|
|
72
68
|
try {
|
|
@@ -79,25 +75,25 @@ export class AssertCheck {
|
|
|
79
75
|
assertion.expected = AssertUtil.cleanValue(assertion.expected);
|
|
80
76
|
}
|
|
81
77
|
|
|
82
|
-
const [actual, expected, message]
|
|
78
|
+
const [actual, expected, message]: [unknown, unknown, string] = castTo(args);
|
|
83
79
|
|
|
84
80
|
// Actually run the assertion
|
|
85
81
|
switch (fn) {
|
|
86
|
-
case 'includes': assertFn(
|
|
87
|
-
case 'test': assertFn((expected
|
|
88
|
-
case 'instanceof': assertFn(actual instanceof (expected
|
|
89
|
-
case 'in': assertFn((actual
|
|
90
|
-
case 'lessThan': assertFn((actual
|
|
91
|
-
case 'lessThanEqual': assertFn((actual
|
|
92
|
-
case 'greaterThan': assertFn((actual
|
|
93
|
-
case 'greaterThanEqual': assertFn((actual
|
|
94
|
-
case 'ok': assertFn(...args
|
|
82
|
+
case 'includes': assertFn(castTo<unknown[]>(actual).includes(expected), message); break;
|
|
83
|
+
case 'test': assertFn(castTo<RegExp>(expected).test(castTo(actual)), message); break;
|
|
84
|
+
case 'instanceof': assertFn(actual instanceof castTo<Class>(expected), message); break;
|
|
85
|
+
case 'in': assertFn(castTo<string>(actual) in castTo<object>(expected), message); break;
|
|
86
|
+
case 'lessThan': assertFn(castTo<number>(actual) < castTo<number>(expected), message); break;
|
|
87
|
+
case 'lessThanEqual': assertFn(castTo<number>(actual) <= castTo<number>(expected), message); break;
|
|
88
|
+
case 'greaterThan': assertFn(castTo<number>(actual) > castTo<number>(expected), message); break;
|
|
89
|
+
case 'greaterThanEqual': assertFn(castTo<number>(actual) >= castTo<number>(expected), message); break;
|
|
90
|
+
case 'ok': assertFn(...castTo<Parameters<typeof assertFn>>(args)); break;
|
|
95
91
|
default:
|
|
96
|
-
if (fn && assert[
|
|
92
|
+
if (fn && assert[castKey<typeof assert>(fn)]) { // Assert call
|
|
97
93
|
if (/not/i.test(fn)) {
|
|
98
94
|
common.state = 'should not';
|
|
99
95
|
}
|
|
100
|
-
assert[
|
|
96
|
+
assert[castTo<'ok'>(fn)].apply(null, castTo(args));
|
|
101
97
|
}
|
|
102
98
|
}
|
|
103
99
|
|
|
@@ -118,7 +114,6 @@ export class AssertCheck {
|
|
|
118
114
|
}
|
|
119
115
|
throw err;
|
|
120
116
|
}
|
|
121
|
-
/* eslint-enable @typescript-eslint/consistent-type-assertions */
|
|
122
117
|
}
|
|
123
118
|
|
|
124
119
|
/**
|
|
@@ -215,8 +210,6 @@ export class AssertCheck {
|
|
|
215
210
|
): void {
|
|
216
211
|
let missed: Error | undefined;
|
|
217
212
|
|
|
218
|
-
assertion.file = RuntimeIndex.getSourceFile(assertion.file);
|
|
219
|
-
|
|
220
213
|
try {
|
|
221
214
|
action();
|
|
222
215
|
if (!positive) {
|
|
@@ -249,8 +242,6 @@ export class AssertCheck {
|
|
|
249
242
|
): Promise<void> {
|
|
250
243
|
let missed: Error | undefined;
|
|
251
244
|
|
|
252
|
-
assertion.file = RuntimeIndex.getSourceFile(assertion.file);
|
|
253
|
-
|
|
254
245
|
try {
|
|
255
246
|
if ('then' in action) {
|
|
256
247
|
await action;
|
|
@@ -274,13 +265,13 @@ export class AssertCheck {
|
|
|
274
265
|
* Look for any unhandled exceptions
|
|
275
266
|
*/
|
|
276
267
|
static checkUnhandled(test: TestConfig, err: Error | assert.AssertionError): void {
|
|
277
|
-
let line = AssertUtil.getPositionOfError(err, test.
|
|
268
|
+
let line = AssertUtil.getPositionOfError(err, test.sourceImport ?? test.import).line;
|
|
278
269
|
if (line === 1) {
|
|
279
270
|
line = test.lineStart;
|
|
280
271
|
}
|
|
281
272
|
|
|
282
273
|
AssertCapture.add({
|
|
283
|
-
|
|
274
|
+
import: test.import,
|
|
284
275
|
line,
|
|
285
276
|
operator: 'throws',
|
|
286
277
|
error: err,
|
package/src/assert/util.ts
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import util from 'node:util';
|
|
2
|
+
import path from 'node:path';
|
|
2
3
|
|
|
3
|
-
import {
|
|
4
|
+
import { asFull, Class, Runtime, RuntimeIndex } from '@travetto/runtime';
|
|
4
5
|
|
|
5
6
|
import { TestConfig, Assertion, TestResult } from '../model/test';
|
|
6
|
-
import { SuiteConfig } from '../model/suite';
|
|
7
|
+
import { SuiteConfig, SuiteFailure, SuiteResult } from '../model/suite';
|
|
7
8
|
|
|
8
9
|
function isCleanable(o: unknown): o is { toClean(): unknown } {
|
|
9
|
-
|
|
10
|
-
return !!o && !!(o as { toClean: unknown }).toClean;
|
|
10
|
+
return !!o && typeof o === 'object' && 'toClean' in o && typeof o.toClean === 'function';
|
|
11
11
|
}
|
|
12
12
|
|
|
13
13
|
/**
|
|
@@ -19,6 +19,7 @@ export class AssertUtil {
|
|
|
19
19
|
*/
|
|
20
20
|
static cleanValue(val: unknown): unknown {
|
|
21
21
|
switch (typeof val) {
|
|
22
|
+
case 'number': case 'boolean': case 'bigint': case 'string': case 'undefined': return val;
|
|
22
23
|
case 'object': {
|
|
23
24
|
if (isCleanable(val)) {
|
|
24
25
|
return val.toClean();
|
|
@@ -27,7 +28,6 @@ export class AssertUtil {
|
|
|
27
28
|
}
|
|
28
29
|
break;
|
|
29
30
|
}
|
|
30
|
-
case 'undefined': case 'string': case 'number': case 'bigint': case 'boolean': return JSON.stringify(val);
|
|
31
31
|
case 'function': {
|
|
32
32
|
if (val.Ⲑid || !val.constructor) {
|
|
33
33
|
return val.name;
|
|
@@ -35,20 +35,22 @@ export class AssertUtil {
|
|
|
35
35
|
break;
|
|
36
36
|
}
|
|
37
37
|
}
|
|
38
|
-
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
39
38
|
return util.inspect(val, false, 1).replace(/\n/g, ' ');
|
|
40
39
|
}
|
|
41
40
|
|
|
42
41
|
/**
|
|
43
42
|
* Determine file location for a given error and the stack trace
|
|
44
43
|
*/
|
|
45
|
-
static getPositionOfError(err: Error,
|
|
46
|
-
const cwd =
|
|
47
|
-
const lines =
|
|
44
|
+
static getPositionOfError(err: Error, imp: string): { import: string, line: number } {
|
|
45
|
+
const cwd = Runtime.mainSourcePath;
|
|
46
|
+
const lines = (err.stack ?? new Error().stack!)
|
|
47
|
+
.replace(/[\\/]/g, '/')
|
|
48
48
|
.split('\n')
|
|
49
49
|
// Exclude node_modules, target self
|
|
50
50
|
.filter(x => x.includes(cwd) && (!x.includes('node_modules') || x.includes('/support/')));
|
|
51
51
|
|
|
52
|
+
const filename = RuntimeIndex.getFromImport(imp)?.sourceFile!;
|
|
53
|
+
|
|
52
54
|
let best = lines.filter(x => x.includes(filename))[0];
|
|
53
55
|
|
|
54
56
|
if (!best) {
|
|
@@ -56,12 +58,12 @@ export class AssertUtil {
|
|
|
56
58
|
}
|
|
57
59
|
|
|
58
60
|
if (!best) {
|
|
59
|
-
return {
|
|
61
|
+
return { import: imp, line: 1 };
|
|
60
62
|
}
|
|
61
63
|
|
|
62
64
|
const pth = best.trim().split(/\s+/g).slice(1).pop()!;
|
|
63
65
|
if (!pth) {
|
|
64
|
-
return {
|
|
66
|
+
return { import: imp, line: 1 };
|
|
65
67
|
}
|
|
66
68
|
|
|
67
69
|
const [file, lineNo] = pth
|
|
@@ -78,7 +80,7 @@ export class AssertUtil {
|
|
|
78
80
|
|
|
79
81
|
const outFile = outFileParts.length > 1 ? outFileParts[1].replace(/^[\/]/, '') : filename;
|
|
80
82
|
|
|
81
|
-
const res = {
|
|
83
|
+
const res = { import: RuntimeIndex.getFromSource(outFile)?.import!, line };
|
|
82
84
|
|
|
83
85
|
return res;
|
|
84
86
|
}
|
|
@@ -86,8 +88,8 @@ export class AssertUtil {
|
|
|
86
88
|
/**
|
|
87
89
|
* Generate a suite error given a suite config, and an error
|
|
88
90
|
*/
|
|
89
|
-
static
|
|
90
|
-
const {
|
|
91
|
+
static generateSuiteFailure(suite: SuiteConfig, methodName: string, error: Error): SuiteFailure {
|
|
92
|
+
const { import: imp, ...pos } = this.getPositionOfError(error, suite.import);
|
|
91
93
|
let line = pos.line;
|
|
92
94
|
|
|
93
95
|
if (line === 1 && suite.lineStart) {
|
|
@@ -96,7 +98,7 @@ export class AssertUtil {
|
|
|
96
98
|
|
|
97
99
|
const msg = error.message.split(/\n/)[0];
|
|
98
100
|
|
|
99
|
-
const core = {
|
|
101
|
+
const core = { import: imp, classId: suite.classId, methodName };
|
|
100
102
|
const coreAll = { ...core, description: msg, lineStart: line, lineEnd: line, lineBodyStart: line };
|
|
101
103
|
|
|
102
104
|
const assert: Assertion = {
|
|
@@ -107,11 +109,24 @@ export class AssertUtil {
|
|
|
107
109
|
...coreAll,
|
|
108
110
|
status: 'failed', error, duration: 0, durationTotal: 0, assertions: [assert], output: {}
|
|
109
111
|
};
|
|
110
|
-
const
|
|
112
|
+
const test: TestConfig = {
|
|
111
113
|
...coreAll,
|
|
112
114
|
class: suite.class, skip: false
|
|
113
115
|
};
|
|
114
116
|
|
|
115
|
-
return { assert, testResult,
|
|
117
|
+
return { assert, testResult, test, suite };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Define import failure as a SuiteFailure object
|
|
122
|
+
*/
|
|
123
|
+
static gernerateImportFailure(imp: string, err: Error): SuiteFailure {
|
|
124
|
+
const name = path.basename(imp);
|
|
125
|
+
const classId = `${RuntimeIndex.getFromImport(imp)?.id}○${name}`;
|
|
126
|
+
const suite = asFull<SuiteConfig & SuiteResult>({
|
|
127
|
+
class: asFull<Class>({ name }), classId, duration: 0, lineStart: 1, lineEnd: 1, import: imp
|
|
128
|
+
});
|
|
129
|
+
err.message = err.message.replaceAll(Runtime.mainSourcePath, '.');
|
|
130
|
+
return this.generateSuiteFailure(suite, 'require', err);
|
|
116
131
|
}
|
|
117
132
|
}
|
package/src/consumer/registry.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { classConstruct, type Class } from '@travetto/runtime';
|
|
2
2
|
import { TestConsumer } from './types';
|
|
3
3
|
|
|
4
4
|
/**
|
|
@@ -45,8 +45,7 @@ class $TestConsumerRegistry {
|
|
|
45
45
|
await this.manualInit();
|
|
46
46
|
|
|
47
47
|
return typeof consumer === 'string' ?
|
|
48
|
-
|
|
49
|
-
new ((this.get(consumer) ?? this.#primary) as ConcreteClass)() :
|
|
48
|
+
classConstruct(this.get(consumer) ?? this.#primary) :
|
|
50
49
|
consumer;
|
|
51
50
|
}
|
|
52
51
|
}
|
|
@@ -1,22 +1,22 @@
|
|
|
1
|
-
import { AppError, TypedObject } from '@travetto/
|
|
1
|
+
import { AppError, TypedObject } from '@travetto/runtime';
|
|
2
2
|
|
|
3
3
|
import { TestEvent, } from '../model/event';
|
|
4
4
|
|
|
5
5
|
|
|
6
6
|
export type SerializedError = { $?: boolean, message: string, stack?: string, name: string };
|
|
7
7
|
|
|
8
|
-
function
|
|
8
|
+
function isError(e: unknown): e is SerializedError {
|
|
9
9
|
return !!e && (typeof e === 'object') && '$' in e;
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
-
export class
|
|
12
|
+
export class SerializeUtil {
|
|
13
13
|
|
|
14
14
|
/**
|
|
15
15
|
* Prepare error for transmission
|
|
16
16
|
*/
|
|
17
|
-
static serializeError(e: Error | SerializedError):
|
|
17
|
+
static serializeError(e: Error | SerializedError): Error;
|
|
18
18
|
static serializeError(e: undefined): undefined;
|
|
19
|
-
static serializeError(e: Error | SerializedError | undefined):
|
|
19
|
+
static serializeError(e: Error | SerializedError | undefined): Error | undefined {
|
|
20
20
|
let error: SerializedError | undefined;
|
|
21
21
|
|
|
22
22
|
if (e) {
|
|
@@ -41,7 +41,7 @@ export class ErrorUtil {
|
|
|
41
41
|
static deserializeError(e: Error | SerializedError): Error;
|
|
42
42
|
static deserializeError(e: undefined): undefined;
|
|
43
43
|
static deserializeError(e: Error | SerializedError | undefined): Error | undefined {
|
|
44
|
-
if (
|
|
44
|
+
if (isError(e)) {
|
|
45
45
|
const err = new Error();
|
|
46
46
|
|
|
47
47
|
for (const k of TypedObject.keys(e)) {
|
|
@@ -54,27 +54,18 @@ export class ErrorUtil {
|
|
|
54
54
|
err.stack = e.stack;
|
|
55
55
|
err.name = e.name;
|
|
56
56
|
return err;
|
|
57
|
-
} else
|
|
57
|
+
} else {
|
|
58
58
|
return e;
|
|
59
59
|
}
|
|
60
60
|
}
|
|
61
61
|
|
|
62
62
|
/**
|
|
63
|
-
* Serialize
|
|
63
|
+
* Serialize to JSON
|
|
64
64
|
*/
|
|
65
|
-
static
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
out.test.error = this.serializeError(out.test.error) as Error;
|
|
71
|
-
}
|
|
72
|
-
} else if (out.type === 'assertion') {
|
|
73
|
-
if (out.assertion.error) {
|
|
74
|
-
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
75
|
-
out.assertion.error = this.serializeError(out.assertion.error) as Error;
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
}
|
|
65
|
+
static serializeToJSON(out: TestEvent): string {
|
|
66
|
+
return JSON.stringify(out, (_, v) =>
|
|
67
|
+
v instanceof Error ? this.serializeError(v) :
|
|
68
|
+
typeof v === 'bigint' ? v.toString() : v
|
|
69
|
+
);
|
|
79
70
|
}
|
|
80
71
|
}
|
|
@@ -1,25 +1,25 @@
|
|
|
1
1
|
import { existsSync } from 'node:fs';
|
|
2
2
|
|
|
3
|
-
import { Class } from '@travetto/
|
|
3
|
+
import { Class, RuntimeIndex } from '@travetto/runtime';
|
|
4
4
|
|
|
5
5
|
import { TestConsumer } from '../types';
|
|
6
6
|
import { TestEvent } from '../../model/event';
|
|
7
7
|
import { TestResult } from '../../model/test';
|
|
8
8
|
import { SuiteResult } from '../../model/suite';
|
|
9
9
|
import { SuiteRegistry } from '../../registry/suite';
|
|
10
|
+
import { DelegatingConsumer } from './delegating';
|
|
10
11
|
|
|
11
12
|
/**
|
|
12
13
|
* Cumulative Summary consumer
|
|
13
14
|
*/
|
|
14
|
-
export class CumulativeSummaryConsumer
|
|
15
|
+
export class CumulativeSummaryConsumer extends DelegatingConsumer {
|
|
15
16
|
/**
|
|
16
17
|
* Total state of all tests run so far
|
|
17
18
|
*/
|
|
18
19
|
#state: Record<string, Record<string, TestResult['status']>> = {};
|
|
19
|
-
#target: TestConsumer;
|
|
20
20
|
|
|
21
21
|
constructor(target: TestConsumer) {
|
|
22
|
-
|
|
22
|
+
super([target]);
|
|
23
23
|
}
|
|
24
24
|
|
|
25
25
|
/**
|
|
@@ -28,17 +28,10 @@ export class CumulativeSummaryConsumer implements TestConsumer {
|
|
|
28
28
|
*/
|
|
29
29
|
summarizeSuite(test: TestResult): SuiteResult {
|
|
30
30
|
// Was only loading to verify existence (TODO: double-check)
|
|
31
|
-
if (existsSync(test.
|
|
32
|
-
this.#state[test.classId]
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
x.Ⲑid === test.classId
|
|
36
|
-
)!;
|
|
37
|
-
if (SuiteCls) {
|
|
38
|
-
return this.computeTotal(SuiteCls);
|
|
39
|
-
} else {
|
|
40
|
-
return this.removeClass(test.classId);
|
|
41
|
-
}
|
|
31
|
+
if (existsSync(RuntimeIndex.getFromImport(test.import)!.sourceFile)) {
|
|
32
|
+
(this.#state[test.classId] ??= {})[test.methodName] = test.status;
|
|
33
|
+
const SuiteCls = SuiteRegistry.getClasses().find(x => x.Ⲑid === test.classId);
|
|
34
|
+
return SuiteCls ? this.computeTotal(SuiteCls) : this.removeClass(test.classId);
|
|
42
35
|
} else {
|
|
43
36
|
return this.removeClass(test.classId);
|
|
44
37
|
}
|
|
@@ -50,7 +43,7 @@ export class CumulativeSummaryConsumer implements TestConsumer {
|
|
|
50
43
|
removeClass(clsId: string): SuiteResult {
|
|
51
44
|
this.#state[clsId] = {};
|
|
52
45
|
return {
|
|
53
|
-
classId: clsId, passed: 0, failed: 0, skipped: 0, total: 0, tests: [], duration: 0,
|
|
46
|
+
classId: clsId, passed: 0, failed: 0, skipped: 0, total: 0, tests: [], duration: 0, import: '', lineStart: 0, lineEnd: 0
|
|
54
47
|
};
|
|
55
48
|
}
|
|
56
49
|
|
|
@@ -70,8 +63,9 @@ export class CumulativeSummaryConsumer implements TestConsumer {
|
|
|
70
63
|
passed: total.passed,
|
|
71
64
|
failed: total.failed,
|
|
72
65
|
skipped: total.skipped,
|
|
73
|
-
|
|
74
|
-
|
|
66
|
+
import: suite.import,
|
|
67
|
+
lineStart: suite.lineStart,
|
|
68
|
+
lineEnd: suite.lineEnd,
|
|
75
69
|
total: total.failed + total.passed,
|
|
76
70
|
tests: [],
|
|
77
71
|
duration: 0
|
|
@@ -82,14 +76,13 @@ export class CumulativeSummaryConsumer implements TestConsumer {
|
|
|
82
76
|
* Listen for event, process the full event, and if the event is an after test,
|
|
83
77
|
* send a full suite summary
|
|
84
78
|
*/
|
|
85
|
-
|
|
86
|
-
this.#target.onEvent(e);
|
|
79
|
+
onEventDone(e: TestEvent): void {
|
|
87
80
|
try {
|
|
88
81
|
if (e.type === 'test' && e.phase === 'after') {
|
|
89
|
-
this
|
|
82
|
+
this.onEvent({
|
|
90
83
|
type: 'suite',
|
|
91
84
|
phase: 'after',
|
|
92
|
-
suite: this.summarizeSuite(e.test)
|
|
85
|
+
suite: this.summarizeSuite(e.test),
|
|
93
86
|
});
|
|
94
87
|
}
|
|
95
88
|
} catch (err) {
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { SuitesSummary, TestConsumer, TestRunState } from '../types';
|
|
2
|
+
import { TestEvent } from '../../model/event';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Delegating event consumer
|
|
6
|
+
*/
|
|
7
|
+
export abstract class DelegatingConsumer implements TestConsumer {
|
|
8
|
+
#consumers: TestConsumer[];
|
|
9
|
+
#transformer?: (ev: TestEvent) => typeof ev;
|
|
10
|
+
#filter?: (ev: TestEvent) => boolean;
|
|
11
|
+
|
|
12
|
+
constructor(consumers: TestConsumer[]) {
|
|
13
|
+
this.#consumers = consumers;
|
|
14
|
+
for (const c of consumers) {
|
|
15
|
+
c.onEvent = c.onEvent.bind(c);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
withTransformer(transformer: (ev: TestEvent) => typeof ev): this {
|
|
20
|
+
this.#transformer = transformer;
|
|
21
|
+
return this;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
withFilter(filter: (ev: TestEvent) => boolean): this {
|
|
25
|
+
this.#filter = filter;
|
|
26
|
+
return this;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async onStart(state: TestRunState): Promise<void> {
|
|
30
|
+
for (const c of this.#consumers) {
|
|
31
|
+
await c.onStart?.(state);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
onEvent(e: TestEvent): void {
|
|
36
|
+
if (this.#transformer) {
|
|
37
|
+
e = this.#transformer(e);
|
|
38
|
+
}
|
|
39
|
+
if (this.#filter?.(e) === false) {
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
for (const c of this.#consumers) {
|
|
43
|
+
c.onEvent(e);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
this.onEventDone?.(e);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async summarize(summary?: SuitesSummary): Promise<void> {
|
|
50
|
+
if (summary) {
|
|
51
|
+
for (const c of this.#consumers) {
|
|
52
|
+
await c.onSummary?.(summary);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
onEventDone?(e: TestEvent): void;
|
|
58
|
+
}
|
|
@@ -2,7 +2,7 @@ import { Writable } from 'node:stream';
|
|
|
2
2
|
|
|
3
3
|
import { TestEvent } from '../../model/event';
|
|
4
4
|
import { TestConsumer } from '../types';
|
|
5
|
-
import {
|
|
5
|
+
import { SerializeUtil } from '../serialize';
|
|
6
6
|
import { Consumable } from '../registry';
|
|
7
7
|
|
|
8
8
|
/**
|
|
@@ -17,8 +17,6 @@ export class EventStreamer implements TestConsumer {
|
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
onEvent(event: TestEvent): void {
|
|
20
|
-
|
|
21
|
-
ErrorUtil.serializeTestErrors(out);
|
|
22
|
-
this.#stream.write(`${JSON.stringify(out)}\n`);
|
|
20
|
+
this.#stream.write(`${SerializeUtil.serializeToJSON(event)}\n`);
|
|
23
21
|
}
|
|
24
22
|
}
|