@travetto/test 7.0.0-rc.1 → 7.0.0-rc.3

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 (46) hide show
  1. package/README.md +7 -8
  2. package/__index__.ts +1 -0
  3. package/package.json +7 -7
  4. package/src/assert/check.ts +46 -46
  5. package/src/assert/util.ts +31 -31
  6. package/src/communication.ts +66 -0
  7. package/src/consumer/registry-index.ts +11 -11
  8. package/src/consumer/types/cumulative.ts +91 -62
  9. package/src/consumer/types/delegating.ts +30 -27
  10. package/src/consumer/types/event.ts +11 -4
  11. package/src/consumer/types/exec.ts +12 -3
  12. package/src/consumer/types/runnable.ts +4 -3
  13. package/src/consumer/types/summarizer.ts +12 -10
  14. package/src/consumer/types/tap-summary.ts +22 -20
  15. package/src/consumer/types/tap.ts +15 -15
  16. package/src/consumer/types/xunit.ts +15 -15
  17. package/src/consumer/types.ts +6 -2
  18. package/src/decorator/suite.ts +2 -2
  19. package/src/decorator/test.ts +6 -4
  20. package/src/execute/barrier.ts +8 -8
  21. package/src/execute/console.ts +1 -1
  22. package/src/execute/executor.ts +32 -21
  23. package/src/execute/phase.ts +7 -7
  24. package/src/execute/run.ts +247 -0
  25. package/src/execute/types.ts +2 -17
  26. package/src/execute/watcher.ts +33 -60
  27. package/src/fixture.ts +2 -2
  28. package/src/model/common.ts +4 -0
  29. package/src/model/event.ts +3 -1
  30. package/src/model/suite.ts +10 -21
  31. package/src/model/test.ts +48 -2
  32. package/src/model/util.ts +8 -0
  33. package/src/registry/registry-adapter.ts +23 -21
  34. package/src/registry/registry-index.ts +25 -25
  35. package/src/worker/child.ts +21 -21
  36. package/src/worker/standard.ts +28 -19
  37. package/src/worker/types.ts +9 -5
  38. package/support/bin/run.ts +10 -10
  39. package/support/cli.test.ts +20 -41
  40. package/support/cli.test_diff.ts +47 -0
  41. package/support/cli.test_digest.ts +7 -7
  42. package/support/cli.test_direct.ts +13 -12
  43. package/support/cli.test_watch.ts +3 -8
  44. package/support/transformer.assert.ts +12 -12
  45. package/src/execute/runner.ts +0 -87
  46. package/src/execute/util.ts +0 -108
@@ -1,26 +1,26 @@
1
1
  import { fork } from 'node:child_process';
2
2
 
3
- import { Env, RuntimeIndex, Util } from '@travetto/runtime';
3
+ import { Env, RuntimeIndex } from '@travetto/runtime';
4
4
  import { IpcChannel } from '@travetto/worker';
5
5
 
6
- import { Events, TestLogEvent } from './types.ts';
7
- import { TestConsumerShape } from '../consumer/types.ts';
8
- import { TestEvent } from '../model/event.ts';
9
- import { TestRun } from '../model/test.ts';
6
+ import { TestWorkerEvents, type TestLogEvent } from './types.ts';
7
+ import type { TestConsumerShape } from '../consumer/types.ts';
8
+ import type { TestEvent, TestRemoveEvent } from '../model/event.ts';
9
+ import type { TestDiffInput, TestRun } from '../model/test.ts';
10
+ import { CommunicationUtil } from '../communication.ts';
10
11
 
11
- const log = (message: string): void => {
12
- process.send?.({ type: 'log', message } satisfies TestLogEvent);
12
+ const log = (message: string | TestLogEvent): void => {
13
+ const event: TestLogEvent = typeof message === 'string' ? { type: 'log', message } : message;
14
+ process.send ? process.send?.(event) : console.debug(event.message);
13
15
  };
14
16
 
15
17
  /**
16
18
  * Produce a handler for the child worker
17
19
  */
18
- export async function buildStandardTestManager(consumer: TestConsumerShape, run: TestRun): Promise<void> {
20
+ export async function buildStandardTestManager(consumer: TestConsumerShape, run: TestRun | TestDiffInput): Promise<void> {
19
21
  log(`Worker Input ${JSON.stringify(run)}`);
20
- log(`Worker Executing ${run.import}`);
21
22
 
22
- const { module } = RuntimeIndex.getFromImport(run.import)!;
23
- const suiteMod = RuntimeIndex.getModule(module)!;
23
+ const suiteMod = RuntimeIndex.findModuleForArbitraryImport(run.import)!;
24
24
 
25
25
  const channel = new IpcChannel<TestEvent & { error?: Error }>(
26
26
  fork(
@@ -37,25 +37,34 @@ export async function buildStandardTestManager(consumer: TestConsumerShape, run:
37
37
  )
38
38
  );
39
39
 
40
- await channel.once(Events.READY); // Wait for the child to be ready
41
- await channel.send(Events.INIT); // Initialize
42
- await channel.once(Events.INIT_COMPLETE); // Wait for complete
40
+ await channel.once(TestWorkerEvents.READY); // Wait for the child to be ready
41
+ await channel.send(TestWorkerEvents.INIT); // Initialize
42
+ await channel.once(TestWorkerEvents.INIT_COMPLETE); // Wait for complete
43
43
 
44
- channel.on('*', async ev => {
44
+ channel.on('*', async event => {
45
45
  try {
46
- await consumer.onEvent(Util.deserializeFromJson(JSON.stringify(ev))); // Connect the consumer with the event stream from the child
46
+ const parsed: TestEvent | TestRemoveEvent | TestLogEvent = CommunicationUtil.deserializeFromObject(event);
47
+ if (parsed.type === 'log') {
48
+ log(parsed);
49
+ } else if (parsed.type === 'removeTest') {
50
+ log(`Received remove event ${JSON.stringify(event)}@${consumer.constructor.name}`);
51
+ await consumer.onRemoveEvent?.(parsed); // Forward remove events
52
+ } else {
53
+ await consumer.onEvent(parsed); // Forward standard events
54
+ }
47
55
  } catch {
48
56
  // Do nothing
49
57
  }
50
58
  });
51
59
 
52
60
  // Listen for child to complete
53
- const complete = channel.once(Events.RUN_COMPLETE);
61
+ const complete = channel.once(TestWorkerEvents.RUN_COMPLETE);
54
62
  // Start test
55
- channel.send(Events.RUN, run);
63
+ channel.send(TestWorkerEvents.RUN, run);
56
64
 
57
65
  // Wait for complete
58
- const result = await complete.then(ev => Util.deserializeFromJson<typeof ev>(JSON.stringify(ev)));
66
+ const completedEvent = await complete;
67
+ const result: { error?: unknown } = await CommunicationUtil.deserializeFromObject(completedEvent);
59
68
 
60
69
  // Kill on complete
61
70
  await channel.destroy();
@@ -1,10 +1,9 @@
1
- import { TestEvent } from '../model/event.ts';
2
- import { TestRun } from '../model/test.ts';
1
+ import { TestEvent, TestRemoveEvent } from '../model/event.ts';
3
2
 
4
3
  /**
5
4
  * Test Run Event Keys
6
5
  */
7
- export const Events = {
6
+ export const TestWorkerEvents = {
8
7
  RUN: 'run',
9
8
  RUN_COMPLETE: 'runComplete',
10
9
  INIT: 'init',
@@ -12,12 +11,17 @@ export const Events = {
12
11
  READY: 'ready'
13
12
  };
14
13
 
15
- export type TestRemovedEvent = { type: 'removeTest', method?: string } & TestRun;
14
+ export type TestRunEvent = { type: 'runTest', import: string };
15
+
16
+ export const isTestRunEvent = (event: unknown): event is TestRunEvent =>
17
+ typeof event === 'object' && !!event && 'type' in event && event.type === 'runTest';
18
+
19
+
16
20
  export type TestReadyEvent = { type: 'ready' };
17
21
  export type TestLogEvent = { type: 'log', message: string };
18
22
 
19
23
  export type TestWatchEvent =
20
24
  TestEvent |
21
- TestRemovedEvent |
25
+ TestRemoveEvent |
22
26
  TestReadyEvent |
23
27
  TestLogEvent;
@@ -2,23 +2,23 @@ import { getClass, Runtime } from '@travetto/runtime';
2
2
  import { SchemaRegistryIndex } from '@travetto/schema';
3
3
 
4
4
  import { TestConsumerRegistryIndex } from '../../src/consumer/registry-index.ts';
5
- import type { RunState } from '../../src/execute/types.ts';
5
+ import type { TestConsumerConfig } from '../../src/execute/types.ts';
6
+ import type { TestRunInput } from '../../src/model/test.ts';
6
7
 
7
8
  /**
8
9
  * Run tests given the input state
9
- * @param opts
10
+ * @param state
10
11
  */
11
- export async function runTests(opts: RunState): Promise<void> {
12
- const { RunnerUtil } = await import('../../src/execute/util.ts');
13
- const { Runner } = await import('../../src/execute/runner.ts');
12
+ export async function runTests(state: TestConsumerConfig, input: TestRunInput): Promise<void> {
13
+ const { RunUtil } = await import('../../src/execute/run.ts');
14
14
 
15
- RunnerUtil.registerCleanup('runner');
15
+ RunUtil.registerCleanup('runner');
16
16
 
17
17
  try {
18
- const res = await new Runner(opts).run();
19
- process.exitCode = res ? 0 : 1;
20
- } catch (err) {
21
- console.error('Test Worker Failed', { error: err });
18
+ const result = await RunUtil.runTests(state, input);
19
+ process.exitCode = result ? 0 : 1;
20
+ } catch (error) {
21
+ console.error('Test Worker Failed', { error });
22
22
  process.exitCode = 1;
23
23
  }
24
24
  }
@@ -1,9 +1,7 @@
1
1
  import { EventEmitter } from 'node:events';
2
- import fs from 'node:fs/promises';
3
- import path from 'node:path';
4
2
 
5
- import { Env } from '@travetto/runtime';
6
- import { CliCommandShape, CliCommand, CliValidationError } from '@travetto/cli';
3
+ import { Env, RuntimeIndex } from '@travetto/runtime';
4
+ import { CliCommandShape, CliCommand, CliUtil } from '@travetto/cli';
7
5
  import { WorkPool } from '@travetto/worker';
8
6
  import { Max, Min } from '@travetto/schema';
9
7
 
@@ -20,11 +18,8 @@ export class TestCommand implements CliCommandShape {
20
18
  @Min(1) @Max(WorkPool.MAX_SIZE)
21
19
  concurrency: number = WorkPool.DEFAULT_SIZE;
22
20
 
23
- /** Test run mode */
24
- mode: 'single' | 'standard' = 'standard';
25
-
26
21
  /**
27
- * Tags to target or exclude
22
+ * Tags to target or exclude when using globs
28
23
  * @alias env.TRV_TEST_TAGS
29
24
  */
30
25
  tags?: string[];
@@ -44,46 +39,30 @@ export class TestCommand implements CliCommandShape {
44
39
  Env.TRV_LOG_TIME.clear();
45
40
  }
46
41
 
47
- isFirstFile(first: string): Promise<boolean> {
48
- return fs.stat(path.resolve(first ?? '')).then(x => x.isFile(), () => false);
49
- }
50
-
51
- async resolvedMode(first: string, rest: string[]): Promise<string> {
52
- return (await this.isFirstFile(first)) && rest.length === 0 ? 'single' : this.mode;
53
- }
54
-
55
42
  async preValidate(): Promise<void> {
56
43
  const { selectConsumer } = await import('./bin/run.ts');
57
44
  await selectConsumer(this);
58
45
  }
59
46
 
60
- async validate(first: string = '**/*', rest: string[]): Promise<CliValidationError | undefined> {
61
- const mode = await this.resolvedMode(first, rest);
62
-
63
- if (mode === 'single' && !await this.isFirstFile(first)) {
64
- return { message: 'You must specify a proper test file to run in single mode', source: 'arg' };
65
- }
66
- }
67
-
68
47
  async main(first: string = '**/*', globs: string[] = []): Promise<void> {
69
48
  const { runTests } = await import('./bin/run.ts');
70
49
 
71
- const isFirst = await this.isFirstFile(first);
72
- const isSingle = this.mode === 'single' || (isFirst && globs.length === 0);
73
- const options = Object.fromEntries((this.formatOptions ?? [])?.map(f => [...f.split(':'), true]));
74
-
75
- return runTests({
76
- concurrency: this.concurrency,
77
- consumer: this.format,
78
- consumerOptions: options,
79
- tags: this.tags,
80
- target: isSingle ?
81
- {
82
- import: first,
83
- classId: globs[0],
84
- methodNames: globs.slice(1),
85
- } :
86
- { globs: [first, ...globs], }
87
- });
50
+ const importPath = RuntimeIndex.getFromImportOrSource(first)?.import;
51
+
52
+ return runTests(
53
+ {
54
+ concurrency: this.concurrency,
55
+ consumer: this.format,
56
+ consumerOptions: CliUtil.readExtendedOptions(this.formatOptions),
57
+ },
58
+ importPath ? {
59
+ import: importPath,
60
+ classId: globs[0],
61
+ methodNames: globs.slice(1),
62
+ } : {
63
+ globs: [first, ...globs],
64
+ tags: this.tags,
65
+ }
66
+ );
88
67
  }
89
68
  }
@@ -0,0 +1,47 @@
1
+ import { Env, JSONUtil, RuntimeIndex } from '@travetto/runtime';
2
+ import { CliCommand, CliUtil } from '@travetto/cli';
3
+ import { IsPrivate } from '@travetto/schema';
4
+
5
+ import { runTests, selectConsumer } from './bin/run.ts';
6
+ import type { TestDiffSource } from '../src/model/test.ts';
7
+
8
+ /** Direct test invocation */
9
+ @CliCommand()
10
+ @IsPrivate()
11
+ export class TestDiffCommand {
12
+
13
+ format: string = 'tap';
14
+
15
+ /**
16
+ * Format options
17
+ * @alias o
18
+ */
19
+ formatOptions?: string[];
20
+
21
+ async preValidate(): Promise<void> {
22
+ await selectConsumer(this);
23
+ }
24
+
25
+ preMain(): void {
26
+ Env.TRV_ROLE.set('test');
27
+ Env.TRV_ENV.set('test');
28
+ Env.TRV_LOG_PLAIN.set(true);
29
+ Env.TRV_LOG_TIME.clear();
30
+ }
31
+
32
+ async main(importOrFile: string, diff: string): Promise<void> {
33
+ const diffSource: TestDiffSource = await JSONUtil.readFile(diff);
34
+ const importPath = RuntimeIndex.getFromImportOrSource(importOrFile)?.import!;
35
+
36
+ return runTests(
37
+ {
38
+ consumer: this.format,
39
+ consumerOptions: CliUtil.readExtendedOptions(this.formatOptions),
40
+ },
41
+ {
42
+ import: importPath,
43
+ diffSource
44
+ }
45
+ );
46
+ }
47
+ }
@@ -4,7 +4,7 @@ import { Registry } from '@travetto/registry';
4
4
  import { IsPrivate } from '@travetto/schema';
5
5
 
6
6
  import { SuiteRegistryIndex } from '../src/registry/registry-index.ts';
7
- import { RunnerUtil } from '../src/execute/util.ts';
7
+ import { RunUtil } from '../src/execute/run.ts';
8
8
 
9
9
  @CliCommand()
10
10
  @IsPrivate()
@@ -19,11 +19,11 @@ export class TestDigestCommand {
19
19
 
20
20
  async main(globs: string[] = ['**/*']) {
21
21
  // Load all tests
22
- for await (const imp of await RunnerUtil.getTestImports(globs)) {
22
+ for await (const imp of await RunUtil.getTestImports(globs)) {
23
23
  try {
24
24
  await Runtime.importFrom(imp);
25
- } catch (err) {
26
- console.error('Failed to import', imp, err);
25
+ } catch (error) {
26
+ console.error('Failed to import', imp, error);
27
27
  }
28
28
  }
29
29
 
@@ -31,9 +31,9 @@ export class TestDigestCommand {
31
31
 
32
32
  const suites = SuiteRegistryIndex.getClasses();
33
33
  const all = suites
34
- .map(c => SuiteRegistryIndex.getConfig(c))
35
- .filter(c => !describeFunction(c.class).abstract)
36
- .flatMap(c => Object.values(c.tests))
34
+ .map(cls => SuiteRegistryIndex.getConfig(cls))
35
+ .filter(config => !describeFunction(config.class).abstract)
36
+ .flatMap(config => Object.values(config.tests))
37
37
  .toSorted((a, b) => {
38
38
  const classComp = a.classId.localeCompare(b.classId);
39
39
  return classComp !== 0 ? classComp : a.methodName.localeCompare(b.methodName);
@@ -1,5 +1,5 @@
1
- import { Env } from '@travetto/runtime';
2
- import { CliCommand } from '@travetto/cli';
1
+ import { Env, RuntimeIndex } from '@travetto/runtime';
2
+ import { CliCommand, CliUtil } from '@travetto/cli';
3
3
  import { IsPrivate } from '@travetto/schema';
4
4
 
5
5
  import { runTests, selectConsumer } from './bin/run.ts';
@@ -9,7 +9,6 @@ import { runTests, selectConsumer } from './bin/run.ts';
9
9
  @IsPrivate()
10
10
  export class TestDirectCommand {
11
11
 
12
- @IsPrivate()
13
12
  format: string = 'tap';
14
13
 
15
14
  /**
@@ -30,17 +29,19 @@ export class TestDirectCommand {
30
29
  }
31
30
 
32
31
  main(importOrFile: string, clsId?: string, methodsNames: string[] = []): Promise<void> {
33
-
34
- const options = Object.fromEntries((this.formatOptions ?? [])?.map(f => [...f.split(':'), true]));
35
-
36
- return runTests({
37
- consumer: this.format,
38
- consumerOptions: options,
39
- target: {
40
- import: importOrFile,
32
+ // Resolve to import
33
+ const importPath = RuntimeIndex.getFromImportOrSource(importOrFile)?.import!;
34
+
35
+ return runTests(
36
+ {
37
+ consumer: this.format,
38
+ consumerOptions: CliUtil.readExtendedOptions(this.formatOptions),
39
+ },
40
+ {
41
+ import: importPath,
41
42
  classId: clsId,
42
43
  methodNames: methodsNames,
43
44
  }
44
- });
45
+ );
45
46
  }
46
47
  }
@@ -1,5 +1,5 @@
1
1
  import { Env } from '@travetto/runtime';
2
- import { CliCommand, CliUtil } from '@travetto/cli';
2
+ import { CliCommand } from '@travetto/cli';
3
3
 
4
4
  import { selectConsumer } from './bin/run.ts';
5
5
 
@@ -18,19 +18,14 @@ export class TestWatcherCommand {
18
18
 
19
19
  preMain(): void {
20
20
  Env.TRV_ROLE.set('test');
21
- Env.TRV_DYNAMIC.set(true);
22
21
  }
23
22
 
24
23
  async main(): Promise<void> {
25
- if (await CliUtil.runWithRestart(this, true)) {
26
- return;
27
- }
28
-
29
24
  try {
30
25
  const { TestWatcher } = await import('../src/execute/watcher.ts');
31
26
  await TestWatcher.watch(this.format, this.mode === 'all');
32
- } catch (err) {
33
- console.error(err);
27
+ } catch (error) {
28
+ console.error(error);
34
29
  }
35
30
  }
36
31
  }
@@ -96,10 +96,10 @@ export class AssertTransformer {
96
96
  static lookupOpToken(key: number): string | undefined {
97
97
  if (OP_TOKEN_TO_NAME.size === 0) {
98
98
  Object.keys(ts.SyntaxKind)
99
- .filter(x => !/^\d+$/.test(x))
100
- .filter((x): x is keyof typeof OPTOKEN_ASSERT => !/^(Last|First)/.test(x))
101
- .forEach(x =>
102
- OP_TOKEN_TO_NAME.set(ts.SyntaxKind[x], x));
99
+ .filter(kind => !/^\d+$/.test(kind))
100
+ .filter((kind): kind is keyof typeof OPTOKEN_ASSERT => !/^(Last|First)/.test(kind))
101
+ .forEach(kind =>
102
+ OP_TOKEN_TO_NAME.set(ts.SyntaxKind[kind], kind));
103
103
  }
104
104
 
105
105
  const name = OP_TOKEN_TO_NAME.get(key)!;
@@ -123,10 +123,10 @@ export class AssertTransformer {
123
123
 
124
124
  // If looking at an identifier, see if it's in a diff file or if its const
125
125
  if (!found && ts.isIdentifier(node)) {
126
- found = !!state.getDeclarations(node).find(x =>
126
+ found = !!state.getDeclarations(node).find(declaration =>
127
127
  // In a separate file or is const
128
- x.getSourceFile().fileName !== state.source.fileName ||
129
- DeclarationUtil.isConstantDeclaration(x));
128
+ declaration.getSourceFile().fileName !== state.source.fileName ||
129
+ DeclarationUtil.isConstantDeclaration(declaration));
130
130
  }
131
131
 
132
132
  return found;
@@ -137,7 +137,7 @@ export class AssertTransformer {
137
137
  */
138
138
  static initState(state: TransformerState & AssertState): void {
139
139
  if (!state[AssertSymbol]) {
140
- const asrt = state.importFile('@travetto/test/src/assert/check.ts').ident;
140
+ const asrt = state.importFile('@travetto/test/src/assert/check.ts').identifier;
141
141
  state[AssertSymbol] = {
142
142
  assert: asrt,
143
143
  assertCheck: CoreUtil.createAccess(state.factory, asrt, ASSERT_UTIL, 'check'),
@@ -156,7 +156,7 @@ export class AssertTransformer {
156
156
  const first = CoreUtil.firstArgument(node);
157
157
  const firstText = first?.getText() ?? node.getText();
158
158
 
159
- cmd.args = cmd.args.filter(x => x !== undefined && x !== null);
159
+ cmd.args = cmd.args.filter(arg => arg !== undefined && arg !== null);
160
160
  const check = state.factory.createCallExpression(state[AssertSymbol]!.assertCheck, undefined, state.factory.createNodeArray([
161
161
  state.fromLiteral({
162
162
  module: state.getModuleIdentifier(),
@@ -236,7 +236,7 @@ export class AssertTransformer {
236
236
  const matched = METHODS[key.text!];
237
237
  if (matched) {
238
238
  const resolved = state.resolveType(root);
239
- if (resolved.key === 'literal' && matched.find(x => resolved.ctor === x)) { // Ensure method is against real type
239
+ if (resolved.key === 'literal' && matched.find(type => resolved.ctor === type)) { // Ensure method is against real type
240
240
  switch (key.text) {
241
241
  case 'includes': return { fn: key.text, args: [comp.expression.expression, comp.arguments[0], ...args.slice(1)] };
242
242
  case 'test': return { fn: key.text, args: [comp.arguments[0], comp.expression.expression, ...args.slice(1)] };
@@ -298,9 +298,9 @@ export class AssertTransformer {
298
298
  }
299
299
  // If calling `assert.*`
300
300
  } else if (ts.isPropertyAccessExpression(exp) && ts.isIdentifier(exp.expression)) { // Assert method call
301
- const ident = exp.expression;
301
+ const identifier = exp.expression;
302
302
  const fn = exp.name.escapedText.toString();
303
- if (ident.escapedText === ASSERT_CMD) {
303
+ if (identifier.escapedText === ASSERT_CMD) {
304
304
  // Look for reject/throw
305
305
  if (fn === 'fail') {
306
306
  node = this.doAssert(state, node, { fn: 'fail', args: node.arguments.slice() });
@@ -1,87 +0,0 @@
1
- import path from 'node:path';
2
-
3
- import { TimeUtil, Runtime, RuntimeIndex } from '@travetto/runtime';
4
- import { WorkPool } from '@travetto/worker';
5
-
6
- import { buildStandardTestManager } from '../worker/standard.ts';
7
- import { RunnableTestConsumer } from '../consumer/types/runnable.ts';
8
- import { TestRun } from '../model/test.ts';
9
-
10
- import { TestExecutor } from './executor.ts';
11
- import { RunnerUtil } from './util.ts';
12
- import { RunState } from './types.ts';
13
- import { TestConsumerRegistryIndex } from '../consumer/registry-index.ts';
14
-
15
- /**
16
- * Test Runner
17
- */
18
- export class Runner {
19
-
20
- #state: RunState;
21
-
22
- constructor(state: RunState) {
23
- this.#state = state;
24
- }
25
-
26
- /**
27
- * Run all files
28
- */
29
- async runFiles(globs?: string[]): Promise<boolean> {
30
- const target = await TestConsumerRegistryIndex.getInstance(this.#state);
31
- const consumer = new RunnableTestConsumer(target);
32
- const tests = await RunnerUtil.getTestDigest(globs, this.#state.tags);
33
- const testRuns = RunnerUtil.getTestRuns(tests)
34
- .toSorted((a, b) => a.runId!.localeCompare(b.runId!));
35
-
36
- await consumer.onStart({ testCount: tests.length });
37
- await WorkPool.run(
38
- f => buildStandardTestManager(consumer, f),
39
- testRuns,
40
- {
41
- idleTimeoutMillis: TimeUtil.asMillis(10, 's'),
42
- min: 1,
43
- max: this.#state.concurrency
44
- });
45
-
46
- return consumer.summarizeAsBoolean();
47
- }
48
-
49
- /**
50
- * Run a single file
51
- */
52
- async runSingle(run: TestRun): Promise<boolean> {
53
- run.import =
54
- RuntimeIndex.getFromImport(run.import)?.import ??
55
- RuntimeIndex.getFromSource(path.resolve(run.import))?.import!;
56
-
57
- const entry = RuntimeIndex.getFromImport(run.import)!;
58
-
59
- if (entry.module !== Runtime.main.name) {
60
- RuntimeIndex.reinitForModule(entry.module);
61
- }
62
-
63
- const target = await TestConsumerRegistryIndex.getInstance(this.#state);
64
-
65
- const consumer = new RunnableTestConsumer(target)
66
- .withTransformer(e => {
67
- // Copy run metadata to event
68
- e.metadata = run.metadata;
69
- return e;
70
- });
71
-
72
- await consumer.onStart({});
73
- await new TestExecutor(consumer).execute(run);
74
- return consumer.summarizeAsBoolean();
75
- }
76
-
77
- /**
78
- * Run the runner, based on the inputs passed to the constructor
79
- */
80
- async run(): Promise<boolean | undefined> {
81
- if ('import' in this.#state.target) {
82
- return await this.runSingle(this.#state.target);
83
- } else {
84
- return await this.runFiles(this.#state.target.globs);
85
- }
86
- }
87
- }
@@ -1,108 +0,0 @@
1
- import { spawn } from 'node:child_process';
2
- import { createReadStream } from 'node:fs';
3
- import fs from 'node:fs/promises';
4
- import readline from 'node:readline/promises';
5
- import path from 'node:path';
6
-
7
- import { Env, ExecUtil, ShutdownManager, Util, RuntimeIndex, Runtime } from '@travetto/runtime';
8
- import { TestConfig, TestRun } from '../model/test.ts';
9
-
10
- /**
11
- * Simple Test Utilities
12
- */
13
- export class RunnerUtil {
14
- /**
15
- * Add 50 ms to the shutdown to allow for buffers to output properly
16
- */
17
- static registerCleanup(scope: string): void {
18
- ShutdownManager.onGracefulShutdown(() => Util.blockingTimeout(50), `test.${scope}.bufferOutput`);
19
- }
20
-
21
- /**
22
- * Determine if a given file path is a valid test file
23
- */
24
- static async isTestFile(file: string): Promise<boolean> {
25
- const reader = readline.createInterface({ input: createReadStream(file) });
26
- const state = { imp: false, suite: false };
27
- for await (const line of reader) {
28
- state.imp ||= line.includes('@travetto/test');
29
- state.suite ||= line.includes('Suite'); // Decorator or name
30
- if (state.imp && state.suite) {
31
- reader.close();
32
- return true;
33
- }
34
- }
35
- return false;
36
- }
37
-
38
- /**
39
- * Find all valid test files given the globs
40
- */
41
- static async* getTestImports(globs?: string[]): AsyncIterable<string> {
42
- const all = RuntimeIndex.find({
43
- module: m => m.roles.includes('test') || m.roles.includes('std'),
44
- folder: f => f === 'test',
45
- file: f => f.role === 'test'
46
- });
47
-
48
- // Collect globs
49
- if (globs?.length) {
50
- const allFiles = new Map(all.map(x => [x.sourceFile, x]));
51
- for await (const item of fs.glob(globs)) {
52
- const src = Runtime.workspaceRelative(path.resolve(item));
53
- const match = allFiles.get(src);
54
- if (match && await this.isTestFile(match.sourceFile)) {
55
- yield match.import;
56
- }
57
- }
58
- } else {
59
- for await (const match of all) {
60
- if (await this.isTestFile(match.sourceFile)) {
61
- yield match.import;
62
- }
63
- }
64
- }
65
- }
66
-
67
- /**
68
- * Get count of tests for a given set of globs
69
- * @param globs
70
- * @returns
71
- */
72
- static async getTestDigest(globs: string[] = ['**/*.ts'], tags?: string[]): Promise<TestConfig[]> {
73
- const countRes = await ExecUtil.getResult(
74
- spawn('npx', ['trv', 'test:digest', '-o', 'json', ...globs], {
75
- env: { ...process.env, ...Env.FORCE_COLOR.export(0), ...Env.NO_COLOR.export(true) }
76
- }),
77
- { catch: true }
78
- );
79
- if (!countRes.valid) {
80
- throw new Error(countRes.stderr);
81
- }
82
-
83
- const testFilter = tags?.length ?
84
- Util.allowDeny<string, [TestConfig]>(
85
- tags,
86
- rule => rule,
87
- (rule, core) => core.tags?.includes(rule) ?? false
88
- ) :
89
- ((): boolean => true);
90
-
91
- const parsed: TestConfig[] = countRes.valid ? JSON.parse(countRes.stdout) : [];
92
- return parsed.filter(testFilter);
93
- }
94
-
95
- /**
96
- * Get run events
97
- */
98
- static getTestRuns(tests: TestConfig[]): TestRun[] {
99
- const events = tests.reduce((acc, test) => {
100
- if (!acc.has(test.classId)) {
101
- acc.set(test.classId, { import: test.import, classId: test.classId, methodNames: [], runId: Util.uuid() });
102
- }
103
- acc.get(test.classId)!.methodNames!.push(test.methodName);
104
- return acc;
105
- }, new Map<string, TestRun>());
106
- return [...events.values()];
107
- }
108
- }