137137.tree-fn.active .tree-label { color: #fff; }
138138.tree-root { background: #252526; font-weight: bold; border-bottom: 1px solid #444; }
139139.tree-root .tree-label { color: #ccc; }
140+ .tree-omit-btn {
141+ margin-left: auto; background: transparent; border: none;
142+ color: #a44; font-size: 0.85em; cursor: pointer; padding: 0 4px;
143+ border-radius: 3px; line-height: 1; flex-shrink: 0;
144+ }
145+ .tree-omit-btn:hover { color: #f99; background: #3c2222; }
140146
141147#detail { flex: 1; overflow: hidden; display: flex; flex-direction: column; padding: 16px 20px; background: #1e1e1e; color: #ccc; }
142148#detail h2 { font-size: 1em; margin-bottom: 10px; color: #9cdcfe; border-bottom: 1px solid #333; padding-bottom: 6px; flex-shrink: 0; }
618624}
619625
620626// Collapse single-child directory chains (a/b/c → a/b/c) for compactness.
621- function treeRowHtml(label, pct, depth, kind, key, isOpen, dataAttr) {
627+ function treeRowHtml(label, pct, depth, kind, key, isOpen, dataAttr, extra ) {
622628 var pad = 10 + depth * 14;
623629 var arrow = kind === 'leaf' ? '' :
624630 '<span class="tree-arrow">' + (isOpen ? '▾' : '▸') + '</span>';
625631 var badge = '<span class="pct ' + pctClass(pct) + '">' + pct + '%</span>';
626632 return '<div class="tree-row tree-' + kind + '" style="padding-left:' + pad + 'px;" '
627633 + dataAttr + '>' + arrow + badge
628- + '<span class="tree-label">' + escSvg(label) + '</span></div>';
634+ + '<span class="tree-label">' + escSvg(label) + '</span>'
635+ + (extra || '') + '</div>';
629636}
630637
631638function renderTreeNode(node, depth, out) {
649656 fileKeys.forEach(function(k) {
650657 var f = node.files[k];
651658 var isOpen = !treeCollapsed[f.path];
659+ var safePath = f.path.replace(/'/g, "\\'");
660+ var omitBtn = '<button class="tree-omit-btn" title="Omit all functions in this file" '
661+ + 'onclick="event.stopPropagation();omitFile(\'' + safePath + '\')">⊘</button>';
652662 out.push(treeRowHtml(f.name, nodePct(f), depth, 'file', f.path, isOpen,
653- 'onclick="toggleTreeNode(\'' + f.path.replace(/'/g, "\\'") + '\')"'));
663+ 'onclick="toggleTreeNode(\'' + safePath + '\')"', omitBtn ));
654664 if (isOpen) {
655665 // functions in this file, sorted by pct asc
656666 f.fns.slice().sort(function(a, b) {
753763 if (nextFn) show(nextFn);
754764}
755765
766+ // Omit all functions belonging to a given source file path.
767+ function omitFile(filePath) {
768+ var fns = Object.keys(fnSrc).filter(function(fn) { return fnSrc[fn] === filePath; });
769+ var visible = fns.filter(function(fn) { return !isOmitted(fn); });
770+ if (!visible.length) return;
771+ if (!confirm('Omit all ' + visible.length + ' function(s) in ' + filePath + '?')) return;
772+ visible.forEach(function(fn) { localOmit[fn] = 1; });
773+ saveLocalOmit();
774+ refreshOmitUi();
775+ onTestChange();
776+ refreshDetail();
777+ if (currentFunc && isOmitted(currentFunc)) {
778+ var first = document.querySelector('.fn-entry:not([style*="display: none"]):not([style*="display:none"])');
779+ if (first) show(first.dataset.fn);
780+ }
781+ }
782+
756783// Restore a single session-omitted function (live — no reload needed).
757784function restoreOmit(fn) {
758785 delete localOmit[fn];
@@ -1027,6 +1054,10 @@ def build_html(funcs, cov_data, test_covs, fn_src, omit_names=None, omit_header=
10271054 execd , total = packet_counts (lines )
10281055 fn_packets [name ] = [execd , total ]
10291056
1057+ total_exec_pkts = sum (v [0 ] for v in fn_packets .values ())
1058+ total_pkts = sum (v [1 ] for v in fn_packets .values ())
1059+ pkt_pct = round (100 * total_exec_pkts / total_pkts ) if total_pkts else 0
1060+
10301061 callees , callers = build_call_graphs (cov_data , tracked_fns )
10311062
10321063 fn_data_js = 'var fnData = {\n ' + ',\n ' .join (fn_data_entries ) + '\n };'
@@ -1149,7 +1180,8 @@ def build_html(funcs, cov_data, test_covs, fn_src, omit_names=None, omit_header=
11491180 '<div id="sidebar">' ,
11501181 '<div id="sidebar-header">' ,
11511182 ' <strong>Coverage Report</strong><br>' ,
1152- f' { total } functions · { covered_any } hit · { covered_100 } at 100%' ,
1183+ f' { total } functions · { covered_any } hit · { covered_100 } at 100%<br>' ,
1184+ f' { total_exec_pkts } / { total_pkts } packets · { pkt_pct } % coverage' ,
11531185 '</div>' ,
11541186 test_selector ,
11551187 threshold ,
@@ -1224,6 +1256,8 @@ def main():
12241256 ap .add_argument ('--test-cov' , action = 'append' , metavar = 'FILE' , default = [],
12251257 dest = 'test_covs' ,
12261258 help = 'Per-test cov.txt path; may be repeated. Test name derived from path.' )
1259+ ap .add_argument ('--min-packets' , type = int , default = 1 , metavar = 'N' , dest = 'min_packets' ,
1260+ help = 'Filter functions with <= N packets (default 1).' )
12271261 args = ap .parse_args ()
12281262
12291263 funcs = parse_rpt (args .rpt )
@@ -1248,7 +1282,7 @@ def main():
12481282 print (f'[cov_html_report] dropped { len (present )} committed-omit functions' ,
12491283 file = sys .stderr )
12501284
1251- func_minimal_packet_length = 1
1285+ func_minimal_packet_length = args . min_packets
12521286 trivial = {name for _ , name in funcs
12531287 if packet_counts (cov_data .get (name , []))[1 ] <= func_minimal_packet_length }
12541288 if trivial :
0 commit comments