mirror of
https://github.com/encounter/dawn-cmake.git
synced 2025-12-14 15:46:28 +00:00
tools: Add documentation for coverage viewer
Change-Id: I5605925bf4dc4012b38f4e2da48f45321d5e047d Reviewed-on: https://dawn-review.googlesource.com/c/dawn/+/113860 Reviewed-by: Antonio Maiorano <amaiorano@google.com> Commit-Queue: Ben Clayton <bclayton@google.com> Kokoro: Kokoro <noreply+kokoro@google.com>
This commit is contained in:
committed by
Dawn LUCI CQ
parent
e3f3de773a
commit
408ace6927
578
tools/src/cov/view-coverage.html
Normal file
578
tools/src/cov/view-coverage.html
Normal file
@@ -0,0 +1,578 @@
|
||||
<!doctype html>
|
||||
<!--
|
||||
Copyright 2022 The Dawn and Tint Authors
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
-->
|
||||
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<title>Dawn Code Coverage viewer</title>
|
||||
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.52.0/codemirror.min.js"></script>
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.52.0/theme/seti.min.css">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.52.0/codemirror.min.css">
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.52.0/mode/clike/clike.min.js"></script>
|
||||
<script src=https://cdnjs.cloudflare.com/ajax/libs/pako/1.0.10/pako.min.js></script>
|
||||
|
||||
<style>
|
||||
::-webkit-scrollbar {
|
||||
background-color: #30353530;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background-color: #80858050;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-corner {
|
||||
background-color: #00000000;
|
||||
}
|
||||
|
||||
.frame {
|
||||
display: flex;
|
||||
left: 0px;
|
||||
right: 0px;
|
||||
top: 0px;
|
||||
bottom: 0px;
|
||||
position: absolute;
|
||||
font-family: monospace;
|
||||
background-color: #151515;
|
||||
color: #c0b070;
|
||||
}
|
||||
|
||||
.left-pane {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.center-pane {
|
||||
flex: 3;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.top-pane {
|
||||
flex: 1;
|
||||
overflow: scroll;
|
||||
}
|
||||
|
||||
.v-flex {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.file-tree {
|
||||
font-size: small;
|
||||
overflow: auto;
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
.test-tree {
|
||||
font-size: small;
|
||||
overflow: auto;
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
.CodeMirror {
|
||||
flex: 3;
|
||||
height: 100%;
|
||||
border: 1px solid #eee;
|
||||
}
|
||||
|
||||
.file-div {
|
||||
margin: 0px;
|
||||
white-space: nowrap;
|
||||
padding: 2px;
|
||||
margin-top: 1px;
|
||||
margin-bottom: 1px;
|
||||
}
|
||||
|
||||
.file-div:hover {
|
||||
background-color: #303030;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.file-div.selected {
|
||||
background-color: #505050;
|
||||
color: #f0f0a0;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.test-name {
|
||||
margin: 0px;
|
||||
white-space: nowrap;
|
||||
padding: 2px;
|
||||
margin-top: 1px;
|
||||
margin-bottom: 1px;
|
||||
}
|
||||
|
||||
.file-coverage {
|
||||
color: black;
|
||||
width: 20pt;
|
||||
padding-right: 3pt;
|
||||
padding-left: 3px;
|
||||
margin-right: 5pt;
|
||||
display: inline-block;
|
||||
text-align: center;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.with-coverage {
|
||||
background-color: #20d04080;
|
||||
border-width: 0px 0px 0px 0px;
|
||||
}
|
||||
|
||||
.with-coverage-start {
|
||||
border-left: solid 1px;
|
||||
border-color: #20f02080;
|
||||
margin-left: -1px;
|
||||
}
|
||||
|
||||
.with-coverage-end {
|
||||
border-right: solid 1px;
|
||||
border-color: #20f02080;
|
||||
margin-right: -1px;
|
||||
}
|
||||
|
||||
.without-coverage {
|
||||
background-color: #d0204080;
|
||||
border-width: 0px 0px 0px 0px;
|
||||
}
|
||||
|
||||
.without-coverage-start {
|
||||
border-left: solid 1px;
|
||||
border-color: #f0202080;
|
||||
margin-left: -1px;
|
||||
}
|
||||
|
||||
.without-coverage-end {
|
||||
border-right: solid 1px;
|
||||
border-color: #f0202080;
|
||||
margin-right: -1px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="frame">
|
||||
<div id="file_tree" class="left-pane file-tree"></div>
|
||||
<div class="center-pane">
|
||||
<div id="source" class="v-flex">
|
||||
<div class="top-pane">
|
||||
<div class="test-tree" id="test_tree"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// "Download" the coverage.dat file if the user presses ctrl-s
|
||||
document.addEventListener('keydown', e => {
|
||||
if (e.ctrlKey && e.key === 's') {
|
||||
e.preventDefault();
|
||||
window.open("coverage.dat");
|
||||
}
|
||||
});
|
||||
|
||||
let current = {
|
||||
file: "",
|
||||
start_line: 0,
|
||||
start_column: 0,
|
||||
end_line: 0,
|
||||
end_column: 0,
|
||||
};
|
||||
|
||||
let pending = { ...current };
|
||||
{
|
||||
let url = new URL(location.href);
|
||||
let query_string = url.search;
|
||||
let search_params = new URLSearchParams(query_string);
|
||||
var f = search_params.get('f');
|
||||
var s = search_params.get('s');
|
||||
var e = search_params.get('e');
|
||||
if (f) {
|
||||
pending.file = f;
|
||||
}
|
||||
if (s) {
|
||||
s = s.split('.');
|
||||
pending.start_line = s.length > 0 ? parseInt(s[0]) : 0;
|
||||
pending.start_column = s.length > 1 ? parseInt(s[1]) : 0;
|
||||
}
|
||||
if (e) {
|
||||
e = e.split('.');
|
||||
pending.end_line = e.length > 0 ? parseInt(e[0]) : 0;
|
||||
pending.end_column = e.length > 1 ? parseInt(e[1]) : 0;
|
||||
}
|
||||
};
|
||||
|
||||
let set_location = (file, start_line, start_column, end_line, end_column) => {
|
||||
current.file = file;
|
||||
current.start_line = start_line;
|
||||
current.start_column = start_column;
|
||||
current.end_line = end_line;
|
||||
current.end_column = end_column;
|
||||
|
||||
let url = new URL(location.href);
|
||||
let query_string = url.search;
|
||||
// Don't use URLSearchParams, as it will unnecessarily escape
|
||||
// characters, such as '/'.
|
||||
url.search = "f=" + file +
|
||||
"&s=" + start_line + "." + end_line +
|
||||
"&e=" + end_line + "." + end_column;
|
||||
window.history.replaceState(null, "", url.toString());
|
||||
};
|
||||
|
||||
let before = (line, col, span) => {
|
||||
if (line < span[0]) { return true; }
|
||||
if (line == span[0]) { return col < span[1]; }
|
||||
return false;
|
||||
};
|
||||
|
||||
let after = (line, col, span) => {
|
||||
if (line > span[2]) { return true; }
|
||||
if (line == span[2]) { return col > span[3]; }
|
||||
return false;
|
||||
};
|
||||
|
||||
let intersects = (span, from, to) => {
|
||||
if (!before(to.line + 1, to.ch + 1, span) &&
|
||||
!after(from.line + 1, from.ch + 1, span)) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
let el_file_tree = document.getElementById("file_tree");
|
||||
let el_test_tree = document.getElementById("test_tree");
|
||||
let el_source = CodeMirror(document.getElementById("source"), {
|
||||
lineNumbers: true,
|
||||
theme: "seti",
|
||||
mode: "text/x-c++src",
|
||||
readOnly: true,
|
||||
});
|
||||
|
||||
window.onload = function () {
|
||||
el_source.doc.setValue("// Loading... ");
|
||||
fetch("coverage.dat").then(response =>
|
||||
response.arrayBuffer()
|
||||
).then(compressed =>
|
||||
pako.inflate(new Uint8Array(compressed))
|
||||
).then(decompressed =>
|
||||
JSON.parse(new TextDecoder("utf-8").decode(decompressed))
|
||||
).then(json => {
|
||||
el_source.doc.setValue("// Select file from the left... ");
|
||||
|
||||
let revision = json.r;
|
||||
let names = json.n;
|
||||
let tests = json.t;
|
||||
let spans = json.s;
|
||||
let files = json.f;
|
||||
|
||||
let glob_group = (file, groupID, span_ids) => {
|
||||
while (true) {
|
||||
let group = file.g[groupID];
|
||||
group.s.forEach(span_id => span_ids.add(span_id));
|
||||
if (!group.e) {
|
||||
break;
|
||||
}
|
||||
groupID = group.e;
|
||||
};
|
||||
};
|
||||
|
||||
let coverage_spans = (file, data, span_ids) => {
|
||||
if (data.g != undefined) {
|
||||
glob_group(file, data.g, span_ids);
|
||||
}
|
||||
if (data.s != undefined) {
|
||||
data.s.forEach(span_id => span_ids.add(span_id));
|
||||
}
|
||||
};
|
||||
|
||||
let glob_node = (file, nodes, span_ids) => {
|
||||
nodes.forEach(node => {
|
||||
let data = node[1];
|
||||
coverage_spans(file, data, span_ids);
|
||||
if (data.c) {
|
||||
glob_node(file, data.c, span_ids);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
let markup = file => {
|
||||
if (file.u) {
|
||||
for (span of file.u) {
|
||||
el_source.doc.markText(
|
||||
{ "line": span[0] - 1, "ch": span[1] - 1 },
|
||||
{ "line": span[2] - 1, "ch": span[3] - 1 },
|
||||
{
|
||||
// inclusiveLeft: true,
|
||||
className: "without-coverage",
|
||||
startStyle: "without-coverage-start",
|
||||
endStyle: "without-coverage-end",
|
||||
});
|
||||
}
|
||||
}
|
||||
let span_ids = new Set();
|
||||
glob_node(file, file.c, span_ids);
|
||||
el_source.operation(() => {
|
||||
span_ids.forEach((span_id) => {
|
||||
let span = spans[span_id];
|
||||
el_source.doc.markText(
|
||||
{ "line": span[0] - 1, "ch": span[1] - 1 },
|
||||
{ "line": span[2] - 1, "ch": span[3] - 1 },
|
||||
{
|
||||
// inclusiveLeft: true,
|
||||
className: "with-coverage",
|
||||
startStyle: "with-coverage-start",
|
||||
endStyle: "with-coverage-end",
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
let NONE_OVERLAP = 0;
|
||||
let ALL_OVERLAP = 1;
|
||||
let SOME_OVERLAP = 2;
|
||||
|
||||
let gather_overlaps = (parent, file, coverage_nodes, from, to) => {
|
||||
if (!coverage_nodes) { return; }
|
||||
|
||||
// Start by populating all the children nodes from the full
|
||||
// test lists. This includes nodes that do not have child
|
||||
// coverage data.
|
||||
for (var index = 0; index < parent.test.length; index++) {
|
||||
if (parent.children.has(index)) { continue; }
|
||||
|
||||
let test_node = parent.test[index];
|
||||
let test_name_id = test_node[0];
|
||||
let test_name = names[test_name_id];
|
||||
let test_children = test_node[1];
|
||||
|
||||
let node = {
|
||||
test: test_children,
|
||||
name: parent.name ? parent.name + test_name : test_name,
|
||||
overlaps: new Map(parent.overlaps), // map: span_id -> OVERLAP
|
||||
children: new Map(), // map: index -> struct
|
||||
is_leaf: test_children.length == 0,
|
||||
};
|
||||
parent.children.set(index, node);
|
||||
}
|
||||
|
||||
// Now update the children that do have coverage data.
|
||||
for (const coverage_node of coverage_nodes) {
|
||||
let index = coverage_node[0];
|
||||
let coverage = coverage_node[1];
|
||||
let node = parent.children.get(index);
|
||||
|
||||
let span_ids = new Set();
|
||||
coverage_spans(file, coverage, span_ids);
|
||||
|
||||
// Update the node overlaps based on the coverage spans.
|
||||
for (const span_id of span_ids) {
|
||||
if (intersects(spans[span_id], from, to)) {
|
||||
let overlap = parent.overlaps.get(span_id) || NONE_OVERLAP;
|
||||
overlap = (overlap == NONE_OVERLAP) ? ALL_OVERLAP : NONE_OVERLAP;
|
||||
node.overlaps.set(span_id, overlap);
|
||||
}
|
||||
}
|
||||
|
||||
// Generate the child nodes.
|
||||
gather_overlaps(node, file, coverage.c, from, to);
|
||||
|
||||
// Gather all the spans used by the children.
|
||||
let all_spans = new Set();
|
||||
for (const [_, child] of node.children) {
|
||||
for (const [span, _] of child.overlaps) {
|
||||
all_spans.add(span);
|
||||
}
|
||||
}
|
||||
|
||||
// Update the node.overlaps based on the child overlaps.
|
||||
for (const span of all_spans) {
|
||||
let overlap = undefined;
|
||||
for (const [_, child] of node.children) {
|
||||
let child_overlap = child.overlaps.get(span);
|
||||
child_overlap = (child_overlap == undefined) ? NONE_OVERLAP : child_overlap;
|
||||
if (overlap == undefined) {
|
||||
overlap = child_overlap;
|
||||
} else {
|
||||
overlap = (child_overlap == overlap) ? overlap : SOME_OVERLAP
|
||||
}
|
||||
}
|
||||
node.overlaps.set(span, overlap);
|
||||
}
|
||||
|
||||
// If all the node.overlaps are NONE_OVERLAP or ALL_OVERLAP
|
||||
// then there's no point holding on to the children -
|
||||
// we know all transitive children either fully overlap
|
||||
// or don't at all.
|
||||
let some_overlap = false;
|
||||
for (const [_, overlap] of node.overlaps) {
|
||||
if (overlap == SOME_OVERLAP) {
|
||||
some_overlap = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!some_overlap) {
|
||||
node.children = null;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let gather_tests = (file, coverage_nodes, test_nodes, from, to) => {
|
||||
let out = [];
|
||||
|
||||
let traverse = (parent) => {
|
||||
for (const [idx, node] of parent.children) {
|
||||
let do_traversal = false;
|
||||
let do_add = false;
|
||||
|
||||
for (const [_, overlap] of node.overlaps) {
|
||||
switch (overlap) {
|
||||
case SOME_OVERLAP:
|
||||
do_traversal = true;
|
||||
break;
|
||||
case ALL_OVERLAP:
|
||||
do_add = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (do_add) {
|
||||
out.push(node.name + (node.is_leaf ? "" : "*"));
|
||||
} else if (do_traversal) {
|
||||
traverse(node);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let tree = {
|
||||
test: test_nodes,
|
||||
overlaps: new Map(), // map: span_id -> OVERLAP
|
||||
children: new Map(), // map: index -> struct
|
||||
};
|
||||
|
||||
gather_overlaps(tree, file, coverage_nodes, from, to);
|
||||
|
||||
traverse(tree);
|
||||
|
||||
return out;
|
||||
};
|
||||
|
||||
let update_selection = (from, to) => {
|
||||
if (from.line > to.line || (from.line == to.line && from.ch > to.ch)) {
|
||||
let tmp = from;
|
||||
from = to;
|
||||
to = tmp;
|
||||
}
|
||||
|
||||
let file = files[current.file];
|
||||
let filtered = gather_tests(file, file.c, tests, from, to);
|
||||
el_test_tree.innerHTML = "";
|
||||
filtered.forEach(test_name => {
|
||||
let element = document.createElement('p');
|
||||
element.className = "test-name";
|
||||
element.innerText = test_name;
|
||||
el_test_tree.appendChild(element);
|
||||
});
|
||||
};
|
||||
|
||||
let load_source = (path) => {
|
||||
if (!files[path]) { return; }
|
||||
|
||||
for (let i = 0; i < el_file_tree.childNodes.length; i++) {
|
||||
let el = el_file_tree.childNodes[i];
|
||||
if (el.path == path) {
|
||||
el.classList.add("selected");
|
||||
} else {
|
||||
el.classList.remove("selected");
|
||||
}
|
||||
}
|
||||
el_source.doc.setValue("// Loading... ");
|
||||
fetch(`${path}`)
|
||||
.then(response => response.text())
|
||||
.then(source => {
|
||||
el_source.doc.setValue(source);
|
||||
current.file = path;
|
||||
markup(files[path]);
|
||||
if (pending.start_line) {
|
||||
var start = {
|
||||
line: pending.start_line - 1,
|
||||
ch: pending.start_column ? pending.start_column - 1 : 0
|
||||
};
|
||||
var end = {
|
||||
line: pending.end_line ? pending.end_line - 1 : pending.start_line - 1,
|
||||
ch: pending.end_column ? pending.end_column - 1 : 0
|
||||
};
|
||||
el_source.doc.setSelection(start, end);
|
||||
update_selection(start, end);
|
||||
}
|
||||
pending = {};
|
||||
});
|
||||
};
|
||||
|
||||
el_source.doc.on("beforeSelectionChange", (doc, selection) => {
|
||||
if (!files[current.file]) { return; }
|
||||
|
||||
let range = selection.ranges[0];
|
||||
let from = range.head;
|
||||
let to = range.anchor;
|
||||
|
||||
set_location(current.file, from.line + 1, from.ch + 1, to.line + 1, to.ch + 1);
|
||||
|
||||
update_selection(from, to);
|
||||
});
|
||||
|
||||
for (const path of Object.keys(files)) {
|
||||
let file = files[path];
|
||||
|
||||
let div = document.createElement('div');
|
||||
div.className = "file-div";
|
||||
div.onclick = () => { pending = {}; load_source(path); }
|
||||
div.path = path;
|
||||
el_file_tree.appendChild(div);
|
||||
|
||||
let coverage = document.createElement('span');
|
||||
coverage.className = "file-coverage";
|
||||
if (file.p != undefined) {
|
||||
let red = 1.0 - file.p;
|
||||
let green = file.p;
|
||||
let normalize = 1.0 / (red * red + green * green);
|
||||
red *= normalize;
|
||||
green *= normalize;
|
||||
coverage.innerText = Math.round(file.p * 100);
|
||||
coverage.style = "background-color: RGB(" + 255 * red + "," + 255 * green + ", 0" + ")";
|
||||
} else {
|
||||
coverage.innerText = "--";
|
||||
coverage.style = "background-color: RGB(180,180,180)";
|
||||
}
|
||||
div.appendChild(coverage);
|
||||
|
||||
let filepath = document.createElement('span');
|
||||
filepath.className = "file-path";
|
||||
filepath.innerText = path;
|
||||
div.appendChild(filepath);
|
||||
}
|
||||
|
||||
if (pending.file) {
|
||||
load_source(pending.file);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
Reference in New Issue
Block a user