/ src / test / settings_tests.cpp
settings_tests.cpp
  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  inline bool operator==(const common::SettingsValue& a, const common::SettingsValue& b)
 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  {
 37      common::SettingsValue out(common::SettingsValue::VOBJ);
 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  
 52  BOOST_AUTO_TEST_CASE(ReadWrite)
 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  
108  //! Check settings struct contents against expected json strings.
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);
112      common::SettingsValue list_value(common::SettingsValue::VARR);
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.
121  BOOST_AUTO_TEST_CASE(Simple)
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.
155  struct MergeTestingSetup : public BasicTestingSetup {
156      //! Max number of actions to sequence together. Can decrease this when
157      //! debugging to make test results easier to understand.
158      static constexpr int MAX_ACTIONS = 3;
159  
160      enum Action { END, SET, NEGATE, SECTION_SET, SECTION_NEGATE };
161      using ActionList = Action[MAX_ACTIONS];
162  
163      //! Enumerate all possible test configurations.
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).
186  BOOST_FIXTURE_TEST_CASE(Merge, MergeTestingSetup)
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  
267  BOOST_AUTO_TEST_SUITE_END()