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

698 lines
28 KiB
Perl
Executable File

#!/usr/bin/perl
####################################################################################################################################
# test.pl - pgBackRest Unit Tests
####################################################################################################################################
####################################################################################################################################
# Perl includes
####################################################################################################################################
use strict;
use warnings FATAL => qw(all);
use Carp qw(confess longmess);
use English '-no_match_vars';
# Convert die to confess to capture the stack trace
$SIG{__DIE__} = sub { Carp::confess @_ };
use File::Basename qw(dirname);
use Getopt::Long qw(GetOptions);
use Cwd qw(abs_path cwd);
use Pod::Usage qw(pod2usage);
use POSIX qw(ceil);
use Time::HiRes qw(gettimeofday);
use lib dirname($0) . '/lib';
use lib dirname($0) . '/../lib';
use lib dirname($0) . '/../doc/lib';
use pgBackRest::Common::Exception;
use pgBackRest::Common::Ini;
use pgBackRest::Common::Log;
use pgBackRest::Common::String;
use pgBackRest::Common::Wait;
use pgBackRest::Config::Config;
use pgBackRest::DbVersion;
use pgBackRest::FileCommon;
use pgBackRest::Version;
use BackRestDoc::Custom::DocCustomRelease;
use pgBackRestTest::Common::ContainerTest;
use pgBackRestTest::Common::ExecuteTest;
use pgBackRestTest::Common::HostGroupTest;
use pgBackRestTest::Common::ListTest;
use pgBackRestTest::Common::RunTest;
use pgBackRestTest::Common::VmTest;
####################################################################################################################################
# Usage
####################################################################################################################################
=head1 NAME
test.pl - pgBackRest Unit Tests
=head1 SYNOPSIS
test.pl [options]
Test Options:
--module test module to execute
--test execute the specified test in a module
--run execute only the specified test run
--process-max max processes to run for compression/transfer (default 1)
--dry-run show only the tests that would be executed but don't execute them
--no-cleanup don't cleaup after the last test is complete - useful for debugging
--db-version version of postgres to test (all, defaults to minimal)
--log-force force overwrite of current test log files
--no-lint disable static source code analysis
--libc-only compile the C library and run tests only
--coverage perform coverage analysis
Configuration Options:
--psql-bin path to the psql executables (e.g. /usr/lib/postgresql/9.3/bin/)
--test-path path where tests are executed (defaults to ./test)
--log-level log level to use for tests (defaults to INFO)
--quiet, -q equivalent to --log-level=off
VM Options:
--vm docker container to build/test (u12, u14, co6, co7)
--vm-build build Docker containers
--vm-force force a rebuild of Docker containers
--vm-out Show VM output (default false)
--vm-max max VMs to run in parallel (default 1)
General Options:
--version display version and exit
--help display usage and exit
=cut
####################################################################################################################################
# Command line parameters
####################################################################################################################################
my $strLogLevel = 'info';
my $bVmOut = false;
my @stryModule;
my @stryModuleTest;
my @iyModuleTestRun;
my $iProcessMax = undef;
my $iVmMax = 1;
my $iVmId = undef;
my $bDryRun = false;
my $bNoCleanup = false;
my $strPgSqlBin;
my $strTestPath;
my $bVersion = false;
my $bHelp = false;
my $bQuiet = false;
my $strDbVersion = 'minimal';
my $bLogForce = false;
my $strVm = VM_ALL;
my $bVmBuild = false;
my $bVmForce = false;
my $bNoLint = false;
my $bLibCOnly = false;
my $bCoverage = false;
GetOptions ('q|quiet' => \$bQuiet,
'version' => \$bVersion,
'help' => \$bHelp,
'pgsql-bin=s' => \$strPgSqlBin,
'test-path=s' => \$strTestPath,
'log-level=s' => \$strLogLevel,
'vm=s' => \$strVm,
'vm-out' => \$bVmOut,
'vm-build' => \$bVmBuild,
'vm-force' => \$bVmForce,
'module=s@' => \@stryModule,
'test=s@' => \@stryModuleTest,
'run=s@' => \@iyModuleTestRun,
'process-max=s' => \$iProcessMax,
'vm-id=s' => \$iVmId,
'vm-max=s' => \$iVmMax,
'dry-run' => \$bDryRun,
'no-cleanup' => \$bNoCleanup,
'db-version=s' => \$strDbVersion,
'log-force' => \$bLogForce,
'no-lint' => \$bNoLint,
'libc-only' => \$bLibCOnly,
'coverage' => \$bCoverage)
or pod2usage(2);
####################################################################################################################################
# Run in eval block to catch errors
####################################################################################################################################
eval
{
# Display version and exit if requested
if ($bVersion || $bHelp)
{
syswrite(*STDOUT, BACKREST_NAME . ' ' . BACKREST_VERSION . " Test Engine\n");
if ($bHelp)
{
syswrite(*STDOUT, "\n");
pod2usage();
}
exit 0;
}
if (@ARGV > 0)
{
syswrite(*STDOUT, "invalid parameter\n\n");
pod2usage();
}
################################################################################################################################
# Setup
################################################################################################################################
# Set a neutral umask so tests work as expected
umask(0);
# Set console log level
if ($bQuiet)
{
$strLogLevel = 'off';
}
logLevelSet(uc($strLogLevel), uc($strLogLevel), OFF);
if (@stryModuleTest != 0 && @stryModule != 1)
{
confess "Only one --module can be provided when --test is specified";
}
if (@iyModuleTestRun != 0 && @stryModuleTest != 1)
{
confess "Only one --test can be provided when --run is specified";
}
# Check process total
if (defined($iProcessMax) && ($iProcessMax < 1 || $iProcessMax > OPTION_DEFAULT_PROCESS_MAX_MAX))
{
confess 'process-max must be between 1 and ' . OPTION_DEFAULT_PROCESS_MAX_MAX;
}
# Set test path if not expicitly set
if (!defined($strTestPath))
{
$strTestPath = cwd() . '/test';
}
# Coverage can only be run with u16 containers due to version compatibility issues
if ($bCoverage)
{
if ($strVm eq VM_ALL)
{
&log(INFO, 'Set --vm=' . VM_U16 . ' for coverage testing');
$strVm = VM_U16;
}
elsif ($strVm ne VM_U16)
{
confess &log(ERROR, 'only --vm=' . VM_U16 . ' can be used for coverage testing');
}
}
# Get the base backrest path
my $strBackRestBase = dirname(dirname(abs_path($0)));
################################################################################################################################
# Build Docker containers
################################################################################################################################
if ($bVmBuild)
{
containerBuild($strVm, $bVmForce, $strDbVersion);
exit 0;
}
################################################################################################################################
# Start VM and run
################################################################################################################################
if (!defined($iVmId))
{
# Load the doc module dynamically since it is not supported on all systems
require BackRestDoc::Common::Doc;
BackRestDoc::Common::Doc->import();
# Make sure version number matches the latest release
my $strReleaseFile = dirname(dirname(abs_path($0))) . '/doc/xml/release.xml';
my $oRelease = (new BackRestDoc::Custom::DocCustomRelease(new BackRestDoc::Common::Doc($strReleaseFile)))->releaseLast();
my $strVersion = $oRelease->paramGet('version');
if ($strVersion =~ /dev$/ && BACKREST_VERSION !~ /dev$/)
{
if ($oRelease->nodeTest('release-core-list'))
{
confess "dev release ${strVersion} must match the program version when core changes have been made";
}
}
elsif ($strVersion ne BACKREST_VERSION)
{
confess 'unable to find version ' . BACKREST_VERSION . " as the most recent release in ${strReleaseFile}";
}
if (!$bDryRun)
{
# Run Perl critic
if (!$bNoLint && !$bLibCOnly)
{
my $strBasePath = dirname(dirname(abs_path($0)));
&log(INFO, "Performing static code analysis using perl -cw");
# Check the exe for warnings
my $strWarning = trim(executeTest("perl -cw ${strBasePath}/bin/pgbackrest 2>&1"));
if ($strWarning ne "${strBasePath}/bin/pgbackrest syntax OK")
{
confess &log(ERROR, "${strBasePath}/bin/pgbackrest failed syntax check:\n${strWarning}");
}
&log(INFO, "Performing static code analysis using perlcritic");
executeTest('perlcritic --quiet --verbose=8 --brutal --top=10' .
' --verbose "[%p] %f: %m at line %l, column %c. %e. (Severity: %s)\n"' .
" \"--profile=${strBasePath}/test/lint/perlcritic.policy\"" .
" ${strBasePath}/bin/pgbackrest ${strBasePath}/lib/*" .
" ${strBasePath}/test/test.pl ${strBasePath}/test/lib/*" .
" ${strBasePath}/doc/doc.pl ${strBasePath}/doc/lib/*");
}
logFileSet(cwd() . "/test");
}
# Build the C Library in host
#-----------------------------------------------------------------------------------------------------------------------
if (!$bDryRun)
{
my $bLogDetail = $strLogLevel eq 'detail';
my $strBuildBasePath = "${strBackRestBase}/test/.vagrant/libc";
my $strBuildPath = "${strBuildBasePath}/host";
my $strMakeFile = "${strBuildPath}/Makefile.PL";
my $strLibCPath = "${strBackRestBase}/libc";
# Find the lastest modified time in the libc dir
my $hManifest = fileManifest($strLibCPath);
my $lTimestampLast = 0;
foreach my $strFile (sort(keys(%{$hManifest})))
{
if ($hManifest->{$strFile}{type} eq 'f' && $hManifest->{$strFile}{modification_time} > $lTimestampLast)
{
$lTimestampLast = $hManifest->{$strFile}{modification_time};
}
}
# Rebuild if the modification time of the makefile does not equal the latest file in libc
if (!fileExists($strMakeFile) || fileStat($strMakeFile)->mtime != $lTimestampLast)
{
&log(INFO, "Build/test/install C library for host (${strBuildPath})");
executeTest("sudo rm -rf ${strBuildBasePath}");
filePathCreate($strBuildPath, undef, true, true);
executeTest("cp -r ${strLibCPath}/* ${strBuildPath}");
executeTest(
"cd ${strBuildPath} && perl Makefile.PL INSTALLMAN1DIR=none INSTALLMAN3DIR=none",
{bShowOutputAsync => $bLogDetail});
executeTest("make -C ${strBuildPath}", {bSuppressStdErr => true, bShowOutputAsync => $bLogDetail});
executeTest("sudo make -C ${strBuildPath} install", {bShowOutputAsync => $bLogDetail});
executeTest("cd ${strBuildPath} && perl t/pgBackRest-LibC.t", {bShowOutputAsync => $bLogDetail});
# Load the module dynamically
require pgBackRest::LibC;
pgBackRest::LibC->import(qw(:debug));
# Do a basic test to make sure it installed correctly
if (&UVSIZE != 8)
{
confess &log(ERROR, 'UVSIZE in C library does not equal 8');
}
# Also check the version number
my $strLibCVersion =
BACKREST_VERSION =~ /dev$/ ?
substr(BACKREST_VERSION, 0, length(BACKREST_VERSION) - 3) . '.999' : BACKREST_VERSION;
if (libCVersion() ne $strLibCVersion)
{
confess &log(ERROR, $strLibCVersion . ' was expected for LibC version but found ' . libCVersion());
}
utime($lTimestampLast, $lTimestampLast, $strMakeFile) or
confess "unable to set time for ${strMakeFile}" . (defined($!) ? ":$!" : '');
}
# Exit if only testing the C library
exit 0 if $bLibCOnly;
}
# Determine which tests to run
#-----------------------------------------------------------------------------------------------------------------------
my $oyTestRun = testListGet(
$strVm, \@stryModule, \@stryModuleTest, \@iyModuleTestRun, $strDbVersion, $iProcessMax, $bCoverage);
if (@{$oyTestRun} == 0)
{
confess &log(ERROR, 'no tests were selected');
}
&log(INFO, @{$oyTestRun} . ' test' . (@{$oyTestRun} > 1 ? 's': '') . " selected\n");
if ($bNoCleanup && @{$oyTestRun} > 1)
{
confess &log(ERROR, '--no-cleanup is not valid when more than one test will run')
}
# Execute tests
#-----------------------------------------------------------------------------------------------------------------------
my $iTestFail = 0;
my $oyProcess = [];
my $strCoveragePath = "${strTestPath}/cover_db";
if (!$bDryRun || $bVmOut)
{
containerRemove('test-([0-9]+|build)');
for (my $iVmIdx = 0; $iVmIdx < 8; $iVmIdx++)
{
push(@{$oyProcess}, undef);
}
executeTest("sudo rm -rf ${strTestPath}/*");
filePathCreate($strCoveragePath, '0770', true, true);
}
# Build the C Library in container
#-----------------------------------------------------------------------------------------------------------------------
if (!$bDryRun)
{
my $bLogDetail = $strLogLevel eq 'detail';
my @stryBuildVm = $strVm eq VM_ALL ? (VM_CO6, VM_U16, VM_D8, VM_CO7, VM_U14, VM_U12) : ($strVm);
foreach my $strBuildVM (sort(@stryBuildVm))
{
my $strBuildPath = "${strBackRestBase}/test/.vagrant/libc/${strBuildVM}";
if (!fileExists($strBuildPath))
{
executeTest(
"docker run -itd -h test-build --name=test-build" .
" -v ${strBackRestBase}:${strBackRestBase} " . containerNamespace() . "/${strBuildVM}-build");
&log(INFO, "Build/test C library for ${strBuildVM} (${strBuildPath})");
filePathCreate($strBuildPath, undef, true, true);
executeTest("cp -r ${strBackRestBase}/libc/* ${strBuildPath}");
executeTest(
"docker exec -i test-build " .
"bash -c 'cd ${strBuildPath} && perl Makefile.PL INSTALLMAN1DIR=none INSTALLMAN3DIR=none'");
executeTest(
"docker exec -i test-build " .
"make -C ${strBuildPath}", {bSuppressStdErr => true});
executeTest(
"docker exec -i test-build " .
"make -C ${strBuildPath} test");
executeTest(
"docker exec -i test-build " .
"make -C ${strBuildPath} install", {bShowOutputAsync => $bLogDetail});
executeTest("docker rm -f test-build");
}
}
}
if ($bDryRun)
{
$iVmMax = 1;
}
my $iTestIdx = 0;
my $iVmTotal;
my $iTestMax = @{$oyTestRun};
my $lStartTime = time();
my $bShowOutputAsync = $bVmOut && (@{$oyTestRun} == 1 || $iVmMax == 1) && ! $bDryRun ? true : false;
do
{
do
{
$iVmTotal = 0;
for (my $iVmIdx = 0; $iVmIdx < $iVmMax; $iVmIdx++)
{
if (defined($$oyProcess[$iVmIdx]))
{
my $oExecDone = $$oyProcess[$iVmIdx]{exec};
my $strTestDone = $$oyProcess[$iVmIdx]{test};
my $iTestDoneIdx = $$oyProcess[$iVmIdx]{idx};
my $iExitStatus = $oExecDone->end(undef, $iVmMax == 1);
if (defined($iExitStatus))
{
if ($bShowOutputAsync)
{
syswrite(*STDOUT, "\n");
}
my $fTestElapsedTime = ceil((gettimeofday() - $$oyProcess[$iVmIdx]{start_time}) * 100) / 100;
if ($iExitStatus != 0)
{
&log(ERROR, "${strTestDone} (err${iExitStatus}-${fTestElapsedTime}s)" .
(defined($oExecDone->{strOutLog}) && !$bShowOutputAsync ?
":\n\n" . trim($oExecDone->{strOutLog}) . "\n" : ''), undef, undef, 4);
$iTestFail++;
}
else
{
&log(INFO, "${strTestDone} (${fTestElapsedTime}s)".
($bVmOut && !$bShowOutputAsync ?
":\n\n" . trim($oExecDone->{strOutLog}) . "\n" : ''), undef, undef, 4);
}
if (!$bNoCleanup)
{
my $strImage = 'test-' . $iVmIdx;
my $strHostTestPath = "${strTestPath}/${strImage}";
containerRemove("test-${iVmIdx}");
executeTest("sudo rm -rf ${strHostTestPath}");
}
$$oyProcess[$iVmIdx] = undef;
}
else
{
$iVmTotal++;
}
}
}
if ($iVmTotal == $iVmMax)
{
waitHiRes(.1);
}
}
while ($iVmTotal == $iVmMax);
for (my $iVmIdx = 0; $iVmIdx < $iVmMax; $iVmIdx++)
{
if (!defined($$oyProcess[$iVmIdx]) && $iTestIdx < @{$oyTestRun})
{
my $oTest = $$oyTestRun[$iTestIdx];
$iTestIdx++;
my $strTest = sprintf('P%0' . length($iVmMax) . 'd-T%0' . length($iTestMax) . 'd/%0' .
length($iTestMax) . "d - ", $iVmIdx, $iTestIdx, $iTestMax) .
'vm=' . $$oTest{&TEST_VM} .
', module=' . $$oTest{&TEST_MODULE} .
', test=' . $$oTest{&TEST_NAME} .
(defined($$oTest{&TEST_RUN}) ? ', run=' . join(',', @{$$oTest{&TEST_RUN}}) : '') .
(defined($$oTest{&TEST_PROCESS}) ? ', process-max=' . $$oTest{&TEST_PROCESS} : '') .
(defined($$oTest{&TEST_DB}) ? ', db=' . $$oTest{&TEST_DB} : '');
my $strImage = 'test-' . $iVmIdx;
my $strDbVersion = (defined($$oTest{&TEST_DB}) ? $$oTest{&TEST_DB} : PG_VERSION_94);
$strDbVersion =~ s/\.//;
&log($bDryRun && !$bVmOut || $bShowOutputAsync ? INFO : DETAIL, "${strTest}" .
($bVmOut || $bShowOutputAsync ? "\n" : ''));
my $strVmTestPath = '/home/' . TEST_USER . "/test/${strImage}";
my $strHostTestPath = "${strTestPath}/${strImage}";
# Don't create the container if this is a dry run unless output from the VM is required. Ouput can be requested
# to get more information about the specific tests that will be run.
if (!$bDryRun || $bVmOut)
{
# Create host test directory
filePathCreate($strHostTestPath, '0770');
if ($$oTest{&TEST_CONTAINER})
{
executeTest(
'docker run -itd -h ' . $$oTest{&TEST_VM} . "-test --name=${strImage}" .
" -v ${strCoveragePath}:${strCoveragePath} " .
" -v ${strHostTestPath}:${strVmTestPath}" .
" -v ${strBackRestBase}:${strBackRestBase} " .
containerNamespace() . '/' . $$oTest{&TEST_VM} .
"-loop-test-pre");
}
}
# Create run parameters
my $strCommandRunParam = '';
foreach my $iRunIdx (@{$$oTest{&TEST_RUN}})
{
$strCommandRunParam .= ' --run=' . $iRunIdx;
}
# Create command
my $strCommand =
($$oTest{&TEST_CONTAINER} ? 'docker exec -i -u ' . TEST_USER . " ${strImage} " : '') .
($bCoverage ? testRunExe(
abs_path($0), dirname($strCoveragePath), $strBackRestBase, $$oTest{&TEST_MODULE},
$$oTest{&TEST_NAME}, defined($$oTest{&TEST_RUN}) ? $$oTest{&TEST_RUN} : 'all') :
abs_path($0)) .
" --test-path=${strVmTestPath}" .
" --vm=$$oTest{&TEST_VM}" .
" --vm-id=${iVmIdx}" .
" --module=" . $$oTest{&TEST_MODULE} .
' --test=' . $$oTest{&TEST_NAME} .
$strCommandRunParam .
(defined($$oTest{&TEST_DB}) ? ' --db-version=' . $$oTest{&TEST_DB} : '') .
(defined($$oTest{&TEST_PROCESS}) ? ' --process-max=' . $$oTest{&TEST_PROCESS} : '') .
($strLogLevel ne lc(INFO) ? " --log-level=${strLogLevel}" : '') .
' --pgsql-bin=' . $$oTest{&TEST_PGSQL_BIN} .
($bCoverage ? ' --coverage' : '') .
($bLogForce ? ' --log-force' : '') .
($bDryRun ? ' --dry-run' : '') .
($bVmOut ? ' --vm-out' : '') .
($bNoCleanup ? " --no-cleanup" : '');
&log(DETAIL, $strCommand);
if (!$bDryRun || $bVmOut)
{
my $fTestStartTime = gettimeofday();
# Set permissions on the Docker test directory. This can be removed once users/groups are sync'd between
# Docker and the host VM.
if ($$oTest{&TEST_CONTAINER})
{
executeTest("docker exec ${strImage} chown " . TEST_USER . ":postgres -R ${strVmTestPath}");
}
my $oExec = new pgBackRestTest::Common::ExecuteTest(
$strCommand,
{bSuppressError => true, bShowOutputAsync => $bShowOutputAsync});
$oExec->begin();
my $oProcess =
{
exec => $oExec,
test => $strTest,
idx => $iTestIdx,
container => $$oTest{&TEST_CONTAINER},
start_time => $fTestStartTime
};
$$oyProcess[$iVmIdx] = $oProcess;
}
$iVmTotal++;
}
}
}
while ($iVmTotal > 0);
# Write out coverage info
#-----------------------------------------------------------------------------------------------------------------------
if ($bCoverage)
{
&log(INFO, 'Writing coverage report');
executeTest("rm -rf ${strBackRestBase}/test/coverage");
executeTest("cp -rp ${strCoveragePath} ${strCoveragePath}_temp");
executeTest("cover -report json -outputdir ${strBackRestBase}/test/coverage ${strCoveragePath}_temp");
executeTest("rm -rf ${strCoveragePath}_temp");
executeTest("cp -rp ${strCoveragePath} ${strCoveragePath}_temp");
executeTest("cover -outputdir ${strBackRestBase}/test/coverage ${strCoveragePath}_temp");
executeTest("rm -rf ${strCoveragePath}_temp");
}
# Print test info and exit
#-----------------------------------------------------------------------------------------------------------------------
if ($bDryRun)
{
&log(INFO, 'DRY RUN COMPLETED');
}
else
{
&log(INFO, 'TESTS COMPLETED ' . ($iTestFail == 0 ? 'SUCCESSFULLY' : "WITH ${iTestFail} FAILURE(S)") .
' (' . (time() - $lStartTime) . 's)');
}
exit 0;
}
################################################################################################################################
# Runs tests
################################################################################################################################
my $iRun = 0;
# Create host group for containers
my $oHostGroup = hostGroupGet();
# Run the test
testRun($stryModule[0], $stryModuleTest[0])->process(
$strVm, $iVmId, # Vm info
$strBackRestBase, # Base backrest directory
$strTestPath, # Path where the tests will run
"${strBackRestBase}/bin/" . BACKREST_EXE, # Path to the backrest executable
$strDbVersion ne 'minimal' ? $strPgSqlBin: undef, # Db bin path
$strDbVersion ne 'minimal' ? $strDbVersion: undef, # Db version
$stryModule[0], $stryModuleTest[0], \@iyModuleTestRun, # Module info
$iProcessMax, $bVmOut, $bDryRun, $bNoCleanup, $bLogForce, # Test options
$bCoverage, # Test options
TEST_USER, BACKREST_USER, TEST_GROUP); # User/group info
if (!$bNoCleanup)
{
if ($oHostGroup->removeAll() > 0)
{
executeTest("sudo rm -rf ${strTestPath}");
}
}
if (!$bDryRun && !$bVmOut)
{
&log(INFO, 'TESTS COMPLETED SUCCESSFULLY (DESPITE ANY ERROR MESSAGES YOU SAW)');
}
# Exit with success
exit 0;
}
####################################################################################################################################
# Check for errors
####################################################################################################################################
or do
{
# If a backrest exception then return the code
exit $EVAL_ERROR->code() if (isException($EVAL_ERROR));
# Else output the unhandled error
print $EVAL_ERROR;
exit ERROR_UNHANDLED;
};
# It shouldn't be possible to get here
&log(ASSERT, 'execution reached invalid location in ' . __FILE__ . ', line ' . __LINE__);
exit ERROR_ASSERT;