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