@travetto/test 5.0.0-rc.8 → 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/README.md +2 -1
- package/package.json +8 -8
- package/src/assert/capture.ts +3 -3
- package/src/assert/check.ts +23 -25
- package/src/assert/util.ts +21 -9
- package/src/consumer/registry.ts +2 -3
- package/src/consumer/{error.ts → serialize.ts} +12 -21
- package/src/consumer/types/cumulative.ts +9 -17
- 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 +2 -2
- package/src/decorator/suite.ts +4 -5
- package/src/execute/executor.ts +82 -95
- package/src/execute/phase.ts +19 -29
- package/src/execute/promise.ts +3 -3
- package/src/execute/runner.ts +29 -20
- package/src/execute/types.ts +12 -10
- package/src/execute/util.ts +30 -5
- package/src/execute/watcher.ts +32 -36
- package/src/model/common.ts +9 -1
- package/src/model/event.ts +9 -5
- package/src/model/suite.ts +11 -1
- package/src/model/test.ts +27 -1
- package/src/registry/suite.ts +28 -24
- package/src/trv.d.ts +4 -0
- package/src/worker/child.ts +9 -15
- package/src/worker/standard.ts +17 -22
- package/src/worker/types.ts +13 -23
- package/support/cli.test.ts +18 -4
- package/support/{cli.test_count.ts → cli.test_digest.ts} +14 -6
- package/support/cli.test_direct.ts +10 -3
- package/support/transformer.assert.ts +10 -10
package/README.md
CHANGED
|
@@ -221,12 +221,13 @@ To run the tests you can either call the [Command Line Interface](https://github
|
|
|
221
221
|
```bash
|
|
222
222
|
$ trv test --help
|
|
223
223
|
|
|
224
|
-
Usage: test [options] [first:string] [
|
|
224
|
+
Usage: test [options] [first:string] [globs...:string]
|
|
225
225
|
|
|
226
226
|
Options:
|
|
227
227
|
-f, --format <string> Output format for test results (default: "tap")
|
|
228
228
|
-c, --concurrency <number> Number of tests to run concurrently (default: 4)
|
|
229
229
|
-m, --mode <single|standard> Test run mode (default: "standard")
|
|
230
|
+
-t, --tags <string> Tags to target or exclude
|
|
230
231
|
-h, --help display help for command
|
|
231
232
|
```
|
|
232
233
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@travetto/test",
|
|
3
|
-
"version": "5.0.0
|
|
3
|
+
"version": "5.0.0",
|
|
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
|
|
33
|
-
"@travetto/worker": "^5.0.0
|
|
34
|
-
"yaml": "^2.
|
|
30
|
+
"@travetto/registry": "^5.0.0",
|
|
31
|
+
"@travetto/runtime": "^5.0.0",
|
|
32
|
+
"@travetto/terminal": "^5.0.0",
|
|
33
|
+
"@travetto/worker": "^5.0.0",
|
|
34
|
+
"yaml": "^2.5.0"
|
|
35
35
|
},
|
|
36
36
|
"peerDependencies": {
|
|
37
|
-
"@travetto/cli": "^5.0.0
|
|
38
|
-
"@travetto/transformer": "^5.0.0
|
|
37
|
+
"@travetto/cli": "^5.0.0",
|
|
38
|
+
"@travetto/transformer": "^5.0.0"
|
|
39
39
|
},
|
|
40
40
|
"peerDependenciesMeta": {
|
|
41
41
|
"@travetto/transformer": {
|
package/src/assert/capture.ts
CHANGED
|
@@ -26,15 +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
31
|
import: a.import ?? a.module!.join('/'),
|
|
32
32
|
classId: test.classId,
|
|
33
33
|
methodName: test.methodName
|
|
34
34
|
};
|
|
35
|
-
assertions.push(
|
|
35
|
+
assertions.push(asrt);
|
|
36
36
|
if (listener) {
|
|
37
|
-
listener(
|
|
37
|
+
listener(asrt);
|
|
38
38
|
}
|
|
39
39
|
};
|
|
40
40
|
|
package/src/assert/check.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import assert from 'node:assert';
|
|
2
2
|
|
|
3
|
-
import { AppError,
|
|
3
|
+
import { AppError, Class, castTo, castKey, asConstructable } from '@travetto/runtime';
|
|
4
4
|
|
|
5
5
|
import { ThrowableError, TestConfig, Assertion } from '../model/test';
|
|
6
6
|
import { AssertCapture, CaptureAssert } from './capture';
|
|
@@ -34,36 +34,35 @@ export class AssertCheck {
|
|
|
34
34
|
// Invert check for negative
|
|
35
35
|
const assertFn = positive ? assert : (x: unknown, msg?: string): unknown => assert(!x, msg);
|
|
36
36
|
|
|
37
|
-
/* eslint-disable @typescript-eslint/consistent-type-assertions */
|
|
38
37
|
// Check fn to call
|
|
39
38
|
if (fn === 'fail') {
|
|
40
39
|
if (args.length > 1) {
|
|
41
|
-
[assertion.actual, assertion.expected, assertion.message, assertion.operator] = args
|
|
40
|
+
[assertion.actual, assertion.expected, assertion.message, assertion.operator] = castTo(args);
|
|
42
41
|
} else {
|
|
43
|
-
[assertion.message] = args
|
|
42
|
+
[assertion.message] = castTo(args);
|
|
44
43
|
}
|
|
45
44
|
} else if (/throw|reject/i.test(fn)) {
|
|
46
45
|
assertion.operator = fn;
|
|
47
46
|
if (typeof args[1] !== 'string') {
|
|
48
|
-
[, assertion.expected, assertion.message] = args
|
|
47
|
+
[, assertion.expected, assertion.message] = castTo(args);
|
|
49
48
|
} else {
|
|
50
|
-
[, assertion.message] = args
|
|
49
|
+
[, assertion.message] = castTo(args);
|
|
51
50
|
}
|
|
52
51
|
} else if (fn === 'ok' || fn === 'assert') {
|
|
53
52
|
fn = assertion.operator = 'ok';
|
|
54
|
-
[assertion.actual, assertion.message] = args
|
|
53
|
+
[assertion.actual, assertion.message] = castTo(args);
|
|
55
54
|
assertion.expected = { toClean: (): string => positive ? 'truthy' : 'falsy' };
|
|
56
55
|
common.state = 'should be';
|
|
57
56
|
} else if (fn === 'includes') {
|
|
58
57
|
assertion.operator = fn;
|
|
59
|
-
[assertion.actual, assertion.expected, assertion.message] = args
|
|
58
|
+
[assertion.actual, assertion.expected, assertion.message] = castTo(args);
|
|
60
59
|
} else if (fn === 'instanceof') {
|
|
61
60
|
assertion.operator = fn;
|
|
62
|
-
[assertion.actual, assertion.expected, assertion.message] = args
|
|
63
|
-
assertion.actual = (assertion.actual
|
|
61
|
+
[assertion.actual, assertion.expected, assertion.message] = castTo(args);
|
|
62
|
+
assertion.actual = asConstructable(assertion.actual)?.constructor;
|
|
64
63
|
} else { // Handle unknown
|
|
65
64
|
assertion.operator = fn ?? '';
|
|
66
|
-
[assertion.actual, assertion.expected, assertion.message] = args
|
|
65
|
+
[assertion.actual, assertion.expected, assertion.message] = castTo(args);
|
|
67
66
|
}
|
|
68
67
|
|
|
69
68
|
try {
|
|
@@ -76,25 +75,25 @@ export class AssertCheck {
|
|
|
76
75
|
assertion.expected = AssertUtil.cleanValue(assertion.expected);
|
|
77
76
|
}
|
|
78
77
|
|
|
79
|
-
const [actual, expected, message]
|
|
78
|
+
const [actual, expected, message]: [unknown, unknown, string] = castTo(args);
|
|
80
79
|
|
|
81
80
|
// Actually run the assertion
|
|
82
81
|
switch (fn) {
|
|
83
|
-
case 'includes': assertFn(
|
|
84
|
-
case 'test': assertFn((expected
|
|
85
|
-
case 'instanceof': assertFn(actual instanceof (expected
|
|
86
|
-
case 'in': assertFn((actual
|
|
87
|
-
case 'lessThan': assertFn((actual
|
|
88
|
-
case 'lessThanEqual': assertFn((actual
|
|
89
|
-
case 'greaterThan': assertFn((actual
|
|
90
|
-
case 'greaterThanEqual': assertFn((actual
|
|
91
|
-
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;
|
|
92
91
|
default:
|
|
93
|
-
if (fn && assert[
|
|
92
|
+
if (fn && assert[castKey<typeof assert>(fn)]) { // Assert call
|
|
94
93
|
if (/not/i.test(fn)) {
|
|
95
94
|
common.state = 'should not';
|
|
96
95
|
}
|
|
97
|
-
assert[
|
|
96
|
+
assert[castTo<'ok'>(fn)].apply(null, castTo(args));
|
|
98
97
|
}
|
|
99
98
|
}
|
|
100
99
|
|
|
@@ -115,7 +114,6 @@ export class AssertCheck {
|
|
|
115
114
|
}
|
|
116
115
|
throw err;
|
|
117
116
|
}
|
|
118
|
-
/* eslint-enable @typescript-eslint/consistent-type-assertions */
|
|
119
117
|
}
|
|
120
118
|
|
|
121
119
|
/**
|
|
@@ -267,7 +265,7 @@ export class AssertCheck {
|
|
|
267
265
|
* Look for any unhandled exceptions
|
|
268
266
|
*/
|
|
269
267
|
static checkUnhandled(test: TestConfig, err: Error | assert.AssertionError): void {
|
|
270
|
-
let line = AssertUtil.getPositionOfError(err, test.import).line;
|
|
268
|
+
let line = AssertUtil.getPositionOfError(err, test.sourceImport ?? test.import).line;
|
|
271
269
|
if (line === 1) {
|
|
272
270
|
line = test.lineStart;
|
|
273
271
|
}
|
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 { Runtime, RuntimeIndex } from '@travetto/runtime';
|
|
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,7 +35,6 @@ 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
|
|
|
@@ -89,7 +88,7 @@ export class AssertUtil {
|
|
|
89
88
|
/**
|
|
90
89
|
* Generate a suite error given a suite config, and an error
|
|
91
90
|
*/
|
|
92
|
-
static
|
|
91
|
+
static generateSuiteFailure(suite: SuiteConfig, methodName: string, error: Error): SuiteFailure {
|
|
93
92
|
const { import: imp, ...pos } = this.getPositionOfError(error, suite.import);
|
|
94
93
|
let line = pos.line;
|
|
95
94
|
|
|
@@ -110,11 +109,24 @@ export class AssertUtil {
|
|
|
110
109
|
...coreAll,
|
|
111
110
|
status: 'failed', error, duration: 0, durationTotal: 0, assertions: [assert], output: {}
|
|
112
111
|
};
|
|
113
|
-
const
|
|
112
|
+
const test: TestConfig = {
|
|
114
113
|
...coreAll,
|
|
115
114
|
class: suite.class, skip: false
|
|
116
115
|
};
|
|
117
116
|
|
|
118
|
-
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);
|
|
119
131
|
}
|
|
120
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
|
}
|
|
@@ -5,18 +5,18 @@ import { TestEvent, } from '../model/event';
|
|
|
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
|
}
|
|
@@ -7,19 +7,19 @@ 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
|
/**
|
|
@@ -29,16 +29,9 @@ export class CumulativeSummaryConsumer implements TestConsumer {
|
|
|
29
29
|
summarizeSuite(test: TestResult): SuiteResult {
|
|
30
30
|
// Was only loading to verify existence (TODO: double-check)
|
|
31
31
|
if (existsSync(RuntimeIndex.getFromImport(test.import)!.sourceFile)) {
|
|
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
|
-
}
|
|
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
|
}
|
|
@@ -83,14 +76,13 @@ export class CumulativeSummaryConsumer implements TestConsumer {
|
|
|
83
76
|
* Listen for event, process the full event, and if the event is an after test,
|
|
84
77
|
* send a full suite summary
|
|
85
78
|
*/
|
|
86
|
-
|
|
87
|
-
this.#target.onEvent(e);
|
|
79
|
+
onEventDone(e: TestEvent): void {
|
|
88
80
|
try {
|
|
89
81
|
if (e.type === 'test' && e.phase === 'after') {
|
|
90
|
-
this
|
|
82
|
+
this.onEvent({
|
|
91
83
|
type: 'suite',
|
|
92
84
|
phase: 'after',
|
|
93
|
-
suite: this.summarizeSuite(e.test)
|
|
85
|
+
suite: this.summarizeSuite(e.test),
|
|
94
86
|
});
|
|
95
87
|
}
|
|
96
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
|
}
|
|
@@ -2,7 +2,7 @@ import { ChildCommChannel } from '@travetto/worker';
|
|
|
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
|
/**
|
|
@@ -12,8 +12,6 @@ import { Consumable } from '../registry';
|
|
|
12
12
|
export class ExecutionEmitter extends ChildCommChannel<TestEvent> implements TestConsumer {
|
|
13
13
|
|
|
14
14
|
onEvent(event: TestEvent): void {
|
|
15
|
-
|
|
16
|
-
ErrorUtil.serializeTestErrors(out);
|
|
17
|
-
this.send(event.type, out);
|
|
15
|
+
this.send(event.type, JSON.parse(SerializeUtil.serializeToJSON(event)));
|
|
18
16
|
}
|
|
19
17
|
}
|
|
@@ -1,62 +1,33 @@
|
|
|
1
|
-
import { TestConsumer
|
|
1
|
+
import { TestConsumer } from '../types';
|
|
2
2
|
import { TestResultsSummarizer } from './summarizer';
|
|
3
3
|
import { TestConsumerRegistry } from '../registry';
|
|
4
4
|
import { TestEvent } from '../../model/event';
|
|
5
|
+
import { DelegatingConsumer } from './delegating';
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* Test consumer with support for multiple nested consumers, and summarization
|
|
8
9
|
*/
|
|
9
|
-
export class RunnableTestConsumer
|
|
10
|
+
export class RunnableTestConsumer extends DelegatingConsumer {
|
|
10
11
|
/**
|
|
11
12
|
* Build a runnable test consumer given a format or a full consumer
|
|
12
13
|
*/
|
|
13
14
|
static async get(consumer: string | TestConsumer): Promise<RunnableTestConsumer> {
|
|
14
|
-
return new RunnableTestConsumer(await TestConsumerRegistry.getInstance(consumer));
|
|
15
|
+
return new RunnableTestConsumer([await TestConsumerRegistry.getInstance(consumer)]);
|
|
15
16
|
}
|
|
16
17
|
|
|
17
|
-
#
|
|
18
|
-
#results: TestResultsSummarizer | undefined;
|
|
18
|
+
#results?: TestResultsSummarizer;
|
|
19
19
|
|
|
20
|
-
constructor(
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
if (!this.#results && c.onSummary) { // If expecting summary
|
|
24
|
-
this.#results = new TestResultsSummarizer();
|
|
25
|
-
}
|
|
26
|
-
c.onEvent = c.onEvent.bind(c);
|
|
27
|
-
}
|
|
20
|
+
constructor(consumers: TestConsumer[]) {
|
|
21
|
+
super(consumers);
|
|
22
|
+
this.#results = consumers.find(x => !!x.onSummary) ? new TestResultsSummarizer() : undefined;
|
|
28
23
|
}
|
|
29
24
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
await c.onStart?.(state);
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
onEvent(e: TestEvent): void {
|
|
37
|
-
if (this.#results) {
|
|
38
|
-
this.#results.onEvent(e);
|
|
39
|
-
}
|
|
40
|
-
for (const c of this.#consumers) {
|
|
41
|
-
c.onEvent(e);
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
async summarize(): Promise<TestResultsSummarizer | undefined> {
|
|
46
|
-
if (this.#results) {
|
|
47
|
-
for (const c of this.#consumers) {
|
|
48
|
-
await c.onSummary?.(this.#results.summary);
|
|
49
|
-
}
|
|
50
|
-
return this.#results;
|
|
51
|
-
}
|
|
25
|
+
onEventDone(e: TestEvent): void {
|
|
26
|
+
this.#results?.onEvent(e);
|
|
52
27
|
}
|
|
53
28
|
|
|
54
29
|
async summarizeAsBoolean(): Promise<boolean> {
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
return result.summary.failed <= 0;
|
|
58
|
-
} else {
|
|
59
|
-
return true;
|
|
60
|
-
}
|
|
30
|
+
await this.summarize(this.#results?.summary);
|
|
31
|
+
return (this.#results?.summary.failed ?? 0) <= 0;
|
|
61
32
|
}
|
|
62
33
|
}
|
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import { Util } from '@travetto/runtime';
|
|
1
|
+
import { Util, AsyncQueue } from '@travetto/runtime';
|
|
2
2
|
import { StyleUtil, Terminal, TerminalUtil } from '@travetto/terminal';
|
|
3
|
-
import { WorkQueue } from '@travetto/worker';
|
|
4
3
|
|
|
5
4
|
import { TestEvent } from '../../model/event';
|
|
6
5
|
import { TestResult } from '../../model/test';
|
|
@@ -17,7 +16,7 @@ import { TapEmitter } from './tap';
|
|
|
17
16
|
export class TapStreamedEmitter implements TestConsumer {
|
|
18
17
|
|
|
19
18
|
#terminal: Terminal;
|
|
20
|
-
#results = new
|
|
19
|
+
#results = new AsyncQueue<TestResult>();
|
|
21
20
|
#progress: Promise<unknown> | undefined;
|
|
22
21
|
#consumer: TapEmitter;
|
|
23
22
|
|
|
@@ -30,16 +29,20 @@ export class TapStreamedEmitter implements TestConsumer {
|
|
|
30
29
|
this.#consumer.onStart();
|
|
31
30
|
|
|
32
31
|
let failed = 0;
|
|
33
|
-
|
|
32
|
+
let skipped = 0;
|
|
33
|
+
let completed = 0;
|
|
34
|
+
const success = StyleUtil.getStyle({ text: '#e5e5e5', background: '#026020' }); // White on dark green
|
|
34
35
|
const fail = StyleUtil.getStyle({ text: '#e5e5e5', background: '#8b0000' }); // White on dark red
|
|
35
36
|
this.#progress = this.#terminal.streamToBottom(
|
|
36
37
|
Util.mapAsyncItr(
|
|
37
38
|
this.#results,
|
|
38
39
|
(value, idx) => {
|
|
39
40
|
failed += (value.status === 'failed' ? 1 : 0);
|
|
40
|
-
|
|
41
|
+
skipped += (value.status === 'skipped' ? 1 : 0);
|
|
42
|
+
completed += (value.status !== 'skipped' ? 1 : 0);
|
|
43
|
+
return { value: `Tests %idx/%total [${failed} failed, ${skipped} skipped] -- ${value.classId}`, total: state.testCount, idx: completed };
|
|
41
44
|
},
|
|
42
|
-
TerminalUtil.progressBarUpdater(this.#terminal, { style: () => ({ complete: failed ? fail :
|
|
45
|
+
TerminalUtil.progressBarUpdater(this.#terminal, { style: () => ({ complete: failed ? fail : success }) })
|
|
43
46
|
),
|
|
44
47
|
{ minDelay: 100 }
|
|
45
48
|
);
|
|
@@ -5,7 +5,7 @@ import { stringify } from 'yaml';
|
|
|
5
5
|
import { TestEvent } from '../../model/event';
|
|
6
6
|
import { SuitesSummary, TestConsumer } from '../types';
|
|
7
7
|
import { Consumable } from '../registry';
|
|
8
|
-
import {
|
|
8
|
+
import { SerializeUtil } from '../serialize';
|
|
9
9
|
import { TestResultsEnhancer, CONSOLE_ENHANCER } from '../enhancer';
|
|
10
10
|
|
|
11
11
|
/**
|
|
@@ -102,7 +102,7 @@ export class TapEmitter implements TestConsumer {
|
|
|
102
102
|
// Handle error
|
|
103
103
|
if (test.status === 'failed') {
|
|
104
104
|
if (test.error && test.error.name !== 'AssertionError') {
|
|
105
|
-
const err =
|
|
105
|
+
const err = SerializeUtil.deserializeError(test.error);
|
|
106
106
|
this.logMeta({ error: err instanceof AppError ? err.toJSON() : err });
|
|
107
107
|
}
|
|
108
108
|
}
|
package/src/decorator/suite.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Class, ClassInstance, describeFunction } from '@travetto/runtime';
|
|
1
|
+
import { castTo, Class, ClassInstance, describeFunction } from '@travetto/runtime';
|
|
2
2
|
|
|
3
3
|
import { SuiteRegistry } from '../registry/suite';
|
|
4
4
|
import { SuiteConfig } from '../model/suite';
|
|
@@ -23,17 +23,16 @@ export function Suite(description?: string | Partial<SuiteConfig>, ...rest: Part
|
|
|
23
23
|
Object.assign(extra, r);
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
-
|
|
27
|
-
const decorator = ((target: Class) => {
|
|
26
|
+
const dec = (target: Class): typeof target => {
|
|
28
27
|
const cfg = { description: descriptionString, ...extra };
|
|
29
28
|
if (describeFunction(target).abstract) {
|
|
30
29
|
cfg.skip = true;
|
|
31
30
|
}
|
|
32
31
|
SuiteRegistry.register(target, cfg);
|
|
33
32
|
return target;
|
|
34
|
-
}
|
|
33
|
+
};
|
|
35
34
|
|
|
36
|
-
return
|
|
35
|
+
return castTo(dec);
|
|
37
36
|
}
|
|
38
37
|
|
|
39
38
|
function listener(phase: SuitePhase) {
|