@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.
@@ -1,19 +1,18 @@
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';
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
- static async #executeTestMethod(test: TestConfig): Promise<Error | undefined> {
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
- const barrier = new Barrier(test.timeout || TEST_TIMEOUT, true)
35
- .add(async () => {
36
- const env = process.env;
37
- process.env = { ...env }; // Created an isolated environment
38
- const pCap = new PromiseCapturer();
39
-
40
- try {
41
- await pCap.run(() =>
42
- castTo<Record<string, Function>>(suite.instance)[test.methodName]()
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
- 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
- }
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
- * 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 } });
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
- static createSuiteResult(suite: SuiteConfig): SuiteResult {
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
- static async executeTest(consumer: TestConsumer, test: TestConfig, suite: SuiteConfig): Promise<TestResult> {
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 a single test within a suite
175
+ * Execute an entire suite
171
176
  */
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);
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(consumer, suite, result);
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
- const skip = await this.#skip(test, suite.instance);
219
- if (!skip) {
220
- // Handle BeforeEach
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(consumer, test, suite);
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
- return result;
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
- static async execute(consumer: TestConsumer, imp: string, ...args: string[]): Promise<void> {
242
+ async execute(run: TestRun): Promise<void> {
259
243
  try {
260
- await Runtime.importFrom(imp);
244
+ await Runtime.importFrom(run.import);
261
245
  } catch (err) {
262
246
  if (!(err instanceof Error)) {
263
247
  throw err;
264
248
  }
265
- this.failFile(consumer, imp, err);
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 params = SuiteRegistry.getRunParams(imp, ...args);
257
+ const suites = SuiteRegistry.getSuiteTests(run);
258
+ if (!suites.length) {
259
+ console.warn('Unable to find suites for ', run);
260
+ }
274
261
 
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);
262
+ for (const { suite, tests } of suites) {
263
+ await this.executeSuite(suite, tests);
286
264
  }
287
265
  }
288
266
  }
@@ -1,12 +1,12 @@
1
- import { Barrier } from '@travetto/worker';
2
1
  import { Env, TimeUtil } from '@travetto/runtime';
3
2
 
4
- import { TestConsumer } from '../consumer/types';
5
- import { SuiteConfig, SuiteResult } from '../model/suite';
3
+ import { SuiteConfig, SuiteFailure, SuiteResult } from '../model/suite';
6
4
  import { AssertUtil } from '../assert/util';
7
- import { TestResult } from '../model/test';
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(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
  /**
@@ -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 new Barrier(TEST_PHASE_TIMEOUT, true)
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 res = await this.triggerSuiteError(`[[${phase}]]`, error);
61
- this.#result.tests.push(res);
62
- this.#result.failed++;
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
- if (!(err instanceof TestBreakout)) {
100
- const res = await this.triggerSuiteError('all', err);
101
- this.#result.tests.push(res);
102
- this.#result.failed++;
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
  }
@@ -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
- 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,26 @@ 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
-
65
- const [, ...args] = this.#state.args;
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(consumer, imp!, ...args);
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
- switch (this.#state.mode) {
77
- case 'single': return await this.runSingle();
78
- case 'standard': return await this.runFiles();
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
  }
@@ -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
  }