@travetto/test 7.0.0-rc.1 → 7.0.0-rc.3
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 +7 -8
- package/__index__.ts +1 -0
- package/package.json +7 -7
- package/src/assert/check.ts +46 -46
- package/src/assert/util.ts +31 -31
- package/src/communication.ts +66 -0
- package/src/consumer/registry-index.ts +11 -11
- package/src/consumer/types/cumulative.ts +91 -62
- package/src/consumer/types/delegating.ts +30 -27
- package/src/consumer/types/event.ts +11 -4
- package/src/consumer/types/exec.ts +12 -3
- package/src/consumer/types/runnable.ts +4 -3
- package/src/consumer/types/summarizer.ts +12 -10
- package/src/consumer/types/tap-summary.ts +22 -20
- package/src/consumer/types/tap.ts +15 -15
- package/src/consumer/types/xunit.ts +15 -15
- package/src/consumer/types.ts +6 -2
- 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 +32 -21
- package/src/execute/phase.ts +7 -7
- package/src/execute/run.ts +247 -0
- package/src/execute/types.ts +2 -17
- package/src/execute/watcher.ts +33 -60
- package/src/fixture.ts +2 -2
- package/src/model/common.ts +4 -0
- package/src/model/event.ts +3 -1
- package/src/model/suite.ts +10 -21
- package/src/model/test.ts +48 -2
- package/src/model/util.ts +8 -0
- package/src/registry/registry-adapter.ts +23 -21
- package/src/registry/registry-index.ts +25 -25
- package/src/worker/child.ts +21 -21
- package/src/worker/standard.ts +28 -19
- package/src/worker/types.ts +9 -5
- package/support/bin/run.ts +10 -10
- package/support/cli.test.ts +20 -41
- package/support/cli.test_diff.ts +47 -0
- package/support/cli.test_digest.ts +7 -7
- package/support/cli.test_direct.ts +13 -12
- package/support/cli.test_watch.ts +3 -8
- package/support/transformer.assert.ts +12 -12
- package/src/execute/runner.ts +0 -87
- package/src/execute/util.ts +0 -108
|
@@ -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
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
30
|
+
getSuite(core: Pick<SuiteCore, 'import' | 'classId'>): CumulativeSuiteResult {
|
|
31
|
+
return this.#state[core.import]?.[core.classId];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
getOrCreateSuite({ tests: _, ...core }: SuiteConfig | SuiteResult): CumulativeSuiteResult {
|
|
35
|
+
return (this.#state[core.import] ??= {})[core.classId] ??= { ...core, tests: {} };
|
|
36
|
+
}
|
|
37
|
+
|
|
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
|
+
}
|
|
49
|
+
|
|
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;
|
|
37
64
|
}
|
|
65
|
+
return { ...result, ...totals, status: TestModelUtil.countsToTestStatus(totals) };
|
|
38
66
|
}
|
|
39
67
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
}
|
|
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;
|
|
48
81
|
}
|
|
49
82
|
|
|
50
83
|
/**
|
|
51
|
-
*
|
|
84
|
+
* Handle cumulative events, and emit a summarized summary
|
|
52
85
|
*/
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
};
|
|
86
|
+
transform(event: TestEvent): TestEvent | undefined {
|
|
87
|
+
try {
|
|
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
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return event;
|
|
102
|
+
} catch (error) {
|
|
103
|
+
console.warn('Summarization Error', { error });
|
|
104
|
+
}
|
|
73
105
|
}
|
|
74
106
|
|
|
75
107
|
/**
|
|
76
|
-
*
|
|
77
|
-
* send a full suite summary
|
|
108
|
+
* Produce diff source for import file
|
|
78
109
|
*/
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
suite: this.summarizeSuite(e.test),
|
|
86
|
-
});
|
|
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!;
|
|
87
116
|
}
|
|
88
|
-
|
|
89
|
-
console.warn('Summarization Error', { error: err });
|
|
117
|
+
output[clsId] = { sourceHash: suite.sourceHash!, methods };
|
|
90
118
|
}
|
|
119
|
+
return output;
|
|
91
120
|
}
|
|
92
121
|
}
|
|
@@ -1,58 +1,61 @@
|
|
|
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?: (ev: TestEvent) => typeof ev;
|
|
10
|
-
#filter?: (ev: TestEvent) => boolean;
|
|
11
9
|
|
|
12
10
|
constructor(consumers: TestConsumerShape[]) {
|
|
13
11
|
this.#consumers = consumers;
|
|
14
|
-
for (const
|
|
15
|
-
|
|
12
|
+
for (const consumer of consumers) {
|
|
13
|
+
consumer.onEvent = consumer.onEvent.bind(consumer);
|
|
16
14
|
}
|
|
17
15
|
}
|
|
18
16
|
|
|
19
|
-
|
|
20
|
-
this.#
|
|
21
|
-
|
|
17
|
+
async onStart(state: TestRunState): Promise<void> {
|
|
18
|
+
for (const consumer of this.#consumers) {
|
|
19
|
+
await consumer.onStart?.(state);
|
|
20
|
+
}
|
|
22
21
|
}
|
|
23
22
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
23
|
+
onRemoveEvent(event: TestRemoveEvent): void {
|
|
24
|
+
let result = event;
|
|
25
|
+
if (this.transformRemove) {
|
|
26
|
+
result = this.transformRemove(event) ?? event;
|
|
27
|
+
}
|
|
28
|
+
if (result) {
|
|
29
|
+
for (const consumer of this.#consumers) {
|
|
30
|
+
consumer.onRemoveEvent?.(result);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
27
33
|
}
|
|
28
34
|
|
|
29
|
-
|
|
30
|
-
for (const
|
|
31
|
-
|
|
35
|
+
delegateEvent(event: TestEvent): void {
|
|
36
|
+
for (const consumer of this.#consumers) {
|
|
37
|
+
consumer.onEvent(event);
|
|
32
38
|
}
|
|
33
39
|
}
|
|
34
40
|
|
|
35
|
-
onEvent(
|
|
36
|
-
|
|
37
|
-
|
|
41
|
+
onEvent(event: TestEvent): void {
|
|
42
|
+
let result = event;
|
|
43
|
+
if (this.transform) {
|
|
44
|
+
result = this.transform(event) ?? event;
|
|
38
45
|
}
|
|
39
|
-
if (
|
|
40
|
-
|
|
46
|
+
if (result) {
|
|
47
|
+
this.delegateEvent(result);
|
|
41
48
|
}
|
|
42
|
-
for (const c of this.#consumers) {
|
|
43
|
-
c.onEvent(e);
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
this.onEventDone?.(e);
|
|
47
49
|
}
|
|
48
50
|
|
|
49
51
|
async summarize(summary?: SuitesSummary): Promise<void> {
|
|
50
52
|
if (summary) {
|
|
51
|
-
for (const
|
|
52
|
-
await
|
|
53
|
+
for (const consumer of this.#consumers) {
|
|
54
|
+
await consumer.onSummary?.(summary);
|
|
53
55
|
}
|
|
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
|
}
|
|
@@ -12,11 +12,12 @@ 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
|
-
|
|
19
|
-
this.#results?.onEvent(
|
|
18
|
+
transform(event: TestEvent): TestEvent | undefined {
|
|
19
|
+
this.#results?.onEvent(event);
|
|
20
|
+
return event;
|
|
20
21
|
}
|
|
21
22
|
|
|
22
23
|
async summarizeAsBoolean(): Promise<boolean> {
|
|
@@ -11,27 +11,29 @@ 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: [],
|
|
17
18
|
errors: []
|
|
18
19
|
};
|
|
19
20
|
|
|
20
|
-
#merge(
|
|
21
|
-
this.summary.suites.push(
|
|
22
|
-
this.summary.failed +=
|
|
23
|
-
this.summary.passed +=
|
|
24
|
-
this.summary.
|
|
25
|
-
this.summary.
|
|
26
|
-
this.summary.
|
|
21
|
+
#merge(result: SuiteResult): void {
|
|
22
|
+
this.summary.suites.push(result);
|
|
23
|
+
this.summary.failed += result.failed;
|
|
24
|
+
this.summary.passed += result.passed;
|
|
25
|
+
this.summary.unknown += result.unknown;
|
|
26
|
+
this.summary.skipped += result.skipped;
|
|
27
|
+
this.summary.duration += result.duration;
|
|
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
|
-
onEvent(
|
|
33
|
-
if (
|
|
34
|
-
this.#merge(
|
|
34
|
+
onEvent(event: TestEvent): void {
|
|
35
|
+
if (event.type === 'suite' && event.phase === 'after') {
|
|
36
|
+
this.#merge(event.suite);
|
|
35
37
|
}
|
|
36
38
|
}
|
|
37
39
|
}
|
|
@@ -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,18 @@ export class TapSummaryEmitter implements TestConsumerShape {
|
|
|
99
99
|
files.set(file, { key: file, duration: 0, tests: 0 });
|
|
100
100
|
}
|
|
101
101
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
102
|
+
const testCount = Object.keys(event.suite.tests).length;
|
|
103
|
+
|
|
104
|
+
suites.set(event.suite.classId, {
|
|
105
|
+
key: event.suite.classId,
|
|
106
|
+
duration: event.suite.duration,
|
|
107
|
+
tests: testCount
|
|
106
108
|
});
|
|
107
109
|
|
|
108
|
-
files.get(file)!.duration +=
|
|
109
|
-
files.get(file)!.tests +=
|
|
110
|
-
modules.get(module)!.duration +=
|
|
111
|
-
modules.get(module)!.tests +=
|
|
110
|
+
files.get(file)!.duration += event.suite.duration;
|
|
111
|
+
files.get(file)!.tests += testCount;
|
|
112
|
+
modules.get(module)!.duration += event.suite.duration;
|
|
113
|
+
modules.get(module)!.tests += testCount;
|
|
112
114
|
}
|
|
113
115
|
}
|
|
114
116
|
|
|
@@ -127,8 +129,8 @@ export class TapSummaryEmitter implements TestConsumerShape {
|
|
|
127
129
|
await this.#consumer.log(`${this.#enhancer.suiteName(`Top ${count} slowest ${title}s`)}: `);
|
|
128
130
|
const top10 = [...results.values()].toSorted((a, b) => b.duration - a.duration).slice(0, count);
|
|
129
131
|
|
|
130
|
-
for (const
|
|
131
|
-
console.log(` * ${this.#enhancer.testName(
|
|
132
|
+
for (const result of top10) {
|
|
133
|
+
console.log(` * ${this.#enhancer.testName(result.key)} - ${this.#enhancer.total(result.duration)}ms / ${this.#enhancer.total(result.tests)} tests`);
|
|
132
134
|
}
|
|
133
135
|
await this.#consumer.log('');
|
|
134
136
|
}
|
|
@@ -48,10 +48,10 @@ export class TapEmitter implements TestConsumerShape {
|
|
|
48
48
|
/**
|
|
49
49
|
* Output supplemental data (e.g. logs)
|
|
50
50
|
*/
|
|
51
|
-
logMeta(
|
|
51
|
+
logMeta(meta: Record<string, unknown>): void {
|
|
52
52
|
const lineLength = this.#terminal.width - 5;
|
|
53
|
-
let body = stringify(
|
|
54
|
-
body = body.split('\n').map(
|
|
53
|
+
let body = stringify(meta, { lineWidth: lineLength, indent: 2 });
|
|
54
|
+
body = body.split('\n').map(line => ` ${line}`).join('\n');
|
|
55
55
|
this.log(`---\n${this.#enhancer.objectInspect(body)}\n...`);
|
|
56
56
|
}
|
|
57
57
|
|
|
@@ -59,16 +59,16 @@ export class TapEmitter implements TestConsumerShape {
|
|
|
59
59
|
* Error to string
|
|
60
60
|
* @param error
|
|
61
61
|
*/
|
|
62
|
-
errorToString(
|
|
63
|
-
if (
|
|
64
|
-
if (
|
|
65
|
-
let out = JSON.stringify(hasToJSON(
|
|
66
|
-
if (this.#options?.verbose &&
|
|
67
|
-
out = `${out}\n${
|
|
62
|
+
errorToString(error?: Error): string | undefined {
|
|
63
|
+
if (error && error.name !== 'AssertionError') {
|
|
64
|
+
if (error instanceof Error) {
|
|
65
|
+
let out = JSON.stringify(hasToJSON(error) ? error.toJSON() : error, null, 2);
|
|
66
|
+
if (this.#options?.verbose && error.stack) {
|
|
67
|
+
out = `${out}\n${error.stack}`;
|
|
68
68
|
}
|
|
69
69
|
return out;
|
|
70
70
|
} else {
|
|
71
|
-
return `${
|
|
71
|
+
return `${error}`;
|
|
72
72
|
}
|
|
73
73
|
}
|
|
74
74
|
}
|
|
@@ -76,9 +76,9 @@ export class TapEmitter implements TestConsumerShape {
|
|
|
76
76
|
/**
|
|
77
77
|
* Listen for each event
|
|
78
78
|
*/
|
|
79
|
-
onEvent(
|
|
80
|
-
if (
|
|
81
|
-
const { test } =
|
|
79
|
+
onEvent(event: TestEvent): void {
|
|
80
|
+
if (event.type === 'test' && event.phase === 'after') {
|
|
81
|
+
const { test } = event;
|
|
82
82
|
const suiteId = this.#enhancer.suiteName(test.classId);
|
|
83
83
|
let header = `${suiteId} - ${this.#enhancer.testName(test.methodName)}`;
|
|
84
84
|
if (test.description) {
|
|
@@ -155,8 +155,8 @@ export class TapEmitter implements TestConsumerShape {
|
|
|
155
155
|
|
|
156
156
|
if (summary.errors.length) {
|
|
157
157
|
this.log('---\n');
|
|
158
|
-
for (const
|
|
159
|
-
const msg = this.errorToString(
|
|
158
|
+
for (const error of summary.errors) {
|
|
159
|
+
const msg = this.errorToString(error);
|
|
160
160
|
if (msg) {
|
|
161
161
|
this.log(this.#enhancer.failure(msg));
|
|
162
162
|
}
|
|
@@ -24,19 +24,19 @@ export class XunitEmitter implements TestConsumerShape {
|
|
|
24
24
|
/**
|
|
25
25
|
* Process metadata information (e.g. logs)
|
|
26
26
|
*/
|
|
27
|
-
buildMeta(
|
|
28
|
-
if (!
|
|
27
|
+
buildMeta(meta: Record<string, unknown>): string {
|
|
28
|
+
if (!meta) {
|
|
29
29
|
return '';
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
-
for (const
|
|
33
|
-
if (!
|
|
34
|
-
delete
|
|
32
|
+
for (const key of Object.keys(meta)) {
|
|
33
|
+
if (!meta[key]) {
|
|
34
|
+
delete meta[key];
|
|
35
35
|
}
|
|
36
36
|
}
|
|
37
|
-
if (Object.keys(
|
|
38
|
-
let body = stringify(
|
|
39
|
-
body = body.split('\n').map(
|
|
37
|
+
if (Object.keys(meta).length) {
|
|
38
|
+
let body = stringify(meta);
|
|
39
|
+
body = body.split('\n').map(line => ` ${line}`).join('\n');
|
|
40
40
|
return `<![CDATA[\n${body}\n]]>`;
|
|
41
41
|
} else {
|
|
42
42
|
return '';
|
|
@@ -46,10 +46,10 @@ export class XunitEmitter implements TestConsumerShape {
|
|
|
46
46
|
/**
|
|
47
47
|
* Handle each test event
|
|
48
48
|
*/
|
|
49
|
-
onEvent(
|
|
50
|
-
if (
|
|
49
|
+
onEvent(event: TestEvent): void {
|
|
50
|
+
if (event.type === 'test' && event.phase === 'after') {
|
|
51
51
|
|
|
52
|
-
const { test } =
|
|
52
|
+
const { test } = event;
|
|
53
53
|
|
|
54
54
|
let name = `${test.methodName}`;
|
|
55
55
|
if (test.description) {
|
|
@@ -59,8 +59,8 @@ export class XunitEmitter implements TestConsumerShape {
|
|
|
59
59
|
let body = '';
|
|
60
60
|
|
|
61
61
|
if (test.error) {
|
|
62
|
-
const
|
|
63
|
-
body = `<failure type="${
|
|
62
|
+
const assertion = test.assertions.find(item => !!item.error)!;
|
|
63
|
+
body = `<failure type="${assertion.text}" message="${encodeURIComponent(assertion.message!)}"><![CDATA[${assertion.error!.stack}]]></failure>`;
|
|
64
64
|
}
|
|
65
65
|
|
|
66
66
|
const groupedByLevel: Record<string, string[]> = {};
|
|
@@ -79,8 +79,8 @@ export class XunitEmitter implements TestConsumerShape {
|
|
|
79
79
|
<system-err>${this.buildMeta({ error: groupedByLevel.error, warn: groupedByLevel.warn })}</system-err>
|
|
80
80
|
</testcase>`
|
|
81
81
|
);
|
|
82
|
-
} else if (
|
|
83
|
-
const { suite } =
|
|
82
|
+
} else if (event.type === 'suite' && event.phase === 'after') {
|
|
83
|
+
const { suite } = event;
|
|
84
84
|
const testBodies = this.#tests.slice(0);
|
|
85
85
|
this.#tests = [];
|
|
86
86
|
|
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
|
*/
|
package/src/decorator/suite.ts
CHANGED
|
@@ -15,7 +15,7 @@ export function Suite(): ClassDecorator;
|
|
|
15
15
|
export function Suite(...rest: Partial<SuiteConfig>[]): ClassDecorator;
|
|
16
16
|
export function Suite(description: string, ...rest: Partial<SuiteConfig>[]): ClassDecorator;
|
|
17
17
|
export function Suite(description?: string | Partial<SuiteConfig>, ...rest: Partial<SuiteConfig>[]): ClassDecorator {
|
|
18
|
-
const
|
|
18
|
+
const decorator = (cls: Class): typeof cls => {
|
|
19
19
|
const isAbstract = describeFunction(cls).abstract;
|
|
20
20
|
SuiteRegistryIndex.getForRegister(cls).register(
|
|
21
21
|
...(typeof description !== 'string' && description ? [description] : []),
|
|
@@ -26,7 +26,7 @@ export function Suite(description?: string | Partial<SuiteConfig>, ...rest: Part
|
|
|
26
26
|
return cls;
|
|
27
27
|
};
|
|
28
28
|
|
|
29
|
-
return castTo(
|
|
29
|
+
return castTo(decorator);
|
|
30
30
|
}
|
|
31
31
|
|
|
32
32
|
/**
|