Files
orca/.github/scripts/pr-test-loc-table.mjs

116 lines
3.6 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.
export const LOC_BLOCK_START = '<!-- orca-pr-loc -->'
export const LOC_BLOCK_END = '<!-- /orca-pr-loc -->'
export const LOC_HANDS_OFF_COMMENT =
'<!-- Generated by the "PR test LoC" GitHub Action and rewritten on every push. Humans and AI agents: do not edit, move, or remove this block when updating the PR body. -->'
// __-wrapped segments unambiguously mark test data; bare fixtures/ could be production, and this classifier bills every PR.
const TEST_DIR_SEGMENT = /(?:^|\/)(?:__tests__|__fixtures__|__snapshots__|e2e|tests)(?:\/|$)/i
const TEST_FILENAME = /\.(?:test|spec|e2e)\.[^/]+$/i
export function isTestPath(path) {
const normalized = path.replaceAll('\\', '/')
return TEST_DIR_SEGMENT.test(normalized) || TEST_FILENAME.test(normalized)
}
export function emptyLocTotals() {
return {
test: { files: 0, added: 0, deleted: 0 },
nonTest: { files: 0, added: 0, deleted: 0 }
}
}
export function sumChangedFiles(files) {
const totals = emptyLocTotals()
for (const file of files) {
const path = file.filename
if (path == null) {
continue
}
const bucket = isTestPath(path) ? totals.test : totals.nonTest
bucket.files += 1
bucket.added += Number(file.additions ?? 0)
bucket.deleted += Number(file.deletions ?? 0)
}
return totals
}
const COLOR_ADDED = '#1a7f37'
const COLOR_DELETED = '#cf222e'
function diffCell(count) {
if (count === 0) {
return '0'
}
if (count > 0) {
return `$\\color{${COLOR_ADDED}}{\\Huge{\\mathbf{+}}}$\u200b${count}`
}
return `$\\color{${COLOR_DELETED}}{\\Huge{\\mathbf{−}}}$\u200b${Math.abs(count)}`
}
function locTableRow(label, bucket) {
const added = bucket.added ?? 0
const deleted = bucket.deleted ?? 0
return `| ${label} | ${bucket.files ?? 0} | ${diffCell(added)} | ${diffCell(-deleted)} | ${diffCell(added - deleted)} |`
}
export function formatLocTable({ test, nonTest }) {
return [
'| | Files | Added | Deleted | Net |',
'| :--- | ---: | ---: | ---: | ---: |',
locTableRow('Test', test),
locTableRow('Prod', nonTest)
].join('\n')
}
export function renderLocBlock(totals) {
return [
LOC_BLOCK_START,
LOC_HANDS_OFF_COMMENT,
'',
formatLocTable(totals),
'',
LOC_BLOCK_END
].join('\n')
}
function stripLocBlock(body) {
const start = body.indexOf(LOC_BLOCK_START)
const end = body.indexOf(LOC_BLOCK_END)
if (start === -1 || end === -1 || end < start) {
return body
}
const before = body.slice(0, start).trimEnd()
const after = body.slice(end + LOC_BLOCK_END.length).trimStart()
return before.length > 0 && after.length > 0 ? `${before}\n\n${after}` : before || after
}
const ELI5_HEADING = /^(#{1,6})[ \t]*ELI5\b.*$/im
// Index of the heading that ends the ELI5 section, or -1 when absent or it runs to the end.
function eli5SectionEnd(body) {
const heading = ELI5_HEADING.exec(body)
if (heading == null) {
return -1
}
const level = heading[1].length
const sectionStart = heading.index + heading[0].length
const nextHeading = new RegExp(`^#{1,${level}}[ \\t]`, 'm').exec(body.slice(sectionStart))
return nextHeading == null ? -1 : sectionStart + nextHeading.index
}
// Slack/GitHub link unfurls show the start of the body, so the table goes after ELI5 (or at the end), never first.
export function mergeLocBlock(body, totals) {
const block = renderLocBlock(totals)
const rest = stripLocBlock(body ?? '')
if (rest.trim().length === 0) {
return `${block}\n`
}
const insertAt = eli5SectionEnd(rest)
if (insertAt === -1) {
return `${rest.trimEnd()}\n\n${block}\n`
}
return `${rest.slice(0, insertAt).trimEnd()}\n\n${block}\n\n${rest.slice(insertAt)}`
}