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