1use crate::backend::volume::{VolumeMount, VolumeValidator};
7use crate::backend::{Backend, Cmd, RunResult};
8use crate::error::{BackendError, Result};
9use crate::policy::Policy;
10use std::sync::Arc;
11use std::time::{Duration, Instant};
12use testcontainers::{core::ExecCommand, runners::SyncRunner, GenericImage, ImageExt};
13
14#[cfg(feature = "otel-traces")]
15use tracing::{info, instrument, warn};
16
17#[derive(Debug, Clone)]
19pub struct TestcontainerBackend {
20 image_name: String,
22 image_tag: String,
23 policy: Policy,
25 timeout: Duration,
27 startup_timeout: Duration,
29 env_vars: std::collections::HashMap<String, String>,
31 default_command: Option<Vec<String>>,
33 volume_mounts: Vec<VolumeMount>,
35 volume_validator: Arc<VolumeValidator>,
37 memory_limit: Option<u64>,
39 cpu_limit: Option<f64>,
41}
42
43impl TestcontainerBackend {
44 pub fn new(image: impl Into<String>) -> Result<Self> {
46 let image_str = image.into();
47
48 let (image_name, image_tag) = if let Some((name, tag)) = image_str.split_once(':') {
50 (name.to_string(), tag.to_string())
51 } else {
52 (image_str, "latest".to_string())
53 };
54
55 Ok(Self {
56 image_name,
57 image_tag,
58 policy: Policy::default(),
59 timeout: Duration::from_secs(30), startup_timeout: Duration::from_secs(10), env_vars: std::collections::HashMap::new(),
62 default_command: None,
63 volume_mounts: Vec::new(),
64 volume_validator: Arc::new(VolumeValidator::default()),
65 memory_limit: None,
66 cpu_limit: None,
67 })
68 }
69
70 pub fn with_policy(mut self, policy: Policy) -> Self {
72 self.policy = policy;
73 self
74 }
75
76 pub fn with_timeout(mut self, timeout: Duration) -> Self {
78 self.timeout = timeout;
79 self
80 }
81
82 pub fn with_startup_timeout(mut self, timeout: Duration) -> Self {
84 self.startup_timeout = timeout;
85 self
86 }
87
88 pub fn is_running(&self) -> bool {
90 true
93 }
94
95 pub fn with_env(mut self, key: &str, val: &str) -> Self {
97 self.env_vars.insert(key.to_string(), val.to_string());
98 self
99 }
100
101 pub fn with_cmd(mut self, cmd: Vec<String>) -> Self {
103 self.default_command = Some(cmd);
104 self
105 }
106
107 pub fn with_volume(
119 mut self,
120 host_path: &str,
121 container_path: &str,
122 read_only: bool,
123 ) -> Result<Self> {
124 let mount = VolumeMount::new(host_path, container_path, read_only)?;
125 self.volume_validator.validate(&mount)?;
126 self.volume_mounts.push(mount);
127 Ok(self)
128 }
129
130 pub fn with_volume_ro(self, host_path: &str, container_path: &str) -> Result<Self> {
134 self.with_volume(host_path, container_path, true)
135 }
136
137 pub fn with_volume_validator(mut self, validator: VolumeValidator) -> Self {
139 self.volume_validator = Arc::new(validator);
140 self
141 }
142
143 pub fn volumes(&self) -> &[VolumeMount] {
145 &self.volume_mounts
146 }
147
148 pub fn with_memory_limit(mut self, limit_mb: u64) -> Self {
150 self.memory_limit = Some(limit_mb);
151 self
152 }
153
154 pub fn with_cpu_limit(mut self, cpus: f64) -> Self {
156 self.cpu_limit = Some(cpus);
157 self
158 }
159
160 pub fn is_available() -> bool {
162 true
164 }
165
166 #[cfg(feature = "otel-traces")]
174 pub fn validate_otel_instrumentation(&self) -> Result<bool> {
175 use crate::telemetry::validation::is_otel_initialized;
177
178 if !is_otel_initialized() {
179 return Err(crate::error::CleanroomError::validation_error(
180 "OpenTelemetry is not initialized. Enable OTEL features and call init_otel()",
181 ));
182 }
183
184 Ok(true)
187 }
188
189 #[cfg(feature = "otel-traces")]
191 pub fn otel_validation_enabled(&self) -> bool {
192 true
193 }
194
195 #[cfg(not(feature = "otel-traces"))]
196 pub fn otel_validation_enabled(&self) -> bool {
197 false
198 }
199
200 #[cfg_attr(feature = "otel-traces", instrument(name = "clnrm.container.exec", skip(self, cmd), fields(container.image = %self.image_name, container.tag = %self.image_tag, component = "container_backend")))]
202 fn execute_in_container(&self, cmd: &Cmd) -> Result<RunResult> {
203 let start_time = Instant::now();
204
205 #[cfg(feature = "otel-traces")]
206 info!(
207 "Starting container with image {}:{}",
208 self.image_name, self.image_tag
209 );
210
211 #[allow(unused_variables)]
213 let container_id = uuid::Uuid::new_v4().to_string();
214
215 #[cfg(feature = "otel-traces")]
216 {
217 use opentelemetry::global;
218 use opentelemetry::trace::{Span, Tracer, TracerProvider};
219 use crate::telemetry::events;
220
221 let tracer_provider = global::tracer_provider();
223 let mut span = tracer_provider
224 .tracer("clnrm-backend")
225 .start("clnrm.container.start");
226
227 events::record_container_start(
228 &mut span,
229 &format!("{}:{}", self.image_name, self.image_tag),
230 &container_id,
231 );
232 span.end();
233 }
234
235 let image = GenericImage::new(self.image_name.clone(), self.image_tag.clone());
239
240 let mut container_request: testcontainers::core::ContainerRequest<
242 testcontainers::GenericImage,
243 > = image.into();
244
245 for (key, value) in &self.env_vars {
247 container_request = container_request.with_env_var(key, value);
248 }
249
250 for (key, value) in &cmd.env {
252 container_request = container_request.with_env_var(key, value);
253 }
254
255 for (key, value) in self.policy.to_env() {
257 container_request = container_request.with_env_var(key, value);
258 }
259
260 for mount in &self.volume_mounts {
262 use testcontainers::core::{AccessMode, Mount};
263
264 let access_mode = if mount.is_read_only() {
265 AccessMode::ReadOnly
266 } else {
267 AccessMode::ReadWrite
268 };
269
270 let bind_mount = Mount::bind_mount(
271 mount.host_path().to_string_lossy().to_string(),
272 mount.container_path().to_string_lossy().to_string(),
273 )
274 .with_access_mode(access_mode);
275
276 container_request = container_request.with_mount(bind_mount);
277 }
278
279 container_request = container_request.with_cmd(vec!["sleep", "3600"]);
282
283 if let Some(workdir) = &cmd.workdir {
285 container_request =
286 container_request.with_working_dir(workdir.to_string_lossy().to_string());
287 }
288
289 let container_start_time = Instant::now();
291 let container = container_request
292 .start()
293 .map_err(|e| {
294 let elapsed = container_start_time.elapsed();
295 if elapsed > Duration::from_secs(10) {
296 #[cfg(feature = "otel-traces")]
297 warn!("Container startup took {}s, which is longer than expected. First pull of image may take time.", elapsed.as_secs());
298 }
299
300 BackendError::Runtime(format!(
301 "Failed to start container with image '{}:{}' after {}s.\n\
302 Possible causes:\n\
303 - Docker daemon not running (try: docker ps)\n\
304 - Image needs to be pulled (first run may take longer)\n\
305 - Network issues preventing image pull\n\
306 Try: Increase startup timeout or check Docker status\n\
307 Original error: {}",
308 self.image_name, self.image_tag, elapsed.as_secs(), e
309 ))
310 })?;
311
312 #[cfg(feature = "otel-traces")]
313 info!("Container started successfully, executing command");
314
315 let cmd_args: Vec<&str> = std::iter::once(cmd.bin.as_str())
317 .chain(cmd.args.iter().map(|s| s.as_str()))
318 .collect();
319
320 #[allow(unused_variables)]
321 let cmd_string = format!("{} {}", cmd.bin, cmd.args.join(" "));
322
323 let exec_cmd = ExecCommand::new(cmd_args);
324 let mut exec_result = container
325 .exec(exec_cmd)
326 .map_err(|e| BackendError::Runtime(format!("Command execution failed: {}", e)))?;
327
328 let duration_ms = start_time.elapsed().as_millis() as u64;
329
330 #[cfg(feature = "otel-traces")]
331 info!("Command completed in {}ms", duration_ms);
332
333 use std::io::Read;
335 let mut stdout = String::new();
336 let mut stderr = String::new();
337
338 exec_result
339 .stdout()
340 .read_to_string(&mut stdout)
341 .map_err(|e| BackendError::Runtime(format!("Failed to read stdout: {}", e)))?;
342 exec_result
343 .stderr()
344 .read_to_string(&mut stderr)
345 .map_err(|e| BackendError::Runtime(format!("Failed to read stderr: {}", e)))?;
346
347 let exit_code = exec_result
350 .exit_code()
351 .map_err(|e| BackendError::Runtime(format!("Failed to get exit code: {}", e)))?
352 .unwrap_or(-1) as i32;
353
354 #[cfg(feature = "otel-traces")]
355 {
356 use opentelemetry::global;
357 use opentelemetry::trace::{Span, Tracer, TracerProvider};
358 use crate::telemetry::events;
359
360 let tracer_provider = global::tracer_provider();
362 let mut exec_span = tracer_provider
363 .tracer("clnrm-backend")
364 .start("clnrm.container.exec");
365
366 events::record_container_exec(&mut exec_span, &cmd_string, exit_code);
367 exec_span.end();
368
369 let mut stop_span = tracer_provider
371 .tracer("clnrm-backend")
372 .start("clnrm.container.stop");
373
374 events::record_container_stop(&mut stop_span, &container_id, exit_code);
375 stop_span.end();
376 }
377
378 Ok(RunResult {
379 exit_code,
380 stdout,
381 stderr,
382 duration_ms,
383 steps: Vec::new(),
384 redacted_env: Vec::new(),
385 backend: "testcontainers".to_string(),
386 concurrent: false,
387 step_order: Vec::new(),
388 })
389 }
390}
391
392impl Backend for TestcontainerBackend {
393 fn run_cmd(&self, cmd: Cmd) -> Result<RunResult> {
394 let start_time = Instant::now();
396
397 let result = self.execute_in_container(&cmd)?;
399
400 if start_time.elapsed() > self.timeout {
402 return Err(crate::error::CleanroomError::timeout_error(format!(
403 "Command execution timed out after {} seconds",
404 self.timeout.as_secs()
405 )));
406 }
407
408 Ok(result)
409 }
410
411 fn name(&self) -> &str {
412 "testcontainers"
413 }
414
415 fn is_available(&self) -> bool {
416 Self::is_available()
417 }
418
419 fn supports_hermetic(&self) -> bool {
420 true
421 }
422
423 fn supports_deterministic(&self) -> bool {
424 true
425 }
426}
427
428#[cfg(test)]
429mod tests {
430 use super::*;
431
432 #[test]
433 fn test_testcontainer_backend_creation() {
434 let backend = TestcontainerBackend::new("alpine:latest");
435 assert!(backend.is_ok());
436 }
437
438 #[test]
439 fn test_testcontainer_backend_with_timeout() -> Result<()> {
440 let timeout = Duration::from_secs(60);
441 let backend = TestcontainerBackend::new("alpine:latest")?.with_timeout(timeout);
442 assert!(backend.is_running());
443 Ok(())
444 }
445
446 #[test]
447 fn test_testcontainer_backend_trait() -> Result<()> {
448 let backend = TestcontainerBackend::new("alpine:latest")?;
449 assert!(backend.is_running());
450 Ok(())
451 }
452
453 #[test]
454 fn test_testcontainer_backend_image() -> Result<()> {
455 let backend = TestcontainerBackend::new("ubuntu:20.04")?;
456 assert!(backend.is_running());
457 Ok(())
458 }
459}
460
461#[cfg(test)]
462mod volume_tests {
463 use super::*;
464 use crate::error::CleanroomError;
465
466 #[test]
471 fn test_with_volume_adds_mount_to_backend() -> Result<()> {
472 let temp_dir = std::env::temp_dir();
474 let host_path = temp_dir.join("test_mount");
475 std::fs::create_dir_all(&host_path)?;
476
477 let backend = TestcontainerBackend::new("alpine:latest")?;
478
479 let backend_with_volume = backend.with_volume(
481 host_path.to_str().ok_or_else(|| {
482 CleanroomError::internal_error("Invalid host path - contains non-UTF8 characters")
483 })?,
484 "/container/path",
485 false,
486 )?;
487
488 assert_eq!(backend_with_volume.volume_mounts.len(), 1);
490 assert_eq!(
491 backend_with_volume.volume_mounts[0]
492 .container_path()
493 .to_str()
494 .unwrap_or("invalid"),
495 "/container/path"
496 );
497
498 std::fs::remove_dir(&host_path)?;
499 Ok(())
500 }
501
502 #[test]
503 fn test_with_volume_supports_multiple_volumes() -> Result<()> {
504 let temp_dir = std::env::temp_dir();
506 let data_path = temp_dir.join("data");
507 let config_path = temp_dir.join("config");
508 let output_path = temp_dir.join("output");
509
510 std::fs::create_dir_all(&data_path)?;
511 std::fs::create_dir_all(&config_path)?;
512 std::fs::create_dir_all(&output_path)?;
513
514 let backend = TestcontainerBackend::new("alpine:latest")?;
515
516 let backend_with_volumes = backend
518 .with_volume(
519 data_path.to_str().ok_or_else(|| {
520 crate::error::CleanroomError::internal_error("Invalid data path")
521 })?,
522 "/data",
523 false,
524 )?
525 .with_volume(
526 config_path.to_str().ok_or_else(|| {
527 crate::error::CleanroomError::internal_error("Invalid config path")
528 })?,
529 "/config",
530 false,
531 )?
532 .with_volume(
533 output_path.to_str().ok_or_else(|| {
534 crate::error::CleanroomError::internal_error("Invalid output path")
535 })?,
536 "/output",
537 false,
538 )?;
539
540 assert_eq!(backend_with_volumes.volume_mounts.len(), 3);
542
543 std::fs::remove_dir(&data_path)?;
544 std::fs::remove_dir(&config_path)?;
545 std::fs::remove_dir(&output_path)?;
546 Ok(())
547 }
548
549 #[test]
550 fn test_with_volume_preserves_other_settings() -> Result<()> {
551 let temp_dir = std::env::temp_dir();
553 let test_path = temp_dir.join("test");
554 std::fs::create_dir_all(&test_path)?;
555
556 let timeout = Duration::from_secs(120);
557 let backend = TestcontainerBackend::new("alpine:latest")?
558 .with_timeout(timeout)
559 .with_env("TEST_VAR", "test_value");
560
561 let backend_with_volume = backend.with_volume(
563 test_path
564 .to_str()
565 .ok_or_else(|| crate::error::CleanroomError::internal_error("Invalid test path"))?,
566 "/test",
567 false,
568 )?;
569
570 assert_eq!(backend_with_volume.timeout, timeout);
572 assert_eq!(
573 backend_with_volume.env_vars.get("TEST_VAR"),
574 Some(&"test_value".to_string())
575 );
576 assert_eq!(backend_with_volume.volume_mounts.len(), 1);
577
578 std::fs::remove_dir(&test_path)?;
579 Ok(())
580 }
581
582 #[test]
587 fn test_with_volume_accepts_absolute_host_paths() -> Result<()> {
588 let temp_dir = std::env::temp_dir();
590 let abs_path = temp_dir.join("absolute");
591 std::fs::create_dir_all(&abs_path)?;
592
593 let backend = TestcontainerBackend::new("alpine:latest")?;
594
595 let backend_with_volume = backend.with_volume(
597 abs_path.to_str().ok_or_else(|| {
598 crate::error::CleanroomError::internal_error("Invalid absolute path")
599 })?,
600 "/container/path",
601 false,
602 )?;
603
604 assert!(backend_with_volume.volume_mounts[0]
606 .host_path()
607 .is_absolute());
608
609 std::fs::remove_dir(&abs_path)?;
610 Ok(())
611 }
612
613 #[test]
614 fn test_with_volume_rejects_relative_container_paths() -> Result<()> {
615 let temp_dir = std::env::temp_dir();
617 let backend = TestcontainerBackend::new("alpine:latest")?;
618
619 let result = backend.with_volume(
621 temp_dir.to_str().ok_or_else(|| {
622 crate::error::CleanroomError::internal_error("Invalid temp dir path")
623 })?,
624 "relative/container/path",
625 false,
626 );
627
628 assert!(result.is_err());
630 Ok(())
631 }
632
633 #[test]
634 fn test_with_volume_handles_special_characters_in_paths() -> Result<()> {
635 let temp_dir = std::env::temp_dir();
637 let special_path = temp_dir.join("test-data_v1.0");
638 std::fs::create_dir_all(&special_path)?;
639
640 let backend = TestcontainerBackend::new("alpine:latest")?;
641
642 let backend_with_volume = backend.with_volume(
644 special_path.to_str().ok_or_else(|| {
645 crate::error::CleanroomError::internal_error("Invalid special path")
646 })?,
647 "/container/test-data/v1.0",
648 false,
649 )?;
650
651 assert_eq!(backend_with_volume.volume_mounts.len(), 1);
653
654 std::fs::remove_dir(&special_path)?;
655 Ok(())
656 }
657
658 #[test]
659 fn test_with_volume_rejects_empty_strings() -> Result<()> {
660 let backend = TestcontainerBackend::new("alpine:latest")?;
662
663 let result = backend.with_volume("", "", false);
665
666 assert!(result.is_err());
668 Ok(())
669 }
670
671 #[test]
676 fn test_volume_builder_chain_with_other_methods() -> Result<()> {
677 let temp_dir = std::env::temp_dir();
679 let data_path = temp_dir.join("data_chain");
680 std::fs::create_dir_all(&data_path)?;
681
682 let backend = TestcontainerBackend::new("alpine:latest")?
683 .with_policy(Policy::default())
684 .with_timeout(Duration::from_secs(60))
685 .with_env("ENV_VAR", "value")
686 .with_volume(
687 data_path.to_str().ok_or_else(|| {
688 CleanroomError::internal_error("Invalid path for volume mount")
689 })?,
690 "/data",
691 false,
692 )?
693 .with_memory_limit(512)
694 .with_cpu_limit(1.0);
695
696 assert_eq!(backend.volume_mounts.len(), 1);
698 assert_eq!(backend.timeout, Duration::from_secs(60));
699 assert_eq!(backend.memory_limit, Some(512));
700 assert_eq!(backend.cpu_limit, Some(1.0));
701
702 std::fs::remove_dir(&data_path)?;
703 Ok(())
704 }
705
706 #[test]
707 fn test_volume_builder_immutability() -> Result<()> {
708 let temp_dir = std::env::temp_dir();
710 let test1_path = temp_dir.join("test1");
711 let test2_path = temp_dir.join("test2");
712 std::fs::create_dir_all(&test1_path)?;
713 std::fs::create_dir_all(&test2_path)?;
714
715 let backend1 = TestcontainerBackend::new("alpine:latest")?;
716
717 let backend2 = backend1.clone().with_volume(
719 test1_path.to_str().ok_or_else(|| {
720 crate::error::CleanroomError::internal_error("Invalid test1 path")
721 })?,
722 "/test1",
723 false,
724 )?;
725 let backend3 = backend1.clone().with_volume(
726 test2_path.to_str().ok_or_else(|| {
727 crate::error::CleanroomError::internal_error("Invalid test2 path")
728 })?,
729 "/test2",
730 false,
731 )?;
732
733 assert_eq!(backend2.volume_mounts.len(), 1);
735 assert_eq!(backend3.volume_mounts.len(), 1);
736 assert_ne!(
737 backend2.volume_mounts[0].container_path(),
738 backend3.volume_mounts[0].container_path()
739 );
740
741 std::fs::remove_dir(&test1_path)?;
742 std::fs::remove_dir(&test2_path)?;
743 Ok(())
744 }
745
746 #[test]
751 fn test_with_volume_duplicate_mounts_allowed() -> Result<()> {
752 let temp_dir = std::env::temp_dir();
754 let data_path = temp_dir.join("data_dup");
755 std::fs::create_dir_all(&data_path)?;
756
757 let backend = TestcontainerBackend::new("alpine:latest")?;
758
759 let backend_with_volumes = backend
761 .with_volume(
762 data_path.to_str().ok_or_else(|| {
763 crate::error::CleanroomError::internal_error("Invalid data path")
764 })?,
765 "/data",
766 false,
767 )?
768 .with_volume(
769 data_path.to_str().ok_or_else(|| {
770 crate::error::CleanroomError::internal_error("Invalid data path")
771 })?,
772 "/data",
773 false,
774 )?;
775
776 assert_eq!(backend_with_volumes.volume_mounts.len(), 2);
778
779 std::fs::remove_dir(&data_path)?;
780 Ok(())
781 }
782
783 #[test]
784 fn test_with_volume_overlapping_container_paths() -> Result<()> {
785 let temp_dir = std::env::temp_dir();
787 let data1_path = temp_dir.join("data1");
788 let data2_path = temp_dir.join("data2");
789 std::fs::create_dir_all(&data1_path)?;
790 std::fs::create_dir_all(&data2_path)?;
791
792 let backend = TestcontainerBackend::new("alpine:latest")?;
793
794 let backend_with_volumes = backend
796 .with_volume(
797 data1_path.to_str().ok_or_else(|| {
798 crate::error::CleanroomError::internal_error("Invalid data1 path")
799 })?,
800 "/shared",
801 false,
802 )?
803 .with_volume(
804 data2_path.to_str().ok_or_else(|| {
805 crate::error::CleanroomError::internal_error("Invalid data2 path")
806 })?,
807 "/shared",
808 false,
809 )?;
810
811 assert_eq!(backend_with_volumes.volume_mounts.len(), 2);
813
814 std::fs::remove_dir(&data1_path)?;
815 std::fs::remove_dir(&data2_path)?;
816 Ok(())
817 }
818
819 #[test]
820 fn test_with_volume_very_long_paths() -> Result<()> {
821 let temp_dir = std::env::temp_dir();
823 let long_name = "a".repeat(100); let long_path = temp_dir.join(long_name);
825 std::fs::create_dir_all(&long_path)?;
826
827 let backend = TestcontainerBackend::new("alpine:latest")?;
828
829 let backend_with_volume = backend.with_volume(
831 long_path
832 .to_str()
833 .ok_or_else(|| crate::error::CleanroomError::internal_error("Invalid long path"))?,
834 "/data",
835 false,
836 )?;
837
838 assert!(
840 backend_with_volume.volume_mounts[0]
841 .host_path()
842 .to_str()
843 .unwrap_or("")
844 .len()
845 > 100
846 );
847
848 std::fs::remove_dir(&long_path)?;
849 Ok(())
850 }
851
852 #[test]
853 #[cfg(target_os = "linux")]
854 fn test_with_volume_unicode_paths() -> Result<()> {
855 let temp_dir = std::env::temp_dir();
857 let unicode_path = temp_dir.join("données");
858 std::fs::create_dir_all(&unicode_path)?;
859
860 let backend = TestcontainerBackend::new("alpine:latest")?;
861
862 let backend_with_volume = backend.with_volume(
864 unicode_path.to_str().ok_or_else(|| {
865 crate::error::CleanroomError::internal_error("Invalid unicode path")
866 })?,
867 "/container/データ",
868 false,
869 )?;
870
871 assert_eq!(backend_with_volume.volume_mounts.len(), 1);
873
874 std::fs::remove_dir(&unicode_path)?;
875 Ok(())
876 }
877
878 #[test]
883 fn test_volume_mounts_per_backend_instance_isolated() -> Result<()> {
884 let temp_dir = std::env::temp_dir();
886 let backend1_path = temp_dir.join("backend1");
887 let backend2_path = temp_dir.join("backend2");
888 std::fs::create_dir_all(&backend1_path)?;
889 std::fs::create_dir_all(&backend2_path)?;
890
891 let backend1 = TestcontainerBackend::new("alpine:latest")?.with_volume(
892 backend1_path.to_str().ok_or_else(|| {
893 crate::error::CleanroomError::internal_error("Invalid backend1 path")
894 })?,
895 "/data",
896 false,
897 )?;
898 let backend2 = TestcontainerBackend::new("alpine:latest")?.with_volume(
899 backend2_path.to_str().ok_or_else(|| {
900 crate::error::CleanroomError::internal_error("Invalid backend2 path")
901 })?,
902 "/data",
903 false,
904 )?;
905
906 assert_eq!(backend1.volume_mounts.len(), 1);
908 assert_eq!(backend2.volume_mounts.len(), 1);
909 assert_ne!(
910 backend1.volume_mounts[0].host_path(),
911 backend2.volume_mounts[0].host_path()
912 );
913
914 std::fs::remove_dir(&backend1_path)?;
915 std::fs::remove_dir(&backend2_path)?;
916 Ok(())
917 }
918
919 #[test]
924 fn test_volume_mounts_storage_format() -> Result<()> {
925 let temp_dir = std::env::temp_dir();
927 let host_path = temp_dir.join("storage_test");
928 std::fs::create_dir_all(&host_path)?;
929
930 let backend = TestcontainerBackend::new("alpine:latest")?.with_volume(
931 host_path
932 .to_str()
933 .ok_or_else(|| crate::error::CleanroomError::internal_error("Invalid host path"))?,
934 "/container/path",
935 false,
936 )?;
937
938 assert_eq!(
940 backend.volume_mounts[0].container_path(),
941 std::path::Path::new("/container/path")
942 );
943
944 std::fs::remove_dir(&host_path)?;
945 Ok(())
946 }
947
948 #[test]
949 fn test_empty_volume_mounts_by_default() -> Result<()> {
950 let backend = TestcontainerBackend::new("alpine:latest")?;
952
953 assert!(backend.volume_mounts.is_empty());
955 Ok(())
956 }
957}