Skip to content

Commit a5fa461

Browse files
authored
petri: add and publish log viewer web tool (#1413)
Create a simple client-side HTML+JS solution for parsing and viewing petri logs. Point it to the OpenVMM test results storage account, which will soon be populated automatically. Publish it at <https://openvmm.dev/test-results>.
1 parent 30a0f2b commit a5fa461

File tree

4 files changed

+689
-2
lines changed

4 files changed

+689
-2
lines changed

.github/workflows/openvmm-docs-ci.yaml

Lines changed: 24 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

flowey/flowey_lib_hvlite/src/_jobs/consolidate_and_publish_gh_pages.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ impl SimpleFlowNode for Node {
3232

3333
fn imports(ctx: &mut ImportCtx<'_>) {
3434
ctx.import::<flowey_lib_common::copy_to_artifact_dir::Node>();
35+
ctx.import::<crate::git_checkout_openvmm_repo::Node>();
3536
}
3637

3738
fn process_request(request: Self::Request, ctx: &mut NodeCtx<'_>) -> anyhow::Result<()> {
@@ -42,14 +43,18 @@ impl SimpleFlowNode for Node {
4243
output,
4344
} = request;
4445

46+
let repo = ctx.reqv(crate::git_checkout_openvmm_repo::req::GetRepoDir);
47+
4548
let consolidated_html = ctx.emit_rust_stepv("generate consolidated gh pages html", |ctx| {
4649
let rendered_guide = rendered_guide.claim(ctx);
4750
let rustdoc_windows = rustdoc_windows.claim(ctx);
4851
let rustdoc_linux = rustdoc_linux.claim(ctx);
52+
let repo = repo.claim(ctx);
4953
|rt| {
5054
let rendered_guide = rt.read(rendered_guide);
5155
let rustdoc_windows = rt.read(rustdoc_windows);
5256
let rustdoc_linux = rt.read(rustdoc_linux);
57+
let repo = rt.read(repo);
5358

5459
let consolidated_html = std::env::current_dir()?.join("out").absolute()?;
5560
fs_err::create_dir(&consolidated_html)?;
@@ -78,6 +83,12 @@ impl SimpleFlowNode for Node {
7883
consolidated_html.join("rustdoc/linux"),
7984
)?;
8085

86+
// Make petri logview available under `openvmm.dev/test-results/`
87+
flowey_lib_common::_util::copy_dir_all(
88+
repo.join("petri/logview"),
89+
consolidated_html.join("test-results"),
90+
)?;
91+
8192
// as we do not currently have any form of "landing page",
8293
// redirect `openvmm.dev` to `openvmm.dev/guide`
8394
fs_err::write(consolidated_html.join("index.html"), REDIRECT)?;

petri/logview/index.html

Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
4+
<head>
5+
<meta charset="UTF-8">
6+
<title>Petri test results</title>
7+
<style type="text/css">
8+
body {
9+
font-family: monospace;
10+
font-size: 14px;
11+
}
12+
</style>
13+
<script>
14+
const baseUrl = "https://openvmmghtestresults.blob.core.windows.net/results";
15+
16+
const cross = "&#10060;"; // Cross for failed tests
17+
const check = "&#9989;"; // Check for passed tests
18+
19+
function parseBlobs(xmlText) {
20+
const parser = new DOMParser();
21+
const xmlDoc = parser.parseFromString(xmlText, "text/xml");
22+
const blobs = xmlDoc.getElementsByTagName("Blob");
23+
let blobNames = [];
24+
for (const blob of blobs) {
25+
const name = blob.getElementsByTagName("Name")[0].textContent;
26+
const date = new Date(blob.getElementsByTagName("Creation-Time")[0].textContent);
27+
let metadata = {};
28+
for (const meta of blob.getElementsByTagName("Metadata")) {
29+
const child = meta.children[0];
30+
metadata[child.tagName] = child.textContent;
31+
}
32+
blobNames.push({
33+
name: name,
34+
creationTime: date,
35+
metadata: metadata,
36+
});
37+
}
38+
return blobNames;
39+
}
40+
41+
// Get the blob list, which is in XML via a GET request.
42+
function getTestList(runName) {
43+
const url = `${baseUrl}?restype=container&comp=list&showonly=files&prefix=${encodeURIComponent(runName)}`;
44+
fetch(url)
45+
.then(response => response.text())
46+
.then(data => {
47+
let blobs = parseBlobs(data);
48+
let run = {};
49+
for (const blob of blobs) {
50+
const nameParts = blob.name.split("/");
51+
let fileName = nameParts[nameParts.length - 1];
52+
let failed;
53+
if (fileName === "petri.passed") {
54+
failed = false;
55+
} else if (fileName === "petri.failed") {
56+
failed = true;
57+
} else {
58+
continue; // Not a test result file.
59+
}
60+
const testName = nameParts[nameParts.length - 2];
61+
const jobName = nameParts[nameParts.length - 3];
62+
const path = nameParts.slice(0, -3).join("/");
63+
const url = `test.html?run=${path}&job=${jobName}&test=${testName}`;
64+
if (!run[jobName]) {
65+
run[jobName] = {
66+
failed: false,
67+
tests: [],
68+
};
69+
}
70+
let job = run[jobName];
71+
job.failed |= failed;
72+
job.tests.push({
73+
name: testName,
74+
url: url,
75+
failed: failed,
76+
});
77+
}
78+
79+
let failedHtml = "";
80+
let passingHtml = "";
81+
82+
for (const job in run) {
83+
run[job].tests.sort((a, b) => {
84+
if (a.failed !== b.failed) {
85+
return a.failed ? -1 : 1; // Failed tests first.
86+
}
87+
return a.name.localeCompare(b.name); // Then by name.
88+
});
89+
let thisHtml = `<li>${job}<ul>`;
90+
for (const test of run[job].tests) {
91+
let icon = test.failed ? cross : check;
92+
thisHtml += `<li><a href="${test.url}">${icon} ${test.name}</a></li>`;
93+
}
94+
thisHtml += "</ul></li>";
95+
if (run[job].failed) {
96+
failedHtml += thisHtml;
97+
} else {
98+
passingHtml += thisHtml;
99+
}
100+
}
101+
102+
let html = `<h2>Failed jobs</h2>
103+
<ul>${failedHtml}</ul>
104+
<h2>Passing jobs</h2>
105+
<ul>${passingHtml}</ul>`;
106+
107+
document.getElementById("runList").innerHTML = html;
108+
})
109+
.catch(error => console.error('Error fetching blob list:', error));
110+
}
111+
112+
function getRunList() {
113+
const url = `${baseUrl}?restype=container&comp=list&showonly=files&include=metadata&prefix=runs/`;
114+
fetch(url)
115+
.then(response => response.text())
116+
.then(data => {
117+
const blobs = parseBlobs(data);
118+
const runs = blobs.map(blob => {
119+
// Remove runs/ prefix.
120+
return {
121+
name: blob.name.replace(/^runs\//, ''),
122+
creationTime: blob.creationTime,
123+
failed: blob.metadata["petrifailed"],
124+
};
125+
});
126+
runs.sort((a, b) => b.creationTime - a.creationTime); // Sort by creation time, newest first.
127+
let html = `<table>
128+
<thead>
129+
<tr>
130+
<th>Time</th>
131+
<th>Run</th>
132+
<th>Failed</th>
133+
</tr>
134+
</thead>
135+
<tbody>`;
136+
for (const run of runs) {
137+
const marker = run.failed > 0 ? cross : check;
138+
html += `<tr>
139+
<td>${run.creationTime.toLocaleString()}</td>
140+
<td><a href="?run=${encodeURIComponent(run.name)}">${run.name} ${marker}</a></td>
141+
<td>${run.failed}</td>
142+
</tr>`;
143+
}
144+
html += "</table>";
145+
if (runs.length === 0) {
146+
html = "No runs found.";
147+
}
148+
document.getElementById("runList").innerHTML = html;
149+
})
150+
.catch(error => console.error('Error fetching run list:', error));
151+
}
152+
153+
window.onload = function () {
154+
const urlParams = new URLSearchParams(window.location.search);
155+
const run = urlParams.get('run');
156+
document.getElementById("runList").innerText = "Loading...";
157+
if (run) {
158+
document.getElementById("runName").innerText = run;
159+
document.getElementById("backToRuns").innerHTML = `<a href="?">All runs</a>`;
160+
getTestList(run);
161+
} else {
162+
document.getElementById("runName").innerText = "Runs";
163+
getRunList();
164+
}
165+
};
166+
</script>
167+
</head>
168+
169+
<body>
170+
<h1 id="runName">Loading</h1>
171+
<div id="backToRuns"></div>
172+
<div id="runList"></div>
173+
</body>
174+
175+
</html>

0 commit comments

Comments
 (0)