@@ -158,6 +158,9 @@ struct TestArgsManager : public ArgsManager
158158 AddArg (args[i], " " , false , OptionsCategory::OPTIONS);
159159 }
160160 }
161+ using ArgsManager::ReadConfigStream;
162+ using ArgsManager::cs_args;
163+ using ArgsManager::m_network;
161164};
162165
163166BOOST_AUTO_TEST_CASE (util_ParseParameters)
@@ -575,6 +578,232 @@ BOOST_AUTO_TEST_CASE(util_GetChainName)
575578 BOOST_CHECK_THROW (test_args.GetChainName (), std::runtime_error);
576579}
577580
581+ // Test different ways settings can be merged, and verify results. This test can
582+ // be used to confirm that updates to settings code don't change behavior
583+ // unintentially.
584+ //
585+ // The test covers:
586+ //
587+ // - Combining different setting actions. Possible actions are: configuring a
588+ // setting, negating a setting (adding "-no" prefix), and configuring/negating
589+ // settings in a network section (adding "main." or "test." prefixes).
590+ //
591+ // - Combining settings from command line arguments and a config file.
592+ //
593+ // - Combining SoftSet and ForceSet calls.
594+ //
595+ // - Testing "main" and "test" network values to make sure settings from network
596+ // sections are applied and to check for mainnet-specific behaviors like
597+ // inheriting settings from the default section.
598+ //
599+ // - Testing network-specific settings like "-wallet", that may be ignored
600+ // outside a network section, and non-network specific settings like "-server"
601+ // that aren't sensitive to the network.
602+ //
603+ struct SettingsMergeTestingSetup : public BasicTestingSetup {
604+ // ! Max number of actions to sequence together. Can decrease this when
605+ // ! debugging to make test results easier to understand.
606+ static constexpr int MAX_ACTIONS = 3 ;
607+
608+ enum Action { SET = 0 , NEGATE, SECTION_SET, SECTION_NEGATE, END };
609+ using ActionList = Action[MAX_ACTIONS];
610+
611+ // ! Enumerate all possible test configurations.
612+ template <typename Fn>
613+ void ForEachMergeSetup (Fn&& fn)
614+ {
615+ ForEachActionList ([&](const ActionList& arg_actions) {
616+ ForEachActionList ([&](const ActionList& conf_actions) {
617+ for (bool soft_set : {false , true }) {
618+ for (bool force_set : {false , true }) {
619+ for (const std::string& section : {CBaseChainParams::MAIN, CBaseChainParams::TESTNET}) {
620+ for (const std::string& network : {CBaseChainParams::MAIN, CBaseChainParams::TESTNET}) {
621+ for (bool net_specific : {false , true }) {
622+ fn (arg_actions, conf_actions, soft_set, force_set, section, network, net_specific);
623+ }
624+ }
625+ }
626+ }
627+ }
628+ });
629+ });
630+ }
631+
632+ // ! Enumerate interesting combinations of actions.
633+ template <typename Fn>
634+ void ForEachActionList (Fn&& fn)
635+ {
636+ ActionList actions = {SET};
637+ for (bool done = false ; !done;) {
638+ int prev_action = -1 ;
639+ bool skip_actions = false ;
640+ for (Action action : actions) {
641+ if ((prev_action == END && action != END) || (prev_action != END && action == prev_action)) {
642+ // To cut down list of enumerated settings, skip enumerating
643+ // settings with ignored actions after an END, and settings that
644+ // repeat the same action twice in a row.
645+ skip_actions = true ;
646+ break ;
647+ }
648+ prev_action = action;
649+ }
650+ if (!skip_actions) fn (actions);
651+ done = true ;
652+ for (Action& action : actions) {
653+ action = Action (action < END ? action + 1 : 0 );
654+ if (action) {
655+ done = false ;
656+ break ;
657+ }
658+ }
659+ }
660+ }
661+
662+ // ! Translate actions into a list of <key>=<value> setting strings.
663+ std::vector<std::string> GetValues (const ActionList& actions,
664+ const std::string& section,
665+ const std::string& name,
666+ const std::string& value_prefix)
667+ {
668+ std::vector<std::string> values;
669+ int suffix = 0 ;
670+ for (Action action : actions) {
671+ if (action == END) break ;
672+ std::string prefix;
673+ if (action == SECTION_SET || action == SECTION_NEGATE) prefix = section + " ." ;
674+ if (action == SET || action == SECTION_SET) {
675+ for (int i = 0 ; i < 2 ; ++i) {
676+ values.push_back (prefix + name + " =" + value_prefix + std::to_string (++suffix));
677+ }
678+ }
679+ if (action == NEGATE || action == SECTION_NEGATE) {
680+ values.push_back (prefix + " no" + name + " =1" );
681+ }
682+ }
683+ return values;
684+ }
685+ };
686+
687+ // Regression test covering different ways config settings can be merged. The
688+ // test parses and merges settings, representing the results as strings that get
689+ // compared against an expected hash. To debug, the result strings can be dumped
690+ // to a file (see below).
691+ BOOST_FIXTURE_TEST_CASE (util_SettingsMerge, SettingsMergeTestingSetup)
692+ {
693+ CHash256 out_sha;
694+ FILE* out_file = nullptr ;
695+ if (const char * out_path = getenv (" SETTINGS_MERGE_TEST_OUT" )) {
696+ out_file = fsbridge::fopen (out_path, " w" );
697+ if (!out_file) throw std::system_error (errno, std::generic_category (), " fopen failed" );
698+ }
699+
700+ ForEachMergeSetup ([&](const ActionList& arg_actions, const ActionList& conf_actions, bool soft_set, bool force_set,
701+ const std::string& section, const std::string& network, bool net_specific) {
702+ TestArgsManager parser;
703+ LOCK (parser.cs_args );
704+
705+ std::string desc = " net=" ;
706+ desc += network;
707+ parser.m_network = network;
708+
709+ const std::string& name = net_specific ? " server" : " wallet" ;
710+ const std::string key = " -" + name;
711+ parser.AddArg (key, name, false , OptionsCategory::OPTIONS);
712+ if (net_specific) parser.SetNetworkOnlyArg (key);
713+
714+ auto args = GetValues (arg_actions, section, name, " a" );
715+ std::vector<const char *> argv = {" ignored" };
716+ for (auto & arg : args) {
717+ arg.insert (0 , " -" );
718+ desc += " " ;
719+ desc += arg;
720+ argv.push_back (arg.c_str ());
721+ }
722+ std::string error;
723+ BOOST_CHECK (parser.ParseParameters (argv.size (), argv.data (), error));
724+ BOOST_CHECK_EQUAL (error, " " );
725+
726+ std::string conf;
727+ for (auto & conf_val : GetValues (conf_actions, section, name, " c" )) {
728+ desc += " " ;
729+ desc += conf_val;
730+ conf += conf_val;
731+ conf += " \n " ;
732+ }
733+ std::istringstream conf_stream (conf);
734+ BOOST_CHECK (parser.ReadConfigStream (conf_stream, " filepath" , error));
735+ BOOST_CHECK_EQUAL (error, " " );
736+
737+ if (soft_set) {
738+ desc += " soft" ;
739+ parser.SoftSetArg (key, " soft1" );
740+ parser.SoftSetArg (key, " soft2" );
741+ }
742+
743+ if (force_set) {
744+ desc += " force" ;
745+ parser.ForceSetArg (key, " force1" );
746+ parser.ForceSetArg (key, " force2" );
747+ }
748+
749+ desc += " || " ;
750+
751+ if (!parser.IsArgSet (key)) {
752+ desc += " unset" ;
753+ BOOST_CHECK (!parser.IsArgNegated (key));
754+ BOOST_CHECK_EQUAL (parser.GetArg (key, " default" ), " default" );
755+ BOOST_CHECK (parser.GetArgs (key).empty ());
756+ } else if (parser.IsArgNegated (key)) {
757+ desc += " negated" ;
758+ BOOST_CHECK_EQUAL (parser.GetArg (key, " default" ), " 0" );
759+ BOOST_CHECK (parser.GetArgs (key).empty ());
760+ } else {
761+ desc += parser.GetArg (key, " default" );
762+ desc += " |" ;
763+ for (const auto & arg : parser.GetArgs (key)) {
764+ desc += " " ;
765+ desc += arg;
766+ }
767+ }
768+
769+ std::set<std::string> ignored = parser.GetUnsuitableSectionOnlyArgs ();
770+ if (!ignored.empty ()) {
771+ desc += " | ignored" ;
772+ for (const auto & arg : ignored) {
773+ desc += " " ;
774+ desc += arg;
775+ }
776+ }
777+
778+ desc += " \n " ;
779+
780+ out_sha.Write ((const unsigned char *)desc.data (), desc.size ());
781+ if (out_file) {
782+ BOOST_REQUIRE (fwrite (desc.data (), 1 , desc.size (), out_file) == desc.size ());
783+ }
784+ });
785+
786+ if (out_file) {
787+ if (fclose (out_file)) throw std::system_error (errno, std::generic_category (), " fclose failed" );
788+ out_file = nullptr ;
789+ }
790+
791+ unsigned char out_sha_bytes[CSHA256::OUTPUT_SIZE];
792+ out_sha.Finalize (out_sha_bytes);
793+ std::string out_sha_hex = HexStr (std::begin (out_sha_bytes), std::end (out_sha_bytes));
794+
795+ // If check below fails, should manually dump the results with:
796+ //
797+ // SETTINGS_MERGE_TEST_OUT=results.txt ./test_bitcoin --run_test=util_tests/util_SettingsMerge
798+ //
799+ // And verify diff against previous results to make sure the changes are expected.
800+ //
801+ // Results file is formatted like:
802+ //
803+ // <input> || <IsArgSet/IsArgNegated/GetArg output> | <GetArgs output> | <GetUnsuitable output>
804+ BOOST_CHECK_EQUAL (out_sha_hex, " 80964e17fbd3c5569d3c824d032e28e2d319ef57494735b0e76eb7aad9957f2c" );
805+ }
806+
578807BOOST_AUTO_TEST_CASE (util_FormatMoney)
579808{
580809 BOOST_CHECK_EQUAL (FormatMoney (0 ), " 0.00" );
0 commit comments