Branch data Line data Source code
# 1 : : // Copyright (c) 2011-2021 The Bitcoin Core developers
# 2 : : // Distributed under the MIT software license, see the accompanying
# 3 : : // file COPYING or http://www.opensource.org/licenses/mit-license.php.
# 4 : :
# 5 : : #include <util/settings.h>
# 6 : :
# 7 : : #include <fs.h>
# 8 : : #include <test/util/setup_common.h>
# 9 : : #include <test/util/str.h>
# 10 : :
# 11 : :
# 12 : : #include <boost/test/unit_test.hpp>
# 13 : : #include <univalue.h>
# 14 : : #include <util/strencodings.h>
# 15 : : #include <util/string.h>
# 16 : : #include <util/system.h>
# 17 : :
# 18 : : #include <fstream>
# 19 : : #include <map>
# 20 : : #include <string>
# 21 : : #include <system_error>
# 22 : : #include <vector>
# 23 : :
# 24 : : inline bool operator==(const util::SettingsValue& a, const util::SettingsValue& b)
# 25 : 8 : {
# 26 : 8 : return a.write() == b.write();
# 27 : 8 : }
# 28 : :
# 29 : : inline std::ostream& operator<<(std::ostream& os, const util::SettingsValue& value)
# 30 : 0 : {
# 31 : 0 : os << value.write();
# 32 : 0 : return os;
# 33 : 0 : }
# 34 : :
# 35 : : inline std::ostream& operator<<(std::ostream& os, const std::pair<std::string, util::SettingsValue>& kv)
# 36 : 0 : {
# 37 : 0 : util::SettingsValue out(util::SettingsValue::VOBJ);
# 38 : 0 : out.__pushKV(kv.first, kv.second);
# 39 : 0 : os << out.write();
# 40 : 0 : return os;
# 41 : 0 : }
# 42 : :
# 43 : : inline void WriteText(const fs::path& path, const std::string& text)
# 44 : 8 : {
# 45 : 8 : std::ofstream file;
# 46 : 8 : file.open(path);
# 47 : 8 : file << text;
# 48 : 8 : }
# 49 : :
# 50 : : BOOST_FIXTURE_TEST_SUITE(settings_tests, BasicTestingSetup)
# 51 : :
# 52 : : BOOST_AUTO_TEST_CASE(ReadWrite)
# 53 : 2 : {
# 54 : 2 : fs::path path = m_args.GetDataDirBase() / "settings.json";
# 55 : :
# 56 : 2 : WriteText(path, R"({
# 57 : 2 : "string": "string",
# 58 : 2 : "num": 5,
# 59 : 2 : "bool": true,
# 60 : 2 : "null": null
# 61 : 2 : })");
# 62 : :
# 63 : 2 : std::map<std::string, util::SettingsValue> expected{
# 64 : 2 : {"string", "string"},
# 65 : 2 : {"num", 5},
# 66 : 2 : {"bool", true},
# 67 : 2 : {"null", {}},
# 68 : 2 : };
# 69 : :
# 70 : : // Check file read.
# 71 : 2 : std::map<std::string, util::SettingsValue> values;
# 72 : 2 : std::vector<std::string> errors;
# 73 : 2 : BOOST_CHECK(util::ReadSettings(path, values, errors));
# 74 : 2 : BOOST_CHECK_EQUAL_COLLECTIONS(values.begin(), values.end(), expected.begin(), expected.end());
# 75 : 2 : BOOST_CHECK(errors.empty());
# 76 : :
# 77 : : // Check no errors if file doesn't exist.
# 78 : 2 : fs::remove(path);
# 79 : 2 : BOOST_CHECK(util::ReadSettings(path, values, errors));
# 80 : 2 : BOOST_CHECK(values.empty());
# 81 : 2 : BOOST_CHECK(errors.empty());
# 82 : :
# 83 : : // Check duplicate keys not allowed
# 84 : 2 : WriteText(path, R"({
# 85 : 2 : "dupe": "string",
# 86 : 2 : "dupe": "dupe"
# 87 : 2 : })");
# 88 : 2 : BOOST_CHECK(!util::ReadSettings(path, values, errors));
# 89 : 2 : std::vector<std::string> dup_keys = {strprintf("Found duplicate key dupe in settings file %s", fs::PathToString(path))};
# 90 : 2 : BOOST_CHECK_EQUAL_COLLECTIONS(errors.begin(), errors.end(), dup_keys.begin(), dup_keys.end());
# 91 : :
# 92 : : // Check non-kv json files not allowed
# 93 : 2 : WriteText(path, R"("non-kv")");
# 94 : 2 : BOOST_CHECK(!util::ReadSettings(path, values, errors));
# 95 : 2 : std::vector<std::string> non_kv = {strprintf("Found non-object value \"non-kv\" in settings file %s", fs::PathToString(path))};
# 96 : 2 : BOOST_CHECK_EQUAL_COLLECTIONS(errors.begin(), errors.end(), non_kv.begin(), non_kv.end());
# 97 : :
# 98 : : // Check invalid json not allowed
# 99 : 2 : WriteText(path, R"(invalid json)");
# 100 : 2 : BOOST_CHECK(!util::ReadSettings(path, values, errors));
# 101 : 2 : std::vector<std::string> fail_parse = {strprintf("Unable to parse settings file %s", fs::PathToString(path))};
# 102 : 2 : BOOST_CHECK_EQUAL_COLLECTIONS(errors.begin(), errors.end(), fail_parse.begin(), fail_parse.end());
# 103 : 2 : }
# 104 : :
# 105 : : //! Check settings struct contents against expected json strings.
# 106 : : static void CheckValues(const util::Settings& settings, const std::string& single_val, const std::string& list_val)
# 107 : 4 : {
# 108 : 4 : util::SettingsValue single_value = GetSetting(settings, "section", "name", false, false, false);
# 109 : 4 : util::SettingsValue list_value(util::SettingsValue::VARR);
# 110 [ + + ]: 10 : for (const auto& item : GetSettingsList(settings, "section", "name", false)) {
# 111 : 10 : list_value.push_back(item);
# 112 : 10 : }
# 113 : 4 : BOOST_CHECK_EQUAL(single_value.write().c_str(), single_val);
# 114 : 4 : BOOST_CHECK_EQUAL(list_value.write().c_str(), list_val);
# 115 : 4 : };
# 116 : :
# 117 : : // Simple settings merge test case.
# 118 : : BOOST_AUTO_TEST_CASE(Simple)
# 119 : 2 : {
# 120 : 2 : util::Settings settings;
# 121 : 2 : settings.command_line_options["name"].push_back("val1");
# 122 : 2 : settings.command_line_options["name"].push_back("val2");
# 123 : 2 : settings.ro_config["section"]["name"].push_back(2);
# 124 : :
# 125 : : // The last given arg takes precedence when specified via commandline.
# 126 : 2 : CheckValues(settings, R"("val2")", R"(["val1","val2",2])");
# 127 : :
# 128 : 2 : util::Settings settings2;
# 129 : 2 : settings2.ro_config["section"]["name"].push_back("val2");
# 130 : 2 : settings2.ro_config["section"]["name"].push_back("val3");
# 131 : :
# 132 : : // The first given arg takes precedence when specified via config file.
# 133 : 2 : CheckValues(settings2, R"("val2")", R"(["val2","val3"])");
# 134 : 2 : }
# 135 : :
# 136 : : // Confirm that a high priority setting overrides a lower priority setting even
# 137 : : // if the high priority setting is null. This behavior is useful for a high
# 138 : : // priority setting source to be able to effectively reset any setting back to
# 139 : : // its default value.
# 140 : : BOOST_AUTO_TEST_CASE(NullOverride)
# 141 : 2 : {
# 142 : 2 : util::Settings settings;
# 143 : 2 : settings.command_line_options["name"].push_back("value");
# 144 : 2 : BOOST_CHECK_EQUAL(R"("value")", GetSetting(settings, "section", "name", false, false, false).write().c_str());
# 145 : 2 : settings.forced_settings["name"] = {};
# 146 : 2 : BOOST_CHECK_EQUAL(R"(null)", GetSetting(settings, "section", "name", false, false, false).write().c_str());
# 147 : 2 : }
# 148 : :
# 149 : : // Test different ways settings can be merged, and verify results. This test can
# 150 : : // be used to confirm that updates to settings code don't change behavior
# 151 : : // unintentionally.
# 152 : : struct MergeTestingSetup : public BasicTestingSetup {
# 153 : : //! Max number of actions to sequence together. Can decrease this when
# 154 : : //! debugging to make test results easier to understand.
# 155 : : static constexpr int MAX_ACTIONS = 3;
# 156 : :
# 157 : : enum Action { END, SET, NEGATE, SECTION_SET, SECTION_NEGATE };
# 158 : : using ActionList = Action[MAX_ACTIONS];
# 159 : :
# 160 : : //! Enumerate all possible test configurations.
# 161 : : template <typename Fn>
# 162 : : void ForEachMergeSetup(Fn&& fn)
# 163 : 2 : {
# 164 : 2 : ActionList arg_actions = {};
# 165 : : // command_line_options do not have sections. Only iterate over SET and NEGATE
# 166 : 14 : ForEachNoDup(arg_actions, SET, NEGATE, [&]{
# 167 : 14 : ActionList conf_actions = {};
# 168 : 742 : ForEachNoDup(conf_actions, SET, SECTION_NEGATE, [&]{
# 169 [ + + ]: 1484 : for (bool force_set : {false, true}) {
# 170 [ + + ]: 2968 : for (bool ignore_default_section_config : {false, true}) {
# 171 : 2968 : fn(arg_actions, conf_actions, force_set, ignore_default_section_config);
# 172 : 2968 : }
# 173 : 1484 : }
# 174 : 742 : });
# 175 : 14 : });
# 176 : 2 : }
# 177 : : };
# 178 : :
# 179 : : // Regression test covering different ways config settings can be merged. The
# 180 : : // test parses and merges settings, representing the results as strings that get
# 181 : : // compared against an expected hash. To debug, the result strings can be dumped
# 182 : : // to a file (see comments below).
# 183 : : BOOST_FIXTURE_TEST_CASE(Merge, MergeTestingSetup)
# 184 : 2 : {
# 185 : 2 : CHash256 out_sha;
# 186 : 2 : FILE* out_file = nullptr;
# 187 [ - + ]: 2 : if (const char* out_path = getenv("SETTINGS_MERGE_TEST_OUT")) {
# 188 : 0 : out_file = fsbridge::fopen(out_path, "w");
# 189 [ # # ]: 0 : if (!out_file) throw std::system_error(errno, std::generic_category(), "fopen failed");
# 190 : 0 : }
# 191 : :
# 192 : 2 : const std::string& network = CBaseChainParams::MAIN;
# 193 : 2 : ForEachMergeSetup([&](const ActionList& arg_actions, const ActionList& conf_actions, bool force_set,
# 194 : 2968 : bool ignore_default_section_config) {
# 195 : 2968 : std::string desc;
# 196 : 2968 : int value_suffix = 0;
# 197 : 2968 : util::Settings settings;
# 198 : :
# 199 [ + + ]: 2968 : const std::string& name = ignore_default_section_config ? "wallet" : "server";
# 200 : 2968 : auto push_values = [&](Action action, const char* value_prefix, const std::string& name_prefix,
# 201 : 17808 : std::vector<util::SettingsValue>& dest) {
# 202 [ + + ][ + + ]: 17808 : if (action == SET || action == SECTION_SET) {
# 203 [ + + ]: 19056 : for (int i = 0; i < 2; ++i) {
# 204 : 12704 : dest.push_back(value_prefix + ToString(++value_suffix));
# 205 : 12704 : desc += " " + name_prefix + name + "=" + dest.back().get_str();
# 206 : 12704 : }
# 207 [ + + ][ + + ]: 11456 : } else if (action == NEGATE || action == SECTION_NEGATE) {
# 208 : 6352 : dest.push_back(false);
# 209 : 6352 : desc += " " + name_prefix + "no" + name;
# 210 : 6352 : }
# 211 : 17808 : };
# 212 : :
# 213 [ + + ]: 2968 : if (force_set) {
# 214 : 1484 : settings.forced_settings[name] = "forced";
# 215 : 1484 : desc += " " + name + "=forced";
# 216 : 1484 : }
# 217 [ + + ]: 8904 : for (Action arg_action : arg_actions) {
# 218 : 8904 : push_values(arg_action, "a", "-", settings.command_line_options[name]);
# 219 : 8904 : }
# 220 [ + + ]: 8904 : for (Action conf_action : conf_actions) {
# 221 [ + + ][ + + ]: 8904 : bool use_section = conf_action == SECTION_SET || conf_action == SECTION_NEGATE;
# 222 [ + + ]: 8904 : push_values(conf_action, "c", use_section ? network + "." : "",
# 223 [ + + ]: 8904 : settings.ro_config[use_section ? network : ""][name]);
# 224 : 8904 : }
# 225 : :
# 226 : 2968 : desc += " || ";
# 227 : 2968 : desc += GetSetting(settings, network, name, ignore_default_section_config, /*ignore_nonpersistent=*/false, /*get_chain_name=*/false).write();
# 228 : 2968 : desc += " |";
# 229 [ + + ]: 6012 : for (const auto& s : GetSettingsList(settings, network, name, ignore_default_section_config)) {
# 230 : 6012 : desc += " ";
# 231 : 6012 : desc += s.write();
# 232 : 6012 : }
# 233 : 2968 : desc += " |";
# 234 [ + + ]: 2968 : if (OnlyHasDefaultSectionSetting(settings, network, name)) desc += " ignored";
# 235 : 2968 : desc += "\n";
# 236 : :
# 237 : 2968 : out_sha.Write(MakeUCharSpan(desc));
# 238 [ - + ]: 2968 : if (out_file) {
# 239 : 0 : BOOST_REQUIRE(fwrite(desc.data(), 1, desc.size(), out_file) == desc.size());
# 240 : 0 : }
# 241 : 2968 : });
# 242 : :
# 243 [ - + ]: 2 : if (out_file) {
# 244 [ # # ]: 0 : if (fclose(out_file)) throw std::system_error(errno, std::generic_category(), "fclose failed");
# 245 : 0 : out_file = nullptr;
# 246 : 0 : }
# 247 : :
# 248 : 2 : unsigned char out_sha_bytes[CSHA256::OUTPUT_SIZE];
# 249 : 2 : out_sha.Finalize(out_sha_bytes);
# 250 : 2 : std::string out_sha_hex = HexStr(out_sha_bytes);
# 251 : :
# 252 : : // If check below fails, should manually dump the results with:
# 253 : : //
# 254 : : // SETTINGS_MERGE_TEST_OUT=results.txt ./test_bitcoin --run_test=settings_tests/Merge
# 255 : : //
# 256 : : // And verify diff against previous results to make sure the changes are expected.
# 257 : : //
# 258 : : // Results file is formatted like:
# 259 : : //
# 260 : : // <input> || GetSetting() | GetSettingsList() | OnlyHasDefaultSectionSetting()
# 261 : 2 : BOOST_CHECK_EQUAL(out_sha_hex, "79db02d74e3e193196541b67c068b40ebd0c124a24b3ecbe9cbf7e85b1c4ba7a");
# 262 : 2 : }
# 263 : :
# 264 : : BOOST_AUTO_TEST_SUITE_END()
|