@travetto/test 6.0.2 → 7.0.0-rc.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,14 +1,14 @@
1
- import { RootRegistry, MethodSource } from '@travetto/registry';
1
+ import { Registry } from '@travetto/registry';
2
2
  import { WorkPool } from '@travetto/worker';
3
3
  import { AsyncQueue, Runtime, RuntimeIndex, castTo, describeFunction } from '@travetto/runtime';
4
4
 
5
- import { SuiteRegistry } from '../registry/suite.ts';
6
5
  import { buildStandardTestManager } from '../worker/standard.ts';
7
- import { TestConsumerRegistry } from '../consumer/registry.ts';
6
+ import { TestConsumerRegistryIndex } from '../consumer/registry-index.ts';
8
7
  import { CumulativeSummaryConsumer } from '../consumer/types/cumulative.ts';
9
8
  import { TestRun } from '../model/test.ts';
10
9
  import { RunnerUtil } from './util.ts';
11
10
  import { TestReadyEvent, TestRemovedEvent } from '../worker/types.ts';
11
+ import { SuiteRegistryIndex } from '../registry/registry-index.ts';
12
12
 
13
13
  /**
14
14
  * Test Watcher.
@@ -23,9 +23,7 @@ export class TestWatcher {
23
23
  static async watch(format: string, runAllOnStart = true): Promise<void> {
24
24
  console.debug('Listening for changes');
25
25
 
26
- await SuiteRegistry.init();
27
- SuiteRegistry.listen(RootRegistry);
28
- await RootRegistry.init();
26
+ await Registry.init();
29
27
 
30
28
  const events: TestRun[] = [];
31
29
 
@@ -36,22 +34,26 @@ export class TestWatcher {
36
34
 
37
35
  const itr = new AsyncQueue(events);
38
36
  const consumer = new CumulativeSummaryConsumer(
39
- await TestConsumerRegistry.getInstance({ consumer: format })
37
+ await TestConsumerRegistryIndex.getInstance({ consumer: format })
40
38
  )
41
39
  .withFilter(x => x.metadata?.partial !== true || x.type !== 'suite');
42
40
 
43
- new MethodSource(RootRegistry).on(e => {
44
- const [cls, method] = (e.prev ?? e.curr ?? []);
41
+ Registry.onMethodChange((event) => {
42
+ const [cls, method] = ('prev' in event && event.prev ? event.prev : null) ??
43
+ ('curr' in event && event.curr ? event.curr : []);
44
+
45
45
  if (!cls || describeFunction(cls).abstract) {
46
46
  return;
47
47
  }
48
+
48
49
  const classId = cls.Ⲑid;
49
50
  if (!method) {
50
51
  consumer.removeClass(classId);
51
52
  return;
52
53
  }
53
- const conf = SuiteRegistry.getByClassAndMethod(cls, method)!;
54
- if (e.type !== 'removing') {
54
+
55
+ const conf = SuiteRegistryIndex.getTestConfig(cls, method)!;
56
+ if (event.type !== 'removing') {
55
57
  if (conf) {
56
58
  const run: TestRun = {
57
59
  import: conf.import, classId: conf.classId, methodNames: [conf.methodName], metadata: { partial: true }
@@ -68,10 +70,10 @@ export class TestWatcher {
68
70
  import: Runtime.getImport(cls)
69
71
  } satisfies TestRemovedEvent);
70
72
  }
71
- });
73
+ }, SuiteRegistryIndex);
72
74
 
73
75
  // If a file is changed, but doesn't emit classes, re-run whole file
74
- RootRegistry.onNonClassChanges(imp => itr.add({ import: imp }));
76
+ Registry.onNonClassChanges(imp => itr.add({ import: imp }));
75
77
 
76
78
  process.on('message', ev => {
77
79
  if (typeof ev === 'object' && ev && 'type' in ev && ev.type === 'run-test') {
@@ -9,10 +9,6 @@ export interface SuiteCore {
9
9
  * The class id
10
10
  */
11
11
  classId: string;
12
- /**
13
- * The tests' description
14
- */
15
- description: string;
16
12
  /**
17
13
  * The import location for the suite
18
14
  */
@@ -29,6 +25,10 @@ export interface SuiteCore {
29
25
  * Tags for a suite or a test
30
26
  */
31
27
  tags?: string[];
28
+ /**
29
+ * Description
30
+ */
31
+ description?: string;
32
32
  }
33
33
 
34
34
  /**
@@ -6,11 +6,11 @@ import { Skip, SuiteCore } from './common.ts';
6
6
  /**
7
7
  * Suite configuration
8
8
  */
9
- export interface SuiteConfig<T = unknown> extends SuiteCore {
9
+ export interface SuiteConfig extends SuiteCore {
10
10
  /**
11
11
  * Class suite is in
12
12
  */
13
- class: Class<T>;
13
+ class: Class;
14
14
  /**
15
15
  * Should this be skipped
16
16
  */
@@ -18,11 +18,11 @@ export interface SuiteConfig<T = unknown> extends SuiteCore {
18
18
  /**
19
19
  * Actual class instance
20
20
  */
21
- instance: T;
21
+ instance?: unknown;
22
22
  /**
23
- * List of tests to run
23
+ * Tests to run
24
24
  */
25
- tests: TestConfig[];
25
+ tests: Record<string | symbol, TestConfig>;
26
26
  /**
27
27
  * Before all handlers
28
28
  */
@@ -0,0 +1,127 @@
1
+ import { RegistryAdapter } from '@travetto/registry';
2
+ import { AppError, asFull, Class, describeFunction, Runtime, safeAssign } from '@travetto/runtime';
3
+ import { SchemaRegistryIndex } from '@travetto/schema';
4
+
5
+ import { SuiteConfig } from '../model/suite';
6
+ import { TestConfig } from '../model/test';
7
+
8
+ function combineClasses(baseConfig: SuiteConfig, ...subConfig: Partial<SuiteConfig>[]): SuiteConfig {
9
+ for (const cfg of subConfig) {
10
+ if (cfg.beforeAll) {
11
+ baseConfig.beforeAll = [...baseConfig.beforeAll, ...cfg.beforeAll];
12
+ }
13
+ if (cfg.beforeEach) {
14
+ baseConfig.beforeEach = [...baseConfig.beforeEach, ...cfg.beforeEach];
15
+ }
16
+ if (cfg.afterAll) {
17
+ baseConfig.afterAll = [...baseConfig.afterAll, ...cfg.afterAll];
18
+ }
19
+ if (cfg.afterEach) {
20
+ baseConfig.afterEach = [...baseConfig.afterEach, ...cfg.afterEach];
21
+ }
22
+ if (cfg.tags) {
23
+ baseConfig.tags = [...baseConfig.tags ?? [], ...cfg.tags];
24
+ }
25
+ if (cfg.tests) {
26
+ for (const [key, test] of Object.entries(cfg.tests ?? {})) {
27
+ baseConfig.tests[key] = {
28
+ ...test,
29
+ sourceImport: Runtime.getImport(baseConfig.class),
30
+ class: baseConfig.class,
31
+ classId: baseConfig.classId,
32
+ import: baseConfig.import,
33
+ };
34
+ }
35
+ }
36
+ }
37
+ return baseConfig;
38
+ }
39
+
40
+ function combineMethods(suite: SuiteConfig, baseConfig: TestConfig, ...subConfig: Partial<TestConfig>[]): TestConfig {
41
+ baseConfig.classId = suite.classId;
42
+ baseConfig.import = suite.import;
43
+ for (const cfg of subConfig) {
44
+ safeAssign(baseConfig, cfg, {
45
+ tags: [
46
+ ...baseConfig.tags ?? [],
47
+ ...cfg.tags ?? []
48
+ ]
49
+ });
50
+ }
51
+ return baseConfig;
52
+ }
53
+
54
+ export class SuiteRegistryAdapter implements RegistryAdapter<SuiteConfig> {
55
+ #cls: Class;
56
+ #config: SuiteConfig;
57
+
58
+ constructor(cls: Class) {
59
+ this.#cls = cls;
60
+ }
61
+
62
+ register(...data: Partial<SuiteConfig>[]): SuiteConfig {
63
+ if (!this.#config) {
64
+ const lines = describeFunction(this.#cls)?.lines;
65
+ this.#config = asFull<SuiteConfig>({
66
+ class: this.#cls,
67
+ classId: this.#cls.Ⲑid,
68
+ tags: [],
69
+ import: Runtime.getImport(this.#cls),
70
+ lineStart: lines?.[0],
71
+ lineEnd: lines?.[1],
72
+ tests: {},
73
+ beforeAll: [],
74
+ beforeEach: [],
75
+ afterAll: [],
76
+ afterEach: []
77
+ });
78
+ }
79
+ combineClasses(this.#config, ...data);
80
+ return this.#config;
81
+ }
82
+
83
+ registerTest(method: string | symbol, ...data: Partial<TestConfig>[]): TestConfig {
84
+ const suite = this.register();
85
+
86
+ if (!(method in this.#config.tests)) {
87
+ const lines = describeFunction(this.#cls)?.methods?.[method]?.lines;
88
+ const config = asFull<TestConfig>({
89
+ class: this.#cls,
90
+ tags: [],
91
+ import: Runtime.getImport(this.#cls),
92
+ lineStart: lines?.[0],
93
+ lineEnd: lines?.[1],
94
+ lineBodyStart: lines?.[2],
95
+ methodName: method.toString(),
96
+ });
97
+ this.#config.tests[method] = config;
98
+ }
99
+
100
+ const result = this.#config.tests[method];
101
+ combineMethods(suite, result, ...data);
102
+ return result;
103
+ }
104
+
105
+ finalize(parent?: SuiteConfig): void {
106
+ if (parent) {
107
+ combineClasses(this.#config, parent);
108
+ }
109
+
110
+ for (const test of Object.values(this.#config.tests)) {
111
+ test.tags = [...test.tags ?? [], ...this.#config.tags ?? []];
112
+ test.description ||= SchemaRegistryIndex.get(this.#cls).getMethod(test.methodName).description;
113
+ }
114
+ }
115
+
116
+ get(): SuiteConfig {
117
+ return this.#config;
118
+ }
119
+
120
+ getMethod(method: string | symbol): TestConfig {
121
+ const test = this.#config.tests[method];
122
+ if (!test) {
123
+ throw new AppError(`Test not registered: ${String(method)} on ${this.#cls.name}`);
124
+ }
125
+ return test;
126
+ }
127
+ }
@@ -0,0 +1,117 @@
1
+ import { AppError, Class, Runtime, describeFunction } from '@travetto/runtime';
2
+ import { ChangeEvent, RegistryIndex, RegistryIndexStore, Registry } from '@travetto/registry';
3
+
4
+ import { SuiteConfig } from '../model/suite.ts';
5
+ import { TestConfig, TestRun } from '../model/test.ts';
6
+ import { SuiteRegistryAdapter } from './registry-adapter.ts';
7
+
8
+ const sortedTests = (cfg: SuiteConfig): TestConfig[] =>
9
+ Object.values(cfg.tests).toSorted((a, b) => a.lineStart - b.lineStart);
10
+
11
+ type SuiteTests = { suite: SuiteConfig, tests: TestConfig[] };
12
+
13
+ /**
14
+ * Test Suite registry
15
+ */
16
+ export class SuiteRegistryIndex implements RegistryIndex {
17
+
18
+ static #instance = Registry.registerIndex(this);
19
+
20
+ static getForRegister(cls: Class): SuiteRegistryAdapter {
21
+ return this.#instance.store.getForRegister(cls);
22
+ }
23
+
24
+ static has(cls: Class): boolean {
25
+ return this.#instance.store.has(cls);
26
+ }
27
+
28
+ static getTestConfig(cls: Class, method: Function): TestConfig | undefined {
29
+ return this.#instance.getTestConfig(cls, method);
30
+ }
31
+
32
+ static getSuiteTests(run: TestRun): SuiteTests[] {
33
+ return this.#instance.getSuiteTests(run);
34
+ }
35
+
36
+ static getConfig(cls: Class): SuiteConfig {
37
+ return this.#instance.store.get(cls).get();
38
+ }
39
+
40
+ static getClasses(): Class[] {
41
+ return this.#instance.store.getClasses();
42
+ }
43
+
44
+ store = new RegistryIndexStore(SuiteRegistryAdapter);
45
+
46
+ process(_events: ChangeEvent<Class>[]): void {
47
+ // No-op for now
48
+ }
49
+
50
+ /**
51
+ * Find all valid tests (ignoring abstract)
52
+ */
53
+ getValidClasses(): Class[] {
54
+ return this.store.getClasses().filter(c => !describeFunction(c).abstract);
55
+ }
56
+
57
+ getConfig(cls: Class): SuiteConfig {
58
+ return this.store.get(cls).get();
59
+ }
60
+
61
+ /**
62
+ * Get run parameters from provided input
63
+ */
64
+ getSuiteTests(run: TestRun): SuiteTests[] {
65
+ const clsId = run.classId;
66
+ const imp = run.import;
67
+ const methodNames = run.methodNames ?? [];
68
+
69
+ if (clsId && /^\d+$/.test(clsId)) { // If we only have a line number
70
+ const line = parseInt(clsId, 10);
71
+ const suites = this.getValidClasses()
72
+ .filter(cls => Runtime.getImport(cls) === imp)
73
+ .map(x => this.getConfig(x)).filter(x => !x.skip);
74
+ const suite = suites.find(x => line >= x.lineStart && line <= x.lineEnd);
75
+
76
+ if (suite) {
77
+ const tests = sortedTests(suite);
78
+ const test = tests.find(x => line >= x.lineStart && line <= x.lineEnd);
79
+ return test ? [{ suite, tests: [test] }] : [{ suite, tests }];
80
+ } else {
81
+ return suites.map(x => ({ suite: x, tests: sortedTests(x) }));
82
+ }
83
+ } else { // Else lookup directly
84
+ if (methodNames.length) {
85
+ const cls = this.getValidClasses().find(x => x.Ⲑid === clsId);
86
+ if (!cls) {
87
+ throw new AppError('Unable to find suite for class ID', { details: { classId: clsId } });
88
+ }
89
+ const suite = this.getConfig(cls);
90
+ const tests = sortedTests(suite).filter(x => methodNames.includes(x.methodName));
91
+ return [{ suite, tests }];
92
+ } else if (clsId) {
93
+ const cls = this.getValidClasses().find(x => x.Ⲑid === clsId)!;
94
+ if (!cls) {
95
+ throw new AppError('Unable to find suite for class ID', { details: { classId: clsId } });
96
+ }
97
+ const suite = this.getConfig(cls);
98
+ return suite ? [{ suite, tests: sortedTests(suite) }] : [];
99
+ } else {
100
+ const suites = this.getValidClasses()
101
+ .map(x => this.getConfig(x))
102
+ .filter(x => !describeFunction(x.class).abstract); // Do not run abstract suites
103
+ return suites.map(x => ({ suite: x, tests: sortedTests(x) }));
104
+ }
105
+ }
106
+ }
107
+
108
+ /**
109
+ * Find a test configuration given class and optionally a method
110
+ */
111
+ getTestConfig(cls: Class, method: Function): TestConfig | undefined {
112
+ if (this.store.has(cls)) {
113
+ const conf = this.getConfig(cls);
114
+ return Object.values(conf.tests).find(x => x.methodName === method.name);
115
+ }
116
+ }
117
+ }
@@ -1,7 +1,7 @@
1
- import { castTo, Runtime } from '@travetto/runtime';
2
- import { SchemaRegistry } from '@travetto/schema';
1
+ import { getClass, Runtime } from '@travetto/runtime';
2
+ import { SchemaRegistryIndex } from '@travetto/schema';
3
3
 
4
- import { TestConsumerRegistry } from '../../src/consumer/registry.ts';
4
+ import { TestConsumerRegistryIndex } from '../../src/consumer/registry-index.ts';
5
5
  import type { RunState } from '../../src/execute/types.ts';
6
6
 
7
7
  /**
@@ -23,20 +23,17 @@ export async function runTests(opts: RunState): Promise<void> {
23
23
  }
24
24
  }
25
25
 
26
- export async function selectConsumer(inst: { format?: string }) {
27
- await TestConsumerRegistry.manualInit();
28
-
29
- let types = TestConsumerRegistry.getTypes();
30
-
31
- if (inst.format?.includes('/')) {
32
- await Runtime.importFrom(inst.format);
33
- types = TestConsumerRegistry.getTypes();
26
+ export async function selectConsumer(instance: { format?: string }) {
27
+ if (instance.format?.includes('/')) {
28
+ await Runtime.importFrom(instance.format);
34
29
  }
35
30
 
36
- const cls = inst.constructor;
31
+ const types = await TestConsumerRegistryIndex.getTypes();
37
32
 
38
- SchemaRegistry.get(castTo(cls)).totalView.schema.format.enum = {
39
- message: `{path} is only allowed to be "${types.join('" or "')}"`,
40
- values: types
41
- };
33
+ SchemaRegistryIndex.getForRegister(getClass(instance), true).registerField('format', {
34
+ enum: {
35
+ message: `{path} is only allowed to be "${types.join('" or "')}"`,
36
+ values: types
37
+ }
38
+ });
42
39
  }
@@ -2,9 +2,11 @@ import { EventEmitter } from 'node:events';
2
2
 
3
3
  import { Env } from '@travetto/runtime';
4
4
  import { CliCommand } from '@travetto/cli';
5
+ import { IsPrivate } from '@travetto/schema';
5
6
 
6
7
  /** Test child worker target */
7
- @CliCommand({ hidden: true })
8
+ @CliCommand()
9
+ @IsPrivate()
8
10
  export class TestChildWorkerCommand {
9
11
  preMain(): void {
10
12
  EventEmitter.defaultMaxListeners = 1000;
@@ -1,10 +1,13 @@
1
1
  import { CliCommand } from '@travetto/cli';
2
2
  import { Env, Runtime, describeFunction } from '@travetto/runtime';
3
+ import { Registry } from '@travetto/registry';
4
+ import { IsPrivate } from '@travetto/schema';
3
5
 
4
- import { SuiteRegistry } from '../src/registry/suite.ts';
6
+ import { SuiteRegistryIndex } from '../src/registry/registry-index.ts';
5
7
  import { RunnerUtil } from '../src/execute/util.ts';
6
8
 
7
- @CliCommand({ hidden: true })
9
+ @CliCommand()
10
+ @IsPrivate()
8
11
  export class TestDigestCommand {
9
12
 
10
13
  output: 'json' | 'text' = 'text';
@@ -24,13 +27,17 @@ export class TestDigestCommand {
24
27
  }
25
28
  }
26
29
 
27
- await SuiteRegistry.init();
30
+ await Registry.init();
28
31
 
29
- const suites = SuiteRegistry.getClasses();
32
+ const suites = SuiteRegistryIndex.getClasses();
30
33
  const all = suites
31
- .map(c => SuiteRegistry.get(c))
34
+ .map(c => SuiteRegistryIndex.getConfig(c))
32
35
  .filter(c => !describeFunction(c.class).abstract)
33
- .flatMap(c => c.tests);
36
+ .flatMap(c => Object.values(c.tests))
37
+ .toSorted((a, b) => {
38
+ const classComp = a.classId.localeCompare(b.classId);
39
+ return classComp !== 0 ? classComp : a.methodName.localeCompare(b.methodName);
40
+ });
34
41
 
35
42
  if (this.output === 'json') {
36
43
  console.log(JSON.stringify(all));
@@ -1,14 +1,23 @@
1
1
  import { Env } from '@travetto/runtime';
2
2
  import { CliCommand } from '@travetto/cli';
3
+ import { IsPrivate } from '@travetto/schema';
3
4
 
4
5
  import { runTests, selectConsumer } from './bin/run.ts';
5
6
 
6
7
  /** Direct test invocation */
7
- @CliCommand({ hidden: true })
8
+ @CliCommand()
9
+ @IsPrivate()
8
10
  export class TestDirectCommand {
9
11
 
12
+ @IsPrivate()
10
13
  format: string = 'tap';
11
14
 
15
+ /**
16
+ * Format options
17
+ * @alias o
18
+ */
19
+ formatOptions?: string[];
20
+
12
21
  async preValidate(): Promise<void> {
13
22
  await selectConsumer(this);
14
23
  }
@@ -21,8 +30,12 @@ export class TestDirectCommand {
21
30
  }
22
31
 
23
32
  main(importOrFile: string, clsId?: string, methodsNames: string[] = []): Promise<void> {
33
+
34
+ const options = Object.fromEntries((this.formatOptions ?? [])?.map(f => [...f.split(':'), true]));
35
+
24
36
  return runTests({
25
37
  consumer: this.format,
38
+ consumerOptions: options,
26
39
  target: {
27
40
  import: importOrFile,
28
41
  classId: clsId,
@@ -1,68 +0,0 @@
1
- import path from 'node:path';
2
-
3
- import { classConstruct, describeFunction, type Class } from '@travetto/runtime';
4
-
5
- import type { TestConsumerShape } from './types.ts';
6
- import type { RunState } from '../execute/types.ts';
7
-
8
- /**
9
- * Test Results Handler Registry
10
- */
11
- class $TestConsumerRegistry {
12
- #registered = new Map<string, Class<TestConsumerShape>>();
13
-
14
- /**
15
- * Manual initialization when running outside of the bootstrap process
16
- */
17
- async manualInit(): Promise<void> {
18
- await import('./types/all.ts');
19
- }
20
-
21
- /**
22
- * Add a new consumer
23
- * @param cls The consumer class
24
- */
25
- add(cls: Class<TestConsumerShape>): void {
26
- const desc = describeFunction(cls);
27
- const key = desc.module?.includes('@travetto') ? path.basename(desc.modulePath) : desc.import;
28
- this.#registered.set(key, cls);
29
- }
30
-
31
- /**
32
- * Retrieve a registered consumer
33
- * @param type The unique identifier
34
- */
35
- get(type: string): Class<TestConsumerShape> {
36
- return this.#registered.get(type)!;
37
- }
38
-
39
- /**
40
- * Get types
41
- */
42
- getTypes(): string[] {
43
- return [...this.#registered.keys()];
44
- }
45
-
46
- /**
47
- * Get a consumer instance that supports summarization
48
- * @param consumer The consumer identifier or the actual consumer
49
- */
50
- async getInstance(state: Pick<RunState, 'consumer' | 'consumerOptions'>): Promise<TestConsumerShape> {
51
- // TODO: Fix consumer registry init
52
- await this.manualInit();
53
- const inst = classConstruct(this.get(state.consumer));
54
- await inst.setOptions?.(state.consumerOptions ?? {});
55
- return inst;
56
- }
57
- }
58
-
59
- export const TestConsumerRegistry = new $TestConsumerRegistry();
60
-
61
- /**
62
- * Registers a class a valid test consumer
63
- */
64
- export function TestConsumer(): (cls: Class<TestConsumerShape>) => void {
65
- return function (cls: Class<TestConsumerShape>): void {
66
- TestConsumerRegistry.add(cls);
67
- };
68
- }
@@ -1,10 +0,0 @@
1
- import './cumulative';
2
- import './event';
3
- import './exec';
4
- import './json';
5
- import './noop';
6
- import './runnable';
7
- import './summarizer';
8
- import './tap-summary';
9
- import './tap';
10
- import './xunit';