Skip to main content

docker_wrapper/
testing.rs

1//! # Testing Utilities
2//!
3//! RAII-style container lifecycle management for integration tests.
4//!
5//! This module provides [`ContainerGuard`] and [`ContainerGuardSet`] for automatic
6//! container lifecycle management. Containers are automatically stopped and removed
7//! when guards go out of scope, ensuring clean test environments.
8//!
9//! ## Why Use This?
10//!
11//! - **Automatic cleanup**: No more forgotten containers cluttering your Docker
12//! - **Panic-safe**: Containers are cleaned up even if your test panics
13//! - **Debug-friendly**: Keep containers alive on failure for inspection
14//! - **Network support**: Automatic network creation for multi-container tests
15//! - **Ready checks**: Wait for services to be ready before running tests
16//!
17//! ## Quick Start
18//!
19//! ```rust,no_run
20//! use docker_wrapper::testing::ContainerGuard;
21//! use docker_wrapper::RedisTemplate;
22//!
23//! #[tokio::test]
24//! async fn test_with_redis() -> Result<(), Box<dyn std::error::Error>> {
25//!     // Container starts and waits for Redis to be ready
26//!     let guard = ContainerGuard::new(RedisTemplate::new("test-redis"))
27//!         .wait_for_ready(true)
28//!         .start()
29//!         .await?;
30//!
31//!     // Get connection string directly from guard
32//!     let url = guard.connection_string();
33//!     // Use Redis at: redis://localhost:6379
34//!
35//!     Ok(())
36//!     // Container automatically stopped and removed here
37//! }
38//! ```
39//!
40//! ## Configuration Options
41//!
42//! ### Lifecycle Control
43//!
44//! ```rust,no_run
45//! # use docker_wrapper::testing::ContainerGuard;
46//! # use docker_wrapper::RedisTemplate;
47//! # async fn example() -> Result<(), Box<dyn std::error::Error>> {
48//! let guard = ContainerGuard::new(RedisTemplate::new("redis"))
49//!     .stop_on_drop(true)      // Stop container on drop (default: true)
50//!     .remove_on_drop(true)    // Remove container on drop (default: true)
51//!     .start()
52//!     .await?;
53//! # Ok(())
54//! # }
55//! ```
56//!
57//! ### Debugging Failed Tests
58//!
59//! Keep containers running when tests fail for debugging:
60//!
61//! ```rust,no_run
62//! # use docker_wrapper::testing::ContainerGuard;
63//! # use docker_wrapper::RedisTemplate;
64//! # async fn example() -> Result<(), Box<dyn std::error::Error>> {
65//! let guard = ContainerGuard::new(RedisTemplate::new("redis"))
66//!     .keep_on_panic(true)     // Keep container if test panics
67//!     .capture_logs(true)      // Print container logs on panic
68//!     .start()
69//!     .await?;
70//! # Ok(())
71//! # }
72//! ```
73//!
74//! ### Ready Checks
75//!
76//! Wait for the service to be ready before proceeding:
77//!
78//! ```rust,no_run
79//! # use docker_wrapper::testing::ContainerGuard;
80//! # use docker_wrapper::RedisTemplate;
81//! # async fn example() -> Result<(), Box<dyn std::error::Error>> {
82//! // Automatic wait during start
83//! let guard = ContainerGuard::new(RedisTemplate::new("redis"))
84//!     .wait_for_ready(true)
85//!     .start()
86//!     .await?;
87//! // Redis is guaranteed ready here
88//!
89//! // Or wait manually later
90//! let guard2 = ContainerGuard::new(RedisTemplate::new("redis2"))
91//!     .start()
92//!     .await?;
93//! guard2.wait_for_ready().await?;
94//! # Ok(())
95//! # }
96//! ```
97//!
98//! ### Container Reuse
99//!
100//! Speed up local development by reusing running containers:
101//!
102//! ```rust,no_run
103//! # use docker_wrapper::testing::ContainerGuard;
104//! # use docker_wrapper::RedisTemplate;
105//! # async fn example() -> Result<(), Box<dyn std::error::Error>> {
106//! let guard = ContainerGuard::new(RedisTemplate::new("redis"))
107//!     .reuse_if_running(true)  // Reuse existing container if found
108//!     .remove_on_drop(false)   // Keep it for next test run
109//!     .stop_on_drop(false)
110//!     .start()
111//!     .await?;
112//!
113//! if guard.was_reused() {
114//!     println!("Reused existing container");
115//! }
116//! # Ok(())
117//! # }
118//! ```
119//!
120//! ### Network Support
121//!
122//! Attach containers to custom networks:
123//!
124//! ```rust,no_run
125//! # use docker_wrapper::testing::ContainerGuard;
126//! # use docker_wrapper::RedisTemplate;
127//! # async fn example() -> Result<(), Box<dyn std::error::Error>> {
128//! let guard = ContainerGuard::new(RedisTemplate::new("redis"))
129//!     .with_network("my-test-network")  // Create and attach to network
130//!     .remove_network_on_drop(true)     // Clean up network after test
131//!     .start()
132//!     .await?;
133//! # Ok(())
134//! # }
135//! ```
136//!
137//! ### Fast Cleanup
138//!
139//! Use a short stop timeout for faster test cleanup:
140//!
141//! ```rust,no_run
142//! # use docker_wrapper::testing::ContainerGuard;
143//! # use docker_wrapper::RedisTemplate;
144//! # use std::time::Duration;
145//! # async fn example() -> Result<(), Box<dyn std::error::Error>> {
146//! let guard = ContainerGuard::new(RedisTemplate::new("redis"))
147//!     .stop_timeout(Duration::from_secs(1))  // 1 second graceful shutdown
148//!     .start()
149//!     .await?;
150//!
151//! // Or immediate SIGKILL
152//! let guard2 = ContainerGuard::new(RedisTemplate::new("redis2"))
153//!     .stop_timeout(Duration::ZERO)
154//!     .start()
155//!     .await?;
156//! # Ok(())
157//! # }
158//! ```
159//!
160//! ## Multi-Container Tests
161//!
162//! Use [`ContainerGuardSet`] for tests requiring multiple services:
163//!
164//! ```rust,no_run
165//! use docker_wrapper::testing::ContainerGuardSet;
166//! use docker_wrapper::RedisTemplate;
167//!
168//! #[tokio::test]
169//! async fn test_multi_container() -> Result<(), Box<dyn std::error::Error>> {
170//!     let guards = ContainerGuardSet::new()
171//!         .with_network("test-network")    // Shared network for all containers
172//!         .add(RedisTemplate::new("redis-primary").port(6379))
173//!         .add(RedisTemplate::new("redis-replica").port(6380))
174//!         .keep_on_panic(true)
175//!         .start_all()
176//!         .await?;
177//!
178//!     assert!(guards.contains("redis-primary"));
179//!     assert!(guards.contains("redis-replica"));
180//!     assert_eq!(guards.len(), 2);
181//!
182//!     // All containers cleaned up together
183//!     Ok(())
184//! }
185//! ```
186//!
187//! ## Accessing Container Information
188//!
189//! ```rust,no_run
190//! # use docker_wrapper::testing::ContainerGuard;
191//! # use docker_wrapper::RedisTemplate;
192//! # async fn example() -> Result<(), Box<dyn std::error::Error>> {
193//! let guard = ContainerGuard::new(RedisTemplate::new("redis").port(6379))
194//!     .start()
195//!     .await?;
196//!
197//! // Connection string (for templates that support it)
198//! let conn = guard.connection_string();
199//!
200//! // Access underlying template
201//! let template = guard.template();
202//!
203//! // Get container ID
204//! if let Some(id) = guard.container_id() {
205//!     println!("Container ID: {}", id);
206//! }
207//!
208//! // Query host port for a container port
209//! let host_port = guard.host_port(6379).await?;
210//!
211//! // Get container logs
212//! let logs = guard.logs().await?;
213//!
214//! // Check if running
215//! let running = guard.is_running().await?;
216//! # Ok(())
217//! # }
218//! ```
219//!
220//! ## Common Patterns
221//!
222//! ### Test Fixtures
223//!
224//! Create reusable test fixtures:
225//!
226//! ```rust,no_run
227//! use docker_wrapper::testing::ContainerGuard;
228//! use docker_wrapper::RedisTemplate;
229//! use docker_wrapper::template::TemplateError;
230//!
231//! async fn redis_fixture(name: &str) -> Result<ContainerGuard<RedisTemplate>, TemplateError> {
232//!     ContainerGuard::new(RedisTemplate::new(name))
233//!         .wait_for_ready(true)
234//!         .keep_on_panic(true)
235//!         .capture_logs(true)
236//!         .start()
237//!         .await
238//! }
239//!
240//! #[tokio::test]
241//! async fn test_using_fixture() -> Result<(), Box<dyn std::error::Error>> {
242//!     let redis = redis_fixture("test-redis").await?;
243//!     // Use redis...
244//!     Ok(())
245//! }
246//! ```
247//!
248//! ### Unique Container Names
249//!
250//! Use UUIDs to avoid name conflicts in parallel tests:
251//!
252//! ```rust,no_run
253//! # use docker_wrapper::testing::ContainerGuard;
254//! # use docker_wrapper::RedisTemplate;
255//! fn unique_name(prefix: &str) -> String {
256//!     format!("{}-{}", prefix, uuid::Uuid::new_v4())
257//! }
258//!
259//! #[tokio::test]
260//! async fn test_parallel_safe() -> Result<(), Box<dyn std::error::Error>> {
261//!     let name = unique_name("redis");
262//!     let guard = ContainerGuard::new(RedisTemplate::new(&name))
263//!         .start()
264//!         .await?;
265//!     Ok(())
266//! }
267//! ```
268//!
269//! ### Manual Cleanup
270//!
271//! Trigger cleanup explicitly when needed:
272//!
273//! ```rust,no_run
274//! # use docker_wrapper::testing::ContainerGuard;
275//! # use docker_wrapper::RedisTemplate;
276//! # async fn example() -> Result<(), Box<dyn std::error::Error>> {
277//! let guard = ContainerGuard::new(RedisTemplate::new("redis"))
278//!     .start()
279//!     .await?;
280//!
281//! // Do some work...
282//!
283//! // Explicitly cleanup (idempotent - safe to call multiple times)
284//! guard.cleanup().await?;
285//!
286//! // Drop will not try to clean up again
287//! # Ok(())
288//! # }
289//! ```
290//!
291//! ## Feature Flag
292//!
293//! This module requires the `testing` feature:
294//!
295//! ```toml
296//! [dev-dependencies]
297//! docker-wrapper = { version = "0.10", features = ["testing", "template-redis"] }
298//! ```
299
300use crate::command::DockerCommand;
301use crate::template::{HasConnectionString, Template, TemplateError};
302use crate::{
303    KillCommand, LogsCommand, NetworkConnectCommand, NetworkCreateCommand,
304    NetworkDisconnectCommand, NetworkRmCommand, PauseCommand, PortCommand, RestartCommand,
305    RmCommand, StopCommand, UnpauseCommand,
306};
307use std::collections::HashMap;
308use std::sync::atomic::{AtomicBool, Ordering};
309use std::sync::Arc;
310use std::time::Duration;
311
312/// Options for controlling container lifecycle behavior.
313#[derive(Debug, Clone)]
314#[allow(clippy::struct_excessive_bools)]
315pub struct GuardOptions {
316    /// Remove container on drop (default: true)
317    pub remove_on_drop: bool,
318    /// Stop container on drop (default: true)
319    pub stop_on_drop: bool,
320    /// Keep container running if test panics (default: false)
321    pub keep_on_panic: bool,
322    /// Capture container logs and print on panic (default: false)
323    pub capture_logs: bool,
324    /// Reuse existing container if already running (default: false)
325    pub reuse_if_running: bool,
326    /// Automatically wait for container to be ready after start (default: false)
327    pub wait_for_ready: bool,
328    /// Network to attach the container to (default: None)
329    pub network: Option<String>,
330    /// Create the network if it doesn't exist (default: true when network is set)
331    pub create_network: bool,
332    /// Remove the network on drop (default: false)
333    pub remove_network_on_drop: bool,
334    /// Timeout for stop operations during cleanup (default: None, uses Docker default)
335    pub stop_timeout: Option<Duration>,
336}
337
338impl Default for GuardOptions {
339    fn default() -> Self {
340        Self {
341            remove_on_drop: true,
342            stop_on_drop: true,
343            keep_on_panic: false,
344            capture_logs: false,
345            reuse_if_running: false,
346            wait_for_ready: false,
347            network: None,
348            create_network: true,
349            remove_network_on_drop: false,
350            stop_timeout: None,
351        }
352    }
353}
354
355/// Builder for creating a [`ContainerGuard`] with custom options.
356pub struct ContainerGuardBuilder<T: Template> {
357    template: T,
358    options: GuardOptions,
359}
360
361impl<T: Template> ContainerGuardBuilder<T> {
362    /// Create a new builder with the given template.
363    #[must_use]
364    pub fn new(template: T) -> Self {
365        Self {
366            template,
367            options: GuardOptions::default(),
368        }
369    }
370
371    /// Set whether to remove the container on drop (default: true).
372    #[must_use]
373    pub fn remove_on_drop(mut self, remove: bool) -> Self {
374        self.options.remove_on_drop = remove;
375        self
376    }
377
378    /// Set whether to stop the container on drop (default: true).
379    #[must_use]
380    pub fn stop_on_drop(mut self, stop: bool) -> Self {
381        self.options.stop_on_drop = stop;
382        self
383    }
384
385    /// Set whether to keep the container running if the test panics (default: false).
386    ///
387    /// This is useful for debugging failed tests - you can inspect the container
388    /// state after the test fails.
389    #[must_use]
390    pub fn keep_on_panic(mut self, keep: bool) -> Self {
391        self.options.keep_on_panic = keep;
392        self
393    }
394
395    /// Set whether to capture container logs and print them on panic (default: false).
396    ///
397    /// When enabled, container logs are buffered and printed to stderr if the
398    /// test panics, making it easier to debug failures.
399    #[must_use]
400    pub fn capture_logs(mut self, capture: bool) -> Self {
401        self.options.capture_logs = capture;
402        self
403    }
404
405    /// Set whether to reuse an existing container if already running (default: false).
406    ///
407    /// This is useful for faster local development iteration - containers can
408    /// be kept running between test runs.
409    #[must_use]
410    pub fn reuse_if_running(mut self, reuse: bool) -> Self {
411        self.options.reuse_if_running = reuse;
412        self
413    }
414
415    /// Set whether to automatically wait for the container to be ready after starting (default: false).
416    ///
417    /// When enabled, `start()` will not return until the container passes its
418    /// readiness check. This is useful for tests that need to immediately connect
419    /// to the service.
420    ///
421    /// # Example
422    ///
423    /// ```rust,no_run
424    /// # use docker_wrapper::testing::ContainerGuard;
425    /// # use docker_wrapper::RedisTemplate;
426    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
427    /// let guard = ContainerGuard::new(RedisTemplate::new("test"))
428    ///     .wait_for_ready(true)
429    ///     .start()
430    ///     .await?;
431    /// // Container is guaranteed ready at this point
432    /// # Ok(())
433    /// # }
434    /// ```
435    #[must_use]
436    pub fn wait_for_ready(mut self, wait: bool) -> Self {
437        self.options.wait_for_ready = wait;
438        self
439    }
440
441    /// Attach the container to a Docker network.
442    ///
443    /// By default, the network will be created if it doesn't exist. Use
444    /// `create_network(false)` to disable automatic network creation.
445    ///
446    /// # Example
447    ///
448    /// ```rust,no_run
449    /// # use docker_wrapper::testing::ContainerGuard;
450    /// # use docker_wrapper::RedisTemplate;
451    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
452    /// let guard = ContainerGuard::new(RedisTemplate::new("redis"))
453    ///     .with_network("test-network")
454    ///     .start()
455    ///     .await?;
456    /// // Container is attached to "test-network"
457    /// # Ok(())
458    /// # }
459    /// ```
460    #[must_use]
461    pub fn with_network(mut self, network: impl Into<String>) -> Self {
462        self.options.network = Some(network.into());
463        self
464    }
465
466    /// Set whether to create the network if it doesn't exist (default: true).
467    ///
468    /// Only applies when a network is specified via `with_network()`.
469    #[must_use]
470    pub fn create_network(mut self, create: bool) -> Self {
471        self.options.create_network = create;
472        self
473    }
474
475    /// Set whether to remove the network on drop (default: false).
476    ///
477    /// This is useful for cleaning up test-specific networks. Only applies
478    /// when a network is specified via `with_network()`.
479    ///
480    /// Note: The network removal will fail silently if other containers are
481    /// still using it.
482    #[must_use]
483    pub fn remove_network_on_drop(mut self, remove: bool) -> Self {
484        self.options.remove_network_on_drop = remove;
485        self
486    }
487
488    /// Set the timeout for stop operations during cleanup (default: Docker default).
489    ///
490    /// This controls how long Docker waits for the container to stop gracefully
491    /// before sending SIGKILL.
492    ///
493    /// # Example
494    ///
495    /// ```rust,no_run
496    /// # use docker_wrapper::testing::ContainerGuard;
497    /// # use docker_wrapper::RedisTemplate;
498    /// # use std::time::Duration;
499    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
500    /// // Fast cleanup with 1 second timeout
501    /// let guard = ContainerGuard::new(RedisTemplate::new("redis"))
502    ///     .stop_timeout(Duration::from_secs(1))
503    ///     .start()
504    ///     .await?;
505    ///
506    /// // Immediate SIGKILL with zero timeout
507    /// let guard = ContainerGuard::new(RedisTemplate::new("redis2"))
508    ///     .stop_timeout(Duration::ZERO)
509    ///     .start()
510    ///     .await?;
511    /// # Ok(())
512    /// # }
513    /// ```
514    #[must_use]
515    pub fn stop_timeout(mut self, timeout: Duration) -> Self {
516        self.options.stop_timeout = Some(timeout);
517        self
518    }
519
520    /// Start the container and return a guard that manages its lifecycle.
521    ///
522    /// If `reuse_if_running` is enabled and a container is already running,
523    /// it will be reused instead of starting a new one.
524    ///
525    /// If `wait_for_ready` is enabled, this method will block until the
526    /// container passes its readiness check.
527    ///
528    /// If a network is specified via `with_network()`, the container will be
529    /// attached to that network. The network will be created if it doesn't
530    /// exist (unless `create_network(false)` was called).
531    ///
532    /// # Errors
533    ///
534    /// Returns an error if the container fails to start or the readiness check times out.
535    pub async fn start(mut self) -> Result<ContainerGuard<T>, TemplateError> {
536        let wait_for_ready = self.options.wait_for_ready;
537        let mut network_created = false;
538
539        // Create network if specified and create_network is enabled
540        if let Some(ref network) = self.options.network {
541            if self.options.create_network {
542                // Try to create the network (ignore errors if it already exists)
543                let result = NetworkCreateCommand::new(network)
544                    .driver("bridge")
545                    .execute()
546                    .await;
547
548                // Track if we successfully created it (for cleanup purposes)
549                network_created = result.is_ok();
550            }
551
552            // Set the network on the template
553            self.template.config_mut().network = Some(network.clone());
554        }
555
556        // Check if we should reuse an existing container
557        if self.options.reuse_if_running {
558            if let Ok(true) = self.template.is_running().await {
559                let guard = ContainerGuard {
560                    template: self.template,
561                    container_id: None, // We don't have the ID for reused containers
562                    options: self.options,
563                    was_reused: true,
564                    network_created,
565                    cleaned_up: Arc::new(AtomicBool::new(false)),
566                };
567
568                // Wait for ready if configured (even for reused containers)
569                if wait_for_ready {
570                    guard.wait_for_ready().await?;
571                }
572
573                return Ok(guard);
574            }
575        }
576
577        // Start the container
578        let container_id = self.template.start_and_wait().await?;
579
580        let guard = ContainerGuard {
581            template: self.template,
582            container_id: Some(container_id),
583            options: self.options,
584            was_reused: false,
585            network_created,
586            cleaned_up: Arc::new(AtomicBool::new(false)),
587        };
588
589        // Wait for ready if configured
590        if wait_for_ready {
591            guard.wait_for_ready().await?;
592        }
593
594        Ok(guard)
595    }
596}
597
598/// RAII guard for automatic container lifecycle management.
599///
600/// When this guard is dropped, the container is automatically stopped and
601/// removed (unless configured otherwise via [`ContainerGuardBuilder`]).
602///
603/// # Example
604///
605/// ```rust,no_run
606/// use docker_wrapper::testing::ContainerGuard;
607/// use docker_wrapper::RedisTemplate;
608///
609/// #[tokio::test]
610/// async fn test_example() -> Result<(), Box<dyn std::error::Error>> {
611///     let guard = ContainerGuard::new(RedisTemplate::new("test"))
612///         .keep_on_panic(true)  // Keep container for debugging if test fails
613///         .capture_logs(true)   // Print logs on failure
614///         .start()
615///         .await?;
616///
617///     // Container is automatically cleaned up when guard goes out of scope
618///     Ok(())
619/// }
620/// ```
621pub struct ContainerGuard<T: Template> {
622    template: T,
623    container_id: Option<String>,
624    options: GuardOptions,
625    was_reused: bool,
626    network_created: bool,
627    cleaned_up: Arc<AtomicBool>,
628}
629
630impl<T: Template> ContainerGuard<T> {
631    /// Create a new builder for a container guard.
632    ///
633    /// Note: This returns a builder, not a `ContainerGuard`. Call `.start().await`
634    /// on the builder to create the guard.
635    #[allow(clippy::new_ret_no_self)]
636    pub fn new(template: T) -> ContainerGuardBuilder<T> {
637        ContainerGuardBuilder::new(template)
638    }
639
640    /// Get a reference to the underlying template.
641    #[must_use]
642    pub fn template(&self) -> &T {
643        &self.template
644    }
645
646    /// Get the container ID, if available.
647    ///
648    /// This may be `None` if the container was reused from a previous run.
649    #[must_use]
650    pub fn container_id(&self) -> Option<&str> {
651        self.container_id.as_deref()
652    }
653
654    /// Check if this guard is reusing an existing container.
655    #[must_use]
656    pub fn was_reused(&self) -> bool {
657        self.was_reused
658    }
659
660    /// Get the network name, if one was configured.
661    #[must_use]
662    pub fn network(&self) -> Option<&str> {
663        self.options.network.as_deref()
664    }
665
666    /// Check if the container is currently running.
667    ///
668    /// # Errors
669    ///
670    /// Returns an error if the Docker command fails.
671    pub async fn is_running(&self) -> Result<bool, TemplateError> {
672        self.template.is_running().await
673    }
674
675    /// Wait for the container to be ready.
676    ///
677    /// This calls the underlying template's readiness check. The exact behavior
678    /// depends on the template implementation - for example, Redis templates
679    /// wait for a successful PING response.
680    ///
681    /// # Errors
682    ///
683    /// Returns an error if the readiness check times out or the Docker command fails.
684    ///
685    /// # Example
686    ///
687    /// ```rust,no_run
688    /// # use docker_wrapper::testing::ContainerGuard;
689    /// # use docker_wrapper::RedisTemplate;
690    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
691    /// let guard = ContainerGuard::new(RedisTemplate::new("test"))
692    ///     .start()
693    ///     .await?;
694    ///
695    /// // Wait for Redis to be ready to accept connections
696    /// guard.wait_for_ready().await?;
697    /// # Ok(())
698    /// # }
699    /// ```
700    pub async fn wait_for_ready(&self) -> Result<(), TemplateError> {
701        self.template.wait_for_ready().await
702    }
703
704    /// Get the host port mapped to a container port.
705    ///
706    /// This is useful when using dynamic port allocation - Docker assigns
707    /// a random available host port which you can query with this method.
708    ///
709    /// # Errors
710    ///
711    /// Returns an error if the Docker command fails or no port mapping is found.
712    ///
713    /// # Example
714    ///
715    /// ```rust,no_run
716    /// # use docker_wrapper::testing::ContainerGuard;
717    /// # use docker_wrapper::RedisTemplate;
718    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
719    /// let guard = ContainerGuard::new(RedisTemplate::new("test"))
720    ///     .start()
721    ///     .await?;
722    ///
723    /// let host_port = guard.host_port(6379).await?;
724    /// println!("Redis available at localhost:{}", host_port);
725    /// # Ok(())
726    /// # }
727    /// ```
728    pub async fn host_port(&self, container_port: u16) -> Result<u16, TemplateError> {
729        let container_name = self.template.config().name.clone();
730        let result = PortCommand::new(&container_name)
731            .port(container_port)
732            .run()
733            .await
734            .map_err(TemplateError::DockerError)?;
735
736        // Return the first matching port mapping
737        if let Some(mapping) = result.port_mappings.first() {
738            return Ok(mapping.host_port);
739        }
740
741        Err(TemplateError::InvalidConfig(format!(
742            "No host port mapping found for container port {container_port}"
743        )))
744    }
745
746    /// Get the container logs.
747    ///
748    /// # Errors
749    ///
750    /// Returns an error if the Docker command fails.
751    pub async fn logs(&self) -> Result<String, TemplateError> {
752        let container_name = self.template.config().name.clone();
753        let result = LogsCommand::new(&container_name)
754            .execute()
755            .await
756            .map_err(TemplateError::DockerError)?;
757
758        Ok(format!("{}{}", result.stdout, result.stderr))
759    }
760
761    /// Manually stop the container.
762    ///
763    /// The container will still be removed on drop if `remove_on_drop` is enabled.
764    ///
765    /// # Errors
766    ///
767    /// Returns an error if the Docker command fails.
768    pub async fn stop(&self) -> Result<(), TemplateError> {
769        self.template.stop().await
770    }
771
772    /// Manually clean up the container (stop and remove).
773    ///
774    /// After calling this, the drop implementation will not attempt cleanup again.
775    ///
776    /// # Errors
777    ///
778    /// Returns an error if the Docker commands fail.
779    pub async fn cleanup(&self) -> Result<(), TemplateError> {
780        if self.cleaned_up.swap(true, Ordering::SeqCst) {
781            return Ok(()); // Already cleaned up
782        }
783
784        if self.options.stop_on_drop {
785            let _ = self.template.stop().await;
786        }
787        if self.options.remove_on_drop {
788            let _ = self.template.remove().await;
789        }
790        Ok(())
791    }
792}
793
794/// Fault-injection helpers for chaos testing.
795///
796/// These are thin convenience wrappers over the underlying Docker commands
797/// ([`PauseCommand`], [`UnpauseCommand`], [`KillCommand`], [`RestartCommand`],
798/// [`NetworkConnectCommand`], [`NetworkDisconnectCommand`]) that target the
799/// guard's container by name. They make it easy to simulate real faults --
800/// hangs, crashes, and network partitions -- against a service under test
801/// without hand-rolling the command plumbing.
802///
803/// # Example
804///
805/// ```rust,no_run
806/// # use docker_wrapper::testing::ContainerGuard;
807/// # use docker_wrapper::RedisTemplate;
808/// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
809/// let guard = ContainerGuard::new(RedisTemplate::new("redis"))
810///     .wait_for_ready(true)
811///     .start()
812///     .await?;
813///
814/// // Freeze the container's processes (simulate a hang), then resume.
815/// guard.pause().await?;
816/// guard.unpause().await?;
817///
818/// // Kill the container outright (simulate a crash).
819/// guard.crash().await?;
820/// # Ok(())
821/// # }
822/// ```
823impl<T: Template> ContainerGuard<T> {
824    /// Pause all processes in the container (simulate a hang).
825    ///
826    /// This freezes the container's processes using `docker pause`. They can be
827    /// resumed with [`unpause`](Self::unpause).
828    ///
829    /// # Errors
830    ///
831    /// Returns an error if the Docker command fails (for example, if the
832    /// container is not running).
833    pub async fn pause(&self) -> Result<(), TemplateError> {
834        let name = self.template.config().name.clone();
835        PauseCommand::new(name)
836            .run()
837            .await
838            .map_err(TemplateError::DockerError)?;
839        Ok(())
840    }
841
842    /// Resume a paused container.
843    ///
844    /// This is the inverse of [`pause`](Self::pause) and uses `docker unpause`.
845    ///
846    /// # Errors
847    ///
848    /// Returns an error if the Docker command fails (for example, if the
849    /// container is not paused).
850    pub async fn unpause(&self) -> Result<(), TemplateError> {
851        let name = self.template.config().name.clone();
852        UnpauseCommand::new(name)
853            .run()
854            .await
855            .map_err(TemplateError::DockerError)?;
856        Ok(())
857    }
858
859    /// Kill the container immediately with SIGKILL (simulate a crash).
860    ///
861    /// This sends `SIGKILL` via `docker kill`, terminating the container
862    /// without a graceful shutdown.
863    ///
864    /// # Errors
865    ///
866    /// Returns an error if the Docker command fails (for example, if the
867    /// container is not running).
868    pub async fn crash(&self) -> Result<(), TemplateError> {
869        let name = self.template.config().name.clone();
870        KillCommand::new(name)
871            .signal("SIGKILL")
872            .run()
873            .await
874            .map_err(TemplateError::DockerError)?;
875        Ok(())
876    }
877
878    /// Restart the container.
879    ///
880    /// This stops and starts the container via `docker restart`, which is useful
881    /// for simulating a recovery after a fault.
882    ///
883    /// # Errors
884    ///
885    /// Returns an error if the Docker command fails.
886    pub async fn restart_container(&self) -> Result<(), TemplateError> {
887        let name = self.template.config().name.clone();
888        RestartCommand::new(name)
889            .execute()
890            .await
891            .map_err(TemplateError::DockerError)?;
892        Ok(())
893    }
894
895    /// Partition the container from a network (simulate a network outage).
896    ///
897    /// This disconnects the container from `network` via `docker network
898    /// disconnect`, cutting it off from other containers on that network. Use
899    /// [`heal`](Self::heal) to reconnect.
900    ///
901    /// # Errors
902    ///
903    /// Returns an error if the Docker command fails (for example, if the
904    /// container is not attached to the network).
905    pub async fn partition(&self, network: impl Into<String>) -> Result<(), TemplateError> {
906        let name = self.template.config().name.clone();
907        NetworkDisconnectCommand::new(network.into(), name)
908            .force()
909            .run()
910            .await
911            .map_err(TemplateError::DockerError)?;
912        Ok(())
913    }
914
915    /// Reconnect the container to a network after a partition.
916    ///
917    /// This is the inverse of [`partition`](Self::partition) and uses `docker
918    /// network connect`.
919    ///
920    /// # Errors
921    ///
922    /// Returns an error if the Docker command fails.
923    pub async fn heal(&self, network: impl Into<String>) -> Result<(), TemplateError> {
924        let name = self.template.config().name.clone();
925        NetworkConnectCommand::new(network.into(), name)
926            .run()
927            .await
928            .map_err(TemplateError::DockerError)?;
929        Ok(())
930    }
931}
932
933impl<T: Template + HasConnectionString> ContainerGuard<T> {
934    /// Get the connection string for the underlying service.
935    ///
936    /// This is a convenience method that delegates to the template's
937    /// `connection_string()` implementation. The format depends on the
938    /// service type (e.g., `redis://host:port` for Redis).
939    ///
940    /// This method is only available for templates that implement
941    /// [`HasConnectionString`].
942    ///
943    /// # Example
944    ///
945    /// ```rust,no_run
946    /// # use docker_wrapper::testing::ContainerGuard;
947    /// # use docker_wrapper::RedisTemplate;
948    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
949    /// let guard = ContainerGuard::new(RedisTemplate::new("redis").port(6379))
950    ///     .start()
951    ///     .await?;
952    ///
953    /// // Direct access to connection string
954    /// let conn = guard.connection_string();
955    /// // Instead of: guard.template().connection_string()
956    /// # Ok(())
957    /// # }
958    /// ```
959    #[must_use]
960    pub fn connection_string(&self) -> String {
961        self.template.connection_string()
962    }
963}
964
965impl<T: Template> Drop for ContainerGuard<T> {
966    fn drop(&mut self) {
967        // Skip cleanup if already done
968        if self.cleaned_up.load(Ordering::SeqCst) {
969            return;
970        }
971
972        // Skip cleanup for reused containers if not configured to clean them
973        if self.was_reused && !self.options.remove_on_drop {
974            return;
975        }
976
977        // Check if we're panicking
978        let panicking = std::thread::panicking();
979
980        if panicking && self.options.keep_on_panic {
981            let name = &self.template.config().name;
982            eprintln!("[ContainerGuard] Test panicked, keeping container '{name}' for debugging");
983
984            if self.options.capture_logs {
985                // Try to get logs - spawn a thread to avoid runtime conflicts
986                let container_name = self.template.config().name.clone();
987                let _ = std::thread::spawn(move || {
988                    if let Ok(rt) = tokio::runtime::Builder::new_current_thread()
989                        .enable_all()
990                        .build()
991                    {
992                        if let Ok(result) =
993                            rt.block_on(async { LogsCommand::new(&container_name).execute().await })
994                        {
995                            let logs = format!("{}{}", result.stdout, result.stderr);
996                            eprintln!("[ContainerGuard] Container logs for '{container_name}':");
997                            eprintln!("{logs}");
998                        }
999                    }
1000                })
1001                .join();
1002            }
1003            return;
1004        }
1005
1006        // Mark as cleaned up
1007        self.cleaned_up.store(true, Ordering::SeqCst);
1008
1009        // Perform cleanup - need to spawn a runtime since Drop isn't async
1010        let should_stop = self.options.stop_on_drop;
1011        let should_remove = self.options.remove_on_drop;
1012        let should_remove_network = self.options.remove_network_on_drop && self.network_created;
1013        let container_name = self.template.config().name.clone();
1014        let network_name = self.options.network.clone();
1015        let stop_timeout = self.options.stop_timeout;
1016
1017        if !should_stop && !should_remove && !should_remove_network {
1018            return;
1019        }
1020
1021        // Perform cleanup - try to use existing runtime if available,
1022        // otherwise create a new one (for non-async contexts)
1023        if tokio::runtime::Handle::try_current().is_ok() {
1024            // We're in an async context - use spawn_blocking to avoid blocking the runtime
1025            let container_name_clone = container_name.clone();
1026            let network_name_clone = network_name.clone();
1027            let _ = std::thread::spawn(move || {
1028                let rt = tokio::runtime::Builder::new_current_thread()
1029                    .enable_all()
1030                    .build()
1031                    .expect("Failed to create runtime for cleanup");
1032                rt.block_on(async {
1033                    if should_stop {
1034                        let mut cmd = StopCommand::new(&container_name_clone);
1035                        if let Some(timeout) = stop_timeout {
1036                            cmd = cmd.timeout_duration(timeout);
1037                        }
1038                        let _ = cmd.execute().await;
1039                    }
1040                    if should_remove {
1041                        let _ = RmCommand::new(&container_name_clone).force().run().await;
1042                    }
1043                    // Remove network after container (network must be empty)
1044                    if should_remove_network {
1045                        if let Some(ref network) = network_name_clone {
1046                            let _ = NetworkRmCommand::new(network).execute().await;
1047                        }
1048                    }
1049                });
1050            })
1051            .join();
1052        } else {
1053            // Not in an async context - create a new runtime
1054            if let Ok(rt) = tokio::runtime::Builder::new_current_thread()
1055                .enable_all()
1056                .build()
1057            {
1058                rt.block_on(async {
1059                    if should_stop {
1060                        let mut cmd = StopCommand::new(&container_name);
1061                        if let Some(timeout) = stop_timeout {
1062                            cmd = cmd.timeout_duration(timeout);
1063                        }
1064                        let _ = cmd.execute().await;
1065                    }
1066                    if should_remove {
1067                        let _ = RmCommand::new(&container_name).force().run().await;
1068                    }
1069                    // Remove network after container (network must be empty)
1070                    if should_remove_network {
1071                        if let Some(ref network) = network_name {
1072                            let _ = NetworkRmCommand::new(network).execute().await;
1073                        }
1074                    }
1075                });
1076            }
1077        }
1078    }
1079}
1080
1081/// A type-erased container guard entry for use in `ContainerGuardSet`.
1082///
1083/// This allows storing guards with different template types in the same collection.
1084#[allow(dead_code)]
1085struct GuardEntry {
1086    /// Container name for lookup
1087    name: String,
1088    /// Cleanup function to stop and remove the container
1089    cleanup_fn: Box<dyn FnOnce() + Send>,
1090}
1091
1092/// Options for `ContainerGuardSet`.
1093#[derive(Debug, Clone, Default)]
1094#[allow(clippy::struct_excessive_bools)]
1095pub struct GuardSetOptions {
1096    /// Shared network for all containers
1097    pub network: Option<String>,
1098    /// Create the network if it doesn't exist (default: true)
1099    pub create_network: bool,
1100    /// Remove the network on drop (default: true when network is set)
1101    pub remove_network_on_drop: bool,
1102    /// Keep containers running if test panics (default: false)
1103    pub keep_on_panic: bool,
1104    /// Wait for each container to be ready after starting (default: true)
1105    pub wait_for_ready: bool,
1106}
1107
1108impl GuardSetOptions {
1109    fn new() -> Self {
1110        Self {
1111            network: None,
1112            create_network: true,
1113            remove_network_on_drop: true,
1114            keep_on_panic: false,
1115            wait_for_ready: true,
1116        }
1117    }
1118}
1119
1120/// A pending template entry waiting to be started.
1121struct PendingEntry<T: Template + 'static> {
1122    template: T,
1123}
1124
1125/// Type-erased pending entry trait.
1126trait PendingEntryTrait: Send {
1127    /// Get the container name
1128    fn name(&self) -> String;
1129    /// Start the container and return a cleanup function
1130    fn start(
1131        self: Box<Self>,
1132        network: Option<String>,
1133        wait_for_ready: bool,
1134        keep_on_panic: bool,
1135    ) -> std::pin::Pin<
1136        Box<dyn std::future::Future<Output = Result<GuardEntry, TemplateError>> + Send>,
1137    >;
1138}
1139
1140impl<T: Template + 'static> PendingEntryTrait for PendingEntry<T> {
1141    fn name(&self) -> String {
1142        self.template.config().name.clone()
1143    }
1144
1145    fn start(
1146        self: Box<Self>,
1147        network: Option<String>,
1148        wait_for_ready: bool,
1149        keep_on_panic: bool,
1150    ) -> std::pin::Pin<
1151        Box<dyn std::future::Future<Output = Result<GuardEntry, TemplateError>> + Send>,
1152    > {
1153        Box::pin(async move {
1154            let mut template = self.template;
1155            let name = template.config().name.clone();
1156
1157            // Set network if provided
1158            if let Some(ref net) = network {
1159                template.config_mut().network = Some(net.clone());
1160            }
1161
1162            // Start the container
1163            template.start_and_wait().await?;
1164
1165            // Wait for ready if configured
1166            if wait_for_ready {
1167                template.wait_for_ready().await?;
1168            }
1169
1170            // Create cleanup function
1171            let cleanup_name = name.clone();
1172            let cleanup_fn: Box<dyn FnOnce() + Send> = Box::new(move || {
1173                // Check if panicking and should keep
1174                if std::thread::panicking() && keep_on_panic {
1175                    eprintln!(
1176                        "[ContainerGuardSet] Test panicked, keeping container '{cleanup_name}' for debugging"
1177                    );
1178                    return;
1179                }
1180
1181                // Perform cleanup in a new runtime
1182                let _ = std::thread::spawn(move || {
1183                    if let Ok(rt) = tokio::runtime::Builder::new_current_thread()
1184                        .enable_all()
1185                        .build()
1186                    {
1187                        rt.block_on(async {
1188                            let _ = StopCommand::new(&cleanup_name).execute().await;
1189                            let _ = RmCommand::new(&cleanup_name).force().run().await;
1190                        });
1191                    }
1192                })
1193                .join();
1194            });
1195
1196            Ok(GuardEntry { name, cleanup_fn })
1197        })
1198    }
1199}
1200
1201/// Builder for creating a [`ContainerGuardSet`].
1202///
1203/// # Example
1204///
1205/// ```rust,no_run
1206/// use docker_wrapper::testing::ContainerGuardSet;
1207/// use docker_wrapper::RedisTemplate;
1208///
1209/// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
1210/// let guards = ContainerGuardSet::new()
1211///     .with_network("test-network")
1212///     .add(RedisTemplate::new("redis-1"))
1213///     .add(RedisTemplate::new("redis-2"))
1214///     .start_all()
1215///     .await?;
1216///
1217/// // Access by name
1218/// assert!(guards.contains("redis-1"));
1219/// # Ok(())
1220/// # }
1221/// ```
1222pub struct ContainerGuardSetBuilder {
1223    entries: Vec<Box<dyn PendingEntryTrait>>,
1224    options: GuardSetOptions,
1225}
1226
1227impl ContainerGuardSetBuilder {
1228    /// Create a new builder.
1229    #[must_use]
1230    pub fn new() -> Self {
1231        Self {
1232            entries: Vec::new(),
1233            options: GuardSetOptions::new(),
1234        }
1235    }
1236
1237    /// Add a template to the set.
1238    ///
1239    /// The container name from the template's config is used as the key for lookup.
1240    #[allow(clippy::should_implement_trait)]
1241    #[must_use]
1242    pub fn add<T: Template + 'static>(mut self, template: T) -> Self {
1243        self.entries.push(Box::new(PendingEntry { template }));
1244        self
1245    }
1246
1247    /// Set a shared network for all containers.
1248    ///
1249    /// The network will be created if it doesn't exist (unless `create_network(false)` is called).
1250    #[must_use]
1251    pub fn with_network(mut self, network: impl Into<String>) -> Self {
1252        self.options.network = Some(network.into());
1253        self
1254    }
1255
1256    /// Set whether to create the network if it doesn't exist (default: true).
1257    #[must_use]
1258    pub fn create_network(mut self, create: bool) -> Self {
1259        self.options.create_network = create;
1260        self
1261    }
1262
1263    /// Set whether to remove the network on drop (default: true when network is set).
1264    #[must_use]
1265    pub fn remove_network_on_drop(mut self, remove: bool) -> Self {
1266        self.options.remove_network_on_drop = remove;
1267        self
1268    }
1269
1270    /// Set whether to keep containers running if the test panics (default: false).
1271    #[must_use]
1272    pub fn keep_on_panic(mut self, keep: bool) -> Self {
1273        self.options.keep_on_panic = keep;
1274        self
1275    }
1276
1277    /// Set whether to wait for each container to be ready (default: true).
1278    #[must_use]
1279    pub fn wait_for_ready(mut self, wait: bool) -> Self {
1280        self.options.wait_for_ready = wait;
1281        self
1282    }
1283
1284    /// Start all containers and return a guard set.
1285    ///
1286    /// Containers are started sequentially in the order they were added.
1287    ///
1288    /// # Errors
1289    ///
1290    /// Returns an error if any container fails to start. Containers that were
1291    /// successfully started before the failure will be cleaned up.
1292    pub async fn start_all(self) -> Result<ContainerGuardSet, TemplateError> {
1293        let mut network_created = false;
1294
1295        // Create network if needed
1296        if let Some(ref network) = self.options.network {
1297            if self.options.create_network {
1298                let result = NetworkCreateCommand::new(network)
1299                    .driver("bridge")
1300                    .execute()
1301                    .await;
1302                network_created = result.is_ok();
1303            }
1304        }
1305
1306        let mut guards: Vec<GuardEntry> = Vec::new();
1307        let mut names: HashMap<String, usize> = HashMap::new();
1308
1309        // Start each container
1310        for entry in self.entries {
1311            let name = entry.name();
1312            match entry
1313                .start(
1314                    self.options.network.clone(),
1315                    self.options.wait_for_ready,
1316                    self.options.keep_on_panic,
1317                )
1318                .await
1319            {
1320                Ok(guard) => {
1321                    names.insert(name, guards.len());
1322                    guards.push(guard);
1323                }
1324                Err(e) => {
1325                    // Clean up already-started containers on failure
1326                    for guard in guards {
1327                        (guard.cleanup_fn)();
1328                    }
1329                    // Clean up network if we created it
1330                    if network_created {
1331                        if let Some(ref network) = self.options.network {
1332                            let net = network.clone();
1333                            let _ = std::thread::spawn(move || {
1334                                if let Ok(rt) = tokio::runtime::Builder::new_current_thread()
1335                                    .enable_all()
1336                                    .build()
1337                                {
1338                                    rt.block_on(async {
1339                                        let _ = NetworkRmCommand::new(&net).execute().await;
1340                                    });
1341                                }
1342                            })
1343                            .join();
1344                        }
1345                    }
1346                    return Err(e);
1347                }
1348            }
1349        }
1350
1351        Ok(ContainerGuardSet {
1352            guards,
1353            names,
1354            options: self.options,
1355            network_created,
1356        })
1357    }
1358}
1359
1360impl Default for ContainerGuardSetBuilder {
1361    fn default() -> Self {
1362        Self::new()
1363    }
1364}
1365
1366/// Manages multiple containers as a group with coordinated lifecycle.
1367///
1368/// All containers are cleaned up when the set is dropped. This is useful for
1369/// integration tests that require multiple services.
1370///
1371/// # Example
1372///
1373/// ```rust,no_run
1374/// use docker_wrapper::testing::ContainerGuardSet;
1375/// use docker_wrapper::RedisTemplate;
1376///
1377/// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
1378/// let guards = ContainerGuardSet::new()
1379///     .with_network("test-network")
1380///     .add(RedisTemplate::new("redis"))
1381///     .keep_on_panic(true)
1382///     .start_all()
1383///     .await?;
1384///
1385/// // Check if container exists
1386/// assert!(guards.contains("redis"));
1387///
1388/// // Get container names
1389/// for name in guards.names() {
1390///     println!("Container: {}", name);
1391/// }
1392/// # Ok(())
1393/// # }
1394/// ```
1395pub struct ContainerGuardSet {
1396    guards: Vec<GuardEntry>,
1397    names: HashMap<String, usize>,
1398    options: GuardSetOptions,
1399    network_created: bool,
1400}
1401
1402impl ContainerGuardSet {
1403    /// Create a new builder for a container guard set.
1404    #[allow(clippy::new_ret_no_self)]
1405    #[must_use]
1406    pub fn new() -> ContainerGuardSetBuilder {
1407        ContainerGuardSetBuilder::new()
1408    }
1409
1410    /// Check if a container with the given name exists in the set.
1411    #[must_use]
1412    pub fn contains(&self, name: &str) -> bool {
1413        self.names.contains_key(name)
1414    }
1415
1416    /// Get an iterator over container names in the set.
1417    pub fn names(&self) -> impl Iterator<Item = &str> {
1418        self.names.keys().map(String::as_str)
1419    }
1420
1421    /// Get the number of containers in the set.
1422    #[must_use]
1423    pub fn len(&self) -> usize {
1424        self.guards.len()
1425    }
1426
1427    /// Check if the set is empty.
1428    #[must_use]
1429    pub fn is_empty(&self) -> bool {
1430        self.guards.is_empty()
1431    }
1432
1433    /// Get the shared network name, if one was configured.
1434    #[must_use]
1435    pub fn network(&self) -> Option<&str> {
1436        self.options.network.as_deref()
1437    }
1438}
1439
1440impl Default for ContainerGuardSet {
1441    fn default() -> Self {
1442        Self {
1443            guards: Vec::new(),
1444            names: HashMap::new(),
1445            options: GuardSetOptions::new(),
1446            network_created: false,
1447        }
1448    }
1449}
1450
1451impl Drop for ContainerGuardSet {
1452    fn drop(&mut self) {
1453        // Clean up all containers
1454        for guard in self.guards.drain(..) {
1455            (guard.cleanup_fn)();
1456        }
1457
1458        // Clean up network if we created it
1459        if self.network_created && self.options.remove_network_on_drop {
1460            if let Some(ref network) = self.options.network {
1461                let net = network.clone();
1462                let _ = std::thread::spawn(move || {
1463                    if let Ok(rt) = tokio::runtime::Builder::new_current_thread()
1464                        .enable_all()
1465                        .build()
1466                    {
1467                        rt.block_on(async {
1468                            let _ = NetworkRmCommand::new(&net).execute().await;
1469                        });
1470                    }
1471                })
1472                .join();
1473            }
1474        }
1475    }
1476}
1477
1478#[cfg(test)]
1479mod tests {
1480    use super::*;
1481
1482    #[test]
1483    fn test_guard_options_default() {
1484        let opts = GuardOptions::default();
1485        assert!(opts.remove_on_drop);
1486        assert!(opts.stop_on_drop);
1487        assert!(!opts.keep_on_panic);
1488        assert!(!opts.capture_logs);
1489        assert!(!opts.reuse_if_running);
1490        assert!(!opts.wait_for_ready);
1491        assert!(opts.network.is_none());
1492        assert!(opts.create_network);
1493        assert!(!opts.remove_network_on_drop);
1494        assert!(opts.stop_timeout.is_none());
1495    }
1496
1497    #[test]
1498    fn test_builder_options() {
1499        // We can't easily test the builder without a real template,
1500        // but we can at least verify the module compiles
1501    }
1502}