@travetto/test 8.0.0-alpha.1 → 8.0.0-alpha.10
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 -1
- package/package.json +8 -8
- package/src/assert/check.ts +3 -7
- 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 +13 -7
- 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/README.md
CHANGED
|
@@ -231,7 +231,7 @@ Options:
|
|
|
231
231
|
-c, --concurrency <number> Number of tests to run concurrently (default: 9)
|
|
232
232
|
-t, --tags <string> Tags to target or exclude when using globs
|
|
233
233
|
-o, --format-options <string> Format options
|
|
234
|
-
|
|
234
|
+
--help display help for command
|
|
235
235
|
```
|
|
236
236
|
|
|
237
237
|
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.10",
|
|
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.8.
|
|
31
|
+
"@travetto/registry": "^8.0.0-alpha.10",
|
|
32
|
+
"@travetto/runtime": "^8.0.0-alpha.10",
|
|
33
|
+
"@travetto/terminal": "^8.0.0-alpha.10",
|
|
34
|
+
"@travetto/worker": "^8.0.0-alpha.10",
|
|
35
|
+
"yaml": "^2.8.3"
|
|
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.15",
|
|
39
|
+
"@travetto/transformer": "^8.0.0-alpha.5"
|
|
40
40
|
},
|
|
41
41
|
"peerDependenciesMeta": {
|
|
42
42
|
"@travetto/transformer": {
|
package/src/assert/check.ts
CHANGED
|
@@ -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
|
/**
|
|
@@ -2,7 +2,7 @@ import { Util, AsyncQueue } from '@travetto/runtime';
|
|
|
2
2
|
import { StyleUtil, 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,20 +89,24 @@ 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
|
-
|
|
98
|
+
this.onTestRunState(state);
|
|
99
|
+
|
|
100
|
+
const total = TestModelUtil.buildSummary();
|
|
93
101
|
const success = StyleUtil.getStyle({ text: '#e5e5e5', background: '#026020' }); // White on dark green
|
|
94
102
|
const fail = StyleUtil.getStyle({ text: '#e5e5e5', background: '#8b0000' }); // White on dark red
|
|
95
103
|
this.#progress = this.#terminal.streamToBottom(
|
|
96
104
|
Util.mapAsyncIterable(
|
|
97
105
|
this.#results,
|
|
98
106
|
(value) => {
|
|
99
|
-
total[value
|
|
100
|
-
total.count += 1;
|
|
107
|
+
TestModelUtil.countTestResult(total, [value]);
|
|
101
108
|
const statusLine = `${total.failed} failed, ${total.errored} errored, ${total.skipped} skipped`;
|
|
102
|
-
return { value: `Tests %idx/%total [${statusLine}] -- ${value.classId}`, total: state.testCount, idx: total.passed };
|
|
103
|
-
|
|
109
|
+
return { value: `Tests %idx/%total [${statusLine}] -- ${value.classId}`, total: this.#state.testCount, idx: total.passed };
|
|
104
110
|
},
|
|
105
111
|
TerminalUtil.progressBarUpdater(this.#terminal, { style: () => ({ complete: (total.failed || total.errored) ? fail : success }) })
|
|
106
112
|
),
|
|
@@ -112,7 +118,7 @@ export class TapSummaryEmitter implements TestConsumerShape {
|
|
|
112
118
|
if (event.type === 'test' && event.phase === 'after') {
|
|
113
119
|
const { test } = event;
|
|
114
120
|
this.#results.add(test);
|
|
115
|
-
if (test.status
|
|
121
|
+
if (test.status !== 'passed' && test.status !== 'skipped') {
|
|
116
122
|
this.#consumer.onEvent(event);
|
|
117
123
|
}
|
|
118
124
|
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
|
*/
|
package/src/execute/executor.ts
CHANGED
|
@@ -26,38 +26,18 @@ export class TestExecutor {
|
|
|
26
26
|
this.#consumer = consumer;
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
-
#onSuiteTestError(result: TestResult, test: TestConfig): void {
|
|
30
|
-
this.#consumer.onEvent({ type: 'test', phase: 'before', test });
|
|
31
|
-
for (const assertion of result.assertions) {
|
|
32
|
-
this.#consumer.onEvent({ type: 'assertion', phase: 'after', assertion });
|
|
33
|
-
}
|
|
34
|
-
this.#consumer.onEvent({ type: 'test', phase: 'after', test: result });
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
#recordSuiteErrors(suiteConfig: SuiteConfig, suiteResult: SuiteResult, errors: TestResult[]): void {
|
|
38
|
-
for (const test of errors) {
|
|
39
|
-
if (!suiteResult.tests[test.methodName]) {
|
|
40
|
-
this.#onSuiteTestError(test, suiteConfig.tests[test.methodName]);
|
|
41
|
-
suiteResult.errored += 1;
|
|
42
|
-
suiteResult.total += 1;
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
|
|
47
29
|
/**
|
|
48
30
|
* Raw execution, runs the method and then returns any thrown errors as the result.
|
|
49
31
|
*
|
|
50
32
|
* This method should never throw under any circumstances.
|
|
51
33
|
*/
|
|
52
|
-
async #executeTestMethod(test: TestConfig): Promise<Error | undefined> {
|
|
53
|
-
const suite = SuiteRegistryIndex.getConfig(test.class);
|
|
54
|
-
|
|
34
|
+
async #executeTestMethod(instance: unknown, test: TestConfig): Promise<Error | undefined> {
|
|
55
35
|
// Ensure all the criteria below are satisfied before moving forward
|
|
56
36
|
return Barrier.awaitOperation(test.timeout || TEST_TIMEOUT, async () => {
|
|
57
37
|
const env = process.env;
|
|
58
38
|
process.env = { ...env }; // Created an isolated environment
|
|
59
39
|
try {
|
|
60
|
-
await castTo<Record<string, Function>>(
|
|
40
|
+
await castTo<Record<string, Function>>(instance)[test.methodName]();
|
|
61
41
|
} finally {
|
|
62
42
|
process.env = env; // Restore
|
|
63
43
|
}
|
|
@@ -73,96 +53,43 @@ export class TestExecutor {
|
|
|
73
53
|
}
|
|
74
54
|
}
|
|
75
55
|
|
|
76
|
-
#skipTest(test: TestConfig, result: SuiteResult): void {
|
|
77
|
-
// Mark test start
|
|
78
|
-
this.#consumer.onEvent({ type: 'test', phase: 'before', test });
|
|
79
|
-
result.skipped += 1;
|
|
80
|
-
result.total += 1;
|
|
81
|
-
this.#consumer.onEvent({
|
|
82
|
-
type: 'test',
|
|
83
|
-
phase: 'after',
|
|
84
|
-
test: {
|
|
85
|
-
...test,
|
|
86
|
-
suiteLineStart: result.lineStart,
|
|
87
|
-
assertions: [], duration: 0, durationTotal: 0, output: [], status: 'skipped'
|
|
88
|
-
}
|
|
89
|
-
});
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
/**
|
|
93
|
-
* An empty suite result based on a suite config
|
|
94
|
-
*/
|
|
95
|
-
createSuiteResult(suite: SuiteConfig, override?: Partial<SuiteResult>): SuiteResult {
|
|
96
|
-
return {
|
|
97
|
-
passed: 0,
|
|
98
|
-
failed: 0,
|
|
99
|
-
errored: 0,
|
|
100
|
-
skipped: 0,
|
|
101
|
-
unknown: 0,
|
|
102
|
-
total: 0,
|
|
103
|
-
status: 'unknown',
|
|
104
|
-
lineStart: suite.lineStart,
|
|
105
|
-
lineEnd: suite.lineEnd,
|
|
106
|
-
import: suite.import,
|
|
107
|
-
classId: suite.classId,
|
|
108
|
-
sourceHash: suite.sourceHash,
|
|
109
|
-
duration: 0,
|
|
110
|
-
tests: {},
|
|
111
|
-
...override
|
|
112
|
-
};
|
|
113
|
-
}
|
|
114
|
-
|
|
115
56
|
/**
|
|
116
57
|
* Execute the test, capture output, assertions and promises
|
|
117
58
|
*/
|
|
118
|
-
async executeTest(test: TestConfig, suite: SuiteConfig): Promise<TestResult> {
|
|
59
|
+
async executeTest(instance: unknown, test: TestConfig, suite: SuiteConfig, override?: Partial<TestResult>): Promise<TestResult> {
|
|
60
|
+
|
|
61
|
+
const result = TestModelUtil.createTestResult(suite, test, override);
|
|
119
62
|
|
|
120
63
|
// Mark test start
|
|
121
64
|
this.#consumer.onEvent({ type: 'test', phase: 'before', test });
|
|
122
65
|
|
|
123
|
-
const startTime = Date.now();
|
|
124
|
-
|
|
125
|
-
const result: TestResult = {
|
|
126
|
-
methodName: test.methodName,
|
|
127
|
-
description: test.description,
|
|
128
|
-
classId: test.classId,
|
|
129
|
-
tags: test.tags,
|
|
130
|
-
suiteLineStart: suite.lineStart,
|
|
131
|
-
lineStart: test.lineStart,
|
|
132
|
-
lineEnd: test.lineEnd,
|
|
133
|
-
lineBodyStart: test.lineBodyStart,
|
|
134
|
-
import: test.import,
|
|
135
|
-
declarationImport: test.declarationImport,
|
|
136
|
-
sourceHash: test.sourceHash,
|
|
137
|
-
status: 'unknown',
|
|
138
|
-
assertions: [],
|
|
139
|
-
duration: 0,
|
|
140
|
-
durationTotal: 0,
|
|
141
|
-
output: [],
|
|
142
|
-
};
|
|
143
66
|
|
|
144
67
|
// Emit every assertion as it occurs
|
|
145
|
-
const getAssertions = AssertCapture.collector(test,
|
|
146
|
-
this.#consumer.onEvent({
|
|
147
|
-
type: 'assertion',
|
|
148
|
-
phase: 'after',
|
|
149
|
-
assertion: asrt
|
|
150
|
-
})
|
|
151
|
-
);
|
|
68
|
+
const getAssertions = AssertCapture.collector(test, item =>
|
|
69
|
+
this.#consumer.onEvent({ type: 'assertion', phase: 'after', assertion: item }));
|
|
152
70
|
|
|
153
71
|
const consoleCapture = new ConsoleCapture().start(); // Capture all output from transpiled code
|
|
154
72
|
|
|
155
|
-
//
|
|
156
|
-
|
|
157
|
-
|
|
73
|
+
// Already finished
|
|
74
|
+
if (result.status !== 'unknown') {
|
|
75
|
+
if (result.error) {
|
|
76
|
+
result.assertions.push(AssertUtil.generateAssertion({ suite, test, error: result.error }));
|
|
77
|
+
}
|
|
78
|
+
for (const item of result.assertions ?? []) { AssertCapture.add(item); }
|
|
79
|
+
} else {
|
|
80
|
+
// Run method and get result
|
|
81
|
+
const startTime = Date.now();
|
|
82
|
+
const error = await this.#executeTestMethod(instance, test);
|
|
83
|
+
const [status, finalError] = AssertCheck.validateTestResultError(test, error);
|
|
84
|
+
result.status = status;
|
|
85
|
+
result.selfDuration = Date.now() - startTime;
|
|
86
|
+
if (finalError) {
|
|
87
|
+
result.error = finalError;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
158
90
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
output: consoleCapture.end(),
|
|
162
|
-
assertions: getAssertions(),
|
|
163
|
-
duration: Date.now() - startTime,
|
|
164
|
-
...(finalError ? { error: finalError } : {})
|
|
165
|
-
});
|
|
91
|
+
result.output = consoleCapture.end();
|
|
92
|
+
result.assertions = getAssertions();
|
|
166
93
|
|
|
167
94
|
// Mark completion
|
|
168
95
|
this.#consumer.onEvent({ type: 'test', phase: 'after', test: result });
|
|
@@ -175,18 +102,21 @@ export class TestExecutor {
|
|
|
175
102
|
*/
|
|
176
103
|
async executeSuite(suite: SuiteConfig, tests: TestConfig[]): Promise<void> {
|
|
177
104
|
|
|
178
|
-
|
|
105
|
+
const instance = classConstruct(suite.class);
|
|
179
106
|
|
|
180
|
-
const shouldSkip = await this.#shouldSkip(suite,
|
|
107
|
+
const shouldSkip = await this.#shouldSkip(suite, instance);
|
|
108
|
+
|
|
109
|
+
const result: SuiteResult = TestModelUtil.createSuiteResult(suite);
|
|
181
110
|
|
|
182
111
|
if (shouldSkip) {
|
|
183
112
|
this.#consumer.onEvent({
|
|
184
113
|
phase: 'after', type: 'suite',
|
|
185
|
-
suite:
|
|
114
|
+
suite: {
|
|
115
|
+
...result,
|
|
186
116
|
status: 'skipped',
|
|
187
117
|
skipped: tests.length,
|
|
188
118
|
total: tests.length
|
|
189
|
-
}
|
|
119
|
+
}
|
|
190
120
|
});
|
|
191
121
|
}
|
|
192
122
|
|
|
@@ -194,69 +124,80 @@ export class TestExecutor {
|
|
|
194
124
|
return;
|
|
195
125
|
}
|
|
196
126
|
|
|
197
|
-
const
|
|
127
|
+
const manager = new TestPhaseManager(suite, instance);
|
|
128
|
+
const originalEnv = { ...process.env };
|
|
129
|
+
const startTime = Date.now();
|
|
130
|
+
const testResultOverrides: Record<string, Partial<TestResult>> = {};
|
|
131
|
+
|
|
198
132
|
const validTestMethodNames = new Set(tests.map(t => t.methodName));
|
|
199
133
|
const testConfigs = Object.fromEntries(
|
|
200
134
|
Object.entries(suite.tests).filter(([key]) => validTestMethodNames.has(key))
|
|
201
135
|
);
|
|
202
136
|
|
|
203
|
-
const startTime = Date.now();
|
|
204
|
-
|
|
205
137
|
// Mark suite start
|
|
206
138
|
this.#consumer.onEvent({ phase: 'before', type: 'suite', suite: { ...suite, tests: testConfigs } });
|
|
207
139
|
|
|
208
|
-
const manager = new TestPhaseManager(suite);
|
|
209
|
-
|
|
210
|
-
const originalEnv = { ...process.env };
|
|
211
|
-
|
|
212
140
|
try {
|
|
213
141
|
// Handle the BeforeAll calls
|
|
214
142
|
await manager.startPhase('all');
|
|
143
|
+
} catch (someError) {
|
|
144
|
+
const suiteError = await manager.onError('all', someError);
|
|
145
|
+
for (const method of validTestMethodNames) {
|
|
146
|
+
testResultOverrides[method] ??= { status: 'errored', error: suiteError };
|
|
147
|
+
}
|
|
148
|
+
}
|
|
215
149
|
|
|
216
|
-
|
|
150
|
+
const suiteEnv = { ...process.env };
|
|
217
151
|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
continue;
|
|
222
|
-
}
|
|
152
|
+
for (const test of tests) {
|
|
153
|
+
// Reset env before each test
|
|
154
|
+
process.env = { ...suiteEnv };
|
|
223
155
|
|
|
224
|
-
|
|
225
|
-
|
|
156
|
+
const testStart = Date.now();
|
|
157
|
+
const testResultOverride = (testResultOverrides[test.methodName] ??= {});
|
|
226
158
|
|
|
227
|
-
|
|
228
|
-
|
|
159
|
+
if (await this.#shouldSkip(test, instance)) {
|
|
160
|
+
testResultOverride.status = 'skipped';
|
|
161
|
+
}
|
|
229
162
|
|
|
230
|
-
|
|
231
|
-
|
|
163
|
+
try {
|
|
164
|
+
// Handle BeforeEach
|
|
165
|
+
testResultOverride.status || await manager.startPhase('each');
|
|
166
|
+
} catch (someError) {
|
|
167
|
+
const testError = await manager.onError('each', someError);
|
|
168
|
+
testResultOverride.error = testError;
|
|
169
|
+
testResultOverride.status = 'errored';
|
|
170
|
+
}
|
|
232
171
|
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
result.tests[testResult.methodName] = testResult;
|
|
236
|
-
result[testResult.status]++;
|
|
237
|
-
result.total += 1;
|
|
172
|
+
// Run test
|
|
173
|
+
const testResult = await this.executeTest(instance, test, suite, testResultOverride);
|
|
238
174
|
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
}
|
|
175
|
+
// Handle after each
|
|
176
|
+
try {
|
|
177
|
+
testResultOverride.status || await manager.endPhase('each');
|
|
178
|
+
} catch (testError) {
|
|
179
|
+
if (!(testError instanceof Error)) { throw testError; };
|
|
180
|
+
console.error('Failed to properly shutdown test', testError.message);
|
|
246
181
|
}
|
|
247
182
|
|
|
183
|
+
result.tests[testResult.methodName] = testResult;
|
|
184
|
+
testResult.duration = Date.now() - testStart;
|
|
185
|
+
TestModelUtil.countTestResult(result, [testResult]);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
try {
|
|
248
189
|
// Handle after all
|
|
249
190
|
await manager.endPhase('all');
|
|
250
191
|
} catch (suiteError) {
|
|
251
|
-
|
|
252
|
-
|
|
192
|
+
if (!(suiteError instanceof Error)) { throw suiteError; };
|
|
193
|
+
console.error('Failed to properly shutdown test', suiteError.message);
|
|
253
194
|
}
|
|
254
195
|
|
|
255
196
|
// Restore env
|
|
256
197
|
process.env = { ...originalEnv };
|
|
257
198
|
|
|
258
199
|
result.duration = Date.now() - startTime;
|
|
259
|
-
result.status = TestModelUtil.
|
|
200
|
+
result.status = TestModelUtil.computeTestStatus(result);
|
|
260
201
|
|
|
261
202
|
// Mark suite complete
|
|
262
203
|
this.#consumer.onEvent({ phase: 'after', type: 'suite', suite: result });
|
|
@@ -265,19 +206,13 @@ export class TestExecutor {
|
|
|
265
206
|
/**
|
|
266
207
|
* Handle executing a suite's test/tests based on command line inputs
|
|
267
208
|
*/
|
|
268
|
-
async execute(run: TestRun): Promise<void> {
|
|
209
|
+
async execute(run: TestRun, singleFile?: boolean): Promise<void> {
|
|
269
210
|
try {
|
|
270
211
|
await Runtime.importFrom(run.import);
|
|
271
212
|
} catch (error) {
|
|
272
|
-
if (!(error instanceof Error)) {
|
|
273
|
-
|
|
274
|
-
}
|
|
213
|
+
if (!(error instanceof Error)) { throw error; }
|
|
214
|
+
const suite = TestModelUtil.createImportErrorSuiteResult(run);
|
|
275
215
|
console.error(error);
|
|
276
|
-
|
|
277
|
-
// Fire import failure as a test failure for each test in the suite
|
|
278
|
-
const { result, test, suite } = AssertUtil.gernerateImportFailure(run.import, error);
|
|
279
|
-
this.#consumer.onEvent({ type: 'suite', phase: 'before', suite });
|
|
280
|
-
this.#onSuiteTestError(result, test);
|
|
281
216
|
this.#consumer.onEvent({ type: 'suite', phase: 'after', suite });
|
|
282
217
|
return;
|
|
283
218
|
}
|
|
@@ -291,6 +226,11 @@ export class TestExecutor {
|
|
|
291
226
|
console.warn('Unable to find suites for ', run);
|
|
292
227
|
}
|
|
293
228
|
|
|
229
|
+
if (singleFile) {
|
|
230
|
+
const testCount = suites.reduce((acc, suite) => acc + suite.tests.length, 0);
|
|
231
|
+
this.#consumer.onTestRunState?.({ testCount });
|
|
232
|
+
}
|
|
233
|
+
|
|
294
234
|
for (const { suite, tests } of suites) {
|
|
295
235
|
await this.executeSuite(suite, tests);
|
|
296
236
|
}
|
package/src/execute/phase.ts
CHANGED
|
@@ -1,9 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Env, TimeUtil } from '@travetto/runtime';
|
|
2
2
|
|
|
3
3
|
import type { SuiteConfig, SuitePhase } from '../model/suite.ts';
|
|
4
|
-
import { AssertUtil } from '../assert/util.ts';
|
|
5
4
|
import { Barrier } from './barrier.ts';
|
|
6
|
-
import type { TestConfig, TestResult } from '../model/test.ts';
|
|
7
5
|
|
|
8
6
|
const TEST_PHASE_TIMEOUT = TimeUtil.duration(Env.TRV_TEST_PHASE_TIMEOUT.value ?? 15000, 'ms');
|
|
9
7
|
|
|
@@ -15,9 +13,11 @@ const TEST_PHASE_TIMEOUT = TimeUtil.duration(Env.TRV_TEST_PHASE_TIMEOUT.value ??
|
|
|
15
13
|
export class TestPhaseManager {
|
|
16
14
|
#progress: ('all' | 'each')[] = [];
|
|
17
15
|
#suite: SuiteConfig;
|
|
16
|
+
#instance: unknown;
|
|
18
17
|
|
|
19
|
-
constructor(suite: SuiteConfig) {
|
|
18
|
+
constructor(suite: SuiteConfig, instance: unknown) {
|
|
20
19
|
this.#suite = suite;
|
|
20
|
+
this.#instance = instance;
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
/**
|
|
@@ -31,12 +31,10 @@ export class TestPhaseManager {
|
|
|
31
31
|
}
|
|
32
32
|
|
|
33
33
|
// Ensure all the criteria below are satisfied before moving forward
|
|
34
|
-
error = await Barrier.awaitOperation(TEST_PHASE_TIMEOUT, async () => handler[phase]?.(this.#
|
|
34
|
+
error = await Barrier.awaitOperation(TEST_PHASE_TIMEOUT, async () => handler[phase]?.(this.#instance));
|
|
35
35
|
|
|
36
36
|
if (error) {
|
|
37
|
-
|
|
38
|
-
Object.assign(toThrow, { import: describeFunction(handler.constructor) ?? undefined });
|
|
39
|
-
throw toThrow;
|
|
37
|
+
throw error;
|
|
40
38
|
}
|
|
41
39
|
}
|
|
42
40
|
}
|
|
@@ -58,21 +56,13 @@ export class TestPhaseManager {
|
|
|
58
56
|
}
|
|
59
57
|
|
|
60
58
|
/**
|
|
61
|
-
*
|
|
59
|
+
* Handle an error during phase operation
|
|
62
60
|
*/
|
|
63
|
-
async
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
// Don't propagate our own errors
|
|
68
|
-
if (error.message === 'afterAll' || error.message === 'afterEach') {
|
|
69
|
-
return [];
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
if (test) {
|
|
73
|
-
return [AssertUtil.generateSuiteTestFailure({ suite, error, test })];
|
|
74
|
-
} else {
|
|
75
|
-
return AssertUtil.generateSuiteTestFailures(suite, error);
|
|
61
|
+
async onError(phase: 'all' | 'each', error: unknown): Promise<Error> {
|
|
62
|
+
if (!(error instanceof Error)) {
|
|
63
|
+
await this.endPhase(phase).catch(() => { });
|
|
64
|
+
throw error;
|
|
76
65
|
}
|
|
66
|
+
return error;
|
|
77
67
|
}
|
|
78
68
|
}
|
package/src/execute/run.ts
CHANGED
|
@@ -3,7 +3,7 @@ import fs from 'node:fs/promises';
|
|
|
3
3
|
import readline from 'node:readline/promises';
|
|
4
4
|
import path from 'node:path';
|
|
5
5
|
|
|
6
|
-
import { Env, ExecUtil, Util, RuntimeIndex, Runtime, TimeUtil, JSONUtil } from '@travetto/runtime';
|
|
6
|
+
import { Env, ExecUtil, Util, RuntimeIndex, Runtime, TimeUtil, JSONUtil, describeFunction } from '@travetto/runtime';
|
|
7
7
|
import { WorkPool } from '@travetto/worker';
|
|
8
8
|
import { Registry } from '@travetto/registry';
|
|
9
9
|
|
|
@@ -126,6 +126,7 @@ export class RunUtil {
|
|
|
126
126
|
const imported = await Registry.manualInit([importPath]);
|
|
127
127
|
const classes = Object.fromEntries(
|
|
128
128
|
imported
|
|
129
|
+
.filter(cls => !describeFunction(cls).abstract)
|
|
129
130
|
.filter(cls => SuiteRegistryIndex.hasConfig(cls))
|
|
130
131
|
.map(cls => [cls.Ⲑid, SuiteRegistryIndex.getConfig(cls)])
|
|
131
132
|
);
|
|
@@ -222,7 +223,7 @@ export class RunUtil {
|
|
|
222
223
|
}
|
|
223
224
|
|
|
224
225
|
if (runs.length === 1) {
|
|
225
|
-
await new TestExecutor(consumer).execute(runs[0]);
|
|
226
|
+
await new TestExecutor(consumer).execute(runs[0], true);
|
|
226
227
|
} else {
|
|
227
228
|
await WorkPool.run(
|
|
228
229
|
run => buildStandardTestManager(consumer, run),
|
package/src/model/suite.ts
CHANGED
|
@@ -19,10 +19,6 @@ export interface SuiteConfig extends SuiteCore {
|
|
|
19
19
|
* Should this be skipped
|
|
20
20
|
*/
|
|
21
21
|
skip: Skip;
|
|
22
|
-
/**
|
|
23
|
-
* Actual class instance
|
|
24
|
-
*/
|
|
25
|
-
instance?: unknown;
|
|
26
22
|
/**
|
|
27
23
|
* Tests to run
|
|
28
24
|
*/
|
|
@@ -34,29 +30,35 @@ export interface SuiteConfig extends SuiteCore {
|
|
|
34
30
|
}
|
|
35
31
|
|
|
36
32
|
/**
|
|
37
|
-
*
|
|
33
|
+
* Test Counts
|
|
38
34
|
*/
|
|
39
|
-
export interface
|
|
35
|
+
export interface ResultsSummary {
|
|
36
|
+
/** Passing Test Count */
|
|
40
37
|
passed: number;
|
|
38
|
+
/** Skipped Test Count */
|
|
41
39
|
skipped: number;
|
|
40
|
+
/** Failed Test Count */
|
|
42
41
|
failed: number;
|
|
42
|
+
/** Errored Test Count */
|
|
43
43
|
errored: number;
|
|
44
|
+
/** Unknown Test Count */
|
|
44
45
|
unknown: number;
|
|
46
|
+
/** Total Test Count */
|
|
45
47
|
total: number;
|
|
48
|
+
/** Test Self Execution Duration */
|
|
49
|
+
selfDuration: number;
|
|
50
|
+
/** Total Duration */
|
|
51
|
+
duration: number;
|
|
46
52
|
}
|
|
47
53
|
|
|
48
54
|
/**
|
|
49
55
|
* Results of a suite run
|
|
50
56
|
*/
|
|
51
|
-
export interface SuiteResult extends
|
|
57
|
+
export interface SuiteResult extends ResultsSummary, SuiteCore {
|
|
52
58
|
/**
|
|
53
59
|
* All test results
|
|
54
60
|
*/
|
|
55
61
|
tests: Record<string, TestResult>;
|
|
56
|
-
/**
|
|
57
|
-
* Suite duration
|
|
58
|
-
*/
|
|
59
|
-
duration: number;
|
|
60
62
|
/**
|
|
61
63
|
* Overall status
|
|
62
64
|
*/
|
package/src/model/test.ts
CHANGED
|
@@ -105,13 +105,13 @@ export interface TestResult extends TestCore {
|
|
|
105
105
|
*/
|
|
106
106
|
assertions: Assertion[];
|
|
107
107
|
/**
|
|
108
|
-
*
|
|
108
|
+
* Self Execution Duration
|
|
109
109
|
*/
|
|
110
|
-
|
|
110
|
+
selfDuration: number;
|
|
111
111
|
/**
|
|
112
112
|
* Total duration including before/after
|
|
113
113
|
*/
|
|
114
|
-
|
|
114
|
+
duration: number;
|
|
115
115
|
/**
|
|
116
116
|
* Logging output
|
|
117
117
|
*/
|
package/src/model/util.ts
CHANGED
|
@@ -1,14 +1,97 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
|
|
3
|
+
import { asFull, RuntimeIndex } from '@travetto/runtime';
|
|
4
|
+
|
|
5
|
+
import type { ResultsSummary, SuiteConfig, SuiteResult } from './suite.ts';
|
|
6
|
+
import type { TestConfig, TestResult, TestRun, TestStatus } from './test.ts';
|
|
3
7
|
|
|
4
8
|
export class TestModelUtil {
|
|
5
|
-
static
|
|
9
|
+
static computeTestStatus(summary: ResultsSummary): TestStatus {
|
|
6
10
|
switch (true) {
|
|
7
|
-
case
|
|
8
|
-
case
|
|
9
|
-
case
|
|
10
|
-
case
|
|
11
|
+
case summary.errored > 0: return 'errored';
|
|
12
|
+
case summary.failed > 0: return 'failed';
|
|
13
|
+
case summary.skipped > 0: return 'skipped';
|
|
14
|
+
case summary.unknown > 0: return 'unknown';
|
|
11
15
|
default: return 'passed';
|
|
12
16
|
}
|
|
13
17
|
}
|
|
18
|
+
|
|
19
|
+
static buildSummary(): ResultsSummary {
|
|
20
|
+
return { passed: 0, failed: 0, skipped: 0, errored: 0, unknown: 0, total: 0, duration: 0, selfDuration: 0 };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
static countTestResult<T extends ResultsSummary>(summary: T, tests: Pick<TestResult, 'status' | 'selfDuration' | 'duration'>[]): T {
|
|
24
|
+
for (const test of tests) {
|
|
25
|
+
summary[test.status] += 1;
|
|
26
|
+
summary.total += 1;
|
|
27
|
+
summary.selfDuration += (test.selfDuration ?? 0);
|
|
28
|
+
summary.duration += (test.duration ?? 0);
|
|
29
|
+
}
|
|
30
|
+
return summary;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* An empty suite result based on a suite config
|
|
36
|
+
*/
|
|
37
|
+
static createSuiteResult(suite: SuiteConfig, override?: Partial<SuiteResult>): SuiteResult {
|
|
38
|
+
return {
|
|
39
|
+
...TestModelUtil.buildSummary(),
|
|
40
|
+
status: 'unknown',
|
|
41
|
+
lineStart: suite.lineStart,
|
|
42
|
+
lineEnd: suite.lineEnd,
|
|
43
|
+
import: suite.import,
|
|
44
|
+
classId: suite.classId,
|
|
45
|
+
sourceHash: suite.sourceHash,
|
|
46
|
+
tests: {},
|
|
47
|
+
duration: 0,
|
|
48
|
+
selfDuration: 0,
|
|
49
|
+
...override
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* An empty test result based on a suite and test config
|
|
55
|
+
*/
|
|
56
|
+
static createTestResult(suite: SuiteConfig, test: TestConfig, override?: Partial<TestResult>): TestResult {
|
|
57
|
+
return {
|
|
58
|
+
methodName: test.methodName,
|
|
59
|
+
description: test.description,
|
|
60
|
+
classId: test.classId,
|
|
61
|
+
tags: test.tags,
|
|
62
|
+
suiteLineStart: suite.lineStart,
|
|
63
|
+
lineStart: test.lineStart,
|
|
64
|
+
lineEnd: test.lineEnd,
|
|
65
|
+
lineBodyStart: test.lineBodyStart,
|
|
66
|
+
import: test.import,
|
|
67
|
+
declarationImport: test.declarationImport,
|
|
68
|
+
sourceHash: test.sourceHash,
|
|
69
|
+
status: 'unknown',
|
|
70
|
+
assertions: [],
|
|
71
|
+
duration: 0,
|
|
72
|
+
selfDuration: 0,
|
|
73
|
+
output: [],
|
|
74
|
+
...override
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
static createImportErrorSuiteResult(run: TestRun): SuiteResult {
|
|
79
|
+
const name = path.basename(run.import);
|
|
80
|
+
const classId = `${RuntimeIndex.getFromImport(run.import)?.id}#${name}`;
|
|
81
|
+
const common = { classId, duration: 0, lineStart: 1, lineEnd: 1, import: run.import } as const;
|
|
82
|
+
return asFull<SuiteResult>({
|
|
83
|
+
...common,
|
|
84
|
+
status: 'errored', errored: 1,
|
|
85
|
+
tests: {
|
|
86
|
+
impport: asFull<TestResult>({
|
|
87
|
+
...common,
|
|
88
|
+
status: 'errored',
|
|
89
|
+
assertions: [{
|
|
90
|
+
...common, line: common.lineStart,
|
|
91
|
+
methodName: 'import', operator: 'import', text: `Failed to import ${run.import}`,
|
|
92
|
+
}]
|
|
93
|
+
})
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
}
|
|
14
97
|
}
|
package/src/worker/child.ts
CHANGED
|
@@ -20,9 +20,7 @@ export class TestChildWorker extends IpcChannel<TestRun> {
|
|
|
20
20
|
await operation();
|
|
21
21
|
this.send(type); // Respond
|
|
22
22
|
} catch (error) {
|
|
23
|
-
if (!(error instanceof Error)) {
|
|
24
|
-
throw error;
|
|
25
|
-
}
|
|
23
|
+
if (!(error instanceof Error)) { throw error; }
|
|
26
24
|
// Mark as errored out
|
|
27
25
|
this.send(type, JSONUtil.cloneForTransmit(error));
|
|
28
26
|
}
|