@travetto/test 7.0.0-rc.0 → 7.0.0-rc.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -6
- package/package.json +7 -7
- package/src/assert/check.ts +46 -46
- package/src/assert/util.ts +30 -30
- package/src/consumer/registry-index.ts +4 -4
- package/src/consumer/types/cumulative.ts +10 -10
- package/src/consumer/types/delegating.ts +17 -17
- package/src/consumer/types/runnable.ts +3 -3
- package/src/consumer/types/summarizer.ts +10 -10
- package/src/consumer/types/tap-summary.ts +20 -20
- package/src/consumer/types/tap.ts +15 -15
- package/src/consumer/types/xunit.ts +15 -15
- package/src/decorator/suite.ts +2 -2
- package/src/decorator/test.ts +6 -4
- package/src/execute/barrier.ts +8 -8
- package/src/execute/console.ts +1 -1
- package/src/execute/executor.ts +11 -11
- package/src/execute/phase.ts +6 -6
- package/src/execute/runner.ts +4 -4
- package/src/execute/util.ts +11 -11
- package/src/execute/watcher.ts +16 -17
- package/src/fixture.ts +2 -2
- package/src/model/suite.ts +1 -1
- package/src/model/test.ts +1 -1
- package/src/registry/registry-adapter.ts +20 -20
- package/src/registry/registry-index.ts +16 -15
- package/src/worker/child.ts +10 -10
- package/src/worker/standard.ts +3 -3
- package/support/bin/run.ts +6 -6
- package/support/cli.test.ts +2 -2
- package/support/cli.test_digest.ts +5 -5
- package/support/cli.test_direct.ts +1 -1
- package/support/cli.test_watch.ts +2 -2
- package/support/transformer.assert.ts +12 -12
|
@@ -48,10 +48,10 @@ export class TapEmitter implements TestConsumerShape {
|
|
|
48
48
|
/**
|
|
49
49
|
* Output supplemental data (e.g. logs)
|
|
50
50
|
*/
|
|
51
|
-
logMeta(
|
|
51
|
+
logMeta(meta: Record<string, unknown>): void {
|
|
52
52
|
const lineLength = this.#terminal.width - 5;
|
|
53
|
-
let body = stringify(
|
|
54
|
-
body = body.split('\n').map(
|
|
53
|
+
let body = stringify(meta, { lineWidth: lineLength, indent: 2 });
|
|
54
|
+
body = body.split('\n').map(line => ` ${line}`).join('\n');
|
|
55
55
|
this.log(`---\n${this.#enhancer.objectInspect(body)}\n...`);
|
|
56
56
|
}
|
|
57
57
|
|
|
@@ -59,16 +59,16 @@ export class TapEmitter implements TestConsumerShape {
|
|
|
59
59
|
* Error to string
|
|
60
60
|
* @param error
|
|
61
61
|
*/
|
|
62
|
-
errorToString(
|
|
63
|
-
if (
|
|
64
|
-
if (
|
|
65
|
-
let out = JSON.stringify(hasToJSON(
|
|
66
|
-
if (this.#options?.verbose &&
|
|
67
|
-
out = `${out}\n${
|
|
62
|
+
errorToString(error?: Error): string | undefined {
|
|
63
|
+
if (error && error.name !== 'AssertionError') {
|
|
64
|
+
if (error instanceof Error) {
|
|
65
|
+
let out = JSON.stringify(hasToJSON(error) ? error.toJSON() : error, null, 2);
|
|
66
|
+
if (this.#options?.verbose && error.stack) {
|
|
67
|
+
out = `${out}\n${error.stack}`;
|
|
68
68
|
}
|
|
69
69
|
return out;
|
|
70
70
|
} else {
|
|
71
|
-
return `${
|
|
71
|
+
return `${error}`;
|
|
72
72
|
}
|
|
73
73
|
}
|
|
74
74
|
}
|
|
@@ -76,9 +76,9 @@ export class TapEmitter implements TestConsumerShape {
|
|
|
76
76
|
/**
|
|
77
77
|
* Listen for each event
|
|
78
78
|
*/
|
|
79
|
-
onEvent(
|
|
80
|
-
if (
|
|
81
|
-
const { test } =
|
|
79
|
+
onEvent(event: TestEvent): void {
|
|
80
|
+
if (event.type === 'test' && event.phase === 'after') {
|
|
81
|
+
const { test } = event;
|
|
82
82
|
const suiteId = this.#enhancer.suiteName(test.classId);
|
|
83
83
|
let header = `${suiteId} - ${this.#enhancer.testName(test.methodName)}`;
|
|
84
84
|
if (test.description) {
|
|
@@ -155,8 +155,8 @@ export class TapEmitter implements TestConsumerShape {
|
|
|
155
155
|
|
|
156
156
|
if (summary.errors.length) {
|
|
157
157
|
this.log('---\n');
|
|
158
|
-
for (const
|
|
159
|
-
const msg = this.errorToString(
|
|
158
|
+
for (const error of summary.errors) {
|
|
159
|
+
const msg = this.errorToString(error);
|
|
160
160
|
if (msg) {
|
|
161
161
|
this.log(this.#enhancer.failure(msg));
|
|
162
162
|
}
|
|
@@ -24,19 +24,19 @@ export class XunitEmitter implements TestConsumerShape {
|
|
|
24
24
|
/**
|
|
25
25
|
* Process metadata information (e.g. logs)
|
|
26
26
|
*/
|
|
27
|
-
buildMeta(
|
|
28
|
-
if (!
|
|
27
|
+
buildMeta(meta: Record<string, unknown>): string {
|
|
28
|
+
if (!meta) {
|
|
29
29
|
return '';
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
-
for (const
|
|
33
|
-
if (!
|
|
34
|
-
delete
|
|
32
|
+
for (const key of Object.keys(meta)) {
|
|
33
|
+
if (!meta[key]) {
|
|
34
|
+
delete meta[key];
|
|
35
35
|
}
|
|
36
36
|
}
|
|
37
|
-
if (Object.keys(
|
|
38
|
-
let body = stringify(
|
|
39
|
-
body = body.split('\n').map(
|
|
37
|
+
if (Object.keys(meta).length) {
|
|
38
|
+
let body = stringify(meta);
|
|
39
|
+
body = body.split('\n').map(line => ` ${line}`).join('\n');
|
|
40
40
|
return `<![CDATA[\n${body}\n]]>`;
|
|
41
41
|
} else {
|
|
42
42
|
return '';
|
|
@@ -46,10 +46,10 @@ export class XunitEmitter implements TestConsumerShape {
|
|
|
46
46
|
/**
|
|
47
47
|
* Handle each test event
|
|
48
48
|
*/
|
|
49
|
-
onEvent(
|
|
50
|
-
if (
|
|
49
|
+
onEvent(event: TestEvent): void {
|
|
50
|
+
if (event.type === 'test' && event.phase === 'after') {
|
|
51
51
|
|
|
52
|
-
const { test } =
|
|
52
|
+
const { test } = event;
|
|
53
53
|
|
|
54
54
|
let name = `${test.methodName}`;
|
|
55
55
|
if (test.description) {
|
|
@@ -59,8 +59,8 @@ export class XunitEmitter implements TestConsumerShape {
|
|
|
59
59
|
let body = '';
|
|
60
60
|
|
|
61
61
|
if (test.error) {
|
|
62
|
-
const
|
|
63
|
-
body = `<failure type="${
|
|
62
|
+
const assertion = test.assertions.find(item => !!item.error)!;
|
|
63
|
+
body = `<failure type="${assertion.text}" message="${encodeURIComponent(assertion.message!)}"><![CDATA[${assertion.error!.stack}]]></failure>`;
|
|
64
64
|
}
|
|
65
65
|
|
|
66
66
|
const groupedByLevel: Record<string, string[]> = {};
|
|
@@ -79,8 +79,8 @@ export class XunitEmitter implements TestConsumerShape {
|
|
|
79
79
|
<system-err>${this.buildMeta({ error: groupedByLevel.error, warn: groupedByLevel.warn })}</system-err>
|
|
80
80
|
</testcase>`
|
|
81
81
|
);
|
|
82
|
-
} else if (
|
|
83
|
-
const { suite } =
|
|
82
|
+
} else if (event.type === 'suite' && event.phase === 'after') {
|
|
83
|
+
const { suite } = event;
|
|
84
84
|
const testBodies = this.#tests.slice(0);
|
|
85
85
|
this.#tests = [];
|
|
86
86
|
|
package/src/decorator/suite.ts
CHANGED
|
@@ -15,7 +15,7 @@ export function Suite(): ClassDecorator;
|
|
|
15
15
|
export function Suite(...rest: Partial<SuiteConfig>[]): ClassDecorator;
|
|
16
16
|
export function Suite(description: string, ...rest: Partial<SuiteConfig>[]): ClassDecorator;
|
|
17
17
|
export function Suite(description?: string | Partial<SuiteConfig>, ...rest: Partial<SuiteConfig>[]): ClassDecorator {
|
|
18
|
-
const
|
|
18
|
+
const decorator = (cls: Class): typeof cls => {
|
|
19
19
|
const isAbstract = describeFunction(cls).abstract;
|
|
20
20
|
SuiteRegistryIndex.getForRegister(cls).register(
|
|
21
21
|
...(typeof description !== 'string' && description ? [description] : []),
|
|
@@ -26,7 +26,7 @@ export function Suite(description?: string | Partial<SuiteConfig>, ...rest: Part
|
|
|
26
26
|
return cls;
|
|
27
27
|
};
|
|
28
28
|
|
|
29
|
-
return castTo(
|
|
29
|
+
return castTo(decorator);
|
|
30
30
|
}
|
|
31
31
|
|
|
32
32
|
/**
|
package/src/decorator/test.ts
CHANGED
|
@@ -3,13 +3,15 @@ import { ClassInstance, getClass } from '@travetto/runtime';
|
|
|
3
3
|
import { TestConfig, ThrowableError } from '../model/test.ts';
|
|
4
4
|
import { SuiteRegistryIndex } from '../registry/registry-index.ts';
|
|
5
5
|
|
|
6
|
+
type MethodDecorator = (instance: ClassInstance, property: string, descriptor: PropertyDescriptor) => PropertyDescriptor | void;
|
|
7
|
+
|
|
6
8
|
/**
|
|
7
9
|
* The `@AssertCheck` indicates that a function's assert calls should be transformed
|
|
8
10
|
* @augments `@travetto/test:AssertCheck`
|
|
9
11
|
* @kind decorator
|
|
10
12
|
*/
|
|
11
13
|
export function AssertCheck(): MethodDecorator {
|
|
12
|
-
return (instance: ClassInstance, property: string
|
|
14
|
+
return (instance: ClassInstance, property: string, descriptor: PropertyDescriptor) => descriptor;
|
|
13
15
|
}
|
|
14
16
|
|
|
15
17
|
/**
|
|
@@ -24,7 +26,7 @@ export function Test(): MethodDecorator;
|
|
|
24
26
|
export function Test(...rest: Partial<TestConfig>[]): MethodDecorator;
|
|
25
27
|
export function Test(description: string, ...rest: Partial<TestConfig>[]): MethodDecorator;
|
|
26
28
|
export function Test(description?: string | Partial<TestConfig>, ...rest: Partial<TestConfig>[]): MethodDecorator {
|
|
27
|
-
return (instance: ClassInstance, property: string
|
|
29
|
+
return (instance: ClassInstance, property: string, descriptor: PropertyDescriptor) => {
|
|
28
30
|
SuiteRegistryIndex.getForRegister(getClass(instance)).registerTest(property, descriptor.value,
|
|
29
31
|
...(typeof description !== 'string' && description) ? [description] : [],
|
|
30
32
|
...rest,
|
|
@@ -40,7 +42,7 @@ export function Test(description?: string | Partial<TestConfig>, ...rest: Partia
|
|
|
40
42
|
* @kind decorator
|
|
41
43
|
*/
|
|
42
44
|
export function ShouldThrow(state: ThrowableError): MethodDecorator {
|
|
43
|
-
return (instance: ClassInstance, property: string
|
|
45
|
+
return (instance: ClassInstance, property: string, descriptor: PropertyDescriptor) => {
|
|
44
46
|
SuiteRegistryIndex.getForRegister(getClass(instance)).registerTest(property, descriptor.value, { shouldThrow: state });
|
|
45
47
|
return descriptor;
|
|
46
48
|
};
|
|
@@ -52,7 +54,7 @@ export function ShouldThrow(state: ThrowableError): MethodDecorator {
|
|
|
52
54
|
* @kind decorator
|
|
53
55
|
*/
|
|
54
56
|
export function Timeout(ms: number): MethodDecorator {
|
|
55
|
-
return (instance: ClassInstance, property: string
|
|
57
|
+
return (instance: ClassInstance, property: string, descriptor: PropertyDescriptor) => {
|
|
56
58
|
SuiteRegistryIndex.getForRegister(getClass(instance)).registerTest(property, descriptor.value, { timeout: ms });
|
|
57
59
|
return descriptor;
|
|
58
60
|
};
|
package/src/execute/barrier.ts
CHANGED
|
@@ -11,14 +11,14 @@ export class Barrier {
|
|
|
11
11
|
/**
|
|
12
12
|
* Track timeout
|
|
13
13
|
*/
|
|
14
|
-
static timeout(duration: number | TimeSpan,
|
|
14
|
+
static timeout(duration: number | TimeSpan, operation: string = 'Operation'): { promise: Promise<void>, resolve: () => unknown } {
|
|
15
15
|
const resolver = Promise.withResolvers<void>();
|
|
16
16
|
const durationMs = TimeUtil.asMillis(duration);
|
|
17
17
|
let timeout: NodeJS.Timeout;
|
|
18
18
|
if (!durationMs) {
|
|
19
19
|
resolver.resolve();
|
|
20
20
|
} else {
|
|
21
|
-
const msg = `${
|
|
21
|
+
const msg = `${operation} timed out after ${duration}${typeof duration === 'number' ? 'ms' : ''}`;
|
|
22
22
|
timeout = setTimeout(() => resolver.reject(new TimeoutError(msg)), durationMs).unref();
|
|
23
23
|
}
|
|
24
24
|
|
|
@@ -31,9 +31,9 @@ export class Barrier {
|
|
|
31
31
|
*/
|
|
32
32
|
static uncaughtErrorPromise(): { promise: Promise<void>, resolve: () => unknown } {
|
|
33
33
|
const uncaught = Promise.withResolvers<void>();
|
|
34
|
-
const onError = (
|
|
35
|
-
UNCAUGHT_ERR_EVENTS.map(
|
|
36
|
-
uncaught.promise.finally(() => { UNCAUGHT_ERR_EVENTS.map(
|
|
34
|
+
const onError = (error: Error): void => { Util.queueMacroTask().then(() => uncaught.reject(error)); };
|
|
35
|
+
UNCAUGHT_ERR_EVENTS.map(key => process.on(key, onError));
|
|
36
|
+
uncaught.promise.finally(() => { UNCAUGHT_ERR_EVENTS.map(key => process.off(key, onError)); });
|
|
37
37
|
return uncaught;
|
|
38
38
|
}
|
|
39
39
|
|
|
@@ -80,7 +80,7 @@ export class Barrier {
|
|
|
80
80
|
/**
|
|
81
81
|
* Wait for operation to finish, with timeout and unhandled error support
|
|
82
82
|
*/
|
|
83
|
-
static async awaitOperation(timeout: number | TimeSpan,
|
|
83
|
+
static async awaitOperation(timeout: number | TimeSpan, operation: () => Promise<unknown>): Promise<Error | undefined> {
|
|
84
84
|
const uncaught = this.uncaughtErrorPromise();
|
|
85
85
|
const timer = this.timeout(timeout);
|
|
86
86
|
const promises = this.capturePromises();
|
|
@@ -88,9 +88,9 @@ export class Barrier {
|
|
|
88
88
|
try {
|
|
89
89
|
await promises.start();
|
|
90
90
|
let capturedError: Error | undefined;
|
|
91
|
-
const opProm =
|
|
91
|
+
const opProm = operation().then(() => promises.finish());
|
|
92
92
|
|
|
93
|
-
await Promise.race([opProm, uncaught.promise, timer.promise]).catch(
|
|
93
|
+
await Promise.race([opProm, uncaught.promise, timer.promise]).catch(error => capturedError ??= error);
|
|
94
94
|
|
|
95
95
|
return capturedError;
|
|
96
96
|
} finally {
|
package/src/execute/console.ts
CHANGED
|
@@ -22,7 +22,7 @@ export class ConsoleCapture implements ConsoleListener {
|
|
|
22
22
|
this.out.push({
|
|
23
23
|
...rest,
|
|
24
24
|
message: args
|
|
25
|
-
.map((
|
|
25
|
+
.map((arg => typeof arg === 'string' ? arg : util.inspect(arg, false, 5)))
|
|
26
26
|
.join(' ')
|
|
27
27
|
});
|
|
28
28
|
}
|
package/src/execute/executor.ts
CHANGED
|
@@ -15,7 +15,7 @@ import { Barrier } from './barrier.ts';
|
|
|
15
15
|
import { ExecutionError } from './error.ts';
|
|
16
16
|
import { SuiteRegistryIndex } from '../registry/registry-index.ts';
|
|
17
17
|
|
|
18
|
-
const TEST_TIMEOUT = TimeUtil.fromValue(Env.TRV_TEST_TIMEOUT.
|
|
18
|
+
const TEST_TIMEOUT = TimeUtil.fromValue(Env.TRV_TEST_TIMEOUT.value) ?? 5000;
|
|
19
19
|
|
|
20
20
|
/**
|
|
21
21
|
* Support execution of the tests
|
|
@@ -73,8 +73,8 @@ export class TestExecutor {
|
|
|
73
73
|
/**
|
|
74
74
|
* Determining if we should skip
|
|
75
75
|
*/
|
|
76
|
-
async #shouldSkip(
|
|
77
|
-
if (typeof
|
|
76
|
+
async #shouldSkip(config: TestConfig | SuiteConfig, inst: unknown): Promise<boolean | undefined> {
|
|
77
|
+
if (typeof config.skip === 'function' ? await config.skip(inst) : config.skip) {
|
|
78
78
|
return true;
|
|
79
79
|
}
|
|
80
80
|
}
|
|
@@ -190,7 +190,7 @@ export class TestExecutor {
|
|
|
190
190
|
// Mark suite start
|
|
191
191
|
this.#consumer.onEvent({ phase: 'before', type: 'suite', suite });
|
|
192
192
|
|
|
193
|
-
const mgr = new TestPhaseManager(suite, result,
|
|
193
|
+
const mgr = new TestPhaseManager(suite, result, event => this.#onSuiteFailure(event));
|
|
194
194
|
|
|
195
195
|
const originalEnv = { ...process.env };
|
|
196
196
|
|
|
@@ -226,8 +226,8 @@ export class TestExecutor {
|
|
|
226
226
|
|
|
227
227
|
// Handle after all
|
|
228
228
|
await mgr.endPhase('all');
|
|
229
|
-
} catch (
|
|
230
|
-
await mgr.onError(
|
|
229
|
+
} catch (error) {
|
|
230
|
+
await mgr.onError(error);
|
|
231
231
|
}
|
|
232
232
|
|
|
233
233
|
// Restore env
|
|
@@ -246,12 +246,12 @@ export class TestExecutor {
|
|
|
246
246
|
async execute(run: TestRun): Promise<void> {
|
|
247
247
|
try {
|
|
248
248
|
await Runtime.importFrom(run.import);
|
|
249
|
-
} catch (
|
|
250
|
-
if (!(
|
|
251
|
-
throw
|
|
249
|
+
} catch (error) {
|
|
250
|
+
if (!(error instanceof Error)) {
|
|
251
|
+
throw error;
|
|
252
252
|
}
|
|
253
|
-
console.error(
|
|
254
|
-
this.#onSuiteFailure(AssertUtil.gernerateImportFailure(run.import,
|
|
253
|
+
console.error(error);
|
|
254
|
+
this.#onSuiteFailure(AssertUtil.gernerateImportFailure(run.import, error));
|
|
255
255
|
return;
|
|
256
256
|
}
|
|
257
257
|
|
package/src/execute/phase.ts
CHANGED
|
@@ -8,7 +8,7 @@ class TestBreakout extends Error {
|
|
|
8
8
|
source?: Error;
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
-
const TEST_PHASE_TIMEOUT = TimeUtil.fromValue(Env.TRV_TEST_PHASE_TIMEOUT.
|
|
11
|
+
const TEST_PHASE_TIMEOUT = TimeUtil.fromValue(Env.TRV_TEST_PHASE_TIMEOUT.value) ?? 15000;
|
|
12
12
|
|
|
13
13
|
/**
|
|
14
14
|
* Test Phase Execution Manager.
|
|
@@ -67,9 +67,9 @@ export class TestPhaseManager {
|
|
|
67
67
|
/**
|
|
68
68
|
* On error, handle stubbing out error for the phases in progress
|
|
69
69
|
*/
|
|
70
|
-
async onError(
|
|
71
|
-
if (!(
|
|
72
|
-
throw
|
|
70
|
+
async onError(error: Error | unknown): Promise<void> {
|
|
71
|
+
if (!(error instanceof Error)) {
|
|
72
|
+
throw error;
|
|
73
73
|
}
|
|
74
74
|
|
|
75
75
|
for (const ph of this.#progress) {
|
|
@@ -82,8 +82,8 @@ export class TestPhaseManager {
|
|
|
82
82
|
|
|
83
83
|
const failure = AssertUtil.generateSuiteFailure(
|
|
84
84
|
this.#suite,
|
|
85
|
-
|
|
86
|
-
|
|
85
|
+
error instanceof TestBreakout ? error.message : 'all',
|
|
86
|
+
error instanceof TestBreakout ? error.source! : error
|
|
87
87
|
);
|
|
88
88
|
|
|
89
89
|
this.#onSuiteFailure(failure);
|
package/src/execute/runner.ts
CHANGED
|
@@ -35,7 +35,7 @@ export class Runner {
|
|
|
35
35
|
|
|
36
36
|
await consumer.onStart({ testCount: tests.length });
|
|
37
37
|
await WorkPool.run(
|
|
38
|
-
|
|
38
|
+
run => buildStandardTestManager(consumer, run),
|
|
39
39
|
testRuns,
|
|
40
40
|
{
|
|
41
41
|
idleTimeoutMillis: TimeUtil.asMillis(10, 's'),
|
|
@@ -63,10 +63,10 @@ export class Runner {
|
|
|
63
63
|
const target = await TestConsumerRegistryIndex.getInstance(this.#state);
|
|
64
64
|
|
|
65
65
|
const consumer = new RunnableTestConsumer(target)
|
|
66
|
-
.withTransformer(
|
|
66
|
+
.withTransformer(event => {
|
|
67
67
|
// Copy run metadata to event
|
|
68
|
-
|
|
69
|
-
return
|
|
68
|
+
event.metadata = run.metadata;
|
|
69
|
+
return event;
|
|
70
70
|
});
|
|
71
71
|
|
|
72
72
|
await consumer.onStart({});
|
package/src/execute/util.ts
CHANGED
|
@@ -40,17 +40,17 @@ export class RunnerUtil {
|
|
|
40
40
|
*/
|
|
41
41
|
static async* getTestImports(globs?: string[]): AsyncIterable<string> {
|
|
42
42
|
const all = RuntimeIndex.find({
|
|
43
|
-
module:
|
|
44
|
-
folder:
|
|
45
|
-
file:
|
|
43
|
+
module: mod => mod.roles.includes('test') || mod.roles.includes('std'),
|
|
44
|
+
folder: folder => folder === 'test',
|
|
45
|
+
file: file => file.role === 'test'
|
|
46
46
|
});
|
|
47
47
|
|
|
48
48
|
// Collect globs
|
|
49
49
|
if (globs?.length) {
|
|
50
|
-
const allFiles = new Map(all.map(
|
|
50
|
+
const allFiles = new Map(all.map(file => [file.sourceFile, file]));
|
|
51
51
|
for await (const item of fs.glob(globs)) {
|
|
52
|
-
const
|
|
53
|
-
const match = allFiles.get(
|
|
52
|
+
const source = Runtime.workspaceRelative(path.resolve(item));
|
|
53
|
+
const match = allFiles.get(source);
|
|
54
54
|
if (match && await this.isTestFile(match.sourceFile)) {
|
|
55
55
|
yield match.import;
|
|
56
56
|
}
|
|
@@ -96,12 +96,12 @@ export class RunnerUtil {
|
|
|
96
96
|
* Get run events
|
|
97
97
|
*/
|
|
98
98
|
static getTestRuns(tests: TestConfig[]): TestRun[] {
|
|
99
|
-
const events = tests.reduce((
|
|
100
|
-
if (!
|
|
101
|
-
|
|
99
|
+
const events = tests.reduce((runs, test) => {
|
|
100
|
+
if (!runs.has(test.classId)) {
|
|
101
|
+
runs.set(test.classId, { import: test.import, classId: test.classId, methodNames: [], runId: Util.uuid() });
|
|
102
102
|
}
|
|
103
|
-
|
|
104
|
-
return
|
|
103
|
+
runs.get(test.classId)!.methodNames!.push(test.methodName);
|
|
104
|
+
return runs;
|
|
105
105
|
}, new Map<string, TestRun>());
|
|
106
106
|
return [...events.values()];
|
|
107
107
|
}
|
package/src/execute/watcher.ts
CHANGED
|
@@ -32,15 +32,14 @@ export class TestWatcher {
|
|
|
32
32
|
events.push(...RunnerUtil.getTestRuns(tests));
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
-
const
|
|
35
|
+
const queue = new AsyncQueue(events);
|
|
36
36
|
const consumer = new CumulativeSummaryConsumer(
|
|
37
37
|
await TestConsumerRegistryIndex.getInstance({ consumer: format })
|
|
38
38
|
)
|
|
39
|
-
.withFilter(
|
|
39
|
+
.withFilter(event => event.metadata?.partial !== true || event.type !== 'suite');
|
|
40
40
|
|
|
41
41
|
Registry.onMethodChange((event) => {
|
|
42
|
-
const [cls, method] =
|
|
43
|
-
('curr' in event && event.curr ? event.curr : []);
|
|
42
|
+
const [cls, method] = 'previous' in event ? event.previous : event.current;
|
|
44
43
|
|
|
45
44
|
if (!cls || describeFunction(cls).abstract) {
|
|
46
45
|
return;
|
|
@@ -52,14 +51,14 @@ export class TestWatcher {
|
|
|
52
51
|
return;
|
|
53
52
|
}
|
|
54
53
|
|
|
55
|
-
const
|
|
54
|
+
const config = SuiteRegistryIndex.getTestConfig(cls, method)!;
|
|
56
55
|
if (event.type !== 'removing') {
|
|
57
|
-
if (
|
|
56
|
+
if (config) {
|
|
58
57
|
const run: TestRun = {
|
|
59
|
-
import:
|
|
58
|
+
import: config.import, classId: config.classId, methodNames: [config.methodName], metadata: { partial: true }
|
|
60
59
|
};
|
|
61
60
|
console.log('Triggering', run);
|
|
62
|
-
|
|
61
|
+
queue.add(run, true); // Shift to front
|
|
63
62
|
}
|
|
64
63
|
} else {
|
|
65
64
|
process.send?.({
|
|
@@ -73,17 +72,17 @@ export class TestWatcher {
|
|
|
73
72
|
}, SuiteRegistryIndex);
|
|
74
73
|
|
|
75
74
|
// If a file is changed, but doesn't emit classes, re-run whole file
|
|
76
|
-
Registry.onNonClassChanges(imp =>
|
|
75
|
+
Registry.onNonClassChanges(imp => queue.add({ import: imp }));
|
|
77
76
|
|
|
78
|
-
process.on('message',
|
|
79
|
-
if (typeof
|
|
80
|
-
console.log('Received message',
|
|
77
|
+
process.on('message', event => {
|
|
78
|
+
if (typeof event === 'object' && event && 'type' in event && event.type === 'run-test') {
|
|
79
|
+
console.log('Received message', event);
|
|
81
80
|
// Legacy
|
|
82
|
-
if ('file' in
|
|
83
|
-
|
|
81
|
+
if ('file' in event && typeof event.file === 'string') {
|
|
82
|
+
event = { import: RuntimeIndex.getFromSource(event.file)?.import! };
|
|
84
83
|
}
|
|
85
|
-
console.debug('Manually triggered',
|
|
86
|
-
|
|
84
|
+
console.debug('Manually triggered', event);
|
|
85
|
+
queue.add(castTo(event), true);
|
|
87
86
|
}
|
|
88
87
|
});
|
|
89
88
|
|
|
@@ -91,7 +90,7 @@ export class TestWatcher {
|
|
|
91
90
|
|
|
92
91
|
await WorkPool.run(
|
|
93
92
|
buildStandardTestManager.bind(null, consumer),
|
|
94
|
-
|
|
93
|
+
queue,
|
|
95
94
|
{
|
|
96
95
|
idleTimeoutMillis: 120000,
|
|
97
96
|
min: 2,
|
package/src/fixture.ts
CHANGED
|
@@ -5,8 +5,8 @@ export class TestFixtures extends FileLoader {
|
|
|
5
5
|
super([
|
|
6
6
|
'@#test/fixtures',
|
|
7
7
|
'@#support/fixtures',
|
|
8
|
-
...modules.flat().map(
|
|
8
|
+
...modules.flat().map(mod => `${mod}#support/fixtures`),
|
|
9
9
|
'@@#support/fixtures'
|
|
10
|
-
].map(
|
|
10
|
+
].map(value => Runtime.modulePath(value)));
|
|
11
11
|
}
|
|
12
12
|
}
|
package/src/model/suite.ts
CHANGED
package/src/model/test.ts
CHANGED
|
@@ -2,7 +2,7 @@ import type { Class, ConsoleEvent, TimeSpan } from '@travetto/runtime';
|
|
|
2
2
|
|
|
3
3
|
import { Skip, TestCore } from './common.ts';
|
|
4
4
|
|
|
5
|
-
export type ThrowableError = string | RegExp | Class<Error> | ((
|
|
5
|
+
export type ThrowableError = string | RegExp | Class<Error> | ((error: Error | string) => boolean | void | undefined);
|
|
6
6
|
export type TestLog = Omit<ConsoleEvent, 'args' | 'scope'> & { message: string };
|
|
7
7
|
|
|
8
8
|
/**
|
|
@@ -6,24 +6,24 @@ import { SuiteConfig } from '../model/suite';
|
|
|
6
6
|
import { TestConfig } from '../model/test';
|
|
7
7
|
|
|
8
8
|
function combineClasses(baseConfig: SuiteConfig, ...subConfig: Partial<SuiteConfig>[]): SuiteConfig {
|
|
9
|
-
for (const
|
|
10
|
-
if (
|
|
11
|
-
baseConfig.beforeAll = [...baseConfig.beforeAll, ...
|
|
9
|
+
for (const config of subConfig) {
|
|
10
|
+
if (config.beforeAll) {
|
|
11
|
+
baseConfig.beforeAll = [...baseConfig.beforeAll, ...config.beforeAll];
|
|
12
12
|
}
|
|
13
|
-
if (
|
|
14
|
-
baseConfig.beforeEach = [...baseConfig.beforeEach, ...
|
|
13
|
+
if (config.beforeEach) {
|
|
14
|
+
baseConfig.beforeEach = [...baseConfig.beforeEach, ...config.beforeEach];
|
|
15
15
|
}
|
|
16
|
-
if (
|
|
17
|
-
baseConfig.afterAll = [...baseConfig.afterAll, ...
|
|
16
|
+
if (config.afterAll) {
|
|
17
|
+
baseConfig.afterAll = [...baseConfig.afterAll, ...config.afterAll];
|
|
18
18
|
}
|
|
19
|
-
if (
|
|
20
|
-
baseConfig.afterEach = [...baseConfig.afterEach, ...
|
|
19
|
+
if (config.afterEach) {
|
|
20
|
+
baseConfig.afterEach = [...baseConfig.afterEach, ...config.afterEach];
|
|
21
21
|
}
|
|
22
|
-
if (
|
|
23
|
-
baseConfig.tags = [...baseConfig.tags ?? [], ...
|
|
22
|
+
if (config.tags) {
|
|
23
|
+
baseConfig.tags = [...baseConfig.tags ?? [], ...config.tags];
|
|
24
24
|
}
|
|
25
|
-
if (
|
|
26
|
-
for (const [key, test] of Object.entries(
|
|
25
|
+
if (config.tests) {
|
|
26
|
+
for (const [key, test] of Object.entries(config.tests ?? {})) {
|
|
27
27
|
baseConfig.tests[key] = {
|
|
28
28
|
...test,
|
|
29
29
|
sourceImport: Runtime.getImport(baseConfig.class),
|
|
@@ -40,11 +40,11 @@ function combineClasses(baseConfig: SuiteConfig, ...subConfig: Partial<SuiteConf
|
|
|
40
40
|
function combineMethods(suite: SuiteConfig, baseConfig: TestConfig, ...subConfig: Partial<TestConfig>[]): TestConfig {
|
|
41
41
|
baseConfig.classId = suite.classId;
|
|
42
42
|
baseConfig.import = suite.import;
|
|
43
|
-
for (const
|
|
44
|
-
safeAssign(baseConfig,
|
|
43
|
+
for (const config of subConfig) {
|
|
44
|
+
safeAssign(baseConfig, config, {
|
|
45
45
|
tags: [
|
|
46
46
|
...baseConfig.tags ?? [],
|
|
47
|
-
...
|
|
47
|
+
...config.tags ?? []
|
|
48
48
|
]
|
|
49
49
|
});
|
|
50
50
|
}
|
|
@@ -80,7 +80,7 @@ export class SuiteRegistryAdapter implements RegistryAdapter<SuiteConfig> {
|
|
|
80
80
|
return this.#config;
|
|
81
81
|
}
|
|
82
82
|
|
|
83
|
-
registerTest(method: string
|
|
83
|
+
registerTest(method: string, ...data: Partial<TestConfig>[]): TestConfig {
|
|
84
84
|
const suite = this.register();
|
|
85
85
|
|
|
86
86
|
if (!(method in this.#config.tests)) {
|
|
@@ -92,7 +92,7 @@ export class SuiteRegistryAdapter implements RegistryAdapter<SuiteConfig> {
|
|
|
92
92
|
lineStart: lines?.[0],
|
|
93
93
|
lineEnd: lines?.[1],
|
|
94
94
|
lineBodyStart: lines?.[2],
|
|
95
|
-
methodName: method
|
|
95
|
+
methodName: method,
|
|
96
96
|
});
|
|
97
97
|
this.#config.tests[method] = config;
|
|
98
98
|
}
|
|
@@ -109,7 +109,7 @@ export class SuiteRegistryAdapter implements RegistryAdapter<SuiteConfig> {
|
|
|
109
109
|
|
|
110
110
|
for (const test of Object.values(this.#config.tests)) {
|
|
111
111
|
test.tags = [...test.tags ?? [], ...this.#config.tags ?? []];
|
|
112
|
-
test.description ||= SchemaRegistryIndex.
|
|
112
|
+
test.description ||= SchemaRegistryIndex.get(this.#cls).getMethod(test.methodName).description;
|
|
113
113
|
}
|
|
114
114
|
}
|
|
115
115
|
|
|
@@ -117,7 +117,7 @@ export class SuiteRegistryAdapter implements RegistryAdapter<SuiteConfig> {
|
|
|
117
117
|
return this.#config;
|
|
118
118
|
}
|
|
119
119
|
|
|
120
|
-
getMethod(method: string
|
|
120
|
+
getMethod(method: string): TestConfig {
|
|
121
121
|
const test = this.#config.tests[method];
|
|
122
122
|
if (!test) {
|
|
123
123
|
throw new AppError(`Test not registered: ${String(method)} on ${this.#cls.name}`);
|