#!/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 Digest::SHA qw(sha1_hex); use File::Basename qw(dirname); use Getopt::Long qw(GetOptions); use Cwd qw(abs_path cwd); use JSON::PP; use Pod::Usage qw(pod2usage); use POSIX qw(ceil strftime); use Time::HiRes qw(gettimeofday); use lib dirname($0) . '/lib'; use lib dirname(dirname($0)) . '/lib'; use lib dirname(dirname($0)) . '/build/lib'; use lib dirname(dirname($0)) . '/doc/lib'; use pgBackRestDoc::Common::Exception; use pgBackRestDoc::Common::Log; use pgBackRestDoc::Common::String; use pgBackRestDoc::ProjectInfo; use pgBackRestBuild::Build; use pgBackRestBuild::Build::Common; use pgBackRestBuild::Config::Build; use pgBackRestBuild::Config::BuildDefine; use pgBackRestBuild::Config::BuildParse; use pgBackRestBuild::Error::Build; use pgBackRestBuild::Error::Data; use pgBackRestTest::Common::BuildTest; use pgBackRestTest::Common::CodeCountTest; use pgBackRestTest::Common::ContainerTest; use pgBackRestTest::Common::CoverageTest; use pgBackRestTest::Common::DefineTest; use pgBackRestTest::Common::ExecuteTest; use pgBackRestTest::Common::HostGroupTest; use pgBackRestTest::Common::JobTest; use pgBackRestTest::Common::ListTest; use pgBackRestTest::Common::RunTest; use pgBackRestTest::Common::Storage; use pgBackRestTest::Common::StoragePosix; use pgBackRestTest::Common::VmTest; use pgBackRestTest::Common::Wait; #################################################################################################################################### # 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 --dry-run show only the tests that would be executed but don't execute them --no-cleanup don't cleanup after the last test is complete - useful for debugging --clean clean working and result paths for a completely fresh build --clean-only execute --clean and exit --pg-version version of postgres to test (all, defaults to minimal) --log-force force overwrite of current test log files --build-only build the binary (and honor --build-package) but don't run tests --build-package build the package --build-max max processes to use for builds (default 4) --coverage-only only run coverage tests (as a subset of selected tests) --c-only only run C tests --container-only only run tests that must be run in a container --gen-only only run auto-generation --no-gen do not run code generation --code-count generate code counts --smart perform bin/package builds only when source timestamps have changed --dev --smart --no-optimize --dev-test does nothing -- kept for backward compatibility --expect --vm=co7 --db=9.6 --log-force --no-valgrind don't run valgrind on C unit tests (saves time) --no-coverage don't run coverage on C unit tests (saves time) --no-optimize don't do compile optimization for C (saves compile time) --backtrace enable backtrace when available (adds stack trace line numbers -- very slow) --profile generate profile info --no-debug don't generate a debug build --scale scale performance tests --tz test with the specified timezone --debug-test-trace test stack trace for low-level functions (slow, esp w/valgrind, may cause timeouts) Report Options: --coverage-summary generate a coverage summary report for the documentation 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 test harness (and Perl tests) (defaults to INFO) --log-level-test log level to use for C tests (defaults to OFF) --log-level-test-file log level to use for file logging in integration tests (defaults to TRACE) --no-log-timestamp suppress timestamps, timings, etc. Used to generate documentation. --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 $bClean; my $bCleanOnly; my $strLogLevel = lc(INFO); my $strLogLevelTest = lc(OFF); my $strLogLevelTestFile = lc(TRACE); my $bNoLogTimestamp = false; my $bVmOut = false; my @stryModule; my @stryModuleTest; my @iyModuleTestRun; 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 $strPgVersion = 'minimal'; my $bLogForce = false; my $strVm; my $strVmHost = VM_HOST_DEFAULT; my $bVmBuild = false; my $bVmForce = false; my $bBuildOnly = false; my $bBuildPackage = false; my $iBuildMax = 4; my $bCoverageOnly = false; my $bCoverageSummary = false; my $bNoCoverage = false; my $bCOnly = false; my $bContainerOnly = false; my $bGenOnly = false; my $bNoGen = false; my $bCodeCount = false; my $bSmart = false; my $bDev = false; my $bDevTest = false; my $bBackTrace = false; my $bProfile = false; my $bExpect = false; my $bNoValgrind = false; my $bNoOptimize = false; my $bNoDebug = false; my $iScale = 1; my $bDebugTestTrace = false; my $iRetry = 0; my $strTimeZone = undef; my @cmdOptions = @ARGV; GetOptions ('q|quiet' => \$bQuiet, 'version' => \$bVersion, 'help' => \$bHelp, 'clean' => \$bClean, 'clean-only' => \$bCleanOnly, 'pgsql-bin=s' => \$strPgSqlBin, 'test-path=s' => \$strTestPath, 'log-level=s' => \$strLogLevel, 'log-level-test=s' => \$strLogLevelTest, 'log-level-test-file=s' => \$strLogLevelTestFile, 'no-log-timestamp' => \$bNoLogTimestamp, 'vm=s' => \$strVm, 'vm-host=s' => \$strVmHost, 'vm-out' => \$bVmOut, 'vm-build' => \$bVmBuild, 'vm-force' => \$bVmForce, 'module=s@' => \@stryModule, 'test=s@' => \@stryModuleTest, 'run=s@' => \@iyModuleTestRun, 'vm-id=s' => \$iVmId, 'vm-max=s' => \$iVmMax, 'dry-run' => \$bDryRun, 'no-cleanup' => \$bNoCleanup, 'pg-version=s' => \$strPgVersion, 'log-force' => \$bLogForce, 'build-only' => \$bBuildOnly, 'build-package' => \$bBuildPackage, 'build-max=s' => \$iBuildMax, 'coverage-only' => \$bCoverageOnly, 'coverage-summary' => \$bCoverageSummary, 'no-coverage' => \$bNoCoverage, 'c-only' => \$bCOnly, 'container-only' => \$bContainerOnly, 'gen-only' => \$bGenOnly, 'no-gen' => \$bNoGen, 'code-count' => \$bCodeCount, 'smart' => \$bSmart, 'dev' => \$bDev, 'dev-test' => \$bDevTest, 'backtrace' => \$bBackTrace, 'profile' => \$bProfile, 'expect' => \$bExpect, 'no-valgrind' => \$bNoValgrind, 'no-optimize' => \$bNoOptimize, 'no-debug', => \$bNoDebug, 'scale=s' => \$iScale, 'tz=s', => \$strTimeZone, 'debug-test-trace', => \$bDebugTestTrace, 'retry=s' => \$iRetry) or pod2usage(2); #################################################################################################################################### # Run in eval block to catch errors #################################################################################################################################### eval { # Record the start time my $lStartTime = time(); # Display version and exit if requested if ($bVersion || $bHelp) { syswrite(*STDOUT, PROJECT_NAME . ' ' . PROJECT_VERSION . " Test Engine\n"); if ($bHelp) { syswrite(*STDOUT, "\n"); pod2usage(); } exit 0; } if (@ARGV > 0) { syswrite(*STDOUT, "invalid parameter\n\n"); pod2usage(); } ################################################################################################################################ # Disable code generation on dry-run ################################################################################################################################ if ($bDryRun) { $bNoGen = true; } ################################################################################################################################ # Update options for --coverage-summary ################################################################################################################################ if ($bCoverageSummary) { $bCoverageOnly = true; $bCOnly = true; } ################################################################################################################################ # Update options for --dev and --dev-fast and --dev-test ################################################################################################################################ if ($bDev && $bDevTest) { confess "cannot combine --dev and --dev-test"; } if ($bDev) { $bSmart = true; $bNoOptimize = true; } ################################################################################################################################ # Update options for --profile ################################################################################################################################ if ($bProfile) { $bNoValgrind = true; $bNoCoverage = true; } ################################################################################################################################ # Update options for --expect ################################################################################################################################ if ($bExpect) { $strVm = VM_EXPECT; $strPgVersion = '9.6'; $bLogForce = true; } ################################################################################################################################ # 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, !$bNoLogTimestamp); &log(INFO, "test begin - log level ${strLogLevel}"); 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"; } # Set test path if not explicitly set if (!defined($strTestPath)) { $strTestPath = cwd() . '/test'; } my $oStorageTest = new pgBackRestTest::Common::Storage( $strTestPath, new pgBackRestTest::Common::StoragePosix({bFileSync => false, bPathSync => false})); if ($bCoverageOnly) { if (!defined($strVm)) { &log(INFO, "Set --vm=${strVmHost} for coverage testing"); $strVm = $strVmHost; } elsif ($strVm eq VM_ALL) { confess &log(ERROR, "select a single Debian-based VM for coverage testing"); } } # If VM is not defined then set it to all if (!defined($strVm)) { $strVm = VM_ALL; } # Else make sure vm is valid elsif ($strVm ne VM_ALL) { vmValid($strVm); } # Get the base backrest path my $strBackRestBase = dirname(dirname(abs_path($0))); my $strVagrantPath = "${strBackRestBase}/test/.vagrant"; my $oStorageBackRest = new pgBackRestTest::Common::Storage( $strBackRestBase, new pgBackRestTest::Common::StoragePosix({bFileSync => false, bPathSync => false})); ################################################################################################################################ # Clean working and result paths ################################################################################################################################ if ($bClean || $bCleanOnly) { &log(INFO, "clean working (${strTestPath}) and result (${strBackRestBase}/test/result) paths"); if ($oStorageTest->pathExists($strTestPath)) { executeTest("find ${strTestPath} -mindepth 1 -print0 | xargs -0 rm -rf"); } if ($oStorageTest->pathExists("${strBackRestBase}/test/result")) { executeTest("find ${strBackRestBase}/test/result -mindepth 1 -print0 | xargs -0 rm -rf"); } # Exit when clean-only exit 0 if $bCleanOnly; } ################################################################################################################################ # Build Docker containers ################################################################################################################################ if ($bVmBuild) { containerBuild($oStorageBackRest, $strVm, $bVmForce); exit 0; } ################################################################################################################################ # Load test definition ################################################################################################################################ testDefLoad(${$oStorageBackRest->get("test/define.yaml")}); ################################################################################################################################ # Start VM and run ################################################################################################################################ if (!defined($iVmId)) { # Make a copy of the repo to track which files have been changed. Eventually all builds will be done from this directory. #--------------------------------------------------------------------------------------------------------------------------- my $strRepoCachePath = "${strTestPath}/repo"; my $strRepoCacheManifest = 'repo.manifest'; # Create the repo path -- this should hopefully prevent obvious rsync errors below $oStorageTest->pathCreate("${strTestPath}/repo", {strMode => '0770', bIgnoreExists => true, bCreateParent => true}); # Check if there are any files existing already. If none, that means a full copy is happening and we shouldn't report # modified files my @stryExistingList = $oStorageTest->list($strRepoCachePath, {bIgnoreMissing => true}); # First check if there is an old manifest that has not been cleared. This indicates that an error happened before all new # files could be processed, which means they should be processed again. my @stryModifiedList; my $rstrModifiedList = $oStorageTest->get( $oStorageTest->openRead("${strRepoCachePath}/${strRepoCacheManifest}", {bIgnoreMissing => true})); if (defined($rstrModifiedList)) { @stryModifiedList = split("\n", trim($$rstrModifiedList)); } push( @stryModifiedList, split( "\n", trim( executeTest( "git -C ${strBackRestBase} ls-files -c --others --exclude-standard |" . " rsync -rtW --out-format=\"\%n\" --delete --ignore-missing-args" . " --exclude=test/result --exclude=repo.manifest" . " ${strBackRestBase}/ --files-from=- ${strRepoCachePath}" . " | grep -E -v '/\$' | cat")))); if (@stryModifiedList > 0) { $oStorageTest->put("${strRepoCachePath}/${strRepoCacheManifest}", join("\n", @stryModifiedList)); if (@stryExistingList > 0) { &log(INFO, "modified since last run: " . join(', ', @stryModifiedList)); } } # Generate code counts #--------------------------------------------------------------------------------------------------------------------------- if ($bCodeCount) { &log(INFO, "classify code files"); codeCountScan($oStorageBackRest, $strBackRestBase); exit 0; } # Auto-generate files unless --no-gen specified #--------------------------------------------------------------------------------------------------------------------------- if (!$bNoGen) { my @stryBuiltAll; &log(INFO, "check code autogenerate"); # Auto-generate version for configure.ac script #----------------------------------------------------------------------------------------------------------------------- if (!$bSmart || grep(/^src\/version\.h/, @stryModifiedList)) { my $strConfigureAcOld = ${$oStorageTest->get("${strBackRestBase}/src/build/configure.ac")}; my $strConfigureAcNew; foreach my $strLine (split("\n", $strConfigureAcOld)) { if ($strLine =~ /^AC_INIT\(/) { $strLine = 'AC_INIT([' . PROJECT_NAME . '], [' . PROJECT_VERSION . '])'; } $strConfigureAcNew .= "${strLine}\n"; } # Save into the src dir my @stryBuilt; my $strBuilt = 'src/build/configure.ac'; if (buildPutDiffers($oStorageBackRest, "${strBackRestBase}/${strBuilt}", $strConfigureAcNew)) { push(@stryBuilt, $strBuilt); push(@stryBuiltAll, @stryBuilt); push(@stryModifiedList, @stryBuilt); } &log(INFO, " autogenerated version in configure.ac script: " . (@stryBuilt ? join(', ', @stryBuilt) : 'no changes')); } # Auto-generate configure script #----------------------------------------------------------------------------------------------------------------------- if (!$bSmart || grep(/^src\/build\/configure\.ac/, @stryModifiedList)) { # Set build file my @stryBuilt; my $strBuilt = 'src/configure'; # Get configure.ac and configure to see if anything has changed my $strConfigureAc = ${$oStorageBackRest->get('src/build/configure.ac')}; my $strConfigureAcHash = sha1_hex($strConfigureAc); my $rstrConfigure = $oStorageBackRest->get($oStorageBackRest->openRead($strBuilt, {bIgnoreMissing => true})); # Check if configure needs to be regenerated if (!defined($rstrConfigure) || !defined($$rstrConfigure) || $strConfigureAcHash ne substr($$rstrConfigure, length($$rstrConfigure) - 41, 40)) { # Generate aclocal.m4 my $strAcLocal = executeTest("cd ${strBackRestBase}/src/build && aclocal --OUT=-"); $strAcLocal = trim($strAcLocal) . "\n"; buildPutDiffers($oStorageBackRest, "${strBackRestBase}/src/build/aclocal.m4", $strAcLocal); # Generate configure my $strConfigure = executeTest("cd ${strBackRestBase}/src/build && autoconf --output=-"); $strConfigure = trim($strConfigure) . "\n\n# Generated from src/build/configure.ac sha1 ${strConfigureAcHash}\n"; # Remove unused options from help $strConfigure =~ s/^ --((?!bin).)*dir=DIR.*\n//mg; $strConfigure =~ s/^ --sbindir=DIR.*\n//mg; # Save into the src dir $oStorageBackRest->put( $oStorageBackRest->openWrite("${strBackRestBase}/${strBuilt}", {strMode => '0755'}), $strConfigure); # Add to built list push(@stryBuilt, $strBuilt); push(@stryBuiltAll, @stryBuilt); push(@stryModifiedList, @stryBuilt); } &log(INFO, " autogenerated configure script: " . (@stryBuilt ? join(', ', @stryBuilt) : 'no changes')); } # Auto-generate C files #----------------------------------------------------------------------------------------------------------------------- if (!$bSmart || grep(/^build\//, @stryModifiedList) || grep(/^doc\/xml\/reference\.xml/, @stryModifiedList)) { errorDefineLoad(${$oStorageBackRest->get("build/error.yaml")}); my $rhBuild = { 'config' => { &BLD_DATA => buildConfig(), &BLD_PATH => 'config', }, 'configDefine' => { &BLD_DATA => buildConfigDefine(), &BLD_PATH => 'config', }, 'configParse' => { &BLD_DATA => buildConfigParse(), &BLD_PATH => 'config', }, 'error' => { &BLD_DATA => buildError(), &BLD_PATH => 'common', }, }; my @stryBuilt = buildAll("${strBackRestBase}/src", $rhBuild); &log(INFO, " autogenerated C code: " . (@stryBuilt ? join(', ', @stryBuilt) : 'no changes')); if (@stryBuilt) { push(@stryBuiltAll, @stryBuilt); push(@stryModifiedList, @stryBuilt); } } # Copy the files that were auto-generated to the repo cache so they will be included in the current build #----------------------------------------------------------------------------------------------------------------------- foreach my $strBuilt (@stryBuiltAll) { executeTest("cp -p ${strBackRestBase}/${strBuilt} ${strRepoCachePath}/${strBuilt}"); } if ($bGenOnly) { exit 0; } } # Check Perl version against release notes and update version in C code if needed #--------------------------------------------------------------------------------------------------------------------------- my $bVersionDev = true; my $strVersionBase; if (!$bDev || $bBuildPackage) { # Make sure version number matches the latest release #----------------------------------------------------------------------------------------------------------------------- &log(INFO, "check version info"); # Load the doc modules dynamically since they are not supported on all systems require pgBackRestDoc::Common::Doc; pgBackRestDoc::Common::Doc->import(); require pgBackRestDoc::Custom::DocCustomRelease; pgBackRestDoc::Custom::DocCustomRelease->import(); my $strReleaseFile = dirname(dirname(abs_path($0))) . '/doc/xml/release.xml'; my $oRelease = (new pgBackRestDoc::Custom::DocCustomRelease(new pgBackRestDoc::Common::Doc($strReleaseFile)))->releaseLast(); my $strVersion = $oRelease->paramGet('version'); $bVersionDev = false; $strVersionBase = $strVersion; if ($strVersion =~ /dev$/) { $bVersionDev = true; $strVersionBase = substr($strVersion, 0, length($strVersion) - 3); if (PROJECT_VERSION !~ /dev$/ && $oRelease->nodeTest('release-core-list')) { confess "dev release ${strVersion} must match the program version when core changes have been made"; } } elsif ($strVersion ne PROJECT_VERSION) { confess 'unable to find version ' . PROJECT_VERSION . " as the most recent release in ${strReleaseFile}"; } } # Clean up #--------------------------------------------------------------------------------------------------------------------------- my $iTestFail = 0; my $iTestRetry = 0; my $oyProcess = []; my $strCodePath = "${strBackRestBase}/test/result/coverage/raw"; if (!$bDryRun || $bVmOut) { &log(INFO, "cleanup old data" . ($strVm ne VM_NONE ? " and containers" : '')); if ($strVm ne VM_NONE) { containerRemove('test-([0-9]+|build)'); } for (my $iVmIdx = 0; $iVmIdx < 8; $iVmIdx++) { push(@{$oyProcess}, undef); } executeTest( "rm -rf ${strTestPath}/temp ${strTestPath}/test-* ${strTestPath}/data-*" . ($bDev ? '' : " ${strTestPath}/gcov-*")); $oStorageTest->pathCreate("${strTestPath}/temp", {strMode => '0770', bIgnoreExists => true, bCreateParent => true}); # Remove old lcov dirs -- do it this way so the dirs stay open in finder/explorer, etc. executeTest("rm -rf ${strBackRestBase}/test/result/coverage/lcov/*"); # Overwrite the C coverage report so it will load but not show old coverage $oStorageTest->pathCreate( "${strBackRestBase}/test/result/coverage", {strMode => '0770', bIgnoreExists => true, bCreateParent => true}); $oStorageBackRest->put( "${strBackRestBase}/test/result/coverage/coverage.html", "