@travetto/test 5.0.0 → 5.0.1
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/util.ts +2 -4
- package/src/execute/barrier.ts +100 -0
- package/src/execute/error.ts +11 -0
- package/src/execute/executor.ts +15 -26
- package/src/execute/phase.ts +2 -4
- package/src/execute/runner.ts +2 -6
- package/src/registry/suite.ts +1 -1
- package/src/execute/promise.ts +0 -49
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@travetto/test",
|
|
3
|
-
"version": "5.0.
|
|
3
|
+
"version": "5.0.1",
|
|
4
4
|
"description": "Declarative test framework",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"unit-testing",
|
|
@@ -27,15 +27,15 @@
|
|
|
27
27
|
"directory": "module/test"
|
|
28
28
|
},
|
|
29
29
|
"dependencies": {
|
|
30
|
-
"@travetto/registry": "^5.0.
|
|
31
|
-
"@travetto/runtime": "^5.0.
|
|
32
|
-
"@travetto/terminal": "^5.0.
|
|
33
|
-
"@travetto/worker": "^5.0.
|
|
30
|
+
"@travetto/registry": "^5.0.1",
|
|
31
|
+
"@travetto/runtime": "^5.0.1",
|
|
32
|
+
"@travetto/terminal": "^5.0.1",
|
|
33
|
+
"@travetto/worker": "^5.0.1",
|
|
34
34
|
"yaml": "^2.5.0"
|
|
35
35
|
},
|
|
36
36
|
"peerDependencies": {
|
|
37
|
-
"@travetto/cli": "^5.0.
|
|
38
|
-
"@travetto/transformer": "^5.0.
|
|
37
|
+
"@travetto/cli": "^5.0.1",
|
|
38
|
+
"@travetto/transformer": "^5.0.1"
|
|
39
39
|
},
|
|
40
40
|
"peerDependenciesMeta": {
|
|
41
41
|
"@travetto/transformer": {
|
package/src/assert/util.ts
CHANGED
|
@@ -1,14 +1,12 @@
|
|
|
1
1
|
import util from 'node:util';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
|
|
4
|
-
import { asFull, Class, Runtime, RuntimeIndex } from '@travetto/runtime';
|
|
4
|
+
import { asFull, Class, hasFunction, Runtime, RuntimeIndex } from '@travetto/runtime';
|
|
5
5
|
|
|
6
6
|
import { TestConfig, Assertion, TestResult } from '../model/test';
|
|
7
7
|
import { SuiteConfig, SuiteFailure, SuiteResult } from '../model/suite';
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
return !!o && typeof o === 'object' && 'toClean' in o && typeof o.toClean === 'function';
|
|
11
|
-
}
|
|
9
|
+
const isCleanable = hasFunction<{ toClean(): unknown }>('toClean');
|
|
12
10
|
|
|
13
11
|
/**
|
|
14
12
|
* Assertion utilities
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { isPromise } from 'node:util/types';
|
|
2
|
+
import { createHook, executionAsyncId } from 'node:async_hooks';
|
|
3
|
+
import { TimeSpan, TimeUtil, Util } from '@travetto/runtime';
|
|
4
|
+
import { ExecutionError, TimeoutError } from './error';
|
|
5
|
+
|
|
6
|
+
const UNCAUGHT_ERR_EVENTS = ['unhandledRejection', 'uncaughtException'] as const;
|
|
7
|
+
|
|
8
|
+
export class Barrier {
|
|
9
|
+
/**
|
|
10
|
+
* Track timeout
|
|
11
|
+
*/
|
|
12
|
+
static timeout(duration: number | TimeSpan, op: string = 'Operation'): { promise: Promise<void>, resolve: () => unknown } {
|
|
13
|
+
const resolver = Util.resolvablePromise();
|
|
14
|
+
const durationMs = TimeUtil.asMillis(duration);
|
|
15
|
+
let timeout: NodeJS.Timeout;
|
|
16
|
+
if (!durationMs) {
|
|
17
|
+
resolver.resolve();
|
|
18
|
+
} else {
|
|
19
|
+
const msg = `${op} timed out after ${duration}${typeof duration === 'number' ? 'ms' : ''}`;
|
|
20
|
+
timeout = setTimeout(() => resolver.reject(new TimeoutError(msg)), durationMs).unref();
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
resolver.promise.finally(() => { clearTimeout(timeout); });
|
|
24
|
+
return resolver;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Track uncaught error
|
|
29
|
+
*/
|
|
30
|
+
static uncaughtErrorPromise(): { promise: Promise<void>, resolve: () => unknown } {
|
|
31
|
+
const uncaught = Util.resolvablePromise<void>();
|
|
32
|
+
const onError = (err: Error): void => { Util.queueMacroTask().then(() => uncaught.reject(err)); };
|
|
33
|
+
UNCAUGHT_ERR_EVENTS.map(k => process.on(k, onError));
|
|
34
|
+
uncaught.promise.finally(() => { UNCAUGHT_ERR_EVENTS.map(k => process.off(k, onError)); });
|
|
35
|
+
return uncaught;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Promise capturer
|
|
40
|
+
*/
|
|
41
|
+
static capturePromises(): { start: () => Promise<void>, finish: () => Promise<void>, cleanup: () => void } {
|
|
42
|
+
const pending = new Map<number, Promise<unknown>>();
|
|
43
|
+
let id: number = 0;
|
|
44
|
+
|
|
45
|
+
const hook = createHook({
|
|
46
|
+
init(asyncId, type, triggerAsyncId, resource) {
|
|
47
|
+
if (id && type === 'PROMISE' && triggerAsyncId === id && isPromise(resource)) {
|
|
48
|
+
pending.set(id, resource);
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
promiseResolve(asyncId: number): void {
|
|
52
|
+
pending.delete(asyncId);
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
return {
|
|
57
|
+
async start(): Promise<void> {
|
|
58
|
+
hook.enable();
|
|
59
|
+
await Util.queueMacroTask();
|
|
60
|
+
id = executionAsyncId();
|
|
61
|
+
},
|
|
62
|
+
async finish(maxTaskCount = 5): Promise<void> {
|
|
63
|
+
let i = maxTaskCount; // Wait upto 5 macro tasks before continuing
|
|
64
|
+
while (pending.size) {
|
|
65
|
+
await Util.queueMacroTask();
|
|
66
|
+
i -= 1;
|
|
67
|
+
if (i === 0) {
|
|
68
|
+
throw new ExecutionError(`Pending promises: ${pending.size}`);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
cleanup(): void {
|
|
73
|
+
hook.disable();
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Wait for operation to finish, with timeout and unhandled error support
|
|
80
|
+
*/
|
|
81
|
+
static async awaitOperation(timeout: number | TimeSpan, op: () => Promise<unknown>): Promise<Error | undefined> {
|
|
82
|
+
const uncaught = this.uncaughtErrorPromise();
|
|
83
|
+
const timer = this.timeout(timeout);
|
|
84
|
+
const promises = this.capturePromises();
|
|
85
|
+
|
|
86
|
+
try {
|
|
87
|
+
await promises.start();
|
|
88
|
+
let capturedError: Error | undefined;
|
|
89
|
+
const opProm = op().then(() => promises.finish());
|
|
90
|
+
|
|
91
|
+
await Promise.race([opProm, uncaught.promise, timer.promise]).catch(err => capturedError ??= err);
|
|
92
|
+
|
|
93
|
+
return capturedError;
|
|
94
|
+
} finally {
|
|
95
|
+
promises.cleanup();
|
|
96
|
+
timer.resolve();
|
|
97
|
+
uncaught.resolve();
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
package/src/execute/executor.ts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { AssertionError } from 'node:assert';
|
|
2
2
|
|
|
3
3
|
import { Env, TimeUtil, Runtime, castTo } from '@travetto/runtime';
|
|
4
|
-
import { Barrier, ExecutionError } from '@travetto/worker';
|
|
5
4
|
|
|
6
5
|
import { SuiteRegistry } from '../registry/suite';
|
|
7
6
|
import { TestConfig, TestResult, TestRun } from '../model/test';
|
|
@@ -11,8 +10,9 @@ import { AssertCheck } from '../assert/check';
|
|
|
11
10
|
import { AssertCapture } from '../assert/capture';
|
|
12
11
|
import { ConsoleCapture } from './console';
|
|
13
12
|
import { TestPhaseManager } from './phase';
|
|
14
|
-
import { PromiseCapturer } from './promise';
|
|
15
13
|
import { AssertUtil } from '../assert/util';
|
|
14
|
+
import { Barrier } from './barrier';
|
|
15
|
+
import { ExecutionError } from './error';
|
|
16
16
|
|
|
17
17
|
const TEST_TIMEOUT = TimeUtil.fromValue(Env.TRV_TEST_TIMEOUT.val) ?? 5000;
|
|
18
18
|
|
|
@@ -22,11 +22,9 @@ const TEST_TIMEOUT = TimeUtil.fromValue(Env.TRV_TEST_TIMEOUT.val) ?? 5000;
|
|
|
22
22
|
export class TestExecutor {
|
|
23
23
|
|
|
24
24
|
#consumer: TestConsumer;
|
|
25
|
-
#testFilter: (config: TestConfig) => boolean;
|
|
26
25
|
|
|
27
|
-
constructor(consumer: TestConsumer
|
|
26
|
+
constructor(consumer: TestConsumer) {
|
|
28
27
|
this.#consumer = consumer;
|
|
29
|
-
this.#testFilter = testFilter || ((): boolean => true);
|
|
30
28
|
}
|
|
31
29
|
|
|
32
30
|
/**
|
|
@@ -60,33 +58,21 @@ export class TestExecutor {
|
|
|
60
58
|
const suite = SuiteRegistry.get(test.class);
|
|
61
59
|
|
|
62
60
|
// Ensure all the criteria below are satisfied before moving forward
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
);
|
|
73
|
-
} finally {
|
|
74
|
-
process.env = env; // Restore
|
|
75
|
-
}
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
// Wait for all barriers to be satisfied
|
|
79
|
-
return barrier.wait();
|
|
61
|
+
return Barrier.awaitOperation(test.timeout || TEST_TIMEOUT, async () => {
|
|
62
|
+
const env = process.env;
|
|
63
|
+
process.env = { ...env }; // Created an isolated environment
|
|
64
|
+
try {
|
|
65
|
+
await castTo<Record<string, Function>>(suite.instance)[test.methodName]();
|
|
66
|
+
} finally {
|
|
67
|
+
process.env = env; // Restore
|
|
68
|
+
}
|
|
69
|
+
});
|
|
80
70
|
}
|
|
81
71
|
|
|
82
72
|
/**
|
|
83
73
|
* Determining if we should skip
|
|
84
74
|
*/
|
|
85
75
|
async #shouldSkip(cfg: TestConfig | SuiteConfig, inst: unknown): Promise<boolean | undefined> {
|
|
86
|
-
if ('methodName' in cfg && !this.#testFilter(cfg)) {
|
|
87
|
-
return true;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
76
|
if (typeof cfg.skip === 'function' ? await cfg.skip(inst) : cfg.skip) {
|
|
91
77
|
return true;
|
|
92
78
|
}
|
|
@@ -269,6 +255,9 @@ export class TestExecutor {
|
|
|
269
255
|
|
|
270
256
|
// Convert inbound arguments to specific tests to run
|
|
271
257
|
const suites = SuiteRegistry.getSuiteTests(run);
|
|
258
|
+
if (!suites.length) {
|
|
259
|
+
console.warn('Unable to find suites for ', run);
|
|
260
|
+
}
|
|
272
261
|
|
|
273
262
|
for (const { suite, tests } of suites) {
|
|
274
263
|
await this.executeSuite(suite, tests);
|
package/src/execute/phase.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { Barrier } from '@travetto/worker';
|
|
2
1
|
import { Env, TimeUtil } from '@travetto/runtime';
|
|
3
2
|
|
|
4
3
|
import { SuiteConfig, SuiteFailure, SuiteResult } from '../model/suite';
|
|
5
4
|
import { AssertUtil } from '../assert/util';
|
|
5
|
+
import { Barrier } from './barrier';
|
|
6
6
|
|
|
7
7
|
class TestBreakout extends Error {
|
|
8
8
|
source?: Error;
|
|
@@ -35,9 +35,7 @@ export class TestPhaseManager {
|
|
|
35
35
|
for (const fn of this.#suite[phase]) {
|
|
36
36
|
|
|
37
37
|
// Ensure all the criteria below are satisfied before moving forward
|
|
38
|
-
error = await
|
|
39
|
-
.add(async () => fn.call(this.#suite.instance))
|
|
40
|
-
.wait();
|
|
38
|
+
error = await Barrier.awaitOperation(TEST_PHASE_TIMEOUT, async () => fn.call(this.#suite.instance));
|
|
41
39
|
|
|
42
40
|
if (error) {
|
|
43
41
|
break;
|
package/src/execute/runner.ts
CHANGED
|
@@ -5,11 +5,11 @@ import { WorkPool } from '@travetto/worker';
|
|
|
5
5
|
|
|
6
6
|
import { buildStandardTestManager } from '../worker/standard';
|
|
7
7
|
import { RunnableTestConsumer } from '../consumer/types/runnable';
|
|
8
|
+
import { TestRun } from '../model/test';
|
|
8
9
|
|
|
9
10
|
import { TestExecutor } from './executor';
|
|
10
11
|
import { RunnerUtil } from './util';
|
|
11
12
|
import { RunState } from './types';
|
|
12
|
-
import { TestConfig, TestRun } from '../model/test';
|
|
13
13
|
|
|
14
14
|
/**
|
|
15
15
|
* Test Runner
|
|
@@ -61,10 +61,6 @@ export class Runner {
|
|
|
61
61
|
RuntimeIndex.reinitForModule(entry.module);
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
-
const filter = (run.methodNames?.length) ?
|
|
65
|
-
(cfg: TestConfig): boolean => run.methodNames!.includes(cfg.methodName) :
|
|
66
|
-
undefined;
|
|
67
|
-
|
|
68
64
|
const consumer = (await RunnableTestConsumer.get(this.#state.consumer ?? this.#state.format))
|
|
69
65
|
.withTransformer(e => {
|
|
70
66
|
// Copy run metadata to event
|
|
@@ -73,7 +69,7 @@ export class Runner {
|
|
|
73
69
|
});
|
|
74
70
|
|
|
75
71
|
await consumer.onStart({});
|
|
76
|
-
await new TestExecutor(consumer
|
|
72
|
+
await new TestExecutor(consumer).execute(run);
|
|
77
73
|
return consumer.summarizeAsBoolean();
|
|
78
74
|
}
|
|
79
75
|
|
package/src/registry/suite.ts
CHANGED
|
@@ -119,7 +119,7 @@ class $SuiteRegistry extends MetadataRegistry<SuiteConfig, TestConfig> {
|
|
|
119
119
|
} else if (clsId) {
|
|
120
120
|
const cls = this.getValidClasses().find(x => x.Ⲑid === clsId)!;
|
|
121
121
|
const suite = this.get(cls);
|
|
122
|
-
return [{ suite, tests: suite.tests }];
|
|
122
|
+
return suite ? [{ suite, tests: suite.tests }] : [];
|
|
123
123
|
} else {
|
|
124
124
|
const suites = this.getValidClasses()
|
|
125
125
|
.map(x => this.get(x))
|
package/src/execute/promise.ts
DELETED
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
import { createHook, executionAsyncId } from 'node:async_hooks';
|
|
2
|
-
import { isPromise } from 'node:util/types';
|
|
3
|
-
|
|
4
|
-
import { ExecutionError } from '@travetto/worker';
|
|
5
|
-
import { Util } from '@travetto/runtime';
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* Promise watcher, to catch any unfinished promises
|
|
9
|
-
*/
|
|
10
|
-
export class PromiseCapturer {
|
|
11
|
-
#pending = new Map<number, Promise<unknown>>();
|
|
12
|
-
#id: number = 0;
|
|
13
|
-
|
|
14
|
-
#init(id: number, type: string, triggerId: number, resource: unknown): void {
|
|
15
|
-
if (this.#id && type === 'PROMISE' && triggerId === this.#id && isPromise(resource)) {
|
|
16
|
-
this.#pending.set(id, resource);
|
|
17
|
-
}
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
#promiseResolve(asyncId: number): void {
|
|
21
|
-
this.#pending.delete(asyncId);
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
async run(op: () => Promise<unknown> | unknown): Promise<unknown> {
|
|
25
|
-
const hook = createHook({
|
|
26
|
-
init: (...args) => this.#init(...args),
|
|
27
|
-
promiseResolve: (id) => this.#promiseResolve(id)
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
hook.enable();
|
|
31
|
-
|
|
32
|
-
await Util.queueMacroTask();
|
|
33
|
-
this.#id = executionAsyncId();
|
|
34
|
-
try {
|
|
35
|
-
const res = await op();
|
|
36
|
-
let i = 5; // Wait upto 5 macro tasks before continuing
|
|
37
|
-
while (this.#pending.size) {
|
|
38
|
-
await Util.queueMacroTask();
|
|
39
|
-
i -= 1;
|
|
40
|
-
if (i === 0) {
|
|
41
|
-
throw new ExecutionError(`Pending promises: ${this.#pending.size}`);
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
return res;
|
|
45
|
-
} finally {
|
|
46
|
-
hook.disable();
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
}
|