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