@travetto/test 8.0.0-alpha.0 → 8.0.0-alpha.10

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.
@@ -26,38 +26,18 @@ export class TestExecutor {
26
26
  this.#consumer = consumer;
27
27
  }
28
28
 
29
- #onSuiteTestError(result: TestResult, test: TestConfig): void {
30
- this.#consumer.onEvent({ type: 'test', phase: 'before', test });
31
- for (const assertion of result.assertions) {
32
- this.#consumer.onEvent({ type: 'assertion', phase: 'after', assertion });
33
- }
34
- this.#consumer.onEvent({ type: 'test', phase: 'after', test: result });
35
- }
36
-
37
- #recordSuiteErrors(suiteConfig: SuiteConfig, suiteResult: SuiteResult, errors: TestResult[]): void {
38
- for (const test of errors) {
39
- if (!suiteResult.tests[test.methodName]) {
40
- this.#onSuiteTestError(test, suiteConfig.tests[test.methodName]);
41
- suiteResult.errored += 1;
42
- suiteResult.total += 1;
43
- }
44
- }
45
- }
46
-
47
29
  /**
48
30
  * Raw execution, runs the method and then returns any thrown errors as the result.
49
31
  *
50
32
  * This method should never throw under any circumstances.
51
33
  */
52
- async #executeTestMethod(test: TestConfig): Promise<Error | undefined> {
53
- const suite = SuiteRegistryIndex.getConfig(test.class);
54
-
34
+ async #executeTestMethod(instance: unknown, test: TestConfig): Promise<Error | undefined> {
55
35
  // Ensure all the criteria below are satisfied before moving forward
56
36
  return Barrier.awaitOperation(test.timeout || TEST_TIMEOUT, async () => {
57
37
  const env = process.env;
58
38
  process.env = { ...env }; // Created an isolated environment
59
39
  try {
60
- await castTo<Record<string, Function>>(suite.instance)[test.methodName]();
40
+ await castTo<Record<string, Function>>(instance)[test.methodName]();
61
41
  } finally {
62
42
  process.env = env; // Restore
63
43
  }
@@ -73,95 +53,43 @@ export class TestExecutor {
73
53
  }
74
54
  }
75
55
 
76
- #skipTest(test: TestConfig, result: SuiteResult): void {
77
- // Mark test start
78
- this.#consumer.onEvent({ type: 'test', phase: 'before', test });
79
- result.skipped += 1;
80
- result.total += 1;
81
- this.#consumer.onEvent({
82
- type: 'test',
83
- phase: 'after',
84
- test: {
85
- ...test,
86
- suiteLineStart: result.lineStart,
87
- assertions: [], duration: 0, durationTotal: 0, output: [], status: 'skipped'
88
- }
89
- });
90
- }
91
-
92
- /**
93
- * An empty suite result based on a suite config
94
- */
95
- createSuiteResult(suite: SuiteConfig): SuiteResult {
96
- return {
97
- passed: 0,
98
- failed: 0,
99
- errored: 0,
100
- skipped: 0,
101
- unknown: 0,
102
- total: 0,
103
- status: 'unknown',
104
- lineStart: suite.lineStart,
105
- lineEnd: suite.lineEnd,
106
- import: suite.import,
107
- classId: suite.classId,
108
- sourceHash: suite.sourceHash,
109
- duration: 0,
110
- tests: {}
111
- };
112
- }
113
-
114
56
  /**
115
57
  * Execute the test, capture output, assertions and promises
116
58
  */
117
- async executeTest(test: TestConfig, suite: SuiteConfig): Promise<TestResult> {
59
+ async executeTest(instance: unknown, test: TestConfig, suite: SuiteConfig, override?: Partial<TestResult>): Promise<TestResult> {
60
+
61
+ const result = TestModelUtil.createTestResult(suite, test, override);
118
62
 
119
63
  // Mark test start
120
64
  this.#consumer.onEvent({ type: 'test', phase: 'before', test });
121
65
 
122
- const startTime = Date.now();
123
-
124
- const result: TestResult = {
125
- methodName: test.methodName,
126
- description: test.description,
127
- classId: test.classId,
128
- tags: test.tags,
129
- suiteLineStart: suite.lineStart,
130
- lineStart: test.lineStart,
131
- lineEnd: test.lineEnd,
132
- lineBodyStart: test.lineBodyStart,
133
- import: test.import,
134
- declarationImport: test.declarationImport,
135
- sourceHash: test.sourceHash,
136
- status: 'unknown',
137
- assertions: [],
138
- duration: 0,
139
- durationTotal: 0,
140
- output: [],
141
- };
142
66
 
143
67
  // Emit every assertion as it occurs
144
- const getAssertions = AssertCapture.collector(test, asrt =>
145
- this.#consumer.onEvent({
146
- type: 'assertion',
147
- phase: 'after',
148
- assertion: asrt
149
- })
150
- );
68
+ const getAssertions = AssertCapture.collector(test, item =>
69
+ this.#consumer.onEvent({ type: 'assertion', phase: 'after', assertion: item }));
151
70
 
152
71
  const consoleCapture = new ConsoleCapture().start(); // Capture all output from transpiled code
153
72
 
154
- // Run method and get result
155
- const error = await this.#executeTestMethod(test);
156
- const [status, finalError] = AssertCheck.validateTestResultError(test, error);
73
+ // Already finished
74
+ if (result.status !== 'unknown') {
75
+ if (result.error) {
76
+ result.assertions.push(AssertUtil.generateAssertion({ suite, test, error: result.error }));
77
+ }
78
+ for (const item of result.assertions ?? []) { AssertCapture.add(item); }
79
+ } else {
80
+ // Run method and get result
81
+ const startTime = Date.now();
82
+ const error = await this.#executeTestMethod(instance, test);
83
+ const [status, finalError] = AssertCheck.validateTestResultError(test, error);
84
+ result.status = status;
85
+ result.selfDuration = Date.now() - startTime;
86
+ if (finalError) {
87
+ result.error = finalError;
88
+ }
89
+ }
157
90
 
158
- Object.assign(result, {
159
- status,
160
- output: consoleCapture.end(),
161
- assertions: getAssertions(),
162
- duration: Date.now() - startTime,
163
- ...(finalError ? { error: finalError } : {})
164
- });
91
+ result.output = consoleCapture.end();
92
+ result.assertions = getAssertions();
165
93
 
166
94
  // Mark completion
167
95
  this.#consumer.onEvent({ type: 'test', phase: 'after', test: result });
@@ -174,75 +102,102 @@ export class TestExecutor {
174
102
  */
175
103
  async executeSuite(suite: SuiteConfig, tests: TestConfig[]): Promise<void> {
176
104
 
177
- suite.instance = classConstruct(suite.class);
105
+ const instance = classConstruct(suite.class);
178
106
 
179
- if (!tests.length || await this.#shouldSkip(suite, suite.instance)) {
107
+ const shouldSkip = await this.#shouldSkip(suite, instance);
108
+
109
+ const result: SuiteResult = TestModelUtil.createSuiteResult(suite);
110
+
111
+ if (shouldSkip) {
112
+ this.#consumer.onEvent({
113
+ phase: 'after', type: 'suite',
114
+ suite: {
115
+ ...result,
116
+ status: 'skipped',
117
+ skipped: tests.length,
118
+ total: tests.length
119
+ }
120
+ });
121
+ }
122
+
123
+ if (shouldSkip || !tests.length) {
180
124
  return;
181
125
  }
182
126
 
183
- const result: SuiteResult = this.createSuiteResult(suite);
127
+ const manager = new TestPhaseManager(suite, instance);
128
+ const originalEnv = { ...process.env };
129
+ const startTime = Date.now();
130
+ const testResultOverrides: Record<string, Partial<TestResult>> = {};
131
+
184
132
  const validTestMethodNames = new Set(tests.map(t => t.methodName));
185
133
  const testConfigs = Object.fromEntries(
186
134
  Object.entries(suite.tests).filter(([key]) => validTestMethodNames.has(key))
187
135
  );
188
136
 
189
- const startTime = Date.now();
190
-
191
137
  // Mark suite start
192
138
  this.#consumer.onEvent({ phase: 'before', type: 'suite', suite: { ...suite, tests: testConfigs } });
193
139
 
194
- const manager = new TestPhaseManager(suite);
195
-
196
- const originalEnv = { ...process.env };
197
-
198
140
  try {
199
141
  // Handle the BeforeAll calls
200
142
  await manager.startPhase('all');
143
+ } catch (someError) {
144
+ const suiteError = await manager.onError('all', someError);
145
+ for (const method of validTestMethodNames) {
146
+ testResultOverrides[method] ??= { status: 'errored', error: suiteError };
147
+ }
148
+ }
201
149
 
202
- const suiteEnv = { ...process.env };
150
+ const suiteEnv = { ...process.env };
203
151
 
204
- for (const test of tests ?? suite.tests) {
205
- if (await this.#shouldSkip(test, suite.instance)) {
206
- this.#skipTest(test, result);
207
- continue;
208
- }
152
+ for (const test of tests) {
153
+ // Reset env before each test
154
+ process.env = { ...suiteEnv };
209
155
 
210
- // Reset env before each test
211
- process.env = { ...suiteEnv };
156
+ const testStart = Date.now();
157
+ const testResultOverride = (testResultOverrides[test.methodName] ??= {});
212
158
 
213
- const testStart = Date.now();
214
- try {
159
+ if (await this.#shouldSkip(test, instance)) {
160
+ testResultOverride.status = 'skipped';
161
+ }
215
162
 
216
- // Handle BeforeEach
217
- await manager.startPhase('each');
163
+ try {
164
+ // Handle BeforeEach
165
+ testResultOverride.status || await manager.startPhase('each');
166
+ } catch (someError) {
167
+ const testError = await manager.onError('each', someError);
168
+ testResultOverride.error = testError;
169
+ testResultOverride.status = 'errored';
170
+ }
218
171
 
219
- // Run test
220
- const testResult = await this.executeTest(test, suite);
221
- result.tests[testResult.methodName] = testResult;
222
- result[testResult.status]++;
223
- result.total += 1;
172
+ // Run test
173
+ const testResult = await this.executeTest(instance, test, suite, testResultOverride);
224
174
 
225
- // Handle after each
226
- await manager.endPhase('each');
227
- testResult.durationTotal = Date.now() - testStart;
228
- } catch (testError) {
229
- const errors = await manager.errorPhase('each', testError, suite, test);
230
- this.#recordSuiteErrors(suite, result, errors);
231
- }
175
+ // Handle after each
176
+ try {
177
+ testResultOverride.status || await manager.endPhase('each');
178
+ } catch (testError) {
179
+ if (!(testError instanceof Error)) { throw testError; };
180
+ console.error('Failed to properly shutdown test', testError.message);
232
181
  }
233
182
 
183
+ result.tests[testResult.methodName] = testResult;
184
+ testResult.duration = Date.now() - testStart;
185
+ TestModelUtil.countTestResult(result, [testResult]);
186
+ }
187
+
188
+ try {
234
189
  // Handle after all
235
190
  await manager.endPhase('all');
236
191
  } catch (suiteError) {
237
- const errors = await manager.errorPhase('all', suiteError, suite);
238
- this.#recordSuiteErrors(suite, result, errors);
192
+ if (!(suiteError instanceof Error)) { throw suiteError; };
193
+ console.error('Failed to properly shutdown test', suiteError.message);
239
194
  }
240
195
 
241
196
  // Restore env
242
197
  process.env = { ...originalEnv };
243
198
 
244
199
  result.duration = Date.now() - startTime;
245
- result.status = TestModelUtil.countsToTestStatus(result);
200
+ result.status = TestModelUtil.computeTestStatus(result);
246
201
 
247
202
  // Mark suite complete
248
203
  this.#consumer.onEvent({ phase: 'after', type: 'suite', suite: result });
@@ -251,19 +206,13 @@ export class TestExecutor {
251
206
  /**
252
207
  * Handle executing a suite's test/tests based on command line inputs
253
208
  */
254
- async execute(run: TestRun): Promise<void> {
209
+ async execute(run: TestRun, singleFile?: boolean): Promise<void> {
255
210
  try {
256
211
  await Runtime.importFrom(run.import);
257
212
  } catch (error) {
258
- if (!(error instanceof Error)) {
259
- throw error;
260
- }
213
+ if (!(error instanceof Error)) { throw error; }
214
+ const suite = TestModelUtil.createImportErrorSuiteResult(run);
261
215
  console.error(error);
262
-
263
- // Fire import failure as a test failure for each test in the suite
264
- const { result, test, suite } = AssertUtil.gernerateImportFailure(run.import, error);
265
- this.#consumer.onEvent({ type: 'suite', phase: 'before', suite });
266
- this.#onSuiteTestError(result, test);
267
216
  this.#consumer.onEvent({ type: 'suite', phase: 'after', suite });
268
217
  return;
269
218
  }
@@ -277,6 +226,11 @@ export class TestExecutor {
277
226
  console.warn('Unable to find suites for ', run);
278
227
  }
279
228
 
229
+ if (singleFile) {
230
+ const testCount = suites.reduce((acc, suite) => acc + suite.tests.length, 0);
231
+ this.#consumer.onTestRunState?.({ testCount });
232
+ }
233
+
280
234
  for (const { suite, tests } of suites) {
281
235
  await this.executeSuite(suite, tests);
282
236
  }
@@ -1,9 +1,7 @@
1
- import { describeFunction, Env, TimeUtil } from '@travetto/runtime';
1
+ import { Env, TimeUtil } from '@travetto/runtime';
2
2
 
3
3
  import type { SuiteConfig, SuitePhase } from '../model/suite.ts';
4
- import { AssertUtil } from '../assert/util.ts';
5
4
  import { Barrier } from './barrier.ts';
6
- import type { TestConfig, TestResult } from '../model/test.ts';
7
5
 
8
6
  const TEST_PHASE_TIMEOUT = TimeUtil.duration(Env.TRV_TEST_PHASE_TIMEOUT.value ?? 15000, 'ms');
9
7
 
@@ -15,9 +13,11 @@ const TEST_PHASE_TIMEOUT = TimeUtil.duration(Env.TRV_TEST_PHASE_TIMEOUT.value ??
15
13
  export class TestPhaseManager {
16
14
  #progress: ('all' | 'each')[] = [];
17
15
  #suite: SuiteConfig;
16
+ #instance: unknown;
18
17
 
19
- constructor(suite: SuiteConfig) {
18
+ constructor(suite: SuiteConfig, instance: unknown) {
20
19
  this.#suite = suite;
20
+ this.#instance = instance;
21
21
  }
22
22
 
23
23
  /**
@@ -31,12 +31,10 @@ export class TestPhaseManager {
31
31
  }
32
32
 
33
33
  // Ensure all the criteria below are satisfied before moving forward
34
- error = await Barrier.awaitOperation(TEST_PHASE_TIMEOUT, async () => handler[phase]?.(this.#suite.instance));
34
+ error = await Barrier.awaitOperation(TEST_PHASE_TIMEOUT, async () => handler[phase]?.(this.#instance));
35
35
 
36
36
  if (error) {
37
- const toThrow = new Error(phase, { cause: error });
38
- Object.assign(toThrow, { import: describeFunction(handler.constructor) ?? undefined });
39
- throw toThrow;
37
+ throw error;
40
38
  }
41
39
  }
42
40
  }
@@ -58,21 +56,13 @@ export class TestPhaseManager {
58
56
  }
59
57
 
60
58
  /**
61
- * Handles if an error occurs during a phase, ensuring that we attempt to end the phase and then return the appropriate test results for the failure
59
+ * Handle an error during phase operation
62
60
  */
63
- async errorPhase(phase: 'all' | 'each', error: unknown, suite: SuiteConfig, test?: TestConfig): Promise<TestResult[]> {
64
- try { await this.endPhase(phase); } catch { }
65
- if (!(error instanceof Error)) { throw error; }
66
-
67
- // Don't propagate our own errors
68
- if (error.message === 'afterAll' || error.message === 'afterEach') {
69
- return [];
70
- }
71
-
72
- if (test) {
73
- return [AssertUtil.generateSuiteTestFailure({ suite, error, test })];
74
- } else {
75
- return AssertUtil.generateSuiteTestFailures(suite, error);
61
+ async onError(phase: 'all' | 'each', error: unknown): Promise<Error> {
62
+ if (!(error instanceof Error)) {
63
+ await this.endPhase(phase).catch(() => { });
64
+ throw error;
76
65
  }
66
+ return error;
77
67
  }
78
68
  }
@@ -3,7 +3,7 @@ import fs from 'node:fs/promises';
3
3
  import readline from 'node:readline/promises';
4
4
  import path from 'node:path';
5
5
 
6
- import { Env, ExecUtil, Util, RuntimeIndex, Runtime, TimeUtil, JSONUtil } from '@travetto/runtime';
6
+ import { Env, ExecUtil, Util, RuntimeIndex, Runtime, TimeUtil, JSONUtil, describeFunction } from '@travetto/runtime';
7
7
  import { WorkPool } from '@travetto/worker';
8
8
  import { Registry } from '@travetto/registry';
9
9
 
@@ -126,6 +126,7 @@ export class RunUtil {
126
126
  const imported = await Registry.manualInit([importPath]);
127
127
  const classes = Object.fromEntries(
128
128
  imported
129
+ .filter(cls => !describeFunction(cls).abstract)
129
130
  .filter(cls => SuiteRegistryIndex.hasConfig(cls))
130
131
  .map(cls => [cls.Ⲑid, SuiteRegistryIndex.getConfig(cls)])
131
132
  );
@@ -222,7 +223,7 @@ export class RunUtil {
222
223
  }
223
224
 
224
225
  if (runs.length === 1) {
225
- await new TestExecutor(consumer).execute(runs[0]);
226
+ await new TestExecutor(consumer).execute(runs[0], true);
226
227
  } else {
227
228
  await WorkPool.run(
228
229
  run => buildStandardTestManager(consumer, run),
@@ -19,10 +19,6 @@ export interface SuiteConfig extends SuiteCore {
19
19
  * Should this be skipped
20
20
  */
21
21
  skip: Skip;
22
- /**
23
- * Actual class instance
24
- */
25
- instance?: unknown;
26
22
  /**
27
23
  * Tests to run
28
24
  */
@@ -34,29 +30,35 @@ export interface SuiteConfig extends SuiteCore {
34
30
  }
35
31
 
36
32
  /**
37
- * All counts for the suite summary
33
+ * Test Counts
38
34
  */
39
- export interface Counts {
35
+ export interface ResultsSummary {
36
+ /** Passing Test Count */
40
37
  passed: number;
38
+ /** Skipped Test Count */
41
39
  skipped: number;
40
+ /** Failed Test Count */
42
41
  failed: number;
42
+ /** Errored Test Count */
43
43
  errored: number;
44
+ /** Unknown Test Count */
44
45
  unknown: number;
46
+ /** Total Test Count */
45
47
  total: number;
48
+ /** Test Self Execution Duration */
49
+ selfDuration: number;
50
+ /** Total Duration */
51
+ duration: number;
46
52
  }
47
53
 
48
54
  /**
49
55
  * Results of a suite run
50
56
  */
51
- export interface SuiteResult extends Counts, SuiteCore {
57
+ export interface SuiteResult extends ResultsSummary, SuiteCore {
52
58
  /**
53
59
  * All test results
54
60
  */
55
61
  tests: Record<string, TestResult>;
56
- /**
57
- * Suite duration
58
- */
59
- duration: number;
60
62
  /**
61
63
  * Overall status
62
64
  */
package/src/model/test.ts CHANGED
@@ -105,13 +105,13 @@ export interface TestResult extends TestCore {
105
105
  */
106
106
  assertions: Assertion[];
107
107
  /**
108
- * Duration for the test
108
+ * Self Execution Duration
109
109
  */
110
- duration: number;
110
+ selfDuration: number;
111
111
  /**
112
112
  * Total duration including before/after
113
113
  */
114
- durationTotal: number;
114
+ duration: number;
115
115
  /**
116
116
  * Logging output
117
117
  */
package/src/model/util.ts CHANGED
@@ -1,14 +1,97 @@
1
- import type { Counts } from './suite.ts';
2
- import type { TestStatus } from './test.ts';
1
+ import path from 'node:path';
2
+
3
+ import { asFull, RuntimeIndex } from '@travetto/runtime';
4
+
5
+ import type { ResultsSummary, SuiteConfig, SuiteResult } from './suite.ts';
6
+ import type { TestConfig, TestResult, TestRun, TestStatus } from './test.ts';
3
7
 
4
8
  export class TestModelUtil {
5
- static countsToTestStatus(counts: Counts): TestStatus {
9
+ static computeTestStatus(summary: ResultsSummary): TestStatus {
6
10
  switch (true) {
7
- case counts.errored > 0: return 'errored';
8
- case counts.failed > 0: return 'failed';
9
- case counts.skipped > 0: return 'skipped';
10
- case counts.unknown > 0: return 'unknown';
11
+ case summary.errored > 0: return 'errored';
12
+ case summary.failed > 0: return 'failed';
13
+ case summary.skipped > 0: return 'skipped';
14
+ case summary.unknown > 0: return 'unknown';
11
15
  default: return 'passed';
12
16
  }
13
17
  }
18
+
19
+ static buildSummary(): ResultsSummary {
20
+ return { passed: 0, failed: 0, skipped: 0, errored: 0, unknown: 0, total: 0, duration: 0, selfDuration: 0 };
21
+ }
22
+
23
+ static countTestResult<T extends ResultsSummary>(summary: T, tests: Pick<TestResult, 'status' | 'selfDuration' | 'duration'>[]): T {
24
+ for (const test of tests) {
25
+ summary[test.status] += 1;
26
+ summary.total += 1;
27
+ summary.selfDuration += (test.selfDuration ?? 0);
28
+ summary.duration += (test.duration ?? 0);
29
+ }
30
+ return summary;
31
+ }
32
+
33
+
34
+ /**
35
+ * An empty suite result based on a suite config
36
+ */
37
+ static createSuiteResult(suite: SuiteConfig, override?: Partial<SuiteResult>): SuiteResult {
38
+ return {
39
+ ...TestModelUtil.buildSummary(),
40
+ status: 'unknown',
41
+ lineStart: suite.lineStart,
42
+ lineEnd: suite.lineEnd,
43
+ import: suite.import,
44
+ classId: suite.classId,
45
+ sourceHash: suite.sourceHash,
46
+ tests: {},
47
+ duration: 0,
48
+ selfDuration: 0,
49
+ ...override
50
+ };
51
+ }
52
+
53
+ /**
54
+ * An empty test result based on a suite and test config
55
+ */
56
+ static createTestResult(suite: SuiteConfig, test: TestConfig, override?: Partial<TestResult>): TestResult {
57
+ return {
58
+ methodName: test.methodName,
59
+ description: test.description,
60
+ classId: test.classId,
61
+ tags: test.tags,
62
+ suiteLineStart: suite.lineStart,
63
+ lineStart: test.lineStart,
64
+ lineEnd: test.lineEnd,
65
+ lineBodyStart: test.lineBodyStart,
66
+ import: test.import,
67
+ declarationImport: test.declarationImport,
68
+ sourceHash: test.sourceHash,
69
+ status: 'unknown',
70
+ assertions: [],
71
+ duration: 0,
72
+ selfDuration: 0,
73
+ output: [],
74
+ ...override
75
+ };
76
+ }
77
+
78
+ static createImportErrorSuiteResult(run: TestRun): SuiteResult {
79
+ const name = path.basename(run.import);
80
+ const classId = `${RuntimeIndex.getFromImport(run.import)?.id}#${name}`;
81
+ const common = { classId, duration: 0, lineStart: 1, lineEnd: 1, import: run.import } as const;
82
+ return asFull<SuiteResult>({
83
+ ...common,
84
+ status: 'errored', errored: 1,
85
+ tests: {
86
+ impport: asFull<TestResult>({
87
+ ...common,
88
+ status: 'errored',
89
+ assertions: [{
90
+ ...common, line: common.lineStart,
91
+ methodName: 'import', operator: 'import', text: `Failed to import ${run.import}`,
92
+ }]
93
+ })
94
+ }
95
+ });
96
+ }
14
97
  }
@@ -8,7 +8,7 @@ import type { TestConfig } from '../model/test.ts';
8
8
  function combineClasses(baseConfig: SuiteConfig, ...subConfig: Partial<SuiteConfig>[]): SuiteConfig {
9
9
  for (const config of subConfig) {
10
10
  if (config.tags) {
11
- baseConfig.tags = [...baseConfig.tags ?? [], ...config.tags];
11
+ baseConfig.tags = [...new Set([...baseConfig.tags ?? [], ...config.tags])];
12
12
  }
13
13
  baseConfig.skip = config.skip ?? baseConfig.skip;
14
14
 
@@ -30,6 +30,21 @@ function combineClasses(baseConfig: SuiteConfig, ...subConfig: Partial<SuiteConf
30
30
  return baseConfig;
31
31
  }
32
32
 
33
+ function combineWithParent(baseConfig: SuiteConfig, parentConfig: SuiteConfig): SuiteConfig {
34
+ baseConfig.tags = [...parentConfig.tags ?? [], ...baseConfig.tags ?? []];
35
+ baseConfig.skip = baseConfig.skip ?? parentConfig.skip;
36
+ baseConfig.phaseHandlers = [...(parentConfig.phaseHandlers ?? []), ...(baseConfig.phaseHandlers ?? [])];
37
+ for (const [key, test] of Object.entries(parentConfig.tests ?? {})) {
38
+ baseConfig.tests[key] = {
39
+ ...test,
40
+ class: baseConfig.class,
41
+ classId: baseConfig.classId,
42
+ import: baseConfig.import,
43
+ };
44
+ }
45
+ return baseConfig;
46
+ }
47
+
33
48
  function combineMethods(suite: SuiteConfig, baseConfig: TestConfig, ...subConfig: Partial<TestConfig>[]): TestConfig {
34
49
  baseConfig.classId = suite.classId;
35
50
  baseConfig.import = suite.import;
@@ -54,12 +69,12 @@ export class SuiteRegistryAdapter implements RegistryAdapter<SuiteConfig> {
54
69
 
55
70
  register(...data: Partial<SuiteConfig>[]): SuiteConfig {
56
71
  if (!this.#config) {
57
- const { lines, hash } = describeFunction(this.#cls) ?? {};
72
+ const { lines, hash, abstract: isAbstract } = describeFunction(this.#cls) ?? {};
58
73
  this.#config = asFull<SuiteConfig>({
59
74
  class: this.#cls,
60
75
  classId: this.#cls.Ⲑid,
61
76
  tags: [],
62
- skip: false,
77
+ skip: isAbstract,
63
78
  import: Runtime.getImport(this.#cls),
64
79
  lineStart: lines?.[0],
65
80
  lineEnd: lines?.[1],
@@ -99,11 +114,11 @@ export class SuiteRegistryAdapter implements RegistryAdapter<SuiteConfig> {
99
114
 
100
115
  finalize(parent?: SuiteConfig): void {
101
116
  if (parent) {
102
- combineClasses(this.#config, parent);
117
+ combineWithParent(this.#config, parent);
103
118
  }
104
119
 
105
120
  for (const test of Object.values(this.#config.tests)) {
106
- test.tags = [...test.tags ?? [], ...this.#config.tags ?? []];
121
+ test.tags = [...new Set([...test.tags ?? [], ...this.#config.tags ?? []])];
107
122
  test.description ||= SchemaRegistryIndex.get(this.#cls).getMethod(test.methodName).description;
108
123
  }
109
124
  }
@@ -20,9 +20,7 @@ export class TestChildWorker extends IpcChannel<TestRun> {
20
20
  await operation();
21
21
  this.send(type); // Respond
22
22
  } catch (error) {
23
- if (!(error instanceof Error)) {
24
- throw error;
25
- }
23
+ if (!(error instanceof Error)) { throw error; }
26
24
  // Mark as errored out
27
25
  this.send(type, JSONUtil.cloneForTransmit(error));
28
26
  }