@travetto/test 8.0.0-alpha.2 → 8.0.0-alpha.21
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 +24 -22
- package/__index__.ts +9 -9
- package/package.json +8 -8
- package/src/assert/capture.ts +1 -2
- package/src/assert/check.ts +56 -33
- package/src/assert/types.ts +1 -1
- package/src/assert/util.ts +26 -81
- package/src/consumer/decorator.ts +2 -2
- package/src/consumer/enhancer.ts +3 -3
- package/src/consumer/registry-adapter.ts +3 -3
- package/src/consumer/registry-index.ts +6 -5
- package/src/consumer/types/cumulative.ts +11 -23
- package/src/consumer/types/delegating.ts +8 -2
- package/src/consumer/types/event.ts +2 -2
- package/src/consumer/types/exec.ts +3 -4
- package/src/consumer/types/json.ts +3 -4
- package/src/consumer/types/noop.ts +3 -3
- package/src/consumer/types/runnable.ts +4 -5
- package/src/consumer/types/summarizer.ts +5 -18
- package/src/consumer/types/tap-summary.ts +39 -30
- package/src/consumer/types/tap.ts +64 -56
- package/src/consumer/types/util.ts +31 -0
- package/src/consumer/types/xunit.ts +10 -8
- package/src/consumer/types.ts +6 -10
- package/src/decorator/suite.ts +3 -3
- package/src/decorator/test.ts +6 -4
- package/src/execute/barrier.ts +15 -9
- package/src/execute/console.ts +3 -4
- package/src/execute/executor.ts +101 -157
- package/src/execute/phase.ts +12 -22
- package/src/execute/run.ts +41 -40
- package/src/execute/watcher.ts +12 -19
- package/src/fixture.ts +6 -7
- package/src/model/common.ts +1 -1
- package/src/model/error.ts +2 -2
- package/src/model/event.ts +14 -9
- package/src/model/suite.ts +15 -13
- package/src/model/test.ts +11 -8
- package/src/model/util.ts +102 -9
- package/src/registry/registry-adapter.ts +10 -13
- package/src/registry/registry-index.ts +13 -11
- package/src/worker/child.ts +10 -9
- package/src/worker/standard.ts +11 -14
- package/src/worker/types.ts +3 -7
- package/support/bin/run.ts +1 -1
- package/support/cli.test.ts +20 -15
- package/support/cli.test_child.ts +8 -3
- package/support/cli.test_diff.ts +11 -7
- package/support/cli.test_digest.ts +10 -4
- package/support/cli.test_direct.ts +11 -7
- package/support/cli.test_watch.ts +6 -4
- package/support/transformer.assert.ts +40 -32
|
@@ -1,15 +1,15 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { SuiteCore } from '../../model/common.ts';
|
|
2
2
|
import type { TestEvent, TestRemoveEvent } from '../../model/event.ts';
|
|
3
|
+
import type { SuiteConfig, SuiteResult } from '../../model/suite.ts';
|
|
3
4
|
import type { TestConfig, TestDiffSource, TestResult } from '../../model/test.ts';
|
|
4
|
-
import type { Counts, SuiteConfig, SuiteResult } from '../../model/suite.ts';
|
|
5
|
-
import { DelegatingConsumer } from './delegating.ts';
|
|
6
|
-
import type { SuiteCore } from '../../model/common.ts';
|
|
7
5
|
import { TestModelUtil } from '../../model/util.ts';
|
|
6
|
+
import type { TestConsumerShape } from '../types.ts';
|
|
7
|
+
import { DelegatingConsumer } from './delegating.ts';
|
|
8
8
|
|
|
9
9
|
type ClassId = string;
|
|
10
10
|
type ImportName = string;
|
|
11
11
|
|
|
12
|
-
type CumulativeTestResult = Pick<TestResult, 'sourceHash' | 'status' | 'duration'>;
|
|
12
|
+
type CumulativeTestResult = Pick<TestResult, 'sourceHash' | 'status' | 'duration' | 'selfDuration'>;
|
|
13
13
|
type CumulativeSuiteResult = Pick<SuiteCore, 'import' | 'classId' | 'sourceHash'> & {
|
|
14
14
|
tests: Record<string, CumulativeTestResult>;
|
|
15
15
|
};
|
|
@@ -32,12 +32,12 @@ export class CumulativeSummaryConsumer extends DelegatingConsumer {
|
|
|
32
32
|
}
|
|
33
33
|
|
|
34
34
|
getOrCreateSuite({ tests: _, ...core }: SuiteConfig | SuiteResult): CumulativeSuiteResult {
|
|
35
|
-
return (this.#state[core.import] ??= {})[core.classId] ??= { ...core, tests: {} };
|
|
35
|
+
return ((this.#state[core.import] ??= {})[core.classId] ??= { ...core, tests: {} });
|
|
36
36
|
}
|
|
37
37
|
|
|
38
38
|
onTestBefore(config: TestConfig): TestConfig {
|
|
39
39
|
const suite = this.getSuite(config);
|
|
40
|
-
suite.tests[config.methodName] = { sourceHash: config.sourceHash, status: 'unknown', duration: 0 };
|
|
40
|
+
suite.tests[config.methodName] = { sourceHash: config.sourceHash, status: 'unknown', duration: 0, selfDuration: 0 };
|
|
41
41
|
return config;
|
|
42
42
|
}
|
|
43
43
|
|
|
@@ -56,21 +56,9 @@ export class CumulativeSummaryConsumer extends DelegatingConsumer {
|
|
|
56
56
|
onSuiteAfter(result: SuiteResult): SuiteResult {
|
|
57
57
|
// Reset counts
|
|
58
58
|
const suite = this.getSuite(result);
|
|
59
|
-
const
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
skipped: 0,
|
|
63
|
-
errored: 0,
|
|
64
|
-
unknown: 0,
|
|
65
|
-
total: 0,
|
|
66
|
-
duration: 0
|
|
67
|
-
};
|
|
68
|
-
for (const test of Object.values(suite.tests)) {
|
|
69
|
-
totals[test.status] += 1;
|
|
70
|
-
totals.total += 1;
|
|
71
|
-
totals.duration += test.duration ?? 0;
|
|
72
|
-
}
|
|
73
|
-
return { ...result, ...totals, status: TestModelUtil.countsToTestStatus(totals) };
|
|
59
|
+
const results = TestModelUtil.buildSummary();
|
|
60
|
+
TestModelUtil.countTestResult(results, Object.values(suite.tests));
|
|
61
|
+
return { ...result, ...results, status: TestModelUtil.computeTestStatus(results) };
|
|
74
62
|
}
|
|
75
63
|
|
|
76
64
|
removeTest(importName: string, classId?: string, methodName?: string): void {
|
|
@@ -126,4 +114,4 @@ export class CumulativeSummaryConsumer extends DelegatingConsumer {
|
|
|
126
114
|
}
|
|
127
115
|
return output;
|
|
128
116
|
}
|
|
129
|
-
}
|
|
117
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type { SuitesSummary, TestConsumerShape, TestRunState } from '../types.ts';
|
|
2
1
|
import type { TestEvent, TestRemoveEvent } from '../../model/event.ts';
|
|
2
|
+
import type { SuitesSummary, TestConsumerShape, TestRunState } from '../types.ts';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Delegating event consumer
|
|
@@ -20,6 +20,12 @@ export abstract class DelegatingConsumer implements TestConsumerShape {
|
|
|
20
20
|
}
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
+
async onTestRunState(state: TestRunState): Promise<void> {
|
|
24
|
+
for (const consumer of this.#consumers) {
|
|
25
|
+
await consumer.onTestRunState?.(state);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
23
29
|
onRemoveEvent(event: TestRemoveEvent): void {
|
|
24
30
|
let result = event;
|
|
25
31
|
if (this.transformRemove) {
|
|
@@ -58,4 +64,4 @@ export abstract class DelegatingConsumer implements TestConsumerShape {
|
|
|
58
64
|
|
|
59
65
|
transform?(event: TestEvent): TestEvent | undefined;
|
|
60
66
|
transformRemove?(event: TestRemoveEvent): TestRemoveEvent | undefined;
|
|
61
|
-
}
|
|
67
|
+
}
|
|
@@ -3,8 +3,8 @@ import type { Writable } from 'node:stream';
|
|
|
3
3
|
import { JSONUtil } from '@travetto/runtime';
|
|
4
4
|
|
|
5
5
|
import type { TestEvent, TestRemoveEvent } from '../../model/event.ts';
|
|
6
|
-
import type { TestConsumerShape } from '../types.ts';
|
|
7
6
|
import { TestConsumer } from '../decorator.ts';
|
|
7
|
+
import type { TestConsumerShape } from '../types.ts';
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
10
|
* Streams all test events a JSON payload, in an nd-json format
|
|
@@ -28,4 +28,4 @@ export class EventStreamer implements TestConsumerShape {
|
|
|
28
28
|
onRemoveEvent(event: TestRemoveEvent): void {
|
|
29
29
|
this.sendPayload(event);
|
|
30
30
|
}
|
|
31
|
-
}
|
|
31
|
+
}
|
|
@@ -1,16 +1,15 @@
|
|
|
1
|
-
import { IpcChannel } from '@travetto/worker';
|
|
2
1
|
import { JSONUtil } from '@travetto/runtime';
|
|
2
|
+
import { IpcChannel } from '@travetto/worker';
|
|
3
3
|
|
|
4
4
|
import type { TestEvent, TestRemoveEvent } from '../../model/event.ts';
|
|
5
|
-
import type { TestConsumerShape } from '../types.ts';
|
|
6
5
|
import { TestConsumer } from '../decorator.ts';
|
|
6
|
+
import type { TestConsumerShape } from '../types.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
13
|
sendPayload(payload: unknown & { type: string }): void {
|
|
15
14
|
this.send(payload.type, JSONUtil.cloneForTransmit(payload));
|
|
16
15
|
}
|
|
@@ -22,4 +21,4 @@ export class ExecutionEmitter extends IpcChannel<TestEvent> implements TestConsu
|
|
|
22
21
|
onRemoveEvent(event: TestRemoveEvent): void {
|
|
23
22
|
this.sendPayload(event);
|
|
24
23
|
}
|
|
25
|
-
}
|
|
24
|
+
}
|
|
@@ -2,24 +2,23 @@ import type { Writable } from 'node:stream';
|
|
|
2
2
|
|
|
3
3
|
import { JSONUtil } from '@travetto/runtime';
|
|
4
4
|
|
|
5
|
-
import type { SuitesSummary } from '../types.ts';
|
|
6
5
|
import { TestConsumer } from '../decorator.ts';
|
|
6
|
+
import type { SuitesSummary } from '../types.ts';
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
9
|
* Returns the entire result set as a single JSON document
|
|
10
10
|
*/
|
|
11
11
|
@TestConsumer()
|
|
12
12
|
export class JSONEmitter {
|
|
13
|
-
|
|
14
13
|
#stream: Writable;
|
|
15
14
|
|
|
16
15
|
constructor(stream: Writable = process.stdout) {
|
|
17
16
|
this.#stream = stream;
|
|
18
17
|
}
|
|
19
18
|
|
|
20
|
-
onEvent(): void {
|
|
19
|
+
onEvent(): void {}
|
|
21
20
|
|
|
22
21
|
onSummary(summary: SuitesSummary): void {
|
|
23
22
|
this.#stream.write(JSONUtil.toUTF8Pretty(summary));
|
|
24
23
|
}
|
|
25
|
-
}
|
|
24
|
+
}
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import type { TestConsumerShape } from '../types.ts';
|
|
2
1
|
import { TestConsumer } from '../decorator.ts';
|
|
2
|
+
import type { TestConsumerShape } from '../types.ts';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Does nothing consumer
|
|
6
6
|
*/
|
|
7
7
|
@TestConsumer()
|
|
8
8
|
export class NoopConsumer implements TestConsumerShape {
|
|
9
|
-
onEvent(): void {
|
|
10
|
-
}
|
|
9
|
+
onEvent(): void {}
|
|
10
|
+
}
|
|
@@ -1,13 +1,12 @@
|
|
|
1
|
-
import type { TestConsumerShape } from '../types.ts';
|
|
2
|
-
import { TestResultsSummarizer } from './summarizer.ts';
|
|
3
1
|
import type { TestEvent } from '../../model/event.ts';
|
|
2
|
+
import type { TestConsumerShape } from '../types.ts';
|
|
4
3
|
import { DelegatingConsumer } from './delegating.ts';
|
|
4
|
+
import { TestResultsSummarizer } from './summarizer.ts';
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* Test consumer with support for multiple nested consumers, and summarization
|
|
8
8
|
*/
|
|
9
9
|
export class RunnableTestConsumer extends DelegatingConsumer {
|
|
10
|
-
|
|
11
10
|
#results?: TestResultsSummarizer;
|
|
12
11
|
|
|
13
12
|
constructor(...consumers: TestConsumerShape[]) {
|
|
@@ -22,6 +21,6 @@ export class RunnableTestConsumer extends DelegatingConsumer {
|
|
|
22
21
|
|
|
23
22
|
async summarizeAsBoolean(): Promise<boolean> {
|
|
24
23
|
await this.summarize(this.#results?.summary);
|
|
25
|
-
return (this.#results?.summary.failed ?? 0) <= 0;
|
|
24
|
+
return (this.#results?.summary.failed ?? 0) <= 0 && (this.#results?.summary.errored ?? 0) <= 0;
|
|
26
25
|
}
|
|
27
|
-
}
|
|
26
|
+
}
|
|
@@ -1,33 +1,20 @@
|
|
|
1
|
-
import type { SuiteResult } from '../../model/suite.ts';
|
|
2
1
|
import type { TestEvent } from '../../model/event.ts';
|
|
2
|
+
import type { SuiteResult } from '../../model/suite.ts';
|
|
3
|
+
import { TestModelUtil } from '../../model/util.ts';
|
|
3
4
|
import type { SuitesSummary, TestConsumerShape } from '../types.ts';
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* Test Result Collector, combines all results into a single Suite Result
|
|
7
8
|
*/
|
|
8
9
|
export class TestResultsSummarizer implements TestConsumerShape {
|
|
9
|
-
|
|
10
10
|
summary: SuitesSummary = {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
errored: 0,
|
|
14
|
-
skipped: 0,
|
|
15
|
-
unknown: 0,
|
|
16
|
-
total: 0,
|
|
17
|
-
duration: 0,
|
|
18
|
-
suites: [],
|
|
19
|
-
errors: []
|
|
11
|
+
...TestModelUtil.buildSummary(),
|
|
12
|
+
suites: []
|
|
20
13
|
};
|
|
21
14
|
|
|
22
15
|
#merge(result: SuiteResult): void {
|
|
16
|
+
TestModelUtil.countTestResult(this.summary, Object.values(result.tests));
|
|
23
17
|
this.summary.suites.push(result);
|
|
24
|
-
this.summary.failed += result.failed;
|
|
25
|
-
this.summary.errored += result.errored;
|
|
26
|
-
this.summary.passed += result.passed;
|
|
27
|
-
this.summary.unknown += result.unknown;
|
|
28
|
-
this.summary.skipped += result.skipped;
|
|
29
|
-
this.summary.duration += result.duration;
|
|
30
|
-
this.summary.total += result.total;
|
|
31
18
|
}
|
|
32
19
|
|
|
33
20
|
/**
|
|
@@ -1,15 +1,14 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { AsyncQueue, Util } from '@travetto/runtime';
|
|
2
|
+
import { Terminal, TerminalUtil } from '@travetto/terminal';
|
|
3
3
|
|
|
4
4
|
import type { TestEvent } from '../../model/event.ts';
|
|
5
|
-
import type {
|
|
6
|
-
|
|
7
|
-
import
|
|
5
|
+
import type { SuiteResult } from '../../model/suite.ts';
|
|
6
|
+
import type { TestResult } from '../../model/test.ts';
|
|
7
|
+
import { TestModelUtil } from '../../model/util.ts';
|
|
8
8
|
import { TestConsumer } from '../decorator.ts';
|
|
9
|
-
|
|
10
|
-
import { TapEmitter } from './tap.ts';
|
|
11
9
|
import { CONSOLE_ENHANCER, type TestResultsEnhancer } from '../enhancer.ts';
|
|
12
|
-
import type {
|
|
10
|
+
import type { SuitesSummary, TestConsumerShape, TestRunState } from '../types.ts';
|
|
11
|
+
import { TapEmitter } from './tap.ts';
|
|
13
12
|
|
|
14
13
|
type Result = {
|
|
15
14
|
key: string;
|
|
@@ -22,7 +21,6 @@ type Result = {
|
|
|
22
21
|
*/
|
|
23
22
|
@TestConsumer()
|
|
24
23
|
export class TapSummaryEmitter implements TestConsumerShape {
|
|
25
|
-
|
|
26
24
|
#timings = new Map<'test' | 'module' | 'file' | 'suite', Map<string, Result>>();
|
|
27
25
|
|
|
28
26
|
#terminal: Terminal;
|
|
@@ -31,6 +29,7 @@ export class TapSummaryEmitter implements TestConsumerShape {
|
|
|
31
29
|
#consumer: TapEmitter;
|
|
32
30
|
#enhancer: TestResultsEnhancer;
|
|
33
31
|
#options?: Record<string, unknown>;
|
|
32
|
+
#state: TestRunState = {};
|
|
34
33
|
|
|
35
34
|
constructor(terminal: Terminal = new Terminal(process.stderr)) {
|
|
36
35
|
this.#terminal = terminal;
|
|
@@ -51,20 +50,16 @@ export class TapSummaryEmitter implements TestConsumerShape {
|
|
|
51
50
|
foundModule.duration += suite.duration;
|
|
52
51
|
foundModule.tests += testCount;
|
|
53
52
|
|
|
54
|
-
const foundFile = this.#timings
|
|
55
|
-
.getOrInsert('file', new Map<string, Result>())
|
|
56
|
-
.getOrInsert(file, { key: file, duration: 0, tests: 0 });
|
|
53
|
+
const foundFile = this.#timings.getOrInsert('file', new Map<string, Result>()).getOrInsert(file, { key: file, duration: 0, tests: 0 });
|
|
57
54
|
|
|
58
55
|
foundFile.duration += suite.duration;
|
|
59
56
|
foundFile.tests += testCount;
|
|
60
57
|
|
|
61
|
-
this.#timings
|
|
62
|
-
.
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
tests: testCount
|
|
67
|
-
});
|
|
58
|
+
this.#timings.getOrInsert('suite', new Map<string, Result>()).set(suite.classId, {
|
|
59
|
+
key: suite.classId,
|
|
60
|
+
duration: suite.duration,
|
|
61
|
+
tests: testCount
|
|
62
|
+
});
|
|
68
63
|
}
|
|
69
64
|
|
|
70
65
|
#renderTimings(): void {
|
|
@@ -75,7 +70,9 @@ export class TapSummaryEmitter implements TestConsumerShape {
|
|
|
75
70
|
const top10 = [...results.values()].toSorted((a, b) => b.duration - a.duration).slice(0, count);
|
|
76
71
|
|
|
77
72
|
for (const result of top10) {
|
|
78
|
-
console.log(
|
|
73
|
+
console.log(
|
|
74
|
+
` * ${this.#enhancer.testName(result.key)} - ${this.#enhancer.total(result.duration)}ms / ${this.#enhancer.total(result.tests)} tests`
|
|
75
|
+
);
|
|
79
76
|
}
|
|
80
77
|
this.#consumer.log('');
|
|
81
78
|
}
|
|
@@ -87,22 +84,34 @@ export class TapSummaryEmitter implements TestConsumerShape {
|
|
|
87
84
|
this.#consumer.setOptions(options);
|
|
88
85
|
}
|
|
89
86
|
|
|
87
|
+
onTestRunState(state: TestRunState): void {
|
|
88
|
+
Object.assign(this.#state, state);
|
|
89
|
+
}
|
|
90
|
+
|
|
90
91
|
async onStart(state: TestRunState): Promise<void> {
|
|
91
92
|
this.#consumer.onStart();
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
const
|
|
93
|
+
this.onTestRunState(state);
|
|
94
|
+
|
|
95
|
+
const total = TestModelUtil.buildSummary();
|
|
95
96
|
this.#progress = this.#terminal.streamToBottom(
|
|
96
97
|
Util.mapAsyncIterable(
|
|
97
98
|
this.#results,
|
|
98
|
-
|
|
99
|
-
total[value
|
|
100
|
-
total.count += 1;
|
|
99
|
+
value => {
|
|
100
|
+
TestModelUtil.countTestResult(total, [value]);
|
|
101
101
|
const statusLine = `${total.failed} failed, ${total.errored} errored, ${total.skipped} skipped`;
|
|
102
|
-
return {
|
|
103
|
-
|
|
102
|
+
return {
|
|
103
|
+
value: `Tests %completed/%total [${statusLine}] -- ${value.classId}`,
|
|
104
|
+
total: this.#state.testCount,
|
|
105
|
+
completed: total.passed,
|
|
106
|
+
failed: total.failed + total.errored
|
|
107
|
+
};
|
|
104
108
|
},
|
|
105
|
-
TerminalUtil.progressBarUpdater(this.#terminal, {
|
|
109
|
+
TerminalUtil.progressBarUpdater(this.#terminal, {
|
|
110
|
+
style: {
|
|
111
|
+
complete: { text: '#e5e5e5', background: '#026020' },
|
|
112
|
+
failed: { text: '#e5e5e5', background: '#8b0000' }
|
|
113
|
+
}
|
|
114
|
+
})
|
|
106
115
|
),
|
|
107
116
|
{ minDelay: 100 }
|
|
108
117
|
);
|
|
@@ -112,7 +121,7 @@ export class TapSummaryEmitter implements TestConsumerShape {
|
|
|
112
121
|
if (event.type === 'test' && event.phase === 'after') {
|
|
113
122
|
const { test } = event;
|
|
114
123
|
this.#results.add(test);
|
|
115
|
-
if (test.status
|
|
124
|
+
if (test.status !== 'passed' && test.status !== 'skipped') {
|
|
116
125
|
this.#consumer.onEvent(event);
|
|
117
126
|
}
|
|
118
127
|
const tests = this.#timings.getOrInsert('test', new Map<string, Result>());
|
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
|
+
|
|
2
3
|
import { stringify } from 'yaml';
|
|
3
4
|
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
5
|
+
import { RuntimeIndex, TimeUtil } from '@travetto/runtime';
|
|
6
|
+
import { StyleUtil, Terminal } from '@travetto/terminal';
|
|
6
7
|
|
|
7
8
|
import type { TestEvent } from '../../model/event.ts';
|
|
8
|
-
import type { SuitesSummary, TestConsumerShape } from '../types.ts';
|
|
9
9
|
import { TestConsumer } from '../decorator.ts';
|
|
10
|
-
import { type TestResultsEnhancer
|
|
10
|
+
import { CONSOLE_ENHANCER, type TestResultsEnhancer } from '../enhancer.ts';
|
|
11
|
+
import type { SuitesSummary, TestConsumerShape } from '../types.ts';
|
|
12
|
+
import { TestConsumerUtil } from './util.ts';
|
|
13
|
+
|
|
14
|
+
const SPACE = ' ';
|
|
11
15
|
|
|
12
16
|
/**
|
|
13
17
|
* TAP Format consumer
|
|
@@ -17,13 +21,10 @@ export class TapEmitter implements TestConsumerShape {
|
|
|
17
21
|
#count = 0;
|
|
18
22
|
#enhancer: TestResultsEnhancer;
|
|
19
23
|
#terminal: Terminal;
|
|
20
|
-
#start: number;
|
|
21
24
|
#options?: Record<string, unknown>;
|
|
25
|
+
#start: number = 0;
|
|
22
26
|
|
|
23
|
-
constructor(
|
|
24
|
-
terminal = new Terminal(),
|
|
25
|
-
enhancer: TestResultsEnhancer = CONSOLE_ENHANCER
|
|
26
|
-
) {
|
|
27
|
+
constructor(terminal = new Terminal(), enhancer: TestResultsEnhancer = CONSOLE_ENHANCER) {
|
|
27
28
|
this.#terminal = terminal;
|
|
28
29
|
this.#enhancer = enhancer;
|
|
29
30
|
}
|
|
@@ -50,28 +51,13 @@ export class TapEmitter implements TestConsumerShape {
|
|
|
50
51
|
logMeta(metadata: Record<string, unknown>): void {
|
|
51
52
|
const lineLength = this.#terminal.width - 5;
|
|
52
53
|
let body = stringify(metadata, { lineWidth: lineLength, indent: 2 });
|
|
53
|
-
body = body
|
|
54
|
+
body = body
|
|
55
|
+
.split('\n')
|
|
56
|
+
.map(line => ` ${line}`)
|
|
57
|
+
.join('\n');
|
|
54
58
|
this.log(`---\n${this.#enhancer.objectInspect(body)}\n...`);
|
|
55
59
|
}
|
|
56
60
|
|
|
57
|
-
/**
|
|
58
|
-
* Error to string
|
|
59
|
-
* @param error
|
|
60
|
-
*/
|
|
61
|
-
errorToString(error?: Error): string | undefined {
|
|
62
|
-
if (error && error.name !== 'AssertionError') {
|
|
63
|
-
if (error instanceof Error) {
|
|
64
|
-
let out = JSONUtil.toUTF8(hasToJSON(error) ? error.toJSON() : error, { indent: 2 });
|
|
65
|
-
if (this.#options?.verbose && error.stack) {
|
|
66
|
-
out = `${out}\n${error.stack}`;
|
|
67
|
-
}
|
|
68
|
-
return out;
|
|
69
|
-
} else {
|
|
70
|
-
return `${error}`;
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
|
|
75
61
|
/**
|
|
76
62
|
* Listen for each event
|
|
77
63
|
*/
|
|
@@ -86,7 +72,7 @@ export class TapEmitter implements TestConsumerShape {
|
|
|
86
72
|
StyleUtil.link(suiteId, `file://${suiteSourceFile}#${test.suiteLineStart ?? 1}`),
|
|
87
73
|
' - ',
|
|
88
74
|
StyleUtil.link(this.#enhancer.testName(test.methodName), `file://${testSourceFile}#${test.lineStart}`),
|
|
89
|
-
...test.description ? [`: ${this.#enhancer.testDescription(test.description)}`] : []
|
|
75
|
+
...(test.description ? [`: ${this.#enhancer.testDescription(test.description)}`] : [])
|
|
90
76
|
].join('');
|
|
91
77
|
|
|
92
78
|
this.log(`# ${header}`);
|
|
@@ -125,9 +111,17 @@ export class TapEmitter implements TestConsumerShape {
|
|
|
125
111
|
// Track test result
|
|
126
112
|
let status = `${this.#enhancer.testNumber(++this.#count)} `;
|
|
127
113
|
switch (test.status) {
|
|
128
|
-
case '
|
|
129
|
-
|
|
130
|
-
|
|
114
|
+
case 'passed':
|
|
115
|
+
`${this.#enhancer.success('ok')} ${status}`;
|
|
116
|
+
break;
|
|
117
|
+
case 'skipped':
|
|
118
|
+
status += ' # SKIP';
|
|
119
|
+
break;
|
|
120
|
+
case 'unknown':
|
|
121
|
+
break;
|
|
122
|
+
default:
|
|
123
|
+
status = `${this.#enhancer.failure('not ok')} ${status}`;
|
|
124
|
+
break;
|
|
131
125
|
}
|
|
132
126
|
status += header;
|
|
133
127
|
|
|
@@ -138,9 +132,9 @@ export class TapEmitter implements TestConsumerShape {
|
|
|
138
132
|
case 'errored':
|
|
139
133
|
case 'failed': {
|
|
140
134
|
if (test.error) {
|
|
141
|
-
const
|
|
142
|
-
if (
|
|
143
|
-
this.logMeta({ error:
|
|
135
|
+
const message = TestConsumerUtil.errorToString(test.error, !!this.#options?.verbose);
|
|
136
|
+
if (message) {
|
|
137
|
+
this.logMeta({ error: message });
|
|
144
138
|
}
|
|
145
139
|
}
|
|
146
140
|
break;
|
|
@@ -168,28 +162,42 @@ export class TapEmitter implements TestConsumerShape {
|
|
|
168
162
|
onSummary(summary: SuitesSummary): void {
|
|
169
163
|
this.log(`${this.#enhancer.testNumber(1)}..${this.#enhancer.testNumber(summary.total)}`);
|
|
170
164
|
|
|
171
|
-
if (summary.errors.length) {
|
|
172
|
-
this.log('---\n');
|
|
173
|
-
for (const error of summary.errors) {
|
|
174
|
-
const msg = this.errorToString(error);
|
|
175
|
-
if (msg) {
|
|
176
|
-
this.log(this.#enhancer.failure(msg));
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
|
|
181
165
|
const allPassed = !summary.failed && !summary.errored;
|
|
182
166
|
|
|
183
|
-
this.log(
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
167
|
+
this.log(
|
|
168
|
+
[
|
|
169
|
+
this.#enhancer[allPassed ? 'success' : 'failure']('Results'),
|
|
170
|
+
SPACE,
|
|
171
|
+
`${this.#enhancer.total(summary.passed)}/${this.#enhancer.total(summary.total)},`,
|
|
172
|
+
SPACE,
|
|
173
|
+
allPassed ? 'failed' : this.#enhancer.failure('failed'),
|
|
174
|
+
SPACE,
|
|
175
|
+
`${this.#enhancer.total(summary.failed)}`,
|
|
176
|
+
SPACE,
|
|
177
|
+
allPassed ? 'errored' : this.#enhancer.failure('errored'),
|
|
178
|
+
SPACE,
|
|
179
|
+
`${this.#enhancer.total(summary.errored)}`,
|
|
180
|
+
SPACE,
|
|
181
|
+
'skipped',
|
|
182
|
+
SPACE,
|
|
183
|
+
this.#enhancer.total(summary.skipped),
|
|
184
|
+
SPACE,
|
|
185
|
+
'#',
|
|
186
|
+
SPACE,
|
|
187
|
+
'(Timings:',
|
|
188
|
+
SPACE,
|
|
189
|
+
'Self=',
|
|
190
|
+
TimeUtil.asClock(summary.selfDuration),
|
|
191
|
+
',',
|
|
192
|
+
SPACE,
|
|
193
|
+
'Total=',
|
|
194
|
+
TimeUtil.asClock(summary.duration),
|
|
195
|
+
',',
|
|
196
|
+
SPACE,
|
|
197
|
+
'Clock=',
|
|
198
|
+
TimeUtil.asClock(Date.now() - this.#start),
|
|
199
|
+
')'
|
|
200
|
+
].join('')
|
|
201
|
+
);
|
|
194
202
|
}
|
|
195
203
|
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { AssertionError } from 'node:assert';
|
|
2
|
+
import util from 'node:util';
|
|
3
|
+
|
|
4
|
+
import { TypedObject } from '@travetto/runtime';
|
|
5
|
+
|
|
6
|
+
export class TestConsumerUtil {
|
|
7
|
+
/**
|
|
8
|
+
* Convert error to string
|
|
9
|
+
*/
|
|
10
|
+
static errorToString(error?: Error, verbose?: boolean): string | undefined {
|
|
11
|
+
if (error instanceof AssertionError) {
|
|
12
|
+
return;
|
|
13
|
+
} else if (error instanceof Error) {
|
|
14
|
+
const stack = error.stack
|
|
15
|
+
? error.stack
|
|
16
|
+
.split(/\n/)
|
|
17
|
+
.slice(0, verbose ? -1 : 5)
|
|
18
|
+
.join('\n')
|
|
19
|
+
: error.message;
|
|
20
|
+
const subObject: Record<string, unknown> = {};
|
|
21
|
+
for (const key of TypedObject.keys(error)) {
|
|
22
|
+
if (key !== 'stack' && key !== 'message' && key !== 'name') {
|
|
23
|
+
subObject[key] = error[key];
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return `${stack}${Object.keys(subObject).length ? `\n${util.inspect(subObject)}` : ''}`;
|
|
27
|
+
} else {
|
|
28
|
+
return `${error}`;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -5,8 +5,9 @@ import { stringify } from 'yaml';
|
|
|
5
5
|
import { RuntimeIndex } from '@travetto/runtime';
|
|
6
6
|
|
|
7
7
|
import type { TestEvent } from '../../model/event.ts';
|
|
8
|
-
import type { SuitesSummary, TestConsumerShape } from '../types.ts';
|
|
9
8
|
import { TestConsumer } from '../decorator.ts';
|
|
9
|
+
import type { SuitesSummary, TestConsumerShape } from '../types.ts';
|
|
10
|
+
import { TestConsumerUtil } from './util.ts';
|
|
10
11
|
|
|
11
12
|
/**
|
|
12
13
|
* Xunit consumer, compatible with JUnit formatters
|
|
@@ -36,7 +37,10 @@ export class XunitEmitter implements TestConsumerShape {
|
|
|
36
37
|
}
|
|
37
38
|
if (Object.keys(metadata).length) {
|
|
38
39
|
let body = stringify(metadata);
|
|
39
|
-
body = body
|
|
40
|
+
body = body
|
|
41
|
+
.split('\n')
|
|
42
|
+
.map(line => ` ${line}`)
|
|
43
|
+
.join('\n');
|
|
40
44
|
return `<![CDATA[\n${body}\n]]>`;
|
|
41
45
|
} else {
|
|
42
46
|
return '';
|
|
@@ -48,7 +52,6 @@ export class XunitEmitter implements TestConsumerShape {
|
|
|
48
52
|
*/
|
|
49
53
|
onEvent(event: TestEvent): void {
|
|
50
54
|
if (event.type === 'test' && event.phase === 'after') {
|
|
51
|
-
|
|
52
55
|
const { test } = event;
|
|
53
56
|
|
|
54
57
|
let name = `${test.methodName}`;
|
|
@@ -59,9 +62,9 @@ export class XunitEmitter implements TestConsumerShape {
|
|
|
59
62
|
let body = '';
|
|
60
63
|
|
|
61
64
|
if (test.error) {
|
|
62
|
-
const
|
|
65
|
+
const errorMessage = TestConsumerUtil.errorToString(test.error);
|
|
63
66
|
const node = test.status === 'failed' ? 'failure' : 'error';
|
|
64
|
-
body = `<${node} type="${
|
|
67
|
+
body = `<${node} type="${test.error.constructor.name}" message="${encodeURIComponent(test.error.message)}"><![CDATA[${errorMessage}]]></${node}>`;
|
|
65
68
|
}
|
|
66
69
|
|
|
67
70
|
const groupedByLevel: Record<string, string[]> = {};
|
|
@@ -78,8 +81,7 @@ export class XunitEmitter implements TestConsumerShape {
|
|
|
78
81
|
${body}
|
|
79
82
|
<system-out>${this.buildMeta({ log: groupedByLevel.log, info: groupedByLevel.info, debug: groupedByLevel.debug })}</system-out>
|
|
80
83
|
<system-err>${this.buildMeta({ error: groupedByLevel.error, warn: groupedByLevel.warn })}</system-err>
|
|
81
|
-
</testcase>`
|
|
82
|
-
);
|
|
84
|
+
</testcase>`);
|
|
83
85
|
} else if (event.type === 'suite' && event.phase === 'after') {
|
|
84
86
|
const { suite } = event;
|
|
85
87
|
const testBodies = this.#tests.slice(0);
|
|
@@ -120,4 +122,4 @@ export class XunitEmitter implements TestConsumerShape {
|
|
|
120
122
|
</testsuites>
|
|
121
123
|
`);
|
|
122
124
|
}
|
|
123
|
-
}
|
|
125
|
+
}
|
package/src/consumer/types.ts
CHANGED
|
@@ -1,24 +1,16 @@
|
|
|
1
1
|
import type { Class } from '@travetto/runtime';
|
|
2
2
|
|
|
3
3
|
import type { TestEvent, TestRemoveEvent } from '../model/event.ts';
|
|
4
|
-
import type {
|
|
4
|
+
import type { ResultsSummary, SuiteResult } from '../model/suite.ts';
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* All suite results
|
|
8
8
|
*/
|
|
9
|
-
export interface SuitesSummary extends
|
|
9
|
+
export interface SuitesSummary extends ResultsSummary {
|
|
10
10
|
/**
|
|
11
11
|
* List of all suites
|
|
12
12
|
*/
|
|
13
13
|
suites: SuiteResult[];
|
|
14
|
-
/**
|
|
15
|
-
* List of all errors
|
|
16
|
-
*/
|
|
17
|
-
errors: Error[];
|
|
18
|
-
/**
|
|
19
|
-
* Total duration
|
|
20
|
-
*/
|
|
21
|
-
duration: number;
|
|
22
14
|
}
|
|
23
15
|
|
|
24
16
|
export type TestRunState = {
|
|
@@ -44,6 +36,10 @@ export interface TestConsumerShape {
|
|
|
44
36
|
* Set options
|
|
45
37
|
*/
|
|
46
38
|
setOptions?(options?: Record<string, unknown>): Promise<void> | void;
|
|
39
|
+
/**
|
|
40
|
+
* Can directly update the known test run state as needed
|
|
41
|
+
*/
|
|
42
|
+
onTestRunState?(state: TestRunState): Promise<void> | void;
|
|
47
43
|
/**
|
|
48
44
|
* Listen for start of the test run
|
|
49
45
|
*/
|