feat(core, contract): improve `determine_ethers_crates` (#1988)
* refactor: determine_ethers_crates * feat: improve crate resolution * export new types and functions * export * fix: file name check * fix: invert condition and check CARGO_MANIFEST_DIR * use abigen macro to generate multicall * chore: rm debug * rm unnecessary .replace * chore: clippy * chore: clippy * Revert "chore: clippy" This reverts commitbd220f308d
. * Revert "chore: clippy" This reverts commit5550f4e856
. * add tests * better tests, docs * add another test * fix docs * refactor: add an environment struct for determining ethers crates * fix: use fmt::Debug to escape paths * docs: rename and rm old docs * feat: use global path for crates * fix: docs * chore: move rand impl to tests mod
This commit is contained in:
parent
fd4da49121
commit
97582cc346
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
@ -1,123 +1,606 @@
|
|||
use cargo_metadata::MetadataCommand;
|
||||
use once_cell::sync::Lazy;
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
env, fmt, fs,
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
use strum::{EnumCount, EnumIter, EnumString, EnumVariantNames, IntoEnumIterator};
|
||||
|
||||
use syn::Path;
|
||||
/// `ethers_crate => name`
|
||||
type CrateNames = HashMap<EthersCrate, &'static str>;
|
||||
|
||||
/// See `determine_ethers_crates`
|
||||
const DIRS: [&str; 3] = ["benches", "examples", "tests"];
|
||||
|
||||
/// Maps an [`EthersCrate`] to its path string.
|
||||
///
|
||||
/// This ensures that the `MetadataCommand` is only run once
|
||||
static ETHERS_CRATES: Lazy<(&'static str, &'static str, &'static str)> =
|
||||
Lazy::new(determine_ethers_crates);
|
||||
/// See [`ProjectEnvironment`] for more information.
|
||||
///
|
||||
/// Note: this static variable cannot hold [`syn::Path`] because it is not [`Sync`], so the names
|
||||
/// must be parsed at every call.
|
||||
static ETHERS_CRATE_NAMES: Lazy<CrateNames> = Lazy::new(|| {
|
||||
ProjectEnvironment::new_from_env()
|
||||
.and_then(|x| x.determine_ethers_crates())
|
||||
.unwrap_or_else(|| EthersCrate::ethers_path_names().collect())
|
||||
});
|
||||
|
||||
/// Convenience function to turn the `ethers_core` name in `ETHERS_CRATE` into a `Path`
|
||||
pub fn ethers_core_crate() -> Path {
|
||||
syn::parse_str(ETHERS_CRATES.0).expect("valid path; qed")
|
||||
}
|
||||
/// Convenience function to turn the `ethers_contract` name in `ETHERS_CRATE` into an `Path`
|
||||
pub fn ethers_contract_crate() -> Path {
|
||||
syn::parse_str(ETHERS_CRATES.1).expect("valid path; qed")
|
||||
}
|
||||
pub fn ethers_providers_crate() -> Path {
|
||||
syn::parse_str(ETHERS_CRATES.2).expect("valid path; qed")
|
||||
/// Returns the `core` crate's [`Path`][syn::Path].
|
||||
#[inline]
|
||||
pub fn ethers_core_crate() -> syn::Path {
|
||||
get_crate_path(EthersCrate::EthersCore)
|
||||
}
|
||||
|
||||
/// The crates name to use when deriving macros: (`core`, `contract`)
|
||||
///
|
||||
/// We try to determine which crate ident to use based on the dependencies of
|
||||
/// the project in which the macro is used. This is useful because the macros,
|
||||
/// like `EthEvent` are provided by the `ethers-contract` crate which depends on
|
||||
/// `ethers_core`. Most commonly `ethers` will be used as dependency which
|
||||
/// reexports all the different crates, essentially `ethers::core` is
|
||||
/// `ethers_core` So depending on the dependency used `ethers` ors `ethers_core
|
||||
/// | ethers_contract`, we need to use the fitting crate ident when expand the
|
||||
/// macros This will attempt to parse the current `Cargo.toml` and check the
|
||||
/// ethers related dependencies.
|
||||
///
|
||||
/// This determines
|
||||
/// - `ethers_*` idents if `ethers-core`, `ethers-contract`, `ethers-providers` are present in
|
||||
/// the manifest or the `ethers` is _not_ present
|
||||
/// - `ethers::*` otherwise
|
||||
///
|
||||
/// This process is a bit hacky, we run `cargo metadata` internally which
|
||||
/// resolves the current package but creates a new `Cargo.lock` file in the
|
||||
/// process. This is not a problem for regular workspaces but becomes an issue
|
||||
/// during publishing with `cargo publish` if the project does not ignore
|
||||
/// `Cargo.lock` in `.gitignore`, because then cargo can't proceed with
|
||||
/// publishing the crate because the created `Cargo.lock` leads to a modified
|
||||
/// workspace, not the `CARGO_MANIFEST_DIR` but the workspace `cargo publish`
|
||||
/// created in `./target/package/..`. Therefore we check prior to executing
|
||||
/// `cargo metadata` if a `Cargo.lock` file exists and delete it afterwards if
|
||||
/// it was created by `cargo metadata`.
|
||||
pub fn determine_ethers_crates() -> (&'static str, &'static str, &'static str) {
|
||||
let manifest_dir = std::env::var("CARGO_MANIFEST_DIR");
|
||||
/// Returns the `contract` crate's [`Path`][syn::Path].
|
||||
#[inline]
|
||||
pub fn ethers_contract_crate() -> syn::Path {
|
||||
get_crate_path(EthersCrate::EthersContract)
|
||||
}
|
||||
|
||||
// if there is no cargo manifest, default to `ethers::`-style imports.
|
||||
let manifest_dir = if let Ok(manifest_dir) = manifest_dir {
|
||||
manifest_dir
|
||||
} else {
|
||||
return ("ethers::core", "ethers::contract", "ethers::providers")
|
||||
};
|
||||
/// Returns the `providers` crate's [`Path`][syn::Path].
|
||||
#[inline]
|
||||
pub fn ethers_providers_crate() -> syn::Path {
|
||||
get_crate_path(EthersCrate::EthersProviders)
|
||||
}
|
||||
|
||||
// check if the lock file exists, if it's missing we need to clean up afterward
|
||||
let lock_file = format!("{manifest_dir}/Cargo.lock");
|
||||
let needs_lock_file_cleanup = !std::path::Path::new(&lock_file).exists();
|
||||
/// Returns an [`EthersCrate`]'s [`Path`][syn::Path] in the current project.
|
||||
#[inline(always)]
|
||||
pub fn get_crate_path(krate: EthersCrate) -> syn::Path {
|
||||
krate.get_path()
|
||||
}
|
||||
|
||||
let res = MetadataCommand::new()
|
||||
.manifest_path(format!("{manifest_dir}/Cargo.toml"))
|
||||
.exec()
|
||||
.ok()
|
||||
.and_then(|metadata| {
|
||||
metadata.root_package().and_then(|pkg| {
|
||||
let sub_crates = Some(("ethers_core", "ethers_contract", "ethers_providers"));
|
||||
/// Represents a generic Rust/Cargo project's environment.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct ProjectEnvironment {
|
||||
manifest_dir: PathBuf,
|
||||
crate_name: Option<String>,
|
||||
}
|
||||
|
||||
// Note(mattsse): this is super hacky but required in order to compile and test
|
||||
// ethers' internal crates
|
||||
if [
|
||||
"ethers-contract",
|
||||
"ethers-derive-eip712",
|
||||
"ethers-signers",
|
||||
"ethers-middleware",
|
||||
"ethers-solc",
|
||||
]
|
||||
.contains(&pkg.name.as_str())
|
||||
{
|
||||
return sub_crates
|
||||
impl ProjectEnvironment {
|
||||
pub fn new<T: Into<PathBuf>, U: Into<String>>(manifest_dir: T, crate_name: U) -> Self {
|
||||
Self { manifest_dir: manifest_dir.into(), crate_name: Some(crate_name.into()) }
|
||||
}
|
||||
|
||||
let mut has_ethers_core = false;
|
||||
let mut has_ethers_contract = false;
|
||||
let mut has_ethers_providers = false;
|
||||
|
||||
for dep in pkg.dependencies.iter() {
|
||||
match dep.name.as_str() {
|
||||
"ethers-core" => {
|
||||
has_ethers_core = true;
|
||||
}
|
||||
"ethers-contract" => {
|
||||
has_ethers_contract = true;
|
||||
}
|
||||
"ethers-providers" => {
|
||||
has_ethers_providers = true;
|
||||
}
|
||||
"ethers" => return None,
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
if has_ethers_core && has_ethers_contract && has_ethers_providers {
|
||||
return sub_crates
|
||||
}
|
||||
|
||||
None
|
||||
pub fn new_from_env() -> Option<Self> {
|
||||
Some(Self {
|
||||
manifest_dir: env::var_os("CARGO_MANIFEST_DIR")?.into(),
|
||||
crate_name: env::var("CARGO_CRATE_NAME").ok(),
|
||||
})
|
||||
})
|
||||
.unwrap_or(("ethers::core", "ethers::contract", "ethers::providers"));
|
||||
}
|
||||
|
||||
if needs_lock_file_cleanup {
|
||||
// delete the `Cargo.lock` file that was created by `cargo metadata`
|
||||
// if the package is not part of a workspace
|
||||
/// Determines the crate paths to use by looking at the [metadata][cargo_metadata] of the
|
||||
/// project.
|
||||
///
|
||||
/// The names will be:
|
||||
/// - `ethers::*` if `ethers` is a dependency for all crates;
|
||||
/// - for each `crate`:
|
||||
/// - `ethers_<crate>` if it is a dependency, otherwise `ethers::<crate>`.
|
||||
#[inline]
|
||||
pub fn determine_ethers_crates(&self) -> Option<CrateNames> {
|
||||
let lock_file = self.manifest_dir.join("Cargo.lock");
|
||||
let lock_file_existed = lock_file.exists();
|
||||
|
||||
let names = self.crate_names_from_metadata();
|
||||
|
||||
// remove the lock file created from running the command
|
||||
if !lock_file_existed && lock_file.exists() {
|
||||
let _ = std::fs::remove_file(lock_file);
|
||||
}
|
||||
|
||||
res
|
||||
names
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn crate_names_from_metadata(&self) -> Option<CrateNames> {
|
||||
let metadata = MetadataCommand::new().current_dir(&self.manifest_dir).exec().ok()?;
|
||||
let pkg = metadata.root_package()?;
|
||||
|
||||
// return ethers_* if the root package is an internal ethers crate since `ethers` is not
|
||||
// available
|
||||
let crate_is_root = self.is_crate_root();
|
||||
if let Ok(current_pkg) = pkg.name.parse::<EthersCrate>() {
|
||||
// replace `current_pkg`'s name with "crate"
|
||||
let names =
|
||||
EthersCrate::path_names()
|
||||
.map(|(pkg, name)| {
|
||||
if crate_is_root && pkg == current_pkg {
|
||||
(pkg, "crate")
|
||||
} else {
|
||||
(pkg, name)
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
return Some(names)
|
||||
} /* else if pkg.name == "ethers" {
|
||||
// should not happen (the root package the `ethers` workspace package itself)
|
||||
} */
|
||||
|
||||
let mut names: CrateNames = EthersCrate::ethers_path_names().collect();
|
||||
for dep in pkg.dependencies.iter() {
|
||||
let name = dep.name.as_str();
|
||||
if name.starts_with("ethers") {
|
||||
if name == "ethers" {
|
||||
return None
|
||||
} else if let Ok(dep) = name.parse::<EthersCrate>() {
|
||||
names.insert(dep, dep.path_name());
|
||||
}
|
||||
}
|
||||
}
|
||||
Some(names)
|
||||
}
|
||||
|
||||
/// Returns whether the `crate` path identifier refers to the root package.
|
||||
///
|
||||
/// This is false for integration tests, benches, and examples, as the `crate` keyword will not
|
||||
/// refer to the root package.
|
||||
///
|
||||
/// We can find this using some [environment variables set by Cargo during compilation][ref]:
|
||||
/// - `CARGO_TARGET_TMPDIR` is only set when building integration test or benchmark code;
|
||||
/// - When `CARGO_MANIFEST_DIR` contains `/benches/` or `/examples/`
|
||||
/// - `CARGO_CRATE_NAME`, see `is_crate_name_in_dirs`.
|
||||
///
|
||||
/// [ref]: https://doc.rust-lang.org/cargo/reference/environment-variables.html#environment-variables-cargo-sets-for-crates
|
||||
#[inline]
|
||||
pub fn is_crate_root(&self) -> bool {
|
||||
env::var_os("CARGO_TARGET_TMPDIR").is_none() &&
|
||||
self.manifest_dir.components().all(|c| {
|
||||
let s = c.as_os_str();
|
||||
s != "examples" && s != "benches"
|
||||
}) &&
|
||||
!self.is_crate_name_in_dirs()
|
||||
}
|
||||
|
||||
/// Returns whether `crate_name` is the name of a file or directory in the first level of
|
||||
/// `manifest_dir/{benches,examples,tests}/`.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// With this project structure:
|
||||
///
|
||||
/// ```text
|
||||
/// .
|
||||
/// ├── Cargo.lock
|
||||
/// ├── Cargo.toml
|
||||
/// ├── src/
|
||||
/// │ ...
|
||||
/// ├── benches/
|
||||
/// │ ├── large-input.rs
|
||||
/// │ └── multi-file-bench/
|
||||
/// │ ├── main.rs
|
||||
/// │ └── bench_module.rs
|
||||
/// ├── examples/
|
||||
/// │ ├── simple.rs
|
||||
/// │ └── multi-file-example/
|
||||
/// │ ├── main.rs
|
||||
/// │ └── ex_module.rs
|
||||
/// └── tests/
|
||||
/// ├── some-integration-tests.rs
|
||||
/// └── multi-file-test/
|
||||
/// ├── main.rs
|
||||
/// └── test_module.rs
|
||||
/// ```
|
||||
///
|
||||
/// The resulting `CARGO_CRATE_NAME` values will be:
|
||||
///
|
||||
/// | Path | Value |
|
||||
/// |:-------------------------------------- | ----------------------:|
|
||||
/// | benches/large-input.rs | large-input |
|
||||
/// | benches/multi-file-bench/\*\*/\*.rs | multi-file-bench |
|
||||
/// | examples/simple.rs | simple |
|
||||
/// | examples/multi-file-example/\*\*/\*.rs | multi-file-example |
|
||||
/// | tests/some-integration-tests.rs | some-integration-tests |
|
||||
/// | tests/multi-file-test/\*\*/\*.rs | multi-file-test |
|
||||
#[inline]
|
||||
pub fn is_crate_name_in_dirs(&self) -> bool {
|
||||
let crate_name = match self.crate_name.as_ref() {
|
||||
Some(name) => name,
|
||||
None => return false,
|
||||
};
|
||||
let dirs = DIRS.map(|dir| self.manifest_dir.join(dir));
|
||||
dirs.iter().any(|dir| {
|
||||
fs::read_dir(dir)
|
||||
.ok()
|
||||
.and_then(|entries| {
|
||||
entries
|
||||
.filter_map(Result::ok)
|
||||
.find(|entry| file_stem_eq(entry.path(), crate_name))
|
||||
})
|
||||
.is_some()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// An `ethers-rs` internal crate.
|
||||
#[derive(
|
||||
Clone,
|
||||
Copy,
|
||||
Debug,
|
||||
PartialEq,
|
||||
Eq,
|
||||
PartialOrd,
|
||||
Ord,
|
||||
Hash,
|
||||
EnumCount,
|
||||
EnumIter,
|
||||
EnumString,
|
||||
EnumVariantNames,
|
||||
)]
|
||||
#[strum(serialize_all = "kebab-case")]
|
||||
pub enum EthersCrate {
|
||||
EthersAddressbook,
|
||||
EthersContract,
|
||||
EthersContractAbigen,
|
||||
EthersContractDerive,
|
||||
EthersCore,
|
||||
EthersDeriveEip712,
|
||||
EthersEtherscan,
|
||||
EthersMiddleware,
|
||||
EthersProviders,
|
||||
EthersSigners,
|
||||
EthersSolc,
|
||||
}
|
||||
|
||||
impl AsRef<str> for EthersCrate {
|
||||
fn as_ref(&self) -> &str {
|
||||
self.crate_name()
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for EthersCrate {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
f.pad(self.as_ref())
|
||||
}
|
||||
}
|
||||
|
||||
impl EthersCrate {
|
||||
/// "`<self as kebab-case>`"
|
||||
#[inline]
|
||||
pub const fn crate_name(self) -> &'static str {
|
||||
match self {
|
||||
Self::EthersAddressbook => "ethers-addressbook",
|
||||
Self::EthersContract => "ethers-contract",
|
||||
Self::EthersContractAbigen => "ethers-contract-abigen",
|
||||
Self::EthersContractDerive => "ethers-contract-derive",
|
||||
Self::EthersCore => "ethers-core",
|
||||
Self::EthersDeriveEip712 => "ethers-derive-eip712",
|
||||
Self::EthersEtherscan => "ethers-etherscan",
|
||||
Self::EthersMiddleware => "ethers-middleware",
|
||||
Self::EthersProviders => "ethers-providers",
|
||||
Self::EthersSigners => "ethers-signers",
|
||||
Self::EthersSolc => "ethers-solc",
|
||||
}
|
||||
}
|
||||
|
||||
/// "`::<self as snake_case>`"
|
||||
#[inline]
|
||||
pub const fn path_name(self) -> &'static str {
|
||||
match self {
|
||||
Self::EthersAddressbook => "::ethers_addressbook",
|
||||
Self::EthersContract => "::ethers_contract",
|
||||
Self::EthersContractAbigen => "::ethers_contract_abigen",
|
||||
Self::EthersContractDerive => "::ethers_contract_derive",
|
||||
Self::EthersCore => "::ethers_core",
|
||||
Self::EthersDeriveEip712 => "::ethers_derive_eip712",
|
||||
Self::EthersEtherscan => "::ethers_etherscan",
|
||||
Self::EthersMiddleware => "::ethers_middleware",
|
||||
Self::EthersProviders => "::ethers_providers",
|
||||
Self::EthersSigners => "::ethers_signers",
|
||||
Self::EthersSolc => "::ethers_solc",
|
||||
}
|
||||
}
|
||||
|
||||
/// "::ethers::`<self in ethers>`"
|
||||
#[inline]
|
||||
pub const fn ethers_path_name(self) -> &'static str {
|
||||
match self {
|
||||
// re-exported in ethers::contract
|
||||
Self::EthersContractAbigen => "::ethers::contract", // partly
|
||||
Self::EthersContractDerive => "::ethers::contract",
|
||||
Self::EthersDeriveEip712 => "::ethers::contract",
|
||||
|
||||
Self::EthersAddressbook => "::ethers::addressbook",
|
||||
Self::EthersContract => "::ethers::contract",
|
||||
Self::EthersCore => "::ethers::core",
|
||||
Self::EthersEtherscan => "::ethers::etherscan",
|
||||
Self::EthersMiddleware => "::ethers::middleware",
|
||||
Self::EthersProviders => "::ethers::providers",
|
||||
Self::EthersSigners => "::ethers::signers",
|
||||
Self::EthersSolc => "::ethers::solc",
|
||||
}
|
||||
}
|
||||
|
||||
/// The path on the file system, from an `ethers-rs` root directory.
|
||||
#[inline]
|
||||
pub const fn fs_path(self) -> &'static str {
|
||||
match self {
|
||||
Self::EthersContractAbigen => "ethers-contract/ethers-contract-abigen",
|
||||
Self::EthersContractDerive => "ethers-contract/ethers-contract-derive",
|
||||
Self::EthersDeriveEip712 => "ethers-core/ethers-derive-eip712",
|
||||
_ => self.crate_name(),
|
||||
}
|
||||
}
|
||||
|
||||
/// `<ethers_*>`
|
||||
#[inline]
|
||||
pub fn path_names() -> impl Iterator<Item = (Self, &'static str)> {
|
||||
Self::iter().map(|x| (x, x.path_name()))
|
||||
}
|
||||
|
||||
/// `<ethers::*>`
|
||||
#[inline]
|
||||
pub fn ethers_path_names() -> impl Iterator<Item = (Self, &'static str)> {
|
||||
Self::iter().map(|x| (x, x.ethers_path_name()))
|
||||
}
|
||||
|
||||
/// Returns the [`Path`][syn::Path] in the current project.
|
||||
#[inline]
|
||||
pub fn get_path(&self) -> syn::Path {
|
||||
let name = ETHERS_CRATE_NAMES[self];
|
||||
syn::parse_str(name).unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
/// `path.file_stem() == s`
|
||||
#[inline]
|
||||
fn file_stem_eq<T: AsRef<Path>, U: AsRef<str>>(path: T, s: U) -> bool {
|
||||
if let Some(stem) = path.as_ref().file_stem() {
|
||||
if let Some(stem) = stem.to_str() {
|
||||
return stem == s.as_ref()
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use rand::{
|
||||
distributions::{Distribution, Standard},
|
||||
thread_rng, Rng,
|
||||
};
|
||||
use std::{
|
||||
collections::{BTreeMap, HashSet},
|
||||
env, fs,
|
||||
};
|
||||
use tempfile::TempDir;
|
||||
|
||||
impl Distribution<EthersCrate> for Standard {
|
||||
fn sample<R: Rng + ?Sized>(&self, rng: &mut R) -> EthersCrate {
|
||||
const RANGE: std::ops::Range<u8> = 0..EthersCrate::COUNT as u8;
|
||||
// SAFETY: generates in the safe range
|
||||
unsafe { std::mem::transmute(rng.gen_range(RANGE)) }
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_names() {
|
||||
fn assert_names(s: &ProjectEnvironment, ethers: bool, dependencies: &[EthersCrate]) {
|
||||
write_manifest(s, ethers, dependencies);
|
||||
|
||||
// speeds up consecutive runs by not having to re-create and delete the lockfile
|
||||
// this is tested separately: test_lock_file
|
||||
std::fs::write(s.manifest_dir.join("Cargo.lock"), "").unwrap();
|
||||
|
||||
let names = s
|
||||
.determine_ethers_crates()
|
||||
.unwrap_or_else(|| EthersCrate::ethers_path_names().collect());
|
||||
|
||||
let krate = s.crate_name.as_ref().and_then(|x| x.parse::<EthersCrate>().ok());
|
||||
let is_internal = krate.is_some();
|
||||
let mut expected: CrateNames = match (is_internal, ethers) {
|
||||
// internal
|
||||
(true, _) => EthersCrate::path_names().collect(),
|
||||
|
||||
// ethers
|
||||
(_, true) => EthersCrate::ethers_path_names().collect(),
|
||||
|
||||
// no ethers
|
||||
(_, false) => {
|
||||
let mut n: CrateNames = EthersCrate::ethers_path_names().collect();
|
||||
for &dep in dependencies {
|
||||
n.insert(dep, dep.path_name());
|
||||
}
|
||||
n
|
||||
}
|
||||
};
|
||||
|
||||
if is_internal {
|
||||
expected.insert(krate.unwrap(), "crate");
|
||||
}
|
||||
|
||||
// don't use assert for a better custom message
|
||||
if names != expected {
|
||||
// BTreeMap sorts the keys
|
||||
let names: BTreeMap<_, _> = names.into_iter().collect();
|
||||
let expected: BTreeMap<_, _> = expected.into_iter().collect();
|
||||
panic!("\nCase failed: (`{:?}`, `{ethers}`, `{dependencies:?}`)\nNames: {names:#?}\nExpected: {expected:#?}\n", s.crate_name);
|
||||
}
|
||||
}
|
||||
|
||||
fn gen_unique<const N: usize>() -> [EthersCrate; N] {
|
||||
assert!(N < EthersCrate::COUNT);
|
||||
let rng = &mut thread_rng();
|
||||
let mut set = HashSet::with_capacity(N);
|
||||
while set.len() < N {
|
||||
set.insert(rng.gen());
|
||||
}
|
||||
let vec: Vec<_> = set.into_iter().collect();
|
||||
vec.try_into().unwrap()
|
||||
}
|
||||
|
||||
let (s, _dir) = test_project();
|
||||
// crate_name -> represents an external crate
|
||||
// "ethers-contract" -> represents an internal crate
|
||||
for name in [s.crate_name.as_ref().unwrap(), "ethers-contract"] {
|
||||
let s = ProjectEnvironment::new(&s.manifest_dir, name);
|
||||
// only ethers
|
||||
assert_names(&s, true, &[]);
|
||||
|
||||
// only others
|
||||
assert_names(&s, false, gen_unique::<3>().as_slice());
|
||||
|
||||
// ethers and others
|
||||
assert_names(&s, true, gen_unique::<3>().as_slice());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_lock_file() {
|
||||
let (s, _dir) = test_project();
|
||||
write_manifest(&s, true, &[]);
|
||||
let lock_file = s.manifest_dir.join("Cargo.lock");
|
||||
|
||||
assert!(!lock_file.exists());
|
||||
s.determine_ethers_crates();
|
||||
assert!(!lock_file.exists());
|
||||
|
||||
std::fs::write(&lock_file, "").unwrap();
|
||||
|
||||
assert!(lock_file.exists());
|
||||
s.determine_ethers_crates();
|
||||
assert!(lock_file.exists());
|
||||
assert!(!std::fs::read(lock_file).unwrap().is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_is_crate_root() {
|
||||
let (s, _dir) = test_project();
|
||||
assert!(s.is_crate_root());
|
||||
|
||||
// `CARGO_MANIFEST_DIR`
|
||||
// complex path has `/{dir_name}/` in the path
|
||||
// name or path validity not checked
|
||||
let s = ProjectEnvironment::new(
|
||||
s.manifest_dir.join("examples/complex_examples"),
|
||||
"complex-examples",
|
||||
);
|
||||
assert!(!s.is_crate_root());
|
||||
let s = ProjectEnvironment::new(
|
||||
s.manifest_dir.join("benches/complex_benches"),
|
||||
"complex-benches",
|
||||
);
|
||||
assert!(!s.is_crate_root());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_is_crate_name_in_dirs() {
|
||||
let (s, _dir) = test_project();
|
||||
let root = &s.manifest_dir;
|
||||
|
||||
for dir_name in DIRS {
|
||||
for ty in ["simple", "complex"] {
|
||||
let s = ProjectEnvironment::new(root, format!("{ty}_{dir_name}"));
|
||||
assert!(s.is_crate_name_in_dirs(), "{s:?}");
|
||||
}
|
||||
}
|
||||
|
||||
let s = ProjectEnvironment::new(root, "non_existant");
|
||||
assert!(!s.is_crate_name_in_dirs());
|
||||
let s = ProjectEnvironment::new(root.join("does-not-exist"), "foo_bar");
|
||||
assert!(!s.is_crate_name_in_dirs());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_file_stem_eq() {
|
||||
let path = Path::new("/tmp/foo.rs");
|
||||
assert!(file_stem_eq(path, "foo"));
|
||||
assert!(!file_stem_eq(path, "tmp"));
|
||||
assert!(!file_stem_eq(path, "foo.rs"));
|
||||
assert!(!file_stem_eq(path, "fo"));
|
||||
assert!(!file_stem_eq(path, "f"));
|
||||
assert!(!file_stem_eq(path, ""));
|
||||
|
||||
let path = Path::new("/tmp/foo/");
|
||||
assert!(file_stem_eq(path, "foo"));
|
||||
assert!(!file_stem_eq(path, "tmp"));
|
||||
assert!(!file_stem_eq(path, "fo"));
|
||||
assert!(!file_stem_eq(path, "f"));
|
||||
assert!(!file_stem_eq(path, ""));
|
||||
}
|
||||
|
||||
// utils
|
||||
|
||||
/// Creates:
|
||||
///
|
||||
/// ```text
|
||||
/// - new_dir
|
||||
/// - src
|
||||
/// - main.rs
|
||||
/// - {dir_name} for dir_name in DIRS
|
||||
/// - simple_{dir_name}.rs
|
||||
/// - complex_{dir_name}
|
||||
/// - src if not "tests"
|
||||
/// - main.rs
|
||||
/// - module.rs
|
||||
/// ```
|
||||
fn test_project() -> (ProjectEnvironment, TempDir) {
|
||||
// change the prefix to one without the default `.` because it is not a valid crate name
|
||||
let dir = tempfile::Builder::new().prefix("tmp").tempdir().unwrap();
|
||||
let root = dir.path();
|
||||
let name = root.file_name().unwrap().to_str().unwrap();
|
||||
|
||||
// No Cargo.toml, git
|
||||
fs::create_dir_all(root).unwrap();
|
||||
let src = root.join("src");
|
||||
fs::create_dir(&src).unwrap();
|
||||
fs::write(src.join("main.rs"), "fn main(){}").unwrap();
|
||||
|
||||
for dir_name in DIRS {
|
||||
let new_dir = root.join(dir_name);
|
||||
fs::create_dir(&new_dir).unwrap();
|
||||
|
||||
let simple = new_dir.join(format!("simple_{dir_name}.rs"));
|
||||
fs::write(simple, "").unwrap();
|
||||
|
||||
let mut complex = new_dir.join(format!("complex_{dir_name}"));
|
||||
if dir_name != "tests" {
|
||||
fs::create_dir(&complex).unwrap();
|
||||
fs::write(complex.join("Cargo.toml"), "").unwrap();
|
||||
complex.push("src");
|
||||
}
|
||||
fs::create_dir(&complex).unwrap();
|
||||
fs::write(complex.join("main.rs"), "").unwrap();
|
||||
fs::write(complex.join("module.rs"), "").unwrap();
|
||||
}
|
||||
|
||||
// create target dirs
|
||||
let target = root.join("target");
|
||||
fs::create_dir(&target).unwrap();
|
||||
fs::create_dir_all(target.join("tmp")).unwrap();
|
||||
|
||||
(ProjectEnvironment::new(root, name), dir)
|
||||
}
|
||||
|
||||
/// Writes a test manifest to `{root}/Cargo.toml`.
|
||||
fn write_manifest(s: &ProjectEnvironment, ethers: bool, dependencies: &[EthersCrate]) {
|
||||
// use paths to avoid downloading dependencies
|
||||
const ETHERS_CORE: &str = env!("CARGO_MANIFEST_DIR");
|
||||
let ethers_root = Path::new(ETHERS_CORE).parent().unwrap();
|
||||
let mut dependencies_toml =
|
||||
String::with_capacity(150 * (ethers as usize + dependencies.len()));
|
||||
|
||||
if ethers {
|
||||
let ethers = format!("ethers = {{ path = {ethers_root:?} }}\n");
|
||||
dependencies_toml.push_str(ðers);
|
||||
}
|
||||
|
||||
for dep in dependencies.iter() {
|
||||
let path = ethers_root.join(dep.fs_path());
|
||||
let dep = format!("{dep} = {{ path = {path:?} }}\n");
|
||||
dependencies_toml.push_str(&dep);
|
||||
}
|
||||
|
||||
let contents = format!(
|
||||
r#"
|
||||
[package]
|
||||
name = "{}"
|
||||
version = "0.0.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
{dependencies_toml}
|
||||
"#,
|
||||
s.crate_name.as_ref().unwrap()
|
||||
);
|
||||
fs::write(s.manifest_dir.join("Cargo.toml"), contents).unwrap();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,2 +1,2 @@
|
|||
mod ethers_crate;
|
||||
pub use ethers_crate::{ethers_contract_crate, ethers_core_crate, ethers_providers_crate};
|
||||
pub use ethers_crate::*;
|
||||
|
|
Loading…
Reference in New Issue