|  | <!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> |