Skip to content

Commit 6698ba1

Browse files
committed
[RF][HS3] Avoid code duplication in testRooFitHS3 with helper functions
1 parent 168a442 commit 6698ba1

1 file changed

Lines changed: 101 additions & 149 deletions

File tree

roofit/hs3/test/testRooFitHS3.cxx

Lines changed: 101 additions & 149 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,9 @@
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

14811559
struct 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

15591622
INSTANTIATE_TEST_SUITE_P(
@@ -1615,132 +1678,21 @@ INSTANTIATE_TEST_SUITE_P(
16151678

16161679
TEST(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

16811690
TEST(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

17461698
TEST(RooFitHS3, HistFactoryConstraintKeyMigration)

0 commit comments

Comments
 (0)