From 0d69255e9e03d6acf584e3f66e84064898fd7483 Mon Sep 17 00:00:00 2001 From: Nils Andreas Svee Date: Thu, 24 Sep 2026 16:51:03 +0700 Subject: [PATCH] docs(client): add executable API examples --- crates/irtt-client/src/async_client.rs | 25 ++++++++++++++ crates/irtt-client/src/client.rs | 25 ++++++++++++++ crates/irtt-client/src/config.rs | 48 ++++++++++++++++++++++++++ crates/irtt-client/src/event.rs | 13 +++++++ crates/irtt-client/src/managed/task.rs | 36 +++++++++++++++++++ 5 files changed, 147 insertions(+) diff --git a/crates/irtt-client/src/async_client.rs b/crates/irtt-client/src/async_client.rs index c531c47..37c9cc2 100644 --- a/crates/irtt-client/src/async_client.rs +++ b/crates/irtt-client/src/async_client.rs @@ -242,6 +242,31 @@ impl AsyncClientTestHooks { /// `AsyncClient` does not construct, own, or store a Tokio runtime. Its async /// methods are polled by the caller and require a current Tokio runtime with I/O /// and time enabled. +/// +/// # Example +/// +/// Use this low-level adapter when the application already owns Tokio and wants +/// to drive the session lifecycle itself. This example type-checks without +/// contacting a server. +/// +/// ```no_run +/// use irtt_client::{AsyncClient, ClientConfig}; +/// +/// # async fn run() -> Result<(), Box> { +/// let mut client = AsyncClient::connect(ClientConfig::default()).await?; +/// let outcome = client.open().await?; +/// println!("opened: {outcome:?}"); +/// let sent = client.send_probe().await?; +/// println!("sent: {sent:?}"); +/// let received = client.recv().await?; +/// println!("received: {received:?}"); +/// let timed_out = client.poll_timeouts()?; +/// println!("timeouts: {timed_out:?}"); +/// let closed = client.close().await?; +/// println!("closed: {closed:?}"); +/// # Ok(()) +/// # } +/// ``` #[derive(Debug)] pub struct AsyncClient { socket: tokio::net::UdpSocket, diff --git a/crates/irtt-client/src/client.rs b/crates/irtt-client/src/client.rs index 2d18a5d..f5586cf 100644 --- a/crates/irtt-client/src/client.rs +++ b/crates/irtt-client/src/client.rs @@ -103,6 +103,31 @@ use crate::{ /// session, send probes, receive replies, poll timeouts, and close. Callers /// that do not need to own this loop can use the unified managed API in /// [`crate::managed`] when the `tokio` feature is enabled. +/// +/// # Example +/// +/// `Client` is the runtime-free adapter. The caller drives receives and timeout +/// polling between sends; see the standalone example for a complete loop. +/// This example type-checks without contacting a server: +/// +/// ```no_run +/// use irtt_client::{Client, ClientConfig}; +/// +/// # fn run() -> Result<(), Box> { +/// let mut client = Client::connect(ClientConfig::default())?; +/// let outcome = client.open()?; +/// println!("opened: {outcome:?}"); +/// let sent = client.send_probe()?; +/// println!("sent: {sent:?}"); +/// let received = client.recv_once()?; +/// println!("received: {received:?}"); +/// let timed_out = client.poll_timeouts()?; +/// println!("timeouts: {timed_out:?}"); +/// let closed = client.close()?; +/// println!("closed: {closed:?}"); +/// # Ok(()) +/// # } +/// ``` #[derive(Debug)] pub struct Client { runtime: SessionMachine, diff --git a/crates/irtt-client/src/config.rs b/crates/irtt-client/src/config.rs index ca4505d..4f3a002 100644 --- a/crates/irtt-client/src/config.rs +++ b/crates/irtt-client/src/config.rs @@ -46,6 +46,31 @@ pub(crate) const DEFAULT_MAX_PENDING: usize = 4096; /// request and the local client behavior used to drive the UDP socket. Values /// that are negotiated by the server are available after opening the session /// through [`NegotiatedParams`](crate::NegotiatedParams). +/// +/// # Example +/// +/// Configure a finite run with larger probes and accept documented server +/// restrictions. Use `duration: None` for continuous mode, or +/// [`RunMode::NoTest`] for negotiation without probes. +/// +/// ``` +/// use std::time::Duration; +/// +/// use irtt_client::{ClientConfig, NegotiationPolicy}; +/// +/// let config = ClientConfig { +/// server_addr: "example.net:2112".into(), +/// duration: Some(Duration::from_secs(10)), +/// interval: Duration::from_millis(250), +/// length: 1200, +/// negotiation_policy: NegotiationPolicy::Loose, +/// ..ClientConfig::default() +/// }; +/// +/// assert_eq!(config.duration, Some(Duration::from_secs(10))); +/// assert_eq!(config.interval, Duration::from_millis(250)); +/// assert_eq!(config.length, 1200); +/// ``` #[derive(Debug, Clone, PartialEq, Eq)] pub struct ClientConfig { /// Remote server name or address. @@ -176,6 +201,29 @@ impl Default for ClientConfig { /// These settings affect how the client binds, resolves, and receives from the /// socket. They do not change the IRTT protocol parameters negotiated with the /// server. +/// +/// # Example +/// +/// Bind to a chosen local endpoint and set the outgoing IPv4 TTL: +/// +/// ``` +/// use std::net::{Ipv4Addr, SocketAddr}; +/// +/// use irtt_client::{ClientConfig, SocketConfig}; +/// +/// let config = ClientConfig { +/// socket_config: SocketConfig { +/// bind_addr: Some(SocketAddr::from((Ipv4Addr::UNSPECIFIED, 0))), +/// ttl: Some(32), +/// ipv4_only: true, +/// ..SocketConfig::default() +/// }, +/// ..ClientConfig::default() +/// }; +/// +/// assert_eq!(config.socket_config.ttl, Some(32)); +/// assert!(config.socket_config.ipv4_only); +/// ``` #[derive(Debug, Clone, PartialEq, Eq, Default)] pub struct SocketConfig { /// Local address to bind before connecting the UDP socket. diff --git a/crates/irtt-client/src/event.rs b/crates/irtt-client/src/event.rs index 911fba9..fbe113b 100644 --- a/crates/irtt-client/src/event.rs +++ b/crates/irtt-client/src/event.rs @@ -260,6 +260,19 @@ pub struct RttSample { /// processing time or comparing wall-clock timestamps from different hosts. It /// should not be used to represent raw elapsed wall-clock or monotonic time, /// which is non-negative and represented by [`Duration`]. +/// +/// # Example +/// +/// Signed timing values retain a negative result instead of clamping clock +/// skew or server processing adjustments to zero: +/// +/// ``` +/// use irtt_client::SignedDuration; +/// +/// let adjusted_rtt = SignedDuration::from_nanos(-250_000); +/// assert!(adjusted_rtt.is_negative()); +/// assert_eq!(adjusted_rtt.as_micros(), -250); +/// ``` #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] pub struct SignedDuration { ns: i128, diff --git a/crates/irtt-client/src/managed/task.rs b/crates/irtt-client/src/managed/task.rs index 15bb2e9..39c4691 100644 --- a/crates/irtt-client/src/managed/task.rs +++ b/crates/irtt-client/src/managed/task.rs @@ -44,6 +44,42 @@ const POST_DEADLINE_RECEIVE_BUDGET: usize = TARGET_WORK_BUDGET; const MAX_BROADCAST_CHANNEL_CAPACITY: usize = usize::MAX >> 1; /// Entry point for constructing a unified Tokio managed task. +/// +/// # Example +/// +/// Construct the task and its separate control handle, subscribe before +/// starting the task, then run it under the caller's Tokio runtime. A reachable +/// server is required at runtime, so this example only type-checks. +/// +/// ```no_run +/// use irtt_client::{ClientConfig, ClientEvent}; +/// use irtt_client::managed::{ +/// ManagedClient, ManagedClientConfig, ManagedEvent, ManagedTargetConfig, +/// }; +/// +/// # async fn run() -> Result<(), Box> { +/// let config = ManagedClientConfig { +/// client: ClientConfig { +/// duration: Some(std::time::Duration::from_secs(10)), +/// ..ClientConfig::default() +/// }, +/// ..ManagedClientConfig::default() +/// }; +/// let targets = vec![ManagedTargetConfig::new("edge", "127.0.0.1:2112")]; +/// let (task, handle) = ManagedClient::task(config, targets)?; +/// let mut events = handle.subscribe()?; +/// let driver = tokio::spawn(task); +/// +/// while let Ok(event) = events.recv().await { +/// if let ManagedEvent::Client { event: ClientEvent::EchoReply { .. }, .. } = event { +/// println!("received a probe reply"); +/// } +/// } +/// let outcome = driver.await?; +/// println!("run ended: {:?}", outcome.end_reason); +/// # Ok(()) +/// # } +/// ``` #[derive(Debug, Default)] pub struct ManagedClient;