Repository: gakonst/ethers-rs
Pull request #1011 (2022-03-11 17:32:56)

feat(solc): add mock project generator
<!--
Thank you for your Pull Request. Please provide a description above and review
the requirements below.

Bug fixes and new features should include tests.

Contributors guide: https://github.com/gakonst/ethers-rs/blob/master/CONTRIBUTING.md

The contributors guide includes instructions for running rustfmt and building the
documentation.
-->

## Motivation
Setting up mock projects is useful to test advanced features
<!--
Explain the context and why you're making that change. What is the problem
you're trying to solve? In some cases there is not a problem and this can be
thought of as being the motivation for your change.
-->

## Solution
Adds a generator that based on some settings can create a basic project skeleton.

This can be used to populate a temp directory with a bunch of source contracts and libraries that (`contract A {}`).

Will be used by follow up PRs that test optimized compile #1009
<!--
Summarize the solution and provide any necessary context needed to understand
the code change.
-->

## PR Checklist

- [x] Added Tests
- [x] Added Documentation
- [ ] Updated the changelog


mattsse: Allow unused macros, they might be useful later on

Patch:
diff --git a/ethers-solc/Cargo.toml b/ethers-solc/Cargo.toml
index 26b14ca7..26e03d0f 100644
--- a/ethers-solc/Cargo.toml
+++ b/ethers-solc/Cargo.toml
@@ -37,6 +37,7 @@ sha2 = { version = "0.9.8", default-features = false }
 dunce = "1.0.2"
 solang-parser = { default-features = false, version = "0.1.10" }
 rayon = "1.5.1"
+rand = { version = "0.8.5", optional = true }
 
 [target.'cfg(not(target_arch = "wasm32"))'.dependencies]
 home = "0.5.3"
@@ -70,12 +71,17 @@ name = "project"
 path = "tests/project.rs"
 required-features = ["async", "svm", "project-util"]
 
+[[test]]
+name = "mocked"
+path = "tests/mocked.rs"
+required-features = ["async", "svm", "project-util"]
+
 [features]
 default = ["rustls"]
 async = ["tokio", "futures-util"]
 full = ["async", "svm", "svm/blocking"]
 # Utilities for creating and testing project workspaces
-project-util = ["tempfile", "fs_extra"]
+project-util = ["tempfile", "fs_extra", "rand"]
 tests = []
 openssl = ["svm/openssl"]
 rustls = ["svm/rustls"]
diff --git a/ethers-solc/src/error.rs b/ethers-solc/src/error.rs
index 2d6751a5..8271cc81 100644
--- a/ethers-solc/src/error.rs
+++ b/ethers-solc/src/error.rs
@@ -57,6 +57,20 @@ impl SolcError {
     }
 }
 
+macro_rules! _format_err {
+    ($($tt:tt)*) => {
+        $crate::error::SolcError::msg(format!($($tt)*))
+    };
+}
+#[allow(unused)]
+pub(crate) use _format_err as format_err;
+
+macro_rules! _bail {
+    ($($tt:tt)*) => { return Err($crate::error::format_err!($($tt)*)) };
+}
+#[allow(unused)]
+pub(crate) use _bail as bail;
+
 #[derive(Debug, Error)]
 #[error("\"{}\": {io}", self.path.display())]
 pub struct SolcIoError {
diff --git a/ethers-solc/src/project_util/mock.rs b/ethers-solc/src/project_util/mock.rs
new file mode 100644
index 00000000..07fee0ea
--- /dev/null
+++ b/ethers-solc/src/project_util/mock.rs
@@ -0,0 +1,397 @@
+//! Helpers to generate mock projects
+
+use crate::{error::Result, remappings::Remapping, ProjectPathsConfig};
+use rand::{self, seq::SliceRandom, Rng};
+use serde::{Deserialize, Serialize};
+use std::{collections::BTreeSet, path::Path};
+
+/// Represents a virtual project
+#[derive(Serialize)]
+pub struct MockProjectGenerator {
+    /// how to name things
+    #[serde(skip)]
+    name_strategy: Box<dyn NamingStrategy + 'static>,
+    /// id counter for a file
+    next_file_id: usize,
+    /// id counter for a file
+    next_lib_id: usize,
+    /// all files for the project
+    files: Vec<MockFile>,
+    /// all libraries
+    libraries: Vec<MockLib>,
+}
+
+impl Default for MockProjectGenerator {
+    fn default() -> Self {
+        Self {
+            name_strategy: Box::new(SimpleNamingStrategy::default()),
+            next_file_id: 0,
+            next_lib_id: 0,
+            files: Default::default(),
+            libraries: Default::default(),
+        }
+    }
+}
+
+impl MockProjectGenerator {
+    /// Generate all solidity files and write under the paths config
+    pub fn write_to(&self, paths: &ProjectPathsConfig, version: impl AsRef<str>) -> Result<()> {
+        let version = version.as_ref();
+        for file in self.files.iter() {
+            let mut imports = Vec::with_capacity(file.imports.len());
+
+            for import in file.imports.iter() {
+                match *import {
+                    MockImport::Internal(f) => {
+                        imports.push(format!("import \"./{}.sol\";", self.files[f].name));
+                    }
+                    MockImport::External(lib, f) => {
+                        imports.push(format!(
+                            "import \"{}/{}.sol\";",
+                            self.libraries[lib].name, self.files[f].name
+                        ));
+                    }
+                }
+            }
+
+            let content = format!(
+                r#"
+// SPDX-License-Identifier: UNLICENSED
+pragma solidity {};
+{}
+contract {} {{}}
+            "#,
+                version,
+                imports.join("\n"),
+                file.name
+            );
+
+            let mut target = if let Some(lib) = file.lib_id {
+                paths.root.join("lib").join(&self.libraries[lib].name).join("src").join(&file.name)
+            } else {
+                paths.sources.join(&file.name)
+            };
+            target.set_extension("sol");
+
+            super::create_contract_file(target, content)?;
+        }
+
+        Ok(())
+    }
+
+    /// Returns all the remappings for the project for the given root path
+    pub fn remappings_at(&self, root: &Path) -> Vec<Remapping> {
+        self.libraries
+            .iter()
+            .map(|lib| {
+                let path = root.join("lib").join(&lib.name).join("src");
+                format!("{}/={}/", lib.name, path.display()).parse().unwrap()
+            })
+            .collect()
+    }
+
+    /// Returns all the remappings for the project
+    pub fn remappings(&self) -> Vec<Remapping> {
+        self.libraries
+            .iter()
+            .map(|lib| format!("{0}/=lib/{0}/src/", lib.name).parse().unwrap())
+            .collect()
+    }
+
+    /// Create a new project and populate it using the given settings
+    pub fn new(settings: &MockProjectSettings) -> Self {
+        let mut mock = Self::default();
+        mock.populate(settings);
+        mock
+    }
+
+    /// Generates a random project with random settings
+    pub fn random() -> Self {
+        let settings = MockProjectSettings::random();
+        let mut mock = Self::default();
+        mock.populate(&settings);
+        mock
+    }
+
+    /// Adds sources and libraries and populates imports based on the settings
+    pub fn populate(&mut self, settings: &MockProjectSettings) -> &mut Self {
+        self.add_sources(settings.num_lib_files);
+        for _ in 0..settings.num_libs {
+            self.add_lib(settings.num_lib_files);
+        }
+        self.populate_imports(settings)
+    }
+
+    fn next_file_id(&mut self) -> usize {
+        let next = self.next_file_id;
+        self.next_file_id += 1;
+        next
+    }
+
+    fn next_lib_id(&mut self) -> usize {
+        let next = self.next_lib_id;
+        self.next_lib_id += 1;
+        next
+    }
+
+    /// Adds a new source file
+    pub fn add_source(&mut self) -> &mut Self {
+        let id = self.next_file_id();
+        let name = self.name_strategy.new_source_file_name(id);
+        let file = MockFile { id, name, imports: Default::default(), lib_id: None };
+        self.files.push(file);
+        self
+    }
+
+    /// Adds `num` new source files
+    pub fn add_sources(&mut self, num: usize) -> &mut Self {
+        for _ in 0..num {
+            self.add_source();
+        }
+        self
+    }
+
+    /// Adds a new lib with the number of lib files
+    pub fn add_lib(&mut self, num_files: usize) -> &mut Self {
+        let lib_id = self.next_lib_id();
+        let lib_name = self.name_strategy.new_lib_name(lib_id);
+        let offset = self.files.len();
+        for _ in 0..num_files {
+            let id = self.next_file_id();
+            let name = self.name_strategy.new_lib_file_name(id);
+            self.files.push(MockFile {
+                id,
+                name,
+                imports: Default::default(),
+                lib_id: Some(lib_id),
+            });
+        }
+        self.libraries.push(MockLib { name: lib_name, id: lib_id, num_files, offset });
+        self
+    }
+
+    /// Populates the imports of the project
+    pub fn populate_imports(&mut self, settings: &MockProjectSettings) -> &mut Self {
+        let mut rng = rand::thread_rng();
+
+        // populate imports
+        for id in 0..self.files.len() {
+            let imports = if let Some(lib) = self.files[id].lib_id {
+                let num_imports = rng
+                    .gen_range(settings.min_imports..=settings.max_imports)
+                    .min(self.libraries[lib].num_files.saturating_sub(1));
+                self.unique_imports_for_lib(&mut rng, lib, id, num_imports)
+            } else {
+                let num_imports = rng
+                    .gen_range(settings.min_imports..=settings.max_imports)
+                    .min(self.files.len().saturating_sub(1));
+                self.unique_imports_for_source(&mut rng, id, num_imports)
+            };
+
+            self.files[id].imports = imports;
+        }
+        self
+    }
+
+    fn get_import(&self, id: usize) -> MockImport {
+        if let Some(lib) = self.files[id].lib_id {
+            MockImport::External(lib, id)
+        } else {
+            MockImport::Internal(id)
+        }
+    }
+
+    /// All file ids
+    pub fn file_ids(&self) -> impl Iterator<Item = usize> + '_ {
+        self.files.iter().map(|f| f.id)
+    }
+
+    /// All ids of internal files
+    pub fn internal_file_ids(&self) -> impl Iterator<Item = usize> + '_ {
+        self.files.iter().filter(|f| !f.is_external()).map(|f| f.id)
+    }
+
+    /// All ids of external files
+    pub fn external_file_ids(&self) -> impl Iterator<Item = usize> + '_ {
+        self.files.iter().filter(|f| f.is_external()).map(|f| f.id)
+    }
+
+    /// generates exactly `num` unique imports in the range of all files
+    ///
+    /// # Panics
+    ///
+    /// if `num` can't be satisfied because the range is too narrow
+    fn unique_imports_for_source<R: Rng + ?Sized>(
+        &self,
+        rng: &mut R,
+        id: usize,
+        num: usize,
+    ) -> BTreeSet<MockImport> {
+        assert!(self.files.len() > num);
+        let mut imports: Vec<_> = (0..self.files.len()).collect();
+        imports.shuffle(rng);
+        imports.into_iter().filter(|i| *i != id).map(|id| self.get_import(id)).take(num).collect()
+    }
+
+    /// generates exactly `num` unique imports in the range of a lib's files
+    ///
+    /// # Panics
+    ///
+    /// if `num` can't be satisfied because the range is too narrow
+    fn unique_imports_for_lib<R: Rng + ?Sized>(
+        &self,
+        rng: &mut R,
+        lib_id: usize,
+        id: usize,
+        num: usize,
+    ) -> BTreeSet<MockImport> {
+        let lib = &self.libraries[lib_id];
+        assert!(lib.num_files > num);
+        let mut imports: Vec<_> = (lib.offset..(lib.offset + lib.len())).collect();
+        imports.shuffle(rng);
+        imports.into_iter().filter(|i| *i != id).map(|id| self.get_import(id)).take(num).collect()
+    }
+}
+
+/// Used to determine the names for elements
+trait NamingStrategy {
+    /// Return a new name for the given source file id
+    fn new_source_file_name(&mut self, id: usize) -> String;
+
+    /// Return a new name for the given source file id
+    fn new_lib_file_name(&mut self, id: usize) -> String;
+
+    /// Return a new name for the given lib id
+    fn new_lib_name(&mut self, id: usize) -> String;
+}
+
+/// A primitive naming that simply uses ids to create unique names
+#[derive(Debug, Clone, Copy, Default)]
+pub struct SimpleNamingStrategy {
+    _priv: (),
+}
+
+impl NamingStrategy for SimpleNamingStrategy {
+    fn new_source_file_name(&mut self, id: usize) -> String {
+        format!("SourceFile{}", id)
+    }
+
+    fn new_lib_file_name(&mut self, id: usize) -> String {
+        format!("LibFile{}", id)
+    }
+
+    fn new_lib_name(&mut self, id: usize) -> String {
+        format!("Lib{}", id)
+    }
+}
+
+/// Skeleton of a mock source file
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct MockFile {
+    /// internal id of this file
+    pub id: usize,
+    /// The source name of this file
+    pub name: String,
+    /// all the imported files
+    pub imports: BTreeSet<MockImport>,
+    /// lib id if this file is part of a lib
+    pub lib_id: Option<usize>,
+}
+
+impl MockFile {
+    /// Returns `true` if this file is part of an external lib
+    pub fn is_external(&self) -> bool {
+        self.lib_id.is_some()
+    }
+}
+
+#[derive(Debug, Clone, Eq, PartialEq, Hash, Ord, PartialOrd, Serialize, Deserialize)]
+pub enum MockImport {
+    /// Import from the same project
+    Internal(usize),
+    /// external library import
+    /// (`lib id`, `file id`)
+    External(usize, usize),
+}
+
+/// Container of a mock lib
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct MockLib {
+    /// name of the lib, like `ds-test`
+    pub name: String,
+    /// internal id of this lib
+    pub id: usize,
+    /// offset in the total set of files
+    pub offset: usize,
+    /// number of files included in this lib
+    pub num_files: usize,
+}
+
+impl MockLib {
+    pub fn len(&self) -> usize {
+        self.num_files
+    }
+
+    pub fn is_empty(&self) -> bool {
+        self.len() == 0
+    }
+}
+
+/// Settings to use when generate a mock project
+#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
+pub struct MockProjectSettings {
+    /// number of source files to generate
+    pub num_sources: usize,
+    /// number of libraries to use
+    pub num_libs: usize,
+    /// how many lib files to generate per lib
+    pub num_lib_files: usize,
+    /// min amount of import statements a file can use
+    pub min_imports: usize,
+    /// max amount of import statements a file can use
+    pub max_imports: usize,
+}
+
+impl MockProjectSettings {
+    /// Generates a new instance with random settings within an arbitrary range
+    pub fn random() -> Self {
+        let mut rng = rand::thread_rng();
+        // arbitrary thresholds
+        MockProjectSettings {
+            num_sources: rng.gen_range(2..25),
+            num_libs: rng.gen_range(0..5),
+            num_lib_files: rng.gen_range(1..10),
+            min_imports: rng.gen_range(0..3),
+            max_imports: rng.gen_range(4..10),
+        }
+    }
+
+    /// Generates settings for a large project
+    pub fn large() -> Self {
+        // arbitrary thresholds
+        MockProjectSettings {
+            num_sources: 35,
+            num_libs: 4,
+            num_lib_files: 15,
+            min_imports: 3,
+            max_imports: 12,
+        }
+    }
+}
+
+impl Default for MockProjectSettings {
+    fn default() -> Self {
+        // these are arbitrary
+        Self { num_sources: 20, num_libs: 2, num_lib_files: 10, min_imports: 0, max_imports: 5 }
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    #[test]
+    fn can_generate_mock_project() {
+        let _ = MockProjectGenerator::random();
+    }
+}
diff --git a/ethers-solc/src/project_util.rs b/ethers-solc/src/project_util/mod.rs
similarity index 61%
rename from ethers-solc/src/project_util.rs
rename to ethers-solc/src/project_util/mod.rs
index 8683f264..1f7ae59c 100644
--- a/ethers-solc/src/project_util.rs
+++ b/ethers-solc/src/project_util/mod.rs
@@ -2,8 +2,9 @@
 use crate::{
     artifacts::Settings,
     config::ProjectPathsConfigBuilder,
-    error::{Result, SolcError},
+    error::{bail, Result, SolcError},
     hh::HardhatArtifacts,
+    project_util::mock::{MockProjectGenerator, MockProjectSettings},
     utils::tempdir,
     ArtifactOutput, ConfigurableArtifacts, PathStyle, Project, ProjectCompileOutput,
     ProjectPathsConfig, SolcIoError,
@@ -15,6 +16,8 @@ use std::{
 };
 use tempfile::TempDir;
 
+pub mod mock;
+
 /// A [`Project`] wrapper that lives in a new temporary directory
 ///
 /// Once `TempProject` is dropped, the temp dir is automatically removed, see [`TempDir::drop()`]
@@ -154,6 +157,27 @@ impl<T: ArtifactOutput> TempProject<T> {
         create_contract_file(lib, content)
     }
 
+    /// Adds a basic lib contract `contract <name> {}` as a new file
+    pub fn add_basic_lib(
+        &self,
+        name: impl AsRef<str>,
+        version: impl AsRef<str>,
+    ) -> Result<PathBuf> {
+        let name = name.as_ref();
+        self.add_lib(
+            name,
+            format!(
+                r#"
+// SPDX-License-Identifier: UNLICENSED
+pragma solidity {};
+contract {} {{}}
+            "#,
+                name,
+                version.as_ref()
+            ),
+        )
+    }
+
     /// Adds a new source file inside the project's source dir
     pub fn add_source(&self, name: impl AsRef<str>, content: impl AsRef<str>) -> Result<PathBuf> {
         let name = contract_file_name(name);
@@ -161,6 +185,27 @@ impl<T: ArtifactOutput> TempProject<T> {
         create_contract_file(source, content)
     }
 
+    /// Adds a basic source contract `contract <name> {}` as a new file
+    pub fn add_basic_source(
+        &self,
+        name: impl AsRef<str>,
+        version: impl AsRef<str>,
+    ) -> Result<PathBuf> {
+        let name = name.as_ref();
+        self.add_source(
+            name,
+            format!(
+                r#"
+// SPDX-License-Identifier: UNLICENSED
+pragma solidity {};
+contract {} {{}}
+            "#,
+                name,
+                version.as_ref()
+            ),
+        )
+    }
+
     /// Adds a solidity contract in the project's root dir.
     /// This will also create all intermediary dirs.
     pub fn add_contract(&self, name: impl AsRef<str>, content: impl AsRef<str>) -> Result<PathBuf> {
@@ -168,6 +213,89 @@ impl<T: ArtifactOutput> TempProject<T> {
         let source = self.root().join(name);
         create_contract_file(source, content)
     }
+
+    /// Populate the project with mock files
+    pub fn mock(&self, gen: &MockProjectGenerator, version: impl AsRef<str>) -> Result<()> {
+        gen.write_to(self.paths(), version)
+    }
+
+    /// Compiles the project and ensures that the output does not contain errors
+    pub fn ensure_no_errors(&self) -> Result<&Self> {
+        let compiled = self.compile().unwrap();
+        if compiled.has_compiler_errors() {
+            bail!("Compiled with errors {}", compiled)
+        }
+        Ok(self)
+    }
+
+    /// Compiles the project and ensures that the output is __unchanged__
+    pub fn ensure_unchanged(&self) -> Result<&Self> {
+        let compiled = self.compile().unwrap();
+        if !compiled.is_unchanged() {
+            bail!("Compiled with detected changes {}", compiled)
+        }
+        Ok(self)
+    }
+
+    /// Compiles the project and ensures that the output has __changed__
+    pub fn ensure_changed(&self) -> Result<&Self> {
+        let compiled = self.compile().unwrap();
+        if compiled.is_unchanged() {
+            bail!("Compiled without detecting changes {}", compiled)
+        }
+        Ok(self)
+    }
+
+    /// Compiles the project and ensures that the output does not contain errors and no changes
+    /// exists on recompiled.
+    ///
+    /// This is a convenience function for
+    ///
+    /// ```no_run
+    /// use ethers_solc::project_util::TempProject;
+    /// let project = TempProject::dapptools().unwrap();
+    //  project.ensure_no_errors().unwrap();
+    //  project.ensure_unchanged().unwrap();
+    /// ```
+    pub fn ensure_no_errors_recompile_unchanged(&self) -> Result<&Self> {
+        self.ensure_no_errors()?.ensure_unchanged()
+    }
+
+    /// Compiles the project and asserts that the output does not contain errors and no changes
+    /// exists on recompiled.
+    ///
+    /// This is a convenience function for
+    ///
+    /// ```no_run
+    /// use ethers_solc::project_util::TempProject;
+    /// let project = TempProject::dapptools().unwrap();
+    //  project.assert_no_errors();
+    //  project.assert_unchanged();
+    /// ```
+    pub fn assert_no_errors_recompile_unchanged(&self) -> &Self {
+        self.assert_no_errors().assert_unchanged()
+    }
+
+    /// Compiles the project and asserts that the output does not contain errors
+    pub fn assert_no_errors(&self) -> &Self {
+        let compiled = self.compile().unwrap();
+        assert!(!compiled.has_compiler_errors());
+        self
+    }
+
+    /// Compiles the project and asserts that the output is unchanged
+    pub fn assert_unchanged(&self) -> &Self {
+        let compiled = self.compile().unwrap();
+        assert!(compiled.is_unchanged());
+        self
+    }
+
+    /// Compiles the project and asserts that the output is _changed_
+    pub fn assert_changed(&self) -> &Self {
+        let compiled = self.compile().unwrap();
+        assert!(!compiled.is_unchanged());
+        self
+    }
 }
 
 impl<T: ArtifactOutput + Default> TempProject<T> {
@@ -197,7 +325,7 @@ impl<T: ArtifactOutput> fmt::Debug for TempProject<T> {
     }
 }
 
-fn create_contract_file(path: PathBuf, content: impl AsRef<str>) -> Result<PathBuf> {
+pub(crate) fn create_contract_file(path: PathBuf, content: impl AsRef<str>) -> Result<PathBuf> {
     if let Some(parent) = path.parent() {
         std::fs::create_dir_all(parent)
             .map_err(|err| SolcIoError::new(err, parent.to_path_buf()))?;
@@ -237,6 +365,40 @@ impl TempProject<ConfigurableArtifacts> {
         let inner = Project::builder().paths(paths).build()?;
         Ok(Self::create_new(tmp_dir, inner)?)
     }
+
+    /// Create a new temporary project and populate it with mock files
+    ///
+    /// ```no_run
+    /// use ethers_solc::project_util::mock::MockProjectSettings;
+    /// use ethers_solc::project_util::TempProject;
+    /// let tmp = TempProject::mocked(&MockProjectSettings::default(), "^0.8.10").unwrap();
+    /// ```
+    pub fn mocked(settings: &MockProjectSettings, version: impl AsRef<str>) -> Result<Self> {
+        let mut tmp = Self::dapptools()?;
+        let gen = MockProjectGenerator::new(settings);
+        tmp.mock(&gen, version)?;
+        let remappings = gen.remappings_at(tmp.root());
+        tmp.paths_mut().remappings.extend(remappings);
+        Ok(tmp)
+    }
+
+    /// Create a new temporary project and populate it with a random layout
+    ///
+    /// ```no_run
+    /// use ethers_solc::project_util::TempProject;
+    /// let tmp = TempProject::mocked_random("^0.8.10").unwrap();
+    /// ```
+    ///
+    /// This is a convenience function for:
+    ///
+    /// ```no_run
+    /// use ethers_solc::project_util::mock::MockProjectSettings;
+    /// use ethers_solc::project_util::TempProject;
+    /// let tmp = TempProject::mocked(&MockProjectSettings::random(), "^0.8.10").unwrap();
+    /// ```
+    pub fn mocked_random(version: impl AsRef<str>) -> Result<Self> {
+        Self::mocked(&MockProjectSettings::random(), version)
+    }
 }
 
 impl<T: ArtifactOutput> AsRef<Project<T>> for TempProject<T> {
@@ -284,3 +446,14 @@ pub fn copy_dir(source: impl AsRef<Path>, target_dir: impl AsRef<Path>) -> Resul
     fs_extra::dir::copy(source, target_dir, &dir_copy_options())?;
     Ok(())
 }
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    #[test]
+    fn can_mock_project() {
+        let _prj = TempProject::mocked(&Default::default(), "^0.8.11").unwrap();
+        let _prj = TempProject::mocked_random("^0.8.11").unwrap();
+    }
+}


Test patch:
diff --git a/ethers-solc/tests/mocked.rs b/ethers-solc/tests/mocked.rs
new file mode 100644
index 00000000..946ae132
--- /dev/null
+++ b/ethers-solc/tests/mocked.rs
@@ -0,0 +1,81 @@
+//! mocked project tests
+
+use ethers_solc::{
+    error::Result,
+    project_util::{
+        mock::{MockProjectGenerator, MockProjectSettings},
+        TempProject,
+    },
+};
+
+// default version to use
+const DEFAULT_VERSION: &str = "^0.8.10";
+
+struct MockSettings {
+    settings: MockProjectSettings,
+    version: &'static str,
+}
+
+impl From<MockProjectSettings> for MockSettings {
+    fn from(settings: MockProjectSettings) -> Self {
+        MockSettings { settings, version: DEFAULT_VERSION }
+    }
+}
+impl From<(MockProjectSettings, &'static str)> for MockSettings {
+    fn from(input: (MockProjectSettings, &'static str)) -> Self {
+        MockSettings { settings: input.0, version: input.1 }
+    }
+}
+
+/// Helper function to run a test and report the used generator if the closure failed.
+fn run_mock(
+    settings: impl Into<MockSettings>,
+    f: impl FnOnce(&mut TempProject) -> Result<()>,
+) -> TempProject {
+    let MockSettings { settings, version } = settings.into();
+    let gen = MockProjectGenerator::new(&settings);
+    let mut project = TempProject::dapptools().unwrap();
+    let remappings = gen.remappings_at(project.root());
+    project.paths_mut().remappings.extend(remappings);
+    project.mock(&gen, version).unwrap();
+
+    if let Err(err) = f(&mut project) {
+        panic!(
+            "mock failed: `{}` with mock settings:\n {}",
+            err,
+            serde_json::to_string(&gen).unwrap()
+        );
+    }
+
+    project
+}
+
+/// Runs a basic set of tests for the given settings
+fn run_basic(settings: impl Into<MockSettings>) {
+    let settings = settings.into();
+    let version = settings.version;
+    run_mock(settings, |project| {
+        project.ensure_no_errors_recompile_unchanged()?;
+        project.add_basic_source("Dummy", version)?;
+        project.ensure_changed()?;
+        Ok(())
+    });
+}
+
+#[test]
+fn can_compile_mocked_random() {
+    run_basic(MockProjectSettings::random());
+}
+
+// compile a bunch of random projects
+#[test]
+fn can_compile_mocked_multi() {
+    for _ in 0..10 {
+        run_basic(MockProjectSettings::random());
+    }
+}
+
+#[test]
+fn can_compile_mocked_large() {
+    run_basic(MockProjectSettings::large())
+}