Skip to content

Commit

Permalink
Support local buildpacks and meta-buildpacks in libcnb-test (#666)
Browse files Browse the repository at this point in the history
* Support local buildpacks and meta-buildpacks in libcnb-test

The test runner has been modified to support a `BuildpackReference::LibCnbRs(BuildpackId)` variant which represents a buildpack (and any of it's dependencies) within the workspace that needs to be compiled for testing. This is similar to `BuildpackReference::Crate` but also supports meta-buildpacks.

---------

Co-authored-by: Ed Morley <[email protected]>
  • Loading branch information
colincasey and edmorley authored Sep 20, 2023
1 parent 31ec5af commit b59a9ce
Show file tree
Hide file tree
Showing 17 changed files with 274 additions and 70 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@
Cargo.lock
**/fixtures/*/target/
**/fixtures/*/packaged/
**/test-fixtures/buildpacks/*/target/
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- `libcnb-data`:
- `ExecDProgramOutputKey`, `ProcessType`, `LayerName`, `BuildpackId` and `StackId` now implement `Ord` and `PartialOrd`. ([#658](https://github.com/heroku/libcnb.rs/pull/658))
- Add `generic::GenericMetadata` as a generic metadata type. Also makes it the default for `BuildpackDescriptor`, `SingleBuildpackDescriptor`, `MetaBuildpackDescriptor` and `LayerContentMetadata`. ([#664](https://github.com/heroku/libcnb.rs/pull/664))
- `libcnb-test`:
- Added the variant `WorkspaceBuildpack` to the `build_config::BuildpackReference` enum which allows any buildpack within the Rust workspace to be referenced for testing. ([#666](https://github.com/heroku/libcnb.rs/pull/666))
- Testing of composite buildpacks is now supported using the `WorkspaceBuildpack` variant - **Requires `pack` CLI version `>=0.30`**. ([#666](https://github.com/heroku/libcnb.rs/pull/666))

### Changed

Expand All @@ -22,6 +25,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- No longer outputs paths for non-libcnb.rs and non-meta buildpacks. ([#657](https://github.com/heroku/libcnb.rs/pull/657))
- Build output for humans changed slightly, output intended for machines/scripting didn't change. ([#657](https://github.com/heroku/libcnb.rs/pull/657))
- When performing buildpack detection, standard ignore files (`.ignore` and `.gitignore`) will be respected. ([#673](https://github.com/heroku/libcnb.rs/pull/673))
- `libcnb-test`:
- Renamed the variant `Crate` to `CurrentCrate` for the `build_config::BuildpackReference` enum which references the buildpack within the Rust Crate currently being tested. ([#666](https://github.com/heroku/libcnb.rs/pull/666))

## [0.14.0] - 2023-08-18

Expand Down
3 changes: 2 additions & 1 deletion libcnb-test/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,14 @@ readme = "README.md"
include = ["src/**/*", "LICENSE", "README.md"]

[dependencies]
cargo_metadata = "0.17.0"
fastrand = "2.0.0"
fs_extra = "1.3.0"
libcnb-common.workspace = true
libcnb-data.workspace = true
libcnb-package.workspace = true
tempfile = "3.7.1"

[dev-dependencies]
indoc = "2.0.3"
ureq = { version = "2.7.1", default-features = false }
libcnb.workspace = true
4 changes: 3 additions & 1 deletion libcnb-test/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -191,13 +191,15 @@ fn dynamic_fixture() {
Building with multiple buildpacks, using [`BuildConfig::buildpacks`]:

```rust,no_run
use libcnb::data::buildpack_id;
use libcnb_test::{BuildConfig, BuildpackReference, TestRunner};
// #[test]
fn additional_buildpacks() {
TestRunner::default().build(
BuildConfig::new("heroku/builder:22", "test-fixtures/app").buildpacks(vec![
BuildpackReference::Crate,
BuildpackReference::CurrentCrate,
BuildpackReference::WorkspaceBuildpack(buildpack_id!("my-project/buildpack")),
BuildpackReference::Other(String::from("heroku/another-buildpack")),
]),
|context| {
Expand Down
124 changes: 89 additions & 35 deletions libcnb-test/src/build.rs
Original file line number Diff line number Diff line change
@@ -1,62 +1,116 @@
use cargo_metadata::MetadataCommand;
use libcnb_package::build::{build_buildpack_binaries, BuildBinariesError};
use libcnb_common::toml_file::{read_toml_file, TomlFileError};
use libcnb_data::buildpack::{BuildpackDescriptor, BuildpackId};
use libcnb_package::buildpack_dependency_graph::{
build_libcnb_buildpacks_dependency_graph, BuildBuildpackDependencyGraphError,
};
use libcnb_package::cross_compile::{cross_compile_assistance, CrossCompileAssistance};
use libcnb_package::{assemble_buildpack_directory, CargoProfile};
use std::path::PathBuf;
use tempfile::{tempdir, TempDir};
use libcnb_package::dependency_graph::{get_dependencies, GetDependenciesError};
use libcnb_package::output::create_packaged_buildpack_dir_resolver;
use libcnb_package::{find_cargo_workspace_root_dir, CargoProfile, FindCargoWorkspaceRootError};
use std::collections::BTreeMap;
use std::fs;
use std::path::{Path, PathBuf};

/// Packages the current crate as a buildpack into a temporary directory.
pub(crate) fn package_crate_buildpack(
cargo_profile: CargoProfile,
target_triple: impl AsRef<str>,
) -> Result<TempDir, PackageCrateBuildpackError> {
let cargo_manifest_dir = std::env::var("CARGO_MANIFEST_DIR")
.map(PathBuf::from)
.map_err(PackageCrateBuildpackError::CannotDetermineCrateDirectory)?;
cargo_manifest_dir: &Path,
target_buildpack_dir: &Path,
) -> Result<PathBuf, PackageBuildpackError> {
let buildpack_toml = cargo_manifest_dir.join("buildpack.toml");

let cargo_metadata = MetadataCommand::new()
.manifest_path(&cargo_manifest_dir.join("Cargo.toml"))
.exec()
.map_err(PackageCrateBuildpackError::CargoMetadataError)?;
assert!(
buildpack_toml.exists(),
"Could not package directory as buildpack! No `buildpack.toml` file exists at {}",
cargo_manifest_dir.display()
);

let cargo_env = match cross_compile_assistance(target_triple.as_ref()) {
let buildpack_descriptor: BuildpackDescriptor = read_toml_file(buildpack_toml)
.map_err(PackageBuildpackError::CannotReadBuildpackDescriptor)?;

package_buildpack(
&buildpack_descriptor.buildpack().id,
cargo_profile,
target_triple,
cargo_manifest_dir,
target_buildpack_dir,
)
}

pub(crate) fn package_buildpack(
buildpack_id: &BuildpackId,
cargo_profile: CargoProfile,
target_triple: impl AsRef<str>,
cargo_manifest_dir: &Path,
target_buildpack_dir: &Path,
) -> Result<PathBuf, PackageBuildpackError> {
let cargo_build_env = match cross_compile_assistance(target_triple.as_ref()) {
CrossCompileAssistance::HelpText(help_text) => {
return Err(PackageCrateBuildpackError::CrossCompileConfigurationError(
return Err(PackageBuildpackError::CrossCompileConfigurationError(
help_text,
));
}
CrossCompileAssistance::NoAssistance => Vec::new(),
CrossCompileAssistance::Configuration { cargo_env } => cargo_env,
};

let buildpack_dir =
tempdir().map_err(PackageCrateBuildpackError::CannotCreateBuildpackTempDirectory)?;
let workspace_root_path = find_cargo_workspace_root_dir(cargo_manifest_dir)
.map_err(PackageBuildpackError::FindCargoWorkspaceRoot)?;

let buildpack_binaries = build_buildpack_binaries(
&cargo_manifest_dir,
&cargo_metadata,
let buildpack_dir_resolver = create_packaged_buildpack_dir_resolver(
target_buildpack_dir,
cargo_profile,
&cargo_env,
target_triple.as_ref(),
)
.map_err(PackageCrateBuildpackError::BuildBinariesError)?;
);

assemble_buildpack_directory(
buildpack_dir.path(),
cargo_manifest_dir.join("buildpack.toml"),
&buildpack_binaries,
let buildpack_dependency_graph = build_libcnb_buildpacks_dependency_graph(&workspace_root_path)
.map_err(PackageBuildpackError::BuildBuildpackDependencyGraph)?;

let root_node = buildpack_dependency_graph
.node_weights()
.find(|node| node.buildpack_id == buildpack_id.clone());

assert!(
root_node.is_some(),
"Could not package directory as buildpack! No buildpack with id `{buildpack_id}` exists in the workspace at {}",
workspace_root_path.display()
);

let build_order = get_dependencies(
&buildpack_dependency_graph,
&[root_node.expect("The root node should exist")],
)
.map_err(PackageCrateBuildpackError::CannotAssembleBuildpackDirectory)?;
.map_err(PackageBuildpackError::GetDependencies)?;

let mut packaged_buildpack_dirs = BTreeMap::new();
for node in &build_order {
let buildpack_destination_dir = buildpack_dir_resolver(&node.buildpack_id);

fs::create_dir_all(&buildpack_destination_dir).unwrap();

libcnb_package::package::package_buildpack(
&node.path,
cargo_profile,
target_triple.as_ref(),
&cargo_build_env,
&buildpack_destination_dir,
&packaged_buildpack_dirs,
)
.map_err(PackageBuildpackError::PackageBuildpack)?;

packaged_buildpack_dirs.insert(node.buildpack_id.clone(), buildpack_destination_dir);
}

Ok(buildpack_dir)
Ok(buildpack_dir_resolver(buildpack_id))
}

#[derive(Debug)]
pub(crate) enum PackageCrateBuildpackError {
BuildBinariesError(BuildBinariesError),
CannotAssembleBuildpackDirectory(std::io::Error),
CannotCreateBuildpackTempDirectory(std::io::Error),
CannotDetermineCrateDirectory(std::env::VarError),
CargoMetadataError(cargo_metadata::Error),
pub(crate) enum PackageBuildpackError {
CannotReadBuildpackDescriptor(TomlFileError),
BuildBuildpackDependencyGraph(BuildBuildpackDependencyGraphError),
CrossCompileConfigurationError(String),
FindCargoWorkspaceRoot(FindCargoWorkspaceRootError),
GetDependencies(GetDependenciesError<BuildpackId>),
PackageBuildpack(libcnb_package::package::PackageBuildpackError),
}
11 changes: 7 additions & 4 deletions libcnb-test/src/build_config.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use libcnb_data::buildpack::BuildpackId;
use std::collections::HashMap;
use std::path::{Path, PathBuf};
use std::rc::Rc;
Expand Down Expand Up @@ -41,7 +42,7 @@ impl BuildConfig {
cargo_profile: CargoProfile::Dev,
target_triple: String::from("x86_64-unknown-linux-musl"),
builder_name: builder_name.into(),
buildpacks: vec![BuildpackReference::Crate],
buildpacks: vec![BuildpackReference::CurrentCrate],
env: HashMap::new(),
app_dir_preprocessor: None,
expected_pack_result: PackResult::Success,
Expand All @@ -50,7 +51,7 @@ impl BuildConfig {

/// Sets the buildpacks (and their ordering) to use when building the app.
///
/// Defaults to [`BuildpackReference::Crate`].
/// Defaults to [`BuildpackReference::CurrentCrate`].
///
/// # Example
/// ```no_run
Expand All @@ -59,7 +60,7 @@ impl BuildConfig {
/// TestRunner::default().build(
/// BuildConfig::new("heroku/builder:22", "test-fixtures/app").buildpacks(vec![
/// BuildpackReference::Other(String::from("heroku/another-buildpack")),
/// BuildpackReference::Crate,
/// BuildpackReference::CurrentCrate,
/// ]),
/// |context| {
/// // ...
Expand Down Expand Up @@ -250,7 +251,9 @@ impl BuildConfig {
#[derive(Debug, Clone, Eq, PartialEq)]
pub enum BuildpackReference {
/// References the buildpack in the Rust Crate currently being tested.
Crate,
CurrentCrate,
/// References a libcnb.rs buildpack within the Rust Workspace that needs to be packaged into a buildpack
WorkspaceBuildpack(BuildpackId),
/// References another buildpack by id, local directory or tarball.
Other(String),
}
Expand Down
2 changes: 2 additions & 0 deletions libcnb-test/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,4 +29,6 @@ pub use crate::test_runner::*;
#[cfg(test)]
use indoc as _;
#[cfg(test)]
use libcnb as _;
#[cfg(test)]
use ureq as _;
47 changes: 31 additions & 16 deletions libcnb-test/src/test_runner.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ use crate::{app, build, util, BuildConfig, BuildpackReference, PackResult, TestC
use std::borrow::Borrow;
use std::env;
use std::path::PathBuf;
use tempfile::tempdir;

/// Runner for libcnb integration tests.
///
Expand Down Expand Up @@ -58,12 +59,13 @@ impl TestRunner {
) {
let config = config.borrow();

let cargo_manifest_dir = env::var("CARGO_MANIFEST_DIR")
.map(PathBuf::from)
.expect("Could not determine Cargo manifest directory");

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)
cargo_manifest_dir.join(&config.app_dir)
} else {
config.app_dir.clone()
};
Expand All @@ -88,14 +90,8 @@ impl TestRunner {
}
};

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 buildpacks_target_dir =
tempdir().expect("Could not create a temporary directory for compiled buildpacks");

let mut pack_command = PackBuildCommand::new(&config.builder_name, &app_dir, &image_name);

Expand All @@ -105,11 +101,30 @@ impl TestRunner {

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::CurrentCrate => {
let crate_buildpack_dir = build::package_crate_buildpack(
config.cargo_profile,
&config.target_triple,
&cargo_manifest_dir,
buildpacks_target_dir.path(),
).expect("Test references crate buildpack, but crate wasn't packaged as a buildpack. This is an internal libcnb-test error, please report any occurrences");
pack_command.buildpack(crate_buildpack_dir);
}

BuildpackReference::WorkspaceBuildpack(builpack_id) => {
let buildpack_dir = build::package_buildpack(
builpack_id,
config.cargo_profile,
&config.target_triple,
&cargo_manifest_dir,
buildpacks_target_dir.path()
).unwrap_or_else(|_| panic!("Test references buildpack `{builpack_id}`, but this directory wasn't packaged as a buildpack. This is an internal libcnb-test error, please report any occurrences"));
pack_command.buildpack(buildpack_dir);
}

BuildpackReference::Other(id) => {
pack_command.buildpack(id.clone());
}
BuildpackReference::Other(id) => pack_command.buildpack(id.clone()),
};
}

Expand Down
5 changes: 5 additions & 0 deletions libcnb-test/test-fixtures/buildpacks/libcnb-test-a/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
[package]
name = "one"
version = "0.0.0"

[workspace]
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
api = "0.8"

[buildpack]
id = "libcnb-test/a"
version = "0.0.0"

[[stacks]]
id = "*"
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
fn main() {
println!("Buildpack A");
}
5 changes: 5 additions & 0 deletions libcnb-test/test-fixtures/buildpacks/libcnb-test-b/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
[package]
name = "two"
version = "0.0.0"

[workspace]
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
api = "0.8"

[buildpack]
id = "libcnb-test/b"
version = "0.0.0"

[[stacks]]
id = "*"
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
fn main() {
println!("Buildpack B");
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
api = "0.8"

[buildpack]
id = "libcnb-test/meta"
name = "Meta-buildpack Test"
version = "0.0.0"
homepage = "https://example.com"
description = "Official test example"
keywords = ["test"]

[[buildpack.licenses]]
type = "BSD-3-Clause"

[[order]]

[[order.group]]
id = "libcnb-test/a"
version = "0.0.0"

[[order.group]]
id = "libcnb-test/b"
version = "0.0.0"
Loading

0 comments on commit b59a9ce

Please sign in to comment.