@travetto/test 5.0.0-rc.9 → 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/check.ts +1 -1
- package/src/assert/util.ts +20 -6
- 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/executor.ts +79 -90
- package/src/execute/phase.ts +19 -29
- 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 +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/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
|
|
@@ -53,34 +82,27 @@ export class TestExecutor {
|
|
|
53
82
|
/**
|
|
54
83
|
* Determining if we should skip
|
|
55
84
|
*/
|
|
56
|
-
|
|
57
|
-
if (cfg
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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;
|
|
61
92
|
}
|
|
62
93
|
}
|
|
63
94
|
|
|
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 } });
|
|
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' } });
|
|
78
100
|
}
|
|
79
101
|
|
|
80
102
|
/**
|
|
81
103
|
* An empty suite result based on a suite config
|
|
82
104
|
*/
|
|
83
|
-
|
|
105
|
+
createSuiteResult(suite: SuiteConfig): SuiteResult {
|
|
84
106
|
return {
|
|
85
107
|
passed: 0,
|
|
86
108
|
failed: 0,
|
|
@@ -98,10 +120,10 @@ export class TestExecutor {
|
|
|
98
120
|
/**
|
|
99
121
|
* Execute the test, capture output, assertions and promises
|
|
100
122
|
*/
|
|
101
|
-
|
|
123
|
+
async executeTest(test: TestConfig): Promise<TestResult> {
|
|
102
124
|
|
|
103
125
|
// Mark test start
|
|
104
|
-
consumer.onEvent({ type: 'test', phase: 'before', test });
|
|
126
|
+
this.#consumer.onEvent({ type: 'test', phase: 'before', test });
|
|
105
127
|
|
|
106
128
|
const startTime = Date.now();
|
|
107
129
|
|
|
@@ -113,6 +135,7 @@ export class TestExecutor {
|
|
|
113
135
|
lineEnd: test.lineEnd,
|
|
114
136
|
lineBodyStart: test.lineBodyStart,
|
|
115
137
|
import: test.import,
|
|
138
|
+
sourceImport: test.sourceImport,
|
|
116
139
|
status: 'skipped',
|
|
117
140
|
assertions: [],
|
|
118
141
|
duration: 0,
|
|
@@ -120,13 +143,9 @@ export class TestExecutor {
|
|
|
120
143
|
output: {},
|
|
121
144
|
};
|
|
122
145
|
|
|
123
|
-
if (await this.#skip(test, suite.instance)) {
|
|
124
|
-
return result;
|
|
125
|
-
}
|
|
126
|
-
|
|
127
146
|
// Emit every assertion as it occurs
|
|
128
147
|
const getAssertions = AssertCapture.collector(test, asrt =>
|
|
129
|
-
consumer.onEvent({
|
|
148
|
+
this.#consumer.onEvent({
|
|
130
149
|
type: 'assertion',
|
|
131
150
|
phase: 'after',
|
|
132
151
|
assertion: asrt
|
|
@@ -161,47 +180,27 @@ export class TestExecutor {
|
|
|
161
180
|
});
|
|
162
181
|
|
|
163
182
|
// Mark completion
|
|
164
|
-
consumer.onEvent({ type: 'test', phase: 'after', test: result });
|
|
183
|
+
this.#consumer.onEvent({ type: 'test', phase: 'after', test: result });
|
|
165
184
|
|
|
166
185
|
return result;
|
|
167
186
|
}
|
|
168
187
|
|
|
169
188
|
/**
|
|
170
|
-
* Execute
|
|
189
|
+
* Execute an entire suite
|
|
171
190
|
*/
|
|
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);
|
|
191
|
+
async executeSuite(suite: SuiteConfig, tests: TestConfig[]): Promise<void> {
|
|
192
|
+
if (!tests.length || await this.#shouldSkip(suite, suite.instance)) {
|
|
193
|
+
return;
|
|
190
194
|
}
|
|
191
|
-
}
|
|
192
195
|
|
|
193
|
-
/**
|
|
194
|
-
* Execute an entire suite
|
|
195
|
-
*/
|
|
196
|
-
static async executeSuite(consumer: TestConsumer, suite: SuiteConfig): Promise<SuiteResult> {
|
|
197
196
|
const result: SuiteResult = this.createSuiteResult(suite);
|
|
198
197
|
|
|
199
198
|
const startTime = Date.now();
|
|
200
199
|
|
|
201
200
|
// Mark suite start
|
|
202
|
-
consumer.onEvent({ phase: 'before', type: 'suite', suite });
|
|
201
|
+
this.#consumer.onEvent({ phase: 'before', type: 'suite', suite });
|
|
203
202
|
|
|
204
|
-
const mgr = new TestPhaseManager(
|
|
203
|
+
const mgr = new TestPhaseManager(suite, result, e => this.#onSuiteFailure(e));
|
|
205
204
|
|
|
206
205
|
const originalEnv = { ...process.env };
|
|
207
206
|
|
|
@@ -211,28 +210,30 @@ export class TestExecutor {
|
|
|
211
210
|
|
|
212
211
|
const suiteEnv = { ...process.env };
|
|
213
212
|
|
|
214
|
-
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
|
+
|
|
215
219
|
// Reset env before each test
|
|
216
220
|
process.env = { ...suiteEnv };
|
|
221
|
+
|
|
217
222
|
const testStart = Date.now();
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
await mgr.startPhase('each');
|
|
222
|
-
}
|
|
223
|
+
|
|
224
|
+
// Handle BeforeEach
|
|
225
|
+
await mgr.startPhase('each');
|
|
223
226
|
|
|
224
227
|
// Run test
|
|
225
|
-
const ret = await this.executeTest(
|
|
228
|
+
const ret = await this.executeTest(test);
|
|
226
229
|
result[ret.status]++;
|
|
227
|
-
|
|
228
|
-
if (!skip) {
|
|
229
|
-
result.tests.push(ret);
|
|
230
|
-
}
|
|
230
|
+
result.tests.push(ret);
|
|
231
231
|
|
|
232
232
|
// Handle after each
|
|
233
233
|
await mgr.endPhase('each');
|
|
234
234
|
ret.durationTotal = Date.now() - testStart;
|
|
235
235
|
}
|
|
236
|
+
|
|
236
237
|
// Handle after all
|
|
237
238
|
await mgr.endPhase('all');
|
|
238
239
|
} catch (err) {
|
|
@@ -243,26 +244,23 @@ export class TestExecutor {
|
|
|
243
244
|
process.env = { ...originalEnv };
|
|
244
245
|
|
|
245
246
|
result.duration = Date.now() - startTime;
|
|
246
|
-
|
|
247
|
-
// Mark suite complete
|
|
248
|
-
consumer.onEvent({ phase: 'after', type: 'suite', suite: result });
|
|
249
|
-
|
|
250
247
|
result.total = result.passed + result.failed;
|
|
251
248
|
|
|
252
|
-
|
|
249
|
+
// Mark suite complete
|
|
250
|
+
this.#consumer.onEvent({ phase: 'after', type: 'suite', suite: result });
|
|
253
251
|
}
|
|
254
252
|
|
|
255
253
|
/**
|
|
256
254
|
* Handle executing a suite's test/tests based on command line inputs
|
|
257
255
|
*/
|
|
258
|
-
|
|
256
|
+
async execute(run: TestRun): Promise<void> {
|
|
259
257
|
try {
|
|
260
|
-
await Runtime.importFrom(
|
|
258
|
+
await Runtime.importFrom(run.import);
|
|
261
259
|
} catch (err) {
|
|
262
260
|
if (!(err instanceof Error)) {
|
|
263
261
|
throw err;
|
|
264
262
|
}
|
|
265
|
-
this.
|
|
263
|
+
this.#onSuiteFailure(AssertUtil.gernerateImportFailure(run.import, err));
|
|
266
264
|
return;
|
|
267
265
|
}
|
|
268
266
|
|
|
@@ -270,19 +268,10 @@ export class TestExecutor {
|
|
|
270
268
|
await SuiteRegistry.init();
|
|
271
269
|
|
|
272
270
|
// Convert inbound arguments to specific tests to run
|
|
273
|
-
const
|
|
271
|
+
const suites = SuiteRegistry.getSuiteTests(run);
|
|
274
272
|
|
|
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);
|
|
273
|
+
for (const { suite, tests } of suites) {
|
|
274
|
+
await this.executeSuite(suite, tests);
|
|
286
275
|
}
|
|
287
276
|
}
|
|
288
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/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
|
}
|
package/src/execute/watcher.ts
CHANGED
|
@@ -1,27 +1,14 @@
|
|
|
1
1
|
import { RootRegistry, MethodSource } from '@travetto/registry';
|
|
2
|
-
import { WorkPool
|
|
3
|
-
import { Runtime, describeFunction } from '@travetto/runtime';
|
|
2
|
+
import { WorkPool } from '@travetto/worker';
|
|
3
|
+
import { AsyncQueue, Runtime, RuntimeIndex, castTo, describeFunction } from '@travetto/runtime';
|
|
4
4
|
|
|
5
5
|
import { SuiteRegistry } from '../registry/suite';
|
|
6
6
|
import { buildStandardTestManager } from '../worker/standard';
|
|
7
7
|
import { TestConsumerRegistry } from '../consumer/registry';
|
|
8
8
|
import { CumulativeSummaryConsumer } from '../consumer/types/cumulative';
|
|
9
|
-
import {
|
|
9
|
+
import { TestRun } from '../model/test';
|
|
10
10
|
import { RunnerUtil } from './util';
|
|
11
|
-
import {
|
|
12
|
-
|
|
13
|
-
function isRunRequest(ev: unknown): ev is RunRequest {
|
|
14
|
-
return typeof ev === 'object' && !!ev && 'type' in ev && typeof ev.type === 'string' && ev.type === 'run-test';
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
type RemoveTestEvent = { type: 'removeTest', method: string, import: string, classId: string };
|
|
18
|
-
|
|
19
|
-
export type TestWatchEvent =
|
|
20
|
-
TestEvent |
|
|
21
|
-
RemoveTestEvent |
|
|
22
|
-
{ type: 'ready' } |
|
|
23
|
-
{ type: 'log', message: string };
|
|
24
|
-
|
|
11
|
+
import { TestReadyEvent, TestRemovedEvent } from '../worker/types';
|
|
25
12
|
/**
|
|
26
13
|
* Test Watcher.
|
|
27
14
|
*
|
|
@@ -35,12 +22,20 @@ export class TestWatcher {
|
|
|
35
22
|
static async watch(format: string, runAllOnStart = true): Promise<void> {
|
|
36
23
|
console.debug('Listening for changes');
|
|
37
24
|
|
|
38
|
-
const itr = new WorkQueue<string | RunRequest>();
|
|
39
|
-
|
|
40
25
|
await SuiteRegistry.init();
|
|
41
26
|
SuiteRegistry.listen(RootRegistry);
|
|
27
|
+
await RootRegistry.init();
|
|
28
|
+
|
|
29
|
+
const events: TestRun[] = [];
|
|
30
|
+
|
|
31
|
+
if (runAllOnStart) {
|
|
32
|
+
const tests = await RunnerUtil.getTestDigest();
|
|
33
|
+
events.push(...RunnerUtil.getTestRuns(tests));
|
|
34
|
+
}
|
|
42
35
|
|
|
43
|
-
const
|
|
36
|
+
const itr = new AsyncQueue(events);
|
|
37
|
+
const consumer = new CumulativeSummaryConsumer(await TestConsumerRegistry.getInstance(format))
|
|
38
|
+
.withFilter(x => x.metadata?.partial !== true || x.type !== 'suite');
|
|
44
39
|
|
|
45
40
|
new MethodSource(RootRegistry).on(e => {
|
|
46
41
|
const [cls, method] = (e.prev ?? e.curr ?? []);
|
|
@@ -54,39 +49,40 @@ export class TestWatcher {
|
|
|
54
49
|
const conf = SuiteRegistry.getByClassAndMethod(cls, method)!;
|
|
55
50
|
if (e.type !== 'removing') {
|
|
56
51
|
if (conf) {
|
|
57
|
-
const
|
|
58
|
-
|
|
52
|
+
const run: TestRun = {
|
|
53
|
+
import: conf.import, classId: conf.classId, methodNames: [conf.methodName], metadata: { partial: true }
|
|
54
|
+
};
|
|
55
|
+
console.log('Triggering', run);
|
|
56
|
+
itr.add(run, true); // Shift to front
|
|
59
57
|
}
|
|
60
58
|
} else {
|
|
61
59
|
process.send?.({
|
|
62
60
|
type: 'removeTest',
|
|
61
|
+
methodNames: method?.name ? [method.name!] : undefined!,
|
|
63
62
|
method: method?.name,
|
|
64
63
|
classId: cls?.Ⲑid,
|
|
65
64
|
import: Runtime.getImport(cls)
|
|
66
|
-
} satisfies
|
|
65
|
+
} satisfies TestRemovedEvent);
|
|
67
66
|
}
|
|
68
67
|
});
|
|
69
68
|
|
|
70
|
-
// If a file is changed, but doesn't emit classes, re-run whole file
|
|
71
|
-
RootRegistry.onNonClassChanges(imp => itr.add(imp));
|
|
72
69
|
|
|
73
|
-
|
|
70
|
+
// If a file is changed, but doesn't emit classes, re-run whole file
|
|
71
|
+
RootRegistry.onNonClassChanges(imp => itr.add({ import: imp }));
|
|
74
72
|
|
|
75
73
|
process.on('message', ev => {
|
|
76
|
-
if (
|
|
74
|
+
if (typeof ev === 'object' && ev && 'type' in ev && ev.type === 'run-test') {
|
|
75
|
+
console.log('Received message', ev);
|
|
76
|
+
// Legacy
|
|
77
|
+
if ('file' in ev && typeof ev.file === 'string') {
|
|
78
|
+
ev = { import: RuntimeIndex.getFromSource(ev.file)?.import! };
|
|
79
|
+
}
|
|
77
80
|
console.debug('Manually triggered', ev);
|
|
78
|
-
itr.add(ev, true);
|
|
81
|
+
itr.add(castTo(ev), true);
|
|
79
82
|
}
|
|
80
83
|
});
|
|
81
84
|
|
|
82
|
-
process.send?.({ type: 'ready' });
|
|
83
|
-
|
|
84
|
-
if (runAllOnStart) {
|
|
85
|
-
for await (const imp of await RunnerUtil.getTestImports()) {
|
|
86
|
-
await Runtime.importFrom(imp);
|
|
87
|
-
itr.add(imp);
|
|
88
|
-
}
|
|
89
|
-
}
|
|
85
|
+
process.send?.({ type: 'ready' } satisfies TestReadyEvent);
|
|
90
86
|
|
|
91
87
|
await WorkPool.run(
|
|
92
88
|
buildStandardTestManager.bind(null, consumer),
|