@travetto/test 3.0.0-rc.4 → 3.0.0-rc.6
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 +19 -18
- package/{index.ts → __index__.ts} +2 -0
- package/package.json +19 -12
- package/src/assert/capture.ts +1 -0
- package/src/assert/check.ts +13 -7
- package/src/assert/util.ts +12 -12
- package/src/consumer/enhancer.ts +16 -24
- package/src/consumer/registry.ts +5 -2
- package/src/consumer/types/{index.ts → all.ts} +1 -1
- package/src/consumer/types/cumulative.ts +11 -6
- package/src/consumer/types/runnable.ts +8 -12
- package/src/consumer/types/tap-streamed.ts +92 -0
- package/src/consumer/types/tap.ts +25 -21
- package/src/consumer/types.ts +2 -2
- package/src/consumer/util.ts +1 -1
- package/src/decorator/suite.ts +3 -2
- package/src/decorator/test.ts +11 -2
- package/src/execute/console.ts +4 -3
- package/src/execute/executor.ts +26 -40
- package/src/execute/phase.ts +2 -2
- package/src/execute/runner.ts +15 -30
- package/src/execute/{types.d.ts → types.ts} +4 -4
- package/src/execute/util.ts +8 -8
- package/src/execute/watcher.ts +43 -20
- package/src/fixture.ts +10 -0
- package/src/model/common.ts +4 -0
- package/src/registry/suite.ts +10 -7
- package/src/worker/child.ts +6 -51
- package/src/worker/standard.ts +39 -18
- package/src/worker/types.ts +0 -1
- package/{bin/lib → support/bin}/run.ts +8 -9
- package/support/cli.test.ts +76 -0
- package/support/main.test-child.ts +32 -0
- package/support/main.test-direct.ts +15 -0
- package/support/main.test-watch.ts +8 -0
- package/support/transformer.annotate.ts +4 -5
- package/support/transformer.assert.ts +22 -20
- package/bin/cli-test.ts +0 -121
- package/bin/test-child.ts +0 -38
- package/bin/test-direct.ts +0 -23
- package/bin/test-watch.ts +0 -25
- package/src/consumer/types/tap-summary.ts +0 -78
- package/src/worker/isolated.ts +0 -19
- package/support/phase.reset.ts +0 -12
|
@@ -1,13 +1,12 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
import {
|
|
1
|
+
import { path } from '@travetto/manifest';
|
|
2
|
+
import { GlobalTerminal, Terminal } from '@travetto/terminal';
|
|
3
|
+
import { ErrorUtil, ObjectUtil, TimeUtil } from '@travetto/base';
|
|
4
4
|
import { YamlUtil } from '@travetto/yaml';
|
|
5
|
-
import { ErrorUtil } from '@travetto/base/src/internal/error';
|
|
6
5
|
|
|
7
6
|
import { TestEvent } from '../../model/event';
|
|
8
7
|
import { SuitesSummary, TestConsumer } from '../types';
|
|
9
8
|
import { Consumable } from '../registry';
|
|
10
|
-
import { TestResultsEnhancer,
|
|
9
|
+
import { TestResultsEnhancer, CONSOLE_ENHANCER } from '../enhancer';
|
|
11
10
|
|
|
12
11
|
/**
|
|
13
12
|
* TAP Format consumer
|
|
@@ -15,25 +14,27 @@ import { TestResultsEnhancer, COLOR_ENHANCER, DUMMY_ENHANCER } from '../enhancer
|
|
|
15
14
|
@Consumable('tap')
|
|
16
15
|
export class TapEmitter implements TestConsumer {
|
|
17
16
|
#count = 0;
|
|
18
|
-
#stream: Writable;
|
|
19
17
|
#enhancer: TestResultsEnhancer;
|
|
18
|
+
#terminal: Terminal;
|
|
19
|
+
#start: number;
|
|
20
20
|
|
|
21
21
|
constructor(
|
|
22
|
-
|
|
23
|
-
enhancer: TestResultsEnhancer =
|
|
22
|
+
terminal = new Terminal({ output: process.stdout }),
|
|
23
|
+
enhancer: TestResultsEnhancer = CONSOLE_ENHANCER
|
|
24
24
|
) {
|
|
25
|
-
this.#
|
|
25
|
+
this.#terminal = terminal;
|
|
26
26
|
this.#enhancer = enhancer;
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
-
|
|
30
|
-
this.#
|
|
29
|
+
log(message: string): void {
|
|
30
|
+
this.#terminal.writeLines(message);
|
|
31
31
|
}
|
|
32
32
|
|
|
33
33
|
/**
|
|
34
34
|
* Preamble
|
|
35
35
|
*/
|
|
36
36
|
onStart(): void {
|
|
37
|
+
this.#start = Date.now();
|
|
37
38
|
this.log(this.#enhancer.suiteName('TAP version 13')!);
|
|
38
39
|
}
|
|
39
40
|
|
|
@@ -41,7 +42,8 @@ export class TapEmitter implements TestConsumer {
|
|
|
41
42
|
* Output supplemental data (e.g. logs)
|
|
42
43
|
*/
|
|
43
44
|
logMeta(obj: Record<string, unknown>): void {
|
|
44
|
-
|
|
45
|
+
const lineLength = GlobalTerminal.width - 5;
|
|
46
|
+
let body = YamlUtil.serialize(obj, { wordwrap: lineLength });
|
|
45
47
|
body = body.split('\n').map(x => ` ${x}`).join('\n');
|
|
46
48
|
this.log(`---\n${this.#enhancer.objectInspect(body)}\n...`);
|
|
47
49
|
}
|
|
@@ -59,27 +61,29 @@ export class TapEmitter implements TestConsumer {
|
|
|
59
61
|
}
|
|
60
62
|
this.log(`# ${header}`);
|
|
61
63
|
|
|
64
|
+
const cwd = path.cwd();
|
|
65
|
+
|
|
62
66
|
// Handle each assertion
|
|
63
67
|
if (test.assertions.length) {
|
|
64
68
|
let subCount = 0;
|
|
65
|
-
for (const
|
|
66
|
-
const text =
|
|
69
|
+
for (const asrt of test.assertions) {
|
|
70
|
+
const text = asrt.message ? `${asrt.text} (${this.#enhancer.failure(asrt.message)})` : asrt.text;
|
|
67
71
|
let subMessage = [
|
|
68
72
|
this.#enhancer.assertNumber(++subCount),
|
|
69
73
|
'-',
|
|
70
74
|
this.#enhancer.assertDescription(text),
|
|
71
|
-
`${this.#enhancer.assertFile(
|
|
75
|
+
`${this.#enhancer.assertFile(asrt.file.replace(cwd, '.'))}:${this.#enhancer.assertLine(asrt.line)}`
|
|
72
76
|
].join(' ');
|
|
73
77
|
|
|
74
|
-
if (
|
|
78
|
+
if (asrt.error) {
|
|
75
79
|
subMessage = `${this.#enhancer.failure('not ok')} ${subMessage}`;
|
|
76
80
|
} else {
|
|
77
81
|
subMessage = `${this.#enhancer.success('ok')} ${subMessage}`;
|
|
78
82
|
}
|
|
79
83
|
this.log(` ${subMessage}`);
|
|
80
84
|
|
|
81
|
-
if (
|
|
82
|
-
this.logMeta({ message:
|
|
85
|
+
if (asrt.message && asrt.message.length > 100) {
|
|
86
|
+
this.logMeta({ message: asrt.message.replace(/\\n/g, '\n') });
|
|
83
87
|
}
|
|
84
88
|
}
|
|
85
89
|
this.log(` ${this.#enhancer.assertNumber(1)}..${this.#enhancer.assertNumber(subCount)}`);
|
|
@@ -100,7 +104,7 @@ export class TapEmitter implements TestConsumer {
|
|
|
100
104
|
if (test.status === 'failed') {
|
|
101
105
|
if (test.error?.stack && !test.error.stack.includes('AssertionError')) {
|
|
102
106
|
const err = ErrorUtil.deserializeError(test.error);
|
|
103
|
-
this.logMeta({ error: err.toJSON
|
|
107
|
+
this.logMeta({ error: ObjectUtil.hasToJSON(err) ? err.toJSON() : err });
|
|
104
108
|
}
|
|
105
109
|
}
|
|
106
110
|
|
|
@@ -125,7 +129,7 @@ export class TapEmitter implements TestConsumer {
|
|
|
125
129
|
this.log('---\n');
|
|
126
130
|
for (const err of summary.errors) {
|
|
127
131
|
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
128
|
-
this.log(this.#enhancer.failure(err
|
|
132
|
+
this.log(this.#enhancer.failure(ObjectUtil.hasToJSON(err) ? err.toJSON() as string : `${err}`));
|
|
129
133
|
}
|
|
130
134
|
}
|
|
131
135
|
|
|
@@ -138,7 +142,7 @@ export class TapEmitter implements TestConsumer {
|
|
|
138
142
|
`${this.#enhancer.total(summary.failed)}`,
|
|
139
143
|
'skipped',
|
|
140
144
|
this.#enhancer.total(summary.skipped),
|
|
141
|
-
`# (Total Time: ${summary.duration}
|
|
145
|
+
`# (Total Test Time: ${TimeUtil.prettyDelta(summary.duration)}, Total Run Time: ${TimeUtil.prettyDelta(Date.now() - this.#start)})`
|
|
142
146
|
].join(' '));
|
|
143
147
|
}
|
|
144
148
|
}
|
package/src/consumer/types.ts
CHANGED
|
@@ -26,7 +26,7 @@ export interface TestConsumer {
|
|
|
26
26
|
/**
|
|
27
27
|
* Listen for start of the test run
|
|
28
28
|
*/
|
|
29
|
-
onStart?(): void;
|
|
29
|
+
onStart?(files: string[]): Promise<void> | void;
|
|
30
30
|
/**
|
|
31
31
|
* Handle individual tests events
|
|
32
32
|
*/
|
|
@@ -34,5 +34,5 @@ export interface TestConsumer {
|
|
|
34
34
|
/**
|
|
35
35
|
* Summarize all results
|
|
36
36
|
*/
|
|
37
|
-
onSummary?(summary: SuitesSummary): void;
|
|
37
|
+
onSummary?(summary: SuitesSummary): Promise<void> | void;
|
|
38
38
|
}
|
package/src/consumer/util.ts
CHANGED
package/src/decorator/suite.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { RootIndex } from '@travetto/manifest';
|
|
1
2
|
import { Class, ClassInstance } from '@travetto/base';
|
|
2
3
|
|
|
3
4
|
import { SuiteRegistry } from '../registry/suite';
|
|
@@ -8,7 +9,7 @@ export type SuitePhase = 'beforeAll' | 'beforeEach' | 'afterAll' | 'afterEach';
|
|
|
8
9
|
/**
|
|
9
10
|
* Register a class to be defined as a test suite, and a candidate for testing
|
|
10
11
|
* @param description The Suite description
|
|
11
|
-
* @augments `@
|
|
12
|
+
* @augments `@travetto/test:Suite`
|
|
12
13
|
*/
|
|
13
14
|
export function Suite(): ClassDecorator;
|
|
14
15
|
export function Suite(...rest: Partial<SuiteConfig>[]): ClassDecorator;
|
|
@@ -26,7 +27,7 @@ export function Suite(description?: string | Partial<SuiteConfig>, ...rest: Part
|
|
|
26
27
|
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
27
28
|
const decorator = ((target: Class) => {
|
|
28
29
|
const cfg = { description: descriptionString, ...extra };
|
|
29
|
-
if (target
|
|
30
|
+
if (RootIndex.getFunctionMetadata(target)?.abstract) {
|
|
30
31
|
cfg.skip = true;
|
|
31
32
|
}
|
|
32
33
|
SuiteRegistry.register(target, cfg);
|
package/src/decorator/test.ts
CHANGED
|
@@ -1,12 +1,21 @@
|
|
|
1
1
|
import { ClassInstance } from '@travetto/base';
|
|
2
|
+
import { RootIndex } from '@travetto/manifest';
|
|
2
3
|
|
|
3
4
|
import { SuiteRegistry } from '../registry/suite';
|
|
4
5
|
import { TestConfig } from '../model/test';
|
|
5
6
|
|
|
7
|
+
/**
|
|
8
|
+
* The `@AssertCheck` indicates that a function's assert calls should be transformed
|
|
9
|
+
*/
|
|
10
|
+
export function AssertCheck(): MethodDecorator {
|
|
11
|
+
return (inst: ClassInstance, prop: string | symbol, descriptor: PropertyDescriptor) => descriptor;
|
|
12
|
+
}
|
|
13
|
+
|
|
6
14
|
/**
|
|
7
15
|
* The `@Test` decorator register a test to be run as part of the enclosing suite.
|
|
8
16
|
* @param description The test description
|
|
9
|
-
* @augments `@
|
|
17
|
+
* @augments `@travetto/test:Test`
|
|
18
|
+
* @augments `@travetto/test:AssertCheck`
|
|
10
19
|
*/
|
|
11
20
|
export function Test(): MethodDecorator;
|
|
12
21
|
export function Test(...rest: Partial<TestConfig>[]): MethodDecorator;
|
|
@@ -23,7 +32,7 @@ export function Test(description?: string | Partial<TestConfig>, ...rest: Partia
|
|
|
23
32
|
return (inst: ClassInstance, prop: string | symbol, descriptor: PropertyDescriptor) => {
|
|
24
33
|
SuiteRegistry.registerField(inst.constructor, descriptor.value, {
|
|
25
34
|
...extra,
|
|
26
|
-
file: inst.constructor
|
|
35
|
+
file: RootIndex.getFunctionMetadata(inst.constructor)!.source,
|
|
27
36
|
description: descriptionString
|
|
28
37
|
});
|
|
29
38
|
return descriptor;
|
package/src/execute/console.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
1
|
+
import util from 'util';
|
|
2
|
+
|
|
3
|
+
import { ConsoleEvent, ConsoleManager } from '@travetto/base';
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* Console capturer. Hooks into the Console manager, and collects the
|
|
@@ -14,7 +15,7 @@ export class ConsoleCapture {
|
|
|
14
15
|
ConsoleManager.set(this);
|
|
15
16
|
}
|
|
16
17
|
|
|
17
|
-
static onLog(
|
|
18
|
+
static onLog({ level, args }: ConsoleEvent): void {
|
|
18
19
|
(this.out[level] = this.out[level] ?? []).push(
|
|
19
20
|
args
|
|
20
21
|
.map((x => typeof x === 'string' ? x : util.inspect(x, false, 4)))
|
package/src/execute/executor.ts
CHANGED
|
@@ -1,12 +1,8 @@
|
|
|
1
|
-
import
|
|
2
|
-
import * as timers from 'timers/promises';
|
|
3
|
-
import * as fs from 'fs/promises';
|
|
1
|
+
import timers from 'timers/promises';
|
|
4
2
|
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
3
|
+
import { path, RootIndex } from '@travetto/manifest';
|
|
4
|
+
import { TimeUtil, Util } from '@travetto/base';
|
|
7
5
|
import { Barrier, ExecutionError } from '@travetto/worker';
|
|
8
|
-
import { SystemUtil } from '@travetto/boot/src/internal/system';
|
|
9
|
-
import { ModuleUtil } from '@travetto/boot/src/internal/module-util';
|
|
10
6
|
|
|
11
7
|
import { SuiteRegistry } from '../registry/suite';
|
|
12
8
|
import { TestConfig, TestResult } from '../model/test';
|
|
@@ -18,9 +14,8 @@ import { ConsoleCapture } from './console';
|
|
|
18
14
|
import { TestPhaseManager } from './phase';
|
|
19
15
|
import { PromiseCapture } from './promise';
|
|
20
16
|
import { AssertUtil } from '../assert/util';
|
|
21
|
-
import { TestEvent } from '../model/event';
|
|
22
17
|
|
|
23
|
-
const TEST_TIMEOUT =
|
|
18
|
+
const TEST_TIMEOUT = TimeUtil.getEnvTime('TRV_TEST_TIMEOUT', '5s');
|
|
24
19
|
|
|
25
20
|
/**
|
|
26
21
|
* Support execution of the tests
|
|
@@ -32,7 +27,7 @@ export class TestExecutor {
|
|
|
32
27
|
*
|
|
33
28
|
* This method should never throw under any circumstances.
|
|
34
29
|
*/
|
|
35
|
-
static #executeTestMethod(test: TestConfig): Promise<Error | undefined> {
|
|
30
|
+
static async #executeTestMethod(test: TestConfig): Promise<Error | undefined> {
|
|
36
31
|
const suite = SuiteRegistry.get(test.class);
|
|
37
32
|
const promCleanup = Util.resolvablePromise();
|
|
38
33
|
|
|
@@ -40,11 +35,15 @@ export class TestExecutor {
|
|
|
40
35
|
const barrier = new Barrier(test.timeout || TEST_TIMEOUT, true)
|
|
41
36
|
.add(promCleanup, true) // If not timeout or unhandled, ensure all promises are cleaned up
|
|
42
37
|
.add(async () => {
|
|
38
|
+
const env = process.env;
|
|
39
|
+
|
|
43
40
|
try {
|
|
44
41
|
PromiseCapture.start(); // Listen for all promises to detect any unfinished, only start once method is invoked
|
|
42
|
+
process.env = { ...env }; // Created an isolated environment
|
|
45
43
|
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
46
44
|
await (suite.instance as Record<string, Function>)[test.methodName](); // Run
|
|
47
45
|
} finally {
|
|
46
|
+
process.env = env; // Restore
|
|
48
47
|
PromiseCapture.stop().then(() => timers.setTimeout(1).then(promCleanup.resolve), promCleanup.reject);
|
|
49
48
|
}
|
|
50
49
|
});
|
|
@@ -69,10 +68,10 @@ export class TestExecutor {
|
|
|
69
68
|
*/
|
|
70
69
|
static failFile(consumer: TestConsumer, file: string, err: Error): void {
|
|
71
70
|
const name = path.basename(file);
|
|
72
|
-
const classId =
|
|
71
|
+
const classId = RootIndex.getId(file, name);
|
|
73
72
|
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
74
73
|
const suite = { class: { name }, classId, duration: 0, lines: { start: 1, end: 1 }, file, } as SuiteConfig & SuiteResult;
|
|
75
|
-
err.message = err.message.replace(
|
|
74
|
+
err.message = err.message.replace(path.cwd(), '.');
|
|
76
75
|
const res = AssertUtil.generateSuiteError(suite, 'require', err);
|
|
77
76
|
consumer.onEvent({ type: 'suite', phase: 'before', suite });
|
|
78
77
|
consumer.onEvent({ type: 'test', phase: 'before', test: res.testConfig });
|
|
@@ -110,6 +109,7 @@ export class TestExecutor {
|
|
|
110
109
|
|
|
111
110
|
const result: TestResult = {
|
|
112
111
|
methodName: test.methodName,
|
|
112
|
+
module: RootIndex.manifest.mainModule,
|
|
113
113
|
description: test.description,
|
|
114
114
|
classId: test.classId,
|
|
115
115
|
lines: { ...test.lines },
|
|
@@ -138,6 +138,7 @@ export class TestExecutor {
|
|
|
138
138
|
|
|
139
139
|
// Run method and get result
|
|
140
140
|
let error = await this.#executeTestMethod(test);
|
|
141
|
+
|
|
141
142
|
if (error) {
|
|
142
143
|
if (error instanceof ExecutionError) { // Errors that are not expected
|
|
143
144
|
AssertCheck.checkUnhandled(test, error);
|
|
@@ -197,11 +198,17 @@ export class TestExecutor {
|
|
|
197
198
|
|
|
198
199
|
const mgr = new TestPhaseManager(consumer, suite, result);
|
|
199
200
|
|
|
201
|
+
const originalEnv = { ...process.env };
|
|
202
|
+
|
|
200
203
|
try {
|
|
201
204
|
// Handle the BeforeAll calls
|
|
202
205
|
await mgr.startPhase('all');
|
|
203
206
|
|
|
207
|
+
const suiteEnv = { ...process.env };
|
|
208
|
+
|
|
204
209
|
for (const test of suite.tests) {
|
|
210
|
+
// Reset env before each test
|
|
211
|
+
process.env = { ...suiteEnv };
|
|
205
212
|
const testStart = Date.now();
|
|
206
213
|
const skip = await this.#skip(test, suite.instance);
|
|
207
214
|
if (!skip) {
|
|
@@ -227,6 +234,9 @@ export class TestExecutor {
|
|
|
227
234
|
await mgr.onError(err);
|
|
228
235
|
}
|
|
229
236
|
|
|
237
|
+
// Restore env
|
|
238
|
+
process.env = { ...originalEnv };
|
|
239
|
+
|
|
230
240
|
result.duration = Date.now() - startTime;
|
|
231
241
|
|
|
232
242
|
// Mark suite complete
|
|
@@ -242,12 +252,12 @@ export class TestExecutor {
|
|
|
242
252
|
*/
|
|
243
253
|
static async execute(consumer: TestConsumer, file: string, ...args: string[]): Promise<void> {
|
|
244
254
|
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
255
|
+
file = path.resolve(file);
|
|
256
|
+
|
|
257
|
+
const entry = RootIndex.getEntry(file)!;
|
|
248
258
|
|
|
249
259
|
try {
|
|
250
|
-
await import(
|
|
260
|
+
await import(entry.import);
|
|
251
261
|
} catch (err) {
|
|
252
262
|
if (!(err instanceof Error)) {
|
|
253
263
|
throw err;
|
|
@@ -275,28 +285,4 @@ export class TestExecutor {
|
|
|
275
285
|
await this.executeSuite(consumer, params.suite);
|
|
276
286
|
}
|
|
277
287
|
}
|
|
278
|
-
|
|
279
|
-
/**
|
|
280
|
-
* Execute isolated
|
|
281
|
-
*/
|
|
282
|
-
static async executeIsolated(consumer: TestConsumer, file: string, ...args: string[]): Promise<void> {
|
|
283
|
-
// Read modules for extensions
|
|
284
|
-
const modules = [...(await fs.readFile(file, 'utf8'))
|
|
285
|
-
.matchAll(/\/\/\s*@with-module\s+(@travetto\/[A-Za-z0-9\-]+)/g)]
|
|
286
|
-
.map(x => x[1]);
|
|
287
|
-
|
|
288
|
-
const proc = ExecUtil.forkMain(require.resolve('../../bin/test-direct'), [file, ...args], {
|
|
289
|
-
env: {
|
|
290
|
-
TRV_MODULES: modules.join(','),
|
|
291
|
-
TRV_RESOURCES: process.env.TRV_RESOURCES,
|
|
292
|
-
TRV_PROFILES: process.env.TRV_PROFILES,
|
|
293
|
-
TRV_SRC_LOCAL: '^test-isolated',
|
|
294
|
-
TRV_SRC_COMMON: process.env.TRV_SRC_COMMON,
|
|
295
|
-
TRV_TEST_FORMAT: 'exec',
|
|
296
|
-
TRV_CACHE: `.trv_cache_${SystemUtil.naiveHash(file)}`
|
|
297
|
-
}
|
|
298
|
-
});
|
|
299
|
-
proc.process.on('message', (e: TestEvent) => consumer.onEvent(e));
|
|
300
|
-
await proc.result;
|
|
301
|
-
}
|
|
302
288
|
}
|
package/src/execute/phase.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Barrier } from '@travetto/worker';
|
|
2
|
-
import {
|
|
2
|
+
import { TimeUtil } from '@travetto/base';
|
|
3
3
|
|
|
4
4
|
import { TestConsumer } from '../consumer/types';
|
|
5
5
|
import { SuiteConfig, SuiteResult } from '../model/suite';
|
|
@@ -8,7 +8,7 @@ import { TestResult } from '../model/test';
|
|
|
8
8
|
|
|
9
9
|
class TestBreakout extends Error { }
|
|
10
10
|
|
|
11
|
-
const TEST_PHASE_TIMEOUT =
|
|
11
|
+
const TEST_PHASE_TIMEOUT = TimeUtil.getEnvTime('TRV_TEST_PHASE_TIMEOUT', '15s');
|
|
12
12
|
|
|
13
13
|
/**
|
|
14
14
|
* Test Phase Execution Manager.
|
package/src/execute/runner.ts
CHANGED
|
@@ -1,14 +1,13 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { path } from '@travetto/manifest';
|
|
2
|
+
import { TimeUtil } from '@travetto/base';
|
|
3
3
|
import { WorkPool, IterableWorkSet } from '@travetto/worker';
|
|
4
4
|
|
|
5
|
-
import { TestExecutor } from './executor';
|
|
6
5
|
import { buildStandardTestManager } from '../worker/standard';
|
|
7
|
-
import {
|
|
6
|
+
import { RunnableTestConsumer } from '../consumer/types/runnable';
|
|
8
7
|
|
|
8
|
+
import { TestExecutor } from './executor';
|
|
9
9
|
import { RunnerUtil } from './util';
|
|
10
10
|
import { RunState } from './types';
|
|
11
|
-
import { RunnableTestConsumer } from '../consumer/types/runnable';
|
|
12
11
|
|
|
13
12
|
/**
|
|
14
13
|
* Test Runner
|
|
@@ -22,37 +21,29 @@ export class Runner {
|
|
|
22
21
|
}
|
|
23
22
|
|
|
24
23
|
get patterns(): RegExp[] {
|
|
25
|
-
return this.#state.args.map(x => new RegExp(
|
|
24
|
+
return this.#state.args.map(x => new RegExp(path.toPosix(x)));
|
|
26
25
|
}
|
|
27
26
|
|
|
28
27
|
/**
|
|
29
28
|
* Run all files
|
|
30
29
|
*/
|
|
31
30
|
async runFiles(): Promise<boolean> {
|
|
32
|
-
const consumer = RunnableTestConsumer.get(this.#state.consumer ?? this.#state.format);
|
|
33
|
-
|
|
34
|
-
const files = (await RunnerUtil.getTestFiles(this.patterns, this.#state.isolated ? 'test-isolated' : 'test'));
|
|
31
|
+
const consumer = await RunnableTestConsumer.get(this.#state.consumer ?? this.#state.format);
|
|
35
32
|
|
|
36
|
-
|
|
33
|
+
const files = (await RunnerUtil.getTestFiles(this.patterns));
|
|
37
34
|
|
|
38
|
-
|
|
35
|
+
console.debug('Running', { files, patterns: this.patterns });
|
|
39
36
|
|
|
40
|
-
const manager =
|
|
41
|
-
buildIsolatedTestManager :
|
|
42
|
-
buildStandardTestManager;
|
|
37
|
+
const manager = buildStandardTestManager;
|
|
43
38
|
|
|
44
39
|
const pool = new WorkPool(manager(consumer), {
|
|
45
|
-
idleTimeoutMillis:
|
|
40
|
+
idleTimeoutMillis: TimeUtil.timeToMs('10s'),
|
|
46
41
|
min: 1,
|
|
47
42
|
max: this.#state.concurrency
|
|
48
43
|
});
|
|
49
44
|
|
|
50
|
-
consumer.onStart();
|
|
51
|
-
|
|
52
|
-
await pool
|
|
53
|
-
.process(new IterableWorkSet(files))
|
|
54
|
-
.finally(() => pool.shutdown());
|
|
55
|
-
|
|
45
|
+
await consumer.onStart(files);
|
|
46
|
+
await pool.process(new IterableWorkSet(files));
|
|
56
47
|
return consumer.summarizeAsBoolean();
|
|
57
48
|
}
|
|
58
49
|
|
|
@@ -60,18 +51,12 @@ export class Runner {
|
|
|
60
51
|
* Run a single file
|
|
61
52
|
*/
|
|
62
53
|
async runSingle(): Promise<boolean> {
|
|
63
|
-
const consumer = RunnableTestConsumer.get(this.#state.consumer ?? this.#state.format);
|
|
64
|
-
consumer.onStart();
|
|
54
|
+
const consumer = await RunnableTestConsumer.get(this.#state.consumer ?? this.#state.format);
|
|
65
55
|
|
|
66
56
|
const [file, ...args] = this.#state.args;
|
|
67
57
|
|
|
68
|
-
await
|
|
69
|
-
|
|
70
|
-
await TestExecutor.executeIsolated(consumer, file, ...args);
|
|
71
|
-
} else {
|
|
72
|
-
await TestExecutor.execute(consumer, file, ...args);
|
|
73
|
-
}
|
|
74
|
-
|
|
58
|
+
await consumer.onStart([path.resolve(file)]);
|
|
59
|
+
await TestExecutor.execute(consumer, file, ...args);
|
|
75
60
|
return consumer.summarizeAsBoolean();
|
|
76
61
|
}
|
|
77
62
|
|
|
@@ -17,13 +17,13 @@ export interface RunState {
|
|
|
17
17
|
*/
|
|
18
18
|
mode?: 'single' | 'watch' | 'standard';
|
|
19
19
|
/**
|
|
20
|
-
*
|
|
20
|
+
* Show progress to stderr
|
|
21
21
|
*/
|
|
22
|
-
|
|
22
|
+
showProgress?: boolean;
|
|
23
23
|
/**
|
|
24
|
-
*
|
|
24
|
+
* Number of test suites to run concurrently, when mode is not single
|
|
25
25
|
*/
|
|
26
|
-
|
|
26
|
+
concurrency: number;
|
|
27
27
|
/**
|
|
28
28
|
* Input arguments
|
|
29
29
|
*/
|
package/src/execute/util.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { createReadStream } from 'fs';
|
|
2
|
-
import
|
|
2
|
+
import readline from 'readline';
|
|
3
3
|
|
|
4
|
-
import { ShutdownManager,
|
|
5
|
-
import {
|
|
4
|
+
import { ShutdownManager, TimeUtil } from '@travetto/base';
|
|
5
|
+
import { RootIndex } from '@travetto/manifest';
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* Simple Test Utilities
|
|
@@ -12,7 +12,7 @@ export class RunnerUtil {
|
|
|
12
12
|
* Add 50 ms to the shutdown to allow for buffers to output properly
|
|
13
13
|
*/
|
|
14
14
|
static registerCleanup(scope: string): void {
|
|
15
|
-
ShutdownManager.onShutdown(`test.${scope}.bufferOutput`, () =>
|
|
15
|
+
ShutdownManager.onShutdown(`test.${scope}.bufferOutput`, () => TimeUtil.wait(50));
|
|
16
16
|
}
|
|
17
17
|
|
|
18
18
|
/**
|
|
@@ -36,12 +36,12 @@ export class RunnerUtil {
|
|
|
36
36
|
/**
|
|
37
37
|
* Find all valid test files given the globs
|
|
38
38
|
*/
|
|
39
|
-
static async getTestFiles(globs: RegExp[]
|
|
40
|
-
const files =
|
|
41
|
-
.filter(f => globs.some(g => g.test(f.
|
|
39
|
+
static async getTestFiles(globs: RegExp[]): Promise<string[]> {
|
|
40
|
+
const files = RootIndex.findTest({})
|
|
41
|
+
.filter(f => globs.some(g => g.test(f.import)));
|
|
42
42
|
|
|
43
43
|
const validFiles = files
|
|
44
|
-
.map(f => this.isTestFile(f.
|
|
44
|
+
.map(f => this.isTestFile(f.source).then(valid => ({ file: f.source, valid })));
|
|
45
45
|
|
|
46
46
|
return (await Promise.all(validFiles))
|
|
47
47
|
.filter(x => x.valid)
|
package/src/execute/watcher.ts
CHANGED
|
@@ -1,13 +1,18 @@
|
|
|
1
|
-
import * as os from 'os';
|
|
2
|
-
|
|
3
1
|
import { RootRegistry, MethodSource } from '@travetto/registry';
|
|
4
2
|
import { WorkPool, IterableWorkSet, ManualAsyncIterator } from '@travetto/worker';
|
|
3
|
+
import { RootIndex } from '@travetto/manifest';
|
|
4
|
+
import { ObjectUtil } from '@travetto/base';
|
|
5
|
+
import { DynamicFileLoader } from '@travetto/base/src/internal/file-loader';
|
|
5
6
|
|
|
6
7
|
import { SuiteRegistry } from '../registry/suite';
|
|
7
8
|
import { buildStandardTestManager } from '../worker/standard';
|
|
8
|
-
import { RunEvent } from '../worker/types';
|
|
9
9
|
import { TestConsumerRegistry } from '../consumer/registry';
|
|
10
10
|
import { CumulativeSummaryConsumer } from '../consumer/types/cumulative';
|
|
11
|
+
import { RunEvent } from '../worker/types';
|
|
12
|
+
|
|
13
|
+
function isRunEvent(ev: unknown): ev is RunEvent {
|
|
14
|
+
return ObjectUtil.isPlainObject(ev) && 'type' in ev && typeof ev.type === 'string' && ev.type === 'run-test';
|
|
15
|
+
}
|
|
11
16
|
|
|
12
17
|
/**
|
|
13
18
|
* Test Watcher.
|
|
@@ -19,53 +24,71 @@ export class TestWatcher {
|
|
|
19
24
|
/**
|
|
20
25
|
* Start watching all test files
|
|
21
26
|
*/
|
|
22
|
-
static async watch(format: string): Promise<void> {
|
|
27
|
+
static async watch(format: string, runAllOnStart = true): Promise<void> {
|
|
23
28
|
console.debug('Listening for changes');
|
|
24
29
|
|
|
25
|
-
const itr = new ManualAsyncIterator<
|
|
30
|
+
const itr = new ManualAsyncIterator<string>();
|
|
26
31
|
const src = new IterableWorkSet(itr);
|
|
27
32
|
|
|
28
33
|
await SuiteRegistry.init();
|
|
29
34
|
SuiteRegistry.listen(RootRegistry);
|
|
30
35
|
|
|
31
|
-
const consumer = new CumulativeSummaryConsumer(TestConsumerRegistry.getInstance(format));
|
|
36
|
+
const consumer = new CumulativeSummaryConsumer(await TestConsumerRegistry.getInstance(format));
|
|
32
37
|
const pool = new WorkPool(buildStandardTestManager(consumer), {
|
|
33
38
|
idleTimeoutMillis: 120000,
|
|
34
39
|
min: 2,
|
|
35
|
-
max:
|
|
40
|
+
max: WorkPool.DEFAULT_SIZE
|
|
36
41
|
});
|
|
37
42
|
|
|
38
43
|
new MethodSource(RootRegistry).on(e => {
|
|
39
44
|
const [cls, method] = (e.prev ?? e.curr ?? []);
|
|
40
|
-
if (!cls) {
|
|
45
|
+
if (!cls || RootIndex.getFunctionMetadata(cls)?.abstract) {
|
|
41
46
|
return;
|
|
42
47
|
}
|
|
43
48
|
if (!method) {
|
|
44
|
-
consumer.removeClass(cls
|
|
49
|
+
consumer.removeClass(cls.Ⲑid);
|
|
45
50
|
return;
|
|
46
51
|
}
|
|
47
52
|
const conf = SuiteRegistry.getByClassAndMethod(cls, method)!;
|
|
48
53
|
if (e.type !== 'removing') {
|
|
49
54
|
if (conf) {
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
file: conf.file,
|
|
53
|
-
class: conf.class.name
|
|
54
|
-
}, true); // Shift to front
|
|
55
|
+
const key = `${conf.file}#${conf.class.name}#${conf.methodName}`;
|
|
56
|
+
itr.add(key, true); // Shift to front
|
|
55
57
|
}
|
|
56
|
-
} else
|
|
57
|
-
process.send({
|
|
58
|
+
} else {
|
|
59
|
+
process.send?.({
|
|
58
60
|
type: 'removeTest',
|
|
59
61
|
method: method?.name,
|
|
60
|
-
classId: cls
|
|
61
|
-
file: cls
|
|
62
|
+
classId: cls?.Ⲑid,
|
|
63
|
+
file: RootIndex.getFunctionMetadata(cls)?.source
|
|
62
64
|
});
|
|
63
65
|
}
|
|
64
66
|
});
|
|
65
67
|
|
|
66
68
|
await RootRegistry.init();
|
|
67
69
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
+
process.send?.({ type: 'watch-init' });
|
|
71
|
+
|
|
72
|
+
process.on('message', ev => {
|
|
73
|
+
if (isRunEvent(ev)) {
|
|
74
|
+
itr.add([ev.file, ev.class, ev.method].filter(x => !!x).join('#'), true);
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
// Re-run all tests on file change
|
|
79
|
+
DynamicFileLoader.onLoadEvent(ev => {
|
|
80
|
+
if (ev.action === 'update') {
|
|
81
|
+
itr.add(ev.file);
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
if (runAllOnStart) {
|
|
86
|
+
for (const test of await RootIndex.findTest({})) {
|
|
87
|
+
await import(test.output);
|
|
88
|
+
itr.add(test.source);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
await pool.process(src);
|
|
70
93
|
}
|
|
71
94
|
}
|
package/src/fixture.ts
ADDED
package/src/model/common.ts
CHANGED