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

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.
package/README.md CHANGED
@@ -220,18 +220,26 @@ class SimpleTest {
220
220
  ## Running Tests
221
221
  To run the tests you can either call the [Command Line Interface](https://github.com/travetto/travetto/tree/main/module/cli#readme "CLI infrastructure for Travetto framework") by invoking
222
222
 
223
- **Terminal: Test Help Output**
223
+ ## CLI - test
224
+
225
+ **Terminal: Help for test**
224
226
  ```bash
225
227
  $ trv test --help
226
228
 
227
229
  Usage: test [options] [first:string] [globs...:string]
228
230
 
231
+ Description:
232
+ Execute the test framework for targeted files, suites, or methods.
233
+
234
+ Supports glob-based discovery, import-based targeting, tag filtering, and
235
+ configurable output consumers for local and CI workflows.
236
+
229
237
  Options:
230
238
  -f, --format <string> Output format for test results (default: "tap")
231
239
  -c, --concurrency <number> Number of tests to run concurrently (default: 9)
232
240
  -t, --tags <string> Tags to target or exclude when using globs
233
241
  -o, --format-options <string> Format options
234
- -h, --help display help for command
242
+ --help display help for command
235
243
  ```
236
244
 
237
245
  The regexes are the patterns of tests you want to run, and all tests must be found under the `test/` folder.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@travetto/test",
3
- "version": "8.0.0-alpha.2",
3
+ "version": "8.0.0-alpha.20",
4
4
  "type": "module",
5
5
  "description": "Declarative test framework",
6
6
  "keywords": [
@@ -28,15 +28,15 @@
28
28
  "directory": "module/test"
29
29
  },
30
30
  "dependencies": {
31
- "@travetto/registry": "^8.0.0-alpha.2",
32
- "@travetto/runtime": "^8.0.0-alpha.2",
33
- "@travetto/terminal": "^8.0.0-alpha.2",
34
- "@travetto/worker": "^8.0.0-alpha.2",
35
- "yaml": "^2.8.2"
31
+ "@travetto/registry": "^8.0.0-alpha.19",
32
+ "@travetto/runtime": "^8.0.0-alpha.19",
33
+ "@travetto/terminal": "^8.0.0-alpha.19",
34
+ "@travetto/worker": "^8.0.0-alpha.19",
35
+ "yaml": "^2.9.0"
36
36
  },
37
37
  "peerDependencies": {
38
- "@travetto/cli": "^8.0.0-alpha.3",
39
- "@travetto/transformer": "^8.0.0-alpha.2"
38
+ "@travetto/cli": "^8.0.0-alpha.27",
39
+ "@travetto/transformer": "^8.0.0-alpha.13"
40
40
  },
41
41
  "peerDependenciesMeta": {
42
42
  "@travetto/transformer": {
@@ -36,7 +36,7 @@ export class AssertCheck {
36
36
  };
37
37
 
38
38
  // Invert check for negative
39
- const assertFn = positive ? assert : (value: unknown, msg?: string): unknown => assert(!value, msg);
39
+ const assertFn = positive ? assert : (value: unknown, msg?: string): unknown => assert(!value, msg!);
40
40
 
41
41
  // Check fn to call
42
42
  if (fn === 'fail') {
@@ -91,7 +91,7 @@ export class AssertCheck {
91
91
  case 'lessThanEqual': assertFn(castTo<number>(actual) <= castTo<number>(expected), message); break;
92
92
  case 'greaterThan': assertFn(castTo<number>(actual) > castTo<number>(expected), message); break;
93
93
  case 'greaterThanEqual': assertFn(castTo<number>(actual) >= castTo<number>(expected), message); break;
94
- case 'ok': assertFn(...castTo<Parameters<typeof assertFn>>(args)); break;
94
+ case 'ok': assertFn.apply(null, castTo(args)); break;
95
95
  default:
96
96
  if (fn && assert[castKey<typeof assert>(fn)]) { // Assert call
97
97
  if (/not/i.test(fn)) {
@@ -178,17 +178,13 @@ export class AssertCheck {
178
178
  static #onError(
179
179
  positive: boolean,
180
180
  message: string | undefined,
181
- error: unknown,
181
+ errorValue: unknown,
182
182
  missed: Error | undefined,
183
183
  shouldThrow: ThrowableError | undefined,
184
184
  assertion: CapturedAssertion
185
185
  ): void {
186
- if (!(error instanceof Error)) {
187
- error = new Error(`${error}`);
188
- }
189
- if (!(error instanceof Error)) {
190
- throw error;
191
- }
186
+ const error = errorValue instanceof Error ? errorValue : new Error(`${errorValue}`);
187
+
192
188
  if (positive) {
193
189
  missed = new assert.AssertionError({ message: 'Error thrown, but expected no errors' });
194
190
  missed.stack = error.stack;
@@ -1,10 +1,9 @@
1
1
  import util from 'node:util';
2
- import path from 'node:path';
3
2
 
4
- import { asFull, type Class, JSONUtil, hasFunction, Runtime, RuntimeIndex, Util } from '@travetto/runtime';
3
+ import { JSONUtil, hasFunction, RuntimeIndex, Util } from '@travetto/runtime';
5
4
 
6
- import type { TestConfig, TestResult } from '../model/test.ts';
7
- import type { SuiteConfig, SuiteResult } from '../model/suite.ts';
5
+ import type { Assertion, TestConfig } from '../model/test.ts';
6
+ import type { SuiteConfig } from '../model/suite.ts';
8
7
 
9
8
  const isCleanable = hasFunction<{ toClean(): unknown }>('toClean');
10
9
 
@@ -52,79 +51,21 @@ export class AssertUtil {
52
51
  /**
53
52
  * Generate a suite error given a suite config, and an error
54
53
  */
55
- static generateSuiteTestFailure(config: { suite: SuiteConfig, test: TestConfig, error: Error, importLocation?: string }): TestResult {
56
- const { suite, test, error, importLocation } = config;
54
+ static generateAssertion(config: { suite: SuiteConfig, test: TestConfig, error: Error, importLocation?: string }): Assertion {
55
+ const { suite, test, error: errorValue, importLocation } = config;
56
+ const error = (errorValue.cause && errorValue.cause instanceof Error) ? errorValue.cause : errorValue;
57
57
  const testImport = importLocation ?? test.import;
58
58
  const position = this.getPositionOfError(error);
59
59
  const line = position?.line ?? (testImport === suite.import ? suite.lineStart : 1);
60
- const testResult: TestResult = {
61
- ...suite.tests[test.methodName],
62
- suiteLineStart: suite.lineStart,
63
- status: 'errored',
60
+ return {
61
+ import: position?.import ?? testImport,
62
+ methodName: test.methodName,
63
+ classId: suite.classId,
64
+ operator: 'throw',
64
65
  error,
65
- duration: 0,
66
- durationTotal: 0,
67
- output: [],
68
- assertions: [{
69
- import: position?.import ?? testImport,
70
- methodName: test.methodName,
71
- classId: suite.classId,
72
- operator: 'throw',
73
- error,
74
- line,
75
- message: error.message.split(/\n/)[0],
76
- text: test.methodName
77
- }],
66
+ line,
67
+ message: error.message.split(/\n/)[0],
68
+ text: test.methodName
78
69
  };
79
-
80
- return testResult;
81
- }
82
-
83
- /**
84
- * Generate suite failure
85
- */
86
- static generateSuiteTestFailures(suite: SuiteConfig, error: Error): TestResult[] {
87
- const finalError = error.cause instanceof Error ? error.cause : error;
88
- return Object.values(suite.tests).map(test => this.generateSuiteTestFailure({ suite, test, error: finalError }));
89
- }
90
-
91
- /**
92
- * Define import failure as a TestResult
93
- */
94
- static gernerateImportFailure(importLocation: string, error: Error): { result: TestResult, test: TestConfig, suite: SuiteResult & SuiteConfig } {
95
- const name = path.basename(importLocation);
96
- const classId = `${RuntimeIndex.getFromImport(importLocation)?.id}#${name}`;
97
- const suite = asFull<SuiteConfig & SuiteResult>({
98
- class: asFull<Class>({ name }), classId, duration: 0, lineStart: 1, lineEnd: 1, import: importLocation
99
- });
100
- error.message = error.message.replaceAll(Runtime.mainSourcePath, '.');
101
- const result = this.generateSuiteTestFailure({
102
- suite,
103
- test: {
104
- methodName: 'require',
105
- classId,
106
- import: importLocation,
107
- class: suite.class,
108
- lineBodyStart: 1,
109
- lineStart: 1,
110
- lineEnd: 1,
111
- skip: false
112
- },
113
- error
114
- });
115
- const test: TestConfig = {
116
- methodName: 'import',
117
- classId,
118
- import: importLocation,
119
- declarationImport: importLocation,
120
- lineStart: 0,
121
- lineEnd: 0,
122
- lineBodyStart: 0,
123
- tags: [],
124
- description: 'Import Failure',
125
- skip: false,
126
- class: undefined!
127
- };
128
- return { result, test, suite };
129
70
  }
130
71
  }
@@ -1,7 +1,7 @@
1
1
  import type { TestConsumerShape } from '../types.ts';
2
2
  import type { TestEvent, TestRemoveEvent } from '../../model/event.ts';
3
3
  import type { TestConfig, TestDiffSource, TestResult } from '../../model/test.ts';
4
- import type { Counts, SuiteConfig, SuiteResult } from '../../model/suite.ts';
4
+ import type { SuiteConfig, SuiteResult } from '../../model/suite.ts';
5
5
  import { DelegatingConsumer } from './delegating.ts';
6
6
  import type { SuiteCore } from '../../model/common.ts';
7
7
  import { TestModelUtil } from '../../model/util.ts';
@@ -9,7 +9,7 @@ import { TestModelUtil } from '../../model/util.ts';
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
  };
@@ -37,7 +37,7 @@ export class CumulativeSummaryConsumer extends DelegatingConsumer {
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 {
@@ -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) {
@@ -22,6 +22,7 @@ export class RunnableTestConsumer extends DelegatingConsumer {
22
22
 
23
23
  async summarizeAsBoolean(): Promise<boolean> {
24
24
  await this.summarize(this.#results?.summary);
25
- return (this.#results?.summary.failed ?? 0) <= 0;
25
+ return (this.#results?.summary.failed ?? 0) <= 0 &&
26
+ (this.#results?.summary.errored ?? 0) <= 0;
26
27
  }
27
28
  }
@@ -1,6 +1,7 @@
1
1
  import type { SuiteResult } from '../../model/suite.ts';
2
2
  import type { TestEvent } from '../../model/event.ts';
3
3
  import type { SuitesSummary, TestConsumerShape } from '../types.ts';
4
+ import { TestModelUtil } from '../../model/util.ts';
4
5
 
5
6
  /**
6
7
  * Test Result Collector, combines all results into a single Suite Result
@@ -8,26 +9,13 @@ import type { SuitesSummary, TestConsumerShape } from '../types.ts';
8
9
  export class TestResultsSummarizer implements TestConsumerShape {
9
10
 
10
11
  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: []
12
+ ...TestModelUtil.buildSummary(),
13
+ suites: []
20
14
  };
21
15
 
22
16
  #merge(result: SuiteResult): void {
17
+ TestModelUtil.countTestResult(this.summary, Object.values(result.tests));
23
18
  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
19
  }
32
20
 
33
21
  /**
@@ -1,8 +1,8 @@
1
1
  import { Util, AsyncQueue } from '@travetto/runtime';
2
- import { StyleUtil, Terminal, TerminalUtil } from '@travetto/terminal';
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';
5
+ import type { TestResult } from '../../model/test.ts';
6
6
 
7
7
  import type { SuitesSummary, TestConsumerShape, TestRunState } from '../types.ts';
8
8
  import { TestConsumer } from '../decorator.ts';
@@ -10,6 +10,7 @@ import { TestConsumer } from '../decorator.ts';
10
10
  import { TapEmitter } from './tap.ts';
11
11
  import { CONSOLE_ENHANCER, type TestResultsEnhancer } from '../enhancer.ts';
12
12
  import type { SuiteResult } from '../../model/suite.ts';
13
+ import { TestModelUtil } from '../../model/util.ts';
13
14
 
14
15
  type Result = {
15
16
  key: string;
@@ -31,6 +32,7 @@ export class TapSummaryEmitter implements TestConsumerShape {
31
32
  #consumer: TapEmitter;
32
33
  #enhancer: TestResultsEnhancer;
33
34
  #options?: Record<string, unknown>;
35
+ #state: TestRunState = {};
34
36
 
35
37
  constructor(terminal: Terminal = new Terminal(process.stderr)) {
36
38
  this.#terminal = terminal;
@@ -87,22 +89,34 @@ export class TapSummaryEmitter implements TestConsumerShape {
87
89
  this.#consumer.setOptions(options);
88
90
  }
89
91
 
92
+ onTestRunState(state: TestRunState): void {
93
+ Object.assign(this.#state, state);
94
+ }
95
+
90
96
  async onStart(state: TestRunState): Promise<void> {
91
97
  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
98
+ this.onTestRunState(state);
99
+
100
+ const total = TestModelUtil.buildSummary();
95
101
  this.#progress = this.#terminal.streamToBottom(
96
102
  Util.mapAsyncIterable(
97
103
  this.#results,
98
104
  (value) => {
99
- total[value.status] += 1;
100
- total.count += 1;
105
+ TestModelUtil.countTestResult(total, [value]);
101
106
  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
-
107
+ return {
108
+ value: `Tests %completed/%total [${statusLine}] -- ${value.classId}`,
109
+ total: this.#state.testCount,
110
+ completed: total.passed,
111
+ failed: total.failed + total.errored
112
+ };
104
113
  },
105
- TerminalUtil.progressBarUpdater(this.#terminal, { style: () => ({ complete: (total.failed || total.errored) ? fail : success }) })
114
+ TerminalUtil.progressBarUpdater(this.#terminal, {
115
+ style: {
116
+ complete: { text: '#e5e5e5', background: '#026020' },
117
+ failed: { text: '#e5e5e5', background: '#8b0000' }
118
+ }
119
+ })
106
120
  ),
107
121
  { minDelay: 100 }
108
122
  );
@@ -112,7 +126,7 @@ export class TapSummaryEmitter implements TestConsumerShape {
112
126
  if (event.type === 'test' && event.phase === 'after') {
113
127
  const { test } = event;
114
128
  this.#results.add(test);
115
- if (test.status === 'failed') {
129
+ if (test.status !== 'passed' && test.status !== 'skipped') {
116
130
  this.#consumer.onEvent(event);
117
131
  }
118
132
  const tests = this.#timings.getOrInsert('test', new Map<string, Result>());
@@ -2,12 +2,15 @@ import path from 'node:path';
2
2
  import { stringify } from 'yaml';
3
3
 
4
4
  import { Terminal, StyleUtil } from '@travetto/terminal';
5
- import { TimeUtil, RuntimeIndex, hasToJSON, JSONUtil } from '@travetto/runtime';
5
+ import { TimeUtil, RuntimeIndex } from '@travetto/runtime';
6
6
 
7
7
  import type { TestEvent } from '../../model/event.ts';
8
8
  import type { SuitesSummary, TestConsumerShape } from '../types.ts';
9
9
  import { TestConsumer } from '../decorator.ts';
10
10
  import { type TestResultsEnhancer, CONSOLE_ENHANCER } from '../enhancer.ts';
11
+ import { TestConsumerUtil } from './util.ts';
12
+
13
+ const SPACE = ' ';
11
14
 
12
15
  /**
13
16
  * TAP Format consumer
@@ -17,8 +20,8 @@ export class TapEmitter implements TestConsumerShape {
17
20
  #count = 0;
18
21
  #enhancer: TestResultsEnhancer;
19
22
  #terminal: Terminal;
20
- #start: number;
21
23
  #options?: Record<string, unknown>;
24
+ #start: number = 0;
22
25
 
23
26
  constructor(
24
27
  terminal = new Terminal(),
@@ -54,24 +57,6 @@ export class TapEmitter implements TestConsumerShape {
54
57
  this.log(`---\n${this.#enhancer.objectInspect(body)}\n...`);
55
58
  }
56
59
 
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
60
  /**
76
61
  * Listen for each event
77
62
  */
@@ -125,9 +110,10 @@ export class TapEmitter implements TestConsumerShape {
125
110
  // Track test result
126
111
  let status = `${this.#enhancer.testNumber(++this.#count)} `;
127
112
  switch (test.status) {
113
+ case 'passed': `${this.#enhancer.success('ok')} ${status}`; break;
128
114
  case 'skipped': status += ' # SKIP'; break;
129
- case 'failed': status = `${this.#enhancer.failure('not ok')} ${status}`; break;
130
- default: status = `${this.#enhancer.success('ok')} ${status}`;
115
+ case 'unknown': break;
116
+ default: status = `${this.#enhancer.failure('not ok')} ${status}`; break;
131
117
  }
132
118
  status += header;
133
119
 
@@ -138,9 +124,9 @@ export class TapEmitter implements TestConsumerShape {
138
124
  case 'errored':
139
125
  case 'failed': {
140
126
  if (test.error) {
141
- const msg = this.errorToString(test.error);
142
- if (msg) {
143
- this.logMeta({ error: msg });
127
+ const message = TestConsumerUtil.errorToString(test.error, !!this.#options?.verbose);
128
+ if (message) {
129
+ this.logMeta({ error: message });
144
130
  }
145
131
  }
146
132
  break;
@@ -168,28 +154,22 @@ export class TapEmitter implements TestConsumerShape {
168
154
  onSummary(summary: SuitesSummary): void {
169
155
  this.log(`${this.#enhancer.testNumber(1)}..${this.#enhancer.testNumber(summary.total)}`);
170
156
 
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
157
  const allPassed = !summary.failed && !summary.errored;
182
158
 
183
159
  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(' '));
160
+ this.#enhancer[allPassed ? 'success' : 'failure']('Results'), SPACE,
161
+ `${this.#enhancer.total(summary.passed)}/${this.#enhancer.total(summary.total)},`, SPACE,
162
+ allPassed ? 'failed' : this.#enhancer.failure('failed'), SPACE,
163
+ `${this.#enhancer.total(summary.failed)}`, SPACE,
164
+ allPassed ? 'errored' : this.#enhancer.failure('errored'), SPACE,
165
+ `${this.#enhancer.total(summary.errored)}`, SPACE,
166
+ 'skipped', SPACE,
167
+ this.#enhancer.total(summary.skipped), SPACE,
168
+ '#', SPACE, '(Timings:', SPACE,
169
+ 'Self=', TimeUtil.asClock(summary.selfDuration), ',', SPACE,
170
+ 'Total=', TimeUtil.asClock(summary.duration), ',', SPACE,
171
+ 'Clock=', TimeUtil.asClock(Date.now() - this.#start),
172
+ ')',
173
+ ].join(''));
194
174
  }
195
175
  }
@@ -0,0 +1,28 @@
1
+ import util from 'node:util';
2
+ import { AssertionError } from 'node:assert';
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.split(/\n/).slice(0, verbose ? -1 : 5).join('\n') :
16
+ error.message;
17
+ const subObject: Record<string, unknown> = {};
18
+ for (const key of TypedObject.keys(error)) {
19
+ if (key !== 'stack' && key !== 'message' && key !== 'name') {
20
+ subObject[key] = error[key];
21
+ }
22
+ }
23
+ return `${stack}${Object.keys(subObject).length ? `\n${util.inspect(subObject)}` : ''}`;
24
+ } else {
25
+ return `${error}`;
26
+ }
27
+ }
28
+ }
@@ -7,6 +7,7 @@ import { RuntimeIndex } from '@travetto/runtime';
7
7
  import type { TestEvent } from '../../model/event.ts';
8
8
  import type { SuitesSummary, TestConsumerShape } from '../types.ts';
9
9
  import { TestConsumer } from '../decorator.ts';
10
+ import { TestConsumerUtil } from './util.ts';
10
11
 
11
12
  /**
12
13
  * Xunit consumer, compatible with JUnit formatters
@@ -59,9 +60,9 @@ export class XunitEmitter implements TestConsumerShape {
59
60
  let body = '';
60
61
 
61
62
  if (test.error) {
62
- const assertion = test.assertions.find(item => !!item.error)!;
63
+ const errorMessage = TestConsumerUtil.errorToString(test.error);
63
64
  const node = test.status === 'failed' ? 'failure' : 'error';
64
- body = `<${node} type="${assertion.text}" message="${encodeURIComponent(assertion.message!)}"><![CDATA[${assertion.error!.stack}]]></${node}>`;
65
+ body = `<${node} type="${test.error.constructor.name}" message="${encodeURIComponent(test.error.message)}"><![CDATA[${errorMessage}]]></${node}>`;
65
66
  }
66
67
 
67
68
  const groupedByLevel: Record<string, string[]> = {};
@@ -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
  */