@travetto/test 5.0.16 → 5.0.17

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
@@ -79,18 +79,18 @@ would translate to:
79
79
  "use strict";
80
80
  Object.defineProperty(exports, "__esModule", { value: true });
81
81
  const tslib_1 = require("tslib");
82
- const Ⲑ_debug_1 = tslib_1.__importStar(require("@travetto/runtime/src/debug.js"));
83
- const Ⲑ_check_1 = tslib_1.__importStar(require("@travetto/test/src/assert/check.js"));
84
- const Ⲑ_function_1 = tslib_1.__importStar(require("@travetto/runtime/src/function.js"));
85
- var ᚕm = ["@travetto/test", "doc/assert-example.ts"];
82
+ const Δdebug = tslib_1.__importStar(require("@travetto/runtime/src/debug.js"));
83
+ const Δcheck = tslib_1.__importStar(require("@travetto/test/src/assert/check.js"));
84
+ const Δfunction = tslib_1.__importStar(require("@travetto/runtime/src/function.js"));
85
+ var mod_1 = ["@travetto/test", "doc/assert-example.ts"];
86
86
  const node_assert_1 = tslib_1.__importDefault(require("node:assert"));
87
87
  const test_1 = require("@travetto/test");
88
88
  let SimpleTest = class SimpleTest {
89
- static Ⲑinit = Ⲑ_function_1.registerFunction(SimpleTest, ᚕm, { hash: 1887908328, lines: [5, 12] }, { test: { hash: 102834457, lines: [8, 11, 10] } }, false, false);
89
+ static { Δfunction.registerFunction(SimpleTest, mod_1, { hash: 1887908328, lines: [5, 12] }, { test: { hash: 102834457, lines: [8, 11, 10] } }, false); }
90
90
  async test() {
91
- if (Ⲑ_debug_1.tryDebugger)
91
+ if (Δdebug.tryDebugger)
92
92
  debugger;
93
- Ⲑ_check_1.AssertCheck.check({ module: ᚕm, line: 10, text: "{ size: 20, address: { state: 'VA' } }", operator: "deepStrictEqual" }, true, { size: 20, address: { state: 'VA' } }, {});
93
+ Δcheck.AssertCheck.check({ module: mod_1, line: 10, text: "{ size: 20, address: { state: 'VA' } }", operator: "deepStrictEqual" }, true, { size: 20, address: { state: 'VA' } }, {});
94
94
  }
95
95
  };
96
96
  tslib_1.__decorate([
@@ -224,11 +224,12 @@ $ trv test --help
224
224
  Usage: test [options] [first:string] [globs...:string]
225
225
 
226
226
  Options:
227
- -f, --format <string> Output format for test results (default: "tap")
228
- -c, --concurrency <number> Number of tests to run concurrently (default: 4)
229
- -m, --mode <single|standard> Test run mode (default: "standard")
230
- -t, --tags <string> Tags to target or exclude
231
- -h, --help display help for command
227
+ -f, --format <string> Output format for test results (default: "tap")
228
+ -c, --concurrency <number> Number of tests to run concurrently (default: 9)
229
+ -m, --mode <single|standard> Test run mode (default: "standard")
230
+ -t, --tags <string> Tags to target or exclude
231
+ -o, --format-options <string> Format options
232
+ -h, --help display help for command
232
233
  ```
233
234
 
234
235
  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": "5.0.16",
3
+ "version": "5.0.17",
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/registry": "^5.0.14",
31
- "@travetto/runtime": "^5.0.14",
32
- "@travetto/terminal": "^5.0.16",
33
- "@travetto/worker": "^5.0.16",
30
+ "@travetto/registry": "^5.0.15",
31
+ "@travetto/runtime": "^5.0.15",
32
+ "@travetto/terminal": "^5.0.17",
33
+ "@travetto/worker": "^5.0.17",
34
34
  "yaml": "^2.6.1"
35
35
  },
36
36
  "peerDependencies": {
37
- "@travetto/cli": "^5.0.17",
38
- "@travetto/transformer": "^5.0.11"
37
+ "@travetto/cli": "^5.0.18",
38
+ "@travetto/transformer": "^5.0.12"
39
39
  },
40
40
  "peerDependenciesMeta": {
41
41
  "@travetto/transformer": {
@@ -120,7 +120,7 @@ export class AssertUtil {
120
120
  */
121
121
  static gernerateImportFailure(imp: string, err: Error): SuiteFailure {
122
122
  const name = path.basename(imp);
123
- const classId = `${RuntimeIndex.getFromImport(imp)?.id}○${name}`;
123
+ const classId = `${RuntimeIndex.getFromImport(imp)?.id}#${name}`;
124
124
  const suite = asFull<SuiteConfig & SuiteResult>({
125
125
  class: asFull<Class>({ name }), classId, duration: 0, lineStart: 1, lineEnd: 1, import: imp
126
126
  });
@@ -1,12 +1,13 @@
1
- import { classConstruct, type Class } from '@travetto/runtime';
2
- import { TestConsumer } from './types';
1
+ import path from 'path';
2
+ import { classConstruct, describeFunction, RuntimeIndex, type Class } from '@travetto/runtime';
3
+ import type { TestConsumerShape } from './types';
4
+ import { RunState } from '../execute/types';
3
5
 
4
6
  /**
5
7
  * Test Results Handler Registry
6
8
  */
7
9
  class $TestConsumerRegistry {
8
- #registered = new Map<string, Class<TestConsumer>>();
9
- #primary: Class<TestConsumer>;
10
+ #registered = new Map<string, Class<TestConsumerShape>>();
10
11
 
11
12
  /**
12
13
  * Manual initialization when running outside of the bootstrap process
@@ -15,38 +16,48 @@ class $TestConsumerRegistry {
15
16
  await import('./types/all');
16
17
  }
17
18
 
19
+ /**
20
+ * Import a specific path and load all consumers there
21
+ */
22
+ async importConsumers(pth: string): Promise<void> {
23
+ await import((RuntimeIndex.getEntry(pth) ?? RuntimeIndex.getFromImport(pth))!.outputFile);
24
+ }
25
+
18
26
  /**
19
27
  * Add a new consumer
20
- * @param type The consumer unique identifier
21
28
  * @param cls The consumer class
22
- * @param isDefault Set as the default consumer
23
29
  */
24
- add(type: string, cls: Class<TestConsumer>, isDefault = false): void {
25
- if (isDefault) {
26
- this.#primary = cls;
27
- }
28
- this.#registered.set(type, cls);
30
+ add(cls: Class<TestConsumerShape>): void {
31
+ const desc = describeFunction(cls);
32
+ const key = desc.module?.includes('@travetto') ? path.basename(desc.modulePath) : desc.import;
33
+ this.#registered.set(key, cls);
29
34
  }
30
35
 
31
36
  /**
32
37
  * Retrieve a registered consumer
33
38
  * @param type The unique identifier
34
39
  */
35
- get(type: string): Class<TestConsumer> {
40
+ get(type: string): Class<TestConsumerShape> {
36
41
  return this.#registered.get(type)!;
37
42
  }
38
43
 
44
+ /**
45
+ * Get types
46
+ */
47
+ getTypes(): string[] {
48
+ return [...this.#registered.keys()];
49
+ }
50
+
39
51
  /**
40
52
  * Get a consumer instance that supports summarization
41
53
  * @param consumer The consumer identifier or the actual consumer
42
54
  */
43
- async getInstance(consumer: string | TestConsumer): Promise<TestConsumer> {
55
+ async getInstance(state: Pick<RunState, 'consumer' | 'consumerOptions'>): Promise<TestConsumerShape> {
44
56
  // TODO: Fix consumer registry init
45
57
  await this.manualInit();
46
-
47
- return typeof consumer === 'string' ?
48
- classConstruct(this.get(consumer) ?? this.#primary) :
49
- consumer;
58
+ const inst = classConstruct(this.get(state.consumer));
59
+ await inst.setOptions?.(state.consumerOptions ?? {});
60
+ return inst;
50
61
  }
51
62
  }
52
63
 
@@ -54,11 +65,9 @@ export const TestConsumerRegistry = new $TestConsumerRegistry();
54
65
 
55
66
  /**
56
67
  * Registers a class a valid test consumer
57
- * @param type The unique identifier for the consumer
58
- * @param isDefault Is this the default consumer. Last one wins
59
68
  */
60
- export function Consumable(type: string, isDefault = false): (cls: Class<TestConsumer>) => void {
61
- return function (cls: Class<TestConsumer>): void {
62
- TestConsumerRegistry.add(type, cls, isDefault);
69
+ export function TestConsumer(): (cls: Class<TestConsumerShape>) => void {
70
+ return function (cls: Class<TestConsumerShape>): void {
71
+ TestConsumerRegistry.add(cls);
63
72
  };
64
73
  }
@@ -1,8 +1,6 @@
1
1
  import { AppError, hasToJSON } from '@travetto/runtime';
2
-
3
2
  import { TestEvent, } from '../model/event';
4
3
 
5
-
6
4
  export type SerializedError = { [K in keyof Error]: Error[K] extends Function ? never : Error[K] } & { $: true };
7
5
 
8
6
  function isError(e: unknown): e is SerializedError {
@@ -1,6 +1,6 @@
1
1
  import './cumulative';
2
2
  import './event';
3
- import './execution';
3
+ import './exec';
4
4
  import './json';
5
5
  import './noop';
6
6
  import './runnable';
@@ -1,11 +1,11 @@
1
1
  import { existsSync } from 'node:fs';
2
2
 
3
- import { Class, RuntimeIndex } from '@travetto/runtime';
3
+ import { type Class, RuntimeIndex } from '@travetto/runtime';
4
4
 
5
- import { TestConsumer } from '../types';
6
- import { TestEvent } from '../../model/event';
7
- import { TestResult } from '../../model/test';
8
- import { SuiteResult } from '../../model/suite';
5
+ import type { TestConsumerShape } from '../types';
6
+ import type { TestEvent } from '../../model/event';
7
+ import type { TestResult } from '../../model/test';
8
+ import type { SuiteResult } from '../../model/suite';
9
9
  import { SuiteRegistry } from '../../registry/suite';
10
10
  import { DelegatingConsumer } from './delegating';
11
11
 
@@ -18,7 +18,7 @@ export class CumulativeSummaryConsumer extends DelegatingConsumer {
18
18
  */
19
19
  #state: Record<string, Record<string, TestResult['status']>> = {};
20
20
 
21
- constructor(target: TestConsumer) {
21
+ constructor(target: TestConsumerShape) {
22
22
  super([target]);
23
23
  }
24
24
 
@@ -1,15 +1,15 @@
1
- import { SuitesSummary, TestConsumer, TestRunState } from '../types';
2
- import { TestEvent } from '../../model/event';
1
+ import type { SuitesSummary, TestConsumerShape, TestRunState } from '../types';
2
+ import type { TestEvent } from '../../model/event';
3
3
 
4
4
  /**
5
5
  * Delegating event consumer
6
6
  */
7
- export abstract class DelegatingConsumer implements TestConsumer {
8
- #consumers: TestConsumer[];
7
+ export abstract class DelegatingConsumer implements TestConsumerShape {
8
+ #consumers: TestConsumerShape[];
9
9
  #transformer?: (ev: TestEvent) => typeof ev;
10
10
  #filter?: (ev: TestEvent) => boolean;
11
11
 
12
- constructor(consumers: TestConsumer[]) {
12
+ constructor(consumers: TestConsumerShape[]) {
13
13
  this.#consumers = consumers;
14
14
  for (const c of consumers) {
15
15
  c.onEvent = c.onEvent.bind(c);
@@ -1,15 +1,15 @@
1
1
  import { Writable } from 'node:stream';
2
2
 
3
- import { TestEvent } from '../../model/event';
4
- import { TestConsumer } from '../types';
3
+ import type { TestEvent } from '../../model/event';
4
+ import type { TestConsumerShape } from '../types';
5
5
  import { SerializeUtil } from '../serialize';
6
- import { Consumable } from '../registry';
6
+ import { TestConsumer } from '../registry';
7
7
 
8
8
  /**
9
9
  * Streams all test events a JSON payload, in an nd-json format
10
10
  */
11
- @Consumable('event')
12
- export class EventStreamer implements TestConsumer {
11
+ @TestConsumer()
12
+ export class EventStreamer implements TestConsumerShape {
13
13
  #stream: Writable;
14
14
 
15
15
  constructor(stream: Writable = process.stdout) {
@@ -1,15 +1,15 @@
1
1
  import { IpcChannel } from '@travetto/worker';
2
2
 
3
- import { TestEvent } from '../../model/event';
4
- import { TestConsumer } from '../types';
3
+ import type { TestEvent } from '../../model/event';
4
+ import type { TestConsumerShape } from '../types';
5
5
  import { SerializeUtil } from '../serialize';
6
- import { Consumable } from '../registry';
6
+ import { TestConsumer } from '../registry';
7
7
 
8
8
  /**
9
9
  * Triggers each event as an IPC command to a parent process
10
10
  */
11
- @Consumable('exec')
12
- export class ExecutionEmitter extends IpcChannel<TestEvent> implements TestConsumer {
11
+ @TestConsumer()
12
+ export class ExecutionEmitter extends IpcChannel<TestEvent> implements TestConsumerShape {
13
13
  onEvent(event: TestEvent): void {
14
14
  this.send(event.type, JSON.parse(SerializeUtil.serializeToJSON(event)));
15
15
  }
@@ -1,14 +1,14 @@
1
- import { Writable } from 'node:stream';
1
+ import type { Writable } from 'node:stream';
2
2
 
3
- import { TestEvent } from '../../model/event';
4
- import { SuitesSummary, TestConsumer } from '../types';
5
- import { Consumable } from '../registry';
3
+ import type { TestEvent } from '../../model/event';
4
+ import type { SuitesSummary } from '../types';
5
+ import { TestConsumer } from '../registry';
6
6
 
7
7
  /**
8
8
  * Returns the entire result set as a single JSON document
9
9
  */
10
- @Consumable('json')
11
- export class JSONEmitter implements TestConsumer {
10
+ @TestConsumer()
11
+ export class JSONEmitter {
12
12
 
13
13
  #stream: Writable;
14
14
 
@@ -1,11 +1,11 @@
1
- import { TestEvent } from '../../model/event';
2
- import { TestConsumer } from '../types';
3
- import { Consumable } from '../registry';
1
+ import type { TestEvent } from '../../model/event';
2
+ import type { TestConsumerShape } from '../types';
3
+ import { TestConsumer } from '../registry';
4
4
 
5
5
  /**
6
6
  * Does nothing consumer
7
7
  */
8
- @Consumable('noop', true)
9
- export class NoopConsumer implements TestConsumer {
8
+ @TestConsumer()
9
+ export class NoopConsumer implements TestConsumerShape {
10
10
  onEvent(event: TestEvent): void { }
11
11
  }
@@ -1,23 +1,16 @@
1
- import { TestConsumer } from '../types';
1
+ import type { TestConsumerShape } from '../types';
2
2
  import { TestResultsSummarizer } from './summarizer';
3
- import { TestConsumerRegistry } from '../registry';
4
- import { TestEvent } from '../../model/event';
3
+ import type { TestEvent } from '../../model/event';
5
4
  import { DelegatingConsumer } from './delegating';
6
5
 
7
6
  /**
8
7
  * Test consumer with support for multiple nested consumers, and summarization
9
8
  */
10
9
  export class RunnableTestConsumer extends DelegatingConsumer {
11
- /**
12
- * Build a runnable test consumer given a format or a full consumer
13
- */
14
- static async get(consumer: string | TestConsumer): Promise<RunnableTestConsumer> {
15
- return new RunnableTestConsumer([await TestConsumerRegistry.getInstance(consumer)]);
16
- }
17
10
 
18
11
  #results?: TestResultsSummarizer;
19
12
 
20
- constructor(consumers: TestConsumer[]) {
13
+ constructor(...consumers: TestConsumerShape[]) {
21
14
  super(consumers);
22
15
  this.#results = consumers.find(x => !!x.onSummary) ? new TestResultsSummarizer() : undefined;
23
16
  }
@@ -1,11 +1,11 @@
1
- import { SuiteResult } from '../../model/suite';
2
- import { TestEvent } from '../../model/event';
3
- import { SuitesSummary, TestConsumer } from '../types';
1
+ import type { SuiteResult } from '../../model/suite';
2
+ import type { TestEvent } from '../../model/event';
3
+ import type { SuitesSummary, TestConsumerShape } from '../types';
4
4
 
5
5
  /**
6
6
  * Test Result Collector, combines all results into a single Suite Result
7
7
  */
8
- export class TestResultsSummarizer implements TestConsumer {
8
+ export class TestResultsSummarizer implements TestConsumerShape {
9
9
 
10
10
  summary: SuitesSummary = {
11
11
  passed: 0,
@@ -1,30 +1,48 @@
1
1
  import { Util, AsyncQueue } from '@travetto/runtime';
2
2
  import { StyleUtil, Terminal, TerminalUtil } from '@travetto/terminal';
3
3
 
4
- import { TestEvent } from '../../model/event';
5
- import { TestResult } from '../../model/test';
4
+ import type { TestEvent } from '../../model/event';
5
+ import type { TestResult } from '../../model/test';
6
6
 
7
- import { SuitesSummary, TestConsumer, TestRunState } from '../types';
8
- import { Consumable } from '../registry';
7
+ import type { SuitesSummary, TestConsumerShape, TestRunState } from '../types';
8
+ import { TestConsumer } from '../registry';
9
9
 
10
10
  import { TapEmitter } from './tap';
11
11
 
12
+ type Result = {
13
+ key: string;
14
+ duration: number;
15
+ tests: number;
16
+ };
17
+
12
18
  /**
13
19
  * Streamed summary results
14
20
  */
15
- @Consumable('tap-streamed')
16
- export class TapStreamedEmitter implements TestConsumer {
21
+ @TestConsumer()
22
+ export class TapStreamedEmitter implements TestConsumerShape {
23
+
24
+ #timings = new Map([
25
+ ['file', new Map<string, Result>()],
26
+ ['module', new Map<string, Result>()],
27
+ ['suite', new Map<string, Result>()],
28
+ ['test', new Map<string, Result>()],
29
+ ] as const);
17
30
 
18
31
  #terminal: Terminal;
19
32
  #results = new AsyncQueue<TestResult>();
20
33
  #progress: Promise<unknown> | undefined;
21
34
  #consumer: TapEmitter;
35
+ #options?: Record<string, unknown>;
22
36
 
23
37
  constructor(terminal: Terminal = new Terminal(process.stderr)) {
24
38
  this.#terminal = terminal;
25
39
  this.#consumer = new TapEmitter(this.#terminal);
26
40
  }
27
41
 
42
+ setOptions(options?: Record<string, unknown>): Promise<void> | void {
43
+ this.#options = options;
44
+ }
45
+
28
46
  async onStart(state: TestRunState): Promise<void> {
29
47
  this.#consumer.onStart();
30
48
 
@@ -55,6 +73,38 @@ export class TapStreamedEmitter implements TestConsumer {
55
73
  if (test.status === 'failed') {
56
74
  this.#consumer.onEvent(ev);
57
75
  }
76
+ const tests = this.#timings.get('test')!;
77
+ tests.set(`${ev.test.classId}/${ev.test.methodName}`, {
78
+ key: `${ev.test.classId}/${ev.test.methodName}`,
79
+ duration: test.duration,
80
+ tests: 1
81
+ });
82
+ } else if (ev.type === 'suite' && ev.phase === 'after') {
83
+ const [module] = ev.suite.classId.split(/:/);
84
+ const [file] = ev.suite.classId.split(/#/);
85
+
86
+ const modules = this.#timings.get('module')!;
87
+ const files = this.#timings.get('file')!;
88
+ const suites = this.#timings.get('suite')!;
89
+
90
+ if (!modules!.has(module)) {
91
+ modules.set(module, { key: module, duration: 0, tests: 0 });
92
+ }
93
+
94
+ if (!files.has(file)) {
95
+ files.set(file, { key: file, duration: 0, tests: 0 });
96
+ }
97
+
98
+ suites.set(ev.suite.classId, {
99
+ key: ev.suite.classId,
100
+ duration: ev.suite.duration,
101
+ tests: ev.suite.tests.length
102
+ });
103
+
104
+ files.get(file)!.duration += ev.suite.duration;
105
+ files.get(file)!.tests += ev.suite.tests.length;
106
+ modules.get(module)!.duration += ev.suite.duration;
107
+ modules.get(module)!.tests += ev.suite.tests.length;
58
108
  }
59
109
  }
60
110
 
@@ -65,5 +115,21 @@ export class TapStreamedEmitter implements TestConsumer {
65
115
  this.#results.close();
66
116
  await this.#progress;
67
117
  await this.#consumer.onSummary?.(summary);
118
+
119
+ const enhancer = this.#consumer.enhancer;
120
+
121
+ if (this.#options?.timings) {
122
+ const count = +(this.#options?.count ?? 5);
123
+ console.log('\n---');
124
+ for (const [title, results] of [...this.#timings.entries()].sort((a, b) => a[0].localeCompare(b[0]))) {
125
+ console.log(`${enhancer.suiteName(`Top ${count} slowest ${title}s`)}: `);
126
+ const top10 = [...results.values()].sort((a, b) => b.duration - a.duration).slice(0, count);
127
+ for (const x of top10) {
128
+ console.log(` * ${enhancer.testName(x.key)} - ${enhancer.total(x.duration)}ms / ${enhancer.total(x.tests)} tests`);
129
+ }
130
+ console.log();
131
+ }
132
+ console.log('...');
133
+ }
68
134
  }
69
135
  }
@@ -3,17 +3,17 @@ import { stringify } from 'yaml';
3
3
  import { Terminal } from '@travetto/terminal';
4
4
  import { TimeUtil, Runtime, RuntimeIndex, hasToJSON } from '@travetto/runtime';
5
5
 
6
- import { TestEvent } from '../../model/event';
7
- import { SuitesSummary, TestConsumer } from '../types';
8
- import { Consumable } from '../registry';
6
+ import type { TestEvent } from '../../model/event';
7
+ import type { SuitesSummary, TestConsumerShape } from '../types';
8
+ import { TestConsumer } from '../registry';
9
9
  import { SerializeUtil } from '../serialize';
10
10
  import { TestResultsEnhancer, CONSOLE_ENHANCER } from '../enhancer';
11
11
 
12
12
  /**
13
13
  * TAP Format consumer
14
14
  */
15
- @Consumable('tap')
16
- export class TapEmitter implements TestConsumer {
15
+ @TestConsumer()
16
+ export class TapEmitter implements TestConsumerShape {
17
17
  #count = 0;
18
18
  #enhancer: TestResultsEnhancer;
19
19
  #terminal: Terminal;
@@ -27,6 +27,10 @@ export class TapEmitter implements TestConsumer {
27
27
  this.#enhancer = enhancer;
28
28
  }
29
29
 
30
+ get enhancer(): TestResultsEnhancer {
31
+ return this.#enhancer;
32
+ }
33
+
30
34
  log(message: string): void {
31
35
  this.#terminal.writer.writeLine(message).commit();
32
36
  }
@@ -36,7 +40,7 @@ export class TapEmitter implements TestConsumer {
36
40
  */
37
41
  onStart(): void {
38
42
  this.#start = Date.now();
39
- this.log(this.#enhancer.suiteName('TAP version 13')!);
43
+ this.log(this.#enhancer.suiteName('TAP version 14')!);
40
44
  }
41
45
 
42
46
  /**
@@ -44,7 +48,7 @@ export class TapEmitter implements TestConsumer {
44
48
  */
45
49
  logMeta(obj: Record<string, unknown>): void {
46
50
  const lineLength = this.#terminal.width - 5;
47
- let body = stringify(obj, { lineWidth: lineLength });
51
+ let body = stringify(obj, { lineWidth: lineLength, indent: 2 });
48
52
  body = body.split('\n').map(x => ` ${x}`).join('\n');
49
53
  this.log(`---\n${this.#enhancer.objectInspect(body)}\n...`);
50
54
  }
@@ -1,18 +1,18 @@
1
- import { Writable } from 'node:stream';
1
+ import type { Writable } from 'node:stream';
2
2
 
3
3
  import { stringify } from 'yaml';
4
4
 
5
5
  import { RuntimeIndex } from '@travetto/runtime';
6
6
 
7
- import { TestEvent } from '../../model/event';
8
- import { SuitesSummary, TestConsumer } from '../types';
9
- import { Consumable } from '../registry';
7
+ import type { TestEvent } from '../../model/event';
8
+ import type { SuitesSummary, TestConsumerShape } from '../types';
9
+ import { TestConsumer } from '../registry';
10
10
 
11
11
  /**
12
12
  * Xunit consumer, compatible with JUnit formatters
13
13
  */
14
- @Consumable('xunit')
15
- export class XunitEmitter implements TestConsumer {
14
+ @TestConsumer()
15
+ export class XunitEmitter implements TestConsumerShape {
16
16
  #tests: string[] = [];
17
17
  #suites: string[] = [];
18
18
  #stream: Writable;
@@ -24,9 +24,13 @@ export type TestRunState = {
24
24
  };
25
25
 
26
26
  /**
27
- * A test result handler
27
+ * A test consumer shape
28
28
  */
29
- export interface TestConsumer {
29
+ export interface TestConsumerShape {
30
+ /**
31
+ * Set options
32
+ */
33
+ setOptions?(options?: Record<string, unknown>): Promise<void> | void;
30
34
  /**
31
35
  * Listen for start of the test run
32
36
  */
@@ -10,6 +10,7 @@ import { TestRun } from '../model/test';
10
10
  import { TestExecutor } from './executor';
11
11
  import { RunnerUtil } from './util';
12
12
  import { RunState } from './types';
13
+ import { TestConsumerRegistry } from '../consumer/registry';
13
14
 
14
15
  /**
15
16
  * Test Runner
@@ -26,7 +27,8 @@ export class Runner {
26
27
  * Run all files
27
28
  */
28
29
  async runFiles(globs?: string[]): Promise<boolean> {
29
- const consumer = await RunnableTestConsumer.get(this.#state.consumer ?? this.#state.format);
30
+ const target = await TestConsumerRegistry.getInstance(this.#state);
31
+ const consumer = new RunnableTestConsumer(target);
30
32
  const tests = await RunnerUtil.getTestDigest(globs, this.#state.tags);
31
33
  const testRuns = RunnerUtil.getTestRuns(tests)
32
34
  .sort((a, b) => a.runId!.localeCompare(b.runId!));
@@ -58,7 +60,9 @@ export class Runner {
58
60
  RuntimeIndex.reinitForModule(entry.module);
59
61
  }
60
62
 
61
- const consumer = (await RunnableTestConsumer.get(this.#state.consumer ?? this.#state.format))
63
+ const target = await TestConsumerRegistry.getInstance(this.#state);
64
+
65
+ const consumer = new RunnableTestConsumer(target)
62
66
  .withTransformer(e => {
63
67
  // Copy run metadata to event
64
68
  e.metadata = run.metadata;
@@ -1,4 +1,3 @@
1
- import { TestConsumer } from '../consumer/types';
2
1
  import { TestRun } from '../model/test';
3
2
 
4
3
  /**
@@ -6,13 +5,13 @@ import { TestRun } from '../model/test';
6
5
  */
7
6
  export interface RunState {
8
7
  /**
9
- * Output format
8
+ * Test result consumer
10
9
  */
11
- format: string;
10
+ consumer: string;
12
11
  /**
13
- * The run consumer
12
+ * Test result consumer options?
14
13
  */
15
- consumer?: TestConsumer;
14
+ consumerOptions?: Record<string, unknown>;
16
15
  /**
17
16
  * Number of test suites to run concurrently, when mode is not single
18
17
  */
@@ -34,7 +34,9 @@ export class TestWatcher {
34
34
  }
35
35
 
36
36
  const itr = new AsyncQueue(events);
37
- const consumer = new CumulativeSummaryConsumer(await TestConsumerRegistry.getInstance(format))
37
+ const consumer = new CumulativeSummaryConsumer(
38
+ await TestConsumerRegistry.getInstance({ consumer: format })
39
+ )
38
40
  .withFilter(x => x.metadata?.partial !== true || x.type !== 'suite');
39
41
 
40
42
  new MethodSource(RootRegistry).on(e => {
@@ -42,8 +44,9 @@ export class TestWatcher {
42
44
  if (!cls || describeFunction(cls).abstract) {
43
45
  return;
44
46
  }
47
+ const classId = cls.Ⲑid;
45
48
  if (!method) {
46
- consumer.removeClass(cls.Ⲑid);
49
+ consumer.removeClass(classId);
47
50
  return;
48
51
  }
49
52
  const conf = SuiteRegistry.getByClassAndMethod(cls, method)!;
@@ -60,13 +63,12 @@ export class TestWatcher {
60
63
  type: 'removeTest',
61
64
  methodNames: method?.name ? [method.name!] : undefined!,
62
65
  method: method?.name,
63
- classId: cls?.Ⲑid,
66
+ classId,
64
67
  import: Runtime.getImport(cls)
65
68
  } satisfies TestRemovedEvent);
66
69
  }
67
70
  });
68
71
 
69
-
70
72
  // If a file is changed, but doesn't emit classes, re-run whole file
71
73
  RootRegistry.onNonClassChanges(imp => itr.add({ import: imp }));
72
74
 
@@ -81,7 +81,7 @@ export class TestChildWorker extends IpcChannel<TestRun> {
81
81
  console.debug('Running', { import: run.import });
82
82
 
83
83
  try {
84
- await new Runner({ format: 'exec', target: run }).run();
84
+ await new Runner({ consumer: 'exec', target: run }).run();
85
85
  } finally {
86
86
  this.#done.resolve();
87
87
  }
@@ -1,3 +1,8 @@
1
+ import { castTo } from '@travetto/runtime';
2
+ import { AllViewSymbol } from '@travetto/schema/src/internal/types';
3
+ import { SchemaRegistry } from '@travetto/schema';
4
+
5
+ import { TestConsumerRegistry } from '../../src/consumer/registry';
1
6
  import type { RunState } from '../../src/execute/types';
2
7
 
3
8
  /**
@@ -17,4 +22,22 @@ export async function runTests(opts: RunState): Promise<void> {
17
22
  console.error('Test Worker Failed', { error: err });
18
23
  process.exitCode = 1;
19
24
  }
25
+ }
26
+
27
+ export async function selectConsumer(inst: { format?: string }) {
28
+ await TestConsumerRegistry.manualInit();
29
+
30
+ let types = TestConsumerRegistry.getTypes();
31
+
32
+ if (inst.format?.includes('/')) {
33
+ await TestConsumerRegistry.importConsumers(inst.format);
34
+ types = TestConsumerRegistry.getTypes();
35
+ }
36
+
37
+ const cls = inst.constructor;
38
+
39
+ SchemaRegistry.get(castTo(cls)).views[AllViewSymbol].schema.format.enum = {
40
+ message: `{path} is only allowed to be "${types.join('" or "')}"`,
41
+ values: types
42
+ };
20
43
  }
@@ -7,26 +7,36 @@ import { CliCommandShape, CliCommand, CliValidationError } from '@travetto/cli';
7
7
  import { WorkPool } from '@travetto/worker';
8
8
  import { Max, Min } from '@travetto/schema';
9
9
 
10
- import { TestFormat, TestMode } from './bin/types';
10
+ import { selectConsumer } from './bin/run';
11
11
 
12
12
  /**
13
13
  * Launch test framework and execute tests
14
14
  */
15
15
  @CliCommand()
16
16
  export class TestCommand implements CliCommandShape {
17
+
17
18
  /** Output format for test results */
18
- format: TestFormat = 'tap';
19
+ format: string = 'tap';
20
+
19
21
  /** Number of tests to run concurrently */
20
22
  @Min(1) @Max(WorkPool.MAX_SIZE)
21
23
  concurrency: number = WorkPool.DEFAULT_SIZE;
24
+
22
25
  /** Test run mode */
23
- mode: TestMode = 'standard';
26
+ mode: 'single' | 'standard' = 'standard';
27
+
24
28
  /**
25
29
  * Tags to target or exclude
26
30
  * @alias env.TRV_TEST_TAGS
27
31
  */
28
32
  tags?: string[];
29
33
 
34
+ /**
35
+ * Format options
36
+ * @alias o
37
+ */
38
+ formatOptions?: string[];
39
+
30
40
  preMain(): void {
31
41
  EventEmitter.defaultMaxListeners = 1000;
32
42
  Env.TRV_ROLE.set('test');
@@ -40,12 +50,15 @@ export class TestCommand implements CliCommandShape {
40
50
  return fs.stat(path.resolve(first ?? '')).then(x => x.isFile(), () => false);
41
51
  }
42
52
 
43
- async resolvedMode(first: string, rest: string[]): Promise<TestMode> {
53
+ async resolvedMode(first: string, rest: string[]): Promise<string> {
44
54
  return (await this.isFirstFile(first)) && rest.length === 0 ? 'single' : this.mode;
45
55
  }
46
56
 
47
- async validate(first: string = '**/*', rest: string[]): Promise<CliValidationError | undefined> {
57
+ async preValidate(): Promise<void> {
58
+ await selectConsumer(this);
59
+ }
48
60
 
61
+ async validate(first: string = '**/*', rest: string[]): Promise<CliValidationError | undefined> {
49
62
  const mode = await this.resolvedMode(first, rest);
50
63
 
51
64
  if (mode === 'single' && !await this.isFirstFile(first)) {
@@ -58,10 +71,12 @@ export class TestCommand implements CliCommandShape {
58
71
 
59
72
  const isFirst = await this.isFirstFile(first);
60
73
  const isSingle = this.mode === 'single' || (isFirst && globs.length === 0);
74
+ const options = Object.fromEntries((this.formatOptions ?? [])?.map(f => [...f.split(':'), true]));
61
75
 
62
76
  return runTests({
63
77
  concurrency: this.concurrency,
64
- format: this.format,
78
+ consumer: this.format,
79
+ consumerOptions: options,
65
80
  tags: this.tags,
66
81
  target: isSingle ?
67
82
  {
@@ -1,14 +1,17 @@
1
- import { Env, RuntimeIndex } from '@travetto/runtime';
1
+ import { Env } from '@travetto/runtime';
2
2
  import { CliCommand } from '@travetto/cli';
3
3
 
4
- import { runTests } from './bin/run';
5
- import { TestFormat } from './bin/types';
4
+ import { runTests, selectConsumer } from './bin/run';
6
5
 
7
6
  /** Direct test invocation */
8
7
  @CliCommand({ hidden: true })
9
8
  export class TestDirectCommand {
10
9
 
11
- format: TestFormat = 'tap';
10
+ format: string = 'tap';
11
+
12
+ async preValidate(): Promise<void> {
13
+ await selectConsumer(this);
14
+ }
12
15
 
13
16
  preMain(): void {
14
17
  Env.TRV_ROLE.set('test');
@@ -19,7 +22,7 @@ export class TestDirectCommand {
19
22
 
20
23
  main(importOrFile: string, clsId?: string, methodsNames: string[] = []): Promise<void> {
21
24
  return runTests({
22
- format: this.format,
25
+ consumer: this.format,
23
26
  target: {
24
27
  import: importOrFile,
25
28
  classId: clsId,
@@ -1,7 +1,7 @@
1
1
  import { Env } from '@travetto/runtime';
2
2
  import { CliCommand, CliUtil } from '@travetto/cli';
3
3
 
4
- import { TestFormat } from './bin/types';
4
+ import { selectConsumer } from './bin/run';
5
5
 
6
6
  /**
7
7
  * Invoke the test watcher
@@ -9,9 +9,13 @@ import { TestFormat } from './bin/types';
9
9
  @CliCommand()
10
10
  export class TestWatcherCommand {
11
11
 
12
- format: TestFormat = 'tap';
12
+ format: string = 'tap';
13
13
  mode: 'all' | 'change' = 'all';
14
14
 
15
+ async preValidate(): Promise<void> {
16
+ await selectConsumer(this);
17
+ }
18
+
15
19
  preMain(): void {
16
20
  Env.TRV_ROLE.set('test');
17
21
  Env.TRV_DYNAMIC.set(true);
@@ -48,21 +48,21 @@ const METHODS: Record<string, Function[]> = {
48
48
 
49
49
  const OP_TOKEN_TO_NAME = new Map<number, keyof typeof OPTOKEN_ASSERT>();
50
50
 
51
- const AssertⲐ = Symbol.for('@travetto/test:assert');
52
- const IsTestⲐ = Symbol.for('@travetto/test:valid');
51
+ const AssertSymbol = Symbol.for('@travetto/test:assert');
52
+ const IsTestSymbol = Symbol.for('@travetto/test:valid');
53
53
 
54
54
  /**
55
55
  * Assert transformation state
56
56
  */
57
57
  interface AssertState {
58
- [AssertⲐ]?: {
58
+ [AssertSymbol]?: {
59
59
  assert: ts.Identifier;
60
60
  hasAssertCall?: boolean;
61
61
  assertCheck: ts.Expression;
62
62
  checkThrow: ts.Expression;
63
63
  checkThrowAsync: ts.Expression;
64
64
  };
65
- [IsTestⲐ]?: boolean;
65
+ [IsTestSymbol]?: boolean;
66
66
  }
67
67
 
68
68
  /**
@@ -136,9 +136,9 @@ export class AssertTransformer {
136
136
  * Initialize transformer state
137
137
  */
138
138
  static initState(state: TransformerState & AssertState): void {
139
- if (!state[AssertⲐ]) {
139
+ if (!state[AssertSymbol]) {
140
140
  const asrt = state.importFile('@travetto/test/src/assert/check').ident;
141
- state[AssertⲐ] = {
141
+ state[AssertSymbol] = {
142
142
  assert: asrt,
143
143
  assertCheck: CoreUtil.createAccess(state.factory, asrt, ASSERT_UTIL, 'check'),
144
144
  checkThrow: CoreUtil.createAccess(state.factory, asrt, ASSERT_UTIL, 'checkThrow'),
@@ -157,7 +157,7 @@ export class AssertTransformer {
157
157
  const firstText = first!.getText();
158
158
 
159
159
  cmd.args = cmd.args.filter(x => x !== undefined && x !== null);
160
- const check = state.factory.createCallExpression(state[AssertⲐ]!.assertCheck, undefined, state.factory.createNodeArray([
160
+ const check = state.factory.createCallExpression(state[AssertSymbol]!.assertCheck, undefined, state.factory.createNodeArray([
161
161
  state.fromLiteral({
162
162
  module: state.getModuleIdentifier(),
163
163
  line: state.fromLiteral(ts.getLineAndCharacterOfPosition(state.source, node.getStart()).line + 1),
@@ -180,7 +180,7 @@ export class AssertTransformer {
180
180
 
181
181
  this.initState(state);
182
182
  return state.factory.createCallExpression(
183
- /reject/i.test(key) ? state[AssertⲐ]!.checkThrowAsync : state[AssertⲐ]!.checkThrow,
183
+ /reject/i.test(key) ? state[AssertSymbol]!.checkThrowAsync : state[AssertSymbol]!.checkThrow,
184
184
  undefined,
185
185
  state.factory.createNodeArray([
186
186
  state.fromLiteral({
@@ -269,13 +269,13 @@ export class AssertTransformer {
269
269
 
270
270
  @OnMethod('AssertCheck')
271
271
  static onAssertCheck(state: TransformerState & AssertState, node: ts.MethodDeclaration): ts.MethodDeclaration {
272
- state[IsTestⲐ] = true;
272
+ state[IsTestSymbol] = true;
273
273
  return node;
274
274
  }
275
275
 
276
276
  @AfterMethod('AssertCheck')
277
277
  static afterAssertCheck(state: TransformerState & AssertState, node: ts.MethodDeclaration): ts.MethodDeclaration {
278
- state[IsTestⲐ] = false;
278
+ state[IsTestSymbol] = false;
279
279
  return node;
280
280
  }
281
281
 
@@ -285,7 +285,7 @@ export class AssertTransformer {
285
285
  @OnCall()
286
286
  static onAssertCall(state: TransformerState & AssertState, node: ts.CallExpression): ts.CallExpression {
287
287
  // Only check in test mode
288
- if (!state[IsTestⲐ]) {
288
+ if (!state[IsTestSymbol]) {
289
289
  return node;
290
290
  }
291
291
 
@@ -1,2 +0,0 @@
1
- export type TestFormat = 'tap' | 'tap-streamed' | 'xunit' | 'event' | 'exec' | 'json';
2
- export type TestMode = 'single' | 'standard';