#!/usr/bin/perl
# SPDX-License-Identifier: MIT
use strict;
use warnings;
use Getopt::Long;
BEGIN { $Pod::Usage::Formatter = 'Pod::Text::Termcap'; }
use Pod::Usage;
use Pod::Man;

my $prefix = qr ".*?(linux)\w*/";

my $title = "";

my %used_func;
my %all_func;
my %all_branch;
my %all_line;
my %used_source;
my %record;
my %files;
my @func_regexes;
my @func_exclude_regexes;
my %test_names;
my @src_regexes;
my @src_exclude_regexes;

my $verbose = 0;
my $ignore_unused = 0;
my $skip_func = 0;

sub is_function_excluded($)
{
	return 0 if (!@func_regexes && !@func_exclude_regexes);

	my $func = shift;

	foreach my $r (@func_exclude_regexes) {
		return 1 if ($func =~ m/$r/);
	}

	return 0 if (!@func_regexes);

	foreach my $r (@func_regexes) {
		return 0 if ($func =~ m/$r/);
	}

	return 1;
}

sub filter_file($)
{
	my $s = shift;

	return 0 if (!@src_regexes && !@src_exclude_regexes);

	foreach my $r (@src_exclude_regexes) {
		return 1 if ($s =~ m/$r/);
	}

	return 0 if (!@src_regexes);

	foreach my $r (@src_regexes) {
		return 0 if ($s =~ m/$r/);
	}

	return 1;
}

# Use something that comes before any real function
my $before_sf = "!!!!";

sub parse_info_data($)
{
	my $file = shift;
	my $was_used = 0;
	my $has_func = 0;
	my $ignore = 0;
	my $source = $before_sf;
	my $func = $before_sf;
	my $cur_test = "";

	# First step: parse data

	print "reading $file...\n" if ($verbose);
	open IN, $file or die "can't open $file";
	# For details on .info file format, see "man geninfo"
	# http://ltp.sourceforge.net/coverage/lcov/geninfo.1.php
	while (<IN>) {
		# TN:<test name>
		if (m/^TN:(.*)/) {
			if ($1 ne $cur_test) {
				$cur_test = $1;
				$test_names{$cur_test} = 1;
			}
			$source = $before_sf;
			$func = $before_sf;
			next;
		}

		# SF:<absolute path to the source file>
		if (m/^[SK]F:(.*)/) {
			$source = $1;

			$was_used = 0;
			$has_func = 0;
			$func = $before_sf;
			$files{$source} = 1;

			# Just ignore files explictly set as such
			$ignore = filter_file($source);
			next;
		}

		# End of record
		if (m/^end_of_record/) {
			if (!$source) {
				print "bad end_of_record field at $file, line $. Ignoring...\n";
				next;
			}

			my $s = $source;

			$source = $before_sf;
			$func = $before_sf;

			next if ($ignore);
			next if ($ignore_unused && !$was_used);

			# Mark that the source was not ignored
			$used_source{$s} = 1;
			next;
		}

		next if ($ignore);

		# Function coverage

		# FN:<line number of function start>,<function name>
		# Note: /w+ intentionally removes IPA gcc-optimization names, as
		# this is more related to branch coverage
		if (m/^FN:(-?\d+),(\w+)/) {
			my $ln = $1;

			$func = $2;
			$has_func = 1;

			if (is_function_excluded($func)) {
				$skip_func = 1;
				next;
			}

			$skip_func = 0;

			$record{$source}{$func}{fn} = $ln;
			$all_func{$func}{$source}->{ln} = $ln;
			next;
		}

		# Parse functions that were actually used
		# FNDA:<execution count>,<function name>
		# Note: /w+ intentionally removes IPA gcc-optimization names, as
		# this is more related to branch coverage
		if (m/^FNDA:(-?\d+),(\w+)/) {
			my $count = $1;

			# Negative gcov results are possible, as reported at:
			# https://gcc.gnu.org/bugzilla/show_bug.cgi?id=67937
			# Lcov ignores those. So, let's do the same here.
			next if ($count <= 0);

			$func = $2;
			$has_func = 1;

			if (is_function_excluded($func)) {
				$skip_func = 1;
				next;
			}

			$skip_func = 0;
			$was_used = 1;

			$record{$source}{$func}{fnda} += $count;
			$used_func{$func}{$source}->{count} += $count;
			next;
		}

		# Ignore data from skipped functions
		next if ($skip_func);

		# Ignore DA/BRDA that aren't associated with functions
		# Those are present on header files (maybe defines?)
		next if (@func_regexes && !$has_func);

		# FNF:<number of functions found>
		if (m/^FNF:(-?\d+)/) {
			$record{$source}{$func}{fnf} = $1;
			next;
		}
		# FNH:<number of function hit>
		if (m/^FNH:(-?\d+)/) {
			my $hits = $1;
			if (!defined($record{$source}{$func}{fnh}) || $record{$source}{$func}{fnh} < $hits) {
				$record{$source}{$func}{fnh} = $hits;
			}
			next;
		}

		# Branch coverage

		# BRDA:<line number>,<block number>,<branch number>,<taken>
		if (m/^BRDA:(-?\d+),(-?\d+),(-?\d+),(.*)/) {
			my $ln = $1;
			my $block = $2;
			my $branch = $3;
			my $taken = $4;

			my $where = "$ln,$block,$branch";

			$taken = 0 if ($taken eq '-');

			# Negative gcov results are possible, as reported at:
			# https://gcc.gnu.org/bugzilla/show_bug.cgi?id=67937
			# Lcov ignores those. So, let's do the same here.
			$taken = 0 if ($taken < 0);

			$was_used = 1 if ($taken > 0);

			$record{$source}{$func}{brda}{$where} += $taken;
			$all_branch{$source}{"$where"} += $taken;
			next;
		}

		# BRF:<number of branches found>
		if (m/^BRF:(-?\d+)/) {
			$record{$source}{$func}{brf} = $1;
			next;
		}
		# BRH:<number of branches hit>
		if (m/^BRH:(-?\d+)/) {
			my $hits = $1;
			if (!defined($record{$source}{$func}{brh}) || $record{$source}{$func}{brh} < $hits) {
				$record{$source}{$func}{brh} = $hits;
			}
			next;
		}

		# Line coverage

		# DA:<line number>,<execution count>[,<checksum>]
		if (m/^DA:(-?\d+),(-?\d+)(,.*)?/) {
			my $ln = $1;
			my $count = $2;

			# Negative gcov results are possible, as reported at:
			# https://gcc.gnu.org/bugzilla/show_bug.cgi?id=67937
			# Lcov ignores those. So, let's do the same here.
			$count = 0 if ($count < 0);

			$was_used = 1 if ($count > 0);

			$record{$source}{$func}{da}{$ln} += $count;
			$all_line{$source}{"$ln"} += $count;
			next;
		}

		# LF:<number of instrumented lines>
		if (m/^LF:(-?\d+)/) {
			$record{$source}{$func}{lf} = $1;
			next;
		}

		# LH:<number of lines with a non-zero execution count>
		if (m/^LH:(-?\d+)/) {
			my $hits = $1;
			if (!defined($record{$source}{$func}{lh}) || $record{$source}{$func}{lh} < $hits) {
				$record{$source}{$func}{lh} = $hits;
			}
			next;
		}

		printf("Warning: invalid line: $_");
	}

	close IN or die;
}

sub write_filtered_file($)
{
	my $filter = shift;

	my $filtered = "";

	if ($title eq "") {
		foreach my $testname(sort keys %test_names) {
			$filtered .= "TN:$testname\n";
		}
	} else {
		$filtered .= "TN:$title\n";
	}

	# Generates filtered data
	foreach my $source(sort keys %record) {
		next if (!$used_source{$source});

		if ($source ne $before_sf) {
			$filtered .= "SF:$source\n";
		}

		foreach my $func(sort keys %{ $record{$source} }) {
			if ($func ne $before_sf) {
				my $fn;
				my $fnda;

				if (defined($record{$source}{$func}{fn})) {
					$filtered .= "FN:" . $record{$source}{$func}{fn} . ",$func\n";
				}
				if (defined($record{$source}{$func}{fnda})) {
					$filtered .= "FNDA:" . $record{$source}{$func}{fnda} . ",$func\n";
				}
				if ($record{$source}{fnf}) {
					$filtered .= "FNF:". $record{$source}{$func}{fnf} ."\n";
				}
				if ($record{$source}{fnh}) {
					$filtered .= "FNH:". $record{$source}{$func}{fnh} ."\n";
				}
			}

			foreach my $ln(sort keys %{ $record{$source}{$func}{da} }) {
				$filtered .= "DA:$ln," . $record{$source}{$func}{da}{$ln} . "\n";
			}
			foreach my $where(sort keys %{ $record{$source}{$func}{brda} }) {
				my $taken = $record{$source}{$func}{brda}{$where};
				$taken = "-" if (!$taken);
				$filtered .= "BRDA:$where,$taken\n";
			}
			if ($record{$source}{$func}{brf}) {
				$filtered .= "BRF:". $record{$source}{$func}{brf} ."\n";
			}
			if ($record{$source}{$func}{brh}) {
				$filtered .= "BRH:". $record{$source}{$func}{brh} ."\n";
			}
			if ($record{$source}{$func}{lf}) {
				$filtered .= "LF:". $record{$source}{$func}{lf} ."\n";
			}
			if ($record{$source}{$func}{lh}) {
				$filtered .= "LH:". $record{$source}{$func}{lh} ."\n";
			}
		}

		$filtered .= "end_of_record\n";
	}
	open OUT, ">$filter" or die "Can't open $filter";
	print OUT $filtered or die "Failed to write to $filter";
	close OUT or die "Failed to close to $filter";
}

sub print_code_coverage($$$)
{
	my $print_used = shift;
	my $print_unused = shift;
	my $show_lines = shift;

	return if (!$print_used && !$print_unused);

	my $prev_file = "";

	foreach my $func (sort keys(%all_func)) {
		my @keys = sort keys(%{$all_func{$func}});
		foreach my $file (@keys) {
			my $count = 0;
			my $name;

			if ($used_func{$func}) {
				if ($used_func{$func}->{$file}) {
					$count = $used_func{$func}->{$file}->{count};
				}
			}

			if ($show_lines) {
				my $ln = $all_func{$func}{$file}->{ln};
				$file =~ s,$prefix,linux/,;
				$name = "$func() from $file";
				$name .= ":" . $ln if ($ln);
			} elsif (scalar @keys > 1) {
				$file =~ s,$prefix,linux/,;
				$name = "$func() from $file:";
			} else {
				$name = "$func():";
			}
			if ($print_unused) {
				if (!$count) {
					print "$name unused\n";
				} elsif ($print_used) {
					print "$name executed $count times\n";
				}
			} elsif ($count) {
				print "$name executed $count times\n";
			}
		}
	}
}

my %stats;

sub gen_stats()
{
	# per-line coverage statistics
	$stats{"line_count"} = 0;
	$stats{"line_reached"} = 0;

	foreach my $source (keys(%all_line)) {
		next if (!$used_source{$source});

		foreach my $where (keys(%{$all_line{$source}})) {
			$stats{"line_count"}++;
			$stats{"line_reached"}++ if ($all_line{$source}{$where} != 0);
		}
	}

	# per-function coverage statistics
	$stats{"func_count"} = 0;
	$stats{"func_used"} = 0;

	foreach my $func (keys(%all_func)) {
		foreach my $file (keys(%{$all_func{$func}})) {
			$stats{"func_count"}++;
			if ($used_func{$func}) {
				if ($used_func{$func}->{$file}) {
					$stats{"func_used"}++;
				}
			}
		}
	}

	# per-branch coverage statistics
	$stats{"branch_count"} = 0;
	$stats{"branch_reached"} = 0;

	foreach my $source (keys(%all_branch)) {
		next if (!$used_source{$source});

		foreach my $where (keys(%{$all_branch{$source}})) {
			$stats{"branch_count"}++;
			$stats{"branch_reached"}++ if ($all_branch{$source}{$where} != 0);
		}
	}

	# per-file coverage stats
	$stats{"all_files"} = scalar keys(%files);
	$stats{"filtered_files"} = scalar keys(%record);
	$stats{"used_files"} = scalar keys(%used_source);
}

sub print_summary()
{
	if ($stats{"line_count"}) {
		my $percent = 100. * $stats{"line_reached"} / $stats{"line_count"};
		printf "  lines......: %.1f%% (%d of %d lines)\n",
			$percent, $stats{"line_reached"}, $stats{"line_count"};
	} else {
		print "No line coverage data.\n";
	}

	if ($stats{"func_count"}) {
		my $percent = 100. * $stats{"func_used"} / $stats{"func_count"};
		printf "  functions..: %.1f%% (%d of %d functions)\n",
			$percent, $stats{"func_used"}, $stats{"func_count"};
	} else {
		print "No functions reported. Wrong filters?\n";
		return;
	}

	if ($stats{"branch_count"}) {
		my $percent = 100. * $stats{"branch_reached"} / $stats{"branch_count"};
		printf "  branches...: %.1f%% (%d of %d branches)\n",
			$percent, $stats{"branch_reached"}, $stats{"branch_count"};
	} else {
		print "No branch coverage data.\n";
	}
}

sub open_filter_file($$$)
{
	my $fname = shift;
	my $include = shift;
	my $exclude = shift;
	my $match_str = "";
	my $not_match_str = "";
	my $filter = "";
	my $i;

	# Handle regexes that came from command line params

	for ($i = 0;$i < scalar(@{$include}); $i++) {
		my $op = @{$include}[$i];
		$match_str .= sprintf "m`$op` ";
		@{$include}[$i] = qr /$op/;
	}

	for ($i = 0;$i < scalar(@{$exclude}); $i++) {
		my $op = @{$exclude}[$i];
		$not_match_str .= sprintf "m`$op` ";
		@{$exclude}[$i] = qr /$op/;
	}

	if ($fname) {
		open IN, $fname or die "Can't open $fname";
		while (<IN>) {
			s/^\s+//;
			s/\s+$//;
			next if (m/^#/ || m/^$/);
			if (m/^([+\-])\s*(.*)/) {
				if ($1 eq "+") {
					$match_str .= sprintf "m`$2` ";
					push @{$include}, qr /$2/;
				} else {
					$not_match_str .= sprintf "m`$2` ";
					push @{$exclude}, qr /$2/;
				}
			} else {
				$match_str .= sprintf "m`$_` ";
				push @{$include}, qr /$_/;
			}
		}
		close IN;
	}

	$filter .= "not match: $not_match_str" if ($not_match_str);
	if ($match_str) {
		$filter .= "and " if ($filter ne "");
		$filter .= "match: $match_str";
	}

	$filter =~ s/\s+$//;

	return $filter;
}

my $gen_report;
my $css_file;
my $html_prolog;
my $html_epilog;
my %report;

sub generate_report($)
{
	my $filter_str = shift;
	my $percent;
	my $prolog = "";
	my $epilog = "";
	my @info_files = sort(keys %report);

	$title = "Code coverage results" if ($title eq "");

	if ($html_prolog) {
		open IN, $html_prolog or die "Can't open prolog file";
		$prolog .= $_ while (<IN>);
		close IN;
	}

	if ($html_epilog) {
		open IN, $html_epilog or die "Can't open epilog file";
		$epilog .= $_ while (<IN>);
		close IN;
	}

	# Re-generate the hashes used to report stats in order to procuce the
	# Total results

	%used_func = ();
	%all_func = ();
	%all_branch = ();
	%all_line = ();
	%used_source = ();
	%files = ();
	%test_names = ();
	my @all_func_keys;
	my %info_files_with_func;

	foreach my $f (@info_files) {
		foreach my $source (keys(%{$report{$f}{"all_line"}})) {
			$used_source{$source} = 1 if ($report{$f}{"used_source"});
			foreach my $where (keys(%{$report{$f}{"all_line"}{$source}})) {
				$all_line{$source}{$where} += $report{$f}{"all_line"}{$source}{$where};
			}
		}
		foreach my $func (keys(%{$report{$f}{"all_func"}})) {
			foreach my $file (keys(%{$report{$f}{"all_func"}{$func}})) {
				$all_func{$func}{$file}->{ln} = $report{$f}{"all_func"}{$func}{$file}->{ln};
				$used_func{$func}->{$file} = 1 if ($report{$f}{"used_func"}{$func}->{$file});
				if ($report{$f}{"used_func"}{$func}->{$file}) {
					$used_func{$func}->{$file} = 1;
					if (!$info_files_with_func{"$file $func"}) {
						$info_files_with_func{"$file $func"} = 1;
					} else {
						$info_files_with_func{"$file $func"}++;
					}
				}
			}
		}
		foreach my $source (keys(%{$report{$f}{"all_branch"}})) {
			foreach my $where (keys(%{$report{$f}{"all_branch"}{$source}})) {
				$all_branch{$source}{"$where"} += $report{$f}{"all_branch"}{$source}{$where};
			}
		}
		for my $source(keys(%{$report{$f}{"files"}})) {
			$files{$source} = 1;
			$used_source{$source} = 1 if ($report{$f}{"used_source"}{$source});
		}
		for my $test(keys(%{$report{$f}{"test_names"}})) {
			$test_names{$test} = 1;
		}
	}
	gen_stats();

	# Gen code coverage set comparision counters
	my $common_func_count = 0;
	foreach my $k (keys %info_files_with_func) {
		$common_func_count++ if ($info_files_with_func{$k} == scalar (@info_files));
	}
	foreach my $f (@info_files) {
		$report{$f}{"more_func"} = 0;
		$report{$f}{"uniq_func"} = 0;
		foreach my $func (keys(%{$report{$f}{"all_func"}})) {
			foreach my $file (keys(%{$report{$f}{"all_func"}{$func}})) {
				next if (!$report{$f}{"used_func"}{$func}->{$file});
				next if ($info_files_with_func{"$file $func"} == scalar (@info_files));

				$report{$f}{"more_func"}++;
				$report{$f}{"uniq_func"}++ if ($info_files_with_func{"$file $func"} == 1);
			}
		}
	}

	# Colors for the html output

	my $red    = "style=\"background-color:#ffb3b3\"";
	my $yellow = "style=\"background-color:#ffffb3\"";
	my $green  = "style=\"background-color:#d9ffd9\"";

	# Open report file

	open OUT, ">$gen_report" or die "Can't open $gen_report";

	print OUT "<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01 Transitional//EN\">\n";

	print OUT "<html lang=\"en\">\n\n";
	print OUT "<head>\n";
	print OUT "  <meta http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\">\n";
	print OUT "  <title>$title</title>\n";
	print OUT "  <link rel=\"stylesheet\" type=\"text/css\" href=\"$css_file\">\n" if ($css_file);
	print OUT "</head>\n\n<body>\n$prolog";

	print OUT "  <h1>$title</h1>\n";

	print OUT "  <h2>Summary</h2>\n";
	# Generates a table containing the code coverage statistics per input

	print OUT "<table width=\"100%\" border=1 cellspacing=0 cellpadding=0>\n  <tr>\n";
	print OUT "    <th></th>\n";
	foreach my $f (@info_files) {
		print OUT "    <th>$f</th>\n";
	}
	print OUT "    <th>TOTAL</th>\n";
	print OUT "    <th>Total count</th>\n";
	print OUT "  </tr><tr>\n";

	print OUT "    <td><b>Functions</b></td>\n";
	foreach my $f (@info_files) {
		my %st = %{$report{$f}{"stats"}};
		if ($st{"func_count"}) {
			$percent = 100. * $st{"func_used"} / $st{"func_count"};

			printf OUT "    <td>%.1f%%</td>\n", $percent;
		} else {
			print OUT "    <td>N. A.</td>\n";
		}
	}
	if ($stats{"func_count"}) {
		$percent = 100. * $stats{"func_used"} / $stats{"func_count"};

		printf OUT "    <td>%.1f%%</td>\n", $percent;
	} else {
		print OUT "    <td>N. A.</td>\n";
	}
	print OUT "    <td>" . $stats{"func_count"} . "</td>\n";
	print OUT "  </tr><tr>\n";

	print OUT "    <td><b>Branches</b></td>\n";
	foreach my $f (@info_files) {
		my %st = %{$report{$f}{"stats"}};
		if ($st{"branch_count"}) {
			$percent = 100. * $st{"branch_reached"} / $st{"branch_count"};

			printf OUT "    <td>%.1f%%</td>\n", $percent;
		} else {
			print OUT "    <td>N. A.</td>\n";
		}
	}
	if ($stats{"branch_count"}) {
		$percent = 100. * $stats{"branch_reached"} / $stats{"branch_count"};

		printf OUT "    <td>%.1f%%</td>\n", $percent;
	} else {
		print OUT "    <td>N. A.</td>\n";
	}
	print OUT "    <td>" . $stats{"branch_count"} . "</td>\n";
	print OUT "  </tr><tr>\n";

	print OUT "    <td><b>Lines</b></td>\n";
	foreach my $f (@info_files) {
		my %st = %{$report{$f}{"stats"}};

		if ($st{"line_count"}) {
			$percent = 100. * $st{"line_reached"} / $st{"line_count"};

			printf OUT "    <td>%.1f%%</td>\n", $percent;
		} else {
			print OUT "    <td>N. A.</td>\n";
		}
	}
	if ($stats{"line_count"}) {
		$percent = 100. * $stats{"line_reached"} / $stats{"line_count"};

		printf OUT "    <td>%.1f%%</td>\n", $percent;
	} else {
		print OUT "    <td>N. A.</td>\n";
	}
	print OUT "    <td>" . $stats{"line_count"} . "</td>\n";

	# If there are more than one tests per file, report them
	my $total = scalar(keys %test_names);
	if ($total > 1) {
		print OUT "  </tr><tr>\n";
		print OUT "    <td><b>Number of tests</b></td>\n";
		foreach my $f (@info_files) {
			my $count = scalar(keys %{$report{$f}{"test_names"}});

			if ($count == 0) {
				print OUT "    <td $red>$count</td>\n";
			} elsif ($count < $total) {
				print OUT "    <td $yellow>$count</td>\n";
			} else {
				print OUT "    <td $green>$count</td>\n";
			}
		}
		print OUT "    <td $green\>$total</td>\n";

	}
	print OUT "  </tr>\n</table><p/>\n\n";

	# Print function diff
	print OUT "  <h2>Differences on function coverage</h2>\n";
	print OUT "<table width=\"100%\" border=1 cellspacing=0 cellpadding=0>\n  <tr>\n";
	print OUT "    <th></th>\n";
	print OUT "    <th>Common to all</th>\n";
	foreach my $f (@info_files) {
		print OUT "    <th>$f</th>\n";
	}
	print OUT "    <th>TOTAL</th>\n";

	print OUT "  </tr><tr>\n";
	print OUT "    <td>#Functions per category</td>\n";

	printf OUT "    <td>%d</td>\n", $common_func_count;

	foreach my $f (@info_files) {
		my %st = %{$report{$f}{"stats"}};
		if ($st{"func_count"}) {
			printf OUT "    <td>%d</td>\n", $st{"func_used"};

		}
	}
	print OUT "    <td>" . $stats{"func_count"} . "</td>\n";

	print OUT "  </tr><tr>\n";
	print OUT "    <td>#functions not in common</td>\n";
	print OUT "    <td></td>\n";
	foreach my $f (@info_files) {
		my %st = %{$report{$f}{"stats"}};
		if ($st{"func_count"}) {
			printf OUT "    <td>%d</td>\n", $report{$f}{"more_func"};

		}
	}
	print OUT "    <td></td>\n";

	print OUT "  </tr><tr>\n";
	print OUT "    <td>Unique functions</td>\n";
	print OUT "    <td></td>\n";
	foreach my $f (@info_files) {
		my %st = %{$report{$f}{"stats"}};
		if ($st{"func_count"}) {
			printf OUT "    <td>%d</td>\n", $report{$f}{"uniq_func"};

		}
	}
	print OUT "    <td></td>\n";

	print OUT "  </tr>\n</table><p/>\n\n";

	# Print the filters applied when generating the report

	if ($filter_str ne "") {
		printf OUT "<p>Filters: %s.</p>\n", $filter_str;
	} else {
		print OUT "<p>(unfiltered results)</p>";
	}

	if ($total > 1) {
		print OUT "<h2>Tests coverage</h2>\n";

		print OUT "<table width=\"100%\" border=1 cellspacing=0 cellpadding=0>\n  <tr>\n";
		print OUT "    <th>Test name</th>\n";
		foreach my $f (@info_files) {
			print OUT "    <th>$f</th>\n";
		}

		foreach my $t (sort keys(%test_names)) {
			print OUT "  </tr><tr>\n";
			printf OUT "    <td>%s</td>\n", $t;
			foreach my $f (@info_files) {
				if (%{$report{$f}{"test_names"}}{$t}) {
					print OUT "    <td $green>YES</td>\n";
				} else {
					print OUT "    <td $red>NO</td>\n";
				}
			}
		}
		print OUT "</tr></table>\n";
	}


	# Generates a table containing per-function detailed data

	print OUT "<h2>Functions coverage</h2>\n";
	print OUT "<table width=\"100%\" border=1 cellspacing=0 cellpadding=0>\n  <tr>\n";
	print OUT "    <th>Function</th>\n";
	print OUT "    <th>Used?</th>\n";
	foreach my $f (@info_files) {
		print OUT "    <th>$f</th>\n";
	}
	print OUT "    <th>File</th>\n";

	foreach my $func (sort keys(%all_func)) {
		my @keys = sort keys(%{$all_func{$func}});
		foreach my $file (@keys) {
			print OUT "  </tr><tr>\n";
			print OUT "    <td>$func</td>\n";
			if ($used_func{$func}->{$file}) {
				print OUT "    <td $green>YES</td>\n";
			} else {
				print OUT "    <td $red>NO</td>\n";
			}
			foreach my $f (@info_files) {
				if ($report{$f}{"used_func"}{$func}->{$file}) {
					print OUT "    <td $green>YES</td>\n";
				} else {
					print OUT "    <td $red>NO</td>\n";
				}
			}
			$file =~ s,$prefix,linux/,;
			print OUT "    <td>$file</td>\n";
		}
	}
	print OUT "</tr></table>\n";

	print OUT "$epilog</body>\n";

	# Close the file and exit

	close OUT;
}

#
# Argument handling
#

my $print_used;
my $print_unused;
my $stat;
my $filter;
my $help;
my $man;
my $func_filters;
my $src_filters;
my $show_files;
my $show_lines;
my $only_i915;
my $only_drm;

GetOptions(
	"print-coverage|print_coverage|print|p" => \$print_used,
	"print-unused|u" => \$print_unused,
	"stat|statistics" => \$stat,
	"output|o=s" => \$filter,
	"verbose|v" => \$verbose,
	"ignore-unused|ignore_unused" => \$ignore_unused,
	"only-i915|only_i915" => \$only_i915,
	"only-drm|only_drm" => \$only_drm,
	"func-filters|f=s" => \$func_filters,
	"include-func=s" => \@func_regexes,
	"exclude-func=s" => \@func_exclude_regexes,
	"source-filters|S=s" => \$src_filters,
	"include-source=s" => \@src_regexes,
	"exclude-source=s" => \@src_exclude_regexes,
	"show-files|show_files" => \$show_files,
	"show-lines|show_lines" => \$show_lines,
	"report|r=s" => \$gen_report,
	"css-file|css|c=s" => \$css_file,
	"title|t=s" => \$title,
	"html-prolog|prolog=s" => \$html_prolog,
	"html-epilog|epilog=s" => \$html_epilog,
	"help" => \$help,
	"man" => \$man,
) or pod2usage(2);

pod2usage(-verbose => 2) if $man;
pod2usage(1) if $help;

if ($#ARGV < 0) {
	print "$0: no input files\n";
	pod2usage(1);
}

# At least one action should be specified
pod2usage(1) if (!$print_used && !$filter && !$stat && !$print_unused && !$gen_report);

pod2usage(1) if ($gen_report && ($print_used || $filter || $stat || $print_unused));

my $filter_str = "";
my $has_filter;
my $str;

if ($only_i915) {
	# Please keep in sync with the documentation
	push @src_exclude_regexes, "selftest";
	push @src_regexes, "drm/i915";
	push @src_regexes, "drm/ttm";
	push @src_regexes, "drm/vgem";
}

if ($only_drm) {
	# Please keep in sync with the documentation
	push @src_exclude_regexes, "trace.*\.h\$";
	push @src_exclude_regexes, "^/drm/";
}

$str = open_filter_file($func_filters, \@func_regexes, \@func_exclude_regexes);
if ($str) {
	$filter_str .= "," if ($filter_str ne "");
	$filter_str .= " function regex ($str)";
	$has_filter = 1;
}

$str = open_filter_file($src_filters, \@src_regexes, \@src_exclude_regexes);
if ($str) {
	$filter_str .= "," if ($filter_str ne "");
	$filter_str .= " source regex ($str)";
	$has_filter = 1;
}

$ignore_unused = 1 if (@func_regexes || @func_exclude_regexes);

if ($ignore_unused) {
	$filter_str .= "," if ($filter_str ne "");
	$filter_str .= " ignored source files where none of its code ran";
	$has_filter = 1;
}

foreach my $f (@ARGV) {
	parse_info_data($f);

	if ($gen_report) {
		$f =~ s,.*/,,;
		$f =~ s/\.info$//;

		gen_stats();

		$report{$f}{"stats"} = { %stats };
		$report{$f}{"all_func"} = { %all_func };
		$report{$f}{"used_func"} = { %used_func };
		$report{$f}{"all_branch"} = { %all_branch };
		$report{$f}{"all_line"} = { %all_line };
		$report{$f}{"used_source"} = { %used_source };
		$report{$f}{"files"} = { %files };
		$report{$f}{"test_names"} = { %test_names };

		%used_func = ();
		%all_func = ();
		%all_branch = ();
		%all_line = ();
		%used_source = ();
		%files = ();
		%test_names = ();
	}
}

$filter_str =~ s/(.*),/$1 and/ if ($filter_str ne "");

if ($gen_report) {
	generate_report($filter_str);
	exit 0;
}

gen_stats();

die "Nothing counted. Wrong input files?" if (!$stats{"all_files"});

print_code_coverage($print_used, $print_unused, $show_lines);

print_summary() if ($stat);

if ($has_filter) {
	my $percent = 100. * $stats{"used_files"} / $stats{"all_files"};

	printf "Filters......:%s.\n", $filter_str;
	printf "Source files.: %.2f%% (%d of %d total)",
		$percent, $stats{"used_files"}, $stats{"all_files"};

	if ($stats{"used_files"} != $stats{"filtered_files"}) {
		my $percent_filtered = 100. * $stats{"used_files"} / $stats{"filtered_files"};

		printf ", %.2f%% (%d of %d filtered)",
			$percent_filtered, $stats{"used_files"}, $stats{"filtered_files"};
	}
	print "\n";
} else {
	printf "Source files: %d\n", scalar keys(%files) if($stat);
}

my $ntests=scalar(%test_names);
printf "Number of tests: %d\n", $ntests if ($ntests > 1);

if ($show_files) {
	for my $f(sort keys %used_source) {
		print "\t$f\n";
	}
}

if ($filter) {
	write_filtered_file($filter);
}

__END__

=head1 NAME

Parses lcov data from .info files.

=head1 SYNOPSIS

code_cov_parse_info <options> [input file(s)]

At least one of the output options should be used, e g.
B<--stat>, B<--print>, B<--print-unused>, B<--report> and/or B<--output>.

Also, B<--report> can't be used together with other output options.

=head1 OPTIONS

=over 8

=item B<--stat> or B<--statistics>

Prints code coverage statistics.

It displays function, line, branch and file coverage percentage.

It also reports when one or more of the filtering parameters are used.

The statistics report is affected by the applied filters.

=item B<--print-coverage> or B<--print_coverage> or B<--print> or B<-p>

Prints the functions that were executed in runtime and how many times
they were reached.

The function coverage report is affected by the applied filters.

=item B<--print-unused> or B<-u>

Prints the functions that were never reached.

The function coverage report is affected by the applied filters.

=item B<--report>  B<[output file]> or B<-r>  B<[output file]>

Generates an html report containing per-test and total statistics.

The function coverage report is affected by the applied filters.

=item B<--css-file> B<[css file]> or B<--css> B<[css file]> or B<-c> B<[css file]

Adds an optional css file to the html report.
Used only with B<--report>.

=item B<--title> B<[title] or B<-t> B<[title]

If used with B<--report>, it defines the title for the for the html report.

If used with B<--output>, it replaces the test names with the title. This
is useful when merging reports from multiple tests into a summarized file.
If not used, the B<[output file]> will contain all test names on its
beginning.

Used with B<--report> AND B<--output>.

=item B<--html-prolog> B<[html file] or B<--prolog> B<[html file]

Adds a prolog at the beginning of the body of the html report.
Used only with B<--report>.

=item B<--html-epilog>  B<[html file] or B<--epilog>  B<[html file]

Adds an epilog before the end of the body of the html report.
Used only with B<--report>.

=item B<--show-lines> or B<--show_lines>

When printing per-function code coverage data, always output the source
file and the line number where the function is defined.

=item B<--output> B<[output file]> or B<-o> B<[output file]>

Produce an output file merging all input files.

The generated output file is affected by the applied filters.

=item B<--only-drm> or B<--only_drm>

Filters out includes outside the DRM subsystem, plus trace files.
E. g. it will exclude *.h files that match the following regular expressions:

	- .*trace.*\.h$

And *.h files that don't match:

	- /drm/

(e. g. macros and other code outside DRM subsystem)

=item B<--only-i915> or B<--only_i915>

Filters out C files and headers outside drm core and drm/i915.

E. g. code coverage results will include only the files that that match
the following regular expressions:

	- drm/i915/
	- drm/ttm
	- drm/vgem

Excluding files that match:

	- selftest

=item B<--func-filters>  B<[filter's file]> or B<-f>  B<[filter's file]>

Use a file containing regular expressions (regex) to filter functions.

Each line at B<[filter's file]> may contain a new regex:

=over 4

- Blank lines and lines starting with B<#> will be ignored;

- Each line of the file will be handled as a new regex;

- If B<+regex> is used, the filter will include B<regex> to the matches;

- If B<-regex> is used, the filter will exclude B<regex> from the matches;

- If the line doesn't start with neither B<+> nor B<->, containing just
  B<regex>, the filter will include B<regex> to the matches.

- Any whitespace/tab before or after B<regex> will be ignored.

=back

When both include and exclude regexes are found, exclude regexes are
applied first and any functions that don't match the include regular
expressions from the B<[filter's file]> will be ignored.

Please notice that, when this filter is used, B<--ignore-unused> will be
automaticaly enabled, as the final goal is to report per-function usage.

=item B<--include-func> B<regex>

Include B<regex> to the function filter. Can be used multiple times.

When used together with B<--func-filters>, regexes here are handled first.

Please notice that, when this filter is used, B<--ignore-unused> will be
automaticaly enabled, as the final goal is to report per-function usage.

=item B<--exclude-func> B<regex>

Include B<regex> to the function filter. Can be used multiple times.

When used together with B<--func-filters>, regexes here are handled first.

Please notice that, when this filter is used, B<--ignore-unused> will be
automaticaly enabled, as the final goal is to report per-function usage.

=item B<--source-filters>  B<[filter's file]> or B<-S>  B<[filter's file]>

Use a file containing regular expressions to filter source files.

Each line of the file will be handled as a new regular expressions.
Blank lines and lines starting with B<#> will be ignored.

Each line at B<[filter's file]> may contain a new regex:

=over 4

- Blank lines and lines starting with B<#> will be ignored;

- Each line of the file will be handled as a new regex;

- If B<+regex> is used, the filter will include B<regex> to the matches;

- If B<-regex> is used, the filter will exclude B<regex> from the matches;

- If the line doesn't start with neither B<+> nor B<->, containing just
  B<regex>, the filter will include B<regex> to the matches.

- Any whitespace/tab before or after B<regex> will be ignored.

=back

When both include and exclude regexes are found, exclude regexes are
applied first and any functions that don't match the include regular
expressions from the B<[filter's file]> will be ignored.

=item B<--include-src> B<regex>

Include B<regex> to the sources filter. Can be used multiple times.

When used together with B<--src-filters>, regexes here are handled first.

=item B<--exclude-src> B<regex>

Include B<regex> to the sources filter. Can be used multiple times.

When used together with B<--src-filters>, regexes here are handled first.

=item B<--ignore-unused> or B<--ignore_unused>

Filters out unused C files and headers from the code coverage results.

Sometimes, it is desired to ignore files where none of the functions on it
were tested.

The rationale is that such files may contain platform-specific drivers
and code that will never be used, so, placing them will just bloat the
report and decrease the code coverage statistics.

This option is automaticaly enabled when B<--func-filters> is used.

=back

=item B<--show-files> or B<--show_files>

Shows the list of files that were used to produce the code coverage
results.

=item B<--verbose> or B<-v>

Prints the name of each parsed file.

=item B<--help>

Print a brief help message and exits.

=item B<--man>

Prints the manual page and exits.

=back

=head1 BUGS

Report bugs to Mauro Carvalho Chehab <mauro.chehab@intel.com>

=head1 COPYRIGHT

Copyright (c) 2022 Intel Corporation

Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the "Software"),
to deal in the Software without restriction, including without limitation
the rights to use, copy, modify, merge, publish, distribute, sublicense,
and/or sell copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice (including the next
paragraph) shall be included in all copies or substantial portions of the
Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL
THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
IN THE SOFTWARE.
=cut
