@travetto/test 5.0.0-rc.9 → 5.0.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.
@@ -1,5 +1,5 @@
1
1
  /** Configuration of a skip */
2
- export type Skip = boolean | ((instance: unknown) => boolean | Promise<boolean>) | (() => boolean | Promise<boolean>);
2
+ export type Skip = boolean | ((instance: unknown) => boolean | Promise<boolean>);
3
3
 
4
4
  /**
5
5
  * Core Suite definition
@@ -25,6 +25,10 @@ export interface SuiteCore {
25
25
  * The last line of the unit
26
26
  */
27
27
  lineEnd: number;
28
+ /**
29
+ * Tags for a suite or a test
30
+ */
31
+ tags?: string[];
28
32
  }
29
33
 
30
34
  /**
@@ -35,4 +39,8 @@ export interface TestCore extends SuiteCore {
35
39
  * The first line of the unit body
36
40
  */
37
41
  lineBodyStart: number;
42
+ /**
43
+ * For extended suites, this is location of the actual file where the test exists
44
+ */
45
+ sourceImport?: string;
38
46
  }
@@ -5,17 +5,21 @@ import { SuiteConfig, SuiteResult } from './suite';
5
5
  * Targets
6
6
  */
7
7
  export type EventEntity = 'test' | 'suite' | 'assertion';
8
+
8
9
  /**
9
10
  * Phases
10
11
  */
11
12
  export type EventPhase = 'before' | 'after';
12
13
 
14
+ type EventTpl<T extends EventEntity, P extends EventPhase, V extends {}> =
15
+ { type: T, phase: P, metadata?: Record<string, unknown> } & V;
16
+
13
17
  /**
14
18
  * Different test event shapes
15
19
  */
16
20
  export type TestEvent =
17
- { type: 'assertion', phase: 'after', assertion: Assertion } |
18
- { type: 'test', phase: 'before', test: TestConfig } |
19
- { type: 'test', phase: 'after', test: TestResult } |
20
- { type: 'suite', phase: 'before', suite: SuiteConfig } |
21
- { type: 'suite', phase: 'after', suite: SuiteResult };
21
+ EventTpl<'assertion', 'after', { assertion: Assertion }> |
22
+ EventTpl<'test', 'before', { test: TestConfig }> |
23
+ EventTpl<'test', 'after', { test: TestResult }> |
24
+ EventTpl<'suite', 'before', { suite: SuiteConfig }> |
25
+ EventTpl<'suite', 'after', { suite: SuiteResult }>;
@@ -1,6 +1,6 @@
1
1
  import type { Class } from '@travetto/runtime';
2
2
 
3
- import { TestConfig, TestResult } from './test';
3
+ import { Assertion, TestConfig, TestResult } from './test';
4
4
  import { Skip, SuiteCore } from './common';
5
5
 
6
6
  /**
@@ -80,3 +80,13 @@ export interface SuiteResult extends Counts {
80
80
  */
81
81
  duration: number;
82
82
  }
83
+
84
+ /**
85
+ * A total suite failure
86
+ */
87
+ export interface SuiteFailure {
88
+ assert: Assertion;
89
+ testResult: TestResult;
90
+ test: TestConfig;
91
+ suite: SuiteConfig;
92
+ }
package/src/model/test.ts CHANGED
@@ -107,4 +107,30 @@ export interface TestResult extends TestCore {
107
107
  * Logging output
108
108
  */
109
109
  output: Record<string, string>;
110
- }
110
+ }
111
+
112
+ /**
113
+ * Test Run
114
+ */
115
+ export type TestRun = {
116
+ /**
117
+ * Import for run
118
+ */
119
+ import: string;
120
+ /**
121
+ * Suite class id
122
+ */
123
+ classId?: string;
124
+ /**
125
+ * Methods names we want to target
126
+ */
127
+ methodNames?: string[];
128
+ /**
129
+ * Test run metadata
130
+ */
131
+ metadata?: Record<string, unknown>;
132
+ /**
133
+ * unique id for the run
134
+ */
135
+ runId?: string;
136
+ };
@@ -2,7 +2,7 @@ import { Class, Runtime, classConstruct, describeFunction, asFull } from '@trave
2
2
  import { MetadataRegistry } from '@travetto/registry';
3
3
 
4
4
  import { SuiteConfig } from '../model/suite';
5
- import { TestConfig } from '../model/test';
5
+ import { TestConfig, TestRun } from '../model/test';
6
6
 
7
7
  /**
8
8
  * Test Suite registry
@@ -21,6 +21,7 @@ class $SuiteRegistry extends MetadataRegistry<SuiteConfig, TestConfig> {
21
21
  return {
22
22
  class: cls,
23
23
  classId: cls.Ⲑid,
24
+ tags: [],
24
25
  import: Runtime.getImport(cls),
25
26
  lineStart: lines?.[0],
26
27
  lineEnd: lines?.[1],
@@ -36,6 +37,7 @@ class $SuiteRegistry extends MetadataRegistry<SuiteConfig, TestConfig> {
36
37
  const lines = describeFunction(cls)?.methods?.[fn.name].lines;
37
38
  return {
38
39
  class: cls,
40
+ tags: [],
39
41
  import: Runtime.getImport(cls),
40
42
  lineStart: lines?.[0],
41
43
  lineEnd: lines?.[1],
@@ -70,18 +72,19 @@ class $SuiteRegistry extends MetadataRegistry<SuiteConfig, TestConfig> {
70
72
  config.beforeEach.push(...pConf.beforeEach);
71
73
  tests.push(...[...pConf.tests.values()].map(t => ({
72
74
  ...t,
75
+ sourceImport: pConf.import,
73
76
  class: cls
74
77
  })));
75
78
  }
76
79
 
77
80
  config.instance = classConstruct(config.class);
78
81
  config.tests = tests!.map(x => asFull(x));
82
+ config.description ||= config.classId;
79
83
 
80
- if (!config.description) {
81
- config.description = config.classId;
82
- }
83
84
  for (const t of config.tests) {
84
85
  t.classId = config.classId;
86
+ t.import = config.import;
87
+ t.tags = [...t.tags!, ...config.tags!];
85
88
  }
86
89
  return config;
87
90
  }
@@ -89,9 +92,13 @@ class $SuiteRegistry extends MetadataRegistry<SuiteConfig, TestConfig> {
89
92
  /**
90
93
  * Get run parameters from provided input
91
94
  */
92
- getRunParams(imp: string, clsName?: string, method?: string): { suites: SuiteConfig[] } | { suite: SuiteConfig, test?: TestConfig } {
93
- if (clsName && /^\d+$/.test(clsName)) { // If we only have a line number
94
- const line = parseInt(clsName, 10);
95
+ getSuiteTests(run: TestRun): { suite: SuiteConfig, tests: TestConfig[] }[] {
96
+ const clsId = run.classId;
97
+ const imp = run.import;
98
+ const methodNames = run.methodNames ?? [];
99
+
100
+ if (clsId && /^\d+$/.test(clsId)) { // If we only have a line number
101
+ const line = parseInt(clsId, 10);
95
102
  const suites = this.getValidClasses()
96
103
  .filter(cls => Runtime.getImport(cls) === imp)
97
104
  .map(x => this.get(x)).filter(x => !x.skip);
@@ -99,25 +106,25 @@ class $SuiteRegistry extends MetadataRegistry<SuiteConfig, TestConfig> {
99
106
 
100
107
  if (suite) {
101
108
  const test = suite.tests.find(x => line >= x.lineStart && line <= x.lineEnd);
102
- return test ? { suite, test } : { suite };
109
+ return test ? [{ suite, tests: [test] }] : [{ suite, tests: suite.tests }];
103
110
  } else {
104
- return { suites };
111
+ return suites.map(x => ({ suite: x, tests: x.tests }));
105
112
  }
106
113
  } else { // Else lookup directly
107
- if (method) {
108
- const cls = this.getValidClasses().find(x => x.name === clsName)!;
114
+ if (methodNames.length) {
115
+ const cls = this.getValidClasses().find(x => x.Ⲑid === clsId)!;
109
116
  const suite = this.get(cls);
110
- const test = suite.tests.find(x => x.methodName === method)!;
111
- return { suite, test };
112
- } else if (clsName) {
113
- const cls = this.getValidClasses().find(x => x.name === clsName)!;
117
+ const tests = suite.tests.filter(x => methodNames.includes(x.methodName))!;
118
+ return [{ suite, tests }];
119
+ } else if (clsId) {
120
+ const cls = this.getValidClasses().find(x => x.Ⲑid === clsId)!;
114
121
  const suite = this.get(cls);
115
- return { suite };
122
+ return [{ suite, tests: suite.tests }];
116
123
  } else {
117
124
  const suites = this.getValidClasses()
118
125
  .map(x => this.get(x))
119
126
  .filter(x => !describeFunction(x.class).abstract); // Do not run abstract suites
120
- return { suites };
127
+ return suites.map(x => ({ suite: x, tests: x.tests }));
121
128
  }
122
129
  }
123
130
  }
package/src/trv.d.ts CHANGED
@@ -12,5 +12,9 @@ declare global {
12
12
  * @default 5s
13
13
  */
14
14
  TRV_TEST_TIMEOUT: TimeSpan | number;
15
+ /**
16
+ * The tags to include or exclude during testing
17
+ */
18
+ TRV_TEST_TAGS: string[];
15
19
  }
16
20
  }
@@ -3,16 +3,17 @@ import { createWriteStream } from 'node:fs';
3
3
  import { ConsoleManager, Env, Util, Runtime } from '@travetto/runtime';
4
4
  import { ChildCommChannel } from '@travetto/worker';
5
5
 
6
- import { ErrorUtil } from '../consumer/error';
6
+ import { SerializeUtil } from '../consumer/serialize';
7
7
  import { RunnerUtil } from '../execute/util';
8
8
  import { Runner } from '../execute/runner';
9
- import { Events, RunEvent } from './types';
9
+ import { Events } from './types';
10
+ import { TestRun } from '../model/test';
10
11
 
11
12
  /**
12
13
  * Child Worker for the Test Runner. Receives events as commands
13
14
  * to run specific tests
14
15
  */
15
- export class TestChildWorker extends ChildCommChannel<RunEvent> {
16
+ export class TestChildWorker extends ChildCommChannel<TestRun> {
16
17
 
17
18
  #done = Util.resolvablePromise();
18
19
 
@@ -25,7 +26,7 @@ export class TestChildWorker extends ChildCommChannel<RunEvent> {
25
26
  throw err;
26
27
  }
27
28
  // Mark as errored out
28
- this.send(type, { error: ErrorUtil.serializeError(err) });
29
+ this.send(type, { error: SerializeUtil.serializeError(err) });
29
30
  }
30
31
  }
31
32
 
@@ -56,7 +57,7 @@ export class TestChildWorker extends ChildCommChannel<RunEvent> {
56
57
  /**
57
58
  * When we receive a command from the parent
58
59
  */
59
- async onCommand(event: RunEvent & { type: string }): Promise<boolean> {
60
+ async onCommand(event: TestRun & { type: string }): Promise<boolean> {
60
61
  console.debug('on message', { ...event });
61
62
 
62
63
  if (event.type === Events.INIT) { // On request to init, start initialization
@@ -76,18 +77,11 @@ export class TestChildWorker extends ChildCommChannel<RunEvent> {
76
77
  /**
77
78
  * Run a specific test/suite
78
79
  */
79
- async onRunCommand(event: RunEvent): Promise<void> {
80
- console.debug('Run');
81
-
82
- console.debug('Running', { import: event.import });
80
+ async onRunCommand(run: TestRun): Promise<void> {
81
+ console.debug('Running', { import: run.import });
83
82
 
84
83
  try {
85
- await new Runner({
86
- format: 'exec',
87
- mode: 'single',
88
- args: [event.import, event.class!, event.method!],
89
- concurrency: 1
90
- }).run();
84
+ await new Runner({ format: 'exec', target: run }).run();
91
85
  } finally {
92
86
  this.#done.resolve();
93
87
  }
@@ -3,39 +3,34 @@ import { fork } from 'node:child_process';
3
3
  import { Env, RuntimeIndex } from '@travetto/runtime';
4
4
  import { ParentCommChannel } from '@travetto/worker';
5
5
 
6
- import { Events, RunEvent, RunRequest } from './types';
6
+ import { Events, TestLogEvent } from './types';
7
7
  import { TestConsumer } from '../consumer/types';
8
- import { ErrorUtil } from '../consumer/error';
8
+ import { SerializeUtil } from '../consumer/serialize';
9
9
  import { TestEvent } from '../model/event';
10
+ import { TestRun } from '../model/test';
11
+
12
+ const log = (message: string): void => {
13
+ process.send?.({ type: 'log', message } satisfies TestLogEvent);
14
+ };
10
15
 
11
16
  /**
12
17
  * Produce a handler for the child worker
13
18
  */
14
- export async function buildStandardTestManager(consumer: TestConsumer, imp: string | RunRequest): Promise<void> {
15
- let event: RunEvent;
16
- process.send?.({ type: 'log', message: `Worker Input ${JSON.stringify(imp)}` });
17
-
18
- if (typeof imp === 'string') {
19
- event = { import: imp };
20
- } else if ('file' in imp) {
21
- event = { import: RuntimeIndex.getFromSource(imp.file)?.import!, class: imp.class, method: imp.method };
22
- } else {
23
- event = imp;
24
- }
25
-
26
- process.send?.({ type: 'log', message: `Worker Executing ${event.import}` });
19
+ export async function buildStandardTestManager(consumer: TestConsumer, run: TestRun): Promise<void> {
20
+ log(`Worker Input ${JSON.stringify(run)}`);
21
+ log(`Worker Executing ${run.import}`);
27
22
 
28
- const { module } = RuntimeIndex.getFromImport(event.import!)!;
29
- const suiteMod = RuntimeIndex.getModule(module);
23
+ const { module } = RuntimeIndex.getFromImport(run.import)!;
24
+ const suiteMod = RuntimeIndex.getModule(module)!;
30
25
 
31
26
  const channel = new ParentCommChannel<TestEvent & { error?: Error }>(
32
27
  fork(
33
28
  RuntimeIndex.resolveFileImport('@travetto/cli/support/entry.trv'), ['test:child'],
34
29
  {
35
- cwd: suiteMod!.sourcePath,
30
+ cwd: suiteMod.sourcePath,
36
31
  env: {
37
32
  ...process.env,
38
- ...Env.TRV_MANIFEST.export(suiteMod!.outputPath),
33
+ ...Env.TRV_MANIFEST.export(suiteMod.outputPath),
39
34
  ...Env.TRV_QUIET.export(true)
40
35
  },
41
36
  stdio: ['ignore', 'ignore', 2, 'ipc']
@@ -58,7 +53,7 @@ export async function buildStandardTestManager(consumer: TestConsumer, imp: stri
58
53
  // Listen for child to complete
59
54
  const complete = channel.once(Events.RUN_COMPLETE);
60
55
  // Start test
61
- channel.send(Events.RUN, event);
56
+ channel.send(Events.RUN, run);
62
57
 
63
58
  // Wait for complete
64
59
  const { error } = await complete;
@@ -66,10 +61,10 @@ export async function buildStandardTestManager(consumer: TestConsumer, imp: stri
66
61
  // Kill on complete
67
62
  await channel.destroy();
68
63
 
69
- process.send?.({ type: 'log', message: `Worker Finished ${event.import}` });
64
+ log(`Worker Finished ${run.import}`);
70
65
 
71
66
  // If we received an error, throw it
72
67
  if (error) {
73
- throw ErrorUtil.deserializeError(error);
68
+ throw SerializeUtil.deserializeError(error);
74
69
  }
75
70
  }
@@ -1,25 +1,5 @@
1
- /**
2
- * Test Run Request
3
- */
4
- export type RunRequest = {
5
- file: string;
6
- class?: string;
7
- method?: string;
8
- } | {
9
- import: string;
10
- class?: string;
11
- method?: string;
12
- };
13
-
14
- /**
15
- * Test Run Event
16
- */
17
- export type RunEvent = {
18
- import: string;
19
- error?: unknown;
20
- class?: string;
21
- method?: string;
22
- };
1
+ import { TestEvent } from '../model/event';
2
+ import { TestRun } from '../model/test';
23
3
 
24
4
  /**
25
5
  * Test Run Event Keys
@@ -30,4 +10,14 @@ export const Events = {
30
10
  INIT: 'init',
31
11
  INIT_COMPLETE: 'initComplete',
32
12
  READY: 'ready'
33
- };
13
+ };
14
+
15
+ export type TestRemovedEvent = { type: 'removeTest', method?: string } & TestRun;
16
+ export type TestReadyEvent = { type: 'ready' };
17
+ export type TestLogEvent = { type: 'log', message: string };
18
+
19
+ export type TestWatchEvent =
20
+ TestEvent |
21
+ TestRemovedEvent |
22
+ TestReadyEvent |
23
+ TestLogEvent;
@@ -21,6 +21,11 @@ export class TestCommand implements CliCommandShape {
21
21
  concurrency: number = WorkPool.DEFAULT_SIZE;
22
22
  /** Test run mode */
23
23
  mode: TestMode = 'standard';
24
+ /**
25
+ * Tags to target or exclude
26
+ * @alias env.TRV_TEST_TAGS
27
+ */
28
+ tags?: string[];
24
29
 
25
30
  preMain(): void {
26
31
  EventEmitter.defaultMaxListeners = 1000;
@@ -48,14 +53,23 @@ export class TestCommand implements CliCommandShape {
48
53
  }
49
54
  }
50
55
 
51
- async main(first: string = '**/*', regexes: string[] = []): Promise<void> {
56
+ async main(first: string = '**/*', globs: string[] = []): Promise<void> {
52
57
  const { runTests } = await import('./bin/run');
53
58
 
59
+ const isFirst = await this.isFirstFile(first);
60
+ const isSingle = this.mode === 'single' || (isFirst && globs.length === 0);
61
+
54
62
  return runTests({
55
- args: [first, ...regexes],
56
- mode: await this.resolvedMode(first, regexes),
57
63
  concurrency: this.concurrency,
58
- format: this.format
64
+ format: this.format,
65
+ tags: this.tags,
66
+ target: isSingle ?
67
+ {
68
+ import: first,
69
+ classId: globs[0],
70
+ methodNames: globs.slice(1),
71
+ } :
72
+ { globs: [first, ...globs], }
59
73
  });
60
74
  }
61
75
  }
@@ -5,16 +5,18 @@ import { SuiteRegistry } from '../src/registry/suite';
5
5
  import { RunnerUtil } from '../src/execute/util';
6
6
 
7
7
  @CliCommand({ hidden: true })
8
- export class TestCountCommand {
8
+ export class TestDigestCommand {
9
+
10
+ output: 'json' | 'text' = 'text';
9
11
 
10
12
  preMain(): void {
11
13
  Env.TRV_ROLE.set('test');
12
14
  Env.DEBUG.set(false);
13
15
  }
14
16
 
15
- async main(patterns: string[]) {
17
+ async main(globs: string[] = ['**/*.ts']) {
16
18
  // Load all tests
17
- for await (const imp of await RunnerUtil.getTestImports(patterns)) {
19
+ for await (const imp of await RunnerUtil.getTestImports(globs)) {
18
20
  try {
19
21
  await Runtime.importFrom(imp);
20
22
  } catch (err) {
@@ -25,11 +27,17 @@ export class TestCountCommand {
25
27
  await SuiteRegistry.init();
26
28
 
27
29
  const suites = SuiteRegistry.getClasses();
28
- const total = suites
30
+ const all = suites
29
31
  .map(c => SuiteRegistry.get(c))
30
32
  .filter(c => !describeFunction(c.class).abstract)
31
- .reduce((acc, c) => acc + (c.tests?.length ?? 0), 0);
33
+ .flatMap(c => c.tests);
32
34
 
33
- console.log(total);
35
+ if (this.output === 'json') {
36
+ console.log(JSON.stringify(all));
37
+ } else {
38
+ for (const item of all) {
39
+ console.log(`${item.classId}#${item.methodName}`, item.tags?.join('|') ?? '');
40
+ }
41
+ }
34
42
  }
35
43
  }
@@ -1,4 +1,4 @@
1
- import { Env } from '@travetto/runtime';
1
+ import { Env, RuntimeIndex } from '@travetto/runtime';
2
2
  import { CliCommand } from '@travetto/cli';
3
3
 
4
4
  import { runTests } from './bin/run';
@@ -17,7 +17,14 @@ export class TestDirectCommand {
17
17
  Env.TRV_LOG_TIME.clear();
18
18
  }
19
19
 
20
- main(file: string, args: string[]): Promise<void> {
21
- return runTests({ args: [file, ...args], format: this.format, mode: 'single', concurrency: 1 });
20
+ main(importOrFile: string, clsId?: string, methodsNames: string[] = []): Promise<void> {
21
+ return runTests({
22
+ format: this.format,
23
+ target: {
24
+ import: importOrFile,
25
+ classId: clsId,
26
+ methodNames: methodsNames,
27
+ }
28
+ });
22
29
  }
23
30
  }
@@ -93,7 +93,7 @@ export class AssertTransformer {
93
93
  /**
94
94
  * Resolves optoken to syntax kind. Relies on `ts`
95
95
  */
96
- static lookupOpToken(key: number): string {
96
+ static lookupOpToken(key: number): string | undefined {
97
97
  if (OP_TOKEN_TO_NAME.size === 0) {
98
98
  Object.keys(ts.SyntaxKind)
99
99
  .filter(x => !/^\d+$/.test(x))
@@ -106,7 +106,7 @@ export class AssertTransformer {
106
106
  if (name in OPTOKEN_ASSERT) {
107
107
  return OPTOKEN_ASSERT[name];
108
108
  } else {
109
- throw new Error(`Unknown optoken: ${name}:${key}`);
109
+ return;
110
110
  }
111
111
  }
112
112