LCOV - code coverage report
Current view: top level - src/test - settings_tests.cpp (source / functions) Hit Total Coverage
Test: total_coverage.info Lines: 126 141 89.4 %
Date: 2026-06-25 07:23:43 Functions: 41 42 97.6 %

          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 <util/settings.h>
       6             : 
       7             : #include <fs.h>
       8             : #include <test/util/setup_common.h>
       9             : #include <test/util/str.h>
      10             : 
      11             : 
      12             : #include <boost/test/unit_test.hpp>
      13             : #include <univalue.h>
      14             : #include <util/strencodings.h>
      15             : #include <util/string.h>
      16             : #include <util/system.h>
      17             : 
      18             : #include <fstream>
      19             : #include <map>
      20             : #include <string>
      21             : #include <system_error>
      22             : #include <vector>
      23             : 
      24           4 : inline bool operator==(const util::SettingsValue& a, const util::SettingsValue& b)
      25             : {
      26           4 :     return a.write() == b.write();
      27           0 : }
      28             : 
      29             : inline std::ostream& operator<<(std::ostream& os, const util::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, util::SettingsValue>& kv)
      36             : {
      37           0 :     util::SettingsValue out(util::SettingsValue::VOBJ);
      38           0 :     out.__pushKV(kv.first, kv.second);
      39           0 :     os << out.write();
      40           0 :     return os;
      41           0 : }
      42             : 
      43           4 : inline void WriteText(const fs::path& path, const std::string& text)
      44             : {
      45           4 :     std::ofstream file;
      46           4 :     file.open(path);
      47           4 :     file << text;
      48           4 : }
      49             : 
      50         146 : BOOST_FIXTURE_TEST_SUITE(settings_tests, BasicTestingSetup)
      51             : 
      52         149 : BOOST_AUTO_TEST_CASE(ReadWrite)
      53             : {
      54           1 :     fs::path path = m_args.GetDataDirBase() / "settings.json";
      55             : 
      56           1 :     WriteText(path, R"({
      57             :         "string": "string",
      58             :         "num": 5,
      59             :         "bool": true,
      60             :         "null": null
      61             :     })");
      62             : 
      63           2 :     std::map<std::string, util::SettingsValue> expected{
      64           1 :         {"string", "string"},
      65           1 :         {"num", 5},
      66           1 :         {"bool", true},
      67           1 :         {"null", {}},
      68             :     };
      69             : 
      70             :     // Check file read.
      71           1 :     std::map<std::string, util::SettingsValue> values;
      72           1 :     std::vector<std::string> errors;
      73           1 :     BOOST_CHECK(util::ReadSettings(path, values, errors));
      74           1 :     BOOST_CHECK_EQUAL_COLLECTIONS(values.begin(), values.end(), expected.begin(), expected.end());
      75           1 :     BOOST_CHECK(errors.empty());
      76             : 
      77             :     // Check no errors if file doesn't exist.
      78           1 :     fs::remove(path);
      79           1 :     BOOST_CHECK(util::ReadSettings(path, values, errors));
      80           1 :     BOOST_CHECK(values.empty());
      81           1 :     BOOST_CHECK(errors.empty());
      82             : 
      83             :     // Check duplicate keys not allowed and that values returns empty if a duplicate is found.
      84           1 :     WriteText(path, R"({
      85             :         "dupe": "string",
      86             :         "dupe": "dupe"
      87             :     })");
      88           1 :     BOOST_CHECK(!util::ReadSettings(path, values, errors));
      89           1 :     std::vector<std::string> dup_keys = {strprintf("Found duplicate key dupe in settings file %s", fs::PathToString(path))};
      90           1 :     BOOST_CHECK_EQUAL_COLLECTIONS(errors.begin(), errors.end(), dup_keys.begin(), dup_keys.end());
      91           1 :     BOOST_CHECK(values.empty());
      92             : 
      93             :     // Check non-kv json files not allowed
      94           1 :     WriteText(path, R"("non-kv")");
      95           1 :     BOOST_CHECK(!util::ReadSettings(path, values, errors));
      96           1 :     std::vector<std::string> non_kv = {strprintf("Found non-object value \"non-kv\" in settings file %s", fs::PathToString(path))};
      97           1 :     BOOST_CHECK_EQUAL_COLLECTIONS(errors.begin(), errors.end(), non_kv.begin(), non_kv.end());
      98             : 
      99             :     // Check invalid json not allowed
     100           1 :     WriteText(path, R"(invalid json)");
     101           1 :     BOOST_CHECK(!util::ReadSettings(path, values, errors));
     102           1 :     std::vector<std::string> fail_parse = {strprintf("Unable to parse settings file %s", fs::PathToString(path))};
     103           1 :     BOOST_CHECK_EQUAL_COLLECTIONS(errors.begin(), errors.end(), fail_parse.begin(), fail_parse.end());
     104           1 : }
     105             : 
     106             : //! Check settings struct contents against expected json strings.
     107           2 : static void CheckValues(const util::Settings& settings, const std::string& single_val, const std::string& list_val)
     108             : {
     109           2 :     util::SettingsValue single_value = GetSetting(settings, "section", "name", false, false, false);
     110           2 :     util::SettingsValue list_value(util::SettingsValue::VARR);
     111           7 :     for (const auto& item : GetSettingsList(settings, "section", "name", false)) {
     112           5 :         list_value.push_back(item);
     113             :     }
     114           2 :     BOOST_CHECK_EQUAL(single_value.write().c_str(), single_val);
     115           2 :     BOOST_CHECK_EQUAL(list_value.write().c_str(), list_val);
     116           2 : };
     117             : 
     118             : // Simple settings merge test case.
     119         149 : BOOST_AUTO_TEST_CASE(Simple)
     120             : {
     121           1 :     util::Settings settings;
     122           1 :     settings.command_line_options["name"].emplace_back("val1");
     123           1 :     settings.command_line_options["name"].emplace_back("val2");
     124           1 :     settings.ro_config["section"]["name"].emplace_back(2);
     125             : 
     126             :     // The last given arg takes precedence when specified via commandline.
     127           1 :     CheckValues(settings, R"("val2")", R"(["val1","val2",2])");
     128             : 
     129           1 :     util::Settings settings2;
     130           1 :     settings2.ro_config["section"]["name"].emplace_back("val2");
     131           1 :     settings2.ro_config["section"]["name"].emplace_back("val3");
     132             : 
     133             :     // The first given arg takes precedence when specified via config file.
     134           1 :     CheckValues(settings2, R"("val2")", R"(["val2","val3"])");
     135           1 : }
     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         149 : BOOST_AUTO_TEST_CASE(NullOverride)
     142             : {
     143           1 :     util::Settings settings;
     144           1 :     settings.command_line_options["name"].emplace_back("value");
     145           1 :     BOOST_CHECK_EQUAL(R"("value")", GetSetting(settings, "section", "name", false, false, false).write().c_str());
     146           1 :     settings.forced_settings["name"] = {};
     147           1 :     BOOST_CHECK_EQUAL(R"(null)", GetSetting(settings, "section", "name", false, false, false).write().c_str());
     148           1 : }
     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           1 : 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           1 :     void ForEachMergeSetup(Fn&& fn)
     164             :     {
     165           1 :         ActionList arg_actions = {};
     166             :         // command_line_options do not have sections. Only iterate over SET and NEGATE
     167           8 :         ForEachNoDup(arg_actions, SET, NEGATE, [&]{
     168           7 :             ActionList conf_actions = {};
     169         378 :             ForEachNoDup(conf_actions, SET, SECTION_NEGATE, [&]{
     170        1113 :                 for (bool force_set : {false, true}) {
     171        2226 :                     for (bool ignore_default_section_config : {false, true}) {
     172        1484 :                         fn(arg_actions, conf_actions, force_set, ignore_default_section_config);
     173             :                     }
     174             :                 }
     175         371 :             });
     176           7 :         });
     177           1 :     }
     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         148 : BOOST_FIXTURE_TEST_CASE(Merge, MergeTestingSetup)
     185             : {
     186           1 :     CHash256 out_sha;
     187           1 :     FILE* out_file = nullptr;
     188           1 :     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           1 :     const std::string& network = CBaseChainParams::MAIN;
     194        1485 :     ForEachMergeSetup([&](const ActionList& arg_actions, const ActionList& conf_actions, bool force_set,
     195             :                           bool ignore_default_section_config) {
     196        1484 :         std::string desc;
     197        1484 :         int value_suffix = 0;
     198        1484 :         util::Settings settings;
     199             : 
     200        1484 :         const std::string& name = ignore_default_section_config ? "wallet" : "server";
     201       10388 :         auto push_values = [&](Action action, const char* value_prefix, const std::string& name_prefix,
     202             :                                std::vector<util::SettingsValue>& dest) {
     203        8904 :             if (action == SET || action == SECTION_SET) {
     204        9528 :                 for (int i = 0; i < 2; ++i) {
     205        6352 :                     dest.emplace_back(value_prefix + ToString(++value_suffix));
     206        6352 :                     desc += " " + name_prefix + name + "=" + dest.back().get_str();
     207        6352 :                 }
     208        8904 :             } else if (action == NEGATE || action == SECTION_NEGATE) {
     209        3176 :                 dest.emplace_back(false);
     210        3176 :                 desc += " " + name_prefix + "no" + name;
     211        3176 :             }
     212        8904 :         };
     213             : 
     214        1484 :         if (force_set) {
     215         742 :             settings.forced_settings[name] = "forced";
     216         742 :             desc += " " + name + "=forced";
     217         742 :         }
     218        5936 :         for (Action arg_action : arg_actions) {
     219        4452 :             push_values(arg_action, "a", "-", settings.command_line_options[name]);
     220             :         }
     221        5936 :         for (Action conf_action : conf_actions) {
     222        4452 :             bool use_section = conf_action == SECTION_SET || conf_action == SECTION_NEGATE;
     223        8904 :             push_values(conf_action, "c", use_section ? network + "." : "",
     224        4452 :                 settings.ro_config[use_section ? network : ""][name]);
     225             :         }
     226             : 
     227        1484 :         desc += " || ";
     228        1484 :         desc += GetSetting(settings, network, name, ignore_default_section_config, /*ignore_nonpersistent=*/false, /*get_chain_name=*/false).write();
     229        1484 :         desc += " |";
     230        4490 :         for (const auto& s : GetSettingsList(settings, network, name, ignore_default_section_config)) {
     231        3006 :             desc += " ";
     232        3006 :             desc += s.write();
     233             :         }
     234        1484 :         desc += " |";
     235        1484 :         if (OnlyHasDefaultSectionSetting(settings, network, name)) desc += " ignored";
     236        1484 :         desc += "\n";
     237             : 
     238        1484 :         out_sha.Write(MakeUCharSpan(desc));
     239        1484 :         if (out_file) {
     240           0 :             BOOST_REQUIRE(fwrite(desc.data(), 1, desc.size(), out_file) == desc.size());
     241           0 :         }
     242        1484 :     });
     243             : 
     244           1 :     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           1 :     out_sha.Finalize(out_sha_bytes);
     251           1 :     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           1 :     BOOST_CHECK_EQUAL(out_sha_hex, "79db02d74e3e193196541b67c068b40ebd0c124a24b3ecbe9cbf7e85b1c4ba7a");
     263           1 : }
     264             : 
     265         146 : BOOST_AUTO_TEST_SUITE_END()

Generated by: LCOV version 1.16