1*d9497217SMartin Matuska#!/usr/bin/env perl 2*d9497217SMartin Matuska 3*d9497217SMartin Matuska# SPDX-License-Identifier: MIT 4*d9497217SMartin Matuska# 5*d9497217SMartin Matuska# Copyright (c) 2025, Rob Norris <robn@despairlabs.com> 6*d9497217SMartin Matuska# Copyright (c) 2026, TrueNAS. 7*d9497217SMartin Matuska# 8*d9497217SMartin Matuska# Permission is hereby granted, free of charge, to any person obtaining a copy 9*d9497217SMartin Matuska# of this software and associated documentation files (the "Software"), to 10*d9497217SMartin Matuska# deal in the Software without restriction, including without limitation the 11*d9497217SMartin Matuska# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 12*d9497217SMartin Matuska# sell copies of the Software, and to permit persons to whom the Software is 13*d9497217SMartin Matuska# furnished to do so, subject to the following conditions: 14*d9497217SMartin Matuska# 15*d9497217SMartin Matuska# The above copyright notice and this permission notice shall be included in 16*d9497217SMartin Matuska# all copies or substantial portions of the Software. 17*d9497217SMartin Matuska# 18*d9497217SMartin Matuska# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19*d9497217SMartin Matuska# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20*d9497217SMartin Matuska# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21*d9497217SMartin Matuska# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22*d9497217SMartin Matuska# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 23*d9497217SMartin Matuska# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 24*d9497217SMartin Matuska# IN THE SOFTWARE. 25*d9497217SMartin Matuska 26*d9497217SMartin Matuska# 27*d9497217SMartin Matuska# usage: coverage_report.pl tests/unit/test_zap.info 28*d9497217SMartin Matuska# coverage_report.pl < tests/unit/test_zap.info 29*d9497217SMartin Matuska# 30*d9497217SMartin Matuska# This program takes an lcov/geninfo coverage tracefile and shows a summary 31*d9497217SMartin Matuska# of line, branch and function coverage for each file. It's focused on the 32*d9497217SMartin Matuska# specific needs of OpenZFS' unit test suite (see tests/unit/README.md) but 33*d9497217SMartin Matuska# it should be adaptable to any place where lcov's HTML output is too heavy 34*d9497217SMartin Matuska# or difficult to use (eg build/CI logs). 35*d9497217SMartin Matuska# 36*d9497217SMartin Matuska# The heart of this program is a small parser for the tracefile format as 37*d9497217SMartin Matuska# described in geninfo(1). The rest is concerned with constructing a useful 38*d9497217SMartin Matuska# colorised table output. 39*d9497217SMartin Matuska# 40*d9497217SMartin Matuska 41*d9497217SMartin Matuska# 42*d9497217SMartin Matuska# Typical output: 43*d9497217SMartin Matuska# 44*d9497217SMartin Matuska# Coverage: test_zap | By line | By branch | By function 45*d9497217SMartin Matuska# | Rate% Total Hit | Rate% Total Hit | Rate% Total Hit 46*d9497217SMartin Matuska# module/zfs/u8_textprep.c | 42.0% 802 337 | 33.5% 510 171 | 50.0% 12 6 47*d9497217SMartin Matuska# module/zfs/zap.c | 52.1% 687 358 | 45.2% 250 113 | 41.1% 90 37 48*d9497217SMartin Matuska# module/zfs/zap_fat.c | 87.8% 665 584 | 58.5% 446 261 | 94.6% 37 35 49*d9497217SMartin Matuska# module/zfs/zap_impl.c | 81.9% 232 190 | 60.3% 146 88 | 92.0% 25 23 50*d9497217SMartin Matuska# module/zfs/zap_leaf.c | 86.7% 466 404 | 69.0% 216 149 | 95.7% 23 22 51*d9497217SMartin Matuska# module/zfs/zap_micro.c | 76.5% 238 182 | 54.2% 142 77 | 92.9% 14 13 52*d9497217SMartin Matuska# 53*d9497217SMartin Matuska 54*d9497217SMartin Matuskause 5.010; 55*d9497217SMartin Matuskause warnings; 56*d9497217SMartin Matuskause strict; 57*d9497217SMartin Matuskause Cwd qw(getcwd); 58*d9497217SMartin Matuskause Term::ANSIColor qw(colored); 59*d9497217SMartin Matuska 60*d9497217SMartin Matuska# Setup for color output. Perl has included Term::ANSIColor since 5.6 (~2000), 61*d9497217SMartin Matuska# but RGB support didn't arrive until v4 in 5.17.8 (~2012). We disable colors 62*d9497217SMartin Matuska# outright on versions < 4, or if output is not attached to a terminal. 63*d9497217SMartin Matuskamy $use_colors = -t \*STDOUT && $Term::ANSIColor::VERSION >= 4; 64*d9497217SMartin Matuska 65*d9497217SMartin Matuska# Palette setup. If Term::ANSIColor and the terminal advertise support for 66*d9497217SMartin Matuska# it, then we set up a pleasant red -> green gradient for the coverage 67*d9497217SMartin Matuska# percentages. If not, we scale those colors down to the older RGB-240 colors 68*d9497217SMartin Matuska# (0-5 for each component), which is still quite nice. 69*d9497217SMartin Matuskamy @palette = !$use_colors ? () : map { 70*d9497217SMartin Matuska state $has_truecolor = 71*d9497217SMartin Matuska $Term::ANSIColor::VERSION >= 5 && $ENV{COLORTERM}; 72*d9497217SMartin Matuska my @rgb = map { hex } m/../g; 73*d9497217SMartin Matuska if ($has_truecolor) { 74*d9497217SMartin Matuska sprintf 'r%dg%db%d', @rgb; 75*d9497217SMartin Matuska } else { 76*d9497217SMartin Matuska sprintf 'rgb%d%d%d', map { $_ * 6 / 255 } @rgb; 77*d9497217SMartin Matuska } 78*d9497217SMartin Matuska} ( 79*d9497217SMartin Matuska # Catppuccin Latte 80*d9497217SMartin Matuska # https://catppuccin.com/palette/ 81*d9497217SMartin Matuska 'd20f39', # Red 82*d9497217SMartin Matuska 'e64553', # Maroon 83*d9497217SMartin Matuska 'fe640b', # Peach 84*d9497217SMartin Matuska 'df8e1d', # Yellow 85*d9497217SMartin Matuska '40a02b', # Green 86*d9497217SMartin Matuska '179299', # Teal 87*d9497217SMartin Matuska); 88*d9497217SMartin Matuska 89*d9497217SMartin Matuska# Test name, from the TN: field if present. 90*d9497217SMartin Matuskamy $test_name = ''; 91*d9497217SMartin Matuska 92*d9497217SMartin Matuska# Per-file data, initially sourced from the tracefile, then augmented 93*d9497217SMartin Matuskamy %filedata; 94*d9497217SMartin Matuska 95*d9497217SMartin Matuska# Tracking for the longest (stringified) value for each key. These are used 96*d9497217SMartin Matuska# later when computing the output table column width. 97*d9497217SMartin Matuskamy %len; 98*d9497217SMartin Matuskasub bump_len { 99*d9497217SMartin Matuska my ($k, $x) = @_; 100*d9497217SMartin Matuska my $l = length "".$x; 101*d9497217SMartin Matuska $len{$k} = $l if ($len{$k} // 0) < $l; 102*d9497217SMartin Matuska} 103*d9497217SMartin Matuska 104*d9497217SMartin Matuska### 105*d9497217SMartin Matuska# Parse the tracefile into per-file data records. 106*d9497217SMartin Matuska 107*d9497217SMartin Matuska# Current working directory. Expected to be the build root. Used to remove 108*d9497217SMartin Matuska# the leading part of the source filenames, so its not the end of the world 109*d9497217SMartin Matuska# if its wrong. 110*d9497217SMartin Matuskamy $cwd = getcwd; 111*d9497217SMartin Matuska 112*d9497217SMartin Matuska# Loop over the input 113*d9497217SMartin Matuskawhile (my $line = <>) { 114*d9497217SMartin Matuska state $data = {}; 115*d9497217SMartin Matuska chomp $line; 116*d9497217SMartin Matuska 117*d9497217SMartin Matuska # skip comments 118*d9497217SMartin Matuska next if $line =~ m/^#/; 119*d9497217SMartin Matuska 120*d9497217SMartin Matuska if ($line eq 'end_of_record') { 121*d9497217SMartin Matuska # end of this file, prep for next 122*d9497217SMartin Matuska $data = {}; 123*d9497217SMartin Matuska next; 124*d9497217SMartin Matuska } 125*d9497217SMartin Matuska 126*d9497217SMartin Matuska # everything else should be a KEY:VALUE line 127*d9497217SMartin Matuska my ($k, $v) = $line =~ m/^([A-Z]+):(.*)$/; 128*d9497217SMartin Matuska unless (defined $k) { 129*d9497217SMartin Matuska say "W: $.: malformed line: $line"; 130*d9497217SMartin Matuska next; 131*d9497217SMartin Matuska } 132*d9497217SMartin Matuska 133*d9497217SMartin Matuska if ($k eq 'TN') { 134*d9497217SMartin Matuska # TN:test_zap 135*d9497217SMartin Matuska 136*d9497217SMartin Matuska # Test name. This is actually per-record (a tracefile can 137*d9497217SMartin Matuska # carry multiple test results) but we only ever generate 138*d9497217SMartin Matuska # them for a single test, so we don't make any effort to 139*d9497217SMartin Matuska # notice or track changes. 140*d9497217SMartin Matuska $test_name = $v; 141*d9497217SMartin Matuska next; 142*d9497217SMartin Matuska } 143*d9497217SMartin Matuska 144*d9497217SMartin Matuska if ($k eq 'SF') { 145*d9497217SMartin Matuska # SF:/home/robn/code/zfs-unit/module/zfs/zap.c 146*d9497217SMartin Matuska 147*d9497217SMartin Matuska # Source file. Value is the name, and the rest of the record 148*d9497217SMartin Matuska # apply to it. 149*d9497217SMartin Matuska 150*d9497217SMartin Matuska # Remove the leading build root name. 151*d9497217SMartin Matuska my $path = $v; 152*d9497217SMartin Matuska $path =~ s{^$cwd/*}{}; 153*d9497217SMartin Matuska 154*d9497217SMartin Matuska # If we haven't seen this file before, create a new data 155*d9497217SMartin Matuska # record for it. 156*d9497217SMartin Matuska $filedata{$v} //= { path => $path }; 157*d9497217SMartin Matuska $data = $filedata{$v}; 158*d9497217SMartin Matuska 159*d9497217SMartin Matuska # Increase path column width if necessary. 160*d9497217SMartin Matuska bump_len('path', $path); 161*d9497217SMartin Matuska next; 162*d9497217SMartin Matuska } 163*d9497217SMartin Matuska 164*d9497217SMartin Matuska # Handle the counter keys. These are single values for the entire 165*d9497217SMartin Matuska # record in the file. L, FN and BR are Line, Function and Branch, 166*d9497217SMartin Matuska # F and H are found (ie total) and hit (ie was executed). 167*d9497217SMartin Matuska if (grep { $_ eq $k } qw(LF LH FNF FNH BRF BRH)) { 168*d9497217SMartin Matuska $data->{lc $k} = $v; 169*d9497217SMartin Matuska bump_len(lc $k, $v); 170*d9497217SMartin Matuska next; 171*d9497217SMartin Matuska } 172*d9497217SMartin Matuska 173*d9497217SMartin Matuska # Older versions of lcov may not emit absolute found/hit counters. To 174*d9497217SMartin Matuska # handle this, we maintain our own counters from other events recorded 175*d9497217SMartin Matuska # in the info file, which we use if we don't get an absolute count. 176*d9497217SMartin Matuska 177*d9497217SMartin Matuska if ($k eq 'DA') { 178*d9497217SMartin Matuska # DA:<line number>,<execution count>[,<checksum>] 179*d9497217SMartin Matuska # DA:463,0 180*d9497217SMartin Matuska # DA:469,153 181*d9497217SMartin Matuska my ($l, $h) = split ',', $v; 182*d9497217SMartin Matuska 183*d9497217SMartin Matuska # One DA: record per actual code line (vs comment or other 184*d9497217SMartin Matuska # non-executable line), so we count records, not line number. 185*d9497217SMartin Matuska $data->{_lf}++; 186*d9497217SMartin Matuska 187*d9497217SMartin Matuska # Only increment the hit count if the line was executed. 188*d9497217SMartin Matuska $data->{_lh}++ if $h > 0; 189*d9497217SMartin Matuska next; 190*d9497217SMartin Matuska } 191*d9497217SMartin Matuska 192*d9497217SMartin Matuska if ($k eq 'FN') { 193*d9497217SMartin Matuska # FN:<start line>,[<end line>,]<function nname> 194*d9497217SMartin Matuska # FN:283,zap_lookup_by_dnode 195*d9497217SMartin Matuska 196*d9497217SMartin Matuska # One FN record per function 197*d9497217SMartin Matuska $data->{_fnf}++; 198*d9497217SMartin Matuska next; 199*d9497217SMartin Matuska } 200*d9497217SMartin Matuska if ($k eq 'FNDA') { 201*d9497217SMartin Matuska # FNDA:<execution count>,<function name> 202*d9497217SMartin Matuska # FNDA:0,zap_lookup 203*d9497217SMartin Matuska # FNDA:78,zap_lookup_by_dnode 204*d9497217SMartin Matuska 205*d9497217SMartin Matuska # Only count hit if more than one execution. 206*d9497217SMartin Matuska my ($c) = split ',', $v; 207*d9497217SMartin Matuska $data->{_fnh}++ if 0+$c > 0; 208*d9497217SMartin Matuska next; 209*d9497217SMartin Matuska } 210*d9497217SMartin Matuska 211*d9497217SMartin Matuska if ($k eq 'BRDA') { 212*d9497217SMartin Matuska # BRDA:<line_number>,[<exception>]<block>,<branch>,<taken> 213*d9497217SMartin Matuska # BRDA:365,0,0,- 214*d9497217SMartin Matuska # BRDA:365,0,1,- 215*d9497217SMartin Matuska my ($l, $b, $br, $c) = split ',', $v; 216*d9497217SMartin Matuska 217*d9497217SMartin Matuska # One BRDA: record per branch 218*d9497217SMartin Matuska $data->{_brf}++; 219*d9497217SMartin Matuska 220*d9497217SMartin Matuska # <taken> is number of times branch arm was taken, or '-' if 221*d9497217SMartin Matuska # never considered (eg surrounding block was never entered) 222*d9497217SMartin Matuska # they're both 0 for our purposes. 223*d9497217SMartin Matuska $c = 0 if $c eq '-'; 224*d9497217SMartin Matuska 225*d9497217SMartin Matuska # Only count hit if more than one execution. 226*d9497217SMartin Matuska $data->{_brh}++ if 0+$c > 0; 227*d9497217SMartin Matuska next; 228*d9497217SMartin Matuska } 229*d9497217SMartin Matuska} 230*d9497217SMartin Matuska 231*d9497217SMartin Matuska### 232*d9497217SMartin Matuska# Synthesize missing counters 233*d9497217SMartin Matuska 234*d9497217SMartin Matuskafor my $file (keys %filedata) { 235*d9497217SMartin Matuska my $data = $filedata{$file}; 236*d9497217SMartin Matuska 237*d9497217SMartin Matuska for my $k (qw(lf lh fnf fnh brf brh)) { 238*d9497217SMartin Matuska # Get our own count, if one exists. 239*d9497217SMartin Matuska my $v = delete $data->{"_$k"} // 0; 240*d9497217SMartin Matuska 241*d9497217SMartin Matuska # If we didn't find a count in the info file, use our own. 242*d9497217SMartin Matuska # Note that this will also set legitimately unseen values to 243*d9497217SMartin Matuska # 0 (eg a source file with no branches). That's actually what 244*d9497217SMartin Matuska # we want. 245*d9497217SMartin Matuska unless (exists $data->{$k}) { 246*d9497217SMartin Matuska $data->{$k} = $v; 247*d9497217SMartin Matuska bump_len($k, $v); 248*d9497217SMartin Matuska } 249*d9497217SMartin Matuska } 250*d9497217SMartin Matuska} 251*d9497217SMartin Matuska 252*d9497217SMartin Matuska### 253*d9497217SMartin Matuska# Synthesize the "rate" percentage field from the "found" and "hit" fields. 254*d9497217SMartin Matuska 255*d9497217SMartin Matuskasub rate { 256*d9497217SMartin Matuska my ($data, $k, $kf, $kh) = @_; 257*d9497217SMartin Matuska my $rate = sprintf '%.01f%%', 258*d9497217SMartin Matuska $data->{$kf} ? (100 * $data->{$kh} / $data->{$kf}) : 0; 259*d9497217SMartin Matuska $data->{$k} = $rate; 260*d9497217SMartin Matuska bump_len($k, $rate); 261*d9497217SMartin Matuska} 262*d9497217SMartin Matuska 263*d9497217SMartin Matuskafor my $file (keys %filedata) { 264*d9497217SMartin Matuska my $data = $filedata{$file}; 265*d9497217SMartin Matuska rate($data, 'lr', 'lf', 'lh'); 266*d9497217SMartin Matuska rate($data, 'brr', 'brf', 'brh'); 267*d9497217SMartin Matuska rate($data, 'fnr', 'fnf', 'fnh'); 268*d9497217SMartin Matuska} 269*d9497217SMartin Matuska 270*d9497217SMartin Matuska### 271*d9497217SMartin Matuska# Set up the header "rows". 272*d9497217SMartin Matuska 273*d9497217SMartin Matuska# We reuse our data record structure a little because outputting these needs to 274*d9497217SMartin Matuska# consider and sometimes contribute to column width. 275*d9497217SMartin Matuska 276*d9497217SMartin Matuska# The top row spans multiple columns. The pad functions below have extra tools 277*d9497217SMartin Matuska# to handle the math. 278*d9497217SMartin Matuskamy $h1data = { 279*d9497217SMartin Matuska path => 'Coverage'.($test_name ? ": $test_name" : ''), 280*d9497217SMartin Matuska l => 'By line', 281*d9497217SMartin Matuska br => 'By branch', 282*d9497217SMartin Matuska fn => 'By function', 283*d9497217SMartin Matuska}; 284*d9497217SMartin Matuskabump_len('path', $h1data->{path}); 285*d9497217SMartin Matuska 286*d9497217SMartin Matuska# The second row is the actual header for each data column, and so may push 287*d9497217SMartin Matuska# the column widths out if necessary. 288*d9497217SMartin Matuskamy $h2data = { 289*d9497217SMartin Matuska lr => 'Rate%', lf => 'Total', lh => 'Hit', 290*d9497217SMartin Matuska brr => 'Rate%', brf => 'Total', brh => 'Hit', 291*d9497217SMartin Matuska fnr => 'Rate%', fnf => 'Total', fnh => 'Hit', 292*d9497217SMartin Matuska}; 293*d9497217SMartin Matuskabump_len($_, $h2data->{$_}) for keys %$h2data; 294*d9497217SMartin Matuska 295*d9497217SMartin Matuska### 296*d9497217SMartin Matuska# Table layout 297*d9497217SMartin Matuska 298*d9497217SMartin Matuska# Internal helper for padr() and padl() below. The idea is to compute the 299*d9497217SMartin Matuska# effective column width, and the string we want to place in it. If it would 300*d9497217SMartin Matuska# fit exactly, we return the string. If not, the passed-in function is called 301*d9497217SMartin Matuska# with the string, its length and the column width, and it will place it 302*d9497217SMartin Matuska# (by adding padding on either side). 303*d9497217SMartin Matuska# 304*d9497217SMartin Matuska# Most calls take a single column key, which makes it very simple - take 305*d9497217SMartin Matuska# the max width for that column (from %len, set by bump_len()), and the value 306*d9497217SMartin Matuska# of that key in this column, and that's all of it. 307*d9497217SMartin Matuska# 308*d9497217SMartin Matuska# For the top heading row (h1data above), a list of column keys can be passed 309*d9497217SMartin Matuska# in. In this case, the string will be constructed as a space-separated list 310*d9497217SMartin Matuska# of all the keys have have a value in the data row. The column width is the 311*d9497217SMartin Matuska# sum of max column widths for all columns that mave a max column width, plus 312*d9497217SMartin Matuska# one for each space separator. This allows us to provide a separate string 313*d9497217SMartin Matuska# to appear in the space, with the amount of space computed from the columns 314*d9497217SMartin Matuska# underneath it. 315*d9497217SMartin Matuska# 316*d9497217SMartin Matuskasub _pad { 317*d9497217SMartin Matuska my ($fn, $data, @k) = @_; 318*d9497217SMartin Matuska my $str = join ' ', map { $data->{$_} // () } @k; 319*d9497217SMartin Matuska my $strlen = length $str; 320*d9497217SMartin Matuska my $colwidth = -1; 321*d9497217SMartin Matuska $colwidth += ($len{$_} // -1)+1 for @k; 322*d9497217SMartin Matuska return $strlen == $colwidth ? $str : $fn->($str, $strlen, $colwidth); 323*d9497217SMartin Matuska} 324*d9497217SMartin Matuska 325*d9497217SMartin Matuska# Return the value of the named fields, with space-padding added to the right. 326*d9497217SMartin Matuskasub padr { 327*d9497217SMartin Matuska _pad(sub { 328*d9497217SMartin Matuska my ($str, $strlen, $colwidth) = @_; 329*d9497217SMartin Matuska $str . (' ' x ($colwidth - $strlen)); 330*d9497217SMartin Matuska }, @_); 331*d9497217SMartin Matuska} 332*d9497217SMartin Matuska 333*d9497217SMartin Matuska# Return the value of the named fields, with space-padding added to the left. 334*d9497217SMartin Matuskasub padl { 335*d9497217SMartin Matuska _pad(sub { 336*d9497217SMartin Matuska my ($str, $strlen, $colwidth) = @_; 337*d9497217SMartin Matuska (' ' x ($colwidth - $strlen)) . $str; 338*d9497217SMartin Matuska }, @_); 339*d9497217SMartin Matuska} 340*d9497217SMartin Matuska 341*d9497217SMartin Matuska# Return the given % string, wrapped in terminal control codes that will give 342*d9497217SMartin Matuska# it an appropriate color from the palette. 343*d9497217SMartin Matuskasub colorpct { 344*d9497217SMartin Matuska my ($pct) = @_; 345*d9497217SMartin Matuska 346*d9497217SMartin Matuska # If colors are disabled, return the string as-is. 347*d9497217SMartin Matuska return $pct unless $use_colors; 348*d9497217SMartin Matuska 349*d9497217SMartin Matuska my ($n) = $pct =~ m/([0-9\.]+)/; 350*d9497217SMartin Matuska 351*d9497217SMartin Matuska # scale 0-100 into palette range 352*d9497217SMartin Matuska my $s = int(($#palette / 100) * $n); 353*d9497217SMartin Matuska my $c = $palette[$s]; 354*d9497217SMartin Matuska 355*d9497217SMartin Matuska return colored([$c], $pct); 356*d9497217SMartin Matuska} 357*d9497217SMartin Matuska 358*d9497217SMartin Matuskamy @rows; 359*d9497217SMartin Matuska 360*d9497217SMartin Matuska# Layout the first header row 361*d9497217SMartin Matuskapush @rows, [ 362*d9497217SMartin Matuska padr($h1data, 'path'), 363*d9497217SMartin Matuska '|', padr($h1data, 'l', 'lr', 'lf', 'lh'), 364*d9497217SMartin Matuska '|', padr($h1data, 'br', 'brr', 'brf', 'brh'), 365*d9497217SMartin Matuska '|', padr($h1data, 'fn', 'fnr', 'fnf', 'fnh'), 366*d9497217SMartin Matuska]; 367*d9497217SMartin Matuska 368*d9497217SMartin Matuska# Layout the second header row 369*d9497217SMartin Matuskapush @rows, [ 370*d9497217SMartin Matuska padr($h2data, 'path'), 371*d9497217SMartin Matuska '|', padr($h2data, 'lr'), padl($h2data, 'lf'), padl($h2data, 'lh'), 372*d9497217SMartin Matuska '|', padr($h2data, 'brr'), padl($h2data, 'brf'), padl($h2data, 'brh'), 373*d9497217SMartin Matuska '|', padr($h2data, 'fnr'), padl($h2data, 'fnf'), padl($h2data, 'fnh'), 374*d9497217SMartin Matuska]; 375*d9497217SMartin Matuska 376*d9497217SMartin Matuska# Layout the data rows, padding colorising as appropriate. 377*d9497217SMartin Matuskafor my $file (sort keys %filedata) { 378*d9497217SMartin Matuska my $data = $filedata{$file}; 379*d9497217SMartin Matuska 380*d9497217SMartin Matuska push @rows, [ 381*d9497217SMartin Matuska padr($data, 'path'), 382*d9497217SMartin Matuska '|', colorpct(padl($data, 'lr')), 383*d9497217SMartin Matuska padl($data, 'lf'), padl($data, 'lh'), 384*d9497217SMartin Matuska '|', colorpct(padl($data, 'brr')), 385*d9497217SMartin Matuska padl($data, 'brf'), padl($data, 'brh'), 386*d9497217SMartin Matuska '|', colorpct(padl($data, 'fnr')), 387*d9497217SMartin Matuska padl($data, 'fnf'), padl($data, 'fnh'), 388*d9497217SMartin Matuska ]; 389*d9497217SMartin Matuska} 390*d9497217SMartin Matuska 391*d9497217SMartin Matuska# And print them all out! 392*d9497217SMartin Matuskasay "@$_" for @rows; 393