@travetto/test 5.0.0-rc.0 → 5.0.0-rc.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.
Files changed (43) hide show
  1. package/README.md +12 -13
  2. package/package.json +8 -8
  3. package/src/assert/capture.ts +5 -4
  4. package/src/assert/check.ts +24 -33
  5. package/src/assert/util.ts +32 -17
  6. package/src/consumer/registry.ts +2 -3
  7. package/src/consumer/{error.ts → serialize.ts} +13 -22
  8. package/src/consumer/types/cumulative.ts +15 -22
  9. package/src/consumer/types/delegating.ts +58 -0
  10. package/src/consumer/types/event.ts +2 -4
  11. package/src/consumer/types/execution.ts +2 -4
  12. package/src/consumer/types/runnable.ts +12 -41
  13. package/src/consumer/types/tap-streamed.ts +9 -6
  14. package/src/consumer/types/tap.ts +5 -5
  15. package/src/consumer/types/xunit.ts +4 -2
  16. package/src/decorator/suite.ts +5 -7
  17. package/src/decorator/test.ts +2 -1
  18. package/src/execute/console.ts +1 -1
  19. package/src/execute/executor.ts +84 -104
  20. package/src/execute/phase.ts +20 -30
  21. package/src/execute/promise.ts +4 -4
  22. package/src/execute/runner.ts +34 -24
  23. package/src/execute/types.ts +12 -10
  24. package/src/execute/util.ts +61 -34
  25. package/src/execute/watcher.ts +34 -36
  26. package/src/fixture.ts +7 -2
  27. package/src/model/common.ts +11 -7
  28. package/src/model/event.ts +9 -5
  29. package/src/model/suite.ts +14 -4
  30. package/src/model/test.ts +30 -4
  31. package/src/registry/suite.ts +42 -39
  32. package/src/trv.d.ts +3 -3
  33. package/src/worker/child.ts +11 -18
  34. package/src/worker/standard.ts +18 -21
  35. package/src/worker/types.ts +13 -10
  36. package/support/cli.test.ts +20 -6
  37. package/support/cli.test_child.ts +1 -1
  38. package/support/cli.test_digest.ts +43 -0
  39. package/support/cli.test_direct.ts +10 -3
  40. package/support/cli.test_watch.ts +1 -1
  41. package/support/transformer.assert.ts +12 -12
  42. package/support/cli.test_count.ts +0 -39
  43. package/support/transformer.annotate.ts +0 -103
package/README.md CHANGED
@@ -18,10 +18,10 @@ This module provides unit testing functionality that integrates with the framewo
18
18
  * [JSON](https://www.json.org), best for integrating with at a code level
19
19
  * [xUnit](https://en.wikipedia.org/wiki/XUnit), standard format for CI/CD systems e.g. Jenkins, Bamboo, etc.
20
20
 
21
- **Note**: All tests should be under the `test/.*` folders. The pattern for tests is defined as a regex and not standard globbing.
21
+ **Note**: All tests should be under the `**/*` folders. The pattern for tests is defined as as a standard glob using [Node](https://nodejs.org)'s built in globbing support.
22
22
 
23
23
  ## Definition
24
- A test suite is a collection of individual tests. All test suites are classes with the [@Suite](https://github.com/travetto/travetto/tree/main/module/test/src/decorator/suite.ts#L14) decorator. Tests are defined as methods on the suite class, using the [@Test](https://github.com/travetto/travetto/tree/main/module/test/src/decorator/test.ts#L19) decorator. All tests intrinsically support `async`/`await`.
24
+ A test suite is a collection of individual tests. All test suites are classes with the [@Suite](https://github.com/travetto/travetto/tree/main/module/test/src/decorator/suite.ts#L13) decorator. Tests are defined as methods on the suite class, using the [@Test](https://github.com/travetto/travetto/tree/main/module/test/src/decorator/test.ts#L20) decorator. All tests intrinsically support `async`/`await`.
25
25
 
26
26
  A simple example would be:
27
27
 
@@ -79,27 +79,25 @@ would translate to:
79
79
  "use strict";
80
80
  Object.defineProperty(exports, "__esModule", { value: true });
81
81
  const tslib_1 = require("tslib");
82
- const Ⲑ_util_1 = tslib_1.__importStar(require("@travetto/test/src/execute/util.js"));
82
+ const Ⲑ_debug_1 = tslib_1.__importStar(require("@travetto/runtime/src/debug.js"));
83
83
  const Ⲑ_check_1 = tslib_1.__importStar(require("@travetto/test/src/assert/check.js"));
84
- const Ⲑ_runtime_1 = tslib_1.__importStar(require("@travetto/manifest/src/runtime.js"));
85
- const Ⲑ_decorator_1 = tslib_1.__importStar(require("@travetto/registry/src/decorator.js"));
86
- var ᚕf = "@travetto/test/doc/assert-example.js";
84
+ const Ⲑ_function_1 = tslib_1.__importStar(require("@travetto/runtime/src/function.js"));
85
+ var ᚕm = ["@travetto/test", "doc/assert-example.ts"];
87
86
  const node_assert_1 = tslib_1.__importDefault(require("node:assert"));
88
87
  const test_1 = require("@travetto/test");
89
88
  let SimpleTest = class SimpleTest {
90
- static Ⲑinit = Ⲑ_runtime_1.RuntimeIndex.registerFunction(SimpleTest, ᚕf, { hash: 1887908328, lines: [5, 12] }, { test: { hash: 102834457, lines: [8, 11] } }, false, false);
89
+ static Ⲑinit = Ⲑ_function_1.registerFunction(SimpleTest, ᚕm, { hash: 1887908328, lines: [5, 12] }, { test: { hash: 102834457, lines: [8, 11, 10] } }, false, false);
91
90
  async test() {
92
- if (Ⲑ_util_1.RunnerUtil.tryDebugger)
91
+ if (Ⲑ_debug_1.tryDebugger)
93
92
  debugger;
94
- Ⲑ_check_1.AssertCheck.check({ file: ᚕf, line: 10, text: "{ size: 20, address: { state: 'VA' } }", operator: "deepStrictEqual" }, true, { size: 20, address: { state: 'VA' } }, {});
93
+ Ⲑ_check_1.AssertCheck.check({ module: ᚕm, line: 10, text: "{ size: 20, address: { state: 'VA' } }", operator: "deepStrictEqual" }, true, { size: 20, address: { state: 'VA' } }, {});
95
94
  }
96
95
  };
97
96
  tslib_1.__decorate([
98
- (0, test_1.Test)({ ident: "@Test()", lineBodyStart: 10 })
97
+ (0, test_1.Test)()
99
98
  ], SimpleTest.prototype, "test", null);
100
99
  SimpleTest = tslib_1.__decorate([
101
- Ⲑ_decorator_1.Register(),
102
- (0, test_1.Suite)({ ident: "@Suite()" })
100
+ (0, test_1.Suite)()
103
101
  ], SimpleTest);
104
102
  ```
105
103
 
@@ -223,12 +221,13 @@ To run the tests you can either call the [Command Line Interface](https://github
223
221
  ```bash
224
222
  $ trv test --help
225
223
 
226
- Usage: test [options] [first:string] [regexes...:string]
224
+ Usage: test [options] [first:string] [globs...:string]
227
225
 
228
226
  Options:
229
227
  -f, --format <string> Output format for test results (default: "tap")
230
228
  -c, --concurrency <number> Number of tests to run concurrently (default: 4)
231
229
  -m, --mode <single|standard> Test run mode (default: "standard")
230
+ -t, --tags <string> Tags to target or exclude
232
231
  -h, --help display help for command
233
232
  ```
234
233
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@travetto/test",
3
- "version": "5.0.0-rc.0",
3
+ "version": "5.0.0-rc.10",
4
4
  "description": "Declarative test framework",
5
5
  "keywords": [
6
6
  "unit-testing",
@@ -27,15 +27,15 @@
27
27
  "directory": "module/test"
28
28
  },
29
29
  "dependencies": {
30
- "@travetto/base": "^5.0.0-rc.0",
31
- "@travetto/registry": "^5.0.0-rc.0",
32
- "@travetto/terminal": "^5.0.0-rc.0",
33
- "@travetto/worker": "^5.0.0-rc.0",
34
- "yaml": "^2.4.5"
30
+ "@travetto/registry": "^5.0.0-rc.10",
31
+ "@travetto/runtime": "^5.0.0-rc.10",
32
+ "@travetto/terminal": "^5.0.0-rc.10",
33
+ "@travetto/worker": "^5.0.0-rc.10",
34
+ "yaml": "^2.5.0"
35
35
  },
36
36
  "peerDependencies": {
37
- "@travetto/cli": "^5.0.0-rc.0",
38
- "@travetto/transformer": "^5.0.0-rc.0"
37
+ "@travetto/cli": "^5.0.0-rc.11",
38
+ "@travetto/transformer": "^5.0.0-rc.7"
39
39
  },
40
40
  "peerDependenciesMeta": {
41
41
  "@travetto/transformer": {
@@ -3,7 +3,7 @@ import { EventEmitter } from 'node:events';
3
3
  import { Assertion, TestConfig } from '../model/test';
4
4
 
5
5
  export interface CaptureAssert extends Partial<Assertion> {
6
- file: string;
6
+ module?: [string, string];
7
7
  line: number;
8
8
  text: string;
9
9
  operator: string;
@@ -26,14 +26,15 @@ class $AssertCapture {
26
26
 
27
27
  // Emit and collect, every assertion as it occurs
28
28
  const handler = (a: CaptureAssert): void => {
29
- const assrt = {
29
+ const asrt: Assertion = {
30
30
  ...a,
31
+ import: a.import ?? a.module!.join('/'),
31
32
  classId: test.classId,
32
33
  methodName: test.methodName
33
34
  };
34
- assertions.push(assrt);
35
+ assertions.push(asrt);
35
36
  if (listener) {
36
- listener(assrt);
37
+ listener(asrt);
37
38
  }
38
39
  };
39
40
 
@@ -1,7 +1,6 @@
1
1
  import assert from 'node:assert';
2
2
 
3
- import { RuntimeIndex } from '@travetto/manifest';
4
- import { AppError, ClassInstance, Class } from '@travetto/base';
3
+ import { AppError, Class, castTo, castKey, asConstructable } from '@travetto/runtime';
5
4
 
6
5
  import { ThrowableError, TestConfig, Assertion } from '../model/test';
7
6
  import { AssertCapture, CaptureAssert } from './capture';
@@ -24,9 +23,6 @@ export class AssertCheck {
24
23
  * @param args The arguments passed in
25
24
  */
26
25
  static check(assertion: CaptureAssert, positive: boolean, ...args: unknown[]): void {
27
- /* eslint-disable @typescript-eslint/consistent-type-assertions */
28
- assertion.file = RuntimeIndex.getSourceFile(assertion.file);
29
-
30
26
  let fn = assertion.operator;
31
27
  assertion.operator = ASSERT_FN_OPERATOR[fn];
32
28
 
@@ -41,32 +37,32 @@ export class AssertCheck {
41
37
  // Check fn to call
42
38
  if (fn === 'fail') {
43
39
  if (args.length > 1) {
44
- [assertion.actual, assertion.expected, assertion.message, assertion.operator] = args as [unknown, unknown, string, string];
40
+ [assertion.actual, assertion.expected, assertion.message, assertion.operator] = castTo(args);
45
41
  } else {
46
- [assertion.message] = args as [string];
42
+ [assertion.message] = castTo(args);
47
43
  }
48
44
  } else if (/throw|reject/i.test(fn)) {
49
45
  assertion.operator = fn;
50
46
  if (typeof args[1] !== 'string') {
51
- [, assertion.expected, assertion.message] = args as [unknown, unknown, string];
47
+ [, assertion.expected, assertion.message] = castTo(args);
52
48
  } else {
53
- [, assertion.message] = args as [unknown, string];
49
+ [, assertion.message] = castTo(args);
54
50
  }
55
51
  } else if (fn === 'ok' || fn === 'assert') {
56
52
  fn = assertion.operator = 'ok';
57
- [assertion.actual, assertion.message] = args as [unknown, string];
53
+ [assertion.actual, assertion.message] = castTo(args);
58
54
  assertion.expected = { toClean: (): string => positive ? 'truthy' : 'falsy' };
59
55
  common.state = 'should be';
60
56
  } else if (fn === 'includes') {
61
57
  assertion.operator = fn;
62
- [assertion.actual, assertion.expected, assertion.message] = args as [unknown, unknown, string];
58
+ [assertion.actual, assertion.expected, assertion.message] = castTo(args);
63
59
  } else if (fn === 'instanceof') {
64
60
  assertion.operator = fn;
65
- [assertion.actual, assertion.expected, assertion.message] = args as [unknown, unknown, string];
66
- assertion.actual = (assertion.actual as ClassInstance)?.constructor;
61
+ [assertion.actual, assertion.expected, assertion.message] = castTo(args);
62
+ assertion.actual = asConstructable(assertion.actual)?.constructor;
67
63
  } else { // Handle unknown
68
64
  assertion.operator = fn ?? '';
69
- [assertion.actual, assertion.expected, assertion.message] = args as [unknown, unknown, string];
65
+ [assertion.actual, assertion.expected, assertion.message] = castTo(args);
70
66
  }
71
67
 
72
68
  try {
@@ -79,25 +75,25 @@ export class AssertCheck {
79
75
  assertion.expected = AssertUtil.cleanValue(assertion.expected);
80
76
  }
81
77
 
82
- const [actual, expected, message] = args as [unknown, unknown, string];
78
+ const [actual, expected, message]: [unknown, unknown, string] = castTo(args);
83
79
 
84
80
  // Actually run the assertion
85
81
  switch (fn) {
86
- case 'includes': assertFn((actual as unknown[]).includes(expected), message); break;
87
- case 'test': assertFn((expected as RegExp).test(actual as string), message); break;
88
- case 'instanceof': assertFn(actual instanceof (expected as Class), message); break;
89
- case 'in': assertFn((actual as string) in (expected as object), message); break;
90
- case 'lessThan': assertFn((actual as number) < (expected as number), message); break;
91
- case 'lessThanEqual': assertFn((actual as number) <= (expected as number), message); break;
92
- case 'greaterThan': assertFn((actual as number) > (expected as number), message); break;
93
- case 'greaterThanEqual': assertFn((actual as number) >= (expected as number), message); break;
94
- case 'ok': assertFn(...args as [unknown, string]); break;
82
+ case 'includes': assertFn(castTo<unknown[]>(actual).includes(expected), message); break;
83
+ case 'test': assertFn(castTo<RegExp>(expected).test(castTo(actual)), message); break;
84
+ case 'instanceof': assertFn(actual instanceof castTo<Class>(expected), message); break;
85
+ case 'in': assertFn(castTo<string>(actual) in castTo<object>(expected), message); break;
86
+ case 'lessThan': assertFn(castTo<number>(actual) < castTo<number>(expected), message); break;
87
+ case 'lessThanEqual': assertFn(castTo<number>(actual) <= castTo<number>(expected), message); break;
88
+ case 'greaterThan': assertFn(castTo<number>(actual) > castTo<number>(expected), message); break;
89
+ case 'greaterThanEqual': assertFn(castTo<number>(actual) >= castTo<number>(expected), message); break;
90
+ case 'ok': assertFn(...castTo<Parameters<typeof assertFn>>(args)); break;
95
91
  default:
96
- if (fn && assert[fn as keyof typeof assert]) { // Assert call
92
+ if (fn && assert[castKey<typeof assert>(fn)]) { // Assert call
97
93
  if (/not/i.test(fn)) {
98
94
  common.state = 'should not';
99
95
  }
100
- assert[fn as 'ok'].apply(null, args as [boolean, string | undefined]);
96
+ assert[castTo<'ok'>(fn)].apply(null, castTo(args));
101
97
  }
102
98
  }
103
99
 
@@ -118,7 +114,6 @@ export class AssertCheck {
118
114
  }
119
115
  throw err;
120
116
  }
121
- /* eslint-enable @typescript-eslint/consistent-type-assertions */
122
117
  }
123
118
 
124
119
  /**
@@ -215,8 +210,6 @@ export class AssertCheck {
215
210
  ): void {
216
211
  let missed: Error | undefined;
217
212
 
218
- assertion.file = RuntimeIndex.getSourceFile(assertion.file);
219
-
220
213
  try {
221
214
  action();
222
215
  if (!positive) {
@@ -249,8 +242,6 @@ export class AssertCheck {
249
242
  ): Promise<void> {
250
243
  let missed: Error | undefined;
251
244
 
252
- assertion.file = RuntimeIndex.getSourceFile(assertion.file);
253
-
254
245
  try {
255
246
  if ('then' in action) {
256
247
  await action;
@@ -274,13 +265,13 @@ export class AssertCheck {
274
265
  * Look for any unhandled exceptions
275
266
  */
276
267
  static checkUnhandled(test: TestConfig, err: Error | assert.AssertionError): void {
277
- let line = AssertUtil.getPositionOfError(err, test.file).line;
268
+ let line = AssertUtil.getPositionOfError(err, test.sourceImport ?? test.import).line;
278
269
  if (line === 1) {
279
270
  line = test.lineStart;
280
271
  }
281
272
 
282
273
  AssertCapture.add({
283
- file: test.file,
274
+ import: test.import,
284
275
  line,
285
276
  operator: 'throws',
286
277
  error: err,
@@ -1,13 +1,13 @@
1
1
  import util from 'node:util';
2
+ import path from 'node:path';
2
3
 
3
- import { path, RuntimeIndex, RuntimeContext } from '@travetto/manifest';
4
+ import { asFull, Class, Runtime, RuntimeIndex } from '@travetto/runtime';
4
5
 
5
6
  import { TestConfig, Assertion, TestResult } from '../model/test';
6
- import { SuiteConfig } from '../model/suite';
7
+ import { SuiteConfig, SuiteFailure, SuiteResult } from '../model/suite';
7
8
 
8
9
  function isCleanable(o: unknown): o is { toClean(): unknown } {
9
- // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
10
- return !!o && !!(o as { toClean: unknown }).toClean;
10
+ return !!o && typeof o === 'object' && 'toClean' in o && typeof o.toClean === 'function';
11
11
  }
12
12
 
13
13
  /**
@@ -19,6 +19,7 @@ export class AssertUtil {
19
19
  */
20
20
  static cleanValue(val: unknown): unknown {
21
21
  switch (typeof val) {
22
+ case 'number': case 'boolean': case 'bigint': case 'string': case 'undefined': return val;
22
23
  case 'object': {
23
24
  if (isCleanable(val)) {
24
25
  return val.toClean();
@@ -27,7 +28,6 @@ export class AssertUtil {
27
28
  }
28
29
  break;
29
30
  }
30
- case 'undefined': case 'string': case 'number': case 'bigint': case 'boolean': return JSON.stringify(val);
31
31
  case 'function': {
32
32
  if (val.Ⲑid || !val.constructor) {
33
33
  return val.name;
@@ -35,20 +35,22 @@ export class AssertUtil {
35
35
  break;
36
36
  }
37
37
  }
38
- // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
39
38
  return util.inspect(val, false, 1).replace(/\n/g, ' ');
40
39
  }
41
40
 
42
41
  /**
43
42
  * Determine file location for a given error and the stack trace
44
43
  */
45
- static getPositionOfError(err: Error, filename: string): { file: string, line: number } {
46
- const cwd = RuntimeIndex.mainModule.sourcePath;
47
- const lines = path.toPosix(err.stack ?? new Error().stack!)
44
+ static getPositionOfError(err: Error, imp: string): { import: string, line: number } {
45
+ const cwd = Runtime.mainSourcePath;
46
+ const lines = (err.stack ?? new Error().stack!)
47
+ .replace(/[\\/]/g, '/')
48
48
  .split('\n')
49
49
  // Exclude node_modules, target self
50
50
  .filter(x => x.includes(cwd) && (!x.includes('node_modules') || x.includes('/support/')));
51
51
 
52
+ const filename = RuntimeIndex.getFromImport(imp)?.sourceFile!;
53
+
52
54
  let best = lines.filter(x => x.includes(filename))[0];
53
55
 
54
56
  if (!best) {
@@ -56,12 +58,12 @@ export class AssertUtil {
56
58
  }
57
59
 
58
60
  if (!best) {
59
- return { file: filename, line: 1 };
61
+ return { import: imp, line: 1 };
60
62
  }
61
63
 
62
64
  const pth = best.trim().split(/\s+/g).slice(1).pop()!;
63
65
  if (!pth) {
64
- return { file: filename, line: 1 };
66
+ return { import: imp, line: 1 };
65
67
  }
66
68
 
67
69
  const [file, lineNo] = pth
@@ -78,7 +80,7 @@ export class AssertUtil {
78
80
 
79
81
  const outFile = outFileParts.length > 1 ? outFileParts[1].replace(/^[\/]/, '') : filename;
80
82
 
81
- const res = { file: outFile, line };
83
+ const res = { import: RuntimeIndex.getFromSource(outFile)?.import!, line };
82
84
 
83
85
  return res;
84
86
  }
@@ -86,8 +88,8 @@ export class AssertUtil {
86
88
  /**
87
89
  * Generate a suite error given a suite config, and an error
88
90
  */
89
- static generateSuiteError(suite: SuiteConfig, methodName: string, error: Error): { assert: Assertion, testResult: TestResult, testConfig: TestConfig } {
90
- const { file, ...pos } = this.getPositionOfError(error, suite.file);
91
+ static generateSuiteFailure(suite: SuiteConfig, methodName: string, error: Error): SuiteFailure {
92
+ const { import: imp, ...pos } = this.getPositionOfError(error, suite.import);
91
93
  let line = pos.line;
92
94
 
93
95
  if (line === 1 && suite.lineStart) {
@@ -96,7 +98,7 @@ export class AssertUtil {
96
98
 
97
99
  const msg = error.message.split(/\n/)[0];
98
100
 
99
- const core = { file, classId: suite.classId, methodName, module: RuntimeContext.main.name };
101
+ const core = { import: imp, classId: suite.classId, methodName };
100
102
  const coreAll = { ...core, description: msg, lineStart: line, lineEnd: line, lineBodyStart: line };
101
103
 
102
104
  const assert: Assertion = {
@@ -107,11 +109,24 @@ export class AssertUtil {
107
109
  ...coreAll,
108
110
  status: 'failed', error, duration: 0, durationTotal: 0, assertions: [assert], output: {}
109
111
  };
110
- const testConfig: TestConfig = {
112
+ const test: TestConfig = {
111
113
  ...coreAll,
112
114
  class: suite.class, skip: false
113
115
  };
114
116
 
115
- return { assert, testResult, testConfig };
117
+ return { assert, testResult, test, suite };
118
+ }
119
+
120
+ /**
121
+ * Define import failure as a SuiteFailure object
122
+ */
123
+ static gernerateImportFailure(imp: string, err: Error): SuiteFailure {
124
+ const name = path.basename(imp);
125
+ const classId = `${RuntimeIndex.getFromImport(imp)?.id}○${name}`;
126
+ const suite = asFull<SuiteConfig & SuiteResult>({
127
+ class: asFull<Class>({ name }), classId, duration: 0, lineStart: 1, lineEnd: 1, import: imp
128
+ });
129
+ err.message = err.message.replaceAll(Runtime.mainSourcePath, '.');
130
+ return this.generateSuiteFailure(suite, 'require', err);
116
131
  }
117
132
  }
@@ -1,4 +1,4 @@
1
- import type { Class, ConcreteClass } from '@travetto/base';
1
+ import { classConstruct, type Class } from '@travetto/runtime';
2
2
  import { TestConsumer } from './types';
3
3
 
4
4
  /**
@@ -45,8 +45,7 @@ class $TestConsumerRegistry {
45
45
  await this.manualInit();
46
46
 
47
47
  return typeof consumer === 'string' ?
48
- // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
49
- new ((this.get(consumer) ?? this.#primary) as ConcreteClass)() :
48
+ classConstruct(this.get(consumer) ?? this.#primary) :
50
49
  consumer;
51
50
  }
52
51
  }
@@ -1,22 +1,22 @@
1
- import { AppError, TypedObject } from '@travetto/base';
1
+ import { AppError, TypedObject } from '@travetto/runtime';
2
2
 
3
3
  import { TestEvent, } from '../model/event';
4
4
 
5
5
 
6
6
  export type SerializedError = { $?: boolean, message: string, stack?: string, name: string };
7
7
 
8
- function isSerialized(e: unknown): e is SerializedError {
8
+ function isError(e: unknown): e is SerializedError {
9
9
  return !!e && (typeof e === 'object') && '$' in e;
10
10
  }
11
11
 
12
- export class ErrorUtil {
12
+ export class SerializeUtil {
13
13
 
14
14
  /**
15
15
  * Prepare error for transmission
16
16
  */
17
- static serializeError(e: Error | SerializedError): SerializedError;
17
+ static serializeError(e: Error | SerializedError): Error;
18
18
  static serializeError(e: undefined): undefined;
19
- static serializeError(e: Error | SerializedError | undefined): SerializedError | undefined {
19
+ static serializeError(e: Error | SerializedError | undefined): Error | undefined {
20
20
  let error: SerializedError | undefined;
21
21
 
22
22
  if (e) {
@@ -41,7 +41,7 @@ export class ErrorUtil {
41
41
  static deserializeError(e: Error | SerializedError): Error;
42
42
  static deserializeError(e: undefined): undefined;
43
43
  static deserializeError(e: Error | SerializedError | undefined): Error | undefined {
44
- if (isSerialized(e)) {
44
+ if (isError(e)) {
45
45
  const err = new Error();
46
46
 
47
47
  for (const k of TypedObject.keys(e)) {
@@ -54,27 +54,18 @@ export class ErrorUtil {
54
54
  err.stack = e.stack;
55
55
  err.name = e.name;
56
56
  return err;
57
- } else if (e) {
57
+ } else {
58
58
  return e;
59
59
  }
60
60
  }
61
61
 
62
62
  /**
63
- * Serialize all errors for a given test for transmission between parent/child
63
+ * Serialize to JSON
64
64
  */
65
- static serializeTestErrors(out: TestEvent): void {
66
- if (out.phase === 'after') {
67
- if (out.type === 'test') {
68
- if (out.test.error) {
69
- // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
70
- out.test.error = this.serializeError(out.test.error) as Error;
71
- }
72
- } else if (out.type === 'assertion') {
73
- if (out.assertion.error) {
74
- // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
75
- out.assertion.error = this.serializeError(out.assertion.error) as Error;
76
- }
77
- }
78
- }
65
+ static serializeToJSON(out: TestEvent): string {
66
+ return JSON.stringify(out, (_, v) =>
67
+ v instanceof Error ? this.serializeError(v) :
68
+ typeof v === 'bigint' ? v.toString() : v
69
+ );
79
70
  }
80
71
  }
@@ -1,25 +1,25 @@
1
1
  import { existsSync } from 'node:fs';
2
2
 
3
- import { Class } from '@travetto/base';
3
+ import { Class, RuntimeIndex } from '@travetto/runtime';
4
4
 
5
5
  import { TestConsumer } from '../types';
6
6
  import { TestEvent } from '../../model/event';
7
7
  import { TestResult } from '../../model/test';
8
8
  import { SuiteResult } from '../../model/suite';
9
9
  import { SuiteRegistry } from '../../registry/suite';
10
+ import { DelegatingConsumer } from './delegating';
10
11
 
11
12
  /**
12
13
  * Cumulative Summary consumer
13
14
  */
14
- export class CumulativeSummaryConsumer implements TestConsumer {
15
+ export class CumulativeSummaryConsumer extends DelegatingConsumer {
15
16
  /**
16
17
  * Total state of all tests run so far
17
18
  */
18
19
  #state: Record<string, Record<string, TestResult['status']>> = {};
19
- #target: TestConsumer;
20
20
 
21
21
  constructor(target: TestConsumer) {
22
- this.#target = target;
22
+ super([target]);
23
23
  }
24
24
 
25
25
  /**
@@ -28,17 +28,10 @@ export class CumulativeSummaryConsumer implements TestConsumer {
28
28
  */
29
29
  summarizeSuite(test: TestResult): SuiteResult {
30
30
  // Was only loading to verify existence (TODO: double-check)
31
- if (existsSync(test.file)) {
32
- this.#state[test.classId] = this.#state[test.classId] ?? {};
33
- this.#state[test.classId][test.methodName] = test.status;
34
- const SuiteCls = SuiteRegistry.getClasses().find(x =>
35
- x.Ⲑid === test.classId
36
- )!;
37
- if (SuiteCls) {
38
- return this.computeTotal(SuiteCls);
39
- } else {
40
- return this.removeClass(test.classId);
41
- }
31
+ if (existsSync(RuntimeIndex.getFromImport(test.import)!.sourceFile)) {
32
+ (this.#state[test.classId] ??= {})[test.methodName] = test.status;
33
+ const SuiteCls = SuiteRegistry.getClasses().find(x => x.Ⲑid === test.classId);
34
+ return SuiteCls ? this.computeTotal(SuiteCls) : this.removeClass(test.classId);
42
35
  } else {
43
36
  return this.removeClass(test.classId);
44
37
  }
@@ -50,7 +43,7 @@ export class CumulativeSummaryConsumer implements TestConsumer {
50
43
  removeClass(clsId: string): SuiteResult {
51
44
  this.#state[clsId] = {};
52
45
  return {
53
- classId: clsId, passed: 0, failed: 0, skipped: 0, total: 0, tests: [], duration: 0, file: '', lines: { start: 0, end: 0 }
46
+ classId: clsId, passed: 0, failed: 0, skipped: 0, total: 0, tests: [], duration: 0, import: '', lineStart: 0, lineEnd: 0
54
47
  };
55
48
  }
56
49
 
@@ -70,8 +63,9 @@ export class CumulativeSummaryConsumer implements TestConsumer {
70
63
  passed: total.passed,
71
64
  failed: total.failed,
72
65
  skipped: total.skipped,
73
- file: suite.file,
74
- lines: suite.lines,
66
+ import: suite.import,
67
+ lineStart: suite.lineStart,
68
+ lineEnd: suite.lineEnd,
75
69
  total: total.failed + total.passed,
76
70
  tests: [],
77
71
  duration: 0
@@ -82,14 +76,13 @@ export class CumulativeSummaryConsumer implements TestConsumer {
82
76
  * Listen for event, process the full event, and if the event is an after test,
83
77
  * send a full suite summary
84
78
  */
85
- onEvent(e: TestEvent): void {
86
- this.#target.onEvent(e);
79
+ onEventDone(e: TestEvent): void {
87
80
  try {
88
81
  if (e.type === 'test' && e.phase === 'after') {
89
- this.#target.onEvent({
82
+ this.onEvent({
90
83
  type: 'suite',
91
84
  phase: 'after',
92
- suite: this.summarizeSuite(e.test)
85
+ suite: this.summarizeSuite(e.test),
93
86
  });
94
87
  }
95
88
  } catch (err) {
@@ -0,0 +1,58 @@
1
+ import { SuitesSummary, TestConsumer, TestRunState } from '../types';
2
+ import { TestEvent } from '../../model/event';
3
+
4
+ /**
5
+ * Delegating event consumer
6
+ */
7
+ export abstract class DelegatingConsumer implements TestConsumer {
8
+ #consumers: TestConsumer[];
9
+ #transformer?: (ev: TestEvent) => typeof ev;
10
+ #filter?: (ev: TestEvent) => boolean;
11
+
12
+ constructor(consumers: TestConsumer[]) {
13
+ this.#consumers = consumers;
14
+ for (const c of consumers) {
15
+ c.onEvent = c.onEvent.bind(c);
16
+ }
17
+ }
18
+
19
+ withTransformer(transformer: (ev: TestEvent) => typeof ev): this {
20
+ this.#transformer = transformer;
21
+ return this;
22
+ }
23
+
24
+ withFilter(filter: (ev: TestEvent) => boolean): this {
25
+ this.#filter = filter;
26
+ return this;
27
+ }
28
+
29
+ async onStart(state: TestRunState): Promise<void> {
30
+ for (const c of this.#consumers) {
31
+ await c.onStart?.(state);
32
+ }
33
+ }
34
+
35
+ onEvent(e: TestEvent): void {
36
+ if (this.#transformer) {
37
+ e = this.#transformer(e);
38
+ }
39
+ if (this.#filter?.(e) === false) {
40
+ return;
41
+ }
42
+ for (const c of this.#consumers) {
43
+ c.onEvent(e);
44
+ }
45
+
46
+ this.onEventDone?.(e);
47
+ }
48
+
49
+ async summarize(summary?: SuitesSummary): Promise<void> {
50
+ if (summary) {
51
+ for (const c of this.#consumers) {
52
+ await c.onSummary?.(summary);
53
+ }
54
+ }
55
+ }
56
+
57
+ onEventDone?(e: TestEvent): void;
58
+ }
@@ -2,7 +2,7 @@ import { Writable } from 'node:stream';
2
2
 
3
3
  import { TestEvent } from '../../model/event';
4
4
  import { TestConsumer } from '../types';
5
- import { ErrorUtil } from '../error';
5
+ import { SerializeUtil } from '../serialize';
6
6
  import { Consumable } from '../registry';
7
7
 
8
8
  /**
@@ -17,8 +17,6 @@ export class EventStreamer implements TestConsumer {
17
17
  }
18
18
 
19
19
  onEvent(event: TestEvent): void {
20
- const out = { ...event };
21
- ErrorUtil.serializeTestErrors(out);
22
- this.#stream.write(`${JSON.stringify(out)}\n`);
20
+ this.#stream.write(`${SerializeUtil.serializeToJSON(event)}\n`);
23
21
  }
24
22
  }