settings_tests.cpp raw

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