@elench/testkit 0.1.16 → 0.1.18
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 +44 -19
- package/bin/testkit.mjs +1 -1
- package/lib/cli/args.mjs +57 -0
- package/lib/cli/args.test.mjs +62 -0
- package/lib/cli/index.mjs +88 -0
- package/lib/config/index.mjs +294 -0
- package/lib/config/index.test.mjs +12 -0
- package/lib/config/model.mjs +422 -0
- package/lib/config/model.test.mjs +193 -0
- package/lib/database/fingerprint.mjs +61 -0
- package/lib/database/fingerprint.test.mjs +93 -0
- package/lib/{database.mjs → database/index.mjs} +45 -160
- package/lib/database/naming.mjs +47 -0
- package/lib/database/naming.test.mjs +39 -0
- package/lib/database/state.mjs +52 -0
- package/lib/database/state.test.mjs +66 -0
- package/lib/reporters/playwright.mjs +125 -0
- package/lib/reporters/playwright.test.mjs +73 -0
- package/lib/runner/index.mjs +1221 -0
- package/lib/runner/metadata.mjs +55 -0
- package/lib/runner/metadata.test.mjs +52 -0
- package/lib/runner/planning.mjs +270 -0
- package/lib/runner/planning.test.mjs +127 -0
- package/lib/runner/results.mjs +285 -0
- package/lib/runner/results.test.mjs +144 -0
- package/lib/runner/state.mjs +71 -0
- package/lib/runner/state.test.mjs +64 -0
- package/lib/runner/template.mjs +320 -0
- package/lib/runner/template.test.mjs +150 -0
- package/lib/telemetry/index.mjs +43 -0
- package/lib/timing/index.mjs +73 -0
- package/lib/timing/index.test.mjs +64 -0
- package/package.json +11 -3
- package/infra/neon-down.sh +0 -18
- package/infra/neon-up.sh +0 -124
- package/lib/cli.mjs +0 -132
- package/lib/config.mjs +0 -666
- package/lib/exec.mjs +0 -20
- package/lib/runner.mjs +0 -1165
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import os from "os";
|
|
3
|
+
import path from "path";
|
|
4
|
+
import { execFileSync } from "child_process";
|
|
5
|
+
import { fileURLToPath } from "url";
|
|
6
|
+
|
|
7
|
+
export function collectGitMetadata(productDir) {
|
|
8
|
+
const read = (args) => {
|
|
9
|
+
try {
|
|
10
|
+
return execaSyncCompat("git", args, { cwd: productDir }).trim() || null;
|
|
11
|
+
} catch {
|
|
12
|
+
return null;
|
|
13
|
+
}
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
return {
|
|
17
|
+
branch: read(["rev-parse", "--abbrev-ref", "HEAD"]),
|
|
18
|
+
commitSha: read(["rev-parse", "HEAD"]),
|
|
19
|
+
repoRoot: read(["rev-parse", "--show-toplevel"]),
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function readPackageMetadata() {
|
|
24
|
+
try {
|
|
25
|
+
const thisFile = fileURLToPath(import.meta.url);
|
|
26
|
+
const packagePath = path.resolve(path.dirname(thisFile), "..", "..", "package.json");
|
|
27
|
+
return JSON.parse(fs.readFileSync(packagePath, "utf8"));
|
|
28
|
+
} catch {
|
|
29
|
+
return { version: "unknown" };
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function safeHostname() {
|
|
34
|
+
try {
|
|
35
|
+
return os.hostname();
|
|
36
|
+
} catch {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function safeUsername() {
|
|
42
|
+
try {
|
|
43
|
+
return os.userInfo().username;
|
|
44
|
+
} catch {
|
|
45
|
+
return process.env.USER || process.env.USERNAME || null;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function execaSyncCompat(command, args, options) {
|
|
50
|
+
return execFileSync(command, args, {
|
|
51
|
+
cwd: options?.cwd,
|
|
52
|
+
encoding: "utf8",
|
|
53
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
54
|
+
});
|
|
55
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import os from "os";
|
|
3
|
+
import path from "path";
|
|
4
|
+
import { execFileSync } from "child_process";
|
|
5
|
+
import { afterEach, describe, expect, it } from "vitest";
|
|
6
|
+
import {
|
|
7
|
+
collectGitMetadata,
|
|
8
|
+
readPackageMetadata,
|
|
9
|
+
safeHostname,
|
|
10
|
+
safeUsername,
|
|
11
|
+
} from "./metadata.mjs";
|
|
12
|
+
|
|
13
|
+
const tempDirs = [];
|
|
14
|
+
|
|
15
|
+
afterEach(() => {
|
|
16
|
+
for (const dir of tempDirs.splice(0)) {
|
|
17
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
18
|
+
}
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
function mkTempDir() {
|
|
22
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "testkit-runner-metadata-"));
|
|
23
|
+
tempDirs.push(dir);
|
|
24
|
+
return dir;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
describe("runner-metadata", () => {
|
|
28
|
+
it("reads package metadata and safe host/user info", () => {
|
|
29
|
+
const pkg = readPackageMetadata();
|
|
30
|
+
const packageJson = JSON.parse(
|
|
31
|
+
fs.readFileSync(path.resolve(process.cwd(), "package.json"), "utf8")
|
|
32
|
+
);
|
|
33
|
+
expect(pkg.version).toBe(packageJson.version);
|
|
34
|
+
expect(typeof safeHostname()).toBe("string");
|
|
35
|
+
expect(typeof safeUsername()).toBe("string");
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("collects git metadata from a real repo", () => {
|
|
39
|
+
const repoDir = mkTempDir();
|
|
40
|
+
execFileSync("git", ["init", "-b", "main"], { cwd: repoDir, stdio: "ignore" });
|
|
41
|
+
execFileSync("git", ["config", "user.email", "test@example.com"], { cwd: repoDir, stdio: "ignore" });
|
|
42
|
+
execFileSync("git", ["config", "user.name", "Test User"], { cwd: repoDir, stdio: "ignore" });
|
|
43
|
+
fs.writeFileSync(path.join(repoDir, "README.md"), "# test\n");
|
|
44
|
+
execFileSync("git", ["add", "README.md"], { cwd: repoDir, stdio: "ignore" });
|
|
45
|
+
execFileSync("git", ["commit", "-m", "init"], { cwd: repoDir, stdio: "ignore" });
|
|
46
|
+
|
|
47
|
+
const metadata = collectGitMetadata(repoDir);
|
|
48
|
+
expect(metadata.branch).toBe("main");
|
|
49
|
+
expect(metadata.commitSha).toMatch(/^[a-f0-9]{40}$/);
|
|
50
|
+
expect(metadata.repoRoot).toBe(repoDir);
|
|
51
|
+
});
|
|
52
|
+
});
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
import { buildTimingKey, estimateTaskDuration } from "../timing/index.mjs";
|
|
2
|
+
|
|
3
|
+
const TYPE_ORDER = ["dal", "integration", "e2e", "load"];
|
|
4
|
+
|
|
5
|
+
export function batchNeedsLocalRuntime(batch) {
|
|
6
|
+
return batch.tasks.some((task) => task.type !== "dal");
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function resolveRuntimeConfigs(targetConfig, configMap) {
|
|
10
|
+
const ordered = [];
|
|
11
|
+
const visiting = new Set();
|
|
12
|
+
const seen = new Set();
|
|
13
|
+
|
|
14
|
+
const visit = (config) => {
|
|
15
|
+
if (seen.has(config.name)) return;
|
|
16
|
+
if (visiting.has(config.name)) {
|
|
17
|
+
throw new Error(`Dependency cycle detected involving "${config.name}"`);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
visiting.add(config.name);
|
|
21
|
+
for (const depName of config.testkit.dependsOn || []) {
|
|
22
|
+
const dep = configMap.get(depName);
|
|
23
|
+
if (!dep) {
|
|
24
|
+
throw new Error(`Service "${config.name}" depends on unknown service "${depName}"`);
|
|
25
|
+
}
|
|
26
|
+
visit(dep);
|
|
27
|
+
}
|
|
28
|
+
visiting.delete(config.name);
|
|
29
|
+
seen.add(config.name);
|
|
30
|
+
ordered.push(config);
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
visit(targetConfig);
|
|
34
|
+
return ordered;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function collectSuites(config, suiteType, suiteNames, frameworkFilter) {
|
|
38
|
+
const types =
|
|
39
|
+
suiteType === "all"
|
|
40
|
+
? orderedTypes(Object.keys(config.suites))
|
|
41
|
+
: [suiteType === "int" ? "integration" : suiteType];
|
|
42
|
+
|
|
43
|
+
const selectedNames = new Set(suiteNames);
|
|
44
|
+
const suites = [];
|
|
45
|
+
let orderIndex = 0;
|
|
46
|
+
|
|
47
|
+
for (const type of types) {
|
|
48
|
+
for (const suite of config.suites[type] || []) {
|
|
49
|
+
const framework = suite.framework || "k6";
|
|
50
|
+
if (selectedNames.size > 0 && !selectedNames.has(suite.name)) continue;
|
|
51
|
+
if (frameworkFilter && frameworkFilter !== "all" && framework !== frameworkFilter) continue;
|
|
52
|
+
|
|
53
|
+
suites.push({
|
|
54
|
+
...suite,
|
|
55
|
+
framework,
|
|
56
|
+
type,
|
|
57
|
+
orderIndex,
|
|
58
|
+
sortKey: `${type}:${suite.name}`,
|
|
59
|
+
weight:
|
|
60
|
+
suite.testkit?.weight ||
|
|
61
|
+
(framework === "playwright"
|
|
62
|
+
? Math.max(2, suite.files.length)
|
|
63
|
+
: Math.max(1, suite.files.length)),
|
|
64
|
+
maxFileConcurrency:
|
|
65
|
+
framework === "k6" ? suite.testkit?.maxFileConcurrency || 1 : 1,
|
|
66
|
+
});
|
|
67
|
+
orderIndex += 1;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return suites;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function applyShard(suites, shard) {
|
|
75
|
+
if (!shard) return suites;
|
|
76
|
+
return suites.filter((unused, index) => index % shard.total === shard.index - 1);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function orderedTypes(types) {
|
|
80
|
+
const ordered = [];
|
|
81
|
+
for (const known of TYPE_ORDER) {
|
|
82
|
+
if (types.includes(known)) ordered.push(known);
|
|
83
|
+
}
|
|
84
|
+
for (const type of types) {
|
|
85
|
+
if (!ordered.includes(type)) ordered.push(type);
|
|
86
|
+
}
|
|
87
|
+
return ordered;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function buildRuntimeGraphs(servicePlans) {
|
|
91
|
+
const executed = servicePlans.filter((plan) => !plan.skipped);
|
|
92
|
+
const uniqueGraphs = [];
|
|
93
|
+
const graphByRuntimeKey = new Map();
|
|
94
|
+
|
|
95
|
+
for (const plan of executed) {
|
|
96
|
+
if (graphByRuntimeKey.has(plan.runtimeKey)) {
|
|
97
|
+
graphByRuntimeKey.get(plan.runtimeKey).exactTargets.push(plan.config.name);
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const graph = {
|
|
102
|
+
key: plan.runtimeKey,
|
|
103
|
+
runtimeNames: plan.runtimeNames,
|
|
104
|
+
runtimeConfigs: plan.runtimeConfigs,
|
|
105
|
+
exactTargets: [plan.config.name],
|
|
106
|
+
assignedTargets: [],
|
|
107
|
+
dirName: null,
|
|
108
|
+
rootConfig: null,
|
|
109
|
+
};
|
|
110
|
+
uniqueGraphs.push(graph);
|
|
111
|
+
graphByRuntimeKey.set(plan.runtimeKey, graph);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const maximalGraphs = uniqueGraphs.filter(
|
|
115
|
+
(graph) =>
|
|
116
|
+
!uniqueGraphs.some(
|
|
117
|
+
(other) =>
|
|
118
|
+
other.key !== graph.key &&
|
|
119
|
+
isRuntimeSuperset(other.runtimeNames, graph.runtimeNames)
|
|
120
|
+
)
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
for (const plan of executed) {
|
|
124
|
+
const compatible = maximalGraphs.filter((graph) =>
|
|
125
|
+
isRuntimeSuperset(graph.runtimeNames, plan.runtimeNames)
|
|
126
|
+
);
|
|
127
|
+
if (compatible.length === 0) {
|
|
128
|
+
throw new Error(`No runtime graph found for service "${plan.config.name}"`);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const assigned = compatible.sort(compareGraphsForAssignment)[0];
|
|
132
|
+
plan.assignedGraphKey = assigned.key;
|
|
133
|
+
assigned.assignedTargets.push(plan.config.name);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
for (const graph of maximalGraphs) {
|
|
137
|
+
const rootName = [...graph.exactTargets].sort()[0];
|
|
138
|
+
const rootPlan = executed.find((plan) => plan.config.name === rootName);
|
|
139
|
+
if (!rootPlan) {
|
|
140
|
+
throw new Error(`Missing root plan for graph "${graph.key}"`);
|
|
141
|
+
}
|
|
142
|
+
graph.rootConfig = rootPlan.config;
|
|
143
|
+
graph.dirName = buildGraphDirName(graph.runtimeNames);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return maximalGraphs.sort((a, b) => a.dirName.localeCompare(b.dirName));
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export function buildTaskQueue(servicePlans, graphs, timings) {
|
|
150
|
+
const graphByKey = new Map(graphs.map((graph) => [graph.key, graph]));
|
|
151
|
+
const tasks = [];
|
|
152
|
+
let nextId = 1;
|
|
153
|
+
|
|
154
|
+
for (const plan of servicePlans) {
|
|
155
|
+
if (plan.skipped) continue;
|
|
156
|
+
|
|
157
|
+
const graph = graphByKey.get(plan.assignedGraphKey);
|
|
158
|
+
if (!graph) {
|
|
159
|
+
throw new Error(`Assigned graph "${plan.assignedGraphKey}" not found for ${plan.config.name}`);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
for (const suite of plan.suites) {
|
|
163
|
+
for (const file of suite.files) {
|
|
164
|
+
const timingKey = buildTimingKey(plan.config.name, suite, file);
|
|
165
|
+
tasks.push({
|
|
166
|
+
id: nextId,
|
|
167
|
+
graphKey: graph.key,
|
|
168
|
+
targetName: plan.config.name,
|
|
169
|
+
serviceName: plan.config.name,
|
|
170
|
+
suiteKey: `${suite.type}:${suite.name}`,
|
|
171
|
+
suiteName: suite.name,
|
|
172
|
+
type: suite.type,
|
|
173
|
+
framework: suite.framework,
|
|
174
|
+
orderIndex: suite.orderIndex,
|
|
175
|
+
file,
|
|
176
|
+
timingKey,
|
|
177
|
+
estimatedDurationMs: estimateTaskDuration(timings, timingKey, suite),
|
|
178
|
+
maxBatchSize:
|
|
179
|
+
suite.framework === "playwright"
|
|
180
|
+
? Number.POSITIVE_INFINITY
|
|
181
|
+
: suite.maxFileConcurrency || 1,
|
|
182
|
+
});
|
|
183
|
+
nextId += 1;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
return tasks.sort(
|
|
189
|
+
(a, b) =>
|
|
190
|
+
b.estimatedDurationMs - a.estimatedDurationMs ||
|
|
191
|
+
a.serviceName.localeCompare(b.serviceName) ||
|
|
192
|
+
a.suiteKey.localeCompare(b.suiteKey) ||
|
|
193
|
+
a.file.localeCompare(b.file)
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
export function claimNextBatch(queue, preferredGraphKey) {
|
|
198
|
+
if (queue.length === 0) return null;
|
|
199
|
+
|
|
200
|
+
let index = -1;
|
|
201
|
+
if (preferredGraphKey) {
|
|
202
|
+
index = queue.findIndex((task) => task.graphKey === preferredGraphKey);
|
|
203
|
+
}
|
|
204
|
+
if (index === -1) index = 0;
|
|
205
|
+
|
|
206
|
+
const seed = queue.splice(index, 1)[0];
|
|
207
|
+
const tasks = [seed];
|
|
208
|
+
|
|
209
|
+
if (seed.framework === "playwright") {
|
|
210
|
+
for (let cursor = queue.length - 1; cursor >= 0; cursor -= 1) {
|
|
211
|
+
const candidate = queue[cursor];
|
|
212
|
+
if (
|
|
213
|
+
candidate.framework === "playwright" &&
|
|
214
|
+
candidate.graphKey === seed.graphKey &&
|
|
215
|
+
candidate.targetName === seed.targetName
|
|
216
|
+
) {
|
|
217
|
+
tasks.push(candidate);
|
|
218
|
+
queue.splice(cursor, 1);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
} else if (seed.maxBatchSize > 1) {
|
|
222
|
+
for (let cursor = queue.length - 1; cursor >= 0 && tasks.length < seed.maxBatchSize; cursor -= 1) {
|
|
223
|
+
const candidate = queue[cursor];
|
|
224
|
+
if (
|
|
225
|
+
candidate.framework === seed.framework &&
|
|
226
|
+
candidate.type === seed.type &&
|
|
227
|
+
candidate.graphKey === seed.graphKey &&
|
|
228
|
+
candidate.targetName === seed.targetName &&
|
|
229
|
+
candidate.suiteKey === seed.suiteKey
|
|
230
|
+
) {
|
|
231
|
+
tasks.push(candidate);
|
|
232
|
+
queue.splice(cursor, 1);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
tasks.sort(
|
|
238
|
+
(a, b) =>
|
|
239
|
+
a.orderIndex - b.orderIndex ||
|
|
240
|
+
a.file.localeCompare(b.file)
|
|
241
|
+
);
|
|
242
|
+
|
|
243
|
+
return {
|
|
244
|
+
graphKey: seed.graphKey,
|
|
245
|
+
targetName: seed.targetName,
|
|
246
|
+
framework: seed.framework,
|
|
247
|
+
type: seed.type,
|
|
248
|
+
tasks,
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
export function isRuntimeSuperset(candidate, target) {
|
|
253
|
+
return target.every((name) => candidate.includes(name));
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
export function compareGraphsForAssignment(left, right) {
|
|
257
|
+
if (left.runtimeNames.length !== right.runtimeNames.length) {
|
|
258
|
+
return left.runtimeNames.length - right.runtimeNames.length;
|
|
259
|
+
}
|
|
260
|
+
return left.key.localeCompare(right.key);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
export function buildGraphDirName(runtimeNames) {
|
|
264
|
+
const slug = runtimeNames.map(slugSegment).join("__");
|
|
265
|
+
return slug.length > 0 ? slug : "graph";
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function slugSegment(value) {
|
|
269
|
+
return value.toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "") || "svc";
|
|
270
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
applyShard,
|
|
4
|
+
buildGraphDirName,
|
|
5
|
+
buildRuntimeGraphs,
|
|
6
|
+
buildTaskQueue,
|
|
7
|
+
claimNextBatch,
|
|
8
|
+
collectSuites,
|
|
9
|
+
resolveRuntimeConfigs,
|
|
10
|
+
} from "./planning.mjs";
|
|
11
|
+
|
|
12
|
+
function makeConfig(name, extras = {}) {
|
|
13
|
+
return {
|
|
14
|
+
name,
|
|
15
|
+
suites: extras.suites || {},
|
|
16
|
+
testkit: {
|
|
17
|
+
dependsOn: extras.dependsOn || [],
|
|
18
|
+
},
|
|
19
|
+
...extras,
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
describe("runner-planning", () => {
|
|
24
|
+
it("orders runtime configs by dependency and detects cycles", () => {
|
|
25
|
+
const api = makeConfig("api");
|
|
26
|
+
const frontend = makeConfig("frontend", { dependsOn: ["api"] });
|
|
27
|
+
const configMap = new Map([
|
|
28
|
+
["api", api],
|
|
29
|
+
["frontend", frontend],
|
|
30
|
+
]);
|
|
31
|
+
|
|
32
|
+
expect(resolveRuntimeConfigs(frontend, configMap).map((config) => config.name)).toEqual([
|
|
33
|
+
"api",
|
|
34
|
+
"frontend",
|
|
35
|
+
]);
|
|
36
|
+
|
|
37
|
+
api.testkit.dependsOn = ["frontend"];
|
|
38
|
+
expect(() => resolveRuntimeConfigs(frontend, configMap)).toThrow("Dependency cycle");
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("collects suites with aliases, weights, and framework filters", () => {
|
|
42
|
+
const config = makeConfig("api", {
|
|
43
|
+
suites: {
|
|
44
|
+
integration: [
|
|
45
|
+
{ name: "health", files: ["health.js"] },
|
|
46
|
+
],
|
|
47
|
+
e2e: [
|
|
48
|
+
{ name: "auth", framework: "playwright", files: ["auth.spec.js", "signup.spec.js"] },
|
|
49
|
+
],
|
|
50
|
+
},
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
expect(collectSuites(config, "int", [], "all")[0]).toMatchObject({
|
|
54
|
+
name: "health",
|
|
55
|
+
type: "integration",
|
|
56
|
+
framework: "k6",
|
|
57
|
+
weight: 1,
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
expect(collectSuites(config, "all", [], "playwright")[0]).toMatchObject({
|
|
61
|
+
name: "auth",
|
|
62
|
+
framework: "playwright",
|
|
63
|
+
weight: 2,
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("applies shards, builds graphs, queues tasks, and claims batches", () => {
|
|
68
|
+
const api = makeConfig("api");
|
|
69
|
+
const frontend = makeConfig("frontend");
|
|
70
|
+
const plans = [
|
|
71
|
+
{
|
|
72
|
+
config: api,
|
|
73
|
+
skipped: false,
|
|
74
|
+
runtimeConfigs: [api],
|
|
75
|
+
runtimeNames: ["api"],
|
|
76
|
+
runtimeKey: "api",
|
|
77
|
+
suites: [
|
|
78
|
+
{
|
|
79
|
+
name: "health",
|
|
80
|
+
type: "integration",
|
|
81
|
+
framework: "k6",
|
|
82
|
+
files: ["a.js", "b.js"],
|
|
83
|
+
orderIndex: 0,
|
|
84
|
+
weight: 2,
|
|
85
|
+
maxFileConcurrency: 2,
|
|
86
|
+
},
|
|
87
|
+
],
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
config: frontend,
|
|
91
|
+
skipped: false,
|
|
92
|
+
runtimeConfigs: [api, frontend],
|
|
93
|
+
runtimeNames: ["api", "frontend"],
|
|
94
|
+
runtimeKey: "api|frontend",
|
|
95
|
+
suites: [
|
|
96
|
+
{
|
|
97
|
+
name: "auth",
|
|
98
|
+
type: "e2e",
|
|
99
|
+
framework: "playwright",
|
|
100
|
+
files: ["auth.spec.js", "signup.spec.js"],
|
|
101
|
+
orderIndex: 0,
|
|
102
|
+
weight: 2,
|
|
103
|
+
maxFileConcurrency: 1,
|
|
104
|
+
},
|
|
105
|
+
],
|
|
106
|
+
},
|
|
107
|
+
];
|
|
108
|
+
|
|
109
|
+
expect(applyShard(["a", "b", "c", "d"], { index: 2, total: 2 })).toEqual(["b", "d"]);
|
|
110
|
+
|
|
111
|
+
const graphs = buildRuntimeGraphs(plans);
|
|
112
|
+
expect(graphs).toHaveLength(1);
|
|
113
|
+
expect(plans[0].assignedGraphKey).toBe("api|frontend");
|
|
114
|
+
expect(buildGraphDirName(["api", "frontend"])).toBe("api__frontend");
|
|
115
|
+
|
|
116
|
+
const queue = buildTaskQueue(plans, graphs, { files: {} });
|
|
117
|
+
expect(queue).toHaveLength(4);
|
|
118
|
+
|
|
119
|
+
const firstBatch = claimNextBatch(queue, "api|frontend");
|
|
120
|
+
expect(firstBatch.tasks).toHaveLength(2);
|
|
121
|
+
expect(firstBatch.framework).toBe("playwright");
|
|
122
|
+
|
|
123
|
+
const secondBatch = claimNextBatch(queue, "api|frontend");
|
|
124
|
+
expect(secondBatch.tasks).toHaveLength(2);
|
|
125
|
+
expect(secondBatch.framework).toBe("k6");
|
|
126
|
+
});
|
|
127
|
+
});
|