@@ -610,3 +610,117 @@ TEST_F(ConfigAggregatorTest, override_wins_for_shared_key_after_base_update)
610610 load_all ({{std::ref (updated_base), " base.conf" },
611611 {std::ref (override_again), " base.conf.d/99-override.conf" }});
612612}
613+
614+ // Invalid scalar in the second file causes the handler to not be called. So
615+ // the aggregator is only notified of the final value in the first file.
616+ TEST_F (ConfigAggregatorTest, invalid_scalar_value_in_one_of_multiple_yields_last_valid_value_1)
617+ {
618+ aggregator.add_int_attribute (a_key, " a scoped int" , [this ](auto ... args) { int_handler (args...); });
619+
620+ // stream1 has a valid value; stream2 (higher priority) has an unparseable value
621+ std::istringstream stream1{a_key.to_string () + " =10\n " };
622+ std::istringstream stream2{a_key.to_string () + " =not_a_number\n " };
623+
624+ // The last (highest-priority) file wins for scalars, and it holds an invalid value → nullopt
625+ EXPECT_CALL (*this , int_handler (a_key, Optional (10 )));
626+
627+ load_all ({{std::ref (stream1), " base.conf" },
628+ {std::ref (stream2), " base.conf.d/10-override.conf" }});
629+ }
630+
631+ // Same as the previous case, except that the 30 is overriden by an invalid
632+ // value. Handlers are still not called.
633+ TEST_F (ConfigAggregatorTest, invalid_scalar_value_in_one_of_multiple_yields_last_valid_value_2)
634+ {
635+ aggregator.add_int_attribute (a_key, " a scoped int" , [this ](auto ... args) { int_handler (args...); });
636+
637+ std::istringstream stream1{a_key.to_string () + " =10\n " + a_key.to_string () + " =20\n " };
638+ std::istringstream stream2{a_key.to_string () + " =30\n " + a_key.to_string () + " =not_a_number\n " };
639+
640+ EXPECT_CALL (*this , int_handler (a_key, Optional (20 )));
641+
642+ load_all ({{std::ref (stream1), " base.conf" },
643+ {std::ref (stream2), " base.conf.d/10-override.conf" }});
644+ }
645+
646+ // Since the invalid value in the second file is overridden by a valid value
647+ // after, its handlers are called and the config aggregator is notified of its
648+ // value (40)
649+ TEST_F (ConfigAggregatorTest, invalid_scalar_value_followed_by_valid_value_in_later_file_yields_valid)
650+ {
651+ aggregator.add_int_attribute (a_key, " a scoped int" , [this ](auto ... args) { int_handler (args...); });
652+
653+ std::istringstream stream1{a_key.to_string () + " =10\n " + a_key.to_string () + " =20\n " };
654+ std::istringstream stream2{a_key.to_string () + " =30\n " + a_key.to_string () + " =also_not_a_number\n " + a_key.to_string () + " =40\n " };
655+
656+ // The last valid assignment in the highest-priority file wins for scalars
657+ EXPECT_CALL (*this , int_handler (a_key, Optional (40 )));
658+
659+ load_all ({{std::ref (stream1), " base.conf" },
660+ {std::ref (stream2), " base.conf.d/10-override.conf" }});
661+ }
662+
663+ TEST_F (ConfigAggregatorTest, invalid_array_value_in_one_of_multiple_files_is_ignored)
664+ {
665+ aggregator.add_ints_attribute (an_ints_key, " ints" , [this ](auto ... args) { ints_handler (args...); });
666+
667+ // stream1 contributes valid entries; stream2 contributes one valid and one invalid entry
668+ std::istringstream stream1{an_ints_key.to_string () + " =1\n " + an_ints_key.to_string () + " =2\n " };
669+ std::istringstream stream2{an_ints_key.to_string () + " =3\n " + an_ints_key.to_string () + " =not_a_number\n " };
670+
671+ EXPECT_CALL (*this , ints_handler (an_ints_key, Optional (ElementsAre (1 , 2 , 3 ))));
672+
673+ load_all ({{std::ref (stream1), " base.conf" },
674+ {std::ref (stream2), " base.conf.d/10-override.conf" }});
675+ }
676+
677+ TEST_F (ConfigAggregatorTest, invalid_ini_file_among_multiple_files_leaves_scalar_as_nullopt)
678+ {
679+ aggregator.add_int_attribute (a_key, " a scoped int" , [this ](auto ... args) { int_handler (args...); });
680+
681+ // stream1 is a valid ini file; stream2 is JSON (not valid ini)
682+ std::istringstream stream1{a_key.to_string () + " =42\n " };
683+ std::istringstream stream2{R"( {"key": "value"})" };
684+
685+ // JSON content cannot be parsed as ini, so only stream1's value is available.
686+ // Because stream2 is the higher-priority source but provides nothing parseable for a_key,
687+ // the scalar from stream1 should be used (no override occurred).
688+ EXPECT_CALL (*this , int_handler (a_key, Optional (42 )));
689+
690+ load_all ({{std::ref (stream1), " base.conf" },
691+ {std::ref (stream2), " base.conf.d/10-override.conf" }});
692+ }
693+
694+ TEST_F (ConfigAggregatorTest, invalid_ini_file_among_multiple_files_leaves_array_values_from_valid_files)
695+ {
696+ aggregator.add_strings_attribute (a_strings_key, " strings" , [this ](auto ... args) { strings_handler (args...); });
697+
698+ // stream1 is a valid ini file; stream2 is JSON (not valid ini)
699+ std::istringstream stream1{a_strings_key.to_string () + " =foo\n " + a_strings_key.to_string () + " =bar\n " };
700+ std::istringstream stream2{R"( {"strings": ["baz"]})" };
701+
702+ // JSON content cannot be parsed as ini; array entries from stream1 remain intact
703+ EXPECT_CALL (*this , strings_handler (a_strings_key, Optional (ElementsAre (" foo" , " bar" ))));
704+
705+ load_all ({{std::ref (stream1), " base.conf" },
706+ {std::ref (stream2), " base.conf.d/10-override.conf" }});
707+ }
708+
709+ TEST_F (ConfigAggregatorTest, clear_followed_by_invalid_array_value_yields_nullopt)
710+ {
711+ aggregator.add_ints_attribute (an_ints_key, " ints" , [this ](auto ... args) { ints_handler (args...); });
712+
713+ // stream1 provides initial values; stream2 first clears the array (empty assignment)
714+ // then tries to append an invalid value
715+ std::istringstream stream1{an_ints_key.to_string () + " =1\n " + an_ints_key.to_string () + " =2\n " };
716+ std::istringstream stream2{
717+ an_ints_key.to_string () + " =\n " + // clears entries from stream1
718+ an_ints_key.to_string () + " =bad_val\n " // invalid integer after the clear
719+ };
720+
721+ // After clearing and then encountering an invalid value, the result is nullopt
722+ EXPECT_CALL (*this , ints_handler (an_ints_key, Optional (IsEmpty ())));
723+
724+ load_all ({{std::ref (stream1), " base.conf" },
725+ {std::ref (stream2), " base.conf.d/10-override.conf" }});
726+ }
0 commit comments