@travetto/test 5.0.0-rc.9 → 5.0.0

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
@@ -221,12 +221,13 @@ To run the tests you can either call the [Command Line Interface](https://github
221
221
  ```bash
222
222
  $ trv test --help
223
223
 
224
- Usage: test [options] [first:string] [regexes...:string]
224
+ Usage: test [options] [first:string] [globs...:string]
225
225
 
226
226
  Options:
227
227
  -f, --format <string> Output format for test results (default: "tap")
228
228
  -c, --concurrency <number> Number of tests to run concurrently (default: 4)
229
229
  -m, --mode <single|standard> Test run mode (default: "standard")
230
+ -t, --tags <string> Tags to target or exclude
230
231
  -h, --help display help for command
231
232
  ```
232
233
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@travetto/test",
3
- "version": "5.0.0-rc.9",
3
+ "version": "5.0.0",
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/runtime": "^5.0.0-rc.9",
31
- "@travetto/registry": "^5.0.0-rc.9",
32
- "@travetto/terminal": "^5.0.0-rc.9",
33
- "@travetto/worker": "^5.0.0-rc.9",
34
- "yaml": "^2.4.5"
30
+ "@travetto/registry": "^5.0.0",
31
+ "@travetto/runtime": "^5.0.0",
32
+ "@travetto/terminal": "^5.0.0",
33
+ "@travetto/worker": "^5.0.0",
34
+ "yaml": "^2.5.0"
35
35
  },
36
36
  "peerDependencies": {
37
- "@travetto/cli": "^5.0.0-rc.9",
38
- "@travetto/transformer": "^5.0.0-rc.6"
37
+ "@travetto/cli": "^5.0.0",
38
+ "@travetto/transformer": "^5.0.0"
39
39
  },
40
40
  "peerDependenciesMeta": {
41
41
  "@travetto/transformer": {
@@ -265,7 +265,7 @@ export class AssertCheck {
265
265
  * Look for any unhandled exceptions
266
266
  */
267
267
  static checkUnhandled(test: TestConfig, err: Error | assert.AssertionError): void {
268
- let line = AssertUtil.getPositionOfError(err, test.import).line;
268
+ let line = AssertUtil.getPositionOfError(err, test.sourceImport ?? test.import).line;
269
269
  if (line === 1) {
270
270
  line = test.lineStart;
271
271
  }
@@ -1,9 +1,10 @@
1
1
  import util from 'node:util';
2
+ import path from 'node:path';
2
3
 
3
- import { Runtime, RuntimeIndex } from '@travetto/runtime';
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
10
  return !!o && typeof o === 'object' && 'toClean' in o && typeof o.toClean === 'function';
@@ -18,6 +19,7 @@ export class AssertUtil {
18
19
  */
19
20
  static cleanValue(val: unknown): unknown {
20
21
  switch (typeof val) {
22
+ case 'number': case 'boolean': case 'bigint': case 'string': case 'undefined': return val;
21
23
  case 'object': {
22
24
  if (isCleanable(val)) {
23
25
  return val.toClean();
@@ -26,7 +28,6 @@ export class AssertUtil {
26
28
  }
27
29
  break;
28
30
  }
29
- case 'undefined': case 'string': case 'number': case 'bigint': case 'boolean': return JSON.stringify(val);
30
31
  case 'function': {
31
32
  if (val.Ⲑid || !val.constructor) {
32
33
  return val.name;
@@ -87,7 +88,7 @@ export class AssertUtil {
87
88
  /**
88
89
  * Generate a suite error given a suite config, and an error
89
90
  */
90
- static generateSuiteError(suite: SuiteConfig, methodName: string, error: Error): { assert: Assertion, testResult: TestResult, testConfig: TestConfig } {
91
+ static generateSuiteFailure(suite: SuiteConfig, methodName: string, error: Error): SuiteFailure {
91
92
  const { import: imp, ...pos } = this.getPositionOfError(error, suite.import);
92
93
  let line = pos.line;
93
94
 
@@ -108,11 +109,24 @@ export class AssertUtil {
108
109
  ...coreAll,
109
110
  status: 'failed', error, duration: 0, durationTotal: 0, assertions: [assert], output: {}
110
111
  };
111
- const testConfig: TestConfig = {
112
+ const test: TestConfig = {
112
113
  ...coreAll,
113
114
  class: suite.class, skip: false
114
115
  };
115
116
 
116
- 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);
117
131
  }
118
132
  }
@@ -5,11 +5,11 @@ import { TestEvent, } from '../model/event';
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
@@ -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,25 +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
- out.test.error = this.serializeError(out.test.error);
70
- }
71
- } else if (out.type === 'assertion') {
72
- if (out.assertion.error) {
73
- out.assertion.error = this.serializeError(out.assertion.error);
74
- }
75
- }
76
- }
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
+ );
77
70
  }
78
71
  }
@@ -7,19 +7,19 @@ 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
  /**
@@ -29,16 +29,9 @@ export class CumulativeSummaryConsumer implements TestConsumer {
29
29
  summarizeSuite(test: TestResult): SuiteResult {
30
30
  // Was only loading to verify existence (TODO: double-check)
31
31
  if (existsSync(RuntimeIndex.getFromImport(test.import)!.sourceFile)) {
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
- }
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
  }
@@ -83,14 +76,13 @@ export class CumulativeSummaryConsumer implements TestConsumer {
83
76
  * Listen for event, process the full event, and if the event is an after test,
84
77
  * send a full suite summary
85
78
  */
86
- onEvent(e: TestEvent): void {
87
- this.#target.onEvent(e);
79
+ onEventDone(e: TestEvent): void {
88
80
  try {
89
81
  if (e.type === 'test' && e.phase === 'after') {
90
- this.#target.onEvent({
82
+ this.onEvent({
91
83
  type: 'suite',
92
84
  phase: 'after',
93
- suite: this.summarizeSuite(e.test)
85
+ suite: this.summarizeSuite(e.test),
94
86
  });
95
87
  }
96
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
  }
@@ -2,7 +2,7 @@ import { ChildCommChannel } from '@travetto/worker';
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
  /**
@@ -12,8 +12,6 @@ import { Consumable } from '../registry';
12
12
  export class ExecutionEmitter extends ChildCommChannel<TestEvent> implements TestConsumer {
13
13
 
14
14
  onEvent(event: TestEvent): void {
15
- const out = { ...event };
16
- ErrorUtil.serializeTestErrors(out);
17
- this.send(event.type, out);
15
+ this.send(event.type, JSON.parse(SerializeUtil.serializeToJSON(event)));
18
16
  }
19
17
  }
@@ -1,62 +1,33 @@
1
- import { TestConsumer, TestRunState } from '../types';
1
+ import { TestConsumer } from '../types';
2
2
  import { TestResultsSummarizer } from './summarizer';
3
3
  import { TestConsumerRegistry } from '../registry';
4
4
  import { TestEvent } from '../../model/event';
5
+ import { DelegatingConsumer } from './delegating';
5
6
 
6
7
  /**
7
8
  * Test consumer with support for multiple nested consumers, and summarization
8
9
  */
9
- export class RunnableTestConsumer implements TestConsumer {
10
+ export class RunnableTestConsumer extends DelegatingConsumer {
10
11
  /**
11
12
  * Build a runnable test consumer given a format or a full consumer
12
13
  */
13
14
  static async get(consumer: string | TestConsumer): Promise<RunnableTestConsumer> {
14
- return new RunnableTestConsumer(await TestConsumerRegistry.getInstance(consumer));
15
+ return new RunnableTestConsumer([await TestConsumerRegistry.getInstance(consumer)]);
15
16
  }
16
17
 
17
- #consumers: TestConsumer[];
18
- #results: TestResultsSummarizer | undefined;
18
+ #results?: TestResultsSummarizer;
19
19
 
20
- constructor(...consumers: TestConsumer[]) {
21
- this.#consumers = consumers;
22
- for (const c of consumers) {
23
- if (!this.#results && c.onSummary) { // If expecting summary
24
- this.#results = new TestResultsSummarizer();
25
- }
26
- c.onEvent = c.onEvent.bind(c);
27
- }
20
+ constructor(consumers: TestConsumer[]) {
21
+ super(consumers);
22
+ this.#results = consumers.find(x => !!x.onSummary) ? new TestResultsSummarizer() : undefined;
28
23
  }
29
24
 
30
- async onStart(state: TestRunState): Promise<void> {
31
- for (const c of this.#consumers) {
32
- await c.onStart?.(state);
33
- }
34
- }
35
-
36
- onEvent(e: TestEvent): void {
37
- if (this.#results) {
38
- this.#results.onEvent(e);
39
- }
40
- for (const c of this.#consumers) {
41
- c.onEvent(e);
42
- }
43
- }
44
-
45
- async summarize(): Promise<TestResultsSummarizer | undefined> {
46
- if (this.#results) {
47
- for (const c of this.#consumers) {
48
- await c.onSummary?.(this.#results.summary);
49
- }
50
- return this.#results;
51
- }
25
+ onEventDone(e: TestEvent): void {
26
+ this.#results?.onEvent(e);
52
27
  }
53
28
 
54
29
  async summarizeAsBoolean(): Promise<boolean> {
55
- const result = await this.summarize();
56
- if (result) {
57
- return result.summary.failed <= 0;
58
- } else {
59
- return true;
60
- }
30
+ await this.summarize(this.#results?.summary);
31
+ return (this.#results?.summary.failed ?? 0) <= 0;
61
32
  }
62
33
  }
@@ -1,6 +1,5 @@
1
- import { Util } from '@travetto/runtime';
1
+ import { Util, AsyncQueue } from '@travetto/runtime';
2
2
  import { StyleUtil, Terminal, TerminalUtil } from '@travetto/terminal';
3
- import { WorkQueue } from '@travetto/worker';
4
3
 
5
4
  import { TestEvent } from '../../model/event';
6
5
  import { TestResult } from '../../model/test';
@@ -17,7 +16,7 @@ import { TapEmitter } from './tap';
17
16
  export class TapStreamedEmitter implements TestConsumer {
18
17
 
19
18
  #terminal: Terminal;
20
- #results = new WorkQueue<TestResult>();
19
+ #results = new AsyncQueue<TestResult>();
21
20
  #progress: Promise<unknown> | undefined;
22
21
  #consumer: TapEmitter;
23
22
 
@@ -30,6 +29,8 @@ export class TapStreamedEmitter implements TestConsumer {
30
29
  this.#consumer.onStart();
31
30
 
32
31
  let failed = 0;
32
+ let skipped = 0;
33
+ let completed = 0;
33
34
  const success = StyleUtil.getStyle({ text: '#e5e5e5', background: '#026020' }); // White on dark green
34
35
  const fail = StyleUtil.getStyle({ text: '#e5e5e5', background: '#8b0000' }); // White on dark red
35
36
  this.#progress = this.#terminal.streamToBottom(
@@ -37,7 +38,9 @@ export class TapStreamedEmitter implements TestConsumer {
37
38
  this.#results,
38
39
  (value, idx) => {
39
40
  failed += (value.status === 'failed' ? 1 : 0);
40
- return { value: `Tests %idx/%total [${failed} failed] -- ${value.classId}`, total: state.testCount, idx };
41
+ skipped += (value.status === 'skipped' ? 1 : 0);
42
+ completed += (value.status !== 'skipped' ? 1 : 0);
43
+ return { value: `Tests %idx/%total [${failed} failed, ${skipped} skipped] -- ${value.classId}`, total: state.testCount, idx: completed };
41
44
  },
42
45
  TerminalUtil.progressBarUpdater(this.#terminal, { style: () => ({ complete: failed ? fail : success }) })
43
46
  ),
@@ -5,7 +5,7 @@ import { stringify } from 'yaml';
5
5
  import { TestEvent } from '../../model/event';
6
6
  import { SuitesSummary, TestConsumer } from '../types';
7
7
  import { Consumable } from '../registry';
8
- import { ErrorUtil } from '../error';
8
+ import { SerializeUtil } from '../serialize';
9
9
  import { TestResultsEnhancer, CONSOLE_ENHANCER } from '../enhancer';
10
10
 
11
11
  /**
@@ -102,7 +102,7 @@ export class TapEmitter implements TestConsumer {
102
102
  // Handle error
103
103
  if (test.status === 'failed') {
104
104
  if (test.error && test.error.name !== 'AssertionError') {
105
- const err = ErrorUtil.deserializeError(test.error);
105
+ const err = SerializeUtil.deserializeError(test.error);
106
106
  this.logMeta({ error: err instanceof AppError ? err.toJSON() : err });
107
107
  }
108
108
  }