Preview of the proposed up-rust native-frame-model branch (up-rust b6b99c6d, up-spec f0e9b17) — not released documentation. branch · write-up

up_rust/communication/
zero_copy.rs

1/********************************************************************************
2 * Copyright (c) 2026 Contributors to the Eclipse Foundation
3 *
4 * See the NOTICE file(s) distributed with this work for additional
5 * information regarding copyright ownership.
6 *
7 * This program and the accompanying materials are made available under the
8 * terms of the Apache License Version 2.0 which is available at
9 * https://www.apache.org/licenses/LICENSE-2.0
10 *
11 * SPDX-License-Identifier: Apache-2.0
12 ********************************************************************************/
13
14//! Zero-copy communication-layer facade.
15//!
16//! This module exposes the L2 operations that preserve zero-copy semantics:
17//! stable/no-zero publish and subscription with the transport's receive lease
18//! type delivered directly to the listener. The facade does not adapt or copy
19//! received payloads, so selected-wire transports retain their typed
20//! [`UWireRx`](crate::UWireRx) decode surface.
21//!
22//! RPC server and request handling remain at the [`UZeroCopyTransport`] layer
23//! because their non-copying shape is tied to the transport receive lease type.
24//! Any future convenience that copies out of a receive lease must use a
25//! `copying` name and must not claim no-copy behavior.
26
27use std::sync::Arc;
28
29use crate::{
30    communication::{CallOptions, PubSubError},
31    payload::loan::LoanPayload,
32    LocalUriProvider, UFrameMetadata, UMessageBuilder, UZeroCopyTransport, UZeroCopyTransportExt,
33};
34#[cfg(feature = "zero-copy-transport")]
35use crate::{
36    payload::{
37        stable::{
38            InitializedStablePayload, StableContainerPayload, StablePayloadInit,
39            StablePayloadInitContext,
40        },
41        UWireError,
42    },
43    UZeroCopyUninitTransport, UZeroCopyUninitTransportExt,
44};
45use crate::{wire::UWirePayload, wire_transport::UHasWire};
46
47/// *Role: the up-L2 subscribe surface over a **selected-wire zero-copy
48/// transport** (experimental) — see the [trait map](crate::guide::trait_map).*
49///
50/// Mirrors the owned-frame [`Subscriber`](crate::communication::owned::Subscriber):
51/// the uSubscription service is informed first, and the zero-copy listener is
52/// registered only when the service reports the subscription active or
53/// pending. Received payloads are delivered as the transport's lease type —
54/// for a selected-wire transport that is [`UWireRx`](crate::UWireRx), whose
55/// [`decode_payload`](crate::UWireRx::decode_payload) reads the typed value in
56/// place.
57#[cfg(feature = "usubscription")]
58pub struct Subscriber<T>
59where
60    T: UZeroCopyTransport + ?Sized,
61{
62    transport: Arc<T>,
63    usubscription: Arc<dyn crate::core::usubscription::USubscription>,
64}
65
66#[cfg(feature = "usubscription")]
67impl<T> core::fmt::Debug for Subscriber<T>
68where
69    T: UZeroCopyTransport + ?Sized,
70{
71    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
72        f.debug_struct("Subscriber").finish_non_exhaustive()
73    }
74}
75
76#[cfg(feature = "usubscription")]
77impl<T> Subscriber<T>
78where
79    T: UZeroCopyTransport + ?Sized,
80{
81    /// Creates a subscriber over a zero-copy transport and a uSubscription
82    /// service client.
83    #[must_use]
84    pub fn new(
85        transport: Arc<T>,
86        usubscription: Arc<dyn crate::core::usubscription::USubscription>,
87    ) -> Self {
88        Self {
89            transport,
90            usubscription,
91        }
92    }
93
94    /// Subscribes to a topic: informs the uSubscription service first, then
95    /// registers the zero-copy listener on success.
96    ///
97    /// # Errors
98    ///
99    /// Returns an error if the topic is not a valid subscription filter, if
100    /// the uSubscription service rejects the subscription or reports a state
101    /// other than active/pending, or if listener registration fails.
102    pub async fn subscribe(
103        &self,
104        topic: &crate::UUri,
105        listener: Arc<dyn crate::UZeroCopyListener<T::Rx>>,
106    ) -> Result<(), crate::communication::RegistrationError> {
107        use crate::communication::{RegistrationError, SubscriptionStatus};
108        crate::communication::validate_listener_topic(topic)?;
109        let state = self
110            .usubscription
111            .subscribe(topic, None, None)
112            .await
113            .map_err(|status| RegistrationError::Unknown(Box::new(status)))?;
114        if state != SubscriptionStatus::Subscribed && state != SubscriptionStatus::SubscribePending
115        {
116            return Err(RegistrationError::Unknown(Box::new(
117                crate::UStatus::fail_with_code(
118                    crate::UCode::FailedPrecondition,
119                    format!("uSubscription service returned {state:?}"),
120                ),
121            )));
122        }
123        self.transport
124            .register_zero_copy_listener(topic, None, listener)
125            .await
126            .map_err(|status| RegistrationError::Unknown(Box::new(status)))
127    }
128
129    /// Unsubscribes from a topic: informs the uSubscription service, then
130    /// unregisters the zero-copy listener.
131    ///
132    /// # Errors
133    ///
134    /// Returns an error if the topic is not a valid subscription filter, or if
135    /// the service call or listener unregistration fails.
136    pub async fn unsubscribe(
137        &self,
138        topic: &crate::UUri,
139        listener: Arc<dyn crate::UZeroCopyListener<T::Rx>>,
140    ) -> Result<(), crate::communication::RegistrationError> {
141        use crate::communication::RegistrationError;
142        crate::communication::validate_listener_topic(topic)?;
143        self.usubscription
144            .unsubscribe(topic)
145            .await
146            .map_err(|status| RegistrationError::Unknown(Box::new(status)))?;
147        self.transport
148            .unregister_zero_copy_listener(topic, None, listener)
149            .await
150            .map_err(|status| RegistrationError::Unknown(Box::new(status)))
151    }
152}
153
154/// *Role: the up-L2 publish and subscribe surface over a **selected-wire
155/// zero-copy transport** — typed payloads use transport loans and receive
156/// leases directly, with role ergonomics. See the
157/// [guide](crate::guide::applications::communication).*
158///
159/// Front door for zero-copy communication-layer clients.
160pub struct Endpoint<T, P>
161where
162    T: UZeroCopyTransport + ?Sized,
163    P: LocalUriProvider + ?Sized,
164{
165    transport: Arc<T>,
166    uri_provider: Arc<P>,
167}
168
169impl<T, P> core::fmt::Debug for Endpoint<T, P>
170where
171    T: UZeroCopyTransport + ?Sized,
172    P: LocalUriProvider + ?Sized,
173{
174    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
175        f.debug_struct("Endpoint").finish_non_exhaustive()
176    }
177}
178
179impl<T, P> Endpoint<T, P>
180where
181    T: UZeroCopyTransport + ?Sized,
182    P: LocalUriProvider + ?Sized,
183{
184    /// Creates a zero-copy communication endpoint.
185    #[must_use]
186    pub fn new(transport: Arc<T>, uri_provider: Arc<P>) -> Self {
187        Self {
188            transport,
189            uri_provider,
190        }
191    }
192
193    /// Creates the subscribe role over this endpoint's transport, using the
194    /// given uSubscription service client.
195    #[cfg(feature = "usubscription")]
196    #[must_use]
197    pub fn subscriber(
198        &self,
199        usubscription: Arc<dyn crate::core::usubscription::USubscription>,
200    ) -> Subscriber<T> {
201        Subscriber::new(self.transport.clone(), usubscription)
202    }
203
204    /// Creates the publish role over this endpoint's transport and identity.
205    #[must_use]
206    pub fn publisher(&self) -> Publisher<T, P> {
207        Publisher::new(self.transport.clone(), self.uri_provider.clone())
208    }
209}
210
211/// Publisher implemented over a selected-wire zero-copy transport.
212pub struct Publisher<T, P>
213where
214    T: UZeroCopyTransport + ?Sized,
215    P: LocalUriProvider + ?Sized,
216{
217    transport: Arc<T>,
218    uri_provider: Arc<P>,
219}
220
221impl<T, P> core::fmt::Debug for Publisher<T, P>
222where
223    T: UZeroCopyTransport + ?Sized,
224    P: LocalUriProvider + ?Sized,
225{
226    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
227        f.debug_struct("Publisher").finish_non_exhaustive()
228    }
229}
230
231impl<T, P> Publisher<T, P>
232where
233    T: UZeroCopyTransport + ?Sized,
234    P: LocalUriProvider + ?Sized,
235{
236    /// Creates a publisher over a zero-copy transport.
237    #[must_use]
238    pub fn new(transport: Arc<T>, uri_provider: Arc<P>) -> Self {
239        Self {
240            transport,
241            uri_provider,
242        }
243    }
244
245    fn build_metadata(
246        &self,
247        resource_id: u16,
248        call_options: CallOptions,
249    ) -> Result<UFrameMetadata, PubSubError> {
250        let mut builder = UMessageBuilder::publish(self.uri_provider.get_resource_uri(resource_id));
251        builder.with_ttl(call_options.ttl());
252        if let Some(message_id) = call_options.message_id() {
253            builder.with_message_id(message_id.clone());
254        }
255        if let Some(priority) = call_options.priority() {
256            builder.with_priority(priority);
257        }
258        let message = builder.build().map_err(|error| {
259            PubSubError::InvalidArgument(format!(
260                "failed to create zero-copy Publish message metadata from parameters: {error}"
261            ))
262        })?;
263        crate::frame::metadata::try_project_umessage_to_frame_metadata(&message).map_err(|error| {
264            PubSubError::InvalidArgument(format!(
265                "failed to create zero-copy Publish frame metadata from parameters: {error}"
266            ))
267        })
268    }
269}
270
271impl<T, P> Publisher<T, P>
272where
273    T: UZeroCopyTransport + UHasWire + ?Sized,
274    P: LocalUriProvider + ?Sized,
275{
276    /// Publishes an initialized stable payload using the transport endpoint's selected wire.
277    pub async fn publish_stable<Payload>(
278        &self,
279        resource_id: u16,
280        call_options: CallOptions,
281        init: impl for<'payload> FnOnce(&'payload mut Payload) + Send,
282    ) -> Result<(), PubSubError>
283    where
284        T::Wire: UWirePayload<Payload>,
285        <T::Wire as UWirePayload<Payload>>::Codec: LoanPayload<Payload> + Send + Sync,
286    {
287        let metadata = self.build_metadata(resource_id, call_options)?;
288        self.transport
289            .send_loaned_payload::<Payload>(metadata, init)
290            .await
291            .map_err(Box::from)
292            .map_err(PubSubError::PublishError)
293    }
294}
295
296#[cfg(feature = "zero-copy-transport")]
297impl<T, P> Publisher<T, P>
298where
299    T: UZeroCopyUninitTransport + UHasWire + ?Sized,
300    P: LocalUriProvider + ?Sized,
301{
302    /// Publishes a stable payload by initializing it directly in uninitialized transport storage.
303    ///
304    pub async fn publish_uninit_stable<Payload>(
305        &self,
306        resource_id: u16,
307        call_options: CallOptions,
308        init: impl for<'payload> FnOnce(
309                StablePayloadInitContext<'payload, Payload>,
310            )
311                -> Result<InitializedStablePayload<'payload, Payload>, UWireError>
312            + Send,
313    ) -> Result<(), PubSubError>
314    where
315        T::Wire: UWirePayload<Payload, Codec = StableContainerPayload<Payload>>,
316        Payload: StablePayloadInit + Send,
317    {
318        let metadata = self.build_metadata(resource_id, call_options)?;
319        self.transport
320            .send_uninit_stable_payload::<Payload>(metadata, init)
321            .await
322            .map_err(Box::from)
323            .map_err(PubSubError::PublishError)
324    }
325}
326
327#[cfg(test)]
328mod tests {
329    use async_trait::async_trait;
330
331    use super::*;
332    use crate::{
333        test_support::StableTestBytes as StableBytes, InMemoryZeroCopyTransport,
334        StableContainerWireFormat, StaticUriProvider, UCode, UFrameView,
335        ULoanedContiguousZeroCopyRxFrame, UStatus, UTxLoanSpec, UVecRxLease, UVecTxBuffer,
336        UVecUninitTxBuffer, UZeroCopyTransportImpl, UZeroCopyUninitTransportImpl,
337    };
338
339    struct SelectedStableTransport {
340        inner: InMemoryZeroCopyTransport,
341        wire: StableContainerWireFormat,
342    }
343
344    impl SelectedStableTransport {
345        fn new() -> Self {
346            Self {
347                inner: InMemoryZeroCopyTransport::default(),
348                wire: StableContainerWireFormat,
349            }
350        }
351
352        fn sent_frames(&self) -> Vec<UVecRxLease> {
353            self.inner.sent_frames()
354        }
355    }
356
357    impl UHasWire for SelectedStableTransport {
358        type Wire = StableContainerWireFormat;
359
360        fn wire(&self) -> &Self::Wire {
361            &self.wire
362        }
363    }
364
365    #[async_trait]
366    impl UZeroCopyTransportImpl for SelectedStableTransport {
367        type Tx = UVecTxBuffer;
368        type Rx = UVecRxLease;
369
370        async fn loan_validated_tx(&self, spec: UTxLoanSpec) -> Result<Self::Tx, UStatus> {
371            UZeroCopyTransportImpl::loan_validated_tx(&self.inner, spec).await
372        }
373
374        async fn send_validated_zero_copy(&self, buffer: Self::Tx) -> Result<(), UStatus> {
375            UZeroCopyTransportImpl::send_validated_zero_copy(&self.inner, buffer).await
376        }
377    }
378
379    #[async_trait]
380    impl UZeroCopyUninitTransportImpl for SelectedStableTransport {
381        type UninitTx = UVecUninitTxBuffer;
382
383        async fn loan_validated_uninit_tx(
384            &self,
385            spec: UTxLoanSpec,
386        ) -> Result<Self::UninitTx, UStatus> {
387            UZeroCopyUninitTransportImpl::loan_validated_uninit_tx(&self.inner, spec).await
388        }
389    }
390
391    fn uri_provider() -> Arc<StaticUriProvider> {
392        Arc::new(StaticUriProvider::new("", 0x0005, 0x02).expect("uri provider"))
393    }
394
395    #[tokio::test]
396    async fn publish_stable_sends_initialized_selected_wire_payload() {
397        let transport = Arc::new(SelectedStableTransport::new());
398        let publisher = Publisher::new(transport.clone(), uri_provider());
399
400        publisher
401            .publish_stable::<StableBytes>(
402                0x9A00,
403                CallOptions::for_publish(None, None, None),
404                |payload| payload.bytes.copy_from_slice(b"init"),
405            )
406            .await
407            .expect("stable publish succeeds");
408
409        let frames = transport.sent_frames();
410        assert_eq!(frames.len(), 1);
411        let frame = frames.first().expect("one sent frame");
412        assert_eq!(frame.payload_len(), std::mem::size_of::<StableBytes>());
413        assert_eq!(
414            frame.borrow_stable_payload::<StableBytes>().unwrap(),
415            &StableBytes { bytes: *b"init" }
416        );
417    }
418
419    #[tokio::test]
420    async fn publish_uninit_stable_sends_no_zero_selected_wire_payload() {
421        let transport = Arc::new(SelectedStableTransport::new());
422        let publisher = Endpoint::new(transport.clone(), uri_provider()).publisher();
423
424        publisher
425            .publish_uninit_stable::<StableBytes>(
426                0x9A00,
427                CallOptions::for_publish(None, None, None),
428                |context| context.into_init().bytes_from_array(b"zero").finish(),
429            )
430            .await
431            .expect("uninit stable publish succeeds");
432
433        let frames = transport.sent_frames();
434        assert_eq!(frames.len(), 1);
435        let frame = frames.first().expect("one sent frame");
436        assert_eq!(
437            frame.borrow_stable_payload::<StableBytes>().unwrap(),
438            &StableBytes { bytes: *b"zero" }
439        );
440    }
441
442    #[tokio::test]
443    async fn publish_stable_rejects_invalid_topic() {
444        let transport = Arc::new(SelectedStableTransport::new());
445        let publisher = Publisher::new(transport.clone(), uri_provider());
446
447        let result = publisher
448            .publish_stable::<StableBytes>(
449                0x1000,
450                CallOptions::for_publish(None, None, None),
451                |payload| payload.bytes.copy_from_slice(b"drop"),
452            )
453            .await;
454
455        assert!(matches!(result, Err(PubSubError::InvalidArgument(_))));
456        assert!(transport.sent_frames().is_empty());
457    }
458
459    struct FailingStableTransport {
460        wire: StableContainerWireFormat,
461    }
462
463    impl UHasWire for FailingStableTransport {
464        type Wire = StableContainerWireFormat;
465
466        fn wire(&self) -> &Self::Wire {
467            &self.wire
468        }
469    }
470
471    #[async_trait]
472    impl UZeroCopyTransportImpl for FailingStableTransport {
473        type Tx = UVecTxBuffer;
474        type Rx = UVecRxLease;
475
476        async fn loan_validated_tx(&self, _spec: UTxLoanSpec) -> Result<Self::Tx, UStatus> {
477            Err(UStatus::fail_with_code(
478                UCode::Unavailable,
479                "transport unavailable",
480            ))
481        }
482
483        async fn send_validated_zero_copy(&self, _buffer: Self::Tx) -> Result<(), UStatus> {
484            Ok(())
485        }
486    }
487
488    #[tokio::test]
489    async fn publish_stable_maps_transport_error() {
490        let publisher = Publisher::new(
491            Arc::new(FailingStableTransport {
492                wire: StableContainerWireFormat,
493            }),
494            uri_provider(),
495        );
496
497        let result = publisher
498            .publish_stable::<StableBytes>(
499                0x9A00,
500                CallOptions::for_publish(None, None, None),
501                |payload| payload.bytes.copy_from_slice(b"fail"),
502            )
503            .await;
504
505        assert!(matches!(result, Err(PubSubError::PublishError(_))));
506    }
507}
508
509#[cfg(all(test, feature = "usubscription"))]
510mod subscriber_tests {
511    use std::sync::{Arc, Mutex};
512
513    use async_trait::async_trait;
514
515    use super::Subscriber;
516    use crate::communication::{RegistrationError, SubscriptionStatus};
517    use crate::core::usubscription::USubscription;
518    use crate::{
519        InMemoryZeroCopyTransport, UStatus, UTxLoanSpec, UUri, UVecRxLease, UVecTxBuffer,
520        UZeroCopyListener, UZeroCopyTransportImpl,
521    };
522
523    struct RecordingZeroCopyTransport {
524        inner: InMemoryZeroCopyTransport,
525        lifecycle: Arc<Mutex<Vec<&'static str>>>,
526    }
527
528    impl RecordingZeroCopyTransport {
529        fn new(lifecycle: Arc<Mutex<Vec<&'static str>>>) -> Self {
530            Self {
531                inner: InMemoryZeroCopyTransport::default(),
532                lifecycle,
533            }
534        }
535
536        fn record(&self, event: &'static str) {
537            self.lifecycle
538                .lock()
539                .expect("lifecycle lock poisoned")
540                .push(event);
541        }
542    }
543
544    #[async_trait]
545    impl UZeroCopyTransportImpl for RecordingZeroCopyTransport {
546        type Tx = UVecTxBuffer;
547        type Rx = UVecRxLease;
548
549        async fn loan_validated_tx(&self, spec: UTxLoanSpec) -> Result<Self::Tx, UStatus> {
550            UZeroCopyTransportImpl::loan_validated_tx(&self.inner, spec).await
551        }
552
553        async fn send_validated_zero_copy(&self, buffer: Self::Tx) -> Result<(), UStatus> {
554            UZeroCopyTransportImpl::send_validated_zero_copy(&self.inner, buffer).await
555        }
556
557        async fn register_validated_zero_copy_listener(
558            &self,
559            source_filter: &UUri,
560            sink_filter: Option<&UUri>,
561            listener: Arc<dyn UZeroCopyListener<Self::Rx>>,
562        ) -> Result<(), UStatus> {
563            self.record("register");
564            UZeroCopyTransportImpl::register_validated_zero_copy_listener(
565                &self.inner,
566                source_filter,
567                sink_filter,
568                listener,
569            )
570            .await
571        }
572
573        async fn unregister_validated_zero_copy_listener(
574            &self,
575            source_filter: &UUri,
576            sink_filter: Option<&UUri>,
577            listener: Arc<dyn UZeroCopyListener<Self::Rx>>,
578        ) -> Result<(), UStatus> {
579            self.record("unregister");
580            UZeroCopyTransportImpl::unregister_validated_zero_copy_listener(
581                &self.inner,
582                source_filter,
583                sink_filter,
584                listener,
585            )
586            .await
587        }
588    }
589
590    #[derive(Default)]
591    struct StubUSubscription {
592        lifecycle: Arc<Mutex<Vec<&'static str>>>,
593        decline: bool,
594    }
595
596    #[async_trait]
597    impl USubscription for StubUSubscription {
598        async fn subscribe(
599            &self,
600            _topic: &UUri,
601            _expiration: Option<u64>,
602            _min_sample_period: Option<u32>,
603        ) -> Result<SubscriptionStatus, UStatus> {
604            self.lifecycle
605                .lock()
606                .expect("lifecycle lock poisoned")
607                .push("subscribe");
608            if self.decline {
609                Ok(SubscriptionStatus::Unsubscribed)
610            } else {
611                Ok(SubscriptionStatus::Subscribed)
612            }
613        }
614        async fn unsubscribe(&self, _topic: &UUri) -> Result<(), UStatus> {
615            self.lifecycle
616                .lock()
617                .expect("lifecycle lock poisoned")
618                .push("unsubscribe");
619            Ok(())
620        }
621        async fn fetch_subscriptions_by_topic(
622            &self,
623            _topic: &UUri,
624        ) -> Result<Vec<crate::core::usubscription::SubscriptionInfo>, UStatus> {
625            Ok(Vec::new())
626        }
627        async fn fetch_subscriptions_by_subscriber(
628            &self,
629            _subscriber: &UUri,
630        ) -> Result<Vec<crate::core::usubscription::SubscriptionInfo>, UStatus> {
631            Ok(Vec::new())
632        }
633        async fn register_for_notifications(&self, _topic: &UUri) -> Result<(), UStatus> {
634            Ok(())
635        }
636        async fn unregister_for_notifications(&self, _topic: &UUri) -> Result<(), UStatus> {
637            Ok(())
638        }
639        async fn fetch_subscribers(&self, _topic: &UUri) -> Result<Vec<UUri>, UStatus> {
640            Ok(Vec::new())
641        }
642        async fn reset(
643            &self,
644            _reason: crate::core::usubscription::ResetReason,
645            _message: Option<String>,
646        ) -> Result<(), UStatus> {
647            Ok(())
648        }
649    }
650
651    struct CountingListener(Mutex<usize>);
652
653    #[async_trait]
654    impl<Rx: crate::UZeroCopyRxLease + Send + 'static> UZeroCopyListener<Rx> for CountingListener {
655        async fn on_receive_zero_copy(&self, _frame: Rx) {
656            *self.0.lock().expect("count lock poisoned") += 1;
657        }
658    }
659
660    fn topic() -> UUri {
661        UUri::try_from_parts("demo", 0x1_0001, 1, 0x8001).expect("valid topic URI")
662    }
663
664    #[tokio::test]
665    async fn subscribe_consults_the_service_before_registering() {
666        let lifecycle = Arc::new(Mutex::new(Vec::new()));
667        let subscriber = Subscriber::new(
668            Arc::new(RecordingZeroCopyTransport::new(lifecycle.clone())),
669            Arc::new(StubUSubscription {
670                lifecycle: lifecycle.clone(),
671                decline: false,
672            }),
673        );
674
675        subscriber
676            .subscribe(&topic(), Arc::new(CountingListener(Mutex::new(0))))
677            .await
678            .expect("subscribe succeeds when the service reports Subscribed");
679
680        assert_eq!(
681            *lifecycle.lock().expect("lifecycle lock poisoned"),
682            ["subscribe", "register"]
683        );
684    }
685
686    #[tokio::test]
687    async fn subscribe_fails_and_registers_nothing_when_the_service_declines() {
688        let lifecycle = Arc::new(Mutex::new(Vec::new()));
689        let subscriber = Subscriber::new(
690            Arc::new(RecordingZeroCopyTransport::new(lifecycle.clone())),
691            Arc::new(StubUSubscription {
692                lifecycle: lifecycle.clone(),
693                decline: true,
694            }),
695        );
696
697        subscriber
698            .subscribe(&topic(), Arc::new(CountingListener(Mutex::new(0))))
699            .await
700            .expect_err("a declined subscription must not register a listener");
701
702        assert_eq!(
703            *lifecycle.lock().expect("lifecycle lock poisoned"),
704            ["subscribe"]
705        );
706    }
707
708    #[tokio::test]
709    async fn unsubscribe_informs_the_service_and_unregisters() {
710        let lifecycle = Arc::new(Mutex::new(Vec::new()));
711        let listener = Arc::new(CountingListener(Mutex::new(0)));
712        let subscriber = Subscriber::new(
713            Arc::new(RecordingZeroCopyTransport::new(lifecycle.clone())),
714            Arc::new(StubUSubscription {
715                lifecycle: lifecycle.clone(),
716                decline: false,
717            }),
718        );
719
720        subscriber
721            .subscribe(&topic(), listener.clone())
722            .await
723            .expect("subscribed");
724        subscriber
725            .unsubscribe(&topic(), listener)
726            .await
727            .expect("unsubscribe succeeds after subscribe");
728
729        assert_eq!(
730            *lifecycle.lock().expect("lifecycle lock poisoned"),
731            ["subscribe", "register", "unsubscribe", "unregister"]
732        );
733    }
734
735    #[tokio::test]
736    async fn unsubscribe_rejects_wildcards_before_contacting_the_service() {
737        let lifecycle = Arc::new(Mutex::new(Vec::new()));
738        let subscriber = Subscriber::new(
739            Arc::new(RecordingZeroCopyTransport::new(lifecycle.clone())),
740            Arc::new(StubUSubscription {
741                lifecycle: lifecycle.clone(),
742                decline: false,
743            }),
744        );
745        let invalid_topic =
746            UUri::try_from("up://my-vin/A15B/1/FFFF").expect("valid wildcard subscription filter");
747
748        let result = subscriber
749            .unsubscribe(&invalid_topic, Arc::new(CountingListener(Mutex::new(0))))
750            .await;
751
752        assert!(matches!(result, Err(RegistrationError::InvalidFilter(_))));
753        assert!(lifecycle
754            .lock()
755            .expect("lifecycle lock poisoned")
756            .is_empty());
757    }
758}