@travetto/test 5.0.0-rc.8 → 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 } 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
@@ -39,8 +68,7 @@ export class TestExecutor {
39
68
 
40
69
  try {
41
70
  await pCap.run(() =>
42
- // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
43
- (suite.instance as Record<string, Function>)[test.methodName]()
71
+ castTo<Record<string, Function>>(suite.instance)[test.methodName]()
44
72
  );
45
73
  } finally {
46
74
  process.env = env; // Restore
@@ -54,35 +82,27 @@ export class TestExecutor {
54
82
  /**
55
83
  * Determining if we should skip
56
84
  */
57
- static async #skip(cfg: TestConfig | SuiteConfig, inst: unknown): Promise<boolean | undefined> {
58
- if (cfg.skip !== undefined) {
59
- if (typeof cfg.skip === 'boolean' ? cfg.skip : await cfg.skip(inst)) {
60
- return true;
61
- }
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;
62
92
  }
63
93
  }
64
94
 
65
- /**
66
- * Fail an entire file, marking the whole file as failed
67
- */
68
- static failFile(consumer: TestConsumer, imp: string, err: Error): void {
69
- const name = path.basename(imp);
70
- const classId = `${RuntimeIndex.getFromImport(imp)?.id}○${name}`;
71
- // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
72
- const suite = { class: { name }, classId, duration: 0, lineStart: 1, lineEnd: 1, import: imp, } as SuiteConfig & SuiteResult;
73
- err.message = err.message.replaceAll(Runtime.mainSourcePath, '.');
74
- const res = AssertUtil.generateSuiteError(suite, 'require', err);
75
- consumer.onEvent({ type: 'suite', phase: 'before', suite });
76
- consumer.onEvent({ type: 'test', phase: 'before', test: res.testConfig });
77
- consumer.onEvent({ type: 'assertion', phase: 'after', assertion: res.assert });
78
- consumer.onEvent({ type: 'test', phase: 'after', test: res.testResult });
79
- 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' } });
80
100
  }
81
101
 
82
102
  /**
83
103
  * An empty suite result based on a suite config
84
104
  */
85
- static createSuiteResult(suite: SuiteConfig): SuiteResult {
105
+ createSuiteResult(suite: SuiteConfig): SuiteResult {
86
106
  return {
87
107
  passed: 0,
88
108
  failed: 0,
@@ -100,10 +120,10 @@ export class TestExecutor {
100
120
  /**
101
121
  * Execute the test, capture output, assertions and promises
102
122
  */
103
- static async executeTest(consumer: TestConsumer, test: TestConfig, suite: SuiteConfig): Promise<TestResult> {
123
+ async executeTest(test: TestConfig): Promise<TestResult> {
104
124
 
105
125
  // Mark test start
106
- consumer.onEvent({ type: 'test', phase: 'before', test });
126
+ this.#consumer.onEvent({ type: 'test', phase: 'before', test });
107
127
 
108
128
  const startTime = Date.now();
109
129
 
@@ -115,6 +135,7 @@ export class TestExecutor {
115
135
  lineEnd: test.lineEnd,
116
136
  lineBodyStart: test.lineBodyStart,
117
137
  import: test.import,
138
+ sourceImport: test.sourceImport,
118
139
  status: 'skipped',
119
140
  assertions: [],
120
141
  duration: 0,
@@ -122,16 +143,12 @@ export class TestExecutor {
122
143
  output: {},
123
144
  };
124
145
 
125
- if (await this.#skip(test, suite.instance)) {
126
- return result;
127
- }
128
-
129
146
  // Emit every assertion as it occurs
130
- const getAssertions = AssertCapture.collector(test, assrt =>
131
- consumer.onEvent({
147
+ const getAssertions = AssertCapture.collector(test, asrt =>
148
+ this.#consumer.onEvent({
132
149
  type: 'assertion',
133
150
  phase: 'after',
134
- assertion: assrt
151
+ assertion: asrt
135
152
  })
136
153
  );
137
154
 
@@ -163,47 +180,27 @@ export class TestExecutor {
163
180
  });
164
181
 
165
182
  // Mark completion
166
- consumer.onEvent({ type: 'test', phase: 'after', test: result });
183
+ this.#consumer.onEvent({ type: 'test', phase: 'after', test: result });
167
184
 
168
185
  return result;
169
186
  }
170
187
 
171
188
  /**
172
- * Execute a single test within a suite
189
+ * Execute an entire suite
173
190
  */
174
- static async executeSuiteTest(consumer: TestConsumer, suite: SuiteConfig, test: TestConfig): Promise<void> {
175
- const result: SuiteResult = this.createSuiteResult(suite);
176
-
177
- const mgr = new TestPhaseManager(consumer, suite, result);
178
-
179
- try {
180
- await mgr.startPhase('all');
181
- const skip = await this.#skip(test, suite.instance);
182
- if (!skip) {
183
- await mgr.startPhase('each');
184
- }
185
- await this.executeTest(consumer, test, suite);
186
- if (!skip) {
187
- await mgr.endPhase('each');
188
- }
189
- await mgr.endPhase('all');
190
- } catch (err) {
191
- 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;
192
194
  }
193
- }
194
195
 
195
- /**
196
- * Execute an entire suite
197
- */
198
- static async executeSuite(consumer: TestConsumer, suite: SuiteConfig): Promise<SuiteResult> {
199
196
  const result: SuiteResult = this.createSuiteResult(suite);
200
197
 
201
198
  const startTime = Date.now();
202
199
 
203
200
  // Mark suite start
204
- consumer.onEvent({ phase: 'before', type: 'suite', suite });
201
+ this.#consumer.onEvent({ phase: 'before', type: 'suite', suite });
205
202
 
206
- const mgr = new TestPhaseManager(consumer, suite, result);
203
+ const mgr = new TestPhaseManager(suite, result, e => this.#onSuiteFailure(e));
207
204
 
208
205
  const originalEnv = { ...process.env };
209
206
 
@@ -213,28 +210,30 @@ export class TestExecutor {
213
210
 
214
211
  const suiteEnv = { ...process.env };
215
212
 
216
- 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
+
217
219
  // Reset env before each test
218
220
  process.env = { ...suiteEnv };
221
+
219
222
  const testStart = Date.now();
220
- const skip = await this.#skip(test, suite.instance);
221
- if (!skip) {
222
- // Handle BeforeEach
223
- await mgr.startPhase('each');
224
- }
223
+
224
+ // Handle BeforeEach
225
+ await mgr.startPhase('each');
225
226
 
226
227
  // Run test
227
- const ret = await this.executeTest(consumer, test, suite);
228
+ const ret = await this.executeTest(test);
228
229
  result[ret.status]++;
229
-
230
- if (!skip) {
231
- result.tests.push(ret);
232
- }
230
+ result.tests.push(ret);
233
231
 
234
232
  // Handle after each
235
233
  await mgr.endPhase('each');
236
234
  ret.durationTotal = Date.now() - testStart;
237
235
  }
236
+
238
237
  // Handle after all
239
238
  await mgr.endPhase('all');
240
239
  } catch (err) {
@@ -245,26 +244,23 @@ export class TestExecutor {
245
244
  process.env = { ...originalEnv };
246
245
 
247
246
  result.duration = Date.now() - startTime;
248
-
249
- // Mark suite complete
250
- consumer.onEvent({ phase: 'after', type: 'suite', suite: result });
251
-
252
247
  result.total = result.passed + result.failed;
253
248
 
254
- return result;
249
+ // Mark suite complete
250
+ this.#consumer.onEvent({ phase: 'after', type: 'suite', suite: result });
255
251
  }
256
252
 
257
253
  /**
258
254
  * Handle executing a suite's test/tests based on command line inputs
259
255
  */
260
- static async execute(consumer: TestConsumer, imp: string, ...args: string[]): Promise<void> {
256
+ async execute(run: TestRun): Promise<void> {
261
257
  try {
262
- await Runtime.importFrom(imp);
258
+ await Runtime.importFrom(run.import);
263
259
  } catch (err) {
264
260
  if (!(err instanceof Error)) {
265
261
  throw err;
266
262
  }
267
- this.failFile(consumer, imp, err);
263
+ this.#onSuiteFailure(AssertUtil.gernerateImportFailure(run.import, err));
268
264
  return;
269
265
  }
270
266
 
@@ -272,19 +268,10 @@ export class TestExecutor {
272
268
  await SuiteRegistry.init();
273
269
 
274
270
  // Convert inbound arguments to specific tests to run
275
- const params = SuiteRegistry.getRunParams(imp, ...args);
271
+ const suites = SuiteRegistry.getSuiteTests(run);
276
272
 
277
- // If running specific suites
278
- if ('suites' in params) {
279
- for (const suite of params.suites) {
280
- if (!(await this.#skip(suite, suite.instance)) && suite.tests.length) {
281
- await this.executeSuite(consumer, suite);
282
- }
283
- }
284
- } else if (params.test) { // If running a single test
285
- await this.executeSuiteTest(consumer, params.suite, params.test);
286
- } else { // Running the suite
287
- await this.executeSuite(consumer, params.suite);
273
+ for (const { suite, tests } of suites) {
274
+ await this.executeSuite(suite, tests);
288
275
  }
289
276
  }
290
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
  }
@@ -1,4 +1,5 @@
1
1
  import { createHook, executionAsyncId } from 'node:async_hooks';
2
+ import { isPromise } from 'node:util/types';
2
3
 
3
4
  import { ExecutionError } from '@travetto/worker';
4
5
  import { Util } from '@travetto/runtime';
@@ -11,9 +12,8 @@ export class PromiseCapturer {
11
12
  #id: number = 0;
12
13
 
13
14
  #init(id: number, type: string, triggerId: number, resource: unknown): void {
14
- if (this.#id && type === 'PROMISE' && triggerId === this.#id) {
15
- // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
16
- this.#pending.set(id, resource as Promise<unknown>);
15
+ if (this.#id && type === 'PROMISE' && triggerId === this.#id && isPromise(resource)) {
16
+ this.#pending.set(id, resource);
17
17
  }
18
18
  }
19
19
 
@@ -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
  }