####################################################################################################################################
# JobTest.pm - Run a test job and monitor progress
####################################################################################################################################
package pgBackRestTest::Common::JobTest;

####################################################################################################################################
# Perl includes
####################################################################################################################################
use strict;
use warnings FATAL => qw(all);
use Carp qw(confess);
use English '-no_match_vars';

use Cwd qw(abs_path);
use Exporter qw(import);
    our @EXPORT = qw();
use File::Basename qw(dirname basename);
use POSIX qw(ceil);
use Time::HiRes qw(gettimeofday usleep);

use pgBackRestDoc::Common::Exception;
use pgBackRestDoc::Common::Log;
use pgBackRestDoc::Common::String;
use pgBackRestDoc::ProjectInfo;

use pgBackRestTest::Common::BuildTest;
use pgBackRestTest::Common::ContainerTest;
use pgBackRestTest::Common::CoverageTest;
use pgBackRestTest::Common::DbVersion;
use pgBackRestTest::Common::DefineTest;
use pgBackRestTest::Common::ExecuteTest;
use pgBackRestTest::Common::ListTest;
use pgBackRestTest::Common::RunTest;
use pgBackRestTest::Common::VmTest;

####################################################################################################################################
# new
####################################################################################################################################
sub new
{
    my $class = shift;          # Class name

    # Create the class hash
    my $self = {};
    bless $self, $class;

    # Assign function parameters, defaults, and log debug info
    (
        my $strOperation,
        $self->{oStorageTest},
        $self->{strBackRestBase},
        $self->{strTestPath},
        $self->{oTest},
        $self->{bDryRun},
        $self->{bVmOut},
        $self->{iVmIdx},
        $self->{iVmMax},
        $self->{strMakeCmd},
        $self->{iTestIdx},
        $self->{iTestMax},
        $self->{strLogLevel},
        $self->{strLogLevelTest},
        $self->{strLogLevelTestFile},
        $self->{bLogTimestamp},
        $self->{bLogForce},
        $self->{bShowOutputAsync},
        $self->{bNoCleanup},
        $self->{iRetry},
        $self->{bValgrindUnit},
        $self->{bCoverageUnit},
        $self->{bCoverageSummary},
        $self->{bOptimize},
        $self->{bBackTrace},
        $self->{bProfile},
        $self->{iScale},
        $self->{strTimeZone},
        $self->{bDebug},
        $self->{bDebugTestTrace},
        $self->{iBuildMax},
    ) =
        logDebugParam
        (
            __PACKAGE__ . '->new', \@_,
            {name => 'oStorageTest'},
            {name => 'strBackRestBase'},
            {name => 'strTestPath'},
            {name => 'oTest'},
            {name => 'bDryRun'},
            {name => 'bVmOut'},
            {name => 'iVmIdx'},
            {name => 'iVmMax'},
            {name => 'strMakeCmd'},
            {name => 'iTestIdx'},
            {name => 'iTestMax'},
            {name => 'strLogLevel'},
            {name => 'strLogLevelTest'},
            {name => 'strLogLevelTestFile'},
            {name => 'bLogTimestamp'},
            {name => 'bLogForce'},
            {name => 'bShowOutputAsync'},
            {name => 'bNoCleanup'},
            {name => 'iRetry'},
            {name => 'bValgrindUnit'},
            {name => 'bCoverageUnit'},
            {name => 'bCoverageSummary'},
            {name => 'bOptimize'},
            {name => 'bBackTrace'},
            {name => 'bProfile'},
            {name => 'iScale'},
            {name => 'strTimeZone', required => false},
            {name => 'bDebug'},
            {name => 'bDebugTestTrace'},
            {name => 'iBuildMax'},
        );

    # Set try to 0
    $self->{iTry} = 0;

    # Setup the path where gcc coverage will be performed
    $self->{strGCovPath} = "$self->{strTestPath}/gcov-$self->{oTest}->{&TEST_VM}-$self->{iVmIdx}";
    $self->{strDataPath} = "$self->{strTestPath}/data-$self->{iVmIdx}";
    $self->{strRepoPath} = "$self->{strTestPath}/repo";

    # Return from function and log return values if any
    return logDebugReturn
    (
        $strOperation,
        {name => 'self', value => $self, trace => true}
    );
}

####################################################################################################################################
# run
####################################################################################################################################
sub run
{
    my $self = shift;

    # Assign function parameters, defaults, and log debug info
    (my $strOperation) = logDebugParam (__PACKAGE__ . '->run', \@_,);

    # Start the test timer
    my $fTestStartTime = gettimeofday();

    # Was the job run?
    my $bRun = false;

    # Should the job be run?
    $self->{iTry}++;

    if ($self->{iTry} <= ($self->{iRetry} + 1))
    {
        if ($self->{iTry} != 1 && $self->{iTry} == ($self->{iRetry} + 1))
        {
            $self->{strLogLevel} = lc(DEBUG);
        }

        my $strTest = sprintf(
            'P%0' . length($self->{iVmMax}) . 'd-T%0' . length($self->{iTestMax}) . 'd/%0' .
            length($self->{iTestMax}) . "d - ", $self->{iVmIdx} + 1, $self->{iTestIdx} + 1, $self->{iTestMax}) .
            'vm=' . $self->{oTest}->{&TEST_VM} .
            ', module=' . $self->{oTest}->{&TEST_MODULE} .
            ', test=' . $self->{oTest}->{&TEST_NAME} .
            (defined($self->{oTest}->{&TEST_RUN}) ? ', run=' . join(',', sort(@{$self->{oTest}->{&TEST_RUN}})) : '') .
            (defined($self->{oTest}->{&TEST_DB}) ? ', pg-version=' . $self->{oTest}->{&TEST_DB} : '') .
            ($self->{iTry} > 1 ? ' (retry ' . ($self->{iTry} - 1) . ')' : '');

        my $strImage = 'test-' . $self->{iVmIdx};
        my $strDbVersion = (defined($self->{oTest}->{&TEST_DB}) ? $self->{oTest}->{&TEST_DB} : PG_VERSION_94);
        $strDbVersion =~ s/\.//;

        &log($self->{bDryRun} && !$self->{bVmOut} || $self->{bShowOutputAsync} ? INFO : DETAIL, "${strTest}" .
             (!($self->{bDryRun} || !$self->{bVmOut}) || $self->{bShowOutputAsync} ? "\n" : ''));

        my $strVmTestPath = '/home/' . TEST_USER . "/test/${strImage}";
        my $strHostTestPath = "$self->{strTestPath}/${strImage}";

        # Don't create the container if this is a dry run unless output from the VM is required. Output can be requested
        # to get more information about the specific tests that will be run.
        if (!$self->{bDryRun} || $self->{bVmOut})
        {
            # Create host test directory
            $self->{oStorageTest}->pathCreate($strHostTestPath, {strMode => '0770'});

            # Create gcov directory
            my $bGCovExists = true;

            if ($self->{oTest}->{&TEST_C} && !$self->{oStorageTest}->pathExists($self->{strGCovPath}))
            {
                $self->{oStorageTest}->pathCreate($self->{strGCovPath}, {strMode => '0770'});
                $bGCovExists = false;
            }

            # Create data directory
            if ($self->{oTest}->{&TEST_C} && !$self->{oStorageTest}->pathExists($self->{strDataPath}))
            {
                $self->{oStorageTest}->pathCreate($self->{strDataPath}, {strMode => '0770'});
            }

            if ($self->{oTest}->{&TEST_CONTAINER})
            {
                if ($self->{oTest}->{&TEST_VM} ne VM_NONE)
                {
                    my $strBinPath = $self->{strTestPath} . '/bin/' . $self->{oTest}->{&TEST_VM} . '/' . PROJECT_EXE;

                    executeTest(
                        'docker run -itd -h ' . $self->{oTest}->{&TEST_VM} . "-test --name=${strImage}" .
                        " -v ${strHostTestPath}:${strVmTestPath}" .
                        ($self->{oTest}->{&TEST_C} ? " -v $self->{strGCovPath}:$self->{strGCovPath}" : '') .
                        ($self->{oTest}->{&TEST_C} ? " -v $self->{strDataPath}:$self->{strDataPath}" : '') .
                        " -v $self->{strBackRestBase}:$self->{strBackRestBase}" .
                        " -v $self->{strRepoPath}:$self->{strRepoPath}:ro" .
                        ($self->{oTest}->{&TEST_BIN_REQ} ? " -v ${strBinPath}:${strBinPath}:ro" : '') .
                        ' ' . containerRepo() . ':' . $self->{oTest}->{&TEST_VM} . '-test',
                        {bSuppressStdErr => true});
                }
            }
        }

        # Create run parameters
        my $strCommandRunParam = '';

        foreach my $iRunIdx (@{$self->{oTest}->{&TEST_RUN}})
        {
            $strCommandRunParam .= ' --run=' . $iRunIdx;
        }

        if (!$self->{bDryRun} || $self->{bVmOut})
        {
            my $strCommand = undef;                                 # Command to run test

            # If testing C code
            if ($self->{oTest}->{&TEST_C})
            {
                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?

                # If the build.processing file exists then wipe the path to start clean
                # ------------------------------------------------------------------------------------------------------------------
                my $strBuildProcessingFile = $self->{strGCovPath} . "/build.processing";

                # If the file exists then processing terminated before test.bin was run in the last test and the path might be in a
                # bad state.
                if ($self->{oStorageTest}->exists($strBuildProcessingFile))
                {
                    executeTest("find $self->{strGCovPath} -mindepth 1 -print0 | xargs -0 rm -rf");
                }

                # Write build.processing to track processing of this test
                $self->{oStorageTest}->put($strBuildProcessingFile);

                # Create Makefile.in
                # ------------------------------------------------------------------------------------------------------------------
                my $strMakefileIn =
                    "CC_CONFIG = \@CC\@\n" .
                    "CFLAGS_CONFIG = \@CFLAGS\@\n" .
                    "CPPFLAGS_CONFIG = \@CPPFLAGS\@\n" .
                    "LDFLAGS_CONFIG = \@LDFLAGS\@\n" .
                    "LIBS_CONFIG = \@LIBS\@ \@LIBS_BUILD\@\n";

                # If Makefile.in has changed then configure needs to be run and all files cleaned
                if (buildPutDiffers($self->{oStorageTest}, $self->{strGCovPath} . "/Makefile.in", $strMakefileIn))
                {
                    $bConfigure = true;
                    $bCleanAll = true;
                }

                # Create Makefile.param
                # ------------------------------------------------------------------------------------------------------------------
                # Disable debug/coverage for performance and profile tests
                my $bPerformance = $self->{oTest}->{&TEST_TYPE} eq TESTDEF_PERFORMANCE;

                if ($bPerformance || $self->{bProfile})
                {
                    $self->{bDebug} = false;
                    $self->{bDebugTestTrace} = false;
                    $self->{bCoverageUnit} = false;
                }

                # Is coverage being tested?
                my $bCoverage = vmCoverageC($self->{oTest}->{&TEST_VM}) && $self->{bCoverageUnit};

                # Generate Makefile.param
                my $strMakefileParam =
                    "CFLAGS =" .
                        " \\\n\t-DERROR_MESSAGE_BUFFER_SIZE=131072" .
                        ($self->{bProfile} ? " \\\n\t-pg" : '') .
                        (vmArchBits($self->{oTest}->{&TEST_VM}) == 32 ? " \\\n\t-D_FILE_OFFSET_BITS=64" : '') .
                        ($self->{bDebug} ? '' : " \\\n\t-DNDEBUG") .
                        ($self->{oTest}->{&TEST_VM} eq VM_RH7 ? " \\\n\t-DDEBUG_EXEC_TIME" : '') .
                        ($bCoverage ? " \\\n\t-DDEBUG_COVERAGE" : '') .
                        ($self->{bDebugTestTrace} && $self->{bDebug} ? " \\\n\t-DDEBUG_TEST_TRACE" : '') .
                        (vmWithBackTrace($self->{oTest}->{&TEST_VM}) && $self->{bBackTrace} ? " \\\n\t-DWITH_BACKTRACE" : '') .
                        ($self->{oTest}->{&TEST_CDEF} ? " \\\n\t$self->{oTest}->{&TEST_CDEF}" : '') .
                        "\n" .
                    "\n" .
                    "CFLAGS_TEST =" .
                        " \\\n\t" . (($self->{bOptimize} && ($self->{bProfile} || $bPerformance)) ? '-O2' : '-O0') .
                        " \\\n\t-DDEBUG_MEM" .
                        (!$self->{bDebugTestTrace} && $self->{bDebug} ? " \\\n\t-DDEBUG_TEST_TRACE" : '') .
                        ($bCoverage ? " \\\n\t-fprofile-arcs -ftest-coverage" : '') .
                        ($self->{oTest}->{&TEST_VM} eq VM_NONE ? '' : " \\\n\t-DTEST_CONTAINER_REQUIRED") .
                        ($self->{oTest}->{&TEST_CTESTDEF} ? " \\\n\t$self->{oTest}->{&TEST_CTESTDEF}" : '') .
                        "\n" .
                    "\n" .
                    "CFLAGS_HARNESS =" .
                        " \\\n\t" . ($self->{bOptimize} ? '-O2' : '-O0') .
                        ($self->{oTest}->{&TEST_CTESTDEF} ? " \\\n\t$self->{oTest}->{&TEST_CTESTDEF}" : '') .
                        "\n" .
                    "\n" .
                    "CFLAGS_CORE =" .
                        " \\\n\t" . ($self->{bOptimize} ? '-O2' : '-O0') .
                        "\n" .
                    "\n" .
                    "LDFLAGS =" .
                        ($self->{bProfile} ? " \\\n\t-pg" : '') .
                        "\n" .
                    "\n" .
                    "LIBS =" .
                        ($bCoverage ? " \\\n\t-lgcov" : '') .
                        (vmWithBackTrace($self->{oTest}->{&TEST_VM}) && $self->{bBackTrace} ? " \\\n\t-lbacktrace" : '') .
                        "\n" .
                    "\n" .
                    "INCLUDE =" .
                        " \\\n\t-I\"${strShimSrcPath}\"" .
                        " \\\n\t-I\"${strShimTestSrcPath}\"" .
                        " \\\n\t-I\"${strRepoCopySrcPath}\"" .
                        " \\\n\t-I\"${strRepoCopyTestSrcPath}\"" .
                        "\n" .
                    "\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))
                {
                    $bCleanAll = true;
                }

                # Generate list of harness files
                # ------------------------------------------------------------------------------------------------------------------
                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 $rhHarness (@{$hTest->{&TESTDEF_HARNESS}})
                {
                    my $bFound = false;
                    my $strFile = "common/harness" . ucfirst($rhHarness->{&TESTDEF_HARNESS_NAME});

                    # Include harness file if present
                    my $strHarnessSrcFile = "${strRepoCopyTestSrcPath}/${strFile}.c";

                    if ($self->{oStorageTest}->exists($strHarnessSrcFile))
                    {
                        $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))
                                    {
                                        # 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) . "; " . $strFunctionShim);
                                                push(@stryShimModuleSrcRenamed, $strLine);

                                                $strFunctionShim = undef;
                                            }
                                            # Else keep constructing the declaration and implementation
                                            else
                                            {
                                                $strFunctionDeclaration .= trim($strLine);
                                                $strFunctionShim .= "${strLine}\n";
                                            }
                                        }
                                        # Else search for shimmed functions
                                        else
                                        {
                                            # Rename shimmed functions
                                            my $bFound = false;

                                            foreach my $strFunction (@{$rhShim->{$strShimModule}{&TESTDEF_HARNESS_SHIM_FUNCTION}})
                                            {
                                                # 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 externed 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}";

                                                    $strLine =~ s/^${strFunction}\(/${strFunction}_SHIMMED\(/;
                                                    $strFunctionShim = "${strLineLast}\n${strLine}";

                                                    $bFound = true;
                                                    last;
                                                }
                                            }

                                            # If the function was not found then just append the line
                                            if (!$bFound)
                                            {
                                                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
                    for my $strFileSub (
                        $self->{oStorageTest}->list("${strRepoCopyTestSrcPath}/${strFile}",
                        {bIgnoreMissing => true, strExpression => '\.c$'}))
                    {
                        push(@stryHarnessFile, "${strFile}/" . substr($strFileSub, 0, length($strFileSub) - 2));
                        $bFound = true;
                    }

                    # Error when no harness files were found
                    if (!$bFound)
                    {
                        confess &log(ERROR, "no files found for harness '$rhHarness->{&TESTDEF_HARNESS_NAME}'");
                    }
                }

                # Generate list of core files (files to be tested/included in this unit will be excluded)
                # ------------------------------------------------------------------------------------------------------------------
                my $hTestCoverage = $hTest->{&TESTDEF_COVERAGE};

                my @stryCoreFile;

                foreach my $strFile (@{$hTest->{&TESTDEF_CORE}})
                {
                    # Skip all files except .c files (including .auto.c and .vendor.c)
                    next if $strFile !~ /(?<!\.auto)$/ || $strFile !~ /(?<!\.vendor)$/;

                    # 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);
                    }
                }

                # Create Makefile
                # ------------------------------------------------------------------------------------------------------------------
                my $strMakefile =
                    "include Makefile.config\n" .
                    "include Makefile.param\n" .
                    "\n" .
                    "SRCS = test.c \\\n" .
                    "\t" . join('.c ', @stryHarnessFile) . ".c \\\n" .
                    (@stryCoreFile > 0 ? "\t" . join('.c ', @stryCoreFile) . ".c\n" : '').
                    "\n" .
                    ".build/test.o: CFLAGS += \$(CFLAGS_TEST)\n" .
                    "\n" .
                    ".build/" . join(".o: CFLAGS += \$(CFLAGS_HARNESS)\n.build/", @stryHarnessFile) .
                        ".o: CFLAGS += \$(CFLAGS_HARNESS)\n" .
                    "\n" .
                    ".build/" . join(".o: CFLAGS += \$(CFLAGS_CORE)\n.build/", @stryCoreFile) .
                        ".o: CFLAGS += \$(CFLAGS_CORE)\n" .
                    "\n" .
                    ".build/\%.o : \%.c\n" .
                    "	\@if test ! -d \$(\@D); then mkdir -p \$(\@D); fi\n" .
                    "	\$(CC_CONFIG) \$(INCLUDE) \$(CFLAGS_CONFIG) \$(CPPFLAGS_CONFIG) \$(CFLAGS)" .
                        " -c -o \$\@ \$< -MMD -MP -MF .build/\$*.dep\n" .
                    "\n" .
                    "OBJS = \$(patsubst \%.c,.build/\%.o,\$(SRCS))\n" .
                    "\n" .
                    "test: \$(OBJS)\n" .
                    "	\$(CC_CONFIG) -o test.bin \$(OBJS) \$(LDFLAGS_CONFIG) \$(LDFLAGS) \$(LIBS_CONFIG) \$(LIBS)\n" .
                    "\n" .
                    "rwildcard = \$(wildcard \$1\$2) \$(foreach d,\$(wildcard \$1*),\$(call rwildcard,\$d/,\$2))\n" .
                    "DEP_FILES = \$(call rwildcard,.build,*.dep)\n" .
                    "include \$(DEP_FILES)\n";

                buildPutDiffers($self->{oStorageTest}, $self->{strGCovPath} . "/Makefile", $strMakefile);

                # Create test.c
                # ------------------------------------------------------------------------------------------------------------------
                # Generate list of C files to include for testing
                my $strTestDepend = '';
                my $strTestFile =
                    "module/$self->{oTest}->{&TEST_MODULE}/" . testRunName($self->{oTest}->{&TEST_NAME}, false) . 'Test.c';
                my $strCInclude;

                foreach my $strFile (sort(keys(%{$hTestCoverage}), @{$hTest->{&TESTDEF_INCLUDE}}))
                {
                    # Don't include the test file as it is already included below
                    next if $strFile =~ /Test$/;

                    # Don't include auto files as they are included in their companion C files
                    next if $strFile =~ /auto$/;

                    # 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";

                    # If the C file does not exist use the header file instead
                    if (!$self->{oStorageTest}->exists("${strRepoCopySrcPath}/${strCIncludeFile}"))
                    {
                        # Error if code was expected
                        if ($hTestCoverage->{$strFile} ne TESTDEF_COVERAGE_NOCODE)
                        {
                            confess &log(ERROR, "unable to find source file '${strRepoCopySrcPath}/${strCIncludeFile}'");
                        }

                        $strCIncludeFile = "${strFile}.h";
                    }

                    $strCInclude .= (defined($strCInclude) ? "\n" : '') . "#include \"${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
                my $strTestC = ${$self->{oStorageTest}->get("${strRepoCopyTestSrcPath}/test.c")};

                if (defined($strCInclude))
                {
                    $strTestC =~ s/\{\[C\_INCLUDE\]\}/$strCInclude/g;
                }
                else
                {
                    $strTestC =~ s/\{\[C\_INCLUDE\]\}//g;
                }

                $strTestC =~ s/\{\[C\_TEST\_INCLUDE\]\}/\#include \"$strTestFile\"/g;
                $strTestDepend .= " ${strTestFile}";

                # Determine where the project exe is located
                my $strProjectExePath = "$self->{strTestPath}/bin/$self->{oTest}->{&TEST_VM}/" . PROJECT_EXE;

                # Is this test running in a container?
                my $strContainer = $self->{oTest}->{&TEST_VM} eq VM_NONE ? 'false' : 'true';

                # What test path should be passed to C?  Containers always have their test path at ~/test but when running with
                # vm=none it should be in a subdirectory of the current directory.
                my $strTestPathC = $self->{oTest}->{&TEST_VM} eq VM_NONE ? $strHostTestPath : $strVmTestPath;

                # Set globals
                $strTestC =~ s/\{\[C\_TEST\_CONTAINER\]\}/$strContainer/g;
                $strTestC =~ s/\{\[C\_TEST\_PROJECT\_EXE\]\}/$strProjectExePath/g;
                $strTestC =~ s/\{\[C\_TEST\_PATH\]\}/$strTestPathC/g;
                $strTestC =~ s/\{\[C\_TEST\_USER\]\}/${\TEST_USER}/g;
                $strTestC =~ s/\{\[C\_TEST\_USER\_ID\]\}/${\TEST_USER_ID}/g;
                $strTestC =~ s/\{\[C\_TEST\_GROUP\]\}/${\TEST_GROUP}/g;
                $strTestC =~ s/\{\[C\_TEST\_GROUP\_ID\]\}/${\TEST_GROUP_ID}/g;
                $strTestC =~ s/\{\[C\_TEST\_PGB\_PATH\]\}/$strRepoCopyPath/g;
                $strTestC =~ s/\{\[C\_HRN\_PATH\]\}/$self->{strDataPath}/g;
                $strTestC =~ s/\{\[C\_TEST\_IDX\]\}/$self->{iVmIdx}/g;
                $strTestC =~ s/\{\[C\_HRN\_PATH\_REPO\]\}/$self->{strBackRestBase}/g;
                $strTestC =~ s/\{\[C\_TEST\_SCALE\]\}/$self->{iScale}/g;

                my $strLogTimestampC = $self->{bLogTimestamp} ? 'true' : 'false';
                $strTestC =~ s/\{\[C\_TEST\_TIMING\]\}/$strLogTimestampC/g;

                # Set timezone
                if (defined($self->{strTimeZone}))
                {
                    $strTestC =~ s/\{\[C\_TEST\_TZ\]\}/setenv\("TZ", "$self->{strTimeZone}", true\);/g;
                }
                else
                {
                    $strTestC =~ s/\{\[C\_TEST\_TZ\]\}/\/\/ No timezone specified/g;
                }

                # Set default log level
                my $strLogLevelTestC = "logLevel" . ucfirst($self->{strLogLevelTest});
                $strTestC =~ s/\{\[C\_LOG\_LEVEL\_TEST\]\}/$strLogLevelTestC/g;

                # Initialize tests
                my $strTestInit;

                for (my $iTestIdx = 1; $iTestIdx <= $hTest->{&TESTDEF_TOTAL}; $iTestIdx++)
                {
                    my $bSelected = false;

                    if (!defined($self->{oTest}->{&TEST_RUN}) || @{$self->{oTest}->{&TEST_RUN}} == 0 ||
                        grep(/^$iTestIdx$/, @{$self->{oTest}->{&TEST_RUN}}))
                    {
                        $bSelected = true;
                    }

                    $strTestInit .=
                        (defined($strTestInit) ? "\n    " : '') .
                        sprintf("hrnAdd(%3d, %8s);" , $iTestIdx, ($bSelected ? 'true' : 'false'));
                }

                $strTestC =~ s/\{\[C\_TEST\_LIST\]\}/$strTestInit/g;

                # Save test.c and make sure it gets a new timestamp
                # ------------------------------------------------------------------------------------------------------------------
                my $strTestCFile = "$self->{strGCovPath}/test.c";

                if (buildPutDiffers($self->{oStorageTest}, "$self->{strGCovPath}/test.c", $strTestC))
                {
                    # Get timestamp for test.bin if it existss
                    my $oTestBinInfo = $self->{oStorageTest}->info("$self->{strGCovPath}/test.bin", {bIgnoreMissing => true});
                    my $iTestBinOriginalTime = defined($oTestBinInfo) ? $oTestBinInfo->mtime : 0;

                    # Get timestamp for test.c
                    my $iTestCNewTime = $self->{oStorageTest}->info($strTestCFile)->mtime;

                    # Is the timestamp for test.c newer than test.bin?
                    while ($iTestCNewTime <= $iTestBinOriginalTime)
                    {
                        # If not then sleep until the next second
                        my $iTimeToSleep = ($iTestBinOriginalTime + 1) - gettimeofday();

                        if ($iTimeToSleep > 0)
                        {
                            usleep($iTimeToSleep * 1000000);
                        }

                        # Save the file again
                        $self->{oStorageTest}->put($self->{oStorageTest}->openWrite($strTestCFile), $strTestC);
                        $iTestCNewTime = $self->{oStorageTest}->info($strTestCFile)->mtime;
                    }
                }

                # Create command
                # ------------------------------------------------------------------------------------------------------------------
                # Build filename for valgrind suppressions
                my $strValgrindSuppress = $self->{strRepoPath} . '/test/src/valgrind.suppress.' . $self->{oTest}->{&TEST_VM};

                $strCommand =
                    ($self->{oTest}->{&TEST_VM} ne VM_NONE ? "docker exec -i -u ${\TEST_USER} ${strImage} bash -l -c '" : '') .
                    " \\\n" .
                    "cd $self->{strGCovPath} && \\\n" .
                    # Clean build
                    ($bCleanAll ? "rm -rf .build && \\\n" : '') .
                    # Remove coverage data
                    (!$bCleanAll && $bCoverage ? "rm -rf .build/test.gcda && \\\n" : '') .
                    # Configure when required
                    ($bConfigure ?
                        "mv Makefile Makefile.tmp && ${strRepoCopySrcPath}/configure -q --enable-test" .
                            " && mv Makefile Makefile.config && mv Makefile.tmp Makefile && \\\n" :
                        '') .
                    $self->{strMakeCmd} . " -j $self->{iBuildMax} -s 2>&1 && \\\n" .
                    "rm ${strBuildProcessingFile} && \\\n" .
                    # Test with valgrind when requested
                    ($self->{bValgrindUnit} && $self->{oTest}->{&TEST_TYPE} ne TESTDEF_PERFORMANCE ?
                        'valgrind -q --gen-suppressions=all' .
                        ($self->{oStorageTest}->exists($strValgrindSuppress) ? " --suppressions=${strValgrindSuppress}" : '') .
                        " --exit-on-first-error=yes --leak-check=full --leak-resolution=high --error-exitcode=25" . ' ' : '') .
                        "./test.bin 2>&1" .
                    ($self->{oTest}->{&TEST_VM} ne VM_NONE ? "'" : '');
            }
            else
            {
                $strCommand =
                    ($self->{oTest}->{&TEST_CONTAINER} ? 'docker exec -i -u ' . TEST_USER . " ${strImage} " : '') .
                    abs_path($0) .
                    " --test-path=${strVmTestPath}" .
                    " --vm=$self->{oTest}->{&TEST_VM}" .
                    " --vm-id=$self->{iVmIdx}" .
                    " --module=" . $self->{oTest}->{&TEST_MODULE} .
                    ' --test=' . $self->{oTest}->{&TEST_NAME} .
                    $strCommandRunParam .
                    (defined($self->{oTest}->{&TEST_DB}) ? ' --pg-version=' . $self->{oTest}->{&TEST_DB} : '') .
                    ($self->{strLogLevel} ne lc(INFO) ? " --log-level=$self->{strLogLevel}" : '') .
                    ($self->{strLogLevelTestFile} ne lc(TRACE) ? " --log-level-test-file=$self->{strLogLevelTestFile}" : '') .
                    ($self->{bLogTimestamp} ? '' : ' --no-log-timestamp') .
                    ' --pgsql-bin=' . $self->{oTest}->{&TEST_PGSQL_BIN} .
                    ($self->{strTimeZone} ? " --tz='$self->{strTimeZone}'" : '') .
                    ($self->{bLogForce} ? ' --log-force' : '') .
                    ($self->{bDryRun} ? ' --dry-run' : '') .
                    ($self->{bDryRun} ? ' --vm-out' : '') .
                    ($self->{bNoCleanup} ? " --no-cleanup" : '');
            }

            my $oExec = new pgBackRestTest::Common::ExecuteTest(
                $strCommand, {bSuppressError => true, bShowOutputAsync => $self->{bShowOutputAsync}});

            $oExec->begin();

            $self->{oProcess} =
            {
                exec => $oExec,
                test => $strTest,
                start_time => $fTestStartTime,
            };

            $bRun = true;
        }
    }

    # Return from function and log return values if any
    return logDebugReturn
    (
        $strOperation,
        {name => 'bRun', value => $bRun, trace => true}
    );
}

####################################################################################################################################
# end
####################################################################################################################################
sub end
{
    my $self = shift;

    # Assign function parameters, defaults, and log debug info
    (my $strOperation) = logDebugParam (__PACKAGE__ . '->run', \@_,);

    # Is the job done?
    my $bDone = false;
    my $bFail = false;

    my $oExecDone = $self->{oProcess}{exec};
    my $strTestDone = $self->{oProcess}{test};
    my $iTestDoneIdx = $self->{oProcess}{idx};

    my $iExitStatus = $oExecDone->end($self->{iVmMax} == 1);

    if (defined($iExitStatus))
    {
        my $strImage = 'test-' . $self->{iVmIdx};

        if ($self->{bShowOutputAsync})
        {
            syswrite(*STDOUT, "\n");
        }

        # If C code generate profile info
        if ($iExitStatus == 0 && $self->{oTest}->{&TEST_C} && $self->{bProfile})
        {
            executeTest(
                ($self->{oTest}->{&TEST_VM} ne VM_NONE  ? 'docker exec -i -u ' . TEST_USER . " ${strImage} " : '') .
                    "gprof $self->{strGCovPath}/test.bin $self->{strGCovPath}/gmon.out > $self->{strGCovPath}/gprof.txt");

            $self->{oStorageTest}->pathCreate(
                "$self->{strBackRestBase}/test/result/profile", {strMode => '0750', bIgnoreExists => true, bCreateParent => true});
            $self->{oStorageTest}->copy(
                "$self->{strGCovPath}/gprof.txt", "$self->{strBackRestBase}/test/result/profile/gprof.txt");
        }

        # If C code generate coverage info
        if ($iExitStatus == 0 && $self->{oTest}->{&TEST_C} && vmCoverageC($self->{oTest}->{&TEST_VM}) && $self->{bCoverageUnit})
        {
            coverageExtract(
                $self->{oStorageTest}, $self->{oTest}->{&TEST_MODULE}, $self->{oTest}->{&TEST_NAME},
                $self->{oTest}->{&TEST_VM} ne VM_NONE, $self->{bCoverageSummary},
                $self->{oTest}->{&TEST_VM} eq VM_NONE ? undef : $strImage, $self->{strTestPath}, "$self->{strTestPath}/temp",
                $self->{strGCovPath}, $self->{strBackRestBase} . '/test/result');
        }

        # Record elapsed time
        my $fTestElapsedTime = ceil((gettimeofday() - $self->{oProcess}{start_time}) * 100) / 100;

        # Output error
        if ($iExitStatus != 0)
        {
            # Get stdout
            my $strOutput = trim($oExecDone->{strOutLog}) ? "STDOUT:\n" . trim($oExecDone->{strOutLog}) : '';

            # Get stderr
            if (trim($oExecDone->{strSuppressedErrorLog}) ne '')
            {
                if ($strOutput ne '')
                {
                    $strOutput .= "\n";
                }

                $strOutput .= "STDERR:\n" . trim($oExecDone->{strSuppressedErrorLog});
            }

            # If no stdout or stderr output something rather than a blank line
            if ($strOutput eq '')
            {
                $strOutput = 'NO OUTPUT ON STDOUT OR STDERR';
            }

            &log(ERROR, "${strTestDone} (err${iExitStatus}" . ($self->{bLogTimestamp} ? "-${fTestElapsedTime}s)" : '') .
                 (defined($oExecDone->{strOutLog}) && !$self->{bShowOutputAsync} ? ":\n\n${strOutput}\n" : ''), undef, undef, 4);

            $bFail = true;
        }
        # Output success
        else
        {
            &log(INFO, "${strTestDone}" . ($self->{bLogTimestamp} ? " (${fTestElapsedTime}s)" : '').
                 ($self->{bVmOut} && !$self->{bShowOutputAsync} ?
                     ":\n\n" . trim($oExecDone->{strOutLog}) . "\n" : ''), undef, undef, 4);
        }

        if (!$self->{bNoCleanup})
        {
            my $strHostTestPath = "$self->{strTestPath}/${strImage}";

            if ($self->{oTest}->{&TEST_VM} ne VM_NONE)
            {
                containerRemove("test-$self->{iVmIdx}");
            }

            executeTest("chmod -R 700 ${strHostTestPath}/* 2>&1;rm -rf ${strHostTestPath}");
        }

        $bDone = true;
    }

    # Return from function and log return values if any
    return logDebugReturn
    (
        $strOperation,
        {name => 'bDone', value => $bDone, trace => true},
        {name => 'bFail', value => $bFail, trace => true}
    );
}

1;
