ethers_solc/project_util/
mod.rs

1//! Utilities for mocking project workspaces
2use crate::{
3    artifacts::Settings,
4    config::ProjectPathsConfigBuilder,
5    error::{bail, Result, SolcError},
6    hh::HardhatArtifacts,
7    project_util::mock::{MockProjectGenerator, MockProjectSettings},
8    remappings::Remapping,
9    utils,
10    utils::tempdir,
11    Artifact, ArtifactOutput, Artifacts, ConfigurableArtifacts, ConfigurableContractArtifact,
12    FileFilter, PathStyle, Project, ProjectCompileOutput, ProjectPathsConfig, SolFilesCache,
13    SolcIoError,
14};
15use fs_extra::{dir, file};
16use std::{
17    fmt,
18    path::{Path, PathBuf},
19    process,
20    process::Command,
21};
22use tempfile::TempDir;
23
24pub mod mock;
25
26/// A [`Project`] wrapper that lives in a new temporary directory
27///
28/// Once `TempProject` is dropped, the temp dir is automatically removed, see [`TempDir::drop()`]
29pub struct TempProject<T: ArtifactOutput = ConfigurableArtifacts> {
30    /// temporary workspace root
31    _root: TempDir,
32    /// actual project workspace with the `root` tempdir as its root
33    inner: Project<T>,
34}
35
36impl<T: ArtifactOutput> TempProject<T> {
37    /// Makes sure all resources are created
38    pub fn create_new(root: TempDir, inner: Project<T>) -> std::result::Result<Self, SolcIoError> {
39        let mut project = Self { _root: root, inner };
40        project.paths().create_all()?;
41        // ignore license warnings
42        project.inner.ignored_error_codes.push(1878);
43        Ok(project)
44    }
45
46    /// Creates a new temp project using the provided paths and artifacts handler.
47    /// sets the project root to a temp dir
48    pub fn with_artifacts(paths: ProjectPathsConfigBuilder, artifacts: T) -> Result<Self> {
49        Self::prefixed_with_artifacts("temp-project", paths, artifacts)
50    }
51
52    /// Creates a new temp project inside a tempdir with a prefixed directory and the given
53    /// artifacts handler
54    pub fn prefixed_with_artifacts(
55        prefix: &str,
56        paths: ProjectPathsConfigBuilder,
57        artifacts: T,
58    ) -> Result<Self> {
59        let tmp_dir = tempdir(prefix)?;
60        let paths = paths.build_with_root(tmp_dir.path());
61        let inner = Project::builder().artifacts(artifacts).paths(paths).build()?;
62        Ok(Self::create_new(tmp_dir, inner)?)
63    }
64
65    /// Overwrites the settings to pass to `solc`
66    pub fn with_settings(mut self, settings: impl Into<Settings>) -> Self {
67        self.inner.solc_config.settings = settings.into();
68        self
69    }
70
71    /// Explicitly sets the solc version for the project
72    #[cfg(all(feature = "svm-solc", not(target_arch = "wasm32")))]
73    pub fn set_solc(&mut self, solc: impl AsRef<str>) -> &mut Self {
74        self.inner.solc = crate::Solc::find_or_install_svm_version(solc).unwrap();
75        self.inner.auto_detect = false;
76        self
77    }
78
79    pub fn project(&self) -> &Project<T> {
80        &self.inner
81    }
82
83    pub fn compile(&self) -> Result<ProjectCompileOutput<T>> {
84        self.project().compile()
85    }
86
87    pub fn compile_sparse<F: FileFilter + 'static>(
88        &self,
89        filter: F,
90    ) -> Result<ProjectCompileOutput<T>> {
91        self.project().compile_sparse(filter)
92    }
93
94    pub fn flatten(&self, target: &Path) -> Result<String> {
95        self.project().flatten(target)
96    }
97
98    pub fn project_mut(&mut self) -> &mut Project<T> {
99        &mut self.inner
100    }
101
102    /// The configured paths of the project
103    pub fn paths(&self) -> &ProjectPathsConfig {
104        &self.project().paths
105    }
106
107    /// The configured paths of the project
108    pub fn paths_mut(&mut self) -> &mut ProjectPathsConfig {
109        &mut self.project_mut().paths
110    }
111
112    /// Returns the path to the artifacts directory
113    pub fn artifacts_path(&self) -> &PathBuf {
114        &self.paths().artifacts
115    }
116
117    /// Returns the path to the sources directory
118    pub fn sources_path(&self) -> &PathBuf {
119        &self.paths().sources
120    }
121
122    /// Returns the path to the cache file
123    pub fn cache_path(&self) -> &PathBuf {
124        &self.paths().cache
125    }
126
127    /// The root path of the temporary workspace
128    pub fn root(&self) -> &Path {
129        self.project().paths.root.as_path()
130    }
131
132    /// Copies a single file into the projects source
133    pub fn copy_source(&self, source: impl AsRef<Path>) -> Result<()> {
134        copy_file(source, &self.paths().sources)
135    }
136
137    pub fn copy_sources<I, S>(&self, sources: I) -> Result<()>
138    where
139        I: IntoIterator<Item = S>,
140        S: AsRef<Path>,
141    {
142        for path in sources {
143            self.copy_source(path)?;
144        }
145        Ok(())
146    }
147
148    fn get_lib(&self) -> Result<PathBuf> {
149        self.paths()
150            .libraries
151            .first()
152            .cloned()
153            .ok_or_else(|| SolcError::msg("No libraries folders configured"))
154    }
155
156    /// Copies a single file into the project's main library directory
157    pub fn copy_lib(&self, lib: impl AsRef<Path>) -> Result<()> {
158        let lib_dir = self.get_lib()?;
159        copy_file(lib, lib_dir)
160    }
161
162    /// Copy a series of files into the main library dir
163    pub fn copy_libs<I, S>(&self, libs: I) -> Result<()>
164    where
165        I: IntoIterator<Item = S>,
166        S: AsRef<Path>,
167    {
168        for path in libs {
169            self.copy_lib(path)?;
170        }
171        Ok(())
172    }
173
174    /// Adds a new library file
175    pub fn add_lib(&self, name: impl AsRef<str>, content: impl AsRef<str>) -> Result<PathBuf> {
176        let name = contract_file_name(name);
177        let lib_dir = self.get_lib()?;
178        let lib = lib_dir.join(name);
179        create_contract_file(lib, content)
180    }
181
182    /// Adds a basic lib contract `contract <name> {}` as a new file
183    pub fn add_basic_lib(
184        &self,
185        name: impl AsRef<str>,
186        version: impl AsRef<str>,
187    ) -> Result<PathBuf> {
188        let name = name.as_ref();
189        let name = name.strip_suffix(".sol").unwrap_or(name);
190        self.add_lib(
191            name,
192            format!(
193                r#"
194// SPDX-License-Identifier: UNLICENSED
195pragma solidity {};
196contract {} {{}}
197            "#,
198                version.as_ref(),
199                name,
200            ),
201        )
202    }
203
204    /// Adds a new test file inside the project's test dir
205    pub fn add_test(&self, name: impl AsRef<str>, content: impl AsRef<str>) -> Result<PathBuf> {
206        let name = contract_file_name(name);
207        let tests = self.paths().tests.join(name);
208        create_contract_file(tests, content)
209    }
210
211    /// Adds a new script file inside the project's script dir
212    pub fn add_script(&self, name: impl AsRef<str>, content: impl AsRef<str>) -> Result<PathBuf> {
213        let name = contract_file_name(name);
214        let script = self.paths().scripts.join(name);
215        create_contract_file(script, content)
216    }
217
218    /// Adds a new source file inside the project's source dir
219    pub fn add_source(&self, name: impl AsRef<str>, content: impl AsRef<str>) -> Result<PathBuf> {
220        let name = contract_file_name(name);
221        let source = self.paths().sources.join(name);
222        create_contract_file(source, content)
223    }
224
225    /// Adds a basic source contract `contract <name> {}` as a new file
226    pub fn add_basic_source(
227        &self,
228        name: impl AsRef<str>,
229        version: impl AsRef<str>,
230    ) -> Result<PathBuf> {
231        let name = name.as_ref();
232        let name = name.strip_suffix(".sol").unwrap_or(name);
233        self.add_source(
234            name,
235            format!(
236                r#"
237// SPDX-License-Identifier: UNLICENSED
238pragma solidity {};
239contract {} {{}}
240            "#,
241                version.as_ref(),
242                name,
243            ),
244        )
245    }
246
247    /// Adds a solidity contract in the project's root dir.
248    /// This will also create all intermediary dirs.
249    pub fn add_contract(&self, name: impl AsRef<str>, content: impl AsRef<str>) -> Result<PathBuf> {
250        let name = contract_file_name(name);
251        let source = self.root().join(name);
252        create_contract_file(source, content)
253    }
254
255    /// Returns a snapshot of all cached artifacts
256    pub fn artifacts_snapshot(&self) -> Result<ArtifactsSnapshot<T::Artifact>> {
257        let cache = self.project().read_cache_file()?;
258        let artifacts = cache.read_artifacts::<T::Artifact>()?;
259        Ok(ArtifactsSnapshot { cache, artifacts })
260    }
261
262    /// Populate the project with mock files
263    pub fn mock(&self, gen: &MockProjectGenerator, version: impl AsRef<str>) -> Result<()> {
264        gen.write_to(self.paths(), version)
265    }
266
267    /// Compiles the project and ensures that the output does not contain errors
268    pub fn ensure_no_errors(&self) -> Result<&Self> {
269        let compiled = self.compile().unwrap();
270        if compiled.has_compiler_errors() {
271            bail!("Compiled with errors {}", compiled)
272        }
273        Ok(self)
274    }
275
276    /// Compiles the project and ensures that the output is __unchanged__
277    pub fn ensure_unchanged(&self) -> Result<&Self> {
278        let compiled = self.compile().unwrap();
279        if !compiled.is_unchanged() {
280            bail!("Compiled with detected changes {}", compiled)
281        }
282        Ok(self)
283    }
284
285    /// Compiles the project and ensures that the output has __changed__
286    pub fn ensure_changed(&self) -> Result<&Self> {
287        let compiled = self.compile().unwrap();
288        if compiled.is_unchanged() {
289            bail!("Compiled without detecting changes {}", compiled)
290        }
291        Ok(self)
292    }
293
294    /// Compiles the project and ensures that the output does not contain errors and no changes
295    /// exists on recompiled.
296    ///
297    /// This is a convenience function for
298    ///
299    /// ```no_run
300    /// use ethers_solc::project_util::TempProject;
301    /// let project = TempProject::dapptools().unwrap();
302    //  project.ensure_no_errors().unwrap();
303    //  project.ensure_unchanged().unwrap();
304    /// ```
305    pub fn ensure_no_errors_recompile_unchanged(&self) -> Result<&Self> {
306        self.ensure_no_errors()?.ensure_unchanged()
307    }
308
309    /// Compiles the project and asserts that the output does not contain errors and no changes
310    /// exists on recompiled.
311    ///
312    /// This is a convenience function for
313    ///
314    /// ```no_run
315    /// use ethers_solc::project_util::TempProject;
316    /// let project = TempProject::dapptools().unwrap();
317    //  project.assert_no_errors();
318    //  project.assert_unchanged();
319    /// ```
320    pub fn assert_no_errors_recompile_unchanged(&self) -> &Self {
321        self.assert_no_errors().assert_unchanged()
322    }
323
324    /// Compiles the project and asserts that the output does not contain errors
325    pub fn assert_no_errors(&self) -> &Self {
326        let compiled = self.compile().unwrap();
327        compiled.assert_success();
328        self
329    }
330
331    /// Compiles the project and asserts that the output is unchanged
332    pub fn assert_unchanged(&self) -> &Self {
333        let compiled = self.compile().unwrap();
334        assert!(compiled.is_unchanged());
335        self
336    }
337
338    /// Compiles the project and asserts that the output is _changed_
339    pub fn assert_changed(&self) -> &Self {
340        let compiled = self.compile().unwrap();
341        assert!(!compiled.is_unchanged());
342        self
343    }
344
345    /// Returns a list of all source files in the project's `src` directory
346    pub fn list_source_files(&self) -> Vec<PathBuf> {
347        utils::source_files(self.project().sources_path())
348    }
349}
350
351impl<T: ArtifactOutput + Default> TempProject<T> {
352    /// Creates a new temp project inside a tempdir with a prefixed directory
353    pub fn prefixed(prefix: &str, paths: ProjectPathsConfigBuilder) -> Result<Self> {
354        Self::prefixed_with_artifacts(prefix, paths, T::default())
355    }
356
357    /// Creates a new temp project for the given `PathStyle`
358    pub fn with_style(prefix: &str, style: PathStyle) -> Result<Self> {
359        let tmp_dir = tempdir(prefix)?;
360        let paths = style.paths(tmp_dir.path())?;
361        let inner = Project::builder().artifacts(T::default()).paths(paths).build()?;
362        Ok(Self::create_new(tmp_dir, inner)?)
363    }
364
365    /// Creates a new temp project using the provided paths and setting the project root to a temp
366    /// dir
367    pub fn new(paths: ProjectPathsConfigBuilder) -> Result<Self> {
368        Self::prefixed("temp-project", paths)
369    }
370}
371
372impl<T: ArtifactOutput> fmt::Debug for TempProject<T> {
373    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
374        f.debug_struct("TempProject").field("paths", self.paths()).finish()
375    }
376}
377
378pub(crate) fn create_contract_file(path: PathBuf, content: impl AsRef<str>) -> Result<PathBuf> {
379    if let Some(parent) = path.parent() {
380        std::fs::create_dir_all(parent)
381            .map_err(|err| SolcIoError::new(err, parent.to_path_buf()))?;
382    }
383    std::fs::write(&path, content.as_ref()).map_err(|err| SolcIoError::new(err, path.clone()))?;
384    Ok(path)
385}
386
387fn contract_file_name(name: impl AsRef<str>) -> String {
388    let name = name.as_ref().trim();
389    if name.ends_with(".sol") {
390        name.to_string()
391    } else {
392        format!("{name}.sol")
393    }
394}
395
396impl TempProject<HardhatArtifacts> {
397    /// Creates an empty new hardhat style workspace in a new temporary dir
398    pub fn hardhat() -> Result<Self> {
399        let tmp_dir = tempdir("tmp_hh")?;
400
401        let paths = ProjectPathsConfig::hardhat(tmp_dir.path())?;
402
403        let inner =
404            Project::builder().artifacts(HardhatArtifacts::default()).paths(paths).build()?;
405        Ok(Self::create_new(tmp_dir, inner)?)
406    }
407}
408
409impl TempProject<ConfigurableArtifacts> {
410    /// Creates an empty new dapptools style workspace in a new temporary dir
411    pub fn dapptools() -> Result<Self> {
412        let tmp_dir = tempdir("tmp_dapp")?;
413        let paths = ProjectPathsConfig::dapptools(tmp_dir.path())?;
414
415        let inner = Project::builder().paths(paths).build()?;
416        Ok(Self::create_new(tmp_dir, inner)?)
417    }
418
419    /// Creates an initialized dapptools style workspace in a new temporary dir
420    pub fn dapptools_init() -> Result<Self> {
421        let mut project = Self::dapptools()?;
422        let orig_root = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("test-data/dapp-sample");
423        copy_dir(orig_root, project.root())?;
424        project.project_mut().paths.remappings = Remapping::find_many(project.root());
425
426        Ok(project)
427    }
428
429    /// Clones the given repo into a temp dir, initializes it recursively and configures it.
430    ///
431    /// # Example
432    ///
433    /// ```
434    /// use ethers_solc::project_util::TempProject;
435    /// # fn t() {
436    /// let project = TempProject::checkout("transmissions11/solmate").unwrap();
437    /// # }
438    /// ```
439    pub fn checkout(repo: impl AsRef<str>) -> Result<Self> {
440        let tmp_dir = tempdir("tmp_checkout")?;
441        clone_remote(&format!("https://github.com/{}", repo.as_ref()), tmp_dir.path())
442            .map_err(|err| SolcIoError::new(err, tmp_dir.path()))?;
443        let paths = ProjectPathsConfig::dapptools(tmp_dir.path())?;
444
445        let inner = Project::builder().paths(paths).build()?;
446        Ok(Self::create_new(tmp_dir, inner)?)
447    }
448
449    /// Create a new temporary project and populate it with mock files
450    ///
451    /// ```no_run
452    /// use ethers_solc::project_util::mock::MockProjectSettings;
453    /// use ethers_solc::project_util::TempProject;
454    /// let tmp = TempProject::mocked(&MockProjectSettings::default(), "^0.8.10").unwrap();
455    /// ```
456    pub fn mocked(settings: &MockProjectSettings, version: impl AsRef<str>) -> Result<Self> {
457        let mut tmp = Self::dapptools()?;
458        let gen = MockProjectGenerator::new(settings);
459        tmp.mock(&gen, version)?;
460        let remappings = gen.remappings_at(tmp.root());
461        tmp.paths_mut().remappings.extend(remappings);
462        Ok(tmp)
463    }
464
465    /// Create a new temporary project and populate it with a random layout
466    ///
467    /// ```no_run
468    /// use ethers_solc::project_util::TempProject;
469    /// let tmp = TempProject::mocked_random("^0.8.10").unwrap();
470    /// ```
471    ///
472    /// This is a convenience function for:
473    ///
474    /// ```no_run
475    /// use ethers_solc::project_util::mock::MockProjectSettings;
476    /// use ethers_solc::project_util::TempProject;
477    /// let tmp = TempProject::mocked(&MockProjectSettings::random(), "^0.8.10").unwrap();
478    /// ```
479    pub fn mocked_random(version: impl AsRef<str>) -> Result<Self> {
480        Self::mocked(&MockProjectSettings::random(), version)
481    }
482}
483
484impl<T: ArtifactOutput> AsRef<Project<T>> for TempProject<T> {
485    fn as_ref(&self) -> &Project<T> {
486        self.project()
487    }
488}
489
490/// The cache file and all the artifacts it references
491#[derive(Debug, Clone)]
492pub struct ArtifactsSnapshot<T> {
493    pub cache: SolFilesCache,
494    pub artifacts: Artifacts<T>,
495}
496
497impl ArtifactsSnapshot<ConfigurableContractArtifact> {
498    /// Ensures that all artifacts have abi, bytecode, deployedbytecode
499    pub fn assert_artifacts_essentials_present(&self) {
500        for artifact in self.artifacts.artifact_files() {
501            let c = artifact.artifact.clone().into_compact_contract();
502            assert!(c.abi.is_some());
503            assert!(c.bin.is_some());
504            assert!(c.bin_runtime.is_some());
505        }
506    }
507}
508
509/// commonly used options for copying entire folders
510fn dir_copy_options() -> dir::CopyOptions {
511    dir::CopyOptions {
512        overwrite: true,
513        skip_exist: false,
514        buffer_size: 64000, //64kb
515        copy_inside: true,
516        content_only: true,
517        depth: 0,
518    }
519}
520
521/// commonly used options for copying files
522fn file_copy_options() -> file::CopyOptions {
523    file::CopyOptions {
524        overwrite: true,
525        skip_exist: false,
526        buffer_size: 64000, //64kb
527    }
528}
529
530/// Copies a single file into the given dir
531pub fn copy_file(source: impl AsRef<Path>, target_dir: impl AsRef<Path>) -> Result<()> {
532    let source = source.as_ref();
533    let target = target_dir.as_ref().join(
534        source
535            .file_name()
536            .ok_or_else(|| SolcError::msg(format!("No file name for {}", source.display())))?,
537    );
538
539    fs_extra::file::copy(source, target, &file_copy_options())?;
540    Ok(())
541}
542
543/// Copies all content of the source dir into the target dir
544pub fn copy_dir(source: impl AsRef<Path>, target_dir: impl AsRef<Path>) -> Result<()> {
545    fs_extra::dir::copy(source, target_dir, &dir_copy_options())?;
546    Ok(())
547}
548
549/// Clones a remote repository into the specified directory.
550pub fn clone_remote(
551    repo_url: &str,
552    target_dir: impl AsRef<Path>,
553) -> std::io::Result<process::Output> {
554    Command::new("git")
555        .args(["clone", "--depth", "1", "--recursive", repo_url])
556        .arg(target_dir.as_ref())
557        .output()
558}
559
560#[cfg(test)]
561mod tests {
562    use super::*;
563
564    #[test]
565    fn can_mock_project() {
566        let _prj = TempProject::mocked(&Default::default(), "^0.8.11").unwrap();
567        let _prj = TempProject::mocked_random("^0.8.11").unwrap();
568    }
569}