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