@travetto/test 3.0.0-rc.4 → 3.0.0-rc.6

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.
Files changed (44) hide show
  1. package/README.md +19 -18
  2. package/{index.ts → __index__.ts} +2 -0
  3. package/package.json +19 -12
  4. package/src/assert/capture.ts +1 -0
  5. package/src/assert/check.ts +13 -7
  6. package/src/assert/util.ts +12 -12
  7. package/src/consumer/enhancer.ts +16 -24
  8. package/src/consumer/registry.ts +5 -2
  9. package/src/consumer/types/{index.ts → all.ts} +1 -1
  10. package/src/consumer/types/cumulative.ts +11 -6
  11. package/src/consumer/types/runnable.ts +8 -12
  12. package/src/consumer/types/tap-streamed.ts +92 -0
  13. package/src/consumer/types/tap.ts +25 -21
  14. package/src/consumer/types.ts +2 -2
  15. package/src/consumer/util.ts +1 -1
  16. package/src/decorator/suite.ts +3 -2
  17. package/src/decorator/test.ts +11 -2
  18. package/src/execute/console.ts +4 -3
  19. package/src/execute/executor.ts +26 -40
  20. package/src/execute/phase.ts +2 -2
  21. package/src/execute/runner.ts +15 -30
  22. package/src/execute/{types.d.ts → types.ts} +4 -4
  23. package/src/execute/util.ts +8 -8
  24. package/src/execute/watcher.ts +43 -20
  25. package/src/fixture.ts +10 -0
  26. package/src/model/common.ts +4 -0
  27. package/src/registry/suite.ts +10 -7
  28. package/src/worker/child.ts +6 -51
  29. package/src/worker/standard.ts +39 -18
  30. package/src/worker/types.ts +0 -1
  31. package/{bin/lib → support/bin}/run.ts +8 -9
  32. package/support/cli.test.ts +76 -0
  33. package/support/main.test-child.ts +32 -0
  34. package/support/main.test-direct.ts +15 -0
  35. package/support/main.test-watch.ts +8 -0
  36. package/support/transformer.annotate.ts +4 -5
  37. package/support/transformer.assert.ts +22 -20
  38. package/bin/cli-test.ts +0 -121
  39. package/bin/test-child.ts +0 -38
  40. package/bin/test-direct.ts +0 -23
  41. package/bin/test-watch.ts +0 -25
  42. package/src/consumer/types/tap-summary.ts +0 -78
  43. package/src/worker/isolated.ts +0 -19
  44. package/support/phase.reset.ts +0 -12
@@ -1,13 +1,12 @@
1
- import { Writable } from 'stream';
2
-
3
- import { ColorUtil, PathUtil } from '@travetto/boot';
1
+ import { path } from '@travetto/manifest';
2
+ import { GlobalTerminal, Terminal } from '@travetto/terminal';
3
+ import { ErrorUtil, ObjectUtil, TimeUtil } from '@travetto/base';
4
4
  import { YamlUtil } from '@travetto/yaml';
5
- import { ErrorUtil } from '@travetto/base/src/internal/error';
6
5
 
7
6
  import { TestEvent } from '../../model/event';
8
7
  import { SuitesSummary, TestConsumer } from '../types';
9
8
  import { Consumable } from '../registry';
10
- import { TestResultsEnhancer, COLOR_ENHANCER, DUMMY_ENHANCER } from '../enhancer';
9
+ import { TestResultsEnhancer, CONSOLE_ENHANCER } from '../enhancer';
11
10
 
12
11
  /**
13
12
  * TAP Format consumer
@@ -15,25 +14,27 @@ import { TestResultsEnhancer, COLOR_ENHANCER, DUMMY_ENHANCER } from '../enhancer
15
14
  @Consumable('tap')
16
15
  export class TapEmitter implements TestConsumer {
17
16
  #count = 0;
18
- #stream: Writable;
19
17
  #enhancer: TestResultsEnhancer;
18
+ #terminal: Terminal;
19
+ #start: number;
20
20
 
21
21
  constructor(
22
- stream: Writable = process.stdout,
23
- enhancer: TestResultsEnhancer = ColorUtil.colorize ? COLOR_ENHANCER : DUMMY_ENHANCER
22
+ terminal = new Terminal({ output: process.stdout }),
23
+ enhancer: TestResultsEnhancer = CONSOLE_ENHANCER
24
24
  ) {
25
- this.#stream = stream;
25
+ this.#terminal = terminal;
26
26
  this.#enhancer = enhancer;
27
27
  }
28
28
 
29
- protected log(message: string): void {
30
- this.#stream.write(`${message}\n`);
29
+ log(message: string): void {
30
+ this.#terminal.writeLines(message);
31
31
  }
32
32
 
33
33
  /**
34
34
  * Preamble
35
35
  */
36
36
  onStart(): void {
37
+ this.#start = Date.now();
37
38
  this.log(this.#enhancer.suiteName('TAP version 13')!);
38
39
  }
39
40
 
@@ -41,7 +42,8 @@ export class TapEmitter implements TestConsumer {
41
42
  * Output supplemental data (e.g. logs)
42
43
  */
43
44
  logMeta(obj: Record<string, unknown>): void {
44
- let body = YamlUtil.serialize(obj, { wordwrap: +(process.env.TRV_CONSOLE_WIDTH ?? process.stdout.columns ?? 80) - 5 });
45
+ const lineLength = GlobalTerminal.width - 5;
46
+ let body = YamlUtil.serialize(obj, { wordwrap: lineLength });
45
47
  body = body.split('\n').map(x => ` ${x}`).join('\n');
46
48
  this.log(`---\n${this.#enhancer.objectInspect(body)}\n...`);
47
49
  }
@@ -59,27 +61,29 @@ export class TapEmitter implements TestConsumer {
59
61
  }
60
62
  this.log(`# ${header}`);
61
63
 
64
+ const cwd = path.cwd();
65
+
62
66
  // Handle each assertion
63
67
  if (test.assertions.length) {
64
68
  let subCount = 0;
65
- for (const a of test.assertions) {
66
- const text = a.message ? `${a.text} (${this.#enhancer.failure(a.message)})` : a.text;
69
+ for (const asrt of test.assertions) {
70
+ const text = asrt.message ? `${asrt.text} (${this.#enhancer.failure(asrt.message)})` : asrt.text;
67
71
  let subMessage = [
68
72
  this.#enhancer.assertNumber(++subCount),
69
73
  '-',
70
74
  this.#enhancer.assertDescription(text),
71
- `${this.#enhancer.assertFile(a.file.replace(PathUtil.cwd, '.'))}:${this.#enhancer.assertLine(a.line)}`
75
+ `${this.#enhancer.assertFile(asrt.file.replace(cwd, '.'))}:${this.#enhancer.assertLine(asrt.line)}`
72
76
  ].join(' ');
73
77
 
74
- if (a.error) {
78
+ if (asrt.error) {
75
79
  subMessage = `${this.#enhancer.failure('not ok')} ${subMessage}`;
76
80
  } else {
77
81
  subMessage = `${this.#enhancer.success('ok')} ${subMessage}`;
78
82
  }
79
83
  this.log(` ${subMessage}`);
80
84
 
81
- if (a.message && a.message.length > 100) {
82
- this.logMeta({ message: a.message.replace(/\\n/g, '\n') });
85
+ if (asrt.message && asrt.message.length > 100) {
86
+ this.logMeta({ message: asrt.message.replace(/\\n/g, '\n') });
83
87
  }
84
88
  }
85
89
  this.log(` ${this.#enhancer.assertNumber(1)}..${this.#enhancer.assertNumber(subCount)}`);
@@ -100,7 +104,7 @@ export class TapEmitter implements TestConsumer {
100
104
  if (test.status === 'failed') {
101
105
  if (test.error?.stack && !test.error.stack.includes('AssertionError')) {
102
106
  const err = ErrorUtil.deserializeError(test.error);
103
- this.logMeta({ error: err.toJSON?.() ?? err });
107
+ this.logMeta({ error: ObjectUtil.hasToJSON(err) ? err.toJSON() : err });
104
108
  }
105
109
  }
106
110
 
@@ -125,7 +129,7 @@ export class TapEmitter implements TestConsumer {
125
129
  this.log('---\n');
126
130
  for (const err of summary.errors) {
127
131
  // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
128
- this.log(this.#enhancer.failure(err instanceof Error ? err.toJSON() as string : `${err}`));
132
+ this.log(this.#enhancer.failure(ObjectUtil.hasToJSON(err) ? err.toJSON() as string : `${err}`));
129
133
  }
130
134
  }
131
135
 
@@ -138,7 +142,7 @@ export class TapEmitter implements TestConsumer {
138
142
  `${this.#enhancer.total(summary.failed)}`,
139
143
  'skipped',
140
144
  this.#enhancer.total(summary.skipped),
141
- `# (Total Time: ${summary.duration}ms)`
145
+ `# (Total Test Time: ${TimeUtil.prettyDelta(summary.duration)}, Total Run Time: ${TimeUtil.prettyDelta(Date.now() - this.#start)})`
142
146
  ].join(' '));
143
147
  }
144
148
  }
@@ -26,7 +26,7 @@ export interface TestConsumer {
26
26
  /**
27
27
  * Listen for start of the test run
28
28
  */
29
- onStart?(): void;
29
+ onStart?(files: string[]): Promise<void> | void;
30
30
  /**
31
31
  * Handle individual tests events
32
32
  */
@@ -34,5 +34,5 @@ export interface TestConsumer {
34
34
  /**
35
35
  * Summarize all results
36
36
  */
37
- onSummary?(summary: SuitesSummary): void;
37
+ onSummary?(summary: SuitesSummary): Promise<void> | void;
38
38
  }
@@ -1,4 +1,4 @@
1
- import { ErrorUtil } from '@travetto/base/src/internal/error';
1
+ import { ErrorUtil } from '@travetto/base';
2
2
  import { TestEvent, } from '../model/event';
3
3
 
4
4
  export class ConsumerUtil {
@@ -1,3 +1,4 @@
1
+ import { RootIndex } from '@travetto/manifest';
1
2
  import { Class, ClassInstance } from '@travetto/base';
2
3
 
3
4
  import { SuiteRegistry } from '../registry/suite';
@@ -8,7 +9,7 @@ export type SuitePhase = 'beforeAll' | 'beforeEach' | 'afterAll' | 'afterEach';
8
9
  /**
9
10
  * Register a class to be defined as a test suite, and a candidate for testing
10
11
  * @param description The Suite description
11
- * @augments `@trv:test/Suite`
12
+ * @augments `@travetto/test:Suite`
12
13
  */
13
14
  export function Suite(): ClassDecorator;
14
15
  export function Suite(...rest: Partial<SuiteConfig>[]): ClassDecorator;
@@ -26,7 +27,7 @@ export function Suite(description?: string | Partial<SuiteConfig>, ...rest: Part
26
27
  // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
27
28
  const decorator = ((target: Class) => {
28
29
  const cfg = { description: descriptionString, ...extra };
29
- if (target.ᚕabstract) {
30
+ if (RootIndex.getFunctionMetadata(target)?.abstract) {
30
31
  cfg.skip = true;
31
32
  }
32
33
  SuiteRegistry.register(target, cfg);
@@ -1,12 +1,21 @@
1
1
  import { ClassInstance } from '@travetto/base';
2
+ import { RootIndex } from '@travetto/manifest';
2
3
 
3
4
  import { SuiteRegistry } from '../registry/suite';
4
5
  import { TestConfig } from '../model/test';
5
6
 
7
+ /**
8
+ * The `@AssertCheck` indicates that a function's assert calls should be transformed
9
+ */
10
+ export function AssertCheck(): MethodDecorator {
11
+ return (inst: ClassInstance, prop: string | symbol, descriptor: PropertyDescriptor) => descriptor;
12
+ }
13
+
6
14
  /**
7
15
  * The `@Test` decorator register a test to be run as part of the enclosing suite.
8
16
  * @param description The test description
9
- * @augments `@trv:test/Test`
17
+ * @augments `@travetto/test:Test`
18
+ * @augments `@travetto/test:AssertCheck`
10
19
  */
11
20
  export function Test(): MethodDecorator;
12
21
  export function Test(...rest: Partial<TestConfig>[]): MethodDecorator;
@@ -23,7 +32,7 @@ export function Test(description?: string | Partial<TestConfig>, ...rest: Partia
23
32
  return (inst: ClassInstance, prop: string | symbol, descriptor: PropertyDescriptor) => {
24
33
  SuiteRegistry.registerField(inst.constructor, descriptor.value, {
25
34
  ...extra,
26
- file: inst.constructor.ᚕfile,
35
+ file: RootIndex.getFunctionMetadata(inst.constructor)!.source,
27
36
  description: descriptionString
28
37
  });
29
38
  return descriptor;
@@ -1,5 +1,6 @@
1
- import * as util from 'util';
2
- import { ConsoleManager, LogLevel } from '@travetto/base';
1
+ import util from 'util';
2
+
3
+ import { ConsoleEvent, ConsoleManager } from '@travetto/base';
3
4
 
4
5
  /**
5
6
  * Console capturer. Hooks into the Console manager, and collects the
@@ -14,7 +15,7 @@ export class ConsoleCapture {
14
15
  ConsoleManager.set(this);
15
16
  }
16
17
 
17
- static onLog(level: LogLevel, ctx: { file: string, line: number }, args: unknown[]): void {
18
+ static onLog({ level, args }: ConsoleEvent): void {
18
19
  (this.out[level] = this.out[level] ?? []).push(
19
20
  args
20
21
  .map((x => typeof x === 'string' ? x : util.inspect(x, false, 4)))
@@ -1,12 +1,8 @@
1
- import * as path from 'path';
2
- import * as timers from 'timers/promises';
3
- import * as fs from 'fs/promises';
1
+ import timers from 'timers/promises';
4
2
 
5
- import { Util } from '@travetto/base';
6
- import { ExecUtil, PathUtil } from '@travetto/boot';
3
+ import { path, RootIndex } from '@travetto/manifest';
4
+ import { TimeUtil, Util } from '@travetto/base';
7
5
  import { Barrier, ExecutionError } from '@travetto/worker';
8
- import { SystemUtil } from '@travetto/boot/src/internal/system';
9
- import { ModuleUtil } from '@travetto/boot/src/internal/module-util';
10
6
 
11
7
  import { SuiteRegistry } from '../registry/suite';
12
8
  import { TestConfig, TestResult } from '../model/test';
@@ -18,9 +14,8 @@ import { ConsoleCapture } from './console';
18
14
  import { TestPhaseManager } from './phase';
19
15
  import { PromiseCapture } from './promise';
20
16
  import { AssertUtil } from '../assert/util';
21
- import { TestEvent } from '../model/event';
22
17
 
23
- const TEST_TIMEOUT = Util.getEnvTime('TRV_TEST_TIMEOUT', '5s');
18
+ const TEST_TIMEOUT = TimeUtil.getEnvTime('TRV_TEST_TIMEOUT', '5s');
24
19
 
25
20
  /**
26
21
  * Support execution of the tests
@@ -32,7 +27,7 @@ export class TestExecutor {
32
27
  *
33
28
  * This method should never throw under any circumstances.
34
29
  */
35
- static #executeTestMethod(test: TestConfig): Promise<Error | undefined> {
30
+ static async #executeTestMethod(test: TestConfig): Promise<Error | undefined> {
36
31
  const suite = SuiteRegistry.get(test.class);
37
32
  const promCleanup = Util.resolvablePromise();
38
33
 
@@ -40,11 +35,15 @@ export class TestExecutor {
40
35
  const barrier = new Barrier(test.timeout || TEST_TIMEOUT, true)
41
36
  .add(promCleanup, true) // If not timeout or unhandled, ensure all promises are cleaned up
42
37
  .add(async () => {
38
+ const env = process.env;
39
+
43
40
  try {
44
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
45
43
  // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
46
44
  await (suite.instance as Record<string, Function>)[test.methodName](); // Run
47
45
  } finally {
46
+ process.env = env; // Restore
48
47
  PromiseCapture.stop().then(() => timers.setTimeout(1).then(promCleanup.resolve), promCleanup.reject);
49
48
  }
50
49
  });
@@ -69,10 +68,10 @@ export class TestExecutor {
69
68
  */
70
69
  static failFile(consumer: TestConsumer, file: string, err: Error): void {
71
70
  const name = path.basename(file);
72
- const classId = ModuleUtil.getId(file, name);
71
+ const classId = RootIndex.getId(file, name);
73
72
  // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
74
73
  const suite = { class: { name }, classId, duration: 0, lines: { start: 1, end: 1 }, file, } as SuiteConfig & SuiteResult;
75
- err.message = err.message.replace(PathUtil.cwd, '.');
74
+ err.message = err.message.replace(path.cwd(), '.');
76
75
  const res = AssertUtil.generateSuiteError(suite, 'require', err);
77
76
  consumer.onEvent({ type: 'suite', phase: 'before', suite });
78
77
  consumer.onEvent({ type: 'test', phase: 'before', test: res.testConfig });
@@ -110,6 +109,7 @@ export class TestExecutor {
110
109
 
111
110
  const result: TestResult = {
112
111
  methodName: test.methodName,
112
+ module: RootIndex.manifest.mainModule,
113
113
  description: test.description,
114
114
  classId: test.classId,
115
115
  lines: { ...test.lines },
@@ -138,6 +138,7 @@ export class TestExecutor {
138
138
 
139
139
  // Run method and get result
140
140
  let error = await this.#executeTestMethod(test);
141
+
141
142
  if (error) {
142
143
  if (error instanceof ExecutionError) { // Errors that are not expected
143
144
  AssertCheck.checkUnhandled(test, error);
@@ -197,11 +198,17 @@ export class TestExecutor {
197
198
 
198
199
  const mgr = new TestPhaseManager(consumer, suite, result);
199
200
 
201
+ const originalEnv = { ...process.env };
202
+
200
203
  try {
201
204
  // Handle the BeforeAll calls
202
205
  await mgr.startPhase('all');
203
206
 
207
+ const suiteEnv = { ...process.env };
208
+
204
209
  for (const test of suite.tests) {
210
+ // Reset env before each test
211
+ process.env = { ...suiteEnv };
205
212
  const testStart = Date.now();
206
213
  const skip = await this.#skip(test, suite.instance);
207
214
  if (!skip) {
@@ -227,6 +234,9 @@ export class TestExecutor {
227
234
  await mgr.onError(err);
228
235
  }
229
236
 
237
+ // Restore env
238
+ process.env = { ...originalEnv };
239
+
230
240
  result.duration = Date.now() - startTime;
231
241
 
232
242
  // Mark suite complete
@@ -242,12 +252,12 @@ export class TestExecutor {
242
252
  */
243
253
  static async execute(consumer: TestConsumer, file: string, ...args: string[]): Promise<void> {
244
254
 
245
- if (!file.startsWith(PathUtil.cwd)) {
246
- file = PathUtil.joinUnix(PathUtil.cwd, file);
247
- }
255
+ file = path.resolve(file);
256
+
257
+ const entry = RootIndex.getEntry(file)!;
248
258
 
249
259
  try {
250
- await import(PathUtil.toUnix(file)); // Path to module
260
+ await import(entry.import);
251
261
  } catch (err) {
252
262
  if (!(err instanceof Error)) {
253
263
  throw err;
@@ -275,28 +285,4 @@ export class TestExecutor {
275
285
  await this.executeSuite(consumer, params.suite);
276
286
  }
277
287
  }
278
-
279
- /**
280
- * Execute isolated
281
- */
282
- static async executeIsolated(consumer: TestConsumer, file: string, ...args: string[]): Promise<void> {
283
- // Read modules for extensions
284
- const modules = [...(await fs.readFile(file, 'utf8'))
285
- .matchAll(/\/\/\s*@with-module\s+(@travetto\/[A-Za-z0-9\-]+)/g)]
286
- .map(x => x[1]);
287
-
288
- const proc = ExecUtil.forkMain(require.resolve('../../bin/test-direct'), [file, ...args], {
289
- env: {
290
- TRV_MODULES: modules.join(','),
291
- TRV_RESOURCES: process.env.TRV_RESOURCES,
292
- TRV_PROFILES: process.env.TRV_PROFILES,
293
- TRV_SRC_LOCAL: '^test-isolated',
294
- TRV_SRC_COMMON: process.env.TRV_SRC_COMMON,
295
- TRV_TEST_FORMAT: 'exec',
296
- TRV_CACHE: `.trv_cache_${SystemUtil.naiveHash(file)}`
297
- }
298
- });
299
- proc.process.on('message', (e: TestEvent) => consumer.onEvent(e));
300
- await proc.result;
301
- }
302
288
  }
@@ -1,5 +1,5 @@
1
1
  import { Barrier } from '@travetto/worker';
2
- import { Util } from '@travetto/base';
2
+ import { 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 = Util.getEnvTime('TRV_TEST_PHASE_TIMEOUT', '15s');
11
+ const TEST_PHASE_TIMEOUT = TimeUtil.getEnvTime('TRV_TEST_PHASE_TIMEOUT', '15s');
12
12
 
13
13
  /**
14
14
  * Test Phase Execution Manager.
@@ -1,14 +1,13 @@
1
- import { PathUtil } from '@travetto/boot';
2
- import { PhaseManager, Util } from '@travetto/base';
1
+ import { path } from '@travetto/manifest';
2
+ import { TimeUtil } from '@travetto/base';
3
3
  import { WorkPool, IterableWorkSet } from '@travetto/worker';
4
4
 
5
- import { TestExecutor } from './executor';
6
5
  import { buildStandardTestManager } from '../worker/standard';
7
- import { buildIsolatedTestManager } from '../worker/isolated';
6
+ import { RunnableTestConsumer } from '../consumer/types/runnable';
8
7
 
8
+ import { TestExecutor } from './executor';
9
9
  import { RunnerUtil } from './util';
10
10
  import { RunState } from './types';
11
- import { RunnableTestConsumer } from '../consumer/types/runnable';
12
11
 
13
12
  /**
14
13
  * Test Runner
@@ -22,37 +21,29 @@ export class Runner {
22
21
  }
23
22
 
24
23
  get patterns(): RegExp[] {
25
- return this.#state.args.map(x => new RegExp(PathUtil.toUnix(x)));
24
+ return this.#state.args.map(x => new RegExp(path.toPosix(x)));
26
25
  }
27
26
 
28
27
  /**
29
28
  * Run all files
30
29
  */
31
30
  async runFiles(): Promise<boolean> {
32
- const consumer = RunnableTestConsumer.get(this.#state.consumer ?? this.#state.format);
33
-
34
- const files = (await RunnerUtil.getTestFiles(this.patterns, this.#state.isolated ? 'test-isolated' : 'test'));
31
+ const consumer = await RunnableTestConsumer.get(this.#state.consumer ?? this.#state.format);
35
32
 
36
- console.debug('Running', { files });
33
+ const files = (await RunnerUtil.getTestFiles(this.patterns));
37
34
 
38
- await PhaseManager.run('test');
35
+ console.debug('Running', { files, patterns: this.patterns });
39
36
 
40
- const manager = this.#state.isolated ?
41
- buildIsolatedTestManager :
42
- buildStandardTestManager;
37
+ const manager = buildStandardTestManager;
43
38
 
44
39
  const pool = new WorkPool(manager(consumer), {
45
- idleTimeoutMillis: Util.timeToMs('10s'),
40
+ idleTimeoutMillis: TimeUtil.timeToMs('10s'),
46
41
  min: 1,
47
42
  max: this.#state.concurrency
48
43
  });
49
44
 
50
- consumer.onStart();
51
-
52
- await pool
53
- .process(new IterableWorkSet(files))
54
- .finally(() => pool.shutdown());
55
-
45
+ await consumer.onStart(files);
46
+ await pool.process(new IterableWorkSet(files));
56
47
  return consumer.summarizeAsBoolean();
57
48
  }
58
49
 
@@ -60,18 +51,12 @@ export class Runner {
60
51
  * Run a single file
61
52
  */
62
53
  async runSingle(): Promise<boolean> {
63
- const consumer = RunnableTestConsumer.get(this.#state.consumer ?? this.#state.format);
64
- consumer.onStart();
54
+ const consumer = await RunnableTestConsumer.get(this.#state.consumer ?? this.#state.format);
65
55
 
66
56
  const [file, ...args] = this.#state.args;
67
57
 
68
- await PhaseManager.run('test');
69
- if (this.#state.isolated) {
70
- await TestExecutor.executeIsolated(consumer, file, ...args);
71
- } else {
72
- await TestExecutor.execute(consumer, file, ...args);
73
- }
74
-
58
+ await consumer.onStart([path.resolve(file)]);
59
+ await TestExecutor.execute(consumer, file, ...args);
75
60
  return consumer.summarizeAsBoolean();
76
61
  }
77
62
 
@@ -17,13 +17,13 @@ export interface RunState {
17
17
  */
18
18
  mode?: 'single' | 'watch' | 'standard';
19
19
  /**
20
- * Number of test suites to run concurrently, when mode is not single
20
+ * Show progress to stderr
21
21
  */
22
- concurrency: number;
22
+ showProgress?: boolean;
23
23
  /**
24
- * Run in isolated mode?
24
+ * Number of test suites to run concurrently, when mode is not single
25
25
  */
26
- isolated?: boolean;
26
+ concurrency: number;
27
27
  /**
28
28
  * Input arguments
29
29
  */
@@ -1,8 +1,8 @@
1
1
  import { createReadStream } from 'fs';
2
- import * as readline from 'readline';
2
+ import readline from 'readline';
3
3
 
4
- import { ShutdownManager, Util } from '@travetto/base';
5
- import { SourceIndex } from '@travetto/boot/src/internal/source';
4
+ import { ShutdownManager, TimeUtil } from '@travetto/base';
5
+ import { RootIndex } from '@travetto/manifest';
6
6
 
7
7
  /**
8
8
  * Simple Test Utilities
@@ -12,7 +12,7 @@ export class RunnerUtil {
12
12
  * Add 50 ms to the shutdown to allow for buffers to output properly
13
13
  */
14
14
  static registerCleanup(scope: string): void {
15
- ShutdownManager.onShutdown(`test.${scope}.bufferOutput`, () => Util.wait(50));
15
+ ShutdownManager.onShutdown(`test.${scope}.bufferOutput`, () => TimeUtil.wait(50));
16
16
  }
17
17
 
18
18
  /**
@@ -36,12 +36,12 @@ export class RunnerUtil {
36
36
  /**
37
37
  * Find all valid test files given the globs
38
38
  */
39
- static async getTestFiles(globs: RegExp[], root = 'test'): Promise<string[]> {
40
- const files = SourceIndex.find({ folder: root, paths: ['.'] })
41
- .filter(f => globs.some(g => g.test(f.module)));
39
+ static async getTestFiles(globs: RegExp[]): Promise<string[]> {
40
+ const files = RootIndex.findTest({})
41
+ .filter(f => globs.some(g => g.test(f.import)));
42
42
 
43
43
  const validFiles = files
44
- .map(f => this.isTestFile(f.file).then(valid => ({ file: f.file, valid })));
44
+ .map(f => this.isTestFile(f.source).then(valid => ({ file: f.source, valid })));
45
45
 
46
46
  return (await Promise.all(validFiles))
47
47
  .filter(x => x.valid)
@@ -1,13 +1,18 @@
1
- import * as os from 'os';
2
-
3
1
  import { RootRegistry, MethodSource } from '@travetto/registry';
4
2
  import { WorkPool, IterableWorkSet, ManualAsyncIterator } from '@travetto/worker';
3
+ import { RootIndex } from '@travetto/manifest';
4
+ import { ObjectUtil } from '@travetto/base';
5
+ import { DynamicFileLoader } from '@travetto/base/src/internal/file-loader';
5
6
 
6
7
  import { SuiteRegistry } from '../registry/suite';
7
8
  import { buildStandardTestManager } from '../worker/standard';
8
- import { RunEvent } from '../worker/types';
9
9
  import { TestConsumerRegistry } from '../consumer/registry';
10
10
  import { CumulativeSummaryConsumer } from '../consumer/types/cumulative';
11
+ import { RunEvent } from '../worker/types';
12
+
13
+ function isRunEvent(ev: unknown): ev is RunEvent {
14
+ return ObjectUtil.isPlainObject(ev) && 'type' in ev && typeof ev.type === 'string' && ev.type === 'run-test';
15
+ }
11
16
 
12
17
  /**
13
18
  * Test Watcher.
@@ -19,53 +24,71 @@ export class TestWatcher {
19
24
  /**
20
25
  * Start watching all test files
21
26
  */
22
- static async watch(format: string): Promise<void> {
27
+ static async watch(format: string, runAllOnStart = true): Promise<void> {
23
28
  console.debug('Listening for changes');
24
29
 
25
- const itr = new ManualAsyncIterator<RunEvent>();
30
+ const itr = new ManualAsyncIterator<string>();
26
31
  const src = new IterableWorkSet(itr);
27
32
 
28
33
  await SuiteRegistry.init();
29
34
  SuiteRegistry.listen(RootRegistry);
30
35
 
31
- const consumer = new CumulativeSummaryConsumer(TestConsumerRegistry.getInstance(format));
36
+ const consumer = new CumulativeSummaryConsumer(await TestConsumerRegistry.getInstance(format));
32
37
  const pool = new WorkPool(buildStandardTestManager(consumer), {
33
38
  idleTimeoutMillis: 120000,
34
39
  min: 2,
35
- max: os.cpus.length - 1
40
+ max: WorkPool.DEFAULT_SIZE
36
41
  });
37
42
 
38
43
  new MethodSource(RootRegistry).on(e => {
39
44
  const [cls, method] = (e.prev ?? e.curr ?? []);
40
- if (!cls) {
45
+ if (!cls || RootIndex.getFunctionMetadata(cls)?.abstract) {
41
46
  return;
42
47
  }
43
48
  if (!method) {
44
- consumer.removeClass(cls.ᚕid);
49
+ consumer.removeClass(cls.Ⲑid);
45
50
  return;
46
51
  }
47
52
  const conf = SuiteRegistry.getByClassAndMethod(cls, method)!;
48
53
  if (e.type !== 'removing') {
49
54
  if (conf) {
50
- itr.add({
51
- method: conf.methodName,
52
- file: conf.file,
53
- class: conf.class.name
54
- }, true); // Shift to front
55
+ const key = `${conf.file}#${conf.class.name}#${conf.methodName}`;
56
+ itr.add(key, true); // Shift to front
55
57
  }
56
- } else if (process.send) {
57
- process.send({
58
+ } else {
59
+ process.send?.({
58
60
  type: 'removeTest',
59
61
  method: method?.name,
60
- classId: cls?.ᚕid,
61
- file: cls?.ᚕfile,
62
+ classId: cls?.Ⲑid,
63
+ file: RootIndex.getFunctionMetadata(cls)?.source
62
64
  });
63
65
  }
64
66
  });
65
67
 
66
68
  await RootRegistry.init();
67
69
 
68
- await pool.process(src)
69
- .finally(() => pool.shutdown());
70
+ process.send?.({ type: 'watch-init' });
71
+
72
+ process.on('message', ev => {
73
+ if (isRunEvent(ev)) {
74
+ itr.add([ev.file, ev.class, ev.method].filter(x => !!x).join('#'), true);
75
+ }
76
+ });
77
+
78
+ // Re-run all tests on file change
79
+ DynamicFileLoader.onLoadEvent(ev => {
80
+ if (ev.action === 'update') {
81
+ itr.add(ev.file);
82
+ }
83
+ });
84
+
85
+ if (runAllOnStart) {
86
+ for (const test of await RootIndex.findTest({})) {
87
+ await import(test.output);
88
+ itr.add(test.source);
89
+ }
90
+ }
91
+
92
+ await pool.process(src);
70
93
  }
71
94
  }
package/src/fixture.ts ADDED
@@ -0,0 +1,10 @@
1
+ import { FileResourceProvider } from '@travetto/base';
2
+
3
+ export class TestFixtures extends FileResourceProvider {
4
+ moduleFolder = 'support/fixtures';
5
+ mainFolder = 'test/fixtures';
6
+
7
+ constructor(paths: string[] = []) {
8
+ super(['@', ...paths,]);
9
+ }
10
+ }
@@ -5,6 +5,10 @@ export type Skip = boolean | ((instance: unknown) => boolean | Promise<boolean>)
5
5
  * Core Suite definition
6
6
  */
7
7
  export interface SuiteCore {
8
+ /**
9
+ * The module the test is declared in
10
+ */
11
+ module: string;
8
12
  /**
9
13
  * The class id
10
14
  */