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 : : using util::ToString;
25 : :
26 : 4 : inline bool operator==(const common::SettingsValue& a, const common::SettingsValue& b)
27 : : {
28 [ + - ]: 4 : 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 : 0 : inline std::ostream& operator<<(std::ostream& os, const std::pair<std::string, common::SettingsValue>& kv)
38 : : {
39 : 0 : common::SettingsValue out(common::SettingsValue::VOBJ);
40 [ # # # # : 0 : out.pushKVEnd(kv.first, kv.second);
# # ]
41 [ # # ]: 0 : os << out.write();
42 : 0 : return os;
43 : 0 : }
44 : :
45 : 4 : inline void WriteText(const fs::path& path, const std::string& text)
46 : : {
47 : 4 : std::ofstream file;
48 [ + - ]: 4 : file.open(path);
49 [ + - ]: 4 : file << text;
50 : 4 : }
51 : :
52 : : BOOST_FIXTURE_TEST_SUITE(settings_tests, BasicTestingSetup)
53 : :
54 [ + - + - : 7 : BOOST_AUTO_TEST_CASE(ReadWrite)
+ - + - +
- + - + -
+ - + - +
- + - + -
+ - + - +
- + - + -
+ - + - +
- + - + -
+ - + - +
- + - + -
+ - + - +
- ]
55 : : {
56 [ + - ]: 2 : fs::path path = m_args.GetDataDirBase() / "settings.json";
57 : :
58 [ + - + - ]: 1 : WriteText(path, R"({
59 : : "string": "string",
60 : : "num": 5,
61 : : "bool": true,
62 : : "null": null
63 : : })");
64 : :
65 : 1 : std::map<std::string, common::SettingsValue> expected{
66 : : {"string", "string"},
67 : 1 : {"num", 5},
68 : : {"bool", true},
69 : : {"null", {}},
70 [ + - - + : 6 : };
+ + - - ]
71 : :
72 : : // Check file read.
73 [ + - ]: 1 : std::map<std::string, common::SettingsValue> values;
74 : 1 : std::vector<std::string> errors;
75 [ + - + - : 2 : BOOST_CHECK(common::ReadSettings(path, values, errors));
+ - + - ]
76 [ + - + - : 2 : BOOST_CHECK_EQUAL_COLLECTIONS(values.begin(), values.end(), expected.begin(), expected.end());
+ - + - ]
77 [ + - + - : 2 : BOOST_CHECK(errors.empty());
+ - ]
78 : :
79 : : // Check no errors if file doesn't exist.
80 [ + - ]: 1 : fs::remove(path);
81 [ + - + - : 2 : BOOST_CHECK(common::ReadSettings(path, values, errors));
+ - + - ]
82 [ + - + - : 2 : BOOST_CHECK(values.empty());
+ - ]
83 [ + - + - : 2 : BOOST_CHECK(errors.empty());
+ - + - ]
84 : :
85 : : // Check duplicate keys not allowed and that values returns empty if a duplicate is found.
86 [ + - + - ]: 1 : WriteText(path, R"({
87 : : "dupe": "string",
88 : : "dupe": "dupe"
89 : : })");
90 [ + - + - : 2 : BOOST_CHECK(!common::ReadSettings(path, values, errors));
+ - + - ]
91 [ - + + + : 2 : std::vector<std::string> dup_keys = {strprintf("Found duplicate key dupe in settings file %s", fs::PathToString(path))};
- - ]
92 [ + - + - : 2 : BOOST_CHECK_EQUAL_COLLECTIONS(errors.begin(), errors.end(), dup_keys.begin(), dup_keys.end());
+ - + - ]
93 [ + - + - : 2 : BOOST_CHECK(values.empty());
+ - ]
94 : :
95 : : // Check non-kv json files not allowed
96 [ + - + - ]: 1 : WriteText(path, R"("non-kv")");
97 [ + - + - : 2 : BOOST_CHECK(!common::ReadSettings(path, values, errors));
+ - + - ]
98 [ - + + + : 2 : std::vector<std::string> non_kv = {strprintf("Found non-object value \"non-kv\" in settings file %s", fs::PathToString(path))};
- - ]
99 [ + - + - : 2 : BOOST_CHECK_EQUAL_COLLECTIONS(errors.begin(), errors.end(), non_kv.begin(), non_kv.end());
+ - + - ]
100 : :
101 : : // Check invalid json not allowed
102 [ + - + - ]: 1 : WriteText(path, R"(invalid json)");
103 [ + - + - : 2 : BOOST_CHECK(!common::ReadSettings(path, values, errors));
+ - + - ]
104 : 1 : 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 [ - + + + : 2 : fs::PathToString(path))};
- - ]
107 [ + - + - : 2 : BOOST_CHECK_EQUAL_COLLECTIONS(errors.begin(), errors.end(), fail_parse.begin(), fail_parse.end());
+ - ]
108 [ + - + - : 9 : }
+ - + - +
- + - + -
+ - + - +
- + - -
- ]
109 : :
110 : : //! Check settings struct contents against expected json strings.
111 : 2 : static void CheckValues(const common::Settings& settings, const std::string& single_val, const std::string& list_val)
112 : : {
113 [ + - + - ]: 4 : common::SettingsValue single_value = GetSetting(settings, "section", "name", false, false, false);
114 : 2 : common::SettingsValue list_value(common::SettingsValue::VARR);
115 [ + - + - : 7 : for (const auto& item : GetSettingsList(settings, "section", "name", false)) {
+ - + + ]
116 [ + - + - ]: 5 : list_value.push_back(item);
117 : 2 : }
118 [ + - + - : 2 : BOOST_CHECK_EQUAL(single_value.write().c_str(), single_val);
+ - ]
119 [ + - + - : 2 : BOOST_CHECK_EQUAL(list_value.write().c_str(), list_val);
+ - ]
120 : 2 : };
121 : :
122 : : // Simple settings merge test case.
123 [ + - + - : 7 : BOOST_AUTO_TEST_CASE(Simple)
+ - + - +
- + - + -
+ - + - +
- + - + -
+ - + - +
- + - + -
+ - + - +
- + - + -
+ - + - +
- + - + -
+ - + - +
- ]
124 : : {
125 : 1 : common::Settings settings;
126 [ + - + - : 1 : settings.command_line_options["name"].emplace_back("val1");
+ - ]
127 [ + - + - : 1 : settings.command_line_options["name"].emplace_back("val2");
+ - ]
128 [ + - + - : 2 : settings.ro_config["section"]["name"].emplace_back(2);
+ - + - +
- ]
129 : :
130 : : // The last given arg takes precedence when specified via commandline.
131 [ + - + - : 2 : CheckValues(settings, R"("val2")", R"(["val1","val2",2])");
+ - ]
132 : :
133 : 1 : common::Settings settings2;
134 [ + - + - : 2 : settings2.ro_config["section"]["name"].emplace_back("val2");
+ - + - +
- ]
135 [ + - + - : 2 : settings2.ro_config["section"]["name"].emplace_back("val3");
+ - + - +
- ]
136 : :
137 : : // The first given arg takes precedence when specified via config file.
138 [ + - + - : 2 : CheckValues(settings2, R"("val2")", R"(["val2","val3"])");
+ - ]
139 : 1 : }
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 [ + - + - : 7 : BOOST_AUTO_TEST_CASE(NullOverride)
+ - + - +
- + - + -
+ - + - +
- + - + -
+ - + - +
- + - + -
+ - + - +
- + - + -
+ - + - +
- + - + -
+ - + - +
- ]
146 : : {
147 : 1 : common::Settings settings;
148 [ + - + - : 1 : settings.command_line_options["name"].emplace_back("value");
+ - ]
149 [ + - + - : 1 : BOOST_CHECK_EQUAL(R"("value")", GetSetting(settings, "section", "name", false, false, false).write().c_str());
+ - + - +
- + - ]
150 [ + - + - ]: 1 : settings.forced_settings["name"] = {};
151 [ + - + - : 1 : BOOST_CHECK_EQUAL(R"(null)", GetSetting(settings, "section", "name", false, false, false).write().c_str());
+ - + - +
- + - ]
152 : 1 : }
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 : 2 : 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 : 1 : void ForEachMergeSetup(Fn&& fn)
168 : : {
169 : 1 : ActionList arg_actions = {};
170 : : // command_line_options do not have sections. Only iterate over SET and NEGATE
171 : 1 : ForEachNoDup(arg_actions, SET, NEGATE, [&]{
172 : 7 : ActionList conf_actions = {};
173 : 7 : ForEachNoDup(conf_actions, SET, SECTION_NEGATE, [&]{
174 [ + + ]: 1113 : for (bool force_set : {false, true}) {
175 [ + + ]: 2226 : for (bool ignore_default_section_config : {false, true}) {
176 : 1484 : fn(arg_actions, conf_actions, force_set, ignore_default_section_config);
177 : : }
178 : : }
179 : : });
180 : : });
181 : 1 : }
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 [ + - + - : 7 : BOOST_FIXTURE_TEST_CASE(Merge, MergeTestingSetup)
+ - + - +
- + - + -
+ - + - +
- + - + -
+ - + - +
- + - + -
+ - + - +
- + - + -
+ - + - +
- + - + -
+ - + - +
- ]
189 : : {
190 : 1 : CHash256 out_sha;
191 : 1 : FILE* out_file = nullptr;
192 [ - + ]: 1 : if (const char* out_path = getenv("SETTINGS_MERGE_TEST_OUT")) {
193 [ # # ]: 0 : out_file = fsbridge::fopen(out_path, "w");
194 [ # # # # ]: 0 : if (!out_file) throw std::system_error(errno, std::generic_category(), "fopen failed");
195 : : }
196 : :
197 : 1 : const std::string& network = ChainTypeToString(ChainType::MAIN);
198 [ + - ]: 1485 : ForEachMergeSetup([&](const ActionList& arg_actions, const ActionList& conf_actions, bool force_set,
199 : : bool ignore_default_section_config) {
200 : 1484 : std::string desc;
201 : 1484 : int value_suffix = 0;
202 : 1484 : common::Settings settings;
203 : :
204 [ + + + - : 2226 : const std::string& name = ignore_default_section_config ? "wallet" : "server";
+ + ]
205 : 10388 : auto push_values = [&](Action action, const char* value_prefix, const std::string& name_prefix,
206 : : std::vector<common::SettingsValue>& dest) {
207 [ + + ]: 8904 : if (action == SET || action == SECTION_SET) {
208 [ + + ]: 9528 : for (int i = 0; i < 2; ++i) {
209 [ + - + - ]: 12704 : dest.emplace_back(value_prefix + ToString(++value_suffix));
210 [ + - + - : 19056 : desc += " " + name_prefix + name + "=" + dest.back().get_str();
+ - ]
211 : : }
212 [ + + ]: 5728 : } else if (action == NEGATE || action == SECTION_NEGATE) {
213 : 3176 : dest.emplace_back(false);
214 [ + - + - : 9528 : desc += " " + name_prefix + "no" + name;
+ - ]
215 : : }
216 : 8904 : };
217 : :
218 [ + + ]: 1484 : if (force_set) {
219 [ + - + - ]: 742 : settings.forced_settings[name] = "forced";
220 [ + - + - ]: 2226 : desc += " " + name + "=forced";
221 : : }
222 [ + + ]: 5936 : for (Action arg_action : arg_actions) {
223 [ + - + - : 8904 : push_values(arg_action, "a", "-", settings.command_line_options[name]);
+ - ]
224 : : }
225 [ + + ]: 5936 : for (Action conf_action : conf_actions) {
226 : 4452 : bool use_section = conf_action == SECTION_SET || conf_action == SECTION_NEGATE;
227 [ + + + - : 8904 : push_values(conf_action, "c", use_section ? network + "." : "",
+ - + - ]
228 [ + + + - : 8904 : settings.ro_config[use_section ? network : ""][name]);
+ - + - +
- ]
229 : : }
230 : :
231 [ + - ]: 1484 : desc += " || ";
232 [ + - + - ]: 2968 : desc += GetSetting(settings, network, name, ignore_default_section_config, /*ignore_nonpersistent=*/false, /*get_chain_type=*/false).write();
233 [ + - ]: 1484 : desc += " |";
234 [ + - + + ]: 4490 : for (const auto& s : GetSettingsList(settings, network, name, ignore_default_section_config)) {
235 [ + - ]: 3006 : desc += " ";
236 [ + - ]: 6012 : desc += s.write();
237 : 1484 : }
238 [ + - ]: 1484 : desc += " |";
239 [ + - + + : 1484 : if (OnlyHasDefaultSectionSetting(settings, network, name)) desc += " ignored";
+ - ]
240 [ + - ]: 1484 : desc += "\n";
241 : :
242 [ + - ]: 1484 : out_sha.Write(MakeUCharSpan(desc));
243 [ - + ]: 1484 : if (out_file) {
244 [ # # # # : 0 : BOOST_REQUIRE(fwrite(desc.data(), 1, desc.size(), out_file) == desc.size());
# # ]
245 : : }
246 : 1484 : });
247 : :
248 [ - + ]: 1 : if (out_file) {
249 [ # # # # : 0 : if (fclose(out_file)) throw std::system_error(errno, std::generic_category(), "fclose failed");
# # ]
250 : 0 : out_file = nullptr;
251 : : }
252 : :
253 : 1 : unsigned char out_sha_bytes[CSHA256::OUTPUT_SIZE];
254 [ + - ]: 1 : out_sha.Finalize(out_sha_bytes);
255 [ + - ]: 1 : 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 [ + - + - ]: 1 : BOOST_CHECK_EQUAL(out_sha_hex, "79db02d74e3e193196541b67c068b40ebd0c124a24b3ecbe9cbf7e85b1c4ba7a");
267 : 1 : }
268 : :
269 : : BOOST_AUTO_TEST_SUITE_END()
|