Skip to content

Commit

Permalink
Adding support for external fixture/example tests
Browse files Browse the repository at this point in the history
Added code to allow external bindings crates to find the UDL files and
cdylibs for examples/fixtures.  Made a new crate for this, since I
didn't want to pull in `serde` as a dependency for `uniffi`.
Eventually, this can replace `uniffi/testing.rs` and we can remove some
other dependencies from `uniffi` like `cargo_metadata`.

Standardized the crate names for fixtures/examples.  I think we're going
to need to publish these to crates.io eventually.

Added `Cargo.toml` config section that lists external clates for
`ext-types`

We copy the dylibs for external crates as well as our own.  I think this
should fix #1183.
  • Loading branch information
bendk committed Apr 4, 2022
1 parent f9bd97a commit 4453fb0
Show file tree
Hide file tree
Showing 10 changed files with 279 additions and 11 deletions.
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ members = [
"uniffi_bindgen",
"uniffi_build",
"uniffi_macros",
"uniffi_testing",
"uniffi",
"examples/arithmetic",
"examples/callbacks",
Expand Down
2 changes: 1 addition & 1 deletion examples/custom-types/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[package]
name = "custom-types"
name = "uniffi-example-custom-types"
edition = "2018"
version = "0.17.0"
authors = ["Firefox Sync Team <sync-team@mozilla.com>"]
Expand Down
2 changes: 1 addition & 1 deletion fixtures/callbacks/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[package]
name = "callbacks"
name = "uniffi-fixture-callbacks"
version = "0.17.0"
authors = ["Firefox Sync Team <sync-team@mozilla.com>"]
edition = "2018"
Expand Down
2 changes: 1 addition & 1 deletion fixtures/coverall/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[package]
name = "coverall"
name = "uniffi-fixture-coverall"
version = "0.17.0"
authors = ["Firefox Sync Team <sync-team@mozilla.com>"]
edition = "2018"
Expand Down
2 changes: 1 addition & 1 deletion fixtures/ext-types/guid/Cargo.toml
Original file line number Diff line number Diff line change
@@ -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 <sync-team@mozilla.com>"]
Expand Down
17 changes: 13 additions & 4 deletions fixtures/ext-types/lib/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,11 +1,18 @@
[package]
name = "ext-types-lib"
name = "uniffi-fixture-ext-types"
edition = "2018"
version = "0.17.0"
authors = ["Firefox Sync Team <sync-team@mozilla.com>"]
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"
Expand All @@ -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"]}


3 changes: 2 additions & 1 deletion fixtures/ext-types/uniffi-one/Cargo.toml
Original file line number Diff line number Diff line change
@@ -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 <sync-team@mozilla.com>"]
Expand All @@ -9,6 +9,7 @@ publish = false

[lib]
crate-type = ["cdylib", "lib"]
name = "uniffi_one"

[dependencies]
anyhow = "1"
Expand Down
4 changes: 2 additions & 2 deletions fixtures/reexport-scaffolding-macro/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
11 changes: 11 additions & 0 deletions uniffi_testing/Cargo.toml
Original file line number Diff line number Diff line change
@@ -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"
246 changes: 246 additions & 0 deletions uniffi_testing/src/lib.rs
Original file line number Diff line number Diff line change
@@ -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<Vec<String>>,
}

// A source to compile for a test
#[derive(Debug)]
pub struct CompileSource {
pub udl_path: PathBuf,
pub config_path: Option<PathBuf>,
}

// 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<Message> = 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<UniFFITestingMetadata>,
}

impl UniFFITestHelper {
pub fn new(name: &str) -> Result<Self> {
let package = Self::find_package(name)?;
let metadata: Option<UniFFITestingMetadata> = 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<Package> {
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<Vec<Package>> {
// 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<PathBuf> {
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::<Vec<Artifact>>();
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<Path>,
script_path: impl AsRef<Path>,
) -> Result<PathBuf> {
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<Path>) -> 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::<Result<Vec<_>>>()?;

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<Vec<CompileSource>> {
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<CompileSource> {
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<F: Fn(&Path) -> bool>(dir: &Path, predicate: F) -> Result<Vec<PathBuf>> {
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<Message> {
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())
}

0 comments on commit 4453fb0

Please sign in to comment.