@travetto/test 8.0.0-alpha.2 → 8.0.0-alpha.20
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 +10 -2
- package/package.json +8 -8
- package/src/assert/check.ts +5 -9
- package/src/assert/util.ts +14 -73
- package/src/consumer/types/cumulative.ts +6 -18
- package/src/consumer/types/delegating.ts +6 -0
- package/src/consumer/types/runnable.ts +2 -1
- package/src/consumer/types/summarizer.ts +4 -16
- package/src/consumer/types/tap-summary.ts +25 -11
- package/src/consumer/types/tap.ts +25 -45
- package/src/consumer/types/util.ts +28 -0
- package/src/consumer/types/xunit.ts +3 -2
- package/src/consumer/types.ts +6 -10
- package/src/execute/executor.ts +86 -146
- package/src/execute/phase.ts +12 -22
- package/src/execute/run.ts +3 -2
- package/src/model/suite.ts +13 -11
- package/src/model/test.ts +3 -3
- package/src/model/util.ts +90 -7
- package/src/worker/child.ts +1 -3
- package/support/cli.test.ts +4 -1
- package/support/cli.test_child.ts +6 -1
- package/support/cli.test_diff.ts +6 -1
- package/support/cli.test_digest.ts +7 -0
- package/support/cli.test_direct.ts +6 -1
- package/support/cli.test_watch.ts +4 -1
package/README.md
CHANGED
|
@@ -220,18 +220,26 @@ class SimpleTest {
|
|
|
220
220
|
## Running Tests
|
|
221
221
|
To run the tests you can either call the [Command Line Interface](https://github.com/travetto/travetto/tree/main/module/cli#readme "CLI infrastructure for Travetto framework") by invoking
|
|
222
222
|
|
|
223
|
-
|
|
223
|
+
## CLI - test
|
|
224
|
+
|
|
225
|
+
**Terminal: Help for test**
|
|
224
226
|
```bash
|
|
225
227
|
$ trv test --help
|
|
226
228
|
|
|
227
229
|
Usage: test [options] [first:string] [globs...:string]
|
|
228
230
|
|
|
231
|
+
Description:
|
|
232
|
+
Execute the test framework for targeted files, suites, or methods.
|
|
233
|
+
|
|
234
|
+
Supports glob-based discovery, import-based targeting, tag filtering, and
|
|
235
|
+
configurable output consumers for local and CI workflows.
|
|
236
|
+
|
|
229
237
|
Options:
|
|
230
238
|
-f, --format <string> Output format for test results (default: "tap")
|
|
231
239
|
-c, --concurrency <number> Number of tests to run concurrently (default: 9)
|
|
232
240
|
-t, --tags <string> Tags to target or exclude when using globs
|
|
233
241
|
-o, --format-options <string> Format options
|
|
234
|
-
|
|
242
|
+
--help display help for command
|
|
235
243
|
```
|
|
236
244
|
|
|
237
245
|
The regexes are the patterns of tests you want to run, and all tests must be found under the `test/` folder.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@travetto/test",
|
|
3
|
-
"version": "8.0.0-alpha.
|
|
3
|
+
"version": "8.0.0-alpha.20",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Declarative test framework",
|
|
6
6
|
"keywords": [
|
|
@@ -28,15 +28,15 @@
|
|
|
28
28
|
"directory": "module/test"
|
|
29
29
|
},
|
|
30
30
|
"dependencies": {
|
|
31
|
-
"@travetto/registry": "^8.0.0-alpha.
|
|
32
|
-
"@travetto/runtime": "^8.0.0-alpha.
|
|
33
|
-
"@travetto/terminal": "^8.0.0-alpha.
|
|
34
|
-
"@travetto/worker": "^8.0.0-alpha.
|
|
35
|
-
"yaml": "^2.
|
|
31
|
+
"@travetto/registry": "^8.0.0-alpha.19",
|
|
32
|
+
"@travetto/runtime": "^8.0.0-alpha.19",
|
|
33
|
+
"@travetto/terminal": "^8.0.0-alpha.19",
|
|
34
|
+
"@travetto/worker": "^8.0.0-alpha.19",
|
|
35
|
+
"yaml": "^2.9.0"
|
|
36
36
|
},
|
|
37
37
|
"peerDependencies": {
|
|
38
|
-
"@travetto/cli": "^8.0.0-alpha.
|
|
39
|
-
"@travetto/transformer": "^8.0.0-alpha.
|
|
38
|
+
"@travetto/cli": "^8.0.0-alpha.27",
|
|
39
|
+
"@travetto/transformer": "^8.0.0-alpha.13"
|
|
40
40
|
},
|
|
41
41
|
"peerDependenciesMeta": {
|
|
42
42
|
"@travetto/transformer": {
|
package/src/assert/check.ts
CHANGED
|
@@ -36,7 +36,7 @@ export class AssertCheck {
|
|
|
36
36
|
};
|
|
37
37
|
|
|
38
38
|
// Invert check for negative
|
|
39
|
-
const assertFn = positive ? assert : (value: unknown, msg?: string): unknown => assert(!value, msg);
|
|
39
|
+
const assertFn = positive ? assert : (value: unknown, msg?: string): unknown => assert(!value, msg!);
|
|
40
40
|
|
|
41
41
|
// Check fn to call
|
|
42
42
|
if (fn === 'fail') {
|
|
@@ -91,7 +91,7 @@ export class AssertCheck {
|
|
|
91
91
|
case 'lessThanEqual': assertFn(castTo<number>(actual) <= castTo<number>(expected), message); break;
|
|
92
92
|
case 'greaterThan': assertFn(castTo<number>(actual) > castTo<number>(expected), message); break;
|
|
93
93
|
case 'greaterThanEqual': assertFn(castTo<number>(actual) >= castTo<number>(expected), message); break;
|
|
94
|
-
case 'ok': assertFn(
|
|
94
|
+
case 'ok': assertFn.apply(null, castTo(args)); break;
|
|
95
95
|
default:
|
|
96
96
|
if (fn && assert[castKey<typeof assert>(fn)]) { // Assert call
|
|
97
97
|
if (/not/i.test(fn)) {
|
|
@@ -178,17 +178,13 @@ export class AssertCheck {
|
|
|
178
178
|
static #onError(
|
|
179
179
|
positive: boolean,
|
|
180
180
|
message: string | undefined,
|
|
181
|
-
|
|
181
|
+
errorValue: unknown,
|
|
182
182
|
missed: Error | undefined,
|
|
183
183
|
shouldThrow: ThrowableError | undefined,
|
|
184
184
|
assertion: CapturedAssertion
|
|
185
185
|
): void {
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
}
|
|
189
|
-
if (!(error instanceof Error)) {
|
|
190
|
-
throw error;
|
|
191
|
-
}
|
|
186
|
+
const error = errorValue instanceof Error ? errorValue : new Error(`${errorValue}`);
|
|
187
|
+
|
|
192
188
|
if (positive) {
|
|
193
189
|
missed = new assert.AssertionError({ message: 'Error thrown, but expected no errors' });
|
|
194
190
|
missed.stack = error.stack;
|
package/src/assert/util.ts
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
import util from 'node:util';
|
|
2
|
-
import path from 'node:path';
|
|
3
2
|
|
|
4
|
-
import {
|
|
3
|
+
import { JSONUtil, hasFunction, RuntimeIndex, Util } from '@travetto/runtime';
|
|
5
4
|
|
|
6
|
-
import type {
|
|
7
|
-
import type { SuiteConfig
|
|
5
|
+
import type { Assertion, TestConfig } from '../model/test.ts';
|
|
6
|
+
import type { SuiteConfig } from '../model/suite.ts';
|
|
8
7
|
|
|
9
8
|
const isCleanable = hasFunction<{ toClean(): unknown }>('toClean');
|
|
10
9
|
|
|
@@ -52,79 +51,21 @@ export class AssertUtil {
|
|
|
52
51
|
/**
|
|
53
52
|
* Generate a suite error given a suite config, and an error
|
|
54
53
|
*/
|
|
55
|
-
static
|
|
56
|
-
const { suite, test, error, importLocation } = config;
|
|
54
|
+
static generateAssertion(config: { suite: SuiteConfig, test: TestConfig, error: Error, importLocation?: string }): Assertion {
|
|
55
|
+
const { suite, test, error: errorValue, importLocation } = config;
|
|
56
|
+
const error = (errorValue.cause && errorValue.cause instanceof Error) ? errorValue.cause : errorValue;
|
|
57
57
|
const testImport = importLocation ?? test.import;
|
|
58
58
|
const position = this.getPositionOfError(error);
|
|
59
59
|
const line = position?.line ?? (testImport === suite.import ? suite.lineStart : 1);
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
60
|
+
return {
|
|
61
|
+
import: position?.import ?? testImport,
|
|
62
|
+
methodName: test.methodName,
|
|
63
|
+
classId: suite.classId,
|
|
64
|
+
operator: 'throw',
|
|
64
65
|
error,
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
assertions: [{
|
|
69
|
-
import: position?.import ?? testImport,
|
|
70
|
-
methodName: test.methodName,
|
|
71
|
-
classId: suite.classId,
|
|
72
|
-
operator: 'throw',
|
|
73
|
-
error,
|
|
74
|
-
line,
|
|
75
|
-
message: error.message.split(/\n/)[0],
|
|
76
|
-
text: test.methodName
|
|
77
|
-
}],
|
|
66
|
+
line,
|
|
67
|
+
message: error.message.split(/\n/)[0],
|
|
68
|
+
text: test.methodName
|
|
78
69
|
};
|
|
79
|
-
|
|
80
|
-
return testResult;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
/**
|
|
84
|
-
* Generate suite failure
|
|
85
|
-
*/
|
|
86
|
-
static generateSuiteTestFailures(suite: SuiteConfig, error: Error): TestResult[] {
|
|
87
|
-
const finalError = error.cause instanceof Error ? error.cause : error;
|
|
88
|
-
return Object.values(suite.tests).map(test => this.generateSuiteTestFailure({ suite, test, error: finalError }));
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
/**
|
|
92
|
-
* Define import failure as a TestResult
|
|
93
|
-
*/
|
|
94
|
-
static gernerateImportFailure(importLocation: string, error: Error): { result: TestResult, test: TestConfig, suite: SuiteResult & SuiteConfig } {
|
|
95
|
-
const name = path.basename(importLocation);
|
|
96
|
-
const classId = `${RuntimeIndex.getFromImport(importLocation)?.id}#${name}`;
|
|
97
|
-
const suite = asFull<SuiteConfig & SuiteResult>({
|
|
98
|
-
class: asFull<Class>({ name }), classId, duration: 0, lineStart: 1, lineEnd: 1, import: importLocation
|
|
99
|
-
});
|
|
100
|
-
error.message = error.message.replaceAll(Runtime.mainSourcePath, '.');
|
|
101
|
-
const result = this.generateSuiteTestFailure({
|
|
102
|
-
suite,
|
|
103
|
-
test: {
|
|
104
|
-
methodName: 'require',
|
|
105
|
-
classId,
|
|
106
|
-
import: importLocation,
|
|
107
|
-
class: suite.class,
|
|
108
|
-
lineBodyStart: 1,
|
|
109
|
-
lineStart: 1,
|
|
110
|
-
lineEnd: 1,
|
|
111
|
-
skip: false
|
|
112
|
-
},
|
|
113
|
-
error
|
|
114
|
-
});
|
|
115
|
-
const test: TestConfig = {
|
|
116
|
-
methodName: 'import',
|
|
117
|
-
classId,
|
|
118
|
-
import: importLocation,
|
|
119
|
-
declarationImport: importLocation,
|
|
120
|
-
lineStart: 0,
|
|
121
|
-
lineEnd: 0,
|
|
122
|
-
lineBodyStart: 0,
|
|
123
|
-
tags: [],
|
|
124
|
-
description: 'Import Failure',
|
|
125
|
-
skip: false,
|
|
126
|
-
class: undefined!
|
|
127
|
-
};
|
|
128
|
-
return { result, test, suite };
|
|
129
70
|
}
|
|
130
71
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { TestConsumerShape } from '../types.ts';
|
|
2
2
|
import type { TestEvent, TestRemoveEvent } from '../../model/event.ts';
|
|
3
3
|
import type { TestConfig, TestDiffSource, TestResult } from '../../model/test.ts';
|
|
4
|
-
import type {
|
|
4
|
+
import type { SuiteConfig, SuiteResult } from '../../model/suite.ts';
|
|
5
5
|
import { DelegatingConsumer } from './delegating.ts';
|
|
6
6
|
import type { SuiteCore } from '../../model/common.ts';
|
|
7
7
|
import { TestModelUtil } from '../../model/util.ts';
|
|
@@ -9,7 +9,7 @@ import { TestModelUtil } from '../../model/util.ts';
|
|
|
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
|
};
|
|
@@ -37,7 +37,7 @@ export class CumulativeSummaryConsumer extends DelegatingConsumer {
|
|
|
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 {
|
|
@@ -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) {
|
|
@@ -22,6 +22,7 @@ export class RunnableTestConsumer extends DelegatingConsumer {
|
|
|
22
22
|
|
|
23
23
|
async summarizeAsBoolean(): Promise<boolean> {
|
|
24
24
|
await this.summarize(this.#results?.summary);
|
|
25
|
-
return (this.#results?.summary.failed ?? 0) <= 0
|
|
25
|
+
return (this.#results?.summary.failed ?? 0) <= 0 &&
|
|
26
|
+
(this.#results?.summary.errored ?? 0) <= 0;
|
|
26
27
|
}
|
|
27
28
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { SuiteResult } from '../../model/suite.ts';
|
|
2
2
|
import type { TestEvent } from '../../model/event.ts';
|
|
3
3
|
import type { SuitesSummary, TestConsumerShape } from '../types.ts';
|
|
4
|
+
import { TestModelUtil } from '../../model/util.ts';
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* Test Result Collector, combines all results into a single Suite Result
|
|
@@ -8,26 +9,13 @@ import type { SuitesSummary, TestConsumerShape } from '../types.ts';
|
|
|
8
9
|
export class TestResultsSummarizer implements TestConsumerShape {
|
|
9
10
|
|
|
10
11
|
summary: SuitesSummary = {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
errored: 0,
|
|
14
|
-
skipped: 0,
|
|
15
|
-
unknown: 0,
|
|
16
|
-
total: 0,
|
|
17
|
-
duration: 0,
|
|
18
|
-
suites: [],
|
|
19
|
-
errors: []
|
|
12
|
+
...TestModelUtil.buildSummary(),
|
|
13
|
+
suites: []
|
|
20
14
|
};
|
|
21
15
|
|
|
22
16
|
#merge(result: SuiteResult): void {
|
|
17
|
+
TestModelUtil.countTestResult(this.summary, Object.values(result.tests));
|
|
23
18
|
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
19
|
}
|
|
32
20
|
|
|
33
21
|
/**
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { Util, AsyncQueue } from '@travetto/runtime';
|
|
2
|
-
import {
|
|
2
|
+
import { Terminal, TerminalUtil } from '@travetto/terminal';
|
|
3
3
|
|
|
4
4
|
import type { TestEvent } from '../../model/event.ts';
|
|
5
|
-
import type { TestResult
|
|
5
|
+
import type { TestResult } from '../../model/test.ts';
|
|
6
6
|
|
|
7
7
|
import type { SuitesSummary, TestConsumerShape, TestRunState } from '../types.ts';
|
|
8
8
|
import { TestConsumer } from '../decorator.ts';
|
|
@@ -10,6 +10,7 @@ import { TestConsumer } from '../decorator.ts';
|
|
|
10
10
|
import { TapEmitter } from './tap.ts';
|
|
11
11
|
import { CONSOLE_ENHANCER, type TestResultsEnhancer } from '../enhancer.ts';
|
|
12
12
|
import type { SuiteResult } from '../../model/suite.ts';
|
|
13
|
+
import { TestModelUtil } from '../../model/util.ts';
|
|
13
14
|
|
|
14
15
|
type Result = {
|
|
15
16
|
key: string;
|
|
@@ -31,6 +32,7 @@ export class TapSummaryEmitter implements TestConsumerShape {
|
|
|
31
32
|
#consumer: TapEmitter;
|
|
32
33
|
#enhancer: TestResultsEnhancer;
|
|
33
34
|
#options?: Record<string, unknown>;
|
|
35
|
+
#state: TestRunState = {};
|
|
34
36
|
|
|
35
37
|
constructor(terminal: Terminal = new Terminal(process.stderr)) {
|
|
36
38
|
this.#terminal = terminal;
|
|
@@ -87,22 +89,34 @@ export class TapSummaryEmitter implements TestConsumerShape {
|
|
|
87
89
|
this.#consumer.setOptions(options);
|
|
88
90
|
}
|
|
89
91
|
|
|
92
|
+
onTestRunState(state: TestRunState): void {
|
|
93
|
+
Object.assign(this.#state, state);
|
|
94
|
+
}
|
|
95
|
+
|
|
90
96
|
async onStart(state: TestRunState): Promise<void> {
|
|
91
97
|
this.#consumer.onStart();
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
const
|
|
98
|
+
this.onTestRunState(state);
|
|
99
|
+
|
|
100
|
+
const total = TestModelUtil.buildSummary();
|
|
95
101
|
this.#progress = this.#terminal.streamToBottom(
|
|
96
102
|
Util.mapAsyncIterable(
|
|
97
103
|
this.#results,
|
|
98
104
|
(value) => {
|
|
99
|
-
total[value
|
|
100
|
-
total.count += 1;
|
|
105
|
+
TestModelUtil.countTestResult(total, [value]);
|
|
101
106
|
const statusLine = `${total.failed} failed, ${total.errored} errored, ${total.skipped} skipped`;
|
|
102
|
-
return {
|
|
103
|
-
|
|
107
|
+
return {
|
|
108
|
+
value: `Tests %completed/%total [${statusLine}] -- ${value.classId}`,
|
|
109
|
+
total: this.#state.testCount,
|
|
110
|
+
completed: total.passed,
|
|
111
|
+
failed: total.failed + total.errored
|
|
112
|
+
};
|
|
104
113
|
},
|
|
105
|
-
TerminalUtil.progressBarUpdater(this.#terminal, {
|
|
114
|
+
TerminalUtil.progressBarUpdater(this.#terminal, {
|
|
115
|
+
style: {
|
|
116
|
+
complete: { text: '#e5e5e5', background: '#026020' },
|
|
117
|
+
failed: { text: '#e5e5e5', background: '#8b0000' }
|
|
118
|
+
}
|
|
119
|
+
})
|
|
106
120
|
),
|
|
107
121
|
{ minDelay: 100 }
|
|
108
122
|
);
|
|
@@ -112,7 +126,7 @@ export class TapSummaryEmitter implements TestConsumerShape {
|
|
|
112
126
|
if (event.type === 'test' && event.phase === 'after') {
|
|
113
127
|
const { test } = event;
|
|
114
128
|
this.#results.add(test);
|
|
115
|
-
if (test.status
|
|
129
|
+
if (test.status !== 'passed' && test.status !== 'skipped') {
|
|
116
130
|
this.#consumer.onEvent(event);
|
|
117
131
|
}
|
|
118
132
|
const tests = this.#timings.getOrInsert('test', new Map<string, Result>());
|
|
@@ -2,12 +2,15 @@ import path from 'node:path';
|
|
|
2
2
|
import { stringify } from 'yaml';
|
|
3
3
|
|
|
4
4
|
import { Terminal, StyleUtil } from '@travetto/terminal';
|
|
5
|
-
import { TimeUtil, RuntimeIndex
|
|
5
|
+
import { TimeUtil, RuntimeIndex } from '@travetto/runtime';
|
|
6
6
|
|
|
7
7
|
import type { TestEvent } from '../../model/event.ts';
|
|
8
8
|
import type { SuitesSummary, TestConsumerShape } from '../types.ts';
|
|
9
9
|
import { TestConsumer } from '../decorator.ts';
|
|
10
10
|
import { type TestResultsEnhancer, CONSOLE_ENHANCER } from '../enhancer.ts';
|
|
11
|
+
import { TestConsumerUtil } from './util.ts';
|
|
12
|
+
|
|
13
|
+
const SPACE = ' ';
|
|
11
14
|
|
|
12
15
|
/**
|
|
13
16
|
* TAP Format consumer
|
|
@@ -17,8 +20,8 @@ export class TapEmitter implements TestConsumerShape {
|
|
|
17
20
|
#count = 0;
|
|
18
21
|
#enhancer: TestResultsEnhancer;
|
|
19
22
|
#terminal: Terminal;
|
|
20
|
-
#start: number;
|
|
21
23
|
#options?: Record<string, unknown>;
|
|
24
|
+
#start: number = 0;
|
|
22
25
|
|
|
23
26
|
constructor(
|
|
24
27
|
terminal = new Terminal(),
|
|
@@ -54,24 +57,6 @@ export class TapEmitter implements TestConsumerShape {
|
|
|
54
57
|
this.log(`---\n${this.#enhancer.objectInspect(body)}\n...`);
|
|
55
58
|
}
|
|
56
59
|
|
|
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
60
|
/**
|
|
76
61
|
* Listen for each event
|
|
77
62
|
*/
|
|
@@ -125,9 +110,10 @@ export class TapEmitter implements TestConsumerShape {
|
|
|
125
110
|
// Track test result
|
|
126
111
|
let status = `${this.#enhancer.testNumber(++this.#count)} `;
|
|
127
112
|
switch (test.status) {
|
|
113
|
+
case 'passed': `${this.#enhancer.success('ok')} ${status}`; break;
|
|
128
114
|
case 'skipped': status += ' # SKIP'; break;
|
|
129
|
-
case '
|
|
130
|
-
default: status = `${this.#enhancer.
|
|
115
|
+
case 'unknown': break;
|
|
116
|
+
default: status = `${this.#enhancer.failure('not ok')} ${status}`; break;
|
|
131
117
|
}
|
|
132
118
|
status += header;
|
|
133
119
|
|
|
@@ -138,9 +124,9 @@ export class TapEmitter implements TestConsumerShape {
|
|
|
138
124
|
case 'errored':
|
|
139
125
|
case 'failed': {
|
|
140
126
|
if (test.error) {
|
|
141
|
-
const
|
|
142
|
-
if (
|
|
143
|
-
this.logMeta({ error:
|
|
127
|
+
const message = TestConsumerUtil.errorToString(test.error, !!this.#options?.verbose);
|
|
128
|
+
if (message) {
|
|
129
|
+
this.logMeta({ error: message });
|
|
144
130
|
}
|
|
145
131
|
}
|
|
146
132
|
break;
|
|
@@ -168,28 +154,22 @@ export class TapEmitter implements TestConsumerShape {
|
|
|
168
154
|
onSummary(summary: SuitesSummary): void {
|
|
169
155
|
this.log(`${this.#enhancer.testNumber(1)}..${this.#enhancer.testNumber(summary.total)}`);
|
|
170
156
|
|
|
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
157
|
const allPassed = !summary.failed && !summary.errored;
|
|
182
158
|
|
|
183
159
|
this.log([
|
|
184
|
-
this.#enhancer[allPassed ? 'success' : 'failure']('Results'),
|
|
185
|
-
`${this.#enhancer.total(summary.passed)}/${this.#enhancer.total(summary.total)},`,
|
|
186
|
-
allPassed ? 'failed' : this.#enhancer.failure('failed'),
|
|
187
|
-
`${this.#enhancer.total(summary.failed)}`,
|
|
188
|
-
allPassed ? 'errored' : this.#enhancer.failure('errored'),
|
|
189
|
-
`${this.#enhancer.total(summary.errored)}`,
|
|
190
|
-
'skipped',
|
|
191
|
-
this.#enhancer.total(summary.skipped),
|
|
192
|
-
|
|
193
|
-
|
|
160
|
+
this.#enhancer[allPassed ? 'success' : 'failure']('Results'), SPACE,
|
|
161
|
+
`${this.#enhancer.total(summary.passed)}/${this.#enhancer.total(summary.total)},`, SPACE,
|
|
162
|
+
allPassed ? 'failed' : this.#enhancer.failure('failed'), SPACE,
|
|
163
|
+
`${this.#enhancer.total(summary.failed)}`, SPACE,
|
|
164
|
+
allPassed ? 'errored' : this.#enhancer.failure('errored'), SPACE,
|
|
165
|
+
`${this.#enhancer.total(summary.errored)}`, SPACE,
|
|
166
|
+
'skipped', SPACE,
|
|
167
|
+
this.#enhancer.total(summary.skipped), SPACE,
|
|
168
|
+
'#', SPACE, '(Timings:', SPACE,
|
|
169
|
+
'Self=', TimeUtil.asClock(summary.selfDuration), ',', SPACE,
|
|
170
|
+
'Total=', TimeUtil.asClock(summary.duration), ',', SPACE,
|
|
171
|
+
'Clock=', TimeUtil.asClock(Date.now() - this.#start),
|
|
172
|
+
')',
|
|
173
|
+
].join(''));
|
|
194
174
|
}
|
|
195
175
|
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import util from 'node:util';
|
|
2
|
+
import { AssertionError } from 'node:assert';
|
|
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.split(/\n/).slice(0, verbose ? -1 : 5).join('\n') :
|
|
16
|
+
error.message;
|
|
17
|
+
const subObject: Record<string, unknown> = {};
|
|
18
|
+
for (const key of TypedObject.keys(error)) {
|
|
19
|
+
if (key !== 'stack' && key !== 'message' && key !== 'name') {
|
|
20
|
+
subObject[key] = error[key];
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return `${stack}${Object.keys(subObject).length ? `\n${util.inspect(subObject)}` : ''}`;
|
|
24
|
+
} else {
|
|
25
|
+
return `${error}`;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -7,6 +7,7 @@ import { RuntimeIndex } from '@travetto/runtime';
|
|
|
7
7
|
import type { TestEvent } from '../../model/event.ts';
|
|
8
8
|
import type { SuitesSummary, TestConsumerShape } from '../types.ts';
|
|
9
9
|
import { TestConsumer } from '../decorator.ts';
|
|
10
|
+
import { TestConsumerUtil } from './util.ts';
|
|
10
11
|
|
|
11
12
|
/**
|
|
12
13
|
* Xunit consumer, compatible with JUnit formatters
|
|
@@ -59,9 +60,9 @@ export class XunitEmitter implements TestConsumerShape {
|
|
|
59
60
|
let body = '';
|
|
60
61
|
|
|
61
62
|
if (test.error) {
|
|
62
|
-
const
|
|
63
|
+
const errorMessage = TestConsumerUtil.errorToString(test.error);
|
|
63
64
|
const node = test.status === 'failed' ? 'failure' : 'error';
|
|
64
|
-
body = `<${node} type="${
|
|
65
|
+
body = `<${node} type="${test.error.constructor.name}" message="${encodeURIComponent(test.error.message)}"><![CDATA[${errorMessage}]]></${node}>`;
|
|
65
66
|
}
|
|
66
67
|
|
|
67
68
|
const groupedByLevel: Record<string, string[]> = {};
|
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
|
*/
|