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:
parent
e31df55c8d
commit
cab7a97ab6
@ -23,6 +23,11 @@
|
|||||||
# because it is used to implement the error handling, so it must do error testing in a more primitive way.
|
# 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
|
# * 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.
|
# 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
|
# * 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.
|
# this option would never be used because it is essentially documenting cross-dependencies in the code.
|
||||||
# * total - total runs in the test
|
# * total - total runs in the test
|
||||||
|
@ -60,6 +60,18 @@ use constant TESTDEF_DEBUG_UNIT_SUPPRESS => 'debugUni
|
|||||||
push @EXPORT, qw(TESTDEF_DEBUG_UNIT_SUPPRESS);
|
push @EXPORT, qw(TESTDEF_DEBUG_UNIT_SUPPRESS);
|
||||||
use constant TESTDEF_HARNESS => 'harness';
|
use constant TESTDEF_HARNESS => 'harness';
|
||||||
push @EXPORT, qw(TESTDEF_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';
|
use constant TESTDEF_INCLUDE => 'include';
|
||||||
push @EXPORT, qw(TESTDEF_INCLUDE);
|
push @EXPORT, qw(TESTDEF_INCLUDE);
|
||||||
use constant TESTDEF_INDIVIDUAL => 'individual';
|
use constant TESTDEF_INDIVIDUAL => 'individual';
|
||||||
@ -98,7 +110,7 @@ sub testDefLoad
|
|||||||
my $hTestDef = Load($strDefineYaml);
|
my $hTestDef = Load($strDefineYaml);
|
||||||
|
|
||||||
# Keep a list of all harnesses added so far. These will make up the harness list for subsequent tests.
|
# 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.
|
# Keep a list of all modules added for coverage so far. These will make up the core list for subsequent tests.
|
||||||
my @stryCoreFile = ();
|
my @stryCoreFile = ();
|
||||||
@ -180,12 +192,46 @@ sub testDefLoad
|
|||||||
push(@{$hTestDefHash->{$strModule}{$strTest}{&TESTDEF_CORE}}, @stryCoreFile);
|
push(@{$hTestDefHash->{$strModule}{$strTest}{&TESTDEF_CORE}}, @stryCoreFile);
|
||||||
|
|
||||||
# Add harness files
|
# Add harness files
|
||||||
|
my $rhHarnessShimDef = {};
|
||||||
|
|
||||||
if (defined($hModuleTest->{&TESTDEF_HARNESS}))
|
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
|
# Add test defines
|
||||||
$hTestDefHash->{$strModule}{$strTest}{&TESTDEF_FEATURE} =
|
$hTestDefHash->{$strModule}{$strTest}{&TESTDEF_FEATURE} =
|
||||||
|
@ -237,6 +237,8 @@ sub run
|
|||||||
my $strRepoCopyPath = $self->{strTestPath} . '/repo'; # Path to repo copy
|
my $strRepoCopyPath = $self->{strTestPath} . '/repo'; # Path to repo copy
|
||||||
my $strRepoCopySrcPath = $strRepoCopyPath . '/src'; # Path to repo copy src
|
my $strRepoCopySrcPath = $strRepoCopyPath . '/src'; # Path to repo copy src
|
||||||
my $strRepoCopyTestSrcPath = $strRepoCopyPath . '/test/src'; # Path to repo copy test 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 $bCleanAll = false; # Do all object files need to be cleaned?
|
||||||
my $bConfigure = false; # Does configure need to be run?
|
my $bConfigure = false; # Does configure need to be run?
|
||||||
@ -329,11 +331,13 @@ sub run
|
|||||||
"\n" .
|
"\n" .
|
||||||
"\n" .
|
"\n" .
|
||||||
"INCLUDE =" .
|
"INCLUDE =" .
|
||||||
|
" \\\n\t-I\"${strShimSrcPath}\"" .
|
||||||
|
" \\\n\t-I\"${strShimTestSrcPath}\"" .
|
||||||
" \\\n\t-I\"${strRepoCopySrcPath}\"" .
|
" \\\n\t-I\"${strRepoCopySrcPath}\"" .
|
||||||
" \\\n\t-I\"${strRepoCopyTestSrcPath}\"" .
|
" \\\n\t-I\"${strRepoCopyTestSrcPath}\"" .
|
||||||
"\n" .
|
"\n" .
|
||||||
"\n" .
|
"\n" .
|
||||||
"vpath \%.c ${strRepoCopySrcPath}:${strRepoCopyTestSrcPath}\n";
|
"vpath \%.c ${strShimSrcPath}:${strShimTestSrcPath}:${strRepoCopySrcPath}:${strRepoCopyTestSrcPath}\n";
|
||||||
|
|
||||||
# If Makefile.param has changed then clean all files
|
# If Makefile.param has changed then clean all files
|
||||||
if (buildPutDiffers($self->{oStorageTest}, $self->{strGCovPath} . "/Makefile.param", $strMakefileParam))
|
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 $hTest = (testDefModuleTest($self->{oTest}->{&TEST_MODULE}, $self->{oTest}->{&TEST_NAME}));
|
||||||
my $strRepoCopyTestSrcHarnessPath = $strRepoCopyTestSrcPath . '/common';
|
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');
|
my @stryHarnessFile = ('common/harnessTest');
|
||||||
|
|
||||||
foreach my $strHarness (@{$hTest->{&TESTDEF_HARNESS}})
|
foreach my $rhHarness (@{$hTest->{&TESTDEF_HARNESS}})
|
||||||
{
|
{
|
||||||
my $bFound = false;
|
my $bFound = false;
|
||||||
my $strFile = "common/harness" . ucfirst($strHarness);
|
my $strFile = "common/harness" . ucfirst($rhHarness->{&TESTDEF_HARNESS_NAME});
|
||||||
|
|
||||||
# Include harness file if present
|
# 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;
|
$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
|
# Include files in the harness directory if present
|
||||||
@ -372,7 +483,7 @@ sub run
|
|||||||
# Error when no harness files were found
|
# Error when no harness files were found
|
||||||
if (!$bFound)
|
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
|
# Skip if no C file exists
|
||||||
next if !$self->{oStorageTest}->exists("${strRepoCopySrcPath}/${strFile}.c");
|
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}}))
|
if (!defined($hTestCoverage->{$strFile}) && !grep(/^$strFile$/, @{$hTest->{&TESTDEF_INCLUDE}}))
|
||||||
{
|
{
|
||||||
push(@stryCoreFile, $strFile);
|
push(@stryCoreFile, $strFile);
|
||||||
@ -449,6 +563,9 @@ sub run
|
|||||||
# Don't include vendor files as they are included in regular C files
|
# Don't include vendor files as they are included in regular C files
|
||||||
next if $strFile =~ /vendor$/;
|
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
|
# Include the C file if it exists
|
||||||
my $strCIncludeFile = "${strFile}.c";
|
my $strCIncludeFile = "${strFile}.c";
|
||||||
|
|
||||||
@ -468,6 +585,13 @@ sub run
|
|||||||
$strTestDepend .= " ${strCIncludeFile}";
|
$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
|
# Update C test file with test module
|
||||||
my $strTestC = ${$self->{oStorageTest}->get("${strRepoCopyTestSrcPath}/test.c")};
|
my $strTestC = ${$self->{oStorageTest}->get("${strRepoCopyTestSrcPath}/test.c")};
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user