@travetto/test 5.0.0-rc.8 → 5.0.0
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 +2 -1
- package/package.json +8 -8
- package/src/assert/capture.ts +3 -3
- package/src/assert/check.ts +23 -25
- package/src/assert/util.ts +21 -9
- package/src/consumer/registry.ts +2 -3
- package/src/consumer/{error.ts → serialize.ts} +12 -21
- package/src/consumer/types/cumulative.ts +9 -17
- package/src/consumer/types/delegating.ts +58 -0
- package/src/consumer/types/event.ts +2 -4
- package/src/consumer/types/execution.ts +2 -4
- package/src/consumer/types/runnable.ts +12 -41
- package/src/consumer/types/tap-streamed.ts +9 -6
- package/src/consumer/types/tap.ts +2 -2
- package/src/decorator/suite.ts +4 -5
- package/src/execute/executor.ts +82 -95
- package/src/execute/phase.ts +19 -29
- package/src/execute/promise.ts +3 -3
- package/src/execute/runner.ts +29 -20
- package/src/execute/types.ts +12 -10
- package/src/execute/util.ts +30 -5
- package/src/execute/watcher.ts +32 -36
- package/src/model/common.ts +9 -1
- package/src/model/event.ts +9 -5
- package/src/model/suite.ts +11 -1
- package/src/model/test.ts +27 -1
- package/src/registry/suite.ts +28 -24
- package/src/trv.d.ts +4 -0
- package/src/worker/child.ts +9 -15
- package/src/worker/standard.ts +17 -22
- package/src/worker/types.ts +13 -23
- package/support/cli.test.ts +18 -4
- package/support/{cli.test_count.ts → cli.test_digest.ts} +14 -6
- package/support/cli.test_direct.ts +10 -3
- package/support/transformer.assert.ts +10 -10
package/src/execute/executor.ts
CHANGED
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
import { AssertionError } from 'node:assert';
|
|
2
|
-
import path from 'node:path';
|
|
3
2
|
|
|
4
|
-
import { Env, TimeUtil, Runtime,
|
|
3
|
+
import { Env, TimeUtil, Runtime, castTo } from '@travetto/runtime';
|
|
5
4
|
import { Barrier, ExecutionError } from '@travetto/worker';
|
|
6
5
|
|
|
7
6
|
import { SuiteRegistry } from '../registry/suite';
|
|
8
|
-
import { TestConfig, TestResult } from '../model/test';
|
|
9
|
-
import { SuiteConfig, SuiteResult } from '../model/suite';
|
|
7
|
+
import { TestConfig, TestResult, TestRun } from '../model/test';
|
|
8
|
+
import { SuiteConfig, SuiteFailure, SuiteResult } from '../model/suite';
|
|
10
9
|
import { TestConsumer } from '../consumer/types';
|
|
11
10
|
import { AssertCheck } from '../assert/check';
|
|
12
11
|
import { AssertCapture } from '../assert/capture';
|
|
@@ -22,12 +21,42 @@ const TEST_TIMEOUT = TimeUtil.fromValue(Env.TRV_TEST_TIMEOUT.val) ?? 5000;
|
|
|
22
21
|
*/
|
|
23
22
|
export class TestExecutor {
|
|
24
23
|
|
|
24
|
+
#consumer: TestConsumer;
|
|
25
|
+
#testFilter: (config: TestConfig) => boolean;
|
|
26
|
+
|
|
27
|
+
constructor(consumer: TestConsumer, testFilter?: (config: TestConfig) => boolean) {
|
|
28
|
+
this.#consumer = consumer;
|
|
29
|
+
this.#testFilter = testFilter || ((): boolean => true);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Handles communicating a suite-level error
|
|
34
|
+
* @param failure
|
|
35
|
+
* @param withSuite
|
|
36
|
+
*/
|
|
37
|
+
#onSuiteFailure(failure: SuiteFailure, triggerSuite?: boolean): void {
|
|
38
|
+
if (triggerSuite) {
|
|
39
|
+
this.#consumer.onEvent({ type: 'suite', phase: 'before', suite: failure.suite });
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
this.#consumer.onEvent({ type: 'test', phase: 'before', test: failure.test });
|
|
43
|
+
this.#consumer.onEvent({ type: 'assertion', phase: 'after', assertion: failure.assert });
|
|
44
|
+
this.#consumer.onEvent({ type: 'test', phase: 'after', test: failure.testResult });
|
|
45
|
+
|
|
46
|
+
if (triggerSuite) {
|
|
47
|
+
this.#consumer.onEvent({
|
|
48
|
+
type: 'suite', phase: 'after',
|
|
49
|
+
suite: { ...castTo(failure.suite), failed: 1, passed: 0, total: 1, skipped: 0 }
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
25
54
|
/**
|
|
26
55
|
* Raw execution, runs the method and then returns any thrown errors as the result.
|
|
27
56
|
*
|
|
28
57
|
* This method should never throw under any circumstances.
|
|
29
58
|
*/
|
|
30
|
-
|
|
59
|
+
async #executeTestMethod(test: TestConfig): Promise<Error | undefined> {
|
|
31
60
|
const suite = SuiteRegistry.get(test.class);
|
|
32
61
|
|
|
33
62
|
// Ensure all the criteria below are satisfied before moving forward
|
|
@@ -39,8 +68,7 @@ export class TestExecutor {
|
|
|
39
68
|
|
|
40
69
|
try {
|
|
41
70
|
await pCap.run(() =>
|
|
42
|
-
|
|
43
|
-
(suite.instance as Record<string, Function>)[test.methodName]()
|
|
71
|
+
castTo<Record<string, Function>>(suite.instance)[test.methodName]()
|
|
44
72
|
);
|
|
45
73
|
} finally {
|
|
46
74
|
process.env = env; // Restore
|
|
@@ -54,35 +82,27 @@ export class TestExecutor {
|
|
|
54
82
|
/**
|
|
55
83
|
* Determining if we should skip
|
|
56
84
|
*/
|
|
57
|
-
|
|
58
|
-
if (cfg
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
85
|
+
async #shouldSkip(cfg: TestConfig | SuiteConfig, inst: unknown): Promise<boolean | undefined> {
|
|
86
|
+
if ('methodName' in cfg && !this.#testFilter(cfg)) {
|
|
87
|
+
return true;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (typeof cfg.skip === 'function' ? await cfg.skip(inst) : cfg.skip) {
|
|
91
|
+
return true;
|
|
62
92
|
}
|
|
63
93
|
}
|
|
64
94
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
const classId = `${RuntimeIndex.getFromImport(imp)?.id}○${name}`;
|
|
71
|
-
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
72
|
-
const suite = { class: { name }, classId, duration: 0, lineStart: 1, lineEnd: 1, import: imp, } as SuiteConfig & SuiteResult;
|
|
73
|
-
err.message = err.message.replaceAll(Runtime.mainSourcePath, '.');
|
|
74
|
-
const res = AssertUtil.generateSuiteError(suite, 'require', err);
|
|
75
|
-
consumer.onEvent({ type: 'suite', phase: 'before', suite });
|
|
76
|
-
consumer.onEvent({ type: 'test', phase: 'before', test: res.testConfig });
|
|
77
|
-
consumer.onEvent({ type: 'assertion', phase: 'after', assertion: res.assert });
|
|
78
|
-
consumer.onEvent({ type: 'test', phase: 'after', test: res.testResult });
|
|
79
|
-
consumer.onEvent({ type: 'suite', phase: 'after', suite: { ...suite, failed: 1, passed: 0, total: 1, skipped: 0 } });
|
|
95
|
+
#skipTest(test: TestConfig, result: SuiteResult): void {
|
|
96
|
+
// Mark test start
|
|
97
|
+
this.#consumer.onEvent({ type: 'test', phase: 'before', test });
|
|
98
|
+
result.skipped++;
|
|
99
|
+
this.#consumer.onEvent({ type: 'test', phase: 'after', test: { ...test, assertions: [], duration: 0, durationTotal: 0, output: {}, status: 'skipped' } });
|
|
80
100
|
}
|
|
81
101
|
|
|
82
102
|
/**
|
|
83
103
|
* An empty suite result based on a suite config
|
|
84
104
|
*/
|
|
85
|
-
|
|
105
|
+
createSuiteResult(suite: SuiteConfig): SuiteResult {
|
|
86
106
|
return {
|
|
87
107
|
passed: 0,
|
|
88
108
|
failed: 0,
|
|
@@ -100,10 +120,10 @@ export class TestExecutor {
|
|
|
100
120
|
/**
|
|
101
121
|
* Execute the test, capture output, assertions and promises
|
|
102
122
|
*/
|
|
103
|
-
|
|
123
|
+
async executeTest(test: TestConfig): Promise<TestResult> {
|
|
104
124
|
|
|
105
125
|
// Mark test start
|
|
106
|
-
consumer.onEvent({ type: 'test', phase: 'before', test });
|
|
126
|
+
this.#consumer.onEvent({ type: 'test', phase: 'before', test });
|
|
107
127
|
|
|
108
128
|
const startTime = Date.now();
|
|
109
129
|
|
|
@@ -115,6 +135,7 @@ export class TestExecutor {
|
|
|
115
135
|
lineEnd: test.lineEnd,
|
|
116
136
|
lineBodyStart: test.lineBodyStart,
|
|
117
137
|
import: test.import,
|
|
138
|
+
sourceImport: test.sourceImport,
|
|
118
139
|
status: 'skipped',
|
|
119
140
|
assertions: [],
|
|
120
141
|
duration: 0,
|
|
@@ -122,16 +143,12 @@ export class TestExecutor {
|
|
|
122
143
|
output: {},
|
|
123
144
|
};
|
|
124
145
|
|
|
125
|
-
if (await this.#skip(test, suite.instance)) {
|
|
126
|
-
return result;
|
|
127
|
-
}
|
|
128
|
-
|
|
129
146
|
// Emit every assertion as it occurs
|
|
130
|
-
const getAssertions = AssertCapture.collector(test,
|
|
131
|
-
consumer.onEvent({
|
|
147
|
+
const getAssertions = AssertCapture.collector(test, asrt =>
|
|
148
|
+
this.#consumer.onEvent({
|
|
132
149
|
type: 'assertion',
|
|
133
150
|
phase: 'after',
|
|
134
|
-
assertion:
|
|
151
|
+
assertion: asrt
|
|
135
152
|
})
|
|
136
153
|
);
|
|
137
154
|
|
|
@@ -163,47 +180,27 @@ export class TestExecutor {
|
|
|
163
180
|
});
|
|
164
181
|
|
|
165
182
|
// Mark completion
|
|
166
|
-
consumer.onEvent({ type: 'test', phase: 'after', test: result });
|
|
183
|
+
this.#consumer.onEvent({ type: 'test', phase: 'after', test: result });
|
|
167
184
|
|
|
168
185
|
return result;
|
|
169
186
|
}
|
|
170
187
|
|
|
171
188
|
/**
|
|
172
|
-
* Execute
|
|
189
|
+
* Execute an entire suite
|
|
173
190
|
*/
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
const mgr = new TestPhaseManager(consumer, suite, result);
|
|
178
|
-
|
|
179
|
-
try {
|
|
180
|
-
await mgr.startPhase('all');
|
|
181
|
-
const skip = await this.#skip(test, suite.instance);
|
|
182
|
-
if (!skip) {
|
|
183
|
-
await mgr.startPhase('each');
|
|
184
|
-
}
|
|
185
|
-
await this.executeTest(consumer, test, suite);
|
|
186
|
-
if (!skip) {
|
|
187
|
-
await mgr.endPhase('each');
|
|
188
|
-
}
|
|
189
|
-
await mgr.endPhase('all');
|
|
190
|
-
} catch (err) {
|
|
191
|
-
await mgr.onError(err);
|
|
191
|
+
async executeSuite(suite: SuiteConfig, tests: TestConfig[]): Promise<void> {
|
|
192
|
+
if (!tests.length || await this.#shouldSkip(suite, suite.instance)) {
|
|
193
|
+
return;
|
|
192
194
|
}
|
|
193
|
-
}
|
|
194
195
|
|
|
195
|
-
/**
|
|
196
|
-
* Execute an entire suite
|
|
197
|
-
*/
|
|
198
|
-
static async executeSuite(consumer: TestConsumer, suite: SuiteConfig): Promise<SuiteResult> {
|
|
199
196
|
const result: SuiteResult = this.createSuiteResult(suite);
|
|
200
197
|
|
|
201
198
|
const startTime = Date.now();
|
|
202
199
|
|
|
203
200
|
// Mark suite start
|
|
204
|
-
consumer.onEvent({ phase: 'before', type: 'suite', suite });
|
|
201
|
+
this.#consumer.onEvent({ phase: 'before', type: 'suite', suite });
|
|
205
202
|
|
|
206
|
-
const mgr = new TestPhaseManager(
|
|
203
|
+
const mgr = new TestPhaseManager(suite, result, e => this.#onSuiteFailure(e));
|
|
207
204
|
|
|
208
205
|
const originalEnv = { ...process.env };
|
|
209
206
|
|
|
@@ -213,28 +210,30 @@ export class TestExecutor {
|
|
|
213
210
|
|
|
214
211
|
const suiteEnv = { ...process.env };
|
|
215
212
|
|
|
216
|
-
for (const test of suite.tests) {
|
|
213
|
+
for (const test of tests ?? suite.tests) {
|
|
214
|
+
if (await this.#shouldSkip(test, suite.instance)) {
|
|
215
|
+
this.#skipTest(test, result);
|
|
216
|
+
continue;
|
|
217
|
+
}
|
|
218
|
+
|
|
217
219
|
// Reset env before each test
|
|
218
220
|
process.env = { ...suiteEnv };
|
|
221
|
+
|
|
219
222
|
const testStart = Date.now();
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
await mgr.startPhase('each');
|
|
224
|
-
}
|
|
223
|
+
|
|
224
|
+
// Handle BeforeEach
|
|
225
|
+
await mgr.startPhase('each');
|
|
225
226
|
|
|
226
227
|
// Run test
|
|
227
|
-
const ret = await this.executeTest(
|
|
228
|
+
const ret = await this.executeTest(test);
|
|
228
229
|
result[ret.status]++;
|
|
229
|
-
|
|
230
|
-
if (!skip) {
|
|
231
|
-
result.tests.push(ret);
|
|
232
|
-
}
|
|
230
|
+
result.tests.push(ret);
|
|
233
231
|
|
|
234
232
|
// Handle after each
|
|
235
233
|
await mgr.endPhase('each');
|
|
236
234
|
ret.durationTotal = Date.now() - testStart;
|
|
237
235
|
}
|
|
236
|
+
|
|
238
237
|
// Handle after all
|
|
239
238
|
await mgr.endPhase('all');
|
|
240
239
|
} catch (err) {
|
|
@@ -245,26 +244,23 @@ export class TestExecutor {
|
|
|
245
244
|
process.env = { ...originalEnv };
|
|
246
245
|
|
|
247
246
|
result.duration = Date.now() - startTime;
|
|
248
|
-
|
|
249
|
-
// Mark suite complete
|
|
250
|
-
consumer.onEvent({ phase: 'after', type: 'suite', suite: result });
|
|
251
|
-
|
|
252
247
|
result.total = result.passed + result.failed;
|
|
253
248
|
|
|
254
|
-
|
|
249
|
+
// Mark suite complete
|
|
250
|
+
this.#consumer.onEvent({ phase: 'after', type: 'suite', suite: result });
|
|
255
251
|
}
|
|
256
252
|
|
|
257
253
|
/**
|
|
258
254
|
* Handle executing a suite's test/tests based on command line inputs
|
|
259
255
|
*/
|
|
260
|
-
|
|
256
|
+
async execute(run: TestRun): Promise<void> {
|
|
261
257
|
try {
|
|
262
|
-
await Runtime.importFrom(
|
|
258
|
+
await Runtime.importFrom(run.import);
|
|
263
259
|
} catch (err) {
|
|
264
260
|
if (!(err instanceof Error)) {
|
|
265
261
|
throw err;
|
|
266
262
|
}
|
|
267
|
-
this.
|
|
263
|
+
this.#onSuiteFailure(AssertUtil.gernerateImportFailure(run.import, err));
|
|
268
264
|
return;
|
|
269
265
|
}
|
|
270
266
|
|
|
@@ -272,19 +268,10 @@ export class TestExecutor {
|
|
|
272
268
|
await SuiteRegistry.init();
|
|
273
269
|
|
|
274
270
|
// Convert inbound arguments to specific tests to run
|
|
275
|
-
const
|
|
271
|
+
const suites = SuiteRegistry.getSuiteTests(run);
|
|
276
272
|
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
for (const suite of params.suites) {
|
|
280
|
-
if (!(await this.#skip(suite, suite.instance)) && suite.tests.length) {
|
|
281
|
-
await this.executeSuite(consumer, suite);
|
|
282
|
-
}
|
|
283
|
-
}
|
|
284
|
-
} else if (params.test) { // If running a single test
|
|
285
|
-
await this.executeSuiteTest(consumer, params.suite, params.test);
|
|
286
|
-
} else { // Running the suite
|
|
287
|
-
await this.executeSuite(consumer, params.suite);
|
|
273
|
+
for (const { suite, tests } of suites) {
|
|
274
|
+
await this.executeSuite(suite, tests);
|
|
288
275
|
}
|
|
289
276
|
}
|
|
290
277
|
}
|
package/src/execute/phase.ts
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { Barrier } from '@travetto/worker';
|
|
2
2
|
import { Env, TimeUtil } from '@travetto/runtime';
|
|
3
3
|
|
|
4
|
-
import {
|
|
5
|
-
import { SuiteConfig, SuiteResult } from '../model/suite';
|
|
4
|
+
import { SuiteConfig, SuiteFailure, SuiteResult } from '../model/suite';
|
|
6
5
|
import { AssertUtil } from '../assert/util';
|
|
7
|
-
import { TestResult } from '../model/test';
|
|
8
6
|
|
|
9
|
-
class TestBreakout extends Error {
|
|
7
|
+
class TestBreakout extends Error {
|
|
8
|
+
source?: Error;
|
|
9
|
+
}
|
|
10
10
|
|
|
11
11
|
const TEST_PHASE_TIMEOUT = TimeUtil.fromValue(Env.TRV_TEST_PHASE_TIMEOUT.val) ?? 15000;
|
|
12
12
|
|
|
@@ -17,27 +17,14 @@ const TEST_PHASE_TIMEOUT = TimeUtil.fromValue(Env.TRV_TEST_PHASE_TIMEOUT.val) ??
|
|
|
17
17
|
*/
|
|
18
18
|
export class TestPhaseManager {
|
|
19
19
|
#progress: ('all' | 'each')[] = [];
|
|
20
|
-
#consumer: TestConsumer;
|
|
21
20
|
#suite: SuiteConfig;
|
|
22
21
|
#result: SuiteResult;
|
|
22
|
+
#onSuiteFailure: (fail: SuiteFailure) => void;
|
|
23
23
|
|
|
24
|
-
constructor(
|
|
25
|
-
this.#consumer = consumer;
|
|
24
|
+
constructor(suite: SuiteConfig, result: SuiteResult, onSuiteFailure: (fail: SuiteFailure) => void) {
|
|
26
25
|
this.#suite = suite;
|
|
27
26
|
this.#result = result;
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* Create the appropriate events when a suite has an error
|
|
32
|
-
*/
|
|
33
|
-
async triggerSuiteError(methodName: string, error: Error): Promise<TestResult> {
|
|
34
|
-
const bad = AssertUtil.generateSuiteError(this.#suite, methodName, error);
|
|
35
|
-
|
|
36
|
-
this.#consumer.onEvent({ type: 'test', phase: 'before', test: bad.testConfig });
|
|
37
|
-
this.#consumer.onEvent({ type: 'assertion', phase: 'after', assertion: bad.assert });
|
|
38
|
-
this.#consumer.onEvent({ type: 'test', phase: 'after', test: bad.testResult });
|
|
39
|
-
|
|
40
|
-
return bad.testResult;
|
|
27
|
+
this.#onSuiteFailure = onSuiteFailure;
|
|
41
28
|
}
|
|
42
29
|
|
|
43
30
|
/**
|
|
@@ -57,10 +44,9 @@ export class TestPhaseManager {
|
|
|
57
44
|
}
|
|
58
45
|
}
|
|
59
46
|
if (error) {
|
|
60
|
-
const
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
throw new TestBreakout();
|
|
47
|
+
const tbo = new TestBreakout(`[[${phase}]]`);
|
|
48
|
+
tbo.source = error;
|
|
49
|
+
throw tbo;
|
|
64
50
|
}
|
|
65
51
|
}
|
|
66
52
|
|
|
@@ -96,10 +82,14 @@ export class TestPhaseManager {
|
|
|
96
82
|
|
|
97
83
|
this.#progress = [];
|
|
98
84
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
85
|
+
const failure = AssertUtil.generateSuiteFailure(
|
|
86
|
+
this.#suite,
|
|
87
|
+
err instanceof TestBreakout ? err.message : 'all',
|
|
88
|
+
err instanceof TestBreakout ? err.source! : err
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
this.#onSuiteFailure(failure);
|
|
92
|
+
this.#result.tests.push(failure.testResult);
|
|
93
|
+
this.#result.failed++;
|
|
104
94
|
}
|
|
105
95
|
}
|
package/src/execute/promise.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createHook, executionAsyncId } from 'node:async_hooks';
|
|
2
|
+
import { isPromise } from 'node:util/types';
|
|
2
3
|
|
|
3
4
|
import { ExecutionError } from '@travetto/worker';
|
|
4
5
|
import { Util } from '@travetto/runtime';
|
|
@@ -11,9 +12,8 @@ export class PromiseCapturer {
|
|
|
11
12
|
#id: number = 0;
|
|
12
13
|
|
|
13
14
|
#init(id: number, type: string, triggerId: number, resource: unknown): void {
|
|
14
|
-
if (this.#id && type === 'PROMISE' && triggerId === this.#id) {
|
|
15
|
-
|
|
16
|
-
this.#pending.set(id, resource as Promise<unknown>);
|
|
15
|
+
if (this.#id && type === 'PROMISE' && triggerId === this.#id && isPromise(resource)) {
|
|
16
|
+
this.#pending.set(id, resource);
|
|
17
17
|
}
|
|
18
18
|
}
|
|
19
19
|
|
package/src/execute/runner.ts
CHANGED
|
@@ -9,6 +9,7 @@ import { RunnableTestConsumer } from '../consumer/types/runnable';
|
|
|
9
9
|
import { TestExecutor } from './executor';
|
|
10
10
|
import { RunnerUtil } from './util';
|
|
11
11
|
import { RunState } from './types';
|
|
12
|
+
import { TestConfig, TestRun } from '../model/test';
|
|
12
13
|
|
|
13
14
|
/**
|
|
14
15
|
* Test Runner
|
|
@@ -24,22 +25,23 @@ export class Runner {
|
|
|
24
25
|
/**
|
|
25
26
|
* Run all files
|
|
26
27
|
*/
|
|
27
|
-
async runFiles(): Promise<boolean> {
|
|
28
|
+
async runFiles(globs?: string[]): Promise<boolean> {
|
|
28
29
|
const consumer = await RunnableTestConsumer.get(this.#state.consumer ?? this.#state.format);
|
|
29
30
|
|
|
30
|
-
|
|
31
|
+
console.debug('Running', { globs });
|
|
31
32
|
|
|
32
|
-
|
|
33
|
+
const tests = await RunnerUtil.getTestDigest(globs, this.#state.tags);
|
|
34
|
+
const testRuns = RunnerUtil.getTestRuns(tests)
|
|
35
|
+
.sort((a, b) => a.runId!.localeCompare(b.runId!));
|
|
33
36
|
|
|
34
|
-
|
|
35
|
-
await consumer.onStart({ testCount });
|
|
37
|
+
await consumer.onStart({ testCount: tests.length });
|
|
36
38
|
await WorkPool.run(
|
|
37
|
-
buildStandardTestManager
|
|
38
|
-
|
|
39
|
+
f => buildStandardTestManager(consumer, f),
|
|
40
|
+
testRuns,
|
|
39
41
|
{
|
|
40
42
|
idleTimeoutMillis: TimeUtil.asMillis(10, 's'),
|
|
41
43
|
min: 1,
|
|
42
|
-
max: this.#state.concurrency
|
|
44
|
+
max: this.#state.concurrency
|
|
43
45
|
});
|
|
44
46
|
|
|
45
47
|
return consumer.summarizeAsBoolean();
|
|
@@ -48,24 +50,30 @@ export class Runner {
|
|
|
48
50
|
/**
|
|
49
51
|
* Run a single file
|
|
50
52
|
*/
|
|
51
|
-
async runSingle(): Promise<boolean> {
|
|
52
|
-
|
|
53
|
+
async runSingle(run: TestRun): Promise<boolean> {
|
|
54
|
+
run.import =
|
|
55
|
+
RuntimeIndex.getFromImport(run.import)?.import ??
|
|
56
|
+
RuntimeIndex.getFromSource(path.resolve(run.import))?.import!;
|
|
53
57
|
|
|
54
|
-
|
|
55
|
-
imp = RuntimeIndex.getFromSource(path.resolve(this.#state.args[0]))?.import;
|
|
56
|
-
}
|
|
58
|
+
const entry = RuntimeIndex.getFromImport(run.import)!;
|
|
57
59
|
|
|
58
|
-
const entry = RuntimeIndex.getFromImport(imp!)!;
|
|
59
60
|
if (entry.module !== Runtime.main.name) {
|
|
60
61
|
RuntimeIndex.reinitForModule(entry.module);
|
|
61
62
|
}
|
|
62
63
|
|
|
63
|
-
const
|
|
64
|
+
const filter = (run.methodNames?.length) ?
|
|
65
|
+
(cfg: TestConfig): boolean => run.methodNames!.includes(cfg.methodName) :
|
|
66
|
+
undefined;
|
|
64
67
|
|
|
65
|
-
const
|
|
68
|
+
const consumer = (await RunnableTestConsumer.get(this.#state.consumer ?? this.#state.format))
|
|
69
|
+
.withTransformer(e => {
|
|
70
|
+
// Copy run metadata to event
|
|
71
|
+
e.metadata = run.metadata;
|
|
72
|
+
return e;
|
|
73
|
+
});
|
|
66
74
|
|
|
67
75
|
await consumer.onStart({});
|
|
68
|
-
await TestExecutor
|
|
76
|
+
await new TestExecutor(consumer, filter).execute(run);
|
|
69
77
|
return consumer.summarizeAsBoolean();
|
|
70
78
|
}
|
|
71
79
|
|
|
@@ -73,9 +81,10 @@ export class Runner {
|
|
|
73
81
|
* Run the runner, based on the inputs passed to the constructor
|
|
74
82
|
*/
|
|
75
83
|
async run(): Promise<boolean | undefined> {
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
84
|
+
if ('import' in this.#state.target) {
|
|
85
|
+
return await this.runSingle(this.#state.target);
|
|
86
|
+
} else {
|
|
87
|
+
return await this.runFiles(this.#state.target.globs);
|
|
79
88
|
}
|
|
80
89
|
}
|
|
81
90
|
}
|
package/src/execute/types.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { TestConsumer } from '../consumer/types';
|
|
2
|
+
import { TestRun } from '../model/test';
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* Run state
|
|
@@ -13,19 +14,20 @@ export interface RunState {
|
|
|
13
14
|
*/
|
|
14
15
|
consumer?: TestConsumer;
|
|
15
16
|
/**
|
|
16
|
-
*
|
|
17
|
-
*/
|
|
18
|
-
mode?: 'single' | 'watch' | 'standard';
|
|
19
|
-
/**
|
|
20
|
-
* Show progress to stderr
|
|
17
|
+
* Number of test suites to run concurrently, when mode is not single
|
|
21
18
|
*/
|
|
22
|
-
|
|
19
|
+
concurrency?: number;
|
|
23
20
|
/**
|
|
24
|
-
*
|
|
21
|
+
* The tags to include or exclude from testing
|
|
25
22
|
*/
|
|
26
|
-
|
|
23
|
+
tags?: string[];
|
|
27
24
|
/**
|
|
28
|
-
*
|
|
25
|
+
* target
|
|
29
26
|
*/
|
|
30
|
-
|
|
27
|
+
target: TestRun | {
|
|
28
|
+
/**
|
|
29
|
+
* Globs to run
|
|
30
|
+
*/
|
|
31
|
+
globs?: string[];
|
|
32
|
+
};
|
|
31
33
|
}
|
package/src/execute/util.ts
CHANGED
|
@@ -4,6 +4,7 @@ import fs from 'node:fs/promises';
|
|
|
4
4
|
import readline from 'node:readline/promises';
|
|
5
5
|
|
|
6
6
|
import { Env, ExecUtil, ShutdownManager, Util, RuntimeIndex, Runtime } from '@travetto/runtime';
|
|
7
|
+
import { TestConfig, TestRun } from '../model/test';
|
|
7
8
|
|
|
8
9
|
/**
|
|
9
10
|
* Simple Test Utilities
|
|
@@ -63,13 +64,13 @@ export class RunnerUtil {
|
|
|
63
64
|
}
|
|
64
65
|
|
|
65
66
|
/**
|
|
66
|
-
* Get count of tests for a given set of
|
|
67
|
-
* @param
|
|
67
|
+
* Get count of tests for a given set of globs
|
|
68
|
+
* @param globs
|
|
68
69
|
* @returns
|
|
69
70
|
*/
|
|
70
|
-
static async
|
|
71
|
+
static async getTestDigest(globs: string[] = ['**/*.ts'], tags?: string[]): Promise<TestConfig[]> {
|
|
71
72
|
const countRes = await ExecUtil.getResult(
|
|
72
|
-
spawn('npx', ['trv', 'test:
|
|
73
|
+
spawn('npx', ['trv', 'test:digest', '-o', 'json', ...globs], {
|
|
73
74
|
env: { ...process.env, ...Env.FORCE_COLOR.export(0), ...Env.NO_COLOR.export(true) }
|
|
74
75
|
}),
|
|
75
76
|
{ catch: true }
|
|
@@ -77,6 +78,30 @@ export class RunnerUtil {
|
|
|
77
78
|
if (!countRes.valid) {
|
|
78
79
|
throw new Error(countRes.stderr);
|
|
79
80
|
}
|
|
80
|
-
|
|
81
|
+
|
|
82
|
+
const testFilter = tags?.length ?
|
|
83
|
+
Util.allowDeny<string, [TestConfig]>(
|
|
84
|
+
tags,
|
|
85
|
+
rule => rule,
|
|
86
|
+
(rule, core) => core.tags?.includes(rule) ?? false
|
|
87
|
+
) :
|
|
88
|
+
((): boolean => true);
|
|
89
|
+
|
|
90
|
+
const res: TestConfig[] = countRes.valid ? JSON.parse(countRes.stdout) : [];
|
|
91
|
+
return res.filter(testFilter);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Get run events
|
|
96
|
+
*/
|
|
97
|
+
static getTestRuns(tests: TestConfig[]): TestRun[] {
|
|
98
|
+
const events = tests.reduce((acc, test) => {
|
|
99
|
+
if (!acc.has(test.classId)) {
|
|
100
|
+
acc.set(test.classId, { import: test.import, classId: test.classId, methodNames: [], runId: Util.uuid() });
|
|
101
|
+
}
|
|
102
|
+
acc.get(test.classId)!.methodNames!.push(test.methodName);
|
|
103
|
+
return acc;
|
|
104
|
+
}, new Map<string, TestRun>());
|
|
105
|
+
return [...events.values()];
|
|
81
106
|
}
|
|
82
107
|
}
|