Skip to main content

zerodds_dcps/
builtin_subscriber.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 ZeroDDS Contributors
3//! Built-in subscriber — preinstalled subscriber with the 4
4//! built-in-topic readers (DDS 1.4 §2.2.2.2.1.7
5//! `get_builtin_subscriber`).
6//!
7//! Per `DomainParticipant` there is exactly **one** built-in
8//! subscriber. It is the application-API-view wrapper over the
9//! runtime's discovery cache: every received SPDP beacon or SEDP
10//! pub/sub event drops a sample into the respective built-in reader,
11//! and user code can pick it up via `take()/read()` — just like any
12//! other DataReader.
13//!
14//! # Spec paths
15//!
16//! - DDS-DCPS 1.4 §2.2.5: Built-in Topics (all 4)
17//! - DDS-DCPS 1.4 §2.2.2.2.1.7: `get_builtin_subscriber()`
18//! - DDSI-RTPS 2.5 §8.5.4: SEDP Built-in Endpoints
19//!
20//! # Built-in DataReader QoS (spec §2.2.5 Table 12)
21//!
22//! Reliability=RELIABLE, Durability=TRANSIENT_LOCAL, History=KEEP_LAST(1).
23//! We set these defaults in `BuiltinSubscriber::new`. Users (per spec)
24//! **cannot** modify them — `lookup_datareader` is read-only.
25
26extern crate alloc;
27use alloc::string::{String, ToString};
28use alloc::sync::Arc;
29use alloc::vec::Vec;
30
31#[cfg(feature = "std")]
32use std::sync::Mutex;
33
34use zerodds_qos::{
35    DurabilityKind, DurabilityQosPolicy, HistoryKind, HistoryQosPolicy, ReliabilityKind,
36    ReliabilityQosPolicy,
37};
38
39use crate::builtin_topics::{
40    ParticipantBuiltinTopicData, PublicationBuiltinTopicData, SubscriptionBuiltinTopicData,
41    TOPIC_NAME_DCPS_PARTICIPANT, TOPIC_NAME_DCPS_PUBLICATION, TOPIC_NAME_DCPS_SUBSCRIPTION,
42    TOPIC_NAME_DCPS_TOPIC, TopicBuiltinTopicData,
43};
44use crate::dds_type::DdsType;
45use crate::error::{DdsError, Result};
46use crate::qos::{DataReaderQos, SubscriberQos, TopicQos};
47use crate::subscriber::{DataReader, Subscriber, SubscriberInner};
48use crate::topic::Topic;
49
50/// Spec-conformant DataReader QoS for built-in topics (DDS 1.4 §2.2.5
51/// Table 12): RELIABLE + TRANSIENT_LOCAL + KEEP_LAST(1).
52#[must_use]
53pub fn builtin_reader_qos() -> DataReaderQos {
54    let mut qos = DataReaderQos::default();
55    qos.reliability = ReliabilityQosPolicy {
56        kind: ReliabilityKind::Reliable,
57        max_blocking_time: qos.reliability.max_blocking_time,
58    };
59    qos.durability = DurabilityQosPolicy {
60        kind: DurabilityKind::TransientLocal,
61    };
62    qos.history = HistoryQosPolicy {
63        kind: HistoryKind::KeepLast,
64        depth: 1,
65    };
66    qos
67}
68
69/// Built-in subscriber. Holds 4 pre-created DataReaders for the
70/// built-in topics. Each reader's `inbox` is exposed as
71/// `Arc<Mutex<Vec<crate::runtime::UserSample>>>` so that the runtime
72/// discovery hook can feed in new samples without lock cycles.
73#[derive(Debug)]
74pub struct BuiltinSubscriber {
75    /// The "transparent" subscriber handle that `get_builtin_subscriber`
76    /// returns — for API symmetry with user subscribers.
77    subscriber: Subscriber,
78    /// Reader for `DCPSParticipant` (discovered participants).
79    participant_reader: DataReader<ParticipantBuiltinTopicData>,
80    /// Reader for `DCPSTopic` (discovered topics).
81    topic_reader: DataReader<TopicBuiltinTopicData>,
82    /// Reader for `DCPSPublication` (discovered writers).
83    publication_reader: DataReader<PublicationBuiltinTopicData>,
84    /// Reader for `DCPSSubscription` (discovered readers).
85    subscription_reader: DataReader<SubscriptionBuiltinTopicData>,
86    /// Sink inboxes (shared with the readers above). Used by the
87    /// runtime discovery hook as push targets.
88    sinks: BuiltinSinks,
89}
90
91/// Bundle of the 4 shared inboxes — handed by `DcpsRuntime` to the
92/// SPDP/SEDP hot path. Cloning is cheap (Arc bumps).
93#[derive(Debug, Clone)]
94pub struct BuiltinSinks {
95    /// Inbox of the `DCPSParticipant` reader.
96    pub participant: Arc<Mutex<Vec<crate::runtime::UserSample>>>,
97    /// Inbox of the `DCPSTopic` reader.
98    pub topic: Arc<Mutex<Vec<crate::runtime::UserSample>>>,
99    /// Inbox of the `DCPSPublication` reader.
100    pub publication: Arc<Mutex<Vec<crate::runtime::UserSample>>>,
101    /// Inbox of the `DCPSSubscription` reader.
102    pub subscription: Arc<Mutex<Vec<crate::runtime::UserSample>>>,
103}
104
105impl BuiltinSinks {
106    /// Convenience helper: encodes a built-in sample and pushes it into
107    /// the matching reader inbox.
108    ///
109    /// # Errors
110    /// `WireError` if the encoding fails; `PreconditionNotMet` if the
111    /// mutex is poisoned.
112    pub fn push_participant(&self, sample: &ParticipantBuiltinTopicData) -> Result<()> {
113        push_into(&self.participant, sample)
114    }
115
116    /// Push for `DCPSTopic`.
117    ///
118    /// # Errors
119    /// As [`Self::push_participant`].
120    pub fn push_topic(&self, sample: &TopicBuiltinTopicData) -> Result<()> {
121        push_into(&self.topic, sample)
122    }
123
124    /// Push for `DCPSPublication`.
125    ///
126    /// # Errors
127    /// As [`Self::push_participant`].
128    pub fn push_publication(&self, sample: &PublicationBuiltinTopicData) -> Result<()> {
129        push_into(&self.publication, sample)
130    }
131
132    /// Push for `DCPSSubscription`.
133    ///
134    /// # Errors
135    /// As [`Self::push_participant`].
136    pub fn push_subscription(&self, sample: &SubscriptionBuiltinTopicData) -> Result<()> {
137        push_into(&self.subscription, sample)
138    }
139
140    /// Marks the `DCPSPublication` instance for `guid` disposed — the remote
141    /// deleted its DataWriter (SEDP dispose). Observers of the built-in
142    /// publications reader then see the instance go `NOT_ALIVE_DISPOSED`
143    /// (DDS-DCPS 1.4 §2.2.5). The instance key of a built-in publication is the
144    /// endpoint GUID, which for a ≤16-byte key is its own KeyHash
145    /// (DDSI-RTPS §9.6.4.8) — so `guid.to_bytes()` addresses exactly the
146    /// instance an earlier ALIVE sample created.
147    pub fn dispose_publication(&self, guid: zerodds_rtps::wire_types::Guid) {
148        push_lifecycle(&self.publication, guid);
149    }
150
151    /// `DCPSSubscription` counterpart of [`Self::dispose_publication`].
152    pub fn dispose_subscription(&self, guid: zerodds_rtps::wire_types::Guid) {
153        push_lifecycle(&self.subscription, guid);
154    }
155}
156
157/// Pushes a `NOT_ALIVE_DISPOSED` lifecycle marker for `guid` into a built-in
158/// reader inbox. Silent no-op if the inbox mutex is poisoned (a dropped
159/// discovery notification is non-fatal).
160fn push_lifecycle(
161    sink: &Arc<Mutex<Vec<crate::runtime::UserSample>>>,
162    guid: zerodds_rtps::wire_types::Guid,
163) {
164    if let Ok(mut guard) = sink.lock() {
165        guard.push(crate::runtime::UserSample::Lifecycle {
166            key_hash: guid.to_bytes(),
167            kind: zerodds_rtps::history_cache::ChangeKind::NotAliveDisposed,
168        });
169    }
170}
171
172fn push_into<T: DdsType>(
173    sink: &Arc<Mutex<Vec<crate::runtime::UserSample>>>,
174    sample: &T,
175) -> Result<()> {
176    let mut buf = Vec::new();
177    sample.encode(&mut buf).map_err(|e| DdsError::WireError {
178        message: format_err(&e),
179    })?;
180    let mut guard = sink.lock().map_err(|_| DdsError::PreconditionNotMet {
181        reason: "builtin sink mutex poisoned",
182    })?;
183    // Built-in samples come from the local discovery path (no remote
184    // writer); writer_guid + writer_strength are default-initialized.
185    // Built-in topics use shared ownership, so no filter activation in
186    // the reader.
187    guard.push(crate::runtime::UserSample::Alive {
188        payload: crate::sample_bytes::SampleBytes::from_vec(buf),
189        writer_guid: [0u8; 16],
190        writer_strength: 0,
191        // Built-in-topic samples are encoded ZeroDDS-internally —
192        // XCDR1 baseline, little-endian.
193        representation: 0,
194        big_endian: false,
195        source_timestamp: None,
196        // Built-in-topic samples are not user data — a durability service does
197        // not ingest them, so there is no source sequence to carry.
198        source_sequence_number: -1,
199    });
200    Ok(())
201}
202
203fn format_err(e: &crate::dds_type::EncodeError) -> String {
204    use core::fmt::Write;
205    let mut s = String::new();
206    let _ = write!(s, "{e}");
207    s
208}
209
210impl BuiltinSubscriber {
211    /// Constructs a built-in subscriber **with** pre-created readers for
212    /// all 4 built-in topics. Called exactly once per
213    /// `DomainParticipant` (by the constructor).
214    #[must_use]
215    pub fn new() -> Self {
216        // Subscriber with default QoS — the subscriber itself is simply
217        // an API wrapper, not a runtime endpoint.
218        let subscriber = Subscriber::new(SubscriberQos::default(), None);
219        let inner = subscriber.inner.clone();
220
221        let qos = builtin_reader_qos();
222
223        // Create 4 inboxes (shared between the DataReader and the
224        // runtime discovery hook).
225        let part_inbox: Arc<Mutex<Vec<crate::runtime::UserSample>>> =
226            Arc::new(Mutex::new(Vec::new()));
227        let topic_inbox: Arc<Mutex<Vec<crate::runtime::UserSample>>> =
228            Arc::new(Mutex::new(Vec::new()));
229        let pub_inbox: Arc<Mutex<Vec<crate::runtime::UserSample>>> =
230            Arc::new(Mutex::new(Vec::new()));
231        let sub_inbox: Arc<Mutex<Vec<crate::runtime::UserSample>>> =
232            Arc::new(Mutex::new(Vec::new()));
233
234        let participant_reader = DataReader::new_builtin(
235            Topic::<ParticipantBuiltinTopicData>::new_orphan(
236                TOPIC_NAME_DCPS_PARTICIPANT.to_string(),
237                TopicQos::default(),
238            ),
239            qos.clone(),
240            inner.clone(),
241            part_inbox.clone(),
242        );
243        let topic_reader = DataReader::new_builtin(
244            Topic::<TopicBuiltinTopicData>::new_orphan(
245                TOPIC_NAME_DCPS_TOPIC.to_string(),
246                TopicQos::default(),
247            ),
248            qos.clone(),
249            inner.clone(),
250            topic_inbox.clone(),
251        );
252        let publication_reader = DataReader::new_builtin(
253            Topic::<PublicationBuiltinTopicData>::new_orphan(
254                TOPIC_NAME_DCPS_PUBLICATION.to_string(),
255                TopicQos::default(),
256            ),
257            qos.clone(),
258            inner.clone(),
259            pub_inbox.clone(),
260        );
261        let subscription_reader = DataReader::new_builtin(
262            Topic::<SubscriptionBuiltinTopicData>::new_orphan(
263                TOPIC_NAME_DCPS_SUBSCRIPTION.to_string(),
264                TopicQos::default(),
265            ),
266            qos,
267            inner,
268            sub_inbox.clone(),
269        );
270
271        Self {
272            subscriber,
273            participant_reader,
274            topic_reader,
275            publication_reader,
276            subscription_reader,
277            sinks: BuiltinSinks {
278                participant: part_inbox,
279                topic: topic_inbox,
280                publication: pub_inbox,
281                subscription: sub_inbox,
282            },
283        }
284    }
285
286    /// Underlying subscriber handle (API mirror of user subscribers).
287    #[must_use]
288    pub fn subscriber(&self) -> &Subscriber {
289        &self.subscriber
290    }
291
292    /// Sinks for the runtime discovery hook. Should only be used
293    /// internally (in `DcpsRuntime`).
294    #[must_use]
295    pub fn sinks(&self) -> BuiltinSinks {
296        self.sinks.clone()
297    }
298
299    /// Returns a copy of the typed DataReader for a built-in topic.
300    /// Spec: `Subscriber::lookup_datareader(topic_name)` (DDS 1.4
301    /// §2.2.2.5.1.5).
302    ///
303    /// **Topic name + type parameter MUST be consistent** (e.g.
304    /// `lookup_datareader::<ParticipantBuiltinTopicData>("DCPSParticipant")`),
305    /// otherwise it returns `BadParameter`.
306    ///
307    /// # Errors
308    /// `BadParameter` if `topic_name` matches no built-in topic, or if
309    /// the type parameter and topic name diverge.
310    pub fn lookup_datareader<T: BuiltinTopic>(&self, topic_name: &str) -> Result<DataReader<T>> {
311        T::lookup(self, topic_name)
312    }
313
314    /// Direct access to the `DCPSParticipant` reader (convenience API,
315    /// avoids generic lookup paths for built-in topics).
316    #[must_use]
317    pub fn participant_reader(&self) -> DataReader<ParticipantBuiltinTopicData> {
318        clone_reader(&self.participant_reader)
319    }
320
321    /// Direct access to the `DCPSTopic` reader.
322    #[must_use]
323    pub fn topic_reader(&self) -> DataReader<TopicBuiltinTopicData> {
324        clone_reader(&self.topic_reader)
325    }
326
327    /// Direct access to the `DCPSPublication` reader.
328    #[must_use]
329    pub fn publication_reader(&self) -> DataReader<PublicationBuiltinTopicData> {
330        clone_reader(&self.publication_reader)
331    }
332
333    /// Direct access to the `DCPSSubscription` reader.
334    #[must_use]
335    pub fn subscription_reader(&self) -> DataReader<SubscriptionBuiltinTopicData> {
336        clone_reader(&self.subscription_reader)
337    }
338}
339
340impl Default for BuiltinSubscriber {
341    fn default() -> Self {
342        Self::new()
343    }
344}
345
346/// Marker trait + lookup routing for the 4 built-in-topic types.
347///
348/// So that `lookup_datareader::<T>(topic_name)` hits the right reader
349/// via the type parameter. External types cannot implement the trait —
350/// `BuiltinTopic` is sealed.
351pub trait BuiltinTopic: DdsType + private::Sealed + Sized {
352    /// Topic name from DDS 1.4 §2.2.5.
353    const TOPIC_NAME: &'static str;
354    #[doc(hidden)]
355    fn lookup(sub: &BuiltinSubscriber, topic_name: &str) -> Result<DataReader<Self>>;
356}
357
358mod private {
359    pub trait Sealed {}
360    impl Sealed for crate::builtin_topics::ParticipantBuiltinTopicData {}
361    impl Sealed for crate::builtin_topics::TopicBuiltinTopicData {}
362    impl Sealed for crate::builtin_topics::PublicationBuiltinTopicData {}
363    impl Sealed for crate::builtin_topics::SubscriptionBuiltinTopicData {}
364}
365
366impl BuiltinTopic for ParticipantBuiltinTopicData {
367    const TOPIC_NAME: &'static str = TOPIC_NAME_DCPS_PARTICIPANT;
368    fn lookup(sub: &BuiltinSubscriber, topic_name: &str) -> Result<DataReader<Self>> {
369        if topic_name != Self::TOPIC_NAME {
370            return Err(DdsError::BadParameter {
371                what: "builtin topic_name does not match type parameter",
372            });
373        }
374        Ok(sub.participant_reader())
375    }
376}
377
378impl BuiltinTopic for TopicBuiltinTopicData {
379    const TOPIC_NAME: &'static str = TOPIC_NAME_DCPS_TOPIC;
380    fn lookup(sub: &BuiltinSubscriber, topic_name: &str) -> Result<DataReader<Self>> {
381        if topic_name != Self::TOPIC_NAME {
382            return Err(DdsError::BadParameter {
383                what: "builtin topic_name does not match type parameter",
384            });
385        }
386        Ok(sub.topic_reader())
387    }
388}
389
390impl BuiltinTopic for PublicationBuiltinTopicData {
391    const TOPIC_NAME: &'static str = TOPIC_NAME_DCPS_PUBLICATION;
392    fn lookup(sub: &BuiltinSubscriber, topic_name: &str) -> Result<DataReader<Self>> {
393        if topic_name != Self::TOPIC_NAME {
394            return Err(DdsError::BadParameter {
395                what: "builtin topic_name does not match type parameter",
396            });
397        }
398        Ok(sub.publication_reader())
399    }
400}
401
402impl BuiltinTopic for SubscriptionBuiltinTopicData {
403    const TOPIC_NAME: &'static str = TOPIC_NAME_DCPS_SUBSCRIPTION;
404    fn lookup(sub: &BuiltinSubscriber, topic_name: &str) -> Result<DataReader<Self>> {
405        if topic_name != Self::TOPIC_NAME {
406            return Err(DdsError::BadParameter {
407                what: "builtin topic_name does not match type parameter",
408            });
409        }
410        Ok(sub.subscription_reader())
411    }
412}
413
414/// Clones a reader by constructing a new reader handle with the same
415/// shared inbox + identical topic/QoS snapshot. We cannot make
416/// `DataReader<T>` directly `Clone`, because the `runtime`/`rx` fields
417/// (mpsc + Mutex) are not `Clone`. For built-in readers, however, both
418/// are `None`, so the special-case clone here is safe.
419fn clone_reader<T: DdsType + Send + Sync + 'static>(r: &DataReader<T>) -> DataReader<T> {
420    DataReader::<T>::new_builtin(
421        r.topic().clone(),
422        r.qos().clone(),
423        builtin_clone_subscriber_inner(r),
424        r.__inbox_handle(),
425    )
426}
427
428fn builtin_clone_subscriber_inner<T: DdsType>(_r: &DataReader<T>) -> Arc<SubscriberInner> {
429    // The built-in subscriber is static: we construct a new inner on
430    // every lookup — it is not used for runtime routing lookup anyway.
431    Arc::new(SubscriberInner {
432        qos: std::sync::Mutex::new(SubscriberQos::default()),
433        entity_state: crate::entity::EntityState::new(),
434        runtime: None,
435        listener: std::sync::Mutex::new(None),
436        participant: std::sync::Mutex::new(None),
437        access_scope: crate::coherent_set::GroupAccessScope::new(),
438        datareaders: std::sync::Mutex::new(alloc::vec::Vec::new()),
439    })
440}
441
442#[cfg(test)]
443#[allow(clippy::expect_used, clippy::unwrap_used)]
444mod tests {
445    use super::*;
446    use crate::builtin_topics::{
447        ParticipantBuiltinTopicData, PublicationBuiltinTopicData, SubscriptionBuiltinTopicData,
448        TopicBuiltinTopicData,
449    };
450    use zerodds_rtps::wire_types::Guid;
451
452    fn mk_guid(seed: u8) -> Guid {
453        let mut b = [0u8; 16];
454        for (i, slot) in b.iter_mut().enumerate() {
455            *slot = seed.wrapping_add(i as u8);
456        }
457        Guid::from_bytes(b)
458    }
459
460    #[test]
461    fn builtin_subscriber_has_four_readers() {
462        let bs = BuiltinSubscriber::new();
463        assert_eq!(bs.participant_reader().topic().name(), "DCPSParticipant");
464        assert_eq!(bs.topic_reader().topic().name(), "DCPSTopic");
465        assert_eq!(bs.publication_reader().topic().name(), "DCPSPublication");
466        assert_eq!(bs.subscription_reader().topic().name(), "DCPSSubscription");
467    }
468
469    #[test]
470    fn builtin_reader_qos_is_spec_default() {
471        let q = builtin_reader_qos();
472        assert_eq!(q.reliability.kind, ReliabilityKind::Reliable);
473        assert_eq!(q.durability.kind, DurabilityKind::TransientLocal);
474        assert_eq!(q.history.kind, HistoryKind::KeepLast);
475        assert_eq!(q.history.depth, 1);
476    }
477
478    #[test]
479    fn lookup_datareader_routes_by_type() {
480        let bs = BuiltinSubscriber::new();
481        let r = bs
482            .lookup_datareader::<ParticipantBuiltinTopicData>("DCPSParticipant")
483            .unwrap();
484        assert_eq!(r.topic().name(), "DCPSParticipant");
485
486        let r = bs
487            .lookup_datareader::<TopicBuiltinTopicData>("DCPSTopic")
488            .unwrap();
489        assert_eq!(r.topic().name(), "DCPSTopic");
490
491        let r = bs
492            .lookup_datareader::<PublicationBuiltinTopicData>("DCPSPublication")
493            .unwrap();
494        assert_eq!(r.topic().name(), "DCPSPublication");
495
496        let r = bs
497            .lookup_datareader::<SubscriptionBuiltinTopicData>("DCPSSubscription")
498            .unwrap();
499        assert_eq!(r.topic().name(), "DCPSSubscription");
500    }
501
502    #[test]
503    fn lookup_datareader_rejects_wrong_topic_name() {
504        let bs = BuiltinSubscriber::new();
505        let err = bs
506            .lookup_datareader::<ParticipantBuiltinTopicData>("DCPSPublication")
507            .unwrap_err();
508        assert!(matches!(err, DdsError::BadParameter { .. }));
509    }
510
511    #[test]
512    fn sinks_push_participant_lands_in_reader() {
513        let bs = BuiltinSubscriber::new();
514        let sample = ParticipantBuiltinTopicData {
515            key: mk_guid(0xA0),
516            user_data: alloc::vec![],
517        };
518        bs.sinks().push_participant(&sample).unwrap();
519        let reader = bs
520            .lookup_datareader::<ParticipantBuiltinTopicData>("DCPSParticipant")
521            .unwrap();
522        let samples = reader.take().unwrap();
523        assert_eq!(samples.len(), 1);
524        assert_eq!(samples[0].key, sample.key);
525    }
526
527    #[test]
528    fn sinks_push_topic_lands_in_reader() {
529        let bs = BuiltinSubscriber::new();
530        let sample = TopicBuiltinTopicData {
531            key: TopicBuiltinTopicData::synthesize_key("MyT", "MyType"),
532            name: "MyT".to_string(),
533            type_name: "MyType".to_string(),
534            durability: DurabilityKind::Volatile,
535            reliability: ReliabilityKind::Reliable,
536        };
537        bs.sinks().push_topic(&sample).unwrap();
538        let reader = bs
539            .lookup_datareader::<TopicBuiltinTopicData>("DCPSTopic")
540            .unwrap();
541        let samples = reader.take().unwrap();
542        assert_eq!(samples.len(), 1);
543        assert_eq!(samples[0].name, "MyT");
544    }
545
546    #[test]
547    fn sinks_push_publication_lands_in_reader() {
548        let bs = BuiltinSubscriber::new();
549        let sample = PublicationBuiltinTopicData {
550            key: mk_guid(0xB0),
551            participant_key: mk_guid(0xC0),
552            topic_name: "T".to_string(),
553            type_name: "T".to_string(),
554            durability: DurabilityKind::Volatile,
555            reliability: ReliabilityKind::BestEffort,
556            ownership: zerodds_qos::OwnershipKind::Shared,
557            ownership_strength: 0,
558            liveliness_lease_seconds: 0,
559            deadline_seconds: 0,
560            lifespan_seconds: 0,
561            partition: alloc::vec![],
562        };
563        bs.sinks().push_publication(&sample).unwrap();
564        let reader = bs
565            .lookup_datareader::<PublicationBuiltinTopicData>("DCPSPublication")
566            .unwrap();
567        let samples = reader.take().unwrap();
568        assert_eq!(samples.len(), 1);
569        assert_eq!(samples[0].key, sample.key);
570    }
571
572    #[test]
573    fn sinks_push_subscription_lands_in_reader() {
574        let bs = BuiltinSubscriber::new();
575        let sample = SubscriptionBuiltinTopicData {
576            key: mk_guid(0xD0),
577            participant_key: mk_guid(0xE0),
578            topic_name: "T".to_string(),
579            type_name: "T".to_string(),
580            durability: DurabilityKind::Volatile,
581            reliability: ReliabilityKind::Reliable,
582            ownership: zerodds_qos::OwnershipKind::Shared,
583            liveliness_lease_seconds: 0,
584            deadline_seconds: 0,
585            partition: alloc::vec![],
586        };
587        bs.sinks().push_subscription(&sample).unwrap();
588        let reader = bs
589            .lookup_datareader::<SubscriptionBuiltinTopicData>("DCPSSubscription")
590            .unwrap();
591        let samples = reader.take().unwrap();
592        assert_eq!(samples.len(), 1);
593        assert_eq!(samples[0].topic_name, "T");
594    }
595
596    #[test]
597    fn subscriber_handle_is_accessible() {
598        let bs = BuiltinSubscriber::new();
599        // Smoke: the inner subscriber handle is accessible (API
600        // symmetry with user subscribers).
601        let _: &Subscriber = bs.subscriber();
602    }
603
604    #[test]
605    fn default_constructs_via_new() {
606        let bs: BuiltinSubscriber = Default::default();
607        assert_eq!(bs.participant_reader().topic().name(), "DCPSParticipant");
608    }
609
610    #[test]
611    fn lookup_topic_with_wrong_topic_name_for_topic_type() {
612        let bs = BuiltinSubscriber::new();
613        let err = bs
614            .lookup_datareader::<TopicBuiltinTopicData>("DCPSParticipant")
615            .unwrap_err();
616        assert!(matches!(err, DdsError::BadParameter { .. }));
617    }
618
619    #[test]
620    fn lookup_publication_with_wrong_topic_name() {
621        let bs = BuiltinSubscriber::new();
622        let err = bs
623            .lookup_datareader::<PublicationBuiltinTopicData>("DCPSTopic")
624            .unwrap_err();
625        assert!(matches!(err, DdsError::BadParameter { .. }));
626    }
627
628    #[test]
629    fn lookup_subscription_with_wrong_topic_name() {
630        let bs = BuiltinSubscriber::new();
631        let err = bs
632            .lookup_datareader::<SubscriptionBuiltinTopicData>("DCPSTopic")
633            .unwrap_err();
634        assert!(matches!(err, DdsError::BadParameter { .. }));
635    }
636
637    #[test]
638    fn read_does_not_remove_samples() {
639        let bs = BuiltinSubscriber::new();
640        let sample = ParticipantBuiltinTopicData {
641            key: mk_guid(0x33),
642            user_data: alloc::vec![],
643        };
644        bs.sinks().push_participant(&sample).unwrap();
645        let reader = bs.participant_reader();
646        let s1 = reader.read().unwrap();
647        let s2 = reader.read().unwrap();
648        assert_eq!(s1.len(), 1);
649        assert_eq!(s2.len(), 1);
650    }
651}