@travetto/test 8.0.0-alpha.2 → 8.0.0-alpha.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +24 -22
- package/__index__.ts +9 -9
- package/package.json +8 -8
- package/src/assert/capture.ts +1 -2
- package/src/assert/check.ts +56 -33
- package/src/assert/types.ts +1 -1
- package/src/assert/util.ts +26 -81
- package/src/consumer/decorator.ts +2 -2
- package/src/consumer/enhancer.ts +3 -3
- package/src/consumer/registry-adapter.ts +3 -3
- package/src/consumer/registry-index.ts +6 -5
- package/src/consumer/types/cumulative.ts +11 -23
- package/src/consumer/types/delegating.ts +8 -2
- package/src/consumer/types/event.ts +2 -2
- package/src/consumer/types/exec.ts +3 -4
- package/src/consumer/types/json.ts +3 -4
- package/src/consumer/types/noop.ts +3 -3
- package/src/consumer/types/runnable.ts +4 -5
- package/src/consumer/types/summarizer.ts +5 -18
- package/src/consumer/types/tap-summary.ts +39 -30
- package/src/consumer/types/tap.ts +64 -56
- package/src/consumer/types/util.ts +31 -0
- package/src/consumer/types/xunit.ts +10 -8
- package/src/consumer/types.ts +6 -10
- package/src/decorator/suite.ts +3 -3
- package/src/decorator/test.ts +6 -4
- package/src/execute/barrier.ts +15 -9
- package/src/execute/console.ts +3 -4
- package/src/execute/executor.ts +101 -157
- package/src/execute/phase.ts +12 -22
- package/src/execute/run.ts +41 -40
- package/src/execute/watcher.ts +12 -19
- package/src/fixture.ts +6 -7
- package/src/model/common.ts +1 -1
- package/src/model/error.ts +2 -2
- package/src/model/event.ts +14 -9
- package/src/model/suite.ts +15 -13
- package/src/model/test.ts +11 -8
- package/src/model/util.ts +102 -9
- package/src/registry/registry-adapter.ts +10 -13
- package/src/registry/registry-index.ts +13 -11
- package/src/worker/child.ts +10 -9
- package/src/worker/standard.ts +11 -14
- package/src/worker/types.ts +3 -7
- package/support/bin/run.ts +1 -1
- package/support/cli.test.ts +20 -15
- package/support/cli.test_child.ts +8 -3
- package/support/cli.test_diff.ts +11 -7
- package/support/cli.test_digest.ts +10 -4
- package/support/cli.test_direct.ts +11 -7
- package/support/cli.test_watch.ts +6 -4
- package/support/transformer.assert.ts +40 -32
package/src/decorator/suite.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { type Class, type ClassInstance, castTo, getClass } from '@travetto/runtime';
|
|
2
2
|
|
|
3
3
|
import type { SuiteConfig } from '../model/suite.ts';
|
|
4
4
|
import { SuiteRegistryIndex } from '../registry/registry-index.ts';
|
|
@@ -18,7 +18,7 @@ export function Suite(description?: string | Partial<SuiteConfig>, ...rest: Part
|
|
|
18
18
|
SuiteRegistryIndex.getForRegister(cls).register(
|
|
19
19
|
...(typeof description !== 'string' && description ? [description] : []),
|
|
20
20
|
...rest,
|
|
21
|
-
...(typeof description === 'string' ? [{ description }] : [])
|
|
21
|
+
...(typeof description === 'string' ? [{ description }] : [])
|
|
22
22
|
);
|
|
23
23
|
return cls;
|
|
24
24
|
};
|
|
@@ -76,4 +76,4 @@ export function AfterEach() {
|
|
|
76
76
|
});
|
|
77
77
|
return descriptor;
|
|
78
78
|
};
|
|
79
|
-
}
|
|
79
|
+
}
|
package/src/decorator/test.ts
CHANGED
|
@@ -27,10 +27,12 @@ export function Test(...rest: Partial<TestConfig>[]): MethodDecorator;
|
|
|
27
27
|
export function Test(description: string, ...rest: Partial<TestConfig>[]): MethodDecorator;
|
|
28
28
|
export function Test(description?: string | Partial<TestConfig>, ...rest: Partial<TestConfig>[]): MethodDecorator {
|
|
29
29
|
return (instance: ClassInstance, property: string, descriptor: PropertyDescriptor) => {
|
|
30
|
-
SuiteRegistryIndex.getForRegister(getClass(instance)).registerTest(
|
|
31
|
-
|
|
30
|
+
SuiteRegistryIndex.getForRegister(getClass(instance)).registerTest(
|
|
31
|
+
property,
|
|
32
|
+
descriptor.value,
|
|
33
|
+
...(typeof description !== 'string' && description ? [description] : []),
|
|
32
34
|
...rest,
|
|
33
|
-
...(typeof description === 'string'
|
|
35
|
+
...(typeof description === 'string' ? [{ description }] : [])
|
|
34
36
|
);
|
|
35
37
|
return descriptor;
|
|
36
38
|
};
|
|
@@ -58,4 +60,4 @@ export function Timeout(ms: number): MethodDecorator {
|
|
|
58
60
|
SuiteRegistryIndex.getForRegister(getClass(instance)).registerTest(property, descriptor.value, { timeout: ms });
|
|
59
61
|
return descriptor;
|
|
60
62
|
};
|
|
61
|
-
}
|
|
63
|
+
}
|
package/src/execute/barrier.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { isPromise } from 'node:util/types';
|
|
2
1
|
import { createHook, executionAsyncId } from 'node:async_hooks';
|
|
2
|
+
import { isPromise } from 'node:util/types';
|
|
3
3
|
|
|
4
4
|
import { type TimeSpan, TimeUtil, Util } from '@travetto/runtime';
|
|
5
5
|
|
|
@@ -11,7 +11,7 @@ export class Barrier {
|
|
|
11
11
|
/**
|
|
12
12
|
* Track timeout
|
|
13
13
|
*/
|
|
14
|
-
static timeout(duration: number | TimeSpan, operation: string = 'Operation'): { promise: Promise<void
|
|
14
|
+
static timeout(duration: number | TimeSpan, operation: string = 'Operation'): { promise: Promise<void>; resolve: () => unknown } {
|
|
15
15
|
const resolver = Promise.withResolvers<void>();
|
|
16
16
|
const durationMs = TimeUtil.duration(duration, 'ms');
|
|
17
17
|
let timeout: NodeJS.Timeout;
|
|
@@ -22,25 +22,31 @@ export class Barrier {
|
|
|
22
22
|
timeout = setTimeout(() => resolver.reject(new TimeoutError(msg)), durationMs).unref();
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
-
resolver.promise.finally(() => {
|
|
25
|
+
resolver.promise.finally(() => {
|
|
26
|
+
clearTimeout(timeout);
|
|
27
|
+
});
|
|
26
28
|
return resolver;
|
|
27
29
|
}
|
|
28
30
|
|
|
29
31
|
/**
|
|
30
32
|
* Track uncaught error
|
|
31
33
|
*/
|
|
32
|
-
static uncaughtErrorPromise(): { promise: Promise<void
|
|
34
|
+
static uncaughtErrorPromise(): { promise: Promise<void>; resolve: () => unknown } {
|
|
33
35
|
const uncaught = Promise.withResolvers<void>();
|
|
34
|
-
const onError = (error: Error): void => {
|
|
36
|
+
const onError = (error: Error): void => {
|
|
37
|
+
Util.queueMacroTask().then(() => uncaught.reject(error));
|
|
38
|
+
};
|
|
35
39
|
UNCAUGHT_ERR_EVENTS.map(key => process.on(key, onError));
|
|
36
|
-
uncaught.promise.finally(() => {
|
|
40
|
+
uncaught.promise.finally(() => {
|
|
41
|
+
UNCAUGHT_ERR_EVENTS.map(key => process.off(key, onError));
|
|
42
|
+
});
|
|
37
43
|
return uncaught;
|
|
38
44
|
}
|
|
39
45
|
|
|
40
46
|
/**
|
|
41
47
|
* Promise capturer
|
|
42
48
|
*/
|
|
43
|
-
static capturePromises(): { start: () => Promise<void
|
|
49
|
+
static capturePromises(): { start: () => Promise<void>; finish: () => Promise<void>; cleanup: () => void } {
|
|
44
50
|
const pending = new Map<number, Promise<unknown>>();
|
|
45
51
|
let id: number = 0;
|
|
46
52
|
|
|
@@ -90,7 +96,7 @@ export class Barrier {
|
|
|
90
96
|
let capturedError: Error | undefined;
|
|
91
97
|
const opProm = operation().then(() => promises.finish());
|
|
92
98
|
|
|
93
|
-
await Promise.race([opProm, uncaught.promise, timer.promise]).catch(error => capturedError ??= error);
|
|
99
|
+
await Promise.race([opProm, uncaught.promise, timer.promise]).catch(error => (capturedError ??= error));
|
|
94
100
|
|
|
95
101
|
return capturedError;
|
|
96
102
|
} finally {
|
|
@@ -99,4 +105,4 @@ export class Barrier {
|
|
|
99
105
|
uncaught.resolve();
|
|
100
106
|
}
|
|
101
107
|
}
|
|
102
|
-
}
|
|
108
|
+
}
|
package/src/execute/console.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import util from 'node:util';
|
|
2
2
|
|
|
3
3
|
import { type ConsoleEvent, type ConsoleListener, ConsoleManager } from '@travetto/runtime';
|
|
4
|
+
|
|
4
5
|
import type { TestLog } from '../model/test.ts';
|
|
5
6
|
|
|
6
7
|
/**
|
|
@@ -21,9 +22,7 @@ export class ConsoleCapture implements ConsoleListener {
|
|
|
21
22
|
log({ args, scope: _, ...rest }: ConsoleEvent): void {
|
|
22
23
|
this.out.push({
|
|
23
24
|
...rest,
|
|
24
|
-
message: args
|
|
25
|
-
.map((arg => typeof arg === 'string' ? arg : util.inspect(arg, false, 5)))
|
|
26
|
-
.join(' ')
|
|
25
|
+
message: args.map(arg => (typeof arg === 'string' ? arg : util.inspect(arg, false, 5))).join(' ')
|
|
27
26
|
});
|
|
28
27
|
}
|
|
29
28
|
|
|
@@ -33,4 +32,4 @@ export class ConsoleCapture implements ConsoleListener {
|
|
|
33
32
|
ConsoleManager.set(ConsoleCapture.#listener);
|
|
34
33
|
return result;
|
|
35
34
|
}
|
|
36
|
-
}
|
|
35
|
+
}
|
package/src/execute/executor.ts
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
|
-
import { Env, TimeUtil, Runtime, castTo, classConstruct } from '@travetto/runtime';
|
|
2
1
|
import { Registry } from '@travetto/registry';
|
|
2
|
+
import { castTo, classConstruct, Env, Runtime, TimeUtil } from '@travetto/runtime';
|
|
3
3
|
|
|
4
|
-
import type { TestConfig, TestResult, TestRun } from '../model/test.ts';
|
|
5
|
-
import type { SuiteConfig, SuiteResult } from '../model/suite.ts';
|
|
6
|
-
import type { TestConsumerShape } from '../consumer/types.ts';
|
|
7
|
-
import { AssertCheck } from '../assert/check.ts';
|
|
8
4
|
import { AssertCapture } from '../assert/capture.ts';
|
|
9
|
-
import {
|
|
10
|
-
import { TestPhaseManager } from './phase.ts';
|
|
5
|
+
import { AssertCheck } from '../assert/check.ts';
|
|
11
6
|
import { AssertUtil } from '../assert/util.ts';
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
7
|
+
import type { TestConsumerShape } from '../consumer/types.ts';
|
|
8
|
+
import type { SuiteConfig, SuiteResult } from '../model/suite.ts';
|
|
9
|
+
import type { TestConfig, TestResult, TestRun } from '../model/test.ts';
|
|
14
10
|
import { TestModelUtil } from '../model/util.ts';
|
|
11
|
+
import { SuiteRegistryIndex } from '../registry/registry-index.ts';
|
|
12
|
+
import { Barrier } from './barrier.ts';
|
|
13
|
+
import { ConsoleCapture } from './console.ts';
|
|
14
|
+
import { TestPhaseManager } from './phase.ts';
|
|
15
15
|
|
|
16
16
|
const TEST_TIMEOUT = TimeUtil.duration(Env.TRV_TEST_TIMEOUT.value || 5000, 'ms');
|
|
17
17
|
|
|
@@ -19,45 +19,24 @@ const TEST_TIMEOUT = TimeUtil.duration(Env.TRV_TEST_TIMEOUT.value || 5000, 'ms')
|
|
|
19
19
|
* Support execution of the tests
|
|
20
20
|
*/
|
|
21
21
|
export class TestExecutor {
|
|
22
|
-
|
|
23
22
|
#consumer: TestConsumerShape;
|
|
24
23
|
|
|
25
24
|
constructor(consumer: TestConsumerShape) {
|
|
26
25
|
this.#consumer = consumer;
|
|
27
26
|
}
|
|
28
27
|
|
|
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
28
|
/**
|
|
48
29
|
* Raw execution, runs the method and then returns any thrown errors as the result.
|
|
49
30
|
*
|
|
50
31
|
* This method should never throw under any circumstances.
|
|
51
32
|
*/
|
|
52
|
-
async #executeTestMethod(test: TestConfig): Promise<Error | undefined> {
|
|
53
|
-
const suite = SuiteRegistryIndex.getConfig(test.class);
|
|
54
|
-
|
|
33
|
+
async #executeTestMethod(instance: unknown, test: TestConfig): Promise<Error | undefined> {
|
|
55
34
|
// Ensure all the criteria below are satisfied before moving forward
|
|
56
35
|
return Barrier.awaitOperation(test.timeout || TEST_TIMEOUT, async () => {
|
|
57
36
|
const env = process.env;
|
|
58
37
|
process.env = { ...env }; // Created an isolated environment
|
|
59
38
|
try {
|
|
60
|
-
await castTo<Record<string, Function>>(
|
|
39
|
+
await castTo<Record<string, Function>>(instance)[test.methodName]();
|
|
61
40
|
} finally {
|
|
62
41
|
process.env = env; // Restore
|
|
63
42
|
}
|
|
@@ -73,96 +52,44 @@ export class TestExecutor {
|
|
|
73
52
|
}
|
|
74
53
|
}
|
|
75
54
|
|
|
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
55
|
/**
|
|
116
56
|
* Execute the test, capture output, assertions and promises
|
|
117
57
|
*/
|
|
118
|
-
async executeTest(test: TestConfig, suite: SuiteConfig): Promise<TestResult> {
|
|
58
|
+
async executeTest(instance: unknown, test: TestConfig, suite: SuiteConfig, override?: Partial<TestResult>): Promise<TestResult> {
|
|
59
|
+
const result = TestModelUtil.createTestResult(suite, test, override);
|
|
119
60
|
|
|
120
61
|
// Mark test start
|
|
121
62
|
this.#consumer.onEvent({ type: 'test', phase: 'before', test });
|
|
122
63
|
|
|
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
|
-
|
|
144
64
|
// 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
|
-
})
|
|
65
|
+
const getAssertions = AssertCapture.collector(test, item =>
|
|
66
|
+
this.#consumer.onEvent({ type: 'assertion', phase: 'after', assertion: item })
|
|
151
67
|
);
|
|
152
68
|
|
|
153
69
|
const consoleCapture = new ConsoleCapture().start(); // Capture all output from transpiled code
|
|
154
70
|
|
|
155
|
-
//
|
|
156
|
-
|
|
157
|
-
|
|
71
|
+
// Already finished
|
|
72
|
+
if (result.status !== 'unknown') {
|
|
73
|
+
if (result.error) {
|
|
74
|
+
result.assertions.push(AssertUtil.generateAssertion({ suite, test, error: result.error }));
|
|
75
|
+
}
|
|
76
|
+
for (const item of result.assertions ?? []) {
|
|
77
|
+
AssertCapture.add(item);
|
|
78
|
+
}
|
|
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 });
|
|
@@ -174,19 +101,22 @@ export class TestExecutor {
|
|
|
174
101
|
* Execute an entire suite
|
|
175
102
|
*/
|
|
176
103
|
async executeSuite(suite: SuiteConfig, tests: TestConfig[]): Promise<void> {
|
|
104
|
+
const instance = classConstruct(suite.class);
|
|
177
105
|
|
|
178
|
-
|
|
106
|
+
const shouldSkip = await this.#shouldSkip(suite, instance);
|
|
179
107
|
|
|
180
|
-
const
|
|
108
|
+
const result: SuiteResult = TestModelUtil.createSuiteResult(suite);
|
|
181
109
|
|
|
182
110
|
if (shouldSkip) {
|
|
183
111
|
this.#consumer.onEvent({
|
|
184
|
-
phase: 'after',
|
|
185
|
-
|
|
112
|
+
phase: 'after',
|
|
113
|
+
type: '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,82 @@ export class TestExecutor {
|
|
|
194
124
|
return;
|
|
195
125
|
}
|
|
196
126
|
|
|
197
|
-
const
|
|
198
|
-
const
|
|
199
|
-
const testConfigs = Object.fromEntries(
|
|
200
|
-
Object.entries(suite.tests).filter(([key]) => validTestMethodNames.has(key))
|
|
201
|
-
);
|
|
202
|
-
|
|
127
|
+
const manager = new TestPhaseManager(suite, instance);
|
|
128
|
+
const originalEnv = { ...process.env };
|
|
203
129
|
const startTime = Date.now();
|
|
130
|
+
const testResultOverrides: Record<string, Partial<TestResult>> = {};
|
|
131
|
+
|
|
132
|
+
const validTestMethodNames = new Set(tests.map(t => t.methodName));
|
|
133
|
+
const testConfigs = Object.fromEntries(Object.entries(suite.tests).filter(([key]) => validTestMethodNames.has(key)));
|
|
204
134
|
|
|
205
135
|
// Mark suite start
|
|
206
136
|
this.#consumer.onEvent({ phase: 'before', type: 'suite', suite: { ...suite, tests: testConfigs } });
|
|
207
137
|
|
|
208
|
-
const manager = new TestPhaseManager(suite);
|
|
209
|
-
|
|
210
|
-
const originalEnv = { ...process.env };
|
|
211
|
-
|
|
212
138
|
try {
|
|
213
139
|
// Handle the BeforeAll calls
|
|
214
140
|
await manager.startPhase('all');
|
|
141
|
+
} catch (someError) {
|
|
142
|
+
const suiteError = await manager.onError('all', someError);
|
|
143
|
+
for (const method of validTestMethodNames) {
|
|
144
|
+
testResultOverrides[method] ??= { status: 'errored', error: suiteError };
|
|
145
|
+
}
|
|
146
|
+
}
|
|
215
147
|
|
|
216
|
-
|
|
148
|
+
const suiteEnv = { ...process.env };
|
|
217
149
|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
continue;
|
|
222
|
-
}
|
|
150
|
+
for (const test of tests) {
|
|
151
|
+
// Reset env before each test
|
|
152
|
+
process.env = { ...suiteEnv };
|
|
223
153
|
|
|
224
|
-
|
|
225
|
-
|
|
154
|
+
const testStart = Date.now();
|
|
155
|
+
const testResultOverride = (testResultOverrides[test.methodName] ??= {});
|
|
226
156
|
|
|
227
|
-
|
|
228
|
-
|
|
157
|
+
if (await this.#shouldSkip(test, instance)) {
|
|
158
|
+
testResultOverride.status = 'skipped';
|
|
159
|
+
}
|
|
229
160
|
|
|
230
|
-
|
|
231
|
-
|
|
161
|
+
try {
|
|
162
|
+
// Handle BeforeEach
|
|
163
|
+
testResultOverride.status || (await manager.startPhase('each'));
|
|
164
|
+
} catch (someError) {
|
|
165
|
+
const testError = await manager.onError('each', someError);
|
|
166
|
+
testResultOverride.error = testError;
|
|
167
|
+
testResultOverride.status = 'errored';
|
|
168
|
+
}
|
|
232
169
|
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
result.tests[testResult.methodName] = testResult;
|
|
236
|
-
result[testResult.status]++;
|
|
237
|
-
result.total += 1;
|
|
170
|
+
// Run test
|
|
171
|
+
const testResult = await this.executeTest(instance, test, suite, testResultOverride);
|
|
238
172
|
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
173
|
+
// Handle after each
|
|
174
|
+
try {
|
|
175
|
+
testResultOverride.status || (await manager.endPhase('each'));
|
|
176
|
+
} catch (testError) {
|
|
177
|
+
if (!(testError instanceof Error)) {
|
|
178
|
+
throw testError;
|
|
245
179
|
}
|
|
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)) {
|
|
193
|
+
throw suiteError;
|
|
194
|
+
}
|
|
195
|
+
console.error('Failed to properly shutdown test', suiteError.message);
|
|
253
196
|
}
|
|
254
197
|
|
|
255
198
|
// Restore env
|
|
256
199
|
process.env = { ...originalEnv };
|
|
257
200
|
|
|
258
201
|
result.duration = Date.now() - startTime;
|
|
259
|
-
result.status = TestModelUtil.
|
|
202
|
+
result.status = TestModelUtil.computeTestStatus(result);
|
|
260
203
|
|
|
261
204
|
// Mark suite complete
|
|
262
205
|
this.#consumer.onEvent({ phase: 'after', type: 'suite', suite: result });
|
|
@@ -265,19 +208,15 @@ export class TestExecutor {
|
|
|
265
208
|
/**
|
|
266
209
|
* Handle executing a suite's test/tests based on command line inputs
|
|
267
210
|
*/
|
|
268
|
-
async execute(run: TestRun): Promise<void> {
|
|
211
|
+
async execute(run: TestRun, singleFile?: boolean): Promise<void> {
|
|
269
212
|
try {
|
|
270
213
|
await Runtime.importFrom(run.import);
|
|
271
214
|
} catch (error) {
|
|
272
215
|
if (!(error instanceof Error)) {
|
|
273
216
|
throw error;
|
|
274
217
|
}
|
|
218
|
+
const suite = TestModelUtil.createImportErrorSuiteResult(run);
|
|
275
219
|
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
220
|
this.#consumer.onEvent({ type: 'suite', phase: 'after', suite });
|
|
282
221
|
return;
|
|
283
222
|
}
|
|
@@ -291,8 +230,13 @@ export class TestExecutor {
|
|
|
291
230
|
console.warn('Unable to find suites for ', run);
|
|
292
231
|
}
|
|
293
232
|
|
|
233
|
+
if (singleFile) {
|
|
234
|
+
const testCount = suites.reduce((acc, suite) => acc + suite.tests.length, 0);
|
|
235
|
+
this.#consumer.onTestRunState?.({ testCount });
|
|
236
|
+
}
|
|
237
|
+
|
|
294
238
|
for (const { suite, tests } of suites) {
|
|
295
239
|
await this.executeSuite(suite, tests);
|
|
296
240
|
}
|
|
297
241
|
}
|
|
298
|
-
}
|
|
242
|
+
}
|
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
|
}
|