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
This commit is contained in:
Matthias Seitz 2021-12-13 00:39:28 +01:00 committed by GitHub
parent 5dec757493
commit 4c677933ce
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 251 additions and 49 deletions

7
Cargo.lock generated
View File

@ -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"

View File

@ -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 = []

View File

@ -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())
}

View File

@ -58,6 +58,21 @@ impl ProjectPathsConfig {
pub fn current_dapptools() -> Result<Self> {
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<ProjectPathsConfig, SolcIoError> {
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<PathBuf>) -> 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<ProjectPathsConfig, SolcIoError> {
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))
}
}

View File

@ -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 {

View File

@ -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<Artifacts: ArtifactOutput = MinimalCombinedArtifacts> {
@ -93,6 +98,21 @@ impl Project {
}
impl<Artifacts: ArtifactOutput> Project<Artifacts> {
/// 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<Artifacts: ArtifactOutput> Project<Artifacts> {
/// 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(())
}

View File

@ -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<T: ArtifactOutput> {
/// temporary workspace root
_root: TempDir,
/// actual project workspace with the `root` tempdir as its root
inner: Project<T>,
}
impl<T: ArtifactOutput> TempProject<T> {
/// Makes sure all resources are created
fn create_new(root: TempDir, inner: Project<T>) -> std::result::Result<Self, SolcIoError> {
let project = Self { _root: root, inner };
project.paths().create_all()?;
Ok(project)
}
pub fn new(paths: ProjectPathsConfigBuilder) -> Result<Self> {
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<T> {
&self.inner
}
pub fn compile(&self) -> Result<ProjectCompileOutput<T>> {
self.project().compile()
}
pub fn project_mut(&mut self) -> &mut Project<T> {
&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<Path>) -> Result<()> {
copy_file(source, &self.paths().sources)
}
pub fn copy_sources<I, S>(&self, sources: I) -> Result<()>
where
I: IntoIterator<Item = S>,
S: AsRef<Path>,
{
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<Path>) -> 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<I, S>(&self, libs: I) -> Result<()>
where
I: IntoIterator<Item = S>,
S: AsRef<Path>,
{
for path in libs {
self.copy_lib(path)?;
}
Ok(())
}
}
impl TempProject<HardhatArtifacts> {
/// Creates an empty new hardhat style workspace in a new temporary dir
pub fn hardhat() -> Result<Self> {
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<MinimalCombinedArtifacts> {
/// Creates an empty new dapptools style workspace in a new temporary dir
pub fn dapptools() -> Result<Self> {
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<T: ArtifactOutput> AsRef<Project<T>> for TempProject<T> {
fn as_ref(&self) -> &Project<T> {
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<Path>, target_dir: impl AsRef<Path>) -> 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<Path>, target_dir: impl AsRef<Path>) -> Result<()> {
fs_extra::dir::copy(source, target_dir, &dir_copy_options())?;
Ok(())
}

View File

@ -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::<MinimalCombinedArtifacts>::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::<MinimalCombinedArtifacts>::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());