Skip to main content

pamoja_ffi/
ladder.rs

1//! The C ABI for the cost-aware transport ladder.
2//!
3//! These functions wrap [`pamoja_ladder`] for callers that reach the SDK through
4//! the flat C boundary. A ladder is the answer to a node that has more than one
5//! way to reach the network and no single one that always works: rungs are tried
6//! in the order they were added, cheapest first, and a message no rung accepts
7//! goes into a buffer rather than being lost.
8//!
9//! The Rust ladder is generic over its buffer, which cannot cross a C ABI, so
10//! this one is built over the store handle from [`crate::sync`]. That handle
11//! already covers both an in-memory and a file-backed queue, so nothing is given
12//! up: a caller still chooses whether the buffer survives a restart.
13
14use std::ffi::c_char;
15use std::ptr;
16
17use pamoja_ladder::{Delivery, TransportLadder};
18
19use crate::sync::{take_store, PamojaStore, StoreKind};
20use crate::transport::{receive_within, take_transport, PamojaMessage, PamojaTransport};
21use crate::{read_bytes, read_str, runtime, set_last_error, PamojaStatus};
22
23/// What became of a message handed to a ladder.
24#[repr(C)]
25#[derive(Clone, Copy, Debug, PartialEq, Eq)]
26pub enum PamojaDelivery {
27    /// A rung took the message and it is on its way.
28    Sent = 0,
29    /// No rung would take it, so it is in the buffer awaiting a flush.
30    Buffered = 1,
31}
32
33/// An opaque handle to a ladder and the buffer behind it.
34///
35/// The Rust builder takes the ladder by value to add a rung, which would move
36/// the handle out from under a caller holding a pointer to it. Holding the
37/// ladder in an option lets it be taken and put back so the handle address stays
38/// good for the life of the ladder.
39pub struct PamojaLadder {
40    inner: Option<TransportLadder<StoreKind>>,
41}
42
43/// Creates a ladder with no rungs, buffering into a store.
44///
45/// # Arguments
46///
47/// * `store` - the buffer to hold messages no rung would take, consumed by this
48///   call.
49///
50/// # Returns
51///
52/// A handle the caller must release with [`pamoja_ladder_free`], or null if
53/// `store` is null.
54///
55/// # Safety
56///
57/// `store` must be a live handle from [`crate::sync`] that has not been freed or
58/// consumed. After this call it must not be used again, whatever the result.
59#[no_mangle]
60pub unsafe extern "C" fn pamoja_ladder_new(store: *mut PamojaStore) -> *mut PamojaLadder {
61    let Some(store) = take_store(store) else {
62        return ptr::null_mut();
63    };
64    Box::into_raw(Box::new(PamojaLadder {
65        inner: Some(TransportLadder::new(store)),
66    }))
67}
68
69/// Adds a rung, which is tried after the rungs already added.
70///
71/// Add the cheapest, most-preferred link first and the costliest fallback last,
72/// because a send takes the first rung that accepts it. A transport that delivers
73/// is subscribed and listened on; one that only sends, a host transport without a
74/// `recv` callback, is an uplink the ladder never listens on.
75///
76/// # Arguments
77///
78/// * `ladder` - the ladder to add to.
79/// * `transport` - the transport to add, consumed by this call.
80///
81/// # Returns
82///
83/// [`PamojaStatus::Ok`] once the rung is added.
84///
85/// # Safety
86///
87/// `ladder` must be a live handle, and `transport` a live transport handle that
88/// has not been freed or consumed. After this call the transport must not be
89/// used again, whatever the result.
90#[no_mangle]
91pub unsafe extern "C" fn pamoja_ladder_rung(
92    ladder: *mut PamojaLadder,
93    transport: *mut PamojaTransport,
94) -> PamojaStatus {
95    add(ladder, transport, false)
96}
97
98/// Adds a rung that only sends, tried after the rungs already added.
99///
100/// The ladder sends over it in its turn but never subscribes it or listens on it,
101/// whatever the transport could do. This is the shape of a satellite messenger, a
102/// LoRa uplink, or any link a node reports over but takes no commands from.
103///
104/// # Arguments
105///
106/// * `ladder` - the ladder to add to.
107/// * `transport` - the transport to add, consumed by this call.
108///
109/// # Returns
110///
111/// [`PamojaStatus::Ok`] once the rung is added.
112///
113/// # Safety
114///
115/// `ladder` must be a live handle, and `transport` a live transport handle that
116/// has not been freed or consumed. After this call the transport must not be
117/// used again, whatever the result.
118#[no_mangle]
119pub unsafe extern "C" fn pamoja_ladder_uplink(
120    ladder: *mut PamojaLadder,
121    transport: *mut PamojaTransport,
122) -> PamojaStatus {
123    add(ladder, transport, true)
124}
125
126/// Adds a transport to a ladder, as an uplink when asked or when it cannot
127/// deliver, and as a rung that is listened on otherwise.
128///
129/// # Safety
130///
131/// As for [`pamoja_ladder_rung`].
132unsafe fn add(
133    ladder: *mut PamojaLadder,
134    transport: *mut PamojaTransport,
135    uplink: bool,
136) -> PamojaStatus {
137    let Some(handle) = ladder_handle(ladder) else {
138        // The transport was promised to this call, so it is released rather than
139        // left for a caller who has already been told not to touch it again.
140        drop(take_transport(transport));
141        return PamojaStatus::InvalidArgument;
142    };
143    let Some(transport) = take_transport(transport) else {
144        return PamojaStatus::InvalidArgument;
145    };
146    let Some(inner) = handle.inner.take() else {
147        set_last_error("this ladder is no longer usable".to_owned());
148        return PamojaStatus::InvalidArgument;
149    };
150    handle.inner = Some(if uplink || !transport.listens() {
151        inner.uplink(transport)
152    } else {
153        inner.rung(transport)
154    });
155    PamojaStatus::Ok
156}
157
158/// Connects every rung, so a send can be tried against each in turn.
159///
160/// A rung that will not connect is left in the ladder: it may come back, and a
161/// send simply falls through it until it does.
162///
163/// # Arguments
164///
165/// * `ladder` - the ladder.
166///
167/// # Returns
168///
169/// [`PamojaStatus::Ok`] once the rungs have been tried.
170///
171/// # Safety
172///
173/// `ladder` must be a live handle from [`pamoja_ladder_new`].
174#[no_mangle]
175pub unsafe extern "C" fn pamoja_ladder_connect(ladder: *mut PamojaLadder) -> PamojaStatus {
176    let Some(inner) = ladder_inner(ladder) else {
177        return PamojaStatus::InvalidArgument;
178    };
179    match runtime().block_on(inner.connect()) {
180        Ok(()) => PamojaStatus::Ok,
181        Err(error) => fail(error),
182    }
183}
184
185/// Sends a payload, falling through the rungs and buffering if none take it.
186///
187/// # Arguments
188///
189/// * `ladder` - the ladder.
190/// * `topic` - the destination topic, as null-terminated UTF-8.
191/// * `payload` - the bytes to send.
192/// * `payload_len` - the length of `payload`.
193/// * `out_delivery` - receives whether the message went out or was buffered.
194///
195/// # Returns
196///
197/// [`PamojaStatus::Ok`] on success, with `out_delivery` saying which happened.
198/// Buffering is a success, not a failure: it is what the ladder exists to do.
199///
200/// # Safety
201///
202/// `ladder` must be a live handle, `topic` a valid null-terminated UTF-8 string,
203/// `payload` must point to at least `payload_len` readable bytes or be null when
204/// that length is 0, and `out_delivery` must be writable or null.
205#[no_mangle]
206pub unsafe extern "C" fn pamoja_ladder_send(
207    ladder: *mut PamojaLadder,
208    topic: *const c_char,
209    payload: *const u8,
210    payload_len: usize,
211    out_delivery: *mut PamojaDelivery,
212) -> PamojaStatus {
213    let Some(topic) = read_str(topic, "topic") else {
214        return PamojaStatus::InvalidArgument;
215    };
216    let payload = match read_bytes(payload, payload_len) {
217        Ok(payload) => payload,
218        Err(status) => return status,
219    };
220    let Some(inner) = ladder_inner(ladder) else {
221        return PamojaStatus::InvalidArgument;
222    };
223    match runtime().block_on(inner.send(topic, &payload)) {
224        Ok(delivery) => {
225            if !out_delivery.is_null() {
226                *out_delivery = match delivery {
227                    Delivery::Sent => PamojaDelivery::Sent,
228                    Delivery::Buffered => PamojaDelivery::Buffered,
229                };
230            }
231            PamojaStatus::Ok
232        }
233        Err(error) => fail(error),
234    }
235}
236
237/// Replays the buffer over the rungs, oldest message first.
238///
239/// # Arguments
240///
241/// * `ladder` - the ladder.
242/// * `out_sent` - receives how many buffered messages went out, or may be null.
243///
244/// # Returns
245///
246/// [`PamojaStatus::Ok`] on success.
247///
248/// # Safety
249///
250/// `ladder` must be a live handle and `out_sent` writable or null.
251#[no_mangle]
252pub unsafe extern "C" fn pamoja_ladder_flush(
253    ladder: *mut PamojaLadder,
254    out_sent: *mut usize,
255) -> PamojaStatus {
256    let Some(inner) = ladder_inner(ladder) else {
257        return PamojaStatus::InvalidArgument;
258    };
259    match runtime().block_on(inner.flush()) {
260        Ok(sent) => {
261            if !out_sent.is_null() {
262                *out_sent = sent;
263            }
264            PamojaStatus::Ok
265        }
266        Err(error) => fail(error),
267    }
268}
269
270/// Reports how many messages are waiting in the buffer.
271///
272/// # Arguments
273///
274/// * `ladder` - the ladder.
275/// * `out_count` - receives the count.
276///
277/// # Returns
278///
279/// [`PamojaStatus::Ok`] on success.
280///
281/// # Safety
282///
283/// `ladder` must be a live handle and `out_count` must be writable.
284#[no_mangle]
285pub unsafe extern "C" fn pamoja_ladder_buffered(
286    ladder: *mut PamojaLadder,
287    out_count: *mut usize,
288) -> PamojaStatus {
289    let Some(inner) = ladder_inner(ladder) else {
290        return PamojaStatus::InvalidArgument;
291    };
292    if out_count.is_null() {
293        set_last_error("out_count must not be null".to_owned());
294        return PamojaStatus::InvalidArgument;
295    }
296    match runtime().block_on(inner.buffered()) {
297        Ok(count) => {
298            *out_count = count;
299            PamojaStatus::Ok
300        }
301        Err(error) => fail(error),
302    }
303}
304
305/// Subscribes every rung that listens to a topic.
306///
307/// The filter is kept for the life of the ladder, so a rung that is down when it
308/// is placed receives it when it next connects. Subscribing while no rung is up
309/// succeeds, the way a send with no rung up is buffered rather than refused.
310///
311/// # Arguments
312///
313/// * `ladder` - the ladder.
314/// * `topic` - the topic filter, as null-terminated UTF-8.
315///
316/// # Returns
317///
318/// [`PamojaStatus::Ok`] once the filter is live on a rung or held for the next
319/// connect, or the status of the refusal if every connected rung refused it.
320///
321/// # Safety
322///
323/// `ladder` must be a live handle and `topic` a valid null-terminated UTF-8
324/// string.
325#[no_mangle]
326pub unsafe extern "C" fn pamoja_ladder_subscribe(
327    ladder: *mut PamojaLadder,
328    topic: *const c_char,
329) -> PamojaStatus {
330    let Some(topic) = read_str(topic, "topic") else {
331        return PamojaStatus::InvalidArgument;
332    };
333    let Some(inner) = ladder_inner(ladder) else {
334        return PamojaStatus::InvalidArgument;
335    };
336    match runtime().block_on(inner.subscribe(topic)) {
337        Ok(()) => PamojaStatus::Ok,
338        Err(error) => fail(error),
339    }
340}
341
342/// Waits for the next message from any rung that listens.
343///
344/// Every connected rung that listens is polled together and the first to deliver
345/// wins. A rung whose link ends is left out until the next
346/// [`pamoja_ladder_connect`].
347///
348/// # Arguments
349///
350/// * `ladder` - the ladder.
351/// * `out_message` - receives a message handle to release with
352///   [`pamoja_message_free`](crate::transport::pamoja_message_free).
353///
354/// # Returns
355///
356/// [`PamojaStatus::Ok`] with a message, or [`PamojaStatus::Closed`] if no
357/// connected rung listens: none was added, the ladder is not connected, or every
358/// listening link has ended.
359///
360/// # Safety
361///
362/// `ladder` must be a live handle and `out_message` must be writable.
363#[no_mangle]
364pub unsafe extern "C" fn pamoja_ladder_recv(
365    ladder: *mut PamojaLadder,
366    out_message: *mut *mut PamojaMessage,
367) -> PamojaStatus {
368    let Some(inner) = ladder_inner(ladder) else {
369        return PamojaStatus::InvalidArgument;
370    };
371    if out_message.is_null() {
372        set_last_error("out_message must not be null".to_owned());
373        return PamojaStatus::InvalidArgument;
374    }
375    *out_message = ptr::null_mut();
376    match runtime().block_on(inner.recv()) {
377        Ok(Some(message)) => {
378            *out_message = PamojaMessage::into_raw(message.topic, message.payload);
379            PamojaStatus::Ok
380        }
381        Ok(None) => PamojaStatus::Ok,
382        Err(error) => fail(error),
383    }
384}
385
386/// Waits a limited time for the next message from any rung that listens.
387///
388/// Running out of time loses nothing: a message that arrives afterwards waits for
389/// the next receive.
390///
391/// # Arguments
392///
393/// * `ladder` - the ladder.
394/// * `timeout_ms` - how long to wait, in milliseconds.
395/// * `out_message` - receives a message handle to release with
396///   [`pamoja_message_free`](crate::transport::pamoja_message_free), or null when
397///   the time ran out.
398/// * `out_timed_out` - receives whether the time ran out before a message arrived.
399///
400/// # Returns
401///
402/// [`PamojaStatus::Ok`] with a message or with the time run out, or
403/// [`PamojaStatus::Closed`] if no connected rung listens.
404///
405/// # Safety
406///
407/// `ladder` must be a live handle, and `out_message` and `out_timed_out` must be
408/// writable.
409#[no_mangle]
410pub unsafe extern "C" fn pamoja_ladder_recv_within(
411    ladder: *mut PamojaLadder,
412    timeout_ms: u64,
413    out_message: *mut *mut PamojaMessage,
414    out_timed_out: *mut bool,
415) -> PamojaStatus {
416    let Some(inner) = ladder_inner(ladder) else {
417        return PamojaStatus::InvalidArgument;
418    };
419    receive_within(timeout_ms, out_message, out_timed_out, || inner.recv())
420}
421
422/// Releases a ladder handle, and the rungs and buffer it owns.
423///
424/// Passing null is a no-op.
425///
426/// # Safety
427///
428/// `ladder` must be a handle from [`pamoja_ladder_new`] that has not already
429/// been freed, or null. After this call it must not be used again.
430#[no_mangle]
431pub unsafe extern "C" fn pamoja_ladder_free(ladder: *mut PamojaLadder) {
432    if !ladder.is_null() {
433        drop(Box::from_raw(ladder));
434    }
435}
436
437/// Borrows a ladder handle, rejecting a null pointer.
438///
439/// # Safety
440///
441/// `ladder` must be a live handle from [`pamoja_ladder_new`], or null.
442unsafe fn ladder_handle<'a>(ladder: *mut PamojaLadder) -> Option<&'a mut PamojaLadder> {
443    if ladder.is_null() {
444        set_last_error("ladder must not be null".to_owned());
445        return None;
446    }
447    Some(&mut *ladder)
448}
449
450/// Borrows the ladder inside a handle, rejecting a null or spent one.
451///
452/// # Safety
453///
454/// `ladder` must be a live handle from [`pamoja_ladder_new`], or null.
455unsafe fn ladder_inner<'a>(
456    ladder: *mut PamojaLadder,
457) -> Option<&'a mut TransportLadder<StoreKind>> {
458    let handle = ladder_handle(ladder)?;
459    match handle.inner.as_mut() {
460        Some(inner) => Some(inner),
461        None => {
462            set_last_error("this ladder is no longer usable".to_owned());
463            None
464        }
465    }
466}
467
468/// Records an error and maps it onto a status.
469fn fail(error: pamoja_core::Error) -> PamojaStatus {
470    let status = PamojaStatus::from_error(&error);
471    set_last_error(error.to_string());
472    status
473}
474
475#[cfg(test)]
476mod tests {
477    use super::*;
478    use crate::loopback::{
479        pamoja_loopback_broker_free, pamoja_loopback_broker_is_reachable,
480        pamoja_loopback_broker_new, pamoja_loopback_broker_set_reachable,
481        pamoja_loopback_transport_connect, pamoja_loopback_transport_free,
482        pamoja_loopback_transport_new, pamoja_loopback_transport_recv,
483        pamoja_loopback_transport_send, pamoja_loopback_transport_subscribe,
484        pamoja_transport_loopback,
485    };
486    use crate::sync::pamoja_store_memory;
487    use crate::transport::{
488        pamoja_message_free, pamoja_message_payload, pamoja_message_payload_len,
489        pamoja_transport_faulty,
490    };
491
492    #[test]
493    fn a_message_no_rung_takes_is_buffered_rather_than_lost() {
494        unsafe {
495            let ladder = pamoja_ladder_new(pamoja_store_memory(0));
496            let topic = std::ffi::CString::new("sensors/1").expect("static");
497
498            let mut delivery = PamojaDelivery::Sent;
499            assert_eq!(
500                pamoja_ladder_send(ladder, topic.as_ptr(), b"21.5".as_ptr(), 4, &mut delivery),
501                PamojaStatus::Ok,
502                "buffering is a success, not a failure"
503            );
504            assert_eq!(delivery, PamojaDelivery::Buffered);
505
506            let mut waiting = 0;
507            assert_eq!(
508                pamoja_ladder_buffered(ladder, &mut waiting),
509                PamojaStatus::Ok
510            );
511            assert_eq!(waiting, 1);
512
513            pamoja_ladder_free(ladder);
514        }
515    }
516
517    #[test]
518    fn a_rung_that_refuses_falls_through_to_the_next() {
519        unsafe {
520            let broker = pamoja_loopback_broker_new();
521            let listener = pamoja_loopback_transport_new(broker);
522            pamoja_loopback_transport_connect(listener);
523            let topic = std::ffi::CString::new("sensors/1").expect("static");
524            pamoja_loopback_transport_subscribe(listener, topic.as_ptr());
525
526            // The first rung fails its next send; the second is the same broker.
527            let failing = pamoja_transport_faulty(pamoja_transport_loopback(broker), 1);
528            let working = pamoja_transport_loopback(broker);
529
530            let ladder = pamoja_ladder_new(pamoja_store_memory(0));
531            assert_eq!(pamoja_ladder_rung(ladder, failing), PamojaStatus::Ok);
532            assert_eq!(pamoja_ladder_rung(ladder, working), PamojaStatus::Ok);
533            assert_eq!(pamoja_ladder_connect(ladder), PamojaStatus::Ok);
534
535            let mut delivery = PamojaDelivery::Buffered;
536            assert_eq!(
537                pamoja_ladder_send(ladder, topic.as_ptr(), b"21.5".as_ptr(), 4, &mut delivery),
538                PamojaStatus::Ok
539            );
540            assert_eq!(
541                delivery,
542                PamojaDelivery::Sent,
543                "the second rung carried what the first refused"
544            );
545
546            let mut message = ptr::null_mut();
547            pamoja_loopback_transport_recv(listener, &mut message);
548            assert!(!message.is_null());
549            let payload = std::slice::from_raw_parts(
550                pamoja_message_payload(message),
551                pamoja_message_payload_len(message),
552            )
553            .to_vec();
554            assert_eq!(payload, b"21.5");
555            pamoja_message_free(message);
556
557            pamoja_ladder_free(ladder);
558            pamoja_loopback_transport_free(listener);
559            pamoja_loopback_broker_free(broker);
560        }
561    }
562
563    #[test]
564    fn a_flush_replays_what_was_buffered() {
565        unsafe {
566            let broker = pamoja_loopback_broker_new();
567            let listener = pamoja_loopback_transport_new(broker);
568            pamoja_loopback_transport_connect(listener);
569            let topic = std::ffi::CString::new("sensors/1").expect("static");
570            pamoja_loopback_transport_subscribe(listener, topic.as_ptr());
571
572            let ladder = pamoja_ladder_new(pamoja_store_memory(0));
573
574            // Nothing to send over yet, so it buffers.
575            pamoja_ladder_send(ladder, topic.as_ptr(), b"one".as_ptr(), 3, ptr::null_mut());
576            pamoja_ladder_send(ladder, topic.as_ptr(), b"two".as_ptr(), 3, ptr::null_mut());
577
578            // The link comes back.
579            assert_eq!(
580                pamoja_ladder_rung(ladder, pamoja_transport_loopback(broker)),
581                PamojaStatus::Ok
582            );
583            assert_eq!(pamoja_ladder_connect(ladder), PamojaStatus::Ok);
584
585            let mut sent = 0;
586            assert_eq!(pamoja_ladder_flush(ladder, &mut sent), PamojaStatus::Ok);
587            assert_eq!(sent, 2, "both buffered messages went out");
588
589            let mut waiting = 1;
590            pamoja_ladder_buffered(ladder, &mut waiting);
591            assert_eq!(waiting, 0, "and the buffer is empty");
592
593            pamoja_ladder_free(ladder);
594            pamoja_loopback_transport_free(listener);
595            pamoja_loopback_broker_free(broker);
596        }
597    }
598
599    #[test]
600    fn a_command_comes_back_through_the_ladder() {
601        unsafe {
602            let broker = pamoja_loopback_broker_new();
603            let ladder = pamoja_ladder_new(pamoja_store_memory(0));
604            assert_eq!(
605                pamoja_ladder_rung(ladder, pamoja_transport_loopback(broker)),
606                PamojaStatus::Ok
607            );
608            assert_eq!(pamoja_ladder_connect(ladder), PamojaStatus::Ok);
609            let topic = std::ffi::CString::new("commands/1").expect("static");
610            assert_eq!(
611                pamoja_ladder_subscribe(ladder, topic.as_ptr()),
612                PamojaStatus::Ok
613            );
614
615            let upstream = pamoja_loopback_transport_new(broker);
616            pamoja_loopback_transport_connect(upstream);
617            pamoja_loopback_transport_send(upstream, topic.as_ptr(), b"open".as_ptr(), 4);
618
619            let mut message = ptr::null_mut();
620            assert_eq!(pamoja_ladder_recv(ladder, &mut message), PamojaStatus::Ok);
621            assert!(
622                !message.is_null(),
623                "the command came back through the ladder"
624            );
625            let payload = std::slice::from_raw_parts(
626                pamoja_message_payload(message),
627                pamoja_message_payload_len(message),
628            )
629            .to_vec();
630            assert_eq!(payload, b"open");
631            pamoja_message_free(message);
632
633            pamoja_ladder_free(ladder);
634            pamoja_loopback_transport_free(upstream);
635            pamoja_loopback_broker_free(broker);
636        }
637    }
638
639    #[test]
640    fn a_timed_receive_runs_out_and_leaves_the_next_message_queued() {
641        unsafe {
642            let broker = pamoja_loopback_broker_new();
643            let ladder = pamoja_ladder_new(pamoja_store_memory(0));
644            pamoja_ladder_rung(ladder, pamoja_transport_loopback(broker));
645            pamoja_ladder_connect(ladder);
646            let topic = std::ffi::CString::new("commands/1").expect("static");
647            pamoja_ladder_subscribe(ladder, topic.as_ptr());
648
649            let mut message = ptr::null_mut();
650            let mut timed_out = false;
651            assert_eq!(
652                pamoja_ladder_recv_within(ladder, 20, &mut message, &mut timed_out),
653                PamojaStatus::Ok
654            );
655            assert!(timed_out && message.is_null());
656
657            let upstream = pamoja_loopback_transport_new(broker);
658            pamoja_loopback_transport_connect(upstream);
659            pamoja_loopback_transport_send(upstream, topic.as_ptr(), b"open".as_ptr(), 4);
660            assert_eq!(
661                pamoja_ladder_recv_within(ladder, 1000, &mut message, &mut timed_out),
662                PamojaStatus::Ok
663            );
664            assert!(!timed_out && !message.is_null());
665            let payload = std::slice::from_raw_parts(
666                pamoja_message_payload(message),
667                pamoja_message_payload_len(message),
668            )
669            .to_vec();
670            assert_eq!(payload, b"open");
671            pamoja_message_free(message);
672
673            pamoja_ladder_free(ladder);
674            pamoja_loopback_transport_free(upstream);
675            pamoja_loopback_broker_free(broker);
676        }
677    }
678
679    #[test]
680    fn an_uplink_carries_what_an_unreachable_rung_refuses_and_is_never_listened_on() {
681        unsafe {
682            let near = pamoja_loopback_broker_new();
683            let far = pamoja_loopback_broker_new();
684            let ashore = pamoja_loopback_transport_new(far);
685            pamoja_loopback_transport_connect(ashore);
686            let reports = std::ffi::CString::new("reports").expect("static");
687            let orders = std::ffi::CString::new("orders").expect("static");
688            pamoja_loopback_transport_subscribe(ashore, reports.as_ptr());
689
690            let ladder = pamoja_ladder_new(pamoja_store_memory(0));
691            assert_eq!(
692                pamoja_ladder_rung(ladder, pamoja_transport_loopback(near)),
693                PamojaStatus::Ok
694            );
695            assert_eq!(
696                pamoja_ladder_uplink(ladder, pamoja_transport_loopback(far)),
697                PamojaStatus::Ok
698            );
699            pamoja_ladder_connect(ladder);
700            assert_eq!(
701                pamoja_ladder_subscribe(ladder, orders.as_ptr()),
702                PamojaStatus::Ok
703            );
704
705            assert_eq!(
706                pamoja_loopback_broker_set_reachable(near, false),
707                PamojaStatus::Ok
708            );
709            assert!(!pamoja_loopback_broker_is_reachable(near));
710            let mut delivery = PamojaDelivery::Buffered;
711            pamoja_ladder_send(ladder, reports.as_ptr(), b"1".as_ptr(), 1, &mut delivery);
712            assert_eq!(delivery, PamojaDelivery::Sent, "the uplink carried it");
713            let mut message = ptr::null_mut();
714            pamoja_loopback_transport_recv(ashore, &mut message);
715            assert!(!message.is_null());
716            pamoja_message_free(message);
717
718            pamoja_loopback_transport_send(ashore, orders.as_ptr(), b"stop".as_ptr(), 4);
719            let mut timed_out = false;
720            pamoja_ladder_recv_within(ladder, 20, &mut message, &mut timed_out);
721            assert!(timed_out, "an uplink is never listened on");
722
723            pamoja_loopback_broker_set_reachable(far, false);
724            pamoja_ladder_send(ladder, reports.as_ptr(), b"2".as_ptr(), 1, &mut delivery);
725            assert_eq!(
726                delivery,
727                PamojaDelivery::Buffered,
728                "with every link out of reach"
729            );
730
731            pamoja_ladder_free(ladder);
732            pamoja_loopback_transport_free(ashore);
733            pamoja_loopback_broker_free(near);
734            pamoja_loopback_broker_free(far);
735        }
736    }
737
738    #[test]
739    fn a_receive_with_no_listening_rung_reports_closed() {
740        unsafe {
741            let ladder = pamoja_ladder_new(pamoja_store_memory(0));
742            let mut message = ptr::null_mut();
743            assert_eq!(
744                pamoja_ladder_recv(ladder, &mut message),
745                PamojaStatus::Closed
746            );
747            assert!(message.is_null());
748            pamoja_ladder_free(ladder);
749        }
750    }
751
752    #[test]
753    fn a_null_handle_is_refused_rather_than_dereferenced() {
754        unsafe {
755            assert!(pamoja_ladder_new(ptr::null_mut()).is_null());
756            assert_eq!(
757                pamoja_ladder_connect(ptr::null_mut()),
758                PamojaStatus::InvalidArgument
759            );
760            assert_eq!(
761                pamoja_ladder_buffered(ptr::null_mut(), ptr::null_mut()),
762                PamojaStatus::InvalidArgument
763            );
764            pamoja_ladder_free(ptr::null_mut());
765        }
766    }
767}