Skip to main content

docker_wrapper/template/
toxiproxy.rs

1//! Toxiproxy template for network fault injection.
2//!
3//! [Toxiproxy](https://github.com/Shopify/toxiproxy) is a TCP proxy that injects
4//! controllable faults (latency, bandwidth limits, connection drops, ...) between
5//! a client and an upstream service. This template runs the
6//! `ghcr.io/shopify/toxiproxy` container, publishes its `:8474` control API, and
7//! exposes a small typed client over that API so you can register proxies and add
8//! toxics without hand-rolling HTTP calls.
9//!
10//! # Quick Start
11//!
12//! ```rust,no_run
13//! use docker_wrapper::template::toxiproxy::{Toxic, ToxiproxyTemplate, ToxicStream};
14//! use docker_wrapper::Template;
15//!
16//! # async fn example() -> Result<(), Box<dyn std::error::Error>> {
17//! // Start Toxiproxy and wait for its control API to be ready.
18//! let toxiproxy = ToxiproxyTemplate::new("chaos")
19//!     .control_port(8474)
20//!     .proxy_port(16379);
21//! toxiproxy.start_and_wait().await?;
22//! toxiproxy.wait_for_control_api().await?;
23//!
24//! // Route a published host port through Toxiproxy to an upstream Redis.
25//! // The proxy listens on 0.0.0.0:16379 inside the container, which is published
26//! // to the host, so host clients can connect through the proxy.
27//! toxiproxy
28//!     .create_proxy("redis", "0.0.0.0:16379", "redis:6379")
29//!     .await?;
30//!
31//! // Inject 500ms of downstream latency.
32//! toxiproxy
33//!     .add_toxic("redis", "slow", ToxicStream::Downstream, Toxic::latency(500))
34//!     .await?;
35//!
36//! // ... run your client against localhost:16379 and observe the fault ...
37//!
38//! toxiproxy.remove_toxic("redis", "slow").await?;
39//! toxiproxy.reset().await?;
40//! # Ok(())
41//! # }
42//! ```
43//!
44//! # Feature Flag
45//!
46//! This template requires the `template-toxiproxy` feature:
47//!
48//! ```toml
49//! [dependencies]
50//! docker-wrapper = { version = "0.11", features = ["template-toxiproxy"] }
51//! ```
52
53#![allow(clippy::doc_markdown)]
54#![allow(clippy::must_use_candidate)]
55#![allow(clippy::return_self_not_must_use)]
56
57use crate::template::{Result, Template, TemplateConfig, TemplateError};
58use async_trait::async_trait;
59use reqwest::Client;
60use serde::{Deserialize, Serialize};
61use std::collections::HashMap;
62use std::time::Duration;
63
64/// Default Toxiproxy image.
65const DEFAULT_IMAGE: &str = "ghcr.io/shopify/toxiproxy";
66/// Default Toxiproxy image tag.
67const DEFAULT_TAG: &str = "2.12.0";
68/// Default control API port.
69const DEFAULT_CONTROL_PORT: u16 = 8474;
70
71/// Direction a toxic applies to.
72///
73/// Toxiproxy applies toxics to one direction of a proxied connection. The
74/// downstream is data flowing from the upstream back to the client; the upstream
75/// is data flowing from the client to the upstream.
76#[derive(Debug, Clone, Copy, PartialEq, Eq)]
77pub enum ToxicStream {
78    /// Data flowing from the upstream service back to the client.
79    Downstream,
80    /// Data flowing from the client to the upstream service.
81    Upstream,
82}
83
84impl ToxicStream {
85    /// The wire value Toxiproxy expects for this stream direction.
86    fn as_str(self) -> &'static str {
87        match self {
88            ToxicStream::Downstream => "downstream",
89            ToxicStream::Upstream => "upstream",
90        }
91    }
92}
93
94/// A typed Toxiproxy toxic.
95///
96/// Each variant maps to a Toxiproxy toxic type and carries its attributes. Use
97/// the associated constructors ([`Toxic::latency`], [`Toxic::bandwidth`], ...) to
98/// build a toxic, then pass it to [`ToxiproxyTemplate::add_toxic`].
99///
100/// See the [Toxiproxy toxics reference](https://github.com/Shopify/toxiproxy#toxics)
101/// for the full semantics of each type.
102#[derive(Debug, Clone, PartialEq)]
103pub enum Toxic {
104    /// Add a delay to all data, with optional random jitter.
105    ///
106    /// `latency` and `jitter` are in milliseconds.
107    Latency {
108        /// Base latency added to each packet, in milliseconds.
109        latency: u64,
110        /// Random jitter added on top of the base latency, in milliseconds.
111        jitter: u64,
112    },
113    /// Limit the connection to a maximum throughput.
114    ///
115    /// `rate` is in kilobytes per second.
116    Bandwidth {
117        /// Throughput limit in kilobytes per second.
118        rate: u64,
119    },
120    /// Stop all data and close the connection after `timeout` milliseconds.
121    ///
122    /// A `timeout` of `0` holds the connection open indefinitely without passing
123    /// data, which is useful for simulating a hung upstream.
124    Timeout {
125        /// Time to wait before closing the connection, in milliseconds.
126        timeout: u64,
127    },
128    /// Slice data into smaller packets to simulate fragmentation.
129    Slicer {
130        /// Average size of each packet, in bytes.
131        average_size: u64,
132        /// Variation in packet size, in bytes.
133        size_variation: u64,
134        /// Delay between sliced packets, in microseconds.
135        delay: u64,
136    },
137    /// Close the connection after a fixed number of bytes have been transmitted.
138    LimitData {
139        /// Number of bytes to allow before closing the connection.
140        bytes: u64,
141    },
142}
143
144impl Toxic {
145    /// Create a latency toxic with no jitter.
146    ///
147    /// `latency` is in milliseconds.
148    #[must_use]
149    pub fn latency(latency: u64) -> Self {
150        Toxic::Latency { latency, jitter: 0 }
151    }
152
153    /// Create a latency toxic with random jitter.
154    ///
155    /// Both `latency` and `jitter` are in milliseconds.
156    #[must_use]
157    pub fn jitter(latency: u64, jitter: u64) -> Self {
158        Toxic::Latency { latency, jitter }
159    }
160
161    /// Create a bandwidth toxic limiting throughput to `rate` kilobytes per second.
162    #[must_use]
163    pub fn bandwidth(rate: u64) -> Self {
164        Toxic::Bandwidth { rate }
165    }
166
167    /// Create a timeout toxic that closes the connection after `timeout` milliseconds.
168    ///
169    /// A `timeout` of `0` holds the connection open indefinitely.
170    #[must_use]
171    pub fn timeout(timeout: u64) -> Self {
172        Toxic::Timeout { timeout }
173    }
174
175    /// Create a slicer toxic that fragments data into smaller packets.
176    ///
177    /// `average_size` and `size_variation` are in bytes; `delay` is in microseconds.
178    #[must_use]
179    pub fn slicer(average_size: u64, size_variation: u64, delay: u64) -> Self {
180        Toxic::Slicer {
181            average_size,
182            size_variation,
183            delay,
184        }
185    }
186
187    /// Create a `limit_data` toxic that closes the connection after `bytes` bytes.
188    #[must_use]
189    pub fn limit_data(bytes: u64) -> Self {
190        Toxic::LimitData { bytes }
191    }
192
193    /// The Toxiproxy type name for this toxic.
194    fn type_name(&self) -> &'static str {
195        match self {
196            Toxic::Latency { .. } => "latency",
197            Toxic::Bandwidth { .. } => "bandwidth",
198            Toxic::Timeout { .. } => "timeout",
199            Toxic::Slicer { .. } => "slicer",
200            Toxic::LimitData { .. } => "limit_data",
201        }
202    }
203
204    /// The attribute map Toxiproxy expects for this toxic.
205    fn attributes(&self) -> HashMap<String, u64> {
206        let mut attrs = HashMap::new();
207        match *self {
208            Toxic::Latency { latency, jitter } => {
209                attrs.insert("latency".to_string(), latency);
210                attrs.insert("jitter".to_string(), jitter);
211            }
212            Toxic::Bandwidth { rate } => {
213                attrs.insert("rate".to_string(), rate);
214            }
215            Toxic::Timeout { timeout } => {
216                attrs.insert("timeout".to_string(), timeout);
217            }
218            Toxic::Slicer {
219                average_size,
220                size_variation,
221                delay,
222            } => {
223                attrs.insert("average_size".to_string(), average_size);
224                attrs.insert("size_variation".to_string(), size_variation);
225                attrs.insert("delay".to_string(), delay);
226            }
227            Toxic::LimitData { bytes } => {
228                attrs.insert("bytes".to_string(), bytes);
229            }
230        }
231        attrs
232    }
233}
234
235/// Request body for creating or updating a proxy via the control API.
236#[derive(Debug, Serialize)]
237struct ProxyRequest {
238    name: String,
239    listen: String,
240    upstream: String,
241    enabled: bool,
242}
243
244/// A proxy as returned by the Toxiproxy control API.
245#[derive(Debug, Clone, Deserialize)]
246pub struct ProxyInfo {
247    /// Proxy name.
248    pub name: String,
249    /// Address the proxy listens on (inside the container).
250    pub listen: String,
251    /// Upstream address the proxy forwards to.
252    pub upstream: String,
253    /// Whether the proxy is currently enabled.
254    pub enabled: bool,
255}
256
257/// Request body for adding a toxic via the control API.
258#[derive(Debug, Serialize)]
259struct ToxicRequest {
260    name: String,
261    #[serde(rename = "type")]
262    toxic_type: String,
263    stream: String,
264    toxicity: f64,
265    attributes: HashMap<String, u64>,
266}
267
268/// Toxiproxy template for TCP fault injection.
269///
270/// Runs the `ghcr.io/shopify/toxiproxy` container and exposes a typed client over
271/// its `:8474` control API. Register proxies with [`create_proxy`](Self::create_proxy)
272/// and add toxics with [`add_toxic`](Self::add_toxic).
273///
274/// Proxies should listen on `0.0.0.0:<port>` and that port must be published (via
275/// [`proxy_port`](Self::proxy_port)) so host clients can connect through the proxy.
276pub struct ToxiproxyTemplate {
277    config: TemplateConfig,
278    control_port: u16,
279    api_ready_timeout: Duration,
280}
281
282impl ToxiproxyTemplate {
283    /// Create a new Toxiproxy template.
284    ///
285    /// The control API is published on port `8474` by default. Use
286    /// [`proxy_port`](Self::proxy_port) to also publish the ports your proxies
287    /// listen on so host clients can reach them.
288    pub fn new(name: impl Into<String>) -> Self {
289        let name = name.into();
290        let config = TemplateConfig {
291            name,
292            image: DEFAULT_IMAGE.to_string(),
293            tag: DEFAULT_TAG.to_string(),
294            ports: vec![(DEFAULT_CONTROL_PORT, DEFAULT_CONTROL_PORT)],
295            env: HashMap::new(),
296            volumes: Vec::new(),
297            network: None,
298            health_check: None,
299            auto_remove: false,
300            memory_limit: None,
301            cpu_limit: None,
302            platform: None,
303        };
304
305        Self {
306            config,
307            control_port: DEFAULT_CONTROL_PORT,
308            api_ready_timeout: Duration::from_secs(30),
309        }
310    }
311
312    /// Set the host port for the control API (default: 8474).
313    ///
314    /// This maps the host port to the container's `8474` control port.
315    pub fn control_port(mut self, port: u16) -> Self {
316        // Replace the existing control-port mapping (container side is always 8474).
317        if let Some(pos) = self
318            .config
319            .ports
320            .iter()
321            .position(|(_, c)| *c == DEFAULT_CONTROL_PORT)
322        {
323            self.config.ports[pos] = (port, DEFAULT_CONTROL_PORT);
324        } else {
325            self.config.ports.push((port, DEFAULT_CONTROL_PORT));
326        }
327        self.control_port = port;
328        self
329    }
330
331    /// Publish a proxy port so host clients can connect through a proxy.
332    ///
333    /// A proxy that listens on `0.0.0.0:<port>` inside the container is only
334    /// reachable from the host if that port is published. Call this once per port
335    /// you intend to proxy on. The same port is used on both the host and
336    /// container sides so the published address matches the proxy's `listen`
337    /// address.
338    ///
339    /// # Example
340    ///
341    /// ```rust
342    /// use docker_wrapper::template::toxiproxy::ToxiproxyTemplate;
343    ///
344    /// let toxiproxy = ToxiproxyTemplate::new("chaos")
345    ///     .proxy_port(16379)
346    ///     .proxy_port(15432);
347    /// ```
348    pub fn proxy_port(mut self, port: u16) -> Self {
349        if !self
350            .config
351            .ports
352            .iter()
353            .any(|(h, c)| *h == port && *c == port)
354        {
355            self.config.ports.push((port, port));
356        }
357        self
358    }
359
360    /// Connect to a specific Docker network.
361    ///
362    /// Use this to place Toxiproxy on the same network as the upstream containers
363    /// it proxies, so it can reach them by container name.
364    pub fn network(mut self, network: impl Into<String>) -> Self {
365        self.config.network = Some(network.into());
366        self
367    }
368
369    /// Enable auto-remove when the container stops.
370    pub fn auto_remove(mut self) -> Self {
371        self.config.auto_remove = true;
372        self
373    }
374
375    /// Use a custom image and tag.
376    pub fn custom_image(mut self, image: impl Into<String>, tag: impl Into<String>) -> Self {
377        self.config.image = image.into();
378        self.config.tag = tag.into();
379        self
380    }
381
382    /// Set the platform for the container (e.g., "linux/arm64", "linux/amd64").
383    pub fn platform(mut self, platform: impl Into<String>) -> Self {
384        self.config.platform = Some(platform.into());
385        self
386    }
387
388    /// Set how long to wait for the control API to become ready (default: 30s).
389    pub fn api_ready_timeout(mut self, timeout: Duration) -> Self {
390        self.api_ready_timeout = timeout;
391        self
392    }
393
394    /// The base URL of the control API on the host.
395    fn control_url(&self) -> String {
396        format!("http://localhost:{}", self.control_port)
397    }
398
399    /// Build an HTTP client for talking to the control API.
400    fn http_client() -> Result<Client> {
401        Client::builder()
402            .timeout(Duration::from_secs(10))
403            .build()
404            .map_err(|e| {
405                TemplateError::DockerError(crate::Error::custom(format!(
406                    "failed to build HTTP client: {e}"
407                )))
408            })
409    }
410
411    /// Wait for the Toxiproxy control API to start responding.
412    ///
413    /// Polls `GET /version` on the control port until it returns success or the
414    /// configured timeout elapses. Call this after [`start`](Template::start)
415    /// (or [`start_and_wait`](Template::start_and_wait)) before registering
416    /// proxies.
417    ///
418    /// # Errors
419    ///
420    /// Returns an error if the control API does not respond within the timeout.
421    pub async fn wait_for_control_api(&self) -> Result<()> {
422        let client = Self::http_client()?;
423        let url = format!("{}/version", self.control_url());
424        let start = std::time::Instant::now();
425
426        while start.elapsed() < self.api_ready_timeout {
427            if let Ok(response) = client.get(&url).send().await {
428                if response.status().is_success() {
429                    return Ok(());
430                }
431            }
432            tokio::time::sleep(Duration::from_millis(250)).await;
433        }
434
435        Err(TemplateError::Timeout(format!(
436            "Toxiproxy control API on port {} did not become ready within {}s",
437            self.control_port,
438            self.api_ready_timeout.as_secs()
439        )))
440    }
441
442    /// Register a new proxy.
443    ///
444    /// - `name` identifies the proxy in subsequent calls.
445    /// - `listen` is the address the proxy listens on inside the container. Use
446    ///   `0.0.0.0:<port>` with a published [`proxy_port`](Self::proxy_port) so host
447    ///   clients can connect through it.
448    /// - `upstream` is the address the proxy forwards to (e.g. `redis:6379` when on
449    ///   a shared network).
450    ///
451    /// # Errors
452    ///
453    /// Returns an error if the control API request fails or returns a non-success
454    /// status (for example, if a proxy with the same name already exists).
455    pub async fn create_proxy(
456        &self,
457        name: impl Into<String>,
458        listen: impl Into<String>,
459        upstream: impl Into<String>,
460    ) -> Result<ProxyInfo> {
461        let client = Self::http_client()?;
462        let body = ProxyRequest {
463            name: name.into(),
464            listen: listen.into(),
465            upstream: upstream.into(),
466            enabled: true,
467        };
468
469        let url = format!("{}/proxies", self.control_url());
470        let response = client
471            .post(&url)
472            .json(&body)
473            .send()
474            .await
475            .map_err(|e| map_request_err(&e))?;
476
477        let status = response.status();
478        if !status.is_success() {
479            let text = response.text().await.unwrap_or_default();
480            return Err(TemplateError::InvalidConfig(format!(
481                "failed to create proxy '{}': HTTP {status}: {text}",
482                body.name
483            )));
484        }
485
486        response
487            .json::<ProxyInfo>()
488            .await
489            .map_err(|e| map_request_err(&e))
490    }
491
492    /// Add a toxic to an existing proxy.
493    ///
494    /// - `proxy` is the proxy name passed to [`create_proxy`](Self::create_proxy).
495    /// - `name` identifies the toxic for later removal.
496    /// - `stream` selects the direction the toxic applies to.
497    /// - `toxic` is the typed fault to inject.
498    ///
499    /// The toxic is applied with full toxicity (`1.0`), so it affects every
500    /// connection.
501    ///
502    /// # Errors
503    ///
504    /// Returns an error if the control API request fails or returns a non-success
505    /// status.
506    pub async fn add_toxic(
507        &self,
508        proxy: &str,
509        name: impl Into<String>,
510        stream: ToxicStream,
511        toxic: Toxic,
512    ) -> Result<()> {
513        let client = Self::http_client()?;
514        let body = ToxicRequest {
515            name: name.into(),
516            toxic_type: toxic.type_name().to_string(),
517            stream: stream.as_str().to_string(),
518            toxicity: 1.0,
519            attributes: toxic.attributes(),
520        };
521
522        let url = format!("{}/proxies/{proxy}/toxics", self.control_url());
523        let response = client
524            .post(&url)
525            .json(&body)
526            .send()
527            .await
528            .map_err(|e| map_request_err(&e))?;
529
530        let status = response.status();
531        if !status.is_success() {
532            let text = response.text().await.unwrap_or_default();
533            return Err(TemplateError::InvalidConfig(format!(
534                "failed to add toxic '{}' to proxy '{proxy}': HTTP {status}: {text}",
535                body.name
536            )));
537        }
538
539        Ok(())
540    }
541
542    /// Remove a toxic from a proxy.
543    ///
544    /// # Errors
545    ///
546    /// Returns an error if the control API request fails or returns a non-success
547    /// status.
548    pub async fn remove_toxic(&self, proxy: &str, toxic: &str) -> Result<()> {
549        let client = Self::http_client()?;
550        let url = format!("{}/proxies/{proxy}/toxics/{toxic}", self.control_url());
551        let response = client
552            .delete(&url)
553            .send()
554            .await
555            .map_err(|e| map_request_err(&e))?;
556
557        let status = response.status();
558        if !status.is_success() {
559            let text = response.text().await.unwrap_or_default();
560            return Err(TemplateError::InvalidConfig(format!(
561                "failed to remove toxic '{toxic}' from proxy '{proxy}': HTTP {status}: {text}"
562            )));
563        }
564
565        Ok(())
566    }
567
568    /// List all registered proxies.
569    ///
570    /// # Errors
571    ///
572    /// Returns an error if the control API request fails or the response cannot be
573    /// parsed.
574    pub async fn list_proxies(&self) -> Result<Vec<ProxyInfo>> {
575        let client = Self::http_client()?;
576        let url = format!("{}/proxies", self.control_url());
577        let response = client
578            .get(&url)
579            .send()
580            .await
581            .map_err(|e| map_request_err(&e))?;
582
583        let status = response.status();
584        if !status.is_success() {
585            let text = response.text().await.unwrap_or_default();
586            return Err(TemplateError::InvalidConfig(format!(
587                "failed to list proxies: HTTP {status}: {text}"
588            )));
589        }
590
591        // The control API returns a map of name -> proxy.
592        let map = response
593            .json::<HashMap<String, ProxyInfo>>()
594            .await
595            .map_err(|e| map_request_err(&e))?;
596        Ok(map.into_values().collect())
597    }
598
599    /// Reset all proxies and remove all toxics.
600    ///
601    /// This re-enables every proxy and clears all toxics, returning Toxiproxy to a
602    /// clean state without restarting the container.
603    ///
604    /// # Errors
605    ///
606    /// Returns an error if the control API request fails or returns a non-success
607    /// status.
608    pub async fn reset(&self) -> Result<()> {
609        let client = Self::http_client()?;
610        let url = format!("{}/reset", self.control_url());
611        let response = client
612            .post(&url)
613            .send()
614            .await
615            .map_err(|e| map_request_err(&e))?;
616
617        let status = response.status();
618        if !status.is_success() {
619            let text = response.text().await.unwrap_or_default();
620            return Err(TemplateError::InvalidConfig(format!(
621                "failed to reset Toxiproxy: HTTP {status}: {text}"
622            )));
623        }
624
625        Ok(())
626    }
627}
628
629/// Map a reqwest error into a `TemplateError`.
630fn map_request_err(e: &reqwest::Error) -> TemplateError {
631    TemplateError::DockerError(crate::Error::custom(format!(
632        "Toxiproxy control API request failed: {e}"
633    )))
634}
635
636#[async_trait]
637impl Template for ToxiproxyTemplate {
638    fn name(&self) -> &str {
639        &self.config.name
640    }
641
642    fn config(&self) -> &TemplateConfig {
643        &self.config
644    }
645
646    fn config_mut(&mut self) -> &mut TemplateConfig {
647        &mut self.config
648    }
649}
650
651#[cfg(test)]
652mod tests {
653    use super::*;
654
655    #[test]
656    fn test_toxiproxy_template_defaults() {
657        let template = ToxiproxyTemplate::new("test-toxiproxy");
658        assert_eq!(template.name(), "test-toxiproxy");
659        assert_eq!(template.config().image, DEFAULT_IMAGE);
660        assert_eq!(template.config().tag, DEFAULT_TAG);
661        assert_eq!(template.control_port, DEFAULT_CONTROL_PORT);
662        assert_eq!(template.config().ports, vec![(8474, 8474)]);
663    }
664
665    #[test]
666    fn test_control_port_replaces_mapping() {
667        let template = ToxiproxyTemplate::new("test").control_port(18474);
668        assert_eq!(template.control_port, 18474);
669        // Container side stays 8474, host side updated; no duplicate mapping.
670        assert_eq!(template.config().ports, vec![(18474, 8474)]);
671        assert_eq!(template.control_url(), "http://localhost:18474");
672    }
673
674    #[test]
675    fn test_proxy_port_published() {
676        let template = ToxiproxyTemplate::new("test")
677            .proxy_port(16379)
678            .proxy_port(16379); // idempotent
679        let ports = &template.config().ports;
680        assert!(ports.contains(&(8474, 8474)));
681        assert!(ports.contains(&(16379, 16379)));
682        // Only added once despite the duplicate call.
683        assert_eq!(ports.iter().filter(|p| **p == (16379, 16379)).count(), 1);
684    }
685
686    #[test]
687    fn test_network_and_custom_image() {
688        let template = ToxiproxyTemplate::new("test")
689            .network("chaos-net")
690            .custom_image("ghcr.io/shopify/toxiproxy", "latest")
691            .platform("linux/arm64");
692        assert_eq!(template.config().network.as_deref(), Some("chaos-net"));
693        assert_eq!(template.config().image, "ghcr.io/shopify/toxiproxy");
694        assert_eq!(template.config().tag, "latest");
695        assert_eq!(template.config().platform.as_deref(), Some("linux/arm64"));
696    }
697
698    #[test]
699    fn test_toxic_latency_attributes() {
700        let toxic = Toxic::latency(500);
701        assert_eq!(toxic.type_name(), "latency");
702        let attrs = toxic.attributes();
703        assert_eq!(attrs.get("latency"), Some(&500));
704        assert_eq!(attrs.get("jitter"), Some(&0));
705
706        let toxic = Toxic::jitter(500, 100);
707        let attrs = toxic.attributes();
708        assert_eq!(attrs.get("latency"), Some(&500));
709        assert_eq!(attrs.get("jitter"), Some(&100));
710    }
711
712    #[test]
713    fn test_toxic_bandwidth_attributes() {
714        let toxic = Toxic::bandwidth(64);
715        assert_eq!(toxic.type_name(), "bandwidth");
716        assert_eq!(toxic.attributes().get("rate"), Some(&64));
717    }
718
719    #[test]
720    fn test_toxic_timeout_attributes() {
721        let toxic = Toxic::timeout(0);
722        assert_eq!(toxic.type_name(), "timeout");
723        assert_eq!(toxic.attributes().get("timeout"), Some(&0));
724    }
725
726    #[test]
727    fn test_toxic_slicer_attributes() {
728        let toxic = Toxic::slicer(64, 32, 10);
729        assert_eq!(toxic.type_name(), "slicer");
730        let attrs = toxic.attributes();
731        assert_eq!(attrs.get("average_size"), Some(&64));
732        assert_eq!(attrs.get("size_variation"), Some(&32));
733        assert_eq!(attrs.get("delay"), Some(&10));
734    }
735
736    #[test]
737    fn test_toxic_limit_data_attributes() {
738        let toxic = Toxic::limit_data(2048);
739        assert_eq!(toxic.type_name(), "limit_data");
740        assert_eq!(toxic.attributes().get("bytes"), Some(&2048));
741    }
742
743    #[test]
744    fn test_toxic_stream_wire_values() {
745        assert_eq!(ToxicStream::Downstream.as_str(), "downstream");
746        assert_eq!(ToxicStream::Upstream.as_str(), "upstream");
747    }
748}