mirror of
https://github.com/stablyai/orca.git
synced 2026-09-22 16:02:32 +00:00
* 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
98 lines
3.2 KiB
JavaScript
98 lines
3.2 KiB
JavaScript
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`)
|
||
}
|
||
}
|