diff --git a/libcnb-test/src/container_config.rs b/libcnb-test/src/container_config.rs index 979e1f7c..be594936 100644 --- a/libcnb-test/src/container_config.rs +++ b/libcnb-test/src/container_config.rs @@ -31,6 +31,7 @@ pub struct ContainerConfig { pub(crate) command: Option>, pub(crate) env: HashMap, pub(crate) exposed_ports: HashSet, + pub(crate) volumes: Option>, } impl ContainerConfig { @@ -198,4 +199,31 @@ impl ContainerConfig { self } + + /// Attaches container volumes. Useful for integration tests that + /// depend on persistent storage shared between container executions. + /// + /// See: [Docker CLI, Mount Volume](https://docs.docker.com/reference/cli/docker/container/run/#volume) + /// + /// # Example + /// ```no_run + /// use libcnb_test::{BuildConfig, ContainerConfig, TestRunner}; + /// + /// TestRunner::default().build( + /// BuildConfig::new("heroku/builder:22", "tests/fixtures/app"), + /// |context| { + /// // ... + /// context.start_container( + /// ContainerConfig::new().volumes(["/shared/cache:/workspace/cache"]), + /// |container| { + /// // ... + /// }, + /// ); + /// }, + /// ); + /// ``` + pub fn volumes, S: Into>(&mut self, volumes: I) -> &mut Self { + self.volumes = Some(volumes.into_iter().map(S::into).collect()); + self + } } diff --git a/libcnb-test/src/docker.rs b/libcnb-test/src/docker.rs index 38eb88a2..67bfdf0c 100644 --- a/libcnb-test/src/docker.rs +++ b/libcnb-test/src/docker.rs @@ -13,6 +13,7 @@ pub(crate) struct DockerRunCommand { image_name: String, platform: Option, remove: bool, + volumes: Option>, } impl DockerRunCommand { @@ -27,6 +28,7 @@ impl DockerRunCommand { image_name: image_name.into(), platform: None, remove: false, + volumes: None, } } @@ -67,6 +69,14 @@ impl DockerRunCommand { self.remove = remove; self } + + pub(crate) fn volumes, S: Into>( + &mut self, + volumes: I, + ) -> &mut Self { + self.volumes = Some(volumes.into_iter().map(S::into).collect()); + self + } } impl From for Command { @@ -98,6 +108,12 @@ impl From for Command { command.args(["--publish", &format!("127.0.0.1::{port}")]); } + if let Some(container_volumes) = docker_run_command.volumes { + command.args(container_volumes.into_iter().fold(vec![], |acc, v| { + [acc, vec!["--volume".to_string(), v]].concat() + })); + } + command.arg(docker_run_command.image_name); if let Some(container_command) = docker_run_command.command { @@ -315,6 +331,7 @@ mod tests { docker_run_command.expose_port(55555); docker_run_command.platform("linux/amd64"); docker_run_command.remove(true); + docker_run_command.volumes(["./.test-cache:/cache"]); let command: Command = docker_run_command.clone().into(); assert_eq!( @@ -337,6 +354,8 @@ mod tests { "127.0.0.1::12345", "--publish", "127.0.0.1::55555", + "--volume", + "./.test-cache:/cache", "my-image", "echo", "hello", diff --git a/libcnb-test/src/test_context.rs b/libcnb-test/src/test_context.rs index 807b349d..9418adab 100644 --- a/libcnb-test/src/test_context.rs +++ b/libcnb-test/src/test_context.rs @@ -112,6 +112,10 @@ impl<'a> TestContext<'a> { docker_run_command.expose_port(*port); }); + if let Some(volume) = &config.volumes { + docker_run_command.volumes(volume); + } + // We create the ContainerContext early to ensure the cleanup in ContainerContext::drop // is still performed even if the Docker command panics. let container_context = ContainerContext {