Skip to main content

sipx_testkit/
call.rs

1//! Application-level and transaction-level call harnesses for downstream tests.
2//!
3//! [`CallHarness`] drives [`sipx_call::dial`] and [`sipx_call::answer`] through ordinary
4//! [`sipx_transport::Handle`] values joined by an in-process signalling path. [`TransactionHarness`]
5//! is the lower-level deterministic fault and virtual-time surface for transaction tests.
6
7use std::net::IpAddr;
8use std::time::Duration;
9
10use bytes::Bytes;
11use sipx_sip::build::ResponseBuilder;
12use sipx_sip::error::BuildError;
13use sipx_sip::transaction::{
14    Dispatch, Output, Reliability, Timer, TransactionKey, TransactionLayer, TuEvent,
15};
16use sipx_sip::{HeaderName, Limits, Request, Response, StatusCode, parse_datagram};
17use sipx_transport::timers::TimerQueue;
18use sipx_transport::{Handle, Incoming, Target, TransportKind};
19use thiserror::Error;
20use tokio::sync::mpsc;
21use tokio::task::JoinHandle;
22
23use crate::link::{Faults, Link, Side};
24use crate::time::Virtual;
25
26/// A call harness operation that could not be performed.
27#[derive(Debug, Error)]
28#[non_exhaustive]
29pub enum HarnessError {
30    /// The request cannot create a client transaction (for example, it is an ACK).
31    #[error("the request has no client transaction key")]
32    NoClientTransaction,
33    /// No INVITE has reached the answering side yet.
34    #[error("no invitation is waiting to be answered")]
35    NoInvitation,
36    /// An answer helper was asked to use a status outside SIP's status range.
37    #[error("{0} is outside the SIP response status range")]
38    InvalidStatus(u16),
39    /// The response could not safely be built from the invitation.
40    #[error(transparent)]
41    Build(#[from] BuildError),
42    /// The in-process endpoint closed before the exchange completed.
43    #[error("the in-process endpoint closed")]
44    EndpointClosed,
45    /// The call framework rejected or could not establish the exchange.
46    #[error(transparent)]
47    Call(#[from] sipx_call::Error),
48    /// The dial task ended without returning its call result.
49    #[error("the dial task stopped before returning a call")]
50    DialTask,
51    /// The dial completed without ever delivering its invitation to the peer.
52    #[error("the dial completed before its invitation reached the peer")]
53    DialBeforeInvitation,
54    /// The request following the answer was not the dialog's ACK.
55    #[error("the established dialog did not deliver its ACK")]
56    MissingAck,
57    /// The signalling harness could not construct its transport boundary.
58    #[error(transparent)]
59    Transport(#[from] sipx_transport::Error),
60}
61
62#[derive(Debug)]
63struct Stack {
64    transactions: TransactionLayer,
65    timers: TimerQueue<(TransactionKey, Timer), Virtual>,
66}
67
68impl Stack {
69    fn new() -> Self {
70        Self {
71            transactions: TransactionLayer::new(sipx_sip::transaction::Timers::default()),
72            timers: TimerQueue::new(),
73        }
74    }
75
76    fn perform(
77        &mut self,
78        key: &TransactionKey,
79        outputs: Vec<Output>,
80        now: Virtual,
81        events: &mut Vec<TuEvent>,
82    ) -> Vec<Bytes> {
83        let mut wire = Vec::new();
84        for output in outputs {
85            match output {
86                Output::Send(message) => wire.push(message.to_bytes()),
87                Output::SetTimer { timer, after } => {
88                    self.timers.set((key.clone(), timer), now, after);
89                }
90                Output::ClearTimer(timer) => self.timers.clear(&(key.clone(), timer)),
91                Output::ToTu(event) => events.push(*event),
92                Output::Terminated(_) => self.timers.forget_matching(|(other, _)| other == key),
93            }
94        }
95        wire
96    }
97
98    fn fire(&mut self, now: Virtual, events: &mut Vec<TuEvent>) -> Vec<Bytes> {
99        let mut wire = Vec::new();
100        for (key, timer) in self.timers.take_due(now) {
101            let outputs = self.transactions.on_timer(&key, timer);
102            wire.extend(self.perform(&key, outputs, now, events));
103        }
104        wire
105    }
106
107    fn receive(&mut self, bytes: Bytes, now: Virtual, events: &mut Vec<TuEvent>) -> Vec<Bytes> {
108        let Ok(message) = parse_datagram(bytes, &Limits::datagram()) else {
109            return Vec::new();
110        };
111        match self.transactions.receive(message, Reliability::Unreliable) {
112            Dispatch::Created { key, outputs } | Dispatch::Matched { key, outputs } => {
113                self.perform(&key, outputs, now, events)
114            }
115            Dispatch::Unmatched(_) => Vec::new(),
116        }
117    }
118}
119
120/// A call harness that exercises the application API over an in-process signalling path.
121#[derive(Debug)]
122pub struct CallHarness {
123    caller: Handle,
124    callee: Handle,
125    callee_incoming: mpsc::Receiver<Incoming>,
126}
127
128/// One invitation whose dial task is waiting for an application answer.
129#[derive(Debug)]
130pub struct PendingCall<'a> {
131    invitation: Incoming,
132    dial: DialTask,
133    callee: &'a Handle,
134    callee_incoming: &'a mut mpsc::Receiver<Incoming>,
135}
136
137#[derive(Debug)]
138struct DialTask(Option<JoinHandle<Result<sipx_call::Call, sipx_call::Error>>>);
139
140impl DialTask {
141    async fn finish(&mut self) -> Result<sipx_call::Call, HarnessError> {
142        let Some(task) = self.0.as_mut() else {
143            return Err(HarnessError::DialTask);
144        };
145        let result = task
146            .await
147            .map_err(|_| HarnessError::DialTask)?
148            .map_err(HarnessError::from);
149        self.0.take();
150        result
151    }
152}
153
154impl Drop for DialTask {
155    fn drop(&mut self) {
156        if let Some(task) = self.0.as_ref() {
157            task.abort();
158        }
159    }
160}
161
162/// Both application call objects after the 2xx and its ACK crossed the in-process path.
163#[derive(Debug)]
164pub struct EstablishedCall {
165    /// The call returned by [`sipx_call::dial`].
166    pub caller: sipx_call::Call,
167    /// The call returned by [`sipx_call::answer`].
168    pub callee: sipx_call::Call,
169}
170
171impl CallHarness {
172    /// Create a harness with two ordinary transport handles and no signalling sockets.
173    ///
174    /// Call establishment still opens the RTP/RTCP ports owned by `sipx-call`; the in-process
175    /// boundary applies to SIP signalling, not media.
176    pub fn new() -> Result<Self, HarnessError> {
177        let ((originating, _originating_incoming), (answering, answering_incoming)) =
178            sipx_transport::in_process_pair(32)?;
179        Ok(Self {
180            caller: originating,
181            callee: answering,
182            callee_incoming: answering_incoming,
183        })
184    }
185
186    /// Begin a real [`sipx_call::dial`] and return only this exchange's invitation.
187    pub async fn dial(
188        &mut self,
189        to: sipx_sip::Uri,
190        options: sipx_call::DialOptions,
191    ) -> Result<PendingCall<'_>, HarnessError> {
192        let endpoint = self.caller.clone();
193        let target = Target::new(self.callee.local_addr(), TransportKind::Udp);
194        let mut dial = DialTask(Some(tokio::spawn(async move {
195            sipx_call::dial(&endpoint, target, &to, &options).await
196        })));
197        let invitation = tokio::select! {
198            result = dial.finish() => {
199                return match result {
200                    Ok(_) => Err(HarnessError::DialBeforeInvitation),
201                    Err(error) => Err(error),
202                };
203            }
204            incoming = self.callee_incoming.recv() => {
205                incoming.ok_or(HarnessError::EndpointClosed)?
206            }
207        };
208        if invitation.request.method != sipx_sip::Method::Invite {
209            return Err(HarnessError::NoInvitation);
210        }
211        Ok(PendingCall {
212            invitation,
213            dial,
214            callee: &self.callee,
215            callee_incoming: &mut self.callee_incoming,
216        })
217    }
218}
219
220impl PendingCall<'_> {
221    /// The exact invitation associated with this pending call.
222    #[must_use]
223    pub const fn invitation(&self) -> &Incoming {
224        &self.invitation
225    }
226
227    /// Answer through [`sipx_call::answer`] and wait until the matching ACK reaches the call.
228    pub async fn answer(self, media_address: IpAddr) -> Result<EstablishedCall, HarnessError> {
229        let PendingCall {
230            invitation,
231            mut dial,
232            callee,
233            callee_incoming,
234        } = self;
235        let answer = sipx_call::answer(callee, &invitation, media_address);
236        let (caller, mut callee_call) = tokio::try_join!(dial.finish(), async {
237            answer.await.map_err(HarnessError::from)
238        })?;
239        let ack = callee_incoming
240            .recv()
241            .await
242            .ok_or(HarnessError::EndpointClosed)?;
243        if ack.request.method != sipx_sip::Method::Ack || !callee_call.handle(&ack).await? {
244            return Err(HarnessError::MissingAck);
245        }
246        Ok(EstablishedCall {
247            caller,
248            callee: callee_call,
249        })
250    }
251}
252
253/// Two SIP transaction layers connected through a seeded virtual-time link.
254#[derive(Debug)]
255pub struct TransactionHarness {
256    now: Virtual,
257    link: Link<Virtual>,
258    caller: Stack,
259    callee: Stack,
260    caller_events: Vec<TuEvent>,
261    callee_events: Vec<TuEvent>,
262    caller_scope: usize,
263    callee_scope: usize,
264}
265
266impl TransactionHarness {
267    /// Start at virtual time zero with a reproducible faulty link.
268    #[must_use]
269    pub fn new(seed: u64, faults: Faults) -> Self {
270        Self {
271            now: Virtual::epoch(),
272            link: Link::new(seed, faults),
273            caller: Stack::new(),
274            callee: Stack::new(),
275            caller_events: Vec::new(),
276            callee_events: Vec::new(),
277            caller_scope: 0,
278            callee_scope: 0,
279        }
280    }
281
282    /// Start with a link that neither loses nor delays signalling.
283    #[must_use]
284    pub fn perfect() -> Self {
285        Self::new(0, Faults::default())
286    }
287
288    /// Place a request from the calling side and deliver everything due now.
289    pub fn place(&mut self, request: Request) -> Result<(), HarnessError> {
290        self.caller_scope = self.caller_events.len();
291        self.callee_scope = self.callee_events.len();
292        let Some((key, outputs)) = self
293            .caller
294            .transactions
295            .send_request(request, Reliability::Unreliable)
296        else {
297            return Err(HarnessError::NoClientTransaction);
298        };
299        let wire = self
300            .caller
301            .perform(&key, outputs, self.now, &mut self.caller_events);
302        self.send(Side::Left, wire);
303        self.pump();
304        Ok(())
305    }
306
307    /// Answer the most recent invitation delivered to the callee.
308    ///
309    /// The harness supplies deterministic `To` and `Contact` values. Tests that need exact header
310    /// policy can build a [`Response`] and use [`Self::answer_with`] instead.
311    pub fn answer(
312        &mut self,
313        status: StatusCode,
314        reason: impl Into<Bytes>,
315    ) -> Result<(), HarnessError> {
316        let Some(request) = self.invitation().cloned() else {
317            return Err(HarnessError::NoInvitation);
318        };
319        let Some(to) = request.headers.value(&HeaderName::To) else {
320            return Err(BuildError::MissingRequiredResponseHeader { header: "To" }.into());
321        };
322        let mut tagged_to = to.into_owned();
323        tagged_to.extend_from_slice(b";tag=sipx-testkit");
324        let contact = Bytes::from(format!("<{}>", request.uri));
325        let response = ResponseBuilder::to_request(&request, status, reason)?
326            .set_header(&HeaderName::To, Bytes::from(tagged_to))?
327            .header(HeaderName::Contact, contact)?
328            .build();
329        self.answer_with(response)
330    }
331
332    /// Answer the pending invitation with `200 OK`.
333    pub fn answer_ok(&mut self) -> Result<(), HarnessError> {
334        let ok = StatusCode::new(200).ok_or(HarnessError::InvalidStatus(200))?;
335        self.answer(ok, "OK")
336    }
337
338    /// Send an application-built response from the answering side.
339    pub fn answer_with(&mut self, response: Response) -> Result<(), HarnessError> {
340        let Some(request) = self.invitation() else {
341            return Err(HarnessError::NoInvitation);
342        };
343        let Some(key) = TransactionKey::from_request(request) else {
344            return Err(HarnessError::NoInvitation);
345        };
346        let outputs = self.callee.transactions.send_response(&key, response);
347        let wire = self
348            .callee
349            .perform(&key, outputs, self.now, &mut self.callee_events);
350        self.send(Side::Right, wire);
351        self.pump();
352        Ok(())
353    }
354
355    /// Move virtual time forward, fire transaction timers, and deliver packets now due.
356    pub fn advance(&mut self, by: Duration) {
357        let until = self.now + by;
358        while self.now < until {
359            let next = [
360                self.link.next_arrival(),
361                self.caller.timers.next_deadline(),
362                self.callee.timers.next_deadline(),
363                Some(until),
364            ]
365            .into_iter()
366            .flatten()
367            .filter(|instant| *instant >= self.now)
368            .min()
369            .unwrap_or(until);
370            self.now = next;
371            self.pump();
372            let wire = self.caller.fire(self.now, &mut self.caller_events);
373            self.send(Side::Left, wire);
374            let wire = self.callee.fire(self.now, &mut self.callee_events);
375            self.send(Side::Right, wire);
376            self.pump();
377            if self.now == until {
378                break;
379            }
380        }
381    }
382
383    /// Current virtual time.
384    #[must_use]
385    pub const fn now(&self) -> Virtual {
386        self.now
387    }
388
389    /// The most recently delivered INVITE, if one reached the callee.
390    #[must_use]
391    pub fn invitation(&self) -> Option<&Request> {
392        self.callee_events
393            .get(self.callee_scope..)
394            .unwrap_or(&[])
395            .iter()
396            .rev()
397            .find_map(|event| match event {
398                TuEvent::Request(request) if request.method == sipx_sip::Method::Invite => {
399                    Some(request.as_ref())
400                }
401                _ => None,
402            })
403    }
404
405    /// The most recently delivered response, if one reached the caller.
406    #[must_use]
407    pub fn response(&self) -> Option<&Response> {
408        self.caller_events
409            .get(self.caller_scope..)
410            .unwrap_or(&[])
411            .iter()
412            .rev()
413            .find_map(|event| match event {
414                TuEvent::Response(response) => Some(response.as_ref()),
415                _ => None,
416            })
417    }
418
419    /// How many datagrams the configured link discarded.
420    #[must_use]
421    pub fn dropped(&self) -> u64 {
422        self.link.dropped()
423    }
424
425    fn send(&mut self, from: Side, wire: Vec<Bytes>) {
426        for bytes in wire {
427            self.link.send(from, bytes, self.now);
428        }
429    }
430
431    fn pump(&mut self) {
432        loop {
433            let deliveries = self.link.take_due(self.now);
434            if deliveries.is_empty() {
435                break;
436            }
437            for delivery in deliveries {
438                let wire = match delivery.to {
439                    Side::Left => {
440                        self.caller
441                            .receive(delivery.bytes, self.now, &mut self.caller_events)
442                    }
443                    Side::Right => {
444                        self.callee
445                            .receive(delivery.bytes, self.now, &mut self.callee_events)
446                    }
447                };
448                self.send(delivery.to, wire);
449            }
450        }
451    }
452}
453
454#[cfg(test)]
455#[allow(
456    clippy::unwrap_used,
457    clippy::expect_used,
458    clippy::panic,
459    clippy::indexing_slicing
460)]
461mod tests {
462    use std::future::{Future, poll_fn};
463    use std::task::Poll;
464
465    use tokio::sync::oneshot;
466
467    use super::DialTask;
468
469    struct OnDrop(Option<oneshot::Sender<()>>);
470
471    impl Drop for OnDrop {
472        fn drop(&mut self) {
473            if let Some(sender) = self.0.take() {
474                let _ = sender.send(());
475            }
476        }
477    }
478
479    #[tokio::test]
480    async fn dropping_a_pending_dial_aborts_its_owned_task() {
481        let (started, running) = oneshot::channel();
482        let (dropped, cancelled) = oneshot::channel();
483        let task = tokio::spawn(async move {
484            let _on_drop = OnDrop(Some(dropped));
485            let _ = started.send(());
486            std::future::pending::<Result<sipx_call::Call, sipx_call::Error>>().await
487        });
488        let dial = DialTask(Some(task));
489        running.await.expect("dial task started");
490
491        drop(dial);
492
493        cancelled.await.expect("dial task was cancelled");
494    }
495
496    #[tokio::test]
497    async fn cancelling_finish_after_it_was_polled_still_aborts_the_dial() {
498        let (started, running) = oneshot::channel();
499        let (dropped, cancelled) = oneshot::channel();
500        let task = tokio::spawn(async move {
501            let _on_drop = OnDrop(Some(dropped));
502            let _ = started.send(());
503            std::future::pending::<Result<sipx_call::Call, sipx_call::Error>>().await
504        });
505        let mut dial = DialTask(Some(task));
506        running.await.expect("dial task started");
507        let mut finish = Box::pin(dial.finish());
508        poll_fn(|context| {
509            assert!(matches!(finish.as_mut().poll(context), Poll::Pending));
510            Poll::Ready(())
511        })
512        .await;
513
514        drop(finish);
515        drop(dial);
516
517        cancelled.await.expect("polled dial task was cancelled");
518    }
519}