@travetto/test 7.0.0-rc.2 → 7.0.0-rc.4
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 -2
- package/__index__.ts +1 -0
- package/package.json +7 -7
- package/src/assert/util.ts +1 -1
- package/src/communication.ts +66 -0
- package/src/consumer/registry-index.ts +7 -7
- package/src/consumer/types/cumulative.ts +90 -61
- package/src/consumer/types/delegating.ts +23 -20
- package/src/consumer/types/event.ts +11 -4
- package/src/consumer/types/exec.ts +12 -3
- package/src/consumer/types/runnable.ts +2 -1
- package/src/consumer/types/summarizer.ts +4 -2
- package/src/consumer/types/tap-summary.ts +5 -3
- package/src/consumer/types.ts +6 -2
- package/src/execute/executor.ts +23 -12
- package/src/execute/phase.ts +1 -1
- package/src/execute/run.ts +247 -0
- package/src/execute/types.ts +2 -17
- package/src/execute/watcher.ts +30 -56
- package/src/model/common.ts +4 -0
- package/src/model/event.ts +3 -1
- package/src/model/suite.ts +9 -20
- package/src/model/test.ts +47 -1
- package/src/model/util.ts +8 -0
- package/src/registry/registry-adapter.ts +4 -2
- package/src/registry/registry-index.ts +10 -11
- package/src/worker/child.ts +12 -12
- package/src/worker/standard.ts +27 -18
- package/src/worker/types.ts +9 -5
- package/support/bin/run.ts +6 -6
- package/support/cli.test.ts +20 -41
- package/support/cli.test_diff.ts +47 -0
- package/support/cli.test_digest.ts +2 -2
- package/support/cli.test_direct.ts +13 -12
- package/support/cli.test_watch.ts +1 -6
- package/src/execute/runner.ts +0 -87
- package/src/execute/util.ts +0 -108
package/README.md
CHANGED
|
@@ -231,8 +231,7 @@ Usage: test [options] [first:string] [globs...:string]
|
|
|
231
231
|
Options:
|
|
232
232
|
-f, --format <string> Output format for test results (default: "tap")
|
|
233
233
|
-c, --concurrency <number> Number of tests to run concurrently (default: 9)
|
|
234
|
-
-
|
|
235
|
-
-t, --tags <string> Tags to target or exclude
|
|
234
|
+
-t, --tags <string> Tags to target or exclude when using globs
|
|
236
235
|
-o, --format-options <string> Format options
|
|
237
236
|
-h, --help display help for command
|
|
238
237
|
```
|
package/__index__.ts
CHANGED
|
@@ -4,6 +4,7 @@ export * from './src/decorator/test.ts';
|
|
|
4
4
|
export * from './src/model/suite.ts';
|
|
5
5
|
export * from './src/model/test.ts';
|
|
6
6
|
export * from './src/model/event.ts';
|
|
7
|
+
export * from './src/model/util.ts';
|
|
7
8
|
export * from './src/registry/registry-index.ts';
|
|
8
9
|
export * from './src/registry/registry-adapter.ts';
|
|
9
10
|
export * from './src/fixture.ts';
|
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.4",
|
|
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.4",
|
|
31
|
+
"@travetto/runtime": "^7.0.0-rc.4",
|
|
32
|
+
"@travetto/terminal": "^7.0.0-rc.4",
|
|
33
|
+
"@travetto/worker": "^7.0.0-rc.4",
|
|
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.4",
|
|
38
|
+
"@travetto/transformer": "^7.0.0-rc.3"
|
|
39
39
|
},
|
|
40
40
|
"peerDependenciesMeta": {
|
|
41
41
|
"@travetto/transformer": {
|
package/src/assert/util.ts
CHANGED
|
@@ -96,7 +96,7 @@ export class AssertUtil {
|
|
|
96
96
|
|
|
97
97
|
const msg = error.message.split(/\n/)[0];
|
|
98
98
|
|
|
99
|
-
const core = { import: imp, classId: suite.classId, methodName };
|
|
99
|
+
const core = { import: imp, classId: suite.classId, methodName, sourceHash: suite.sourceHash };
|
|
100
100
|
const coreAll = { ...core, description: msg, lineStart: line, lineEnd: line, lineBodyStart: line };
|
|
101
101
|
|
|
102
102
|
const assert: Assertion = {
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { AppError, hasToJSON, JSONUtil } from '@travetto/runtime';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Tools for communication serialization/deserialization especially with errors
|
|
5
|
+
*/
|
|
6
|
+
export class CommunicationUtil {
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Serialize to JSON
|
|
10
|
+
*/
|
|
11
|
+
static serialize<T>(out: T): string {
|
|
12
|
+
return JSON.stringify(out, function (key, value) {
|
|
13
|
+
const objectValue = this[key];
|
|
14
|
+
if (objectValue && objectValue instanceof Error) {
|
|
15
|
+
return {
|
|
16
|
+
$: true,
|
|
17
|
+
...hasToJSON(objectValue) ? objectValue.toJSON() : objectValue,
|
|
18
|
+
name: objectValue.name,
|
|
19
|
+
message: objectValue.message,
|
|
20
|
+
stack: objectValue.stack,
|
|
21
|
+
};
|
|
22
|
+
} else if (typeof value === 'bigint') {
|
|
23
|
+
return `${value.toString()}$n`;
|
|
24
|
+
} else {
|
|
25
|
+
return value;
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Serialize to a standard object, instead of a string
|
|
32
|
+
*/
|
|
33
|
+
static serializeToObject<R = Record<string, unknown>, T = unknown>(out: T): R {
|
|
34
|
+
return JSONUtil.parseSafe(this.serialize(out));
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Deserialize from JSON
|
|
39
|
+
*/
|
|
40
|
+
static deserialize<T = unknown>(input: string): T {
|
|
41
|
+
return JSONUtil.parseSafe(input, function (key, value) {
|
|
42
|
+
if (value && typeof value === 'object' && '$' in value) {
|
|
43
|
+
const error = AppError.fromJSON(value) ?? new Error();
|
|
44
|
+
if (!(error instanceof AppError)) {
|
|
45
|
+
const { $: _, ...rest } = value;
|
|
46
|
+
Object.assign(error, rest);
|
|
47
|
+
}
|
|
48
|
+
error.message = value.message;
|
|
49
|
+
error.stack = value.stack;
|
|
50
|
+
error.name = value.name;
|
|
51
|
+
return error;
|
|
52
|
+
} else if (typeof value === 'string' && /^\d+[$]n$/.test(value)) {
|
|
53
|
+
return BigInt(value.split('$')[0]);
|
|
54
|
+
} else {
|
|
55
|
+
return value;
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Deserialize from a standard object, instead of a string
|
|
62
|
+
*/
|
|
63
|
+
static deserializeFromObject<R = unknown, T = R>(input: T): R {
|
|
64
|
+
return this.deserialize<R>(JSON.stringify(input));
|
|
65
|
+
}
|
|
66
|
+
}
|
|
@@ -2,7 +2,7 @@ import { RuntimeIndex, type Class } from '@travetto/runtime';
|
|
|
2
2
|
import { Registry, RegistryIndex, RegistryIndexStore } from '@travetto/registry';
|
|
3
3
|
|
|
4
4
|
import type { TestConsumerShape } from './types.ts';
|
|
5
|
-
import type {
|
|
5
|
+
import type { TestConsumerConfig } from '../execute/types.ts';
|
|
6
6
|
import { TestConsumerRegistryAdapter } from './registry-adapter.ts';
|
|
7
7
|
|
|
8
8
|
/**
|
|
@@ -25,15 +25,17 @@ export class TestConsumerRegistryIndex implements RegistryIndex {
|
|
|
25
25
|
|
|
26
26
|
/**
|
|
27
27
|
* Get a consumer instance that supports summarization
|
|
28
|
-
* @param
|
|
28
|
+
* @param consumerConfig The consumer configuration
|
|
29
29
|
*/
|
|
30
|
-
static getInstance(
|
|
31
|
-
return this.#instance.getInstance(
|
|
30
|
+
static getInstance(consumerConfig: TestConsumerConfig): Promise<TestConsumerShape> {
|
|
31
|
+
return this.#instance.getInstance(consumerConfig);
|
|
32
32
|
}
|
|
33
33
|
|
|
34
34
|
#initialized: Promise<void>;
|
|
35
35
|
store = new RegistryIndexStore(TestConsumerRegistryAdapter);
|
|
36
36
|
|
|
37
|
+
/** @private */ constructor(source: unknown) { Registry.validateConstructor(source); }
|
|
38
|
+
|
|
37
39
|
/**
|
|
38
40
|
* Manual initialization when running outside of the bootstrap process
|
|
39
41
|
*/
|
|
@@ -50,8 +52,6 @@ export class TestConsumerRegistryIndex implements RegistryIndex {
|
|
|
50
52
|
}
|
|
51
53
|
}
|
|
52
54
|
|
|
53
|
-
process(): void { }
|
|
54
|
-
|
|
55
55
|
/**
|
|
56
56
|
* Get types
|
|
57
57
|
*/
|
|
@@ -69,7 +69,7 @@ export class TestConsumerRegistryIndex implements RegistryIndex {
|
|
|
69
69
|
* Get a consumer instance that supports summarization
|
|
70
70
|
* @param consumer The consumer identifier or the actual consumer
|
|
71
71
|
*/
|
|
72
|
-
async getInstance(state: Pick<
|
|
72
|
+
async getInstance(state: Pick<TestConsumerConfig, 'consumer' | 'consumerOptions'>): Promise<TestConsumerShape> {
|
|
73
73
|
await (this.#initialized ??= this.#init());
|
|
74
74
|
for (const cls of this.store.getClasses()) {
|
|
75
75
|
const adapter = this.store.get(cls);
|
|
@@ -1,13 +1,18 @@
|
|
|
1
|
-
import { existsSync } from 'node:fs';
|
|
2
|
-
|
|
3
|
-
import { type Class, RuntimeIndex } from '@travetto/runtime';
|
|
4
|
-
|
|
5
1
|
import type { TestConsumerShape } from '../types.ts';
|
|
6
|
-
import type { TestEvent } from '../../model/event.ts';
|
|
7
|
-
import type { TestResult } from '../../model/test.ts';
|
|
8
|
-
import type { SuiteResult } from '../../model/suite.ts';
|
|
2
|
+
import type { TestEvent, TestRemoveEvent } from '../../model/event.ts';
|
|
3
|
+
import type { TestConfig, TestDiffSource, TestResult } from '../../model/test.ts';
|
|
4
|
+
import type { Counts, SuiteConfig, SuiteResult } from '../../model/suite.ts';
|
|
9
5
|
import { DelegatingConsumer } from './delegating.ts';
|
|
10
|
-
import {
|
|
6
|
+
import { SuiteCore } from '../../model/common.ts';
|
|
7
|
+
import { TestModelUtil } from '../../model/util.ts';
|
|
8
|
+
|
|
9
|
+
type ClassId = string;
|
|
10
|
+
type ImportName = string;
|
|
11
|
+
|
|
12
|
+
type CumulativeTestResult = Pick<TestResult, 'sourceHash' | 'status' | 'duration'>;
|
|
13
|
+
type CumulativeSuiteResult = Pick<SuiteCore, 'import' | 'classId' | 'sourceHash'> & {
|
|
14
|
+
tests: Record<string, CumulativeTestResult>;
|
|
15
|
+
};
|
|
11
16
|
|
|
12
17
|
/**
|
|
13
18
|
* Cumulative Summary consumer
|
|
@@ -16,77 +21,101 @@ export class CumulativeSummaryConsumer extends DelegatingConsumer {
|
|
|
16
21
|
/**
|
|
17
22
|
* Total state of all tests run so far
|
|
18
23
|
*/
|
|
19
|
-
#state: Record<
|
|
24
|
+
#state: Record<ImportName, Record<ClassId, CumulativeSuiteResult>> = {};
|
|
20
25
|
|
|
21
26
|
constructor(target: TestConsumerShape) {
|
|
22
27
|
super([target]);
|
|
23
28
|
}
|
|
24
29
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
* state
|
|
28
|
-
*/
|
|
29
|
-
summarizeSuite(test: TestResult): SuiteResult {
|
|
30
|
-
// Was only loading to verify existence (TODO: double-check)
|
|
31
|
-
if (existsSync(RuntimeIndex.getFromImport(test.import)!.sourceFile)) {
|
|
32
|
-
(this.#state[test.classId] ??= {})[test.methodName] = test.status;
|
|
33
|
-
const SuiteCls = SuiteRegistryIndex.getClasses().find(cls => cls.Ⲑid === test.classId);
|
|
34
|
-
return SuiteCls ? this.computeTotal(SuiteCls) : this.removeClass(test.classId);
|
|
35
|
-
} else {
|
|
36
|
-
return this.removeClass(test.classId);
|
|
37
|
-
}
|
|
30
|
+
getSuite(core: Pick<SuiteCore, 'import' | 'classId'>): CumulativeSuiteResult {
|
|
31
|
+
return this.#state[core.import]?.[core.classId];
|
|
38
32
|
}
|
|
39
33
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
*/
|
|
43
|
-
removeClass(clsId: string): SuiteResult {
|
|
44
|
-
this.#state[clsId] = {};
|
|
45
|
-
return {
|
|
46
|
-
classId: clsId, passed: 0, failed: 0, skipped: 0, total: 0, tests: [], duration: 0, import: '', lineStart: 0, lineEnd: 0
|
|
47
|
-
};
|
|
34
|
+
getOrCreateSuite({ tests: _, ...core }: SuiteConfig | SuiteResult): CumulativeSuiteResult {
|
|
35
|
+
return (this.#state[core.import] ??= {})[core.classId] ??= { ...core, tests: {} };
|
|
48
36
|
}
|
|
49
37
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
38
|
+
onTestBefore(config: TestConfig): TestConfig {
|
|
39
|
+
const suite = this.getSuite(config);
|
|
40
|
+
suite.tests[config.methodName] = { sourceHash: config.sourceHash, status: 'unknown', duration: 0 };
|
|
41
|
+
return config;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
onTestAfter(result: TestResult): TestResult {
|
|
45
|
+
const test = this.getSuite(result).tests[result.methodName];
|
|
46
|
+
Object.assign(test, { status: result.status, duration: result.duration });
|
|
47
|
+
return result;
|
|
48
|
+
}
|
|
60
49
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
50
|
+
onSuiteBefore(config: SuiteConfig): SuiteConfig {
|
|
51
|
+
const suite = this.getOrCreateSuite(config);
|
|
52
|
+
suite.sourceHash = config.sourceHash;
|
|
53
|
+
return config;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
onSuiteAfter(result: SuiteResult): SuiteResult {
|
|
57
|
+
// Reset counts
|
|
58
|
+
const suite = this.getSuite(result);
|
|
59
|
+
const totals: Counts & { duration: number } = { passed: 0, failed: 0, skipped: 0, unknown: 0, total: 0, duration: 0 };
|
|
60
|
+
for (const test of Object.values(suite.tests)) {
|
|
61
|
+
totals[test.status] += 1;
|
|
62
|
+
totals.total += 1;
|
|
63
|
+
totals.duration += test.duration ?? 0;
|
|
64
|
+
}
|
|
65
|
+
return { ...result, ...totals, status: TestModelUtil.countsToTestStatus(totals) };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
removeTest(importName: string, classId?: string, methodName?: string): void {
|
|
69
|
+
if (methodName && classId && importName) {
|
|
70
|
+
delete this.getSuite({ import: importName, classId }).tests[methodName];
|
|
71
|
+
} else if (classId && importName) {
|
|
72
|
+
delete this.#state[importName][classId];
|
|
73
|
+
} else if (importName) {
|
|
74
|
+
delete this.#state[importName];
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
transformRemove(event: TestRemoveEvent): TestRemoveEvent {
|
|
79
|
+
this.removeTest(event.import, event.classId, event.methodName);
|
|
80
|
+
return event;
|
|
73
81
|
}
|
|
74
82
|
|
|
75
83
|
/**
|
|
76
|
-
*
|
|
77
|
-
* send a full suite summary
|
|
84
|
+
* Handle cumulative events, and emit a summarized summary
|
|
78
85
|
*/
|
|
79
|
-
|
|
86
|
+
transform(event: TestEvent): TestEvent | undefined {
|
|
80
87
|
try {
|
|
81
|
-
if (event.type === '
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
suite: this.
|
|
86
|
-
}
|
|
88
|
+
if (event.type === 'suite') {
|
|
89
|
+
if (event.phase === 'before') {
|
|
90
|
+
return { ...event, suite: this.onSuiteBefore(event.suite) };
|
|
91
|
+
} else if (event.phase === 'after') {
|
|
92
|
+
return { ...event, suite: this.onSuiteAfter(event.suite) };
|
|
93
|
+
}
|
|
94
|
+
} else if (event.type === 'test') {
|
|
95
|
+
if (event.phase === 'before') {
|
|
96
|
+
return { ...event, test: this.onTestBefore(event.test) };
|
|
97
|
+
} else if (event.phase === 'after') {
|
|
98
|
+
return { ...event, test: this.onTestAfter(event.test) };
|
|
99
|
+
}
|
|
87
100
|
}
|
|
101
|
+
return event;
|
|
88
102
|
} catch (error) {
|
|
89
103
|
console.warn('Summarization Error', { error });
|
|
90
104
|
}
|
|
91
105
|
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Produce diff source for import file
|
|
109
|
+
*/
|
|
110
|
+
produceDiffSource(importName: string): TestDiffSource {
|
|
111
|
+
const output: TestDiffSource = {};
|
|
112
|
+
for (const [clsId, suite] of Object.entries(this.#state[importName] || {})) {
|
|
113
|
+
const methods: TestDiffSource[string]['methods'] = {};
|
|
114
|
+
for (const [methodName, test] of Object.entries(suite.tests)) {
|
|
115
|
+
methods[methodName] = test.sourceHash!;
|
|
116
|
+
}
|
|
117
|
+
output[clsId] = { sourceHash: suite.sourceHash!, methods };
|
|
118
|
+
}
|
|
119
|
+
return output;
|
|
120
|
+
}
|
|
92
121
|
}
|
|
@@ -1,13 +1,11 @@
|
|
|
1
1
|
import type { SuitesSummary, TestConsumerShape, TestRunState } from '../types.ts';
|
|
2
|
-
import type { TestEvent } from '../../model/event.ts';
|
|
2
|
+
import type { TestEvent, TestRemoveEvent } from '../../model/event.ts';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Delegating event consumer
|
|
6
6
|
*/
|
|
7
7
|
export abstract class DelegatingConsumer implements TestConsumerShape {
|
|
8
8
|
#consumers: TestConsumerShape[];
|
|
9
|
-
#transformer?: (event: TestEvent) => typeof event;
|
|
10
|
-
#filter?: (event: TestEvent) => boolean;
|
|
11
9
|
|
|
12
10
|
constructor(consumers: TestConsumerShape[]) {
|
|
13
11
|
this.#consumers = consumers;
|
|
@@ -16,34 +14,38 @@ export abstract class DelegatingConsumer implements TestConsumerShape {
|
|
|
16
14
|
}
|
|
17
15
|
}
|
|
18
16
|
|
|
19
|
-
withTransformer(transformer: (event: TestEvent) => typeof event): this {
|
|
20
|
-
this.#transformer = transformer;
|
|
21
|
-
return this;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
withFilter(filter: (event: TestEvent) => boolean): this {
|
|
25
|
-
this.#filter = filter;
|
|
26
|
-
return this;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
17
|
async onStart(state: TestRunState): Promise<void> {
|
|
30
18
|
for (const consumer of this.#consumers) {
|
|
31
19
|
await consumer.onStart?.(state);
|
|
32
20
|
}
|
|
33
21
|
}
|
|
34
22
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
23
|
+
onRemoveEvent(event: TestRemoveEvent): void {
|
|
24
|
+
let result = event;
|
|
25
|
+
if (this.transformRemove) {
|
|
26
|
+
result = this.transformRemove(event) ?? event;
|
|
38
27
|
}
|
|
39
|
-
if (
|
|
40
|
-
|
|
28
|
+
if (result) {
|
|
29
|
+
for (const consumer of this.#consumers) {
|
|
30
|
+
consumer.onRemoveEvent?.(result);
|
|
31
|
+
}
|
|
41
32
|
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
delegateEvent(event: TestEvent): void {
|
|
42
36
|
for (const consumer of this.#consumers) {
|
|
43
37
|
consumer.onEvent(event);
|
|
44
38
|
}
|
|
39
|
+
}
|
|
45
40
|
|
|
46
|
-
|
|
41
|
+
onEvent(event: TestEvent): void {
|
|
42
|
+
let result = event;
|
|
43
|
+
if (this.transform) {
|
|
44
|
+
result = this.transform(event) ?? event;
|
|
45
|
+
}
|
|
46
|
+
if (result) {
|
|
47
|
+
this.delegateEvent(result);
|
|
48
|
+
}
|
|
47
49
|
}
|
|
48
50
|
|
|
49
51
|
async summarize(summary?: SuitesSummary): Promise<void> {
|
|
@@ -54,5 +56,6 @@ export abstract class DelegatingConsumer implements TestConsumerShape {
|
|
|
54
56
|
}
|
|
55
57
|
}
|
|
56
58
|
|
|
57
|
-
|
|
59
|
+
transform?(event: TestEvent): TestEvent | undefined;
|
|
60
|
+
transformRemove?(event: TestRemoveEvent): TestRemoveEvent | undefined;
|
|
58
61
|
}
|
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
import { Writable } from 'node:stream';
|
|
2
2
|
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
import type { TestEvent } from '../../model/event.ts';
|
|
3
|
+
import type { TestEvent, TestRemoveEvent } from '../../model/event.ts';
|
|
6
4
|
import type { TestConsumerShape } from '../types.ts';
|
|
7
5
|
import { TestConsumer } from '../decorator.ts';
|
|
6
|
+
import { CommunicationUtil } from '../../communication.ts';
|
|
8
7
|
|
|
9
8
|
/**
|
|
10
9
|
* Streams all test events a JSON payload, in an nd-json format
|
|
@@ -17,7 +16,15 @@ export class EventStreamer implements TestConsumerShape {
|
|
|
17
16
|
this.#stream = stream;
|
|
18
17
|
}
|
|
19
18
|
|
|
19
|
+
sendPayload(payload: unknown): void {
|
|
20
|
+
this.#stream.write(`${CommunicationUtil.serialize(payload)}\n`);
|
|
21
|
+
}
|
|
22
|
+
|
|
20
23
|
onEvent(event: TestEvent): void {
|
|
21
|
-
this
|
|
24
|
+
this.sendPayload(event);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
onRemoveEvent(event: TestRemoveEvent): void {
|
|
28
|
+
this.sendPayload(event);
|
|
22
29
|
}
|
|
23
30
|
}
|
|
@@ -1,16 +1,25 @@
|
|
|
1
1
|
import { IpcChannel } from '@travetto/worker';
|
|
2
|
-
import { Util } from '@travetto/runtime';
|
|
3
2
|
|
|
4
|
-
import type { TestEvent } from '../../model/event.ts';
|
|
3
|
+
import type { TestEvent, TestRemoveEvent } from '../../model/event.ts';
|
|
5
4
|
import type { TestConsumerShape } from '../types.ts';
|
|
6
5
|
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
|
|
10
10
|
*/
|
|
11
11
|
@TestConsumer()
|
|
12
12
|
export class ExecutionEmitter extends IpcChannel<TestEvent> implements TestConsumerShape {
|
|
13
|
+
|
|
14
|
+
sendPayload(payload: unknown & { type: string }): void {
|
|
15
|
+
this.send(payload.type, CommunicationUtil.serializeToObject(payload));
|
|
16
|
+
}
|
|
17
|
+
|
|
13
18
|
onEvent(event: TestEvent): void {
|
|
14
|
-
this.
|
|
19
|
+
this.sendPayload(event);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
onRemoveEvent(event: TestRemoveEvent): void {
|
|
23
|
+
this.sendPayload(event);
|
|
15
24
|
}
|
|
16
25
|
}
|
|
@@ -15,8 +15,9 @@ export class RunnableTestConsumer extends DelegatingConsumer {
|
|
|
15
15
|
this.#results = consumers.find(consumer => !!consumer.onSummary) ? new TestResultsSummarizer() : undefined;
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
-
|
|
18
|
+
transform(event: TestEvent): TestEvent | undefined {
|
|
19
19
|
this.#results?.onEvent(event);
|
|
20
|
+
return event;
|
|
20
21
|
}
|
|
21
22
|
|
|
22
23
|
async summarizeAsBoolean(): Promise<boolean> {
|
|
@@ -11,6 +11,7 @@ export class TestResultsSummarizer implements TestConsumerShape {
|
|
|
11
11
|
passed: 0,
|
|
12
12
|
failed: 0,
|
|
13
13
|
skipped: 0,
|
|
14
|
+
unknown: 0,
|
|
14
15
|
total: 0,
|
|
15
16
|
duration: 0,
|
|
16
17
|
suites: [],
|
|
@@ -21,16 +22,17 @@ export class TestResultsSummarizer implements TestConsumerShape {
|
|
|
21
22
|
this.summary.suites.push(result);
|
|
22
23
|
this.summary.failed += result.failed;
|
|
23
24
|
this.summary.passed += result.passed;
|
|
25
|
+
this.summary.unknown += result.unknown;
|
|
24
26
|
this.summary.skipped += result.skipped;
|
|
25
27
|
this.summary.duration += result.duration;
|
|
26
|
-
this.summary.total +=
|
|
28
|
+
this.summary.total += result.total;
|
|
27
29
|
}
|
|
28
30
|
|
|
29
31
|
/**
|
|
30
32
|
* Merge all test results into a single Suite Result
|
|
31
33
|
*/
|
|
32
34
|
onEvent(event: TestEvent): void {
|
|
33
|
-
if (event.
|
|
35
|
+
if (event.type === 'suite' && event.phase === 'after') {
|
|
34
36
|
this.#merge(event.suite);
|
|
35
37
|
}
|
|
36
38
|
}
|
|
@@ -99,16 +99,18 @@ export class TapSummaryEmitter implements TestConsumerShape {
|
|
|
99
99
|
files.set(file, { key: file, duration: 0, tests: 0 });
|
|
100
100
|
}
|
|
101
101
|
|
|
102
|
+
const testCount = Object.keys(event.suite.tests).length;
|
|
103
|
+
|
|
102
104
|
suites.set(event.suite.classId, {
|
|
103
105
|
key: event.suite.classId,
|
|
104
106
|
duration: event.suite.duration,
|
|
105
|
-
tests:
|
|
107
|
+
tests: testCount
|
|
106
108
|
});
|
|
107
109
|
|
|
108
110
|
files.get(file)!.duration += event.suite.duration;
|
|
109
|
-
files.get(file)!.tests +=
|
|
111
|
+
files.get(file)!.tests += testCount;
|
|
110
112
|
modules.get(module)!.duration += event.suite.duration;
|
|
111
|
-
modules.get(module)!.tests +=
|
|
113
|
+
modules.get(module)!.tests += testCount;
|
|
112
114
|
}
|
|
113
115
|
}
|
|
114
116
|
|
package/src/consumer/types.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Class } from '@travetto/runtime';
|
|
2
2
|
|
|
3
|
-
import { TestEvent } from '../model/event.ts';
|
|
4
|
-
import { Counts, SuiteResult } from '../model/suite.ts';
|
|
3
|
+
import type { TestEvent, TestRemoveEvent } from '../model/event.ts';
|
|
4
|
+
import type { Counts, SuiteResult } from '../model/suite.ts';
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* All suite results
|
|
@@ -52,6 +52,10 @@ export interface TestConsumerShape {
|
|
|
52
52
|
* Handle individual tests events
|
|
53
53
|
*/
|
|
54
54
|
onEvent(event: TestEvent): void;
|
|
55
|
+
/**
|
|
56
|
+
* Handle when a remove event is fired
|
|
57
|
+
*/
|
|
58
|
+
onRemoveEvent?(event: TestRemoveEvent): void;
|
|
55
59
|
/**
|
|
56
60
|
* Summarize all results
|
|
57
61
|
*/
|