@elench/testkit 0.1.29 → 0.1.31
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/lib/runner/index.mjs +20 -3
- package/lib/runner/planning.mjs +8 -18
- package/lib/runner/planning.test.mjs +43 -3
- package/lib/runner/results.mjs +28 -10
- package/lib/runner/results.test.mjs +121 -0
- package/package.json +1 -1
package/lib/runner/index.mjs
CHANGED
|
@@ -631,7 +631,7 @@ async function runPlaywrightBatch(targetConfig, batch) {
|
|
|
631
631
|
}
|
|
632
632
|
|
|
633
633
|
console.log(
|
|
634
|
-
`\n── ${targetConfig.workerLabel} playwright:${targetConfig.name} (${batch.tasks.length} file${batch.tasks.length === 1 ? "" : "s"}) ──`
|
|
634
|
+
`\n── ${targetConfig.workerLabel} playwright:${targetConfig.name} (${batch.tasks.length} file${batch.tasks.length === 1 ? "" : "s"})${formatPlaywrightBatchFiles(batch)} ──`
|
|
635
635
|
);
|
|
636
636
|
|
|
637
637
|
const cwd = resolveServiceCwd(targetConfig.productDir, local.cwd);
|
|
@@ -655,7 +655,8 @@ async function runPlaywrightBatch(targetConfig, batch) {
|
|
|
655
655
|
}
|
|
656
656
|
|
|
657
657
|
const parsed = parsePlaywrightJsonResults(result.stdout, cwd);
|
|
658
|
-
const
|
|
658
|
+
const finishedAt = Date.now();
|
|
659
|
+
const batchDurationMs = finishedAt - startedAt;
|
|
659
660
|
const genericError =
|
|
660
661
|
result.exitCode === 0
|
|
661
662
|
? parsed.errors[0] || null
|
|
@@ -677,6 +678,8 @@ async function runPlaywrightBatch(targetConfig, batch) {
|
|
|
677
678
|
fileResult.durationMs > 0
|
|
678
679
|
? fileResult.durationMs
|
|
679
680
|
: Math.round(batchDurationMs / Math.max(1, batch.tasks.length)),
|
|
681
|
+
startedAt,
|
|
682
|
+
finishedAt,
|
|
680
683
|
};
|
|
681
684
|
}
|
|
682
685
|
|
|
@@ -685,6 +688,8 @@ async function runPlaywrightBatch(targetConfig, batch) {
|
|
|
685
688
|
failed: result.exitCode !== 0,
|
|
686
689
|
error: result.exitCode !== 0 ? genericError : null,
|
|
687
690
|
durationMs: Math.round(batchDurationMs / Math.max(1, batch.tasks.length)),
|
|
691
|
+
startedAt,
|
|
692
|
+
finishedAt,
|
|
688
693
|
};
|
|
689
694
|
});
|
|
690
695
|
}
|
|
@@ -912,6 +917,15 @@ function formatBatchDescriptor(batch) {
|
|
|
912
917
|
return frameworkLabel ? ` (${frameworkLabel}, ${fileLabel})` : ` (${fileLabel})`;
|
|
913
918
|
}
|
|
914
919
|
|
|
920
|
+
function formatPlaywrightBatchFiles(batch) {
|
|
921
|
+
if (!batch?.tasks?.length) return "";
|
|
922
|
+
const files = batch.tasks.map((task) => task.file);
|
|
923
|
+
if (files.length === 1) return ` · ${files[0]}`;
|
|
924
|
+
const preview = files.slice(0, 3).join(", ");
|
|
925
|
+
const suffix = files.length > 3 ? `, +${files.length - 3} more` : "";
|
|
926
|
+
return ` · ${preview}${suffix}`;
|
|
927
|
+
}
|
|
928
|
+
|
|
915
929
|
function formatFrameworkLabel(framework) {
|
|
916
930
|
if (!framework || framework === "k6") return "";
|
|
917
931
|
return framework;
|
|
@@ -1087,12 +1101,15 @@ async function runDefaultRuntimeTask(targetConfig, task, args) {
|
|
|
1087
1101
|
|
|
1088
1102
|
const summary = readDefaultRuntimeSummary(summaryFile);
|
|
1089
1103
|
const runtimeError = determineDefaultRuntimeFailure(result, summary);
|
|
1104
|
+
const finishedAt = Date.now();
|
|
1090
1105
|
|
|
1091
1106
|
return {
|
|
1092
1107
|
task,
|
|
1093
1108
|
failed: runtimeError !== null,
|
|
1094
1109
|
error: runtimeError,
|
|
1095
|
-
durationMs:
|
|
1110
|
+
durationMs: finishedAt - startedAt,
|
|
1111
|
+
startedAt,
|
|
1112
|
+
finishedAt,
|
|
1096
1113
|
};
|
|
1097
1114
|
}
|
|
1098
1115
|
|
package/lib/runner/planning.mjs
CHANGED
|
@@ -69,7 +69,9 @@ export function collectSuites(config, suiteType, suiteNames, frameworkFilter, fi
|
|
|
69
69
|
? Math.max(2, files.length)
|
|
70
70
|
: Math.max(1, files.length)),
|
|
71
71
|
maxFileConcurrency:
|
|
72
|
-
framework === "k6"
|
|
72
|
+
framework === "k6" || framework === "playwright"
|
|
73
|
+
? suite.testkit?.maxFileConcurrency || 1
|
|
74
|
+
: 1,
|
|
73
75
|
});
|
|
74
76
|
orderIndex += 1;
|
|
75
77
|
}
|
|
@@ -183,9 +185,7 @@ export function buildTaskQueue(servicePlans, graphs, timings) {
|
|
|
183
185
|
timingKey,
|
|
184
186
|
estimatedDurationMs: estimateTaskDuration(timings, timingKey, suite),
|
|
185
187
|
maxBatchSize:
|
|
186
|
-
suite.
|
|
187
|
-
? Number.POSITIVE_INFINITY
|
|
188
|
-
: suite.maxFileConcurrency || 1,
|
|
188
|
+
suite.maxFileConcurrency || 1,
|
|
189
189
|
});
|
|
190
190
|
nextId += 1;
|
|
191
191
|
}
|
|
@@ -213,20 +213,9 @@ export function claimNextBatch(queue, preferredGraphKey) {
|
|
|
213
213
|
const seed = queue.splice(index, 1)[0];
|
|
214
214
|
const tasks = [seed];
|
|
215
215
|
|
|
216
|
-
if (seed.
|
|
217
|
-
for (let cursor =
|
|
218
|
-
|
|
219
|
-
if (
|
|
220
|
-
candidate.framework === "playwright" &&
|
|
221
|
-
candidate.graphKey === seed.graphKey &&
|
|
222
|
-
candidate.targetName === seed.targetName
|
|
223
|
-
) {
|
|
224
|
-
tasks.push(candidate);
|
|
225
|
-
queue.splice(cursor, 1);
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
} else if (seed.maxBatchSize > 1) {
|
|
229
|
-
for (let cursor = queue.length - 1; cursor >= 0 && tasks.length < seed.maxBatchSize; cursor -= 1) {
|
|
216
|
+
if (seed.maxBatchSize > 1) {
|
|
217
|
+
for (let cursor = 0; cursor < queue.length; cursor += 1) {
|
|
218
|
+
if (tasks.length >= seed.maxBatchSize) break;
|
|
230
219
|
const candidate = queue[cursor];
|
|
231
220
|
if (
|
|
232
221
|
candidate.framework === seed.framework &&
|
|
@@ -237,6 +226,7 @@ export function claimNextBatch(queue, preferredGraphKey) {
|
|
|
237
226
|
) {
|
|
238
227
|
tasks.push(candidate);
|
|
239
228
|
queue.splice(cursor, 1);
|
|
229
|
+
cursor -= 1;
|
|
240
230
|
}
|
|
241
231
|
}
|
|
242
232
|
}
|
|
@@ -137,11 +137,51 @@ describe("runner-planning", () => {
|
|
|
137
137
|
expect(queue).toHaveLength(4);
|
|
138
138
|
|
|
139
139
|
const firstBatch = claimNextBatch(queue, "api|frontend");
|
|
140
|
-
expect(firstBatch.tasks).toHaveLength(
|
|
140
|
+
expect(firstBatch.tasks).toHaveLength(1);
|
|
141
141
|
expect(firstBatch.framework).toBe("playwright");
|
|
142
142
|
|
|
143
143
|
const secondBatch = claimNextBatch(queue, "api|frontend");
|
|
144
|
-
expect(secondBatch.tasks).toHaveLength(
|
|
145
|
-
expect(secondBatch.framework).toBe("
|
|
144
|
+
expect(secondBatch.tasks).toHaveLength(1);
|
|
145
|
+
expect(secondBatch.framework).toBe("playwright");
|
|
146
|
+
|
|
147
|
+
const thirdBatch = claimNextBatch(queue, "api|frontend");
|
|
148
|
+
expect(thirdBatch.tasks).toHaveLength(2);
|
|
149
|
+
expect(thirdBatch.framework).toBe("k6");
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it("allows Playwright suites to opt into bounded multi-file batches", () => {
|
|
153
|
+
const frontend = makeConfig("frontend");
|
|
154
|
+
const plans = [
|
|
155
|
+
{
|
|
156
|
+
config: frontend,
|
|
157
|
+
skipped: false,
|
|
158
|
+
runtimeConfigs: [frontend],
|
|
159
|
+
runtimeNames: ["frontend"],
|
|
160
|
+
runtimeKey: "frontend",
|
|
161
|
+
suites: [
|
|
162
|
+
{
|
|
163
|
+
name: "auth",
|
|
164
|
+
type: "e2e",
|
|
165
|
+
framework: "playwright",
|
|
166
|
+
files: ["a.spec.js", "b.spec.js", "c.spec.js"],
|
|
167
|
+
orderIndex: 0,
|
|
168
|
+
weight: 3,
|
|
169
|
+
maxFileConcurrency: 2,
|
|
170
|
+
},
|
|
171
|
+
],
|
|
172
|
+
},
|
|
173
|
+
];
|
|
174
|
+
|
|
175
|
+
const graphs = buildRuntimeGraphs(plans);
|
|
176
|
+
const queue = buildTaskQueue(plans, graphs, { files: {} });
|
|
177
|
+
|
|
178
|
+
const firstBatch = claimNextBatch(queue, "frontend");
|
|
179
|
+
expect(firstBatch.tasks.map((task) => task.file)).toEqual(["a.spec.js", "b.spec.js"]);
|
|
180
|
+
expect(firstBatch.tasks).toHaveLength(2);
|
|
181
|
+
expect(firstBatch.framework).toBe("playwright");
|
|
182
|
+
|
|
183
|
+
const secondBatch = claimNextBatch(queue, "frontend");
|
|
184
|
+
expect(secondBatch.tasks).toHaveLength(1);
|
|
185
|
+
expect(secondBatch.framework).toBe("playwright");
|
|
146
186
|
});
|
|
147
187
|
});
|
package/lib/runner/results.mjs
CHANGED
|
@@ -17,6 +17,7 @@ export function buildServiceTrackers(servicePlans, startedAt) {
|
|
|
17
17
|
startedAt,
|
|
18
18
|
firstTaskAt: null,
|
|
19
19
|
lastTaskAt: null,
|
|
20
|
+
totalTaskDurationMs: 0,
|
|
20
21
|
});
|
|
21
22
|
continue;
|
|
22
23
|
}
|
|
@@ -72,6 +73,7 @@ export function buildServiceTrackers(servicePlans, startedAt) {
|
|
|
72
73
|
startedAt,
|
|
73
74
|
firstTaskAt: null,
|
|
74
75
|
lastTaskAt: null,
|
|
76
|
+
totalTaskDurationMs: 0,
|
|
75
77
|
});
|
|
76
78
|
}
|
|
77
79
|
|
|
@@ -82,26 +84,38 @@ export function recordTaskOutcome(trackers, task, outcome, finishedAt = Date.now
|
|
|
82
84
|
const tracker = trackers.get(task.serviceName);
|
|
83
85
|
if (!tracker || tracker.skipped) return;
|
|
84
86
|
|
|
85
|
-
if (!tracker.firstTaskAt) tracker.firstTaskAt = finishedAt;
|
|
86
|
-
tracker.lastTaskAt = finishedAt;
|
|
87
|
-
|
|
88
87
|
const suite = tracker.suitesByKey.get(task.suiteKey);
|
|
89
88
|
if (!suite) return;
|
|
90
89
|
|
|
90
|
+
const outcomeDurationMs = Number(outcome.durationMs ?? 0);
|
|
91
|
+
const outcomeFinishedAt = Number(outcome.finishedAt ?? finishedAt);
|
|
92
|
+
const outcomeStartedAt = Number(
|
|
93
|
+
outcome.startedAt ?? Math.max(0, outcomeFinishedAt - outcomeDurationMs)
|
|
94
|
+
);
|
|
95
|
+
tracker.firstTaskAt =
|
|
96
|
+
tracker.firstTaskAt === null
|
|
97
|
+
? outcomeStartedAt
|
|
98
|
+
: Math.min(tracker.firstTaskAt, outcomeStartedAt);
|
|
99
|
+
tracker.lastTaskAt =
|
|
100
|
+
tracker.lastTaskAt === null
|
|
101
|
+
? outcomeFinishedAt
|
|
102
|
+
: Math.max(tracker.lastTaskAt, outcomeFinishedAt);
|
|
103
|
+
tracker.totalTaskDurationMs += outcomeDurationMs;
|
|
104
|
+
|
|
91
105
|
suite.completedFileCount += 1;
|
|
92
|
-
suite.durationMs +=
|
|
106
|
+
suite.durationMs += outcomeDurationMs;
|
|
93
107
|
const normalizedPath = normalizePathSeparators(task.file);
|
|
94
108
|
const existingFileResult = suite.fileResultsByPath.get(normalizedPath);
|
|
95
109
|
if (existingFileResult) {
|
|
96
110
|
existingFileResult.failed = outcome.failed;
|
|
97
|
-
existingFileResult.durationMs =
|
|
111
|
+
existingFileResult.durationMs = outcomeDurationMs;
|
|
98
112
|
existingFileResult.error = outcome.error;
|
|
99
113
|
existingFileResult.status = outcome.failed ? "failed" : "passed";
|
|
100
114
|
} else {
|
|
101
115
|
const fileResult = {
|
|
102
116
|
path: normalizedPath,
|
|
103
117
|
failed: outcome.failed,
|
|
104
|
-
durationMs:
|
|
118
|
+
durationMs: outcomeDurationMs,
|
|
105
119
|
error: outcome.error,
|
|
106
120
|
status: outcome.failed ? "failed" : "passed",
|
|
107
121
|
};
|
|
@@ -144,6 +158,7 @@ export function finalizeServiceResult(tracker, startedAt, finishedAt) {
|
|
|
144
158
|
completedSuiteCount: 0,
|
|
145
159
|
failedSuiteCount: 0,
|
|
146
160
|
durationMs: 0,
|
|
161
|
+
totalTaskDurationMs: 0,
|
|
147
162
|
suites: [],
|
|
148
163
|
errors: [],
|
|
149
164
|
};
|
|
@@ -169,11 +184,12 @@ export function finalizeServiceResult(tracker, startedAt, finishedAt) {
|
|
|
169
184
|
0
|
|
170
185
|
);
|
|
171
186
|
const notRunFileCount = totalFileCount - completedFileCount;
|
|
172
|
-
const
|
|
187
|
+
const totalTaskDurationMs =
|
|
188
|
+
tracker.totalTaskDurationMs || suites.reduce((sum, suite) => sum + suite.durationMs, 0);
|
|
173
189
|
const durationMs =
|
|
174
|
-
tracker.firstTaskAt && tracker.lastTaskAt
|
|
175
|
-
? Math.max(tracker.lastTaskAt - tracker.firstTaskAt
|
|
176
|
-
: Math.max(finishedAt - startedAt
|
|
190
|
+
tracker.firstTaskAt !== null && tracker.lastTaskAt !== null
|
|
191
|
+
? Math.max(0, tracker.lastTaskAt - tracker.firstTaskAt)
|
|
192
|
+
: Math.max(0, finishedAt - startedAt);
|
|
177
193
|
|
|
178
194
|
return {
|
|
179
195
|
name: tracker.name,
|
|
@@ -192,6 +208,7 @@ export function finalizeServiceResult(tracker, startedAt, finishedAt) {
|
|
|
192
208
|
failedFileCount,
|
|
193
209
|
notRunFileCount,
|
|
194
210
|
durationMs,
|
|
211
|
+
totalTaskDurationMs,
|
|
195
212
|
suites: suites.map((suite) => ({
|
|
196
213
|
name: suite.name,
|
|
197
214
|
type: suite.type,
|
|
@@ -385,6 +402,7 @@ export function buildRunArtifact({
|
|
|
385
402
|
failedFileCount: result.failedFileCount,
|
|
386
403
|
notRunFileCount: result.notRunFileCount,
|
|
387
404
|
durationMs: result.durationMs,
|
|
405
|
+
totalTaskDurationMs: result.totalTaskDurationMs,
|
|
388
406
|
dbBackend: result.dbBackend,
|
|
389
407
|
suites: result.suites,
|
|
390
408
|
errors: result.errors,
|
|
@@ -81,6 +81,123 @@ describe("runner-results", () => {
|
|
|
81
81
|
]);
|
|
82
82
|
});
|
|
83
83
|
|
|
84
|
+
it("reports service duration as wall-clock time, not accumulated file time", () => {
|
|
85
|
+
const trackers = buildServiceTrackers(
|
|
86
|
+
[
|
|
87
|
+
{
|
|
88
|
+
skipped: false,
|
|
89
|
+
config: {
|
|
90
|
+
name: "frontend",
|
|
91
|
+
testkit: {
|
|
92
|
+
database: {
|
|
93
|
+
selectedBackend: null,
|
|
94
|
+
},
|
|
95
|
+
},
|
|
96
|
+
},
|
|
97
|
+
suites: [
|
|
98
|
+
{
|
|
99
|
+
name: "browser",
|
|
100
|
+
type: "e2e",
|
|
101
|
+
framework: "playwright",
|
|
102
|
+
files: ["tests/a.pw.testkit.ts", "tests/b.pw.testkit.ts"],
|
|
103
|
+
orderIndex: 0,
|
|
104
|
+
},
|
|
105
|
+
],
|
|
106
|
+
},
|
|
107
|
+
],
|
|
108
|
+
1000
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
recordTaskOutcome(
|
|
112
|
+
trackers,
|
|
113
|
+
{
|
|
114
|
+
serviceName: "frontend",
|
|
115
|
+
suiteKey: "e2e:browser",
|
|
116
|
+
file: "tests/a.pw.testkit.ts",
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
failed: false,
|
|
120
|
+
durationMs: 30_000,
|
|
121
|
+
startedAt: 1000,
|
|
122
|
+
finishedAt: 8000,
|
|
123
|
+
error: null,
|
|
124
|
+
},
|
|
125
|
+
8000
|
|
126
|
+
);
|
|
127
|
+
recordTaskOutcome(
|
|
128
|
+
trackers,
|
|
129
|
+
{
|
|
130
|
+
serviceName: "frontend",
|
|
131
|
+
suiteKey: "e2e:browser",
|
|
132
|
+
file: "tests/b.pw.testkit.ts",
|
|
133
|
+
},
|
|
134
|
+
{
|
|
135
|
+
failed: false,
|
|
136
|
+
durationMs: 45_000,
|
|
137
|
+
startedAt: 3000,
|
|
138
|
+
finishedAt: 9000,
|
|
139
|
+
error: null,
|
|
140
|
+
},
|
|
141
|
+
9000
|
|
142
|
+
);
|
|
143
|
+
|
|
144
|
+
const result = finalizeServiceResult(trackers.get("frontend"), 1000, 10_000);
|
|
145
|
+
|
|
146
|
+
expect(result.durationMs).toBe(8000);
|
|
147
|
+
expect(result.totalTaskDurationMs).toBe(75_000);
|
|
148
|
+
expect(result.suites[0].durationMs).toBe(75_000);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it("handles epoch-zero task timestamps when calculating wall-clock duration", () => {
|
|
152
|
+
const trackers = buildServiceTrackers(
|
|
153
|
+
[
|
|
154
|
+
{
|
|
155
|
+
skipped: false,
|
|
156
|
+
config: {
|
|
157
|
+
name: "frontend",
|
|
158
|
+
testkit: {
|
|
159
|
+
database: {
|
|
160
|
+
selectedBackend: null,
|
|
161
|
+
},
|
|
162
|
+
},
|
|
163
|
+
},
|
|
164
|
+
suites: [
|
|
165
|
+
{
|
|
166
|
+
name: "browser",
|
|
167
|
+
type: "e2e",
|
|
168
|
+
framework: "playwright",
|
|
169
|
+
files: ["tests/a.pw.testkit.ts"],
|
|
170
|
+
orderIndex: 0,
|
|
171
|
+
},
|
|
172
|
+
],
|
|
173
|
+
},
|
|
174
|
+
],
|
|
175
|
+
0
|
|
176
|
+
);
|
|
177
|
+
|
|
178
|
+
recordTaskOutcome(
|
|
179
|
+
trackers,
|
|
180
|
+
{
|
|
181
|
+
serviceName: "frontend",
|
|
182
|
+
suiteKey: "e2e:browser",
|
|
183
|
+
file: "tests/a.pw.testkit.ts",
|
|
184
|
+
},
|
|
185
|
+
{
|
|
186
|
+
failed: false,
|
|
187
|
+
durationMs: 3_000,
|
|
188
|
+
startedAt: 0,
|
|
189
|
+
finishedAt: 8_000,
|
|
190
|
+
error: null,
|
|
191
|
+
},
|
|
192
|
+
8_000
|
|
193
|
+
);
|
|
194
|
+
|
|
195
|
+
const result = finalizeServiceResult(trackers.get("frontend"), 0, 10_000);
|
|
196
|
+
|
|
197
|
+
expect(result.durationMs).toBe(8_000);
|
|
198
|
+
expect(result.totalTaskDurationMs).toBe(3_000);
|
|
199
|
+
});
|
|
200
|
+
|
|
84
201
|
it("builds run artifacts and formatting helpers", () => {
|
|
85
202
|
const results = [
|
|
86
203
|
{
|
|
@@ -96,6 +213,7 @@ describe("runner-results", () => {
|
|
|
96
213
|
failedFileCount: 0,
|
|
97
214
|
notRunFileCount: 0,
|
|
98
215
|
durationMs: 1200,
|
|
216
|
+
totalTaskDurationMs: 2400,
|
|
99
217
|
dbBackend: "local",
|
|
100
218
|
suites: [],
|
|
101
219
|
errors: [],
|
|
@@ -113,6 +231,7 @@ describe("runner-results", () => {
|
|
|
113
231
|
failedFileCount: 0,
|
|
114
232
|
notRunFileCount: 0,
|
|
115
233
|
durationMs: 0,
|
|
234
|
+
totalTaskDurationMs: 0,
|
|
116
235
|
dbBackend: null,
|
|
117
236
|
suites: [],
|
|
118
237
|
errors: [],
|
|
@@ -154,6 +273,8 @@ describe("runner-results", () => {
|
|
|
154
273
|
failed: 0,
|
|
155
274
|
notRun: 0,
|
|
156
275
|
});
|
|
276
|
+
expect(artifact.services[0].durationMs).toBe(1200);
|
|
277
|
+
expect(artifact.services[0].totalTaskDurationMs).toBe(2400);
|
|
157
278
|
expect(summarizeDbBackend(results)).toBe("local");
|
|
158
279
|
expect(formatDuration(65_000)).toBe("1m 05s");
|
|
159
280
|
expect(
|