@travetto/test 5.0.0-rc.3 → 5.0.0-rc.5

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
@@ -21,7 +21,7 @@ This module provides unit testing functionality that integrates with the framewo
21
21
  **Note**: All tests should be under the `test/.*` folders. The pattern for tests is defined as a regex and not standard globbing.
22
22
 
23
23
  ## Definition
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#L13) 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#L19) 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#L13) 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`.
25
25
 
26
26
  A simple example would be:
27
27
 
@@ -79,27 +79,25 @@ would translate to:
79
79
  "use strict";
80
80
  Object.defineProperty(exports, "__esModule", { value: true });
81
81
  const tslib_1 = require("tslib");
82
- const Ⲑ_util_1 = tslib_1.__importStar(require("@travetto/test/src/execute/util.js"));
82
+ const Ⲑ_debug_1 = tslib_1.__importStar(require("@travetto/runtime/src/debug.js"));
83
83
  const Ⲑ_check_1 = tslib_1.__importStar(require("@travetto/test/src/assert/check.js"));
84
- const Ⲑ_decorator_1 = tslib_1.__importStar(require("@travetto/registry/src/decorator.js"));
85
84
  const Ⲑ_function_1 = tslib_1.__importStar(require("@travetto/runtime/src/function.js"));
86
85
  var ᚕm = ["@travetto/test", "doc/assert-example"];
87
86
  const node_assert_1 = tslib_1.__importDefault(require("node:assert"));
88
87
  const test_1 = require("@travetto/test");
89
88
  let SimpleTest = class SimpleTest {
90
- static Ⲑinit = Ⲑ_function_1.register(SimpleTest, ᚕm, { hash: 1887908328, lines: [5, 12] }, { test: { hash: 102834457, lines: [8, 11] } }, false, false);
89
+ static Ⲑinit = Ⲑ_function_1.registerFunction(SimpleTest, ᚕm, { hash: 1887908328, lines: [5, 12] }, { test: { hash: 102834457, lines: [8, 11, 10] } }, false, false);
91
90
  async test() {
92
- if (Ⲑ_util_1.RunnerUtil.tryDebugger)
91
+ if (Ⲑ_debug_1.tryDebugger)
93
92
  debugger;
94
93
  Ⲑ_check_1.AssertCheck.check({ module: ᚕm, line: 10, text: "{ size: 20, address: { state: 'VA' } }", operator: "deepStrictEqual" }, true, { size: 20, address: { state: 'VA' } }, {});
95
94
  }
96
95
  };
97
96
  tslib_1.__decorate([
98
- (0, test_1.Test)({ ident: "@Test()", lineBodyStart: 10 })
97
+ (0, test_1.Test)()
99
98
  ], SimpleTest.prototype, "test", null);
100
99
  SimpleTest = tslib_1.__decorate([
101
- Ⲑ_decorator_1.Register(),
102
- (0, test_1.Suite)({ ident: "@Suite()" })
100
+ (0, test_1.Suite)()
103
101
  ], SimpleTest);
104
102
  ```
105
103
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@travetto/test",
3
- "version": "5.0.0-rc.3",
3
+ "version": "5.0.0-rc.5",
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/runtime": "^5.0.0-rc.3",
31
- "@travetto/registry": "^5.0.0-rc.3",
32
- "@travetto/terminal": "^5.0.0-rc.3",
33
- "@travetto/worker": "^5.0.0-rc.3",
30
+ "@travetto/runtime": "^5.0.0-rc.5",
31
+ "@travetto/registry": "^5.0.0-rc.5",
32
+ "@travetto/terminal": "^5.0.0-rc.5",
33
+ "@travetto/worker": "^5.0.0-rc.5",
34
34
  "yaml": "^2.4.5"
35
35
  },
36
36
  "peerDependencies": {
37
- "@travetto/cli": "^5.0.0-rc.3",
38
- "@travetto/transformer": "^5.0.0-rc.2"
37
+ "@travetto/cli": "^5.0.0-rc.5",
38
+ "@travetto/transformer": "^5.0.0-rc.3"
39
39
  },
40
40
  "peerDependenciesMeta": {
41
41
  "@travetto/transformer": {
@@ -1,11 +1,9 @@
1
1
  import { EventEmitter } from 'node:events';
2
- import { RuntimeIndex } from '@travetto/runtime';
3
2
 
4
3
  import { Assertion, TestConfig } from '../model/test';
5
4
 
6
5
  export interface CaptureAssert extends Partial<Assertion> {
7
- module: [string, string];
8
- file: string;
6
+ module?: [string, string];
9
7
  line: number;
10
8
  text: string;
11
9
  operator: string;
@@ -28,9 +26,9 @@ class $AssertCapture {
28
26
 
29
27
  // Emit and collect, every assertion as it occurs
30
28
  const handler = (a: CaptureAssert): void => {
31
- const assrt = {
29
+ const assrt: Assertion = {
32
30
  ...a,
33
- file: RuntimeIndex.getSourceFile(a.module),
31
+ import: a.import ?? a.module!.join('/'),
34
32
  classId: test.classId,
35
33
  methodName: test.methodName
36
34
  };
@@ -1,6 +1,6 @@
1
1
  import assert from 'node:assert';
2
2
 
3
- import { AppError, ClassInstance, Class, RuntimeIndex } from '@travetto/runtime';
3
+ import { AppError, ClassInstance, Class } from '@travetto/runtime';
4
4
 
5
5
  import { ThrowableError, TestConfig, Assertion } from '../model/test';
6
6
  import { AssertCapture, CaptureAssert } from './capture';
@@ -23,9 +23,6 @@ export class AssertCheck {
23
23
  * @param args The arguments passed in
24
24
  */
25
25
  static check(assertion: CaptureAssert, positive: boolean, ...args: unknown[]): void {
26
- /* eslint-disable @typescript-eslint/consistent-type-assertions */
27
- assertion.file = RuntimeIndex.getSourceFile(assertion.module);
28
-
29
26
  let fn = assertion.operator;
30
27
  assertion.operator = ASSERT_FN_OPERATOR[fn];
31
28
 
@@ -37,6 +34,7 @@ export class AssertCheck {
37
34
  // Invert check for negative
38
35
  const assertFn = positive ? assert : (x: unknown, msg?: string): unknown => assert(!x, msg);
39
36
 
37
+ /* eslint-disable @typescript-eslint/consistent-type-assertions */
40
38
  // Check fn to call
41
39
  if (fn === 'fail') {
42
40
  if (args.length > 1) {
@@ -214,8 +212,6 @@ export class AssertCheck {
214
212
  ): void {
215
213
  let missed: Error | undefined;
216
214
 
217
- assertion.file = RuntimeIndex.getSourceFile(assertion.module);
218
-
219
215
  try {
220
216
  action();
221
217
  if (!positive) {
@@ -248,8 +244,6 @@ export class AssertCheck {
248
244
  ): Promise<void> {
249
245
  let missed: Error | undefined;
250
246
 
251
- assertion.file = RuntimeIndex.getSourceFile(assertion.module);
252
-
253
247
  try {
254
248
  if ('then' in action) {
255
249
  await action;
@@ -273,15 +267,13 @@ export class AssertCheck {
273
267
  * Look for any unhandled exceptions
274
268
  */
275
269
  static checkUnhandled(test: TestConfig, err: Error | assert.AssertionError): void {
276
- let line = AssertUtil.getPositionOfError(err, test.file).line;
270
+ let line = AssertUtil.getPositionOfError(err, test.import).line;
277
271
  if (line === 1) {
278
272
  line = test.lineStart;
279
273
  }
280
274
 
281
- const entry = RuntimeIndex.getFromSource(test.file)!;
282
275
  AssertCapture.add({
283
- module: [entry.module, entry.relativeFile],
284
- file: test.file,
276
+ import: test.import,
285
277
  line,
286
278
  operator: 'throws',
287
279
  error: err,
@@ -1,6 +1,6 @@
1
1
  import util from 'node:util';
2
2
 
3
- import { Runtime } from '@travetto/runtime';
3
+ import { Runtime, RuntimeIndex } from '@travetto/runtime';
4
4
 
5
5
  import { TestConfig, Assertion, TestResult } from '../model/test';
6
6
  import { SuiteConfig } from '../model/suite';
@@ -42,7 +42,7 @@ export class AssertUtil {
42
42
  /**
43
43
  * Determine file location for a given error and the stack trace
44
44
  */
45
- static getPositionOfError(err: Error, filename: string): { file: string, line: number } {
45
+ static getPositionOfError(err: Error, imp: string): { import: string, line: number } {
46
46
  const cwd = Runtime.mainSourcePath;
47
47
  const lines = (err.stack ?? new Error().stack!)
48
48
  .replace(/[\\/]/g, '/')
@@ -50,6 +50,8 @@ export class AssertUtil {
50
50
  // Exclude node_modules, target self
51
51
  .filter(x => x.includes(cwd) && (!x.includes('node_modules') || x.includes('/support/')));
52
52
 
53
+ const filename = RuntimeIndex.getFromImport(imp)?.sourceFile!;
54
+
53
55
  let best = lines.filter(x => x.includes(filename))[0];
54
56
 
55
57
  if (!best) {
@@ -57,12 +59,12 @@ export class AssertUtil {
57
59
  }
58
60
 
59
61
  if (!best) {
60
- return { file: filename, line: 1 };
62
+ return { import: imp, line: 1 };
61
63
  }
62
64
 
63
65
  const pth = best.trim().split(/\s+/g).slice(1).pop()!;
64
66
  if (!pth) {
65
- return { file: filename, line: 1 };
67
+ return { import: imp, line: 1 };
66
68
  }
67
69
 
68
70
  const [file, lineNo] = pth
@@ -79,7 +81,7 @@ export class AssertUtil {
79
81
 
80
82
  const outFile = outFileParts.length > 1 ? outFileParts[1].replace(/^[\/]/, '') : filename;
81
83
 
82
- const res = { file: outFile, line };
84
+ const res = { import: RuntimeIndex.getFromSource(outFile)?.import!, line };
83
85
 
84
86
  return res;
85
87
  }
@@ -88,7 +90,7 @@ export class AssertUtil {
88
90
  * Generate a suite error given a suite config, and an error
89
91
  */
90
92
  static generateSuiteError(suite: SuiteConfig, methodName: string, error: Error): { assert: Assertion, testResult: TestResult, testConfig: TestConfig } {
91
- const { file, ...pos } = this.getPositionOfError(error, suite.file);
93
+ const { import: imp, ...pos } = this.getPositionOfError(error, suite.import);
92
94
  let line = pos.line;
93
95
 
94
96
  if (line === 1 && suite.lineStart) {
@@ -97,7 +99,7 @@ export class AssertUtil {
97
99
 
98
100
  const msg = error.message.split(/\n/)[0];
99
101
 
100
- const core = { file, classId: suite.classId, methodName, module: Runtime.main.name };
102
+ const core = { import: imp, classId: suite.classId, methodName };
101
103
  const coreAll = { ...core, description: msg, lineStart: line, lineEnd: line, lineBodyStart: line };
102
104
 
103
105
  const assert: Assertion = {
@@ -1,6 +1,6 @@
1
1
  import { existsSync } from 'node:fs';
2
2
 
3
- import { Class } from '@travetto/runtime';
3
+ import { Class, RuntimeIndex } from '@travetto/runtime';
4
4
 
5
5
  import { TestConsumer } from '../types';
6
6
  import { TestEvent } from '../../model/event';
@@ -28,7 +28,7 @@ export class CumulativeSummaryConsumer implements TestConsumer {
28
28
  */
29
29
  summarizeSuite(test: TestResult): SuiteResult {
30
30
  // Was only loading to verify existence (TODO: double-check)
31
- if (existsSync(test.file)) {
31
+ if (existsSync(RuntimeIndex.getFromImport(test.import)!.sourceFile)) {
32
32
  this.#state[test.classId] = this.#state[test.classId] ?? {};
33
33
  this.#state[test.classId][test.methodName] = test.status;
34
34
  const SuiteCls = SuiteRegistry.getClasses().find(x =>
@@ -50,7 +50,7 @@ export class CumulativeSummaryConsumer implements TestConsumer {
50
50
  removeClass(clsId: string): SuiteResult {
51
51
  this.#state[clsId] = {};
52
52
  return {
53
- classId: clsId, passed: 0, failed: 0, skipped: 0, total: 0, tests: [], duration: 0, file: '', lines: { start: 0, end: 0 }
53
+ classId: clsId, passed: 0, failed: 0, skipped: 0, total: 0, tests: [], duration: 0, import: '', lineStart: 0, lineEnd: 0
54
54
  };
55
55
  }
56
56
 
@@ -70,8 +70,9 @@ export class CumulativeSummaryConsumer implements TestConsumer {
70
70
  passed: total.passed,
71
71
  failed: total.failed,
72
72
  skipped: total.skipped,
73
- file: suite.file,
74
- lines: suite.lines,
73
+ import: suite.import,
74
+ lineStart: suite.lineStart,
75
+ lineEnd: suite.lineEnd,
75
76
  total: total.failed + total.passed,
76
77
  tests: [],
77
78
  duration: 0
@@ -1,5 +1,5 @@
1
1
  import { Terminal } from '@travetto/terminal';
2
- import { AppError, TimeUtil, Runtime } from '@travetto/runtime';
2
+ import { AppError, TimeUtil, Runtime, RuntimeIndex } from '@travetto/runtime';
3
3
  import { stringify } from 'yaml';
4
4
 
5
5
  import { TestEvent } from '../../model/event';
@@ -66,11 +66,12 @@ export class TapEmitter implements TestConsumer {
66
66
  let subCount = 0;
67
67
  for (const asrt of test.assertions) {
68
68
  const text = asrt.message ? `${asrt.text} (${this.#enhancer.failure(asrt.message)})` : asrt.text;
69
+ const pth = RuntimeIndex.getFromImport(asrt.import)!.sourceFile.replace(Runtime.mainSourcePath, '.');
69
70
  let subMessage = [
70
71
  this.#enhancer.assertNumber(++subCount),
71
72
  '-',
72
73
  this.#enhancer.assertDescription(text),
73
- `${this.#enhancer.assertFile(asrt.file.replace(Runtime.mainSourcePath, '.'))}:${this.#enhancer.assertLine(asrt.line)}`
74
+ `${this.#enhancer.assertFile(pth)}:${this.#enhancer.assertLine(asrt.line)}`
74
75
  ].join(' ');
75
76
 
76
77
  if (asrt.error) {
@@ -2,6 +2,8 @@ import { Writable } from 'node:stream';
2
2
 
3
3
  import { stringify } from 'yaml';
4
4
 
5
+ import { RuntimeIndex } from '@travetto/runtime';
6
+
5
7
  import { TestEvent } from '../../model/event';
6
8
  import { SuitesSummary, TestConsumer } from '../types';
7
9
  import { Consumable } from '../registry';
@@ -85,7 +87,7 @@ export class XunitEmitter implements TestConsumer {
85
87
  failures="${suite.failed}"
86
88
  errors="${suite.failed}"
87
89
  skipped="${suite.skipped}"
88
- file="${suite.file}"
90
+ file="${RuntimeIndex.getFromImport(suite.import)!.sourceFile}"
89
91
  >
90
92
  ${testBodies.join('\n')}
91
93
  </testsuite>
@@ -101,7 +103,7 @@ export class XunitEmitter implements TestConsumer {
101
103
  this.#stream.write(`
102
104
  <?xml version="1.0" encoding="UTF-8"?>
103
105
  <testsuites
104
- name="${summary.suites.length ? summary.suites[0].file : 'nameless'}"
106
+ name="${summary.suites.length ? RuntimeIndex.getFromImport(summary.suites[0].import)?.sourceFile : 'nameless'}"
105
107
  time="${summary.duration}"
106
108
  tests="${summary.total}"
107
109
  failures="${summary.failed}"
@@ -15,6 +15,7 @@ export function AssertCheck(): MethodDecorator {
15
15
  * @param description The test description
16
16
  * @augments `@travetto/test:Test`
17
17
  * @augments `@travetto/test:AssertCheck`
18
+ * @augments `@travetto/runtime:DebugBreak`
18
19
  */
19
20
  export function Test(): MethodDecorator;
20
21
  export function Test(...rest: Partial<TestConfig>[]): MethodDecorator;
@@ -65,11 +65,11 @@ export class TestExecutor {
65
65
  /**
66
66
  * Fail an entire file, marking the whole file as failed
67
67
  */
68
- static failFile(consumer: TestConsumer, file: string, err: Error): void {
69
- const name = path.basename(file);
70
- const classId = `${RuntimeIndex.getEntry(file)?.id}○${name}`;
68
+ static failFile(consumer: TestConsumer, imp: string, err: Error): void {
69
+ const name = path.basename(imp);
70
+ const classId = `${RuntimeIndex.getFromImport(imp)?.id}○${name}`;
71
71
  // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
72
- const suite = { class: { name }, classId, duration: 0, lineStart: 1, lineEnd: 1, file, } as SuiteConfig & SuiteResult;
72
+ const suite = { class: { name }, classId, duration: 0, lineStart: 1, lineEnd: 1, import: imp, } as SuiteConfig & SuiteResult;
73
73
  err.message = err.message.replaceAll(Runtime.mainSourcePath, '.');
74
74
  const res = AssertUtil.generateSuiteError(suite, 'require', err);
75
75
  consumer.onEvent({ type: 'suite', phase: 'before', suite });
@@ -90,7 +90,7 @@ export class TestExecutor {
90
90
  total: 0,
91
91
  lineStart: suite.lineStart,
92
92
  lineEnd: suite.lineEnd,
93
- file: suite.file,
93
+ import: suite.import,
94
94
  classId: suite.classId,
95
95
  duration: 0,
96
96
  tests: []
@@ -109,13 +109,12 @@ export class TestExecutor {
109
109
 
110
110
  const result: TestResult = {
111
111
  methodName: test.methodName,
112
- module: Runtime.main.name,
113
112
  description: test.description,
114
113
  classId: test.classId,
115
114
  lineStart: test.lineStart,
116
115
  lineEnd: test.lineEnd,
117
116
  lineBodyStart: test.lineBodyStart,
118
- file: test.file,
117
+ import: test.import,
119
118
  status: 'skipped',
120
119
  assertions: [],
121
120
  duration: 0,
@@ -258,19 +257,15 @@ export class TestExecutor {
258
257
  /**
259
258
  * Handle executing a suite's test/tests based on command line inputs
260
259
  */
261
- static async execute(consumer: TestConsumer, file: string, ...args: string[]): Promise<void> {
262
-
263
- file = path.resolve(file);
264
-
265
- const entry = RuntimeIndex.getEntry(file)!;
260
+ static async execute(consumer: TestConsumer, imp: string, ...args: string[]): Promise<void> {
266
261
 
267
262
  try {
268
- await import(entry.import);
263
+ await import(imp);
269
264
  } catch (err) {
270
265
  if (!(err instanceof Error)) {
271
266
  throw err;
272
267
  }
273
- this.failFile(consumer, file, err);
268
+ this.failFile(consumer, imp, err);
274
269
  return;
275
270
  }
276
271
 
@@ -278,7 +273,7 @@ export class TestExecutor {
278
273
  await SuiteRegistry.init();
279
274
 
280
275
  // Convert inbound arguments to specific tests to run
281
- const params = SuiteRegistry.getRunParams(file, ...args);
276
+ const params = SuiteRegistry.getRunParams(imp, ...args);
282
277
 
283
278
  // If running specific suites
284
279
  if ('suites' in params) {
@@ -1,6 +1,5 @@
1
1
  import path from 'node:path';
2
2
 
3
- import { path as mp } from '@travetto/manifest';
4
3
  import { TimeUtil, Runtime, RuntimeIndex } from '@travetto/runtime';
5
4
  import { WorkPool } from '@travetto/worker';
6
5
 
@@ -22,25 +21,21 @@ export class Runner {
22
21
  this.#state = state;
23
22
  }
24
23
 
25
- get patterns(): RegExp[] {
26
- return this.#state.args.map(x => new RegExp(mp.toPosix(x)));
27
- }
28
-
29
24
  /**
30
25
  * Run all files
31
26
  */
32
27
  async runFiles(): Promise<boolean> {
33
28
  const consumer = await RunnableTestConsumer.get(this.#state.consumer ?? this.#state.format);
34
29
 
35
- const files = (await RunnerUtil.getTestFiles(this.patterns)).map(f => f.sourceFile);
30
+ const imports = await RunnerUtil.getTestImports(this.#state.args);
36
31
 
37
- console.debug('Running', { files, patterns: this.patterns });
32
+ console.debug('Running', { imports, patterns: this.#state.args });
38
33
 
39
34
  const testCount = await RunnerUtil.getTestCount(this.#state.args);
40
35
  await consumer.onStart({ testCount });
41
36
  await WorkPool.run(
42
37
  buildStandardTestManager.bind(null, consumer),
43
- files,
38
+ imports,
44
39
  {
45
40
  idleTimeoutMillis: TimeUtil.asMillis(10, 's'),
46
41
  min: 1,
@@ -54,17 +49,23 @@ export class Runner {
54
49
  * Run a single file
55
50
  */
56
51
  async runSingle(): Promise<boolean> {
57
- const mod = RuntimeIndex.getEntry(path.resolve(this.#state.args[0]))!;
58
- if (mod.module !== Runtime.main.name) {
59
- RuntimeIndex.reinitForModule(mod.module);
52
+ let imp = RuntimeIndex.getFromImport(this.#state.args[0])?.import;
53
+
54
+ if (!imp) {
55
+ imp = RuntimeIndex.getFromSource(path.resolve(this.#state.args[0]))?.import;
56
+ }
57
+
58
+ const entry = RuntimeIndex.getFromImport(imp!)!;
59
+ if (entry.module !== Runtime.main.name) {
60
+ RuntimeIndex.reinitForModule(entry.module);
60
61
  }
61
62
 
62
63
  const consumer = await RunnableTestConsumer.get(this.#state.consumer ?? this.#state.format);
63
64
 
64
- const [file, ...args] = this.#state.args;
65
+ const [, ...args] = this.#state.args;
65
66
 
66
67
  await consumer.onStart({});
67
- await TestExecutor.execute(consumer, file, ...args);
68
+ await TestExecutor.execute(consumer, imp!, ...args);
68
69
  return consumer.summarizeAsBoolean();
69
70
  }
70
71
 
@@ -1,9 +1,9 @@
1
1
  import { spawn } from 'node:child_process';
2
2
  import { createReadStream } from 'node:fs';
3
- import readline from 'node:readline';
3
+ import fs from 'node:fs/promises';
4
+ import readline from 'node:readline/promises';
4
5
 
5
- import { Env, ExecUtil, ShutdownManager, Util, RuntimeIndex } from '@travetto/runtime';
6
- import type { IndexedFile } from '@travetto/manifest';
6
+ import { Env, ExecUtil, ShutdownManager, Util, RuntimeIndex, Runtime } from '@travetto/runtime';
7
7
 
8
8
  /**
9
9
  * Simple Test Utilities
@@ -19,38 +19,45 @@ export class RunnerUtil {
19
19
  /**
20
20
  * Determine if a given file path is a valid test file
21
21
  */
22
- static isTestFile(file: string): Promise<boolean> {
23
- return new Promise<boolean>((resolve) => {
24
- const input = createReadStream(file);
25
- const reader = readline.createInterface({ input })
26
- .on('line', line => {
27
- if (line.includes('@Suite')) {
28
- resolve(true);
29
- reader.close();
30
- }
31
- })
32
- .on('end', resolve.bind(null, false))
33
- .on('close', resolve.bind(null, false));
34
- });
22
+ static async isTestFile(file: string): Promise<boolean> {
23
+ const reader = readline.createInterface({ input: createReadStream(file) });
24
+ const state = { imp: false, suite: false };
25
+ for await (const line of reader) {
26
+ state.imp ||= line.includes('@travetto/test');
27
+ state.suite ||= line.includes('Suite'); // Decorator or name
28
+ if (state.imp && state.suite) {
29
+ reader.close();
30
+ return true;
31
+ }
32
+ }
33
+ return false;
35
34
  }
36
35
 
37
36
  /**
38
37
  * Find all valid test files given the globs
39
38
  */
40
- static async getTestFiles(globs?: RegExp[]): Promise<IndexedFile[]> {
41
- const files = RuntimeIndex.find({
39
+ static async getTestImports(globs?: string[]): Promise<string[]> {
40
+ const files = new Set<string>();
41
+ // Collect globs
42
+ if (globs) {
43
+ for await (const item of fs.glob(globs)) {
44
+ files.add(Runtime.workspaceRelative(item));
45
+ }
46
+ }
47
+
48
+ const found = RuntimeIndex.find({
42
49
  module: m => m.roles.includes('test') || m.roles.includes('std'),
43
50
  folder: f => f === 'test',
44
51
  file: f => f.role === 'test'
45
52
  })
46
- .filter(f => globs?.some(g => g.test(f.sourceFile)) ?? true);
53
+ .filter(f => files.size === 0 || files.has(f.sourceFile));
47
54
 
48
- const validFiles = files
49
- .map(f => this.isTestFile(f.sourceFile).then(valid => ({ file: f, valid })));
55
+ const validImports = found
56
+ .map(f => this.isTestFile(f.sourceFile).then(valid => ({ import: f.import, valid })));
50
57
 
51
- return (await Promise.all(validFiles))
58
+ return (await Promise.all(validImports))
52
59
  .filter(x => x.valid)
53
- .map(x => x.file);
60
+ .map(x => x.import);
54
61
  }
55
62
 
56
63
  /**
@@ -70,11 +77,4 @@ export class RunnerUtil {
70
77
  }
71
78
  return countRes.valid ? +countRes.stdout : 0;
72
79
  }
73
-
74
- /**
75
- * Determine if we should invoke the debugger
76
- */
77
- static get tryDebugger(): boolean {
78
- return Env.TRV_TEST_BREAK_ENTRY.isTrue;
79
- }
80
80
  }
@@ -6,17 +6,19 @@ import { SuiteRegistry } from '../registry/suite';
6
6
  import { buildStandardTestManager } from '../worker/standard';
7
7
  import { TestConsumerRegistry } from '../consumer/registry';
8
8
  import { CumulativeSummaryConsumer } from '../consumer/types/cumulative';
9
- import { RunEvent } from '../worker/types';
9
+ import { RunRequest } from '../worker/types';
10
10
  import { RunnerUtil } from './util';
11
11
  import { TestEvent } from '../model/event';
12
12
 
13
- function isRunEvent(ev: unknown): ev is RunEvent {
13
+ function isRunRequest(ev: unknown): ev is RunRequest {
14
14
  return typeof ev === 'object' && !!ev && 'type' in ev && typeof ev.type === 'string' && ev.type === 'run-test';
15
15
  }
16
16
 
17
+ type RemoveTestEvent = { type: 'removeTest', method: string, import: string, classId: string };
18
+
17
19
  export type TestWatchEvent =
18
20
  TestEvent |
19
- { type: 'removeTest', method: string, file: string, classId: string } |
21
+ RemoveTestEvent |
20
22
  { type: 'ready' } |
21
23
  { type: 'log', message: string };
22
24
 
@@ -33,7 +35,7 @@ export class TestWatcher {
33
35
  static async watch(format: string, runAllOnStart = true): Promise<void> {
34
36
  console.debug('Listening for changes');
35
37
 
36
- const itr = new WorkQueue<string>();
38
+ const itr = new WorkQueue<string | RunRequest>();
37
39
 
38
40
  await SuiteRegistry.init();
39
41
  SuiteRegistry.listen(RootRegistry);
@@ -52,7 +54,7 @@ export class TestWatcher {
52
54
  const conf = SuiteRegistry.getByClassAndMethod(cls, method)!;
53
55
  if (e.type !== 'removing') {
54
56
  if (conf) {
55
- const key = `${conf.file}#${conf.class.name}#${conf.methodName}`;
57
+ const key = { import: conf.import, class: conf.class.name, method: conf.methodName };
56
58
  itr.add(key, true); // Shift to front
57
59
  }
58
60
  } else {
@@ -60,29 +62,29 @@ export class TestWatcher {
60
62
  type: 'removeTest',
61
63
  method: method?.name,
62
64
  classId: cls?.Ⲑid,
63
- file: Runtime.getSource(cls)
64
- });
65
+ import: Runtime.getImport(cls)
66
+ } satisfies RemoveTestEvent);
65
67
  }
66
68
  });
67
69
 
68
70
  // If a file is changed, but doesn't emit classes, re-run whole file
69
- RootRegistry.onNonClassChanges(file => itr.add(file));
71
+ RootRegistry.onNonClassChanges(imp => itr.add(imp));
70
72
 
71
73
  await RootRegistry.init();
72
74
 
73
75
  process.on('message', ev => {
74
- if (isRunEvent(ev)) {
76
+ if (isRunRequest(ev)) {
75
77
  console.debug('Manually triggered', ev);
76
- itr.add([ev.file, ev.class, ev.method].filter(x => !!x).join('#'), true);
78
+ itr.add(ev, true);
77
79
  }
78
80
  });
79
81
 
80
82
  process.send?.({ type: 'ready' });
81
83
 
82
84
  if (runAllOnStart) {
83
- for (const test of await RunnerUtil.getTestFiles()) {
84
- await import(test.import);
85
- itr.add(test.sourceFile);
85
+ for (const imp of await RunnerUtil.getTestImports()) {
86
+ await import(imp);
87
+ itr.add(imp);
86
88
  }
87
89
  }
88
90
 
@@ -5,10 +5,6 @@ export type Skip = boolean | ((instance: unknown) => boolean | Promise<boolean>)
5
5
  * Core Suite definition
6
6
  */
7
7
  export interface SuiteCore {
8
- /**
9
- * The module the test is declared in
10
- */
11
- module: string;
12
8
  /**
13
9
  * The class id
14
10
  */
@@ -18,9 +14,9 @@ export interface SuiteCore {
18
14
  */
19
15
  description: string;
20
16
  /**
21
- * It's file
17
+ * The import location for the suite
22
18
  */
23
- file: string;
19
+ import: string;
24
20
  /**
25
21
  * The first line of the unit
26
22
  */
@@ -60,9 +60,9 @@ export interface SuiteResult extends Counts {
60
60
  */
61
61
  classId: string;
62
62
  /**
63
- * File suite is in
63
+ * Import for the suite
64
64
  */
65
- file: string;
65
+ import: string;
66
66
  /**
67
67
  * Start of the suite
68
68
  */
package/src/model/test.ts CHANGED
@@ -62,9 +62,9 @@ export interface Assertion {
62
62
  */
63
63
  error?: Error;
64
64
  /**
65
- * File of assertion
65
+ * Import of assertion
66
66
  */
67
- file: string;
67
+ import: string;
68
68
  /**
69
69
  * Line number
70
70
  */
@@ -20,9 +20,8 @@ class $SuiteRegistry extends MetadataRegistry<SuiteConfig, TestConfig> {
20
20
  const lines = describeFunction(cls)?.lines;
21
21
  return {
22
22
  class: cls,
23
- module: Runtime.main.name,
24
23
  classId: cls.Ⲑid,
25
- file: Runtime.getSource(cls),
24
+ import: Runtime.getImport(cls),
26
25
  lineStart: lines?.[0],
27
26
  lineEnd: lines?.[1],
28
27
  tests: [],
@@ -37,10 +36,10 @@ class $SuiteRegistry extends MetadataRegistry<SuiteConfig, TestConfig> {
37
36
  const lines = describeFunction(cls)?.methods?.[fn.name].lines;
38
37
  return {
39
38
  class: cls,
40
- module: Runtime.main.name,
41
- file: Runtime.getSource(cls),
39
+ import: Runtime.getImport(cls),
42
40
  lineStart: lines?.[0],
43
41
  lineEnd: lines?.[1],
42
+ lineBodyStart: lines?.[2],
44
43
  methodName: fn.name
45
44
  };
46
45
  }
@@ -93,11 +92,11 @@ class $SuiteRegistry extends MetadataRegistry<SuiteConfig, TestConfig> {
93
92
  /**
94
93
  * Get run parameters from provided input
95
94
  */
96
- getRunParams(file: string, clsName?: string, method?: string): { suites: SuiteConfig[] } | { suite: SuiteConfig, test?: TestConfig } {
95
+ getRunParams(imp: string, clsName?: string, method?: string): { suites: SuiteConfig[] } | { suite: SuiteConfig, test?: TestConfig } {
97
96
  if (clsName && /^\d+$/.test(clsName)) { // If we only have a line number
98
97
  const line = parseInt(clsName, 10);
99
98
  const suites = this.getValidClasses()
100
- .filter(cls => Runtime.getSource(cls) === file)
99
+ .filter(cls => Runtime.getImport(cls) === imp)
101
100
  .map(x => this.get(x)).filter(x => !x.skip);
102
101
  const suite = suites.find(x => line >= x.lineStart && line <= x.lineEnd);
103
102
 
package/src/trv.d.ts CHANGED
@@ -12,9 +12,5 @@ declare global {
12
12
  * @default 5s
13
13
  */
14
14
  TRV_TEST_TIMEOUT: TimeSpan | number;
15
- /**
16
- * Should the test break on the first line of debugging
17
- */
18
- TRV_TEST_BREAK_ENTRY: boolean;
19
15
  }
20
16
  }
@@ -79,13 +79,13 @@ export class TestChildWorker extends ChildCommChannel<RunEvent> {
79
79
  async onRunCommand(event: RunEvent): Promise<void> {
80
80
  console.debug('Run');
81
81
 
82
- console.debug('Running', { file: event.file });
82
+ console.debug('Running', { import: event.import });
83
83
 
84
84
  try {
85
85
  await new Runner({
86
86
  format: 'exec',
87
87
  mode: 'single',
88
- args: [event.file!, event.class!, event.method!],
88
+ args: [event.import, event.class!, event.method!],
89
89
  concurrency: 1
90
90
  }).run();
91
91
  } finally {
@@ -3,7 +3,7 @@ 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 } from './types';
6
+ import { Events, RunEvent, RunRequest } from './types';
7
7
  import { TestConsumer } from '../consumer/types';
8
8
  import { ErrorUtil } from '../consumer/error';
9
9
  import { TestEvent } from '../model/event';
@@ -11,28 +11,29 @@ import { TestEvent } from '../model/event';
11
11
  /**
12
12
  * Produce a handler for the child worker
13
13
  */
14
- export async function buildStandardTestManager(consumer: TestConsumer, file: string): Promise<void> {
15
- process.send?.({ type: 'log', message: `Worker Executing ${file}` });
14
+ export async function buildStandardTestManager(consumer: TestConsumer, imp: string | RunRequest): Promise<void> {
15
+ process.send?.({ type: 'log', message: `Worker Executing ${imp}` });
16
16
 
17
17
  let event: RunEvent;
18
- if (file.includes('#')) {
19
- const [f, cls, method] = file.split('#');
20
- event = { file: f, class: cls, method };
18
+ if (typeof imp === 'string') {
19
+ event = { import: imp };
20
+ } else if ('file' in imp) {
21
+ event = { import: RuntimeIndex.getFromSource(imp.file)?.sourceFile!, class: imp.class, method: imp.method };
21
22
  } else {
22
- event = { file };
23
+ event = imp;
23
24
  }
24
25
 
25
- const { module } = RuntimeIndex.getEntry(event.file!)!;
26
- const cwd = RuntimeIndex.getModule(module)!.sourcePath;
26
+ const { module } = RuntimeIndex.getFromImport(event.import!)!;
27
+ const suiteMod = RuntimeIndex.getModule(module);
27
28
 
28
29
  const channel = new ParentCommChannel<TestEvent & { error?: Error }>(
29
30
  fork(
30
31
  RuntimeIndex.resolveFileImport('@travetto/cli/support/entry.trv'), ['test:child'],
31
32
  {
32
- cwd,
33
+ cwd: suiteMod!.sourcePath,
33
34
  env: {
34
35
  ...process.env,
35
- ...Env.TRV_MANIFEST.export(RuntimeIndex.getModule(module)!.outputPath),
36
+ ...Env.TRV_MANIFEST.export(suiteMod!.outputPath),
36
37
  ...Env.TRV_QUIET.export(true)
37
38
  },
38
39
  stdio: ['ignore', 'ignore', 2, 'ipc']
@@ -63,7 +64,7 @@ export async function buildStandardTestManager(consumer: TestConsumer, file: str
63
64
  // Kill on complete
64
65
  await channel.destroy();
65
66
 
66
- process.send?.({ type: 'log', message: `Worker Finished ${file}` });
67
+ process.send?.({ type: 'log', message: `Worker Finished ${imp}` });
67
68
 
68
69
  // If we received an error, throw it
69
70
  if (error) {
@@ -1,8 +1,21 @@
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
+
1
14
  /**
2
15
  * Test Run Event
3
16
  */
4
17
  export type RunEvent = {
5
- file?: string;
18
+ import: string;
6
19
  error?: unknown;
7
20
  class?: string;
8
21
  method?: string;
@@ -13,15 +13,14 @@ export class TestCountCommand {
13
13
  }
14
14
 
15
15
  async main(patterns: string[]) {
16
- const regexes = patterns.map(x => new RegExp(x));
17
- const files = await RunnerUtil.getTestFiles(regexes);
16
+ const imports = await RunnerUtil.getTestImports(patterns);
18
17
 
19
18
  // Load all tests
20
- for (const file of files) {
19
+ for (const imp of imports) {
21
20
  try {
22
- await import(file.import);
21
+ await import(imp);
23
22
  } catch (err) {
24
- console.error('Failed to import', file.sourceFile, err);
23
+ console.error('Failed to import', imp, err);
25
24
  }
26
25
  }
27
26
 
@@ -1,103 +0,0 @@
1
- import ts from 'typescript';
2
-
3
- import {
4
- TransformerState, DecoratorMeta, OnMethod, OnClass, CoreUtil, DecoratorUtil
5
- } from '@travetto/transformer';
6
-
7
- const RUN_UTIL = 'RunnerUtil';
8
-
9
- const RunUtilⲐ = Symbol.for('@travetto/test:runner');
10
-
11
- /**
12
- * Annotate transformation state
13
- */
14
- interface AnnotateState {
15
- [RunUtilⲐ]?: ts.Expression;
16
- }
17
-
18
- /**
19
- * Annotate tests and suites for better diagnostics
20
- */
21
- export class AnnotationTransformer {
22
-
23
-
24
- /**
25
- * Initialize transformer state
26
- */
27
- static initState(state: TransformerState & AnnotateState): void {
28
- if (!state[RunUtilⲐ]) {
29
- const runUtil = state.importFile('@travetto/test/src/execute/util').ident;
30
- state[RunUtilⲐ] = CoreUtil.createAccess(state.factory, runUtil, RUN_UTIL, 'tryDebugger');
31
- }
32
- }
33
-
34
- /**
35
- * Build source annotation, indicating line ranges
36
- * @param state
37
- * @param node
38
- * @param dec
39
- */
40
- static buildAnnotation(state: TransformerState & AnnotateState, node: ts.Node, dec: ts.Decorator, expression: ts.CallExpression): ts.Decorator {
41
- const ogN = (CoreUtil.hasOriginal(node) ? node.original : node);
42
- const n = ts.isMethodDeclaration(ogN) ? ogN : undefined;
43
-
44
- const newDec = state.factory.updateDecorator(
45
- dec,
46
- state.factory.createCallExpression(
47
- expression.expression,
48
- expression.typeArguments,
49
- [
50
- ...(expression.arguments ?? []),
51
- state.fromLiteral({
52
- ident: `@${DecoratorUtil.getDecoratorIdent(dec).text}()`,
53
- lineBodyStart: CoreUtil.getRangeOf(state.source, n?.body?.statements[0])?.[0]
54
- })
55
- ]
56
- )
57
- );
58
- return newDec;
59
- }
60
-
61
- @OnClass('Suite')
62
- static annotateSuiteDetails(state: TransformerState & AnnotateState, node: ts.ClassDeclaration, dm?: DecoratorMeta): ts.ClassDeclaration {
63
- const dec = dm?.dec;
64
-
65
- if (dec && ts.isCallExpression(dec.expression)) {
66
- const newDec = this.buildAnnotation(state, node, dec, dec.expression);
67
- return state.factory.updateClassDeclaration(node,
68
- DecoratorUtil.spliceDecorators(node, dec, [newDec]),
69
- node.name,
70
- node.typeParameters,
71
- node.heritageClauses,
72
- node.members
73
- );
74
- }
75
- return node;
76
- }
77
-
78
- @OnMethod('Test')
79
- static annotateTestDetails(state: TransformerState & AnnotateState, node: ts.MethodDeclaration, dm?: DecoratorMeta): ts.MethodDeclaration {
80
- this.initState(state);
81
-
82
- const dec = dm?.dec;
83
-
84
- if (dec && ts.isCallExpression(dec.expression)) {
85
- const newDec = this.buildAnnotation(state, node, dec, dec.expression);
86
- return state.factory.updateMethodDeclaration(node,
87
- DecoratorUtil.spliceDecorators(node, dec, [newDec]),
88
- node.asteriskToken,
89
- node.name,
90
- node.questionToken,
91
- node.typeParameters,
92
- node.parameters,
93
- node.type,
94
- node.body ? state.factory.updateBlock(node.body, [
95
- state.factory.createIfStatement(state[RunUtilⲐ]!,
96
- state.factory.createExpressionStatement(state.factory.createIdentifier('debugger'))),
97
- ...node.body.statements
98
- ]) : node.body
99
- );
100
- }
101
- return node;
102
- }
103
- }