@empiricalrun/test-run 0.16.0 → 0.17.0

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.
Files changed (41) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/dist/bin/commands/failed-list.js +1 -1
  3. package/dist/bin/commands/run.d.ts.map +1 -1
  4. package/dist/bin/commands/run.js +13 -14
  5. package/dist/bin/index.js +0 -4
  6. package/dist/dashboard.d.ts +1 -1
  7. package/dist/dashboard.d.ts.map +1 -1
  8. package/dist/dashboard.js +4 -6
  9. package/dist/failed-test-list.d.ts +25 -2
  10. package/dist/failed-test-list.d.ts.map +1 -1
  11. package/dist/failed-test-list.js +115 -17
  12. package/dist/index.d.ts +1 -2
  13. package/dist/index.d.ts.map +1 -1
  14. package/dist/index.js +6 -3
  15. package/dist/lib/cmd.d.ts.map +1 -1
  16. package/dist/lib/cmd.js +10 -39
  17. package/dist/lib/merge-reports/index.d.ts +4 -1
  18. package/dist/lib/merge-reports/index.d.ts.map +1 -1
  19. package/dist/lib/merge-reports/index.js +17 -9
  20. package/dist/lib/merge-reports/types.d.ts +1 -1
  21. package/dist/lib/run-all-tests.d.ts.map +1 -1
  22. package/dist/lib/run-all-tests.js +11 -5
  23. package/dist/lib/run-specific-test.d.ts.map +1 -1
  24. package/dist/lib/run-specific-test.js +13 -22
  25. package/dist/utils/index.d.ts +0 -2
  26. package/dist/utils/index.d.ts.map +1 -1
  27. package/dist/utils/index.js +1 -12
  28. package/package.json +5 -5
  29. package/tsconfig.tsbuildinfo +1 -1
  30. package/vitest.config.ts +7 -0
  31. package/dist/bin/commands/estimate-time-shard.d.ts +0 -3
  32. package/dist/bin/commands/estimate-time-shard.d.ts.map +0 -1
  33. package/dist/bin/commands/estimate-time-shard.js +0 -122
  34. package/dist/bin/commands/optimize-shards.d.ts +0 -3
  35. package/dist/bin/commands/optimize-shards.d.ts.map +0 -1
  36. package/dist/bin/commands/optimize-shards.js +0 -544
  37. package/dist/lib/cancellation-watcher.d.ts +0 -5
  38. package/dist/lib/cancellation-watcher.d.ts.map +0 -1
  39. package/dist/lib/cancellation-watcher.js +0 -49
  40. package/test-data/blob-report/report-1.zip +0 -0
  41. package/test-data/blob-report/report-2.zip +0 -0
@@ -1,544 +0,0 @@
1
- "use strict";
2
- var __importDefault = (this && this.__importDefault) || function (mod) {
3
- return (mod && mod.__esModule) ? mod : { "default": mod };
4
- };
5
- Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.registerOptimizeShardsCommand = registerOptimizeShardsCommand;
7
- const reporter_1 = require("@empiricalrun/reporter");
8
- const child_process_1 = require("child_process");
9
- const fs_1 = __importDefault(require("fs"));
10
- const path_1 = __importDefault(require("path"));
11
- const SUITES_DELIMITER = " › ";
12
- const BATCH_SIZE = 100;
13
- async function fetchTestHistoryBatch(testCaseIds, dashboardUrl, apiKey, cacheKey) {
14
- const url = `${dashboardUrl}/api/test-cases/history/batch`;
15
- const response = await fetch(url, {
16
- method: "POST",
17
- headers: {
18
- "Content-Type": "application/json",
19
- Authorization: `Bearer ${apiKey}`,
20
- },
21
- body: JSON.stringify({
22
- test_case_ids: testCaseIds,
23
- ...(cacheKey && { cache_key: cacheKey }),
24
- }),
25
- });
26
- if (!response.ok) {
27
- throw new Error(`Failed to fetch test history: ${response.status}`);
28
- }
29
- return response.json();
30
- }
31
- async function fetchTestHistory(testCaseIds, dashboardUrl, cacheKey) {
32
- const apiKey = process.env.EMPIRICALRUN_API_KEY;
33
- if (!apiKey) {
34
- throw new Error("EMPIRICALRUN_API_KEY environment variable is required");
35
- }
36
- const result = {};
37
- for (let i = 0; i < testCaseIds.length; i += BATCH_SIZE) {
38
- const batch = testCaseIds.slice(i, i + BATCH_SIZE);
39
- const batchResult = await fetchTestHistoryBatch(batch, dashboardUrl, apiKey, cacheKey);
40
- Object.assign(result, batchResult);
41
- }
42
- return result;
43
- }
44
- function getPlaywrightTestList(shard) {
45
- const shardArg = shard ? `--shard ${shard}` : "";
46
- try {
47
- const output = (0, child_process_1.execSync)(`npx playwright test ${shardArg} --list --reporter=json`.trim(), {
48
- encoding: "utf-8",
49
- stdio: ["pipe", "pipe", "pipe"],
50
- });
51
- return JSON.parse(output);
52
- }
53
- catch (error) {
54
- if (error.stdout) {
55
- try {
56
- return JSON.parse(error.stdout);
57
- }
58
- catch {
59
- throw new Error(`Failed to parse playwright JSON output: ${error.message}`);
60
- }
61
- }
62
- throw new Error(`Failed to get test list from playwright: ${error.message}`);
63
- }
64
- }
65
- function getShardInfoReporterPath() {
66
- try {
67
- const reporterPath = require.resolve("@empiricalrun/reporter/shard-info-reporter");
68
- return reporterPath;
69
- }
70
- catch {
71
- const fallbackPath = path_1.default.resolve(__dirname, "../../../../reporter/dist/shard-info-reporter.js");
72
- if (fs_1.default.existsSync(fallbackPath)) {
73
- return fallbackPath;
74
- }
75
- throw new Error("Could not resolve @empiricalrun/reporter/shard-info-reporter. " +
76
- "Make sure @empiricalrun/reporter is installed.");
77
- }
78
- }
79
- function getShardInfoReport() {
80
- const reporterPath = getShardInfoReporterPath();
81
- try {
82
- const output = (0, child_process_1.execSync)(`npx playwright test --list --reporter=${reporterPath}`, {
83
- encoding: "utf-8",
84
- stdio: ["pipe", "pipe", "pipe"],
85
- });
86
- return JSON.parse(output);
87
- }
88
- catch (error) {
89
- if (error.stdout) {
90
- try {
91
- return JSON.parse(error.stdout);
92
- }
93
- catch {
94
- throw new Error(`Failed to parse shard info output: ${error.message}`);
95
- }
96
- }
97
- throw new Error(`Failed to get shard info from playwright: ${error.message}`);
98
- }
99
- }
100
- function extractSuiteMetadata(suites, configFullyParallel) {
101
- const metadata = new Map();
102
- function traverse(suite, parentParallelMode) {
103
- let effectiveMode;
104
- if (suite.parallelMode !== "none") {
105
- effectiveMode = suite.parallelMode;
106
- }
107
- else if (parentParallelMode !== "none") {
108
- effectiveMode = parentParallelMode;
109
- }
110
- else if (configFullyParallel) {
111
- effectiveMode = "parallel";
112
- }
113
- else {
114
- effectiveMode = "default";
115
- }
116
- const specIds = suite.specs.map((s) => s.id);
117
- if (specIds.length > 0) {
118
- const key = `${suite.file}:${suite.line}`;
119
- metadata.set(key, {
120
- file: suite.file,
121
- line: suite.line,
122
- parallelMode: effectiveMode,
123
- hasBeforeAllHooks: suite.hasBeforeAllHooks,
124
- hasAfterAllHooks: suite.hasAfterAllHooks,
125
- specIds,
126
- });
127
- }
128
- for (const child of suite.suites) {
129
- traverse(child, effectiveMode);
130
- }
131
- }
132
- for (const suite of suites) {
133
- traverse(suite, "none");
134
- }
135
- return metadata;
136
- }
137
- function buildSpecToGroupKeyMap(suiteMetadata) {
138
- const specToGroupKey = new Map();
139
- for (const [suiteKey, meta] of suiteMetadata) {
140
- for (const specId of meta.specIds) {
141
- if (meta.parallelMode === "serial" || meta.parallelMode === "default") {
142
- specToGroupKey.set(specId, `file:${meta.file}`);
143
- }
144
- else if (meta.hasBeforeAllHooks || meta.hasAfterAllHooks) {
145
- specToGroupKey.set(specId, `hooks:${suiteKey}`);
146
- }
147
- else {
148
- specToGroupKey.set(specId, `test:${specId}`);
149
- }
150
- }
151
- }
152
- return specToGroupKey;
153
- }
154
- function flattenedSpecsToTestInfos(specs, historyResponse, includeRetries, defaultDuration, specToGroupKey) {
155
- const tests = [];
156
- for (const spec of specs) {
157
- for (const test of spec.tests) {
158
- const history = historyResponse[spec.id] || [];
159
- let estimatedDuration = defaultDuration;
160
- if (history.length > 0) {
161
- const durationField = includeRetries
162
- ? "duration_total"
163
- : "duration_per_retry";
164
- const durations = history
165
- .map((r) => r[durationField])
166
- .sort((a, b) => a - b);
167
- const p75Index = Math.floor(durations.length * 0.75);
168
- estimatedDuration =
169
- durations[Math.min(p75Index, durations.length - 1)] ??
170
- defaultDuration;
171
- }
172
- const baseGroupKey = specToGroupKey?.get(spec.id) ?? `test:${spec.id}`;
173
- const groupKey = `${test.projectName}:${baseGroupKey}`;
174
- tests.push({
175
- id: spec.id,
176
- title: spec.title,
177
- file: spec.file,
178
- projectName: test.projectName,
179
- nesting: spec.nesting,
180
- suitesString: spec.suitesString,
181
- estimatedDuration,
182
- groupKey,
183
- });
184
- }
185
- }
186
- return tests;
187
- }
188
- function buildTestGroups(tests) {
189
- const groupMap = new Map();
190
- for (const test of tests) {
191
- let group = groupMap.get(test.groupKey);
192
- if (!group) {
193
- group = {
194
- key: test.groupKey,
195
- tests: [],
196
- totalDuration: 0,
197
- };
198
- groupMap.set(test.groupKey, group);
199
- }
200
- group.tests.push(test);
201
- group.totalDuration += test.estimatedDuration;
202
- }
203
- return [...groupMap.values()];
204
- }
205
- function packGroupsIntoShards(groups, numShards) {
206
- // Deterministic sort: by duration DESC, then by key ASC for stability
207
- const sorted = [...groups].sort((a, b) => {
208
- const durationDiff = b.totalDuration - a.totalDuration;
209
- if (durationDiff !== 0)
210
- return durationDiff;
211
- return a.key.localeCompare(b.key);
212
- });
213
- const shards = Array.from({ length: numShards }, (_, i) => ({
214
- index: i + 1,
215
- groups: [],
216
- tests: [],
217
- totalDuration: 0,
218
- }));
219
- if (shards.length === 0) {
220
- return shards;
221
- }
222
- for (const group of sorted) {
223
- // Deterministic selection: pick lowest index shard when durations are equal
224
- let targetShard = shards[0];
225
- for (const shard of shards) {
226
- if (shard.totalDuration < targetShard.totalDuration) {
227
- targetShard = shard;
228
- }
229
- }
230
- targetShard.groups.push(group);
231
- targetShard.tests.push(...group.tests);
232
- targetShard.totalDuration += group.totalDuration;
233
- }
234
- return shards;
235
- }
236
- function formatTestListLine(test) {
237
- const suites = test.nesting.slice(1, -1);
238
- const suitesAndTitle = suites.length > 0
239
- ? [...suites, test.title].join(SUITES_DELIMITER)
240
- : test.title;
241
- return `[${test.projectName}] › ${test.file} › ${suitesAndTitle}`;
242
- }
243
- function generateTestListContent(shard, workers) {
244
- const parallelizedDuration = shard.totalDuration / workers;
245
- const totalSeconds = Math.round(parallelizedDuration / 1000);
246
- const minutes = Math.floor(totalSeconds / 60);
247
- const seconds = totalSeconds % 60;
248
- const lines = [
249
- `# Shard ${shard.index} - Optimized bin packing`,
250
- `# Tests: ${shard.tests.length}`,
251
- `# Total duration: ${Math.round(shard.totalDuration)}ms`,
252
- `# Estimated with ${workers} workers: ${minutes}m ${seconds}s`,
253
- `# Generated: ${new Date().toISOString()}`,
254
- "",
255
- ];
256
- for (const test of shard.tests) {
257
- lines.push(formatTestListLine(test));
258
- }
259
- return lines.join("\n");
260
- }
261
- function formatDuration(ms, workers) {
262
- const parallelizedDuration = ms / workers;
263
- const totalSeconds = Math.round(parallelizedDuration / 1000);
264
- const minutes = Math.floor(totalSeconds / 60);
265
- const seconds = totalSeconds % 60;
266
- return `${minutes}m ${seconds}s`;
267
- }
268
- function buildProjectDurations(tests) {
269
- const projectDurations = new Map();
270
- for (const t of tests) {
271
- const prev = projectDurations.get(t.projectName) ?? 0;
272
- projectDurations.set(t.projectName, prev + t.estimatedDuration);
273
- }
274
- return projectDurations;
275
- }
276
- function moveTest(test, from, to) {
277
- const idx = from.tests.findIndex((t) => t.id === test.id && t.projectName === test.projectName);
278
- if (idx !== -1)
279
- from.tests.splice(idx, 1);
280
- to.tests.push(test);
281
- from.totalDuration -= test.estimatedDuration;
282
- to.totalDuration += test.estimatedDuration;
283
- const project = test.projectName;
284
- const fromDur = (from.projectDurations.get(project) ?? 0) - test.estimatedDuration;
285
- if (fromDur <= 0)
286
- from.projectDurations.delete(project);
287
- else
288
- from.projectDurations.set(project, fromDur);
289
- const toDur = (to.projectDurations.get(project) ?? 0) + test.estimatedDuration;
290
- to.projectDurations.set(project, toDur);
291
- }
292
- function isParallelTest(test) {
293
- return test.groupKey.includes(":test:");
294
- }
295
- function buildMoveCandidates(tests) {
296
- const parallelTests = [];
297
- const groupedTests = new Map();
298
- for (const t of tests) {
299
- if (isParallelTest(t)) {
300
- parallelTests.push({ tests: [t], totalDuration: t.estimatedDuration });
301
- }
302
- else {
303
- const group = groupedTests.get(t.groupKey) ?? [];
304
- group.push(t);
305
- groupedTests.set(t.groupKey, group);
306
- }
307
- }
308
- const serialGroups = [...groupedTests.values()].map((group) => ({
309
- tests: group,
310
- totalDuration: group.reduce((sum, t) => sum + t.estimatedDuration, 0),
311
- }));
312
- return [...parallelTests, ...serialGroups];
313
- }
314
- function tryMoveOneGroup(heavyShard, allShards, currentMakespan) {
315
- const destCandidates = [...allShards]
316
- .filter((s) => s.index !== heavyShard.index)
317
- .sort((a, b) => a.totalDuration - b.totalDuration);
318
- const moveCandidates = buildMoveCandidates(heavyShard.tests);
319
- moveCandidates.sort((a, b) => b.totalDuration - a.totalDuration);
320
- for (const dest of destCandidates) {
321
- const uninvolvedShardsMax = Math.max(...allShards
322
- .filter((s) => s.index !== heavyShard.index && s.index !== dest.index)
323
- .map((s) => s.totalDuration), 0);
324
- const eligibleCandidates = moveCandidates.filter((c) => c.tests.every((t) => dest.projectDurations.has(t.projectName)));
325
- if (eligibleCandidates.length === 0)
326
- continue;
327
- for (const candidate of eligibleCandidates) {
328
- const newHeavyDuration = heavyShard.totalDuration - candidate.totalDuration;
329
- const newDestDuration = dest.totalDuration + candidate.totalDuration;
330
- const newMakespan = Math.max(newHeavyDuration, newDestDuration, uninvolvedShardsMax);
331
- if (newMakespan < currentMakespan) {
332
- for (const t of candidate.tests) {
333
- moveTest(t, heavyShard, dest);
334
- }
335
- return true;
336
- }
337
- }
338
- }
339
- return false;
340
- }
341
- function rebalanceShardsIncrementally(shards, options) {
342
- const maxMoves = options?.maxMoves ?? 1000;
343
- const minImbalanceMs = options?.minImbalanceMs ?? 0;
344
- const getMakespan = () => Math.max(...shards.map((s) => s.totalDuration));
345
- const getImbalance = () => {
346
- const durations = shards.map((s) => s.totalDuration);
347
- return Math.max(...durations) - Math.min(...durations);
348
- };
349
- let moves = 0;
350
- while (moves < maxMoves) {
351
- const imbalance = getImbalance();
352
- if (imbalance <= minImbalanceMs)
353
- break;
354
- const currentMakespan = getMakespan();
355
- const shardsByDuration = [...shards].sort((a, b) => b.totalDuration - a.totalDuration);
356
- let moved = false;
357
- for (const heavyShard of shardsByDuration) {
358
- if (heavyShard.totalDuration < currentMakespan * 0.9)
359
- break;
360
- if (tryMoveOneGroup(heavyShard, shards, currentMakespan)) {
361
- moved = true;
362
- break;
363
- }
364
- }
365
- if (!moved)
366
- break;
367
- moves++;
368
- }
369
- return shards;
370
- }
371
- function registerOptimizeShardsCommand(program) {
372
- program
373
- .command("optimize-shards")
374
- .description("Generate optimized shard test-list files using bin packing algorithm")
375
- .requiredOption("--shards <count>", "Number of shards to create")
376
- .option("--dashboard-url <url>", "Dashboard URL for fetching test history", process.env.DASHBOARD_DOMAIN || "https://dash.empirical.run")
377
- .option("--workers <workers>", "Number of parallel workers per shard", "8")
378
- .option("--include-retries", "Use total duration including retries (accounts for flakiness)", false)
379
- .option("--default-duration <ms>", "Default duration for tests without history", "30000")
380
- .option("--output-dir <dir>", "Output directory for test-list files", "./shards")
381
- .option("--cache-key <key>", "Cache key for duration data (e.g., commit SHA). Ensures all workers see same data.")
382
- .option("--strategy <strategy>", "Optimization strategy: 'lpt' (bin packing from scratch) or 'incremental' (improve Playwright defaults)", "incremental")
383
- .action(async (options) => {
384
- const { shards: shardCountStr, dashboardUrl, includeRetries, outputDir, cacheKey, strategy, } = options;
385
- const workers = parseInt(options.workers, 10);
386
- const shardCount = parseInt(shardCountStr, 10);
387
- const defaultDuration = parseInt(options.defaultDuration, 10);
388
- if (shardCount < 1) {
389
- console.error("Shard count must be at least 1");
390
- process.exit(1);
391
- }
392
- console.log(`Fetching test metadata from playwright...`);
393
- const shardInfoReport = getShardInfoReport();
394
- const allTestsReport = getPlaywrightTestList();
395
- const allSpecs = (0, reporter_1.getFlattenedTestList)(allTestsReport.suites);
396
- if (allSpecs.length === 0) {
397
- console.log("No tests found.");
398
- process.exit(0);
399
- }
400
- const testCaseIds = allSpecs.map((spec) => spec.id);
401
- console.log(`Found ${testCaseIds.length} tests. Fetching history...`);
402
- let historyResponse = {};
403
- try {
404
- historyResponse = await fetchTestHistory(testCaseIds, dashboardUrl, cacheKey);
405
- }
406
- catch (error) {
407
- console.error("Failed to fetch test history:", error.message);
408
- process.exit(1);
409
- }
410
- const testsWithHistory = testCaseIds.filter((id) => (historyResponse[id]?.length ?? 0) > 0).length;
411
- console.log(`History found for ${testsWithHistory}/${testCaseIds.length} tests`);
412
- const configFullyParallel = shardInfoReport.config.fullyParallel;
413
- console.log(`Config fullyParallel: ${configFullyParallel}`);
414
- const suiteMetadata = extractSuiteMetadata(shardInfoReport.suites, configFullyParallel);
415
- const specToGroupKey = buildSpecToGroupKeyMap(suiteMetadata);
416
- const serialCount = [...specToGroupKey.values()].filter((k) => k.startsWith("file:")).length;
417
- const parallelCount = [...specToGroupKey.values()].filter((k) => k.startsWith("test:")).length;
418
- const hooksCount = [...specToGroupKey.values()].filter((k) => k.startsWith("hooks:")).length;
419
- console.log(`Test grouping: ${parallelCount} parallel, ${serialCount} serial/default, ${hooksCount} with hooks`);
420
- const allTests = flattenedSpecsToTestInfos(allSpecs, historyResponse, includeRetries, defaultDuration, specToGroupKey);
421
- const validStrategies = ["lpt", "incremental"];
422
- if (!validStrategies.includes(strategy)) {
423
- console.error(`Invalid strategy: ${strategy}. Must be one of: ${validStrategies.join(", ")}`);
424
- process.exit(1);
425
- }
426
- console.log(`\n--- Playwright Default Sharding ---`);
427
- const defaultShards = [];
428
- for (let i = 1; i <= shardCount; i++) {
429
- const shardReport = getPlaywrightTestList(`${i}/${shardCount}`);
430
- const shardSpecs = (0, reporter_1.getFlattenedTestList)(shardReport.suites);
431
- const shardTests = flattenedSpecsToTestInfos(shardSpecs, historyResponse, includeRetries, defaultDuration);
432
- const totalDuration = shardTests.reduce((sum, t) => sum + t.estimatedDuration, 0);
433
- defaultShards.push({
434
- index: i,
435
- tests: shardTests,
436
- totalDuration,
437
- projectDurations: buildProjectDurations(shardTests),
438
- });
439
- console.log(` Shard ${i}/${shardCount}: ${shardTests.length} tests, ~${formatDuration(totalDuration, workers)}`);
440
- }
441
- const playwrightMaxDuration = Math.max(...defaultShards.map((s) => s.totalDuration));
442
- const playwrightMinDuration = Math.min(...defaultShards.map((s) => s.totalDuration));
443
- console.log(` Makespan (max shard): ${formatDuration(playwrightMaxDuration, workers)}`);
444
- console.log(` Imbalance: ${formatDuration(playwrightMaxDuration - playwrightMinDuration, workers)}`);
445
- let finalShards;
446
- let optimizedMaxDuration;
447
- let optimizedMinDuration;
448
- if (strategy === "lpt") {
449
- console.log(`\n--- Optimized Bin Packing (LPT) ---`);
450
- const testGroups = buildTestGroups(allTests);
451
- console.log(` Built ${testGroups.length} test groups from ${allTests.length} tests`);
452
- const lptShards = packGroupsIntoShards(testGroups, shardCount);
453
- finalShards = lptShards.map((s) => ({
454
- index: s.index,
455
- tests: s.tests,
456
- totalDuration: s.totalDuration,
457
- projectDurations: buildProjectDurations(s.tests),
458
- }));
459
- for (const shard of finalShards) {
460
- console.log(` Shard ${shard.index}/${shardCount}: ${shard.tests.length} tests, ~${formatDuration(shard.totalDuration, workers)}`);
461
- }
462
- }
463
- else {
464
- console.log(`\n--- Incremental Project-aware Rebalancing ---`);
465
- const shardsToRebalance = defaultShards.map((s) => ({
466
- index: s.index,
467
- tests: [...s.tests],
468
- totalDuration: s.totalDuration,
469
- projectDurations: new Map(s.projectDurations),
470
- }));
471
- finalShards = rebalanceShardsIncrementally(shardsToRebalance, {
472
- minImbalanceMs: workers * 5000,
473
- maxMoves: 1000,
474
- });
475
- for (const shard of finalShards) {
476
- console.log(` Shard ${shard.index}/${shardCount}: ${shard.tests.length} tests, ~${formatDuration(shard.totalDuration, workers)}`);
477
- }
478
- }
479
- optimizedMaxDuration = Math.max(...finalShards.map((s) => s.totalDuration));
480
- optimizedMinDuration = Math.min(...finalShards.map((s) => s.totalDuration));
481
- console.log(` Makespan (max shard): ${formatDuration(optimizedMaxDuration, workers)}`);
482
- console.log(` Imbalance: ${formatDuration(optimizedMaxDuration - optimizedMinDuration, workers)}`);
483
- const improvement = ((playwrightMaxDuration - optimizedMaxDuration) /
484
- playwrightMaxDuration) *
485
- 100;
486
- console.log(`\n Improvement: ${improvement.toFixed(1)}% faster makespan`);
487
- const absoluteOutputDir = path_1.default.isAbsolute(outputDir)
488
- ? outputDir
489
- : path_1.default.join(process.cwd(), outputDir);
490
- if (!fs_1.default.existsSync(absoluteOutputDir)) {
491
- fs_1.default.mkdirSync(absoluteOutputDir, { recursive: true });
492
- }
493
- console.log(`\n--- Writing Test List Files ---`);
494
- for (const shard of finalShards) {
495
- const shardForContent = {
496
- index: shard.index,
497
- groups: [],
498
- tests: shard.tests,
499
- totalDuration: shard.totalDuration,
500
- };
501
- const content = generateTestListContent(shardForContent, workers);
502
- const filePath = path_1.default.join(absoluteOutputDir, `shard-${shard.index}.txt`);
503
- fs_1.default.writeFileSync(filePath, content, "utf-8");
504
- console.log(` Written: ${filePath}`);
505
- }
506
- const comparison = finalShards.map((os) => {
507
- const pw = defaultShards.find((p) => p.index === os.index);
508
- return {
509
- shardIndex: os.index,
510
- playwrightTestCount: pw.tests.length,
511
- playwrightEstimatedMs: Math.round(pw.totalDuration / workers),
512
- optimizedTestCount: os.tests.length,
513
- optimizedEstimatedMs: Math.round(os.totalDuration / workers),
514
- };
515
- });
516
- const summaryPath = path_1.default.join(absoluteOutputDir, "summary.json");
517
- fs_1.default.writeFileSync(summaryPath, JSON.stringify({
518
- generatedAt: new Date().toISOString(),
519
- shardCount,
520
- workers,
521
- strategy,
522
- totalTests: allTests.length,
523
- testsWithHistory,
524
- testsWithoutHistory: allTests.length - testsWithHistory,
525
- includeRetries,
526
- defaultDurationMs: defaultDuration,
527
- playwright: {
528
- makespanMs: Math.round(playwrightMaxDuration / workers),
529
- imbalanceMs: Math.round((playwrightMaxDuration - playwrightMinDuration) / workers),
530
- },
531
- optimized: {
532
- makespanMs: Math.round(optimizedMaxDuration / workers),
533
- imbalanceMs: Math.round((optimizedMaxDuration - optimizedMinDuration) / workers),
534
- },
535
- improvementPercent: parseFloat(improvement.toFixed(1)),
536
- shards: comparison,
537
- }, null, 2), "utf-8");
538
- console.log(` Written: ${summaryPath}`);
539
- console.log(`\nDone! Run shards with:`);
540
- for (let i = 1; i <= shardCount; i++) {
541
- console.log(` npx playwright test --test-list ${path_1.default.join(outputDir, `shard-${i}.txt`)}`);
542
- }
543
- });
544
- }
@@ -1,5 +0,0 @@
1
- export type CancellationWatcher = {
2
- stop: () => void;
3
- };
4
- export declare function startCancellationWatcher(testRunId: string, apiKey: string, onCancel: () => void, pollIntervalMs?: number): CancellationWatcher;
5
- //# sourceMappingURL=cancellation-watcher.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"cancellation-watcher.d.ts","sourceRoot":"","sources":["../../src/lib/cancellation-watcher.ts"],"names":[],"mappings":"AAqCA,MAAM,MAAM,mBAAmB,GAAG;IAChC,IAAI,EAAE,MAAM,IAAI,CAAC;CAClB,CAAC;AAEF,wBAAgB,wBAAwB,CACtC,SAAS,EAAE,MAAM,EACjB,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,MAAM,IAAI,EACpB,cAAc,SAA2B,GACxC,mBAAmB,CAgCrB"}
@@ -1,49 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.startCancellationWatcher = startCancellationWatcher;
4
- const DOMAIN = process.env.DASHBOARD_DOMAIN || "https://dash.empirical.run";
5
- const DEFAULT_POLL_INTERVAL_MS = 30_000;
6
- async function checkTestRunStatus(testRunId, apiKey) {
7
- const url = `${DOMAIN}/api/test-runs/${testRunId}/status`;
8
- try {
9
- const response = await fetch(url, {
10
- headers: { Authorization: `Bearer ${apiKey}` },
11
- });
12
- if (!response.ok) {
13
- console.log(`[CancellationWatcher] Failed to check status: HTTP ${response.status} from ${url}`);
14
- return { isTerminal: false, status: null };
15
- }
16
- const result = (await response.json());
17
- const status = result.data || { isTerminal: false, status: null };
18
- return status;
19
- }
20
- catch (error) {
21
- const message = error instanceof Error ? error.message : String(error);
22
- console.log(`[CancellationWatcher] Failed to check status: ${message}`);
23
- return { isTerminal: false, status: null };
24
- }
25
- }
26
- function startCancellationWatcher(testRunId, apiKey, onCancel, pollIntervalMs = DEFAULT_POLL_INTERVAL_MS) {
27
- let stopped = false;
28
- console.log(`[CancellationWatcher] Starting watcher for test run ${testRunId} (polling every ${pollIntervalMs}ms)`);
29
- console.log(`[CancellationWatcher] Dashboard domain: ${DOMAIN}`);
30
- const poll = async () => {
31
- while (!stopped) {
32
- const { status } = await checkTestRunStatus(testRunId, apiKey);
33
- if (status === "cancelling" || status === "cancelled") {
34
- console.log(`[CancellationWatcher] Test run ${testRunId} is ${status}, triggering cancellation`);
35
- onCancel();
36
- stopped = true;
37
- return;
38
- }
39
- await new Promise((resolve) => setTimeout(resolve, pollIntervalMs));
40
- }
41
- };
42
- poll();
43
- return {
44
- stop: () => {
45
- console.log(`[CancellationWatcher] Stopping watcher for ${testRunId}`);
46
- stopped = true;
47
- },
48
- };
49
- }
Binary file
Binary file