diff --git a/.gitignore b/.gitignore index 6addfe54..8f9e1c79 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ Cargo.lock **/fixtures/*/target/ **/fixtures/*/packaged/ +**/test-fixtures/buildpacks/*/target/ diff --git a/CHANGELOG.md b/CHANGELOG.md index 45c19f93..38d8c802 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 @@ -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 diff --git a/libcnb-test/Cargo.toml b/libcnb-test/Cargo.toml index 9c67538c..ec5dbc04 100644 --- a/libcnb-test/Cargo.toml +++ b/libcnb-test/Cargo.toml @@ -12,9 +12,9 @@ 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" @@ -22,3 +22,4 @@ tempfile = "3.7.1" [dev-dependencies] indoc = "2.0.3" ureq = { version = "2.7.1", default-features = false } +libcnb.workspace = true diff --git a/libcnb-test/README.md b/libcnb-test/README.md index 8f8d0e28..b903f200 100644 --- a/libcnb-test/README.md +++ b/libcnb-test/README.md @@ -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| { diff --git a/libcnb-test/src/build.rs b/libcnb-test/src/build.rs index bfde88c9..9114f224 100644 --- a/libcnb-test/src/build.rs +++ b/libcnb-test/src/build.rs @@ -1,27 +1,53 @@ -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, -) -> Result { - 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 { + 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, + cargo_manifest_dir: &Path, + target_buildpack_dir: &Path, +) -> Result { + 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, )); } @@ -29,34 +55,62 @@ pub(crate) fn package_crate_buildpack( 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), + PackageBuildpack(libcnb_package::package::PackageBuildpackError), } diff --git a/libcnb-test/src/build_config.rs b/libcnb-test/src/build_config.rs index 8466a7fb..152c0691 100644 --- a/libcnb-test/src/build_config.rs +++ b/libcnb-test/src/build_config.rs @@ -1,3 +1,4 @@ +use libcnb_data::buildpack::BuildpackId; use std::collections::HashMap; use std::path::{Path, PathBuf}; use std::rc::Rc; @@ -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, @@ -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 @@ -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| { /// // ... @@ -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), } diff --git a/libcnb-test/src/lib.rs b/libcnb-test/src/lib.rs index dcf7a9f8..9ad2cc71 100644 --- a/libcnb-test/src/lib.rs +++ b/libcnb-test/src/lib.rs @@ -29,4 +29,6 @@ pub use crate::test_runner::*; #[cfg(test)] use indoc as _; #[cfg(test)] +use libcnb as _; +#[cfg(test)] use ureq as _; diff --git a/libcnb-test/src/test_runner.rs b/libcnb-test/src/test_runner.rs index 67284e71..ef5eccfe 100644 --- a/libcnb-test/src/test_runner.rs +++ b/libcnb-test/src/test_runner.rs @@ -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. /// @@ -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() }; @@ -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); @@ -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()), }; } diff --git a/libcnb-test/test-fixtures/buildpacks/libcnb-test-a/Cargo.toml b/libcnb-test/test-fixtures/buildpacks/libcnb-test-a/Cargo.toml new file mode 100644 index 00000000..d8e77621 --- /dev/null +++ b/libcnb-test/test-fixtures/buildpacks/libcnb-test-a/Cargo.toml @@ -0,0 +1,5 @@ +[package] +name = "one" +version = "0.0.0" + +[workspace] diff --git a/libcnb-test/test-fixtures/buildpacks/libcnb-test-a/buildpack.toml b/libcnb-test/test-fixtures/buildpacks/libcnb-test-a/buildpack.toml new file mode 100644 index 00000000..f2e10539 --- /dev/null +++ b/libcnb-test/test-fixtures/buildpacks/libcnb-test-a/buildpack.toml @@ -0,0 +1,8 @@ +api = "0.8" + +[buildpack] +id = "libcnb-test/a" +version = "0.0.0" + +[[stacks]] +id = "*" diff --git a/libcnb-test/test-fixtures/buildpacks/libcnb-test-a/src/main.rs b/libcnb-test/test-fixtures/buildpacks/libcnb-test-a/src/main.rs new file mode 100644 index 00000000..89cd3f18 --- /dev/null +++ b/libcnb-test/test-fixtures/buildpacks/libcnb-test-a/src/main.rs @@ -0,0 +1,3 @@ +fn main() { + println!("Buildpack A"); +} diff --git a/libcnb-test/test-fixtures/buildpacks/libcnb-test-b/Cargo.toml b/libcnb-test/test-fixtures/buildpacks/libcnb-test-b/Cargo.toml new file mode 100644 index 00000000..504aeb84 --- /dev/null +++ b/libcnb-test/test-fixtures/buildpacks/libcnb-test-b/Cargo.toml @@ -0,0 +1,5 @@ +[package] +name = "two" +version = "0.0.0" + +[workspace] diff --git a/libcnb-test/test-fixtures/buildpacks/libcnb-test-b/buildpack.toml b/libcnb-test/test-fixtures/buildpacks/libcnb-test-b/buildpack.toml new file mode 100644 index 00000000..fd1940bf --- /dev/null +++ b/libcnb-test/test-fixtures/buildpacks/libcnb-test-b/buildpack.toml @@ -0,0 +1,8 @@ +api = "0.8" + +[buildpack] +id = "libcnb-test/b" +version = "0.0.0" + +[[stacks]] +id = "*" diff --git a/libcnb-test/test-fixtures/buildpacks/libcnb-test-b/src/main.rs b/libcnb-test/test-fixtures/buildpacks/libcnb-test-b/src/main.rs new file mode 100644 index 00000000..2e9f2ee2 --- /dev/null +++ b/libcnb-test/test-fixtures/buildpacks/libcnb-test-b/src/main.rs @@ -0,0 +1,3 @@ +fn main() { + println!("Buildpack B"); +} diff --git a/libcnb-test/test-fixtures/buildpacks/libcnb-test-meta/buildpack.toml b/libcnb-test/test-fixtures/buildpacks/libcnb-test-meta/buildpack.toml new file mode 100644 index 00000000..b21aeca3 --- /dev/null +++ b/libcnb-test/test-fixtures/buildpacks/libcnb-test-meta/buildpack.toml @@ -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" diff --git a/libcnb-test/test-fixtures/buildpacks/libcnb-test-meta/package.toml b/libcnb-test/test-fixtures/buildpacks/libcnb-test-meta/package.toml new file mode 100644 index 00000000..5371f16a --- /dev/null +++ b/libcnb-test/test-fixtures/buildpacks/libcnb-test-meta/package.toml @@ -0,0 +1,9 @@ +[buildpack] +uri = "." + +[[dependencies]] +uri = "libcnb:libcnb-test/a" + +[[dependencies]] +uri = "libcnb:libcnb-test/b" + diff --git a/libcnb-test/tests/integration_test.rs b/libcnb-test/tests/integration_test.rs index 0eabc1b3..529f92ae 100644 --- a/libcnb-test/tests/integration_test.rs +++ b/libcnb-test/tests/integration_test.rs @@ -8,6 +8,7 @@ #![warn(clippy::pedantic)] use indoc::indoc; +use libcnb_data::buildpack_id; use libcnb_test::{ assert_contains, assert_empty, assert_not_contains, BuildConfig, BuildpackReference, ContainerConfig, PackResult, TestRunner, @@ -63,13 +64,21 @@ fn rebuild() { #[test] #[ignore = "integration test"] -#[should_panic( - expected = "Could not package current crate as buildpack: BuildBinariesError(CannotDetermineBuildpackCargoTargetName(NoBinTargets))" -)] fn buildpack_packaging_failure() { - TestRunner::default().build(BuildConfig::new("invalid!", "test-fixtures/empty"), |_| { - unreachable!("The test should panic prior to the TestContext being invoked."); + let result = std::panic::catch_unwind(|| { + TestRunner::default().build(BuildConfig::new("invalid!", "test-fixtures/empty"), |_| { + unreachable!("The test should panic prior to the TestContext being invoked."); + }); }); + match result { + Ok(_) => panic!("expected a failure"), + Err(error) => { + assert_eq!( + error.downcast_ref::().unwrap().to_string(), + format!("Could not package directory as buildpack! No `buildpack.toml` file exists at {}", env::var("CARGO_MANIFEST_DIR").unwrap()) + ); + } + } } #[test] @@ -131,15 +140,25 @@ fn expected_pack_failure() { #[test] #[ignore = "integration test"] -#[should_panic( - expected = "Could not package current crate as buildpack: BuildBinariesError(CannotDetermineBuildpackCargoTargetName(NoBinTargets))" -)] fn expected_pack_failure_still_panics_for_non_pack_failure() { - TestRunner::default().build( - BuildConfig::new("invalid!", "test-fixtures/empty") - .expected_pack_result(PackResult::Failure), - |_| {}, - ); + let result = std::panic::catch_unwind(|| { + TestRunner::default().build( + BuildConfig::new("invalid!", "test-fixtures/empty") + .expected_pack_result(PackResult::Failure), + |_| { + unreachable!("The test should panic prior to the TestContext being invoked."); + }, + ); + }); + match result { + Ok(_) => panic!("expected a failure"), + Err(error) => { + assert_eq!( + error.downcast_ref::().unwrap().to_string(), + format!("Could not package directory as buildpack! No `buildpack.toml` file exists at {}", env::var("CARGO_MANIFEST_DIR").unwrap()) + ); + } + } } #[test] @@ -522,3 +541,42 @@ fn address_for_port_when_container_crashed() { }, ); } + +#[test] +#[ignore = "integration test"] +fn basic_build_with_libcnb_reference_to_single_buildpack() { + TestRunner::default().build( + BuildConfig::new("heroku/builder:22", "test-fixtures/empty").buildpacks(vec![ + BuildpackReference::WorkspaceBuildpack(buildpack_id!("libcnb-test/a")), + ]), + |context| { + assert_empty!(context.pack_stderr); + assert_contains!( + context.pack_stdout, + indoc! {" + Buildpack A + "} + ); + }, + ); +} + +#[test] +#[ignore = "integration test"] +fn basic_build_with_libcnb_reference_to_meta_buildpack() { + TestRunner::default().build( + BuildConfig::new("heroku/builder:22", "test-fixtures/empty").buildpacks(vec![ + BuildpackReference::WorkspaceBuildpack(buildpack_id!("libcnb-test/meta")), + ]), + |context| { + assert_empty!(context.pack_stderr); + assert_contains!( + context.pack_stdout, + indoc! {" + Buildpack A + Buildpack B + "} + ); + }, + ); +}