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

Validate configuration options in a single pass.

By pre-calculating and storing the option dependencies in parse.auto.c validation can be completed in a single pass, which is both simpler and faster.
This commit is contained in:
David Steele 2018-08-11 12:55:33 -04:00
parent f06bf9e832
commit cd5df3570b
5 changed files with 398 additions and 178 deletions

View File

@ -28,6 +28,7 @@ use pgBackRestBuild::Config::Data;
use constant BLDLCL_FILE_DEFINE => 'parse';
use constant BLDLCL_DATA_OPTION => '01-dataOption';
use constant BLDLCL_DATA_OPTION_RESOLVE => '01-dataOptionResolve';
####################################################################################################################################
# Definitions for constants and data to build
@ -48,6 +49,11 @@ my $rhBuild =
{
&BLD_SUMMARY => 'Option parse data',
},
&BLDLCL_DATA_OPTION_RESOLVE =>
{
&BLD_SUMMARY => 'Order for option parse resolution',
},
},
},
},
@ -171,6 +177,75 @@ sub buildConfigParse
$rhBuild->{&BLD_FILE}{&BLDLCL_FILE_DEFINE}{&BLD_DATA}{&BLDLCL_DATA_OPTION}{&BLD_SOURCE} = $strBuildSource;
# Build option resolve order list. This allows the option validation in C to take place in a single pass.
#
# Always process the stanza option first since confusing error message are produced if it is missing.
#-------------------------------------------------------------------------------------------------------------------------------
my @stryResolveOrder = (buildConfigOptionEnum(CFGOPT_STANZA));
my $rhResolved = {&CFGOPT_STANZA => true};
my $bAllResolved;
do
{
# Assume that we will finish on this loop
$bAllResolved = true;
# Loop through all options
foreach my $strOption (sort(keys(%{$rhConfigDefine})))
{
my $bSkip = false;
# Check the default depend
my $strOptionDepend =
ref($rhConfigDefine->{$strOption}{&CFGDEF_DEPEND}) eq 'HASH' ?
$rhConfigDefine->{$strOption}{&CFGDEF_DEPEND}{&CFGDEF_DEPEND_OPTION} :
$rhConfigDefine->{$strOption}{&CFGDEF_DEPEND};
if (defined($strOptionDepend) && !$rhResolved->{$strOptionDepend})
{
# &log(WARN, "$strOptionDepend is not resolved");
$bSkip = true;
}
# Check the command depends
foreach my $strCommand (sort(keys(%{$rhConfigDefine->{$strOption}{&CFGDEF_COMMAND}})))
{
my $strOptionDepend =
ref($rhConfigDefine->{$strOption}{&CFGDEF_COMMAND}{$strCommand}{&CFGDEF_DEPEND}) eq 'HASH' ?
$rhConfigDefine->{$strOption}{&CFGDEF_COMMAND}{$strCommand}{&CFGDEF_DEPEND}{&CFGDEF_DEPEND_OPTION} :
$rhConfigDefine->{$strOption}{&CFGDEF_COMMAND}{$strCommand}{&CFGDEF_DEPEND};
if (defined($strOptionDepend) && !$rhResolved->{$strOptionDepend})
{
$bSkip = true;
}
}
# If dependency was not found try again on the next loop
if ($bSkip)
{
$bAllResolved = false;
}
# Else add option to resolve order list
elsif (!$rhResolved->{$strOption})
{
$rhResolved->{$strOption} = true;
for (my $iIndex = 0; $iIndex < $rhConfigDefine->{$strOption}{&CFGDEF_INDEX_TOTAL}; $iIndex++)
{
push(@stryResolveOrder, buildConfigOptionEnum($strOption) . ($iIndex == 0 ? '' : " + $iIndex"));
}
}
}
}
while (!$bAllResolved);
$rhBuild->{&BLD_FILE}{&BLDLCL_FILE_DEFINE}{&BLD_DATA}{&BLDLCL_DATA_OPTION_RESOLVE}{&BLD_SOURCE} =
"static const ConfigOption optionResolveOrder[CFG_OPTION_TOTAL] =\n" .
"{\n" .
" " . join(",\n ", @stryResolveOrder) . ",\n" .
"};\n";
return $rhBuild;
}

View File

@ -45,6 +45,10 @@
</release-improvement-list>
<release-development-list>
<release-item>
<p>Validate configuration options in a single pass. By pre-calculating and storing the option dependencies in <file>parse.auto.c</file> validation can be completed in a single pass, which is both simpler and faster.</p>
</release-item>
<release-item>
<release-item-contributor-list>
<release-item-ideator id="frost.stephen"/>

View File

@ -2190,3 +2190,171 @@ static const struct option optionList[] =
.name = NULL
}
};
/***********************************************************************************************************************************
Order for option parse resolution
***********************************************************************************************************************************/
static const ConfigOption optionResolveOrder[CFG_OPTION_TOTAL] =
{
cfgOptStanza,
cfgOptArchiveAsync,
cfgOptArchiveGetQueueMax,
cfgOptArchivePushQueueMax,
cfgOptArchiveTimeout,
cfgOptBackupStandby,
cfgOptBufferSize,
cfgOptChecksumPage,
cfgOptCmdSsh,
cfgOptCommand,
cfgOptCompress,
cfgOptCompressLevel,
cfgOptCompressLevelNetwork,
cfgOptConfig,
cfgOptConfigIncludePath,
cfgOptConfigPath,
cfgOptDbInclude,
cfgOptDbTimeout,
cfgOptDelta,
cfgOptHostId,
cfgOptLinkAll,
cfgOptLinkMap,
cfgOptLockPath,
cfgOptLogLevelConsole,
cfgOptLogLevelFile,
cfgOptLogLevelStderr,
cfgOptLogPath,
cfgOptLogTimestamp,
cfgOptManifestSaveThreshold,
cfgOptNeutralUmask,
cfgOptOnline,
cfgOptOutput,
cfgOptPerlOption,
cfgOptPgHost,
cfgOptPgHost + 1,
cfgOptPgHost + 2,
cfgOptPgHost + 3,
cfgOptPgHost + 4,
cfgOptPgHost + 5,
cfgOptPgHost + 6,
cfgOptPgHost + 7,
cfgOptPgHostCmd,
cfgOptPgHostCmd + 1,
cfgOptPgHostCmd + 2,
cfgOptPgHostCmd + 3,
cfgOptPgHostCmd + 4,
cfgOptPgHostCmd + 5,
cfgOptPgHostCmd + 6,
cfgOptPgHostCmd + 7,
cfgOptPgHostConfig,
cfgOptPgHostConfig + 1,
cfgOptPgHostConfig + 2,
cfgOptPgHostConfig + 3,
cfgOptPgHostConfig + 4,
cfgOptPgHostConfig + 5,
cfgOptPgHostConfig + 6,
cfgOptPgHostConfig + 7,
cfgOptPgHostConfigIncludePath,
cfgOptPgHostConfigIncludePath + 1,
cfgOptPgHostConfigIncludePath + 2,
cfgOptPgHostConfigIncludePath + 3,
cfgOptPgHostConfigIncludePath + 4,
cfgOptPgHostConfigIncludePath + 5,
cfgOptPgHostConfigIncludePath + 6,
cfgOptPgHostConfigIncludePath + 7,
cfgOptPgHostConfigPath,
cfgOptPgHostConfigPath + 1,
cfgOptPgHostConfigPath + 2,
cfgOptPgHostConfigPath + 3,
cfgOptPgHostConfigPath + 4,
cfgOptPgHostConfigPath + 5,
cfgOptPgHostConfigPath + 6,
cfgOptPgHostConfigPath + 7,
cfgOptPgHostPort,
cfgOptPgHostPort + 1,
cfgOptPgHostPort + 2,
cfgOptPgHostPort + 3,
cfgOptPgHostPort + 4,
cfgOptPgHostPort + 5,
cfgOptPgHostPort + 6,
cfgOptPgHostPort + 7,
cfgOptPgHostUser,
cfgOptPgHostUser + 1,
cfgOptPgHostUser + 2,
cfgOptPgHostUser + 3,
cfgOptPgHostUser + 4,
cfgOptPgHostUser + 5,
cfgOptPgHostUser + 6,
cfgOptPgHostUser + 7,
cfgOptPgPath,
cfgOptPgPath + 1,
cfgOptPgPath + 2,
cfgOptPgPath + 3,
cfgOptPgPath + 4,
cfgOptPgPath + 5,
cfgOptPgPath + 6,
cfgOptPgPath + 7,
cfgOptPgPort,
cfgOptPgPort + 1,
cfgOptPgPort + 2,
cfgOptPgPort + 3,
cfgOptPgPort + 4,
cfgOptPgPort + 5,
cfgOptPgPort + 6,
cfgOptPgPort + 7,
cfgOptPgSocketPath,
cfgOptPgSocketPath + 1,
cfgOptPgSocketPath + 2,
cfgOptPgSocketPath + 3,
cfgOptPgSocketPath + 4,
cfgOptPgSocketPath + 5,
cfgOptPgSocketPath + 6,
cfgOptPgSocketPath + 7,
cfgOptProcess,
cfgOptProcessMax,
cfgOptProtocolTimeout,
cfgOptRepoCipherType,
cfgOptRepoHardlink,
cfgOptRepoHost,
cfgOptRepoHostCmd,
cfgOptRepoHostConfig,
cfgOptRepoHostConfigIncludePath,
cfgOptRepoHostConfigPath,
cfgOptRepoHostPort,
cfgOptRepoHostUser,
cfgOptRepoPath,
cfgOptRepoRetentionArchive,
cfgOptRepoRetentionArchiveType,
cfgOptRepoRetentionDiff,
cfgOptRepoRetentionFull,
cfgOptRepoType,
cfgOptResume,
cfgOptSet,
cfgOptSpoolPath,
cfgOptStartFast,
cfgOptStopAuto,
cfgOptTablespaceMap,
cfgOptTablespaceMapAll,
cfgOptTest,
cfgOptTestDelay,
cfgOptTestPoint,
cfgOptType,
cfgOptArchiveCheck,
cfgOptArchiveCopy,
cfgOptForce,
cfgOptRecoveryOption,
cfgOptRepoCipherPass,
cfgOptRepoS3Bucket,
cfgOptRepoS3CaFile,
cfgOptRepoS3CaPath,
cfgOptRepoS3Endpoint,
cfgOptRepoS3Host,
cfgOptRepoS3Key,
cfgOptRepoS3KeySecret,
cfgOptRepoS3Region,
cfgOptRepoS3Token,
cfgOptRepoS3VerifySsl,
cfgOptTarget,
cfgOptTargetAction,
cfgOptTargetExclusive,
cfgOptTargetTimeline,
};

View File

@ -674,204 +674,178 @@ configParse(unsigned int argListSize, const char *argList[], bool resetLogLevel)
// Phase 3: validate option definitions and load into configuration
// ---------------------------------------------------------------------------------------------------------------------
bool allResolved = false;
bool optionResolved[CFG_OPTION_TOTAL] = {false};
ConfigOption optionId = 0;
// If stanza is required but not set then many other options will likely not be set unless they were specified on the
// command line, which leads to confusing errors like 'command requires option: pg1-path' when what is really missing is
// the stanza. In this case start start the loop at the stanza option so the user gets a sensible error.
if (!parseOptionList[cfgOptStanza].found && cfgDefOptionRequired(commandDefId, cfgDefOptStanza))
optionId = cfgOptStanza;
do
for (unsigned int optionOrderIdx = 0; optionOrderIdx < CFG_OPTION_TOTAL; optionOrderIdx++)
{
// If first loop starts partway through we know that all options cannot possibly be resolved on the first loop.
// After that we'll assume that all options will be resolved in the next loop and set allResolved = false if we find
// something that can't be resolved yet.
if (optionId == 0)
allResolved = true;
// Validate options based on the option resolve order. This allows resolving all options in a single pass.
ConfigOption optionId = optionResolveOrder[optionOrderIdx];
// Loop through all options
for (; optionId < CFG_OPTION_TOTAL; optionId++)
// Get the option data parsed from the command-line
ParseOption *parseOption = &parseOptionList[optionId];
// Get the option definition id -- will be used to look up option rules
ConfigDefineOption optionDefId = cfgOptionDefIdFromId(optionId);
ConfigDefineOptionType optionDefType = cfgDefOptionType(optionDefId);
// Error if the option is not valid for this command
if (parseOption->found && !cfgDefOptionValid(commandDefId, optionDefId))
{
// Get the option data parsed from the command-line
ParseOption *parseOption = &parseOptionList[optionId];
THROW_FMT(
OptionInvalidError, "option '%s' not valid for command '%s'", cfgOptionName(optionId),
cfgCommandName(cfgCommand()));
}
// Get the option definition id -- will be used to look up option rules
ConfigDefineOption optionDefId = cfgOptionDefIdFromId(optionId);
ConfigDefineOptionType optionDefType = cfgDefOptionType(optionDefId);
// Error if this option is secure and cannot be passed on the command line
if (parseOption->found && parseOption->source == cfgSourceParam && cfgDefOptionSecure(optionDefId))
{
THROW_FMT(
OptionInvalidError,
"option '%s' is not allowed on the command-line\n"
"HINT: this option could expose secrets in the process list.\n"
"HINT: specify the option in '%s' instead.",
cfgOptionName(optionId), cfgDefOptionDefault(commandDefId, cfgDefOptConfig));
}
// Skip this option if it has already been resolved
if (optionResolved[optionId])
continue;
// Error if this option does not allow multiple arguments
if (parseOption->valueList != NULL && strLstSize(parseOption->valueList) > 1 &&
!(cfgDefOptionType(cfgOptionDefIdFromId(optionId)) == cfgDefOptTypeHash ||
cfgDefOptionType(cfgOptionDefIdFromId(optionId)) == cfgDefOptTypeList))
{
THROW_FMT(OptionInvalidError, "option '%s' cannot have multiple arguments", cfgOptionName(optionId));
}
// Error if the option is not valid for this command
if (parseOption->found && !cfgDefOptionValid(commandDefId, optionDefId))
// Is the option valid for this command? If not, there is nothing more to do.
cfgOptionValidSet(optionId, cfgDefOptionValid(commandDefId, optionDefId));
if (!cfgOptionValid(optionId))
continue;
// Is the value set for this option?
bool optionSet =
parseOption->found && (optionDefType == cfgDefOptTypeBoolean || !parseOption->negate) &&
!parseOption->reset;
// Set negate flag
cfgOptionNegateSet(optionId, parseOption->negate);
// Set reset flag
cfgOptionResetSet(optionId, parseOption->reset);
// Check option dependencies
bool dependResolved = true;
if (cfgDefOptionDepend(commandDefId, optionDefId))
{
ConfigOption dependOptionId =
cfgOptionIdFromDefId(cfgDefOptionDependOption(commandDefId, optionDefId), cfgOptionIndex(optionId));
ConfigDefineOption dependOptionDefId = cfgOptionDefIdFromId(dependOptionId);
ConfigDefineOptionType dependOptionDefType = cfgDefOptionType(dependOptionDefId);
// Get the depend option value
const Variant *dependValue = cfgOption(dependOptionId);
if (dependValue != NULL)
{
THROW_FMT(
OptionInvalidError, "option '%s' not valid for command '%s'", cfgOptionName(optionId),
cfgCommandName(cfgCommand()));
}
// Error if this option is secure and cannot be passed on the command line
if (parseOption->found && parseOption->source == cfgSourceParam && cfgDefOptionSecure(optionDefId))
{
THROW_FMT(
OptionInvalidError,
"option '%s' is not allowed on the command-line\n"
"HINT: this option could expose secrets in the process list.\n"
"HINT: specify the option in '%s' instead.",
cfgOptionName(optionId), cfgDefOptionDefault(commandDefId, cfgDefOptConfig));
}
// Error if this option does not allow multiple arguments
if (parseOption->valueList != NULL && strLstSize(parseOption->valueList) > 1 &&
!(cfgDefOptionType(cfgOptionDefIdFromId(optionId)) == cfgDefOptTypeHash ||
cfgDefOptionType(cfgOptionDefIdFromId(optionId)) == cfgDefOptTypeList))
{
THROW_FMT(OptionInvalidError, "option '%s' cannot have multiple arguments", cfgOptionName(optionId));
}
// Is the option valid for this command? If not, mark it as resolved since there is nothing more to do.
cfgOptionValidSet(optionId, cfgDefOptionValid(commandDefId, optionDefId));
if (!cfgOptionValid(optionId))
{
optionResolved[optionId] = true;
continue;
}
// Is the value set for this option?
bool optionSet =
parseOption->found && (optionDefType == cfgDefOptTypeBoolean || !parseOption->negate) &&
!parseOption->reset;
// Set negate flag
cfgOptionNegateSet(optionId, parseOption->negate);
// Set reset flag
cfgOptionResetSet(optionId, parseOption->reset);
// Check option dependencies
bool dependResolved = true;
if (cfgDefOptionDepend(commandDefId, optionDefId))
{
ConfigOption dependOptionId =
cfgOptionIdFromDefId(cfgDefOptionDependOption(commandDefId, optionDefId), cfgOptionIndex(optionId));
ConfigDefineOption dependOptionDefId = cfgOptionDefIdFromId(dependOptionId);
ConfigDefineOptionType dependOptionDefType = cfgDefOptionType(dependOptionDefId);
// Make sure the depend option has been resolved, otherwise skip this option for now
if (!optionResolved[dependOptionId])
if (dependOptionDefType == cfgDefOptTypeBoolean)
{
allResolved = false;
continue;
if (cfgOptionBool(dependOptionId))
dependValue = varNewStrZ("1");
else
dependValue = varNewStrZ("0");
}
}
// Get the depend option value
const Variant *dependValue = cfgOption(dependOptionId);
// Can't resolve if the depend option value is null
if (dependValue == NULL)
{
dependResolved = false;
if (dependValue != NULL)
// If depend not resolved and option value is set on the command-line then error. See unresolved list
// depend below for a detailed explanation.
if (optionSet && parseOption->source == cfgSourceParam)
{
if (dependOptionDefType == cfgDefOptTypeBoolean)
{
if (cfgOptionBool(dependOptionId))
dependValue = varNewStrZ("1");
else
dependValue = varNewStrZ("0");
}
THROW_FMT(
OptionInvalidError, "option '%s' not valid without option '%s'", cfgOptionName(optionId),
cfgOptionName(dependOptionId));
}
}
// If a depend list exists, make sure the value is in the list
else if (cfgDefOptionDependValueTotal(commandDefId, optionDefId) > 0)
{
dependResolved = cfgDefOptionDependValueValid(commandDefId, optionDefId, strPtr(varStr(dependValue)));
// Can't resolve if the depend option value is null
if (dependValue == NULL)
// If depend not resolved and option value is set on the command-line then error. It's OK to have
// unresolved options in the config file because they may be there for another command. For instance,
// spool-path is only loaded for the archive-push command when archive-async=y, and the presense of
// spool-path in the config file should not cause an error here, it will just end up null.
if (!dependResolved && optionSet && parseOption->source == cfgSourceParam)
{
dependResolved = false;
// Get the depend option name
String *dependOptionName = strNew(cfgOptionName(dependOptionId));
// if (optionSet)
if (optionSet && parseOption->source == cfgSourceParam)
// Build the list of possible depend values
StringList *dependValueList = strLstNew();
for (unsigned int listIdx = 0;
listIdx < cfgDefOptionDependValueTotal(commandDefId, optionDefId); listIdx++)
{
THROW_FMT(
OptionInvalidError, "option '%s' not valid without option '%s'", cfgOptionName(optionId),
cfgOptionName(dependOptionId));
}
}
// If a depend list exists, make sure the value is in the list
else if (cfgDefOptionDependValueTotal(commandDefId, optionDefId) > 0)
{
dependResolved = cfgDefOptionDependValueValid(commandDefId, optionDefId, strPtr(varStr(dependValue)));
const char *dependValue = cfgDefOptionDependValue(commandDefId, optionDefId, listIdx);
// If depend not resolved and option value is set on the command-line then error. It's OK to have
// unresolved options in the config file because they may be there for another command. For instance,
// spool-path is only loaded for the archive-push command when archive-async=y, and the presense of
// spool-path in the config file should not cause an error here, it will just end up null.
if (!dependResolved && optionSet && parseOption->source == cfgSourceParam)
{
// Get the depend option name
String *dependOptionName = strNew(cfgOptionName(dependOptionId));
// Build the list of possible depend values
StringList *dependValueList = strLstNew();
for (unsigned int listIdx = 0;
listIdx < cfgDefOptionDependValueTotal(commandDefId, optionDefId); listIdx++)
// Build list based on depend option type
switch (dependOptionDefType)
{
const char *dependValue = cfgDefOptionDependValue(commandDefId, optionDefId, listIdx);
// Build list based on depend option type
switch (dependOptionDefType)
// Boolean outputs depend option name as no-* when false
case cfgDefOptTypeBoolean:
{
// Boolean outputs depend option name as no-* when false
case cfgDefOptTypeBoolean:
{
if (strcmp(dependValue, "0") == 0)
dependOptionName = strNewFmt("no-%s", cfgOptionName(dependOptionId));
if (strcmp(dependValue, "0") == 0)
dependOptionName = strNewFmt("no-%s", cfgOptionName(dependOptionId));
break;
}
break;
}
// String is output with quotes
case cfgDefOptTypeString:
{
strLstAdd(dependValueList, strNewFmt("'%s'", dependValue));
break;
}
// String is output with quotes
case cfgDefOptTypeString:
{
strLstAdd(dependValueList, strNewFmt("'%s'", dependValue));
break;
}
// Other types are output plain
case cfgDefOptTypeFloat: // {uncovered - no depends of other types}
case cfgDefOptTypeHash:
case cfgDefOptTypeInteger:
case cfgDefOptTypeList:
case cfgDefOptTypeSize:
{
strLstAddZ(dependValueList, dependValue); // {+uncovered}
break; // {+uncovered}
}
// Other types are output plain
case cfgDefOptTypeFloat: // {uncovered - no depends of other types}
case cfgDefOptTypeHash:
case cfgDefOptTypeInteger:
case cfgDefOptTypeList:
case cfgDefOptTypeSize:
{
strLstAddZ(dependValueList, dependValue); // {+uncovered}
break; // {+uncovered}
}
}
// Build the error string
String *errorValue = strNew("");
if (strLstSize(dependValueList) == 1)
errorValue = strNewFmt(" = %s", strPtr(strLstGet(dependValueList, 0)));
else if (strLstSize(dependValueList) > 1)
errorValue = strNewFmt(" in (%s)", strPtr(strLstJoin(dependValueList, ", ")));
// Throw the error
THROW(
OptionInvalidError,
strPtr(
strNewFmt(
"option '%s' not valid without option '%s'%s", cfgOptionName(optionId),
strPtr(dependOptionName), strPtr(errorValue))));
}
// Build the error string
String *errorValue = strNew("");
if (strLstSize(dependValueList) == 1)
errorValue = strNewFmt(" = %s", strPtr(strLstGet(dependValueList, 0)));
else if (strLstSize(dependValueList) > 1)
errorValue = strNewFmt(" in (%s)", strPtr(strLstJoin(dependValueList, ", ")));
// Throw the error
THROW(
OptionInvalidError,
strPtr(
strNewFmt(
"option '%s' not valid without option '%s'%s", cfgOptionName(optionId),
strPtr(dependOptionName), strPtr(errorValue))));
}
}
}
// Is the option defined?
if (optionSet && dependResolved)
// Is the option resolved?
if (dependResolved)
{
// Is the option set?
if (optionSet)
{
if (optionDefType == cfgDefOptTypeBoolean)
{
@ -962,10 +936,10 @@ configParse(unsigned int argListSize, const char *argList[], bool resetLogLevel)
cfgOptionSet(optionId, parseOption->source, varNewStr(value));
}
}
else if (dependResolved && parseOption->negate)
else if (parseOption->negate)
cfgOptionSet(optionId, parseOption->source, NULL);
// Else try to set a default
else if (dependResolved)
else
{
// Get the default value for this option
const char *value = cfgDefOptionDefault(commandDefId, optionDefId);
@ -985,15 +959,8 @@ configParse(unsigned int argListSize, const char *argList[], bool resetLogLevel)
cfgOptionName(optionId), hint);
}
}
// Option is now resolved
optionResolved[optionId] = true;
}
// Restart option loop
optionId = 0;
}
while (!allResolved);
}
}
MEM_CONTEXT_TEMP_END();

View File

@ -552,6 +552,10 @@ testRun(void)
StringList *argList = NULL;
String *configFile = strNewFmt("%s/test.config", testPath());
TEST_RESULT_INT(
sizeof(optionResolveOrder) / sizeof(ConfigOption), CFG_OPTION_TOTAL,
"check that the option resolve list contains an entry for every option");
// -------------------------------------------------------------------------------------------------------------------------
argList = strLstNew();
strLstAdd(argList, strNew(TEST_BACKREST_EXE));
@ -718,6 +722,8 @@ testRun(void)
strLstAdd(argList, strNew("--stanza=db"));
strLstAdd(argList, strNew("--repo1-type=s3"));
strLstAdd(argList, strNew("--repo1-s3-key=xxx"));
strLstAdd(argList, strNew("--repo1-s3-bucket=xxx"));
strLstAdd(argList, strNew("--repo1-s3-endpoint=xxx"));
strLstAdd(argList, strNew(TEST_COMMAND_BACKUP));
TEST_ERROR(
configParse(strLstSize(argList), strLstPtr(argList), false), OptionInvalidError,