@travetto/test 3.0.2 → 3.1.0-rc.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,6 +1,7 @@
1
1
  <!-- This file was generated by @travetto/doc and should not be modified directly -->
2
- <!-- Please modify https://github.com/travetto/travetto/tree/main/module/test/DOC.ts and execute "npx trv doc" to rebuild -->
2
+ <!-- Please modify https://github.com/travetto/travetto/tree/main/module/test/DOC.tsx and execute "npx trv doc" to rebuild -->
3
3
  # Testing
4
+
4
5
  ## Declarative test framework
5
6
 
6
7
  **Install: @travetto/test**
@@ -13,8 +14,6 @@ yarn add @travetto/test
13
14
  ```
14
15
 
15
16
  This module provides unit testing functionality that integrates with the framework. It is a declarative framework, using decorators to define tests and suites. The test produces results in the following formats:
16
-
17
-
18
17
  * [TAP 13](https://testanything.org/tap-version-13-specification.html), default and human-readable
19
18
  * [JSON](https://www.json.org), best for integrating with at a code level
20
19
  * [xUnit](https://en.wikipedia.org/wiki/XUnit), standard format for CI/CD systems e.g. Jenkins, Bamboo, etc.
@@ -22,8 +21,7 @@ This module provides unit testing functionality that integrates with the framewo
22
21
  **Note**: All tests should be under the `test/.*` folders. The pattern for tests is defined as a regex and not standard globbing.
23
22
 
24
23
  ## Definition
25
-
26
- A test suite is a collection of individual tests. All test suites are classes with the [@Suite](https://github.com/travetto/travetto/tree/main/module/test/src/decorator/suite.ts#L14) decorator. Tests are defined as methods on the suite class, using the [@Test](https://github.com/travetto/travetto/tree/main/module/test/src/decorator/test.ts#L20) decorator. All tests intrinsically support `async`/`await`.
24
+ A test suite is a collection of individual tests. All test suites are classes with the [@Suite](https://github.com/travetto/travetto/tree/main/module/test/src/decorator/suite.ts#L14) decorator. Tests are defined as methods on the suite class, using the [@Test](https://github.com/travetto/travetto/tree/main/module/test/src/decorator/test.ts#L20) decorator. All tests intrinsically support `async`/`await`.
27
25
 
28
26
  A simple example would be:
29
27
 
@@ -112,8 +110,6 @@ AssertionError(
112
110
  ```
113
111
 
114
112
  The equivalences for all of the [assert](https://nodejs.org/api/assert.html) operations are:
115
-
116
-
117
113
  * `assert(a == b)` as `assert.equal(a, b)`
118
114
  * `assert(a !== b)` as `assert.notEqual(a, b)`
119
115
  * `assert(a === b)` as `assert.strictEqual(a, b)`
@@ -125,65 +121,62 @@ The equivalences for all of the [assert](https://nodejs.org/api/assert.html) ope
125
121
  * `assert(a instanceof b)` as `assert.instanceOf(a, b)`
126
122
  * `assert(a.includes(b))` as `assert.ok(a.includes(b))`
127
123
  * `assert(/a/.test(b))` as `assert.ok(/a/.test(b))`
128
-
129
124
  In addition to the standard operations, there is support for throwing/rejecting errors (or the inverse). This is useful for testing error states or ensuring errors do not occur.
130
125
 
131
-
132
- * `throws`/`doesNotThrow` is for catching synchronous rejections
133
-
134
- **Code: Throws vs Does Not Throw**
135
- ```typescript
136
- import assert from 'assert';
137
-
138
- import { Suite, Test } from '@travetto/test';
139
-
140
- @Suite()
141
- class SimpleTest {
142
-
143
- @Test()
144
- async testThrows() {
145
- assert.throws(() => {
146
- throw new Error();
147
- });
148
-
149
- assert.doesNotThrow(() => {
150
-
151
- let a = 5;
152
- });
153
- }
154
- }
155
- ```
156
-
157
-
158
- * `rejects`/`doesNotReject` is for catching asynchronous rejections
159
-
160
- **Code: Rejects vs Does Not Reject**
161
- ```typescript
162
- import assert from 'assert';
163
-
164
- import { Suite, Test } from '@travetto/test';
165
-
166
- @Suite()
167
- class SimpleTest {
168
-
169
- @Test()
170
- async testRejects() {
171
- await assert.rejects(async () => {
172
- throw new Error();
173
- });
174
-
175
- await assert.doesNotReject(async () => {
176
-
177
- let a = 5;
178
- });
179
- }
180
- }
181
- ```
182
-
183
-
184
-
185
- Additionally, the `throws`/`rejects` assertions take in a secondary parameter to allow for specification of the type of error expected. This can be:
186
-
126
+ ### Throws
127
+ `throws`/`doesNotThrow` is for catching synchronous rejections
128
+
129
+ **Code: Throws vs Does Not Throw**
130
+ ```typescript
131
+ import assert from 'assert';
132
+
133
+ import { Suite, Test } from '@travetto/test';
134
+
135
+ @Suite()
136
+ class SimpleTest {
137
+
138
+ @Test()
139
+ async testThrows() {
140
+ assert.throws(() => {
141
+ throw new Error();
142
+ });
143
+
144
+ assert.doesNotThrow(() => {
145
+
146
+ let a = 5;
147
+ });
148
+ }
149
+ }
150
+ ```
151
+
152
+ ### Rejects
153
+ `rejects`/`doesNotReject` is for catching asynchronous rejections
154
+
155
+ **Code: Rejects vs Does Not Reject**
156
+ ```typescript
157
+ import assert from 'assert';
158
+
159
+ import { Suite, Test } from '@travetto/test';
160
+
161
+ @Suite()
162
+ class SimpleTest {
163
+
164
+ @Test()
165
+ async testRejects() {
166
+ await assert.rejects(async () => {
167
+ throw new Error();
168
+ });
169
+
170
+ await assert.doesNotReject(async () => {
171
+
172
+ let a = 5;
173
+ });
174
+ }
175
+ }
176
+ ```
177
+
178
+ ### Error Matching
179
+ Additionally, the `throws`/`rejects` assertions take in a secondary parameter to allow for specification of the type of error expected. This can be:
187
180
  * A regular expression or string to match against the error's message
188
181
  * A class to ensure the returned error is an instance of the class passed in
189
182
  * A function to allow for whatever custom verification of the error is needed
@@ -221,29 +214,27 @@ class SimpleTest {
221
214
  ```
222
215
 
223
216
  ## Running Tests
224
-
225
217
  To run the tests you can either call the [Command Line Interface](https://github.com/travetto/travetto/tree/main/module/cli#readme "CLI infrastructure for Travetto framework") by invoking
226
218
 
227
219
  **Terminal: Test Help Output**
228
220
  ```bash
229
221
  $ trv test --help
230
222
 
231
- Usage: test [options] [regexes...]
223
+ Usage: test [options] [first:string] [regexes...:string]
232
224
 
233
225
  Options:
234
- -f, --format <format> Output format for test results (default: "tap")
235
- -c, --concurrency <concurrency> Number of tests to run concurrently (default: 4)
236
- -m, --mode <mode> Test run mode (default: "standard")
237
- -h, --help display help for command
226
+ -f, --format <string> Output format for test results (default: "tap")
227
+ -c, --concurrency <number> Number of tests to run concurrently (default: 4)
228
+ -m, --mode <single|standard> Test run mode (default: "standard")
229
+ -h, --help display help for command
238
230
  ```
239
231
 
240
232
  The regexes are the patterns of tests you want to run, and all tests must be found under the `test/` folder.
241
233
 
242
234
  ### Travetto Plugin
243
-
244
235
  The [VSCode plugin](https://marketplace.visualstudio.com/items?itemName=arcsine.travetto-plugin) also supports test running, which will provide even more functionality for real-time testing and debugging.
245
236
 
246
237
  ## Additional Considerations
247
- During the test execution, a few things additionally happen that should be helpful. The primary addition, is that all console output is captured, and will be exposed in the test output. This allows for investigation at a later point in time by analyzing the output.
238
+ During the test execution, a few things additionally happen that should be helpful. The primary addition, is that all console output is captured, and will be exposed in the test output. This allows for investigation at a later point in time by analyzing the output.
248
239
 
249
240
  Like output, all promises are also intercepted. This allows the code to ensure that all promises have been resolved before completing the test. Any uncompleted promises will automatically trigger an error state and fail the test.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@travetto/test",
3
- "version": "3.0.2",
3
+ "version": "3.1.0-rc.0",
4
4
  "description": "Declarative test framework",
5
5
  "keywords": [
6
6
  "unit-testing",
@@ -27,15 +27,15 @@
27
27
  "directory": "module/test"
28
28
  },
29
29
  "dependencies": {
30
- "@travetto/base": "^3.0.2",
31
- "@travetto/registry": "^3.0.2",
32
- "@travetto/terminal": "^3.0.2",
33
- "@travetto/worker": "^3.0.2",
34
- "@travetto/yaml": "^3.0.2"
30
+ "@travetto/base": "^3.1.0-rc.0",
31
+ "@travetto/registry": "^3.1.0-rc.0",
32
+ "@travetto/terminal": "^3.1.0-rc.0",
33
+ "@travetto/worker": "^3.1.0-rc.0",
34
+ "@travetto/yaml": "^3.1.0-rc.0"
35
35
  },
36
36
  "peerDependencies": {
37
- "@travetto/cli": "^3.0.2",
38
- "@travetto/transformer": "^3.0.2"
37
+ "@travetto/cli": "^3.1.0-rc.0",
38
+ "@travetto/transformer": "^3.1.0-rc.0"
39
39
  },
40
40
  "peerDependenciesMeta": {
41
41
  "@travetto/transformer": {
@@ -70,7 +70,7 @@ export class AssertCheck {
70
70
  } else if (fn === 'includes') {
71
71
  assertion.operator = fn;
72
72
  // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
73
- [assertion.expected, assertion.actual, assertion.message] = args as [unknown, unknown, string];
73
+ [assertion.actual, assertion.expected, assertion.message] = args as [unknown, unknown, string];
74
74
  } else if (fn === 'instanceof') {
75
75
  assertion.operator = fn;
76
76
  // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
@@ -98,6 +98,10 @@ export class AssertCheck {
98
98
 
99
99
  // Actually run the assertion
100
100
  switch (fn) {
101
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
102
+ case 'includes': assertFn((actual as unknown[]).includes(expected), message); break;
103
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
104
+ case 'test': assertFn((expected as RegExp).test(actual as string), message); break;
101
105
  // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
102
106
  case 'instanceof': assertFn(actual instanceof (expected as Class), message); break;
103
107
  // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
@@ -120,10 +124,6 @@ export class AssertCheck {
120
124
  }
121
125
  // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
122
126
  assert[fn as 'ok'].apply(null, args as [boolean, string | undefined]);
123
- // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
124
- } else if (expected && !!(expected as Record<string, Function>)[fn]) { // Dotted Method call (e.g. assert.rejects)
125
- // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
126
- assertFn((expected as typeof assert)[fn as 'ok'](actual));
127
127
  }
128
128
  }
129
129
 
@@ -41,7 +41,7 @@ export class AssertUtil {
41
41
  * Determine file location for a given error and the stack trace
42
42
  */
43
43
  static getPositionOfError(err: Error, filename: string): { file: string, line: number } {
44
- const cwd = path.cwd();
44
+ const cwd = RootIndex.mainModule.sourcePath;
45
45
  const lines = path.toPosix(err.stack ?? new Error().stack!)
46
46
  .split('\n')
47
47
  // Exclude node_modules, target self
@@ -58,9 +58,8 @@ export const TestConsumerRegistry = new $TestConsumerRegistry();
58
58
  * @param type The unique identifier for the consumer
59
59
  * @param isDefault Is this the default consumer. Last one wins
60
60
  */
61
- export function Consumable(type: string, isDefault = false): ClassDecorator {
62
- // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
63
- return function (cls: Class<TestConsumer>) {
61
+ export function Consumable(type: string, isDefault = false): (cls: Class<TestConsumer>) => void {
62
+ return function (cls: Class<TestConsumer>): void {
64
63
  TestConsumerRegistry.add(type, cls, isDefault);
65
- } as ClassDecorator;
64
+ };
66
65
  }
@@ -1,4 +1,4 @@
1
- import { TestConsumer } from '../types';
1
+ import { TestConsumer, TestRunState } from '../types';
2
2
  import { TestResultsSummarizer } from './summarizer';
3
3
  import { TestConsumerRegistry } from '../registry';
4
4
  import { TestEvent } from '../../model/event';
@@ -27,9 +27,9 @@ export class RunnableTestConsumer implements TestConsumer {
27
27
  }
28
28
  }
29
29
 
30
- async onStart(files: string[]): Promise<void> {
30
+ async onStart(state: TestRunState): Promise<void> {
31
31
  for (const c of this.#consumers) {
32
- await c.onStart?.(files);
32
+ await c.onStart?.(state);
33
33
  }
34
34
  }
35
35
 
@@ -1,13 +1,12 @@
1
1
  import { GlobalTerminal, TermStyleInput, Terminal } from '@travetto/terminal';
2
2
  import { ManualAsyncIterator } from '@travetto/worker';
3
- import { RootIndex } from '@travetto/manifest';
4
-
5
- import { SuitesSummary, TestConsumer } from '../types';
6
- import { Consumable } from '../registry';
7
3
 
8
4
  import { TestEvent } from '../../model/event';
9
5
  import { TestResult } from '../../model/test';
10
- import { SuiteRegistry } from '../../registry/suite';
6
+
7
+ import { SuitesSummary, TestConsumer, TestRunState } from '../types';
8
+ import { Consumable } from '../registry';
9
+
11
10
  import { TapEmitter } from './tap';
12
11
 
13
12
  /**
@@ -49,25 +48,12 @@ export class TapStreamedEmitter implements TestConsumer {
49
48
  this.#consumer = new TapEmitter(this.#terminal);
50
49
  }
51
50
 
52
- async onStart(files: string[]): Promise<void> {
51
+ async onStart(state: TestRunState): Promise<void> {
53
52
  this.#consumer.onStart();
54
53
 
55
- // Load all tests
56
- for (const file of files) {
57
- await import(RootIndex.getFromSource(file)!.import);
58
- }
59
-
60
- await SuiteRegistry.init();
61
-
62
- const suites = SuiteRegistry.getClasses();
63
- const total = suites
64
- .map(c => SuiteRegistry.get(c))
65
- .filter(c => !RootIndex.getFunctionMetadata(c.class)?.abstract)
66
- .reduce((acc, c) => acc + (c.tests?.length ?? 0), 0);
67
-
68
54
  this.#progress = this.#terminal.streamToPosition(this.#results,
69
- TapStreamedEmitter.makeProgressBar(this.#terminal, total),
70
- { position: 'bottom' }
55
+ TapStreamedEmitter.makeProgressBar(this.#terminal, state.testCount ?? 0),
56
+ { position: 'bottom', minDelay: 100 }
71
57
  );
72
58
  }
73
59
 
@@ -1,4 +1,4 @@
1
- import { path } from '@travetto/manifest';
1
+ import { RootIndex } from '@travetto/manifest';
2
2
  import { GlobalTerminal, Terminal } from '@travetto/terminal';
3
3
  import { ErrorUtil, ObjectUtil, TimeUtil } from '@travetto/base';
4
4
  import { YamlUtil } from '@travetto/yaml';
@@ -61,8 +61,6 @@ export class TapEmitter implements TestConsumer {
61
61
  }
62
62
  this.log(`# ${header}`);
63
63
 
64
- const cwd = path.cwd();
65
-
66
64
  // Handle each assertion
67
65
  if (test.assertions.length) {
68
66
  let subCount = 0;
@@ -72,7 +70,7 @@ export class TapEmitter implements TestConsumer {
72
70
  this.#enhancer.assertNumber(++subCount),
73
71
  '-',
74
72
  this.#enhancer.assertDescription(text),
75
- `${this.#enhancer.assertFile(asrt.file.replace(cwd, '.'))}:${this.#enhancer.assertLine(asrt.line)}`
73
+ `${this.#enhancer.assertFile(asrt.file.replace(RootIndex.mainModule.sourcePath, '.'))}:${this.#enhancer.assertLine(asrt.line)}`
76
74
  ].join(' ');
77
75
 
78
76
  if (asrt.error) {
@@ -19,6 +19,10 @@ export interface SuitesSummary extends Counts {
19
19
  duration: number;
20
20
  }
21
21
 
22
+ export type TestRunState = {
23
+ testCount?: number;
24
+ };
25
+
22
26
  /**
23
27
  * A test result handler
24
28
  */
@@ -26,7 +30,7 @@ export interface TestConsumer {
26
30
  /**
27
31
  * Listen for start of the test run
28
32
  */
29
- onStart?(files: string[]): Promise<void> | void;
33
+ onStart?(testState: TestRunState): Promise<void> | void;
30
34
  /**
31
35
  * Handle individual tests events
32
36
  */
@@ -71,7 +71,7 @@ export class TestExecutor {
71
71
  const classId = RootIndex.getId(file, name);
72
72
  // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
73
73
  const suite = { class: { name }, classId, duration: 0, lines: { start: 1, end: 1 }, file, } as SuiteConfig & SuiteResult;
74
- err.message = err.message.replace(path.cwd(), '.');
74
+ err.message = err.message.replaceAll(RootIndex.mainModule.sourcePath, '.');
75
75
  const res = AssertUtil.generateSuiteError(suite, 'require', err);
76
76
  consumer.onEvent({ type: 'suite', phase: 'before', suite });
77
77
  consumer.onEvent({ type: 'test', phase: 'before', test: res.testConfig });
@@ -42,7 +42,8 @@ export class Runner {
42
42
  max: this.#state.concurrency
43
43
  });
44
44
 
45
- await consumer.onStart(files);
45
+ const testCount = await RunnerUtil.getTestCount(this.#state.args);
46
+ await consumer.onStart({ testCount });
46
47
  await pool.process(new IterableWorkSet(files));
47
48
  return consumer.summarizeAsBoolean();
48
49
  }
@@ -55,7 +56,7 @@ export class Runner {
55
56
 
56
57
  const [file, ...args] = this.#state.args;
57
58
 
58
- await consumer.onStart([path.resolve(file)]);
59
+ await consumer.onStart({});
59
60
  await TestExecutor.execute(consumer, file, ...args);
60
61
  return consumer.summarizeAsBoolean();
61
62
  }
@@ -1,7 +1,7 @@
1
1
  import { createReadStream } from 'fs';
2
2
  import readline from 'readline';
3
3
 
4
- import { ShutdownManager, TimeUtil } from '@travetto/base';
4
+ import { ExecUtil, ShutdownManager, TimeUtil } from '@travetto/base';
5
5
  import { RootIndex } from '@travetto/manifest';
6
6
 
7
7
  /**
@@ -47,4 +47,18 @@ export class RunnerUtil {
47
47
  .filter(x => x.valid)
48
48
  .map(x => x.file);
49
49
  }
50
+
51
+ /**
52
+ * Get count of tests for a given set of patterns
53
+ * @param patterns
54
+ * @returns
55
+ */
56
+ static async getTestCount(patterns: string[]): Promise<number> {
57
+ const proc = ExecUtil.spawn('npx', ['trv', 'test:count', ...patterns], { stdio: 'pipe', catchAsResult: true });
58
+ const countRes = await proc.result;
59
+ if (!countRes.valid) {
60
+ throw new Error(countRes.stderr);
61
+ }
62
+ return countRes.valid ? +countRes.stdout : 0;
63
+ }
50
64
  }
@@ -1,7 +1,7 @@
1
1
  import { RootRegistry, MethodSource } from '@travetto/registry';
2
2
  import { WorkPool, IterableWorkSet, ManualAsyncIterator } from '@travetto/worker';
3
3
  import { RootIndex } from '@travetto/manifest';
4
- import { ConsoleManager, defineGlobalEnv, ObjectUtil } from '@travetto/base';
4
+ import { ObjectUtil } from '@travetto/base';
5
5
 
6
6
  import { SuiteRegistry } from '../registry/suite';
7
7
  import { buildStandardTestManager } from '../worker/standard';
@@ -86,20 +86,4 @@ export class TestWatcher {
86
86
 
87
87
  await pool.process(src);
88
88
  }
89
- }
90
-
91
- export async function main(format: string = 'tap', runAllOnStart: string = 'true'): Promise<void> {
92
- defineGlobalEnv({ test: true, dynamic: true });
93
- console.log('Starting');
94
- ConsoleManager.setDebugFromEnv();
95
- // Quit on parent disconnect
96
- if (process.send) {
97
- process.on('disconnect', () => process.exit(0));
98
- }
99
- try {
100
- await TestWatcher.watch(format, runAllOnStart !== 'false');
101
- console.log('Done');
102
- } catch (err) {
103
- console.error(err);
104
- }
105
89
  }
@@ -1,7 +1,7 @@
1
1
  import fs from 'fs/promises';
2
2
 
3
3
  import { path } from '@travetto/manifest';
4
- import { ConsoleManager, defineGlobalEnv, ErrorUtil, TimeUtil } from '@travetto/base';
4
+ import { ConsoleManager, ErrorUtil, TimeUtil } from '@travetto/base';
5
5
  import { ChildCommChannel } from '@travetto/worker';
6
6
 
7
7
  import { RunnerUtil } from '../execute/util';
@@ -88,10 +88,4 @@ export class TestChildWorker extends ChildCommChannel<RunEvent> {
88
88
  concurrency: 1
89
89
  }).run();
90
90
  }
91
- }
92
-
93
- export async function main(): Promise<void> {
94
- defineGlobalEnv({ test: true, set: { FORCE_COLOR: 0 } });
95
- ConsoleManager.setDebugFromEnv();
96
- await new TestChildWorker().activate();
97
91
  }
@@ -37,11 +37,11 @@ export function buildStandardTestManager(consumer: TestConsumer): () => Worker<s
37
37
  const channel = new ParentCommChannel<TestEvent & { error?: Error }>(
38
38
  ExecUtil.fork(
39
39
  RootIndex.resolveFileImport('@travetto/cli/support/entry.cli'),
40
- ['main', '@travetto/test/src/worker/child.ts'],
40
+ ['test:child'],
41
41
  {
42
42
  cwd,
43
- env: { TRV_MANIFEST: RootIndex.getModule(module)!.outputPath },
44
- stdio: [0, 'ignore', 2, 'ipc']
43
+ env: { TRV_MANIFEST: RootIndex.getModule(module)!.outputPath, TRV_QUIET: '1' },
44
+ stdio: ['ignore', 'ignore', 2, 'ipc']
45
45
  }
46
46
  )
47
47
  );
@@ -1,8 +1,5 @@
1
1
  import { ShutdownManager, TimeUtil } from '@travetto/base';
2
2
 
3
- import { RunnerUtil } from '../../src/execute/util';
4
- import { Runner } from '../../src/execute/runner';
5
-
6
3
  import type { RunState } from '../../src/execute/types';
7
4
 
8
5
  declare global {
@@ -19,6 +16,9 @@ declare global {
19
16
  * @param opts
20
17
  */
21
18
  export async function runTests(opts: RunState): Promise<void> {
19
+ const { RunnerUtil } = await import('../../src/execute/util.js');
20
+ const { Runner } = await import('../../src/execute/runner.js');
21
+
22
22
  RunnerUtil.registerCleanup('runner');
23
23
 
24
24
  if (process.env.TRV_TEST_DELAY) {
@@ -0,0 +1,2 @@
1
+ export type TestFormat = 'tap' | 'tap-streamed' | 'xunit' | 'event' | 'exec';
2
+ export type TestMode = 'single' | 'standard';
@@ -1,76 +1,65 @@
1
- import { readFileSync } from 'fs';
2
1
  import fs from 'fs/promises';
3
2
 
4
- import { path, RootIndex } from '@travetto/manifest';
3
+ import { path } from '@travetto/manifest';
5
4
  import { GlobalEnvConfig } from '@travetto/base';
6
- import { CliCommand, OptionConfig } from '@travetto/cli';
5
+ import { CliCommandShape, CliCommand, CliValidationError } from '@travetto/cli';
7
6
  import { WorkPool } from '@travetto/worker';
7
+ import { Max, Min } from '@travetto/schema';
8
8
 
9
- import type { RunState } from '../src/execute/types';
10
-
11
- const modes = ['single', 'standard'] as const;
12
-
13
- type Options = {
14
- format: OptionConfig<string>;
15
- concurrency: OptionConfig<number>;
16
- mode: OptionConfig<(typeof modes)[number]>;
17
- };
9
+ import { TestFormat, TestMode } from './bin/types';
18
10
 
19
11
  /**
20
12
  * Launch test framework and execute tests
21
13
  */
22
- export class TestCommand extends CliCommand<Options> {
23
- name = 'test';
24
- _types: string[];
25
-
26
- getTypes(): string[] {
27
- if (!this._types) {
28
- this._types = RootIndex
29
- .findSrc({ filter: /consumer\/types\/.*/, profiles: ['test'] })
30
- .map(x => readFileSync(`${x.outputFile}`, 'utf8').match(/Consumable.?[(]'([^']+)/)?.[1])
31
- .filter((x?: string): x is string => !!x);
32
- }
33
- return this._types;
34
- }
35
-
36
- getOptions(): Options {
37
- return {
38
- format: this.choiceOption({ desc: 'Output format for test results', def: 'tap', choices: this.getTypes() }),
39
- concurrency: this.intOption({ desc: 'Number of tests to run concurrently', lower: 1, upper: 32, def: WorkPool.DEFAULT_SIZE }),
40
- mode: this.choiceOption({ desc: 'Test run mode', def: 'standard', choices: [...modes] })
41
- };
42
- }
14
+ @CliCommand()
15
+ export class TestCommand implements CliCommandShape {
16
+ /** Output format for test results */
17
+ format: TestFormat = 'tap';
18
+ /** Number of tests to run concurrently */
19
+ @Min(1) @Max(WorkPool.MAX_SIZE)
20
+ concurrency: number = WorkPool.MAX_SIZE;
21
+ /** Test run mode */
22
+ mode: TestMode = 'standard';
43
23
 
44
24
  envInit(): GlobalEnvConfig {
45
25
  return { test: true };
46
26
  }
47
27
 
48
- getArgs(): string {
49
- return '[regexes...]';
28
+ isFirstFile(first: string): Promise<boolean> {
29
+ return fs.stat(path.resolve(first ?? '')).then(x => x.isFile(), () => false);
50
30
  }
51
31
 
52
- async action(regexes: string[]): Promise<void> {
53
- const { runTests } = await import('./bin/run.js');
54
-
55
- const [first] = regexes;
32
+ async resolvedMode(first: string, rest: string[]): Promise<TestMode> {
33
+ return (await this.isFirstFile(first)) && rest.length === 0 ? 'single' : this.mode;
34
+ }
56
35
 
57
- const isFile = await fs.stat(path.resolve(first ?? '')).then(x => x.isFile(), () => false);
36
+ async validate(first: string = 'test/.*', rest: string[]): Promise<CliValidationError | undefined> {
58
37
 
59
- const state: RunState = {
60
- args: !first ? ['test/.*'] : regexes,
61
- mode: isFile && regexes.length === 1 ? 'single' : this.cmd.mode,
62
- concurrency: this.cmd.concurrency,
63
- format: this.cmd.format
64
- };
38
+ const mode = await this.resolvedMode(first, rest);
65
39
 
66
- if (state.mode === 'single') {
67
- if (!isFile) {
68
- return this.showHelp('You must specify a proper test file to run in single mode');
69
- } else if (!/test\//.test(first)) {
70
- return this.showHelp('Only files in the test/ folder are permitted to be run');
71
- }
40
+ if (mode === 'single' && !await this.isFirstFile(first)) {
41
+ return {
42
+ message: 'You must specify a proper test file to run in single mode',
43
+ kind: 'required',
44
+ path: 'regexes'
45
+ };
46
+ } else if (!/test\//.test(first)) {
47
+ return {
48
+ message: 'Only files in the test/ folder are permitted to be run',
49
+ kind: 'required',
50
+ path: 'regexes'
51
+ };
72
52
  }
53
+ }
54
+
55
+ async main(first: string = 'test/.*', regexes: string[] = []): Promise<void> {
56
+ const { runTests } = await import('./bin/run.js');
73
57
 
74
- await runTests(state);
58
+ return runTests({
59
+ args: [first, ...regexes],
60
+ mode: await this.resolvedMode(first, regexes),
61
+ concurrency: this.concurrency,
62
+ format: this.format
63
+ });
75
64
  }
76
65
  }
@@ -0,0 +1,19 @@
1
+ import { GlobalEnvConfig, ShutdownManager } from '@travetto/base';
2
+ import { CliCommand } from '@travetto/cli';
3
+
4
+ /** Test child worker target */
5
+ @CliCommand({ hidden: true })
6
+ export class TestChildWorkerCommand {
7
+ envInit(): GlobalEnvConfig {
8
+ return { test: true, set: { FORCE_COLOR: 0 } };
9
+ }
10
+
11
+ async main(): Promise<void> {
12
+ if (process.send) {
13
+ // Shutdown when ipc bridge is closed
14
+ process.on('disconnect', () => ShutdownManager.execute());
15
+ }
16
+ const { TestChildWorker } = await import('../src/worker/child.js');
17
+ return new TestChildWorker().activate();
18
+ }
19
+ }
@@ -0,0 +1,34 @@
1
+ import { CliCommand } from '@travetto/cli';
2
+ import { RootIndex } from '@travetto/manifest';
3
+
4
+ import { SuiteRegistry } from '../src/registry/suite';
5
+ import { RunnerUtil } from '../src/execute/util';
6
+ import { GlobalEnvConfig } from '@travetto/base';
7
+
8
+ @CliCommand({ hidden: true })
9
+ export class TestCountCommand {
10
+
11
+ envInit(): GlobalEnvConfig {
12
+ return { debug: false, test: true };
13
+ }
14
+
15
+ async main(patterns: string[]) {
16
+ const regexes = patterns.map(x => new RegExp(x));
17
+ const files = await RunnerUtil.getTestFiles(regexes);
18
+
19
+ // Load all tests
20
+ for (const file of files) {
21
+ await import(RootIndex.getFromSource(file)!.import);
22
+ }
23
+
24
+ await SuiteRegistry.init();
25
+
26
+ const suites = SuiteRegistry.getClasses();
27
+ const total = suites
28
+ .map(c => SuiteRegistry.get(c))
29
+ .filter(c => !RootIndex.getFunctionMetadata(c.class)?.abstract)
30
+ .reduce((acc, c) => acc + (c.tests?.length ?? 0), 0);
31
+
32
+ console.log(total);
33
+ }
34
+ }
@@ -0,0 +1,20 @@
1
+ import { GlobalEnvConfig } from '@travetto/base';
2
+ import { CliCommand } from '@travetto/cli';
3
+
4
+ import { runTests } from './bin/run';
5
+ import { TestFormat } from './bin/types';
6
+
7
+ /** Direct test invocation */
8
+ @CliCommand({ hidden: true })
9
+ export class TestDirectCommand {
10
+
11
+ format: TestFormat = 'tap';
12
+
13
+ envInit(): GlobalEnvConfig {
14
+ return { test: true };
15
+ }
16
+
17
+ main(file: string, args: string[]): Promise<void> {
18
+ return runTests({ args: [file, ...args], format: this.format, mode: 'single', concurrency: 1 });
19
+ }
20
+ }
@@ -0,0 +1,34 @@
1
+ import { GlobalEnvConfig } from '@travetto/base';
2
+ import { CliCommand } from '@travetto/cli';
3
+
4
+ import { TestFormat } from './bin/types';
5
+
6
+ /**
7
+ * Invoke the test watcher
8
+ */
9
+ @CliCommand()
10
+ export class TestWatcherCommand {
11
+
12
+ format: TestFormat = 'tap';
13
+ mode: 'all' | 'change' = 'all';
14
+
15
+ envInit(): GlobalEnvConfig {
16
+ return { test: true, dynamic: true };
17
+ }
18
+
19
+ async main(): Promise<void> {
20
+ // Quit on parent disconnect
21
+ if (process.send) {
22
+ process.on('disconnect', () => process.exit(0));
23
+ }
24
+
25
+ try {
26
+ const { TestWatcher } = await import('../src/execute/watcher.js');
27
+ console.log('Starting');
28
+ await TestWatcher.watch(this.format, this.mode === 'all');
29
+ console.log('Done');
30
+ } catch (err) {
31
+ console.error(err);
32
+ }
33
+ }
34
+ }
@@ -237,10 +237,10 @@ export class AssertTransformer {
237
237
  if (matched) {
238
238
  const resolved = state.resolveType(root);
239
239
  if (resolved.key === 'literal' && matched.find(x => resolved.ctor === x)) { // Ensure method is against real type
240
- return {
241
- fn: key.text,
242
- args: [comp.arguments[0], comp.expression.expression, ...args.slice(1)]
243
- };
240
+ switch (key.text) {
241
+ case 'includes': return { fn: key.text, args: [comp.expression.expression, comp.arguments[0], ...args.slice(1)] };
242
+ case 'test': return { fn: key.text, args: [comp.arguments[0], comp.expression.expression, ...args.slice(1)] };
243
+ }
244
244
  }
245
245
  }
246
246
  }
@@ -1,15 +0,0 @@
1
- import { ConsoleManager, defineGlobalEnv } from '@travetto/base';
2
- import { runTests } from './run';
3
-
4
- // Direct entry point
5
- export function main(...args: string[]): Promise<void> {
6
- defineGlobalEnv({ test: true });
7
- ConsoleManager.setDebugFromEnv();
8
-
9
- return runTests({
10
- args,
11
- format: process.env.TRV_TEST_FORMAT ?? 'tap',
12
- mode: 'single',
13
- concurrency: 1
14
- });
15
- }