From 4c677933ce4407fb1307f9077aefc31b9e811cdb Mon Sep 17 00:00:00 2001 From: Matthias Seitz Date: Mon, 13 Dec 2021 00:39:28 +0100 Subject: [PATCH] feat(solc): add workspace utils (#678) * feat(solc): add workspace mocking * test: add more tests * test: update tests * rename to project-utils * add fs extra error * clean up functions * simplify path construction --- Cargo.lock | 7 ++ ethers-solc/Cargo.toml | 11 ++- ethers-solc/src/artifacts.rs | 1 + ethers-solc/src/config.rs | 40 ++++++-- ethers-solc/src/error.rs | 4 + ethers-solc/src/lib.rs | 34 +++++-- ethers-solc/src/project_util.rs | 161 ++++++++++++++++++++++++++++++++ ethers-solc/tests/project.rs | 42 +++------ 8 files changed, 251 insertions(+), 49 deletions(-) create mode 100644 ethers-solc/src/project_util.rs diff --git a/Cargo.lock b/Cargo.lock index a2dede5b..7b8043fd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1343,6 +1343,7 @@ dependencies = [ "colored", "criterion", "ethers-core", + "fs_extra", "futures-util", "getrandom 0.2.3", "glob", @@ -1441,6 +1442,12 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "fs_extra" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2022715d62ab30faffd124d40b76f4134a550a87792276512b18d63272333394" + [[package]] name = "fuchsia-cprng" version = "0.1.1" diff --git a/ethers-solc/Cargo.toml b/ethers-solc/Cargo.toml index 27d63109..eb13c035 100644 --- a/ethers-solc/Cargo.toml +++ b/ethers-solc/Cargo.toml @@ -32,6 +32,8 @@ glob = "0.3.0" tracing = "0.1.29" num_cpus = "1.13.0" tiny-keccak = { version = "2.0.2", default-features = false } +tempdir = { version = "0.3.7", optional = true } +fs_extra = { version = "1.2.0", optional = true } [target.'cfg(any(not(any(target_arch = "x86", target_arch = "x86_64")), target_env = "msvc"))'.dependencies] sha2 = { version = "0.9.8", default-features = false } @@ -48,14 +50,21 @@ getrandom = { version = "0.2", features = ["js"] } [dev-dependencies] criterion = { version = "0.3", features = ["async_tokio"] } -tokio = { version = "1.12.0", features = ["full"] } tempdir = "0.3.7" +tokio = { version = "1.12.0", features = ["full"] } [[bench]] name = "compile_many" harness = false +[[test]] +name = "project" +path = "tests/project.rs" +required-features = ["project-util"] + [features] async = ["tokio", "futures-util"] full = ["async", "svm"] +# Utilities for creating and testing project workspaces +project-util = ["tempdir", "fs_extra"] tests = [] diff --git a/ethers-solc/src/artifacts.rs b/ethers-solc/src/artifacts.rs index ddf57d56..20719dcc 100644 --- a/ethers-solc/src/artifacts.rs +++ b/ethers-solc/src/artifacts.rs @@ -523,6 +523,7 @@ pub struct OutputDiagnostics<'a> { } impl<'a> OutputDiagnostics<'a> { + /// Returns true if there is at least one error of high severity pub fn has_error(&self) -> bool { self.errors.iter().any(|err| err.severity.is_error()) } diff --git a/ethers-solc/src/config.rs b/ethers-solc/src/config.rs index 6e1891fa..7332529e 100644 --- a/ethers-solc/src/config.rs +++ b/ethers-solc/src/config.rs @@ -58,6 +58,21 @@ impl ProjectPathsConfig { pub fn current_dapptools() -> Result { Self::dapptools(std::env::current_dir().map_err(|err| SolcError::io(err, "."))?) } + + /// Creates all configured dirs and files + pub fn create_all(&self) -> std::result::Result<(), SolcIoError> { + if let Some(parent) = self.cache.parent() { + fs::create_dir_all(parent).map_err(|err| SolcIoError::new(err, parent))?; + } + fs::create_dir_all(&self.artifacts) + .map_err(|err| SolcIoError::new(err, &self.artifacts))?; + fs::create_dir_all(&self.sources).map_err(|err| SolcIoError::new(err, &self.sources))?; + fs::create_dir_all(&self.tests).map_err(|err| SolcIoError::new(err, &self.tests))?; + for lib in &self.libraries { + fs::create_dir_all(lib).map_err(|err| SolcIoError::new(err, lib))?; + } + Ok(()) + } } #[derive(Debug, Clone, Eq, PartialEq)] @@ -158,15 +173,9 @@ impl ProjectPathsConfigBuilder { self } - pub fn build(self) -> std::result::Result { - let root = self - .root - .map(Ok) - .unwrap_or_else(std::env::current_dir) - .map_err(|err| SolcIoError::new(err, "."))?; - let root = std::fs::canonicalize(&root).map_err(|err| SolcIoError::new(err, root))?; - - Ok(ProjectPathsConfig { + pub fn build_with_root(self, root: impl Into) -> ProjectPathsConfig { + let root = root.into(); + ProjectPathsConfig { cache: self .cache .unwrap_or_else(|| root.join("cache").join(SOLIDITY_FILES_CACHE_FILENAME)), @@ -176,7 +185,18 @@ impl ProjectPathsConfigBuilder { libraries: self.libraries.unwrap_or_default(), remappings: self.remappings.unwrap_or_default(), root, - }) + } + } + + pub fn build(self) -> std::result::Result { + let root = self + .root + .clone() + .map(Ok) + .unwrap_or_else(std::env::current_dir) + .map_err(|err| SolcIoError::new(err, "."))?; + let root = std::fs::canonicalize(&root).map_err(|err| SolcIoError::new(err, &root))?; + Ok(self.build_with_root(root)) } } diff --git a/ethers-solc/src/error.rs b/ethers-solc/src/error.rs index 35299112..943c7644 100644 --- a/ethers-solc/src/error.rs +++ b/ethers-solc/src/error.rs @@ -33,6 +33,10 @@ pub enum SolcError { /// General purpose message #[error("{0}")] Message(String), + + #[cfg(feature = "project-util")] + #[error(transparent)] + FsExtra(#[from] fs_extra::error::Error), } impl SolcError { diff --git a/ethers-solc/src/lib.rs b/ethers-solc/src/lib.rs index 1d0afc21..21b0edcd 100644 --- a/ethers-solc/src/lib.rs +++ b/ethers-solc/src/lib.rs @@ -7,6 +7,7 @@ use std::collections::btree_map::Entry; pub mod cache; pub mod hh; +pub use hh::{HardhatArtifact, HardhatArtifacts}; mod compile; @@ -37,6 +38,10 @@ use std::{ path::PathBuf, }; +/// Utilities for creating, mocking and testing of (temporary) projects +#[cfg(feature = "project-util")] +pub mod project_util; + /// Represents a project workspace and handles `solc` compiling of all contracts in that workspace. #[derive(Debug)] pub struct Project { @@ -93,6 +98,21 @@ impl Project { } impl Project { + /// Returns the path to the artifacts directory + pub fn artifacts_path(&self) -> &PathBuf { + &self.paths.artifacts + } + + /// Returns the path to the sources directory + pub fn sources_path(&self) -> &PathBuf { + &self.paths.sources + } + + /// Returns the path to the cache file + pub fn cache_path(&self) -> &PathBuf { + &self.paths.cache + } + /// Sets the maximum number of parallel `solc` processes to run simultaneously. pub fn set_solc_jobs(&mut self, jobs: usize) { assert!(jobs > 0); @@ -468,15 +488,15 @@ impl Project { /// Removes the project's artifacts and cache file pub fn cleanup(&self) -> std::result::Result<(), SolcIoError> { tracing::trace!("clean up project"); - if self.paths.cache.exists() { - std::fs::remove_file(&self.paths.cache) - .map_err(|err| SolcIoError::new(err, self.paths.cache.clone()))?; - tracing::trace!("removed cache file \"{}\"", self.paths.cache.display()); + if self.cache_path().exists() { + std::fs::remove_file(self.cache_path()) + .map_err(|err| SolcIoError::new(err, self.cache_path()))?; + tracing::trace!("removed cache file \"{}\"", self.cache_path().display()); } if self.paths.artifacts.exists() { - std::fs::remove_dir_all(&self.paths.artifacts) - .map_err(|err| SolcIoError::new(err, self.paths.artifacts.clone()))?; - tracing::trace!("removed artifacts dir \"{}\"", self.paths.artifacts.display()); + std::fs::remove_dir_all(self.artifacts_path()) + .map_err(|err| SolcIoError::new(err, self.artifacts_path().clone()))?; + tracing::trace!("removed artifacts dir \"{}\"", self.artifacts_path().display()); } Ok(()) } diff --git a/ethers-solc/src/project_util.rs b/ethers-solc/src/project_util.rs new file mode 100644 index 00000000..ab76cc2d --- /dev/null +++ b/ethers-solc/src/project_util.rs @@ -0,0 +1,161 @@ +//! Utilities for mocking project workspaces +use crate::{ + config::ProjectPathsConfigBuilder, + error::{Result, SolcError}, + hh::HardhatArtifacts, + ArtifactOutput, MinimalCombinedArtifacts, Project, ProjectCompileOutput, ProjectPathsConfig, + SolcIoError, +}; +use fs_extra::{dir, file}; +use std::path::Path; +use tempdir::TempDir; + +pub struct TempProject { + /// temporary workspace root + _root: TempDir, + /// actual project workspace with the `root` tempdir as its root + inner: Project, +} + +impl TempProject { + /// Makes sure all resources are created + fn create_new(root: TempDir, inner: Project) -> std::result::Result { + let project = Self { _root: root, inner }; + project.paths().create_all()?; + Ok(project) + } + + pub fn new(paths: ProjectPathsConfigBuilder) -> Result { + let tmp_dir = TempDir::new("root").map_err(|err| SolcError::io(err, "root"))?; + let paths = paths.build_with_root(tmp_dir.path()); + let inner = Project::builder().artifacts().paths(paths).build()?; + Ok(Self::create_new(tmp_dir, inner)?) + } + + pub fn project(&self) -> &Project { + &self.inner + } + + pub fn compile(&self) -> Result> { + self.project().compile() + } + + pub fn project_mut(&mut self) -> &mut Project { + &mut self.inner + } + + /// The configured paths of the project + pub fn paths(&self) -> &ProjectPathsConfig { + &self.project().paths + } + + /// The root path of the temporary workspace + pub fn root(&self) -> &Path { + self.project().paths.root.as_path() + } + + /// Copies a single file into the projects source + pub fn copy_source(&self, source: impl AsRef) -> Result<()> { + copy_file(source, &self.paths().sources) + } + + pub fn copy_sources(&self, sources: I) -> Result<()> + where + I: IntoIterator, + S: AsRef, + { + for path in sources { + self.copy_source(path)?; + } + Ok(()) + } + + /// Copies a single file into the project's main library directory + pub fn copy_lib(&self, lib: impl AsRef) -> Result<()> { + let lib_dir = self + .paths() + .libraries + .get(0) + .ok_or_else(|| SolcError::msg("No libraries folders configured"))?; + copy_file(lib, lib_dir) + } + + /// Copy a series of files into the main library dir + pub fn copy_libs(&self, libs: I) -> Result<()> + where + I: IntoIterator, + S: AsRef, + { + for path in libs { + self.copy_lib(path)?; + } + Ok(()) + } +} + +impl TempProject { + /// Creates an empty new hardhat style workspace in a new temporary dir + pub fn hardhat() -> Result { + let tmp_dir = TempDir::new("tmp_hh").map_err(|err| SolcError::io(err, "tmp_hh"))?; + + let paths = ProjectPathsConfig::hardhat(tmp_dir.path())?; + + let inner = Project::builder().artifacts().paths(paths).build()?; + Ok(Self::create_new(tmp_dir, inner)?) + } +} + +impl TempProject { + /// Creates an empty new dapptools style workspace in a new temporary dir + pub fn dapptools() -> Result { + let tmp_dir = TempDir::new("tmp_dapp").map_err(|err| SolcError::io(err, "temp_dapp"))?; + let paths = ProjectPathsConfig::dapptools(tmp_dir.path())?; + + let inner = Project::builder().artifacts().paths(paths).build()?; + Ok(Self::create_new(tmp_dir, inner)?) + } +} + +impl AsRef> for TempProject { + fn as_ref(&self) -> &Project { + self.project() + } +} + +fn dir_copy_options() -> dir::CopyOptions { + dir::CopyOptions { + overwrite: true, + skip_exist: false, + buffer_size: 64000, //64kb + copy_inside: true, + content_only: false, + depth: 0, + } +} + +fn file_copy_options() -> file::CopyOptions { + file::CopyOptions { + overwrite: true, + skip_exist: false, + buffer_size: 64000, //64kb + } +} + +/// Copies a single file into the given dir +pub fn copy_file(source: impl AsRef, target_dir: impl AsRef) -> Result<()> { + let source = source.as_ref(); + let target = target_dir.as_ref().join( + source + .file_name() + .ok_or_else(|| SolcError::msg(format!("No file name for {}", source.display())))?, + ); + + fs_extra::file::copy(source, target, &file_copy_options())?; + Ok(()) +} + +/// Copies all content of the source dir into the target dir +pub fn copy_dir(source: impl AsRef, target_dir: impl AsRef) -> Result<()> { + fs_extra::dir::copy(source, target_dir, &dir_copy_options())?; + Ok(()) +} diff --git a/ethers-solc/tests/project.rs b/ethers-solc/tests/project.rs index 3157b66a..0073b388 100644 --- a/ethers-solc/tests/project.rs +++ b/ethers-solc/tests/project.rs @@ -1,6 +1,9 @@ //! project tests -use ethers_solc::{cache::SOLIDITY_FILES_CACHE_FILENAME, Project, ProjectPathsConfig}; +use ethers_solc::{ + cache::SOLIDITY_FILES_CACHE_FILENAME, project_util::*, MinimalCombinedArtifacts, Project, + ProjectPathsConfig, +}; use std::{ io, path::{Path, PathBuf}, @@ -9,23 +12,12 @@ use tempdir::TempDir; #[test] fn can_compile_hardhat_sample() { - let tmp_dir = TempDir::new("root").unwrap(); - let cache = tmp_dir.path().join("cache"); - let cache = cache.join(SOLIDITY_FILES_CACHE_FILENAME); - let artifacts = tmp_dir.path().join("artifacts"); - let root = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("test-data/hardhat-sample"); let paths = ProjectPathsConfig::builder() - .cache(cache) .sources(root.join("contracts")) - .artifacts(artifacts) - .lib(root.join("node_modules")) - .root(root) - .build() - .unwrap(); - // let paths = ProjectPathsConfig::hardhat(root).unwrap(); + .lib(root.join("node_modules")); + let project = TempProject::::new(paths).unwrap(); - let project = Project::builder().paths(paths).build().unwrap(); let compiled = project.compile().unwrap(); assert!(compiled.find("Greeter").is_some()); assert!(compiled.find("console").is_some()); @@ -38,7 +30,7 @@ fn can_compile_hardhat_sample() { assert!(compiled.is_unchanged()); // delete artifacts - std::fs::remove_dir_all(&project.paths.artifacts).unwrap(); + std::fs::remove_dir_all(&project.paths().artifacts).unwrap(); let compiled = project.compile().unwrap(); assert!(compiled.find("Greeter").is_some()); assert!(compiled.find("console").is_some()); @@ -47,22 +39,10 @@ fn can_compile_hardhat_sample() { #[test] fn can_compile_dapp_sample() { - let tmp_dir = TempDir::new("root").unwrap(); - let cache = tmp_dir.path().join("cache"); - let cache = cache.join(SOLIDITY_FILES_CACHE_FILENAME); - let artifacts = tmp_dir.path().join("out"); - let root = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("test-data/dapp-sample"); - let paths = ProjectPathsConfig::builder() - .cache(cache) - .sources(root.join("src")) - .artifacts(artifacts) - .lib(root.join("lib")) - .root(root) - .build() - .unwrap(); + let paths = ProjectPathsConfig::builder().sources(root.join("src")).lib(root.join("lib")); + let project = TempProject::::new(paths).unwrap(); - let project = Project::builder().paths(paths).build().unwrap(); let compiled = project.compile().unwrap(); assert!(compiled.find("Dapp").is_some()); assert!(!compiled.has_compiler_errors()); @@ -73,7 +53,7 @@ fn can_compile_dapp_sample() { assert!(compiled.is_unchanged()); // delete artifacts - std::fs::remove_dir_all(&project.paths.artifacts).unwrap(); + std::fs::remove_dir_all(&project.paths().artifacts).unwrap(); let compiled = project.compile().unwrap(); assert!(compiled.find("Dapp").is_some()); assert!(!compiled.is_unchanged()); @@ -111,7 +91,7 @@ fn can_compile_dapp_sample_with_cache() { assert!(compiled.is_unchanged()); // deleted artifacts cause recompile even with cache - std::fs::remove_dir_all(&project.paths.artifacts).unwrap(); + std::fs::remove_dir_all(&project.artifacts_path()).unwrap(); let compiled = project.compile().unwrap(); assert!(compiled.find("Dapp").is_some()); assert!(!compiled.is_unchanged());