This commit is contained in:
Ryan Ofsky 2025-03-13 02:11:09 +01:00 committed by GitHub
commit 6d216396e4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 836 additions and 88 deletions

View file

@ -101,11 +101,43 @@ KeyInfo InterpretKey(std::string key)
* @param[in] flags ArgsManager registered argument flags
* @param[out] error Error description if settings value is not valid
*
* @return parsed settings value if it is valid, otherwise nullopt accompanied
* @return parsed settings value if it is valid, otherwise `nullopt` accompanied
* by a descriptive error string
*
* @note By design, the \ref InterpretValue function does mostly lossless
* conversions of command line arguments and configuration file values to JSON
* `common::SettingsValue` values, so higher level application code and GetArg
* helper methods can unambiguously determine original configuration strings
* from the JSON values, and flexibly interpret settings and provide good error
* feedback. Specifically:
* \n
* - JSON `null` value is never returned and is reserved for settings that were
* not configured at all.
*
* - JSON `false` value is returned for negated settings like `-nosetting` or
* `-nosetting=1`. `false` is also returned for boolean-only settings that
* have the ALLOW_BOOL flag and false values like `setting=0`.
*
* - JSON `true` value is returned for settings that have the ALLOW_BOOL flag
* and are specified on the command line without a value like `-setting`.
* `true` is also returned for boolean-only settings that have the ALLOW_BOOL
* flag and true values like `setting=1`. `true` is also returned for untyped
* legacy settings (see \ref IsTypedArg) that use double negation like
* `-nosetting=0`.
*
* - JSON `""` empty string value is returned for settings like `-setting=`
* that specify empty values. `""` is also returned for untyped legacy
* settings (see \ref IsTypedArg) that are specified on the command line
* without a value like `-setting`.
*
* - JSON strings like `"abc"` are returned for settings like `-setting=abc` if
* the setting has the ALLOW_STRING flag or is an untyped legacy setting.
*
* - JSON numbers like `123` are returned for settings like `-setting=123` if
* the setting enables integer parsing with the ALLOW_INT flag.
*/
std::optional<common::SettingsValue> InterpretValue(const KeyInfo& key, const std::string* value,
unsigned int flags, std::string& error)
unsigned int flags, std::string& error)
{
// Return negated settings as false values.
if (key.negated) {
@ -113,6 +145,16 @@ std::optional<common::SettingsValue> InterpretValue(const KeyInfo& key, const st
error = strprintf("Negating of -%s is meaningless and therefore forbidden", key.name);
return std::nullopt;
}
if (IsTypedArg(flags)) {
// If argument is typed, only allow negation with no value or with
// literal "1" value. Avoid calling InterpretBool and accepting
// other values which could be ambiguous.
if (value && *value != "1") {
error = strprintf("Cannot negate -%s at the same time as setting a value ('%s').", key.name, *value);
return std::nullopt;
}
return false;
}
// Double negatives like -nofoo=0 are supported (but discouraged)
if (value && !InterpretBool(*value)) {
LogPrintf("Warning: parsed potentially confusing double-negative -%s=%s\n", key.name, *value);
@ -120,11 +162,63 @@ std::optional<common::SettingsValue> InterpretValue(const KeyInfo& key, const st
}
return false;
}
if (!value && (flags & ArgsManager::DISALLOW_ELISION)) {
error = strprintf("Can not set -%s with no value. Please specify value with -%s=value.", key.name, key.name);
return std::nullopt;
if (value) {
if ((flags & ArgsManager::ALLOW_STRING) || !IsTypedArg(flags) || value->empty()) return *value;
if (flags & ArgsManager::ALLOW_INT) {
if (auto parsed_int = ToIntegral<int64_t>(*value)) return *parsed_int;
}
if (flags & ArgsManager::ALLOW_BOOL) {
if (*value == "0") return false;
if (*value == "1") return true;
}
error = strprintf("Cannot set -%s value to '%s'.", key.name, *value);
} else {
if (flags & ArgsManager::ALLOW_BOOL) return true;
if (!(flags & ArgsManager::DISALLOW_ELISION) && !IsTypedArg(flags)) return "";
error = strprintf("Cannot set -%s with no value. Please specify value with -%s=value.", key.name, key.name);
}
return value ? *value : "";
if (flags & ArgsManager::ALLOW_STRING) {
error = strprintf("%s %s", error, "It must be set to a string.");
} else if (flags & ArgsManager::ALLOW_INT) {
error = strprintf("%s %s", error, "It must be set to an integer.");
} else if (flags & ArgsManager::ALLOW_BOOL) {
error = strprintf("%s %s", error, "It must be set to 0 or 1.");
}
return std::nullopt;
}
//! Return string if setting is a nonempty string or number (-setting=abc,
//! -setting=123), "" if setting is false (-nosetting), otherwise return
//! nullopt. For legacy untyped args, coerce bool settings to strings as well.
static inline std::optional<std::string> ConvertToString(const common::SettingsValue& value, bool typed_arg)
{
if (value.isStr() && !value.get_str().empty()) return value.get_str();
if (value.isNum()) return value.getValStr();
if (typed_arg && value.isFalse()) return "";
if (!typed_arg && !value.isNull()) {
if (value.isBool()) return value.get_bool() ? "1" : "0";
return value.get_str();
}
return {};
}
//! Return int64 if setting is a number or bool, otherwise return nullopt. For
//! legacy untyped args, coerce string settings as well.
static inline std::optional<int64_t> ConvertToInt(const common::SettingsValue& value, bool typed_arg)
{
if (value.isNum()) return value.getInt<int64_t>();
if (value.isBool()) return value.get_bool();
if (!typed_arg && !value.isNull()) return LocaleIndependentAtoi<int64_t>(value.get_str());
return {};
}
//! Return bool if setting is a bool, otherwise return nullopt. For legacy
//! untyped args, coerce string settings as well.
static inline std::optional<bool> ConvertToBool(const common::SettingsValue& value, bool typed_arg)
{
if (value.isBool()) return value.get_bool();
if (!typed_arg && !value.isNull()) return InterpretBool(value.get_str());
return {};
}
// Define default constructor and destructor that are not inline, so code instantiating this class doesn't need to
@ -269,6 +363,29 @@ std::optional<unsigned int> ArgsManager::GetArgFlags(const std::string& name) co
return std::nullopt;
}
/**
* Check that arg has the right flags for use in a given context. Raises
* logic_error if this isn't the case, indicating the argument was registered
* with bad AddArg flags.
*
* Returns true if the arg is registered and has type checking enabled. Returns
* false if the arg was never registered or is untyped.
*/
bool ArgsManager::CheckArgFlags(const std::string& name,
uint32_t require,
uint32_t forbid,
const char* context) const
{
std::optional<unsigned int> flags = GetArgFlags(name);
if (!flags || !IsTypedArg(*flags)) return false;
if ((*flags & require) != require || (*flags & forbid) != 0) {
throw std::logic_error(
strprintf("Bug: Can't call %s on arg %s registered with flags 0x%08x (requires 0x%x, disallows 0x%x)",
context, name, *flags, require, forbid));
}
return true;
}
fs::path ArgsManager::GetPathArg(std::string arg, const fs::path& default_value) const
{
if (IsArgNegated(arg)) return fs::path{};
@ -361,9 +478,10 @@ std::optional<const ArgsManager::Command> ArgsManager::GetCommand() const
std::vector<std::string> ArgsManager::GetArgs(const std::string& strArg) const
{
bool typed_arg = CheckArgFlags(strArg, /*require=*/ ALLOW_STRING | ALLOW_LIST, /*forbid=*/ 0, __func__);
std::vector<std::string> result;
for (const common::SettingsValue& value : GetSettingsList(strArg)) {
result.push_back(value.isFalse() ? "0" : value.isTrue() ? "1" : value.get_str());
result.push_back(ConvertToString(value, typed_arg).value_or(""));
}
return result;
}
@ -461,22 +579,13 @@ std::string ArgsManager::GetArg(const std::string& strArg, const std::string& st
std::optional<std::string> ArgsManager::GetArg(const std::string& strArg) const
{
const common::SettingsValue value = GetSetting(strArg);
return SettingToString(value);
}
std::optional<std::string> SettingToString(const common::SettingsValue& value)
{
if (value.isNull()) return std::nullopt;
if (value.isFalse()) return "0";
if (value.isTrue()) return "1";
if (value.isNum()) return value.getValStr();
return value.get_str();
bool typed_arg = CheckArgFlags(strArg, /*require=*/ ALLOW_STRING, /*forbid=*/ ALLOW_LIST, __func__);
return ConvertToString(GetSetting(strArg), typed_arg);
}
std::string SettingToString(const common::SettingsValue& value, const std::string& strDefault)
{
return SettingToString(value).value_or(strDefault);
return ConvertToString(value, /*typed_arg=*/false).value_or(strDefault);
}
int64_t ArgsManager::GetIntArg(const std::string& strArg, int64_t nDefault) const
@ -486,22 +595,13 @@ int64_t ArgsManager::GetIntArg(const std::string& strArg, int64_t nDefault) cons
std::optional<int64_t> ArgsManager::GetIntArg(const std::string& strArg) const
{
const common::SettingsValue value = GetSetting(strArg);
return SettingToInt(value);
}
std::optional<int64_t> SettingToInt(const common::SettingsValue& value)
{
if (value.isNull()) return std::nullopt;
if (value.isFalse()) return 0;
if (value.isTrue()) return 1;
if (value.isNum()) return value.getInt<int64_t>();
return LocaleIndependentAtoi<int64_t>(value.get_str());
bool typed_arg = CheckArgFlags(strArg, /*require=*/ ALLOW_INT, /*forbid=*/ ALLOW_LIST, __func__);
return ConvertToInt(GetSetting(strArg), typed_arg);
}
int64_t SettingToInt(const common::SettingsValue& value, int64_t nDefault)
{
return SettingToInt(value).value_or(nDefault);
return ConvertToInt(value, /*typed_arg=*/false).value_or(nDefault);
}
bool ArgsManager::GetBoolArg(const std::string& strArg, bool fDefault) const
@ -511,20 +611,13 @@ bool ArgsManager::GetBoolArg(const std::string& strArg, bool fDefault) const
std::optional<bool> ArgsManager::GetBoolArg(const std::string& strArg) const
{
const common::SettingsValue value = GetSetting(strArg);
return SettingToBool(value);
}
std::optional<bool> SettingToBool(const common::SettingsValue& value)
{
if (value.isNull()) return std::nullopt;
if (value.isBool()) return value.get_bool();
return InterpretBool(value.get_str());
bool typed_arg = CheckArgFlags(strArg, /*require=*/ ALLOW_BOOL, /*forbid=*/ ALLOW_LIST, __func__);
return ConvertToBool(GetSetting(strArg), typed_arg);
}
bool SettingToBool(const common::SettingsValue& value, bool fDefault)
{
return SettingToBool(value).value_or(fDefault);
return ConvertToBool(value, /*typed_arg=*/false).value_or(fDefault);
}
bool ArgsManager::SoftSetArg(const std::string& strArg, const std::string& strValue)
@ -537,15 +630,17 @@ bool ArgsManager::SoftSetArg(const std::string& strArg, const std::string& strVa
bool ArgsManager::SoftSetBoolArg(const std::string& strArg, bool fValue)
{
if (fValue)
return SoftSetArg(strArg, std::string("1"));
else
return SoftSetArg(strArg, std::string("0"));
LOCK(cs_args);
CheckArgFlags(strArg, /*require=*/ ALLOW_BOOL, /*forbid=*/ ALLOW_LIST, __func__);
if (IsArgSet(strArg)) return false;
m_settings.forced_settings[SettingName(strArg)] = fValue;
return true;
}
void ArgsManager::ForceSetArg(const std::string& strArg, const std::string& strValue)
{
LOCK(cs_args);
CheckArgFlags(strArg, /*require=*/ ALLOW_STRING, /*forbid=*/ 0, __func__);
m_settings.forced_settings[SettingName(strArg)] = strValue;
}
@ -580,6 +675,24 @@ void ArgsManager::AddArg(const std::string& name, const std::string& help, unsig
if (flags & ArgsManager::NETWORK_ONLY) {
m_network_only_args.emplace(arg_name);
}
// Disallow flag combinations that would result in nonsensical behavior or a bad UX.
if ((flags & ALLOW_ANY) && (flags & (ALLOW_BOOL | ALLOW_INT | ALLOW_STRING))) {
throw std::logic_error(strprintf("Bug: bad %s flags. ALLOW_{BOOL|INT|STRING} flags are incompatible with "
"ALLOW_ANY (typed arguments need to be type checked)", arg_name));
}
if ((flags & ALLOW_BOOL) && (flags & DISALLOW_ELISION)) {
throw std::logic_error(strprintf("Bug: bad %s flags. ALLOW_BOOL flag is incompatible with DISALLOW_ELISION "
"(boolean arguments should not require argument values)", arg_name));
}
if ((flags & ALLOW_INT) && (flags & ALLOW_STRING)) {
throw std::logic_error(strprintf("Bug: bad %s flags. ALLOW_INT flag is incompatible with ALLOW_STRING "
"(any valid integer is also a valid string)", arg_name));
}
if ((flags & ALLOW_BOOL) && (flags & (ALLOW_INT | ALLOW_STRING))) {
throw std::logic_error(strprintf("Bug: bad %s flags. ALLOW_BOOL flag may not currently be specified with ALLOW_INT or ALLOW_STRING "
"(integer and string argument values cannot currently be omitted)", arg_name));
}
}
void ArgsManager::AddHiddenArgs(const std::vector<std::string>& names)
@ -793,7 +906,7 @@ std::variant<ChainType, std::string> ArgsManager::GetChainArg() const
/* ignore_default_section_config= */ false,
/*ignore_nonpersistent=*/false,
/* get_chain_type= */ true);
return value.isNull() ? false : value.isBool() ? value.get_bool() : InterpretBool(value.get_str());
return ConvertToBool(value, /*typed_arg=*/false).value_or(false);
};
const bool fRegTest = get_net("-regtest");

View file

@ -87,27 +87,94 @@ struct SectionInfo {
};
std::string SettingToString(const common::SettingsValue&, const std::string&);
std::optional<std::string> SettingToString(const common::SettingsValue&);
int64_t SettingToInt(const common::SettingsValue&, int64_t);
std::optional<int64_t> SettingToInt(const common::SettingsValue&);
bool SettingToBool(const common::SettingsValue&, bool);
std::optional<bool> SettingToBool(const common::SettingsValue&);
class ArgsManager
{
public:
/**
* Flags controlling how config and command line arguments are validated and
* interpreted.
* Flags controlling how config and command line arguments are parsed.
*
* The flags below provide very basic type checking, designed to catch
* obvious configuration mistakes and provide helpful error messages.
* Specifying these flags is not a substitute for actually validating
* setting values that are parsed and making sure they are legitimate.
*
* Summary of recommended flags:
*
* - For most settings, just use standalone ALLOW_BOOL, ALLOW_INT, or
* ALLOW_STRING flags.
*
* - If your setting accepts multiple values and you want to read all the
* values, not just the last value, add | ALLOW_LIST to the flags.
*
* - Only use the DISALLOW_NEGATION flag if your setting really cannot
* function without a value, so the command line interface will generally
* support negation and be more consistent.
*
* Detailed description of flags:
*
* The ALLOW_STRING, ALLOW_INT, and ALLOW_BOOL flags control what syntaxes are
* accepted, according to the following chart:
*
* | Syntax | STRING | INT | BOOL |
* | -------- | :----: | :-: | :--: |
* | -foo=abc | X | | |
* | -foo=123 | X | X | |
* | -foo=0 | X | X | X |
* | -foo=1 | X | X | X |
* | -foo | | | X |
* | -foo= | X | X | X |
* | -nofoo | X | X | X |
* | -nofoo=1 | X | X | X |
*
* Once validated, settings can be retrieved by called GetSetting(),
* GetArg(), GetIntArg(), and GetBoolArg(). GetSetting() is the most general
* way to access settings, returning them as JSON values. The other
* functions just wrap GetSetting() for convenience.
*
* As can be seen in the chart, the default behavior of the flags is not
* very restrictive, although it can be restricted further. It tries to
* accommodate parsing command lines and configuration files written by
* human beings, not just machines, understanding that users may have
* different configuration styles and debugging needs. So the flags do not
* mandate one way to configure things or try to prevent every possible
* error, but instead catch the most common and blatant errors, and allow
* application code to impose additional restrictions, since application
* code needs to parse settings and reject invalid values anyway.
*
* Specifically, by default:
*
* - All settings can be specified multiple times, not just ALLOW_LIST
* settings. This allows users to override the config file from the
* command line, and override earlier command line settings with later
* ones. Application code can disable this behavior by calling the
* GetArgs() function and raising an error if more than one value is
* returned.
*
* - All settings can be negated. This provides a consistent command line
* interface where settings support -nofoo syntax when meaningful.
* GetSetting() returns a false JSON value for negated settings, and
* GetArg(), GetIntArg(), and GetBoolArg() return "", 0, and false
* respectively. Application code can disallow negation by specifying the
* DISALLOW_NEGATION flag, or just handling "", 0, and false values and
* rejecting them if they do not make sense.
*
* - All settings can be empty. Since all settings are optional, it is
* useful to have a way to set them, and a way to unset them. It is also
* unambiguous in most cases to treat empty -foo= syntax as not setting a
* value, so by default this syntax is allowed and causes GetSetting() to
* return JSON "", and GetArg(), GetIntArg() and GetBoolArg() to return
* std::nullopt. Application code can override this behavior by rejecting
* these values.
*/
enum Flags : uint32_t {
ALLOW_ANY = 0x01, //!< disable validation
// ALLOW_BOOL = 0x02, //!< unimplemented, draft implementation in #16545
// ALLOW_INT = 0x04, //!< unimplemented, draft implementation in #16545
// ALLOW_STRING = 0x08, //!< unimplemented, draft implementation in #16545
// ALLOW_LIST = 0x10, //!< unimplemented, draft implementation in #16545
ALLOW_ANY = 0x01, //!< allow any argument value (no type checking)
ALLOW_BOOL = 0x02, //!< allow -foo=1, -foo=0, -foo, -nofoo, -nofoo=1, and -foo=
ALLOW_INT = 0x04, //!< allow -foo=123, -nofoo, -nofoo=1, and -foo=
ALLOW_STRING = 0x08, //!< allow -foo=abc, -nofoo, -nofoo=1, and -foo=
ALLOW_LIST = 0x10, //!< allow multiple -foo=bar -foo=baz values
DISALLOW_NEGATION = 0x20, //!< disallow -nofoo syntax
DISALLOW_ELISION = 0x40, //!< disallow -foo syntax that doesn't assign any value
@ -154,12 +221,64 @@ protected:
bool UseDefaultSection(const std::string& arg) const EXCLUSIVE_LOCKS_REQUIRED(cs_args);
public:
ArgsManager();
~ArgsManager();
/**
* @name GetArg Functions
*
* GetArg functions are an easy way to access most settings. They are
* wrappers around the lower-level GetSetting() function that provide
* greater convenience.
*
* Examples:
*
* GetArg("-foo") // returns "abc" if -foo=abc was specified, or nullopt if unset
* GetIntArg("-foo") // returns 123 if -foo=123 was specified, or nullopt if unset
* GetBoolArg("-foo") // returns true if -foo was specified, or nullopt if unset
* GetBoolArg("-foo") // returns false if -nofoo was specified, or nullopt if unset
*
* If no type flags (ALLOW_STRING, ALLOW_INT, or ALLOW_BOOL) are set, GetArg
* functions do many type coercions and can have surprising behaviors which
* legacy code relies on, like parsing -nofoo as string "0" or -foo=true as
* boolean false.
*
* If any type flags are set, then:
*
* - Only GetArg functions with types matching the flags can be called. For
* example, it is an error to call GetIntArg() if ALLOW_INT is not set.
*
* - GetArg functions act like std::get_if<T>(), returning null if the
* requested type is not available or the setting is unspecified or empty.
*
* - "Widening" type conversions from smaller to bigger types are done if
* unambiguous (bool -> int -> string). For example, if settings.json
* contains {"foo":123}, GetArg("-foo") will return "123". If it contains
* {"foo":true}, GetIntArg("-foo") will return 1.
*
* - "Narrowing" type conversions in the other direction are not done even
* when they would be unambiguous. For example, if settings.json contains
* {"foo":"abc"} or {"foo":"123"} GetIntArg("-foo") will return nullopt in
* both cases.
*
* More examples of GetArg function usage can be found in the
* @ref example_options::ReadOptions() function in
* @ref argsman_tests.cpp
*@{*/
std::optional<std::string> GetArg(const std::string& strArg) const;
std::optional<int64_t> GetIntArg(const std::string& strArg) const;
std::optional<bool> GetBoolArg(const std::string& strArg) const;
/**@}*/
/**
* Get setting value.
*
* Result will be null if setting was unset, true if "-setting" argument was passed
* false if "-nosetting" argument was passed, and a string if a "-setting=value"
* argument was passed.
* Result will be null if setting was unspecified, true if `-setting`
* argument was passed, false if `-nosetting` argument was passed, and will
* be a string, integer, or boolean if a `-setting=value` argument was
* passed (which of the three depends on ALLOW_STRING, ALLOW_INT, and
* ALLOW_BOOL flags). See \ref InterpretValue for a full description of how
* command line and configuration strings map to JSON values.
*/
common::SettingsValue GetSetting(const std::string& arg) const;
@ -168,9 +287,6 @@ protected:
*/
std::vector<common::SettingsValue> GetSettingsList(const std::string& arg) const;
ArgsManager();
~ArgsManager();
/**
* Select the network in use
*/
@ -271,7 +387,6 @@ protected:
* @return command-line argument or default value
*/
std::string GetArg(const std::string& strArg, const std::string& strDefault) const;
std::optional<std::string> GetArg(const std::string& strArg) const;
/**
* Return path argument or default value
@ -293,7 +408,6 @@ protected:
* @return command-line argument (0 if invalid number) or default value
*/
int64_t GetIntArg(const std::string& strArg, int64_t nDefault) const;
std::optional<int64_t> GetIntArg(const std::string& strArg) const;
/**
* Return boolean argument or default value
@ -303,7 +417,6 @@ protected:
* @return command-line argument or default value
*/
bool GetBoolArg(const std::string& strArg, bool fDefault) const;
std::optional<bool> GetBoolArg(const std::string& strArg) const;
/**
* Set an argument if it doesn't already have a value
@ -423,6 +536,8 @@ protected:
void LogArgs() const;
private:
bool CheckArgFlags(const std::string& name, uint32_t require, uint32_t forbid, const char* context) const;
/**
* Get data directory path
*
@ -446,6 +561,13 @@ private:
const std::map<std::string, std::vector<common::SettingsValue>>& args) const;
};
//! Whether the type of the argument has been specified and extra validation
//! rules should apply.
inline bool IsTypedArg(uint32_t flags)
{
return flags & (ArgsManager::ALLOW_BOOL | ArgsManager::ALLOW_INT | ArgsManager::ALLOW_STRING);
}
extern ArgsManager gArgs;
/**

View file

@ -102,6 +102,10 @@ bool ArgsManager::ReadConfigStream(std::istream& stream, const std::string& file
std::optional<unsigned int> flags = GetArgFlags('-' + key.name);
if (!IsConfSupported(key, error)) return false;
if (flags) {
if (IsTypedArg(*flags) && !(*flags & ALLOW_LIST) && m_settings.ro_config[key.section].count(key.name)) {
error = strprintf("Multiple values specified for -%s in same section of config file.", key.name);
return false;
}
std::optional<common::SettingsValue> value = InterpretValue(key, &option.second, *flags, error);
if (!value) {
return false;

View file

@ -24,6 +24,313 @@ using util::ToString;
BOOST_FIXTURE_TEST_SUITE(argsman_tests, BasicTestingSetup)
//! Example code showing how to declare and parse options using ArgsManager flags.
namespace example_options {
struct Address {
std::string host;
uint16_t port;
};
struct RescanOptions {
std::optional<int> start_height;
};
struct Options {
//! Whether to use UPnP to map the listening port.
//! Example of a boolean option defaulting to false.
bool enable_upnp{false};
//! Whether to listen for RPC commands.
//! Example of a boolean option defaulting to true.
bool enable_rpc_server{true};
//! Whether to look for peers with DNS lookup.
//! Example of a boolean option without a default value. (If unspecified,
//! default behavior depends on other options.)
std::optional<bool> enable_dns_seed;
//! Amount of time to ban peers
//! Example of a simple integer setting.
std::chrono::seconds bantime{86400};
//! Equivalent bytes per sigop.
//! Example of a where negation should be disallowed.
int bytes_per_sigop{20};
//! Hash of block to assume valid and skip script verification.
//! Example of a simple string option.
std::optional<uint256> assumevalid;
//! Path to log file
//! Example of a simple string option with a default value.
fs::path log_file{"debug.log"};
//! Chain name.
//! Example of a simple string option that canoot be negated
ChainType chain{ChainType::MAIN};
//! Paths of block files to load before starting.
//! Example of a simple string list setting.
std::vector<fs::path> load_block;
//! Addresses to listen on.
//! Example of a list setting where negating the setting is different than
//! not specifying it.
std::optional<std::vector<Address>> listen_addresses;
};
void RegisterArgs(ArgsManager& args)
{
args.AddArg("-upnp", "", ArgsManager::ALLOW_BOOL, {});
args.AddArg("-rpcserver", "", ArgsManager::ALLOW_BOOL, {});
args.AddArg("-dnsseed", "", ArgsManager::ALLOW_BOOL, {});
args.AddArg("-bantime", "", ArgsManager::ALLOW_INT, {});
args.AddArg("-bytespersigop", "", ArgsManager::ALLOW_INT | ArgsManager::DISALLOW_NEGATION, {});
args.AddArg("-assumevalid", "", ArgsManager::ALLOW_STRING, {});
args.AddArg("-logfile", "", ArgsManager::ALLOW_STRING, {});
args.AddArg("-chain", "", ArgsManager::ALLOW_STRING | ArgsManager::DISALLOW_NEGATION, {});
args.AddArg("-loadblock", "", ArgsManager::ALLOW_STRING | ArgsManager::ALLOW_LIST, {});
args.AddArg("-listen", "", ArgsManager::ALLOW_STRING | ArgsManager::ALLOW_LIST, {});
}
void ReadOptions(const ArgsManager& args, Options& options)
{
if (auto value = args.GetBoolArg("-upnp")) options.enable_upnp = *value;
if (auto value = args.GetBoolArg("-rpcserver")) options.enable_rpc_server = *value;
if (auto value = args.GetBoolArg("-dnsseed")) options.enable_dns_seed = *value;
if (auto value = args.GetIntArg("-bantime")) {
if (*value < 0) throw std::runtime_error(strprintf("-bantime value %i is negative", *value));
options.bantime = std::chrono::seconds{*value};
}
if (auto value = args.GetIntArg("-bytespersigop")) {
if (*value < 1) throw std::runtime_error(strprintf("-bytespersigop value %i is less than 1", *value));
options.bytes_per_sigop = *value;
}
if (auto value = args.GetArg("-assumevalid"); value && !value->empty()) {
if (auto hash{uint256::FromHex(*value)}) {
options.assumevalid = *hash;
} else {
throw std::runtime_error(strprintf("-assumevalid value '%s' is not a valid hash", *value));
}
}
if (auto value = args.GetArg("-logfile")) {
options.log_file = fs::PathFromString(*value);
}
if (auto value = args.GetArg("-chain")) {
if (auto chain_type{ChainTypeFromString(*value)}) {
options.chain = *chain_type;
} else {
throw std::runtime_error(strprintf("Invalid chain type '%s'", *value));
}
}
for (const std::string& value : args.GetArgs("-loadblock")) {
if (value.empty()) throw std::runtime_error(strprintf("-loadblock value '%s' is not a valid file path", value));
options.load_block.push_back(fs::PathFromString(value));
}
if (args.IsArgNegated("-listen")) {
// If -nolisten was passed, disable listening by assigning an empty list
// of listening addresses.
options.listen_addresses.emplace();
} else if (auto addresses{args.GetArgs("-listen")}; !addresses.empty()) {
// If -listen=<addresses> options were passed, explicitly add these as
// listening addresses, otherwise leave listening option unset to enable
// default listening behavior.
options.listen_addresses.emplace();
for (const std::string& value : addresses) {
Address addr{"", 8333};
if (!SplitHostPort(value, addr.port, addr.host) || addr.host.empty()) {
throw std::runtime_error(strprintf("-listen address '%s' is not a valid host[:port]", value));
}
options.listen_addresses->emplace_back(std::move(addr));
}
}
}
//! Return Options::load_block as a human readable string for easier testing.
std::string LoadBlockStr(const Options& options)
{
std::string ret;
for (const auto& block : options.load_block) {
if (!ret.empty()) ret += " ";
ret += fs::PathToString(block);
}
return ret;
}
//! Return Options::listen_addresses as a human readable string for easier
//! testing.
std::string ListenStr(const Options& options)
{
if (!options.listen_addresses) {
// Default listening behavior in this example is just to listen on port
// 8333. In reality, it could be arbitrarily complicated and depend on
// other settings.
return "0.0.0.0:8333";
} else {
std::string ret;
for (const auto& addr : *options.listen_addresses) {
if (!ret.empty()) ret += " ";
ret += strprintf("%s:%d", addr.host, addr.port);
}
return ret;
}
}
struct TestSetup : public BasicTestingSetup
{
Options ParseOptions(const std::vector<std::string>& opts)
{
ArgsManager args;
RegisterArgs(args);
std::vector<const char*> argv{"ignored"};
for (const auto& opt : opts) {
argv.push_back(opt.c_str());
}
std::string error;
if (!args.ParseParameters(argv.size(), argv.data(), error)) {
throw std::runtime_error(error);
}
BOOST_CHECK_EQUAL(error, "");
Options options;
ReadOptions(args, options);
return options;
}
};
} // namespace example_options
BOOST_FIXTURE_TEST_CASE(ExampleOptions, example_options::TestSetup)
{
// Check default upnp value is false
BOOST_CHECK_EQUAL(ParseOptions({}).enable_upnp, false);
// Check passing -upnp makes it true.
BOOST_CHECK_EQUAL(ParseOptions({"-upnp"}).enable_upnp, true);
// Check passing -upnp=1 makes it true.
BOOST_CHECK_EQUAL(ParseOptions({"-upnp=1"}).enable_upnp, true);
// Check adding -upnp= sets it back to default.
BOOST_CHECK_EQUAL(ParseOptions({"-upnp=1", "-upnp="}).enable_upnp, false);
// Check passing invalid value.
BOOST_CHECK_EXCEPTION(ParseOptions({"-upnp=yes"}), std::exception, HasReason{"Cannot set -upnp value to 'yes'. It must be set to 0 or 1."});
// Check default rpcserver value is true.
BOOST_CHECK_EQUAL(ParseOptions({}).enable_rpc_server, true);
// Check passing -norpcserver makes it false.
BOOST_CHECK_EQUAL(ParseOptions({"-norpcserver"}).enable_rpc_server, false);
// Check passing -rpcserver=0 makes it false.
BOOST_CHECK_EQUAL(ParseOptions({"-rpcserver=0"}).enable_rpc_server, false);
// Check adding -rpcserver= sets it back to default.
BOOST_CHECK_EQUAL(ParseOptions({"-rpcserver=0", "-rpcserver="}).enable_rpc_server, true);
// Check passing invalid value.
BOOST_CHECK_EXCEPTION(ParseOptions({"-rpcserver=yes"}), std::exception, HasReason{"Cannot set -rpcserver value to 'yes'. It must be set to 0 or 1."});
// Check default dnsseed value is unset.
BOOST_CHECK_EQUAL(ParseOptions({}).enable_dns_seed, std::nullopt);
// Check passing -dnsseed makes it true.
BOOST_CHECK_EQUAL(ParseOptions({"-dnsseed"}).enable_dns_seed, true);
// Check passing -dnsseed=1 makes it true.
BOOST_CHECK_EQUAL(ParseOptions({"-dnsseed=1"}).enable_dns_seed, true);
// Check passing -nodnsseed makes it false.
BOOST_CHECK_EQUAL(ParseOptions({"-nodnsseed"}).enable_dns_seed, false);
// Check passing -dnsseed=0 makes it false.
BOOST_CHECK_EQUAL(ParseOptions({"-dnsseed=0"}).enable_dns_seed, false);
// Check adding -dnsseed= sets it back to default.
BOOST_CHECK_EQUAL(ParseOptions({"-dnsseed=1", "-dnsseed="}).enable_dns_seed, std::nullopt);
// Check passing invalid value.
BOOST_CHECK_EXCEPTION(ParseOptions({"-dnsseed=yes"}), std::exception, HasReason{"Cannot set -dnsseed value to 'yes'. It must be set to 0 or 1."});
// Check default bantime value is unset.
BOOST_CHECK_EQUAL(ParseOptions({}).bantime.count(), 86400);
// Check passing -bantime=3600 overrides it.
BOOST_CHECK_EQUAL(ParseOptions({"-bantime=3600"}).bantime.count(), 3600);
// Check passing -nobantime makes it 0.
BOOST_CHECK_EQUAL(ParseOptions({"-nobantime"}).bantime.count(), 0);
// Check passing -bantime=0 makes it 0.
BOOST_CHECK_EQUAL(ParseOptions({"-bantime=0"}).bantime.count(), 0);
// Check adding -bantime= sets it back to default.
BOOST_CHECK_EQUAL(ParseOptions({"-bantime=3600", "-bantime="}).bantime.count(), 86400);
// Check passing invalid values.
BOOST_CHECK_EXCEPTION(ParseOptions({"-bantime"}), std::exception, HasReason{"Cannot set -bantime with no value. Please specify value with -bantime=value. It must be set to an integer."});
BOOST_CHECK_EXCEPTION(ParseOptions({"-bantime=abc"}), std::exception, HasReason{"Cannot set -bantime value to 'abc'. It must be set to an integer."});
BOOST_CHECK_EXCEPTION(ParseOptions({"-bantime=-1000"}), std::exception, HasReason{"-bantime value -1000 is negative"});
// Check default bytespersigop value.
BOOST_CHECK_EQUAL(ParseOptions({}).bytes_per_sigop, 20);
// Check passing -bytespersigop=30 overrides it.
BOOST_CHECK_EQUAL(ParseOptions({"-bytespersigop=30"}).bytes_per_sigop, 30);
// Check adding -bytespersigop= sets it back to default.
BOOST_CHECK_EQUAL(ParseOptions({"-bytespersigop=30", "-bytespersigop="}).bytes_per_sigop, 20);
// Check passing invalid values.
BOOST_CHECK_EXCEPTION(ParseOptions({"-bytespersigop"}), std::exception, HasReason{"Cannot set -bytespersigop with no value. Please specify value with -bytespersigop=value. It must be set to an integer."});
BOOST_CHECK_EXCEPTION(ParseOptions({"-nobytespersigop"}), std::exception, HasReason{"Negating of -bytespersigop is meaningless and therefore forbidden"});
BOOST_CHECK_EXCEPTION(ParseOptions({"-bytespersigop=0"}), std::exception, HasReason{"-bytespersigop value 0 is less than 1"});
BOOST_CHECK_EXCEPTION(ParseOptions({"-bytespersigop=abc"}), std::exception, HasReason{"Cannot set -bytespersigop value to 'abc'. It must be set to an integer."});
// Check default assumevalid value is unset.
BOOST_CHECK_EQUAL(ParseOptions({}).assumevalid, std::nullopt);
// Check passing -assumevalid=<hash> makes it set that hash.
BOOST_CHECK_EQUAL(ParseOptions({"-assumevalid=0000111122223333444455556666777788889999aaaabbbbccccddddeeeeffff"}).assumevalid, uint256{"0000111122223333444455556666777788889999aaaabbbbccccddddeeeeffff"});
// Check passing -noassumevalid makes it not assumevalid.
BOOST_CHECK_EQUAL(ParseOptions({"-noassumevalid"}).assumevalid, std::nullopt);
// Check adding -assumevalid= sets it back to default.
BOOST_CHECK_EQUAL(ParseOptions({"-assumevalid=0000111122223333444455556666777788889999aaaabbbbccccddddeeeeffff", "-assumevalid="}).assumevalid, std::nullopt);
// Check passing invalid values.
BOOST_CHECK_EXCEPTION(ParseOptions({"-assumevalid"}), std::exception, HasReason{"Cannot set -assumevalid with no value. Please specify value with -assumevalid=value. It must be set to a string."});
BOOST_CHECK_EXCEPTION(ParseOptions({"-assumevalid=1"}), std::exception, HasReason{"-assumevalid value '1' is not a valid hash"});
// Check default logfile value.
BOOST_CHECK_EQUAL(ParseOptions({}).log_file, fs::path{"debug.log"});
// Check passing -logfile=custom.log overrides it.
BOOST_CHECK_EQUAL(ParseOptions({"-logfile=custom.log"}).log_file, fs::path{"custom.log"});
// Check passing -nologfile makes it disables logging.
BOOST_CHECK_EQUAL(ParseOptions({"-nologfile"}).log_file, fs::path{});
// Check adding -logfile= sets it back to default.
BOOST_CHECK_EQUAL(ParseOptions({"-logfile=custom.log", "-logfile="}).log_file, fs::path{"debug.log"});
BOOST_CHECK_EQUAL(ParseOptions({"-nologfile", "-logfile="}).log_file, fs::path{"debug.log"});
// Check passing invalid values.
BOOST_CHECK_EXCEPTION(ParseOptions({"-logfile"}), std::exception, HasReason{"Cannot set -logfile with no value. Please specify value with -logfile=value. It must be set to a string."});
// Check default chain value.
BOOST_CHECK_EQUAL(ParseOptions({}).chain, ChainType::MAIN);
// Check passing -chain=regtest overrides it.
BOOST_CHECK_EQUAL(ParseOptions({"-chain=regtest"}).chain, ChainType::REGTEST);
// Check adding -chain= sets it back to default.
BOOST_CHECK_EQUAL(ParseOptions({"-chain=regtest", "-chain="}).chain, ChainType::MAIN);
// Check passing invalid values.
BOOST_CHECK_EXCEPTION(ParseOptions({"-chain"}), std::exception, HasReason{"Cannot set -chain with no value. Please specify value with -chain=value. It must be set to a string."});
BOOST_CHECK_EXCEPTION(ParseOptions({"-chain=abc"}), std::exception, HasReason{"Invalid chain type 'abc'"});
BOOST_CHECK_EXCEPTION(ParseOptions({"-nochain"}), std::exception, HasReason{"Negating of -chain is meaningless and therefore forbidden"});
// Check default loadblock value is empty.
BOOST_CHECK_EQUAL(LoadBlockStr(ParseOptions({})), "");
// Check passing -loadblock can set multiple values.
BOOST_CHECK_EQUAL(LoadBlockStr(ParseOptions({"-loadblock=a", "-loadblock=b"})), "a b");
// Check passing -noloadblock clears previous values.
BOOST_CHECK_EQUAL(LoadBlockStr(ParseOptions({"-loadblock=a", "-noloadblock", "-loadblock=b", "-loadblock=c"})), "b c");
// Check passing invalid values.
BOOST_CHECK_EXCEPTION(ParseOptions({"-loadblock"}), std::exception, HasReason{"Cannot set -loadblock with no value. Please specify value with -loadblock=value. It must be set to a string."});
BOOST_CHECK_EXCEPTION(ParseOptions({"-loadblock="}), std::exception, HasReason{"-loadblock value '' is not a valid file path"});
// Check default listen value.
BOOST_CHECK_EQUAL(ListenStr(ParseOptions({})), "0.0.0.0:8333");
// Check passing -listen can set multiple values.
BOOST_CHECK_EQUAL(ListenStr(ParseOptions({"-listen=a", "-listen=b"})), "a:8333 b:8333");
// Check passing -nolisten clears previous values.
BOOST_CHECK_EQUAL(ListenStr(ParseOptions({"-listen=a", "-nolisten", "-listen=b", "-listen=c"})), "b:8333 c:8333");
// Check final -nolisten disables listening.
BOOST_CHECK_EQUAL(ListenStr(ParseOptions({"-listen=a", "-nolisten"})), "");
// Check passing invalid values.
BOOST_CHECK_EXCEPTION(ParseOptions({"-listen"}), std::exception, HasReason{"Cannot set -listen with no value. Please specify value with -listen=value. It must be set to a string."});
BOOST_CHECK_EXCEPTION(ParseOptions({"-listen="}), std::exception, HasReason{"-listen address '' is not a valid host[:port]"});
}
BOOST_AUTO_TEST_CASE(util_datadir)
{
// Use local args variable instead of m_args to avoid making assumptions about test setup
@ -127,7 +434,7 @@ public:
if (expect.error) {
BOOST_CHECK(!success);
BOOST_CHECK_NE(error.find(expect.error), std::string::npos);
BOOST_CHECK_EQUAL(error, expect.error);
} else {
BOOST_CHECK(success);
BOOST_CHECK_EQUAL(error, "");
@ -137,16 +444,26 @@ public:
BOOST_CHECK_EQUAL(test.GetArg("-value", "zzzzz"), "zzzzz");
} else if (expect.string_value) {
BOOST_CHECK_EQUAL(test.GetArg("-value", "zzzzz"), expect.string_value);
} else {
BOOST_CHECK(!success);
} else if (success) {
// Extra check to ensure complete test coverage. Assert that if
// caller did not call Expect::DefaultString() or Expect::String(),
// then this test case must be one where ParseParameters() fails, or
// one where GetArg() throws logic_error because ALLOW_STRING is not
// specified.
BOOST_CHECK_THROW(test.GetArg("-value", "zzzzz"), std::logic_error);
}
if (expect.default_int) {
BOOST_CHECK_EQUAL(test.GetIntArg("-value", 99999), 99999);
} else if (expect.int_value) {
BOOST_CHECK_EQUAL(test.GetIntArg("-value", 99999), *expect.int_value);
} else {
BOOST_CHECK(!success);
} else if (success) {
// Extra check to ensure complete test coverage. Assert that if
// caller did not call Expect::DefaultInt() or Expect::Int(), then
// this test case must be one where ParseParameters() fails, or one
// where GetArg() throws logic_error because ALLOW_INT is not
// specified.
BOOST_CHECK_THROW(test.GetIntArg("-value", 99999), std::logic_error);
}
if (expect.default_bool) {
@ -155,15 +472,21 @@ public:
} else if (expect.bool_value) {
BOOST_CHECK_EQUAL(test.GetBoolArg("-value", false), *expect.bool_value);
BOOST_CHECK_EQUAL(test.GetBoolArg("-value", true), *expect.bool_value);
} else {
BOOST_CHECK(!success);
} else if (success) {
// Extra check to ensure complete test coverage. Assert that if
// caller did not call Expect::DefaultBool() or Expect::Bool(), then
// this test case must be one where ParseParameters() fails, or one
// where GetArg() throws logic_error because ALLOW_BOOL is not
// specified.
BOOST_CHECK_THROW(test.GetBoolArg("-value", false), std::logic_error);
BOOST_CHECK_THROW(test.GetBoolArg("-value", true), std::logic_error);
}
if (expect.list_value) {
auto l = test.GetArgs("-value");
BOOST_CHECK_EQUAL_COLLECTIONS(l.begin(), l.end(), expect.list_value->begin(), expect.list_value->end());
} else {
BOOST_CHECK(!success);
} else if (success) {
BOOST_CHECK_THROW(test.GetArgs("-value"), std::logic_error);
}
}
};
@ -185,6 +508,98 @@ BOOST_FIXTURE_TEST_CASE(util_CheckValue, CheckValueTest)
CheckValue(M::ALLOW_ANY, "-value=1", Expect{"1"}.String("1").Int(1).Bool(true).List({"1"}));
CheckValue(M::ALLOW_ANY, "-value=2", Expect{"2"}.String("2").Int(2).Bool(true).List({"2"}));
CheckValue(M::ALLOW_ANY, "-value=abc", Expect{"abc"}.String("abc").Int(0).Bool(false).List({"abc"}));
CheckValue(M::ALLOW_BOOL, nullptr, Expect{{}}.DefaultBool());
CheckValue(M::ALLOW_BOOL, "-novalue", Expect{false}.Bool(false));
CheckValue(M::ALLOW_BOOL, "-novalue=", Expect{{}}.Error("Cannot negate -value at the same time as setting a value ('')."));
CheckValue(M::ALLOW_BOOL, "-novalue=0", Expect{{}}.Error("Cannot negate -value at the same time as setting a value ('0')."));
CheckValue(M::ALLOW_BOOL, "-novalue=1", Expect{false}.Bool(false));
CheckValue(M::ALLOW_BOOL, "-novalue=2", Expect{{}}.Error("Cannot negate -value at the same time as setting a value ('2')."));
CheckValue(M::ALLOW_BOOL, "-novalue=abc", Expect{{}}.Error("Cannot negate -value at the same time as setting a value ('abc')."));
CheckValue(M::ALLOW_BOOL, "-value", Expect{true}.Bool(true));
CheckValue(M::ALLOW_BOOL, "-value=", Expect{""}.DefaultBool());
CheckValue(M::ALLOW_BOOL, "-value=0", Expect{false}.Bool(false));
CheckValue(M::ALLOW_BOOL, "-value=1", Expect{true}.Bool(true));
CheckValue(M::ALLOW_BOOL, "-value=2", Expect{{}}.Error("Cannot set -value value to '2'. It must be set to 0 or 1."));
CheckValue(M::ALLOW_BOOL, "-value=abc", Expect{{}}.Error("Cannot set -value value to 'abc'. It must be set to 0 or 1."));
CheckValue(M::ALLOW_INT, nullptr, Expect{{}}.DefaultInt());
CheckValue(M::ALLOW_INT, "-novalue", Expect{false}.Int(0));
CheckValue(M::ALLOW_INT, "-novalue=", Expect{{}}.Error("Cannot negate -value at the same time as setting a value ('')."));
CheckValue(M::ALLOW_INT, "-novalue=0", Expect{{}}.Error("Cannot negate -value at the same time as setting a value ('0')."));
CheckValue(M::ALLOW_INT, "-novalue=1", Expect{false}.Int(0));
CheckValue(M::ALLOW_INT, "-novalue=2", Expect{{}}.Error("Cannot negate -value at the same time as setting a value ('2')."));
CheckValue(M::ALLOW_INT, "-novalue=abc", Expect{{}}.Error("Cannot negate -value at the same time as setting a value ('abc')."));
CheckValue(M::ALLOW_INT, "-value", Expect{{}}.Error("Cannot set -value with no value. Please specify value with -value=value. It must be set to an integer."));
CheckValue(M::ALLOW_INT, "-value=", Expect{""}.DefaultInt());
CheckValue(M::ALLOW_INT, "-value=0", Expect{0}.Int(0));
CheckValue(M::ALLOW_INT, "-value=1", Expect{1}.Int(1));
CheckValue(M::ALLOW_INT, "-value=2", Expect{2}.Int(2));
CheckValue(M::ALLOW_INT, "-value=abc", Expect{{}}.Error("Cannot set -value value to 'abc'. It must be set to an integer."));
CheckValue(M::ALLOW_STRING, nullptr, Expect{{}}.DefaultString());
CheckValue(M::ALLOW_STRING, "-novalue", Expect{false}.String(""));
CheckValue(M::ALLOW_STRING, "-novalue=", Expect{{}}.Error("Cannot negate -value at the same time as setting a value ('')."));
CheckValue(M::ALLOW_STRING, "-novalue=0", Expect{{}}.Error("Cannot negate -value at the same time as setting a value ('0')."));
CheckValue(M::ALLOW_STRING, "-novalue=1", Expect{false}.String(""));
CheckValue(M::ALLOW_STRING, "-novalue=2", Expect{{}}.Error("Cannot negate -value at the same time as setting a value ('2')."));
CheckValue(M::ALLOW_STRING, "-novalue=abc", Expect{{}}.Error("Cannot negate -value at the same time as setting a value ('abc')."));
CheckValue(M::ALLOW_STRING, "-value", Expect{{}}.Error("Cannot set -value with no value. Please specify value with -value=value. It must be set to a string."));
CheckValue(M::ALLOW_STRING, "-value=", Expect{""}.DefaultString());
CheckValue(M::ALLOW_STRING, "-value=0", Expect{"0"}.String("0"));
CheckValue(M::ALLOW_STRING, "-value=1", Expect{"1"}.String("1"));
CheckValue(M::ALLOW_STRING, "-value=2", Expect{"2"}.String("2"));
CheckValue(M::ALLOW_STRING, "-value=abc", Expect{"abc"}.String("abc"));
CheckValue(M::ALLOW_STRING | M::ALLOW_LIST, nullptr, Expect{{}}.List({}));
CheckValue(M::ALLOW_STRING | M::ALLOW_LIST, "-novalue", Expect{false}.List({}));
CheckValue(M::ALLOW_STRING | M::ALLOW_LIST, "-novalue=", Expect{{}}.Error("Cannot negate -value at the same time as setting a value ('')."));
CheckValue(M::ALLOW_STRING | M::ALLOW_LIST, "-novalue=0", Expect{{}}.Error("Cannot negate -value at the same time as setting a value ('0')."));
CheckValue(M::ALLOW_STRING | M::ALLOW_LIST, "-novalue=1", Expect{false}.List({}));
CheckValue(M::ALLOW_STRING | M::ALLOW_LIST, "-novalue=2", Expect{{}}.Error("Cannot negate -value at the same time as setting a value ('2')."));
CheckValue(M::ALLOW_STRING | M::ALLOW_LIST, "-novalue=abc", Expect{{}}.Error("Cannot negate -value at the same time as setting a value ('abc')."));
CheckValue(M::ALLOW_STRING | M::ALLOW_LIST, "-value", Expect{{}}.Error("Cannot set -value with no value. Please specify value with -value=value. It must be set to a string."));
CheckValue(M::ALLOW_STRING | M::ALLOW_LIST, "-value=", Expect{""}.List({""}));
CheckValue(M::ALLOW_STRING | M::ALLOW_LIST, "-value=0", Expect{"0"}.List({"0"}));
CheckValue(M::ALLOW_STRING | M::ALLOW_LIST, "-value=1", Expect{"1"}.List({"1"}));
CheckValue(M::ALLOW_STRING | M::ALLOW_LIST, "-value=2", Expect{"2"}.List({"2"}));
CheckValue(M::ALLOW_STRING | M::ALLOW_LIST, "-value=abc", Expect{"abc"}.List({"abc"}));
}
BOOST_FIXTURE_TEST_CASE(util_CheckBoolStringsNotSpecial, CheckValueTest)
{
// Check that "true" and "false" strings are rejected for ALLOW_BOOL
// arguments. We might want to change this behavior in the future and
// interpret strings like "true" as true, and strings like "false" as false.
// But because it would be confusing to interpret "true" as true for
// ALLOW_BOOL arguments but false for ALLOW_ANY arguments (because
// atoi("true")==0), for now it is safer to just disallow strings like
// "true" and "false" for ALLOW_BOOL arguments as long as there are still
// other boolean arguments interpreted with ALLOW_ANY.
using M = ArgsManager;
CheckValue(M::ALLOW_BOOL, "-value=true", Expect{{}}.Error("Cannot set -value value to 'true'. It must be set to 0 or 1."));
CheckValue(M::ALLOW_BOOL, "-value=false", Expect{{}}.Error("Cannot set -value value to 'false'. It must be set to 0 or 1."));
}
BOOST_AUTO_TEST_CASE(util_CheckSingleValue)
{
TestArgsManager test;
test.SetupArgs({{"-single", ArgsManager::ALLOW_INT}});
std::istringstream stream("single=1\nsingle=2\n");
std::string error;
BOOST_CHECK(!test.ReadConfigStream(stream, "file.conf", error));
BOOST_CHECK_EQUAL(error, "Multiple values specified for -single in same section of config file.");
}
BOOST_AUTO_TEST_CASE(util_CheckBadFlagCombinations)
{
TestArgsManager test;
using M = ArgsManager;
BOOST_CHECK_THROW(test.AddArg("-arg1", "name", M::ALLOW_BOOL | M::ALLOW_ANY, OptionsCategory::OPTIONS), std::logic_error);
BOOST_CHECK_THROW(test.AddArg("-arg2", "name", M::ALLOW_BOOL | M::DISALLOW_ELISION, OptionsCategory::OPTIONS), std::logic_error);
BOOST_CHECK_THROW(test.AddArg("-arg3", "name", M::ALLOW_INT | M::ALLOW_STRING, OptionsCategory::OPTIONS), std::logic_error);
BOOST_CHECK_THROW(test.AddArg("-arg4", "name", M::ALLOW_INT | M::ALLOW_BOOL, OptionsCategory::OPTIONS), std::logic_error);
BOOST_CHECK_THROW(test.AddArg("-arg5", "name", M::ALLOW_STRING | M::ALLOW_BOOL, OptionsCategory::OPTIONS), std::logic_error);
}
struct NoIncludeConfTest {

View file

@ -46,7 +46,12 @@ FUZZ_TARGET(system, .init = initialize_system)
[&] {
auto str_arg = fuzzed_data_provider.ConsumeRandomLengthString(16);
auto str_value = fuzzed_data_provider.ConsumeRandomLengthString(16);
args_manager.SoftSetArg(str_arg, str_value);
// Avoid Can't call SoftSetArg on arg registered with flags 0x8d8d8d00 (requires 0x2, disallows 0x10)
try {
args_manager.SoftSetArg(str_arg, str_value);
} catch (const std::logic_error& e) {
if (std::string_view(e.what()).find("Can't call ForceSetArg on arg") == std::string_view::npos) throw;
}
},
[&] {
auto str_arg = fuzzed_data_provider.ConsumeRandomLengthString(16);
@ -56,7 +61,12 @@ FUZZ_TARGET(system, .init = initialize_system)
[&] {
auto str_arg = fuzzed_data_provider.ConsumeRandomLengthString(16);
auto f_value = fuzzed_data_provider.ConsumeBool();
args_manager.SoftSetBoolArg(str_arg, f_value);
// Avoid Can't call SoftSetBoolArg on arg registered with flags 0x8d8d8d00 (requires 0x2, disallows 0x10)
try {
args_manager.SoftSetBoolArg(str_arg, f_value);
} catch (const std::logic_error& e) {
if (std::string_view(e.what()).find("Can't call SoftSetBoolArg on arg") == std::string_view::npos) throw;
}
},
[&] {
const OptionsCategory options_category = fuzzed_data_provider.PickValueInArray<OptionsCategory>({OptionsCategory::OPTIONS, OptionsCategory::CONNECTION, OptionsCategory::WALLET, OptionsCategory::WALLET_DEBUG_TEST, OptionsCategory::ZMQ, OptionsCategory::DEBUG_TEST, OptionsCategory::CHAINPARAMS, OptionsCategory::NODE_RELAY, OptionsCategory::BLOCK_CREATION, OptionsCategory::RPC, OptionsCategory::GUI, OptionsCategory::COMMANDS, OptionsCategory::REGISTER_COMMANDS, OptionsCategory::CLI_COMMANDS, OptionsCategory::IPC, OptionsCategory::HIDDEN});
@ -68,6 +78,11 @@ FUZZ_TARGET(system, .init = initialize_system)
}
auto help = fuzzed_data_provider.ConsumeRandomLengthString(16);
auto flags = fuzzed_data_provider.ConsumeIntegral<unsigned int>() & ~ArgsManager::COMMAND;
// Avoid hitting "ALLOW_INT flag is incompatible with ALLOW_STRING", etc exceptions
if (flags & ArgsManager::ALLOW_ANY) flags &= ~(ArgsManager::ALLOW_BOOL | ArgsManager::ALLOW_INT | ArgsManager::ALLOW_STRING);
if (flags & ArgsManager::ALLOW_BOOL) flags &= ~ArgsManager::DISALLOW_ELISION;
if (flags & ArgsManager::ALLOW_STRING) flags &= ~ArgsManager::ALLOW_INT;
if (flags & ArgsManager::ALLOW_BOOL) flags &= ~(ArgsManager::ALLOW_INT | ArgsManager::ALLOW_STRING);
args_manager.AddArg(argument_name, help, flags, options_category);
},
[&] {
@ -110,11 +125,27 @@ FUZZ_TARGET(system, .init = initialize_system)
const int64_t i64 = fuzzed_data_provider.ConsumeIntegral<int64_t>();
const bool b = fuzzed_data_provider.ConsumeBool();
(void)args_manager.GetIntArg(s1, i64);
(void)args_manager.GetArg(s1, s2);
try {
(void)args_manager.GetIntArg(s1, i64);
} catch (const std::logic_error& e) {
if (std::string_view(e.what()).find("Can't call GetIntArg on arg") == std::string_view::npos) throw;
}
try {
(void)args_manager.GetArg(s1, s2);
} catch (const std::logic_error& e) {
if (std::string_view(e.what()).find("Can't call GetArg on arg") == std::string_view::npos) throw;
}
(void)args_manager.GetArgFlags(s1);
(void)args_manager.GetArgs(s1);
(void)args_manager.GetBoolArg(s1, b);
try {
(void)args_manager.GetArgs(s1);
} catch (const std::logic_error& e) {
if (std::string_view(e.what()).find("Can't call GetArgs on arg") == std::string_view::npos) throw;
}
try {
(void)args_manager.GetBoolArg(s1, b);
} catch (const std::logic_error& e) {
if (std::string_view(e.what()).find("Can't call GetBoolArg on arg") == std::string_view::npos) throw;
}
try {
(void)args_manager.GetChainTypeString();
} catch (const std::runtime_error&) {

View file

@ -52,9 +52,11 @@ void SetupArgs(ArgsManager& local_args, const std::vector<std::pair<std::string,
// functions. The GetSetting method can always be used instead of GetArg
// methods to retrieve original values, and there's not always an objective
// answer to what GetArg behavior is best in every case. This test makes sure
// there's test coverage for whatever the current behavior is, so it's not
// broken or changed unintentionally.
BOOST_AUTO_TEST_CASE(setting_args)
// there's test coverage for the current behavior with ALLOW_ANY flag, so
// it's not broken or changed unintentionally. Additional test cases with
// flags other than ALLOW_ANY can be found in the setting_arg_allow_types
// test below.
BOOST_AUTO_TEST_CASE(setting_args_allow_any)
{
ArgsManager args;
SetupArgs(args, {{"-foo", ArgsManager::ALLOW_ANY}});
@ -157,6 +159,62 @@ BOOST_AUTO_TEST_CASE(setting_args)
BOOST_CHECK_EQUAL(args.GetBoolArg("foo", false), false);
}
// Test behavior of GetArg functions with a settings.json file when
// ALLOW_BOOL and ALLOW_INT flags are specified, in contrast to
// setting_args_allow_any test above, which tests legacy behavior with the
// ALLOW_ANY flag.
//
// Currently, the ReadSettingsFile() function ignores type flags and just copies
// JSON values in the file directly into the Settings::rw_settings map without
// converting the values to types specified by the flags, or returning errors if
// the values were invalid and couldn't be converted. In the future it would be
// nice to improve ReadSettingsFile() to use the flags so the parsing could be
// more robust and return errors if problems were detected. This test could be
// extended in that case.
BOOST_AUTO_TEST_CASE(setting_args_allow_types)
{
{
ArgsManager args;
args.LockSettings([&](common::Settings& settings) {
settings.rw_settings["boolarg1"] = true;
settings.rw_settings["boolarg2"] = false;
});
args.ForceSetArg("-datadir", fs::PathToString(m_path_root));
BOOST_CHECK(args.WriteSettingsFile());
}
{
ArgsManager args;
args.ForceSetArg("-datadir", fs::PathToString(m_path_root));
BOOST_CHECK(args.ReadSettingsFile());
BOOST_CHECK_EQUAL(args.GetSetting("-boolarg1").write(), "true");
BOOST_CHECK_EQUAL(args.GetSetting("-boolarg2").write(), "false");
}
{
ArgsManager args;
args.AddArg("-boolarg1", "", ArgsManager::ALLOW_BOOL, OptionsCategory::OPTIONS);
args.AddArg("-boolarg2", "", ArgsManager::ALLOW_BOOL, OptionsCategory::OPTIONS);
args.ForceSetArg("-datadir", fs::PathToString(m_path_root));
BOOST_CHECK(args.ReadSettingsFile());
BOOST_CHECK_EQUAL(args.GetBoolArg("-boolarg1").value(), true);
BOOST_CHECK_EQUAL(args.GetBoolArg("-boolarg2").value(), false);
}
{
ArgsManager args;
args.AddArg("-boolarg1", "", ArgsManager::ALLOW_INT, OptionsCategory::OPTIONS);
args.AddArg("-boolarg2", "", ArgsManager::ALLOW_INT, OptionsCategory::OPTIONS);
args.ForceSetArg("-datadir", fs::PathToString(m_path_root));
BOOST_CHECK(args.ReadSettingsFile());
BOOST_CHECK_EQUAL(args.GetIntArg("-boolarg1").value(), 1);
BOOST_CHECK_EQUAL(args.GetIntArg("-boolarg2").value(), 0);
}
}
BOOST_AUTO_TEST_CASE(boolarg)
{
ArgsManager local_args;

View file

@ -286,6 +286,10 @@ inline std::ostream& operator<<(std::ostream& os, const std::optional<T>& v)
return v ? os << *v
: os << "std::nullopt";
}
inline std::ostream& operator<<(std::ostream& os, const std::nullopt_t)
{
return os << "std::nullopt";
}
} // namespace std
std::ostream& operator<<(std::ostream& os, const arith_uint256& num);

View file

@ -216,7 +216,7 @@ class ConfArgsTest(BitcoinTestFramework):
def test_invalid_command_line_options(self):
self.nodes[0].assert_start_raises_init_error(
expected_msg='Error: Error parsing command line arguments: Can not set -proxy with no value. Please specify value with -proxy=value.',
expected_msg='Error: Error parsing command line arguments: Cannot set -proxy with no value. Please specify value with -proxy=value.',
extra_args=['-proxy'],
)
# Provide a value different from 1 to the -wallet negated option

View file

@ -14,11 +14,12 @@ from subprocess import check_output
import re
FOLDER_GREP = 'src'
FOLDER_TEST = 'src/test/'
EXCLUDE_PATHS = ['src/test/', 'src/common/args.h']
EXCLUDE_ARGS = ' '.join(f"':(exclude){path}'" for path in EXCLUDE_PATHS)
REGEX_ARG = r'\b(?:GetArg|GetArgs|GetBoolArg|GetIntArg|GetPathArg|IsArgSet|get_net)\("(-[^"]+)"'
REGEX_DOC = r'AddArg\("(-[^"=]+?)(?:=|")'
CMD_ROOT_DIR = '$(git rev-parse --show-toplevel)/{}'.format(FOLDER_GREP)
CMD_GREP_ARGS = r"git grep --perl-regexp '{}' -- {} ':(exclude){}'".format(REGEX_ARG, CMD_ROOT_DIR, FOLDER_TEST)
CMD_GREP_ARGS = f"git grep --perl-regexp '{REGEX_ARG}' -- {CMD_ROOT_DIR} {EXCLUDE_ARGS}"
CMD_GREP_WALLET_ARGS = r"git grep --function-context 'void WalletInit::AddWalletOptions' -- {} | grep AddArg".format(CMD_ROOT_DIR)
CMD_GREP_WALLET_HIDDEN_ARGS = r"git grep --function-context 'void DummyWalletInit::AddWalletOptions' -- {}".format(CMD_ROOT_DIR)
CMD_GREP_DOCS = r"git grep --perl-regexp '{}' {}".format(REGEX_DOC, CMD_ROOT_DIR)