diff --git a/ORE-SWIG/QuantExt-SWIG/SWIG/qle_common.i b/ORE-SWIG/QuantExt-SWIG/SWIG/qle_common.i index 1ad9fe8f3f..68551d83a1 100644 --- a/ORE-SWIG/QuantExt-SWIG/SWIG/qle_common.i +++ b/ORE-SWIG/QuantExt-SWIG/SWIG/qle_common.i @@ -23,6 +23,18 @@ #include %} +// SWIG registers its smart-pointer machinery under the bare namespace `ext`: +// QuantLib-SWIG's common.i sets SWIG_SHARED_PTR_NAMESPACE to `ext`, and the C++ +// alias `namespace ext = QuantLib::ext` it relies on lives inside a %{ %} block +// that the SWIG parser never sees. A declaration spelled +// `QuantLib::ext::shared_ptr` is therefore registered as a type distinct from +// `ext::shared_ptr` and is wrapped as an opaque pointer instead of a proper +// proxy, so a value produced by one spelling cannot be passed to the other -- for +// example OREApp::getCube() to InputParameters::setCube(). Declaring the alias +// where SWIG can see it collapses both spellings onto the same wrapped type. +// See https://github.com/OpenSourceRisk/Engine/issues/351 +namespace QuantLib { namespace ext = ::ext; } + #if defined(SWIGPYTHON) %typemap(in) boost::optional %{ diff --git a/ORE-SWIG/test/test_cube_reuse.py b/ORE-SWIG/test/test_cube_reuse.py new file mode 100644 index 0000000000..f41dec35b9 --- /dev/null +++ b/ORE-SWIG/test/test_cube_reuse.py @@ -0,0 +1,59 @@ +"""Regression tests for https://github.com/OpenSourceRisk/Engine/issues/351. + +SWIG registers its smart-pointer machinery under the bare namespace ``ext``, so +a declaration spelled ``QuantLib::ext::shared_ptr`` used to be registered as +a type distinct from ``ext::shared_ptr`` and wrapped as an opaque pointer. +Setters written with the qualified spelling then rejected every value the +bindings could produce -- ``InputParameters.setCube`` could not be handed the +cube returned by ``OREApp.getCube``, which made in-memory cube reuse +unreachable from Python. +""" + +import unittest + +import ORE + + +class QualifiedSharedPtrSetterTest(unittest.TestCase): + """Setters declared with the qualified spelling must accept wrapped objects.""" + + #: Setters that take a qualified ``shared_ptr`` whose argument type can be + #: constructed directly from Python. + ENGINE_DATA_SETTERS = ( + "setXvaSensiPricingEngine", + "setParConversionPricingEngine", + "setParStressPricingEngine", + "setZeroToParShiftPricingEngine", + ) + + def test_engine_data_setters_accept_a_wrapped_engine_data(self) -> None: + """These rejected every EngineData the bindings could produce.""" + params = ORE.InputParameters() + for name in self.ENGINE_DATA_SETTERS: + if not hasattr(params, name): + continue # setter predates this release + with self.subTest(setter=name): + getattr(params, name)(ORE.EngineData()) + + def test_set_cube_accepts_a_wrapped_cube(self) -> None: + """The round-trip issue #351 is about: hand a live cube to setCube. + + Building the cube needs the in-memory cube classes to be concrete + (issue #354); where they are still abstract the round-trip cannot be + exercised and the test is skipped rather than failed. + + setMarketCube is fixed by the same change but stays untested here: + AggregationScenarioData has no constructible subclass in the bindings. + """ + asof = ORE.Date(2, ORE.March, 2026) + dates = [ORE.Date(2, ORE.March, 2027), ORE.Date(2, ORE.March, 2028)] + try: + cube = ORE.DoublePrecisionInMemoryCubeN(asof, {"trade1"}, dates, 4) + except AttributeError: + self.skipTest("in-memory cubes not constructible here (issue #354)") + params = ORE.InputParameters() + params.setCube(cube) + + +if __name__ == "__main__": + unittest.main()