@travetto/test 5.0.0-rc.9 → 5.0.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 +2 -1
- package/package.json +8 -8
- package/src/assert/check.ts +1 -1
- package/src/assert/util.ts +21 -9
- package/src/consumer/{error.ts → serialize.ts} +10 -17
- 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 +7 -4
- package/src/consumer/types/tap.ts +2 -2
- package/src/execute/barrier.ts +100 -0
- package/src/execute/error.ts +11 -0
- package/src/execute/executor.ts +87 -109
- package/src/execute/phase.ts +21 -33
- package/src/execute/runner.ts +26 -21
- 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 +24 -17
- 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 +2 -2
- package/src/execute/promise.ts +0 -49
package/src/execute/executor.ts
CHANGED
|
@@ -1,19 +1,18 @@
|
|
|
1
1
|
import { AssertionError } from 'node:assert';
|
|
2
|
-
import path from 'node:path';
|
|
3
2
|
|
|
4
|
-
import { Env, TimeUtil, Runtime,
|
|
5
|
-
import { Barrier, ExecutionError } from '@travetto/worker';
|
|
3
|
+
import { Env, TimeUtil, Runtime, castTo } from '@travetto/runtime';
|
|
6
4
|
|
|
7
5
|
import { SuiteRegistry } from '../registry/suite';
|
|
8
|
-
import { TestConfig, TestResult } from '../model/test';
|
|
9
|
-
import { SuiteConfig, SuiteResult } from '../model/suite';
|
|
6
|
+
import { TestConfig, TestResult, TestRun } from '../model/test';
|
|
7
|
+
import { SuiteConfig, SuiteFailure, SuiteResult } from '../model/suite';
|
|
10
8
|
import { TestConsumer } from '../consumer/types';
|
|
11
9
|
import { AssertCheck } from '../assert/check';
|
|
12
10
|
import { AssertCapture } from '../assert/capture';
|
|
13
11
|
import { ConsoleCapture } from './console';
|
|
14
12
|
import { TestPhaseManager } from './phase';
|
|
15
|
-
import { PromiseCapturer } from './promise';
|
|
16
13
|
import { AssertUtil } from '../assert/util';
|
|
14
|
+
import { Barrier } from './barrier';
|
|
15
|
+
import { ExecutionError } from './error';
|
|
17
16
|
|
|
18
17
|
const TEST_TIMEOUT = TimeUtil.fromValue(Env.TRV_TEST_TIMEOUT.val) ?? 5000;
|
|
19
18
|
|
|
@@ -22,65 +21,74 @@ const TEST_TIMEOUT = TimeUtil.fromValue(Env.TRV_TEST_TIMEOUT.val) ?? 5000;
|
|
|
22
21
|
*/
|
|
23
22
|
export class TestExecutor {
|
|
24
23
|
|
|
24
|
+
#consumer: TestConsumer;
|
|
25
|
+
|
|
26
|
+
constructor(consumer: TestConsumer) {
|
|
27
|
+
this.#consumer = consumer;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Handles communicating a suite-level error
|
|
32
|
+
* @param failure
|
|
33
|
+
* @param withSuite
|
|
34
|
+
*/
|
|
35
|
+
#onSuiteFailure(failure: SuiteFailure, triggerSuite?: boolean): void {
|
|
36
|
+
if (triggerSuite) {
|
|
37
|
+
this.#consumer.onEvent({ type: 'suite', phase: 'before', suite: failure.suite });
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
this.#consumer.onEvent({ type: 'test', phase: 'before', test: failure.test });
|
|
41
|
+
this.#consumer.onEvent({ type: 'assertion', phase: 'after', assertion: failure.assert });
|
|
42
|
+
this.#consumer.onEvent({ type: 'test', phase: 'after', test: failure.testResult });
|
|
43
|
+
|
|
44
|
+
if (triggerSuite) {
|
|
45
|
+
this.#consumer.onEvent({
|
|
46
|
+
type: 'suite', phase: 'after',
|
|
47
|
+
suite: { ...castTo(failure.suite), failed: 1, passed: 0, total: 1, skipped: 0 }
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
25
52
|
/**
|
|
26
53
|
* Raw execution, runs the method and then returns any thrown errors as the result.
|
|
27
54
|
*
|
|
28
55
|
* This method should never throw under any circumstances.
|
|
29
56
|
*/
|
|
30
|
-
|
|
57
|
+
async #executeTestMethod(test: TestConfig): Promise<Error | undefined> {
|
|
31
58
|
const suite = SuiteRegistry.get(test.class);
|
|
32
59
|
|
|
33
60
|
// Ensure all the criteria below are satisfied before moving forward
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
);
|
|
44
|
-
} finally {
|
|
45
|
-
process.env = env; // Restore
|
|
46
|
-
}
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
// Wait for all barriers to be satisfied
|
|
50
|
-
return barrier.wait();
|
|
61
|
+
return Barrier.awaitOperation(test.timeout || TEST_TIMEOUT, async () => {
|
|
62
|
+
const env = process.env;
|
|
63
|
+
process.env = { ...env }; // Created an isolated environment
|
|
64
|
+
try {
|
|
65
|
+
await castTo<Record<string, Function>>(suite.instance)[test.methodName]();
|
|
66
|
+
} finally {
|
|
67
|
+
process.env = env; // Restore
|
|
68
|
+
}
|
|
69
|
+
});
|
|
51
70
|
}
|
|
52
71
|
|
|
53
72
|
/**
|
|
54
73
|
* Determining if we should skip
|
|
55
74
|
*/
|
|
56
|
-
|
|
57
|
-
if (cfg.skip
|
|
58
|
-
|
|
59
|
-
return true;
|
|
60
|
-
}
|
|
75
|
+
async #shouldSkip(cfg: TestConfig | SuiteConfig, inst: unknown): Promise<boolean | undefined> {
|
|
76
|
+
if (typeof cfg.skip === 'function' ? await cfg.skip(inst) : cfg.skip) {
|
|
77
|
+
return true;
|
|
61
78
|
}
|
|
62
79
|
}
|
|
63
80
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
const classId = `${RuntimeIndex.getFromImport(imp)?.id}○${name}`;
|
|
70
|
-
const suite = asFull<SuiteConfig & SuiteResult>({ class: asFull<Class>({ name }), classId, duration: 0, lineStart: 1, lineEnd: 1, import: imp, });
|
|
71
|
-
err.message = err.message.replaceAll(Runtime.mainSourcePath, '.');
|
|
72
|
-
const res = AssertUtil.generateSuiteError(suite, 'require', err);
|
|
73
|
-
consumer.onEvent({ type: 'suite', phase: 'before', suite });
|
|
74
|
-
consumer.onEvent({ type: 'test', phase: 'before', test: res.testConfig });
|
|
75
|
-
consumer.onEvent({ type: 'assertion', phase: 'after', assertion: res.assert });
|
|
76
|
-
consumer.onEvent({ type: 'test', phase: 'after', test: res.testResult });
|
|
77
|
-
consumer.onEvent({ type: 'suite', phase: 'after', suite: { ...suite, failed: 1, passed: 0, total: 1, skipped: 0 } });
|
|
81
|
+
#skipTest(test: TestConfig, result: SuiteResult): void {
|
|
82
|
+
// Mark test start
|
|
83
|
+
this.#consumer.onEvent({ type: 'test', phase: 'before', test });
|
|
84
|
+
result.skipped++;
|
|
85
|
+
this.#consumer.onEvent({ type: 'test', phase: 'after', test: { ...test, assertions: [], duration: 0, durationTotal: 0, output: {}, status: 'skipped' } });
|
|
78
86
|
}
|
|
79
87
|
|
|
80
88
|
/**
|
|
81
89
|
* An empty suite result based on a suite config
|
|
82
90
|
*/
|
|
83
|
-
|
|
91
|
+
createSuiteResult(suite: SuiteConfig): SuiteResult {
|
|
84
92
|
return {
|
|
85
93
|
passed: 0,
|
|
86
94
|
failed: 0,
|
|
@@ -98,10 +106,10 @@ export class TestExecutor {
|
|
|
98
106
|
/**
|
|
99
107
|
* Execute the test, capture output, assertions and promises
|
|
100
108
|
*/
|
|
101
|
-
|
|
109
|
+
async executeTest(test: TestConfig): Promise<TestResult> {
|
|
102
110
|
|
|
103
111
|
// Mark test start
|
|
104
|
-
consumer.onEvent({ type: 'test', phase: 'before', test });
|
|
112
|
+
this.#consumer.onEvent({ type: 'test', phase: 'before', test });
|
|
105
113
|
|
|
106
114
|
const startTime = Date.now();
|
|
107
115
|
|
|
@@ -113,6 +121,7 @@ export class TestExecutor {
|
|
|
113
121
|
lineEnd: test.lineEnd,
|
|
114
122
|
lineBodyStart: test.lineBodyStart,
|
|
115
123
|
import: test.import,
|
|
124
|
+
sourceImport: test.sourceImport,
|
|
116
125
|
status: 'skipped',
|
|
117
126
|
assertions: [],
|
|
118
127
|
duration: 0,
|
|
@@ -120,13 +129,9 @@ export class TestExecutor {
|
|
|
120
129
|
output: {},
|
|
121
130
|
};
|
|
122
131
|
|
|
123
|
-
if (await this.#skip(test, suite.instance)) {
|
|
124
|
-
return result;
|
|
125
|
-
}
|
|
126
|
-
|
|
127
132
|
// Emit every assertion as it occurs
|
|
128
133
|
const getAssertions = AssertCapture.collector(test, asrt =>
|
|
129
|
-
consumer.onEvent({
|
|
134
|
+
this.#consumer.onEvent({
|
|
130
135
|
type: 'assertion',
|
|
131
136
|
phase: 'after',
|
|
132
137
|
assertion: asrt
|
|
@@ -161,47 +166,27 @@ export class TestExecutor {
|
|
|
161
166
|
});
|
|
162
167
|
|
|
163
168
|
// Mark completion
|
|
164
|
-
consumer.onEvent({ type: 'test', phase: 'after', test: result });
|
|
169
|
+
this.#consumer.onEvent({ type: 'test', phase: 'after', test: result });
|
|
165
170
|
|
|
166
171
|
return result;
|
|
167
172
|
}
|
|
168
173
|
|
|
169
174
|
/**
|
|
170
|
-
* Execute
|
|
175
|
+
* Execute an entire suite
|
|
171
176
|
*/
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
const mgr = new TestPhaseManager(consumer, suite, result);
|
|
176
|
-
|
|
177
|
-
try {
|
|
178
|
-
await mgr.startPhase('all');
|
|
179
|
-
const skip = await this.#skip(test, suite.instance);
|
|
180
|
-
if (!skip) {
|
|
181
|
-
await mgr.startPhase('each');
|
|
182
|
-
}
|
|
183
|
-
await this.executeTest(consumer, test, suite);
|
|
184
|
-
if (!skip) {
|
|
185
|
-
await mgr.endPhase('each');
|
|
186
|
-
}
|
|
187
|
-
await mgr.endPhase('all');
|
|
188
|
-
} catch (err) {
|
|
189
|
-
await mgr.onError(err);
|
|
177
|
+
async executeSuite(suite: SuiteConfig, tests: TestConfig[]): Promise<void> {
|
|
178
|
+
if (!tests.length || await this.#shouldSkip(suite, suite.instance)) {
|
|
179
|
+
return;
|
|
190
180
|
}
|
|
191
|
-
}
|
|
192
181
|
|
|
193
|
-
/**
|
|
194
|
-
* Execute an entire suite
|
|
195
|
-
*/
|
|
196
|
-
static async executeSuite(consumer: TestConsumer, suite: SuiteConfig): Promise<SuiteResult> {
|
|
197
182
|
const result: SuiteResult = this.createSuiteResult(suite);
|
|
198
183
|
|
|
199
184
|
const startTime = Date.now();
|
|
200
185
|
|
|
201
186
|
// Mark suite start
|
|
202
|
-
consumer.onEvent({ phase: 'before', type: 'suite', suite });
|
|
187
|
+
this.#consumer.onEvent({ phase: 'before', type: 'suite', suite });
|
|
203
188
|
|
|
204
|
-
const mgr = new TestPhaseManager(
|
|
189
|
+
const mgr = new TestPhaseManager(suite, result, e => this.#onSuiteFailure(e));
|
|
205
190
|
|
|
206
191
|
const originalEnv = { ...process.env };
|
|
207
192
|
|
|
@@ -211,28 +196,30 @@ export class TestExecutor {
|
|
|
211
196
|
|
|
212
197
|
const suiteEnv = { ...process.env };
|
|
213
198
|
|
|
214
|
-
for (const test of suite.tests) {
|
|
199
|
+
for (const test of tests ?? suite.tests) {
|
|
200
|
+
if (await this.#shouldSkip(test, suite.instance)) {
|
|
201
|
+
this.#skipTest(test, result);
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
|
|
215
205
|
// Reset env before each test
|
|
216
206
|
process.env = { ...suiteEnv };
|
|
207
|
+
|
|
217
208
|
const testStart = Date.now();
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
await mgr.startPhase('each');
|
|
222
|
-
}
|
|
209
|
+
|
|
210
|
+
// Handle BeforeEach
|
|
211
|
+
await mgr.startPhase('each');
|
|
223
212
|
|
|
224
213
|
// Run test
|
|
225
|
-
const ret = await this.executeTest(
|
|
214
|
+
const ret = await this.executeTest(test);
|
|
226
215
|
result[ret.status]++;
|
|
227
|
-
|
|
228
|
-
if (!skip) {
|
|
229
|
-
result.tests.push(ret);
|
|
230
|
-
}
|
|
216
|
+
result.tests.push(ret);
|
|
231
217
|
|
|
232
218
|
// Handle after each
|
|
233
219
|
await mgr.endPhase('each');
|
|
234
220
|
ret.durationTotal = Date.now() - testStart;
|
|
235
221
|
}
|
|
222
|
+
|
|
236
223
|
// Handle after all
|
|
237
224
|
await mgr.endPhase('all');
|
|
238
225
|
} catch (err) {
|
|
@@ -243,26 +230,23 @@ export class TestExecutor {
|
|
|
243
230
|
process.env = { ...originalEnv };
|
|
244
231
|
|
|
245
232
|
result.duration = Date.now() - startTime;
|
|
246
|
-
|
|
247
|
-
// Mark suite complete
|
|
248
|
-
consumer.onEvent({ phase: 'after', type: 'suite', suite: result });
|
|
249
|
-
|
|
250
233
|
result.total = result.passed + result.failed;
|
|
251
234
|
|
|
252
|
-
|
|
235
|
+
// Mark suite complete
|
|
236
|
+
this.#consumer.onEvent({ phase: 'after', type: 'suite', suite: result });
|
|
253
237
|
}
|
|
254
238
|
|
|
255
239
|
/**
|
|
256
240
|
* Handle executing a suite's test/tests based on command line inputs
|
|
257
241
|
*/
|
|
258
|
-
|
|
242
|
+
async execute(run: TestRun): Promise<void> {
|
|
259
243
|
try {
|
|
260
|
-
await Runtime.importFrom(
|
|
244
|
+
await Runtime.importFrom(run.import);
|
|
261
245
|
} catch (err) {
|
|
262
246
|
if (!(err instanceof Error)) {
|
|
263
247
|
throw err;
|
|
264
248
|
}
|
|
265
|
-
this.
|
|
249
|
+
this.#onSuiteFailure(AssertUtil.gernerateImportFailure(run.import, err));
|
|
266
250
|
return;
|
|
267
251
|
}
|
|
268
252
|
|
|
@@ -270,19 +254,13 @@ export class TestExecutor {
|
|
|
270
254
|
await SuiteRegistry.init();
|
|
271
255
|
|
|
272
256
|
// Convert inbound arguments to specific tests to run
|
|
273
|
-
const
|
|
257
|
+
const suites = SuiteRegistry.getSuiteTests(run);
|
|
258
|
+
if (!suites.length) {
|
|
259
|
+
console.warn('Unable to find suites for ', run);
|
|
260
|
+
}
|
|
274
261
|
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
for (const suite of params.suites) {
|
|
278
|
-
if (!(await this.#skip(suite, suite.instance)) && suite.tests.length) {
|
|
279
|
-
await this.executeSuite(consumer, suite);
|
|
280
|
-
}
|
|
281
|
-
}
|
|
282
|
-
} else if (params.test) { // If running a single test
|
|
283
|
-
await this.executeSuiteTest(consumer, params.suite, params.test);
|
|
284
|
-
} else { // Running the suite
|
|
285
|
-
await this.executeSuite(consumer, params.suite);
|
|
262
|
+
for (const { suite, tests } of suites) {
|
|
263
|
+
await this.executeSuite(suite, tests);
|
|
286
264
|
}
|
|
287
265
|
}
|
|
288
266
|
}
|
package/src/execute/phase.ts
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
import { Barrier } from '@travetto/worker';
|
|
2
1
|
import { Env, TimeUtil } from '@travetto/runtime';
|
|
3
2
|
|
|
4
|
-
import {
|
|
5
|
-
import { SuiteConfig, SuiteResult } from '../model/suite';
|
|
3
|
+
import { SuiteConfig, SuiteFailure, SuiteResult } from '../model/suite';
|
|
6
4
|
import { AssertUtil } from '../assert/util';
|
|
7
|
-
import {
|
|
5
|
+
import { Barrier } from './barrier';
|
|
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
|
/**
|
|
@@ -48,19 +35,16 @@ export class TestPhaseManager {
|
|
|
48
35
|
for (const fn of this.#suite[phase]) {
|
|
49
36
|
|
|
50
37
|
// Ensure all the criteria below are satisfied before moving forward
|
|
51
|
-
error = await
|
|
52
|
-
.add(async () => fn.call(this.#suite.instance))
|
|
53
|
-
.wait();
|
|
38
|
+
error = await Barrier.awaitOperation(TEST_PHASE_TIMEOUT, async () => fn.call(this.#suite.instance));
|
|
54
39
|
|
|
55
40
|
if (error) {
|
|
56
41
|
break;
|
|
57
42
|
}
|
|
58
43
|
}
|
|
59
44
|
if (error) {
|
|
60
|
-
const
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
throw new TestBreakout();
|
|
45
|
+
const tbo = new TestBreakout(`[[${phase}]]`);
|
|
46
|
+
tbo.source = error;
|
|
47
|
+
throw tbo;
|
|
64
48
|
}
|
|
65
49
|
}
|
|
66
50
|
|
|
@@ -96,10 +80,14 @@ export class TestPhaseManager {
|
|
|
96
80
|
|
|
97
81
|
this.#progress = [];
|
|
98
82
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
83
|
+
const failure = AssertUtil.generateSuiteFailure(
|
|
84
|
+
this.#suite,
|
|
85
|
+
err instanceof TestBreakout ? err.message : 'all',
|
|
86
|
+
err instanceof TestBreakout ? err.source! : err
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
this.#onSuiteFailure(failure);
|
|
90
|
+
this.#result.tests.push(failure.testResult);
|
|
91
|
+
this.#result.failed++;
|
|
104
92
|
}
|
|
105
93
|
}
|
package/src/execute/runner.ts
CHANGED
|
@@ -5,6 +5,7 @@ import { WorkPool } from '@travetto/worker';
|
|
|
5
5
|
|
|
6
6
|
import { buildStandardTestManager } from '../worker/standard';
|
|
7
7
|
import { RunnableTestConsumer } from '../consumer/types/runnable';
|
|
8
|
+
import { TestRun } from '../model/test';
|
|
8
9
|
|
|
9
10
|
import { TestExecutor } from './executor';
|
|
10
11
|
import { RunnerUtil } from './util';
|
|
@@ -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,26 @@ 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 consumer = await RunnableTestConsumer.get(this.#state.consumer ?? this.#state.format)
|
|
64
|
-
|
|
65
|
-
|
|
64
|
+
const consumer = (await RunnableTestConsumer.get(this.#state.consumer ?? this.#state.format))
|
|
65
|
+
.withTransformer(e => {
|
|
66
|
+
// Copy run metadata to event
|
|
67
|
+
e.metadata = run.metadata;
|
|
68
|
+
return e;
|
|
69
|
+
});
|
|
66
70
|
|
|
67
71
|
await consumer.onStart({});
|
|
68
|
-
await TestExecutor.execute(
|
|
72
|
+
await new TestExecutor(consumer).execute(run);
|
|
69
73
|
return consumer.summarizeAsBoolean();
|
|
70
74
|
}
|
|
71
75
|
|
|
@@ -73,9 +77,10 @@ export class Runner {
|
|
|
73
77
|
* Run the runner, based on the inputs passed to the constructor
|
|
74
78
|
*/
|
|
75
79
|
async run(): Promise<boolean | undefined> {
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
80
|
+
if ('import' in this.#state.target) {
|
|
81
|
+
return await this.runSingle(this.#state.target);
|
|
82
|
+
} else {
|
|
83
|
+
return await this.runFiles(this.#state.target.globs);
|
|
79
84
|
}
|
|
80
85
|
}
|
|
81
86
|
}
|
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
|
}
|