@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.
@@ -1,12 +1,11 @@
1
1
  import { AssertionError } from 'node:assert';
2
- import path from 'node:path';
3
2
 
4
- import { Env, TimeUtil, Runtime, RuntimeIndex, castTo, asFull, Class } from '@travetto/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
- static async #executeTestMethod(test: TestConfig): Promise<Error | undefined> {
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
- static async #skip(cfg: TestConfig | SuiteConfig, inst: unknown): Promise<boolean | undefined> {
57
- if (cfg.skip !== undefined) {
58
- if (typeof cfg.skip === 'boolean' ? cfg.skip : await cfg.skip(inst)) {
59
- return true;
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
- * Fail an entire file, marking the whole file as failed
66
- */
67
- static failFile(consumer: TestConsumer, imp: string, err: Error): void {
68
- const name = path.basename(imp);
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
- static createSuiteResult(suite: SuiteConfig): SuiteResult {
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
- static async executeTest(consumer: TestConsumer, test: TestConfig, suite: SuiteConfig): Promise<TestResult> {
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 a single test within a suite
189
+ * Execute an entire suite
171
190
  */
172
- static async executeSuiteTest(consumer: TestConsumer, suite: SuiteConfig, test: TestConfig): Promise<void> {
173
- const result: SuiteResult = this.createSuiteResult(suite);
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(consumer, suite, result);
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
- const skip = await this.#skip(test, suite.instance);
219
- if (!skip) {
220
- // Handle BeforeEach
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(consumer, test, suite);
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
- return result;
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
- static async execute(consumer: TestConsumer, imp: string, ...args: string[]): Promise<void> {
256
+ async execute(run: TestRun): Promise<void> {
259
257
  try {
260
- await Runtime.importFrom(imp);
258
+ await Runtime.importFrom(run.import);
261
259
  } catch (err) {
262
260
  if (!(err instanceof Error)) {
263
261
  throw err;
264
262
  }
265
- this.failFile(consumer, imp, err);
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 params = SuiteRegistry.getRunParams(imp, ...args);
271
+ const suites = SuiteRegistry.getSuiteTests(run);
274
272
 
275
- // If running specific suites
276
- if ('suites' in params) {
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
  }
@@ -1,12 +1,12 @@
1
1
  import { Barrier } from '@travetto/worker';
2
2
  import { Env, TimeUtil } from '@travetto/runtime';
3
3
 
4
- import { TestConsumer } from '../consumer/types';
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(consumer: TestConsumer, suite: SuiteConfig, result: SuiteResult) {
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 res = await this.triggerSuiteError(`[[${phase}]]`, error);
61
- this.#result.tests.push(res);
62
- this.#result.failed++;
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
- if (!(err instanceof TestBreakout)) {
100
- const res = await this.triggerSuiteError('all', err);
101
- this.#result.tests.push(res);
102
- this.#result.failed++;
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
  }
@@ -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
- const imports = await RunnerUtil.getTestImports(this.#state.args);
31
+ console.debug('Running', { globs });
31
32
 
32
- console.debug('Running', { patterns: this.#state.args });
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
- const testCount = await RunnerUtil.getTestCount(this.#state.args);
35
- await consumer.onStart({ testCount });
37
+ await consumer.onStart({ testCount: tests.length });
36
38
  await WorkPool.run(
37
- buildStandardTestManager.bind(null, consumer),
38
- imports,
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
- let imp = RuntimeIndex.getFromImport(this.#state.args[0])?.import;
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
- if (!imp) {
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
+ const filter = (run.methodNames?.length) ?
65
+ (cfg: TestConfig): boolean => run.methodNames!.includes(cfg.methodName) :
66
+ undefined;
64
67
 
65
- const [, ...args] = this.#state.args;
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.execute(consumer, imp!, ...args);
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
- switch (this.#state.mode) {
77
- case 'single': return await this.runSingle();
78
- case 'standard': return await this.runFiles();
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
  }
@@ -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
- * Test mode
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
- showProgress?: boolean;
19
+ concurrency?: number;
23
20
  /**
24
- * Number of test suites to run concurrently, when mode is not single
21
+ * The tags to include or exclude from testing
25
22
  */
26
- concurrency: number;
23
+ tags?: string[];
27
24
  /**
28
- * Input arguments
25
+ * target
29
26
  */
30
- args: string[];
27
+ target: TestRun | {
28
+ /**
29
+ * Globs to run
30
+ */
31
+ globs?: string[];
32
+ };
31
33
  }
@@ -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 patterns
67
- * @param patterns
67
+ * Get count of tests for a given set of globs
68
+ * @param globs
68
69
  * @returns
69
70
  */
70
- static async getTestCount(patterns: string[]): Promise<number> {
71
+ static async getTestDigest(globs: string[] = ['**/*.ts'], tags?: string[]): Promise<TestConfig[]> {
71
72
  const countRes = await ExecUtil.getResult(
72
- spawn('npx', ['trv', 'test:count', ...patterns], {
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
- return countRes.valid ? +countRes.stdout : 0;
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
  }
@@ -1,27 +1,14 @@
1
1
  import { RootRegistry, MethodSource } from '@travetto/registry';
2
- import { WorkPool, WorkQueue } from '@travetto/worker';
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 { RunRequest } from '../worker/types';
9
+ import { TestRun } from '../model/test';
10
10
  import { RunnerUtil } from './util';
11
- import { TestEvent } from '../model/event';
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 consumer = new CumulativeSummaryConsumer(await TestConsumerRegistry.getInstance(format));
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 key = { import: conf.import, class: conf.class.name, method: conf.methodName };
58
- itr.add(key, true); // Shift to front
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 RemoveTestEvent);
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
- await RootRegistry.init();
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 (isRunRequest(ev)) {
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),