diff --git a/docs/source/Support/bskReleaseNotes.rst b/docs/source/Support/bskReleaseNotes.rst index b80e8d37d1..c0a9c407ee 100644 --- a/docs/source/Support/bskReleaseNotes.rst +++ b/docs/source/Support/bskReleaseNotes.rst @@ -146,6 +146,8 @@ Version 2.6.0 (Feb. 21, 2025) - Download ``cspice`` using ``conan`` instead of providing custom libraries. This ensures all platforms are using the same version of ``cspice``. - Ensured that the ability to designate an external BSK folder still works with ``conan2`` +- Added a fix for the message subscription bug in which the message data was not retained after going out of scope. +- Added unit test for message subscription and message data retention after going out of scope to ``test_messaging.py``. Version 2.5.0 (Sept. 30, 2024) diff --git a/src/architecture/_GeneralModuleFiles/swig_c_wrap.i b/src/architecture/_GeneralModuleFiles/swig_c_wrap.i index 16aa13483f..b1ee0fca8d 100644 --- a/src/architecture/_GeneralModuleFiles/swig_c_wrap.i +++ b/src/architecture/_GeneralModuleFiles/swig_c_wrap.i @@ -23,6 +23,8 @@ #include #include #include + #include + #include %} %include @@ -63,6 +65,22 @@ class CWrapper : public SysModel { std::unique_ptr config; //!< class variable }; +class CModuleWrapper { +private: + std::vector> destructionCallbacks; + +public: + void addDestructionCallback(std::function callback) { + destructionCallbacks.push_back(callback); + } + + ~CModuleWrapper() { + for (auto& callback : destructionCallbacks) { + callback(); + } + } +}; + %} %define %c_wrap_3(moduleName, configName, functionSuffix) @@ -83,7 +101,7 @@ class CWrapper : public SysModel { in overload resolution than methods using the explicit type. This means that: - + Reset_hillPoint(hillPointConfig*, uint64_t, int64_t) { ... } will always be chosen before: @@ -103,7 +121,7 @@ class CWrapper : public SysModel { if (len(args)) > 0: args[0].thisown = False %} - + %include "moduleName.h" %template(moduleName) CWrapper; diff --git a/src/architecture/messaging/messaging.h b/src/architecture/messaging/messaging.h index 6f29a4a6c8..3b8fda5522 100644 --- a/src/architecture/messaging/messaging.h +++ b/src/architecture/messaging/messaging.h @@ -23,6 +23,7 @@ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF #include "architecture/utilities/bskLogging.h" #include #include +#include /*! forward-declare sim message for use by read functor */ template @@ -38,6 +39,8 @@ class ReadFunctor{ messageType* payloadPointer; //!< -- pointer to the incoming msg data MsgHeader *headerPointer; //!< -- pointer to the incoming msg header bool initialized; //!< -- flag indicating if the input message is connect to another message + std::function refIncCallback; + std::function refDecCallback; public: //!< -- BSK Logging @@ -46,10 +49,18 @@ class ReadFunctor{ //! constructor - ReadFunctor() : initialized(false) {}; + ReadFunctor() : + initialized(false), + refIncCallback([](){}), + refDecCallback([](){}) {} //! constructor - ReadFunctor(messageType* payloadPtr, MsgHeader *headerPtr) : payloadPointer(payloadPtr), headerPointer(headerPtr), initialized(true){}; + ReadFunctor(messageType* payloadPtr, MsgHeader *headerPtr) : + payloadPointer(payloadPtr), + headerPointer(headerPtr), + initialized(true), + refIncCallback([](){}), + refDecCallback([](){}) {} //! constructor const messageType& operator()(){ @@ -174,6 +185,42 @@ class ReadFunctor{ //! Recorder method description Recorder recorder(uint64_t timeDiff = 0){return Recorder(this, timeDiff);} + + /** + * @brief Set the reference counting callbacks for the source message + * @param incRef Function to increment source reference count + * @param decRef Function to decrement source reference count + */ + void setSourceRef(std::function incRef, std::function decRef) { + if (refDecCallback) { + refDecCallback(); + } + refIncCallback = incRef; + refDecCallback = decRef; + if (refIncCallback) { + refIncCallback(); + } + } + + /*! Check if this reader is subscribed to a specific message source + * @param source Pointer to message source to check + * @return 1 if subscribed, 0 if not + */ + uint8_t isSubscribedTo(const Message* source) const { + if (!source) return 0; + return (this->payloadPointer == source->getPayloadPtr() && + this->headerPointer == source->getHeaderPtr()); + } + + /*! Check if this reader is subscribed to a void pointer source + * @param source Void pointer to message source to check + * @return 1 if subscribed, 0 if not + */ + uint8_t isSubscribedTo(const void* source) const { + const Message* msgSource = + static_cast*>(source); + return isSubscribedTo(msgSource); + } }; /*! Write Functor */ @@ -232,6 +279,16 @@ class Message{ //! Return the memory size of the payload, be careful about dynamically sized things uint64_t getPayloadSize() {return sizeof(messageType);}; + + /*! Get pointer to message payload + * @return Const pointer to payload data + */ + const messageType* getPayloadPtr() const { return &payload; } + + /*! Get pointer to message header + * @return Const pointer to message header + */ + const MsgHeader* getHeaderPtr() const { return &header; } }; diff --git a/src/architecture/messaging/newMessaging.ih b/src/architecture/messaging/newMessaging.ih index b382c842ac..cd4fb4ceec 100644 --- a/src/architecture/messaging/newMessaging.ih +++ b/src/architecture/messaging/newMessaging.ih @@ -34,38 +34,50 @@ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. %template(messageType ## Reader) ReadFunctor; %extend ReadFunctor { - %pythoncode %{ - def subscribeTo(self, source): - if type(source) == messageType: - self.__subscribe_to(source) + void setPySourceRef(PyObject* source) { + if (source) { + self->setSourceRef( + [source]() { Py_INCREF(source); }, + [source]() { Py_DECREF(source); } + ); + } + } + + %pythoncode %{ + def subscribeTo(self, source): + if type(source) == messageType: + self.__subscribe_to(source) + self.setPySourceRef(source) + return + + try: + from Basilisk.architecture.messaging.messageType ## Payload import messageType ## _C + if type(source) == messageType ## _C: + self.__subscribe_to_C(source) + self.setPySourceRef(source) return - - try: - from Basilisk.architecture.messaging.messageType ## Payload import messageType ## _C - if type(source) == messageType ## _C: - self.__subscribe_to_C(source) - return - except ImportError: - pass - - raise Exception('tried to subscribe ReadFunctor to output message type' - + str(type(source))) - - - def isSubscribedTo(self, source): - if type(source) == messageType: - return self.__is_subscribed_to(source) - - try: - from Basilisk.architecture.messaging.messageType ## Payload import messageType ## _C - except ImportError: - return 0 - + except ImportError: + pass + + raise Exception('tried to subscribe ReadFunctor to output message type' + + str(type(source))) + %} + + %pythoncode %{ + def isSubscribedTo(self, source): + """Check if this reader is subscribed to the given source""" + if type(source) == messageType: + return self.__is_subscribed_to(source) + + try: + from Basilisk.architecture.messaging.messageType ## Payload import messageType ## _C if type(source) == messageType ## _C: return self.__is_subscribed_to_C(source) - else: - return 0 - %} + except ImportError: + pass + + return 0 + %} }; %template(messageType ## Writer) WriteFunctor; @@ -110,7 +122,7 @@ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. for el, k in zip(attr, range(len(attr))): self.explore_and_find_subattr(el, attr_name + "[" + str(k) + "]", content) else: - # The attribute is a list of common types + # The attribute is a list of common types content[attr_name] = attr elif "Basilisk" in str(type(attr)): # The attribute is a swigged BSK object diff --git a/src/tests/test_messaging.py b/src/tests/test_messaging.py index 1e83935e00..e7ad679b91 100644 --- a/src/tests/test_messaging.py +++ b/src/tests/test_messaging.py @@ -20,7 +20,7 @@ # # Integrated tests # -# Purpose: This script runs a series of test to ensure that the messaging +# Purpose: This script runs a series of test to ensure that the messaging # interface is properly set up for C and Cpp messages # Author: Benjamin Bercovici # Creation Date: June 4th, 2021 @@ -29,6 +29,13 @@ from Basilisk.architecture import bskLogging from Basilisk.architecture import messaging +from Basilisk.moduleTemplates import cppModuleTemplate +from Basilisk.utilities import SimulationBaseClass +from Basilisk.utilities import macros +from Basilisk.moduleTemplates import cModuleTemplate +import numpy as np +import gc +import weakref # uncomment this line is this test is to be skipped in the global unit test run, adjust message as needed @@ -47,6 +54,12 @@ def messaging_unit_tests(): test_c_msg_subscription_check() test_cpp_2_c_msg_subscription_check() test_c_2_cpp_msg_subscription_check() + test_standalone_message_scope() + test_standalone_message_multiple_readers() + test_standalone_message_cpp_reader_cleanup() + test_standalone_message_c_reader_cleanup() + test_standalone_message_mixed_readers_cleanup() + test_standalone_message_python_combinations() @@ -56,13 +69,13 @@ def test_cpp_msg_subscription_check(): bskLogging.setDefaultLogLevel(bskLogging.BSK_WARNING) testFailCount = 0 # zero unit test result counter testMessages = [] # create empty array to store test log messages - + # Try out all the existing cpp messages - cppMessages = [ el for el in dir(messaging) if el.endswith("Msg")] - + cppMessages = [ el for el in dir(messaging) if el.endswith("Msg")] + for el in cppMessages: - + # Create three messages msgA = eval("messaging." + el + "()") msgB = eval("messaging." + el + "()") @@ -86,8 +99,8 @@ def test_cpp_msg_subscription_check(): if msgC_subscriber.isSubscribedTo(msgA) != 0: testFailCount += 1 testMessages.append(el + ": msgC_subscriber.isSubscribedTo(msgA) should be False") - - + + # Change subscription pattern msgB_subscriber.subscribeTo(msgC) # Subscribe B to C msgC_subscriber.subscribeTo(msgA) # Subscribe C to A @@ -103,8 +116,8 @@ def test_cpp_msg_subscription_check(): testFailCount += 1 testMessages.append(el + ": msgC_subscriber.isSubscribedTo(msgB) should be False") - - + + if testFailCount == 0: print("PASSED") else: @@ -122,14 +135,14 @@ def test_c_msg_subscription_check(): bskLogging.setDefaultLogLevel(bskLogging.BSK_WARNING) testFailCount = 0 # zero unit test result counter testMessages = [] # create empty array to store test log messages - + # Try out all the existing c messages - cMessages = [ el for el in dir(messaging) if el.endswith("Msg_C")] + cMessages = [ el for el in dir(messaging) if el.endswith("Msg_C")] + - for el in cMessages: - + # Create three messages msgA = eval("messaging." + el + "()") msgB = eval("messaging." + el + "()") @@ -149,8 +162,8 @@ def test_c_msg_subscription_check(): if msgC.isSubscribedTo(msgA) != 0: testFailCount += 1 testMessages.append(el + ": msgC.isSubscribedTo(msgA) should be False") - - + + # Change subscription pattern msgB.subscribeTo(msgC) # Subscribe B to C msgC.subscribeTo(msgA) # Subscribe C to A @@ -166,8 +179,8 @@ def test_c_msg_subscription_check(): testFailCount += 1 testMessages.append(el + ": msgC.isSubscribedTo(msgB) should be False") - - + + if testFailCount == 0: print("PASSED") else: @@ -176,22 +189,22 @@ def test_c_msg_subscription_check(): def test_c_2_cpp_msg_subscription_check(): - + bskLogging.setDefaultLogLevel(bskLogging.BSK_WARNING) testFailCount = 0 # zero unit test result counter testMessages = [] # create empty array to store test log messages - + # Try out all the existing messages - cppMessages = [ el for el in dir(messaging) if el.endswith("Msg")] - cMessages = [ el for el in dir(messaging) if el.endswith("Msg_C")] - + cppMessages = [ el for el in dir(messaging) if el.endswith("Msg")] + cMessages = [ el for el in dir(messaging) if el.endswith("Msg_C")] + # Find common messages common_messages = [el for el in cppMessages if el + "_C" in cMessages] for el in common_messages: - + # Create c and cpp messages msgA = eval("messaging." + el + "()") msgB = eval("messaging." + el + "()") @@ -199,11 +212,11 @@ def test_c_2_cpp_msg_subscription_check(): msgC = eval("messaging." + el + "_C()") msgD = eval("messaging." + el + "_C()") - + # Subscribe msgC.subscribeTo(msgA) # Subscribe C to A msgD.subscribeTo(msgB) # Subscribe D to B - + # Check if msgC.isSubscribedTo(msgA) != 1: testFailCount += 1 @@ -217,7 +230,7 @@ def test_c_2_cpp_msg_subscription_check(): if msgC.isSubscribedTo(msgB) != 0: testFailCount += 1 testMessages.append(el + ": msgC.isSubscribedTo(msgB) should be False") - + # Change subscription pattern msgC.subscribeTo(msgB) # Subscribe C to B msgD.subscribeTo(msgA) # Subscribe D to A @@ -236,8 +249,8 @@ def test_c_2_cpp_msg_subscription_check(): testFailCount += 1 testMessages.append(el + ": msgD.isSubscribedTo(msgB) should be False") - - + + if testFailCount == 0: print("PASSED") else: @@ -249,18 +262,18 @@ def test_cpp_2_c_msg_subscription_check(): bskLogging.setDefaultLogLevel(bskLogging.BSK_WARNING) testFailCount = 0 # zero unit test result counter testMessages = [] # create empty array to store test log messages - + # Try out all the existing messages - cppMessages = [ el for el in dir(messaging) if el.endswith("Msg")] - cMessages = [ el for el in dir(messaging) if el.endswith("Msg_C")] - + cppMessages = [ el for el in dir(messaging) if el.endswith("Msg")] + cMessages = [ el for el in dir(messaging) if el.endswith("Msg_C")] + # Find common messages common_messages = [el for el in cppMessages if el + "_C" in cMessages] for el in common_messages: - + # Create c and cpp messages msgA = eval("messaging." + el + "_C()") msgB = eval("messaging." + el + "_C()") @@ -271,7 +284,7 @@ def test_cpp_2_c_msg_subscription_check(): # Create subscribers to pair messages msgC_subscriber = msgC.addSubscriber() msgD_subscriber = msgD.addSubscriber() - + # Subscribe msgC_subscriber.subscribeTo(msgA) # Subscribe C to A msgD_subscriber.subscribeTo(msgB) # Subscribe D to B @@ -289,7 +302,7 @@ def test_cpp_2_c_msg_subscription_check(): if msgC_subscriber.isSubscribedTo(msgB) != 0: testFailCount += 1 testMessages.append(el + ": msgC_subscriber.isSubscribedTo(msgB) should be False") - + # Change subscription pattern msgC_subscriber.subscribeTo(msgB) # Subscribe C to B msgD_subscriber.subscribeTo(msgA) # Subscribe D to A @@ -308,8 +321,8 @@ def test_cpp_2_c_msg_subscription_check(): testFailCount += 1 testMessages.append(el + ": msgD_subscriber.isSubscribedTo(msgB) should be False") - - + + if testFailCount == 0: print("PASSED") else: @@ -317,6 +330,335 @@ def test_cpp_2_c_msg_subscription_check(): assert(testFailCount == 0) +def test_standalone_message_scope(): + """ + Test that subscribed messages don't get garbage collected when going out of scope + """ + bskLogging.setDefaultLogLevel(bskLogging.BSK_WARNING) + testFailCount = 0 # zero unit test result counter + testMessages = [] # create empty array to store test log messages + + # Create a sim module as an empty container + scSim = SimulationBaseClass.SimBaseClass() + + # create the simulation process + dynProcess = scSim.CreateNewProcess("dynamicsProcess") + + # create the dynamics task and specify the integration update time + dynProcess.addTask(scSim.CreateNewTask("dynamicsTask", macros.sec2nano(1.))) + + # create modules + mod1 = cppModuleTemplate.CppModuleTemplate() + mod1.ModelTag = "cppModule1" + scSim.AddModelToTask("dynamicsTask", mod1) + + # setup message recording + msgRec = mod1.dataOutMsg.recorder() + scSim.AddModelToTask("dynamicsTask", msgRec) + + # Create a local scope to test message lifetime + def addLocalStandaloneMessage(module): + """Create a stand-alone message in local scope and subscribe to it""" + msgData = messaging.CModuleTemplateMsgPayload() + msgData.dataVector = [10., 20., 30.] + msg = messaging.CModuleTemplateMsg().write(msgData) + module.dataInMsg.subscribeTo(msg) + + # Subscribe to the input message in a local scope + addLocalStandaloneMessage(mod1) + + # initialize Simulation: + scSim.InitializeSimulation() + + # configure a simulation stop time and execute the simulation run + scSim.ConfigureStopTime(macros.sec2nano(3.0)) + scSim.ExecuteSimulation() + + # Verify output matches expected values + expected = np.array([ + [11., 20., 30.], + [12., 20., 30.], + [13., 20., 30.], + [14., 20., 30.] + ]) + + if not (msgRec.dataVector == expected).all(): + testFailCount += 1 + testMessages.append("Output data does not match expected values") + + if testFailCount == 0: + print("PASSED") + else: + [print(msg) for msg in testMessages] + assert testFailCount < 1, testMessages + + +def test_standalone_message_multiple_readers(): + """Test standalone message with multiple readers - verify message stays alive until all readers gone""" + bskLogging.setDefaultLogLevel(bskLogging.BSK_WARNING) + testFailCount = 0 + testMessages = [] + + # Create standalone message with data + msgData = messaging.CModuleTemplateMsgPayload() + msgData.dataVector = [1., 2., 3.] + standalone_msg = messaging.CModuleTemplateMsg().write(msgData) + + # Create multiple modules to act as readers + mod1 = cppModuleTemplate.CppModuleTemplate() + mod1.ModelTag = "cppModule1" + mod2 = cppModuleTemplate.CppModuleTemplate() + mod2.ModelTag = "cppModule2" + mod3 = cppModuleTemplate.CppModuleTemplate() + mod3.ModelTag = "cppModule3" + + # Subscribe all modules to standalone message + mod1.dataInMsg.subscribeTo(standalone_msg) + mod2.dataInMsg.subscribeTo(standalone_msg) + mod3.dataInMsg.subscribeTo(standalone_msg) + + # Verify all modules can read the message + if not np.array_equal(mod1.dataInMsg().dataVector, msgData.dataVector): + testFailCount += 1 + testMessages.append("mod1 failed to read standalone message") + if not np.array_equal(mod2.dataInMsg().dataVector, msgData.dataVector): + testFailCount += 1 + testMessages.append("mod2 failed to read standalone message") + if not np.array_equal(mod3.dataInMsg().dataVector, msgData.dataVector): + testFailCount += 1 + testMessages.append("mod3 failed to read standalone message") + + # Remove references one by one and verify remaining readers still work + del mod1 + gc.collect() + + if not np.array_equal(mod2.dataInMsg().dataVector, msgData.dataVector): + testFailCount += 1 + testMessages.append("mod2 failed after mod1 deletion") + if not np.array_equal(mod3.dataInMsg().dataVector, msgData.dataVector): + testFailCount += 1 + testMessages.append("mod3 failed after mod1 deletion") + + del mod2 + gc.collect() + + if not np.array_equal(mod3.dataInMsg().dataVector, msgData.dataVector): + testFailCount += 1 + testMessages.append("mod3 failed after mod2 deletion") + + # Final cleanup + del mod3 + gc.collect() + + if testFailCount == 0: + print("PASSED") + else: + [print(msg) for msg in testMessages] + assert testFailCount < 1, testMessages + +def test_standalone_message_cpp_reader_cleanup(): + """Test cleanup when both message and C++ reader go out of scope""" + bskLogging.setDefaultLogLevel(bskLogging.BSK_WARNING) + testFailCount = 0 + testMessages = [] + + def create_and_subscribe(): + # Create message in local scope + msgData = messaging.CModuleTemplateMsgPayload() + msgData.dataVector = [1., 2., 3.] + msg = messaging.CModuleTemplateMsg().write(msgData) + msg_ref = weakref.ref(msg) + + # Create reader in local scope + mod = cppModuleTemplate.CppModuleTemplate() + mod.ModelTag = "cppModule" + mod.dataInMsg.subscribeTo(msg) + + # Verify initial read + if not np.array_equal(mod.dataInMsg().dataVector, msgData.dataVector): + return False, None, None + return True, msg_ref, mod # Return the module too + + # Test in local scope + success, msg_ref, mod = create_and_subscribe() + if not success: + testFailCount += 1 + testMessages.append("Failed to read message in local scope") + + # Delete message first, then module + del msg_ref + gc.collect() + del mod + gc.collect() + + if testFailCount == 0: + print("PASSED") + else: + [print(msg) for msg in testMessages] + assert testFailCount < 1, testMessages + +def test_standalone_message_c_reader_cleanup(): + """Test cleanup when both message and C reader go out of scope""" + bskLogging.setDefaultLogLevel(bskLogging.BSK_WARNING) + testFailCount = 0 + testMessages = [] + + def create_and_subscribe(): + # Create message in local scope + msgData = messaging.CModuleTemplateMsgPayload() + msgData.dataVector = [1., 2., 3.] + msg = messaging.CModuleTemplateMsg_C().write(msgData) + + # Store weak reference to check cleanup + msg_ref = weakref.ref(msg) + + # Create C reader in local scope + mod = cModuleTemplate.cModuleTemplate() + mod.ModelTag = "cModule" + mod.dataInMsg.subscribeTo(msg) + + # Verify initial read + readData = mod.dataInMsg.read() + if not np.array_equal(readData.dataVector, msgData.dataVector): + return False, None + return True, msg_ref + + # Test in local scope + success, msg_ref = create_and_subscribe() + if not success: + testFailCount += 1 + testMessages.append("Failed to read message in local scope") + + # Force cleanup + gc.collect() + + # Verify message was actually cleaned up + if msg_ref() is not None: + testFailCount += 1 + testMessages.append("Message was not properly cleaned up") + + if testFailCount == 0: + print("PASSED") + else: + [print(msg) for msg in testMessages] + assert testFailCount < 1, testMessages + +def test_standalone_message_mixed_readers_cleanup(): + """Test cleanup with mix of C++ and C readers""" + bskLogging.setDefaultLogLevel(bskLogging.BSK_WARNING) + testFailCount = 0 + testMessages = [] + + def create_and_subscribe(): + # Create standalone message (using C-style message) + msgData = messaging.CModuleTemplateMsgPayload() + msgData.dataVector = [1., 2., 3.] + standalone_msg = messaging.CModuleTemplateMsg_C().write(msgData) + msg_ref = weakref.ref(standalone_msg) + + # Create mix of C++ and C readers + cpp_mod = cppModuleTemplate.CppModuleTemplate() + cpp_mod.ModelTag = "cppModule" + c_mod = cModuleTemplate.cModuleTemplate() + c_mod.ModelTag = "cModule" + + # Subscribe both types + cpp_mod.dataInMsg.subscribeTo(standalone_msg) + c_mod.dataInMsg.subscribeTo(standalone_msg) + gc.collect() + + # Verify both can read + if not np.array_equal(cpp_mod.dataInMsg().dataVector, msgData.dataVector): + return False, None, None, None, None + readData = c_mod.dataInMsg.read() + if not np.array_equal(readData.dataVector, msgData.dataVector): + return False, None, None, None, None + + return True, msg_ref, cpp_mod, c_mod, standalone_msg + + # Test in local scope + success, msg_ref, cpp_mod, c_mod, standalone_msg = create_and_subscribe() + if not success: + testFailCount += 1 + testMessages.append("Failed to read message in local scope") + + # Cleanup in correct order (following pattern from test_standalone_message_python_combinations) + del cpp_mod # Delete C++ module first + gc.collect() + del c_mod # Then delete C module + gc.collect() + del standalone_msg # Delete message explicitly + gc.collect() + del msg_ref # Finally delete reference + gc.collect() + + if testFailCount == 0: + print("PASSED") + else: + [print(msg) for msg in testMessages] + assert testFailCount < 1, testMessages + +def test_standalone_message_python_combinations(): + """Test combinations involving Python modules and standalone readers""" + bskLogging.setDefaultLogLevel(bskLogging.BSK_WARNING) + testFailCount = 0 + testMessages = [] + + # Test C++ message with Python reader + msgData = messaging.CModuleTemplateMsgPayload() + msgData.dataVector = [1., 2., 3.] + cpp_msg = messaging.CModuleTemplateMsg() + cpp_msg.write(msgData) + cpp_reader = cpp_msg.addSubscriber() + msg_ref = weakref.ref(cpp_msg) + gc.collect() + + # Verify C++ reader works + if not np.array_equal(cpp_reader().dataVector, msgData.dataVector): + testFailCount += 1 + testMessages.append("Python C++ reader failed initial read") + + # Keep message alive while reader is active + readData = cpp_reader() + if not np.array_equal(readData.dataVector, msgData.dataVector): + testFailCount += 1 + testMessages.append("Python C++ reader failed after message deletion") + + # Cleanup in correct order + del cpp_reader + gc.collect() + del cpp_msg + gc.collect() + + # Test C message with Python reader + c_msg = messaging.CModuleTemplateMsg_C() + c_msg.write(msgData) + c_reader = c_msg # C messages are their own readers + c_msg_ref = weakref.ref(c_msg) + gc.collect() + + # Keep C message alive while testing reader + readData = c_reader.read() + if not np.array_equal(readData.dataVector, msgData.dataVector): + testFailCount += 1 + testMessages.append("Python C reader failed initial read") + + # Cleanup C message (no need to delete cpp_reader again) + del c_msg + del c_reader + gc.collect() + + # Verify C message cleanup + if c_msg_ref() is not None: + testFailCount += 1 + testMessages.append("C message was not properly cleaned up") + + if testFailCount == 0: + print("PASSED") + else: + [print(msg) for msg in testMessages] + assert testFailCount < 1, testMessages + if __name__ == "__main__": messaging_unit_tests()