@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@travetto/test",
3
- "version": "5.0.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.0",
31
- "@travetto/runtime": "^5.0.0",
32
- "@travetto/terminal": "^5.0.0",
33
- "@travetto/worker": "^5.0.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.0",
38
- "@travetto/transformer": "^5.0.0"
37
+ "@travetto/cli": "^5.0.1",
38
+ "@travetto/transformer": "^5.0.1"
39
39
  },
40
40
  "peerDependenciesMeta": {
41
41
  "@travetto/transformer": {
@@ -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
- function isCleanable(o: unknown): o is { toClean(): unknown } {
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
+ }
@@ -0,0 +1,11 @@
1
+ import { AppError } from '@travetto/runtime';
2
+
3
+ /**
4
+ * Represents an execution error
5
+ */
6
+ export class ExecutionError extends AppError { }
7
+
8
+ /**
9
+ * Timeout execution error
10
+ */
11
+ export class TimeoutError extends ExecutionError { }
@@ -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, testFilter?: (config: TestConfig) => boolean) {
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
- const barrier = new Barrier(test.timeout || TEST_TIMEOUT, true)
64
- .add(async () => {
65
- const env = process.env;
66
- process.env = { ...env }; // Created an isolated environment
67
- const pCap = new PromiseCapturer();
68
-
69
- try {
70
- await pCap.run(() =>
71
- castTo<Record<string, Function>>(suite.instance)[test.methodName]()
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);
@@ -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 new Barrier(TEST_PHASE_TIMEOUT, true)
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;
@@ -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, filter).execute(run);
72
+ await new TestExecutor(consumer).execute(run);
77
73
  return consumer.summarizeAsBoolean();
78
74
  }
79
75
 
@@ -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))
@@ -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
- }