@travetto/test 8.0.0-alpha.0 → 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 +19 -80
- 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/decorator/suite.ts +1 -3
- package/src/execute/executor.ts +97 -143
- 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/registry/registry-adapter.ts +20 -5
- package/src/worker/child.ts +1 -3
- package/support/bin/run.ts +1 -18
- package/support/cli.test.ts +4 -7
- package/support/cli.test_diff.ts +3 -6
- package/support/cli.test_direct.ts +3 -6
- package/support/cli.test_watch.ts +4 -6
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,95 +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): 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
|
-
};
|
|
112
|
-
}
|
|
113
|
-
|
|
114
56
|
/**
|
|
115
57
|
* Execute the test, capture output, assertions and promises
|
|
116
58
|
*/
|
|
117
|
-
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);
|
|
118
62
|
|
|
119
63
|
// Mark test start
|
|
120
64
|
this.#consumer.onEvent({ type: 'test', phase: 'before', test });
|
|
121
65
|
|
|
122
|
-
const startTime = Date.now();
|
|
123
|
-
|
|
124
|
-
const result: TestResult = {
|
|
125
|
-
methodName: test.methodName,
|
|
126
|
-
description: test.description,
|
|
127
|
-
classId: test.classId,
|
|
128
|
-
tags: test.tags,
|
|
129
|
-
suiteLineStart: suite.lineStart,
|
|
130
|
-
lineStart: test.lineStart,
|
|
131
|
-
lineEnd: test.lineEnd,
|
|
132
|
-
lineBodyStart: test.lineBodyStart,
|
|
133
|
-
import: test.import,
|
|
134
|
-
declarationImport: test.declarationImport,
|
|
135
|
-
sourceHash: test.sourceHash,
|
|
136
|
-
status: 'unknown',
|
|
137
|
-
assertions: [],
|
|
138
|
-
duration: 0,
|
|
139
|
-
durationTotal: 0,
|
|
140
|
-
output: [],
|
|
141
|
-
};
|
|
142
66
|
|
|
143
67
|
// Emit every assertion as it occurs
|
|
144
|
-
const getAssertions = AssertCapture.collector(test,
|
|
145
|
-
this.#consumer.onEvent({
|
|
146
|
-
type: 'assertion',
|
|
147
|
-
phase: 'after',
|
|
148
|
-
assertion: asrt
|
|
149
|
-
})
|
|
150
|
-
);
|
|
68
|
+
const getAssertions = AssertCapture.collector(test, item =>
|
|
69
|
+
this.#consumer.onEvent({ type: 'assertion', phase: 'after', assertion: item }));
|
|
151
70
|
|
|
152
71
|
const consoleCapture = new ConsoleCapture().start(); // Capture all output from transpiled code
|
|
153
72
|
|
|
154
|
-
//
|
|
155
|
-
|
|
156
|
-
|
|
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
|
+
}
|
|
157
90
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
output: consoleCapture.end(),
|
|
161
|
-
assertions: getAssertions(),
|
|
162
|
-
duration: Date.now() - startTime,
|
|
163
|
-
...(finalError ? { error: finalError } : {})
|
|
164
|
-
});
|
|
91
|
+
result.output = consoleCapture.end();
|
|
92
|
+
result.assertions = getAssertions();
|
|
165
93
|
|
|
166
94
|
// Mark completion
|
|
167
95
|
this.#consumer.onEvent({ type: 'test', phase: 'after', test: result });
|
|
@@ -174,75 +102,102 @@ export class TestExecutor {
|
|
|
174
102
|
*/
|
|
175
103
|
async executeSuite(suite: SuiteConfig, tests: TestConfig[]): Promise<void> {
|
|
176
104
|
|
|
177
|
-
|
|
105
|
+
const instance = classConstruct(suite.class);
|
|
178
106
|
|
|
179
|
-
|
|
107
|
+
const shouldSkip = await this.#shouldSkip(suite, instance);
|
|
108
|
+
|
|
109
|
+
const result: SuiteResult = TestModelUtil.createSuiteResult(suite);
|
|
110
|
+
|
|
111
|
+
if (shouldSkip) {
|
|
112
|
+
this.#consumer.onEvent({
|
|
113
|
+
phase: 'after', type: 'suite',
|
|
114
|
+
suite: {
|
|
115
|
+
...result,
|
|
116
|
+
status: 'skipped',
|
|
117
|
+
skipped: tests.length,
|
|
118
|
+
total: tests.length
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (shouldSkip || !tests.length) {
|
|
180
124
|
return;
|
|
181
125
|
}
|
|
182
126
|
|
|
183
|
-
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
|
+
|
|
184
132
|
const validTestMethodNames = new Set(tests.map(t => t.methodName));
|
|
185
133
|
const testConfigs = Object.fromEntries(
|
|
186
134
|
Object.entries(suite.tests).filter(([key]) => validTestMethodNames.has(key))
|
|
187
135
|
);
|
|
188
136
|
|
|
189
|
-
const startTime = Date.now();
|
|
190
|
-
|
|
191
137
|
// Mark suite start
|
|
192
138
|
this.#consumer.onEvent({ phase: 'before', type: 'suite', suite: { ...suite, tests: testConfigs } });
|
|
193
139
|
|
|
194
|
-
const manager = new TestPhaseManager(suite);
|
|
195
|
-
|
|
196
|
-
const originalEnv = { ...process.env };
|
|
197
|
-
|
|
198
140
|
try {
|
|
199
141
|
// Handle the BeforeAll calls
|
|
200
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
|
+
}
|
|
201
149
|
|
|
202
|
-
|
|
150
|
+
const suiteEnv = { ...process.env };
|
|
203
151
|
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
continue;
|
|
208
|
-
}
|
|
152
|
+
for (const test of tests) {
|
|
153
|
+
// Reset env before each test
|
|
154
|
+
process.env = { ...suiteEnv };
|
|
209
155
|
|
|
210
|
-
|
|
211
|
-
|
|
156
|
+
const testStart = Date.now();
|
|
157
|
+
const testResultOverride = (testResultOverrides[test.methodName] ??= {});
|
|
212
158
|
|
|
213
|
-
|
|
214
|
-
|
|
159
|
+
if (await this.#shouldSkip(test, instance)) {
|
|
160
|
+
testResultOverride.status = 'skipped';
|
|
161
|
+
}
|
|
215
162
|
|
|
216
|
-
|
|
217
|
-
|
|
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
|
+
}
|
|
218
171
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
result.tests[testResult.methodName] = testResult;
|
|
222
|
-
result[testResult.status]++;
|
|
223
|
-
result.total += 1;
|
|
172
|
+
// Run test
|
|
173
|
+
const testResult = await this.executeTest(instance, test, suite, testResultOverride);
|
|
224
174
|
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
}
|
|
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);
|
|
232
181
|
}
|
|
233
182
|
|
|
183
|
+
result.tests[testResult.methodName] = testResult;
|
|
184
|
+
testResult.duration = Date.now() - testStart;
|
|
185
|
+
TestModelUtil.countTestResult(result, [testResult]);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
try {
|
|
234
189
|
// Handle after all
|
|
235
190
|
await manager.endPhase('all');
|
|
236
191
|
} catch (suiteError) {
|
|
237
|
-
|
|
238
|
-
|
|
192
|
+
if (!(suiteError instanceof Error)) { throw suiteError; };
|
|
193
|
+
console.error('Failed to properly shutdown test', suiteError.message);
|
|
239
194
|
}
|
|
240
195
|
|
|
241
196
|
// Restore env
|
|
242
197
|
process.env = { ...originalEnv };
|
|
243
198
|
|
|
244
199
|
result.duration = Date.now() - startTime;
|
|
245
|
-
result.status = TestModelUtil.
|
|
200
|
+
result.status = TestModelUtil.computeTestStatus(result);
|
|
246
201
|
|
|
247
202
|
// Mark suite complete
|
|
248
203
|
this.#consumer.onEvent({ phase: 'after', type: 'suite', suite: result });
|
|
@@ -251,19 +206,13 @@ export class TestExecutor {
|
|
|
251
206
|
/**
|
|
252
207
|
* Handle executing a suite's test/tests based on command line inputs
|
|
253
208
|
*/
|
|
254
|
-
async execute(run: TestRun): Promise<void> {
|
|
209
|
+
async execute(run: TestRun, singleFile?: boolean): Promise<void> {
|
|
255
210
|
try {
|
|
256
211
|
await Runtime.importFrom(run.import);
|
|
257
212
|
} catch (error) {
|
|
258
|
-
if (!(error instanceof Error)) {
|
|
259
|
-
|
|
260
|
-
}
|
|
213
|
+
if (!(error instanceof Error)) { throw error; }
|
|
214
|
+
const suite = TestModelUtil.createImportErrorSuiteResult(run);
|
|
261
215
|
console.error(error);
|
|
262
|
-
|
|
263
|
-
// Fire import failure as a test failure for each test in the suite
|
|
264
|
-
const { result, test, suite } = AssertUtil.gernerateImportFailure(run.import, error);
|
|
265
|
-
this.#consumer.onEvent({ type: 'suite', phase: 'before', suite });
|
|
266
|
-
this.#onSuiteTestError(result, test);
|
|
267
216
|
this.#consumer.onEvent({ type: 'suite', phase: 'after', suite });
|
|
268
217
|
return;
|
|
269
218
|
}
|
|
@@ -277,6 +226,11 @@ export class TestExecutor {
|
|
|
277
226
|
console.warn('Unable to find suites for ', run);
|
|
278
227
|
}
|
|
279
228
|
|
|
229
|
+
if (singleFile) {
|
|
230
|
+
const testCount = suites.reduce((acc, suite) => acc + suite.tests.length, 0);
|
|
231
|
+
this.#consumer.onTestRunState?.({ testCount });
|
|
232
|
+
}
|
|
233
|
+
|
|
280
234
|
for (const { suite, tests } of suites) {
|
|
281
235
|
await this.executeSuite(suite, tests);
|
|
282
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
|
}
|
|
@@ -8,7 +8,7 @@ import type { TestConfig } from '../model/test.ts';
|
|
|
8
8
|
function combineClasses(baseConfig: SuiteConfig, ...subConfig: Partial<SuiteConfig>[]): SuiteConfig {
|
|
9
9
|
for (const config of subConfig) {
|
|
10
10
|
if (config.tags) {
|
|
11
|
-
baseConfig.tags = [...baseConfig.tags ?? [], ...config.tags];
|
|
11
|
+
baseConfig.tags = [...new Set([...baseConfig.tags ?? [], ...config.tags])];
|
|
12
12
|
}
|
|
13
13
|
baseConfig.skip = config.skip ?? baseConfig.skip;
|
|
14
14
|
|
|
@@ -30,6 +30,21 @@ function combineClasses(baseConfig: SuiteConfig, ...subConfig: Partial<SuiteConf
|
|
|
30
30
|
return baseConfig;
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
+
function combineWithParent(baseConfig: SuiteConfig, parentConfig: SuiteConfig): SuiteConfig {
|
|
34
|
+
baseConfig.tags = [...parentConfig.tags ?? [], ...baseConfig.tags ?? []];
|
|
35
|
+
baseConfig.skip = baseConfig.skip ?? parentConfig.skip;
|
|
36
|
+
baseConfig.phaseHandlers = [...(parentConfig.phaseHandlers ?? []), ...(baseConfig.phaseHandlers ?? [])];
|
|
37
|
+
for (const [key, test] of Object.entries(parentConfig.tests ?? {})) {
|
|
38
|
+
baseConfig.tests[key] = {
|
|
39
|
+
...test,
|
|
40
|
+
class: baseConfig.class,
|
|
41
|
+
classId: baseConfig.classId,
|
|
42
|
+
import: baseConfig.import,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
return baseConfig;
|
|
46
|
+
}
|
|
47
|
+
|
|
33
48
|
function combineMethods(suite: SuiteConfig, baseConfig: TestConfig, ...subConfig: Partial<TestConfig>[]): TestConfig {
|
|
34
49
|
baseConfig.classId = suite.classId;
|
|
35
50
|
baseConfig.import = suite.import;
|
|
@@ -54,12 +69,12 @@ export class SuiteRegistryAdapter implements RegistryAdapter<SuiteConfig> {
|
|
|
54
69
|
|
|
55
70
|
register(...data: Partial<SuiteConfig>[]): SuiteConfig {
|
|
56
71
|
if (!this.#config) {
|
|
57
|
-
const { lines, hash } = describeFunction(this.#cls) ?? {};
|
|
72
|
+
const { lines, hash, abstract: isAbstract } = describeFunction(this.#cls) ?? {};
|
|
58
73
|
this.#config = asFull<SuiteConfig>({
|
|
59
74
|
class: this.#cls,
|
|
60
75
|
classId: this.#cls.Ⲑid,
|
|
61
76
|
tags: [],
|
|
62
|
-
skip:
|
|
77
|
+
skip: isAbstract,
|
|
63
78
|
import: Runtime.getImport(this.#cls),
|
|
64
79
|
lineStart: lines?.[0],
|
|
65
80
|
lineEnd: lines?.[1],
|
|
@@ -99,11 +114,11 @@ export class SuiteRegistryAdapter implements RegistryAdapter<SuiteConfig> {
|
|
|
99
114
|
|
|
100
115
|
finalize(parent?: SuiteConfig): void {
|
|
101
116
|
if (parent) {
|
|
102
|
-
|
|
117
|
+
combineWithParent(this.#config, parent);
|
|
103
118
|
}
|
|
104
119
|
|
|
105
120
|
for (const test of Object.values(this.#config.tests)) {
|
|
106
|
-
test.tags = [...test.tags ?? [], ...this.#config.tags ?? []];
|
|
121
|
+
test.tags = [...new Set([...test.tags ?? [], ...this.#config.tags ?? []])];
|
|
107
122
|
test.description ||= SchemaRegistryIndex.get(this.#cls).getMethod(test.methodName).description;
|
|
108
123
|
}
|
|
109
124
|
}
|
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
|
}
|