@travetto/test 8.0.0-alpha.2 → 8.0.0-alpha.21

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 (52) hide show
  1. package/README.md +24 -22
  2. package/__index__.ts +9 -9
  3. package/package.json +8 -8
  4. package/src/assert/capture.ts +1 -2
  5. package/src/assert/check.ts +56 -33
  6. package/src/assert/types.ts +1 -1
  7. package/src/assert/util.ts +26 -81
  8. package/src/consumer/decorator.ts +2 -2
  9. package/src/consumer/enhancer.ts +3 -3
  10. package/src/consumer/registry-adapter.ts +3 -3
  11. package/src/consumer/registry-index.ts +6 -5
  12. package/src/consumer/types/cumulative.ts +11 -23
  13. package/src/consumer/types/delegating.ts +8 -2
  14. package/src/consumer/types/event.ts +2 -2
  15. package/src/consumer/types/exec.ts +3 -4
  16. package/src/consumer/types/json.ts +3 -4
  17. package/src/consumer/types/noop.ts +3 -3
  18. package/src/consumer/types/runnable.ts +4 -5
  19. package/src/consumer/types/summarizer.ts +5 -18
  20. package/src/consumer/types/tap-summary.ts +39 -30
  21. package/src/consumer/types/tap.ts +64 -56
  22. package/src/consumer/types/util.ts +31 -0
  23. package/src/consumer/types/xunit.ts +10 -8
  24. package/src/consumer/types.ts +6 -10
  25. package/src/decorator/suite.ts +3 -3
  26. package/src/decorator/test.ts +6 -4
  27. package/src/execute/barrier.ts +15 -9
  28. package/src/execute/console.ts +3 -4
  29. package/src/execute/executor.ts +101 -157
  30. package/src/execute/phase.ts +12 -22
  31. package/src/execute/run.ts +41 -40
  32. package/src/execute/watcher.ts +12 -19
  33. package/src/fixture.ts +6 -7
  34. package/src/model/common.ts +1 -1
  35. package/src/model/error.ts +2 -2
  36. package/src/model/event.ts +14 -9
  37. package/src/model/suite.ts +15 -13
  38. package/src/model/test.ts +11 -8
  39. package/src/model/util.ts +102 -9
  40. package/src/registry/registry-adapter.ts +10 -13
  41. package/src/registry/registry-index.ts +13 -11
  42. package/src/worker/child.ts +10 -9
  43. package/src/worker/standard.ts +11 -14
  44. package/src/worker/types.ts +3 -7
  45. package/support/bin/run.ts +1 -1
  46. package/support/cli.test.ts +20 -15
  47. package/support/cli.test_child.ts +8 -3
  48. package/support/cli.test_diff.ts +11 -7
  49. package/support/cli.test_digest.ts +10 -4
  50. package/support/cli.test_direct.ts +11 -7
  51. package/support/cli.test_watch.ts +6 -4
  52. package/support/transformer.assert.ts +40 -32
@@ -1,15 +1,15 @@
1
- import type { TestConsumerShape } from '../types.ts';
1
+ import type { SuiteCore } from '../../model/common.ts';
2
2
  import type { TestEvent, TestRemoveEvent } from '../../model/event.ts';
3
+ import type { SuiteConfig, SuiteResult } from '../../model/suite.ts';
3
4
  import type { TestConfig, TestDiffSource, TestResult } from '../../model/test.ts';
4
- import type { Counts, SuiteConfig, SuiteResult } from '../../model/suite.ts';
5
- import { DelegatingConsumer } from './delegating.ts';
6
- import type { SuiteCore } from '../../model/common.ts';
7
5
  import { TestModelUtil } from '../../model/util.ts';
6
+ import type { TestConsumerShape } from '../types.ts';
7
+ import { DelegatingConsumer } from './delegating.ts';
8
8
 
9
9
  type ClassId = string;
10
10
  type ImportName = string;
11
11
 
12
- type CumulativeTestResult = Pick<TestResult, 'sourceHash' | 'status' | 'duration'>;
12
+ type CumulativeTestResult = Pick<TestResult, 'sourceHash' | 'status' | 'duration' | 'selfDuration'>;
13
13
  type CumulativeSuiteResult = Pick<SuiteCore, 'import' | 'classId' | 'sourceHash'> & {
14
14
  tests: Record<string, CumulativeTestResult>;
15
15
  };
@@ -32,12 +32,12 @@ export class CumulativeSummaryConsumer extends DelegatingConsumer {
32
32
  }
33
33
 
34
34
  getOrCreateSuite({ tests: _, ...core }: SuiteConfig | SuiteResult): CumulativeSuiteResult {
35
- return (this.#state[core.import] ??= {})[core.classId] ??= { ...core, tests: {} };
35
+ return ((this.#state[core.import] ??= {})[core.classId] ??= { ...core, tests: {} });
36
36
  }
37
37
 
38
38
  onTestBefore(config: TestConfig): TestConfig {
39
39
  const suite = this.getSuite(config);
40
- suite.tests[config.methodName] = { sourceHash: config.sourceHash, status: 'unknown', duration: 0 };
40
+ suite.tests[config.methodName] = { sourceHash: config.sourceHash, status: 'unknown', duration: 0, selfDuration: 0 };
41
41
  return config;
42
42
  }
43
43
 
@@ -56,21 +56,9 @@ export class CumulativeSummaryConsumer extends DelegatingConsumer {
56
56
  onSuiteAfter(result: SuiteResult): SuiteResult {
57
57
  // Reset counts
58
58
  const suite = this.getSuite(result);
59
- const totals: Counts & { duration: number } = {
60
- passed: 0,
61
- failed: 0,
62
- skipped: 0,
63
- errored: 0,
64
- unknown: 0,
65
- total: 0,
66
- duration: 0
67
- };
68
- for (const test of Object.values(suite.tests)) {
69
- totals[test.status] += 1;
70
- totals.total += 1;
71
- totals.duration += test.duration ?? 0;
72
- }
73
- return { ...result, ...totals, status: TestModelUtil.countsToTestStatus(totals) };
59
+ const results = TestModelUtil.buildSummary();
60
+ TestModelUtil.countTestResult(results, Object.values(suite.tests));
61
+ return { ...result, ...results, status: TestModelUtil.computeTestStatus(results) };
74
62
  }
75
63
 
76
64
  removeTest(importName: string, classId?: string, methodName?: string): void {
@@ -126,4 +114,4 @@ export class CumulativeSummaryConsumer extends DelegatingConsumer {
126
114
  }
127
115
  return output;
128
116
  }
129
- }
117
+ }
@@ -1,5 +1,5 @@
1
- import type { SuitesSummary, TestConsumerShape, TestRunState } from '../types.ts';
2
1
  import type { TestEvent, TestRemoveEvent } from '../../model/event.ts';
2
+ import type { SuitesSummary, TestConsumerShape, TestRunState } from '../types.ts';
3
3
 
4
4
  /**
5
5
  * Delegating event consumer
@@ -20,6 +20,12 @@ export abstract class DelegatingConsumer implements TestConsumerShape {
20
20
  }
21
21
  }
22
22
 
23
+ async onTestRunState(state: TestRunState): Promise<void> {
24
+ for (const consumer of this.#consumers) {
25
+ await consumer.onTestRunState?.(state);
26
+ }
27
+ }
28
+
23
29
  onRemoveEvent(event: TestRemoveEvent): void {
24
30
  let result = event;
25
31
  if (this.transformRemove) {
@@ -58,4 +64,4 @@ export abstract class DelegatingConsumer implements TestConsumerShape {
58
64
 
59
65
  transform?(event: TestEvent): TestEvent | undefined;
60
66
  transformRemove?(event: TestRemoveEvent): TestRemoveEvent | undefined;
61
- }
67
+ }
@@ -3,8 +3,8 @@ import type { Writable } from 'node:stream';
3
3
  import { JSONUtil } from '@travetto/runtime';
4
4
 
5
5
  import type { TestEvent, TestRemoveEvent } from '../../model/event.ts';
6
- import type { TestConsumerShape } from '../types.ts';
7
6
  import { TestConsumer } from '../decorator.ts';
7
+ import type { TestConsumerShape } from '../types.ts';
8
8
 
9
9
  /**
10
10
  * Streams all test events a JSON payload, in an nd-json format
@@ -28,4 +28,4 @@ export class EventStreamer implements TestConsumerShape {
28
28
  onRemoveEvent(event: TestRemoveEvent): void {
29
29
  this.sendPayload(event);
30
30
  }
31
- }
31
+ }
@@ -1,16 +1,15 @@
1
- import { IpcChannel } from '@travetto/worker';
2
1
  import { JSONUtil } from '@travetto/runtime';
2
+ import { IpcChannel } from '@travetto/worker';
3
3
 
4
4
  import type { TestEvent, TestRemoveEvent } from '../../model/event.ts';
5
- import type { TestConsumerShape } from '../types.ts';
6
5
  import { TestConsumer } from '../decorator.ts';
6
+ import type { TestConsumerShape } from '../types.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
13
  sendPayload(payload: unknown & { type: string }): void {
15
14
  this.send(payload.type, JSONUtil.cloneForTransmit(payload));
16
15
  }
@@ -22,4 +21,4 @@ export class ExecutionEmitter extends IpcChannel<TestEvent> implements TestConsu
22
21
  onRemoveEvent(event: TestRemoveEvent): void {
23
22
  this.sendPayload(event);
24
23
  }
25
- }
24
+ }
@@ -2,24 +2,23 @@ import type { Writable } from 'node:stream';
2
2
 
3
3
  import { JSONUtil } from '@travetto/runtime';
4
4
 
5
- import type { SuitesSummary } from '../types.ts';
6
5
  import { TestConsumer } from '../decorator.ts';
6
+ import type { SuitesSummary } from '../types.ts';
7
7
 
8
8
  /**
9
9
  * Returns the entire result set as a single JSON document
10
10
  */
11
11
  @TestConsumer()
12
12
  export class JSONEmitter {
13
-
14
13
  #stream: Writable;
15
14
 
16
15
  constructor(stream: Writable = process.stdout) {
17
16
  this.#stream = stream;
18
17
  }
19
18
 
20
- onEvent(): void { }
19
+ onEvent(): void {}
21
20
 
22
21
  onSummary(summary: SuitesSummary): void {
23
22
  this.#stream.write(JSONUtil.toUTF8Pretty(summary));
24
23
  }
25
- }
24
+ }
@@ -1,10 +1,10 @@
1
- import type { TestConsumerShape } from '../types.ts';
2
1
  import { TestConsumer } from '../decorator.ts';
2
+ import type { TestConsumerShape } from '../types.ts';
3
3
 
4
4
  /**
5
5
  * Does nothing consumer
6
6
  */
7
7
  @TestConsumer()
8
8
  export class NoopConsumer implements TestConsumerShape {
9
- onEvent(): void { }
10
- }
9
+ onEvent(): void {}
10
+ }
@@ -1,13 +1,12 @@
1
- import type { TestConsumerShape } from '../types.ts';
2
- import { TestResultsSummarizer } from './summarizer.ts';
3
1
  import type { TestEvent } from '../../model/event.ts';
2
+ import type { TestConsumerShape } from '../types.ts';
4
3
  import { DelegatingConsumer } from './delegating.ts';
4
+ import { TestResultsSummarizer } from './summarizer.ts';
5
5
 
6
6
  /**
7
7
  * Test consumer with support for multiple nested consumers, and summarization
8
8
  */
9
9
  export class RunnableTestConsumer extends DelegatingConsumer {
10
-
11
10
  #results?: TestResultsSummarizer;
12
11
 
13
12
  constructor(...consumers: TestConsumerShape[]) {
@@ -22,6 +21,6 @@ export class RunnableTestConsumer extends DelegatingConsumer {
22
21
 
23
22
  async summarizeAsBoolean(): Promise<boolean> {
24
23
  await this.summarize(this.#results?.summary);
25
- return (this.#results?.summary.failed ?? 0) <= 0;
24
+ return (this.#results?.summary.failed ?? 0) <= 0 && (this.#results?.summary.errored ?? 0) <= 0;
26
25
  }
27
- }
26
+ }
@@ -1,33 +1,20 @@
1
- import type { SuiteResult } from '../../model/suite.ts';
2
1
  import type { TestEvent } from '../../model/event.ts';
2
+ import type { SuiteResult } from '../../model/suite.ts';
3
+ import { TestModelUtil } from '../../model/util.ts';
3
4
  import type { SuitesSummary, TestConsumerShape } from '../types.ts';
4
5
 
5
6
  /**
6
7
  * Test Result Collector, combines all results into a single Suite Result
7
8
  */
8
9
  export class TestResultsSummarizer implements TestConsumerShape {
9
-
10
10
  summary: SuitesSummary = {
11
- passed: 0,
12
- failed: 0,
13
- errored: 0,
14
- skipped: 0,
15
- unknown: 0,
16
- total: 0,
17
- duration: 0,
18
- suites: [],
19
- errors: []
11
+ ...TestModelUtil.buildSummary(),
12
+ suites: []
20
13
  };
21
14
 
22
15
  #merge(result: SuiteResult): void {
16
+ TestModelUtil.countTestResult(this.summary, Object.values(result.tests));
23
17
  this.summary.suites.push(result);
24
- this.summary.failed += result.failed;
25
- this.summary.errored += result.errored;
26
- this.summary.passed += result.passed;
27
- this.summary.unknown += result.unknown;
28
- this.summary.skipped += result.skipped;
29
- this.summary.duration += result.duration;
30
- this.summary.total += result.total;
31
18
  }
32
19
 
33
20
  /**
@@ -1,15 +1,14 @@
1
- import { Util, AsyncQueue } from '@travetto/runtime';
2
- import { StyleUtil, Terminal, TerminalUtil } from '@travetto/terminal';
1
+ import { AsyncQueue, Util } from '@travetto/runtime';
2
+ import { Terminal, TerminalUtil } from '@travetto/terminal';
3
3
 
4
4
  import type { TestEvent } from '../../model/event.ts';
5
- import type { TestResult, TestStatus } from '../../model/test.ts';
6
-
7
- import type { SuitesSummary, TestConsumerShape, TestRunState } from '../types.ts';
5
+ import type { SuiteResult } from '../../model/suite.ts';
6
+ import type { TestResult } from '../../model/test.ts';
7
+ import { TestModelUtil } from '../../model/util.ts';
8
8
  import { TestConsumer } from '../decorator.ts';
9
-
10
- import { TapEmitter } from './tap.ts';
11
9
  import { CONSOLE_ENHANCER, type TestResultsEnhancer } from '../enhancer.ts';
12
- import type { SuiteResult } from '../../model/suite.ts';
10
+ import type { SuitesSummary, TestConsumerShape, TestRunState } from '../types.ts';
11
+ import { TapEmitter } from './tap.ts';
13
12
 
14
13
  type Result = {
15
14
  key: string;
@@ -22,7 +21,6 @@ type Result = {
22
21
  */
23
22
  @TestConsumer()
24
23
  export class TapSummaryEmitter implements TestConsumerShape {
25
-
26
24
  #timings = new Map<'test' | 'module' | 'file' | 'suite', Map<string, Result>>();
27
25
 
28
26
  #terminal: Terminal;
@@ -31,6 +29,7 @@ export class TapSummaryEmitter implements TestConsumerShape {
31
29
  #consumer: TapEmitter;
32
30
  #enhancer: TestResultsEnhancer;
33
31
  #options?: Record<string, unknown>;
32
+ #state: TestRunState = {};
34
33
 
35
34
  constructor(terminal: Terminal = new Terminal(process.stderr)) {
36
35
  this.#terminal = terminal;
@@ -51,20 +50,16 @@ export class TapSummaryEmitter implements TestConsumerShape {
51
50
  foundModule.duration += suite.duration;
52
51
  foundModule.tests += testCount;
53
52
 
54
- const foundFile = this.#timings
55
- .getOrInsert('file', new Map<string, Result>())
56
- .getOrInsert(file, { key: file, duration: 0, tests: 0 });
53
+ const foundFile = this.#timings.getOrInsert('file', new Map<string, Result>()).getOrInsert(file, { key: file, duration: 0, tests: 0 });
57
54
 
58
55
  foundFile.duration += suite.duration;
59
56
  foundFile.tests += testCount;
60
57
 
61
- this.#timings
62
- .getOrInsert('suite', new Map<string, Result>())
63
- .set(suite.classId, {
64
- key: suite.classId,
65
- duration: suite.duration,
66
- tests: testCount
67
- });
58
+ this.#timings.getOrInsert('suite', new Map<string, Result>()).set(suite.classId, {
59
+ key: suite.classId,
60
+ duration: suite.duration,
61
+ tests: testCount
62
+ });
68
63
  }
69
64
 
70
65
  #renderTimings(): void {
@@ -75,7 +70,9 @@ export class TapSummaryEmitter implements TestConsumerShape {
75
70
  const top10 = [...results.values()].toSorted((a, b) => b.duration - a.duration).slice(0, count);
76
71
 
77
72
  for (const result of top10) {
78
- console.log(` * ${this.#enhancer.testName(result.key)} - ${this.#enhancer.total(result.duration)}ms / ${this.#enhancer.total(result.tests)} tests`);
73
+ console.log(
74
+ ` * ${this.#enhancer.testName(result.key)} - ${this.#enhancer.total(result.duration)}ms / ${this.#enhancer.total(result.tests)} tests`
75
+ );
79
76
  }
80
77
  this.#consumer.log('');
81
78
  }
@@ -87,22 +84,34 @@ export class TapSummaryEmitter implements TestConsumerShape {
87
84
  this.#consumer.setOptions(options);
88
85
  }
89
86
 
87
+ onTestRunState(state: TestRunState): void {
88
+ Object.assign(this.#state, state);
89
+ }
90
+
90
91
  async onStart(state: TestRunState): Promise<void> {
91
92
  this.#consumer.onStart();
92
- const total: Record<TestStatus | 'count', number> = { errored: 0, failed: 0, passed: 0, skipped: 0, unknown: 0, count: 0 };
93
- const success = StyleUtil.getStyle({ text: '#e5e5e5', background: '#026020' }); // White on dark green
94
- const fail = StyleUtil.getStyle({ text: '#e5e5e5', background: '#8b0000' }); // White on dark red
93
+ this.onTestRunState(state);
94
+
95
+ const total = TestModelUtil.buildSummary();
95
96
  this.#progress = this.#terminal.streamToBottom(
96
97
  Util.mapAsyncIterable(
97
98
  this.#results,
98
- (value) => {
99
- total[value.status] += 1;
100
- total.count += 1;
99
+ value => {
100
+ TestModelUtil.countTestResult(total, [value]);
101
101
  const statusLine = `${total.failed} failed, ${total.errored} errored, ${total.skipped} skipped`;
102
- return { value: `Tests %idx/%total [${statusLine}] -- ${value.classId}`, total: state.testCount, idx: total.passed };
103
-
102
+ return {
103
+ value: `Tests %completed/%total [${statusLine}] -- ${value.classId}`,
104
+ total: this.#state.testCount,
105
+ completed: total.passed,
106
+ failed: total.failed + total.errored
107
+ };
104
108
  },
105
- TerminalUtil.progressBarUpdater(this.#terminal, { style: () => ({ complete: (total.failed || total.errored) ? fail : success }) })
109
+ TerminalUtil.progressBarUpdater(this.#terminal, {
110
+ style: {
111
+ complete: { text: '#e5e5e5', background: '#026020' },
112
+ failed: { text: '#e5e5e5', background: '#8b0000' }
113
+ }
114
+ })
106
115
  ),
107
116
  { minDelay: 100 }
108
117
  );
@@ -112,7 +121,7 @@ export class TapSummaryEmitter implements TestConsumerShape {
112
121
  if (event.type === 'test' && event.phase === 'after') {
113
122
  const { test } = event;
114
123
  this.#results.add(test);
115
- if (test.status === 'failed') {
124
+ if (test.status !== 'passed' && test.status !== 'skipped') {
116
125
  this.#consumer.onEvent(event);
117
126
  }
118
127
  const tests = this.#timings.getOrInsert('test', new Map<string, Result>());
@@ -1,13 +1,17 @@
1
1
  import path from 'node:path';
2
+
2
3
  import { stringify } from 'yaml';
3
4
 
4
- import { Terminal, StyleUtil } from '@travetto/terminal';
5
- import { TimeUtil, RuntimeIndex, hasToJSON, JSONUtil } from '@travetto/runtime';
5
+ import { RuntimeIndex, TimeUtil } from '@travetto/runtime';
6
+ import { StyleUtil, Terminal } from '@travetto/terminal';
6
7
 
7
8
  import type { TestEvent } from '../../model/event.ts';
8
- import type { SuitesSummary, TestConsumerShape } from '../types.ts';
9
9
  import { TestConsumer } from '../decorator.ts';
10
- import { type TestResultsEnhancer, CONSOLE_ENHANCER } from '../enhancer.ts';
10
+ import { CONSOLE_ENHANCER, type TestResultsEnhancer } from '../enhancer.ts';
11
+ import type { SuitesSummary, TestConsumerShape } from '../types.ts';
12
+ import { TestConsumerUtil } from './util.ts';
13
+
14
+ const SPACE = ' ';
11
15
 
12
16
  /**
13
17
  * TAP Format consumer
@@ -17,13 +21,10 @@ export class TapEmitter implements TestConsumerShape {
17
21
  #count = 0;
18
22
  #enhancer: TestResultsEnhancer;
19
23
  #terminal: Terminal;
20
- #start: number;
21
24
  #options?: Record<string, unknown>;
25
+ #start: number = 0;
22
26
 
23
- constructor(
24
- terminal = new Terminal(),
25
- enhancer: TestResultsEnhancer = CONSOLE_ENHANCER
26
- ) {
27
+ constructor(terminal = new Terminal(), enhancer: TestResultsEnhancer = CONSOLE_ENHANCER) {
27
28
  this.#terminal = terminal;
28
29
  this.#enhancer = enhancer;
29
30
  }
@@ -50,28 +51,13 @@ export class TapEmitter implements TestConsumerShape {
50
51
  logMeta(metadata: Record<string, unknown>): void {
51
52
  const lineLength = this.#terminal.width - 5;
52
53
  let body = stringify(metadata, { lineWidth: lineLength, indent: 2 });
53
- body = body.split('\n').map(line => ` ${line}`).join('\n');
54
+ body = body
55
+ .split('\n')
56
+ .map(line => ` ${line}`)
57
+ .join('\n');
54
58
  this.log(`---\n${this.#enhancer.objectInspect(body)}\n...`);
55
59
  }
56
60
 
57
- /**
58
- * Error to string
59
- * @param error
60
- */
61
- errorToString(error?: Error): string | undefined {
62
- if (error && error.name !== 'AssertionError') {
63
- if (error instanceof Error) {
64
- let out = JSONUtil.toUTF8(hasToJSON(error) ? error.toJSON() : error, { indent: 2 });
65
- if (this.#options?.verbose && error.stack) {
66
- out = `${out}\n${error.stack}`;
67
- }
68
- return out;
69
- } else {
70
- return `${error}`;
71
- }
72
- }
73
- }
74
-
75
61
  /**
76
62
  * Listen for each event
77
63
  */
@@ -86,7 +72,7 @@ export class TapEmitter implements TestConsumerShape {
86
72
  StyleUtil.link(suiteId, `file://${suiteSourceFile}#${test.suiteLineStart ?? 1}`),
87
73
  ' - ',
88
74
  StyleUtil.link(this.#enhancer.testName(test.methodName), `file://${testSourceFile}#${test.lineStart}`),
89
- ...test.description ? [`: ${this.#enhancer.testDescription(test.description)}`] : []
75
+ ...(test.description ? [`: ${this.#enhancer.testDescription(test.description)}`] : [])
90
76
  ].join('');
91
77
 
92
78
  this.log(`# ${header}`);
@@ -125,9 +111,17 @@ export class TapEmitter implements TestConsumerShape {
125
111
  // Track test result
126
112
  let status = `${this.#enhancer.testNumber(++this.#count)} `;
127
113
  switch (test.status) {
128
- case 'skipped': status += ' # SKIP'; break;
129
- case 'failed': status = `${this.#enhancer.failure('not ok')} ${status}`; break;
130
- default: status = `${this.#enhancer.success('ok')} ${status}`;
114
+ case 'passed':
115
+ `${this.#enhancer.success('ok')} ${status}`;
116
+ break;
117
+ case 'skipped':
118
+ status += ' # SKIP';
119
+ break;
120
+ case 'unknown':
121
+ break;
122
+ default:
123
+ status = `${this.#enhancer.failure('not ok')} ${status}`;
124
+ break;
131
125
  }
132
126
  status += header;
133
127
 
@@ -138,9 +132,9 @@ export class TapEmitter implements TestConsumerShape {
138
132
  case 'errored':
139
133
  case 'failed': {
140
134
  if (test.error) {
141
- const msg = this.errorToString(test.error);
142
- if (msg) {
143
- this.logMeta({ error: msg });
135
+ const message = TestConsumerUtil.errorToString(test.error, !!this.#options?.verbose);
136
+ if (message) {
137
+ this.logMeta({ error: message });
144
138
  }
145
139
  }
146
140
  break;
@@ -168,28 +162,42 @@ export class TapEmitter implements TestConsumerShape {
168
162
  onSummary(summary: SuitesSummary): void {
169
163
  this.log(`${this.#enhancer.testNumber(1)}..${this.#enhancer.testNumber(summary.total)}`);
170
164
 
171
- if (summary.errors.length) {
172
- this.log('---\n');
173
- for (const error of summary.errors) {
174
- const msg = this.errorToString(error);
175
- if (msg) {
176
- this.log(this.#enhancer.failure(msg));
177
- }
178
- }
179
- }
180
-
181
165
  const allPassed = !summary.failed && !summary.errored;
182
166
 
183
- this.log([
184
- this.#enhancer[allPassed ? 'success' : 'failure']('Results'),
185
- `${this.#enhancer.total(summary.passed)}/${this.#enhancer.total(summary.total)},`,
186
- allPassed ? 'failed' : this.#enhancer.failure('failed'),
187
- `${this.#enhancer.total(summary.failed)}`,
188
- allPassed ? 'errored' : this.#enhancer.failure('errored'),
189
- `${this.#enhancer.total(summary.errored)}`,
190
- 'skipped',
191
- this.#enhancer.total(summary.skipped),
192
- `# (Total Test Time: ${TimeUtil.asClock(summary.duration)}, Total Run Time: ${TimeUtil.asClock(Date.now() - this.#start)})`
193
- ].join(' '));
167
+ this.log(
168
+ [
169
+ this.#enhancer[allPassed ? 'success' : 'failure']('Results'),
170
+ SPACE,
171
+ `${this.#enhancer.total(summary.passed)}/${this.#enhancer.total(summary.total)},`,
172
+ SPACE,
173
+ allPassed ? 'failed' : this.#enhancer.failure('failed'),
174
+ SPACE,
175
+ `${this.#enhancer.total(summary.failed)}`,
176
+ SPACE,
177
+ allPassed ? 'errored' : this.#enhancer.failure('errored'),
178
+ SPACE,
179
+ `${this.#enhancer.total(summary.errored)}`,
180
+ SPACE,
181
+ 'skipped',
182
+ SPACE,
183
+ this.#enhancer.total(summary.skipped),
184
+ SPACE,
185
+ '#',
186
+ SPACE,
187
+ '(Timings:',
188
+ SPACE,
189
+ 'Self=',
190
+ TimeUtil.asClock(summary.selfDuration),
191
+ ',',
192
+ SPACE,
193
+ 'Total=',
194
+ TimeUtil.asClock(summary.duration),
195
+ ',',
196
+ SPACE,
197
+ 'Clock=',
198
+ TimeUtil.asClock(Date.now() - this.#start),
199
+ ')'
200
+ ].join('')
201
+ );
194
202
  }
195
203
  }
@@ -0,0 +1,31 @@
1
+ import { AssertionError } from 'node:assert';
2
+ import util from 'node:util';
3
+
4
+ import { TypedObject } from '@travetto/runtime';
5
+
6
+ export class TestConsumerUtil {
7
+ /**
8
+ * Convert error to string
9
+ */
10
+ static errorToString(error?: Error, verbose?: boolean): string | undefined {
11
+ if (error instanceof AssertionError) {
12
+ return;
13
+ } else if (error instanceof Error) {
14
+ const stack = error.stack
15
+ ? error.stack
16
+ .split(/\n/)
17
+ .slice(0, verbose ? -1 : 5)
18
+ .join('\n')
19
+ : error.message;
20
+ const subObject: Record<string, unknown> = {};
21
+ for (const key of TypedObject.keys(error)) {
22
+ if (key !== 'stack' && key !== 'message' && key !== 'name') {
23
+ subObject[key] = error[key];
24
+ }
25
+ }
26
+ return `${stack}${Object.keys(subObject).length ? `\n${util.inspect(subObject)}` : ''}`;
27
+ } else {
28
+ return `${error}`;
29
+ }
30
+ }
31
+ }
@@ -5,8 +5,9 @@ import { stringify } from 'yaml';
5
5
  import { RuntimeIndex } from '@travetto/runtime';
6
6
 
7
7
  import type { TestEvent } from '../../model/event.ts';
8
- import type { SuitesSummary, TestConsumerShape } from '../types.ts';
9
8
  import { TestConsumer } from '../decorator.ts';
9
+ import type { SuitesSummary, TestConsumerShape } from '../types.ts';
10
+ import { TestConsumerUtil } from './util.ts';
10
11
 
11
12
  /**
12
13
  * Xunit consumer, compatible with JUnit formatters
@@ -36,7 +37,10 @@ export class XunitEmitter implements TestConsumerShape {
36
37
  }
37
38
  if (Object.keys(metadata).length) {
38
39
  let body = stringify(metadata);
39
- body = body.split('\n').map(line => ` ${line}`).join('\n');
40
+ body = body
41
+ .split('\n')
42
+ .map(line => ` ${line}`)
43
+ .join('\n');
40
44
  return `<![CDATA[\n${body}\n]]>`;
41
45
  } else {
42
46
  return '';
@@ -48,7 +52,6 @@ export class XunitEmitter implements TestConsumerShape {
48
52
  */
49
53
  onEvent(event: TestEvent): void {
50
54
  if (event.type === 'test' && event.phase === 'after') {
51
-
52
55
  const { test } = event;
53
56
 
54
57
  let name = `${test.methodName}`;
@@ -59,9 +62,9 @@ export class XunitEmitter implements TestConsumerShape {
59
62
  let body = '';
60
63
 
61
64
  if (test.error) {
62
- const assertion = test.assertions.find(item => !!item.error)!;
65
+ const errorMessage = TestConsumerUtil.errorToString(test.error);
63
66
  const node = test.status === 'failed' ? 'failure' : 'error';
64
- body = `<${node} type="${assertion.text}" message="${encodeURIComponent(assertion.message!)}"><![CDATA[${assertion.error!.stack}]]></${node}>`;
67
+ body = `<${node} type="${test.error.constructor.name}" message="${encodeURIComponent(test.error.message)}"><![CDATA[${errorMessage}]]></${node}>`;
65
68
  }
66
69
 
67
70
  const groupedByLevel: Record<string, string[]> = {};
@@ -78,8 +81,7 @@ export class XunitEmitter implements TestConsumerShape {
78
81
  ${body}
79
82
  <system-out>${this.buildMeta({ log: groupedByLevel.log, info: groupedByLevel.info, debug: groupedByLevel.debug })}</system-out>
80
83
  <system-err>${this.buildMeta({ error: groupedByLevel.error, warn: groupedByLevel.warn })}</system-err>
81
- </testcase>`
82
- );
84
+ </testcase>`);
83
85
  } else if (event.type === 'suite' && event.phase === 'after') {
84
86
  const { suite } = event;
85
87
  const testBodies = this.#tests.slice(0);
@@ -120,4 +122,4 @@ export class XunitEmitter implements TestConsumerShape {
120
122
  </testsuites>
121
123
  `);
122
124
  }
123
- }
125
+ }
@@ -1,24 +1,16 @@
1
1
  import type { Class } from '@travetto/runtime';
2
2
 
3
3
  import type { TestEvent, TestRemoveEvent } from '../model/event.ts';
4
- import type { Counts, SuiteResult } from '../model/suite.ts';
4
+ import type { ResultsSummary, SuiteResult } from '../model/suite.ts';
5
5
 
6
6
  /**
7
7
  * All suite results
8
8
  */
9
- export interface SuitesSummary extends Counts {
9
+ export interface SuitesSummary extends ResultsSummary {
10
10
  /**
11
11
  * List of all suites
12
12
  */
13
13
  suites: SuiteResult[];
14
- /**
15
- * List of all errors
16
- */
17
- errors: Error[];
18
- /**
19
- * Total duration
20
- */
21
- duration: number;
22
14
  }
23
15
 
24
16
  export type TestRunState = {
@@ -44,6 +36,10 @@ export interface TestConsumerShape {
44
36
  * Set options
45
37
  */
46
38
  setOptions?(options?: Record<string, unknown>): Promise<void> | void;
39
+ /**
40
+ * Can directly update the known test run state as needed
41
+ */
42
+ onTestRunState?(state: TestRunState): Promise<void> | void;
47
43
  /**
48
44
  * Listen for start of the test run
49
45
  */