@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
@@ -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/base';
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,16 +29,20 @@ export class TapStreamedEmitter implements TestConsumer {
30
29
  this.#consumer.onStart();
31
30
 
32
31
  let failed = 0;
33
- const succ = StyleUtil.getStyle({ text: '#e5e5e5', background: '#026020' }); // White on dark green
32
+ let skipped = 0;
33
+ let completed = 0;
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(
36
37
  Util.mapAsyncItr(
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
- TerminalUtil.progressBarUpdater(this.#terminal, { style: () => ({ complete: failed ? fail : succ }) })
45
+ TerminalUtil.progressBarUpdater(this.#terminal, { style: () => ({ complete: failed ? fail : success }) })
43
46
  ),
44
47
  { minDelay: 100 }
45
48
  );
@@ -1,12 +1,11 @@
1
- import { RuntimeIndex } from '@travetto/manifest';
2
1
  import { Terminal } from '@travetto/terminal';
3
- import { AppError, TimeUtil } from '@travetto/base';
2
+ import { AppError, TimeUtil, Runtime, RuntimeIndex } from '@travetto/runtime';
4
3
  import { stringify } from 'yaml';
5
4
 
6
5
  import { TestEvent } from '../../model/event';
7
6
  import { SuitesSummary, TestConsumer } from '../types';
8
7
  import { Consumable } from '../registry';
9
- import { ErrorUtil } from '../error';
8
+ import { SerializeUtil } from '../serialize';
10
9
  import { TestResultsEnhancer, CONSOLE_ENHANCER } from '../enhancer';
11
10
 
12
11
  /**
@@ -67,11 +66,12 @@ export class TapEmitter implements TestConsumer {
67
66
  let subCount = 0;
68
67
  for (const asrt of test.assertions) {
69
68
  const text = asrt.message ? `${asrt.text} (${this.#enhancer.failure(asrt.message)})` : asrt.text;
69
+ const pth = RuntimeIndex.getFromImport(asrt.import)!.sourceFile.replace(Runtime.mainSourcePath, '.');
70
70
  let subMessage = [
71
71
  this.#enhancer.assertNumber(++subCount),
72
72
  '-',
73
73
  this.#enhancer.assertDescription(text),
74
- `${this.#enhancer.assertFile(asrt.file.replace(RuntimeIndex.mainModule.sourcePath, '.'))}:${this.#enhancer.assertLine(asrt.line)}`
74
+ `${this.#enhancer.assertFile(pth)}:${this.#enhancer.assertLine(asrt.line)}`
75
75
  ].join(' ');
76
76
 
77
77
  if (asrt.error) {
@@ -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
  }
@@ -2,6 +2,8 @@ import { Writable } from 'node:stream';
2
2
 
3
3
  import { stringify } from 'yaml';
4
4
 
5
+ import { RuntimeIndex } from '@travetto/runtime';
6
+
5
7
  import { TestEvent } from '../../model/event';
6
8
  import { SuitesSummary, TestConsumer } from '../types';
7
9
  import { Consumable } from '../registry';
@@ -85,7 +87,7 @@ export class XunitEmitter implements TestConsumer {
85
87
  failures="${suite.failed}"
86
88
  errors="${suite.failed}"
87
89
  skipped="${suite.skipped}"
88
- file="${suite.file}"
90
+ file="${RuntimeIndex.getFromImport(suite.import)!.sourceFile}"
89
91
  >
90
92
  ${testBodies.join('\n')}
91
93
  </testsuite>
@@ -101,7 +103,7 @@ export class XunitEmitter implements TestConsumer {
101
103
  this.#stream.write(`
102
104
  <?xml version="1.0" encoding="UTF-8"?>
103
105
  <testsuites
104
- name="${summary.suites.length ? summary.suites[0].file : 'nameless'}"
106
+ name="${summary.suites.length ? RuntimeIndex.getFromImport(summary.suites[0].import)?.sourceFile : 'nameless'}"
105
107
  time="${summary.duration}"
106
108
  tests="${summary.total}"
107
109
  failures="${summary.failed}"
@@ -1,5 +1,4 @@
1
- import { RuntimeIndex } from '@travetto/manifest';
2
- import { Class, ClassInstance } from '@travetto/base';
1
+ import { castTo, Class, ClassInstance, describeFunction } from '@travetto/runtime';
3
2
 
4
3
  import { SuiteRegistry } from '../registry/suite';
5
4
  import { SuiteConfig } from '../model/suite';
@@ -24,17 +23,16 @@ export function Suite(description?: string | Partial<SuiteConfig>, ...rest: Part
24
23
  Object.assign(extra, r);
25
24
  }
26
25
 
27
- // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
28
- const decorator = ((target: Class) => {
26
+ const dec = (target: Class): typeof target => {
29
27
  const cfg = { description: descriptionString, ...extra };
30
- if (RuntimeIndex.getFunctionMetadata(target)?.abstract) {
28
+ if (describeFunction(target).abstract) {
31
29
  cfg.skip = true;
32
30
  }
33
31
  SuiteRegistry.register(target, cfg);
34
32
  return target;
35
- }) as ClassDecorator;
33
+ };
36
34
 
37
- return decorator;
35
+ return castTo(dec);
38
36
  }
39
37
 
40
38
  function listener(phase: SuitePhase) {
@@ -1,4 +1,4 @@
1
- import { ClassInstance } from '@travetto/base';
1
+ import { ClassInstance } from '@travetto/runtime';
2
2
 
3
3
  import { SuiteRegistry } from '../registry/suite';
4
4
  import { TestConfig } from '../model/test';
@@ -15,6 +15,7 @@ export function AssertCheck(): MethodDecorator {
15
15
  * @param description The test description
16
16
  * @augments `@travetto/test:Test`
17
17
  * @augments `@travetto/test:AssertCheck`
18
+ * @augments `@travetto/runtime:DebugBreak`
18
19
  */
19
20
  export function Test(): MethodDecorator;
20
21
  export function Test(...rest: Partial<TestConfig>[]): MethodDecorator;
@@ -1,6 +1,6 @@
1
1
  import util from 'node:util';
2
2
 
3
- import { ConsoleEvent, ConsoleListener, ConsoleManager } from '@travetto/base';
3
+ import { ConsoleEvent, ConsoleListener, ConsoleManager } from '@travetto/runtime';
4
4
 
5
5
  /**
6
6
  * Console capturer. Hooks into the Console manager, and collects the
@@ -1,13 +1,11 @@
1
1
  import { AssertionError } from 'node:assert';
2
- import path from 'node:path';
3
2
 
4
- import { RuntimeIndex, RuntimeContext } from '@travetto/manifest';
5
- import { Env, TimeUtil } from '@travetto/base';
3
+ import { Env, TimeUtil, Runtime, castTo } from '@travetto/runtime';
6
4
  import { Barrier, ExecutionError } from '@travetto/worker';
7
5
 
8
6
  import { SuiteRegistry } from '../registry/suite';
9
- import { TestConfig, TestResult } from '../model/test';
10
- import { SuiteConfig, SuiteResult } from '../model/suite';
7
+ import { TestConfig, TestResult, TestRun } from '../model/test';
8
+ import { SuiteConfig, SuiteFailure, SuiteResult } from '../model/suite';
11
9
  import { TestConsumer } from '../consumer/types';
12
10
  import { AssertCheck } from '../assert/check';
13
11
  import { AssertCapture } from '../assert/capture';
@@ -23,12 +21,42 @@ const TEST_TIMEOUT = TimeUtil.fromValue(Env.TRV_TEST_TIMEOUT.val) ?? 5000;
23
21
  */
24
22
  export class TestExecutor {
25
23
 
24
+ #consumer: TestConsumer;
25
+ #testFilter: (config: TestConfig) => boolean;
26
+
27
+ constructor(consumer: TestConsumer, testFilter?: (config: TestConfig) => boolean) {
28
+ this.#consumer = consumer;
29
+ this.#testFilter = testFilter || ((): boolean => true);
30
+ }
31
+
32
+ /**
33
+ * Handles communicating a suite-level error
34
+ * @param failure
35
+ * @param withSuite
36
+ */
37
+ #onSuiteFailure(failure: SuiteFailure, triggerSuite?: boolean): void {
38
+ if (triggerSuite) {
39
+ this.#consumer.onEvent({ type: 'suite', phase: 'before', suite: failure.suite });
40
+ }
41
+
42
+ this.#consumer.onEvent({ type: 'test', phase: 'before', test: failure.test });
43
+ this.#consumer.onEvent({ type: 'assertion', phase: 'after', assertion: failure.assert });
44
+ this.#consumer.onEvent({ type: 'test', phase: 'after', test: failure.testResult });
45
+
46
+ if (triggerSuite) {
47
+ this.#consumer.onEvent({
48
+ type: 'suite', phase: 'after',
49
+ suite: { ...castTo(failure.suite), failed: 1, passed: 0, total: 1, skipped: 0 }
50
+ });
51
+ }
52
+ }
53
+
26
54
  /**
27
55
  * Raw execution, runs the method and then returns any thrown errors as the result.
28
56
  *
29
57
  * This method should never throw under any circumstances.
30
58
  */
31
- static async #executeTestMethod(test: TestConfig): Promise<Error | undefined> {
59
+ async #executeTestMethod(test: TestConfig): Promise<Error | undefined> {
32
60
  const suite = SuiteRegistry.get(test.class);
33
61
 
34
62
  // Ensure all the criteria below are satisfied before moving forward
@@ -40,8 +68,7 @@ export class TestExecutor {
40
68
 
41
69
  try {
42
70
  await pCap.run(() =>
43
- // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
44
- (suite.instance as Record<string, Function>)[test.methodName]()
71
+ castTo<Record<string, Function>>(suite.instance)[test.methodName]()
45
72
  );
46
73
  } finally {
47
74
  process.env = env; // Restore
@@ -55,35 +82,27 @@ export class TestExecutor {
55
82
  /**
56
83
  * Determining if we should skip
57
84
  */
58
- static async #skip(cfg: TestConfig | SuiteConfig, inst: unknown): Promise<boolean | undefined> {
59
- if (cfg.skip !== undefined) {
60
- if (typeof cfg.skip === 'boolean' ? cfg.skip : await cfg.skip(inst)) {
61
- return true;
62
- }
85
+ async #shouldSkip(cfg: TestConfig | SuiteConfig, inst: unknown): Promise<boolean | undefined> {
86
+ if ('methodName' in cfg && !this.#testFilter(cfg)) {
87
+ return true;
88
+ }
89
+
90
+ if (typeof cfg.skip === 'function' ? await cfg.skip(inst) : cfg.skip) {
91
+ return true;
63
92
  }
64
93
  }
65
94
 
66
- /**
67
- * Fail an entire file, marking the whole file as failed
68
- */
69
- static failFile(consumer: TestConsumer, file: string, err: Error): void {
70
- const name = path.basename(file);
71
- const classId = RuntimeIndex.getId(file, name);
72
- // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
73
- const suite = { class: { name }, classId, duration: 0, lineStart: 1, lineEnd: 1, file, } as SuiteConfig & SuiteResult;
74
- err.message = err.message.replaceAll(RuntimeIndex.mainModule.sourcePath, '.');
75
- const res = AssertUtil.generateSuiteError(suite, 'require', err);
76
- consumer.onEvent({ type: 'suite', phase: 'before', suite });
77
- consumer.onEvent({ type: 'test', phase: 'before', test: res.testConfig });
78
- consumer.onEvent({ type: 'assertion', phase: 'after', assertion: res.assert });
79
- consumer.onEvent({ type: 'test', phase: 'after', test: res.testResult });
80
- consumer.onEvent({ type: 'suite', phase: 'after', suite: { ...suite, failed: 1, passed: 0, total: 1, skipped: 0 } });
95
+ #skipTest(test: TestConfig, result: SuiteResult): void {
96
+ // Mark test start
97
+ this.#consumer.onEvent({ type: 'test', phase: 'before', test });
98
+ result.skipped++;
99
+ this.#consumer.onEvent({ type: 'test', phase: 'after', test: { ...test, assertions: [], duration: 0, durationTotal: 0, output: {}, status: 'skipped' } });
81
100
  }
82
101
 
83
102
  /**
84
103
  * An empty suite result based on a suite config
85
104
  */
86
- static createSuiteResult(suite: SuiteConfig): SuiteResult {
105
+ createSuiteResult(suite: SuiteConfig): SuiteResult {
87
106
  return {
88
107
  passed: 0,
89
108
  failed: 0,
@@ -91,7 +110,7 @@ export class TestExecutor {
91
110
  total: 0,
92
111
  lineStart: suite.lineStart,
93
112
  lineEnd: suite.lineEnd,
94
- file: suite.file,
113
+ import: suite.import,
95
114
  classId: suite.classId,
96
115
  duration: 0,
97
116
  tests: []
@@ -101,22 +120,22 @@ export class TestExecutor {
101
120
  /**
102
121
  * Execute the test, capture output, assertions and promises
103
122
  */
104
- static async executeTest(consumer: TestConsumer, test: TestConfig, suite: SuiteConfig): Promise<TestResult> {
123
+ async executeTest(test: TestConfig): Promise<TestResult> {
105
124
 
106
125
  // Mark test start
107
- consumer.onEvent({ type: 'test', phase: 'before', test });
126
+ this.#consumer.onEvent({ type: 'test', phase: 'before', test });
108
127
 
109
128
  const startTime = Date.now();
110
129
 
111
130
  const result: TestResult = {
112
131
  methodName: test.methodName,
113
- module: RuntimeContext.main.name,
114
132
  description: test.description,
115
133
  classId: test.classId,
116
134
  lineStart: test.lineStart,
117
135
  lineEnd: test.lineEnd,
118
136
  lineBodyStart: test.lineBodyStart,
119
- file: test.file,
137
+ import: test.import,
138
+ sourceImport: test.sourceImport,
120
139
  status: 'skipped',
121
140
  assertions: [],
122
141
  duration: 0,
@@ -124,16 +143,12 @@ export class TestExecutor {
124
143
  output: {},
125
144
  };
126
145
 
127
- if (await this.#skip(test, suite.instance)) {
128
- return result;
129
- }
130
-
131
146
  // Emit every assertion as it occurs
132
- const getAssertions = AssertCapture.collector(test, assrt =>
133
- consumer.onEvent({
147
+ const getAssertions = AssertCapture.collector(test, asrt =>
148
+ this.#consumer.onEvent({
134
149
  type: 'assertion',
135
150
  phase: 'after',
136
- assertion: assrt
151
+ assertion: asrt
137
152
  })
138
153
  );
139
154
 
@@ -165,47 +180,27 @@ export class TestExecutor {
165
180
  });
166
181
 
167
182
  // Mark completion
168
- consumer.onEvent({ type: 'test', phase: 'after', test: result });
183
+ this.#consumer.onEvent({ type: 'test', phase: 'after', test: result });
169
184
 
170
185
  return result;
171
186
  }
172
187
 
173
188
  /**
174
- * Execute a single test within a suite
189
+ * Execute an entire suite
175
190
  */
176
- static async executeSuiteTest(consumer: TestConsumer, suite: SuiteConfig, test: TestConfig): Promise<void> {
177
- const result: SuiteResult = this.createSuiteResult(suite);
178
-
179
- const mgr = new TestPhaseManager(consumer, suite, result);
180
-
181
- try {
182
- await mgr.startPhase('all');
183
- const skip = await this.#skip(test, suite.instance);
184
- if (!skip) {
185
- await mgr.startPhase('each');
186
- }
187
- await this.executeTest(consumer, test, suite);
188
- if (!skip) {
189
- await mgr.endPhase('each');
190
- }
191
- await mgr.endPhase('all');
192
- } catch (err) {
193
- await mgr.onError(err);
191
+ async executeSuite(suite: SuiteConfig, tests: TestConfig[]): Promise<void> {
192
+ if (!tests.length || await this.#shouldSkip(suite, suite.instance)) {
193
+ return;
194
194
  }
195
- }
196
195
 
197
- /**
198
- * Execute an entire suite
199
- */
200
- static async executeSuite(consumer: TestConsumer, suite: SuiteConfig): Promise<SuiteResult> {
201
196
  const result: SuiteResult = this.createSuiteResult(suite);
202
197
 
203
198
  const startTime = Date.now();
204
199
 
205
200
  // Mark suite start
206
- consumer.onEvent({ phase: 'before', type: 'suite', suite });
201
+ this.#consumer.onEvent({ phase: 'before', type: 'suite', suite });
207
202
 
208
- const mgr = new TestPhaseManager(consumer, suite, result);
203
+ const mgr = new TestPhaseManager(suite, result, e => this.#onSuiteFailure(e));
209
204
 
210
205
  const originalEnv = { ...process.env };
211
206
 
@@ -215,28 +210,30 @@ export class TestExecutor {
215
210
 
216
211
  const suiteEnv = { ...process.env };
217
212
 
218
- for (const test of suite.tests) {
213
+ for (const test of tests ?? suite.tests) {
214
+ if (await this.#shouldSkip(test, suite.instance)) {
215
+ this.#skipTest(test, result);
216
+ continue;
217
+ }
218
+
219
219
  // Reset env before each test
220
220
  process.env = { ...suiteEnv };
221
+
221
222
  const testStart = Date.now();
222
- const skip = await this.#skip(test, suite.instance);
223
- if (!skip) {
224
- // Handle BeforeEach
225
- await mgr.startPhase('each');
226
- }
223
+
224
+ // Handle BeforeEach
225
+ await mgr.startPhase('each');
227
226
 
228
227
  // Run test
229
- const ret = await this.executeTest(consumer, test, suite);
228
+ const ret = await this.executeTest(test);
230
229
  result[ret.status]++;
231
-
232
- if (!skip) {
233
- result.tests.push(ret);
234
- }
230
+ result.tests.push(ret);
235
231
 
236
232
  // Handle after each
237
233
  await mgr.endPhase('each');
238
234
  ret.durationTotal = Date.now() - testStart;
239
235
  }
236
+
240
237
  // Handle after all
241
238
  await mgr.endPhase('all');
242
239
  } catch (err) {
@@ -247,31 +244,23 @@ export class TestExecutor {
247
244
  process.env = { ...originalEnv };
248
245
 
249
246
  result.duration = Date.now() - startTime;
250
-
251
- // Mark suite complete
252
- consumer.onEvent({ phase: 'after', type: 'suite', suite: result });
253
-
254
247
  result.total = result.passed + result.failed;
255
248
 
256
- return result;
249
+ // Mark suite complete
250
+ this.#consumer.onEvent({ phase: 'after', type: 'suite', suite: result });
257
251
  }
258
252
 
259
253
  /**
260
254
  * Handle executing a suite's test/tests based on command line inputs
261
255
  */
262
- static async execute(consumer: TestConsumer, file: string, ...args: string[]): Promise<void> {
263
-
264
- file = path.resolve(file);
265
-
266
- const entry = RuntimeIndex.getEntry(file)!;
267
-
256
+ async execute(run: TestRun): Promise<void> {
268
257
  try {
269
- await import(entry.import);
258
+ await Runtime.importFrom(run.import);
270
259
  } catch (err) {
271
260
  if (!(err instanceof Error)) {
272
261
  throw err;
273
262
  }
274
- this.failFile(consumer, file, err);
263
+ this.#onSuiteFailure(AssertUtil.gernerateImportFailure(run.import, err));
275
264
  return;
276
265
  }
277
266
 
@@ -279,19 +268,10 @@ export class TestExecutor {
279
268
  await SuiteRegistry.init();
280
269
 
281
270
  // Convert inbound arguments to specific tests to run
282
- const params = SuiteRegistry.getRunParams(file, ...args);
271
+ const suites = SuiteRegistry.getSuiteTests(run);
283
272
 
284
- // If running specific suites
285
- if ('suites' in params) {
286
- for (const suite of params.suites) {
287
- if (!(await this.#skip(suite, suite.instance)) && suite.tests.length) {
288
- await this.executeSuite(consumer, suite);
289
- }
290
- }
291
- } else if (params.test) { // If running a single test
292
- await this.executeSuiteTest(consumer, params.suite, params.test);
293
- } else { // Running the suite
294
- await this.executeSuite(consumer, params.suite);
273
+ for (const { suite, tests } of suites) {
274
+ await this.executeSuite(suite, tests);
295
275
  }
296
276
  }
297
277
  }
@@ -1,12 +1,12 @@
1
1
  import { Barrier } from '@travetto/worker';
2
- import { Env, TimeUtil } from '@travetto/base';
2
+ import { Env, TimeUtil } from '@travetto/runtime';
3
3
 
4
- import { TestConsumer } from '../consumer/types';
5
- import { SuiteConfig, SuiteResult } from '../model/suite';
4
+ import { SuiteConfig, SuiteFailure, SuiteResult } from '../model/suite';
6
5
  import { AssertUtil } from '../assert/util';
7
- import { TestResult } from '../model/test';
8
6
 
9
- class TestBreakout extends Error { }
7
+ class TestBreakout extends Error {
8
+ source?: Error;
9
+ }
10
10
 
11
11
  const TEST_PHASE_TIMEOUT = TimeUtil.fromValue(Env.TRV_TEST_PHASE_TIMEOUT.val) ?? 15000;
12
12
 
@@ -17,27 +17,14 @@ const TEST_PHASE_TIMEOUT = TimeUtil.fromValue(Env.TRV_TEST_PHASE_TIMEOUT.val) ??
17
17
  */
18
18
  export class TestPhaseManager {
19
19
  #progress: ('all' | 'each')[] = [];
20
- #consumer: TestConsumer;
21
20
  #suite: SuiteConfig;
22
21
  #result: SuiteResult;
22
+ #onSuiteFailure: (fail: SuiteFailure) => void;
23
23
 
24
- constructor(consumer: TestConsumer, suite: SuiteConfig, result: SuiteResult) {
25
- this.#consumer = consumer;
24
+ constructor(suite: SuiteConfig, result: SuiteResult, onSuiteFailure: (fail: SuiteFailure) => void) {
26
25
  this.#suite = suite;
27
26
  this.#result = result;
28
- }
29
-
30
- /**
31
- * Create the appropriate events when a suite has an error
32
- */
33
- async triggerSuiteError(methodName: string, error: Error): Promise<TestResult> {
34
- const bad = AssertUtil.generateSuiteError(this.#suite, methodName, error);
35
-
36
- this.#consumer.onEvent({ type: 'test', phase: 'before', test: bad.testConfig });
37
- this.#consumer.onEvent({ type: 'assertion', phase: 'after', assertion: bad.assert });
38
- this.#consumer.onEvent({ type: 'test', phase: 'after', test: bad.testResult });
39
-
40
- return bad.testResult;
27
+ this.#onSuiteFailure = onSuiteFailure;
41
28
  }
42
29
 
43
30
  /**
@@ -57,10 +44,9 @@ export class TestPhaseManager {
57
44
  }
58
45
  }
59
46
  if (error) {
60
- const res = await this.triggerSuiteError(`[[${phase}]]`, error);
61
- this.#result.tests.push(res);
62
- this.#result.failed++;
63
- throw new TestBreakout();
47
+ const tbo = new TestBreakout(`[[${phase}]]`);
48
+ tbo.source = error;
49
+ throw tbo;
64
50
  }
65
51
  }
66
52
 
@@ -96,10 +82,14 @@ export class TestPhaseManager {
96
82
 
97
83
  this.#progress = [];
98
84
 
99
- if (!(err instanceof TestBreakout)) {
100
- const res = await this.triggerSuiteError('all', err);
101
- this.#result.tests.push(res);
102
- this.#result.failed++;
103
- }
85
+ const failure = AssertUtil.generateSuiteFailure(
86
+ this.#suite,
87
+ err instanceof TestBreakout ? err.message : 'all',
88
+ err instanceof TestBreakout ? err.source! : err
89
+ );
90
+
91
+ this.#onSuiteFailure(failure);
92
+ this.#result.tests.push(failure.testResult);
93
+ this.#result.failed++;
104
94
  }
105
95
  }