1
0
mirror of https://github.com/pgbackrest/pgbackrest.git synced 2024-12-12 10:04:14 +02:00

Add shim feature for unit tests.

A shim allows a test harness to access static functions and variables in a C module, and also allows functions to be shimmed (i.e. overridden) for the purposes of testing.

For instance, coverage testing works when a process that is normally exec'd is run as a forked child process instead.
This commit is contained in:
David Steele 2021-05-20 18:47:31 -04:00
parent e31df55c8d
commit cab7a97ab6
3 changed files with 184 additions and 9 deletions

View File

@ -23,6 +23,11 @@
# because it is used to implement the error handling, so it must do error testing in a more primitive way.
# * harness - Adds a harness module that contains functions to aid in testing. For example, the "log" harness includes the
# common/harnessLog module to aid in expect log testing.
# * shim - list of modules that are shimmed in the harness. This allows the harness to access static elements of the module to
# provide additional services for unit testing. A shim can have a 'function' list. In this case the listed functions in the
# C module will be appended with _SHIMMED and an implementation with the same function signature must be provided in the
# harness. Generally speaking this function will default to calling the original function, but after some initialization the
# shim may implement other logic that is useful for unit testing.
# * depend - Source modules that this test depends on that have not already been included in prior tests via coverage. Ideally
# this option would never be used because it is essentially documenting cross-dependencies in the code.
# * total - total runs in the test

View File

@ -60,6 +60,18 @@ use constant TESTDEF_DEBUG_UNIT_SUPPRESS => 'debugUni
push @EXPORT, qw(TESTDEF_DEBUG_UNIT_SUPPRESS);
use constant TESTDEF_HARNESS => 'harness';
push @EXPORT, qw(TESTDEF_HARNESS);
# Harness name which must match the harness implementation file name
use constant TESTDEF_HARNESS_NAME => 'name';
push @EXPORT, qw(TESTDEF_HARNESS_NAME);
# The harness contains shimmed elements
use constant TESTDEF_HARNESS_SHIM => 'shim';
push @EXPORT, qw(TESTDEF_HARNESS_SHIM);
# The harness shim was first defined in the module/test
use constant TESTDEF_HARNESS_SHIM_DEF => 'harnessShimDef';
push @EXPORT, qw(TESTDEF_HARNESS_SHIM_DEF);
# List of shimmed functions for a C module
use constant TESTDEF_HARNESS_SHIM_FUNCTION => 'function';
push @EXPORT, qw(TESTDEF_HARNESS_SHIM_FUNCTION);
use constant TESTDEF_INCLUDE => 'include';
push @EXPORT, qw(TESTDEF_INCLUDE);
use constant TESTDEF_INDIVIDUAL => 'individual';
@ -98,7 +110,7 @@ sub testDefLoad
my $hTestDef = Load($strDefineYaml);
# Keep a list of all harnesses added so far. These will make up the harness list for subsequent tests.
my @stryHarnessFile = ();
my @rhyHarnessFile = ();
# Keep a list of all modules added for coverage so far. These will make up the core list for subsequent tests.
my @stryCoreFile = ();
@ -180,12 +192,46 @@ sub testDefLoad
push(@{$hTestDefHash->{$strModule}{$strTest}{&TESTDEF_CORE}}, @stryCoreFile);
# Add harness files
my $rhHarnessShimDef = {};
if (defined($hModuleTest->{&TESTDEF_HARNESS}))
{
push(@stryHarnessFile, $hModuleTest->{&TESTDEF_HARNESS});
my $rhHarness = {};
# If the harness is a hash then it contains shims
if (ref($hModuleTest->{&TESTDEF_HARNESS}))
{
# Harness name must be set
if (!defined($hModuleTest->{&TESTDEF_HARNESS}{&TESTDEF_HARNESS_NAME}))
{
confess &log(ERROR, "must define 'name' for harness in test '$strTest'");
}
# Don't use hash syntax when there are no shims
if (!defined($hModuleTest->{&TESTDEF_HARNESS}{&TESTDEF_HARNESS_SHIM}))
{
confess &log(
ERROR,
"use 'harness: $hModuleTest->{&TESTDEF_HARNESS}{&TESTDEF_HARNESS_NAME}' if there are no shims");
}
# Note that this shim is defined in the module
$rhHarnessShimDef->{$hModuleTest->{&TESTDEF_HARNESS}{&TESTDEF_HARNESS_NAME}} = true;
# Set the harness
$rhHarness = $hModuleTest->{&TESTDEF_HARNESS};
}
# Else set the harness with just a name
else
{
$rhHarness->{&TESTDEF_HARNESS_NAME} = $hModuleTest->{&TESTDEF_HARNESS};
}
push(@rhyHarnessFile, $rhHarness);
}
push(@{$hTestDefHash->{$strModule}{$strTest}{&TESTDEF_HARNESS}}, @stryHarnessFile);
push(@{$hTestDefHash->{$strModule}{$strTest}{&TESTDEF_HARNESS}}, @rhyHarnessFile);
$hTestDefHash->{$strModule}{$strTest}{&TESTDEF_HARNESS_SHIM_DEF} = $rhHarnessShimDef;
# Add test defines
$hTestDefHash->{$strModule}{$strTest}{&TESTDEF_FEATURE} =

View File

@ -237,6 +237,8 @@ sub run
my $strRepoCopyPath = $self->{strTestPath} . '/repo'; # Path to repo copy
my $strRepoCopySrcPath = $strRepoCopyPath . '/src'; # Path to repo copy src
my $strRepoCopyTestSrcPath = $strRepoCopyPath . '/test/src'; # Path to repo copy test src
my $strShimSrcPath = $self->{strGCovPath} . '/src'; # Path to shim src
my $strShimTestSrcPath = $self->{strGCovPath} . '/test/src'; # Path to shim test src
my $bCleanAll = false; # Do all object files need to be cleaned?
my $bConfigure = false; # Does configure need to be run?
@ -329,11 +331,13 @@ sub run
"\n" .
"\n" .
"INCLUDE =" .
" \\\n\t-I\"${strShimSrcPath}\"" .
" \\\n\t-I\"${strShimTestSrcPath}\"" .
" \\\n\t-I\"${strRepoCopySrcPath}\"" .
" \\\n\t-I\"${strRepoCopyTestSrcPath}\"" .
"\n" .
"\n" .
"vpath \%.c ${strRepoCopySrcPath}:${strRepoCopyTestSrcPath}\n";
"vpath \%.c ${strShimSrcPath}:${strShimTestSrcPath}:${strRepoCopySrcPath}:${strRepoCopyTestSrcPath}\n";
# If Makefile.param has changed then clean all files
if (buildPutDiffers($self->{oStorageTest}, $self->{strGCovPath} . "/Makefile.param", $strMakefileParam))
@ -346,18 +350,125 @@ sub run
my $hTest = (testDefModuleTest($self->{oTest}->{&TEST_MODULE}, $self->{oTest}->{&TEST_NAME}));
my $strRepoCopyTestSrcHarnessPath = $strRepoCopyTestSrcPath . '/common';
# C modules included in harness files that should not be added to the make list
my $rhHarnessCModule = {};
# List of harness files to include in make
my @stryHarnessFile = ('common/harnessTest');
foreach my $strHarness (@{$hTest->{&TESTDEF_HARNESS}})
foreach my $rhHarness (@{$hTest->{&TESTDEF_HARNESS}})
{
my $bFound = false;
my $strFile = "common/harness" . ucfirst($strHarness);
my $strFile = "common/harness" . ucfirst($rhHarness->{&TESTDEF_HARNESS_NAME});
# Include harness file if present
if ($self->{oStorageTest}->exists("${strRepoCopyTestSrcPath}/${strFile}.c"))
my $strHarnessSrcFile = "${strRepoCopyTestSrcPath}/${strFile}.c";
if ($self->{oStorageTest}->exists($strHarnessSrcFile))
{
push(@stryHarnessFile, $strFile);
$bFound = true;
if (!defined($hTest->{&TESTDEF_HARNESS_SHIM_DEF}{$rhHarness->{&TESTDEF_HARNESS_NAME}}))
{
push(@stryHarnessFile, $strFile);
}
# Install shim
my $rhShim = $rhHarness->{&TESTDEF_HARNESS_SHIM};
if (defined($rhShim))
{
my $strHarnessSrc = ${$self->{oStorageTest}->get($strHarnessSrcFile)};
# Error if there is no placeholder for the shimmed modules
if ($strHarnessSrc !~ /\{\[SHIM\_MODULE\]\}/)
{
confess &log(ERROR, "{[SHIM_MODULE]} tag not found in '${strFile}' harness with shims");
}
# Build list of shimmed C modules
my $strShimModuleList = undef;
foreach my $strShimModule (sort(keys(%{$rhShim})))
{
# If there are shimmed elements the C module will need to be updated and saved to the test path
if (defined($rhShim->{$strShimModule}))
{
my $strShimModuleSrc = ${$self->{oStorageTest}->get(
"${strRepoCopySrcPath}/${strShimModule}.c")};
my @stryShimModuleSrcRenamed;
my $strFunctionDeclaration = undef;
my $strFunctionShim = undef;
foreach my $strLine (split("\n", $strShimModuleSrc))
{
# Renamed shimmed functions
foreach my $strFunction (@{$rhShim->{$strShimModule}{&TESTDEF_HARNESS_SHIM_FUNCTION}})
{
# If shimmed function declaration construction is in progress
if (defined($strFunctionShim))
{
# When the beginning of the function block is found, output both the constructed
# declaration and the renamed implementation.
if ($strLine =~ /^{/)
{
push(@stryShimModuleSrcRenamed, trim($strFunctionDeclaration) . ";");
push(@stryShimModuleSrcRenamed, $strFunctionShim);
push(@stryShimModuleSrcRenamed, $strLine);
$strFunctionShim = undef;
}
# Else keep constructing the declaration and implementation
else
{
$strFunctionDeclaration .= "${strLine}\n";
$strFunctionShim .= "${strLine}\n";
}
}
# Else search for shimmed functions
else
{
# If the function to shim is static then we need to create a declaration with the
# original name so references to the original name in the C module will compile.
# This is not necessary for extern'd functions since they should already have a
# declaration in the header file.
if ($strLine =~ /^${strFunction}\(/ && $stryShimModuleSrcRenamed[-1] =~ /^static /)
{
my $strLineLast = pop(@stryShimModuleSrcRenamed);
$strFunctionDeclaration = "${strLineLast} ${strLine}\n";
$strLine =~ s/^${strFunction}\(/${strFunction}_SHIMMED\(/;
$strFunctionShim = "${strLineLast}\n${strLine}\n";
}
# Else just append the line
else
{
push(@stryShimModuleSrcRenamed, $strLine);
}
}
}
}
buildPutDiffers(
$self->{oStorageTest}, "${strShimSrcPath}/${strShimModule}.c",
join("\n", @stryShimModuleSrcRenamed));
}
# Build list to include in the harness
if (defined($strShimModuleList))
{
$strShimModuleList .= "\n";
}
$rhHarnessCModule->{$strShimModule} = true;
$strShimModuleList .= "#include \"${strShimModule}.c\"";
}
# Replace modules and save
$strHarnessSrc =~ s/\{\[SHIM\_MODULE\]\}/${strShimModuleList}/g;
buildPutDiffers($self->{oStorageTest}, "${strShimTestSrcPath}/${strFile}.c", $strHarnessSrc);
}
}
# Include files in the harness directory if present
@ -372,7 +483,7 @@ sub run
# Error when no harness files were found
if (!$bFound)
{
confess &log(ERROR, "no files found for harness '${strHarness}'");
confess &log(ERROR, "no files found for harness '$rhHarness->{&TESTDEF_HARNESS_NAME}'");
}
}
@ -390,6 +501,9 @@ sub run
# Skip if no C file exists
next if !$self->{oStorageTest}->exists("${strRepoCopySrcPath}/${strFile}.c");
# Skip if the C file is included in the harness
next if defined($rhHarnessCModule->{$strFile});
if (!defined($hTestCoverage->{$strFile}) && !grep(/^$strFile$/, @{$hTest->{&TESTDEF_INCLUDE}}))
{
push(@stryCoreFile, $strFile);
@ -449,6 +563,9 @@ sub run
# Don't include vendor files as they are included in regular C files
next if $strFile =~ /vendor$/;
# Skip if the C file is included in the harness
next if defined($rhHarnessCModule->{$strFile});
# Include the C file if it exists
my $strCIncludeFile = "${strFile}.c";
@ -468,6 +585,13 @@ sub run
$strTestDepend .= " ${strCIncludeFile}";
}
# Add harnesses with shims that are first defined in this module
foreach my $strHarness (sort(keys(%{$hTest->{&TESTDEF_HARNESS_SHIM_DEF}})))
{
$strCInclude .=
(defined($strCInclude) ? "\n" : '') . "#include \"common/harness" . ucfirst($strHarness) . ".c\"";
}
# Update C test file with test module
my $strTestC = ${$self->{oStorageTest}->get("${strRepoCopyTestSrcPath}/test.c")};