diff options
Diffstat (limited to 'third_party/rust/uniffi_testing')
-rw-r--r-- | third_party/rust/uniffi_testing/.cargo-checksum.json | 1 | ||||
-rw-r--r-- | third_party/rust/uniffi_testing/Cargo.toml | 43 | ||||
-rw-r--r-- | third_party/rust/uniffi_testing/README.md | 18 | ||||
-rw-r--r-- | third_party/rust/uniffi_testing/src/lib.rs | 275 |
4 files changed, 337 insertions, 0 deletions
diff --git a/third_party/rust/uniffi_testing/.cargo-checksum.json b/third_party/rust/uniffi_testing/.cargo-checksum.json new file mode 100644 index 0000000000..45c384e054 --- /dev/null +++ b/third_party/rust/uniffi_testing/.cargo-checksum.json @@ -0,0 +1 @@ +{"files":{"Cargo.toml":"6077dda68909ce797d4b68f6a617bafb83a7a140ea43a90633b2bb78f37fab0a","README.md":"ec6aba24af9a011ef6647422aa22efabdee519cdee3da1a9f9033b07b7cbdb0d","src/lib.rs":"925b93068b9d3a9d890f3ee3e6d760fd032359ac083350fd919cbb71a2a6580c"},"package":"92b0570953ec41d97ce23e3b92161ac18231670a1f97523258a6d2ab76d7f76c"}
\ No newline at end of file diff --git a/third_party/rust/uniffi_testing/Cargo.toml b/third_party/rust/uniffi_testing/Cargo.toml new file mode 100644 index 0000000000..9869e861c3 --- /dev/null +++ b/third_party/rust/uniffi_testing/Cargo.toml @@ -0,0 +1,43 @@ +# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO +# +# When uploading crates to the registry Cargo will automatically +# "normalize" Cargo.toml files for maximal compatibility +# with all versions of Cargo and also rewrite `path` dependencies +# to registry (e.g., crates.io) dependencies. +# +# If you are reading this file be aware that the original Cargo.toml +# will likely look very different (and much more reasonable). +# See Cargo.toml.orig for the original contents. + +[package] +edition = "2021" +name = "uniffi_testing" +version = "0.23.0" +authors = ["Firefox Sync Team <sync-team@mozilla.com>"] +description = "a multi-language bindings generator for rust (testing helpers)" +homepage = "https://mozilla.github.io/uniffi-rs" +documentation = "https://mozilla.github.io/uniffi-rs" +readme = "README.md" +license = "MPL-2.0" +repository = "https://github.com/mozilla/uniffi-rs" + +[dependencies.anyhow] +version = "1" + +[dependencies.camino] +version = "1.0.8" + +[dependencies.cargo_metadata] +version = "0.15" + +[dependencies.fs-err] +version = "2.7.0" + +[dependencies.once_cell] +version = "1.12" + +[dependencies.serde] +version = "1" + +[dependencies.serde_json] +version = "1" diff --git a/third_party/rust/uniffi_testing/README.md b/third_party/rust/uniffi_testing/README.md new file mode 100644 index 0000000000..c171b26373 --- /dev/null +++ b/third_party/rust/uniffi_testing/README.md @@ -0,0 +1,18 @@ +This crate contains helper code for testing bindings. Our general system is to +generate bindings for the libraries from the examples and fixtures +directories, then execute a script that tests the bindings. + +Each bindings crate can do this in a different way, but the typical system is: + + - Construct a `UniFFITestHelper` struct to assist the process + - Call `UniFFITestHelper.create_out_dir()` to create a temp directory to + store testing files + - Call `UniFFITestHelper.copy_cdylibs_to_out_dir()` to copy the dylib + artifacts for the example/fixture library to the `out_dir`. This is needed + because the bindings code dynamically links to or loads from this library. + - Call `UniFFITestHelper.get_compile_sources()` to iterate over (`udl_path`, + `uniffi_config_path`) pairs and generate the bindings from them. This step + is specific to the bindings language, it may mean creating a .jar file, + compiling a binary, or just copying script files over. + - Execute the test script and check if it succeeds. This step is also + specific to the bindings language. diff --git a/third_party/rust/uniffi_testing/src/lib.rs b/third_party/rust/uniffi_testing/src/lib.rs new file mode 100644 index 0000000000..cc08895214 --- /dev/null +++ b/third_party/rust/uniffi_testing/src/lib.rs @@ -0,0 +1,275 @@ +/* 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 camino::{Utf8Path, Utf8PathBuf}; +use cargo_metadata::{Message, Metadata, MetadataCommand, Package, Target}; +use fs_err as fs; +use once_cell::sync::Lazy; +use serde::Deserialize; +use std::{ + collections::hash_map::DefaultHasher, + env, + env::consts::DLL_EXTENSION, + hash::{Hash, Hasher}, + process::{Command, Stdio}, +}; + +#[derive(Debug, Deserialize)] +struct UniFFITestingMetadata { + /// Crates that hold external types used by this crate. When running the tests, we will build + /// the libraries and generate the source files for those crates and put them in the test + /// directory + #[serde(rename = "external-crates")] + external_crates: Option<Vec<String>>, +} + +// A source to compile for a test +#[derive(Debug)] +pub struct CompileSource { + pub udl_path: Utf8PathBuf, + pub config_path: Option<Utf8PathBuf>, +} + +// Store Cargo output to in a static to avoid calling it more than once. + +static CARGO_METADATA: Lazy<Metadata> = Lazy::new(get_cargo_metadata); +static CARGO_BUILD_MESSAGES: Lazy<Vec<Message>> = Lazy::new(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 {n} packages named {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<Utf8PathBuf> { + 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 {n} cdylib targets for {}", 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, + }); + let cdylib_files: Vec<Utf8PathBuf> = artifacts + .into_iter() + .flat_map(|artifact| { + artifact + .filenames + .into_iter() + .filter(|nm| matches!(nm.extension(), Some(DLL_EXTENSION))) + .collect::<Vec<Utf8PathBuf>>() + }) + .collect(); + + match cdylib_files.len() { + 1 => Ok(cdylib_files[0].to_owned()), + n => bail!("Found {n} cdylib files for {}", package.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<Utf8Path>, + script_path: impl AsRef<Utf8Path>, + ) -> Result<Utf8PathBuf> { + let dirname = format!("{}-{}", self.name, hash_path(script_path.as_ref())); + let out_dir = temp_dir.as_ref().join(dirname); + if out_dir.exists() { + // Clean out any files from previous runs + fs::remove_dir_all(&out_dir)?; + } + 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<Utf8Path>) -> Result<()> { + let cdylib_paths = + std::iter::once(self.cdylib_path()?).chain(self.external_cdylib_paths()?); + + for path in cdylib_paths { + let dest = out_dir.as_ref().join(path.file_name().unwrap()); + fs::copy(&path, dest)?; + } + Ok(()) + } + + /// Get the path to the cdylib file for this package + pub fn cdylib_path(&self) -> Result<Utf8PathBuf> { + Self::find_cdylib_path(&self.package) + } + + /// Get the path to the cdylib file for external crates listed in `Cargo.toml` + pub fn external_cdylib_paths(&self) -> Result<Vec<Utf8PathBuf>> { + self.find_packages_for_external_crates()? + .into_iter() + .map(|p| Self::find_cdylib_path(&p)) + .collect() + } + + /// Get paths to the UDL and config files for a fixture + pub fn get_compile_sources(&self) -> Result<Vec<CompileSource>> { + Ok(std::iter::once(self.get_main_compile_source()?) + .chain(self.get_external_compile_sources()?) + .collect()) + } + + pub fn get_main_compile_source(&self) -> Result<CompileSource> { + self.find_compile_source(&self.package.clone()) + } + + pub fn get_external_compile_sources(&self) -> Result<Vec<CompileSource>> { + self.find_packages_for_external_crates()? + .into_iter() + .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(); + 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 {n} UDL files in {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 {n} UDL files in {crate_root}"), + }; + + Ok(CompileSource { + udl_path, + config_path, + }) + } +} + +fn find_files<F: Fn(&Utf8Path) -> bool>(dir: &Utf8Path, predicate: F) -> Result<Vec<Utf8PathBuf>> { + fs::read_dir(dir)? + .flatten() + .map(|entry| entry.path().try_into()) + .try_fold(Vec::new(), |mut vec, path| { + let path: Utf8PathBuf = path?; + if predicate(&path) { + vec.push(path); + } + Ok(vec) + }) +} + +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: &Utf8Path) -> String { + let mut hasher = DefaultHasher::new(); + path.hash(&mut hasher); + format!("{:x}", hasher.finish()) +} |