From e4effb6d670aeb2a02e174691aef36ca821c530c Mon Sep 17 00:00:00 2001
From: matveyev <yury.matveev@desy.de>
Date: Mon, 11 Aug 2025 16:56:28 +0200
Subject: [PATCH 2/2] Catch2 tests: add "Event re-subscribes with the same IDL"
 test

In test we check, that after server restart EventConsumerKeepAliveThread re-subscribe all events to the same maximum supported by client IDL

Test does analysis of debug printouts, due to I have not found any other working solution to check, which IDL was requested
---
 tests/CMakeLists.txt                |   1 +
 tests/catch2_event_reconnection.cpp | 177 ++++++++++++++++++++++++++++
 2 files changed, 178 insertions(+)
 create mode 100644 tests/catch2_event_reconnection.cpp

diff --git a/lib/cpp/tests/CMakeLists.txt b/lib/cpp/tests/CMakeLists.txt
index cf60db7f5..d40ef041b 100644
--- a/lib/cpp/tests/CMakeLists.txt
+++ b/lib/cpp/tests/CMakeLists.txt
@@ -312,6 +312,7 @@ tango_catch2_tests_create(
     catch2_attr_polling.cpp
     catch2_cmd_polling.cpp
     catch2_connection.cpp
+    catch2_event_reconnection.cpp
     catch2_test_dtypes.cpp
     catch2_dev_state.cpp
     catch2_internal_utils.cpp
diff --git a/lib/cpp/tests/catch2_event_reconnection.cpp b/lib/cpp/tests/catch2_event_reconnection.cpp
new file mode 100644
index 000000000..d80064ff9
--- /dev/null
+++ b/lib/cpp/tests/catch2_event_reconnection.cpp
@@ -0,0 +1,177 @@
+#include "catch2_common.h"
+#include <regex>
+
+#include <tango/internal/utils.h>
+
+namespace
+{
+const std::string TestExceptReason = "Ahhh!";
+const Tango::DevShort k_inital_short = 5678;
+using CallbackMockType = TangoTest::CallbackMock<Tango::EventData>;
+
+} // anonymous namespace
+
+template <class Base>
+class SimpleEventDevice : public Base
+{
+  public:
+    using Base::Base;
+
+    void init_device() override { }
+
+    void push_change_event(Tango::DevString attr)
+    {
+        if(!strcmp(attr, "Short_attr"))
+        {
+            Base::push_change_event(attr, &short_value);
+        }
+        else
+        {
+            TANGO_THROW_EXCEPTION(TestExceptReason, "This is a test");
+        }
+    }
+
+    void read_attribute(Tango::Attribute &att)
+    {
+        if(att.get_name() == "Short_attr")
+        {
+            att.set_value(&short_value);
+        }
+        else
+        {
+            TANGO_THROW_EXCEPTION(TestExceptReason, "This is a test");
+        }
+    }
+
+    static void attribute_factory(std::vector<Tango::Attr *> &attrs)
+    {
+        auto short_attr = new TangoTest::AutoAttr<&SimpleEventDevice::read_attribute>("Short_attr", Tango::DEV_SHORT);
+        short_attr->set_change_event(true, false);
+        attrs.push_back(short_attr);
+    }
+
+    static void command_factory(std::vector<Tango::Command *> &cmds)
+    {
+        cmds.push_back(new TangoTest::AutoCommand<&SimpleEventDevice::push_change_event>("PushChangeEvent"));
+    }
+
+  private:
+    Tango::DevShort short_value{k_inital_short};
+};
+
+/* Returns true if all occurrences of
+ * Attribute::set_client_lib(N,change)
+ * in 'input' use the same N
+ */
+bool checkSameClientLib(const std::string &input)
+{
+    std::regex re(R"(Attribute::set_client_lib\(([0-9]+),change\))");
+    std::smatch match;
+    std::string::const_iterator it = input.cbegin();
+
+    bool seenFirst = false;
+    int firstValue = Tango::detail::INVALID_IDL_VERSION;
+
+    while(std::regex_search(it, input.cend(), match, re))
+    {
+        int val = parse_as<int>(match[1].str());
+
+        if(!seenFirst)
+        {
+            firstValue = val;
+            seenFirst = true;
+        }
+        else if(val != firstValue)
+        {
+            return false;
+        }
+
+        it = match.suffix().first;
+    }
+
+    if(!seenFirst)
+    {
+        return false;
+    }
+
+    return true;
+}
+
+TANGO_TEST_AUTO_DEV_TMPL_INSTANTIATE(SimpleEventDevice, 4)
+
+SCENARIO("Event re-subscribes with the same IDL", "[slow]")
+{
+    const std::string log_path = TangoTest::get_current_log_file_path();
+
+    // the previous full contents across GENERATE
+    static std::string prev_contents;
+
+    int idlver = GENERATE(TangoTest::idlversion(4));
+    GIVEN("a device proxy to a simple IDLv" << idlver << " device")
+    {
+        TangoTest::Context ctx{"event_reconnection", "SimpleEventDevice", idlver};
+        std::shared_ptr<Tango::DeviceProxy> device = ctx.get_proxy();
+
+        REQUIRE(idlver == device->get_idl_version());
+
+        AND_GIVEN("an change event subscription to that attribute")
+        {
+            TangoTest::CallbackMock<Tango::EventData> callback;
+            auto event_id = device->subscribe_event("Short_attr", Tango::CHANGE_EVENT, &callback);
+
+            auto maybe_event = callback.pop_next_event();
+            REQUIRE(maybe_event != std::nullopt);
+
+            WHEN("when we restart server")
+            {
+                ctx.stop_server();
+                ctx.restart_server();
+
+                WHEN("an change event is generated after another error event")
+                {
+                    using namespace Catch::Matchers;
+
+                    auto maybe_event = callback.pop_next_event(std::chrono::seconds{20});
+
+                    REQUIRE(maybe_event != std::nullopt);
+                    REQUIRE(maybe_event->err);
+                    REQUIRE(std::string(Tango::API_EventTimeout) == maybe_event->errors[0].reason.in());
+
+                    maybe_event = callback.pop_next_event(std::chrono::seconds{20});
+
+                    REQUIRE(maybe_event != std::nullopt);
+                    REQUIRE(maybe_event->event == Tango::EventName[Tango::CHANGE_EVENT]);
+
+                    THEN("an change event is generated after another error event")
+                    {
+                        std::string all_server_log = load_file(log_path);
+
+                        std::string new_chunk;
+                        if(all_server_log.size() > prev_contents.size())
+                        {
+                            new_chunk = all_server_log.substr(prev_contents.size());
+                        }
+                        else
+                        {
+                            // log was truncated or rotated, then treat entire file as new
+                            new_chunk = all_server_log;
+                        }
+                        prev_contents = std::move(all_server_log);
+
+                        // sanity-check: we should actually have something new
+                        REQUIRE(!new_chunk.empty());
+
+                        REQUIRE(checkSameClientLib(new_chunk));
+
+                        std::regex re(R"(Attribute::set_client_lib\(([0-9]+),change\))");
+                        std::smatch m;
+                        REQUIRE(std::regex_search(new_chunk, m, re));
+                        int found = parse_as<int>(m[1].str());
+                        CHECK(found == idlver);
+                    }
+                }
+            }
+            device->unsubscribe_event(event_id);
+        }
+    }
+}
-- 
2.39.5

