libcnb_test/
test_runner.rs1use crate::docker::{DockerRemoveImageCommand, DockerRemoveVolumeCommand};
2use crate::pack::PackBuildCommand;
3use crate::util::CommandError;
4use crate::{BuildConfig, BuildpackReference, PackResult, TestContext, app, build, util};
5use std::borrow::Borrow;
6use std::env;
7use std::path::PathBuf;
8use tempfile::tempdir;
9
10#[derive(Default)]
25pub struct TestRunner {}
26
27impl TestRunner {
28 pub fn build<C: Borrow<BuildConfig>, F: FnOnce(TestContext)>(&self, config: C, f: F) {
53 let image_name = util::random_docker_identifier();
54 let docker_resources = TemporaryDockerResources {
55 build_cache_volume_name: format!("{image_name}.build-cache"),
56 launch_cache_volume_name: format!("{image_name}.launch-cache"),
57 image_name,
58 };
59 self.build_internal(docker_resources, config, f);
60 }
61
62 pub(crate) fn build_internal<C: Borrow<BuildConfig>, F: FnOnce(TestContext)>(
63 &self,
64 docker_resources: TemporaryDockerResources,
65 config: C,
66 f: F,
67 ) {
68 let config = config.borrow();
69
70 let cargo_manifest_dir = env::var("CARGO_MANIFEST_DIR").map_or_else(
71 |error| panic!("Error determining Cargo manifest directory: {error}"),
72 PathBuf::from,
73 );
74
75 let app_dir = {
76 let normalized_app_dir_path = if config.app_dir.is_relative() {
77 cargo_manifest_dir.join(&config.app_dir)
78 } else {
79 config.app_dir.clone()
80 };
81
82 assert!(
83 normalized_app_dir_path.is_dir(),
84 "App dir is not a valid directory: {}",
85 normalized_app_dir_path.display()
86 );
87
88 if let Some(app_dir_preprocessor) = &config.app_dir_preprocessor {
91 let temporary_app_dir = app::copy_app(&normalized_app_dir_path)
92 .expect("Error copying app fixture to temporary location");
93
94 (app_dir_preprocessor)(temporary_app_dir.as_path().to_owned());
95
96 temporary_app_dir
97 } else {
98 normalized_app_dir_path.into()
99 }
100 };
101
102 let buildpacks_target_dir =
103 tempdir().expect("Error creating temporary directory for compiled buildpacks");
104
105 let mut pack_command = PackBuildCommand::new(
106 &config.builder_name,
107 &app_dir,
108 &docker_resources.image_name,
109 &docker_resources.build_cache_volume_name,
110 &docker_resources.launch_cache_volume_name,
111 );
112
113 config.env.iter().for_each(|(key, value)| {
114 pack_command.env(key, value);
115 });
116
117 for buildpack in &config.buildpacks {
118 match buildpack {
119 BuildpackReference::CurrentCrate => {
120 let crate_buildpack_dir = build::package_crate_buildpack(
121 config.cargo_profile,
122 &config.target_triple,
123 &cargo_manifest_dir,
124 buildpacks_target_dir.path(),
125 )
126 .unwrap_or_else(|error| {
127 panic!("Error packaging current crate as buildpack: {error}")
128 });
129 pack_command.buildpack(crate_buildpack_dir);
130 }
131
132 BuildpackReference::WorkspaceBuildpack(buildpack_id) => {
133 let buildpack_dir = build::package_buildpack(
134 buildpack_id,
135 config.cargo_profile,
136 &config.target_triple,
137 &cargo_manifest_dir,
138 buildpacks_target_dir.path(),
139 )
140 .unwrap_or_else(|error| {
141 panic!("Error packaging buildpack '{buildpack_id}': {error}")
142 });
143 pack_command.buildpack(buildpack_dir);
144 }
145
146 BuildpackReference::Other(id) => {
147 pack_command.buildpack(id.clone());
148 }
149 }
150 }
151
152 let pack_result = util::run_command(pack_command);
153
154 let output = match (&config.expected_pack_result, pack_result) {
155 (PackResult::Success, Ok(output)) => output,
156 (PackResult::Failure, Err(CommandError::NonZeroExitCode { log_output, .. })) => {
157 log_output
158 }
159 (PackResult::Failure, Ok(log_output)) => {
160 panic!("The pack build was expected to fail, but did not:\n\n{log_output}");
161 }
162 (_, Err(command_err)) => {
163 panic!("Error performing pack build:\n\n{command_err}");
164 }
165 };
166
167 let test_context = TestContext {
168 pack_stdout: output.stdout,
169 pack_stderr: output.stderr,
170 docker_resources,
171 config: config.clone(),
172 runner: self,
173 };
174
175 f(test_context);
176 }
177}
178
179#[allow(clippy::struct_field_names)]
180pub(crate) struct TemporaryDockerResources {
181 pub(crate) build_cache_volume_name: String,
182 pub(crate) image_name: String,
183 pub(crate) launch_cache_volume_name: String,
184}
185
186impl Drop for TemporaryDockerResources {
187 fn drop(&mut self) {
188 let _ = util::run_command(DockerRemoveImageCommand::new(&self.image_name));
193 let _ = util::run_command(DockerRemoveVolumeCommand::new([
194 &self.build_cache_volume_name,
195 &self.launch_cache_volume_name,
196 ]));
197 }
198}