3838#include < RooWorkspace.h>
3939
4040#include < cmath>
41+ #include < memory>
42+ #include < string_view>
43+ #include < vector>
4144
4245#include < TROOT.h>
4346
@@ -190,6 +193,90 @@ class ScopedNoDomainConstVarImportFlag {
190193 bool _oldValue;
191194};
192195
196+ // Runs `action` and returns everything that was logged at the given message level and topic while it ran.
197+ template <class Action >
198+ std::string captureMessages (RooFit::MsgLevel level, RooFit::MsgTopic topic, Action &&action)
199+ {
200+ RooHelpers::HijackMessageStream stream{level, topic};
201+ action ();
202+ return stream.str ();
203+ }
204+
205+ // Counts the number of (non-overlapping) occurrences of `needle` in `haystack`.
206+ std::size_t countOccurrences (std::string_view haystack, std::string_view needle)
207+ {
208+ std::size_t result = 0 ;
209+ for (std::size_t pos = 0 ; (pos = haystack.find (needle, pos)) != std::string_view::npos; pos += needle.size ()) {
210+ ++result;
211+ }
212+ return result;
213+ }
214+
215+ // Asserts that exporting `ws` to HS3 throws and logs an error message containing `expectedReason`.
216+ void expectExportThrowsWithError (RooWorkspace &ws, std::string const &expectedReason)
217+ {
218+ bool threw = false ;
219+ std::string exported;
220+ const std::string errors = captureMessages (RooFit::ERROR , RooFit::IO , [&] {
221+ try {
222+ exported = RooJSONFactoryWSTool{ws}.exportJSONtoString ();
223+ } catch (const std::runtime_error &) {
224+ threw = true ;
225+ }
226+ });
227+
228+ EXPECT_TRUE (threw);
229+ EXPECT_TRUE (exported.empty ()) << " A HistFactory object was returned despite incompatible duplicate modifiers" ;
230+ EXPECT_NE (errors.find (expectedReason), std::string::npos) << errors;
231+ }
232+
233+ // Builds a single-bin-observable RooDataHist filled with the given bin contents. Returns an owning pointer so that it
234+ // can be handed to the RooHistFunc constructor that takes ownership (the const-reference overload keeps only an unowned
235+ // pointer, which would dangle for a temporary).
236+ std::unique_ptr<RooDataHist> makeDataHist (std::string const &name, RooRealVar &obs, std::vector<double > const &contents)
237+ {
238+ auto dh = std::make_unique<RooDataHist>(name.c_str (), name.c_str (), RooArgList{obs});
239+ for (std::size_t i = 0 ; i < contents.size (); ++i) {
240+ dh->set (i, contents[i], -1.0 );
241+ }
242+ return dh;
243+ }
244+
245+ // Imports a "model_channel0" RooRealSumPdf with a single sample whose shape carries two histosys variations sharing the
246+ // same parameter (i.e. a duplicate histosys). The first variation always lives on a 2-bin observable "x"; the second
247+ // variation lives on `obs2` if given, or on the same "x" otherwise. Used to check that incompatible duplicates are
248+ // rejected on export.
249+ void importDuplicateHistoSysModel (RooWorkspace &ws, int interpCode, std::vector<double > const &low2Contents,
250+ std::vector<double > const &high2Contents, RooRealVar *obs2 = nullptr )
251+ {
252+ RooRealVar x{" x" , " x" , 0.0 , 2.0 };
253+ x.setBins (2 );
254+ RooRealVar &var2Obs = obs2 ? *obs2 : x;
255+
256+ RooHistFunc nominal{" nominal" , " nominal" , RooArgSet{x}, makeDataHist (" nominalData" , x, {10.0 , 20.0 })};
257+ RooHistFunc low1{" low1" , " low1" , RooArgSet{x}, makeDataHist (" lowData1" , x, {8.0 , 18.0 })};
258+ RooHistFunc high1{" high1" , " high1" , RooArgSet{x}, makeDataHist (" highData1" , x, {12.0 , 22.0 })};
259+ RooHistFunc low2{" low2" , " low2" , RooArgSet{var2Obs}, makeDataHist (" lowData2" , var2Obs, low2Contents)};
260+ RooHistFunc high2{" high2" , " high2" , RooArgSet{var2Obs}, makeDataHist (" highData2" , var2Obs, high2Contents)};
261+
262+ RooRealVar alphaShape{" alpha_shape" , " alpha_shape" , 0.0 , -5.0 , 5.0 };
263+ alphaShape.setConstant (true );
264+ RooArgList parameters;
265+ parameters.add (alphaShape);
266+ parameters.add (alphaShape);
267+ RooArgList lowVariations{low1, low2};
268+ RooArgList highVariations{high1, high2};
269+ PiecewiseInterpolation interpolation{" duplicate_shape" , " duplicate_shape" , nominal,
270+ lowVariations, highVariations, parameters};
271+ interpolation.setAllInterpCodes (interpCode);
272+
273+ RooProduct sampleShapes{" sample_shapes" , " sample_shapes" , RooArgList{interpolation}};
274+ RooConstVar sampleScale{" sample_scale" , " sample_scale" , 1.0 };
275+ RooRealSumPdf model{" model_channel0" , " model_channel0" , RooArgList{sampleShapes}, RooArgList{sampleScale}, true };
276+
277+ ws.import (model, RooFit::Silence ());
278+ }
279+
193280} // namespace
194281
195282// Test that the IO of attributes and string attributes works.
@@ -1383,12 +1470,9 @@ TEST(RooFitHS3, HistFactoryDuplicateModifiersAreCombined)
13831470 ASSERT_TRUE (RooJSONFactoryWSTool{ws1}.importJSONfromString (jsonStr));
13841471
13851472 std::string exported;
1386- std::string warnings;
1387- {
1388- RooHelpers::HijackMessageStream warningMessages{RooFit::WARNING , RooFit::IO };
1473+ const std::string warnings = captureMessages (RooFit::WARNING , RooFit::IO , [&] {
13891474 exported = RooJSONFactoryWSTool{ws1}.exportJSONtoString ();
1390- warnings = warningMessages.str ();
1391- }
1475+ });
13921476
13931477 EXPECT_NE (warnings.find (" combined 2 duplicate modifiers named 'norm' of type 'normsys'" ), std::string::npos)
13941478 << warnings;
@@ -1397,16 +1481,9 @@ TEST(RooFitHS3, HistFactoryDuplicateModifiersAreCombined)
13971481 EXPECT_NE (warnings.find (" combined 2 duplicate modifiers named 'shape' of type 'histosys'" ), std::string::npos)
13981482 << warnings;
13991483
1400- auto count = [&](std::string_view text) {
1401- std::size_t result = 0 ;
1402- for (std::size_t pos = 0 ; (pos = exported.find (text, pos)) != std::string::npos; pos += text.size ()) {
1403- ++result;
1404- }
1405- return result;
1406- };
1407- EXPECT_EQ (count (" \" name\" :\" norm\" " ), 1u ) << exported;
1408- EXPECT_EQ (count (" \" name\" :\" poly\" " ), 1u ) << exported;
1409- EXPECT_EQ (count (" \" name\" :\" shape\" " ), 1u ) << exported;
1484+ EXPECT_EQ (countOccurrences (exported, " \" name\" :\" norm\" " ), 1u ) << exported;
1485+ EXPECT_EQ (countOccurrences (exported, " \" name\" :\" poly\" " ), 1u ) << exported;
1486+ EXPECT_EQ (countOccurrences (exported, " \" name\" :\" shape\" " ), 1u ) << exported;
14101487
14111488 RooWorkspace ws2{" ws_duplicate_modifiers_roundtrip" };
14121489 ASSERT_TRUE (RooJSONFactoryWSTool{ws2}.importJSONfromString (exported));
@@ -1473,9 +1550,10 @@ TEST(RooFitHS3, HistFactoryDuplicateModifiersAreCombined)
14731550 }
14741551
14751552 // The operation must be logged as a warning, not as an error.
1476- RooHelpers::HijackMessageStream errorMessages{RooFit::ERROR , RooFit::IO };
1477- RooJSONFactoryWSTool::warning (" duplicate-modifier warning-level sentinel" );
1478- EXPECT_TRUE (errorMessages.str ().empty ()) << errorMessages.str ();
1553+ const std::string sentinelErrors = captureMessages (RooFit::ERROR , RooFit::IO , [] {
1554+ RooJSONFactoryWSTool::warning (" duplicate-modifier warning-level sentinel" );
1555+ });
1556+ EXPECT_TRUE (sentinelErrors.empty ()) << sentinelErrors;
14791557}
14801558
14811559struct IncompatibleModifierCase {
@@ -1538,22 +1616,7 @@ TEST_P(HistFactoryIncompatibleDuplicateModifiers, ExportFails)
15381616 RooWorkspace ws{" ws_incompatible_duplicate_modifiers" };
15391617 ASSERT_TRUE (RooJSONFactoryWSTool{ws}.importJSONfromString (makeIncompatibleModifierWorkspace (testCase)));
15401618
1541- bool threw = false ;
1542- std::string exported;
1543- std::string errors;
1544- {
1545- RooHelpers::HijackMessageStream errorMessages{RooFit::ERROR , RooFit::IO };
1546- try {
1547- exported = RooJSONFactoryWSTool{ws}.exportJSONtoString ();
1548- } catch (const std::runtime_error &) {
1549- threw = true ;
1550- }
1551- errors = errorMessages.str ();
1552- }
1553-
1554- EXPECT_TRUE (threw);
1555- EXPECT_TRUE (exported.empty ()) << " A HistFactory object was returned despite incompatible duplicate modifiers" ;
1556- EXPECT_NE (errors.find (testCase.expectedReason ), std::string::npos) << errors;
1619+ expectExportThrowsWithError (ws, testCase.expectedReason );
15571620}
15581621
15591622INSTANTIATE_TEST_SUITE_P (
@@ -1615,132 +1678,21 @@ INSTANTIATE_TEST_SUITE_P(
16151678
16161679TEST (RooFitHS3, HistFactoryDuplicateHistoSysWithDifferentBinningFails)
16171680{
1618- RooRealVar x{" x" , " x" , 0.0 , 2.0 };
1619- x.setBins (2 );
16201681 RooRealVar y{" y" , " y" , 0.0 , 3.0 };
16211682 y.setBins (3 );
16221683
1623- RooDataHist nominalData{" nominalData" , " nominalData" , RooArgList{x}};
1624- nominalData.set (0 , 10.0 );
1625- nominalData.set (1 , 20.0 );
1626- RooDataHist lowData1{" lowData1" , " lowData1" , RooArgList{x}};
1627- lowData1.set (0 , 8.0 );
1628- lowData1.set (1 , 18.0 );
1629- RooDataHist highData1{" highData1" , " highData1" , RooArgList{x}};
1630- highData1.set (0 , 12.0 );
1631- highData1.set (1 , 22.0 );
1632- RooDataHist lowData2{" lowData2" , " lowData2" , RooArgList{y}};
1633- RooDataHist highData2{" highData2" , " highData2" , RooArgList{y}};
1634- for (int i = 0 ; i < 3 ; ++i) {
1635- lowData2.set (i, 7.0 + i);
1636- highData2.set (i, 13.0 + i);
1637- }
1638-
1639- RooHistFunc nominal{" nominal" , " nominal" , RooArgSet{x}, nominalData};
1640- RooHistFunc low1{" low1" , " low1" , RooArgSet{x}, lowData1};
1641- RooHistFunc high1{" high1" , " high1" , RooArgSet{x}, highData1};
1642- RooHistFunc low2{" low2" , " low2" , RooArgSet{y}, lowData2};
1643- RooHistFunc high2{" high2" , " high2" , RooArgSet{y}, highData2};
1644-
1645- RooRealVar alphaShape{" alpha_shape" , " alpha_shape" , 0.0 , -5.0 , 5.0 };
1646- alphaShape.setConstant (true );
1647- RooArgList parameters;
1648- parameters.add (alphaShape);
1649- parameters.add (alphaShape);
1650- RooArgList lowVariations{low1, low2};
1651- RooArgList highVariations{high1, high2};
1652- PiecewiseInterpolation interpolation{" duplicate_shape" , " duplicate_shape" , nominal, lowVariations,
1653- highVariations, parameters};
1654- interpolation.setAllInterpCodes (4 );
1655-
1656- RooProduct sampleShapes{" sample_shapes" , " sample_shapes" , RooArgList{interpolation}};
1657- RooConstVar sampleScale{" sample_scale" , " sample_scale" , 1.0 };
1658- RooRealSumPdf model{" model_channel0" , " model_channel0" , RooArgList{sampleShapes}, RooArgList{sampleScale}, true };
1659-
16601684 RooWorkspace ws{" ws_incompatible_histosys_binning" };
1661- ws. import (model, RooFit::Silence () );
1685+ importDuplicateHistoSysModel (ws, /* interpCode= */ 4 , /* low2= */ { 7.0 , 8.0 , 9.0 }, /* high2= */ { 13.0 , 14.0 , 15.0 }, &y );
16621686
1663- bool threw = false ;
1664- std::string exported;
1665- std::string errors;
1666- {
1667- RooHelpers::HijackMessageStream errorMessages{RooFit::ERROR , RooFit::IO };
1668- try {
1669- exported = RooJSONFactoryWSTool{ws}.exportJSONtoString ();
1670- } catch (const std::runtime_error &) {
1671- threw = true ;
1672- }
1673- errors = errorMessages.str ();
1674- }
1675-
1676- EXPECT_TRUE (threw);
1677- EXPECT_TRUE (exported.empty ());
1678- EXPECT_NE (errors.find (" histogram binning differs" ), std::string::npos) << errors;
1687+ expectExportThrowsWithError (ws, " histogram binning differs" );
16791688}
16801689
16811690TEST (RooFitHS3, HistFactoryDuplicateHistoSysWithNonDefaultInterpolationFails)
16821691{
1683- RooRealVar x{" x" , " x" , 0.0 , 2.0 };
1684- x.setBins (2 );
1685-
1686- RooDataHist nominalData{" nominalData" , " nominalData" , RooArgList{x}};
1687- nominalData.set (0 , 10.0 );
1688- nominalData.set (1 , 20.0 );
1689- RooDataHist lowData1{" lowData1" , " lowData1" , RooArgList{x}};
1690- lowData1.set (0 , 8.0 );
1691- lowData1.set (1 , 18.0 );
1692- RooDataHist highData1{" highData1" , " highData1" , RooArgList{x}};
1693- highData1.set (0 , 12.0 );
1694- highData1.set (1 , 22.0 );
1695- RooDataHist lowData2{" lowData2" , " lowData2" , RooArgList{x}};
1696- lowData2.set (0 , 9.0 );
1697- lowData2.set (1 , 16.0 );
1698- RooDataHist highData2{" highData2" , " highData2" , RooArgList{x}};
1699- highData2.set (0 , 11.0 );
1700- highData2.set (1 , 24.0 );
1701-
1702- RooHistFunc nominal{" nominal" , " nominal" , RooArgSet{x}, nominalData};
1703- RooHistFunc low1{" low1" , " low1" , RooArgSet{x}, lowData1};
1704- RooHistFunc high1{" high1" , " high1" , RooArgSet{x}, highData1};
1705- RooHistFunc low2{" low2" , " low2" , RooArgSet{x}, lowData2};
1706- RooHistFunc high2{" high2" , " high2" , RooArgSet{x}, highData2};
1707-
1708- RooRealVar alphaShape{" alpha_shape" , " alpha_shape" , 0.0 , -5.0 , 5.0 };
1709- alphaShape.setConstant (true );
1710- RooArgList parameters;
1711- parameters.add (alphaShape);
1712- parameters.add (alphaShape);
1713- RooArgList lowVariations{low1, low2};
1714- RooArgList highVariations{high1, high2};
1715- PiecewiseInterpolation interpolation{" duplicate_shape" , " duplicate_shape" , nominal,
1716- lowVariations, highVariations, parameters};
1717- interpolation.setAllInterpCodes (2 );
1718-
1719- RooProduct sampleShapes{" sample_shapes" , " sample_shapes" , RooArgList{interpolation}};
1720- RooConstVar sampleScale{" sample_scale" , " sample_scale" , 1.0 };
1721- RooRealSumPdf model{" model_channel0" , " model_channel0" , RooArgList{sampleShapes}, RooArgList{sampleScale}, true };
1722-
17231692 RooWorkspace ws{" ws_incompatible_histosys_interpolation" };
1724- ws. import (model, RooFit::Silence () );
1693+ importDuplicateHistoSysModel (ws, /* interpCode= */ 2 , /* low2= */ { 9.0 , 16.0 }, /* high2= */ { 11.0 , 24.0 } );
17251694
1726- bool threw = false ;
1727- std::string exported;
1728- std::string errors;
1729- {
1730- RooHelpers::HijackMessageStream errorMessages{RooFit::ERROR , RooFit::IO };
1731- try {
1732- exported = RooJSONFactoryWSTool{ws}.exportJSONtoString ();
1733- } catch (const std::runtime_error &) {
1734- threw = true ;
1735- }
1736- errors = errorMessages.str ();
1737- }
1738-
1739- EXPECT_TRUE (threw);
1740- EXPECT_TRUE (exported.empty ());
1741- EXPECT_NE (errors.find (" non-default interpolation cannot currently be represented by the HS3 exporter" ),
1742- std::string::npos)
1743- << errors;
1695+ expectExportThrowsWithError (ws, " non-default interpolation cannot currently be represented by the HS3 exporter" );
17441696}
17451697
17461698TEST (RooFitHS3, HistFactoryConstraintKeyMigration)
0 commit comments