@travetto/test 8.0.0-alpha.4 → 8.0.0-alpha.5

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@travetto/test",
3
- "version": "8.0.0-alpha.4",
3
+ "version": "8.0.0-alpha.5",
4
4
  "type": "module",
5
5
  "description": "Declarative test framework",
6
6
  "keywords": [
@@ -28,15 +28,15 @@
28
28
  "directory": "module/test"
29
29
  },
30
30
  "dependencies": {
31
- "@travetto/registry": "^8.0.0-alpha.4",
32
- "@travetto/runtime": "^8.0.0-alpha.4",
33
- "@travetto/terminal": "^8.0.0-alpha.4",
34
- "@travetto/worker": "^8.0.0-alpha.4",
31
+ "@travetto/registry": "^8.0.0-alpha.5",
32
+ "@travetto/runtime": "^8.0.0-alpha.5",
33
+ "@travetto/terminal": "^8.0.0-alpha.5",
34
+ "@travetto/worker": "^8.0.0-alpha.5",
35
35
  "yaml": "^2.8.2"
36
36
  },
37
37
  "peerDependencies": {
38
- "@travetto/cli": "^8.0.0-alpha.7",
39
- "@travetto/transformer": "^8.0.0-alpha.3"
38
+ "@travetto/cli": "^8.0.0-alpha.8",
39
+ "@travetto/transformer": "^8.0.0-alpha.4"
40
40
  },
41
41
  "peerDependenciesMeta": {
42
42
  "@travetto/transformer": {
@@ -178,17 +178,13 @@ export class AssertCheck {
178
178
  static #onError(
179
179
  positive: boolean,
180
180
  message: string | undefined,
181
- error: unknown,
181
+ errorValue: unknown,
182
182
  missed: Error | undefined,
183
183
  shouldThrow: ThrowableError | undefined,
184
184
  assertion: CapturedAssertion
185
185
  ): void {
186
- if (!(error instanceof Error)) {
187
- error = new Error(`${error}`);
188
- }
189
- if (!(error instanceof Error)) {
190
- throw error;
191
- }
186
+ const error = errorValue instanceof Error ? errorValue : new Error(`${errorValue}`);
187
+
192
188
  if (positive) {
193
189
  missed = new assert.AssertionError({ message: 'Error thrown, but expected no errors' });
194
190
  missed.stack = error.stack;
@@ -1,10 +1,9 @@
1
1
  import util from 'node:util';
2
- import path from 'node:path';
3
2
 
4
- import { asFull, type Class, JSONUtil, hasFunction, Runtime, RuntimeIndex, Util } from '@travetto/runtime';
3
+ import { JSONUtil, hasFunction, RuntimeIndex, Util } from '@travetto/runtime';
5
4
 
6
- import type { TestConfig, TestResult } from '../model/test.ts';
7
- import type { SuiteConfig, SuiteResult } from '../model/suite.ts';
5
+ import type { Assertion, TestConfig } from '../model/test.ts';
6
+ import type { SuiteConfig } from '../model/suite.ts';
8
7
 
9
8
  const isCleanable = hasFunction<{ toClean(): unknown }>('toClean');
10
9
 
@@ -52,79 +51,21 @@ export class AssertUtil {
52
51
  /**
53
52
  * Generate a suite error given a suite config, and an error
54
53
  */
55
- static generateSuiteTestFailure(config: { suite: SuiteConfig, test: TestConfig, error: Error, importLocation?: string }): TestResult {
56
- const { suite, test, error, importLocation } = config;
54
+ static generateAssertion(config: { suite: SuiteConfig, test: TestConfig, error: Error, importLocation?: string }): Assertion {
55
+ const { suite, test, error: errorValue, importLocation } = config;
56
+ const error = (errorValue.cause && errorValue.cause instanceof Error) ? errorValue.cause : errorValue;
57
57
  const testImport = importLocation ?? test.import;
58
58
  const position = this.getPositionOfError(error);
59
59
  const line = position?.line ?? (testImport === suite.import ? suite.lineStart : 1);
60
- const testResult: TestResult = {
61
- ...suite.tests[test.methodName],
62
- suiteLineStart: suite.lineStart,
63
- status: 'errored',
60
+ return {
61
+ import: position?.import ?? testImport,
62
+ methodName: test.methodName,
63
+ classId: suite.classId,
64
+ operator: 'throw',
64
65
  error,
65
- duration: 0,
66
- durationTotal: 0,
67
- output: [],
68
- assertions: [{
69
- import: position?.import ?? testImport,
70
- methodName: test.methodName,
71
- classId: suite.classId,
72
- operator: 'throw',
73
- error,
74
- line,
75
- message: error.message.split(/\n/)[0],
76
- text: test.methodName
77
- }],
66
+ line,
67
+ message: error.message.split(/\n/)[0],
68
+ text: test.methodName
78
69
  };
79
-
80
- return testResult;
81
- }
82
-
83
- /**
84
- * Generate suite failure
85
- */
86
- static generateSuiteTestFailures(suite: SuiteConfig, error: Error): TestResult[] {
87
- const finalError = error.cause instanceof Error ? error.cause : error;
88
- return Object.values(suite.tests).map(test => this.generateSuiteTestFailure({ suite, test, error: finalError }));
89
- }
90
-
91
- /**
92
- * Define import failure as a TestResult
93
- */
94
- static gernerateImportFailure(importLocation: string, error: Error): { result: TestResult, test: TestConfig, suite: SuiteResult & SuiteConfig } {
95
- const name = path.basename(importLocation);
96
- const classId = `${RuntimeIndex.getFromImport(importLocation)?.id}#${name}`;
97
- const suite = asFull<SuiteConfig & SuiteResult>({
98
- class: asFull<Class>({ name }), classId, duration: 0, lineStart: 1, lineEnd: 1, import: importLocation
99
- });
100
- error.message = error.message.replaceAll(Runtime.mainSourcePath, '.');
101
- const result = this.generateSuiteTestFailure({
102
- suite,
103
- test: {
104
- methodName: 'require',
105
- classId,
106
- import: importLocation,
107
- class: suite.class,
108
- lineBodyStart: 1,
109
- lineStart: 1,
110
- lineEnd: 1,
111
- skip: false
112
- },
113
- error
114
- });
115
- const test: TestConfig = {
116
- methodName: 'import',
117
- classId,
118
- import: importLocation,
119
- declarationImport: importLocation,
120
- lineStart: 0,
121
- lineEnd: 0,
122
- lineBodyStart: 0,
123
- tags: [],
124
- description: 'Import Failure',
125
- skip: false,
126
- class: undefined!
127
- };
128
- return { result, test, suite };
129
70
  }
130
71
  }
@@ -1,7 +1,7 @@
1
1
  import type { TestConsumerShape } from '../types.ts';
2
2
  import type { TestEvent, TestRemoveEvent } from '../../model/event.ts';
3
3
  import type { TestConfig, TestDiffSource, TestResult } from '../../model/test.ts';
4
- import type { Counts, SuiteConfig, SuiteResult } from '../../model/suite.ts';
4
+ import type { SuiteConfig, SuiteResult } from '../../model/suite.ts';
5
5
  import { DelegatingConsumer } from './delegating.ts';
6
6
  import type { SuiteCore } from '../../model/common.ts';
7
7
  import { TestModelUtil } from '../../model/util.ts';
@@ -9,7 +9,7 @@ import { TestModelUtil } from '../../model/util.ts';
9
9
  type ClassId = string;
10
10
  type ImportName = string;
11
11
 
12
- type CumulativeTestResult = Pick<TestResult, 'sourceHash' | 'status' | 'duration'>;
12
+ type CumulativeTestResult = Pick<TestResult, 'sourceHash' | 'status' | 'duration' | 'selfDuration'>;
13
13
  type CumulativeSuiteResult = Pick<SuiteCore, 'import' | 'classId' | 'sourceHash'> & {
14
14
  tests: Record<string, CumulativeTestResult>;
15
15
  };
@@ -37,7 +37,7 @@ export class CumulativeSummaryConsumer extends DelegatingConsumer {
37
37
 
38
38
  onTestBefore(config: TestConfig): TestConfig {
39
39
  const suite = this.getSuite(config);
40
- suite.tests[config.methodName] = { sourceHash: config.sourceHash, status: 'unknown', duration: 0 };
40
+ suite.tests[config.methodName] = { sourceHash: config.sourceHash, status: 'unknown', duration: 0, selfDuration: 0 };
41
41
  return config;
42
42
  }
43
43
 
@@ -56,21 +56,9 @@ export class CumulativeSummaryConsumer extends DelegatingConsumer {
56
56
  onSuiteAfter(result: SuiteResult): SuiteResult {
57
57
  // Reset counts
58
58
  const suite = this.getSuite(result);
59
- const totals: Counts & { duration: number } = {
60
- passed: 0,
61
- failed: 0,
62
- skipped: 0,
63
- errored: 0,
64
- unknown: 0,
65
- total: 0,
66
- duration: 0
67
- };
68
- for (const test of Object.values(suite.tests)) {
69
- totals[test.status] += 1;
70
- totals.total += 1;
71
- totals.duration += test.duration ?? 0;
72
- }
73
- return { ...result, ...totals, status: TestModelUtil.countsToTestStatus(totals) };
59
+ const results = TestModelUtil.buildSummary();
60
+ TestModelUtil.countTestResult(results, Object.values(suite.tests));
61
+ return { ...result, ...results, status: TestModelUtil.computeTestStatus(results) };
74
62
  }
75
63
 
76
64
  removeTest(importName: string, classId?: string, methodName?: string): void {
@@ -20,6 +20,12 @@ export abstract class DelegatingConsumer implements TestConsumerShape {
20
20
  }
21
21
  }
22
22
 
23
+ async onTestRunState(state: TestRunState): Promise<void> {
24
+ for (const consumer of this.#consumers) {
25
+ await consumer.onTestRunState?.(state);
26
+ }
27
+ }
28
+
23
29
  onRemoveEvent(event: TestRemoveEvent): void {
24
30
  let result = event;
25
31
  if (this.transformRemove) {
@@ -22,6 +22,7 @@ export class RunnableTestConsumer extends DelegatingConsumer {
22
22
 
23
23
  async summarizeAsBoolean(): Promise<boolean> {
24
24
  await this.summarize(this.#results?.summary);
25
- return (this.#results?.summary.failed ?? 0) <= 0;
25
+ return (this.#results?.summary.failed ?? 0) <= 0 &&
26
+ (this.#results?.summary.errored ?? 0) <= 0;
26
27
  }
27
28
  }
@@ -1,6 +1,7 @@
1
1
  import type { SuiteResult } from '../../model/suite.ts';
2
2
  import type { TestEvent } from '../../model/event.ts';
3
3
  import type { SuitesSummary, TestConsumerShape } from '../types.ts';
4
+ import { TestModelUtil } from '../../model/util.ts';
4
5
 
5
6
  /**
6
7
  * Test Result Collector, combines all results into a single Suite Result
@@ -8,26 +9,13 @@ import type { SuitesSummary, TestConsumerShape } from '../types.ts';
8
9
  export class TestResultsSummarizer implements TestConsumerShape {
9
10
 
10
11
  summary: SuitesSummary = {
11
- passed: 0,
12
- failed: 0,
13
- errored: 0,
14
- skipped: 0,
15
- unknown: 0,
16
- total: 0,
17
- duration: 0,
18
- suites: [],
19
- errors: []
12
+ ...TestModelUtil.buildSummary(),
13
+ suites: []
20
14
  };
21
15
 
22
16
  #merge(result: SuiteResult): void {
17
+ TestModelUtil.countTestResult(this.summary, Object.values(result.tests));
23
18
  this.summary.suites.push(result);
24
- this.summary.failed += result.failed;
25
- this.summary.errored += result.errored;
26
- this.summary.passed += result.passed;
27
- this.summary.unknown += result.unknown;
28
- this.summary.skipped += result.skipped;
29
- this.summary.duration += result.duration;
30
- this.summary.total += result.total;
31
19
  }
32
20
 
33
21
  /**
@@ -2,7 +2,7 @@ import { Util, AsyncQueue } from '@travetto/runtime';
2
2
  import { StyleUtil, Terminal, TerminalUtil } from '@travetto/terminal';
3
3
 
4
4
  import type { TestEvent } from '../../model/event.ts';
5
- import type { TestResult, TestStatus } from '../../model/test.ts';
5
+ import type { TestResult } from '../../model/test.ts';
6
6
 
7
7
  import type { SuitesSummary, TestConsumerShape, TestRunState } from '../types.ts';
8
8
  import { TestConsumer } from '../decorator.ts';
@@ -10,6 +10,7 @@ import { TestConsumer } from '../decorator.ts';
10
10
  import { TapEmitter } from './tap.ts';
11
11
  import { CONSOLE_ENHANCER, type TestResultsEnhancer } from '../enhancer.ts';
12
12
  import type { SuiteResult } from '../../model/suite.ts';
13
+ import { TestModelUtil } from '../../model/util.ts';
13
14
 
14
15
  type Result = {
15
16
  key: string;
@@ -31,6 +32,7 @@ export class TapSummaryEmitter implements TestConsumerShape {
31
32
  #consumer: TapEmitter;
32
33
  #enhancer: TestResultsEnhancer;
33
34
  #options?: Record<string, unknown>;
35
+ #state: TestRunState = {};
34
36
 
35
37
  constructor(terminal: Terminal = new Terminal(process.stderr)) {
36
38
  this.#terminal = terminal;
@@ -87,20 +89,24 @@ export class TapSummaryEmitter implements TestConsumerShape {
87
89
  this.#consumer.setOptions(options);
88
90
  }
89
91
 
92
+ onTestRunState(state: TestRunState): void {
93
+ Object.assign(this.#state, state);
94
+ }
95
+
90
96
  async onStart(state: TestRunState): Promise<void> {
91
97
  this.#consumer.onStart();
92
- const total: Record<TestStatus | 'count', number> = { errored: 0, failed: 0, passed: 0, skipped: 0, unknown: 0, count: 0 };
98
+ this.onTestRunState(state);
99
+
100
+ const total = TestModelUtil.buildSummary();
93
101
  const success = StyleUtil.getStyle({ text: '#e5e5e5', background: '#026020' }); // White on dark green
94
102
  const fail = StyleUtil.getStyle({ text: '#e5e5e5', background: '#8b0000' }); // White on dark red
95
103
  this.#progress = this.#terminal.streamToBottom(
96
104
  Util.mapAsyncIterable(
97
105
  this.#results,
98
106
  (value) => {
99
- total[value.status] += 1;
100
- total.count += 1;
107
+ TestModelUtil.countTestResult(total, [value]);
101
108
  const statusLine = `${total.failed} failed, ${total.errored} errored, ${total.skipped} skipped`;
102
- return { value: `Tests %idx/%total [${statusLine}] -- ${value.classId}`, total: state.testCount, idx: total.passed };
103
-
109
+ return { value: `Tests %idx/%total [${statusLine}] -- ${value.classId}`, total: this.#state.testCount, idx: total.passed };
104
110
  },
105
111
  TerminalUtil.progressBarUpdater(this.#terminal, { style: () => ({ complete: (total.failed || total.errored) ? fail : success }) })
106
112
  ),
@@ -112,7 +118,7 @@ export class TapSummaryEmitter implements TestConsumerShape {
112
118
  if (event.type === 'test' && event.phase === 'after') {
113
119
  const { test } = event;
114
120
  this.#results.add(test);
115
- if (test.status === 'failed') {
121
+ if (test.status !== 'passed' && test.status !== 'skipped') {
116
122
  this.#consumer.onEvent(event);
117
123
  }
118
124
  const tests = this.#timings.getOrInsert('test', new Map<string, Result>());
@@ -1,14 +1,17 @@
1
1
  import path from 'node:path';
2
+ import { AssertionError } from 'node:assert';
2
3
  import { stringify } from 'yaml';
3
4
 
4
5
  import { Terminal, StyleUtil } from '@travetto/terminal';
5
- import { TimeUtil, RuntimeIndex, hasToJSON, JSONUtil } from '@travetto/runtime';
6
+ import { TimeUtil, RuntimeIndex } from '@travetto/runtime';
6
7
 
7
8
  import type { TestEvent } from '../../model/event.ts';
8
9
  import type { SuitesSummary, TestConsumerShape } from '../types.ts';
9
10
  import { TestConsumer } from '../decorator.ts';
10
11
  import { type TestResultsEnhancer, CONSOLE_ENHANCER } from '../enhancer.ts';
11
12
 
13
+ const SPACE = ' ';
14
+
12
15
  /**
13
16
  * TAP Format consumer
14
17
  */
@@ -17,8 +20,8 @@ export class TapEmitter implements TestConsumerShape {
17
20
  #count = 0;
18
21
  #enhancer: TestResultsEnhancer;
19
22
  #terminal: Terminal;
20
- #start: number;
21
23
  #options?: Record<string, unknown>;
24
+ #start: number = 0;
22
25
 
23
26
  constructor(
24
27
  terminal = new Terminal(),
@@ -59,16 +62,14 @@ export class TapEmitter implements TestConsumerShape {
59
62
  * @param error
60
63
  */
61
64
  errorToString(error?: Error): string | undefined {
62
- if (error && error.name !== 'AssertionError') {
63
- if (error instanceof Error) {
64
- let out = JSONUtil.toUTF8(hasToJSON(error) ? error.toJSON() : error, { indent: 2 });
65
- if (this.#options?.verbose && error.stack) {
66
- out = `${out}\n${error.stack}`;
67
- }
68
- return out;
69
- } else {
70
- return `${error}`;
71
- }
65
+ if (error instanceof AssertionError) {
66
+ return;
67
+ } else if (error instanceof Error) {
68
+ return error.stack ?
69
+ error.stack.split(/\n/).slice(0, this.#options?.verbose ? -1 : 5).join('\n') :
70
+ error.message;
71
+ } else {
72
+ return `${error}`;
72
73
  }
73
74
  }
74
75
 
@@ -125,9 +126,10 @@ export class TapEmitter implements TestConsumerShape {
125
126
  // Track test result
126
127
  let status = `${this.#enhancer.testNumber(++this.#count)} `;
127
128
  switch (test.status) {
129
+ case 'passed': `${this.#enhancer.success('ok')} ${status}`; break;
128
130
  case 'skipped': status += ' # SKIP'; break;
129
- case 'failed': status = `${this.#enhancer.failure('not ok')} ${status}`; break;
130
- default: status = `${this.#enhancer.success('ok')} ${status}`;
131
+ case 'unknown': break;
132
+ default: status = `${this.#enhancer.failure('not ok')} ${status}`; break;
131
133
  }
132
134
  status += header;
133
135
 
@@ -138,9 +140,9 @@ export class TapEmitter implements TestConsumerShape {
138
140
  case 'errored':
139
141
  case 'failed': {
140
142
  if (test.error) {
141
- const msg = this.errorToString(test.error);
142
- if (msg) {
143
- this.logMeta({ error: msg });
143
+ const message = this.errorToString(test.error);
144
+ if (message) {
145
+ this.logMeta({ error: message });
144
146
  }
145
147
  }
146
148
  break;
@@ -168,28 +170,22 @@ export class TapEmitter implements TestConsumerShape {
168
170
  onSummary(summary: SuitesSummary): void {
169
171
  this.log(`${this.#enhancer.testNumber(1)}..${this.#enhancer.testNumber(summary.total)}`);
170
172
 
171
- if (summary.errors.length) {
172
- this.log('---\n');
173
- for (const error of summary.errors) {
174
- const msg = this.errorToString(error);
175
- if (msg) {
176
- this.log(this.#enhancer.failure(msg));
177
- }
178
- }
179
- }
180
-
181
173
  const allPassed = !summary.failed && !summary.errored;
182
174
 
183
175
  this.log([
184
- this.#enhancer[allPassed ? 'success' : 'failure']('Results'),
185
- `${this.#enhancer.total(summary.passed)}/${this.#enhancer.total(summary.total)},`,
186
- allPassed ? 'failed' : this.#enhancer.failure('failed'),
187
- `${this.#enhancer.total(summary.failed)}`,
188
- allPassed ? 'errored' : this.#enhancer.failure('errored'),
189
- `${this.#enhancer.total(summary.errored)}`,
190
- 'skipped',
191
- this.#enhancer.total(summary.skipped),
192
- `# (Total Test Time: ${TimeUtil.asClock(summary.duration)}, Total Run Time: ${TimeUtil.asClock(Date.now() - this.#start)})`
193
- ].join(' '));
176
+ this.#enhancer[allPassed ? 'success' : 'failure']('Results'), SPACE,
177
+ `${this.#enhancer.total(summary.passed)}/${this.#enhancer.total(summary.total)},`, SPACE,
178
+ allPassed ? 'failed' : this.#enhancer.failure('failed'), SPACE,
179
+ `${this.#enhancer.total(summary.failed)}`, SPACE,
180
+ allPassed ? 'errored' : this.#enhancer.failure('errored'), SPACE,
181
+ `${this.#enhancer.total(summary.errored)}`, SPACE,
182
+ 'skipped', SPACE,
183
+ this.#enhancer.total(summary.skipped), SPACE,
184
+ '#', SPACE, '(Timings:', SPACE,
185
+ 'Self=', TimeUtil.asClock(summary.selfDuration), ',', SPACE,
186
+ 'Total=', TimeUtil.asClock(summary.duration), ',', SPACE,
187
+ 'Clock=', TimeUtil.asClock(Date.now() - this.#start),
188
+ ')',
189
+ ].join(''));
194
190
  }
195
191
  }
@@ -1,24 +1,16 @@
1
1
  import type { Class } from '@travetto/runtime';
2
2
 
3
3
  import type { TestEvent, TestRemoveEvent } from '../model/event.ts';
4
- import type { Counts, SuiteResult } from '../model/suite.ts';
4
+ import type { ResultsSummary, SuiteResult } from '../model/suite.ts';
5
5
 
6
6
  /**
7
7
  * All suite results
8
8
  */
9
- export interface SuitesSummary extends Counts {
9
+ export interface SuitesSummary extends ResultsSummary {
10
10
  /**
11
11
  * List of all suites
12
12
  */
13
13
  suites: SuiteResult[];
14
- /**
15
- * List of all errors
16
- */
17
- errors: Error[];
18
- /**
19
- * Total duration
20
- */
21
- duration: number;
22
14
  }
23
15
 
24
16
  export type TestRunState = {
@@ -44,6 +36,10 @@ export interface TestConsumerShape {
44
36
  * Set options
45
37
  */
46
38
  setOptions?(options?: Record<string, unknown>): Promise<void> | void;
39
+ /**
40
+ * Can directly update the known test run state as needed
41
+ */
42
+ onTestRunState?(state: TestRunState): Promise<void> | void;
47
43
  /**
48
44
  * Listen for start of the test run
49
45
  */
@@ -26,24 +26,6 @@ export class TestExecutor {
26
26
  this.#consumer = consumer;
27
27
  }
28
28
 
29
- #onSuiteTestError(result: TestResult, test: TestConfig): void {
30
- this.#consumer.onEvent({ type: 'test', phase: 'before', test });
31
- for (const assertion of result.assertions) {
32
- this.#consumer.onEvent({ type: 'assertion', phase: 'after', assertion });
33
- }
34
- this.#consumer.onEvent({ type: 'test', phase: 'after', test: result });
35
- }
36
-
37
- #recordSuiteErrors(suiteConfig: SuiteConfig, suiteResult: SuiteResult, errors: TestResult[]): void {
38
- for (const test of errors) {
39
- if (!suiteResult.tests[test.methodName]) {
40
- this.#onSuiteTestError(test, suiteConfig.tests[test.methodName]);
41
- suiteResult.errored += 1;
42
- suiteResult.total += 1;
43
- }
44
- }
45
- }
46
-
47
29
  /**
48
30
  * Raw execution, runs the method and then returns any thrown errors as the result.
49
31
  *
@@ -73,96 +55,43 @@ export class TestExecutor {
73
55
  }
74
56
  }
75
57
 
76
- #skipTest(test: TestConfig, result: SuiteResult): void {
77
- // Mark test start
78
- this.#consumer.onEvent({ type: 'test', phase: 'before', test });
79
- result.skipped += 1;
80
- result.total += 1;
81
- this.#consumer.onEvent({
82
- type: 'test',
83
- phase: 'after',
84
- test: {
85
- ...test,
86
- suiteLineStart: result.lineStart,
87
- assertions: [], duration: 0, durationTotal: 0, output: [], status: 'skipped'
88
- }
89
- });
90
- }
91
-
92
- /**
93
- * An empty suite result based on a suite config
94
- */
95
- createSuiteResult(suite: SuiteConfig, override?: Partial<SuiteResult>): SuiteResult {
96
- return {
97
- passed: 0,
98
- failed: 0,
99
- errored: 0,
100
- skipped: 0,
101
- unknown: 0,
102
- total: 0,
103
- status: 'unknown',
104
- lineStart: suite.lineStart,
105
- lineEnd: suite.lineEnd,
106
- import: suite.import,
107
- classId: suite.classId,
108
- sourceHash: suite.sourceHash,
109
- duration: 0,
110
- tests: {},
111
- ...override
112
- };
113
- }
114
-
115
58
  /**
116
59
  * Execute the test, capture output, assertions and promises
117
60
  */
118
- async executeTest(test: TestConfig, suite: SuiteConfig): Promise<TestResult> {
61
+ async executeTest(test: TestConfig, suite: SuiteConfig, override?: Partial<TestResult>): Promise<TestResult> {
62
+
63
+ const result = TestModelUtil.createTestResult(suite, test, override);
119
64
 
120
65
  // Mark test start
121
66
  this.#consumer.onEvent({ type: 'test', phase: 'before', test });
122
67
 
123
- const startTime = Date.now();
124
-
125
- const result: TestResult = {
126
- methodName: test.methodName,
127
- description: test.description,
128
- classId: test.classId,
129
- tags: test.tags,
130
- suiteLineStart: suite.lineStart,
131
- lineStart: test.lineStart,
132
- lineEnd: test.lineEnd,
133
- lineBodyStart: test.lineBodyStart,
134
- import: test.import,
135
- declarationImport: test.declarationImport,
136
- sourceHash: test.sourceHash,
137
- status: 'unknown',
138
- assertions: [],
139
- duration: 0,
140
- durationTotal: 0,
141
- output: [],
142
- };
143
68
 
144
69
  // Emit every assertion as it occurs
145
- const getAssertions = AssertCapture.collector(test, asrt =>
146
- this.#consumer.onEvent({
147
- type: 'assertion',
148
- phase: 'after',
149
- assertion: asrt
150
- })
151
- );
70
+ const getAssertions = AssertCapture.collector(test, item =>
71
+ this.#consumer.onEvent({ type: 'assertion', phase: 'after', assertion: item }));
152
72
 
153
73
  const consoleCapture = new ConsoleCapture().start(); // Capture all output from transpiled code
154
74
 
155
- // Run method and get result
156
- const error = await this.#executeTestMethod(test);
157
- const [status, finalError] = AssertCheck.validateTestResultError(test, error);
75
+ // Already finished
76
+ if (result.status !== 'unknown') {
77
+ if (result.error) {
78
+ result.assertions.push(AssertUtil.generateAssertion({ suite, test, error: result.error }));
79
+ }
80
+ for (const item of result.assertions ?? []) { AssertCapture.add(item); }
81
+ } else {
82
+ // Run method and get result
83
+ const startTime = Date.now();
84
+ const error = await this.#executeTestMethod(test);
85
+ const [status, finalError] = AssertCheck.validateTestResultError(test, error);
86
+ result.status = status;
87
+ result.selfDuration = Date.now() - startTime;
88
+ if (finalError) {
89
+ result.error = finalError;
90
+ }
91
+ }
158
92
 
159
- Object.assign(result, {
160
- status,
161
- output: consoleCapture.end(),
162
- assertions: getAssertions(),
163
- duration: Date.now() - startTime,
164
- ...(finalError ? { error: finalError } : {})
165
- });
93
+ result.output = consoleCapture.end();
94
+ result.assertions = getAssertions();
166
95
 
167
96
  // Mark completion
168
97
  this.#consumer.onEvent({ type: 'test', phase: 'after', test: result });
@@ -179,14 +108,17 @@ export class TestExecutor {
179
108
 
180
109
  const shouldSkip = await this.#shouldSkip(suite, suite.instance);
181
110
 
111
+ const result: SuiteResult = TestModelUtil.createSuiteResult(suite);
112
+
182
113
  if (shouldSkip) {
183
114
  this.#consumer.onEvent({
184
115
  phase: 'after', type: 'suite',
185
- suite: this.createSuiteResult(suite, {
116
+ suite: {
117
+ ...result,
186
118
  status: 'skipped',
187
119
  skipped: tests.length,
188
120
  total: tests.length
189
- })
121
+ }
190
122
  });
191
123
  }
192
124
 
@@ -194,69 +126,80 @@ export class TestExecutor {
194
126
  return;
195
127
  }
196
128
 
197
- const result: SuiteResult = this.createSuiteResult(suite);
129
+ const manager = new TestPhaseManager(suite);
130
+ const originalEnv = { ...process.env };
131
+ const startTime = Date.now();
132
+ const testResultOverrides: Record<string, Partial<TestResult>> = {};
133
+
198
134
  const validTestMethodNames = new Set(tests.map(t => t.methodName));
199
135
  const testConfigs = Object.fromEntries(
200
136
  Object.entries(suite.tests).filter(([key]) => validTestMethodNames.has(key))
201
137
  );
202
138
 
203
- const startTime = Date.now();
204
-
205
139
  // Mark suite start
206
140
  this.#consumer.onEvent({ phase: 'before', type: 'suite', suite: { ...suite, tests: testConfigs } });
207
141
 
208
- const manager = new TestPhaseManager(suite);
209
-
210
- const originalEnv = { ...process.env };
211
-
212
142
  try {
213
143
  // Handle the BeforeAll calls
214
144
  await manager.startPhase('all');
145
+ } catch (someError) {
146
+ const suiteError = await manager.onError('all', someError);
147
+ for (const method of validTestMethodNames) {
148
+ testResultOverrides[method] ??= { status: 'errored', error: suiteError };
149
+ }
150
+ }
215
151
 
216
- const suiteEnv = { ...process.env };
152
+ const suiteEnv = { ...process.env };
217
153
 
218
- for (const test of tests ?? suite.tests) {
219
- if (await this.#shouldSkip(test, suite.instance)) {
220
- this.#skipTest(test, result);
221
- continue;
222
- }
154
+ for (const test of tests) {
155
+ // Reset env before each test
156
+ process.env = { ...suiteEnv };
223
157
 
224
- // Reset env before each test
225
- process.env = { ...suiteEnv };
158
+ const testStart = Date.now();
159
+ const testResultOverride = (testResultOverrides[test.methodName] ??= {});
226
160
 
227
- const testStart = Date.now();
228
- try {
161
+ if (await this.#shouldSkip(test, suite.instance)) {
162
+ testResultOverride.status = 'skipped';
163
+ }
229
164
 
230
- // Handle BeforeEach
231
- await manager.startPhase('each');
165
+ try {
166
+ // Handle BeforeEach
167
+ testResultOverride.status || await manager.startPhase('each');
168
+ } catch (someError) {
169
+ const testError = await manager.onError('each', someError);
170
+ testResultOverride.error = testError;
171
+ testResultOverride.status = 'errored';
172
+ }
232
173
 
233
- // Run test
234
- const testResult = await this.executeTest(test, suite);
235
- result.tests[testResult.methodName] = testResult;
236
- result[testResult.status]++;
237
- result.total += 1;
174
+ // Run test
175
+ const testResult = await this.executeTest(test, suite, testResultOverride);
238
176
 
239
- // Handle after each
240
- await manager.endPhase('each');
241
- testResult.durationTotal = Date.now() - testStart;
242
- } catch (testError) {
243
- const errors = await manager.errorPhase('each', testError, suite, test);
244
- this.#recordSuiteErrors(suite, result, errors);
245
- }
177
+ // Handle after each
178
+ try {
179
+ testResultOverride.status || await manager.endPhase('each');
180
+ } catch (testError) {
181
+ if (!(testError instanceof Error)) { throw testError; };
182
+ console.error('Failed to properly shutdown test', testError.message);
246
183
  }
247
184
 
185
+ result.tests[testResult.methodName] = testResult;
186
+ testResult.duration = Date.now() - testStart;
187
+ TestModelUtil.countTestResult(result, [testResult]);
188
+ }
189
+
190
+ try {
248
191
  // Handle after all
249
192
  await manager.endPhase('all');
250
193
  } catch (suiteError) {
251
- const errors = await manager.errorPhase('all', suiteError, suite);
252
- this.#recordSuiteErrors(suite, result, errors);
194
+ if (!(suiteError instanceof Error)) { throw suiteError; };
195
+ console.error('Failed to properly shutdown test', suiteError.message);
253
196
  }
254
197
 
255
198
  // Restore env
256
199
  process.env = { ...originalEnv };
257
200
 
258
201
  result.duration = Date.now() - startTime;
259
- result.status = TestModelUtil.countsToTestStatus(result);
202
+ result.status = TestModelUtil.computeTestStatus(result);
260
203
 
261
204
  // Mark suite complete
262
205
  this.#consumer.onEvent({ phase: 'after', type: 'suite', suite: result });
@@ -265,19 +208,13 @@ export class TestExecutor {
265
208
  /**
266
209
  * Handle executing a suite's test/tests based on command line inputs
267
210
  */
268
- async execute(run: TestRun): Promise<void> {
211
+ async execute(run: TestRun, singleFile?: boolean): Promise<void> {
269
212
  try {
270
213
  await Runtime.importFrom(run.import);
271
214
  } catch (error) {
272
- if (!(error instanceof Error)) {
273
- throw error;
274
- }
215
+ if (!(error instanceof Error)) { throw error; }
216
+ const suite = TestModelUtil.createImportErrorSuiteResult(run);
275
217
  console.error(error);
276
-
277
- // Fire import failure as a test failure for each test in the suite
278
- const { result, test, suite } = AssertUtil.gernerateImportFailure(run.import, error);
279
- this.#consumer.onEvent({ type: 'suite', phase: 'before', suite });
280
- this.#onSuiteTestError(result, test);
281
218
  this.#consumer.onEvent({ type: 'suite', phase: 'after', suite });
282
219
  return;
283
220
  }
@@ -291,6 +228,11 @@ export class TestExecutor {
291
228
  console.warn('Unable to find suites for ', run);
292
229
  }
293
230
 
231
+ if (singleFile) {
232
+ const testCount = suites.reduce((acc, suite) => acc + suite.tests.length, 0);
233
+ this.#consumer.onTestRunState?.({ testCount });
234
+ }
235
+
294
236
  for (const { suite, tests } of suites) {
295
237
  await this.executeSuite(suite, tests);
296
238
  }
@@ -1,9 +1,7 @@
1
- import { describeFunction, Env, TimeUtil } from '@travetto/runtime';
1
+ import { Env, TimeUtil } from '@travetto/runtime';
2
2
 
3
3
  import type { SuiteConfig, SuitePhase } from '../model/suite.ts';
4
- import { AssertUtil } from '../assert/util.ts';
5
4
  import { Barrier } from './barrier.ts';
6
- import type { TestConfig, TestResult } from '../model/test.ts';
7
5
 
8
6
  const TEST_PHASE_TIMEOUT = TimeUtil.duration(Env.TRV_TEST_PHASE_TIMEOUT.value ?? 15000, 'ms');
9
7
 
@@ -34,9 +32,7 @@ export class TestPhaseManager {
34
32
  error = await Barrier.awaitOperation(TEST_PHASE_TIMEOUT, async () => handler[phase]?.(this.#suite.instance));
35
33
 
36
34
  if (error) {
37
- const toThrow = new Error(phase, { cause: error });
38
- Object.assign(toThrow, { import: describeFunction(handler.constructor) ?? undefined });
39
- throw toThrow;
35
+ throw error;
40
36
  }
41
37
  }
42
38
  }
@@ -58,21 +54,13 @@ export class TestPhaseManager {
58
54
  }
59
55
 
60
56
  /**
61
- * Handles if an error occurs during a phase, ensuring that we attempt to end the phase and then return the appropriate test results for the failure
57
+ * Handle an error during phase operation
62
58
  */
63
- async errorPhase(phase: 'all' | 'each', error: unknown, suite: SuiteConfig, test?: TestConfig): Promise<TestResult[]> {
64
- try { await this.endPhase(phase); } catch { }
65
- if (!(error instanceof Error)) { throw error; }
66
-
67
- // Don't propagate our own errors
68
- if (error.message === 'afterAll' || error.message === 'afterEach') {
69
- return [];
70
- }
71
-
72
- if (test) {
73
- return [AssertUtil.generateSuiteTestFailure({ suite, error, test })];
74
- } else {
75
- return AssertUtil.generateSuiteTestFailures(suite, error);
59
+ async onError(phase: 'all' | 'each', error: unknown): Promise<Error> {
60
+ if (!(error instanceof Error)) {
61
+ await this.endPhase(phase).catch(() => { });
62
+ throw error;
76
63
  }
64
+ return error;
77
65
  }
78
66
  }
@@ -222,7 +222,7 @@ export class RunUtil {
222
222
  }
223
223
 
224
224
  if (runs.length === 1) {
225
- await new TestExecutor(consumer).execute(runs[0]);
225
+ await new TestExecutor(consumer).execute(runs[0], true);
226
226
  } else {
227
227
  await WorkPool.run(
228
228
  run => buildStandardTestManager(consumer, run),
@@ -34,29 +34,35 @@ export interface SuiteConfig extends SuiteCore {
34
34
  }
35
35
 
36
36
  /**
37
- * All counts for the suite summary
37
+ * Test Counts
38
38
  */
39
- export interface Counts {
39
+ export interface ResultsSummary {
40
+ /** Passing Test Count */
40
41
  passed: number;
42
+ /** Skipped Test Count */
41
43
  skipped: number;
44
+ /** Failed Test Count */
42
45
  failed: number;
46
+ /** Errored Test Count */
43
47
  errored: number;
48
+ /** Unknown Test Count */
44
49
  unknown: number;
50
+ /** Total Test Count */
45
51
  total: number;
52
+ /** Test Self Execution Duration */
53
+ selfDuration: number;
54
+ /** Total Duration */
55
+ duration: number;
46
56
  }
47
57
 
48
58
  /**
49
59
  * Results of a suite run
50
60
  */
51
- export interface SuiteResult extends Counts, SuiteCore {
61
+ export interface SuiteResult extends ResultsSummary, SuiteCore {
52
62
  /**
53
63
  * All test results
54
64
  */
55
65
  tests: Record<string, TestResult>;
56
- /**
57
- * Suite duration
58
- */
59
- duration: number;
60
66
  /**
61
67
  * Overall status
62
68
  */
package/src/model/test.ts CHANGED
@@ -105,13 +105,13 @@ export interface TestResult extends TestCore {
105
105
  */
106
106
  assertions: Assertion[];
107
107
  /**
108
- * Duration for the test
108
+ * Self Execution Duration
109
109
  */
110
- duration: number;
110
+ selfDuration: number;
111
111
  /**
112
112
  * Total duration including before/after
113
113
  */
114
- durationTotal: number;
114
+ duration: number;
115
115
  /**
116
116
  * Logging output
117
117
  */
package/src/model/util.ts CHANGED
@@ -1,14 +1,97 @@
1
- import type { Counts } from './suite.ts';
2
- import type { TestStatus } from './test.ts';
1
+ import path from 'node:path';
2
+
3
+ import { asFull, RuntimeIndex } from '@travetto/runtime';
4
+
5
+ import type { ResultsSummary, SuiteConfig, SuiteResult } from './suite.ts';
6
+ import type { TestConfig, TestResult, TestRun, TestStatus } from './test.ts';
3
7
 
4
8
  export class TestModelUtil {
5
- static countsToTestStatus(counts: Counts): TestStatus {
9
+ static computeTestStatus(summary: ResultsSummary): TestStatus {
6
10
  switch (true) {
7
- case counts.errored > 0: return 'errored';
8
- case counts.failed > 0: return 'failed';
9
- case counts.skipped > 0: return 'skipped';
10
- case counts.unknown > 0: return 'unknown';
11
+ case summary.errored > 0: return 'errored';
12
+ case summary.failed > 0: return 'failed';
13
+ case summary.skipped > 0: return 'skipped';
14
+ case summary.unknown > 0: return 'unknown';
11
15
  default: return 'passed';
12
16
  }
13
17
  }
18
+
19
+ static buildSummary(): ResultsSummary {
20
+ return { passed: 0, failed: 0, skipped: 0, errored: 0, unknown: 0, total: 0, duration: 0, selfDuration: 0 };
21
+ }
22
+
23
+ static countTestResult<T extends ResultsSummary>(summary: T, tests: Pick<TestResult, 'status' | 'selfDuration' | 'duration'>[]): T {
24
+ for (const test of tests) {
25
+ summary[test.status] += 1;
26
+ summary.total += 1;
27
+ summary.selfDuration += (test.selfDuration ?? 0);
28
+ summary.duration += (test.duration ?? 0);
29
+ }
30
+ return summary;
31
+ }
32
+
33
+
34
+ /**
35
+ * An empty suite result based on a suite config
36
+ */
37
+ static createSuiteResult(suite: SuiteConfig, override?: Partial<SuiteResult>): SuiteResult {
38
+ return {
39
+ ...TestModelUtil.buildSummary(),
40
+ status: 'unknown',
41
+ lineStart: suite.lineStart,
42
+ lineEnd: suite.lineEnd,
43
+ import: suite.import,
44
+ classId: suite.classId,
45
+ sourceHash: suite.sourceHash,
46
+ tests: {},
47
+ duration: 0,
48
+ selfDuration: 0,
49
+ ...override
50
+ };
51
+ }
52
+
53
+ /**
54
+ * An empty test result based on a suite and test config
55
+ */
56
+ static createTestResult(suite: SuiteConfig, test: TestConfig, override?: Partial<TestResult>): TestResult {
57
+ return {
58
+ methodName: test.methodName,
59
+ description: test.description,
60
+ classId: test.classId,
61
+ tags: test.tags,
62
+ suiteLineStart: suite.lineStart,
63
+ lineStart: test.lineStart,
64
+ lineEnd: test.lineEnd,
65
+ lineBodyStart: test.lineBodyStart,
66
+ import: test.import,
67
+ declarationImport: test.declarationImport,
68
+ sourceHash: test.sourceHash,
69
+ status: 'unknown',
70
+ assertions: [],
71
+ duration: 0,
72
+ selfDuration: 0,
73
+ output: [],
74
+ ...override
75
+ };
76
+ }
77
+
78
+ static createImportErrorSuiteResult(run: TestRun): SuiteResult {
79
+ const name = path.basename(run.import);
80
+ const classId = `${RuntimeIndex.getFromImport(run.import)?.id}#${name}`;
81
+ const common = { classId, duration: 0, lineStart: 1, lineEnd: 1, import: run.import } as const;
82
+ return asFull<SuiteResult>({
83
+ ...common,
84
+ status: 'errored', errored: 1,
85
+ tests: {
86
+ impport: asFull<TestResult>({
87
+ ...common,
88
+ status: 'errored',
89
+ assertions: [{
90
+ ...common, line: common.lineStart,
91
+ methodName: 'import', operator: 'import', text: `Failed to import ${run.import}`,
92
+ }]
93
+ })
94
+ }
95
+ });
96
+ }
14
97
  }
@@ -20,9 +20,7 @@ export class TestChildWorker extends IpcChannel<TestRun> {
20
20
  await operation();
21
21
  this.send(type); // Respond
22
22
  } catch (error) {
23
- if (!(error instanceof Error)) {
24
- throw error;
25
- }
23
+ if (!(error instanceof Error)) { throw error; }
26
24
  // Mark as errored out
27
25
  this.send(type, JSONUtil.cloneForTransmit(error));
28
26
  }