libcnb_test/
build_config.rs

1use libcnb_data::buildpack::BuildpackId;
2use std::collections::HashMap;
3use std::path::{Path, PathBuf};
4use std::rc::Rc;
5
6pub use libcnb_package::CargoProfile;
7
8/// Configuration for a test.
9#[derive(Clone)]
10pub struct BuildConfig {
11    pub(crate) app_dir: PathBuf,
12    pub(crate) cargo_profile: CargoProfile,
13    pub(crate) target_triple: String,
14    pub(crate) builder_name: String,
15    pub(crate) buildpacks: Vec<BuildpackReference>,
16    pub(crate) env: HashMap<String, String>,
17    pub(crate) app_dir_preprocessor: Option<Rc<dyn Fn(PathBuf)>>,
18    pub(crate) expected_pack_result: PackResult,
19}
20
21impl BuildConfig {
22    /// Creates a new build configuration.
23    ///
24    /// If the `app_dir` parameter is a relative path, it is treated as relative to the Cargo
25    /// manifest directory ([`CARGO_MANIFEST_DIR`](https://doc.rust-lang.org/cargo/reference/environment-variables.html#environment-variables-cargo-sets-for-crates)),
26    /// i.e. the package's root directory.
27    ///
28    /// # Example
29    /// ```no_run
30    /// use libcnb_test::{BuildConfig, TestRunner};
31    ///
32    /// TestRunner::default().build(
33    ///     BuildConfig::new("heroku/builder:22", "tests/fixtures/app"),
34    ///     |context| {
35    ///         // ...
36    ///     },
37    /// );
38    /// ```
39    pub fn new(builder_name: impl Into<String>, app_dir: impl AsRef<Path>) -> Self {
40        Self {
41            app_dir: PathBuf::from(app_dir.as_ref()),
42            cargo_profile: CargoProfile::Dev,
43            target_triple: String::from("x86_64-unknown-linux-musl"),
44            builder_name: builder_name.into(),
45            buildpacks: vec![BuildpackReference::CurrentCrate],
46            env: HashMap::new(),
47            app_dir_preprocessor: None,
48            expected_pack_result: PackResult::Success,
49        }
50    }
51
52    /// Sets the buildpacks (and their ordering) to use when building the app.
53    ///
54    /// Defaults to [`BuildpackReference::CurrentCrate`].
55    ///
56    /// # Example
57    /// ```no_run
58    /// use libcnb::data::buildpack_id;
59    /// use libcnb_test::{BuildConfig, BuildpackReference, TestRunner};
60    ///
61    /// TestRunner::default().build(
62    ///     BuildConfig::new("heroku/builder:22", "tests/fixtures/app").buildpacks([
63    ///         BuildpackReference::CurrentCrate,
64    ///         BuildpackReference::WorkspaceBuildpack(buildpack_id!("my-project/buildpack")),
65    ///         BuildpackReference::Other(String::from("heroku/another-buildpack")),
66    ///     ]),
67    ///     |context| {
68    ///         // ...
69    ///     },
70    /// );
71    /// ```
72    pub fn buildpacks(&mut self, buildpacks: impl Into<Vec<BuildpackReference>>) -> &mut Self {
73        self.buildpacks = buildpacks.into();
74        self
75    }
76
77    /// Sets the Cargo profile used when compiling the buildpack.
78    ///
79    /// Defaults to [`CargoProfile::Dev`].
80    ///
81    /// # Example
82    /// ```no_run
83    /// use libcnb_test::{BuildConfig, CargoProfile, TestRunner};
84    ///
85    /// TestRunner::default().build(
86    ///     BuildConfig::new("heroku/builder:22", "tests/fixtures/app")
87    ///         .cargo_profile(CargoProfile::Release),
88    ///     |context| {
89    ///         // ...
90    ///     },
91    /// );
92    /// ```
93    pub fn cargo_profile(&mut self, cargo_profile: CargoProfile) -> &mut Self {
94        self.cargo_profile = cargo_profile;
95        self
96    }
97
98    /// Sets the target triple used when compiling the buildpack.
99    ///
100    /// Defaults to `x86_64-unknown-linux-musl`.
101    ///
102    /// # Example
103    /// ```no_run
104    /// use libcnb_test::{BuildConfig, TestRunner};
105    ///
106    /// TestRunner::default().build(
107    ///     BuildConfig::new("heroku/builder:22", "tests/fixtures/app")
108    ///         .target_triple("x86_64-unknown-linux-musl"),
109    ///     |context| {
110    ///         // ...
111    ///     },
112    /// );
113    /// ```
114    pub fn target_triple(&mut self, target_triple: impl Into<String>) -> &mut Self {
115        self.target_triple = target_triple.into();
116        self
117    }
118
119    /// Inserts or updates an environment variable mapping for the build process.
120    ///
121    /// Note: This does not set this environment variable for running containers, it's only
122    /// available during the build.
123    ///
124    /// # Example
125    /// ```no_run
126    /// use libcnb_test::{BuildConfig, TestRunner};
127    ///
128    /// TestRunner::default().build(
129    ///     BuildConfig::new("heroku/builder:22", "tests/fixtures/app")
130    ///         .env("ENV_VAR_ONE", "VALUE ONE")
131    ///         .env("ENV_VAR_TWO", "SOME OTHER VALUE"),
132    ///     |context| {
133    ///         // ...
134    ///     },
135    /// );
136    /// ```
137    pub fn env(&mut self, k: impl Into<String>, v: impl Into<String>) -> &mut Self {
138        self.env.insert(k.into(), v.into());
139        self
140    }
141
142    /// Adds or updates multiple environment variable mappings for the build process.
143    ///
144    /// Note: This does not set environment variables for running containers, they're only
145    /// available during the build.
146    ///
147    /// # Example
148    /// ```no_run
149    /// use libcnb_test::{BuildConfig, TestRunner};
150    ///
151    /// TestRunner::default().build(
152    ///     BuildConfig::new("heroku/builder:22", "tests/fixtures/app").envs([
153    ///         ("ENV_VAR_ONE", "VALUE ONE"),
154    ///         ("ENV_VAR_TWO", "SOME OTHER VALUE"),
155    ///     ]),
156    ///     |context| {
157    ///         // ...
158    ///     },
159    /// );
160    /// ```
161    pub fn envs<K: Into<String>, V: Into<String>, I: IntoIterator<Item = (K, V)>>(
162        &mut self,
163        envs: I,
164    ) -> &mut Self {
165        envs.into_iter().for_each(|(key, value)| {
166            self.env(key.into(), value.into());
167        });
168
169        self
170    }
171
172    /// Sets an app directory preprocessor function.
173    ///
174    /// It will be run after the app directory has been copied for the current integration test run,
175    /// the changes will not affect other integration test runs.
176    ///
177    /// Generally, we suggest using dedicated test fixtures. However, in some cases it is more
178    /// economical to slightly modify a fixture programmatically before a test instead.
179    ///
180    /// # Example
181    /// ```no_run
182    /// use libcnb_test::{BuildConfig, TestRunner};
183    ///
184    /// TestRunner::default().build(
185    ///     BuildConfig::new("heroku/builder:22", "tests/fixtures/app").app_dir_preprocessor(
186    ///         |app_dir| {
187    ///             std::fs::remove_file(app_dir.join("Procfile")).unwrap();
188    ///         },
189    ///     ),
190    ///     |context| {
191    ///         // ...
192    ///     },
193    /// );
194    /// ```
195    pub fn app_dir_preprocessor<F: 'static + Fn(PathBuf)>(&mut self, f: F) -> &mut Self {
196        self.app_dir_preprocessor = Some(Rc::new(f));
197        self
198    }
199
200    /// Sets the app directory.
201    ///
202    /// The app directory is normally set in the [`BuildConfig::new`] call, but when sharing test
203    /// configuration, it might be necessary to change the app directory but keep everything else
204    /// the same.
205    ///
206    /// # Example
207    /// ```no_run
208    /// use libcnb_test::{BuildConfig, TestRunner};
209    ///
210    /// fn default_config() -> BuildConfig {
211    ///     BuildConfig::new("heroku/builder:22", "tests/fixtures/app")
212    /// }
213    ///
214    /// TestRunner::default().build(
215    ///     default_config().app_dir("tests/fixtures/a-different-app"),
216    ///     |context| {
217    ///         // ...
218    ///     },
219    /// );
220    /// ```
221    pub fn app_dir<P: Into<PathBuf>>(&mut self, path: P) -> &mut Self {
222        self.app_dir = path.into();
223        self
224    }
225
226    /// Set the expected `pack` command result.
227    ///
228    /// In some cases, users might want to explicitly test that a build fails and assert against
229    /// error output. When passed [`PackResult::Failure`], the test will fail if the pack build
230    /// succeeds and vice-versa.
231    ///
232    /// Defaults to [`PackResult::Success`].
233    ///
234    /// # Example
235    /// ```no_run
236    /// use libcnb_test::{assert_contains, BuildConfig, PackResult, TestRunner};
237    ///
238    /// TestRunner::default().build(
239    ///     BuildConfig::new("heroku/builder:22", "tests/fixtures/app")
240    ///         .expected_pack_result(PackResult::Failure),
241    ///     |context| {
242    ///         assert_contains!(context.pack_stderr, "ERROR: Invalid Procfile!");
243    ///     },
244    /// );
245    /// ```
246    pub fn expected_pack_result(&mut self, pack_result: PackResult) -> &mut Self {
247        self.expected_pack_result = pack_result;
248        self
249    }
250}
251
252/// References a Cloud Native Buildpack.
253#[derive(Debug, Clone, Eq, PartialEq)]
254pub enum BuildpackReference {
255    /// References the buildpack in the Rust Crate currently being tested.
256    ///
257    /// Is equivalent to `BuildpackReference::WorkspaceBuildpack(buildpack_id!("<buildpack ID of current crate"))`.
258    CurrentCrate,
259    /// References a libcnb.rs or composite buildpack within the Cargo workspace that needs to be packaged into a buildpack.
260    WorkspaceBuildpack(BuildpackId),
261    /// References another buildpack by id, local directory or tarball.
262    Other(String),
263}
264
265/// Result of a pack execution.
266#[derive(Debug, Clone, Eq, PartialEq)]
267pub enum PackResult {
268    /// Pack executed successfully.
269    Success,
270    /// Pack execution failed.
271    Failure,
272}