@travetto/test 4.1.0 → 5.0.0-rc.0

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": "4.1.0",
3
+ "version": "5.0.0-rc.0",
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/base": "^4.1.0",
31
- "@travetto/registry": "^4.1.0",
32
- "@travetto/terminal": "^4.1.0",
33
- "@travetto/worker": "^4.1.0",
34
- "@travetto/yaml": "^4.1.0"
30
+ "@travetto/base": "^5.0.0-rc.0",
31
+ "@travetto/registry": "^5.0.0-rc.0",
32
+ "@travetto/terminal": "^5.0.0-rc.0",
33
+ "@travetto/worker": "^5.0.0-rc.0",
34
+ "yaml": "^2.4.5"
35
35
  },
36
36
  "peerDependencies": {
37
- "@travetto/cli": "^4.1.0",
38
- "@travetto/transformer": "^4.1.0"
37
+ "@travetto/cli": "^5.0.0-rc.0",
38
+ "@travetto/transformer": "^5.0.0-rc.0"
39
39
  },
40
40
  "peerDependenciesMeta": {
41
41
  "@travetto/transformer": {
@@ -1,19 +1,13 @@
1
1
  import assert from 'node:assert';
2
2
 
3
3
  import { RuntimeIndex } from '@travetto/manifest';
4
- import { ObjectUtil, AppError, ClassInstance, Class } from '@travetto/base';
4
+ import { AppError, ClassInstance, Class } from '@travetto/base';
5
5
 
6
6
  import { ThrowableError, TestConfig, Assertion } from '../model/test';
7
7
  import { AssertCapture, CaptureAssert } from './capture';
8
8
  import { AssertUtil } from './util';
9
9
  import { ASSERT_FN_OPERATOR, OP_MAPPING } from './types';
10
10
 
11
- declare module 'assert' {
12
- interface AssertionError {
13
- toJSON(): Record<string, unknown>;
14
- }
15
- }
16
-
17
11
  type StringFields<T> = {
18
12
  [K in Extract<keyof T, string>]:
19
13
  (T[K] extends string ? K : never)
@@ -30,6 +24,7 @@ export class AssertCheck {
30
24
  * @param args The arguments passed in
31
25
  */
32
26
  static check(assertion: CaptureAssert, positive: boolean, ...args: unknown[]): void {
27
+ /* eslint-disable @typescript-eslint/consistent-type-assertions */
33
28
  assertion.file = RuntimeIndex.getSourceFile(assertion.file);
34
29
 
35
30
  let fn = assertion.operator;
@@ -46,40 +41,31 @@ export class AssertCheck {
46
41
  // Check fn to call
47
42
  if (fn === 'fail') {
48
43
  if (args.length > 1) {
49
- // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
50
44
  [assertion.actual, assertion.expected, assertion.message, assertion.operator] = args as [unknown, unknown, string, string];
51
45
  } else {
52
- // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
53
46
  [assertion.message] = args as [string];
54
47
  }
55
48
  } else if (/throw|reject/i.test(fn)) {
56
49
  assertion.operator = fn;
57
50
  if (typeof args[1] !== 'string') {
58
- // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
59
51
  [, assertion.expected, assertion.message] = args as [unknown, unknown, string];
60
52
  } else {
61
- // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
62
53
  [, assertion.message] = args as [unknown, string];
63
54
  }
64
55
  } else if (fn === 'ok' || fn === 'assert') {
65
56
  fn = assertion.operator = 'ok';
66
- // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
67
57
  [assertion.actual, assertion.message] = args as [unknown, string];
68
58
  assertion.expected = { toClean: (): string => positive ? 'truthy' : 'falsy' };
69
59
  common.state = 'should be';
70
60
  } else if (fn === 'includes') {
71
61
  assertion.operator = fn;
72
- // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
73
62
  [assertion.actual, assertion.expected, assertion.message] = args as [unknown, unknown, string];
74
63
  } else if (fn === 'instanceof') {
75
64
  assertion.operator = fn;
76
- // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
77
65
  [assertion.actual, assertion.expected, assertion.message] = args as [unknown, unknown, string];
78
- // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
79
66
  assertion.actual = (assertion.actual as ClassInstance)?.constructor;
80
67
  } else { // Handle unknown
81
68
  assertion.operator = fn ?? '';
82
- // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
83
69
  [assertion.actual, assertion.expected, assertion.message] = args as [unknown, unknown, string];
84
70
  }
85
71
 
@@ -93,36 +79,24 @@ export class AssertCheck {
93
79
  assertion.expected = AssertUtil.cleanValue(assertion.expected);
94
80
  }
95
81
 
96
- // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
97
82
  const [actual, expected, message] = args as [unknown, unknown, string];
98
83
 
99
84
  // Actually run the assertion
100
85
  switch (fn) {
101
- // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
102
86
  case 'includes': assertFn((actual as unknown[]).includes(expected), message); break;
103
- // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
104
87
  case 'test': assertFn((expected as RegExp).test(actual as string), message); break;
105
- // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
106
88
  case 'instanceof': assertFn(actual instanceof (expected as Class), message); break;
107
- // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
108
89
  case 'in': assertFn((actual as string) in (expected as object), message); break;
109
- // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
110
90
  case 'lessThan': assertFn((actual as number) < (expected as number), message); break;
111
- // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
112
91
  case 'lessThanEqual': assertFn((actual as number) <= (expected as number), message); break;
113
- // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
114
92
  case 'greaterThan': assertFn((actual as number) > (expected as number), message); break;
115
- // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
116
93
  case 'greaterThanEqual': assertFn((actual as number) >= (expected as number), message); break;
117
- // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
118
94
  case 'ok': assertFn(...args as [unknown, string]); break;
119
95
  default:
120
- // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
121
96
  if (fn && assert[fn as keyof typeof assert]) { // Assert call
122
97
  if (/not/i.test(fn)) {
123
98
  common.state = 'should not';
124
99
  }
125
- // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
126
100
  assert[fn as 'ok'].apply(null, args as [boolean, string | undefined]);
127
101
  }
128
102
  }
@@ -144,6 +118,7 @@ export class AssertCheck {
144
118
  }
145
119
  throw err;
146
120
  }
121
+ /* eslint-enable @typescript-eslint/consistent-type-assertions */
147
122
  }
148
123
 
149
124
  /**
@@ -205,14 +180,15 @@ export class AssertCheck {
205
180
  shouldThrow: ThrowableError | undefined,
206
181
  assertion: CaptureAssert
207
182
  ): void {
208
- if (ObjectUtil.isPrimitive(err)) {
183
+ if (!(err instanceof Error)) {
209
184
  err = new Error(`${err}`);
210
185
  }
211
186
  if (!(err instanceof Error)) {
212
187
  throw err;
213
188
  }
214
189
  if (positive) {
215
- missed = new AppError('Error thrown, but expected no errors', 'general', {}, err.stack);
190
+ missed = new AppError('Error thrown, but expected no errors');
191
+ missed.stack = err.stack;
216
192
  }
217
193
 
218
194
  const resolvedErr = (missed && err) ?? this.checkError(shouldThrow, err);
@@ -244,8 +220,8 @@ export class AssertCheck {
244
220
  try {
245
221
  action();
246
222
  if (!positive) {
247
- if (!ObjectUtil.isPrimitive(shouldThrow)) {
248
- shouldThrow = shouldThrow?.name;
223
+ if (typeof shouldThrow === 'function') {
224
+ shouldThrow = shouldThrow.name;
249
225
  }
250
226
  throw (missed = new AppError(`No error thrown, but expected ${shouldThrow ?? 'an error'}`));
251
227
  }
@@ -282,8 +258,8 @@ export class AssertCheck {
282
258
  await action();
283
259
  }
284
260
  if (!positive) {
285
- if (!ObjectUtil.isPrimitive(shouldThrow)) {
286
- shouldThrow = shouldThrow?.name;
261
+ if (typeof shouldThrow === 'function') {
262
+ shouldThrow = shouldThrow.name;
287
263
  }
288
264
  throw (missed = new AppError(`No error thrown, but expected ${shouldThrow ?? 'an error'} `));
289
265
  }
@@ -1,7 +1,6 @@
1
1
  import util from 'node:util';
2
2
 
3
3
  import { path, RuntimeIndex, RuntimeContext } from '@travetto/manifest';
4
- import { Class, ClassInstance, ObjectUtil } from '@travetto/base';
5
4
 
6
5
  import { TestConfig, Assertion, TestResult } from '../model/test';
7
6
  import { SuiteConfig } from '../model/suite';
@@ -19,22 +18,25 @@ export class AssertUtil {
19
18
  * Clean a value for displaying in the output
20
19
  */
21
20
  static cleanValue(val: unknown): unknown {
22
- if (isCleanable(val)) {
23
- return val.toClean();
24
- } else if (val === null || val === undefined
25
- || (!(val instanceof RegExp) && ObjectUtil.isPrimitive(val))
26
- || ObjectUtil.isPlainObject(val) || Array.isArray(val)
27
- ) {
28
- return JSON.stringify(val);
29
- } else {
30
- // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
31
- const subV = val as (Class | ClassInstance);
32
- if (subV.Ⲑid || !subV.constructor || (!subV.constructor.Ⲑid && ObjectUtil.isFunction(subV))) { // If a function, show name
33
- return subV.name;
34
- } else { // Else inspect
35
- return util.inspect(val, false, 1).replace(/\n/g, ' ');
21
+ switch (typeof val) {
22
+ case 'object': {
23
+ if (isCleanable(val)) {
24
+ return val.toClean();
25
+ } else if (val === null || val.constructor === Object || Array.isArray(val) || val instanceof Date) {
26
+ return JSON.stringify(val);
27
+ }
28
+ break;
29
+ }
30
+ case 'undefined': case 'string': case 'number': case 'bigint': case 'boolean': return JSON.stringify(val);
31
+ case 'function': {
32
+ if (val.Ⲑid || !val.constructor) {
33
+ return val.name;
34
+ }
35
+ break;
36
36
  }
37
37
  }
38
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
39
+ return util.inspect(val, false, 1).replace(/\n/g, ' ');
38
40
  }
39
41
 
40
42
  /**
@@ -1,4 +1,4 @@
1
- import { TypedObject, ObjectUtil } from '@travetto/base';
1
+ import { AppError, TypedObject } from '@travetto/base';
2
2
 
3
3
  import { TestEvent, } from '../model/event';
4
4
 
@@ -25,7 +25,7 @@ export class ErrorUtil {
25
25
  error[k] = e[k];
26
26
  }
27
27
  error.name = e.name;
28
- if (ObjectUtil.hasToJSON(e)) {
28
+ if (e instanceof AppError) {
29
29
  Object.assign(error, e.toJSON());
30
30
  }
31
31
  error.message ||= e.message;
@@ -44,8 +44,11 @@ export class ErrorUtil {
44
44
  if (isSerialized(e)) {
45
45
  const err = new Error();
46
46
 
47
- for (const k of TypedObject.keys<{ name: string }>(e)) {
48
- err[k] = e[k];
47
+ for (const k of TypedObject.keys(e)) {
48
+ if (k === '$') {
49
+ continue;
50
+ }
51
+ err[k] = e[k]!;
49
52
  }
50
53
  err.message = e.message;
51
54
  err.stack = e.stack;
@@ -1,7 +1,7 @@
1
1
  import { RuntimeIndex } from '@travetto/manifest';
2
2
  import { Terminal } from '@travetto/terminal';
3
- import { ObjectUtil, TimeUtil } from '@travetto/base';
4
- import { YamlUtil } from '@travetto/yaml';
3
+ import { AppError, TimeUtil } from '@travetto/base';
4
+ import { stringify } from 'yaml';
5
5
 
6
6
  import { TestEvent } from '../../model/event';
7
7
  import { SuitesSummary, TestConsumer } from '../types';
@@ -44,7 +44,7 @@ export class TapEmitter implements TestConsumer {
44
44
  */
45
45
  logMeta(obj: Record<string, unknown>): void {
46
46
  const lineLength = this.#terminal.width - 5;
47
- let body = YamlUtil.serialize(obj, { wordwrap: lineLength });
47
+ let body = stringify(obj, { lineWidth: lineLength });
48
48
  body = body.split('\n').map(x => ` ${x}`).join('\n');
49
49
  this.log(`---\n${this.#enhancer.objectInspect(body)}\n...`);
50
50
  }
@@ -101,9 +101,9 @@ export class TapEmitter implements TestConsumer {
101
101
 
102
102
  // Handle error
103
103
  if (test.status === 'failed') {
104
- if (test.error?.stack && !test.error.stack.includes('AssertionError')) {
104
+ if (test.error && test.error.name !== 'AssertionError') {
105
105
  const err = ErrorUtil.deserializeError(test.error);
106
- this.logMeta({ error: ObjectUtil.hasToJSON(err) ? err.toJSON() : err });
106
+ this.logMeta({ error: err instanceof AppError ? err.toJSON() : err });
107
107
  }
108
108
  }
109
109
 
@@ -127,8 +127,7 @@ export class TapEmitter implements TestConsumer {
127
127
  if (summary.errors.length) {
128
128
  this.log('---\n');
129
129
  for (const err of summary.errors) {
130
- // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
131
- this.log(this.#enhancer.failure(ObjectUtil.hasToJSON(err) ? err.toJSON() as string : `${err}`));
130
+ this.log(this.#enhancer.failure(err instanceof AppError ? JSON.stringify(err.toJSON(), null, 2) : `${err}`));
132
131
  }
133
132
  }
134
133
 
@@ -141,7 +140,7 @@ export class TapEmitter implements TestConsumer {
141
140
  `${this.#enhancer.total(summary.failed)}`,
142
141
  'skipped',
143
142
  this.#enhancer.total(summary.skipped),
144
- `# (Total Test Time: ${TimeUtil.prettyDelta(summary.duration)}, Total Run Time: ${TimeUtil.prettyDeltaSinceTime(this.#start)})`
143
+ `# (Total Test Time: ${TimeUtil.asClock(summary.duration)}, Total Run Time: ${TimeUtil.asClock(Date.now() - this.#start)})`
145
144
  ].join(' '));
146
145
  }
147
146
  }
@@ -1,6 +1,6 @@
1
1
  import { Writable } from 'node:stream';
2
2
 
3
- import { YamlUtil } from '@travetto/yaml';
3
+ import { stringify } from 'yaml';
4
4
 
5
5
  import { TestEvent } from '../../model/event';
6
6
  import { SuitesSummary, TestConsumer } from '../types';
@@ -33,7 +33,7 @@ export class XunitEmitter implements TestConsumer {
33
33
  }
34
34
  }
35
35
  if (Object.keys(obj).length) {
36
- let body = YamlUtil.serialize(obj);
36
+ let body = stringify(obj);
37
37
  body = body.split('\n').map(x => ` ${x}`).join('\n');
38
38
  return `<![CDATA[\n${body}\n]]>`;
39
39
  } else {
@@ -1,21 +1,23 @@
1
1
  import util from 'node:util';
2
2
 
3
- import { ConsoleEvent, ConsoleManager } from '@travetto/base';
3
+ import { ConsoleEvent, ConsoleListener, ConsoleManager } from '@travetto/base';
4
4
 
5
5
  /**
6
6
  * Console capturer. Hooks into the Console manager, and collects the
7
7
  * output into a map for test results
8
8
  */
9
- export class ConsoleCapture {
9
+ export class ConsoleCapture implements ConsoleListener {
10
+ static #listener: ConsoleListener = ConsoleManager.get();
10
11
 
11
- static out: Record<string, string[]>;
12
+ out: Record<string, string[]>;
12
13
 
13
- static start(): void {
14
+ start(): this {
14
15
  this.out = {};
15
16
  ConsoleManager.set(this);
17
+ return this;
16
18
  }
17
19
 
18
- static onLog({ level, args }: ConsoleEvent): void {
20
+ log({ level, args }: ConsoleEvent): void {
19
21
  (this.out[level] = this.out[level] ?? []).push(
20
22
  args
21
23
  .map((x => typeof x === 'string' ? x : util.inspect(x, false, 5)))
@@ -23,10 +25,10 @@ export class ConsoleCapture {
23
25
  );
24
26
  }
25
27
 
26
- static end(): Record<string, string> {
28
+ end(): Record<string, string> {
27
29
  const ret = this.out ?? {};
28
30
  this.out = {};
29
- ConsoleManager.clear();
31
+ ConsoleManager.set(ConsoleCapture.#listener);
30
32
  return Object.fromEntries(Object.entries(ret).map(([k, v]) => [k, v.join('\n')]));
31
33
  }
32
34
  }
@@ -1,7 +1,8 @@
1
1
  import { AssertionError } from 'node:assert';
2
+ import path from 'node:path';
2
3
 
3
- import { path, RuntimeIndex, RuntimeContext } from '@travetto/manifest';
4
- import { Env, Util } from '@travetto/base';
4
+ import { RuntimeIndex, RuntimeContext } from '@travetto/manifest';
5
+ import { Env, TimeUtil } from '@travetto/base';
5
6
  import { Barrier, ExecutionError } from '@travetto/worker';
6
7
 
7
8
  import { SuiteRegistry } from '../registry/suite';
@@ -12,10 +13,10 @@ import { AssertCheck } from '../assert/check';
12
13
  import { AssertCapture } from '../assert/capture';
13
14
  import { ConsoleCapture } from './console';
14
15
  import { TestPhaseManager } from './phase';
15
- import { PromiseCapture } from './promise';
16
+ import { PromiseCapturer } from './promise';
16
17
  import { AssertUtil } from '../assert/util';
17
18
 
18
- const TEST_TIMEOUT = Env.TRV_TEST_TIMEOUT.time ?? 5000;
19
+ const TEST_TIMEOUT = TimeUtil.fromValue(Env.TRV_TEST_TIMEOUT.val) ?? 5000;
19
20
 
20
21
  /**
21
22
  * Support execution of the tests
@@ -29,22 +30,21 @@ export class TestExecutor {
29
30
  */
30
31
  static async #executeTestMethod(test: TestConfig): Promise<Error | undefined> {
31
32
  const suite = SuiteRegistry.get(test.class);
32
- const promCleanup = Util.resolvablePromise();
33
33
 
34
34
  // Ensure all the criteria below are satisfied before moving forward
35
35
  const barrier = new Barrier(test.timeout || TEST_TIMEOUT, true)
36
- .add(promCleanup, true) // If not timeout or unhandled, ensure all promises are cleaned up
37
36
  .add(async () => {
38
37
  const env = process.env;
38
+ process.env = { ...env }; // Created an isolated environment
39
+ const pCap = new PromiseCapturer();
39
40
 
40
41
  try {
41
- PromiseCapture.start(); // Listen for all promises to detect any unfinished, only start once method is invoked
42
- process.env = { ...env }; // Created an isolated environment
43
- // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
44
- await (suite.instance as Record<string, Function>)[test.methodName](); // Run
42
+ await pCap.run(() =>
43
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
44
+ (suite.instance as Record<string, Function>)[test.methodName]()
45
+ );
45
46
  } finally {
46
47
  process.env = env; // Restore
47
- PromiseCapture.stop().then(() => Util.queueMacroTask().then(promCleanup.resolve), promCleanup.reject);
48
48
  }
49
49
  });
50
50
 
@@ -137,7 +137,7 @@ export class TestExecutor {
137
137
  })
138
138
  );
139
139
 
140
- ConsoleCapture.start(); // Capture all output from transpiled code
140
+ const consoleCapture = new ConsoleCapture().start(); // Capture all output from transpiled code
141
141
 
142
142
  // Run method and get result
143
143
  let error = await this.#executeTestMethod(test);
@@ -158,7 +158,7 @@ export class TestExecutor {
158
158
 
159
159
  Object.assign(result, {
160
160
  status: error ? 'failed' : 'passed',
161
- output: ConsoleCapture.end(),
161
+ output: consoleCapture.end(),
162
162
  assertions: getAssertions(),
163
163
  duration: Date.now() - startTime,
164
164
  ...(error ? { error } : {})
@@ -1,5 +1,5 @@
1
1
  import { Barrier } from '@travetto/worker';
2
- import { Env } from '@travetto/base';
2
+ import { Env, TimeUtil } from '@travetto/base';
3
3
 
4
4
  import { TestConsumer } from '../consumer/types';
5
5
  import { SuiteConfig, SuiteResult } from '../model/suite';
@@ -8,7 +8,7 @@ import { TestResult } from '../model/test';
8
8
 
9
9
  class TestBreakout extends Error { }
10
10
 
11
- const TEST_PHASE_TIMEOUT = Env.TRV_TEST_PHASE_TIMEOUT.time ?? 15000;
11
+ const TEST_PHASE_TIMEOUT = TimeUtil.fromValue(Env.TRV_TEST_PHASE_TIMEOUT.val) ?? 15000;
12
12
 
13
13
  /**
14
14
  * Test Phase Execution Manager.
@@ -1,72 +1,49 @@
1
- import { ExecutionError } from '@travetto/worker';
2
-
3
- const og = Promise;
4
-
5
- declare global {
6
- interface Promise<T> {
7
- status: 'ok' | 'error';
8
- }
9
- }
1
+ import { createHook, executionAsyncId } from 'node:async_hooks';
10
2
 
11
- /**
12
- * Promise stub to track creation
13
- */
14
- function Wrapped(this: Promise<unknown>, ex: (res: (v: unknown) => unknown, rej?: (err: unknown) => unknown) => void): Promise<unknown> {
15
- const prom = new og(ex);
16
- this.then = prom.then.bind(prom);
17
- this.catch = prom.catch.bind(prom);
18
- this.finally = prom.finally.bind(prom);
19
- this.then(() => prom.status = 'ok',
20
- () => prom.status = 'error');
21
-
22
- if (PromiseCapture.pending) {
23
- PromiseCapture.pending.push(prom);
24
- }
25
- return this;
26
- }
27
-
28
- Wrapped.allSettled = Promise.allSettled.bind(Promise);
29
- Wrapped.race = Promise.race.bind(Promise);
30
- Wrapped.all = Promise.all.bind(Promise);
31
- Wrapped.resolve = Promise.resolve.bind(Promise);
32
- Wrapped.reject = Promise.reject.bind(Promise);
3
+ import { ExecutionError } from '@travetto/worker';
4
+ import { Util } from '@travetto/base';
33
5
 
34
6
  /**
35
7
  * Promise watcher, to catch any unfinished promises
36
8
  */
37
- export class PromiseCapture {
38
- static pending: Promise<unknown>[];
39
-
40
- /**
41
- * Swap method and track progress
42
- */
43
- static start(): void {
44
- this.pending = [];
45
- // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
46
- global.Promise = Wrapped as unknown as typeof Promise;
9
+ export class PromiseCapturer {
10
+ #pending = new Map<number, Promise<unknown>>();
11
+ #id: number = 0;
12
+
13
+ #init(id: number, type: string, triggerId: number, resource: unknown): void {
14
+ if (this.#id && type === 'PROMISE' && triggerId === this.#id) {
15
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
16
+ this.#pending.set(id, resource as Promise<unknown>);
17
+ }
47
18
  }
48
19
 
49
- /**
50
- * Wait for all promises to resolve
51
- */
52
- static async resolvePending(pending: Promise<unknown>[]): Promise<void> {
53
- if (pending.length) {
54
- let final: Error | undefined;
55
- console.debug('Resolving', { pending: this.pending.length });
56
- await Promise.all(pending).catch(err => final = err);
57
-
58
- // If any return in an error, make that the final result
59
- throw new ExecutionError(`Pending promises: ${pending.length}`, final?.stack);
60
- }
20
+ #promiseResolve(asyncId: number): void {
21
+ this.#pending.delete(asyncId);
61
22
  }
62
23
 
63
- /**
64
- * Stop the capture
65
- */
66
- static stop(): Promise<void> {
67
- console.debug('Stopping', { pending: this.pending.length });
68
- // Restore the promise
69
- global.Promise = og;
70
- return this.resolvePending(this.pending.filter(x => x.status === undefined));
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
+ }
71
48
  }
72
49
  }
@@ -1,4 +1,6 @@
1
- import { RuntimeContext, RuntimeIndex, path } from '@travetto/manifest';
1
+ import path from 'node:path';
2
+
3
+ import { path as mp, RuntimeContext, RuntimeIndex } from '@travetto/manifest';
2
4
  import { TimeUtil } from '@travetto/base';
3
5
  import { WorkPool } from '@travetto/worker';
4
6
 
@@ -21,7 +23,7 @@ export class Runner {
21
23
  }
22
24
 
23
25
  get patterns(): RegExp[] {
24
- return this.#state.args.map(x => new RegExp(path.toPosix(x)));
26
+ return this.#state.args.map(x => new RegExp(mp.toPosix(x)));
25
27
  }
26
28
 
27
29
  /**
@@ -37,10 +39,10 @@ export class Runner {
37
39
  const testCount = await RunnerUtil.getTestCount(this.#state.args);
38
40
  await consumer.onStart({ testCount });
39
41
  await WorkPool.run(
40
- () => buildStandardTestManager(consumer),
42
+ buildStandardTestManager.bind(null, consumer),
41
43
  files,
42
44
  {
43
- idleTimeoutMillis: TimeUtil.timeToMs('10s'),
45
+ idleTimeoutMillis: TimeUtil.asMillis(10, 's'),
44
46
  min: 1,
45
47
  max: this.#state.concurrency,
46
48
  });
@@ -1,7 +1,6 @@
1
1
  import { RootRegistry, MethodSource } from '@travetto/registry';
2
2
  import { WorkPool, WorkQueue } from '@travetto/worker';
3
3
  import { RuntimeIndex } from '@travetto/manifest';
4
- import { ObjectUtil } from '@travetto/base';
5
4
 
6
5
  import { SuiteRegistry } from '../registry/suite';
7
6
  import { buildStandardTestManager } from '../worker/standard';
@@ -12,7 +11,7 @@ import { RunnerUtil } from './util';
12
11
  import { TestEvent } from '../model/event';
13
12
 
14
13
  function isRunEvent(ev: unknown): ev is RunEvent {
15
- return ObjectUtil.isPlainObject(ev) && 'type' in ev && typeof ev.type === 'string' && ev.type === 'run-test';
14
+ return typeof ev === 'object' && !!ev && 'type' in ev && typeof ev.type === 'string' && ev.type === 'run-test';
16
15
  }
17
16
 
18
17
  export type TestWatchEvent =
@@ -88,7 +87,7 @@ export class TestWatcher {
88
87
  }
89
88
 
90
89
  await WorkPool.run(
91
- () => buildStandardTestManager(consumer),
90
+ buildStandardTestManager.bind(null, consumer),
92
91
  itr,
93
92
  {
94
93
  idleTimeoutMillis: 120000,
package/src/fixture.ts CHANGED
@@ -2,6 +2,6 @@ import { FileLoader } from '@travetto/base';
2
2
 
3
3
  export class TestFixtures extends FileLoader {
4
4
  constructor(modules: string[] = []) {
5
- super(['@#test/fixtures', ...['@', ...modules.flat()].map(x => `${x}#support/fixtures`)]);
5
+ super(['@#test/fixtures', ...['@', ...modules.flat(), '@@'].map(x => `${x}#support/fixtures`)]);
6
6
  }
7
7
  }
@@ -1,7 +1,7 @@
1
1
  import { createWriteStream } from 'node:fs';
2
2
 
3
3
  import { RuntimeContext } from '@travetto/manifest';
4
- import { ConsoleManager, Env, TimeUtil, Util } from '@travetto/base';
4
+ import { ConsoleManager, Env, Util } from '@travetto/base';
5
5
  import { ChildCommChannel } from '@travetto/worker';
6
6
 
7
7
  import { ErrorUtil } from '../consumer/error';
@@ -15,6 +15,8 @@ import { Events, RunEvent } from './types';
15
15
  */
16
16
  export class TestChildWorker extends ChildCommChannel<RunEvent> {
17
17
 
18
+ #done = Util.resolvablePromise();
19
+
18
20
  async #exec(op: () => Promise<unknown>, type: string): Promise<void> {
19
21
  try {
20
22
  await op();
@@ -36,9 +38,9 @@ export class TestChildWorker extends ChildCommChannel<RunEvent> {
36
38
  const file = RuntimeContext.toolPath(`test-worker.${process.pid}.log`);
37
39
  const stdout = createWriteStream(file, { flags: 'a' });
38
40
  const c = new console.Console({ stdout, inspectOptions: { depth: 4, colors: false } });
39
- ConsoleManager.set({ onLog: (ev) => c[ev.level](process.pid, ...ev.args) });
41
+ ConsoleManager.set({ log: (ev) => c[ev.level](process.pid, ...ev.args) });
40
42
  } else {
41
- ConsoleManager.set({ onLog: () => { } });
43
+ ConsoleManager.set({ log: () => { } });
42
44
  }
43
45
 
44
46
  RunnerUtil.registerCleanup('worker');
@@ -49,7 +51,7 @@ export class TestChildWorker extends ChildCommChannel<RunEvent> {
49
51
  // Let parent know the child is ready for handling commands
50
52
  this.send(Events.READY);
51
53
 
52
- await Util.blockingTimeout(TimeUtil.timeToMs('10m'));
54
+ await this.#done.promise;
53
55
  }
54
56
 
55
57
  /**
@@ -80,11 +82,15 @@ export class TestChildWorker extends ChildCommChannel<RunEvent> {
80
82
 
81
83
  console.debug('Running', { file: event.file });
82
84
 
83
- await new Runner({
84
- format: 'exec',
85
- mode: 'single',
86
- args: [event.file!, event.class!, event.method!],
87
- concurrency: 1
88
- }).run();
85
+ try {
86
+ await new Runner({
87
+ format: 'exec',
88
+ mode: 'single',
89
+ args: [event.file!, event.class!, event.method!],
90
+ concurrency: 1
91
+ }).run();
92
+ } finally {
93
+ this.#done.resolve();
94
+ }
89
95
  }
90
96
  }
@@ -2,88 +2,72 @@ import { fork } from 'node:child_process';
2
2
 
3
3
  import { RuntimeIndex } from '@travetto/manifest';
4
4
  import { Env } from '@travetto/base';
5
- import { ParentCommChannel, Worker } from '@travetto/worker';
5
+ import { ParentCommChannel } from '@travetto/worker';
6
6
 
7
7
  import { Events, RunEvent } from './types';
8
8
  import { TestConsumer } from '../consumer/types';
9
9
  import { ErrorUtil } from '../consumer/error';
10
10
  import { TestEvent } from '../model/event';
11
11
 
12
- let i = 0;
13
-
14
- function buildEvent(ev: string): RunEvent {
15
- if (ev.includes('#')) {
16
- const [file, cls, method] = ev.split('#');
17
- return { file, class: cls, method };
18
- } else {
19
- return { file: ev };
20
- }
21
- }
22
-
23
12
  /**
24
13
  * Produce a handler for the child worker
25
14
  */
26
- export function buildStandardTestManager(consumer: TestConsumer): Worker<string> {
27
- /**
28
- * Spawn a child
29
- */
30
- return {
31
- id: i += 1,
32
- active: true,
33
- async destroy(): Promise<void> { },
34
- async execute(file: string): Promise<void> {
35
-
36
- process.send?.({ type: 'log', message: `Worker Executing ${file}` });
37
-
38
- const event = buildEvent(file);
15
+ export async function buildStandardTestManager(consumer: TestConsumer, file: string): Promise<void> {
16
+ process.send?.({ type: 'log', message: `Worker Executing ${file}` });
39
17
 
40
- const { module } = RuntimeIndex.getEntry(event.file!)!;
41
- const cwd = RuntimeIndex.getModule(module)!.sourcePath;
18
+ let event: RunEvent;
19
+ if (file.includes('#')) {
20
+ const [f, cls, method] = file.split('#');
21
+ event = { file: f, class: cls, method };
22
+ } else {
23
+ event = { file };
24
+ }
42
25
 
43
- const channel = new ParentCommChannel<TestEvent & { error?: Error }>(
44
- fork(
45
- RuntimeIndex.resolveFileImport('@travetto/cli/support/entry.trv'), ['test:child'],
46
- {
47
- cwd,
48
- env: {
49
- ...process.env,
50
- ...Env.TRV_MANIFEST.export(RuntimeIndex.getModule(module)!.outputPath),
51
- ...Env.TRV_QUIET.export(true)
52
- },
53
- stdio: ['ignore', 'ignore', 2, 'ipc']
54
- }
55
- )
56
- );
26
+ const { module } = RuntimeIndex.getEntry(event.file!)!;
27
+ const cwd = RuntimeIndex.getModule(module)!.sourcePath;
28
+
29
+ const channel = new ParentCommChannel<TestEvent & { error?: Error }>(
30
+ fork(
31
+ RuntimeIndex.resolveFileImport('@travetto/cli/support/entry.trv'), ['test:child'],
32
+ {
33
+ cwd,
34
+ env: {
35
+ ...process.env,
36
+ ...Env.TRV_MANIFEST.export(RuntimeIndex.getModule(module)!.outputPath),
37
+ ...Env.TRV_QUIET.export(true)
38
+ },
39
+ stdio: ['ignore', 'ignore', 2, 'ipc']
40
+ }
41
+ )
42
+ );
57
43
 
58
- await channel.once(Events.READY); // Wait for the child to be ready
59
- await channel.send(Events.INIT); // Initialize
60
- await channel.once(Events.INIT_COMPLETE); // Wait for complete
44
+ await channel.once(Events.READY); // Wait for the child to be ready
45
+ await channel.send(Events.INIT); // Initialize
46
+ await channel.once(Events.INIT_COMPLETE); // Wait for complete
61
47
 
62
- channel.on('*', async ev => {
63
- try {
64
- await consumer.onEvent(ev); // Connect the consumer with the event stream from the child
65
- } catch {
66
- // Do nothing
67
- }
68
- });
48
+ channel.on('*', async ev => {
49
+ try {
50
+ await consumer.onEvent(ev); // Connect the consumer with the event stream from the child
51
+ } catch {
52
+ // Do nothing
53
+ }
54
+ });
69
55
 
70
- // Listen for child to complete
71
- const complete = channel.once(Events.RUN_COMPLETE);
72
- // Start test
73
- channel.send(Events.RUN, event);
56
+ // Listen for child to complete
57
+ const complete = channel.once(Events.RUN_COMPLETE);
58
+ // Start test
59
+ channel.send(Events.RUN, event);
74
60
 
75
- // Wait for complete
76
- const { error } = await complete;
61
+ // Wait for complete
62
+ const { error } = await complete;
77
63
 
78
- // Kill on complete
79
- await channel.destroy();
64
+ // Kill on complete
65
+ await channel.destroy();
80
66
 
81
- process.send?.({ type: 'log', message: `Worker Finished ${file}` });
67
+ process.send?.({ type: 'log', message: `Worker Finished ${file}` });
82
68
 
83
- // If we received an error, throw it
84
- if (error) {
85
- throw ErrorUtil.deserializeError(error);
86
- }
87
- },
88
- };
69
+ // If we received an error, throw it
70
+ if (error) {
71
+ throw ErrorUtil.deserializeError(error);
72
+ }
89
73
  }
@@ -1,7 +1,7 @@
1
1
  import { EventEmitter } from 'node:events';
2
2
  import fs from 'node:fs/promises';
3
+ import path from 'node:path';
3
4
 
4
- import { path } from '@travetto/manifest';
5
5
  import { Env } from '@travetto/base';
6
6
  import { CliCommandShape, CliCommand, CliValidationError } from '@travetto/cli';
7
7
  import { WorkPool } from '@travetto/worker';
@@ -1,6 +1,6 @@
1
1
  import { EventEmitter } from 'node:events';
2
2
 
3
- import { Env, ExecUtil } from '@travetto/base';
3
+ import { Env } from '@travetto/base';
4
4
  import { CliCommand } from '@travetto/cli';
5
5
 
6
6
  /** Test child worker target */
@@ -17,7 +17,7 @@ export class TestChildWorkerCommand {
17
17
  }
18
18
 
19
19
  async main(): Promise<void> {
20
- ExecUtil.exitOnDisconnect();
20
+ process.once('disconnect', () => process.exit());
21
21
  const { TestChildWorker } = await import('../src/worker/child');
22
22
  return new TestChildWorker().activate();
23
23
  }