1
0
mirror of https://github.com/pgbackrest/pgbackrest.git synced 2025-08-10 22:21:39 +02:00

Move config parsing out of Perl tests.

cfgParseTest() is provided in the C library for parsing configs in unit tests.
This commit is contained in:
David Steele
2018-02-05 12:32:30 -05:00
parent db21b7a360
commit c3f47bf240
15 changed files with 210 additions and 910 deletions

View File

@@ -99,8 +99,11 @@ sub configLoad
eval
{
$rhOption = (JSON::PP->new()->allow_nonref())->decode($strConfigJson);
return true;
# Hacky fix for backslashes that need to be escaped
$strConfigJson =~ s/\\/\\\\/g;
$rhOption = (JSON::PP->new()->allow_nonref())->decode($strConfigJson);
return true;
}
or do
{
@@ -171,28 +174,6 @@ sub configLoad
umask(0000);
}
# Set pg-host-cmd and repo-host-cmd to defaults if they are not set. The command depends on the currently running exe so can't
# be calculated correctly in the C Library -- perhaps in the future this value will be passed in or set some other way
# ??? THIS IS IMPLEMENTED IN C AND SHOULD BE REMOVED FROM HERE WITHOUT DUPLICATING TEST CODE
if (cfgOptionValid(CFGOPT_REPO_HOST_CMD) && cfgOptionTest(CFGOPT_REPO_HOST) && !cfgOptionTest(CFGOPT_REPO_HOST_CMD))
{
cfgOptionSet(CFGOPT_REPO_HOST_CMD, backrestBin());
$oOption{cfgOptionName(CFGOPT_REPO_HOST_CMD)}{source} = CFGDEF_SOURCE_DEFAULT;
}
if (cfgOptionValid(CFGOPT_PG_HOST_CMD))
{
for (my $iOptionIdx = 1; $iOptionIdx <= cfgOptionIndexTotal(CFGOPT_PG_HOST); $iOptionIdx++)
{
if (cfgOptionTest(cfgOptionIdFromIndex(CFGOPT_PG_HOST, $iOptionIdx)) &&
!cfgOptionTest(cfgOptionIdFromIndex(CFGOPT_PG_HOST_CMD, $iOptionIdx)))
{
cfgOptionSet(cfgOptionIdFromIndex(CFGOPT_PG_HOST_CMD, $iOptionIdx), backrestBin());
$oOption{cfgOptionIdFromIndex(CFGOPT_PG_HOST_CMD, $iOptionIdx)}{source} = CFGDEF_SOURCE_DEFAULT;
}
}
}
# Protocol timeout should be greater than db timeout
if (cfgOptionTest(CFGOPT_DB_TIMEOUT) && cfgOptionTest(CFGOPT_PROTOCOL_TIMEOUT) &&
cfgOption(CFGOPT_PROTOCOL_TIMEOUT) <= cfgOption(CFGOPT_DB_TIMEOUT))

View File

@@ -45,6 +45,8 @@ These includes are from the src directory. There is no Perl-specific code in th
#include "common/error.h"
#include "config/config.h"
#include "config/define.h"
#include "config/load.h"
#include "perl/exec.h"
#include "postgres/pageChecksum.h"
/***********************************************************************************************************************************
@@ -97,5 +99,6 @@ INCLUDE: xs/cipher/block.xs
INCLUDE: xs/cipher/random.xs
INCLUDE: xs/common/encode.xs
INCLUDE: xs/config/config.xs
INCLUDE: xs/config/configTest.xs
INCLUDE: xs/config/define.xs
INCLUDE: xs/postgres/pageChecksum.xs

View File

@@ -81,7 +81,11 @@ my @stryCFile =
'common/encode/base64.c',
'common/error.c',
'common/errorType.c',
'common/ini.c',
'common/log.c',
'common/memContext.c',
'common/regExp.c',
'common/time.c',
'common/type/buffer.c',
'common/type/keyValue.c',
'common/type/list.c',
@@ -91,7 +95,12 @@ my @stryCFile =
'common/type/variantList.c',
'config/config.c',
'config/define.c',
'config/load.c',
'config/parse.c',
'perl/exec.c',
'postgres/pageChecksum.c',
'storage/helper.c',
'storage/storage.c',
);
# Add ../src for files that are outside libc

View File

@@ -74,23 +74,8 @@ my $rhExport =
{
&BLD_EXPORTTYPE_SUB => [qw(
cfgCommandId
cfgDefOptionAllowList
cfgDefOptionAllowListValue
cfgDefOptionAllowListValueTotal
cfgDefOptionAllowListValueValid
cfgDefOptionAllowRange
cfgDefOptionAllowRangeMax
cfgDefOptionAllowRangeMin
cfgDefOptionDefault
cfgDefOptionDepend
cfgDefOptionDependOption
cfgDefOptionDependValue
cfgDefOptionDependValueTotal
cfgDefOptionDependValueValid
cfgDefOptionNegate
cfgDefOptionPrefix
cfgDefOptionRequired
cfgDefOptionSection
cfgDefOptionSecure
cfgDefOptionType
cfgDefOptionValid
@@ -128,6 +113,13 @@ my $rhExport =
randomBytes
)],
},
'test' =>
{
&BLD_EXPORTTYPE_SUB => [qw(
cfgParseTest
)],
},
};
####################################################################################################################################

View File

@@ -214,23 +214,8 @@ sub libcAutoExportTag
'CFGDEF_TYPE_LIST',
'CFGDEF_TYPE_STRING',
'cfgCommandId',
'cfgDefOptionAllowList',
'cfgDefOptionAllowListValue',
'cfgDefOptionAllowListValueTotal',
'cfgDefOptionAllowListValueValid',
'cfgDefOptionAllowRange',
'cfgDefOptionAllowRangeMax',
'cfgDefOptionAllowRangeMin',
'cfgDefOptionDefault',
'cfgDefOptionDepend',
'cfgDefOptionDependOption',
'cfgDefOptionDependValue',
'cfgDefOptionDependValueTotal',
'cfgDefOptionDependValueValid',
'cfgDefOptionNegate',
'cfgDefOptionPrefix',
'cfgDefOptionRequired',
'cfgDefOptionSection',
'cfgDefOptionSecure',
'cfgDefOptionType',
'cfgDefOptionValid',
@@ -255,6 +240,11 @@ sub libcAutoExportTag
[
'randomBytes',
],
test =>
[
'cfgParseTest',
],
}
}

View File

@@ -0,0 +1,34 @@
# ----------------------------------------------------------------------------------------------------------------------------------
# Config Test Perl Exports
# ----------------------------------------------------------------------------------------------------------------------------------
MODULE = pgBackRest::LibC PACKAGE = pgBackRest::LibC
####################################################################################################################################
# Parse command line and return a JSON object with results
####################################################################################################################################
SV *
cfgParseTest(backrestBin, parseParam)
const char *backrestBin
const char *parseParam
CODE:
RETVAL = NULL;
ERROR_XS_BEGIN()
{
// This should run in a temp context but for some reason getopt_long gets upset when if gets called again after the previous
// arg list being freed. So, this is a memory leak but it is only used for testing, not production.
StringList *paramList = strLstNewSplitZ(strCat(strNew("pgbackrest|"), parseParam), "|");
cfgLoadParam(strLstSize(paramList), strLstPtr(paramList), strNew(backrestBin));
String *result = perlOptionJson();
RETVAL = newSV(strSize(result));
SvPOK_only(RETVAL);
strcpy(SvPV_nolen(RETVAL), strPtr(result));
SvCUR_set(RETVAL, strSize(result));
}
ERROR_XS_END()
OUTPUT:
RETVAL

View File

@@ -32,113 +32,6 @@ CODE:
OUTPUT:
RETVAL
bool
cfgDefOptionAllowList(commandId, optionId)
U32 commandId
U32 optionId
CODE:
RETVAL = false;
ERROR_XS_BEGIN()
{
RETVAL = cfgDefOptionAllowList(cfgCommandDefIdFromId(commandId), cfgOptionDefIdFromId(optionId));
}
ERROR_XS_END();
OUTPUT:
RETVAL
const char *
cfgDefOptionAllowListValue(commandId, optionId, valueId)
U32 commandId
U32 optionId
U32 valueId
CODE:
RETVAL = NULL;
ERROR_XS_BEGIN()
{
RETVAL = cfgDefOptionAllowListValue(cfgCommandDefIdFromId(commandId), cfgOptionDefIdFromId(optionId), valueId);
}
ERROR_XS_END();
OUTPUT:
RETVAL
I32
cfgDefOptionAllowListValueTotal(commandId, optionId)
U32 commandId
U32 optionId
CODE:
RETVAL = 0;
ERROR_XS_BEGIN()
{
RETVAL = cfgDefOptionAllowListValueTotal(cfgCommandDefIdFromId(commandId), cfgOptionDefIdFromId(optionId));
}
ERROR_XS_END();
OUTPUT:
RETVAL
bool
cfgDefOptionAllowListValueValid(commandId, optionId, value);
U32 commandId
U32 optionId
const char *value
CODE:
RETVAL = false;
ERROR_XS_BEGIN()
{
RETVAL = cfgDefOptionAllowListValueValid(cfgCommandDefIdFromId(commandId), cfgOptionDefIdFromId(optionId), value);
}
ERROR_XS_END();
OUTPUT:
RETVAL
bool
cfgDefOptionAllowRange(commandId, optionId)
U32 commandId
U32 optionId
CODE:
RETVAL = false;
ERROR_XS_BEGIN()
{
RETVAL = cfgDefOptionAllowRange(cfgCommandDefIdFromId(commandId), cfgOptionDefIdFromId(optionId));
}
ERROR_XS_END();
OUTPUT:
RETVAL
double
cfgDefOptionAllowRangeMax(commandId, optionId)
U32 commandId
U32 optionId
CODE:
RETVAL = 0;
ERROR_XS_BEGIN()
{
RETVAL = cfgDefOptionAllowRangeMax(cfgCommandDefIdFromId(commandId), cfgOptionDefIdFromId(optionId));
}
ERROR_XS_END();
OUTPUT:
RETVAL
double
cfgDefOptionAllowRangeMin(commandId, optionId)
U32 commandId
U32 optionId
CODE:
RETVAL = 0;
ERROR_XS_BEGIN()
{
RETVAL = cfgDefOptionAllowRangeMin(cfgCommandDefIdFromId(commandId), cfgOptionDefIdFromId(optionId));
}
ERROR_XS_END();
OUTPUT:
RETVAL
const char *
cfgDefOptionDefault(commandId, optionId)
U32 commandId
@@ -154,98 +47,6 @@ CODE:
OUTPUT:
RETVAL
bool
cfgDefOptionDepend(commandId, optionId);
U32 commandId
U32 optionId
CODE:
RETVAL = false;
ERROR_XS_BEGIN()
{
RETVAL = cfgDefOptionDepend(cfgCommandDefIdFromId(commandId), cfgOptionDefIdFromId(optionId));
}
ERROR_XS_END();
OUTPUT:
RETVAL
I32
cfgDefOptionDependOption(commandId, optionId)
U32 commandId
U32 optionId
CODE:
RETVAL = 0;
ERROR_XS_BEGIN()
{
RETVAL = cfgOptionIdFromDefId(
cfgDefOptionDependOption(
cfgCommandDefIdFromId(commandId), cfgOptionDefIdFromId(optionId)), cfgOptionIndex(optionId));
}
ERROR_XS_END();
OUTPUT:
RETVAL
const char *
cfgDefOptionDependValue(commandId, optionId, valueId)
U32 commandId
U32 optionId
U32 valueId
CODE:
RETVAL = NULL;
ERROR_XS_BEGIN()
{
RETVAL = cfgDefOptionDependValue(cfgCommandDefIdFromId(commandId), cfgOptionDefIdFromId(optionId), valueId);
}
ERROR_XS_END();
OUTPUT:
RETVAL
I32
cfgDefOptionDependValueTotal(commandId, optionId)
U32 commandId
U32 optionId
CODE:
RETVAL = 0;
ERROR_XS_BEGIN()
{
RETVAL = cfgDefOptionDependValueTotal(cfgCommandDefIdFromId(commandId), cfgOptionDefIdFromId(optionId));
}
ERROR_XS_END();
OUTPUT:
RETVAL
bool
cfgDefOptionDependValueValid(commandId, optionId, value)
U32 commandId
U32 optionId
const char *value
CODE:
RETVAL = false;
ERROR_XS_BEGIN()
{
RETVAL = cfgDefOptionDependValueValid(cfgCommandDefIdFromId(commandId), cfgOptionDefIdFromId(optionId), value);
}
ERROR_XS_END();
OUTPUT:
RETVAL
bool cfgDefOptionNegate(optionId)
U32 optionId
CODE:
RETVAL = false;
ERROR_XS_BEGIN()
{
RETVAL = cfgDefOptionNegate(cfgOptionDefIdFromId(optionId));
}
ERROR_XS_END();
OUTPUT:
RETVAL
const char *
cfgDefOptionPrefix(optionId)
U32 optionId
@@ -260,52 +61,6 @@ CODE:
OUTPUT:
RETVAL
bool
cfgDefOptionRequired(commandId, optionId)
U32 commandId
U32 optionId
CODE:
RETVAL = false;
ERROR_XS_BEGIN()
{
// Only the first indexed option is ever required
if (cfgOptionIndex(optionId) == 0)
{
RETVAL = cfgDefOptionRequired(cfgCommandDefIdFromId(commandId), cfgOptionDefIdFromId(optionId));
}
}
ERROR_XS_END();
OUTPUT:
RETVAL
const char *
cfgDefOptionSection(optionId)
U32 optionId
CODE:
RETVAL = NULL;
ERROR_XS_BEGIN()
{
switch (cfgDefOptionSection(cfgOptionDefIdFromId(optionId)))
{
case cfgDefSectionGlobal:
RETVAL = "global";
break;
case cfgDefSectionStanza:
RETVAL = "stanza";
break;
default:
RETVAL = NULL;
break;
}
}
ERROR_XS_END();
OUTPUT:
RETVAL
bool
cfgDefOptionSecure(optionId)
U32 optionId

View File

@@ -6,6 +6,7 @@ Configuration Load
#include "common/memContext.h"
#include "common/log.h"
#include "config/config.h"
#include "config/load.h"
#include "config/parse.h"
/***********************************************************************************************************************************
@@ -13,6 +14,12 @@ Load the configuration
***********************************************************************************************************************************/
void
cfgLoad(int argListSize, const char *argList[])
{
cfgLoadParam(argListSize, argList, NULL);
}
void
cfgLoadParam(int argListSize, const char *argList[], String *exe)
{
MEM_CONTEXT_TEMP_BEGIN()
{
@@ -38,6 +45,10 @@ cfgLoad(int argListSize, const char *argList[])
logInit(logLevelConsole, logLevelStdErr, logTimestamp);
}
// If an exe was passed in the use that
if (exe != NULL)
cfgExeSet(exe);
// Set default for repo-host-cmd
if (cfgOptionValid(cfgOptRepoHost) && cfgOption(cfgOptRepoHost) != NULL &&
cfgOptionSource(cfgOptRepoHostCmd) == cfgSourceDefault)

View File

@@ -8,5 +8,6 @@ Configuration Load
Functions
***********************************************************************************************************************************/
void cfgLoad(int argListSize, const char *argList[]);
void cfgLoadParam(int argListSize, const char *argList[], String *exe);
#endif

View File

@@ -24,6 +24,121 @@ Constants used to build perl options
#define PGBACKREST_MODULE PGBACKREST_NAME "::Main"
#define PGBACKREST_MAIN PGBACKREST_MODULE "::main"
/***********************************************************************************************************************************
Build JSON output from options
***********************************************************************************************************************************/
String *
perlOptionJson()
{
String *result = strNew("{");
MEM_CONTEXT_TEMP_BEGIN()
{
for (ConfigOption optionId = 0; optionId < CFG_OPTION_TOTAL; optionId++)
{
// Skip the option if it is not valid
if (!cfgOptionValid(optionId))
continue;
// Output option
if (strSize(result) != 1)
strCat(result, ",");
strCatFmt(result, "\"%s\":{", cfgOptionName(optionId));
// Output source unless it is default
if (cfgOptionSource(optionId) != cfgSourceDefault)
{
strCat(result, "\"source\":\"");
if (cfgOptionSource(optionId) == cfgSourceParam)
strCat(result, "param");
else
strCat(result, "config");
strCat(result, "\"");
}
// If option was negated
if (cfgOptionNegate(optionId))
strCatFmt(result, ",\"negate\":%s", strPtr(varStrForce(varNewBool(true))));
// Else not negated and has a value
else if (cfgOption(optionId) != NULL)
{
if (cfgOptionSource(optionId) != cfgSourceDefault)
strCat(result, ",");
strCat(result, "\"value\":");
switch (cfgDefOptionType(cfgOptionDefIdFromId(optionId)))
{
case cfgDefOptTypeBoolean:
case cfgDefOptTypeFloat:
case cfgDefOptTypeInteger:
{
strCat(result, strPtr(varStrForce(cfgOption(optionId))));
break;
}
case cfgDefOptTypeString:
{
strCatFmt(result, "\"%s\"", strPtr(cfgOptionStr(optionId)));
break;
}
case cfgDefOptTypeHash:
{
const KeyValue *valueKv = cfgOptionKv(optionId);
const VariantList *keyList = kvKeyList(valueKv);
strCat(result, "{");
for (unsigned int listIdx = 0; listIdx < varLstSize(keyList); listIdx++)
{
if (listIdx != 0)
strCat(result, ",");
strCatFmt(
result, "\"%s\":\"%s\"", strPtr(varStr(varLstGet(keyList, listIdx))),
strPtr(varStr(kvGet(valueKv, varLstGet(keyList, listIdx)))));
}
strCat(result, "}");
break;
}
case cfgDefOptTypeList:
{
StringList *valueList = strLstNewVarLst(cfgOptionLst(optionId));
strCat(result, "{");
for (unsigned int listIdx = 0; listIdx < strLstSize(valueList); listIdx++)
{
if (listIdx != 0)
strCat(result, ",");
strCatFmt(result, "\"%s\":true", strPtr(strLstGet(valueList, listIdx)));
}
strCat(result, "}");
break;
}
}
}
strCat(result, "}");
}
strCat(result, "}");
}
MEM_CONTEXT_TEMP_END();
return result;
}
/***********************************************************************************************************************************
Build list of perl options to use for exec
***********************************************************************************************************************************/
@@ -43,109 +158,6 @@ perlCommand()
strLstAdd(perlArgList, strNew(PERL_EXE));
}
// Construct option list to pass to main
String *configJson = strNew("{");
for (ConfigOption optionId = 0; optionId < CFG_OPTION_TOTAL; optionId++)
{
// Skip the option if it is not valid
if (!cfgOptionValid(optionId))
continue;
// Output option
if (strSize(configJson) != 1)
strCat(configJson, ",");
strCatFmt(configJson, "\"%s\":{", cfgOptionName(optionId));
// Output source unless it is default
if (cfgOptionSource(optionId) != cfgSourceDefault)
{
strCat(configJson, "\"source\":\"");
if (cfgOptionSource(optionId) == cfgSourceParam)
strCat(configJson, "param");
else
strCat(configJson, "config");
strCat(configJson, "\"");
}
// If option was negated
if (cfgOptionNegate(optionId))
strCatFmt(configJson, ",\"negate\":%s", strPtr(varStrForce(varNewBool(true))));
// Else not negated and has a value
else if (cfgOption(optionId) != NULL)
{
if (cfgOptionSource(optionId) != cfgSourceDefault)
strCat(configJson, ",");
strCat(configJson, "\"value\":");
switch (cfgDefOptionType(cfgOptionDefIdFromId(optionId)))
{
case cfgDefOptTypeBoolean:
case cfgDefOptTypeFloat:
case cfgDefOptTypeInteger:
{
strCat(configJson, strPtr(varStrForce(cfgOption(optionId))));
break;
}
case cfgDefOptTypeString:
{
strCatFmt(configJson, "\"%s\"", strPtr(cfgOptionStr(optionId)));
break;
}
case cfgDefOptTypeHash:
{
const KeyValue *valueKv = cfgOptionKv(optionId);
const VariantList *keyList = kvKeyList(valueKv);
strCat(configJson, "{");
for (unsigned int listIdx = 0; listIdx < varLstSize(keyList); listIdx++)
{
if (listIdx != 0)
strCat(configJson, ",");
strCatFmt(
configJson, "\"%s\":\"%s\"", strPtr(varStr(varLstGet(keyList, listIdx))),
strPtr(varStr(kvGet(valueKv, varLstGet(keyList, listIdx)))));
}
strCat(configJson, "}");
break;
}
case cfgDefOptTypeList:
{
StringList *valueList = strLstNewVarLst(cfgOptionLst(optionId));
strCat(configJson, "{");
for (unsigned int listIdx = 0; listIdx < strLstSize(valueList); listIdx++)
{
if (listIdx != 0)
strCat(configJson, ",");
strCatFmt(configJson, "\"%s\":true", strPtr(strLstGet(valueList, listIdx)));
}
strCat(configJson, "}");
break;
}
}
}
strCat(configJson, "}");
}
strCat(configJson, "}");
// Add command arguments to pass to main
String *commandParam = strNew("");
@@ -161,7 +173,7 @@ perlCommand()
// Construct Perl main call
String *mainCall = strNewFmt(
PGBACKREST_MAIN "('%s','%s','%s'%s)", strPtr(cfgExe()), cfgCommandName(cfgCommand()), strPtr(configJson),
PGBACKREST_MAIN "('%s','%s','%s'%s)", strPtr(cfgExe()), cfgCommandName(cfgCommand()), strPtr(perlOptionJson()),
strPtr(commandParam));
// End arg list for perl exec

View File

@@ -9,6 +9,7 @@ Execute Perl for Legacy Functionality
/***********************************************************************************************************************************
Functions
***********************************************************************************************************************************/
String *perlOptionJson();
StringList *perlCommand();
void perlExec(StringList *perlArgList);

View File

@@ -211,9 +211,8 @@ sub run
$strCommand =
($self->{oTest}->{&TEST_CONTAINER} ? 'docker exec -i -u ' . TEST_USER . " ${strImage} " : '') .
testRunExe(
vmCoverage($self->{oTest}->{&TEST_VM}), undef, abs_path($0),
dirname($self->{strCoveragePath}), $self->{strBackRestBase}, $self->{oTest}->{&TEST_MODULE},
$self->{oTest}->{&TEST_NAME}, defined($self->{oTest}->{&TEST_RUN}) ? $self->{oTest}->{&TEST_RUN} : 'all') .
vmCoverage($self->{oTest}->{&TEST_VM}), undef, abs_path($0), dirname($self->{strCoveragePath}),
$self->{strBackRestBase}, $self->{oTest}->{&TEST_MODULE}, $self->{oTest}->{&TEST_NAME}) .
" --test-path=${strVmTestPath}" .
" --vm=$self->{oTest}->{&TEST_VM}" .
" --vm-id=$self->{iVmIdx}" .

View File

@@ -157,6 +157,13 @@ sub process
# Initialize test storage
$oStorage = new pgBackRest::Storage::Local($self->testPath(), new pgBackRest::Storage::Posix::Driver());
# Generate backrest exe
$self->{strBackRestExe} = testRunExe(
$self->coverage(), $self->{strBackRestExeC}, $self->{strBackRestExeHelper}, dirname($self->testPath()), $self->basePath(),
$self->module(), $self->moduleTest(), true);
backrestBinSet($self->{strBackRestExe});
# Init, run, and end the test(s)
$self->initModule();
$self->run();
@@ -236,13 +243,6 @@ sub begin
return false;
}
# Generate backrest exe
$self->{strBackRestExe} = testRunExe(
$self->coverage(), $self->{strBackRestExeC}, $self->{strBackRestExeHelper}, dirname($self->testPath()), $self->basePath(),
$self->module(), $self->moduleTest(), $self->runCurrent(), true);
backrestBinSet($self->{strBackRestExe});
# Create an ExpectTest object
if ($self->doExpect())
{
@@ -532,7 +532,7 @@ sub testRunGet
push @EXPORT, qw(testRunGet);
####################################################################################################################################
# testExe
# Generate test executable
####################################################################################################################################
sub testRunExe
{
@@ -543,7 +543,6 @@ sub testRunExe
my $strBackRestBasePath = shift;
my $strModule = shift;
my $strTest = shift;
my $iRun = shift;
my $bLog = shift;
my $strExe = defined($strExeC) ? $strExeC : undef;

View File

@@ -17,6 +17,7 @@ use Getopt::Long qw(GetOptions);
use pgBackRest::Common::Exception;
use pgBackRest::Common::Log;
use pgBackRest::Config::Config;
use pgBackRest::LibC qw(:test);
use pgBackRest::Version;
use constant CONFIGENVTEST => 'ConfigEnvTest';
@@ -103,333 +104,6 @@ sub commandTestWrite
return @szyParam;
}
####################################################################################################################################
# testOptionValidate
#
# Make sure the command-line options are valid based on the command.
####################################################################################################################################
sub testOptionValidate
{
my $iCommandId = shift;
# Build hash with all valid command-line options
my @stryOptionAllow;
for (my $iOptionId = 0; $iOptionId < cfgOptionTotal(); $iOptionId++)
{
my $strOption = cfgOptionName($iOptionId);
if (cfgDefOptionType($iOptionId) == CFGDEF_TYPE_HASH || cfgDefOptionType($iOptionId) == CFGDEF_TYPE_LIST)
{
$strOption .= '=s@';
}
elsif (cfgDefOptionType($iOptionId) != CFGDEF_TYPE_BOOLEAN)
{
$strOption .= '=s';
}
push(@stryOptionAllow, $strOption);
# Check if the option can be negated
if (cfgDefOptionNegate($iOptionId))
{
push(@stryOptionAllow, 'no-' . cfgOptionName($iOptionId));
}
}
# Get command-line options
my $oOptionTest = {};
# Parse command line options
if (!GetOptions($oOptionTest, @stryOptionAllow))
{
confess &log(ASSERT, "error parsing command line");
}
# Store results of validation
my $rhOption = {};
# Keep track of unresolved dependencies
my $bDependUnresolved = true;
my %oOptionResolved;
# Loop through all possible options
while ($bDependUnresolved)
{
# Assume that all dependencies will be resolved in this loop
$bDependUnresolved = false;
for (my $iOptionId = 0; $iOptionId < cfgOptionTotal(); $iOptionId++)
{
my $strOption = cfgOptionName($iOptionId);
# Skip the option if it has been resolved in a prior loop
if (defined($oOptionResolved{$strOption}))
{
next;
}
# Determine if an option is valid for a command
$rhOption->{$strOption}{valid} = cfgDefOptionValid($iCommandId, $iOptionId);
if (!$rhOption->{$strOption}{valid})
{
$oOptionResolved{$strOption} = true;
next;
}
# Store the option value
my $strValue = $oOptionTest->{$strOption};
# Check to see if an option can be negated. Make sure that it is not set and negated at the same time.
$rhOption->{$strOption}{negate} = false;
if (cfgDefOptionNegate($iOptionId))
{
$rhOption->{$strOption}{negate} = defined($$oOptionTest{'no-' . $strOption});
if ($rhOption->{$strOption}{negate} && defined($strValue))
{
confess &log(ERROR, "option '${strOption}' cannot be both set and negated", ERROR_OPTION_NEGATE);
}
if ($rhOption->{$strOption}{negate} && cfgDefOptionType($iOptionId) eq CFGDEF_TYPE_BOOLEAN)
{
$strValue = false;
}
}
# Check dependency for the command then for the option
my $bDependResolved = true;
my $strDependOption;
my $strDependValue;
my $strDependType;
if (cfgDefOptionDepend($iCommandId, $iOptionId))
{
# Check if the depend option has a value
my $iDependOptionId = cfgDefOptionDependOption($iCommandId, $iOptionId);
$strDependOption = cfgOptionName($iDependOptionId);
$strDependValue = $rhOption->{$strDependOption}{value};
# Make sure the depend option has been resolved, otherwise skip this option for now
if (!defined($oOptionResolved{$strDependOption}))
{
$bDependUnresolved = true;
next;
}
if (!defined($strDependValue))
{
$bDependResolved = false;
$strDependType = 'source';
}
# If a depend value exists, make sure the option value matches
if ($bDependResolved && cfgDefOptionDependValueTotal($iCommandId, $iOptionId) == 1 &&
cfgDefOptionDependValue($iCommandId, $iOptionId, 0) ne $strDependValue)
{
$bDependResolved = false;
$strDependType = 'value';
}
# If a depend list exists, make sure the value is in the list
if ($bDependResolved && cfgDefOptionDependValueTotal($iCommandId, $iOptionId) > 1 &&
!cfgDefOptionDependValueValid($iCommandId, $iOptionId, $strDependValue))
{
$bDependResolved = false;
$strDependType = 'list';
}
}
if (cfgDefOptionDepend($iCommandId, $iOptionId) && !$bDependResolved && defined($strValue))
{
my $strError = "option '${strOption}' not valid without option ";
my $iDependOptionId = cfgOptionId($strDependOption);
if ($strDependType eq 'source')
{
confess &log(ERROR, "${strError}'${strDependOption}'", ERROR_OPTION_INVALID);
}
# If a depend value exists, make sure the option value matches
if ($strDependType eq 'value')
{
if (cfgDefOptionType($iDependOptionId) eq CFGDEF_TYPE_BOOLEAN)
{
$strError .=
"'" . (cfgDefOptionDependValue($iCommandId, $iOptionId, 0) ? '' : 'no-') . "${strDependOption}'";
}
else
{
$strError .= "'${strDependOption}' = '" . cfgDefOptionDependValue($iCommandId, $iOptionId, 0) . "'";
}
confess &log(ERROR, $strError, ERROR_OPTION_INVALID);
}
$strError .= "'${strDependOption}'";
# If a depend list exists, make sure the value is in the list
if ($strDependType eq 'list')
{
my @oyValue;
for (my $iValueId = 0; $iValueId < cfgDefOptionDependValueTotal($iCommandId, $iOptionId); $iValueId++)
{
push(@oyValue, "'" . cfgDefOptionDependValue($iCommandId, $iOptionId, $iValueId) . "'");
}
$strError .= @oyValue == 1 ? " = $oyValue[0]" : " in (" . join(", ", @oyValue) . ")";
confess &log(ERROR, $strError, ERROR_OPTION_INVALID);
}
}
# Is the option defined?
if (defined($strValue))
{
# Check that floats and integers are valid
if (cfgDefOptionType($iOptionId) eq CFGDEF_TYPE_INTEGER ||
cfgDefOptionType($iOptionId) eq CFGDEF_TYPE_FLOAT)
{
# Test that the string is a valid float or integer by adding 1 to it. It's pretty hokey but it works and it
# beats requiring Scalar::Util::Numeric to do it properly.
my $bError = false;
eval
{
my $strTest = $strValue + 1;
return true;
}
or do
{
$bError = true;
};
# Check that integers are really integers
if (!$bError && cfgDefOptionType($iOptionId) eq CFGDEF_TYPE_INTEGER &&
(int($strValue) . 'S') ne ($strValue . 'S'))
{
$bError = true;
}
# Error if the value did not pass tests
!$bError
or confess &log(ERROR, "'${strValue}' is not valid for '${strOption}' option", ERROR_OPTION_INVALID_VALUE);
}
# Process an allow list for the command then for the option
if (cfgDefOptionAllowList($iCommandId, $iOptionId) &&
!cfgDefOptionAllowListValueValid($iCommandId, $iOptionId, $strValue))
{
confess &log(ERROR, "'${strValue}' is not valid for '${strOption}' option", ERROR_OPTION_INVALID_VALUE);
}
# Process an allow range for the command then for the option
if (cfgDefOptionAllowRange($iCommandId, $iOptionId) &&
($strValue < cfgDefOptionAllowRangeMin($iCommandId, $iOptionId) ||
$strValue > cfgDefOptionAllowRangeMax($iCommandId, $iOptionId)))
{
confess &log(ERROR, "'${strValue}' is not valid for '${strOption}' option", ERROR_OPTION_INVALID_RANGE);
}
# Set option value
if (ref($strValue) eq 'ARRAY' &&
(cfgDefOptionType($iOptionId) eq CFGDEF_TYPE_HASH || cfgDefOptionType($iOptionId) eq CFGDEF_TYPE_LIST))
{
foreach my $strItem (@{$strValue})
{
my $strKey;
my $strValue;
# If the keys are expected to have values
if (cfgDefOptionType($iOptionId) eq CFGDEF_TYPE_HASH)
{
# Check for = and make sure there is a least one character on each side
my $iEqualPos = index($strItem, '=');
if ($iEqualPos < 1 || length($strItem) <= $iEqualPos + 1)
{
confess &log(ERROR, "'${strItem}' not valid key/value for '${strOption}' option",
ERROR_OPTION_INVALID_PAIR);
}
$strKey = substr($strItem, 0, $iEqualPos);
$strValue = substr($strItem, $iEqualPos + 1);
}
# Else no values are expected so set value to true
else
{
$strKey = $strItem;
$strValue = true;
}
# Check that the key has not already been set
if (defined($rhOption->{$strOption}{$strKey}{value}))
{
confess &log(ERROR, "'${$strItem}' already defined for '${strOption}' option",
ERROR_OPTION_DUPLICATE_KEY);
}
# Set key/value
$rhOption->{$strOption}{value}{$strKey} = $strValue;
}
}
else
{
$rhOption->{$strOption}{value} = $strValue;
}
# If not config sourced then it must be a param
if (!defined($rhOption->{$strOption}{source}))
{
$rhOption->{$strOption}{source} = CFGDEF_SOURCE_PARAM;
}
}
# Else try to set a default
elsif ($bDependResolved)
{
# Source is default for this option
$rhOption->{$strOption}{source} = CFGDEF_SOURCE_DEFAULT;
# Check for default in command then option
my $strDefault = cfgDefOptionDefault($iCommandId, $iOptionId);
# If default is defined
if (defined($strDefault))
{
# Only set default if dependency is resolved
$rhOption->{$strOption}{value} = $strDefault if !$rhOption->{$strOption}{negate};
}
# Else check required
elsif (cfgDefOptionRequired($iCommandId, $iOptionId))
{
confess &log(ERROR,
cfgCommandName($iCommandId) . " command requires option: ${strOption}" .
ERROR_OPTION_REQUIRED);
}
}
$oOptionResolved{$strOption} = true;
}
}
# Make sure all options specified on the command line are valid
foreach my $strOption (sort(keys(%{$oOptionTest})))
{
# Strip "no-" off the option
$strOption = $strOption =~ /^no\-/ ? substr($strOption, 3) : $strOption;
if (!$rhOption->{$strOption}{valid})
{
confess &log(
ERROR, "option '${strOption}' not valid for command '" . cfgCommandName($iCommandId) . "'", ERROR_OPTION_COMMAND);
}
}
return $rhOption;
}
####################################################################################################################################
# Load the configuration
####################################################################################################################################
@@ -438,174 +112,11 @@ sub configTestLoad
my $self = shift;
my $iCommandId = shift;
@ARGV = $self->commandTestWrite(cfgCommandName($iCommandId), $self->{&CONFIGENVTEST});
my @stryArg = $self->commandTestWrite(cfgCommandName($iCommandId), $self->{&CONFIGENVTEST});
my $strConfigJson = cfgParseTest(backrestBin(), join('|', @stryArg));
$self->testResult(
sub {configLoad(
false, backrestBin(), cfgCommandName($iCommandId),
(JSON::PP->new()->allow_nonref())->encode(testOptionValidate($iCommandId)))},
true, 'config load: ' . join(" ", @ARGV));
}
####################################################################################################################################
# optionTestSetByName - used only by config unit tests, general option set should be done with optionTestSet
####################################################################################################################################
sub optionTestSetByName
{
my $self = shift;
my $strOption = shift;
my $strValue = shift;
$self->{&CONFIGENVTEST}{option}{$strOption} = $strValue;
}
####################################################################################################################################
# configTestLoadExpect - used only by config unit tests, for general config load use configTestLoad()
####################################################################################################################################
sub configTestLoadExpect
{
my $self = shift;
my $strCommand = shift;
my $iExpectedError = shift;
my $strErrorParam1 = shift;
my $strErrorParam2 = shift;
my $strErrorParam3 = shift;
@ARGV = $self->commandTestWrite($strCommand, $self->{&CONFIGENVTEST});
$self->configTestClear();
&log(INFO, " command line: " . join(" ", @ARGV));
my $bErrorFound = false;
eval
{
configLoad(false);
return true;
}
or do
{
my $oException = $EVAL_ERROR;
if (!defined($iExpectedError))
{
confess $oException;
}
$bErrorFound = true;
if (isException(\$oException))
{
if ($oException->code() != $iExpectedError)
{
confess "expected error ${iExpectedError} from configLoad but got [" . $oException->code() .
"] '" . $oException->message() . "'";
}
my $strError;
if ($iExpectedError == ERROR_OPTION_REQUIRED)
{
$strError = "${strCommand} command requires option: ${strErrorParam1}" .
(defined($strErrorParam2) ? "\nHINT: ${strErrorParam2}" : '');
}
elsif ($iExpectedError == ERROR_COMMAND_REQUIRED)
{
$strError = "command must be specified";
}
elsif ($iExpectedError == ERROR_OPTION_INVALID)
{
$strError = "option '${strErrorParam1}' not valid without option '${strErrorParam2}'";
if (defined($strErrorParam3))
{
$strError .= @{$strErrorParam3} == 1 ? " = '$$strErrorParam3[0]'" :
" in ('" . join("', '",@{ $strErrorParam3}) . "')";
}
}
elsif ($iExpectedError == ERROR_OPTION_COMMAND)
{
$strError = "option '${strErrorParam1}' not valid for command '${strErrorParam2}'";
}
elsif ($iExpectedError == ERROR_OPTION_INVALID_VALUE)
{
$strError = "'${strErrorParam1}' is not valid for '${strErrorParam2}' option" .
(defined($strErrorParam3) ? "\nHINT: ${strErrorParam3}." : '');
}
elsif ($iExpectedError == ERROR_OPTION_MULTIPLE_VALUE)
{
$strError = "option '${strErrorParam1}' cannot be specified multiple times";
}
elsif ($iExpectedError == ERROR_OPTION_INVALID_RANGE)
{
$strError = "'${strErrorParam1}' is not valid for '${strErrorParam2}' option";
}
elsif ($iExpectedError == ERROR_OPTION_INVALID_PAIR)
{
$strError = "'${strErrorParam1}' not valid key/value for '${strErrorParam2}' option";
}
elsif ($iExpectedError == ERROR_OPTION_NEGATE)
{
$strError = "option '${strErrorParam1}' cannot be both set and negated";
}
elsif ($iExpectedError == ERROR_FILE_INVALID)
{
$strError = "'${strErrorParam1}' is not a file";
}
else
{
confess
"must construct message for error ${iExpectedError}, use this as an example: '" . $oException->message() . "'";
}
if ($oException->message() ne $strError)
{
confess "expected error message \"${strError}\" from configLoad but got \"" . $oException->message() . "\"";
}
}
else
{
confess "configLoad should throw pgBackRest::Common::Exception:\n$oException";
}
};
if (!$bErrorFound && defined($iExpectedError))
{
confess "expected error ${iExpectedError} from configLoad but got success";
}
}
####################################################################################################################################
# configTestExpect - used only by config unit tests
####################################################################################################################################
sub optionTestExpect
{
my $self = shift;
my $iOptionId = shift;
my $strExpectedValue = shift;
my $strExpectedKey = shift;
my $strOption = cfgOptionName($iOptionId);
if (defined($strExpectedValue))
{
my $strActualValue = cfgOption($iOptionId);
if (defined($strExpectedKey))
{
$strActualValue = $$strActualValue{$strExpectedKey};
}
if (!defined($strActualValue))
{
confess "expected option ${strOption} to have value ${strExpectedValue} but [undef] found instead";
}
$strActualValue eq $strExpectedValue
or confess "expected option ${strOption} to have value ${strExpectedValue} but ${strActualValue} found instead";
}
elsif (cfgOptionTest(cfgOptionId($strOption)))
{
confess "expected option ${strOption} to be [undef], but " . cfgOption(cfgOptionId($strOption)) . ' found instead';
}
sub {configLoad(false, backrestBin(), cfgCommandName($iCommandId), $strConfigJson)},
true, 'config load: ' . join(" ", @stryArg));
}
1;

View File

@@ -35,6 +35,7 @@ testRun()
TEST_RESULT_VOID(cfgLoad(strLstSize(argList), strLstPtr(argList)), "load local config");
TEST_RESULT_STR(strPtr(cfgExe()), "pgbackrest", "check exe");
TEST_RESULT_INT(logLevelStdOut, logLevelOff, "console logging is off");
TEST_RESULT_INT(logLevelStdErr, logLevelWarn, "stderr logging is warn");
@@ -46,8 +47,9 @@ testRun()
strLstAdd(argList, strNew("--repo1-path=/path/to/repo"));
strLstAdd(argList, strNew("stanza-create"));
TEST_RESULT_VOID(cfgLoad(strLstSize(argList), strLstPtr(argList)), "load local config");
TEST_RESULT_VOID(cfgLoadParam(strLstSize(argList), strLstPtr(argList), strNew("pgbackrest2")), "load local config");
TEST_RESULT_STR(strPtr(cfgExe()), "pgbackrest2", "check exe");
TEST_RESULT_INT(logLevelStdOut, logLevelWarn, "console logging is off");
TEST_RESULT_INT(logLevelStdErr, logLevelOff, "stderr logging is off");