@travetto/test 7.0.0-rc.1 → 7.0.0-rc.3

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.
Files changed (46) hide show
  1. package/README.md +7 -8
  2. package/__index__.ts +1 -0
  3. package/package.json +7 -7
  4. package/src/assert/check.ts +46 -46
  5. package/src/assert/util.ts +31 -31
  6. package/src/communication.ts +66 -0
  7. package/src/consumer/registry-index.ts +11 -11
  8. package/src/consumer/types/cumulative.ts +91 -62
  9. package/src/consumer/types/delegating.ts +30 -27
  10. package/src/consumer/types/event.ts +11 -4
  11. package/src/consumer/types/exec.ts +12 -3
  12. package/src/consumer/types/runnable.ts +4 -3
  13. package/src/consumer/types/summarizer.ts +12 -10
  14. package/src/consumer/types/tap-summary.ts +22 -20
  15. package/src/consumer/types/tap.ts +15 -15
  16. package/src/consumer/types/xunit.ts +15 -15
  17. package/src/consumer/types.ts +6 -2
  18. package/src/decorator/suite.ts +2 -2
  19. package/src/decorator/test.ts +6 -4
  20. package/src/execute/barrier.ts +8 -8
  21. package/src/execute/console.ts +1 -1
  22. package/src/execute/executor.ts +32 -21
  23. package/src/execute/phase.ts +7 -7
  24. package/src/execute/run.ts +247 -0
  25. package/src/execute/types.ts +2 -17
  26. package/src/execute/watcher.ts +33 -60
  27. package/src/fixture.ts +2 -2
  28. package/src/model/common.ts +4 -0
  29. package/src/model/event.ts +3 -1
  30. package/src/model/suite.ts +10 -21
  31. package/src/model/test.ts +48 -2
  32. package/src/model/util.ts +8 -0
  33. package/src/registry/registry-adapter.ts +23 -21
  34. package/src/registry/registry-index.ts +25 -25
  35. package/src/worker/child.ts +21 -21
  36. package/src/worker/standard.ts +28 -19
  37. package/src/worker/types.ts +9 -5
  38. package/support/bin/run.ts +10 -10
  39. package/support/cli.test.ts +20 -41
  40. package/support/cli.test_diff.ts +47 -0
  41. package/support/cli.test_digest.ts +7 -7
  42. package/support/cli.test_direct.ts +13 -12
  43. package/support/cli.test_watch.ts +3 -8
  44. package/support/transformer.assert.ts +12 -12
  45. package/src/execute/runner.ts +0 -87
  46. package/src/execute/util.ts +0 -108
@@ -3,13 +3,15 @@ import { ClassInstance, getClass } from '@travetto/runtime';
3
3
  import { TestConfig, ThrowableError } from '../model/test.ts';
4
4
  import { SuiteRegistryIndex } from '../registry/registry-index.ts';
5
5
 
6
+ type MethodDecorator = (instance: ClassInstance, property: string, descriptor: PropertyDescriptor) => PropertyDescriptor | void;
7
+
6
8
  /**
7
9
  * The `@AssertCheck` indicates that a function's assert calls should be transformed
8
10
  * @augments `@travetto/test:AssertCheck`
9
11
  * @kind decorator
10
12
  */
11
13
  export function AssertCheck(): MethodDecorator {
12
- return (instance: ClassInstance, property: string | symbol, descriptor: PropertyDescriptor) => descriptor;
14
+ return (instance: ClassInstance, property: string, descriptor: PropertyDescriptor) => descriptor;
13
15
  }
14
16
 
15
17
  /**
@@ -24,7 +26,7 @@ export function Test(): MethodDecorator;
24
26
  export function Test(...rest: Partial<TestConfig>[]): MethodDecorator;
25
27
  export function Test(description: string, ...rest: Partial<TestConfig>[]): MethodDecorator;
26
28
  export function Test(description?: string | Partial<TestConfig>, ...rest: Partial<TestConfig>[]): MethodDecorator {
27
- return (instance: ClassInstance, property: string | symbol, descriptor: PropertyDescriptor) => {
29
+ return (instance: ClassInstance, property: string, descriptor: PropertyDescriptor) => {
28
30
  SuiteRegistryIndex.getForRegister(getClass(instance)).registerTest(property, descriptor.value,
29
31
  ...(typeof description !== 'string' && description) ? [description] : [],
30
32
  ...rest,
@@ -40,7 +42,7 @@ export function Test(description?: string | Partial<TestConfig>, ...rest: Partia
40
42
  * @kind decorator
41
43
  */
42
44
  export function ShouldThrow(state: ThrowableError): MethodDecorator {
43
- return (instance: ClassInstance, property: string | symbol, descriptor: PropertyDescriptor) => {
45
+ return (instance: ClassInstance, property: string, descriptor: PropertyDescriptor) => {
44
46
  SuiteRegistryIndex.getForRegister(getClass(instance)).registerTest(property, descriptor.value, { shouldThrow: state });
45
47
  return descriptor;
46
48
  };
@@ -52,7 +54,7 @@ export function ShouldThrow(state: ThrowableError): MethodDecorator {
52
54
  * @kind decorator
53
55
  */
54
56
  export function Timeout(ms: number): MethodDecorator {
55
- return (instance: ClassInstance, property: string | symbol, descriptor: PropertyDescriptor) => {
57
+ return (instance: ClassInstance, property: string, descriptor: PropertyDescriptor) => {
56
58
  SuiteRegistryIndex.getForRegister(getClass(instance)).registerTest(property, descriptor.value, { timeout: ms });
57
59
  return descriptor;
58
60
  };
@@ -11,14 +11,14 @@ export class Barrier {
11
11
  /**
12
12
  * Track timeout
13
13
  */
14
- static timeout(duration: number | TimeSpan, op: string = 'Operation'): { promise: Promise<void>, resolve: () => unknown } {
14
+ static timeout(duration: number | TimeSpan, operation: string = 'Operation'): { promise: Promise<void>, resolve: () => unknown } {
15
15
  const resolver = Promise.withResolvers<void>();
16
16
  const durationMs = TimeUtil.asMillis(duration);
17
17
  let timeout: NodeJS.Timeout;
18
18
  if (!durationMs) {
19
19
  resolver.resolve();
20
20
  } else {
21
- const msg = `${op} timed out after ${duration}${typeof duration === 'number' ? 'ms' : ''}`;
21
+ const msg = `${operation} timed out after ${duration}${typeof duration === 'number' ? 'ms' : ''}`;
22
22
  timeout = setTimeout(() => resolver.reject(new TimeoutError(msg)), durationMs).unref();
23
23
  }
24
24
 
@@ -31,9 +31,9 @@ export class Barrier {
31
31
  */
32
32
  static uncaughtErrorPromise(): { promise: Promise<void>, resolve: () => unknown } {
33
33
  const uncaught = Promise.withResolvers<void>();
34
- const onError = (err: Error): void => { Util.queueMacroTask().then(() => uncaught.reject(err)); };
35
- UNCAUGHT_ERR_EVENTS.map(k => process.on(k, onError));
36
- uncaught.promise.finally(() => { UNCAUGHT_ERR_EVENTS.map(k => process.off(k, onError)); });
34
+ const onError = (error: Error): void => { Util.queueMacroTask().then(() => uncaught.reject(error)); };
35
+ UNCAUGHT_ERR_EVENTS.map(key => process.on(key, onError));
36
+ uncaught.promise.finally(() => { UNCAUGHT_ERR_EVENTS.map(key => process.off(key, onError)); });
37
37
  return uncaught;
38
38
  }
39
39
 
@@ -80,7 +80,7 @@ export class Barrier {
80
80
  /**
81
81
  * Wait for operation to finish, with timeout and unhandled error support
82
82
  */
83
- static async awaitOperation(timeout: number | TimeSpan, op: () => Promise<unknown>): Promise<Error | undefined> {
83
+ static async awaitOperation(timeout: number | TimeSpan, operation: () => Promise<unknown>): Promise<Error | undefined> {
84
84
  const uncaught = this.uncaughtErrorPromise();
85
85
  const timer = this.timeout(timeout);
86
86
  const promises = this.capturePromises();
@@ -88,9 +88,9 @@ export class Barrier {
88
88
  try {
89
89
  await promises.start();
90
90
  let capturedError: Error | undefined;
91
- const opProm = op().then(() => promises.finish());
91
+ const opProm = operation().then(() => promises.finish());
92
92
 
93
- await Promise.race([opProm, uncaught.promise, timer.promise]).catch(err => capturedError ??= err);
93
+ await Promise.race([opProm, uncaught.promise, timer.promise]).catch(error => capturedError ??= error);
94
94
 
95
95
  return capturedError;
96
96
  } finally {
@@ -22,7 +22,7 @@ export class ConsoleCapture implements ConsoleListener {
22
22
  this.out.push({
23
23
  ...rest,
24
24
  message: args
25
- .map((x => typeof x === 'string' ? x : util.inspect(x, false, 5)))
25
+ .map((arg => typeof arg === 'string' ? arg : util.inspect(arg, false, 5)))
26
26
  .join(' ')
27
27
  });
28
28
  }
@@ -3,7 +3,7 @@ import { AssertionError } from 'node:assert';
3
3
  import { Env, TimeUtil, Runtime, castTo, classConstruct } from '@travetto/runtime';
4
4
  import { Registry } from '@travetto/registry';
5
5
 
6
- import { TestConfig, TestResult, TestRun } from '../model/test.ts';
6
+ import { TestConfig, TestResult, type TestRun } from '../model/test.ts';
7
7
  import { SuiteConfig, SuiteFailure, SuiteResult } from '../model/suite.ts';
8
8
  import { TestConsumerShape } from '../consumer/types.ts';
9
9
  import { AssertCheck } from '../assert/check.ts';
@@ -14,8 +14,9 @@ import { AssertUtil } from '../assert/util.ts';
14
14
  import { Barrier } from './barrier.ts';
15
15
  import { ExecutionError } from './error.ts';
16
16
  import { SuiteRegistryIndex } from '../registry/registry-index.ts';
17
+ import { TestModelUtil } from '../model/util.ts';
17
18
 
18
- const TEST_TIMEOUT = TimeUtil.fromValue(Env.TRV_TEST_TIMEOUT.val) ?? 5000;
19
+ const TEST_TIMEOUT = TimeUtil.fromValue(Env.TRV_TEST_TIMEOUT.value) ?? 5000;
19
20
 
20
21
  /**
21
22
  * Support execution of the tests
@@ -73,8 +74,8 @@ export class TestExecutor {
73
74
  /**
74
75
  * Determining if we should skip
75
76
  */
76
- async #shouldSkip(cfg: TestConfig | SuiteConfig, inst: unknown): Promise<boolean | undefined> {
77
- if (typeof cfg.skip === 'function' ? await cfg.skip(inst) : cfg.skip) {
77
+ async #shouldSkip(config: TestConfig | SuiteConfig, inst: unknown): Promise<boolean | undefined> {
78
+ if (typeof config.skip === 'function' ? await config.skip(inst) : config.skip) {
78
79
  return true;
79
80
  }
80
81
  }
@@ -94,13 +95,16 @@ export class TestExecutor {
94
95
  passed: 0,
95
96
  failed: 0,
96
97
  skipped: 0,
98
+ unknown: 0,
97
99
  total: 0,
100
+ status: 'unknown',
98
101
  lineStart: suite.lineStart,
99
102
  lineEnd: suite.lineEnd,
100
103
  import: suite.import,
101
104
  classId: suite.classId,
105
+ sourceHash: suite.sourceHash,
102
106
  duration: 0,
103
- tests: []
107
+ tests: {}
104
108
  };
105
109
  }
106
110
 
@@ -118,12 +122,14 @@ export class TestExecutor {
118
122
  methodName: test.methodName,
119
123
  description: test.description,
120
124
  classId: test.classId,
125
+ tags: test.tags,
121
126
  lineStart: test.lineStart,
122
127
  lineEnd: test.lineEnd,
123
128
  lineBodyStart: test.lineBodyStart,
124
129
  import: test.import,
125
130
  sourceImport: test.sourceImport,
126
- status: 'skipped',
131
+ sourceHash: test.sourceHash,
132
+ status: 'unknown',
127
133
  assertions: [],
128
134
  duration: 0,
129
135
  durationTotal: 0,
@@ -184,19 +190,23 @@ export class TestExecutor {
184
190
  }
185
191
 
186
192
  const result: SuiteResult = this.createSuiteResult(suite);
193
+ const validTestMethodNames = new Set(tests.map(t => t.methodName));
194
+ const testConfigs = Object.fromEntries(
195
+ Object.entries(suite.tests).filter(([key]) => validTestMethodNames.has(key))
196
+ );
187
197
 
188
198
  const startTime = Date.now();
189
199
 
190
200
  // Mark suite start
191
- this.#consumer.onEvent({ phase: 'before', type: 'suite', suite });
201
+ this.#consumer.onEvent({ phase: 'before', type: 'suite', suite: { ...suite, tests: testConfigs } });
192
202
 
193
- const mgr = new TestPhaseManager(suite, result, e => this.#onSuiteFailure(e));
203
+ const manager = new TestPhaseManager(suite, result, event => this.#onSuiteFailure(event));
194
204
 
195
205
  const originalEnv = { ...process.env };
196
206
 
197
207
  try {
198
208
  // Handle the BeforeAll calls
199
- await mgr.startPhase('all');
209
+ await manager.startPhase('all');
200
210
 
201
211
  const suiteEnv = { ...process.env };
202
212
 
@@ -212,29 +222,30 @@ export class TestExecutor {
212
222
  const testStart = Date.now();
213
223
 
214
224
  // Handle BeforeEach
215
- await mgr.startPhase('each');
225
+ await manager.startPhase('each');
216
226
 
217
227
  // Run test
218
228
  const testResult = await this.executeTest(test);
219
229
  result[testResult.status]++;
220
- result.tests.push(testResult);
230
+ result.tests[testResult.methodName] = testResult;
221
231
 
222
232
  // Handle after each
223
- await mgr.endPhase('each');
233
+ await manager.endPhase('each');
224
234
  testResult.durationTotal = Date.now() - testStart;
225
235
  }
226
236
 
227
237
  // Handle after all
228
- await mgr.endPhase('all');
229
- } catch (err) {
230
- await mgr.onError(err);
238
+ await manager.endPhase('all');
239
+ } catch (error) {
240
+ await manager.onError(error);
231
241
  }
232
242
 
233
243
  // Restore env
234
244
  process.env = { ...originalEnv };
235
245
 
236
246
  result.duration = Date.now() - startTime;
237
- result.total = result.passed + result.failed;
247
+ result.total = result.passed + result.failed + result.skipped;
248
+ result.status = TestModelUtil.countsToTestStatus(result);
238
249
 
239
250
  // Mark suite complete
240
251
  this.#consumer.onEvent({ phase: 'after', type: 'suite', suite: result });
@@ -246,12 +257,12 @@ export class TestExecutor {
246
257
  async execute(run: TestRun): Promise<void> {
247
258
  try {
248
259
  await Runtime.importFrom(run.import);
249
- } catch (err) {
250
- if (!(err instanceof Error)) {
251
- throw err;
260
+ } catch (error) {
261
+ if (!(error instanceof Error)) {
262
+ throw error;
252
263
  }
253
- console.error(err);
254
- this.#onSuiteFailure(AssertUtil.gernerateImportFailure(run.import, err));
264
+ console.error(error);
265
+ this.#onSuiteFailure(AssertUtil.gernerateImportFailure(run.import, error));
255
266
  return;
256
267
  }
257
268
 
@@ -8,7 +8,7 @@ class TestBreakout extends Error {
8
8
  source?: Error;
9
9
  }
10
10
 
11
- const TEST_PHASE_TIMEOUT = TimeUtil.fromValue(Env.TRV_TEST_PHASE_TIMEOUT.val) ?? 15000;
11
+ const TEST_PHASE_TIMEOUT = TimeUtil.fromValue(Env.TRV_TEST_PHASE_TIMEOUT.value) ?? 15000;
12
12
 
13
13
  /**
14
14
  * Test Phase Execution Manager.
@@ -67,9 +67,9 @@ export class TestPhaseManager {
67
67
  /**
68
68
  * On error, handle stubbing out error for the phases in progress
69
69
  */
70
- async onError(err: Error | unknown): Promise<void> {
71
- if (!(err instanceof Error)) {
72
- throw err;
70
+ async onError(error: Error | unknown): Promise<void> {
71
+ if (!(error instanceof Error)) {
72
+ throw error;
73
73
  }
74
74
 
75
75
  for (const ph of this.#progress) {
@@ -82,12 +82,12 @@ export class TestPhaseManager {
82
82
 
83
83
  const failure = AssertUtil.generateSuiteFailure(
84
84
  this.#suite,
85
- err instanceof TestBreakout ? err.message : 'all',
86
- err instanceof TestBreakout ? err.source! : err
85
+ error instanceof TestBreakout ? error.message : 'all',
86
+ error instanceof TestBreakout ? error.source! : error
87
87
  );
88
88
 
89
89
  this.#onSuiteFailure(failure);
90
- this.#result.tests.push(failure.testResult);
90
+ this.#result.tests[failure.testResult.methodName] = failure.testResult;
91
91
  this.#result.failed++;
92
92
  }
93
93
  }
@@ -0,0 +1,247 @@
1
+ import { createReadStream } from 'node:fs';
2
+ import fs from 'node:fs/promises';
3
+ import readline from 'node:readline/promises';
4
+ import path from 'node:path';
5
+
6
+ import { Env, ExecUtil, ShutdownManager, Util, RuntimeIndex, Runtime, TimeUtil, JSONUtil } from '@travetto/runtime';
7
+ import { WorkPool } from '@travetto/worker';
8
+ import { Registry } from '@travetto/registry';
9
+
10
+ import type { TestConfig, TestRunInput, TestRun, TestGlobInput, TestDiffInput } from '../model/test.ts';
11
+ import type { TestRemoveEvent } from '../model/event.ts';
12
+ import type { TestConsumerShape } from '../consumer/types.ts';
13
+ import { RunnableTestConsumer } from '../consumer/types/runnable.ts';
14
+ import type { TestConsumerConfig } from './types.ts';
15
+ import { TestConsumerRegistryIndex } from '../consumer/registry-index.ts';
16
+ import { TestExecutor } from './executor.ts';
17
+ import { buildStandardTestManager } from '../worker/standard.ts';
18
+ import { SuiteRegistryIndex } from '../registry/registry-index.ts';
19
+
20
+ type RunState = {
21
+ runs: TestRun[];
22
+ removes?: TestRemoveEvent[];
23
+ };
24
+
25
+ /**
26
+ * Test Utilities for Running
27
+ */
28
+ export class RunUtil {
29
+ /**
30
+ * Add 50 ms to the shutdown to allow for buffers to output properly
31
+ */
32
+ static registerCleanup(scope: string): void {
33
+ ShutdownManager.onGracefulShutdown(() => Util.blockingTimeout(50), `test.${scope}.bufferOutput`);
34
+ }
35
+
36
+ /**
37
+ * Determine if a given file path is a valid test file
38
+ */
39
+ static async isTestFile(file: string): Promise<boolean> {
40
+ const reader = readline.createInterface({ input: createReadStream(file) });
41
+ const state = { imp: false, suite: false };
42
+ for await (const line of reader) {
43
+ state.imp ||= line.includes('@travetto/test');
44
+ state.suite ||= line.includes('Suite'); // Decorator or name
45
+ if (state.imp && state.suite) {
46
+ reader.close();
47
+ return true;
48
+ }
49
+ }
50
+ return false;
51
+ }
52
+
53
+ /**
54
+ * Find all valid test files given the globs
55
+ */
56
+ static async* getTestImports(globs?: string[]): AsyncIterable<string> {
57
+ const all = RuntimeIndex.find({
58
+ module: mod => mod.roles.includes('test') || mod.roles.includes('std'),
59
+ folder: folder => folder === 'test',
60
+ file: file => file.role === 'test'
61
+ });
62
+
63
+ // Collect globs
64
+ if (globs?.length) {
65
+ const allFiles = new Map(all.map(file => [file.sourceFile, file]));
66
+ for await (const item of fs.glob(globs)) {
67
+ const source = Runtime.workspaceRelative(path.resolve(item));
68
+ const match = allFiles.get(source);
69
+ if (match && await this.isTestFile(match.sourceFile)) {
70
+ yield match.import;
71
+ }
72
+ }
73
+ } else {
74
+ for await (const match of all) {
75
+ if (await this.isTestFile(match.sourceFile)) {
76
+ yield match.import;
77
+ }
78
+ }
79
+ }
80
+ }
81
+
82
+ /**
83
+ * Get count of tests for a given set of globs
84
+ * @param input
85
+ */
86
+ static async resolveGlobInput({ globs, tags, metadata }: TestGlobInput): Promise<TestRun[]> {
87
+ const digestProcess = await ExecUtil.getResult(
88
+ ExecUtil.spawnTrv('test:digest', ['-o', 'json', ...globs], {
89
+ env: { ...process.env, ...Env.FORCE_COLOR.export(0), ...Env.NO_COLOR.export(true) },
90
+ }),
91
+ { catch: true }
92
+ );
93
+
94
+ if (!digestProcess.valid) {
95
+ throw new Error(digestProcess.stderr);
96
+ }
97
+
98
+ const testFilter = tags?.length ?
99
+ Util.allowDeny<string, [TestConfig]>(
100
+ tags,
101
+ rule => rule,
102
+ (rule, core) => core.tags?.includes(rule) ?? false
103
+ ) :
104
+ ((): boolean => true);
105
+
106
+ const parsed: TestConfig[] = JSONUtil.parseSafe(digestProcess.stdout);
107
+
108
+ const events = parsed.filter(testFilter).reduce((runs, test) => {
109
+ if (!runs.has(test.classId)) {
110
+ runs.set(test.classId, { import: test.import, classId: test.classId, methodNames: [], runId: Util.uuid(), metadata });
111
+ }
112
+ runs.get(test.classId)!.methodNames!.push(test.methodName);
113
+ return runs;
114
+ }, new Map<string, TestRun>());
115
+
116
+ return [...events.values()].sort((a, b) => a.runId!.localeCompare(b.runId!));
117
+ }
118
+
119
+ /**
120
+ * Resolve a test diff source to ensure we are only running changed tests
121
+ */
122
+ static async resolveDiffInput({ import: importPath, diffSource: diff, metadata }: TestDiffInput): Promise<RunState> {
123
+ // Runs, defaults to new classes
124
+ const runs: TestRun[] = [];
125
+ const addRun = (clsId: string | undefined, methods?: string[]): void => {
126
+ runs.push({ import: importPath, classId: clsId, methodNames: methods?.length ? methods : undefined, metadata });
127
+ };
128
+ const removes: TestRemoveEvent[] = [];
129
+ const removeTest = (clsId: string, methodName?: string): void => {
130
+ removes.push({ type: 'removeTest', import: importPath, classId: clsId, methodName });
131
+ };
132
+
133
+ const imported = await Registry.manualInit([importPath]);
134
+ const classes = Object.fromEntries(
135
+ imported
136
+ .filter(cls => SuiteRegistryIndex.hasConfig(cls))
137
+ .map(cls => [cls.Ⲑid, SuiteRegistryIndex.getConfig(cls)])
138
+ );
139
+
140
+ // New classes
141
+ for (const clsId of Object.keys(classes)) {
142
+ if (!diff[clsId]) {
143
+ addRun(clsId);
144
+ }
145
+ }
146
+
147
+ // Looking at Diff
148
+ for (const [clsId, config] of Object.entries(diff)) {
149
+ const local = classes[clsId];
150
+ if (!local) { // Removed classes
151
+ removeTest(clsId);
152
+ } else if (local.sourceHash !== config.sourceHash) { // Class changed or added
153
+ // Methods to run, defaults to newly added
154
+ const methods: string[] = Object.keys(local.tests ?? {}).filter(key => !config.methods[key]);
155
+ let didRemove = false;
156
+ for (const key of Object.keys(config.methods)) {
157
+ const localMethod = local.tests?.[key];
158
+ if (!localMethod) { // Test is removed
159
+ removeTest(clsId, key);
160
+ didRemove = true;
161
+ } else if (localMethod.sourceHash !== config.methods[key]) { // Method changed or added
162
+ methods.push(key);
163
+ }
164
+ }
165
+ if (!didRemove || methods.length > 0) {
166
+ addRun(clsId, methods);
167
+ }
168
+ }
169
+ }
170
+
171
+ if (runs.length === 0 && removes.length === 0) { // Re-run entire file, classes unchanged
172
+ addRun(undefined);
173
+ }
174
+
175
+ return { runs, removes };
176
+ }
177
+
178
+ /**
179
+ * Reinitialize the manifest if needed, mainly for single test runs
180
+ */
181
+ static async reinitManifestIfNeeded(runs: TestRun[]): Promise<void> {
182
+ if (runs.length === 1) {
183
+ const [run] = runs;
184
+ const entry = RuntimeIndex.getFromImport(run.import)!;
185
+
186
+ if (entry.module !== Runtime.main.name) {
187
+ RuntimeIndex.reinitForModule(entry.module);
188
+ }
189
+ }
190
+ }
191
+
192
+ /**
193
+ * Build test consumer that wraps a given targeted consumer, and the tests to be run
194
+ */
195
+ static async getRunnableConsumer(target: TestConsumerShape, testRuns: TestRun[]): Promise<RunnableTestConsumer> {
196
+ const consumer = new RunnableTestConsumer(target);
197
+ const testCount = testRuns.reduce((acc, cur) => acc + (cur.methodNames ? cur.methodNames.length : 0), 0);
198
+
199
+ await consumer.onStart({ testCount });
200
+ return consumer;
201
+ }
202
+
203
+ /**
204
+ * Resolve input into run state
205
+ */
206
+ static async resolveInput(input: TestRunInput): Promise<RunState> {
207
+ if ('diffSource' in input) {
208
+ return await this.resolveDiffInput(input);
209
+ } else if ('globs' in input) {
210
+ return { runs: await this.resolveGlobInput(input) };
211
+ } else {
212
+ return { runs: [input], removes: [] };
213
+ }
214
+ }
215
+
216
+ /**
217
+ * Run tests
218
+ */
219
+ static async runTests(consumerConfig: TestConsumerConfig, input: TestRunInput): Promise<boolean | undefined> {
220
+ const { runs, removes } = await this.resolveInput(input);
221
+
222
+ await this.reinitManifestIfNeeded(runs);
223
+
224
+ const targetConsumer = await TestConsumerRegistryIndex.getInstance(consumerConfig);
225
+ const consumer = await this.getRunnableConsumer(targetConsumer, runs);
226
+
227
+ for (const item of removes ?? []) {
228
+ consumer.onRemoveEvent(item);
229
+ }
230
+
231
+ if (runs.length === 1) {
232
+ await new TestExecutor(consumer).execute(runs[0]);
233
+ } else {
234
+ await WorkPool.run(
235
+ run => buildStandardTestManager(consumer, run),
236
+ runs,
237
+ {
238
+ idleTimeoutMillis: TimeUtil.asMillis(10, 's'),
239
+ min: 1,
240
+ max: consumerConfig.concurrency
241
+ }
242
+ );
243
+ }
244
+
245
+ return consumer.summarizeAsBoolean();
246
+ }
247
+ }
@@ -1,9 +1,7 @@
1
- import { TestRun } from '../model/test.ts';
2
-
3
1
  /**
4
- * Run state
2
+ * Test Consumer Configuration
5
3
  */
6
- export interface RunState {
4
+ export interface TestConsumerConfig {
7
5
  /**
8
6
  * Test result consumer
9
7
  */
@@ -16,17 +14,4 @@ export interface RunState {
16
14
  * Number of test suites to run concurrently, when mode is not single
17
15
  */
18
16
  concurrency?: number;
19
- /**
20
- * The tags to include or exclude from testing
21
- */
22
- tags?: string[];
23
- /**
24
- * target
25
- */
26
- target: TestRun | {
27
- /**
28
- * Globs to run
29
- */
30
- globs?: string[];
31
- };
32
17
  }