Files
orca/config/scripts/ci-e2e-shard-plan.mjs
Neil 403b62a8d8 ci: balance existing unit and E2E shards using recorded timings (#20367)
* ci: balance existing unit and E2E shards using recorded timings

* ci: fix timing refresh units and deferred-menu test traversal

* ci: preserve isolated E2E window launch policy

* ci: keep diagnostic artifact outages from failing tests
2026-09-12 03:10:05 -07:00

98 lines
3.2 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { readFileSync, writeFileSync } from 'node:fs'
import { join } from 'node:path'
import { pathToFileURL } from 'node:url'
import {
balanceFiles,
compareIds,
readTimingBaseline,
writeAssignment
} from './ci-shard-assignment.mjs'
export function discoverE2eFiles(report) {
if (report.errors?.length) {
throw new Error('Playwright discovery reported errors')
}
const files = new Map()
function visit(suite) {
for (const spec of suite.specs ?? []) {
const file = spec.file.replaceAll('\\', '/')
if (file.startsWith('/') || file.split('/').includes('..') || /[\n\r>]/.test(file)) {
throw new Error(`Unsafe test-list path: ${file}`)
}
for (const test of spec.tests) {
const id = `${test.projectName}:${spec.id}`
const ids = files.get(file) ?? []
ids.push(id)
files.set(file, ids)
}
}
for (const child of suite.suites ?? []) {
visit(child)
}
}
for (const suite of report.suites) {
visit(suite)
}
if (!files.size) {
throw new Error('Playwright discovered no tests')
}
const ids = [...files.values()].flat()
if (new Set(ids).size !== ids.length) {
throw new Error('Duplicate discovered test identity')
}
return Object.fromEntries([...files.entries()].sort(([a], [b]) => compareIds(a, b)))
}
export function planE2e(report, count, baseline) {
const testsByFile = discoverE2eFiles(report)
const timings = Object.fromEntries(
Object.entries(baseline.timings).map(([file, duration]) => [
file.replace(/^tests\/e2e\//, ''),
duration
])
)
const assignment = balanceFiles(Object.keys(testsByFile), count, timings)
return { ...assignment, testsByFile, baselineSha256: baseline.baselineSha256 }
}
export function verifyE2eSelection(assignment, report) {
const actual = Object.values(discoverE2eFiles(report)).flat().sort(compareIds)
const expected = assignment.shards[assignment.selectedShard - 1].files
.flatMap((file) => assignment.testsByFile[file])
.sort(compareIds)
if (JSON.stringify(actual) !== JSON.stringify(expected)) {
throw new Error('Native Playwright selection differs from shard assignment')
}
}
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
if (process.argv[2] === '--verify') {
verifyE2eSelection(
JSON.parse(readFileSync(process.argv[3], 'utf8')),
JSON.parse(readFileSync(process.argv[4], 'utf8'))
)
} else {
const [input, shard, directory] = process.argv.slice(2)
const match = shard?.match(/^(\d+)\/(\d+)$/)
if (!input || !directory || !match) {
throw new Error('Usage: ci-e2e-shard-plan.mjs DISCOVERY INDEX/COUNT OUTPUT_DIRECTORY')
}
const index = Number(match[1])
const count = Number(match[2])
if (index < 1 || index > count) {
throw new Error('Invalid shard index')
}
const assignment = planE2e(
JSON.parse(readFileSync(input, 'utf8')),
count,
readTimingBaseline('e2e')
)
const selected = assignment.shards[index - 1].files
if (!selected.length) {
throw new Error('Empty E2E shard')
}
writeAssignment(join(directory, 'assignment.json'), { ...assignment, selectedShard: index })
writeFileSync(join(directory, 'selected.txt'), `${selected.join('\n')}\n`)
}
}