use crate::pack::{run_pack_command, PackBuildCommand};
use crate::{app, build, util, BuildConfig, BuildpackReference, TestContext};
use bollard::Docker;
use std::borrow::Borrow;
use std::env;
use std::env::VarError;
use std::path::PathBuf;
pub struct TestRunner {
pub(crate) docker: Docker,
pub(crate) tokio_runtime: tokio::runtime::Runtime,
}
impl Default for TestRunner {
fn default() -> Self {
let tokio_runtime =
tokio::runtime::Runtime::new().expect("Could not create internal Tokio runtime");
let docker = match env::var("DOCKER_HOST") {
#[cfg(target_family = "unix")]
Ok(docker_host) if docker_host.starts_with("unix://") => {
Docker::connect_with_unix_defaults()
}
Ok(docker_host)
if docker_host.starts_with("tcp://") || docker_host.starts_with("https://") =>
{
#[cfg(not(feature = "remote-docker"))]
panic!("Cannot connect to DOCKER_HOST '{docker_host}' since it requires TLS. Please use a local Docker daemon instead (recommended), or else enable the experimental `remote-docker` feature.");
#[cfg(feature = "remote-docker")]
Docker::connect_with_ssl_defaults()
}
Ok(docker_host) => panic!("Cannot connect to unsupported DOCKER_HOST '{docker_host}'"),
Err(VarError::NotPresent) => Docker::connect_with_local_defaults(),
Err(VarError::NotUnicode(_)) => {
panic!("DOCKER_HOST environment variable is not unicode encoded!")
}
}
.expect("Could not connect to local Docker daemon");
Self::new(tokio_runtime, docker)
}
}
impl TestRunner {
pub fn new(tokio_runtime: tokio::runtime::Runtime, docker: Docker) -> Self {
Self {
docker,
tokio_runtime,
}
}
pub fn build<C: Borrow<BuildConfig>, F: FnOnce(TestContext)>(&self, config: C, f: F) {
self.build_internal(util::random_docker_identifier(), config, f);
}
pub(crate) fn build_internal<C: Borrow<BuildConfig>, F: FnOnce(TestContext)>(
&self,
image_name: String,
config: C,
f: F,
) {
let config = config.borrow();
let app_dir = {
let normalized_app_dir_path = if config.app_dir.is_relative() {
env::var("CARGO_MANIFEST_DIR")
.map(PathBuf::from)
.expect("Could not determine Cargo manifest directory")
.join(&config.app_dir)
} else {
config.app_dir.clone()
};
assert!(
normalized_app_dir_path.is_dir(),
"App dir is not a valid directory: {}",
normalized_app_dir_path.display()
);
if let Some(app_dir_preprocessor) = &config.app_dir_preprocessor {
let temporary_app_dir = app::copy_app(&normalized_app_dir_path)
.expect("Could not copy app to temporary location");
(app_dir_preprocessor)(temporary_app_dir.as_path().to_owned());
temporary_app_dir
} else {
normalized_app_dir_path.into()
}
};
let temp_crate_buildpack_dir =
config
.buildpacks
.contains(&BuildpackReference::Crate)
.then(|| {
build::package_crate_buildpack(config.cargo_profile, &config.target_triple)
.expect("Could not package current crate as buildpack")
});
let mut pack_command = PackBuildCommand::new(&config.builder_name, &app_dir, &image_name);
config.env.iter().for_each(|(key, value)| {
pack_command.env(key, value);
});
for buildpack in &config.buildpacks {
match buildpack {
BuildpackReference::Crate => {
pack_command.buildpack(temp_crate_buildpack_dir.as_ref()
.expect("Test references crate buildpack, but crate wasn't packaged as a buildpack. This is an internal libcnb-test error, please report any occurrences."))
}
BuildpackReference::Other(id) => pack_command.buildpack(id.clone()),
};
}
let output = run_pack_command(pack_command, &config.expected_pack_result);
let test_context = TestContext {
pack_stdout: String::from_utf8_lossy(&output.stdout).into_owned(),
pack_stderr: String::from_utf8_lossy(&output.stderr).into_owned(),
image_name,
config: config.clone(),
runner: self,
};
f(test_context);
}
}