xref: /freebsd/sys/contrib/openzfs/scripts/coverage_report.pl (revision d9497217456002b0ddad3cd319570d0b098daa29)
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