@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 +7 -7
- package/src/assert/check.ts +3 -7
- package/src/assert/util.ts +14 -73
- package/src/consumer/types/cumulative.ts +6 -18
- package/src/consumer/types/delegating.ts +6 -0
- package/src/consumer/types/runnable.ts +2 -1
- package/src/consumer/types/summarizer.ts +4 -16
- package/src/consumer/types/tap-summary.ts +13 -7
- package/src/consumer/types/tap.ts +33 -37
- package/src/consumer/types.ts +6 -10
- package/src/execute/executor.ts +82 -140
- package/src/execute/phase.ts +8 -20
- package/src/execute/run.ts +1 -1
- package/src/model/suite.ts +13 -7
- package/src/model/test.ts +3 -3
- package/src/model/util.ts +90 -7
- package/src/worker/child.ts +1 -3
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@travetto/test",
|
|
3
|
-
"version": "8.0.0-alpha.
|
|
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.
|
|
32
|
-
"@travetto/runtime": "^8.0.0-alpha.
|
|
33
|
-
"@travetto/terminal": "^8.0.0-alpha.
|
|
34
|
-
"@travetto/worker": "^8.0.0-alpha.
|
|
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.
|
|
39
|
-
"@travetto/transformer": "^8.0.0-alpha.
|
|
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": {
|
package/src/assert/check.ts
CHANGED
|
@@ -178,17 +178,13 @@ export class AssertCheck {
|
|
|
178
178
|
static #onError(
|
|
179
179
|
positive: boolean,
|
|
180
180
|
message: string | undefined,
|
|
181
|
-
|
|
181
|
+
errorValue: unknown,
|
|
182
182
|
missed: Error | undefined,
|
|
183
183
|
shouldThrow: ThrowableError | undefined,
|
|
184
184
|
assertion: CapturedAssertion
|
|
185
185
|
): void {
|
|
186
|
-
|
|
187
|
-
|
|
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;
|
package/src/assert/util.ts
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
import util from 'node:util';
|
|
2
|
-
import path from 'node:path';
|
|
3
2
|
|
|
4
|
-
import {
|
|
3
|
+
import { JSONUtil, hasFunction, RuntimeIndex, Util } from '@travetto/runtime';
|
|
5
4
|
|
|
6
|
-
import type {
|
|
7
|
-
import type { SuiteConfig
|
|
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
|
|
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
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
60
|
+
return {
|
|
61
|
+
import: position?.import ?? testImport,
|
|
62
|
+
methodName: test.methodName,
|
|
63
|
+
classId: suite.classId,
|
|
64
|
+
operator: 'throw',
|
|
64
65
|
error,
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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 {
|
|
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
|
|
60
|
-
|
|
61
|
-
|
|
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
|
-
|
|
12
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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 '
|
|
130
|
-
default: status = `${this.#enhancer.
|
|
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
|
|
142
|
-
if (
|
|
143
|
-
this.logMeta({ error:
|
|
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
|
-
|
|
193
|
-
|
|
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
|
}
|
package/src/consumer/types.ts
CHANGED
|
@@ -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 {
|
|
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
|
|
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
|
*/
|
package/src/execute/executor.ts
CHANGED
|
@@ -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,
|
|
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
|
-
//
|
|
156
|
-
|
|
157
|
-
|
|
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
|
-
|
|
160
|
-
|
|
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:
|
|
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
|
|
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
|
-
|
|
152
|
+
const suiteEnv = { ...process.env };
|
|
217
153
|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
continue;
|
|
222
|
-
}
|
|
154
|
+
for (const test of tests) {
|
|
155
|
+
// Reset env before each test
|
|
156
|
+
process.env = { ...suiteEnv };
|
|
223
157
|
|
|
224
|
-
|
|
225
|
-
|
|
158
|
+
const testStart = Date.now();
|
|
159
|
+
const testResultOverride = (testResultOverrides[test.methodName] ??= {});
|
|
226
160
|
|
|
227
|
-
|
|
228
|
-
|
|
161
|
+
if (await this.#shouldSkip(test, suite.instance)) {
|
|
162
|
+
testResultOverride.status = 'skipped';
|
|
163
|
+
}
|
|
229
164
|
|
|
230
|
-
|
|
231
|
-
|
|
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
|
-
|
|
234
|
-
|
|
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
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
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
|
-
|
|
252
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
}
|
package/src/execute/phase.ts
CHANGED
|
@@ -1,9 +1,7 @@
|
|
|
1
|
-
import {
|
|
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
|
-
|
|
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
|
-
*
|
|
57
|
+
* Handle an error during phase operation
|
|
62
58
|
*/
|
|
63
|
-
async
|
|
64
|
-
|
|
65
|
-
|
|
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
|
}
|
package/src/execute/run.ts
CHANGED
|
@@ -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),
|
package/src/model/suite.ts
CHANGED
|
@@ -34,29 +34,35 @@ export interface SuiteConfig extends SuiteCore {
|
|
|
34
34
|
}
|
|
35
35
|
|
|
36
36
|
/**
|
|
37
|
-
*
|
|
37
|
+
* Test Counts
|
|
38
38
|
*/
|
|
39
|
-
export interface
|
|
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
|
|
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
|
-
*
|
|
108
|
+
* Self Execution Duration
|
|
109
109
|
*/
|
|
110
|
-
|
|
110
|
+
selfDuration: number;
|
|
111
111
|
/**
|
|
112
112
|
* Total duration including before/after
|
|
113
113
|
*/
|
|
114
|
-
|
|
114
|
+
duration: number;
|
|
115
115
|
/**
|
|
116
116
|
* Logging output
|
|
117
117
|
*/
|
package/src/model/util.ts
CHANGED
|
@@ -1,14 +1,97 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
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
|
|
9
|
+
static computeTestStatus(summary: ResultsSummary): TestStatus {
|
|
6
10
|
switch (true) {
|
|
7
|
-
case
|
|
8
|
-
case
|
|
9
|
-
case
|
|
10
|
-
case
|
|
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
|
}
|
package/src/worker/child.ts
CHANGED
|
@@ -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
|
}
|