|
| 1 | +// Copyright 2025 Proyectos y Sistemas de Mantenimiento SL (eProsima). |
| 2 | +// |
| 3 | +// Licensed under the Apache License, Version 2.0 (the "License"); |
| 4 | +// you may not use this file except in compliance with the License. |
| 5 | +// You may obtain a copy of the License at |
| 6 | +// |
| 7 | +// http://www.apache.org/licenses/LICENSE-2.0 |
| 8 | +// |
| 9 | +// Unless required by applicable law or agreed to in writing, software |
| 10 | +// distributed under the License is distributed on an "AS IS" BASIS, |
| 11 | +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 12 | +// See the License for the specific language governing permissions and |
| 13 | +// limitations under the License. |
| 14 | + |
| 15 | +#include <atomic> |
| 16 | +#include <thread> |
| 17 | + |
| 18 | +#include <fastdds/dds/core/detail/DDSReturnCode.hpp> |
| 19 | + |
| 20 | +#include <cpp_utils/Log.hpp> |
| 21 | +#include <cpp_utils/testing/gtest_aux.hpp> |
| 22 | + |
| 23 | +#include <ddspipe_core/types/topic/filter/WildcardDdsFilterTopic.hpp> |
| 24 | + |
| 25 | +#include <ddspipe_participants/configuration/SimpleParticipantConfiguration.hpp> |
| 26 | +#include <ddspipe_participants/configuration/XmlParticipantConfiguration.hpp> |
| 27 | + |
| 28 | +#include <ddsrouter_core/core/DdsRouter.hpp> |
| 29 | +#include <ddsrouter_core/types/ParticipantKind.hpp> |
| 30 | + |
| 31 | +#include <test_participants.hpp> |
| 32 | + |
| 33 | +#include <gtest/gtest.h> |
| 34 | + |
| 35 | +using namespace eprosima; |
| 36 | +using namespace eprosima::ddspipe; |
| 37 | +using namespace eprosima::ddsrouter::core; |
| 38 | + |
| 39 | +namespace test { |
| 40 | + |
| 41 | +constexpr const uint32_t DEFAULT_SAMPLES_TO_RECEIVE = 5; |
| 42 | +constexpr const uint32_t DEFAULT_MILLISECONDS_PUBLISH_LOOP = 100; |
| 43 | +constexpr const uint32_t DEFAULT_MESSAGE_SIZE = 1; // x50 bytes |
| 44 | + |
| 45 | +// Maximum time to wait for the forwarded dispose to be received by the subscriber. |
| 46 | +constexpr const uint32_t MAX_MILLISECONDS_WAIT_DISPOSE = 5000; |
| 47 | +constexpr const uint32_t MILLISECONDS_WAIT_STEP = 100; |
| 48 | + |
| 49 | +/** |
| 50 | + * @brief Create a DDS Router configuration that bridges \c TOPIC_NAME between: |
| 51 | + * - a Simple participant in domain 0 (publisher side), and |
| 52 | + * - an XML (DDS) participant in domain 1 (subscriber side) configured as a REPEATER. |
| 53 | + * |
| 54 | + * The XML participant is the one that exercises the buggy code path: as a repeater its |
| 55 | + * DataWriter installs a \c dds::RepeaterDataFilter prefilter. When the dispose/unregister |
| 56 | + * sample produced by the publisher is forwarded to the subscriber through this writer, the |
| 57 | + * prefilter is evaluated for a sample whose \c user_write_data is null (dispose/unregister |
| 58 | + * are sent through DataWriter::dispose / unregister_instance, which cannot carry WriteParams). |
| 59 | + * |
| 60 | + * Note: no participant_profile is set on the XML participant, so XmlParticipant falls back to |
| 61 | + * the configured domain (1) — this keeps the test self-contained (no external XML profile file). |
| 62 | + */ |
| 63 | +DdsRouterConfiguration xml_repeater_dispose_configuration() |
| 64 | +{ |
| 65 | + DdsRouterConfiguration conf; |
| 66 | + |
| 67 | + // -- Filter the test topic by name --------------------------------------- |
| 68 | + // (any type, keyed or not) |
| 69 | + core::types::WildcardDdsFilterTopic topic; |
| 70 | + topic.topic_name.set_value(TOPIC_NAME); |
| 71 | + conf.ddspipe_configuration.allowlist.insert( |
| 72 | + utils::Heritable<core::types::WildcardDdsFilterTopic>::make_heritable(topic)); |
| 73 | + |
| 74 | + // -- Create the participants --------------------------------------------- |
| 75 | + |
| 76 | + // Simple participant in domain 0 (publisher side) |
| 77 | + { |
| 78 | + auto part = std::make_shared<participants::SimpleParticipantConfiguration>(); |
| 79 | + part->id = core::types::ParticipantId("simple_participant_0"); |
| 80 | + part->domain.domain_id = 0u; |
| 81 | + conf.participants_configurations.insert({types::ParticipantKind::simple, part}); |
| 82 | + } |
| 83 | + |
| 84 | + // XML (DDS) participant in domain 1 (subscriber side), in REPEATER mode. |
| 85 | + { |
| 86 | + auto part = std::make_shared<participants::XmlParticipantConfiguration>(); |
| 87 | + part->id = core::types::ParticipantId("xml_repeater_participant_1"); |
| 88 | + part->domain.domain_id = 1u; |
| 89 | + part->is_repeater = true; |
| 90 | + conf.participants_configurations.insert({types::ParticipantKind::xml, part}); |
| 91 | + } |
| 92 | + |
| 93 | + return conf; |
| 94 | +} |
| 95 | + |
| 96 | +/** |
| 97 | + * Regression test for the segfault in dds::RepeaterDataFilter::evaluate when forwarding a |
| 98 | + * dispose/unregister sample (null user_write_data) through a repeater XML participant. |
| 99 | + * |
| 100 | + * Without the fix the router process crashes (SIGSEGV) while forwarding the dispose, so this |
| 101 | + * test aborts. With the fix the dispose is forwarded and received by the subscriber. |
| 102 | + */ |
| 103 | +void test_xml_repeater_key_dispose( |
| 104 | + DdsRouterConfiguration ddsrouter_configuration, |
| 105 | + uint32_t samples_to_receive = DEFAULT_SAMPLES_TO_RECEIVE, |
| 106 | + uint32_t time_between_samples = DEFAULT_MILLISECONDS_PUBLISH_LOOP, |
| 107 | + uint32_t msg_size = DEFAULT_MESSAGE_SIZE) |
| 108 | +{ |
| 109 | + uint32_t samples_sent = 0; |
| 110 | + std::atomic<uint32_t> samples_received(0); |
| 111 | + |
| 112 | + HelloWorldKeyed msg; |
| 113 | + HelloWorldKeyedPubSubType type; |
| 114 | + |
| 115 | + std::string msg_str; |
| 116 | + for (uint32_t i = 0; i < msg_size; i++) |
| 117 | + { |
| 118 | + msg_str += "Testing DdsRouter Blackbox Repeater Dispose ..."; |
| 119 | + } |
| 120 | + msg.message(msg_str); |
| 121 | + msg.id(666); |
| 122 | + |
| 123 | + // -- Create keyed Publisher, Subscriber & Router ------------------------- |
| 124 | + |
| 125 | + // Publisher (domain 0) |
| 126 | + TestPublisher<HelloWorldKeyed> publisher(type.is_compute_key_provided); |
| 127 | + ASSERT_TRUE(publisher.init(0)); |
| 128 | + |
| 129 | + // Subscriber (domain 1) |
| 130 | + TestSubscriber<HelloWorldKeyed> subscriber(type.is_compute_key_provided); |
| 131 | + ASSERT_TRUE(subscriber.init(1, &msg, &samples_received)); |
| 132 | + |
| 133 | + // Create and start the DDS Router |
| 134 | + DdsRouter router(ddsrouter_configuration); |
| 135 | + router.start(); |
| 136 | + |
| 137 | + // -- Publish some messages ----------------------------------------------- |
| 138 | + while (samples_received.load() < samples_to_receive) |
| 139 | + { |
| 140 | + msg.index(++samples_sent); |
| 141 | + ASSERT_EQ(publisher.publish(msg), fastdds::dds::RETCODE_OK) << samples_sent; |
| 142 | + |
| 143 | + if (time_between_samples > 0) |
| 144 | + { |
| 145 | + std::this_thread::sleep_for(std::chrono::milliseconds(time_between_samples)); |
| 146 | + } |
| 147 | + } |
| 148 | + |
| 149 | + // -- Dispose the keyed publisher ----------------------------------------- |
| 150 | + |
| 151 | + // The dispose is routed to the XML repeater participant's writer, |
| 152 | + // whose RepeaterDataFilter prefilter is evaluated with a null user_write_data. |
| 153 | + // >>> Without the fix the router segfaults at this point. <<< |
| 154 | + ASSERT_EQ(publisher.dispose_key(msg), fastdds::dds::RETCODE_OK); |
| 155 | + |
| 156 | + // With the fix, the dispose is considered relevant and forwarded to the subscriber |
| 157 | + uint32_t waited = 0; |
| 158 | + while (subscriber.n_disposed() < 1u && waited < MAX_MILLISECONDS_WAIT_DISPOSE) |
| 159 | + { |
| 160 | + std::this_thread::sleep_for(std::chrono::milliseconds(MILLISECONDS_WAIT_STEP)); |
| 161 | + waited += MILLISECONDS_WAIT_STEP; |
| 162 | + } |
| 163 | + |
| 164 | + ASSERT_GE(subscriber.n_disposed(), 1u); |
| 165 | + |
| 166 | + router.stop(); |
| 167 | +} |
| 168 | + |
| 169 | +} /* namespace test */ |
| 170 | + |
| 171 | +/** |
| 172 | + * Test that a dispose forwarded through a repeater XML (DDS) participant does not crash the router |
| 173 | + * and is correctly received by the subscriber. |
| 174 | + * |
| 175 | + * Regression test for the null user_write_data dereference in dds::RepeaterDataFilter::evaluate. |
| 176 | + */ |
| 177 | +TEST(DDSTestRepeaterDisposeKey, end_to_end_xml_repeater_key_dispose) |
| 178 | +{ |
| 179 | + test::test_xml_repeater_key_dispose( |
| 180 | + test::xml_repeater_dispose_configuration()); |
| 181 | +} |
| 182 | + |
| 183 | +int main( |
| 184 | + int argc, |
| 185 | + char** argv) |
| 186 | +{ |
| 187 | + ::testing::InitGoogleTest(&argc, argv); |
| 188 | + return RUN_ALL_TESTS(); |
| 189 | +} |
0 commit comments