@elench/testkit 0.1.37 → 0.1.39
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 +12 -3
- package/lib/cli/args.mjs +6 -7
- package/lib/cli/args.test.mjs +9 -4
- package/lib/cli/index.mjs +15 -4
- package/lib/config/index.mjs +126 -2
- package/lib/runner/default-runtime-runner.mjs +4 -4
- package/lib/runner/execution-config.mjs +108 -0
- package/lib/runner/execution-config.test.mjs +101 -0
- package/lib/runner/lifecycle.mjs +7 -7
- package/lib/runner/orchestrator.mjs +51 -24
- package/lib/runner/planning.mjs +42 -2
- package/lib/runner/planning.test.mjs +175 -3
- package/lib/runner/playwright-config.test.mjs +1 -1
- package/lib/runner/playwright-runner.mjs +2 -2
- package/lib/runner/readiness.mjs +2 -2
- package/lib/runner/reporting.mjs +5 -2
- package/lib/runner/reporting.test.mjs +12 -1
- package/lib/runner/runtime-contexts.mjs +38 -47
- package/lib/runner/services.mjs +4 -4
- package/lib/runner/stack-manager.mjs +146 -0
- package/lib/runner/state-io.mjs +23 -0
- package/lib/runner/template.mjs +107 -39
- package/lib/runner/template.test.mjs +68 -41
- package/lib/runner/worker-loop.mjs +17 -14
- package/lib/setup/index.d.ts +25 -1
- package/package.json +1 -1
|
@@ -35,6 +35,8 @@ import {
|
|
|
35
35
|
safeHostname,
|
|
36
36
|
safeUsername,
|
|
37
37
|
} from "./metadata.mjs";
|
|
38
|
+
import { resolveExecutionConfig } from "./execution-config.mjs";
|
|
39
|
+
import { createStackManager } from "./stack-manager.mjs";
|
|
38
40
|
import { createWorker, runWorker } from "./worker-loop.mjs";
|
|
39
41
|
import { findUnmatchedRequestedFiles, isFullRunSelection } from "./selection.mjs";
|
|
40
42
|
import { uploadTelemetryArtifact } from "../telemetry/index.mjs";
|
|
@@ -88,10 +90,22 @@ export async function runAll(configs, typeValues, suiteSelectors, opts, allConfi
|
|
|
88
90
|
);
|
|
89
91
|
}
|
|
90
92
|
|
|
91
|
-
const
|
|
93
|
+
const execution = resolveExecutionConfig({
|
|
94
|
+
cli: opts,
|
|
95
|
+
repo: configs[0]?.testkit?.execution || null,
|
|
96
|
+
});
|
|
97
|
+
const servicePlans = collectServicePlans(
|
|
98
|
+
configs,
|
|
99
|
+
configMap,
|
|
100
|
+
typeValues,
|
|
101
|
+
suiteSelectors,
|
|
102
|
+
opts,
|
|
103
|
+
execution
|
|
104
|
+
);
|
|
92
105
|
const trackers = buildServiceTrackers(servicePlans, startedAt);
|
|
93
106
|
const executedPlans = servicePlans.filter((plan) => !plan.skipped);
|
|
94
107
|
let workerCount = 0;
|
|
108
|
+
let stackCount = 0;
|
|
95
109
|
let exitCode = 0;
|
|
96
110
|
const lifecycle = createRunLifecycle(productDir);
|
|
97
111
|
lifecycle.markRunning();
|
|
@@ -104,36 +118,46 @@ export async function runAll(configs, typeValues, suiteSelectors, opts, allConfi
|
|
|
104
118
|
const timings = loadTimings(productDir);
|
|
105
119
|
const graphs = buildRuntimeGraphs(executedPlans);
|
|
106
120
|
const queue = buildTaskQueue(executedPlans, graphs, timings);
|
|
107
|
-
workerCount = Math.max(1, Math.min(
|
|
108
|
-
|
|
121
|
+
workerCount = Math.max(1, Math.min(execution.workers, queue.length));
|
|
122
|
+
stackCount = execution.stackMode === "shared" ? 1 : execution.stackCount;
|
|
109
123
|
const workers = Array.from({ length: workerCount }, (_unused, index) =>
|
|
110
124
|
createWorker(index + 1, productDir)
|
|
111
125
|
);
|
|
126
|
+
const stackManager = createStackManager({
|
|
127
|
+
productDir,
|
|
128
|
+
graphs,
|
|
129
|
+
execution,
|
|
130
|
+
lifecycle,
|
|
131
|
+
});
|
|
112
132
|
const timingUpdates = [];
|
|
113
133
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
134
|
+
try {
|
|
135
|
+
const workerResults = await Promise.allSettled(
|
|
136
|
+
workers.map((worker) =>
|
|
137
|
+
runWorker(
|
|
138
|
+
worker,
|
|
139
|
+
queue,
|
|
140
|
+
stackManager,
|
|
141
|
+
trackers,
|
|
142
|
+
timingUpdates,
|
|
143
|
+
lifecycle,
|
|
144
|
+
claimNextBatch,
|
|
145
|
+
recordTaskOutcome,
|
|
146
|
+
recordGraphError
|
|
147
|
+
)
|
|
126
148
|
)
|
|
127
|
-
)
|
|
128
|
-
);
|
|
149
|
+
);
|
|
129
150
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
151
|
+
for (const result of workerResults) {
|
|
152
|
+
if (result.status === "rejected") {
|
|
153
|
+
const message = formatError(result.reason);
|
|
154
|
+
for (const tracker of trackers.values()) {
|
|
155
|
+
if (!tracker.skipped) addTrackerError(tracker, message);
|
|
156
|
+
}
|
|
135
157
|
}
|
|
136
158
|
}
|
|
159
|
+
} finally {
|
|
160
|
+
await stackManager.cleanupAll();
|
|
137
161
|
}
|
|
138
162
|
|
|
139
163
|
saveTimings(productDir, timings, timingUpdates);
|
|
@@ -148,8 +172,9 @@ export async function runAll(configs, typeValues, suiteSelectors, opts, allConfi
|
|
|
148
172
|
results,
|
|
149
173
|
startedAt,
|
|
150
174
|
finishedAt,
|
|
151
|
-
|
|
175
|
+
execution,
|
|
152
176
|
workerCount,
|
|
177
|
+
stackCount,
|
|
153
178
|
typeValues,
|
|
154
179
|
suiteSelectors,
|
|
155
180
|
fileNames: requestedFiles,
|
|
@@ -192,7 +217,7 @@ export async function runAll(configs, typeValues, suiteSelectors, opts, allConfi
|
|
|
192
217
|
}
|
|
193
218
|
}
|
|
194
219
|
|
|
195
|
-
function collectServicePlans(configs, configMap, typeValues, suiteSelectors, opts) {
|
|
220
|
+
function collectServicePlans(configs, configMap, typeValues, suiteSelectors, opts, execution) {
|
|
196
221
|
return configs.map((config) => {
|
|
197
222
|
console.log(`\n══ ${config.name} ══`);
|
|
198
223
|
const suites = applyShard(
|
|
@@ -206,6 +231,7 @@ function collectServicePlans(configs, configMap, typeValues, suiteSelectors, opt
|
|
|
206
231
|
);
|
|
207
232
|
return {
|
|
208
233
|
config,
|
|
234
|
+
execution,
|
|
209
235
|
skipped: true,
|
|
210
236
|
suites: [],
|
|
211
237
|
runtimeConfigs: [],
|
|
@@ -217,6 +243,7 @@ function collectServicePlans(configs, configMap, typeValues, suiteSelectors, opt
|
|
|
217
243
|
const runtimeConfigs = resolveRuntimeConfigs(config, configMap);
|
|
218
244
|
return {
|
|
219
245
|
config,
|
|
246
|
+
execution,
|
|
220
247
|
skipped: false,
|
|
221
248
|
suites,
|
|
222
249
|
runtimeConfigs,
|
package/lib/runner/planning.mjs
CHANGED
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
matchesSuiteSelectors,
|
|
5
5
|
suiteSelectionType,
|
|
6
6
|
} from "./suite-selection.mjs";
|
|
7
|
+
import { resolveBatchAccessMode, resolveBatchStackMode } from "./execution-config.mjs";
|
|
7
8
|
|
|
8
9
|
const TYPE_ORDER = ["dal", "integration", "e2e", "load"];
|
|
9
10
|
|
|
@@ -63,7 +64,6 @@ export function collectSuites(config, typeValues, suiteSelectors, fileNames = []
|
|
|
63
64
|
selectedSuiteFiles,
|
|
64
65
|
opts
|
|
65
66
|
);
|
|
66
|
-
|
|
67
67
|
suites.push({
|
|
68
68
|
...suite,
|
|
69
69
|
files,
|
|
@@ -181,6 +181,7 @@ export function buildTaskQueue(servicePlans, graphs, timings) {
|
|
|
181
181
|
|
|
182
182
|
for (const suite of plan.suites) {
|
|
183
183
|
for (const file of suite.files) {
|
|
184
|
+
const stackMode = resolveTaskStackMode(plan.config, plan.execution, suite, file);
|
|
184
185
|
const timingKey = buildTimingKey(plan.config.name, suite, file);
|
|
185
186
|
tasks.push({
|
|
186
187
|
id: nextId,
|
|
@@ -191,6 +192,12 @@ export function buildTaskQueue(servicePlans, graphs, timings) {
|
|
|
191
192
|
suiteName: suite.name,
|
|
192
193
|
type: suite.type,
|
|
193
194
|
framework: suite.framework,
|
|
195
|
+
stackMode,
|
|
196
|
+
accessMode: resolveBatchAccessMode({
|
|
197
|
+
framework: suite.framework,
|
|
198
|
+
type: suite.type,
|
|
199
|
+
stackMode,
|
|
200
|
+
}),
|
|
194
201
|
orderIndex: suite.orderIndex,
|
|
195
202
|
file,
|
|
196
203
|
timingKey,
|
|
@@ -233,7 +240,9 @@ export function claimNextBatch(queue, preferredGraphKey) {
|
|
|
233
240
|
candidate.type === seed.type &&
|
|
234
241
|
candidate.graphKey === seed.graphKey &&
|
|
235
242
|
candidate.targetName === seed.targetName &&
|
|
236
|
-
candidate.suiteKey === seed.suiteKey
|
|
243
|
+
candidate.suiteKey === seed.suiteKey &&
|
|
244
|
+
candidate.stackMode === seed.stackMode &&
|
|
245
|
+
candidate.accessMode === seed.accessMode
|
|
237
246
|
) {
|
|
238
247
|
tasks.push(candidate);
|
|
239
248
|
queue.splice(cursor, 1);
|
|
@@ -253,6 +262,8 @@ export function claimNextBatch(queue, preferredGraphKey) {
|
|
|
253
262
|
targetName: seed.targetName,
|
|
254
263
|
framework: seed.framework,
|
|
255
264
|
type: seed.type,
|
|
265
|
+
stackMode: seed.stackMode,
|
|
266
|
+
accessMode: seed.accessMode,
|
|
256
267
|
tasks,
|
|
257
268
|
};
|
|
258
269
|
}
|
|
@@ -313,6 +324,35 @@ function applySkipRules(config, displayType, suiteName, files, opts = {}) {
|
|
|
313
324
|
};
|
|
314
325
|
}
|
|
315
326
|
|
|
327
|
+
function resolveSuiteStackMode(config, displayType, suiteName) {
|
|
328
|
+
const defaultStackMode = config.testkit.execution.stackMode;
|
|
329
|
+
const rules = config.testkit.serviceExecution?.suites || [];
|
|
330
|
+
const matchedRule = rules.find((rule) =>
|
|
331
|
+
matchesSuiteSelectors(displayType, suiteName, [rule.selector])
|
|
332
|
+
);
|
|
333
|
+
return resolveBatchStackMode(defaultStackMode, matchedRule?.stackMode || null);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
function resolveTaskStackMode(config, execution, suite, file) {
|
|
337
|
+
const effectiveExecution = execution || config.testkit.execution;
|
|
338
|
+
const normalizedFile = normalizePathSeparators(file);
|
|
339
|
+
const fileOverride = config.testkit.serviceExecution?.fileStackModeByPath?.get(normalizedFile);
|
|
340
|
+
if (fileOverride) {
|
|
341
|
+
return resolveBatchStackMode(effectiveExecution.stackMode, fileOverride);
|
|
342
|
+
}
|
|
343
|
+
return resolveSuiteStackMode(
|
|
344
|
+
{
|
|
345
|
+
...config,
|
|
346
|
+
testkit: {
|
|
347
|
+
...config.testkit,
|
|
348
|
+
execution: effectiveExecution,
|
|
349
|
+
},
|
|
350
|
+
},
|
|
351
|
+
suite.displayType,
|
|
352
|
+
suite.name
|
|
353
|
+
);
|
|
354
|
+
}
|
|
355
|
+
|
|
316
356
|
export function buildGraphDirName(runtimeNames) {
|
|
317
357
|
const slug = runtimeNames.map(slugSegment).join("__");
|
|
318
358
|
return slug.length > 0 ? slug : "graph";
|
|
@@ -10,12 +10,24 @@ import {
|
|
|
10
10
|
} from "./planning.mjs";
|
|
11
11
|
|
|
12
12
|
function makeConfig(name, extras = {}) {
|
|
13
|
+
const providedTestkit = extras.testkit || {};
|
|
13
14
|
return {
|
|
14
15
|
name,
|
|
15
16
|
suites: extras.suites || {},
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
17
|
+
testkit: {
|
|
18
|
+
dependsOn: extras.dependsOn || providedTestkit.dependsOn || [],
|
|
19
|
+
execution: providedTestkit.execution || {
|
|
20
|
+
workers: 1,
|
|
21
|
+
stackMode: "isolated",
|
|
22
|
+
stackCount: 1,
|
|
23
|
+
},
|
|
24
|
+
serviceExecution: providedTestkit.serviceExecution || {
|
|
25
|
+
suites: [],
|
|
26
|
+
files: [],
|
|
27
|
+
fileStackModeByPath: new Map(),
|
|
28
|
+
},
|
|
29
|
+
...providedTestkit,
|
|
30
|
+
},
|
|
19
31
|
...extras,
|
|
20
32
|
};
|
|
21
33
|
}
|
|
@@ -55,6 +67,17 @@ describe("runner-planning", () => {
|
|
|
55
67
|
},
|
|
56
68
|
],
|
|
57
69
|
},
|
|
70
|
+
testkit: {
|
|
71
|
+
dependsOn: [],
|
|
72
|
+
execution: {
|
|
73
|
+
workers: 1,
|
|
74
|
+
stackMode: "shared",
|
|
75
|
+
stackCount: 1,
|
|
76
|
+
},
|
|
77
|
+
serviceExecution: {
|
|
78
|
+
suites: [],
|
|
79
|
+
},
|
|
80
|
+
},
|
|
58
81
|
});
|
|
59
82
|
|
|
60
83
|
expect(collectSuites(config, ["int"], [], [])[0]).toMatchObject({
|
|
@@ -100,6 +123,14 @@ describe("runner-planning", () => {
|
|
|
100
123
|
},
|
|
101
124
|
testkit: {
|
|
102
125
|
dependsOn: [],
|
|
126
|
+
execution: {
|
|
127
|
+
workers: 1,
|
|
128
|
+
stackMode: "shared",
|
|
129
|
+
stackCount: 1,
|
|
130
|
+
},
|
|
131
|
+
serviceExecution: {
|
|
132
|
+
suites: [],
|
|
133
|
+
},
|
|
103
134
|
skip: {
|
|
104
135
|
fileReasonByPath: new Map([
|
|
105
136
|
["__testkit__/billing/a.int.testkit.ts", "Billing is stubbed"],
|
|
@@ -136,6 +167,90 @@ describe("runner-planning", () => {
|
|
|
136
167
|
]);
|
|
137
168
|
});
|
|
138
169
|
|
|
170
|
+
it("applies file-level stack isolation overrides within the same suite", () => {
|
|
171
|
+
const api = makeConfig("api", {
|
|
172
|
+
suites: {
|
|
173
|
+
integration: [
|
|
174
|
+
{
|
|
175
|
+
name: "routes",
|
|
176
|
+
framework: "k6",
|
|
177
|
+
files: [
|
|
178
|
+
"src/api/routes/__testkit__/health.int.testkit.ts",
|
|
179
|
+
"src/api/routes/__testkit__/crawls-worker-concurrency.int.testkit.ts",
|
|
180
|
+
],
|
|
181
|
+
orderIndex: 0,
|
|
182
|
+
weight: 2,
|
|
183
|
+
maxFileConcurrency: 1,
|
|
184
|
+
},
|
|
185
|
+
],
|
|
186
|
+
},
|
|
187
|
+
testkit: {
|
|
188
|
+
execution: {
|
|
189
|
+
workers: 8,
|
|
190
|
+
stackMode: "shared",
|
|
191
|
+
stackCount: 1,
|
|
192
|
+
},
|
|
193
|
+
serviceExecution: {
|
|
194
|
+
suites: [],
|
|
195
|
+
files: [
|
|
196
|
+
{
|
|
197
|
+
path: "src/api/routes/__testkit__/crawls-worker-concurrency.int.testkit.ts",
|
|
198
|
+
stackMode: "isolated",
|
|
199
|
+
},
|
|
200
|
+
],
|
|
201
|
+
fileStackModeByPath: new Map([
|
|
202
|
+
[
|
|
203
|
+
"src/api/routes/__testkit__/crawls-worker-concurrency.int.testkit.ts",
|
|
204
|
+
"isolated",
|
|
205
|
+
],
|
|
206
|
+
]),
|
|
207
|
+
},
|
|
208
|
+
},
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
const graphs = buildRuntimeGraphs([
|
|
212
|
+
{
|
|
213
|
+
config: api,
|
|
214
|
+
skipped: false,
|
|
215
|
+
runtimeConfigs: [api],
|
|
216
|
+
runtimeNames: ["api"],
|
|
217
|
+
runtimeKey: "api",
|
|
218
|
+
suites: collectSuites(api, ["int"], [], []),
|
|
219
|
+
},
|
|
220
|
+
]);
|
|
221
|
+
|
|
222
|
+
const queue = buildTaskQueue(
|
|
223
|
+
[
|
|
224
|
+
{
|
|
225
|
+
config: api,
|
|
226
|
+
skipped: false,
|
|
227
|
+
runtimeConfigs: [api],
|
|
228
|
+
runtimeNames: ["api"],
|
|
229
|
+
runtimeKey: "api",
|
|
230
|
+
assignedGraphKey: "api",
|
|
231
|
+
suites: collectSuites(api, ["int"], [], []),
|
|
232
|
+
},
|
|
233
|
+
],
|
|
234
|
+
graphs,
|
|
235
|
+
{ files: {} }
|
|
236
|
+
);
|
|
237
|
+
|
|
238
|
+
expect(queue).toEqual(
|
|
239
|
+
expect.arrayContaining([
|
|
240
|
+
expect.objectContaining({
|
|
241
|
+
file: "src/api/routes/__testkit__/health.int.testkit.ts",
|
|
242
|
+
stackMode: "shared",
|
|
243
|
+
accessMode: "shared",
|
|
244
|
+
}),
|
|
245
|
+
expect.objectContaining({
|
|
246
|
+
file: "src/api/routes/__testkit__/crawls-worker-concurrency.int.testkit.ts",
|
|
247
|
+
stackMode: "isolated",
|
|
248
|
+
accessMode: "exclusive",
|
|
249
|
+
}),
|
|
250
|
+
])
|
|
251
|
+
);
|
|
252
|
+
});
|
|
253
|
+
|
|
139
254
|
it("applies shards, builds graphs, queues tasks, and claims batches", () => {
|
|
140
255
|
const api = makeConfig("api");
|
|
141
256
|
const frontend = makeConfig("frontend");
|
|
@@ -155,6 +270,8 @@ describe("runner-planning", () => {
|
|
|
155
270
|
orderIndex: 0,
|
|
156
271
|
weight: 2,
|
|
157
272
|
maxFileConcurrency: 2,
|
|
273
|
+
stackMode: "isolated",
|
|
274
|
+
accessMode: "exclusive",
|
|
158
275
|
},
|
|
159
276
|
],
|
|
160
277
|
},
|
|
@@ -173,6 +290,8 @@ describe("runner-planning", () => {
|
|
|
173
290
|
orderIndex: 0,
|
|
174
291
|
weight: 2,
|
|
175
292
|
maxFileConcurrency: 1,
|
|
293
|
+
stackMode: "isolated",
|
|
294
|
+
accessMode: "exclusive",
|
|
176
295
|
},
|
|
177
296
|
],
|
|
178
297
|
},
|
|
@@ -191,6 +310,7 @@ describe("runner-planning", () => {
|
|
|
191
310
|
const firstBatch = claimNextBatch(queue, "api|frontend");
|
|
192
311
|
expect(firstBatch.tasks).toHaveLength(1);
|
|
193
312
|
expect(firstBatch.framework).toBe("playwright");
|
|
313
|
+
expect(firstBatch.accessMode).toBe("exclusive");
|
|
194
314
|
|
|
195
315
|
const secondBatch = claimNextBatch(queue, "api|frontend");
|
|
196
316
|
expect(secondBatch.tasks).toHaveLength(1);
|
|
@@ -219,6 +339,8 @@ describe("runner-planning", () => {
|
|
|
219
339
|
orderIndex: 0,
|
|
220
340
|
weight: 3,
|
|
221
341
|
maxFileConcurrency: 2,
|
|
342
|
+
stackMode: "isolated",
|
|
343
|
+
accessMode: "exclusive",
|
|
222
344
|
},
|
|
223
345
|
],
|
|
224
346
|
},
|
|
@@ -236,4 +358,54 @@ describe("runner-planning", () => {
|
|
|
236
358
|
expect(secondBatch.tasks).toHaveLength(1);
|
|
237
359
|
expect(secondBatch.framework).toBe("playwright");
|
|
238
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
|
+
},
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
const graphs = buildRuntimeGraphs([
|
|
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",
|
|
409
|
+
});
|
|
410
|
+
});
|
|
239
411
|
});
|
|
@@ -26,7 +26,7 @@ function makeTempDir(prefix) {
|
|
|
26
26
|
describe("runner-playwright-config", () => {
|
|
27
27
|
it("uses a shard-local output directory under the state dir", async () => {
|
|
28
28
|
const productDir = makeTempDir("testkit-playwright-product-");
|
|
29
|
-
const stateDir = path.join(productDir, ".testkit", "
|
|
29
|
+
const stateDir = path.join(productDir, ".testkit", "stack-3");
|
|
30
30
|
const cwd = path.join(productDir, "frontend");
|
|
31
31
|
fs.mkdirSync(cwd, { recursive: true });
|
|
32
32
|
fs.writeFileSync(
|
|
@@ -19,7 +19,7 @@ export async function runPlaywrightBatch(targetConfig, batch, lifecycle) {
|
|
|
19
19
|
}
|
|
20
20
|
|
|
21
21
|
console.log(
|
|
22
|
-
`\n── ${targetConfig.
|
|
22
|
+
`\n── ${targetConfig.stackLabel} playwright:${targetConfig.name} (${batch.tasks.length} file${batch.tasks.length === 1 ? "" : "s"})${formatPlaywrightBatchFiles(batch)} ──`
|
|
23
23
|
);
|
|
24
24
|
|
|
25
25
|
const cwd = resolveServiceCwd(targetConfig.productDir, local.cwd);
|
|
@@ -41,7 +41,7 @@ export async function runPlaywrightBatch(targetConfig, batch, lifecycle) {
|
|
|
41
41
|
);
|
|
42
42
|
|
|
43
43
|
if (result.stderr) {
|
|
44
|
-
printBufferedOutput(result.stderr, `[${targetConfig.
|
|
44
|
+
printBufferedOutput(result.stderr, `[${targetConfig.stackLabel}:${targetConfig.name}:playwright]`);
|
|
45
45
|
}
|
|
46
46
|
|
|
47
47
|
const parsed = parsePlaywrightJsonResults(result.stdout, cwd);
|
package/lib/runner/readiness.mjs
CHANGED
|
@@ -54,11 +54,11 @@ export async function assertLocalServicePortsAvailable(config, isPortInUse) {
|
|
|
54
54
|
const owner = findPortOwner(config.productDir, socket);
|
|
55
55
|
const ownerDetail = owner
|
|
56
56
|
? owner.active
|
|
57
|
-
? ` Active testkit run ${formatRunSummary(owner.manifest)} owns ${key} via ${owner.service.
|
|
57
|
+
? ` Active testkit run ${formatRunSummary(owner.manifest)} owns ${key} via ${owner.service.stackLabel}:${owner.service.serviceName}.`
|
|
58
58
|
: ` Stale testkit run ${formatRunSummary(owner.manifest)} owns ${key}.`
|
|
59
59
|
: "";
|
|
60
60
|
throw new Error(
|
|
61
|
-
`Cannot start "${config.
|
|
61
|
+
`Cannot start "${config.stackLabel}:${config.name}" because ${key} is already in use. ` +
|
|
62
62
|
`Stop the existing process and rerun testkit.${ownerDetail}`
|
|
63
63
|
);
|
|
64
64
|
}
|
package/lib/runner/reporting.mjs
CHANGED
|
@@ -94,8 +94,9 @@ export function buildRunArtifact({
|
|
|
94
94
|
results,
|
|
95
95
|
startedAt,
|
|
96
96
|
finishedAt,
|
|
97
|
-
|
|
97
|
+
execution,
|
|
98
98
|
workerCount,
|
|
99
|
+
stackCount,
|
|
99
100
|
typeValues,
|
|
100
101
|
suiteSelectors,
|
|
101
102
|
fileNames,
|
|
@@ -133,8 +134,10 @@ export function buildRunArtifact({
|
|
|
133
134
|
startedAt: new Date(startedAt).toISOString(),
|
|
134
135
|
finishedAt: new Date(finishedAt).toISOString(),
|
|
135
136
|
durationMs: finishedAt - startedAt,
|
|
136
|
-
|
|
137
|
+
workers: execution.workers,
|
|
137
138
|
workerCount,
|
|
139
|
+
stackMode: execution.stackMode,
|
|
140
|
+
stackCount,
|
|
138
141
|
dbBackend,
|
|
139
142
|
types: typeValues,
|
|
140
143
|
suiteSelectors: suiteSelectors.map((selector) => selector.raw),
|
|
@@ -51,8 +51,13 @@ describe("runner reporting", () => {
|
|
|
51
51
|
results,
|
|
52
52
|
startedAt: 1000,
|
|
53
53
|
finishedAt: 4000,
|
|
54
|
-
|
|
54
|
+
execution: {
|
|
55
|
+
workers: 2,
|
|
56
|
+
stackMode: "shared",
|
|
57
|
+
stackCount: 1,
|
|
58
|
+
},
|
|
55
59
|
workerCount: 1,
|
|
60
|
+
stackCount: 1,
|
|
56
61
|
typeValues: ["all"],
|
|
57
62
|
suiteSelectors: [],
|
|
58
63
|
fileNames: [],
|
|
@@ -75,6 +80,12 @@ describe("runner reporting", () => {
|
|
|
75
80
|
|
|
76
81
|
expect(artifact.product.name).toBe("my-product");
|
|
77
82
|
expect(artifact.schemaVersion).toBe(3);
|
|
83
|
+
expect(artifact.run).toMatchObject({
|
|
84
|
+
workers: 2,
|
|
85
|
+
workerCount: 1,
|
|
86
|
+
stackMode: "shared",
|
|
87
|
+
stackCount: 1,
|
|
88
|
+
});
|
|
78
89
|
expect(artifact.summary.services).toEqual({
|
|
79
90
|
total: 1,
|
|
80
91
|
passed: 1,
|