@travetto/test 7.1.4 → 8.0.0-alpha.1
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/__index__.ts +2 -1
- package/package.json +7 -7
- package/src/assert/capture.ts +1 -0
- package/src/assert/check.ts +26 -9
- package/src/assert/util.ts +73 -73
- package/src/consumer/types/cumulative.ts +9 -1
- package/src/consumer/types/event.ts +3 -2
- package/src/consumer/types/exec.ts +2 -2
- package/src/consumer/types/json.ts +3 -1
- package/src/consumer/types/summarizer.ts +2 -0
- package/src/consumer/types/tap-summary.ts +59 -56
- package/src/consumer/types/tap.ts +35 -17
- package/src/consumer/types/xunit.ts +12 -10
- package/src/decorator/suite.ts +13 -9
- package/src/execute/barrier.ts +3 -3
- package/src/execute/executor.ts +80 -64
- package/src/execute/phase.ts +26 -41
- package/src/execute/run.ts +6 -7
- package/src/execute/watcher.ts +1 -1
- package/src/model/common.ts +2 -2
- package/src/model/error.ts +11 -0
- package/src/model/suite.ts +10 -27
- package/src/model/test.ts +5 -1
- package/src/model/util.ts +7 -1
- package/src/registry/registry-adapter.ts +28 -24
- package/src/registry/registry-index.ts +3 -3
- package/src/worker/child.ts +2 -3
- package/src/worker/standard.ts +8 -9
- package/support/bin/run.ts +1 -18
- package/support/cli.test.ts +4 -7
- package/support/cli.test_diff.ts +6 -7
- package/support/cli.test_digest.ts +3 -3
- package/support/cli.test_direct.ts +3 -6
- package/support/cli.test_watch.ts +4 -6
- package/src/communication.ts +0 -66
- package/src/execute/error.ts +0 -11
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
2
|
import { stringify } from 'yaml';
|
|
3
3
|
|
|
4
|
-
import { Terminal } from '@travetto/terminal';
|
|
5
|
-
import { TimeUtil, RuntimeIndex, hasToJSON } from '@travetto/runtime';
|
|
4
|
+
import { Terminal, StyleUtil } from '@travetto/terminal';
|
|
5
|
+
import { TimeUtil, RuntimeIndex, hasToJSON, JSONUtil } from '@travetto/runtime';
|
|
6
6
|
|
|
7
7
|
import type { TestEvent } from '../../model/event.ts';
|
|
8
8
|
import type { SuitesSummary, TestConsumerShape } from '../types.ts';
|
|
@@ -41,15 +41,15 @@ export class TapEmitter implements TestConsumerShape {
|
|
|
41
41
|
*/
|
|
42
42
|
onStart(): void {
|
|
43
43
|
this.#start = Date.now();
|
|
44
|
-
this.log(this.#enhancer.suiteName('TAP version 14')
|
|
44
|
+
this.log(this.#enhancer.suiteName('TAP version 14'));
|
|
45
45
|
}
|
|
46
46
|
|
|
47
47
|
/**
|
|
48
48
|
* Output supplemental data (e.g. logs)
|
|
49
49
|
*/
|
|
50
|
-
logMeta(
|
|
50
|
+
logMeta(metadata: Record<string, unknown>): void {
|
|
51
51
|
const lineLength = this.#terminal.width - 5;
|
|
52
|
-
let body = stringify(
|
|
52
|
+
let body = stringify(metadata, { lineWidth: lineLength, indent: 2 });
|
|
53
53
|
body = body.split('\n').map(line => ` ${line}`).join('\n');
|
|
54
54
|
this.log(`---\n${this.#enhancer.objectInspect(body)}\n...`);
|
|
55
55
|
}
|
|
@@ -61,7 +61,7 @@ export class TapEmitter implements TestConsumerShape {
|
|
|
61
61
|
errorToString(error?: Error): string | undefined {
|
|
62
62
|
if (error && error.name !== 'AssertionError') {
|
|
63
63
|
if (error instanceof Error) {
|
|
64
|
-
let out =
|
|
64
|
+
let out = JSONUtil.toUTF8(hasToJSON(error) ? error.toJSON() : error, { indent: 2 });
|
|
65
65
|
if (this.#options?.verbose && error.stack) {
|
|
66
66
|
out = `${out}\n${error.stack}`;
|
|
67
67
|
}
|
|
@@ -79,23 +79,33 @@ export class TapEmitter implements TestConsumerShape {
|
|
|
79
79
|
if (event.type === 'test' && event.phase === 'after') {
|
|
80
80
|
const { test } = event;
|
|
81
81
|
const suiteId = this.#enhancer.suiteName(test.classId);
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
82
|
+
const suiteSourceFile = RuntimeIndex.getFromImport(test.import)!.sourceFile;
|
|
83
|
+
const testSourceFile = test.declarationImport ? RuntimeIndex.getFromImport(test.declarationImport)!.sourceFile : suiteSourceFile;
|
|
84
|
+
|
|
85
|
+
const header = [
|
|
86
|
+
StyleUtil.link(suiteId, `file://${suiteSourceFile}#${test.suiteLineStart ?? 1}`),
|
|
87
|
+
' - ',
|
|
88
|
+
StyleUtil.link(this.#enhancer.testName(test.methodName), `file://${testSourceFile}#${test.lineStart}`),
|
|
89
|
+
...test.description ? [`: ${this.#enhancer.testDescription(test.description)}`] : []
|
|
90
|
+
].join('');
|
|
91
|
+
|
|
86
92
|
this.log(`# ${header}`);
|
|
87
93
|
|
|
88
94
|
// Handle each assertion
|
|
89
95
|
if (test.assertions.length) {
|
|
90
96
|
let subCount = 0;
|
|
91
97
|
for (const asrt of test.assertions) {
|
|
98
|
+
const assertSourceFile = RuntimeIndex.getFromImport(asrt.import)!.sourceFile;
|
|
92
99
|
const text = asrt.message ? `${asrt.text} (${this.#enhancer.failure(asrt.message)})` : asrt.text;
|
|
93
|
-
const
|
|
100
|
+
const location = asrt.import ? `./${path.relative(process.cwd(), assertSourceFile)}` : '<unknown>';
|
|
94
101
|
let subMessage = [
|
|
95
102
|
this.#enhancer.assertNumber(++subCount),
|
|
96
103
|
'-',
|
|
97
104
|
this.#enhancer.assertDescription(text),
|
|
98
|
-
|
|
105
|
+
StyleUtil.link(
|
|
106
|
+
`${this.#enhancer.assertFile(location)}:${this.#enhancer.assertLine(asrt.line)}`,
|
|
107
|
+
`file://${assertSourceFile}#${asrt.line}`
|
|
108
|
+
)
|
|
99
109
|
].join(' ');
|
|
100
110
|
|
|
101
111
|
if (asrt.error) {
|
|
@@ -124,10 +134,16 @@ export class TapEmitter implements TestConsumerShape {
|
|
|
124
134
|
this.log(status);
|
|
125
135
|
|
|
126
136
|
// Handle error
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
137
|
+
switch (test.status) {
|
|
138
|
+
case 'errored':
|
|
139
|
+
case 'failed': {
|
|
140
|
+
if (test.error) {
|
|
141
|
+
const msg = this.errorToString(test.error);
|
|
142
|
+
if (msg) {
|
|
143
|
+
this.logMeta({ error: msg });
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
break;
|
|
131
147
|
}
|
|
132
148
|
}
|
|
133
149
|
|
|
@@ -162,13 +178,15 @@ export class TapEmitter implements TestConsumerShape {
|
|
|
162
178
|
}
|
|
163
179
|
}
|
|
164
180
|
|
|
165
|
-
const allPassed = summary.failed
|
|
181
|
+
const allPassed = !summary.failed && !summary.errored;
|
|
166
182
|
|
|
167
183
|
this.log([
|
|
168
184
|
this.#enhancer[allPassed ? 'success' : 'failure']('Results'),
|
|
169
185
|
`${this.#enhancer.total(summary.passed)}/${this.#enhancer.total(summary.total)},`,
|
|
170
186
|
allPassed ? 'failed' : this.#enhancer.failure('failed'),
|
|
171
187
|
`${this.#enhancer.total(summary.failed)}`,
|
|
188
|
+
allPassed ? 'errored' : this.#enhancer.failure('errored'),
|
|
189
|
+
`${this.#enhancer.total(summary.errored)}`,
|
|
172
190
|
'skipped',
|
|
173
191
|
this.#enhancer.total(summary.skipped),
|
|
174
192
|
`# (Total Test Time: ${TimeUtil.asClock(summary.duration)}, Total Run Time: ${TimeUtil.asClock(Date.now() - this.#start)})`
|
|
@@ -24,18 +24,18 @@ export class XunitEmitter implements TestConsumerShape {
|
|
|
24
24
|
/**
|
|
25
25
|
* Process metadata information (e.g. logs)
|
|
26
26
|
*/
|
|
27
|
-
buildMeta(
|
|
28
|
-
if (!
|
|
27
|
+
buildMeta(metadata: Record<string, unknown>): string {
|
|
28
|
+
if (!metadata) {
|
|
29
29
|
return '';
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
-
for (const key of Object.keys(
|
|
33
|
-
if (!
|
|
34
|
-
delete
|
|
32
|
+
for (const key of Object.keys(metadata)) {
|
|
33
|
+
if (!metadata[key]) {
|
|
34
|
+
delete metadata[key];
|
|
35
35
|
}
|
|
36
36
|
}
|
|
37
|
-
if (Object.keys(
|
|
38
|
-
let body = stringify(
|
|
37
|
+
if (Object.keys(metadata).length) {
|
|
38
|
+
let body = stringify(metadata);
|
|
39
39
|
body = body.split('\n').map(line => ` ${line}`).join('\n');
|
|
40
40
|
return `<![CDATA[\n${body}\n]]>`;
|
|
41
41
|
} else {
|
|
@@ -60,7 +60,8 @@ export class XunitEmitter implements TestConsumerShape {
|
|
|
60
60
|
|
|
61
61
|
if (test.error) {
|
|
62
62
|
const assertion = test.assertions.find(item => !!item.error)!;
|
|
63
|
-
|
|
63
|
+
const node = test.status === 'failed' ? 'failure' : 'error';
|
|
64
|
+
body = `<${node} type="${assertion.text}" message="${encodeURIComponent(assertion.message!)}"><![CDATA[${assertion.error!.stack}]]></${node}>`;
|
|
64
65
|
}
|
|
65
66
|
|
|
66
67
|
const groupedByLevel: Record<string, string[]> = {};
|
|
@@ -90,7 +91,7 @@ export class XunitEmitter implements TestConsumerShape {
|
|
|
90
91
|
time="${suite.duration}"
|
|
91
92
|
tests="${suite.total}"
|
|
92
93
|
failures="${suite.failed}"
|
|
93
|
-
errors="${suite.
|
|
94
|
+
errors="${suite.errored}"
|
|
94
95
|
skipped="${suite.skipped}"
|
|
95
96
|
file="${RuntimeIndex.getFromImport(suite.import)!.sourceFile}"
|
|
96
97
|
>
|
|
@@ -112,7 +113,8 @@ export class XunitEmitter implements TestConsumerShape {
|
|
|
112
113
|
time="${summary.duration}"
|
|
113
114
|
tests="${summary.total}"
|
|
114
115
|
failures="${summary.failed}"
|
|
115
|
-
errors="${summary.
|
|
116
|
+
errors="${summary.errored}"
|
|
117
|
+
skipped="${summary.skipped}"
|
|
116
118
|
>
|
|
117
119
|
${this.#suites.join('\n')}
|
|
118
120
|
</testsuites>
|
package/src/decorator/suite.ts
CHANGED
|
@@ -1,10 +1,8 @@
|
|
|
1
|
-
import { castTo, type Class, type ClassInstance,
|
|
1
|
+
import { castTo, type Class, type ClassInstance, getClass } from '@travetto/runtime';
|
|
2
2
|
|
|
3
3
|
import type { SuiteConfig } from '../model/suite.ts';
|
|
4
4
|
import { SuiteRegistryIndex } from '../registry/registry-index.ts';
|
|
5
5
|
|
|
6
|
-
export type SuitePhase = 'beforeAll' | 'beforeEach' | 'afterAll' | 'afterEach';
|
|
7
|
-
|
|
8
6
|
/**
|
|
9
7
|
* Register a class to be defined as a test suite, and a candidate for testing
|
|
10
8
|
* @param description The Suite description
|
|
@@ -17,11 +15,9 @@ export function Suite(...rest: Partial<SuiteConfig>[]): ClassDecorator;
|
|
|
17
15
|
export function Suite(description: string, ...rest: Partial<SuiteConfig>[]): ClassDecorator;
|
|
18
16
|
export function Suite(description?: string | Partial<SuiteConfig>, ...rest: Partial<SuiteConfig>[]): ClassDecorator {
|
|
19
17
|
const decorator = (cls: Class): typeof cls => {
|
|
20
|
-
const isAbstract = describeFunction(cls).abstract;
|
|
21
18
|
SuiteRegistryIndex.getForRegister(cls).register(
|
|
22
19
|
...(typeof description !== 'string' && description ? [description] : []),
|
|
23
20
|
...rest,
|
|
24
|
-
...isAbstract ? [{ skip: true }] : [],
|
|
25
21
|
...(typeof description === 'string' ? [{ description }] : []),
|
|
26
22
|
);
|
|
27
23
|
return cls;
|
|
@@ -36,7 +32,9 @@ export function Suite(description?: string | Partial<SuiteConfig>, ...rest: Part
|
|
|
36
32
|
*/
|
|
37
33
|
export function BeforeAll() {
|
|
38
34
|
return (instance: ClassInstance, property: string, descriptor: PropertyDescriptor): PropertyDescriptor => {
|
|
39
|
-
SuiteRegistryIndex.getForRegister(getClass(instance)).register({
|
|
35
|
+
SuiteRegistryIndex.getForRegister(getClass(instance)).register({
|
|
36
|
+
phaseHandlers: [{ beforeAll: (realInstance: unknown): unknown => descriptor.value.call(realInstance) }]
|
|
37
|
+
});
|
|
40
38
|
return descriptor;
|
|
41
39
|
};
|
|
42
40
|
}
|
|
@@ -47,7 +45,9 @@ export function BeforeAll() {
|
|
|
47
45
|
*/
|
|
48
46
|
export function BeforeEach() {
|
|
49
47
|
return (instance: ClassInstance, property: string, descriptor: PropertyDescriptor): PropertyDescriptor => {
|
|
50
|
-
SuiteRegistryIndex.getForRegister(getClass(instance)).register({
|
|
48
|
+
SuiteRegistryIndex.getForRegister(getClass(instance)).register({
|
|
49
|
+
phaseHandlers: [{ beforeEach: (realInstance: unknown): unknown => descriptor.value.call(realInstance) }]
|
|
50
|
+
});
|
|
51
51
|
return descriptor;
|
|
52
52
|
};
|
|
53
53
|
}
|
|
@@ -58,7 +58,9 @@ export function BeforeEach() {
|
|
|
58
58
|
*/
|
|
59
59
|
export function AfterAll() {
|
|
60
60
|
return (instance: ClassInstance, property: string, descriptor: PropertyDescriptor): PropertyDescriptor => {
|
|
61
|
-
SuiteRegistryIndex.getForRegister(getClass(instance)).register({
|
|
61
|
+
SuiteRegistryIndex.getForRegister(getClass(instance)).register({
|
|
62
|
+
phaseHandlers: [{ afterAll: (realInstance: unknown): unknown => descriptor.value.call(realInstance) }]
|
|
63
|
+
});
|
|
62
64
|
return descriptor;
|
|
63
65
|
};
|
|
64
66
|
}
|
|
@@ -69,7 +71,9 @@ export function AfterAll() {
|
|
|
69
71
|
*/
|
|
70
72
|
export function AfterEach() {
|
|
71
73
|
return (instance: ClassInstance, property: string, descriptor: PropertyDescriptor): PropertyDescriptor => {
|
|
72
|
-
SuiteRegistryIndex.getForRegister(getClass(instance)).register({
|
|
74
|
+
SuiteRegistryIndex.getForRegister(getClass(instance)).register({
|
|
75
|
+
phaseHandlers: [{ afterEach: (realInstance: unknown): unknown => descriptor.value.call(realInstance) }]
|
|
76
|
+
});
|
|
73
77
|
return descriptor;
|
|
74
78
|
};
|
|
75
79
|
}
|
package/src/execute/barrier.ts
CHANGED
|
@@ -3,7 +3,7 @@ import { createHook, executionAsyncId } from 'node:async_hooks';
|
|
|
3
3
|
|
|
4
4
|
import { type TimeSpan, TimeUtil, Util } from '@travetto/runtime';
|
|
5
5
|
|
|
6
|
-
import {
|
|
6
|
+
import { TestExecutionError, TimeoutError } from '../model/error.ts';
|
|
7
7
|
|
|
8
8
|
const UNCAUGHT_ERR_EVENTS = ['unhandledRejection', 'uncaughtException'] as const;
|
|
9
9
|
|
|
@@ -13,7 +13,7 @@ export class Barrier {
|
|
|
13
13
|
*/
|
|
14
14
|
static timeout(duration: number | TimeSpan, operation: string = 'Operation'): { promise: Promise<void>, resolve: () => unknown } {
|
|
15
15
|
const resolver = Promise.withResolvers<void>();
|
|
16
|
-
const durationMs = TimeUtil.
|
|
16
|
+
const durationMs = TimeUtil.duration(duration, 'ms');
|
|
17
17
|
let timeout: NodeJS.Timeout;
|
|
18
18
|
if (!durationMs) {
|
|
19
19
|
resolver.resolve();
|
|
@@ -67,7 +67,7 @@ export class Barrier {
|
|
|
67
67
|
await Util.queueMacroTask();
|
|
68
68
|
i -= 1;
|
|
69
69
|
if (i === 0) {
|
|
70
|
-
throw new
|
|
70
|
+
throw new TestExecutionError(`Pending promises: ${pending.size}`);
|
|
71
71
|
}
|
|
72
72
|
}
|
|
73
73
|
},
|
package/src/execute/executor.ts
CHANGED
|
@@ -1,10 +1,8 @@
|
|
|
1
|
-
import { AssertionError } from 'node:assert';
|
|
2
|
-
|
|
3
1
|
import { Env, TimeUtil, Runtime, castTo, classConstruct } from '@travetto/runtime';
|
|
4
2
|
import { Registry } from '@travetto/registry';
|
|
5
3
|
|
|
6
4
|
import type { TestConfig, TestResult, TestRun } from '../model/test.ts';
|
|
7
|
-
import type { SuiteConfig,
|
|
5
|
+
import type { SuiteConfig, SuiteResult } from '../model/suite.ts';
|
|
8
6
|
import type { TestConsumerShape } from '../consumer/types.ts';
|
|
9
7
|
import { AssertCheck } from '../assert/check.ts';
|
|
10
8
|
import { AssertCapture } from '../assert/capture.ts';
|
|
@@ -12,11 +10,10 @@ import { ConsoleCapture } from './console.ts';
|
|
|
12
10
|
import { TestPhaseManager } from './phase.ts';
|
|
13
11
|
import { AssertUtil } from '../assert/util.ts';
|
|
14
12
|
import { Barrier } from './barrier.ts';
|
|
15
|
-
import { ExecutionError } from './error.ts';
|
|
16
13
|
import { SuiteRegistryIndex } from '../registry/registry-index.ts';
|
|
17
14
|
import { TestModelUtil } from '../model/util.ts';
|
|
18
15
|
|
|
19
|
-
const TEST_TIMEOUT = TimeUtil.
|
|
16
|
+
const TEST_TIMEOUT = TimeUtil.duration(Env.TRV_TEST_TIMEOUT.value || 5000, 'ms');
|
|
20
17
|
|
|
21
18
|
/**
|
|
22
19
|
* Support execution of the tests
|
|
@@ -29,25 +26,21 @@ export class TestExecutor {
|
|
|
29
26
|
this.#consumer = consumer;
|
|
30
27
|
}
|
|
31
28
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
*/
|
|
37
|
-
#onSuiteFailure(failure: SuiteFailure, triggerSuite?: boolean): void {
|
|
38
|
-
if (triggerSuite) {
|
|
39
|
-
this.#consumer.onEvent({ type: 'suite', phase: 'before', suite: failure.suite });
|
|
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 });
|
|
40
33
|
}
|
|
34
|
+
this.#consumer.onEvent({ type: 'test', phase: 'after', test: result });
|
|
35
|
+
}
|
|
41
36
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
suite: { ...castTo(failure.suite), failed: 1, passed: 0, total: 1, skipped: 0 }
|
|
50
|
-
});
|
|
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
|
+
}
|
|
51
44
|
}
|
|
52
45
|
}
|
|
53
46
|
|
|
@@ -83,17 +76,27 @@ export class TestExecutor {
|
|
|
83
76
|
#skipTest(test: TestConfig, result: SuiteResult): void {
|
|
84
77
|
// Mark test start
|
|
85
78
|
this.#consumer.onEvent({ type: 'test', phase: 'before', test });
|
|
86
|
-
result.skipped
|
|
87
|
-
|
|
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
|
+
});
|
|
88
90
|
}
|
|
89
91
|
|
|
90
92
|
/**
|
|
91
93
|
* An empty suite result based on a suite config
|
|
92
94
|
*/
|
|
93
|
-
createSuiteResult(suite: SuiteConfig): SuiteResult {
|
|
95
|
+
createSuiteResult(suite: SuiteConfig, override?: Partial<SuiteResult>): SuiteResult {
|
|
94
96
|
return {
|
|
95
97
|
passed: 0,
|
|
96
98
|
failed: 0,
|
|
99
|
+
errored: 0,
|
|
97
100
|
skipped: 0,
|
|
98
101
|
unknown: 0,
|
|
99
102
|
total: 0,
|
|
@@ -104,14 +107,15 @@ export class TestExecutor {
|
|
|
104
107
|
classId: suite.classId,
|
|
105
108
|
sourceHash: suite.sourceHash,
|
|
106
109
|
duration: 0,
|
|
107
|
-
tests: {}
|
|
110
|
+
tests: {},
|
|
111
|
+
...override
|
|
108
112
|
};
|
|
109
113
|
}
|
|
110
114
|
|
|
111
115
|
/**
|
|
112
116
|
* Execute the test, capture output, assertions and promises
|
|
113
117
|
*/
|
|
114
|
-
async executeTest(test: TestConfig): Promise<TestResult> {
|
|
118
|
+
async executeTest(test: TestConfig, suite: SuiteConfig): Promise<TestResult> {
|
|
115
119
|
|
|
116
120
|
// Mark test start
|
|
117
121
|
this.#consumer.onEvent({ type: 'test', phase: 'before', test });
|
|
@@ -123,11 +127,12 @@ export class TestExecutor {
|
|
|
123
127
|
description: test.description,
|
|
124
128
|
classId: test.classId,
|
|
125
129
|
tags: test.tags,
|
|
130
|
+
suiteLineStart: suite.lineStart,
|
|
126
131
|
lineStart: test.lineStart,
|
|
127
132
|
lineEnd: test.lineEnd,
|
|
128
133
|
lineBodyStart: test.lineBodyStart,
|
|
129
134
|
import: test.import,
|
|
130
|
-
|
|
135
|
+
declarationImport: test.declarationImport,
|
|
131
136
|
sourceHash: test.sourceHash,
|
|
132
137
|
status: 'unknown',
|
|
133
138
|
assertions: [],
|
|
@@ -148,28 +153,15 @@ export class TestExecutor {
|
|
|
148
153
|
const consoleCapture = new ConsoleCapture().start(); // Capture all output from transpiled code
|
|
149
154
|
|
|
150
155
|
// Run method and get result
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
if (!error) {
|
|
154
|
-
error = AssertCheck.checkError(test.shouldThrow, error); // Rewrite error
|
|
155
|
-
} else {
|
|
156
|
-
if (error instanceof AssertionError) {
|
|
157
|
-
// Pass, do nothing
|
|
158
|
-
} else if (error instanceof ExecutionError) { // Errors that are not expected
|
|
159
|
-
AssertCheck.checkUnhandled(test, error);
|
|
160
|
-
} else if (test.shouldThrow) {
|
|
161
|
-
error = AssertCheck.checkError(test.shouldThrow, error); // Rewrite error
|
|
162
|
-
} else if (error instanceof Error) {
|
|
163
|
-
AssertCheck.checkUnhandled(test, error);
|
|
164
|
-
}
|
|
165
|
-
}
|
|
156
|
+
const error = await this.#executeTestMethod(test);
|
|
157
|
+
const [status, finalError] = AssertCheck.validateTestResultError(test, error);
|
|
166
158
|
|
|
167
159
|
Object.assign(result, {
|
|
168
|
-
status
|
|
160
|
+
status,
|
|
169
161
|
output: consoleCapture.end(),
|
|
170
162
|
assertions: getAssertions(),
|
|
171
163
|
duration: Date.now() - startTime,
|
|
172
|
-
...(
|
|
164
|
+
...(finalError ? { error: finalError } : {})
|
|
173
165
|
});
|
|
174
166
|
|
|
175
167
|
// Mark completion
|
|
@@ -185,7 +177,20 @@ export class TestExecutor {
|
|
|
185
177
|
|
|
186
178
|
suite.instance = classConstruct(suite.class);
|
|
187
179
|
|
|
188
|
-
|
|
180
|
+
const shouldSkip = await this.#shouldSkip(suite, suite.instance);
|
|
181
|
+
|
|
182
|
+
if (shouldSkip) {
|
|
183
|
+
this.#consumer.onEvent({
|
|
184
|
+
phase: 'after', type: 'suite',
|
|
185
|
+
suite: this.createSuiteResult(suite, {
|
|
186
|
+
status: 'skipped',
|
|
187
|
+
skipped: tests.length,
|
|
188
|
+
total: tests.length
|
|
189
|
+
})
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if (shouldSkip || !tests.length) {
|
|
189
194
|
return;
|
|
190
195
|
}
|
|
191
196
|
|
|
@@ -200,7 +205,7 @@ export class TestExecutor {
|
|
|
200
205
|
// Mark suite start
|
|
201
206
|
this.#consumer.onEvent({ phase: 'before', type: 'suite', suite: { ...suite, tests: testConfigs } });
|
|
202
207
|
|
|
203
|
-
const manager = new TestPhaseManager(suite
|
|
208
|
+
const manager = new TestPhaseManager(suite);
|
|
204
209
|
|
|
205
210
|
const originalEnv = { ...process.env };
|
|
206
211
|
|
|
@@ -220,31 +225,37 @@ export class TestExecutor {
|
|
|
220
225
|
process.env = { ...suiteEnv };
|
|
221
226
|
|
|
222
227
|
const testStart = Date.now();
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
228
|
+
try {
|
|
229
|
+
|
|
230
|
+
// Handle BeforeEach
|
|
231
|
+
await manager.startPhase('each');
|
|
232
|
+
|
|
233
|
+
// Run test
|
|
234
|
+
const testResult = await this.executeTest(test, suite);
|
|
235
|
+
result.tests[testResult.methodName] = testResult;
|
|
236
|
+
result[testResult.status]++;
|
|
237
|
+
result.total += 1;
|
|
238
|
+
|
|
239
|
+
// Handle after each
|
|
240
|
+
await manager.endPhase('each');
|
|
241
|
+
testResult.durationTotal = Date.now() - testStart;
|
|
242
|
+
} catch (testError) {
|
|
243
|
+
const errors = await manager.errorPhase('each', testError, suite, test);
|
|
244
|
+
this.#recordSuiteErrors(suite, result, errors);
|
|
245
|
+
}
|
|
235
246
|
}
|
|
236
247
|
|
|
237
248
|
// Handle after all
|
|
238
249
|
await manager.endPhase('all');
|
|
239
|
-
} catch (
|
|
240
|
-
await manager.
|
|
250
|
+
} catch (suiteError) {
|
|
251
|
+
const errors = await manager.errorPhase('all', suiteError, suite);
|
|
252
|
+
this.#recordSuiteErrors(suite, result, errors);
|
|
241
253
|
}
|
|
242
254
|
|
|
243
255
|
// Restore env
|
|
244
256
|
process.env = { ...originalEnv };
|
|
245
257
|
|
|
246
258
|
result.duration = Date.now() - startTime;
|
|
247
|
-
result.total = result.passed + result.failed + result.skipped;
|
|
248
259
|
result.status = TestModelUtil.countsToTestStatus(result);
|
|
249
260
|
|
|
250
261
|
// Mark suite complete
|
|
@@ -262,12 +273,17 @@ export class TestExecutor {
|
|
|
262
273
|
throw error;
|
|
263
274
|
}
|
|
264
275
|
console.error(error);
|
|
265
|
-
|
|
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
|
+
this.#consumer.onEvent({ type: 'suite', phase: 'after', suite });
|
|
266
282
|
return;
|
|
267
283
|
}
|
|
268
284
|
|
|
269
285
|
// Initialize registry (after loading the above)
|
|
270
|
-
|
|
286
|
+
Registry.finalizeForIndex(SuiteRegistryIndex);
|
|
271
287
|
|
|
272
288
|
// Convert inbound arguments to specific tests to run
|
|
273
289
|
const suites = SuiteRegistryIndex.getSuiteTests(run);
|
package/src/execute/phase.ts
CHANGED
|
@@ -1,14 +1,11 @@
|
|
|
1
|
-
import { Env, TimeUtil } from '@travetto/runtime';
|
|
1
|
+
import { describeFunction, Env, TimeUtil } from '@travetto/runtime';
|
|
2
2
|
|
|
3
|
-
import type { SuiteConfig,
|
|
3
|
+
import type { SuiteConfig, SuitePhase } from '../model/suite.ts';
|
|
4
4
|
import { AssertUtil } from '../assert/util.ts';
|
|
5
5
|
import { Barrier } from './barrier.ts';
|
|
6
|
+
import type { TestConfig, TestResult } from '../model/test.ts';
|
|
6
7
|
|
|
7
|
-
|
|
8
|
-
source?: Error;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
const TEST_PHASE_TIMEOUT = TimeUtil.fromValue(Env.TRV_TEST_PHASE_TIMEOUT.value) ?? 15000;
|
|
8
|
+
const TEST_PHASE_TIMEOUT = TimeUtil.duration(Env.TRV_TEST_PHASE_TIMEOUT.value ?? 15000, 'ms');
|
|
12
9
|
|
|
13
10
|
/**
|
|
14
11
|
* Test Phase Execution Manager.
|
|
@@ -18,34 +15,30 @@ const TEST_PHASE_TIMEOUT = TimeUtil.fromValue(Env.TRV_TEST_PHASE_TIMEOUT.value)
|
|
|
18
15
|
export class TestPhaseManager {
|
|
19
16
|
#progress: ('all' | 'each')[] = [];
|
|
20
17
|
#suite: SuiteConfig;
|
|
21
|
-
#result: SuiteResult;
|
|
22
|
-
#onSuiteFailure: (fail: SuiteFailure) => void;
|
|
23
18
|
|
|
24
|
-
constructor(suite: SuiteConfig
|
|
19
|
+
constructor(suite: SuiteConfig) {
|
|
25
20
|
this.#suite = suite;
|
|
26
|
-
this.#result = result;
|
|
27
|
-
this.#onSuiteFailure = onSuiteFailure;
|
|
28
21
|
}
|
|
29
22
|
|
|
30
23
|
/**
|
|
31
24
|
* Run a distinct phase of the test execution
|
|
32
25
|
*/
|
|
33
|
-
async runPhase(phase:
|
|
26
|
+
async runPhase(phase: SuitePhase): Promise<void> {
|
|
34
27
|
let error: Error | undefined;
|
|
35
|
-
for (const
|
|
28
|
+
for (const handler of this.#suite.phaseHandlers) {
|
|
29
|
+
if (!handler[phase]) {
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
36
32
|
|
|
37
33
|
// Ensure all the criteria below are satisfied before moving forward
|
|
38
|
-
error = await Barrier.awaitOperation(TEST_PHASE_TIMEOUT, async () =>
|
|
34
|
+
error = await Barrier.awaitOperation(TEST_PHASE_TIMEOUT, async () => handler[phase]?.(this.#suite.instance));
|
|
39
35
|
|
|
40
36
|
if (error) {
|
|
41
|
-
|
|
37
|
+
const toThrow = new Error(phase, { cause: error });
|
|
38
|
+
Object.assign(toThrow, { import: describeFunction(handler.constructor) ?? undefined });
|
|
39
|
+
throw toThrow;
|
|
42
40
|
}
|
|
43
41
|
}
|
|
44
|
-
if (error) {
|
|
45
|
-
const tbo = new TestBreakout(`[[${phase}]]`);
|
|
46
|
-
tbo.source = error;
|
|
47
|
-
throw tbo;
|
|
48
|
-
}
|
|
49
42
|
}
|
|
50
43
|
|
|
51
44
|
/**
|
|
@@ -65,29 +58,21 @@ export class TestPhaseManager {
|
|
|
65
58
|
}
|
|
66
59
|
|
|
67
60
|
/**
|
|
68
|
-
*
|
|
61
|
+
* Handles if an error occurs during a phase, ensuring that we attempt to end the phase and then return the appropriate test results for the failure
|
|
69
62
|
*/
|
|
70
|
-
async
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
}
|
|
63
|
+
async errorPhase(phase: 'all' | 'each', error: unknown, suite: SuiteConfig, test?: TestConfig): Promise<TestResult[]> {
|
|
64
|
+
try { await this.endPhase(phase); } catch { }
|
|
65
|
+
if (!(error instanceof Error)) { throw error; }
|
|
74
66
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
} catch { /* Do nothing */ }
|
|
67
|
+
// Don't propagate our own errors
|
|
68
|
+
if (error.message === 'afterAll' || error.message === 'afterEach') {
|
|
69
|
+
return [];
|
|
79
70
|
}
|
|
80
71
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
error instanceof TestBreakout ? error.source! : error
|
|
87
|
-
);
|
|
88
|
-
|
|
89
|
-
this.#onSuiteFailure(failure);
|
|
90
|
-
this.#result.tests[failure.testResult.methodName] = failure.testResult;
|
|
91
|
-
this.#result.failed++;
|
|
72
|
+
if (test) {
|
|
73
|
+
return [AssertUtil.generateSuiteTestFailure({ suite, error, test })];
|
|
74
|
+
} else {
|
|
75
|
+
return AssertUtil.generateSuiteTestFailures(suite, error);
|
|
76
|
+
}
|
|
92
77
|
}
|
|
93
78
|
}
|
package/src/execute/run.ts
CHANGED
|
@@ -65,7 +65,7 @@ export class RunUtil {
|
|
|
65
65
|
}
|
|
66
66
|
}
|
|
67
67
|
} else {
|
|
68
|
-
for
|
|
68
|
+
for (const match of all) {
|
|
69
69
|
if (await this.isTestFile(match.sourceFile)) {
|
|
70
70
|
yield match.import;
|
|
71
71
|
}
|
|
@@ -97,13 +97,12 @@ export class RunUtil {
|
|
|
97
97
|
) :
|
|
98
98
|
((): boolean => true);
|
|
99
99
|
|
|
100
|
-
const parsed: TestConfig[] = JSONUtil.
|
|
100
|
+
const parsed: TestConfig[] = JSONUtil.fromUTF8(digestProcess.stdout);
|
|
101
101
|
|
|
102
102
|
const events = parsed.filter(testFilter).reduce((runs, test) => {
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
runs.get(test.classId)!.methodNames!.push(test.methodName);
|
|
103
|
+
runs.getOrInsert(test.classId,
|
|
104
|
+
{ import: test.import, classId: test.classId, methodNames: [], runId: Util.uuid(), metadata }
|
|
105
|
+
).methodNames!.push(test.methodName);
|
|
107
106
|
return runs;
|
|
108
107
|
}, new Map<string, TestRun>());
|
|
109
108
|
|
|
@@ -229,7 +228,7 @@ export class RunUtil {
|
|
|
229
228
|
run => buildStandardTestManager(consumer, run),
|
|
230
229
|
runs,
|
|
231
230
|
{
|
|
232
|
-
idleTimeoutMillis: TimeUtil.
|
|
231
|
+
idleTimeoutMillis: TimeUtil.duration('10s', 'ms'),
|
|
233
232
|
min: 1,
|
|
234
233
|
max: consumerConfig.concurrency
|
|
235
234
|
}
|