@travetto/test 7.0.0-rc.2 → 7.0.0-rc.4
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 +1 -2
- package/__index__.ts +1 -0
- package/package.json +7 -7
- package/src/assert/util.ts +1 -1
- package/src/communication.ts +66 -0
- package/src/consumer/registry-index.ts +7 -7
- package/src/consumer/types/cumulative.ts +90 -61
- package/src/consumer/types/delegating.ts +23 -20
- package/src/consumer/types/event.ts +11 -4
- package/src/consumer/types/exec.ts +12 -3
- package/src/consumer/types/runnable.ts +2 -1
- package/src/consumer/types/summarizer.ts +4 -2
- package/src/consumer/types/tap-summary.ts +5 -3
- package/src/consumer/types.ts +6 -2
- package/src/execute/executor.ts +23 -12
- package/src/execute/phase.ts +1 -1
- package/src/execute/run.ts +247 -0
- package/src/execute/types.ts +2 -17
- package/src/execute/watcher.ts +30 -56
- package/src/model/common.ts +4 -0
- package/src/model/event.ts +3 -1
- package/src/model/suite.ts +9 -20
- package/src/model/test.ts +47 -1
- package/src/model/util.ts +8 -0
- package/src/registry/registry-adapter.ts +4 -2
- package/src/registry/registry-index.ts +10 -11
- package/src/worker/child.ts +12 -12
- package/src/worker/standard.ts +27 -18
- package/src/worker/types.ts +9 -5
- package/support/bin/run.ts +6 -6
- package/support/cli.test.ts +20 -41
- package/support/cli.test_diff.ts +47 -0
- package/support/cli.test_digest.ts +2 -2
- package/support/cli.test_direct.ts +13 -12
- package/support/cli.test_watch.ts +1 -6
- package/src/execute/runner.ts +0 -87
- package/src/execute/util.ts +0 -108
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,6 +14,7 @@ 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
19
|
const TEST_TIMEOUT = TimeUtil.fromValue(Env.TRV_TEST_TIMEOUT.value) ?? 5000;
|
|
19
20
|
|
|
@@ -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
|
|
238
|
+
await manager.endPhase('all');
|
|
229
239
|
} catch (error) {
|
|
230
|
-
await
|
|
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 });
|
package/src/execute/phase.ts
CHANGED
|
@@ -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
|
}
|
package/src/execute/watcher.ts
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
|
+
import { ManifestModuleUtil } from '@travetto/manifest';
|
|
1
2
|
import { Registry } from '@travetto/registry';
|
|
2
3
|
import { WorkPool } from '@travetto/worker';
|
|
3
|
-
import { AsyncQueue,
|
|
4
|
+
import { AsyncQueue, RuntimeIndex, TimeUtil, watchCompiler } from '@travetto/runtime';
|
|
4
5
|
|
|
5
6
|
import { buildStandardTestManager } from '../worker/standard.ts';
|
|
6
7
|
import { TestConsumerRegistryIndex } from '../consumer/registry-index.ts';
|
|
7
8
|
import { CumulativeSummaryConsumer } from '../consumer/types/cumulative.ts';
|
|
8
|
-
import { TestRun } from '../model/test.ts';
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import { SuiteRegistryIndex } from '../registry/registry-index.ts';
|
|
9
|
+
import type { TestDiffInput, TestRun } from '../model/test.ts';
|
|
10
|
+
import { RunUtil } from './run.ts';
|
|
11
|
+
import { isTestRunEvent, type TestReadyEvent } from '../worker/types.ts';
|
|
12
12
|
|
|
13
13
|
/**
|
|
14
14
|
* Test Watcher.
|
|
@@ -25,77 +25,51 @@ export class TestWatcher {
|
|
|
25
25
|
|
|
26
26
|
await Registry.init();
|
|
27
27
|
|
|
28
|
-
const events: TestRun[] = [];
|
|
28
|
+
const events: (TestRun | TestDiffInput)[] = [];
|
|
29
29
|
|
|
30
30
|
if (runAllOnStart) {
|
|
31
|
-
|
|
32
|
-
events.push(...RunnerUtil.getTestRuns(tests));
|
|
31
|
+
events.push(...await RunUtil.resolveGlobInput({ globs: [] }));
|
|
33
32
|
}
|
|
34
33
|
|
|
35
34
|
const queue = new AsyncQueue(events);
|
|
36
35
|
const consumer = new CumulativeSummaryConsumer(
|
|
37
36
|
await TestConsumerRegistryIndex.getInstance({ consumer: format })
|
|
38
|
-
)
|
|
39
|
-
.withFilter(event => event.metadata?.partial !== true || event.type !== 'suite');
|
|
40
|
-
|
|
41
|
-
Registry.onMethodChange((event) => {
|
|
42
|
-
const [cls, method] = 'previous' in event ? event.previous : event.current;
|
|
43
|
-
|
|
44
|
-
if (!cls || describeFunction(cls).abstract) {
|
|
45
|
-
return;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
const classId = cls.Ⲑid;
|
|
49
|
-
if (!method) {
|
|
50
|
-
consumer.removeClass(classId);
|
|
51
|
-
return;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
const config = SuiteRegistryIndex.getTestConfig(cls, method)!;
|
|
55
|
-
if (event.type !== 'removing') {
|
|
56
|
-
if (config) {
|
|
57
|
-
const run: TestRun = {
|
|
58
|
-
import: config.import, classId: config.classId, methodNames: [config.methodName], metadata: { partial: true }
|
|
59
|
-
};
|
|
60
|
-
console.log('Triggering', run);
|
|
61
|
-
queue.add(run, true); // Shift to front
|
|
62
|
-
}
|
|
63
|
-
} else {
|
|
64
|
-
process.send?.({
|
|
65
|
-
type: 'removeTest',
|
|
66
|
-
methodNames: method?.name ? [method.name!] : undefined!,
|
|
67
|
-
method: method?.name,
|
|
68
|
-
classId,
|
|
69
|
-
import: Runtime.getImport(cls)
|
|
70
|
-
} satisfies TestRemovedEvent);
|
|
71
|
-
}
|
|
72
|
-
}, SuiteRegistryIndex);
|
|
73
|
-
|
|
74
|
-
// If a file is changed, but doesn't emit classes, re-run whole file
|
|
75
|
-
Registry.onNonClassChanges(imp => queue.add({ import: imp }));
|
|
37
|
+
);
|
|
76
38
|
|
|
77
39
|
process.on('message', event => {
|
|
78
|
-
if (
|
|
79
|
-
|
|
80
|
-
// Legacy
|
|
81
|
-
if ('file' in event && typeof event.file === 'string') {
|
|
82
|
-
event = { import: RuntimeIndex.getFromSource(event.file)?.import! };
|
|
83
|
-
}
|
|
84
|
-
console.debug('Manually triggered', event);
|
|
85
|
-
queue.add(castTo(event), true);
|
|
40
|
+
if (isTestRunEvent(event)) {
|
|
41
|
+
queue.add(event, true);
|
|
86
42
|
}
|
|
87
43
|
});
|
|
88
44
|
|
|
89
45
|
process.send?.({ type: 'ready' } satisfies TestReadyEvent);
|
|
90
46
|
|
|
91
|
-
|
|
47
|
+
const queueProcessor = WorkPool.run(
|
|
92
48
|
buildStandardTestManager.bind(null, consumer),
|
|
93
49
|
queue,
|
|
94
50
|
{
|
|
95
|
-
idleTimeoutMillis:
|
|
51
|
+
idleTimeoutMillis: TimeUtil.asMillis('2m'),
|
|
96
52
|
min: 2,
|
|
97
53
|
max: WorkPool.DEFAULT_SIZE
|
|
98
54
|
}
|
|
99
55
|
);
|
|
56
|
+
|
|
57
|
+
for await (const event of watchCompiler()) {
|
|
58
|
+
const fileType = ManifestModuleUtil.getFileType(event.file);
|
|
59
|
+
if (
|
|
60
|
+
(fileType === 'ts' || fileType === 'js') &&
|
|
61
|
+
RuntimeIndex.findModuleForArbitraryFile(event.file) !== undefined
|
|
62
|
+
) {
|
|
63
|
+
if (event.action === 'delete') {
|
|
64
|
+
consumer.removeTest(event.import);
|
|
65
|
+
} else {
|
|
66
|
+
const diffSource = consumer.produceDiffSource(event.import);
|
|
67
|
+
queue.add({ import: event.import, diffSource }, true);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Cleanup
|
|
73
|
+
await queueProcessor;
|
|
100
74
|
}
|
|
101
75
|
}
|
package/src/model/common.ts
CHANGED
package/src/model/event.ts
CHANGED
|
@@ -14,6 +14,8 @@ export type EventPhase = 'before' | 'after';
|
|
|
14
14
|
type EventTpl<T extends EventEntity, P extends EventPhase, V extends {}> =
|
|
15
15
|
{ type: T, phase: P, metadata?: Record<string, unknown> } & V;
|
|
16
16
|
|
|
17
|
+
export type TestRemoveEvent = { type: 'removeTest', import: string, classId?: string, methodName?: string, metadata?: Record<string, unknown> };
|
|
18
|
+
|
|
17
19
|
/**
|
|
18
20
|
* Different test event shapes
|
|
19
21
|
*/
|
|
@@ -22,4 +24,4 @@ export type TestEvent =
|
|
|
22
24
|
EventTpl<'test', 'before', { test: TestConfig }> |
|
|
23
25
|
EventTpl<'test', 'after', { test: TestResult }> |
|
|
24
26
|
EventTpl<'suite', 'before', { suite: SuiteConfig }> |
|
|
25
|
-
EventTpl<'suite', 'after', { suite: SuiteResult }>;
|
|
27
|
+
EventTpl<'suite', 'after', { suite: SuiteResult }>;
|
package/src/model/suite.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { Class } from '@travetto/runtime';
|
|
2
2
|
|
|
3
|
-
import { Assertion, TestConfig, TestResult } from './test.ts';
|
|
3
|
+
import { Assertion, TestConfig, TestResult, type TestStatus } from './test.ts';
|
|
4
4
|
import { Skip, SuiteCore } from './common.ts';
|
|
5
5
|
|
|
6
6
|
/**
|
|
@@ -48,37 +48,26 @@ export interface Counts {
|
|
|
48
48
|
passed: number;
|
|
49
49
|
skipped: number;
|
|
50
50
|
failed: number;
|
|
51
|
+
unknown: number;
|
|
51
52
|
total: number;
|
|
52
53
|
}
|
|
53
54
|
|
|
54
55
|
/**
|
|
55
56
|
* Results of a suite run
|
|
56
57
|
*/
|
|
57
|
-
export interface SuiteResult extends Counts {
|
|
58
|
+
export interface SuiteResult extends Counts, SuiteCore {
|
|
58
59
|
/**
|
|
59
|
-
*
|
|
60
|
+
* All test results
|
|
60
61
|
*/
|
|
61
|
-
|
|
62
|
-
/**
|
|
63
|
-
* Import for the suite
|
|
64
|
-
*/
|
|
65
|
-
import: string;
|
|
66
|
-
/**
|
|
67
|
-
* Start of the suite
|
|
68
|
-
*/
|
|
69
|
-
lineStart: number;
|
|
70
|
-
/**
|
|
71
|
-
* End of the suite
|
|
72
|
-
*/
|
|
73
|
-
lineEnd: number;
|
|
74
|
-
/**
|
|
75
|
-
* ALl test results
|
|
76
|
-
*/
|
|
77
|
-
tests: TestResult[];
|
|
62
|
+
tests: Record<string, TestResult>;
|
|
78
63
|
/**
|
|
79
64
|
* Suite duration
|
|
80
65
|
*/
|
|
81
66
|
duration: number;
|
|
67
|
+
/**
|
|
68
|
+
* Overall status
|
|
69
|
+
*/
|
|
70
|
+
status: TestStatus;
|
|
82
71
|
}
|
|
83
72
|
|
|
84
73
|
/**
|
package/src/model/test.ts
CHANGED
|
@@ -5,6 +5,13 @@ import { Skip, TestCore } from './common.ts';
|
|
|
5
5
|
export type ThrowableError = string | RegExp | Class<Error> | ((error: Error | string) => boolean | void | undefined);
|
|
6
6
|
export type TestLog = Omit<ConsoleEvent, 'args' | 'scope'> & { message: string };
|
|
7
7
|
|
|
8
|
+
export type TestDiffSource = Record<string, {
|
|
9
|
+
sourceHash: number;
|
|
10
|
+
methods: Record<string, number>;
|
|
11
|
+
}>;
|
|
12
|
+
|
|
13
|
+
export type TestStatus = 'passed' | 'skipped' | 'failed' | 'unknown';
|
|
14
|
+
|
|
8
15
|
/**
|
|
9
16
|
* Specific configuration for a test
|
|
10
17
|
*/
|
|
@@ -88,7 +95,7 @@ export interface TestResult extends TestCore {
|
|
|
88
95
|
/**
|
|
89
96
|
* status
|
|
90
97
|
*/
|
|
91
|
-
status:
|
|
98
|
+
status: TestStatus;
|
|
92
99
|
/**
|
|
93
100
|
* Error if failed
|
|
94
101
|
*/
|
|
@@ -136,3 +143,42 @@ export type TestRun = {
|
|
|
136
143
|
*/
|
|
137
144
|
runId?: string;
|
|
138
145
|
};
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Test Diff Input
|
|
149
|
+
*/
|
|
150
|
+
export type TestDiffInput = {
|
|
151
|
+
/**
|
|
152
|
+
* Import for run
|
|
153
|
+
*/
|
|
154
|
+
import: string;
|
|
155
|
+
/**
|
|
156
|
+
* Diff Source
|
|
157
|
+
*/
|
|
158
|
+
diffSource: TestDiffSource;
|
|
159
|
+
/**
|
|
160
|
+
* Test run metadata
|
|
161
|
+
*/
|
|
162
|
+
metadata?: Record<string, unknown>;
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Test Glob Input
|
|
167
|
+
*/
|
|
168
|
+
export type TestGlobInput = {
|
|
169
|
+
/**
|
|
170
|
+
* Globs to run
|
|
171
|
+
*/
|
|
172
|
+
globs: string[];
|
|
173
|
+
/**
|
|
174
|
+
* Tags to filter by
|
|
175
|
+
*/
|
|
176
|
+
tags?: string[];
|
|
177
|
+
/**
|
|
178
|
+
* Test run metadata
|
|
179
|
+
*/
|
|
180
|
+
metadata?: Record<string, unknown>;
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
export type TestRunInput = TestRun | TestDiffInput | TestGlobInput;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { Counts } from './suite';
|
|
2
|
+
import type { TestStatus } from './test';
|
|
3
|
+
|
|
4
|
+
export class TestModelUtil {
|
|
5
|
+
static countsToTestStatus(counts: Counts): TestStatus {
|
|
6
|
+
return counts.failed ? 'failed' : (counts.passed ? 'passed' : counts.skipped ? 'skipped' : 'unknown');
|
|
7
|
+
}
|
|
8
|
+
}
|