@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.
package/README.md CHANGED
@@ -231,7 +231,7 @@ Options:
231
231
  -c, --concurrency <number> Number of tests to run concurrently (default: 9)
232
232
  -t, --tags <string> Tags to target or exclude when using globs
233
233
  -o, --format-options <string> Format options
234
- -h, --help display help for command
234
+ --help display help for command
235
235
  ```
236
236
 
237
237
  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.0",
3
+ "version": "8.0.0-alpha.10",
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.0",
32
- "@travetto/runtime": "^8.0.0-alpha.0",
33
- "@travetto/terminal": "^8.0.0-alpha.0",
34
- "@travetto/worker": "^8.0.0-alpha.0",
35
- "yaml": "^2.8.2"
31
+ "@travetto/registry": "^8.0.0-alpha.10",
32
+ "@travetto/runtime": "^8.0.0-alpha.10",
33
+ "@travetto/terminal": "^8.0.0-alpha.10",
34
+ "@travetto/worker": "^8.0.0-alpha.10",
35
+ "yaml": "^2.8.3"
36
36
  },
37
37
  "peerDependencies": {
38
- "@travetto/cli": "^8.0.0-alpha.0",
39
- "@travetto/transformer": "^8.0.0-alpha.0"
38
+ "@travetto/cli": "^8.0.0-alpha.15",
39
+ "@travetto/transformer": "^8.0.0-alpha.5"
40
40
  },
41
41
  "peerDependenciesMeta": {
42
42
  "@travetto/transformer": {
@@ -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
 
@@ -40,93 +39,33 @@ export class AssertUtil {
40
39
  * Determine file location for a given error and the stack trace
41
40
  */
42
41
  static getPositionOfError(error: Error): { import: string, line: number } | undefined {
43
- const stack = error.stack ?? new Error().stack!;
44
- const frames = Util.stackTraceToParts(stack)
42
+ const frames = Util.stackTraceToParts(error.stack ?? new Error().stack!)
45
43
  .map(frame => {
46
- const imp = (RuntimeIndex.getFromSource(frame.filename) ?? RuntimeIndex.getEntry(frame.filename))?.import;
47
- return { ...frame, import: imp! };
48
- })
49
- .filter(frame => !!frame.import);
44
+ const entry = RuntimeIndex.getEntry(frame.filename);
45
+ return { ...frame, import: entry?.import!, line: entry?.type === 'ts' ? frame.line : 1 };
46
+ });
50
47
 
51
- return frames[0];
48
+ return frames.find(frame => frame.import);
52
49
  }
53
50
 
54
51
  /**
55
52
  * Generate a suite error given a suite config, and an error
56
53
  */
57
- static generateSuiteTestFailure(config: { suite: SuiteConfig, test: TestConfig, error: Error, importLocation?: string }): TestResult {
58
- 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;
59
57
  const testImport = importLocation ?? test.import;
60
58
  const position = this.getPositionOfError(error);
61
59
  const line = position?.line ?? (testImport === suite.import ? suite.lineStart : 1);
62
- const testResult: TestResult = {
63
- ...suite.tests[test.methodName],
64
- suiteLineStart: suite.lineStart,
65
- status: 'errored',
60
+ return {
61
+ import: position?.import ?? testImport,
62
+ methodName: test.methodName,
63
+ classId: suite.classId,
64
+ operator: 'throw',
66
65
  error,
67
- duration: 0,
68
- durationTotal: 0,
69
- output: [],
70
- assertions: [{
71
- import: position?.import ?? testImport,
72
- methodName: test.methodName,
73
- classId: suite.classId,
74
- operator: 'throw',
75
- error,
76
- line,
77
- message: error.message.split(/\n/)[0],
78
- text: test.methodName
79
- }],
66
+ line,
67
+ message: error.message.split(/\n/)[0],
68
+ text: test.methodName
80
69
  };
81
-
82
- return testResult;
83
- }
84
-
85
- /**
86
- * Generate suite failure
87
- */
88
- static generateSuiteTestFailures(suite: SuiteConfig, error: Error): TestResult[] {
89
- const finalError = error.cause instanceof Error ? error.cause : error;
90
- return Object.values(suite.tests).map(test => this.generateSuiteTestFailure({ suite, test, error: finalError }));
91
- }
92
-
93
- /**
94
- * Define import failure as a TestResult
95
- */
96
- static gernerateImportFailure(importLocation: string, error: Error): { result: TestResult, test: TestConfig, suite: SuiteResult & SuiteConfig } {
97
- const name = path.basename(importLocation);
98
- const classId = `${RuntimeIndex.getFromImport(importLocation)?.id}#${name}`;
99
- const suite = asFull<SuiteConfig & SuiteResult>({
100
- class: asFull<Class>({ name }), classId, duration: 0, lineStart: 1, lineEnd: 1, import: importLocation
101
- });
102
- error.message = error.message.replaceAll(Runtime.mainSourcePath, '.');
103
- const result = this.generateSuiteTestFailure({
104
- suite,
105
- test: {
106
- methodName: 'require',
107
- classId,
108
- import: importLocation,
109
- class: suite.class,
110
- lineBodyStart: 1,
111
- lineStart: 1,
112
- lineEnd: 1,
113
- skip: false
114
- },
115
- error
116
- });
117
- const test: TestConfig = {
118
- methodName: 'import',
119
- classId,
120
- import: importLocation,
121
- declarationImport: importLocation,
122
- lineStart: 0,
123
- lineEnd: 0,
124
- lineBodyStart: 0,
125
- tags: [],
126
- description: 'Import Failure',
127
- skip: false,
128
- class: undefined!
129
- };
130
- return { result, test, suite };
131
70
  }
132
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
  /**
@@ -2,7 +2,7 @@ import { Util, AsyncQueue } from '@travetto/runtime';
2
2
  import { StyleUtil, 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,20 +89,24 @@ 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 };
98
+ this.onTestRunState(state);
99
+
100
+ const total = TestModelUtil.buildSummary();
93
101
  const success = StyleUtil.getStyle({ text: '#e5e5e5', background: '#026020' }); // White on dark green
94
102
  const fail = StyleUtil.getStyle({ text: '#e5e5e5', background: '#8b0000' }); // White on dark red
95
103
  this.#progress = this.#terminal.streamToBottom(
96
104
  Util.mapAsyncIterable(
97
105
  this.#results,
98
106
  (value) => {
99
- total[value.status] += 1;
100
- total.count += 1;
107
+ TestModelUtil.countTestResult(total, [value]);
101
108
  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.count };
103
-
109
+ return { value: `Tests %idx/%total [${statusLine}] -- ${value.classId}`, total: this.#state.testCount, idx: total.passed };
104
110
  },
105
111
  TerminalUtil.progressBarUpdater(this.#terminal, { style: () => ({ complete: (total.failed || total.errored) ? fail : success }) })
106
112
  ),
@@ -112,7 +118,7 @@ export class TapSummaryEmitter implements TestConsumerShape {
112
118
  if (event.type === 'test' && event.phase === 'after') {
113
119
  const { test } = event;
114
120
  this.#results.add(test);
115
- if (test.status === 'failed') {
121
+ if (test.status !== 'passed' && test.status !== 'skipped') {
116
122
  this.#consumer.onEvent(event);
117
123
  }
118
124
  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
  */
@@ -1,4 +1,4 @@
1
- import { castTo, type Class, type ClassInstance, describeFunction, getClass } from '@travetto/runtime';
1
+ import { castTo, type Class, type ClassInstance, getClass } from '@travetto/runtime';
2
2
 
3
3
  import type { SuiteConfig } from '../model/suite.ts';
4
4
  import { SuiteRegistryIndex } from '../registry/registry-index.ts';
@@ -15,11 +15,9 @@ export function Suite(...rest: Partial<SuiteConfig>[]): ClassDecorator;
15
15
  export function Suite(description: string, ...rest: Partial<SuiteConfig>[]): ClassDecorator;
16
16
  export function Suite(description?: string | Partial<SuiteConfig>, ...rest: Partial<SuiteConfig>[]): ClassDecorator {
17
17
  const decorator = (cls: Class): typeof cls => {
18
- const isAbstract = describeFunction(cls).abstract;
19
18
  SuiteRegistryIndex.getForRegister(cls).register(
20
19
  ...(typeof description !== 'string' && description ? [description] : []),
21
20
  ...rest,
22
- ...isAbstract ? [{ skip: true }] : [],
23
21
  ...(typeof description === 'string' ? [{ description }] : []),
24
22
  );
25
23
  return cls;