Bitcoin Core  27.99.0
P2P Digital Currency
settings_tests.cpp
Go to the documentation of this file.
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 
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 
25 {
26  return a.write() == b.write();
27 }
28 
29 inline std::ostream& operator<<(std::ostream& os, const common::SettingsValue& value)
30 {
31  os << value.write();
32  return os;
33 }
34 
35 inline std::ostream& operator<<(std::ostream& os, const std::pair<std::string, common::SettingsValue>& kv)
36 {
38  out.pushKVEnd(kv.first, kv.second);
39  os << out.write();
40  return os;
41 }
42 
43 inline void WriteText(const fs::path& path, const std::string& text)
44 {
45  std::ofstream file;
46  file.open(path);
47  file << text;
48 }
49 
50 BOOST_FIXTURE_TEST_SUITE(settings_tests, BasicTestingSetup)
51 
53 {
54  fs::path path = m_args.GetDataDirBase() / "settings.json";
55 
56  WriteText(path, R"({
57  "string": "string",
58  "num": 5,
59  "bool": true,
60  "null": null
61  })");
62 
63  std::map<std::string, common::SettingsValue> expected{
64  {"string", "string"},
65  {"num", 5},
66  {"bool", true},
67  {"null", {}},
68  };
69 
70  // Check file read.
71  std::map<std::string, common::SettingsValue> values;
72  std::vector<std::string> errors;
73  BOOST_CHECK(common::ReadSettings(path, values, errors));
74  BOOST_CHECK_EQUAL_COLLECTIONS(values.begin(), values.end(), expected.begin(), expected.end());
75  BOOST_CHECK(errors.empty());
76 
77  // Check no errors if file doesn't exist.
78  fs::remove(path);
79  BOOST_CHECK(common::ReadSettings(path, values, errors));
80  BOOST_CHECK(values.empty());
81  BOOST_CHECK(errors.empty());
82 
83  // Check duplicate keys not allowed and that values returns empty if a duplicate is found.
84  WriteText(path, R"({
85  "dupe": "string",
86  "dupe": "dupe"
87  })");
88  BOOST_CHECK(!common::ReadSettings(path, values, errors));
89  std::vector<std::string> dup_keys = {strprintf("Found duplicate key dupe in settings file %s", fs::PathToString(path))};
90  BOOST_CHECK_EQUAL_COLLECTIONS(errors.begin(), errors.end(), dup_keys.begin(), dup_keys.end());
91  BOOST_CHECK(values.empty());
92 
93  // Check non-kv json files not allowed
94  WriteText(path, R"("non-kv")");
95  BOOST_CHECK(!common::ReadSettings(path, values, errors));
96  std::vector<std::string> non_kv = {strprintf("Found non-object value \"non-kv\" in settings file %s", fs::PathToString(path))};
97  BOOST_CHECK_EQUAL_COLLECTIONS(errors.begin(), errors.end(), non_kv.begin(), non_kv.end());
98 
99  // Check invalid json not allowed
100  WriteText(path, R"(invalid json)");
101  BOOST_CHECK(!common::ReadSettings(path, values, errors));
102  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, "
103  "and can be fixed by removing the file, which will reset settings to default values.",
104  fs::PathToString(path))};
105  BOOST_CHECK_EQUAL_COLLECTIONS(errors.begin(), errors.end(), fail_parse.begin(), fail_parse.end());
106 }
107 
109 static void CheckValues(const common::Settings& settings, const std::string& single_val, const std::string& list_val)
110 {
111  common::SettingsValue single_value = GetSetting(settings, "section", "name", false, false, false);
113  for (const auto& item : GetSettingsList(settings, "section", "name", false)) {
114  list_value.push_back(item);
115  }
116  BOOST_CHECK_EQUAL(single_value.write().c_str(), single_val);
117  BOOST_CHECK_EQUAL(list_value.write().c_str(), list_val);
118 };
119 
120 // Simple settings merge test case.
122 {
123  common::Settings settings;
124  settings.command_line_options["name"].emplace_back("val1");
125  settings.command_line_options["name"].emplace_back("val2");
126  settings.ro_config["section"]["name"].emplace_back(2);
127 
128  // The last given arg takes precedence when specified via commandline.
129  CheckValues(settings, R"("val2")", R"(["val1","val2",2])");
130 
131  common::Settings settings2;
132  settings2.ro_config["section"]["name"].emplace_back("val2");
133  settings2.ro_config["section"]["name"].emplace_back("val3");
134 
135  // The first given arg takes precedence when specified via config file.
136  CheckValues(settings2, R"("val2")", R"(["val2","val3"])");
137 }
138 
139 // Confirm that a high priority setting overrides a lower priority setting even
140 // if the high priority setting is null. This behavior is useful for a high
141 // priority setting source to be able to effectively reset any setting back to
142 // its default value.
143 BOOST_AUTO_TEST_CASE(NullOverride)
144 {
145  common::Settings settings;
146  settings.command_line_options["name"].emplace_back("value");
147  BOOST_CHECK_EQUAL(R"("value")", GetSetting(settings, "section", "name", false, false, false).write().c_str());
148  settings.forced_settings["name"] = {};
149  BOOST_CHECK_EQUAL(R"(null)", GetSetting(settings, "section", "name", false, false, false).write().c_str());
150 }
151 
152 // Test different ways settings can be merged, and verify results. This test can
153 // be used to confirm that updates to settings code don't change behavior
154 // unintentionally.
158  static constexpr int MAX_ACTIONS = 3;
159 
162 
164  template <typename Fn>
165  void ForEachMergeSetup(Fn&& fn)
166  {
167  ActionList arg_actions = {};
168  // command_line_options do not have sections. Only iterate over SET and NEGATE
169  ForEachNoDup(arg_actions, SET, NEGATE, [&]{
170  ActionList conf_actions = {};
171  ForEachNoDup(conf_actions, SET, SECTION_NEGATE, [&]{
172  for (bool force_set : {false, true}) {
173  for (bool ignore_default_section_config : {false, true}) {
174  fn(arg_actions, conf_actions, force_set, ignore_default_section_config);
175  }
176  }
177  });
178  });
179  }
180 };
181 
182 // Regression test covering different ways config settings can be merged. The
183 // test parses and merges settings, representing the results as strings that get
184 // compared against an expected hash. To debug, the result strings can be dumped
185 // to a file (see comments below).
187 {
188  CHash256 out_sha;
189  FILE* out_file = nullptr;
190  if (const char* out_path = getenv("SETTINGS_MERGE_TEST_OUT")) {
191  out_file = fsbridge::fopen(out_path, "w");
192  if (!out_file) throw std::system_error(errno, std::generic_category(), "fopen failed");
193  }
194 
195  const std::string& network = ChainTypeToString(ChainType::MAIN);
196  ForEachMergeSetup([&](const ActionList& arg_actions, const ActionList& conf_actions, bool force_set,
197  bool ignore_default_section_config) {
198  std::string desc;
199  int value_suffix = 0;
200  common::Settings settings;
201 
202  const std::string& name = ignore_default_section_config ? "wallet" : "server";
203  auto push_values = [&](Action action, const char* value_prefix, const std::string& name_prefix,
204  std::vector<common::SettingsValue>& dest) {
205  if (action == SET || action == SECTION_SET) {
206  for (int i = 0; i < 2; ++i) {
207  dest.emplace_back(value_prefix + ToString(++value_suffix));
208  desc += " " + name_prefix + name + "=" + dest.back().get_str();
209  }
210  } else if (action == NEGATE || action == SECTION_NEGATE) {
211  dest.emplace_back(false);
212  desc += " " + name_prefix + "no" + name;
213  }
214  };
215 
216  if (force_set) {
217  settings.forced_settings[name] = "forced";
218  desc += " " + name + "=forced";
219  }
220  for (Action arg_action : arg_actions) {
221  push_values(arg_action, "a", "-", settings.command_line_options[name]);
222  }
223  for (Action conf_action : conf_actions) {
224  bool use_section = conf_action == SECTION_SET || conf_action == SECTION_NEGATE;
225  push_values(conf_action, "c", use_section ? network + "." : "",
226  settings.ro_config[use_section ? network : ""][name]);
227  }
228 
229  desc += " || ";
230  desc += GetSetting(settings, network, name, ignore_default_section_config, /*ignore_nonpersistent=*/false, /*get_chain_type=*/false).write();
231  desc += " |";
232  for (const auto& s : GetSettingsList(settings, network, name, ignore_default_section_config)) {
233  desc += " ";
234  desc += s.write();
235  }
236  desc += " |";
237  if (OnlyHasDefaultSectionSetting(settings, network, name)) desc += " ignored";
238  desc += "\n";
239 
240  out_sha.Write(MakeUCharSpan(desc));
241  if (out_file) {
242  BOOST_REQUIRE(fwrite(desc.data(), 1, desc.size(), out_file) == desc.size());
243  }
244  });
245 
246  if (out_file) {
247  if (fclose(out_file)) throw std::system_error(errno, std::generic_category(), "fclose failed");
248  out_file = nullptr;
249  }
250 
251  unsigned char out_sha_bytes[CSHA256::OUTPUT_SIZE];
252  out_sha.Finalize(out_sha_bytes);
253  std::string out_sha_hex = HexStr(out_sha_bytes);
254 
255  // If check below fails, should manually dump the results with:
256  //
257  // SETTINGS_MERGE_TEST_OUT=results.txt ./test_bitcoin --run_test=settings_tests/Merge
258  //
259  // And verify diff against previous results to make sure the changes are expected.
260  //
261  // Results file is formatted like:
262  //
263  // <input> || GetSetting() | GetSettingsList() | OnlyHasDefaultSectionSetting()
264  BOOST_CHECK_EQUAL(out_sha_hex, "79db02d74e3e193196541b67c068b40ebd0c124a24b3ecbe9cbf7e85b1c4ba7a");
265 }
266 
std::string ChainTypeToString(ChainType chain)
Definition: chaintype.cpp:11
A hasher class for Bitcoin's 256-bit hash (double SHA-256).
Definition: hash.h:24
CHash256 & Write(Span< const unsigned char > input)
Definition: hash.h:37
void Finalize(Span< unsigned char > output)
Definition: hash.h:30
static const size_t OUTPUT_SIZE
Definition: sha256.h:21
void push_back(UniValue val)
Definition: univalue.cpp:104
@ VOBJ
Definition: univalue.h:24
@ VARR
Definition: univalue.h:24
std::string write(unsigned int prettyIndent=0, unsigned int indentLevel=0) const
Path class wrapper to block calls to the fs::path(std::string) implicit constructor and the fs::path:...
Definition: fs.h:33
BOOST_AUTO_TEST_SUITE_END()
bool ReadSettings(const fs::path &path, std::map< std::string, SettingsValue > &values, std::vector< std::string > &errors)
Read settings file.
Definition: settings.cpp:74
SettingsValue GetSetting(const Settings &settings, const std::string &section, const std::string &name, bool ignore_default_section_config, bool ignore_nonpersistent, bool get_chain_type)
Get settings value from combined sources: forced settings, command line arguments,...
Definition: settings.cpp:148
std::vector< SettingsValue > GetSettingsList(const Settings &settings, const std::string &section, const std::string &name, bool ignore_default_section_config)
Get combined setting value similar to GetSetting(), except if setting was specified multiple times,...
Definition: settings.cpp:205
bool OnlyHasDefaultSectionSetting(const Settings &settings, const std::string &section, const std::string &name)
Return true if a setting is set in the default config file section, and not overridden by a higher pr...
Definition: settings.cpp:250
static std::string PathToString(const path &path)
Convert path object to a byte string.
Definition: fs.h:151
FILE * fopen(const fs::path &p, const char *mode)
Definition: fs.cpp:26
#define BOOST_CHECK_EQUAL(v1, v2)
Definition: object.cpp:18
#define BOOST_CHECK(expr)
Definition: object.cpp:17
const char * name
Definition: rest.cpp:50
static const int64_t values[]
A selection of numbers that do not trigger int64_t overflow when added/subtracted.
BOOST_AUTO_TEST_CASE(ReadWrite)
static void CheckValues(const common::Settings &settings, const std::string &single_val, const std::string &list_val)
Check settings struct contents against expected json strings.
void WriteText(const fs::path &path, const std::string &text)
BOOST_FIXTURE_TEST_CASE(Merge, MergeTestingSetup)
std::ostream & operator<<(std::ostream &os, const common::SettingsValue &value)
bool operator==(const common::SettingsValue &a, const common::SettingsValue &b)
constexpr auto MakeUCharSpan(V &&v) -> decltype(UCharSpanCast(Span{std::forward< V >(v)}))
Like the Span constructor, but for (const) unsigned char member types only.
Definition: span.h:304
void ForEachNoDup(CharType(&string)[StringLength], CharType min_char, CharType max_char, Fn &&fn)
Iterate over string values and call function for each string without successive duplicate characters.
Definition: str.h:32
std::string ToString(const T &t)
Locale-independent version of std::to_string.
Definition: string.h:110
Basic testing setup.
Definition: setup_common.h:52
void ForEachMergeSetup(Fn &&fn)
Enumerate all possible test configurations.
Action[MAX_ACTIONS] ActionList
static constexpr int MAX_ACTIONS
Max number of actions to sequence together.
Stored settings.
Definition: settings.h:32
std::map< std::string, std::map< std::string, std::vector< SettingsValue > > > ro_config
Map of config section name and setting name to list of config file values.
Definition: settings.h:40
std::map< std::string, SettingsValue > forced_settings
Map of setting name to forced setting value.
Definition: settings.h:34
std::map< std::string, std::vector< SettingsValue > > command_line_options
Map of setting name to list of command line values.
Definition: settings.h:36
#define strprintf
Format arguments and return the string or write to given std::ostream (see tinyformat::format doc for...
Definition: tinyformat.h:1162
std::string HexStr(const Span< const uint8_t > s)
Convert a span of bytes to a lower-case hexadecimal string.