diff --git a/test/define.yaml b/test/define.yaml index 3edc95d4b..9bf01c499 100644 --- a/test/define.yaml +++ b/test/define.yaml @@ -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 diff --git a/test/lib/pgBackRestTest/Common/DefineTest.pm b/test/lib/pgBackRestTest/Common/DefineTest.pm index 1930b8d35..d70296e30 100644 --- a/test/lib/pgBackRestTest/Common/DefineTest.pm +++ b/test/lib/pgBackRestTest/Common/DefineTest.pm @@ -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} = diff --git a/test/lib/pgBackRestTest/Common/JobTest.pm b/test/lib/pgBackRestTest/Common/JobTest.pm index 4bfeb4d53..895e5109d 100644 --- a/test/lib/pgBackRestTest/Common/JobTest.pm +++ b/test/lib/pgBackRestTest/Common/JobTest.pm @@ -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")};