mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-12 06:08:30 +09:00
[CI] upload self-contained retrace summary
This commit is contained in:
@@ -537,11 +537,6 @@ jobs:
|
|||||||
pattern: MobileGL-android-retrace-result-*
|
pattern: MobileGL-android-retrace-result-*
|
||||||
path: retrace-artifacts
|
path: retrace-artifacts
|
||||||
|
|
||||||
- name: Install PDF renderer
|
|
||||||
run: |
|
|
||||||
npm install --no-save --no-package-lock playwright
|
|
||||||
npx playwright install --with-deps chromium
|
|
||||||
|
|
||||||
- name: Render retrace summary
|
- name: Render retrace summary
|
||||||
run: |
|
run: |
|
||||||
node tools/trace_replay/render_retrace_summary.mjs \
|
node tools/trace_replay/render_retrace_summary.mjs \
|
||||||
@@ -549,21 +544,11 @@ jobs:
|
|||||||
--output-dir android-retrace-summary \
|
--output-dir android-retrace-summary \
|
||||||
--title "MobileGL Android retrace overview" \
|
--title "MobileGL Android retrace overview" \
|
||||||
--group-label "Android Emulator" \
|
--group-label "Android Emulator" \
|
||||||
--html mobilegl-android-retrace-overview.html \
|
--html mobilegl-android-retrace-overview.html
|
||||||
--pdf mobilegl-android-retrace-overview.pdf
|
|
||||||
|
|
||||||
- name: Upload Android retrace summary
|
- name: Upload Android retrace summary
|
||||||
uses: actions/upload-artifact@v4
|
|
||||||
with:
|
|
||||||
name: MobileGL-android-retrace-summary-${{ env.date_today }}-${{ env.short_sha }}
|
|
||||||
path: |
|
|
||||||
android-retrace-summary/**
|
|
||||||
!android-retrace-summary/mobilegl-android-retrace-overview.pdf
|
|
||||||
if-no-files-found: error
|
|
||||||
|
|
||||||
- name: Upload Android retrace summary PDF
|
|
||||||
uses: actions/upload-artifact@v7
|
uses: actions/upload-artifact@v7
|
||||||
with:
|
with:
|
||||||
path: android-retrace-summary/mobilegl-android-retrace-overview.pdf
|
path: android-retrace-summary/mobilegl-android-retrace-overview.html
|
||||||
archive: false
|
archive: false
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
|
|||||||
@@ -448,11 +448,6 @@ jobs:
|
|||||||
pattern: retrace-result-*
|
pattern: retrace-result-*
|
||||||
path: retrace-artifacts
|
path: retrace-artifacts
|
||||||
|
|
||||||
- name: Install PDF renderer
|
|
||||||
run: |
|
|
||||||
npm install --no-save --no-package-lock playwright
|
|
||||||
npx playwright install --with-deps chromium
|
|
||||||
|
|
||||||
- name: Render retrace summary
|
- name: Render retrace summary
|
||||||
run: |
|
run: |
|
||||||
node tools/trace_replay/render_retrace_summary.mjs \
|
node tools/trace_replay/render_retrace_summary.mjs \
|
||||||
@@ -460,22 +455,12 @@ jobs:
|
|||||||
--output-dir retrace-summary \
|
--output-dir retrace-summary \
|
||||||
--title "MobileGL Linux retrace overview" \
|
--title "MobileGL Linux retrace overview" \
|
||||||
--group-label "Linux" \
|
--group-label "Linux" \
|
||||||
--html mobilegl-linux-retrace-overview.html \
|
--html mobilegl-linux-retrace-overview.html
|
||||||
--pdf mobilegl-linux-retrace-overview.pdf
|
|
||||||
|
|
||||||
- name: Upload retrace summary
|
- name: Upload retrace summary
|
||||||
uses: actions/upload-artifact@v4
|
|
||||||
with:
|
|
||||||
name: retrace-summary-${{ env.date_today }}-${{ env.short_sha }}
|
|
||||||
path: |
|
|
||||||
retrace-summary/**
|
|
||||||
!retrace-summary/mobilegl-linux-retrace-overview.pdf
|
|
||||||
if-no-files-found: error
|
|
||||||
|
|
||||||
- name: Upload retrace summary PDF
|
|
||||||
uses: actions/upload-artifact@v7
|
uses: actions/upload-artifact@v7
|
||||||
with:
|
with:
|
||||||
path: retrace-summary/mobilegl-linux-retrace-overview.pdf
|
path: retrace-summary/mobilegl-linux-retrace-overview.html
|
||||||
archive: false
|
archive: false
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
#!/usr/bin/env node
|
#!/usr/bin/env node
|
||||||
|
|
||||||
import { copyFile, mkdir, readdir, readFile, stat, writeFile } from "node:fs/promises";
|
import { mkdir, readdir, readFile, stat, writeFile } from "node:fs/promises";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import { pathToFileURL } from "node:url";
|
|
||||||
|
|
||||||
const BACKENDS = ["DirectGLES", "DirectVulkan"];
|
const BACKENDS = ["DirectGLES", "DirectVulkan"];
|
||||||
const CASES = [
|
const CASES = [
|
||||||
@@ -33,10 +32,7 @@ function usage() {
|
|||||||
node tools/trace_replay/render_retrace_summary.mjs \\
|
node tools/trace_replay/render_retrace_summary.mjs \\
|
||||||
--input DIR [--input DIR ...] \\
|
--input DIR [--input DIR ...] \\
|
||||||
--output-dir DIR \\
|
--output-dir DIR \\
|
||||||
[--title TITLE] [--group-label LABEL] [--html NAME] [--pdf NAME]
|
[--title TITLE] [--group-label LABEL] [--html NAME]`);
|
||||||
|
|
||||||
When --pdf is provided, the script requires Playwright to be installed in the
|
|
||||||
current Node environment.`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseArgs(argv) {
|
function parseArgs(argv) {
|
||||||
@@ -46,7 +42,6 @@ function parseArgs(argv) {
|
|||||||
title: "MobileGL retrace overview",
|
title: "MobileGL retrace overview",
|
||||||
groupLabel: "retrace",
|
groupLabel: "retrace",
|
||||||
htmlName: "mobilegl-retrace-overview.html",
|
htmlName: "mobilegl-retrace-overview.html",
|
||||||
pdfName: "",
|
|
||||||
};
|
};
|
||||||
for (let i = 0; i < argv.length; i += 1) {
|
for (let i = 0; i < argv.length; i += 1) {
|
||||||
const arg = argv[i];
|
const arg = argv[i];
|
||||||
@@ -66,9 +61,6 @@ function parseArgs(argv) {
|
|||||||
} else if (arg === "--html" && value) {
|
} else if (arg === "--html" && value) {
|
||||||
args.htmlName = value;
|
args.htmlName = value;
|
||||||
i += 1;
|
i += 1;
|
||||||
} else if (arg === "--pdf" && value) {
|
|
||||||
args.pdfName = value;
|
|
||||||
i += 1;
|
|
||||||
} else if (arg === "--help" || arg === "-h") {
|
} else if (arg === "--help" || arg === "-h") {
|
||||||
usage();
|
usage();
|
||||||
process.exit(0);
|
process.exit(0);
|
||||||
@@ -274,14 +266,11 @@ function htmlEscape(value) {
|
|||||||
.replaceAll('"', """);
|
.replaceAll('"', """);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function copyAsset(source, outputDir, assetName) {
|
async function embedPng(source) {
|
||||||
if (!source) return "";
|
if (!source) return "";
|
||||||
if (!(await isUsablePng(source))) return "";
|
if (!(await isUsablePng(source))) return "";
|
||||||
const assetsDir = path.join(outputDir, "assets");
|
const data = await readFile(source);
|
||||||
await mkdir(assetsDir, { recursive: true });
|
return `data:image/png;base64,${data.toString("base64")}`;
|
||||||
const destination = path.join(assetsDir, assetName);
|
|
||||||
await copyFile(source, destination);
|
|
||||||
return normalizeSlashes(path.relative(outputDir, destination));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function statusText(row) {
|
function statusText(row) {
|
||||||
@@ -304,27 +293,26 @@ async function renderHtml(rows, outputDir, title, groupLabel, htmlName) {
|
|||||||
|
|
||||||
const cells = new Map();
|
const cells = new Map();
|
||||||
for (const row of rows) {
|
for (const row of rows) {
|
||||||
const base = `${safeCase(row.group)}-${row.backend}-${safeCase(row.caseName)}`;
|
|
||||||
cells.set(`${row.group}\0${row.backend}\0${row.caseName}`, {
|
cells.set(`${row.group}\0${row.backend}\0${row.caseName}`, {
|
||||||
...row,
|
...row,
|
||||||
actualRel: await copyAsset(row.actual, outputDir, `${base}-actual.png`),
|
actualSrc: await embedPng(row.actual),
|
||||||
goldenRel: await copyAsset(row.golden, outputDir, `${base}-golden.png`),
|
goldenSrc: await embedPng(row.golden),
|
||||||
diffRel: await copyAsset(row.diff, outputDir, `${base}-diff.png`),
|
diffSrc: await embedPng(row.diff),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const thumb = (src, label) => `
|
const thumb = (src, label) => `
|
||||||
<figure class="thumb">
|
<figure class="thumb">
|
||||||
<figcaption>${label}</figcaption>
|
<figcaption>${htmlEscape(label)}</figcaption>
|
||||||
${src ? `<img src="${htmlEscape(src)}" alt="${label}">` : `<div class="no-image">NO IMAGE</div>`}
|
${src ? `<button class="image-button" type="button" aria-label="Open ${htmlEscape(label)} image"><img src="${htmlEscape(src)}" alt="${htmlEscape(label)}"></button>` : `<div class="no-image">NO IMAGE</div>`}
|
||||||
</figure>`;
|
</figure>`;
|
||||||
|
|
||||||
const missingImages = (row) => !row.actualRel || !row.goldenRel || !row.diffRel;
|
const missingImages = (row) => !row.actualSrc || !row.goldenSrc || !row.diffSrc;
|
||||||
const cellFor = (group, backend, caseName) => cells.get(`${group}\0${backend}\0${caseName}`) ?? {
|
const cellFor = (group, backend, caseName) => cells.get(`${group}\0${backend}\0${caseName}`) ?? {
|
||||||
status: "NO_RESULT",
|
status: "NO_RESULT",
|
||||||
actualRel: "",
|
actualSrc: "",
|
||||||
goldenRel: "",
|
goldenSrc: "",
|
||||||
diffRel: "",
|
diffSrc: "",
|
||||||
};
|
};
|
||||||
|
|
||||||
const backendCell = (group, backend, caseName) => {
|
const backendCell = (group, backend, caseName) => {
|
||||||
@@ -335,9 +323,9 @@ async function renderHtml(rows, outputDir, title, groupLabel, htmlName) {
|
|||||||
<section class="backend-cell ${statusClass} ${imageClass}">
|
<section class="backend-cell ${statusClass} ${imageClass}">
|
||||||
<header>${backend} <strong>${statusText(row)}</strong></header>
|
<header>${backend} <strong>${statusText(row)}</strong></header>
|
||||||
<div class="thumbs">
|
<div class="thumbs">
|
||||||
${thumb(row.actualRel, "actual")}
|
${thumb(row.actualSrc, "actual")}
|
||||||
${thumb(row.goldenRel, "golden")}
|
${thumb(row.goldenSrc, "golden")}
|
||||||
${thumb(row.diffRel, "diff")}
|
${thumb(row.diffSrc, "diff")}
|
||||||
</div>
|
</div>
|
||||||
</section>`;
|
</section>`;
|
||||||
};
|
};
|
||||||
@@ -347,10 +335,12 @@ async function renderHtml(rows, outputDir, title, groupLabel, htmlName) {
|
|||||||
const passed = groupCells.filter((row) => row.status === "PASS").length;
|
const passed = groupCells.filter((row) => row.status === "PASS").length;
|
||||||
const failed = groupCells.filter((row) => row.status === "FAIL").length;
|
const failed = groupCells.filter((row) => row.status === "FAIL").length;
|
||||||
const noResult = groupCells.filter((row) => row.status !== "PASS" && row.status !== "FAIL").length;
|
const noResult = groupCells.filter((row) => row.status !== "PASS" && row.status !== "FAIL").length;
|
||||||
|
const passRateDenominator = passed + failed;
|
||||||
|
const passRate = passRateDenominator === 0 ? "0.0%" : `${(passed * 100 / passRateDenominator).toFixed(1)}%`;
|
||||||
return `
|
return `
|
||||||
<section class="group">
|
<section class="group">
|
||||||
<h1>${htmlEscape(title)} - ${htmlEscape(group)}</h1>
|
<h1>${htmlEscape(title)} - ${htmlEscape(group)}</h1>
|
||||||
<p>Each backend cell shows actual / golden / diff. Failed cases are red; absent results are orange; incomplete image sets get an orange notch.<br>PASS ${passed}, FAIL ${failed}, NO RESULT ${noResult}.</p>
|
<p>Each backend cell shows actual / golden / diff. Failed cases are red; absent results are orange; incomplete image sets get an orange notch.<br>PASS ${passed}, FAIL ${failed}, NO RESULT ${noResult}, PASS RATE ${passRate}.</p>
|
||||||
<div class="grid header-row">
|
<div class="grid header-row">
|
||||||
<div>case</div>
|
<div>case</div>
|
||||||
<div>DirectGLES</div>
|
<div>DirectGLES</div>
|
||||||
@@ -478,6 +468,16 @@ async function renderHtml(rows, outputDir, title, groupLabel, htmlName) {
|
|||||||
font-size: 9px;
|
font-size: 9px;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
}
|
}
|
||||||
|
.image-button {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: block;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
border: 0;
|
||||||
|
background: transparent;
|
||||||
|
cursor: zoom-in;
|
||||||
|
}
|
||||||
.thumb img {
|
.thumb img {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
@@ -493,10 +493,30 @@ async function renderHtml(rows, outputDir, title, groupLabel, htmlName) {
|
|||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
color: var(--text);
|
color: var(--text);
|
||||||
}
|
}
|
||||||
|
.lightbox {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
z-index: 20;
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
padding: 28px;
|
||||||
|
background: rgb(0 0 0 / 86%);
|
||||||
|
cursor: zoom-out;
|
||||||
|
}
|
||||||
|
.lightbox[hidden] {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.lightbox img {
|
||||||
|
max-width: min(96vw, 1600px);
|
||||||
|
max-height: 94vh;
|
||||||
|
object-fit: contain;
|
||||||
|
box-shadow: 0 18px 70px rgb(0 0 0 / 70%);
|
||||||
|
}
|
||||||
@page { size: 1170px 2380px; margin: 0; }
|
@page { size: 1170px 2380px; margin: 0; }
|
||||||
@media print {
|
@media print {
|
||||||
main { padding: 10px; }
|
main { padding: 10px; }
|
||||||
.group { margin-bottom: 0; }
|
.group { margin-bottom: 0; }
|
||||||
|
.lightbox { display: none; }
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
@@ -504,6 +524,35 @@ async function renderHtml(rows, outputDir, title, groupLabel, htmlName) {
|
|||||||
<main>
|
<main>
|
||||||
${groupSections}
|
${groupSections}
|
||||||
</main>
|
</main>
|
||||||
|
<div class="lightbox" id="lightbox" hidden>
|
||||||
|
<img id="lightbox-image" alt="">
|
||||||
|
</div>
|
||||||
|
<script>
|
||||||
|
(() => {
|
||||||
|
const lightbox = document.getElementById("lightbox");
|
||||||
|
const lightboxImage = document.getElementById("lightbox-image");
|
||||||
|
const closeLightbox = () => {
|
||||||
|
lightbox.hidden = true;
|
||||||
|
lightboxImage.removeAttribute("src");
|
||||||
|
lightboxImage.alt = "";
|
||||||
|
};
|
||||||
|
|
||||||
|
document.querySelectorAll(".image-button").forEach((button) => {
|
||||||
|
button.addEventListener("click", () => {
|
||||||
|
const image = button.querySelector("img");
|
||||||
|
if (!image) return;
|
||||||
|
lightboxImage.src = image.src;
|
||||||
|
lightboxImage.alt = image.alt;
|
||||||
|
lightbox.hidden = false;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
lightbox.addEventListener("click", closeLightbox);
|
||||||
|
document.addEventListener("keydown", (event) => {
|
||||||
|
if (event.key === "Escape" && !lightbox.hidden) closeLightbox();
|
||||||
|
});
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
`;
|
`;
|
||||||
@@ -512,66 +561,12 @@ ${groupSections}
|
|||||||
return output;
|
return output;
|
||||||
}
|
}
|
||||||
|
|
||||||
function csvEscape(value) {
|
|
||||||
const text = String(value ?? "");
|
|
||||||
return /[",\n]/.test(text) ? `"${text.replaceAll('"', '""')}"` : text;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function writeCsv(rows, outputDir) {
|
|
||||||
const header = ["Group", "Backend", "Case", "Status", "SSIM", "Message", "Actual", "Golden", "Diff"];
|
|
||||||
const sorted = [...rows].sort((a, b) =>
|
|
||||||
a.group.localeCompare(b.group) ||
|
|
||||||
BACKENDS.indexOf(a.backend) - BACKENDS.indexOf(b.backend) ||
|
|
||||||
CASES.indexOf(a.caseName) - CASES.indexOf(b.caseName)
|
|
||||||
);
|
|
||||||
const serialize = (row) => [
|
|
||||||
row.group,
|
|
||||||
row.backend,
|
|
||||||
row.caseName,
|
|
||||||
row.status,
|
|
||||||
row.ssim,
|
|
||||||
row.message,
|
|
||||||
row.actual,
|
|
||||||
row.golden,
|
|
||||||
row.diff,
|
|
||||||
].map(csvEscape).join(",");
|
|
||||||
await writeFile(path.join(outputDir, "summary.csv"), [header.join(","), ...sorted.map(serialize)].join("\n"), "utf8");
|
|
||||||
await writeFile(path.join(outputDir, "failures.csv"), [header.join(","), ...sorted.filter((row) => row.status !== "PASS").map(serialize)].join("\n"), "utf8");
|
|
||||||
}
|
|
||||||
|
|
||||||
async function renderPdf(htmlPath, pdfPath) {
|
|
||||||
let chromium;
|
|
||||||
try {
|
|
||||||
({ chromium } = await import("playwright"));
|
|
||||||
} catch (error) {
|
|
||||||
throw new Error("Playwright is required for --pdf. Install it with: npm install playwright && npx playwright install chromium");
|
|
||||||
}
|
|
||||||
const browser = await chromium.launch();
|
|
||||||
try {
|
|
||||||
const page = await browser.newPage({ viewport: { width: 1170, height: 2380 }, deviceScaleFactor: 1 });
|
|
||||||
await page.goto(pathToFileURL(htmlPath).href, { waitUntil: "networkidle" });
|
|
||||||
await page.pdf({
|
|
||||||
path: pdfPath,
|
|
||||||
printBackground: true,
|
|
||||||
preferCSSPageSize: true,
|
|
||||||
});
|
|
||||||
} finally {
|
|
||||||
await browser.close();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
const args = parseArgs(process.argv.slice(2));
|
const args = parseArgs(process.argv.slice(2));
|
||||||
await mkdir(args.outputDir, { recursive: true });
|
await mkdir(args.outputDir, { recursive: true });
|
||||||
const rows = await collectRows(args.inputs, args.groupLabel);
|
const rows = await collectRows(args.inputs, args.groupLabel);
|
||||||
const htmlPath = await renderHtml(rows, args.outputDir, args.title, args.groupLabel, args.htmlName);
|
const htmlPath = await renderHtml(rows, args.outputDir, args.title, args.groupLabel, args.htmlName);
|
||||||
await writeCsv(rows, args.outputDir);
|
|
||||||
console.log(htmlPath);
|
console.log(htmlPath);
|
||||||
if (args.pdfName) {
|
|
||||||
const pdfPath = path.join(args.outputDir, args.pdfName);
|
|
||||||
await renderPdf(htmlPath, pdfPath);
|
|
||||||
console.log(pdfPath);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
main().catch((error) => {
|
main().catch((error) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user