| <!doctype html> | |
| <!-- | |
| Copyright 2022 The Dawn & Tint Authors | |
| Redistribution and use in source and binary forms, with or without | |
| modification, are permitted provided that the following conditions are met: | |
| 1. Redistributions of source code must retain the above copyright notice, this | |
| list of conditions and the following disclaimer. | |
| 2. Redistributions in binary form must reproduce the above copyright notice, | |
| this list of conditions and the following disclaimer in the documentation | |
| and/or other materials provided with the distribution. | |
| 3. Neither the name of the copyright holder nor the names of its | |
| contributors may be used to endorse or promote products derived from | |
| this software without specific prior written permission. | |
| THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" | |
| AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE | |
| IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE | |
| DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE | |
| FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL | |
| DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR | |
| SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER | |
| CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, | |
| OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| --> | |
| <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, | |
| }); | |
| addEventListener('beforeunload', () => { | |
| fetch("viewer.closed"); | |
| }); | |
| 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> |