@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
@@ -1,13 +1,18 @@
1
- import { existsSync } from 'node:fs';
2
-
3
- import { type Class, RuntimeIndex } from '@travetto/runtime';
4
-
5
1
  import type { TestConsumerShape } from '../types.ts';
6
- import type { TestEvent } from '../../model/event.ts';
7
- import type { TestResult } from '../../model/test.ts';
8
- import type { SuiteResult } from '../../model/suite.ts';
2
+ import type { TestEvent, TestRemoveEvent } from '../../model/event.ts';
3
+ import type { TestConfig, TestDiffSource, TestResult } from '../../model/test.ts';
4
+ import type { Counts, SuiteConfig, SuiteResult } from '../../model/suite.ts';
9
5
  import { DelegatingConsumer } from './delegating.ts';
10
- import { SuiteRegistryIndex } from '../../registry/registry-index.ts';
6
+ import { SuiteCore } from '../../model/common.ts';
7
+ import { TestModelUtil } from '../../model/util.ts';
8
+
9
+ type ClassId = string;
10
+ type ImportName = string;
11
+
12
+ type CumulativeTestResult = Pick<TestResult, 'sourceHash' | 'status' | 'duration'>;
13
+ type CumulativeSuiteResult = Pick<SuiteCore, 'import' | 'classId' | 'sourceHash'> & {
14
+ tests: Record<string, CumulativeTestResult>;
15
+ };
11
16
 
12
17
  /**
13
18
  * Cumulative Summary consumer
@@ -16,77 +21,101 @@ export class CumulativeSummaryConsumer extends DelegatingConsumer {
16
21
  /**
17
22
  * Total state of all tests run so far
18
23
  */
19
- #state: Record<string, Record<string, TestResult['status']>> = {};
24
+ #state: Record<ImportName, Record<ClassId, CumulativeSuiteResult>> = {};
20
25
 
21
26
  constructor(target: TestConsumerShape) {
22
27
  super([target]);
23
28
  }
24
29
 
25
- /**
26
- * Summarize a given test suite using the new result and the historical
27
- * state
28
- */
29
- summarizeSuite(test: TestResult): SuiteResult {
30
- // Was only loading to verify existence (TODO: double-check)
31
- if (existsSync(RuntimeIndex.getFromImport(test.import)!.sourceFile)) {
32
- (this.#state[test.classId] ??= {})[test.methodName] = test.status;
33
- const SuiteCls = SuiteRegistryIndex.getClasses().find(x => x.Ⲑid === test.classId);
34
- return SuiteCls ? this.computeTotal(SuiteCls) : this.removeClass(test.classId);
35
- } else {
36
- return this.removeClass(test.classId);
30
+ getSuite(core: Pick<SuiteCore, 'import' | 'classId'>): CumulativeSuiteResult {
31
+ return this.#state[core.import]?.[core.classId];
32
+ }
33
+
34
+ getOrCreateSuite({ tests: _, ...core }: SuiteConfig | SuiteResult): CumulativeSuiteResult {
35
+ return (this.#state[core.import] ??= {})[core.classId] ??= { ...core, tests: {} };
36
+ }
37
+
38
+ onTestBefore(config: TestConfig): TestConfig {
39
+ const suite = this.getSuite(config);
40
+ suite.tests[config.methodName] = { sourceHash: config.sourceHash, status: 'unknown', duration: 0 };
41
+ return config;
42
+ }
43
+
44
+ onTestAfter(result: TestResult): TestResult {
45
+ const test = this.getSuite(result).tests[result.methodName];
46
+ Object.assign(test, { status: result.status, duration: result.duration });
47
+ return result;
48
+ }
49
+
50
+ onSuiteBefore(config: SuiteConfig): SuiteConfig {
51
+ const suite = this.getOrCreateSuite(config);
52
+ suite.sourceHash = config.sourceHash;
53
+ return config;
54
+ }
55
+
56
+ onSuiteAfter(result: SuiteResult): SuiteResult {
57
+ // Reset counts
58
+ const suite = this.getSuite(result);
59
+ const totals: Counts & { duration: number } = { passed: 0, failed: 0, skipped: 0, unknown: 0, total: 0, duration: 0 };
60
+ for (const test of Object.values(suite.tests)) {
61
+ totals[test.status] += 1;
62
+ totals.total += 1;
63
+ totals.duration += test.duration ?? 0;
37
64
  }
65
+ return { ...result, ...totals, status: TestModelUtil.countsToTestStatus(totals) };
38
66
  }
39
67
 
40
- /**
41
- * Remove a class
42
- */
43
- removeClass(clsId: string): SuiteResult {
44
- this.#state[clsId] = {};
45
- return {
46
- classId: clsId, passed: 0, failed: 0, skipped: 0, total: 0, tests: [], duration: 0, import: '', lineStart: 0, lineEnd: 0
47
- };
68
+ removeTest(importName: string, classId?: string, methodName?: string): void {
69
+ if (methodName && classId && importName) {
70
+ delete this.getSuite({ import: importName, classId }).tests[methodName];
71
+ } else if (classId && importName) {
72
+ delete this.#state[importName][classId];
73
+ } else if (importName) {
74
+ delete this.#state[importName];
75
+ }
76
+ }
77
+
78
+ transformRemove(event: TestRemoveEvent): TestRemoveEvent {
79
+ this.removeTest(event.import, event.classId, event.methodName);
80
+ return event;
48
81
  }
49
82
 
50
83
  /**
51
- * Compute totals
84
+ * Handle cumulative events, and emit a summarized summary
52
85
  */
53
- computeTotal(cls: Class): SuiteResult {
54
- const suite = SuiteRegistryIndex.getConfig(cls);
55
- const total = Object.values(suite.tests).reduce((acc, x) => {
56
- const status = this.#state[x.classId][x.methodName] ?? 'unknown';
57
- acc[status] += 1;
58
- return acc;
59
- }, { skipped: 0, passed: 0, failed: 0, unknown: 0 });
60
-
61
- return {
62
- classId: suite.classId,
63
- passed: total.passed,
64
- failed: total.failed,
65
- skipped: total.skipped,
66
- import: suite.import,
67
- lineStart: suite.lineStart,
68
- lineEnd: suite.lineEnd,
69
- total: total.failed + total.passed,
70
- tests: [],
71
- duration: 0
72
- };
86
+ transform(event: TestEvent): TestEvent | undefined {
87
+ try {
88
+ if (event.type === 'suite') {
89
+ if (event.phase === 'before') {
90
+ return { ...event, suite: this.onSuiteBefore(event.suite) };
91
+ } else if (event.phase === 'after') {
92
+ return { ...event, suite: this.onSuiteAfter(event.suite) };
93
+ }
94
+ } else if (event.type === 'test') {
95
+ if (event.phase === 'before') {
96
+ return { ...event, test: this.onTestBefore(event.test) };
97
+ } else if (event.phase === 'after') {
98
+ return { ...event, test: this.onTestAfter(event.test) };
99
+ }
100
+ }
101
+ return event;
102
+ } catch (error) {
103
+ console.warn('Summarization Error', { error });
104
+ }
73
105
  }
74
106
 
75
107
  /**
76
- * Listen for event, process the full event, and if the event is an after test,
77
- * send a full suite summary
108
+ * Produce diff source for import file
78
109
  */
79
- onEventDone(e: TestEvent): void {
80
- try {
81
- if (e.type === 'test' && e.phase === 'after') {
82
- this.onEvent({
83
- type: 'suite',
84
- phase: 'after',
85
- suite: this.summarizeSuite(e.test),
86
- });
110
+ produceDiffSource(importName: string): TestDiffSource {
111
+ const output: TestDiffSource = {};
112
+ for (const [clsId, suite] of Object.entries(this.#state[importName] || {})) {
113
+ const methods: TestDiffSource[string]['methods'] = {};
114
+ for (const [methodName, test] of Object.entries(suite.tests)) {
115
+ methods[methodName] = test.sourceHash!;
87
116
  }
88
- } catch (err) {
89
- console.warn('Summarization Error', { error: err });
117
+ output[clsId] = { sourceHash: suite.sourceHash!, methods };
90
118
  }
119
+ return output;
91
120
  }
92
121
  }
@@ -1,58 +1,61 @@
1
1
  import type { SuitesSummary, TestConsumerShape, TestRunState } from '../types.ts';
2
- import type { TestEvent } from '../../model/event.ts';
2
+ import type { TestEvent, TestRemoveEvent } from '../../model/event.ts';
3
3
 
4
4
  /**
5
5
  * Delegating event consumer
6
6
  */
7
7
  export abstract class DelegatingConsumer implements TestConsumerShape {
8
8
  #consumers: TestConsumerShape[];
9
- #transformer?: (ev: TestEvent) => typeof ev;
10
- #filter?: (ev: TestEvent) => boolean;
11
9
 
12
10
  constructor(consumers: TestConsumerShape[]) {
13
11
  this.#consumers = consumers;
14
- for (const c of consumers) {
15
- c.onEvent = c.onEvent.bind(c);
12
+ for (const consumer of consumers) {
13
+ consumer.onEvent = consumer.onEvent.bind(consumer);
16
14
  }
17
15
  }
18
16
 
19
- withTransformer(transformer: (ev: TestEvent) => typeof ev): this {
20
- this.#transformer = transformer;
21
- return this;
17
+ async onStart(state: TestRunState): Promise<void> {
18
+ for (const consumer of this.#consumers) {
19
+ await consumer.onStart?.(state);
20
+ }
22
21
  }
23
22
 
24
- withFilter(filter: (ev: TestEvent) => boolean): this {
25
- this.#filter = filter;
26
- return this;
23
+ onRemoveEvent(event: TestRemoveEvent): void {
24
+ let result = event;
25
+ if (this.transformRemove) {
26
+ result = this.transformRemove(event) ?? event;
27
+ }
28
+ if (result) {
29
+ for (const consumer of this.#consumers) {
30
+ consumer.onRemoveEvent?.(result);
31
+ }
32
+ }
27
33
  }
28
34
 
29
- async onStart(state: TestRunState): Promise<void> {
30
- for (const c of this.#consumers) {
31
- await c.onStart?.(state);
35
+ delegateEvent(event: TestEvent): void {
36
+ for (const consumer of this.#consumers) {
37
+ consumer.onEvent(event);
32
38
  }
33
39
  }
34
40
 
35
- onEvent(e: TestEvent): void {
36
- if (this.#transformer) {
37
- e = this.#transformer(e);
41
+ onEvent(event: TestEvent): void {
42
+ let result = event;
43
+ if (this.transform) {
44
+ result = this.transform(event) ?? event;
38
45
  }
39
- if (this.#filter?.(e) === false) {
40
- return;
46
+ if (result) {
47
+ this.delegateEvent(result);
41
48
  }
42
- for (const c of this.#consumers) {
43
- c.onEvent(e);
44
- }
45
-
46
- this.onEventDone?.(e);
47
49
  }
48
50
 
49
51
  async summarize(summary?: SuitesSummary): Promise<void> {
50
52
  if (summary) {
51
- for (const c of this.#consumers) {
52
- await c.onSummary?.(summary);
53
+ for (const consumer of this.#consumers) {
54
+ await consumer.onSummary?.(summary);
53
55
  }
54
56
  }
55
57
  }
56
58
 
57
- onEventDone?(e: TestEvent): void;
59
+ transform?(event: TestEvent): TestEvent | undefined;
60
+ transformRemove?(event: TestRemoveEvent): TestRemoveEvent | undefined;
58
61
  }
@@ -1,10 +1,9 @@
1
1
  import { Writable } from 'node:stream';
2
2
 
3
- import { Util } from '@travetto/runtime';
4
-
5
- import type { TestEvent } from '../../model/event.ts';
3
+ import type { TestEvent, TestRemoveEvent } from '../../model/event.ts';
6
4
  import type { TestConsumerShape } from '../types.ts';
7
5
  import { TestConsumer } from '../decorator.ts';
6
+ import { CommunicationUtil } from '../../communication.ts';
8
7
 
9
8
  /**
10
9
  * Streams all test events a JSON payload, in an nd-json format
@@ -17,7 +16,15 @@ export class EventStreamer implements TestConsumerShape {
17
16
  this.#stream = stream;
18
17
  }
19
18
 
19
+ sendPayload(payload: unknown): void {
20
+ this.#stream.write(`${CommunicationUtil.serialize(payload)}\n`);
21
+ }
22
+
20
23
  onEvent(event: TestEvent): void {
21
- this.#stream.write(`${Util.serializeToJSON(event)}\n`);
24
+ this.sendPayload(event);
25
+ }
26
+
27
+ onRemoveEvent(event: TestRemoveEvent): void {
28
+ this.sendPayload(event);
22
29
  }
23
30
  }
@@ -1,16 +1,25 @@
1
1
  import { IpcChannel } from '@travetto/worker';
2
- import { Util } from '@travetto/runtime';
3
2
 
4
- import type { TestEvent } from '../../model/event.ts';
3
+ import type { TestEvent, TestRemoveEvent } from '../../model/event.ts';
5
4
  import type { TestConsumerShape } from '../types.ts';
6
5
  import { TestConsumer } from '../decorator.ts';
6
+ import { CommunicationUtil } from '../../communication.ts';
7
7
 
8
8
  /**
9
9
  * Triggers each event as an IPC command to a parent process
10
10
  */
11
11
  @TestConsumer()
12
12
  export class ExecutionEmitter extends IpcChannel<TestEvent> implements TestConsumerShape {
13
+
14
+ sendPayload(payload: unknown & { type: string }): void {
15
+ this.send(payload.type, CommunicationUtil.serializeToObject(payload));
16
+ }
17
+
13
18
  onEvent(event: TestEvent): void {
14
- this.send(event.type, JSON.parse(Util.serializeToJSON(event)));
19
+ this.sendPayload(event);
20
+ }
21
+
22
+ onRemoveEvent(event: TestRemoveEvent): void {
23
+ this.sendPayload(event);
15
24
  }
16
25
  }
@@ -12,11 +12,12 @@ export class RunnableTestConsumer extends DelegatingConsumer {
12
12
 
13
13
  constructor(...consumers: TestConsumerShape[]) {
14
14
  super(consumers);
15
- this.#results = consumers.find(x => !!x.onSummary) ? new TestResultsSummarizer() : undefined;
15
+ this.#results = consumers.find(consumer => !!consumer.onSummary) ? new TestResultsSummarizer() : undefined;
16
16
  }
17
17
 
18
- onEventDone(e: TestEvent): void {
19
- this.#results?.onEvent(e);
18
+ transform(event: TestEvent): TestEvent | undefined {
19
+ this.#results?.onEvent(event);
20
+ return event;
20
21
  }
21
22
 
22
23
  async summarizeAsBoolean(): Promise<boolean> {
@@ -11,27 +11,29 @@ export class TestResultsSummarizer implements TestConsumerShape {
11
11
  passed: 0,
12
12
  failed: 0,
13
13
  skipped: 0,
14
+ unknown: 0,
14
15
  total: 0,
15
16
  duration: 0,
16
17
  suites: [],
17
18
  errors: []
18
19
  };
19
20
 
20
- #merge(src: SuiteResult): void {
21
- this.summary.suites.push(src);
22
- this.summary.failed += src.failed;
23
- this.summary.passed += src.passed;
24
- this.summary.skipped += src.skipped;
25
- this.summary.duration += src.duration;
26
- this.summary.total += (src.failed + src.passed + src.skipped);
21
+ #merge(result: SuiteResult): void {
22
+ this.summary.suites.push(result);
23
+ this.summary.failed += result.failed;
24
+ this.summary.passed += result.passed;
25
+ this.summary.unknown += result.unknown;
26
+ this.summary.skipped += result.skipped;
27
+ this.summary.duration += result.duration;
28
+ this.summary.total += result.total;
27
29
  }
28
30
 
29
31
  /**
30
32
  * Merge all test results into a single Suite Result
31
33
  */
32
- onEvent(e: TestEvent): void {
33
- if (e.phase === 'after' && e.type === 'suite') {
34
- this.#merge(e.suite);
34
+ onEvent(event: TestEvent): void {
35
+ if (event.type === 'suite' && event.phase === 'after') {
36
+ this.#merge(event.suite);
35
37
  }
36
38
  }
37
39
  }
@@ -56,7 +56,7 @@ export class TapSummaryEmitter implements TestConsumerShape {
56
56
  const success = StyleUtil.getStyle({ text: '#e5e5e5', background: '#026020' }); // White on dark green
57
57
  const fail = StyleUtil.getStyle({ text: '#e5e5e5', background: '#8b0000' }); // White on dark red
58
58
  this.#progress = this.#terminal.streamToBottom(
59
- Util.mapAsyncItr(
59
+ Util.mapAsyncIterable(
60
60
  this.#results,
61
61
  (value) => {
62
62
  failed += (value.status === 'failed' ? 1 : 0);
@@ -70,22 +70,22 @@ export class TapSummaryEmitter implements TestConsumerShape {
70
70
  );
71
71
  }
72
72
 
73
- onEvent(ev: TestEvent): void {
74
- if (ev.type === 'test' && ev.phase === 'after') {
75
- const { test } = ev;
73
+ onEvent(event: TestEvent): void {
74
+ if (event.type === 'test' && event.phase === 'after') {
75
+ const { test } = event;
76
76
  this.#results.add(test);
77
77
  if (test.status === 'failed') {
78
- this.#consumer.onEvent(ev);
78
+ this.#consumer.onEvent(event);
79
79
  }
80
80
  const tests = this.#timings.get('test')!;
81
- tests.set(`${ev.test.classId}/${ev.test.methodName}`, {
82
- key: `${ev.test.classId}/${ev.test.methodName}`,
81
+ tests.set(`${event.test.classId}/${event.test.methodName}`, {
82
+ key: `${event.test.classId}/${event.test.methodName}`,
83
83
  duration: test.duration,
84
84
  tests: 1
85
85
  });
86
- } else if (ev.type === 'suite' && ev.phase === 'after') {
87
- const [module] = ev.suite.classId.split(/:/);
88
- const [file] = ev.suite.classId.split(/#/);
86
+ } else if (event.type === 'suite' && event.phase === 'after') {
87
+ const [module] = event.suite.classId.split(/:/);
88
+ const [file] = event.suite.classId.split(/#/);
89
89
 
90
90
  const modules = this.#timings.get('module')!;
91
91
  const files = this.#timings.get('file')!;
@@ -99,16 +99,18 @@ export class TapSummaryEmitter implements TestConsumerShape {
99
99
  files.set(file, { key: file, duration: 0, tests: 0 });
100
100
  }
101
101
 
102
- suites.set(ev.suite.classId, {
103
- key: ev.suite.classId,
104
- duration: ev.suite.duration,
105
- tests: ev.suite.tests.length
102
+ const testCount = Object.keys(event.suite.tests).length;
103
+
104
+ suites.set(event.suite.classId, {
105
+ key: event.suite.classId,
106
+ duration: event.suite.duration,
107
+ tests: testCount
106
108
  });
107
109
 
108
- files.get(file)!.duration += ev.suite.duration;
109
- files.get(file)!.tests += ev.suite.tests.length;
110
- modules.get(module)!.duration += ev.suite.duration;
111
- modules.get(module)!.tests += ev.suite.tests.length;
110
+ files.get(file)!.duration += event.suite.duration;
111
+ files.get(file)!.tests += testCount;
112
+ modules.get(module)!.duration += event.suite.duration;
113
+ modules.get(module)!.tests += testCount;
112
114
  }
113
115
  }
114
116
 
@@ -127,8 +129,8 @@ export class TapSummaryEmitter implements TestConsumerShape {
127
129
  await this.#consumer.log(`${this.#enhancer.suiteName(`Top ${count} slowest ${title}s`)}: `);
128
130
  const top10 = [...results.values()].toSorted((a, b) => b.duration - a.duration).slice(0, count);
129
131
 
130
- for (const x of top10) {
131
- console.log(` * ${this.#enhancer.testName(x.key)} - ${this.#enhancer.total(x.duration)}ms / ${this.#enhancer.total(x.tests)} tests`);
132
+ for (const result of top10) {
133
+ console.log(` * ${this.#enhancer.testName(result.key)} - ${this.#enhancer.total(result.duration)}ms / ${this.#enhancer.total(result.tests)} tests`);
132
134
  }
133
135
  await this.#consumer.log('');
134
136
  }
@@ -48,10 +48,10 @@ export class TapEmitter implements TestConsumerShape {
48
48
  /**
49
49
  * Output supplemental data (e.g. logs)
50
50
  */
51
- logMeta(obj: Record<string, unknown>): void {
51
+ logMeta(meta: Record<string, unknown>): void {
52
52
  const lineLength = this.#terminal.width - 5;
53
- let body = stringify(obj, { lineWidth: lineLength, indent: 2 });
54
- body = body.split('\n').map(x => ` ${x}`).join('\n');
53
+ let body = stringify(meta, { lineWidth: lineLength, indent: 2 });
54
+ body = body.split('\n').map(line => ` ${line}`).join('\n');
55
55
  this.log(`---\n${this.#enhancer.objectInspect(body)}\n...`);
56
56
  }
57
57
 
@@ -59,16 +59,16 @@ export class TapEmitter implements TestConsumerShape {
59
59
  * Error to string
60
60
  * @param error
61
61
  */
62
- errorToString(err?: Error): string | undefined {
63
- if (err && err.name !== 'AssertionError') {
64
- if (err instanceof Error) {
65
- let out = JSON.stringify(hasToJSON(err) ? err.toJSON() : err, null, 2);
66
- if (this.#options?.verbose && err.stack) {
67
- out = `${out}\n${err.stack}`;
62
+ errorToString(error?: Error): string | undefined {
63
+ if (error && error.name !== 'AssertionError') {
64
+ if (error instanceof Error) {
65
+ let out = JSON.stringify(hasToJSON(error) ? error.toJSON() : error, null, 2);
66
+ if (this.#options?.verbose && error.stack) {
67
+ out = `${out}\n${error.stack}`;
68
68
  }
69
69
  return out;
70
70
  } else {
71
- return `${err}`;
71
+ return `${error}`;
72
72
  }
73
73
  }
74
74
  }
@@ -76,9 +76,9 @@ export class TapEmitter implements TestConsumerShape {
76
76
  /**
77
77
  * Listen for each event
78
78
  */
79
- onEvent(e: TestEvent): void {
80
- if (e.type === 'test' && e.phase === 'after') {
81
- const { test } = e;
79
+ onEvent(event: TestEvent): void {
80
+ if (event.type === 'test' && event.phase === 'after') {
81
+ const { test } = event;
82
82
  const suiteId = this.#enhancer.suiteName(test.classId);
83
83
  let header = `${suiteId} - ${this.#enhancer.testName(test.methodName)}`;
84
84
  if (test.description) {
@@ -155,8 +155,8 @@ export class TapEmitter implements TestConsumerShape {
155
155
 
156
156
  if (summary.errors.length) {
157
157
  this.log('---\n');
158
- for (const err of summary.errors) {
159
- const msg = this.errorToString(err);
158
+ for (const error of summary.errors) {
159
+ const msg = this.errorToString(error);
160
160
  if (msg) {
161
161
  this.log(this.#enhancer.failure(msg));
162
162
  }
@@ -24,19 +24,19 @@ export class XunitEmitter implements TestConsumerShape {
24
24
  /**
25
25
  * Process metadata information (e.g. logs)
26
26
  */
27
- buildMeta(obj: Record<string, unknown>): string {
28
- if (!obj) {
27
+ buildMeta(meta: Record<string, unknown>): string {
28
+ if (!meta) {
29
29
  return '';
30
30
  }
31
31
 
32
- for (const k of Object.keys(obj)) {
33
- if (!obj[k]) {
34
- delete obj[k];
32
+ for (const key of Object.keys(meta)) {
33
+ if (!meta[key]) {
34
+ delete meta[key];
35
35
  }
36
36
  }
37
- if (Object.keys(obj).length) {
38
- let body = stringify(obj);
39
- body = body.split('\n').map(x => ` ${x}`).join('\n');
37
+ if (Object.keys(meta).length) {
38
+ let body = stringify(meta);
39
+ body = body.split('\n').map(line => ` ${line}`).join('\n');
40
40
  return `<![CDATA[\n${body}\n]]>`;
41
41
  } else {
42
42
  return '';
@@ -46,10 +46,10 @@ export class XunitEmitter implements TestConsumerShape {
46
46
  /**
47
47
  * Handle each test event
48
48
  */
49
- onEvent(e: TestEvent): void {
50
- if (e.type === 'test' && e.phase === 'after') {
49
+ onEvent(event: TestEvent): void {
50
+ if (event.type === 'test' && event.phase === 'after') {
51
51
 
52
- const { test } = e;
52
+ const { test } = event;
53
53
 
54
54
  let name = `${test.methodName}`;
55
55
  if (test.description) {
@@ -59,8 +59,8 @@ export class XunitEmitter implements TestConsumerShape {
59
59
  let body = '';
60
60
 
61
61
  if (test.error) {
62
- const assertErr = test.assertions.find(x => !!x.error)!;
63
- body = `<failure type="${assertErr.text}" message="${encodeURIComponent(assertErr.message!)}"><![CDATA[${assertErr.error!.stack}]]></failure>`;
62
+ const assertion = test.assertions.find(item => !!item.error)!;
63
+ body = `<failure type="${assertion.text}" message="${encodeURIComponent(assertion.message!)}"><![CDATA[${assertion.error!.stack}]]></failure>`;
64
64
  }
65
65
 
66
66
  const groupedByLevel: Record<string, string[]> = {};
@@ -79,8 +79,8 @@ export class XunitEmitter implements TestConsumerShape {
79
79
  <system-err>${this.buildMeta({ error: groupedByLevel.error, warn: groupedByLevel.warn })}</system-err>
80
80
  </testcase>`
81
81
  );
82
- } else if (e.type === 'suite' && e.phase === 'after') {
83
- const { suite } = e;
82
+ } else if (event.type === 'suite' && event.phase === 'after') {
83
+ const { suite } = event;
84
84
  const testBodies = this.#tests.slice(0);
85
85
  this.#tests = [];
86
86
 
@@ -1,7 +1,7 @@
1
1
  import { Class } from '@travetto/runtime';
2
2
 
3
- import { TestEvent } from '../model/event.ts';
4
- import { Counts, SuiteResult } from '../model/suite.ts';
3
+ import type { TestEvent, TestRemoveEvent } from '../model/event.ts';
4
+ import type { Counts, SuiteResult } from '../model/suite.ts';
5
5
 
6
6
  /**
7
7
  * All suite results
@@ -52,6 +52,10 @@ export interface TestConsumerShape {
52
52
  * Handle individual tests events
53
53
  */
54
54
  onEvent(event: TestEvent): void;
55
+ /**
56
+ * Handle when a remove event is fired
57
+ */
58
+ onRemoveEvent?(event: TestRemoveEvent): void;
55
59
  /**
56
60
  * Summarize all results
57
61
  */
@@ -15,7 +15,7 @@ export function Suite(): ClassDecorator;
15
15
  export function Suite(...rest: Partial<SuiteConfig>[]): ClassDecorator;
16
16
  export function Suite(description: string, ...rest: Partial<SuiteConfig>[]): ClassDecorator;
17
17
  export function Suite(description?: string | Partial<SuiteConfig>, ...rest: Partial<SuiteConfig>[]): ClassDecorator {
18
- const dec = (cls: Class): typeof cls => {
18
+ const decorator = (cls: Class): typeof cls => {
19
19
  const isAbstract = describeFunction(cls).abstract;
20
20
  SuiteRegistryIndex.getForRegister(cls).register(
21
21
  ...(typeof description !== 'string' && description ? [description] : []),
@@ -26,7 +26,7 @@ export function Suite(description?: string | Partial<SuiteConfig>, ...rest: Part
26
26
  return cls;
27
27
  };
28
28
 
29
- return castTo(dec);
29
+ return castTo(decorator);
30
30
  }
31
31
 
32
32
  /**