diff --git a/Cargo.toml b/Cargo.toml index 26dd14093e..6462f7759c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,6 +3,7 @@ members = [ "uniffi_bindgen", "uniffi_build", "uniffi_macros", + "uniffi_testing", "uniffi", "examples/arithmetic", "examples/callbacks", diff --git a/examples/custom-types/Cargo.toml b/examples/custom-types/Cargo.toml index 2ffbdf3c18..88e9fd1340 100644 --- a/examples/custom-types/Cargo.toml +++ b/examples/custom-types/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "custom-types" +name = "uniffi-example-custom-types" edition = "2018" version = "0.17.0" authors = ["Firefox Sync Team "] diff --git a/fixtures/callbacks/Cargo.toml b/fixtures/callbacks/Cargo.toml index 548f94e574..066cf875c5 100644 --- a/fixtures/callbacks/Cargo.toml +++ b/fixtures/callbacks/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "callbacks" +name = "uniffi-fixture-callbacks" version = "0.17.0" authors = ["Firefox Sync Team "] edition = "2018" diff --git a/fixtures/coverall/Cargo.toml b/fixtures/coverall/Cargo.toml index 652cc1bfdb..ab79f95d07 100644 --- a/fixtures/coverall/Cargo.toml +++ b/fixtures/coverall/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "coverall" +name = "uniffi-fixture-coverall" version = "0.17.0" authors = ["Firefox Sync Team "] edition = "2018" diff --git a/fixtures/ext-types/guid/Cargo.toml b/fixtures/ext-types/guid/Cargo.toml index 54e6c69f37..875516b47f 100644 --- a/fixtures/ext-types/guid/Cargo.toml +++ b/fixtures/ext-types/guid/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "ext-types-guid" +name = "uniffi-fixture-ext-types-guid" edition = "2018" version = "0.17.0" authors = ["Firefox Sync Team "] diff --git a/fixtures/ext-types/lib/Cargo.toml b/fixtures/ext-types/lib/Cargo.toml index ade9d364f3..5044b983e2 100644 --- a/fixtures/ext-types/lib/Cargo.toml +++ b/fixtures/ext-types/lib/Cargo.toml @@ -1,11 +1,18 @@ [package] -name = "ext-types-lib" +name = "uniffi-fixture-ext-types" edition = "2018" version = "0.17.0" authors = ["Firefox Sync Team "] license = "MPL-2.0" publish = false +[package.metadata.uniffi.testing] +external-crates = [ + "uniffi-fixture-ext-types-guid", + "uniffi-fixture-ext-types-lib-one", + "uniffi-example-custom-types", +] + [lib] crate-type = ["cdylib"] name = "uniffi_ext_types_lib" @@ -16,13 +23,15 @@ bytes = "1.0" uniffi_macros = {path = "../../../uniffi_macros"} uniffi = {path = "../../../uniffi", features=["builtin-bindgen"]} -uniffi-one = {path = "../uniffi-one"} -ext-types-guid = {path = "../guid"} +uniffi-fixture-ext-types-lib-one = {path = "../uniffi-one"} +uniffi-fixture-ext-types-guid = {path = "../guid"} # Reuse one of our examples. -custom-types = {path = "../../../examples/custom-types"} +uniffi-example-custom-types = {path = "../../../examples/custom-types"} url = "2.2" [build-dependencies] uniffi_build = {path = "../../../uniffi_build", features=["builtin-bindgen"]} + + diff --git a/fixtures/ext-types/uniffi-one/Cargo.toml b/fixtures/ext-types/uniffi-one/Cargo.toml index 70a4bea692..a460e7137f 100644 --- a/fixtures/ext-types/uniffi-one/Cargo.toml +++ b/fixtures/ext-types/uniffi-one/Cargo.toml @@ -1,6 +1,6 @@ [package] # TODO: modify the crate name to test non-default names. -name = "uniffi-one" +name = "uniffi-fixture-ext-types-lib-one" edition = "2018" version = "0.17.0" authors = ["Firefox Sync Team "] @@ -9,6 +9,7 @@ publish = false [lib] crate-type = ["cdylib", "lib"] +name = "uniffi_one" [dependencies] anyhow = "1" diff --git a/fixtures/reexport-scaffolding-macro/Cargo.toml b/fixtures/reexport-scaffolding-macro/Cargo.toml index 1aa10e51bc..4a0c91c106 100644 --- a/fixtures/reexport-scaffolding-macro/Cargo.toml +++ b/fixtures/reexport-scaffolding-macro/Cargo.toml @@ -10,8 +10,8 @@ name = "reexport_scaffolding_macro" crate-type = ["cdylib"] [dependencies] -callbacks = { path = "../callbacks" } -coverall = { path = "../coverall" } +uniffi-fixture-callbacks = { path = "../callbacks" } +uniffi-fixture-coverall = { path = "../coverall" } uniffi = { path = "../../uniffi", features=["builtin-bindgen"] } [dev-dependencies] diff --git a/uniffi_testing/Cargo.toml b/uniffi_testing/Cargo.toml new file mode 100644 index 0000000000..7258bff76f --- /dev/null +++ b/uniffi_testing/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "uniffi_testing" +version = "0.1.0" +edition = "2021" + +[dependencies] +anyhow = "1" +cargo_metadata = "0.13" +lazy_static = "1.4" +serde = "1" +serde_json = "1" diff --git a/uniffi_testing/src/lib.rs b/uniffi_testing/src/lib.rs new file mode 100644 index 0000000000..74f6a0e917 --- /dev/null +++ b/uniffi_testing/src/lib.rs @@ -0,0 +1,246 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public +License, v. 2.0. If a copy of the MPL was not distributed with this +* file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +use anyhow::{bail, Result}; +use cargo_metadata::{Artifact, Message, Metadata, MetadataCommand, Package, Target}; +use serde::Deserialize; +use std::{ + collections::hash_map::DefaultHasher, + env, + env::consts::DLL_EXTENSION, + fs::{copy, read_dir}, + hash::{Hash, Hasher}, + path::{Path, PathBuf}, + process::{Command, Stdio}, +}; + +#[derive(Deserialize)] +struct UniFFITestingMetadata { + #[serde(rename = "external-crates")] + external_crates: Option>, +} + +// A source to compile for a test +#[derive(Debug)] +pub struct CompileSource { + pub udl_path: PathBuf, + pub config_path: Option, +} + +// Store Cargo output to in a lazy_static to avoid calling it more than once. +lazy_static::lazy_static! { + static ref CARGO_METADATA: Metadata = get_cargo_metadata(); + static ref CARGO_BUILD_MESSAGES: Vec = get_cargo_build_messages(); +} + +/// Struct for running fixture and example tests for bindings generators +/// +/// Expectations: +/// - Used from a integration test (a `.rs` file in the tests/ directory) +/// - The working directory is the project root for the bindings crate. This is the normal case +/// for test code, just make sure you don't cd somewhere else. +/// - The bindings crate has a dev-dependency on the fixture crate +/// - The fixture crate produces a cdylib library +/// - The fixture crate, and any external-crates, has 1 UDL file in it's src/ directory +pub struct UniFFITestHelper { + name: String, + package: Package, + metadata: Option, +} + +impl UniFFITestHelper { + pub fn new(name: &str) -> Result { + let package = Self::find_package(name)?; + let metadata: Option = package + .metadata + .pointer("/uniffi/testing") + .cloned() + .map(serde_json::from_value) + .transpose()?; + Ok(Self { + name: name.to_string(), + package, + metadata, + }) + } + + fn find_package(name: &str) -> Result { + let matching: Vec<&Package> = CARGO_METADATA + .packages + .iter() + .filter(|p| p.name == name) + .collect(); + match matching.len() { + 1 => Ok(matching[0].clone()), + n => bail!("cargo metadata return {} packages named {}", n, name), + } + } + + fn find_packages_for_external_crates(&self) -> Result> { + // Add any external crates listed in `Cargo.toml` + match &self.metadata { + None => Ok(vec![]), + Some(metadata) => metadata + .external_crates + .iter() + .flatten() + .map(|name| Self::find_package(name)) + .collect(), + } + } + + fn find_cdylib_path(package: &Package) -> Result { + let cdylib_targets: Vec<&Target> = package + .targets + .iter() + .filter(|t| t.crate_types.iter().any(|t| t == "cdylib")) + .collect(); + let target = match cdylib_targets.len() { + 1 => cdylib_targets[0], + n => bail!("Found {} cdylib targets for {}", n, package.name), + }; + + let artifacts = CARGO_BUILD_MESSAGES + .iter() + .filter_map(|message| match message { + Message::CompilerArtifact(artifact) => { + if artifact.target == *target { + Some(artifact.clone()) + } else { + None + } + } + _ => None, + }) + .collect::>(); + let artifact = match artifacts.len() { + 1 => &artifacts[0], + n => bail!("Found {} artifacts for target {}", n, target.name), + }; + let cdylib_files: Vec<_> = artifact + .filenames + .iter() + .filter(|nm| matches!(nm.extension(), Some(DLL_EXTENSION))) + .collect(); + + match cdylib_files.len() { + 1 => Ok(cdylib_files[0].as_std_path().to_path_buf()), + n => bail!("Found {} cdylib files for {}", n, artifact.target.name), + } + } + + /// Create at `out_dir` for testing + /// + /// This directory can be used for: + /// - Generated bindings files (usually via the `--out-dir` param) + /// - cdylib libraries that the bindings depend on + /// - Anything else that's useful for testing + /// + /// This directory typically created as a subdirectory of `CARGO_TARGET_TMPDIR` when running an + /// integration test. + /// + /// We use the script path to create a hash included in the outpuit directory. This avoids + /// path collutions when 2 scripts run against the same fixture. + pub fn create_out_dir( + &self, + temp_dir: impl AsRef, + script_path: impl AsRef, + ) -> Result { + let dirname = format!("{}-{}", self.name, hash_path(script_path.as_ref())); + let out_dir = Path::new(temp_dir.as_ref()).join(dirname); + if out_dir.exists() { + // Clean out any files from previous runs + std::fs::remove_dir_all(&out_dir)?; + } + std::fs::create_dir(&out_dir)?; + Ok(out_dir) + } + + /// Copy the `cdylib` for a fixture into the out_dir + /// + /// This is typically needed for the bindings to open it when running the tests + /// + /// Returns the path to the copied library + pub fn copy_cdylibs_to_out_dir(&self, out_dir: impl AsRef) -> Result<()> { + let cdylib_paths = std::iter::once(self.package.clone()) + .chain(self.find_packages_for_external_crates()?) + .map(|p| Self::find_cdylib_path(&p)) + .collect::>>()?; + + for path in cdylib_paths.into_iter() { + let dest = out_dir.as_ref().join(path.file_name().unwrap()); + copy(&path, &dest)?; + } + Ok(()) + } + + /// Get paths to the UDL and config files for a fixture + pub fn get_compile_sources(&self) -> Result> { + std::iter::once(self.package.clone()) + .chain(self.find_packages_for_external_crates()?) + .map(|p| self.find_compile_source(&p)) + .collect() + } + + fn find_compile_source(&self, package: &Package) -> Result { + let crate_root = package.manifest_path.parent().unwrap().as_std_path(); + let src_dir = crate_root.join("src"); + let mut udl_paths = find_files( + &src_dir, + |path| matches!(path.extension(), Some(ext) if ext.to_ascii_lowercase() == "udl"), + )?; + let udl_path = match udl_paths.len() { + 1 => udl_paths.remove(0), + n => bail!("Found {} UDL files in {:?}", n, src_dir), + }; + let mut config_paths = find_files( + crate_root, + |path| matches!(path.file_name(), Some(name) if name == "uniffi.toml"), + )?; + let config_path = match config_paths.len() { + 0 => None, + 1 => Some(config_paths.remove(0)), + n => bail!("Found {} UDL files in {:?}", n, crate_root), + }; + + Ok(CompileSource { + udl_path, + config_path, + }) + } +} + +fn find_files bool>(dir: &Path, predicate: F) -> Result> { + Ok(read_dir(&dir)? + .flatten() + .map(|entry| entry.path()) + .filter(|p| predicate(p)) + .collect()) +} + +fn get_cargo_metadata() -> Metadata { + MetadataCommand::new() + .exec() + .expect("error running cargo metadata") +} + +fn get_cargo_build_messages() -> Vec { + let mut child = Command::new(env!("CARGO")) + .arg("build") + .arg("--message-format=json") + .arg("--tests") + .stdout(Stdio::piped()) + .spawn() + .expect("Error running cargo build"); + let output = std::io::BufReader::new(child.stdout.take().unwrap()); + Message::parse_stream(output) + .map(|m| m.expect("Error parsing cargo build messages")) + .collect() +} + +fn hash_path(path: &Path) -> String { + let mut hasher = DefaultHasher::new(); + path.hash(&mut hasher); + format!("{:x}", hasher.finish()) +}