settings_tests.cpp raw
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 may be caused by a crash, power loss, full disk, or storage error, "
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()
270