@travetto/test 7.0.0-rc.1 → 7.0.0-rc.3
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 +7 -8
- package/__index__.ts +1 -0
- package/package.json +7 -7
- package/src/assert/check.ts +46 -46
- package/src/assert/util.ts +31 -31
- package/src/communication.ts +66 -0
- package/src/consumer/registry-index.ts +11 -11
- package/src/consumer/types/cumulative.ts +91 -62
- package/src/consumer/types/delegating.ts +30 -27
- package/src/consumer/types/event.ts +11 -4
- package/src/consumer/types/exec.ts +12 -3
- package/src/consumer/types/runnable.ts +4 -3
- package/src/consumer/types/summarizer.ts +12 -10
- package/src/consumer/types/tap-summary.ts +22 -20
- package/src/consumer/types/tap.ts +15 -15
- package/src/consumer/types/xunit.ts +15 -15
- package/src/consumer/types.ts +6 -2
- package/src/decorator/suite.ts +2 -2
- package/src/decorator/test.ts +6 -4
- package/src/execute/barrier.ts +8 -8
- package/src/execute/console.ts +1 -1
- package/src/execute/executor.ts +32 -21
- package/src/execute/phase.ts +7 -7
- package/src/execute/run.ts +247 -0
- package/src/execute/types.ts +2 -17
- package/src/execute/watcher.ts +33 -60
- package/src/fixture.ts +2 -2
- package/src/model/common.ts +4 -0
- package/src/model/event.ts +3 -1
- package/src/model/suite.ts +10 -21
- package/src/model/test.ts +48 -2
- package/src/model/util.ts +8 -0
- package/src/registry/registry-adapter.ts +23 -21
- package/src/registry/registry-index.ts +25 -25
- package/src/worker/child.ts +21 -21
- package/src/worker/standard.ts +28 -19
- package/src/worker/types.ts +9 -5
- package/support/bin/run.ts +10 -10
- package/support/cli.test.ts +20 -41
- package/support/cli.test_diff.ts +47 -0
- package/support/cli.test_digest.ts +7 -7
- package/support/cli.test_direct.ts +13 -12
- package/support/cli.test_watch.ts +3 -8
- package/support/transformer.assert.ts +12 -12
- package/src/execute/runner.ts +0 -87
- package/src/execute/util.ts +0 -108
package/src/decorator/test.ts
CHANGED
|
@@ -3,13 +3,15 @@ import { ClassInstance, getClass } from '@travetto/runtime';
|
|
|
3
3
|
import { TestConfig, ThrowableError } from '../model/test.ts';
|
|
4
4
|
import { SuiteRegistryIndex } from '../registry/registry-index.ts';
|
|
5
5
|
|
|
6
|
+
type MethodDecorator = (instance: ClassInstance, property: string, descriptor: PropertyDescriptor) => PropertyDescriptor | void;
|
|
7
|
+
|
|
6
8
|
/**
|
|
7
9
|
* The `@AssertCheck` indicates that a function's assert calls should be transformed
|
|
8
10
|
* @augments `@travetto/test:AssertCheck`
|
|
9
11
|
* @kind decorator
|
|
10
12
|
*/
|
|
11
13
|
export function AssertCheck(): MethodDecorator {
|
|
12
|
-
return (instance: ClassInstance, property: string
|
|
14
|
+
return (instance: ClassInstance, property: string, descriptor: PropertyDescriptor) => descriptor;
|
|
13
15
|
}
|
|
14
16
|
|
|
15
17
|
/**
|
|
@@ -24,7 +26,7 @@ export function Test(): MethodDecorator;
|
|
|
24
26
|
export function Test(...rest: Partial<TestConfig>[]): MethodDecorator;
|
|
25
27
|
export function Test(description: string, ...rest: Partial<TestConfig>[]): MethodDecorator;
|
|
26
28
|
export function Test(description?: string | Partial<TestConfig>, ...rest: Partial<TestConfig>[]): MethodDecorator {
|
|
27
|
-
return (instance: ClassInstance, property: string
|
|
29
|
+
return (instance: ClassInstance, property: string, descriptor: PropertyDescriptor) => {
|
|
28
30
|
SuiteRegistryIndex.getForRegister(getClass(instance)).registerTest(property, descriptor.value,
|
|
29
31
|
...(typeof description !== 'string' && description) ? [description] : [],
|
|
30
32
|
...rest,
|
|
@@ -40,7 +42,7 @@ export function Test(description?: string | Partial<TestConfig>, ...rest: Partia
|
|
|
40
42
|
* @kind decorator
|
|
41
43
|
*/
|
|
42
44
|
export function ShouldThrow(state: ThrowableError): MethodDecorator {
|
|
43
|
-
return (instance: ClassInstance, property: string
|
|
45
|
+
return (instance: ClassInstance, property: string, descriptor: PropertyDescriptor) => {
|
|
44
46
|
SuiteRegistryIndex.getForRegister(getClass(instance)).registerTest(property, descriptor.value, { shouldThrow: state });
|
|
45
47
|
return descriptor;
|
|
46
48
|
};
|
|
@@ -52,7 +54,7 @@ export function ShouldThrow(state: ThrowableError): MethodDecorator {
|
|
|
52
54
|
* @kind decorator
|
|
53
55
|
*/
|
|
54
56
|
export function Timeout(ms: number): MethodDecorator {
|
|
55
|
-
return (instance: ClassInstance, property: string
|
|
57
|
+
return (instance: ClassInstance, property: string, descriptor: PropertyDescriptor) => {
|
|
56
58
|
SuiteRegistryIndex.getForRegister(getClass(instance)).registerTest(property, descriptor.value, { timeout: ms });
|
|
57
59
|
return descriptor;
|
|
58
60
|
};
|
package/src/execute/barrier.ts
CHANGED
|
@@ -11,14 +11,14 @@ export class Barrier {
|
|
|
11
11
|
/**
|
|
12
12
|
* Track timeout
|
|
13
13
|
*/
|
|
14
|
-
static timeout(duration: number | TimeSpan,
|
|
14
|
+
static timeout(duration: number | TimeSpan, operation: string = 'Operation'): { promise: Promise<void>, resolve: () => unknown } {
|
|
15
15
|
const resolver = Promise.withResolvers<void>();
|
|
16
16
|
const durationMs = TimeUtil.asMillis(duration);
|
|
17
17
|
let timeout: NodeJS.Timeout;
|
|
18
18
|
if (!durationMs) {
|
|
19
19
|
resolver.resolve();
|
|
20
20
|
} else {
|
|
21
|
-
const msg = `${
|
|
21
|
+
const msg = `${operation} timed out after ${duration}${typeof duration === 'number' ? 'ms' : ''}`;
|
|
22
22
|
timeout = setTimeout(() => resolver.reject(new TimeoutError(msg)), durationMs).unref();
|
|
23
23
|
}
|
|
24
24
|
|
|
@@ -31,9 +31,9 @@ export class Barrier {
|
|
|
31
31
|
*/
|
|
32
32
|
static uncaughtErrorPromise(): { promise: Promise<void>, resolve: () => unknown } {
|
|
33
33
|
const uncaught = Promise.withResolvers<void>();
|
|
34
|
-
const onError = (
|
|
35
|
-
UNCAUGHT_ERR_EVENTS.map(
|
|
36
|
-
uncaught.promise.finally(() => { UNCAUGHT_ERR_EVENTS.map(
|
|
34
|
+
const onError = (error: Error): void => { Util.queueMacroTask().then(() => uncaught.reject(error)); };
|
|
35
|
+
UNCAUGHT_ERR_EVENTS.map(key => process.on(key, onError));
|
|
36
|
+
uncaught.promise.finally(() => { UNCAUGHT_ERR_EVENTS.map(key => process.off(key, onError)); });
|
|
37
37
|
return uncaught;
|
|
38
38
|
}
|
|
39
39
|
|
|
@@ -80,7 +80,7 @@ export class Barrier {
|
|
|
80
80
|
/**
|
|
81
81
|
* Wait for operation to finish, with timeout and unhandled error support
|
|
82
82
|
*/
|
|
83
|
-
static async awaitOperation(timeout: number | TimeSpan,
|
|
83
|
+
static async awaitOperation(timeout: number | TimeSpan, operation: () => Promise<unknown>): Promise<Error | undefined> {
|
|
84
84
|
const uncaught = this.uncaughtErrorPromise();
|
|
85
85
|
const timer = this.timeout(timeout);
|
|
86
86
|
const promises = this.capturePromises();
|
|
@@ -88,9 +88,9 @@ export class Barrier {
|
|
|
88
88
|
try {
|
|
89
89
|
await promises.start();
|
|
90
90
|
let capturedError: Error | undefined;
|
|
91
|
-
const opProm =
|
|
91
|
+
const opProm = operation().then(() => promises.finish());
|
|
92
92
|
|
|
93
|
-
await Promise.race([opProm, uncaught.promise, timer.promise]).catch(
|
|
93
|
+
await Promise.race([opProm, uncaught.promise, timer.promise]).catch(error => capturedError ??= error);
|
|
94
94
|
|
|
95
95
|
return capturedError;
|
|
96
96
|
} finally {
|
package/src/execute/console.ts
CHANGED
|
@@ -22,7 +22,7 @@ export class ConsoleCapture implements ConsoleListener {
|
|
|
22
22
|
this.out.push({
|
|
23
23
|
...rest,
|
|
24
24
|
message: args
|
|
25
|
-
.map((
|
|
25
|
+
.map((arg => typeof arg === 'string' ? arg : util.inspect(arg, false, 5)))
|
|
26
26
|
.join(' ')
|
|
27
27
|
});
|
|
28
28
|
}
|
package/src/execute/executor.ts
CHANGED
|
@@ -3,7 +3,7 @@ import { AssertionError } from 'node:assert';
|
|
|
3
3
|
import { Env, TimeUtil, Runtime, castTo, classConstruct } from '@travetto/runtime';
|
|
4
4
|
import { Registry } from '@travetto/registry';
|
|
5
5
|
|
|
6
|
-
import { TestConfig, TestResult, TestRun } from '../model/test.ts';
|
|
6
|
+
import { TestConfig, TestResult, type TestRun } from '../model/test.ts';
|
|
7
7
|
import { SuiteConfig, SuiteFailure, SuiteResult } from '../model/suite.ts';
|
|
8
8
|
import { TestConsumerShape } from '../consumer/types.ts';
|
|
9
9
|
import { AssertCheck } from '../assert/check.ts';
|
|
@@ -14,8 +14,9 @@ import { AssertUtil } from '../assert/util.ts';
|
|
|
14
14
|
import { Barrier } from './barrier.ts';
|
|
15
15
|
import { ExecutionError } from './error.ts';
|
|
16
16
|
import { SuiteRegistryIndex } from '../registry/registry-index.ts';
|
|
17
|
+
import { TestModelUtil } from '../model/util.ts';
|
|
17
18
|
|
|
18
|
-
const TEST_TIMEOUT = TimeUtil.fromValue(Env.TRV_TEST_TIMEOUT.
|
|
19
|
+
const TEST_TIMEOUT = TimeUtil.fromValue(Env.TRV_TEST_TIMEOUT.value) ?? 5000;
|
|
19
20
|
|
|
20
21
|
/**
|
|
21
22
|
* Support execution of the tests
|
|
@@ -73,8 +74,8 @@ export class TestExecutor {
|
|
|
73
74
|
/**
|
|
74
75
|
* Determining if we should skip
|
|
75
76
|
*/
|
|
76
|
-
async #shouldSkip(
|
|
77
|
-
if (typeof
|
|
77
|
+
async #shouldSkip(config: TestConfig | SuiteConfig, inst: unknown): Promise<boolean | undefined> {
|
|
78
|
+
if (typeof config.skip === 'function' ? await config.skip(inst) : config.skip) {
|
|
78
79
|
return true;
|
|
79
80
|
}
|
|
80
81
|
}
|
|
@@ -94,13 +95,16 @@ export class TestExecutor {
|
|
|
94
95
|
passed: 0,
|
|
95
96
|
failed: 0,
|
|
96
97
|
skipped: 0,
|
|
98
|
+
unknown: 0,
|
|
97
99
|
total: 0,
|
|
100
|
+
status: 'unknown',
|
|
98
101
|
lineStart: suite.lineStart,
|
|
99
102
|
lineEnd: suite.lineEnd,
|
|
100
103
|
import: suite.import,
|
|
101
104
|
classId: suite.classId,
|
|
105
|
+
sourceHash: suite.sourceHash,
|
|
102
106
|
duration: 0,
|
|
103
|
-
tests:
|
|
107
|
+
tests: {}
|
|
104
108
|
};
|
|
105
109
|
}
|
|
106
110
|
|
|
@@ -118,12 +122,14 @@ export class TestExecutor {
|
|
|
118
122
|
methodName: test.methodName,
|
|
119
123
|
description: test.description,
|
|
120
124
|
classId: test.classId,
|
|
125
|
+
tags: test.tags,
|
|
121
126
|
lineStart: test.lineStart,
|
|
122
127
|
lineEnd: test.lineEnd,
|
|
123
128
|
lineBodyStart: test.lineBodyStart,
|
|
124
129
|
import: test.import,
|
|
125
130
|
sourceImport: test.sourceImport,
|
|
126
|
-
|
|
131
|
+
sourceHash: test.sourceHash,
|
|
132
|
+
status: 'unknown',
|
|
127
133
|
assertions: [],
|
|
128
134
|
duration: 0,
|
|
129
135
|
durationTotal: 0,
|
|
@@ -184,19 +190,23 @@ export class TestExecutor {
|
|
|
184
190
|
}
|
|
185
191
|
|
|
186
192
|
const result: SuiteResult = this.createSuiteResult(suite);
|
|
193
|
+
const validTestMethodNames = new Set(tests.map(t => t.methodName));
|
|
194
|
+
const testConfigs = Object.fromEntries(
|
|
195
|
+
Object.entries(suite.tests).filter(([key]) => validTestMethodNames.has(key))
|
|
196
|
+
);
|
|
187
197
|
|
|
188
198
|
const startTime = Date.now();
|
|
189
199
|
|
|
190
200
|
// Mark suite start
|
|
191
|
-
this.#consumer.onEvent({ phase: 'before', type: 'suite', suite });
|
|
201
|
+
this.#consumer.onEvent({ phase: 'before', type: 'suite', suite: { ...suite, tests: testConfigs } });
|
|
192
202
|
|
|
193
|
-
const
|
|
203
|
+
const manager = new TestPhaseManager(suite, result, event => this.#onSuiteFailure(event));
|
|
194
204
|
|
|
195
205
|
const originalEnv = { ...process.env };
|
|
196
206
|
|
|
197
207
|
try {
|
|
198
208
|
// Handle the BeforeAll calls
|
|
199
|
-
await
|
|
209
|
+
await manager.startPhase('all');
|
|
200
210
|
|
|
201
211
|
const suiteEnv = { ...process.env };
|
|
202
212
|
|
|
@@ -212,29 +222,30 @@ export class TestExecutor {
|
|
|
212
222
|
const testStart = Date.now();
|
|
213
223
|
|
|
214
224
|
// Handle BeforeEach
|
|
215
|
-
await
|
|
225
|
+
await manager.startPhase('each');
|
|
216
226
|
|
|
217
227
|
// Run test
|
|
218
228
|
const testResult = await this.executeTest(test);
|
|
219
229
|
result[testResult.status]++;
|
|
220
|
-
result.tests.
|
|
230
|
+
result.tests[testResult.methodName] = testResult;
|
|
221
231
|
|
|
222
232
|
// Handle after each
|
|
223
|
-
await
|
|
233
|
+
await manager.endPhase('each');
|
|
224
234
|
testResult.durationTotal = Date.now() - testStart;
|
|
225
235
|
}
|
|
226
236
|
|
|
227
237
|
// Handle after all
|
|
228
|
-
await
|
|
229
|
-
} catch (
|
|
230
|
-
await
|
|
238
|
+
await manager.endPhase('all');
|
|
239
|
+
} catch (error) {
|
|
240
|
+
await manager.onError(error);
|
|
231
241
|
}
|
|
232
242
|
|
|
233
243
|
// Restore env
|
|
234
244
|
process.env = { ...originalEnv };
|
|
235
245
|
|
|
236
246
|
result.duration = Date.now() - startTime;
|
|
237
|
-
result.total = result.passed + result.failed;
|
|
247
|
+
result.total = result.passed + result.failed + result.skipped;
|
|
248
|
+
result.status = TestModelUtil.countsToTestStatus(result);
|
|
238
249
|
|
|
239
250
|
// Mark suite complete
|
|
240
251
|
this.#consumer.onEvent({ phase: 'after', type: 'suite', suite: result });
|
|
@@ -246,12 +257,12 @@ export class TestExecutor {
|
|
|
246
257
|
async execute(run: TestRun): Promise<void> {
|
|
247
258
|
try {
|
|
248
259
|
await Runtime.importFrom(run.import);
|
|
249
|
-
} catch (
|
|
250
|
-
if (!(
|
|
251
|
-
throw
|
|
260
|
+
} catch (error) {
|
|
261
|
+
if (!(error instanceof Error)) {
|
|
262
|
+
throw error;
|
|
252
263
|
}
|
|
253
|
-
console.error(
|
|
254
|
-
this.#onSuiteFailure(AssertUtil.gernerateImportFailure(run.import,
|
|
264
|
+
console.error(error);
|
|
265
|
+
this.#onSuiteFailure(AssertUtil.gernerateImportFailure(run.import, error));
|
|
255
266
|
return;
|
|
256
267
|
}
|
|
257
268
|
|
package/src/execute/phase.ts
CHANGED
|
@@ -8,7 +8,7 @@ class TestBreakout extends Error {
|
|
|
8
8
|
source?: Error;
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
-
const TEST_PHASE_TIMEOUT = TimeUtil.fromValue(Env.TRV_TEST_PHASE_TIMEOUT.
|
|
11
|
+
const TEST_PHASE_TIMEOUT = TimeUtil.fromValue(Env.TRV_TEST_PHASE_TIMEOUT.value) ?? 15000;
|
|
12
12
|
|
|
13
13
|
/**
|
|
14
14
|
* Test Phase Execution Manager.
|
|
@@ -67,9 +67,9 @@ export class TestPhaseManager {
|
|
|
67
67
|
/**
|
|
68
68
|
* On error, handle stubbing out error for the phases in progress
|
|
69
69
|
*/
|
|
70
|
-
async onError(
|
|
71
|
-
if (!(
|
|
72
|
-
throw
|
|
70
|
+
async onError(error: Error | unknown): Promise<void> {
|
|
71
|
+
if (!(error instanceof Error)) {
|
|
72
|
+
throw error;
|
|
73
73
|
}
|
|
74
74
|
|
|
75
75
|
for (const ph of this.#progress) {
|
|
@@ -82,12 +82,12 @@ export class TestPhaseManager {
|
|
|
82
82
|
|
|
83
83
|
const failure = AssertUtil.generateSuiteFailure(
|
|
84
84
|
this.#suite,
|
|
85
|
-
|
|
86
|
-
|
|
85
|
+
error instanceof TestBreakout ? error.message : 'all',
|
|
86
|
+
error instanceof TestBreakout ? error.source! : error
|
|
87
87
|
);
|
|
88
88
|
|
|
89
89
|
this.#onSuiteFailure(failure);
|
|
90
|
-
this.#result.tests.
|
|
90
|
+
this.#result.tests[failure.testResult.methodName] = failure.testResult;
|
|
91
91
|
this.#result.failed++;
|
|
92
92
|
}
|
|
93
93
|
}
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
import { createReadStream } from 'node:fs';
|
|
2
|
+
import fs from 'node:fs/promises';
|
|
3
|
+
import readline from 'node:readline/promises';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
|
|
6
|
+
import { Env, ExecUtil, ShutdownManager, Util, RuntimeIndex, Runtime, TimeUtil, JSONUtil } from '@travetto/runtime';
|
|
7
|
+
import { WorkPool } from '@travetto/worker';
|
|
8
|
+
import { Registry } from '@travetto/registry';
|
|
9
|
+
|
|
10
|
+
import type { TestConfig, TestRunInput, TestRun, TestGlobInput, TestDiffInput } from '../model/test.ts';
|
|
11
|
+
import type { TestRemoveEvent } from '../model/event.ts';
|
|
12
|
+
import type { TestConsumerShape } from '../consumer/types.ts';
|
|
13
|
+
import { RunnableTestConsumer } from '../consumer/types/runnable.ts';
|
|
14
|
+
import type { TestConsumerConfig } from './types.ts';
|
|
15
|
+
import { TestConsumerRegistryIndex } from '../consumer/registry-index.ts';
|
|
16
|
+
import { TestExecutor } from './executor.ts';
|
|
17
|
+
import { buildStandardTestManager } from '../worker/standard.ts';
|
|
18
|
+
import { SuiteRegistryIndex } from '../registry/registry-index.ts';
|
|
19
|
+
|
|
20
|
+
type RunState = {
|
|
21
|
+
runs: TestRun[];
|
|
22
|
+
removes?: TestRemoveEvent[];
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Test Utilities for Running
|
|
27
|
+
*/
|
|
28
|
+
export class RunUtil {
|
|
29
|
+
/**
|
|
30
|
+
* Add 50 ms to the shutdown to allow for buffers to output properly
|
|
31
|
+
*/
|
|
32
|
+
static registerCleanup(scope: string): void {
|
|
33
|
+
ShutdownManager.onGracefulShutdown(() => Util.blockingTimeout(50), `test.${scope}.bufferOutput`);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Determine if a given file path is a valid test file
|
|
38
|
+
*/
|
|
39
|
+
static async isTestFile(file: string): Promise<boolean> {
|
|
40
|
+
const reader = readline.createInterface({ input: createReadStream(file) });
|
|
41
|
+
const state = { imp: false, suite: false };
|
|
42
|
+
for await (const line of reader) {
|
|
43
|
+
state.imp ||= line.includes('@travetto/test');
|
|
44
|
+
state.suite ||= line.includes('Suite'); // Decorator or name
|
|
45
|
+
if (state.imp && state.suite) {
|
|
46
|
+
reader.close();
|
|
47
|
+
return true;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Find all valid test files given the globs
|
|
55
|
+
*/
|
|
56
|
+
static async* getTestImports(globs?: string[]): AsyncIterable<string> {
|
|
57
|
+
const all = RuntimeIndex.find({
|
|
58
|
+
module: mod => mod.roles.includes('test') || mod.roles.includes('std'),
|
|
59
|
+
folder: folder => folder === 'test',
|
|
60
|
+
file: file => file.role === 'test'
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
// Collect globs
|
|
64
|
+
if (globs?.length) {
|
|
65
|
+
const allFiles = new Map(all.map(file => [file.sourceFile, file]));
|
|
66
|
+
for await (const item of fs.glob(globs)) {
|
|
67
|
+
const source = Runtime.workspaceRelative(path.resolve(item));
|
|
68
|
+
const match = allFiles.get(source);
|
|
69
|
+
if (match && await this.isTestFile(match.sourceFile)) {
|
|
70
|
+
yield match.import;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
} else {
|
|
74
|
+
for await (const match of all) {
|
|
75
|
+
if (await this.isTestFile(match.sourceFile)) {
|
|
76
|
+
yield match.import;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Get count of tests for a given set of globs
|
|
84
|
+
* @param input
|
|
85
|
+
*/
|
|
86
|
+
static async resolveGlobInput({ globs, tags, metadata }: TestGlobInput): Promise<TestRun[]> {
|
|
87
|
+
const digestProcess = await ExecUtil.getResult(
|
|
88
|
+
ExecUtil.spawnTrv('test:digest', ['-o', 'json', ...globs], {
|
|
89
|
+
env: { ...process.env, ...Env.FORCE_COLOR.export(0), ...Env.NO_COLOR.export(true) },
|
|
90
|
+
}),
|
|
91
|
+
{ catch: true }
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
if (!digestProcess.valid) {
|
|
95
|
+
throw new Error(digestProcess.stderr);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const testFilter = tags?.length ?
|
|
99
|
+
Util.allowDeny<string, [TestConfig]>(
|
|
100
|
+
tags,
|
|
101
|
+
rule => rule,
|
|
102
|
+
(rule, core) => core.tags?.includes(rule) ?? false
|
|
103
|
+
) :
|
|
104
|
+
((): boolean => true);
|
|
105
|
+
|
|
106
|
+
const parsed: TestConfig[] = JSONUtil.parseSafe(digestProcess.stdout);
|
|
107
|
+
|
|
108
|
+
const events = parsed.filter(testFilter).reduce((runs, test) => {
|
|
109
|
+
if (!runs.has(test.classId)) {
|
|
110
|
+
runs.set(test.classId, { import: test.import, classId: test.classId, methodNames: [], runId: Util.uuid(), metadata });
|
|
111
|
+
}
|
|
112
|
+
runs.get(test.classId)!.methodNames!.push(test.methodName);
|
|
113
|
+
return runs;
|
|
114
|
+
}, new Map<string, TestRun>());
|
|
115
|
+
|
|
116
|
+
return [...events.values()].sort((a, b) => a.runId!.localeCompare(b.runId!));
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Resolve a test diff source to ensure we are only running changed tests
|
|
121
|
+
*/
|
|
122
|
+
static async resolveDiffInput({ import: importPath, diffSource: diff, metadata }: TestDiffInput): Promise<RunState> {
|
|
123
|
+
// Runs, defaults to new classes
|
|
124
|
+
const runs: TestRun[] = [];
|
|
125
|
+
const addRun = (clsId: string | undefined, methods?: string[]): void => {
|
|
126
|
+
runs.push({ import: importPath, classId: clsId, methodNames: methods?.length ? methods : undefined, metadata });
|
|
127
|
+
};
|
|
128
|
+
const removes: TestRemoveEvent[] = [];
|
|
129
|
+
const removeTest = (clsId: string, methodName?: string): void => {
|
|
130
|
+
removes.push({ type: 'removeTest', import: importPath, classId: clsId, methodName });
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
const imported = await Registry.manualInit([importPath]);
|
|
134
|
+
const classes = Object.fromEntries(
|
|
135
|
+
imported
|
|
136
|
+
.filter(cls => SuiteRegistryIndex.hasConfig(cls))
|
|
137
|
+
.map(cls => [cls.Ⲑid, SuiteRegistryIndex.getConfig(cls)])
|
|
138
|
+
);
|
|
139
|
+
|
|
140
|
+
// New classes
|
|
141
|
+
for (const clsId of Object.keys(classes)) {
|
|
142
|
+
if (!diff[clsId]) {
|
|
143
|
+
addRun(clsId);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Looking at Diff
|
|
148
|
+
for (const [clsId, config] of Object.entries(diff)) {
|
|
149
|
+
const local = classes[clsId];
|
|
150
|
+
if (!local) { // Removed classes
|
|
151
|
+
removeTest(clsId);
|
|
152
|
+
} else if (local.sourceHash !== config.sourceHash) { // Class changed or added
|
|
153
|
+
// Methods to run, defaults to newly added
|
|
154
|
+
const methods: string[] = Object.keys(local.tests ?? {}).filter(key => !config.methods[key]);
|
|
155
|
+
let didRemove = false;
|
|
156
|
+
for (const key of Object.keys(config.methods)) {
|
|
157
|
+
const localMethod = local.tests?.[key];
|
|
158
|
+
if (!localMethod) { // Test is removed
|
|
159
|
+
removeTest(clsId, key);
|
|
160
|
+
didRemove = true;
|
|
161
|
+
} else if (localMethod.sourceHash !== config.methods[key]) { // Method changed or added
|
|
162
|
+
methods.push(key);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
if (!didRemove || methods.length > 0) {
|
|
166
|
+
addRun(clsId, methods);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (runs.length === 0 && removes.length === 0) { // Re-run entire file, classes unchanged
|
|
172
|
+
addRun(undefined);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return { runs, removes };
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Reinitialize the manifest if needed, mainly for single test runs
|
|
180
|
+
*/
|
|
181
|
+
static async reinitManifestIfNeeded(runs: TestRun[]): Promise<void> {
|
|
182
|
+
if (runs.length === 1) {
|
|
183
|
+
const [run] = runs;
|
|
184
|
+
const entry = RuntimeIndex.getFromImport(run.import)!;
|
|
185
|
+
|
|
186
|
+
if (entry.module !== Runtime.main.name) {
|
|
187
|
+
RuntimeIndex.reinitForModule(entry.module);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Build test consumer that wraps a given targeted consumer, and the tests to be run
|
|
194
|
+
*/
|
|
195
|
+
static async getRunnableConsumer(target: TestConsumerShape, testRuns: TestRun[]): Promise<RunnableTestConsumer> {
|
|
196
|
+
const consumer = new RunnableTestConsumer(target);
|
|
197
|
+
const testCount = testRuns.reduce((acc, cur) => acc + (cur.methodNames ? cur.methodNames.length : 0), 0);
|
|
198
|
+
|
|
199
|
+
await consumer.onStart({ testCount });
|
|
200
|
+
return consumer;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Resolve input into run state
|
|
205
|
+
*/
|
|
206
|
+
static async resolveInput(input: TestRunInput): Promise<RunState> {
|
|
207
|
+
if ('diffSource' in input) {
|
|
208
|
+
return await this.resolveDiffInput(input);
|
|
209
|
+
} else if ('globs' in input) {
|
|
210
|
+
return { runs: await this.resolveGlobInput(input) };
|
|
211
|
+
} else {
|
|
212
|
+
return { runs: [input], removes: [] };
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Run tests
|
|
218
|
+
*/
|
|
219
|
+
static async runTests(consumerConfig: TestConsumerConfig, input: TestRunInput): Promise<boolean | undefined> {
|
|
220
|
+
const { runs, removes } = await this.resolveInput(input);
|
|
221
|
+
|
|
222
|
+
await this.reinitManifestIfNeeded(runs);
|
|
223
|
+
|
|
224
|
+
const targetConsumer = await TestConsumerRegistryIndex.getInstance(consumerConfig);
|
|
225
|
+
const consumer = await this.getRunnableConsumer(targetConsumer, runs);
|
|
226
|
+
|
|
227
|
+
for (const item of removes ?? []) {
|
|
228
|
+
consumer.onRemoveEvent(item);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
if (runs.length === 1) {
|
|
232
|
+
await new TestExecutor(consumer).execute(runs[0]);
|
|
233
|
+
} else {
|
|
234
|
+
await WorkPool.run(
|
|
235
|
+
run => buildStandardTestManager(consumer, run),
|
|
236
|
+
runs,
|
|
237
|
+
{
|
|
238
|
+
idleTimeoutMillis: TimeUtil.asMillis(10, 's'),
|
|
239
|
+
min: 1,
|
|
240
|
+
max: consumerConfig.concurrency
|
|
241
|
+
}
|
|
242
|
+
);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
return consumer.summarizeAsBoolean();
|
|
246
|
+
}
|
|
247
|
+
}
|
package/src/execute/types.ts
CHANGED
|
@@ -1,9 +1,7 @@
|
|
|
1
|
-
import { TestRun } from '../model/test.ts';
|
|
2
|
-
|
|
3
1
|
/**
|
|
4
|
-
*
|
|
2
|
+
* Test Consumer Configuration
|
|
5
3
|
*/
|
|
6
|
-
export interface
|
|
4
|
+
export interface TestConsumerConfig {
|
|
7
5
|
/**
|
|
8
6
|
* Test result consumer
|
|
9
7
|
*/
|
|
@@ -16,17 +14,4 @@ export interface RunState {
|
|
|
16
14
|
* Number of test suites to run concurrently, when mode is not single
|
|
17
15
|
*/
|
|
18
16
|
concurrency?: number;
|
|
19
|
-
/**
|
|
20
|
-
* The tags to include or exclude from testing
|
|
21
|
-
*/
|
|
22
|
-
tags?: string[];
|
|
23
|
-
/**
|
|
24
|
-
* target
|
|
25
|
-
*/
|
|
26
|
-
target: TestRun | {
|
|
27
|
-
/**
|
|
28
|
-
* Globs to run
|
|
29
|
-
*/
|
|
30
|
-
globs?: string[];
|
|
31
|
-
};
|
|
32
17
|
}
|