-
Notifications
You must be signed in to change notification settings - Fork 240
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Adding support for external fixture/example tests
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
Showing
10 changed files
with
279 additions
and
11 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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()) | ||
} |