Skip to content

Commit

Permalink
Support for code coverage
Browse files Browse the repository at this point in the history
Summary:
This diff adds code coverage support to MySQL code repos.

The following options are added to mysql-test-run.pl:

  * **--coverage:** the main option to invoke code coverage mode.

  * **--coverage-scope=<scope_option>:** indicates the scope of the coverage run which is described by the value of *scope_option* which could take one of the following values:
    * **full:** indicates that the code coverage is generated for the complete source code. Specifying this option requires '--coverage' also to be specified.
    * **diff:** indicates that the code coverage is to be generated only for the files modified by the git commit hash 'HEAD'. Specifying this option requires '--coverage' also to be specified.
    * **diff:<commit_hash>:** indicates that the code coverage is to be generated only for the files modified by the git commit hash 'commit_hash'. Specifying this option requires '--coverage' also to be specified.

  * **--coverage-format=<text|html>:** indicates the format of the coverage report which can be either 'text' or 'html'. If this option is not specified then 'text' format is treated as the default. Specifying this option requires '--coverage' also to be specified.

* **--coverage-src-path=<dir>:** indicates the directory path for the source files of code coverage. This is required for the diff level code coverage so that the list of files specified can be accessed by the coverage command.

* **--coverage-llvm-path=<dir>:** indicates the directory path for the llvm binaries used to generate the coverage reports.

If the option *'--coverage-scope'* is not specified then *'--coverage-scope=full'* is treated as the default option. Similarly, if *'--coverage-format'* option is not specified then *'--coverage-format=text'* is treated as the default option.

The coverage report is generated in the directory *'_build-5.6-Coverage/reports/last'* which points to another directory in *'_build-5.6-Coverage/reports/'*. For every run of the coverage run, a new directory is created in *'_build-5.6-Coverage/reports/'* based on the epoch value returned by perl function *'time()'*.

For git diff level code coverage, the list of files modified by the diff are extracted using the command *'git diff --name-only commit_hash^ commit_hash'* and only those files are included to generate the coverage report.

The code coverage report is generated in two steps using the following commands:

* ```llvm-prof merge --output=file.profdata <list of code*.profraw>```
* ```llvm-cov show <binary_path> --instr-profile=file.profdata <optional_list_of_files_for_diff_level_coverage> --format <text|html> --output-dir=<output_dir>```

The report generated is stored in *'_build-5.6-Coverage/last/index[.txt|.html]'*. For every source files that is included in the coverage report, the annotated source file that shows line level coverage is stored in the same directory.

Reviewed By: luqun

Differential Revision: D27247420

fbshipit-source-id: f70b2f012dc
  • Loading branch information
Satya Valluri authored and facebook-github-bot committed Mar 24, 2021
1 parent 64ba7e2 commit 9dbfb25
Show file tree
Hide file tree
Showing 3 changed files with 292 additions and 1 deletion.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -166,3 +166,6 @@ tests/mysql_client_test

# Clangd code completion index.
/.clangd

# LLVM coverage files
*.profraw
220 changes: 220 additions & 0 deletions mysql-test/lib/mtr_coverage.pl
Original file line number Diff line number Diff line change
@@ -0,0 +1,220 @@
use strict;

# Extract coverage option
#
# Arguments:
# $option coverage option
# $delim delimiter for splitting the string
# $option_error error to be printed in case of failure
sub coverage_extract_option {
my ($option, $delim, $option_error) = @_;
# check for sanity of option which should be of the format --option=value
if (length($option) == 0) {
print "**** ERROR **** ",
"Invalid coverage option specified for $option_error\n";
exit(1);
}

# split the string on delimiter '='
my @option_arr = split(/$delim/, $option);
$option=$option_arr[$#option_arr];

return $option;
}

# Prepare to generate coverage data
#
# Arguments:
# $dir basedir, normally the build directory
# $scope coverage option which is of the form:
# * full : complete code coverage
# * diff : coverage of the git diff HEAD
# * diff:<commit_hash> : coverage of git diff commit_hash
# $src_path directory path for coverage source files
# $llvm_path directory for llvm coverage binaries
# $format format for coverage report which is of the form:
# * text : text format
# * html : html format
sub coverage_prepare($$) {
my ($dir, $scope, $src_path, $llvm_path, $format) = @_;

print "Purging coverage information from '$dir'...\n";
system("find $dir -name \"code\*.profraw\" | xargs rm");

my $scope = coverage_extract_option($scope, "=", "coverage-scope");

my $commit_hash = "HEAD"; # default commit hash is 'HEAD'
# if the coverage scope is "--full" then extract the git commithash
if ($scope =~ m/^diff/) {
my $invalid_commit_hash = 0; # is this commit hash valid?
# if the coverage scope is of the form 'diff:<commit_hash>'
if ($scope =~ /^diff:/) {
$commit_hash = coverage_extract_option($scope, ":",
"coverage-scope");
# sanity check for commit hash
if (length($commit_hash) == 0) {
$invalid_commit_hash = 1;
}
}
# if the coverage scope is of the form '--diff'
elsif ($scope ne "diff") {
$invalid_commit_hash = 1;
}

if ($invalid_commit_hash) {
print "**** ERROR **** ",
"Invalid coverage scope diff option: $scope\n";
exit(1);
}
}
# make sure that the coverage scope is "--full"
elsif ($scope ne "full") {
print "**** ERROR **** ", "Invalid coverage scope: $scope\n";
exit(1);
}

# Update the scope of the coverage
if ($scope eq "full") {
$_[1] = $scope;
}
else {
$_[1] = $commit_hash;
}

# extract directory for coverage source files
$src_path = coverage_extract_option($src_path, "=", "coverage-src-path")."/";

# Update the coverage src path
$_[2] = $src_path;

$llvm_path = coverage_extract_option($llvm_path, "=", "coverage-llvm-path");

# append "/" at the end of the llvm_path
if (length($llvm_path) > 0 && ! ($llvm_path =~ m/\/$/) ) {
$llvm_path .= "/";
}

# Update the coverage llvm path
$_[3] = $llvm_path;

# extract format for coverage report
$format = coverage_extract_option($format, "=", "coverage-format");

# sanity check for coverage format
if ( ! ( ($format eq "text") || ($format eq "html")) ) {
print "**** ERROR **** ", "Invalid coverage-format option: $format\n";
exit(1);
}

$_[4] = $format;
}

# Get the files modified by a git diff
#
# Arguments:
# $src_dir directory for coverage source files
# $commit_hash git commit hash
sub coverage_get_diff_files ($$) {
my ($src_dir, $commit_hash) = @_;

# command to extract files modified by a git commit hash
my $cmd = "git diff --name-only $commit_hash"."^ $commit_hash";
open(PIPE, "$cmd|");

my $commit_hash_files; # concatenated list of files

while(<PIPE>) {
chomp;
if (/\.h$/ or /\.cc$/) {
$commit_hash_files .= $src_dir.$_." ";
}
}
return $commit_hash_files;
}

# Collect coverage information
#
# Arguments:
# $test_dir directory of the tests
# $binary_path path to mysqld binary
# $scope coverage option which is of the form:
# * full : complete code coverage
# * HEAD : coverage of the git diff HEAD
# * <commit_hash> : coverage of git diff commit_hash
# $src_path directory path for coverage source files
# $llvm_path directory for llvm coverage binaries
# $format format for coverage report which is of the form:
# * text : text format
# * html : html format
sub coverage_collect ($$$) {
my ($test_dir, $binary_path, $scope, $src_path, $llvm_path, $format) = @_;

my $files_modified=""; # list of files modified concatenated into one string

if ($scope ne "full") {
$files_modified = coverage_get_diff_files($src_path, $scope);
}

print "Generating coverage information";
if ($scope eq "full") {
print "for complete source code ";
}
else {
print "for git commit hash $scope";
if ($scope eq "HEAD") {
# command to extract git commit hash of 'HEAD'
my $cmd = "git rev-parse $scope";
open(PIPE, "$cmd|");
my $head_commit_hash = <PIPE>;
chomp($head_commit_hash);
print " ($head_commit_hash)";
}
}
print " ...\n";

# Create directory to store the coverage results
my $result_dir = "$test_dir/reports/".time();
my $mkdir_cmd = "mkdir -p $result_dir";
system($mkdir_cmd);

# Recreate the 'last' directory to point to the latest coverage
# results directory
my $rm_link_cmd = "rm -f $test_dir/reports/last";
system($rm_link_cmd);

my $create_link_cmd = "ln -s $result_dir $test_dir/reports/last";
system($create_link_cmd);

# Merge coverage reports using command
# llvm-prof merge --output=file.profdata <list of code*.profraw>
my $merge_cov = $llvm_path."llvm-profdata merge --output=";
$merge_cov .= $result_dir."/combined.profdata ";
$merge_cov .= "`find $test_dir -name \"code*.profraw\"`";
system("$merge_cov");

# Generate coverage report using command
# llvm-cov show <binary_path> --instr-profile=file.profdata \
# --format <text|html> --output-dir=<output_dir>
my $generate_cov
= $llvm_path."llvm-cov show $binary_path -instr-profile=";
$generate_cov .= $result_dir."/combined.profdata ";

# Add the list of files modified if the coverage report is for
# a specific git diff
if (length($files_modified) > 0) {
$generate_cov .= $files_modified;
}
$generate_cov .= "--format $format"; # coverage report format
$generate_cov .= " --output-dir=$result_dir 2>/dev/null";
system($generate_cov);

# Delete profdata file
my $rm_profdata = "rm -f ".$result_dir."/combined.profdata";
system($rm_profdata);

print "Completed generating coverage information in ",
"$format format.\n";
print "Coverage results directory: $test_dir/reports/last\n";
}

1;
70 changes: 69 additions & 1 deletion mysql-test/mysql-test-run.pl
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@ BEGIN
require "lib/mtr_gcov.pl";
require "lib/mtr_gprof.pl";
require "lib/mtr_misc.pl";
require "lib/mtr_coverage.pl";

$SIG{INT}= sub { mtr_error("Got ^C signal"); };

Expand Down Expand Up @@ -343,6 +344,12 @@ ($)
my $source_dist= 0;
my $new_test_option = 0; # is '--new-tests' option specified

my $coverage_on = 0; # is coverage mode specified?
my $coverage_scope = "--coverage-scope=full"; # default coverage option
my $coverage_src_path = ""; # directory path for coverage source files
my $coverage_llvm_path = ""; # directory path for llvm coverage binaries
my $coverage_format = "--coverage-format=text"; # default coverage format

my $opt_max_save_core= env_or_val(MTR_MAX_SAVE_CORE => 5);
my $opt_max_save_datadir= env_or_val(MTR_MAX_SAVE_DATADIR => 20);
my $opt_max_test_fail= env_or_val(MTR_MAX_TEST_FAIL => 10);
Expand Down Expand Up @@ -382,6 +389,12 @@ sub main {
gcov_prepare($basedir);
}

# Prepare to collect code coverage information
if ($coverage_on) {
coverage_prepare($basedir, $coverage_scope, $coverage_src_path,
$coverage_llvm_path, $coverage_format);
}

if (!$opt_suites) {
$opt_suites= $DEFAULT_SUITES;
}
Expand Down Expand Up @@ -599,6 +612,17 @@ sub main {
$opt_gcov_msg, $opt_gcov_err);
}

# collect code coverage information
if ($coverage_on) {
# if directory for coverage source files is not specified then use $basedir
if (length($coverage_src_path) == 0) {
$coverage_src_path = $basedir;
}
coverage_collect($bindir, find_mysqld($basedir), $coverage_scope,
$coverage_src_path, $coverage_llvm_path,
$coverage_format);
}

if ($ctest_report) {
print "$ctest_report\n";
mtr_print_line();
Expand Down Expand Up @@ -1419,6 +1443,51 @@ sub command_line_setup {
# that the lone '--' separating options from arguments survives,
# simply ignore it.
}
elsif ( $arg eq "--coverage")
{
# --coverage is specified
$coverage_on = 1;
}
elsif ( $arg =~ /^--coverage-scope=/) # --coverage-scope=<>
{
# coverage option can be specified only with '--coverage' option
if (!$coverage_on) {
print "**** ERROR **** ",
"Option $arg is specified without --coverage\n";
exit(1);
}
$coverage_scope = $arg;
}
elsif ( $arg =~ /^--coverage-src-path=/)
{
# coverage source directory
if (!$coverage_on) {
print "**** ERROR **** ",
"Option $arg is specified without --coverage\n";
exit(1);
}
$coverage_src_path = $arg;
}
elsif ( $arg =~ /^--coverage-llvm-path=/)
{
# coverage source directory
if (!$coverage_on) {
print "**** ERROR **** ",
"Option $arg is specified without --coverage\n";
exit(1);
}
$coverage_llvm_path = $arg;
}
elsif ( $arg =~ /^--coverage-format=/)
{
# coverage format
if (!$coverage_on) {
print "**** ERROR **** ",
"Option $arg is specified without --coverage\n";
exit(1);
}
$coverage_format = $arg;
}
elsif ( $arg eq "--new-tests")
{
# Run tests that are modified from the last commit
Expand Down Expand Up @@ -7019,4 +7088,3 @@ ($)

exit(1);
}

0 comments on commit 9dbfb25

Please sign in to comment.