@elench/testkit 0.1.40 → 0.1.41
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 +25 -13
- package/lib/cli/args.mjs +0 -4
- package/lib/cli/args.test.mjs +0 -5
- package/lib/cli/index.mjs +0 -9
- package/lib/config/index.mjs +67 -24
- package/lib/database/index.mjs +19 -7
- package/lib/database/naming.mjs +2 -2
- package/lib/database/naming.test.mjs +2 -2
- package/lib/runner/default-runtime-runner.mjs +31 -53
- package/lib/runner/execution-config.mjs +14 -70
- package/lib/runner/execution-config.test.mjs +22 -74
- package/lib/runner/formatting.mjs +0 -15
- package/lib/runner/formatting.test.mjs +0 -18
- package/lib/runner/lifecycle.mjs +7 -7
- package/lib/runner/orchestrator.mjs +9 -10
- package/lib/runner/planning.mjs +42 -136
- package/lib/runner/planning.test.mjs +70 -174
- package/lib/runner/playwright-config.mjs +8 -2
- package/lib/runner/playwright-config.test.mjs +20 -5
- package/lib/runner/playwright-runner.mjs +32 -54
- package/lib/runner/readiness.mjs +2 -2
- package/lib/runner/reporting.mjs +2 -3
- package/lib/runner/reporting.test.mjs +2 -5
- package/lib/runner/results.mjs +1 -1
- package/lib/runner/results.test.mjs +1 -1
- package/lib/runner/runtime-contexts.mjs +20 -24
- package/lib/runner/runtime-manager.mjs +181 -0
- package/lib/runner/runtime-manager.test.mjs +181 -0
- package/lib/runner/services.mjs +4 -4
- package/lib/runner/state.mjs +1 -2
- package/lib/runner/state.test.mjs +2 -4
- package/lib/runner/template.mjs +90 -60
- package/lib/runner/template.test.mjs +59 -27
- package/lib/runner/worker-loop.mjs +29 -32
- package/lib/setup/index.d.ts +14 -10
- package/package.json +1 -1
- package/lib/runner/stack-manager.mjs +0 -146
package/lib/runner/planning.mjs
CHANGED
|
@@ -4,12 +4,11 @@ import {
|
|
|
4
4
|
matchesSuiteSelectors,
|
|
5
5
|
suiteSelectionType,
|
|
6
6
|
} from "./suite-selection.mjs";
|
|
7
|
-
import { resolveBatchAccessMode, resolveBatchStackMode } from "./execution-config.mjs";
|
|
8
7
|
|
|
9
8
|
const TYPE_ORDER = ["dal", "integration", "e2e", "load"];
|
|
10
9
|
|
|
11
|
-
export function
|
|
12
|
-
return
|
|
10
|
+
export function taskNeedsLocalRuntime(task) {
|
|
11
|
+
return task.type !== "dal";
|
|
13
12
|
}
|
|
14
13
|
|
|
15
14
|
export function resolveRuntimeConfigs(targetConfig, configMap) {
|
|
@@ -78,10 +77,6 @@ export function collectSuites(config, typeValues, suiteSelectors, fileNames = []
|
|
|
78
77
|
(framework === "playwright"
|
|
79
78
|
? Math.max(2, Math.max(1, files.length))
|
|
80
79
|
: Math.max(1, files.length)),
|
|
81
|
-
maxFileConcurrency:
|
|
82
|
-
framework === "k6" || framework === "playwright"
|
|
83
|
-
? suite.testkit?.maxFileConcurrency || 1
|
|
84
|
-
: 1,
|
|
85
80
|
totalFileCount: selectedSuiteFiles.length,
|
|
86
81
|
});
|
|
87
82
|
orderIndex += 1;
|
|
@@ -93,7 +88,7 @@ export function collectSuites(config, typeValues, suiteSelectors, fileNames = []
|
|
|
93
88
|
|
|
94
89
|
export function applyShard(suites, shard) {
|
|
95
90
|
if (!shard) return suites;
|
|
96
|
-
return suites.filter((
|
|
91
|
+
return suites.filter((_unused, index) => index % shard.total === shard.index - 1);
|
|
97
92
|
}
|
|
98
93
|
|
|
99
94
|
export function orderedTypes(types) {
|
|
@@ -109,12 +104,15 @@ export function orderedTypes(types) {
|
|
|
109
104
|
|
|
110
105
|
export function buildRuntimeGraphs(servicePlans) {
|
|
111
106
|
const executed = servicePlans.filter((plan) => !plan.skipped);
|
|
112
|
-
const
|
|
107
|
+
const graphs = [];
|
|
113
108
|
const graphByRuntimeKey = new Map();
|
|
114
109
|
|
|
115
110
|
for (const plan of executed) {
|
|
116
|
-
|
|
117
|
-
|
|
111
|
+
plan.assignedGraphKey = plan.runtimeKey;
|
|
112
|
+
const existing = graphByRuntimeKey.get(plan.runtimeKey);
|
|
113
|
+
if (existing) {
|
|
114
|
+
existing.targetNames.push(plan.config.name);
|
|
115
|
+
existing.instanceCount = Math.max(existing.instanceCount, plan.config.testkit.runtime.instances);
|
|
118
116
|
continue;
|
|
119
117
|
}
|
|
120
118
|
|
|
@@ -122,48 +120,22 @@ export function buildRuntimeGraphs(servicePlans) {
|
|
|
122
120
|
key: plan.runtimeKey,
|
|
123
121
|
runtimeNames: plan.runtimeNames,
|
|
124
122
|
runtimeConfigs: plan.runtimeConfigs,
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
rootConfig: null,
|
|
123
|
+
targetNames: [plan.config.name],
|
|
124
|
+
dirName: buildGraphDirName(plan.runtimeNames),
|
|
125
|
+
instanceCount: plan.config.testkit.runtime.instances,
|
|
129
126
|
};
|
|
130
|
-
|
|
127
|
+
graphs.push(graph);
|
|
131
128
|
graphByRuntimeKey.set(plan.runtimeKey, graph);
|
|
132
129
|
}
|
|
133
130
|
|
|
134
|
-
const
|
|
135
|
-
|
|
136
|
-
!uniqueGraphs.some(
|
|
137
|
-
(other) =>
|
|
138
|
-
other.key !== graph.key &&
|
|
139
|
-
isRuntimeSuperset(other.runtimeNames, graph.runtimeNames)
|
|
140
|
-
)
|
|
141
|
-
);
|
|
142
|
-
|
|
143
|
-
for (const plan of executed) {
|
|
144
|
-
const compatible = maximalGraphs.filter((graph) =>
|
|
145
|
-
isRuntimeSuperset(graph.runtimeNames, plan.runtimeNames)
|
|
146
|
-
);
|
|
147
|
-
if (compatible.length === 0) {
|
|
148
|
-
throw new Error(`No runtime graph found for service "${plan.config.name}"`);
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
const assigned = compatible.sort(compareGraphsForAssignment)[0];
|
|
152
|
-
plan.assignedGraphKey = assigned.key;
|
|
153
|
-
assigned.assignedTargets.push(plan.config.name);
|
|
154
|
-
}
|
|
131
|
+
const sortedGraphs = graphs.sort((left, right) => left.dirName.localeCompare(right.dirName));
|
|
132
|
+
const maxInstanceCount = Math.max(1, ...sortedGraphs.map((graph) => graph.instanceCount));
|
|
155
133
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
}
|
|
162
|
-
graph.rootConfig = rootPlan.config;
|
|
163
|
-
graph.dirName = buildGraphDirName(graph.runtimeNames);
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
return maximalGraphs.sort((a, b) => a.dirName.localeCompare(b.dirName));
|
|
134
|
+
return sortedGraphs.map((graph, index) => ({
|
|
135
|
+
...graph,
|
|
136
|
+
portNamespaceIndex: index,
|
|
137
|
+
portNamespaceStride: maxInstanceCount,
|
|
138
|
+
}));
|
|
167
139
|
}
|
|
168
140
|
|
|
169
141
|
export function buildTaskQueue(servicePlans, graphs, timings) {
|
|
@@ -181,7 +153,6 @@ export function buildTaskQueue(servicePlans, graphs, timings) {
|
|
|
181
153
|
|
|
182
154
|
for (const suite of plan.suites) {
|
|
183
155
|
for (const file of suite.files) {
|
|
184
|
-
const stackMode = resolveTaskStackMode(plan.config, plan.execution, suite, file);
|
|
185
156
|
const timingKey = buildTimingKey(plan.config.name, suite, file);
|
|
186
157
|
tasks.push({
|
|
187
158
|
id: nextId,
|
|
@@ -192,18 +163,11 @@ export function buildTaskQueue(servicePlans, graphs, timings) {
|
|
|
192
163
|
suiteName: suite.name,
|
|
193
164
|
type: suite.type,
|
|
194
165
|
framework: suite.framework,
|
|
195
|
-
stackMode,
|
|
196
|
-
accessMode: resolveBatchAccessMode({
|
|
197
|
-
framework: suite.framework,
|
|
198
|
-
type: suite.type,
|
|
199
|
-
stackMode,
|
|
200
|
-
}),
|
|
201
166
|
orderIndex: suite.orderIndex,
|
|
202
167
|
file,
|
|
168
|
+
locks: resolveTaskLocks(plan.config, suite, file),
|
|
203
169
|
timingKey,
|
|
204
170
|
estimatedDurationMs: estimateTaskDuration(timings, timingKey, suite),
|
|
205
|
-
maxBatchSize:
|
|
206
|
-
suite.maxFileConcurrency || 1,
|
|
207
171
|
});
|
|
208
172
|
nextId += 1;
|
|
209
173
|
}
|
|
@@ -219,7 +183,7 @@ export function buildTaskQueue(servicePlans, graphs, timings) {
|
|
|
219
183
|
);
|
|
220
184
|
}
|
|
221
185
|
|
|
222
|
-
export function
|
|
186
|
+
export function claimNextTask(queue, preferredGraphKey) {
|
|
223
187
|
if (queue.length === 0) return null;
|
|
224
188
|
|
|
225
189
|
let index = -1;
|
|
@@ -228,59 +192,12 @@ export function claimNextBatch(queue, preferredGraphKey) {
|
|
|
228
192
|
}
|
|
229
193
|
if (index === -1) index = 0;
|
|
230
194
|
|
|
231
|
-
|
|
232
|
-
const tasks = [seed];
|
|
233
|
-
|
|
234
|
-
if (seed.maxBatchSize > 1) {
|
|
235
|
-
for (let cursor = 0; cursor < queue.length; cursor += 1) {
|
|
236
|
-
if (tasks.length >= seed.maxBatchSize) break;
|
|
237
|
-
const candidate = queue[cursor];
|
|
238
|
-
if (
|
|
239
|
-
candidate.framework === seed.framework &&
|
|
240
|
-
candidate.type === seed.type &&
|
|
241
|
-
candidate.graphKey === seed.graphKey &&
|
|
242
|
-
candidate.targetName === seed.targetName &&
|
|
243
|
-
candidate.suiteKey === seed.suiteKey &&
|
|
244
|
-
candidate.stackMode === seed.stackMode &&
|
|
245
|
-
candidate.accessMode === seed.accessMode
|
|
246
|
-
) {
|
|
247
|
-
tasks.push(candidate);
|
|
248
|
-
queue.splice(cursor, 1);
|
|
249
|
-
cursor -= 1;
|
|
250
|
-
}
|
|
251
|
-
}
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
tasks.sort(
|
|
255
|
-
(a, b) =>
|
|
256
|
-
a.orderIndex - b.orderIndex ||
|
|
257
|
-
a.file.localeCompare(b.file)
|
|
258
|
-
);
|
|
259
|
-
|
|
260
|
-
return {
|
|
261
|
-
graphKey: seed.graphKey,
|
|
262
|
-
targetName: seed.targetName,
|
|
263
|
-
framework: seed.framework,
|
|
264
|
-
type: seed.type,
|
|
265
|
-
stackMode: seed.stackMode,
|
|
266
|
-
accessMode: seed.accessMode,
|
|
267
|
-
tasks,
|
|
268
|
-
};
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
export function isRuntimeSuperset(candidate, target) {
|
|
272
|
-
return target.every((name) => candidate.includes(name));
|
|
195
|
+
return queue.splice(index, 1)[0];
|
|
273
196
|
}
|
|
274
197
|
|
|
275
|
-
export function
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
}
|
|
279
|
-
return left.key.localeCompare(right.key);
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
function normalizePathSeparators(filePath) {
|
|
283
|
-
return String(filePath).split("\\").join("/");
|
|
198
|
+
export function buildGraphDirName(runtimeNames) {
|
|
199
|
+
const slug = runtimeNames.map(slugSegment).join("__");
|
|
200
|
+
return slug.length > 0 ? slug : "graph";
|
|
284
201
|
}
|
|
285
202
|
|
|
286
203
|
function applySkipRules(config, displayType, suiteName, files, opts = {}) {
|
|
@@ -324,38 +241,27 @@ function applySkipRules(config, displayType, suiteName, files, opts = {}) {
|
|
|
324
241
|
};
|
|
325
242
|
}
|
|
326
243
|
|
|
327
|
-
function
|
|
328
|
-
const
|
|
329
|
-
const
|
|
330
|
-
const
|
|
331
|
-
matchesSuiteSelectors(displayType,
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
}
|
|
244
|
+
function resolveTaskLocks(config, suite, file) {
|
|
245
|
+
const locks = new Set();
|
|
246
|
+
const matchedSuiteRules = config.testkit.requirements?.suites || [];
|
|
247
|
+
for (const rule of matchedSuiteRules) {
|
|
248
|
+
if (matchesSuiteSelectors(suite.displayType, suite.name, [rule.selector])) {
|
|
249
|
+
for (const lockName of rule.locks || []) {
|
|
250
|
+
locks.add(lockName);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
335
254
|
|
|
336
|
-
function resolveTaskStackMode(config, execution, suite, file) {
|
|
337
|
-
const effectiveExecution = execution || config.testkit.execution;
|
|
338
255
|
const normalizedFile = normalizePathSeparators(file);
|
|
339
|
-
const
|
|
340
|
-
|
|
341
|
-
return resolveBatchStackMode(effectiveExecution.stackMode, fileOverride);
|
|
256
|
+
for (const lockName of config.testkit.requirements?.fileLocksByPath?.get(normalizedFile) || []) {
|
|
257
|
+
locks.add(lockName);
|
|
342
258
|
}
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
...config,
|
|
346
|
-
testkit: {
|
|
347
|
-
...config.testkit,
|
|
348
|
-
execution: effectiveExecution,
|
|
349
|
-
},
|
|
350
|
-
},
|
|
351
|
-
suite.displayType,
|
|
352
|
-
suite.name
|
|
353
|
-
);
|
|
259
|
+
|
|
260
|
+
return [...locks].sort();
|
|
354
261
|
}
|
|
355
262
|
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
return slug.length > 0 ? slug : "graph";
|
|
263
|
+
function normalizePathSeparators(filePath) {
|
|
264
|
+
return String(filePath).split("\\").join("/");
|
|
359
265
|
}
|
|
360
266
|
|
|
361
267
|
function slugSegment(value) {
|
|
@@ -4,7 +4,7 @@ import {
|
|
|
4
4
|
buildGraphDirName,
|
|
5
5
|
buildRuntimeGraphs,
|
|
6
6
|
buildTaskQueue,
|
|
7
|
-
|
|
7
|
+
claimNextTask,
|
|
8
8
|
collectSuites,
|
|
9
9
|
resolveRuntimeConfigs,
|
|
10
10
|
} from "./planning.mjs";
|
|
@@ -14,20 +14,22 @@ function makeConfig(name, extras = {}) {
|
|
|
14
14
|
return {
|
|
15
15
|
name,
|
|
16
16
|
suites: extras.suites || {},
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
stackCount: 1,
|
|
23
|
-
},
|
|
24
|
-
serviceExecution: providedTestkit.serviceExecution || {
|
|
25
|
-
suites: [],
|
|
26
|
-
files: [],
|
|
27
|
-
fileStackModeByPath: new Map(),
|
|
28
|
-
},
|
|
29
|
-
...providedTestkit,
|
|
17
|
+
testkit: {
|
|
18
|
+
dependsOn: extras.dependsOn || providedTestkit.dependsOn || [],
|
|
19
|
+
execution: providedTestkit.execution || {
|
|
20
|
+
workers: 1,
|
|
21
|
+
fileTimeoutSeconds: 60,
|
|
30
22
|
},
|
|
23
|
+
runtime: providedTestkit.runtime || {
|
|
24
|
+
instances: 1,
|
|
25
|
+
},
|
|
26
|
+
requirements: providedTestkit.requirements || {
|
|
27
|
+
suites: [],
|
|
28
|
+
files: [],
|
|
29
|
+
fileLocksByPath: new Map(),
|
|
30
|
+
},
|
|
31
|
+
...providedTestkit,
|
|
32
|
+
},
|
|
31
33
|
...extras,
|
|
32
34
|
};
|
|
33
35
|
}
|
|
@@ -67,17 +69,6 @@ describe("runner-planning", () => {
|
|
|
67
69
|
},
|
|
68
70
|
],
|
|
69
71
|
},
|
|
70
|
-
testkit: {
|
|
71
|
-
dependsOn: [],
|
|
72
|
-
execution: {
|
|
73
|
-
workers: 1,
|
|
74
|
-
stackMode: "shared",
|
|
75
|
-
stackCount: 1,
|
|
76
|
-
},
|
|
77
|
-
serviceExecution: {
|
|
78
|
-
suites: [],
|
|
79
|
-
},
|
|
80
|
-
},
|
|
81
72
|
});
|
|
82
73
|
|
|
83
74
|
expect(collectSuites(config, ["int"], [], [])[0]).toMatchObject({
|
|
@@ -122,15 +113,6 @@ describe("runner-planning", () => {
|
|
|
122
113
|
],
|
|
123
114
|
},
|
|
124
115
|
testkit: {
|
|
125
|
-
dependsOn: [],
|
|
126
|
-
execution: {
|
|
127
|
-
workers: 1,
|
|
128
|
-
stackMode: "shared",
|
|
129
|
-
stackCount: 1,
|
|
130
|
-
},
|
|
131
|
-
serviceExecution: {
|
|
132
|
-
suites: [],
|
|
133
|
-
},
|
|
134
116
|
skip: {
|
|
135
117
|
fileReasonByPath: new Map([
|
|
136
118
|
["__testkit__/billing/a.int.testkit.ts", "Billing is stubbed"],
|
|
@@ -153,21 +135,9 @@ describe("runner-planning", () => {
|
|
|
153
135
|
totalFileCount: 2,
|
|
154
136
|
}),
|
|
155
137
|
]);
|
|
156
|
-
|
|
157
|
-
expect(collectSuites(config, ["int"], [], [], { ignoreSkipRules: true })).toEqual([
|
|
158
|
-
expect.objectContaining({
|
|
159
|
-
name: "billing",
|
|
160
|
-
files: [
|
|
161
|
-
"__testkit__/billing/a.int.testkit.ts",
|
|
162
|
-
"__testkit__/billing/b.int.testkit.ts",
|
|
163
|
-
],
|
|
164
|
-
skippedFiles: [],
|
|
165
|
-
totalFileCount: 2,
|
|
166
|
-
}),
|
|
167
|
-
]);
|
|
168
138
|
});
|
|
169
139
|
|
|
170
|
-
it("applies
|
|
140
|
+
it("applies lock requirements to matching suites and files", () => {
|
|
171
141
|
const api = makeConfig("api", {
|
|
172
142
|
suites: {
|
|
173
143
|
integration: [
|
|
@@ -180,28 +150,30 @@ describe("runner-planning", () => {
|
|
|
180
150
|
],
|
|
181
151
|
orderIndex: 0,
|
|
182
152
|
weight: 2,
|
|
183
|
-
maxFileConcurrency: 1,
|
|
184
153
|
},
|
|
185
154
|
],
|
|
186
155
|
},
|
|
187
156
|
testkit: {
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
stackMode: "shared",
|
|
191
|
-
stackCount: 1,
|
|
157
|
+
runtime: {
|
|
158
|
+
instances: 3,
|
|
192
159
|
},
|
|
193
|
-
|
|
194
|
-
suites: [
|
|
160
|
+
requirements: {
|
|
161
|
+
suites: [
|
|
162
|
+
{
|
|
163
|
+
selector: { kind: "typed", type: "int", name: "routes", raw: "int:routes" },
|
|
164
|
+
locks: ["suite-lock"],
|
|
165
|
+
},
|
|
166
|
+
],
|
|
195
167
|
files: [
|
|
196
168
|
{
|
|
197
169
|
path: "src/api/routes/__testkit__/crawls-worker-concurrency.int.testkit.ts",
|
|
198
|
-
|
|
170
|
+
locks: ["worker-loop"],
|
|
199
171
|
},
|
|
200
172
|
],
|
|
201
|
-
|
|
173
|
+
fileLocksByPath: new Map([
|
|
202
174
|
[
|
|
203
175
|
"src/api/routes/__testkit__/crawls-worker-concurrency.int.testkit.ts",
|
|
204
|
-
"
|
|
176
|
+
["worker-loop"],
|
|
205
177
|
],
|
|
206
178
|
]),
|
|
207
179
|
},
|
|
@@ -239,21 +211,33 @@ describe("runner-planning", () => {
|
|
|
239
211
|
expect.arrayContaining([
|
|
240
212
|
expect.objectContaining({
|
|
241
213
|
file: "src/api/routes/__testkit__/health.int.testkit.ts",
|
|
242
|
-
|
|
243
|
-
accessMode: "shared",
|
|
214
|
+
locks: ["suite-lock"],
|
|
244
215
|
}),
|
|
245
216
|
expect.objectContaining({
|
|
246
217
|
file: "src/api/routes/__testkit__/crawls-worker-concurrency.int.testkit.ts",
|
|
247
|
-
|
|
248
|
-
accessMode: "exclusive",
|
|
218
|
+
locks: ["suite-lock", "worker-loop"],
|
|
249
219
|
}),
|
|
250
220
|
])
|
|
251
221
|
);
|
|
252
222
|
});
|
|
253
223
|
|
|
254
|
-
it("
|
|
255
|
-
const api = makeConfig("api"
|
|
256
|
-
|
|
224
|
+
it("builds exact runtime graphs and claims single file tasks", () => {
|
|
225
|
+
const api = makeConfig("api", {
|
|
226
|
+
testkit: {
|
|
227
|
+
runtime: {
|
|
228
|
+
instances: 2,
|
|
229
|
+
},
|
|
230
|
+
},
|
|
231
|
+
});
|
|
232
|
+
const frontend = makeConfig("frontend", {
|
|
233
|
+
dependsOn: ["api"],
|
|
234
|
+
testkit: {
|
|
235
|
+
runtime: {
|
|
236
|
+
instances: 1,
|
|
237
|
+
},
|
|
238
|
+
},
|
|
239
|
+
});
|
|
240
|
+
|
|
257
241
|
const plans = [
|
|
258
242
|
{
|
|
259
243
|
config: api,
|
|
@@ -269,9 +253,6 @@ describe("runner-planning", () => {
|
|
|
269
253
|
files: ["a.js", "b.js"],
|
|
270
254
|
orderIndex: 0,
|
|
271
255
|
weight: 2,
|
|
272
|
-
maxFileConcurrency: 2,
|
|
273
|
-
stackMode: "isolated",
|
|
274
|
-
accessMode: "exclusive",
|
|
275
256
|
},
|
|
276
257
|
],
|
|
277
258
|
},
|
|
@@ -286,12 +267,9 @@ describe("runner-planning", () => {
|
|
|
286
267
|
name: "auth",
|
|
287
268
|
type: "e2e",
|
|
288
269
|
framework: "playwright",
|
|
289
|
-
files: ["auth.spec.js"
|
|
270
|
+
files: ["auth.spec.js"],
|
|
290
271
|
orderIndex: 0,
|
|
291
|
-
weight:
|
|
292
|
-
maxFileConcurrency: 1,
|
|
293
|
-
stackMode: "isolated",
|
|
294
|
-
accessMode: "exclusive",
|
|
272
|
+
weight: 1,
|
|
295
273
|
},
|
|
296
274
|
],
|
|
297
275
|
},
|
|
@@ -300,112 +278,30 @@ describe("runner-planning", () => {
|
|
|
300
278
|
expect(applyShard(["a", "b", "c", "d"], { index: 2, total: 2 })).toEqual(["b", "d"]);
|
|
301
279
|
|
|
302
280
|
const graphs = buildRuntimeGraphs(plans);
|
|
303
|
-
expect(graphs).
|
|
304
|
-
|
|
281
|
+
expect(graphs).toEqual([
|
|
282
|
+
expect.objectContaining({
|
|
283
|
+
key: "api",
|
|
284
|
+
instanceCount: 2,
|
|
285
|
+
targetNames: ["api"],
|
|
286
|
+
}),
|
|
287
|
+
expect.objectContaining({
|
|
288
|
+
key: "api|frontend",
|
|
289
|
+
instanceCount: 1,
|
|
290
|
+
targetNames: ["frontend"],
|
|
291
|
+
}),
|
|
292
|
+
]);
|
|
305
293
|
expect(buildGraphDirName(["api", "frontend"])).toBe("api__frontend");
|
|
306
294
|
|
|
307
295
|
const queue = buildTaskQueue(plans, graphs, { files: {} });
|
|
308
|
-
expect(queue).toHaveLength(
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
expect(firstBatch.accessMode).toBe("exclusive");
|
|
314
|
-
|
|
315
|
-
const secondBatch = claimNextBatch(queue, "api|frontend");
|
|
316
|
-
expect(secondBatch.tasks).toHaveLength(1);
|
|
317
|
-
expect(secondBatch.framework).toBe("playwright");
|
|
318
|
-
|
|
319
|
-
const thirdBatch = claimNextBatch(queue, "api|frontend");
|
|
320
|
-
expect(thirdBatch.tasks).toHaveLength(2);
|
|
321
|
-
expect(thirdBatch.framework).toBe("k6");
|
|
322
|
-
});
|
|
323
|
-
|
|
324
|
-
it("allows Playwright suites to opt into bounded multi-file batches", () => {
|
|
325
|
-
const frontend = makeConfig("frontend");
|
|
326
|
-
const plans = [
|
|
327
|
-
{
|
|
328
|
-
config: frontend,
|
|
329
|
-
skipped: false,
|
|
330
|
-
runtimeConfigs: [frontend],
|
|
331
|
-
runtimeNames: ["frontend"],
|
|
332
|
-
runtimeKey: "frontend",
|
|
333
|
-
suites: [
|
|
334
|
-
{
|
|
335
|
-
name: "auth",
|
|
336
|
-
type: "e2e",
|
|
337
|
-
framework: "playwright",
|
|
338
|
-
files: ["a.spec.js", "b.spec.js", "c.spec.js"],
|
|
339
|
-
orderIndex: 0,
|
|
340
|
-
weight: 3,
|
|
341
|
-
maxFileConcurrency: 2,
|
|
342
|
-
stackMode: "isolated",
|
|
343
|
-
accessMode: "exclusive",
|
|
344
|
-
},
|
|
345
|
-
],
|
|
346
|
-
},
|
|
347
|
-
];
|
|
348
|
-
|
|
349
|
-
const graphs = buildRuntimeGraphs(plans);
|
|
350
|
-
const queue = buildTaskQueue(plans, graphs, { files: {} });
|
|
351
|
-
|
|
352
|
-
const firstBatch = claimNextBatch(queue, "frontend");
|
|
353
|
-
expect(firstBatch.tasks.map((task) => task.file)).toEqual(["a.spec.js", "b.spec.js"]);
|
|
354
|
-
expect(firstBatch.tasks).toHaveLength(2);
|
|
355
|
-
expect(firstBatch.framework).toBe("playwright");
|
|
356
|
-
|
|
357
|
-
const secondBatch = claimNextBatch(queue, "frontend");
|
|
358
|
-
expect(secondBatch.tasks).toHaveLength(1);
|
|
359
|
-
expect(secondBatch.framework).toBe("playwright");
|
|
360
|
-
});
|
|
361
|
-
|
|
362
|
-
it("applies typed suite execution rules from config", () => {
|
|
363
|
-
const config = makeConfig("api", {
|
|
364
|
-
suites: {
|
|
365
|
-
integration: [{ name: "health", files: ["__testkit__/health/health.int.testkit.ts"] }],
|
|
366
|
-
},
|
|
367
|
-
testkit: {
|
|
368
|
-
dependsOn: [],
|
|
369
|
-
execution: {
|
|
370
|
-
workers: 8,
|
|
371
|
-
stackMode: "shared",
|
|
372
|
-
stackCount: 1,
|
|
373
|
-
},
|
|
374
|
-
serviceExecution: {
|
|
375
|
-
suites: [{ selector: { kind: "typed", type: "int", name: "health", raw: "int:health" }, stackMode: "isolated" }],
|
|
376
|
-
},
|
|
377
|
-
},
|
|
296
|
+
expect(queue).toHaveLength(3);
|
|
297
|
+
expect(claimNextTask(queue, "api|frontend")).toMatchObject({
|
|
298
|
+
framework: "playwright",
|
|
299
|
+
graphKey: "api|frontend",
|
|
300
|
+
file: "auth.spec.js",
|
|
378
301
|
});
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
config,
|
|
383
|
-
skipped: false,
|
|
384
|
-
runtimeConfigs: [config],
|
|
385
|
-
runtimeNames: ["api"],
|
|
386
|
-
runtimeKey: "api",
|
|
387
|
-
suites: collectSuites(config, ["int"], [], []),
|
|
388
|
-
},
|
|
389
|
-
]);
|
|
390
|
-
const queue = buildTaskQueue(
|
|
391
|
-
[
|
|
392
|
-
{
|
|
393
|
-
config,
|
|
394
|
-
skipped: false,
|
|
395
|
-
runtimeConfigs: [config],
|
|
396
|
-
runtimeNames: ["api"],
|
|
397
|
-
runtimeKey: "api",
|
|
398
|
-
assignedGraphKey: "api",
|
|
399
|
-
suites: collectSuites(config, ["int"], [], []),
|
|
400
|
-
},
|
|
401
|
-
],
|
|
402
|
-
graphs,
|
|
403
|
-
{ files: {} }
|
|
404
|
-
);
|
|
405
|
-
|
|
406
|
-
expect(queue[0]).toMatchObject({
|
|
407
|
-
stackMode: "isolated",
|
|
408
|
-
accessMode: "exclusive",
|
|
302
|
+
expect(claimNextTask(queue, "api")).toMatchObject({
|
|
303
|
+
framework: "k6",
|
|
304
|
+
graphKey: "api",
|
|
409
305
|
});
|
|
410
306
|
});
|
|
411
307
|
});
|
|
@@ -3,8 +3,14 @@ import path from "path";
|
|
|
3
3
|
import { pathToFileURL } from "url";
|
|
4
4
|
import { normalizePathSeparators } from "./state.mjs";
|
|
5
5
|
|
|
6
|
-
export function ensurePlaywrightTestConfig(targetConfig, cwd, requestedFiles) {
|
|
7
|
-
|
|
6
|
+
export function ensurePlaywrightTestConfig(targetConfig, cwd, requestedFiles, lease) {
|
|
7
|
+
if (!lease?.leaseDir) {
|
|
8
|
+
throw new Error(
|
|
9
|
+
`Playwright task for service "${targetConfig.name}" requires a lease-scoped directory`
|
|
10
|
+
);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const stateDir = lease.leaseDir;
|
|
8
14
|
const outputDir = resolvePlaywrightOutputDir(stateDir);
|
|
9
15
|
fs.mkdirSync(stateDir, { recursive: true });
|
|
10
16
|
fs.mkdirSync(outputDir, { recursive: true });
|
|
@@ -24,9 +24,9 @@ function makeTempDir(prefix) {
|
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
describe("runner-playwright-config", () => {
|
|
27
|
-
it("uses a
|
|
27
|
+
it("uses a lease-local output directory under the lease dir", async () => {
|
|
28
28
|
const productDir = makeTempDir("testkit-playwright-product-");
|
|
29
|
-
const
|
|
29
|
+
const leaseDir = path.join(productDir, ".testkit", "leases", "lease-12");
|
|
30
30
|
const cwd = path.join(productDir, "frontend");
|
|
31
31
|
fs.mkdirSync(cwd, { recursive: true });
|
|
32
32
|
fs.writeFileSync(
|
|
@@ -35,13 +35,14 @@ describe("runner-playwright-config", () => {
|
|
|
35
35
|
);
|
|
36
36
|
|
|
37
37
|
const configPath = ensurePlaywrightTestConfig(
|
|
38
|
-
{ productDir, stateDir },
|
|
38
|
+
{ name: "frontend", productDir, stateDir: path.join(productDir, ".testkit", "frontend") },
|
|
39
39
|
cwd,
|
|
40
|
-
["frontend/__testkit__/homepage/homepage.pw.testkit.ts"]
|
|
40
|
+
["frontend/__testkit__/homepage/homepage.pw.testkit.ts"],
|
|
41
|
+
{ leaseDir }
|
|
41
42
|
);
|
|
42
43
|
const generated = await import(pathToFileURL(configPath).href + `?t=${Date.now()}`);
|
|
43
44
|
|
|
44
|
-
const expectedOutputDir = resolvePlaywrightOutputDir(
|
|
45
|
+
const expectedOutputDir = resolvePlaywrightOutputDir(leaseDir);
|
|
45
46
|
expect(generated.default.outputDir).toBe(expectedOutputDir);
|
|
46
47
|
expect(fs.existsSync(expectedOutputDir)).toBe(true);
|
|
47
48
|
expect(fs.readFileSync(configPath, "utf8")).toContain(
|
|
@@ -49,6 +50,20 @@ describe("runner-playwright-config", () => {
|
|
|
49
50
|
);
|
|
50
51
|
});
|
|
51
52
|
|
|
53
|
+
it("requires a lease-scoped directory", () => {
|
|
54
|
+
const productDir = makeTempDir("testkit-playwright-product-");
|
|
55
|
+
const cwd = path.join(productDir, "frontend");
|
|
56
|
+
fs.mkdirSync(cwd, { recursive: true });
|
|
57
|
+
|
|
58
|
+
expect(() =>
|
|
59
|
+
ensurePlaywrightTestConfig(
|
|
60
|
+
{ name: "frontend", productDir, stateDir: path.join(productDir, ".testkit", "frontend") },
|
|
61
|
+
cwd,
|
|
62
|
+
["frontend/__testkit__/homepage/homepage.pw.testkit.ts"]
|
|
63
|
+
)
|
|
64
|
+
).toThrow('Playwright task for service "frontend" requires a lease-scoped directory');
|
|
65
|
+
});
|
|
66
|
+
|
|
52
67
|
it("finds a supported playwright config file", () => {
|
|
53
68
|
const cwd = makeTempDir("testkit-playwright-cwd-");
|
|
54
69
|
fs.writeFileSync(path.join(cwd, "playwright.config.ts"), "export default {};\n");
|