ethers_solc/project_util/
mod.rs1use 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
26pub struct TempProject<T: ArtifactOutput = ConfigurableArtifacts> {
30 _root: TempDir,
32 inner: Project<T>,
34}
35
36impl<T: ArtifactOutput> TempProject<T> {
37 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 project.inner.ignored_error_codes.push(1878);
43 Ok(project)
44 }
45
46 pub fn with_artifacts(paths: ProjectPathsConfigBuilder, artifacts: T) -> Result<Self> {
49 Self::prefixed_with_artifacts("temp-project", paths, artifacts)
50 }
51
52 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 pub fn with_settings(mut self, settings: impl Into<Settings>) -> Self {
67 self.inner.solc_config.settings = settings.into();
68 self
69 }
70
71 #[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 pub fn paths(&self) -> &ProjectPathsConfig {
104 &self.project().paths
105 }
106
107 pub fn paths_mut(&mut self) -> &mut ProjectPathsConfig {
109 &mut self.project_mut().paths
110 }
111
112 pub fn artifacts_path(&self) -> &PathBuf {
114 &self.paths().artifacts
115 }
116
117 pub fn sources_path(&self) -> &PathBuf {
119 &self.paths().sources
120 }
121
122 pub fn cache_path(&self) -> &PathBuf {
124 &self.paths().cache
125 }
126
127 pub fn root(&self) -> &Path {
129 self.project().paths.root.as_path()
130 }
131
132 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 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 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 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 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 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 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 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 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 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 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 pub fn mock(&self, gen: &MockProjectGenerator, version: impl AsRef<str>) -> Result<()> {
264 gen.write_to(self.paths(), version)
265 }
266
267 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 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 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 pub fn ensure_no_errors_recompile_unchanged(&self) -> Result<&Self> {
306 self.ensure_no_errors()?.ensure_unchanged()
307 }
308
309 pub fn assert_no_errors_recompile_unchanged(&self) -> &Self {
321 self.assert_no_errors().assert_unchanged()
322 }
323
324 pub fn assert_no_errors(&self) -> &Self {
326 let compiled = self.compile().unwrap();
327 compiled.assert_success();
328 self
329 }
330
331 pub fn assert_unchanged(&self) -> &Self {
333 let compiled = self.compile().unwrap();
334 assert!(compiled.is_unchanged());
335 self
336 }
337
338 pub fn assert_changed(&self) -> &Self {
340 let compiled = self.compile().unwrap();
341 assert!(!compiled.is_unchanged());
342 self
343 }
344
345 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 pub fn prefixed(prefix: &str, paths: ProjectPathsConfigBuilder) -> Result<Self> {
354 Self::prefixed_with_artifacts(prefix, paths, T::default())
355 }
356
357 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 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 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 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 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 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 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 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#[derive(Debug, Clone)]
492pub struct ArtifactsSnapshot<T> {
493 pub cache: SolFilesCache,
494 pub artifacts: Artifacts<T>,
495}
496
497impl ArtifactsSnapshot<ConfigurableContractArtifact> {
498 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
509fn dir_copy_options() -> dir::CopyOptions {
511 dir::CopyOptions {
512 overwrite: true,
513 skip_exist: false,
514 buffer_size: 64000, copy_inside: true,
516 content_only: true,
517 depth: 0,
518 }
519}
520
521fn file_copy_options() -> file::CopyOptions {
523 file::CopyOptions {
524 overwrite: true,
525 skip_exist: false,
526 buffer_size: 64000, }
528}
529
530pub 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
543pub 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
549pub 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}