foundry_test_utils/
util.rs1use foundry_compilers::{Project, ProjectCompileOutput, Vyper, utils::RuntimeOrHandle};
2use foundry_config::Config;
3use std::{
4 env,
5 fs::{self, File},
6 io::{IsTerminal, Read, Seek, Write},
7 path::{Path, PathBuf},
8 process::Command,
9 sync::LazyLock,
10};
11
12const SKIP_DIRS: &[&str] = &["out", "cache", "broadcast"];
16
17pub use crate::{ext::*, prj::*};
18
19pub const FORGE_STD_REVISION: &str = include_str!("../../../testdata/forge-std-rev");
21
22pub static IS_TTY: LazyLock<bool> = LazyLock::new(|| std::io::stdout().is_terminal());
24
25static TEMPLATE_PATH: LazyLock<PathBuf> =
28 LazyLock::new(|| env::temp_dir().join("foundry-forge-test-template"));
29
30static TEMPLATE_LOCK: LazyLock<PathBuf> =
33 LazyLock::new(|| env::temp_dir().join("foundry-forge-test-template.lock"));
34
35pub const SOLC_VERSION: &str = "0.8.33";
37
38pub const OTHER_SOLC_VERSION: &str = "0.8.26";
42
43pub fn initialize(target: &Path) {
59 test_debug!("initializing {}", target.display());
60
61 let tpath = TEMPLATE_PATH.as_path();
62 pretty_err(tpath, fs::create_dir_all(tpath));
63
64 let mut lock = crate::fd_lock::new_lock(TEMPLATE_LOCK.as_path());
66 let mut _read = lock.read().unwrap();
67 if !crate::fd_lock::lock_exists(TEMPLATE_LOCK.as_path()) {
68 drop(_read);
78 let mut write = lock.write().unwrap();
79
80 let mut data = Vec::new();
81 write.read_to_end(&mut data).unwrap();
82 if data != crate::fd_lock::LOCK_TOKEN {
83 let (prj, mut cmd) = setup_forge("template", foundry_compilers::PathStyle::Dapptools);
85 test_debug!("- initializing template dir in {}", prj.root().display());
86
87 cmd.args(["init", "--force", "--empty"]).assert_success();
88 prj.write_config(Config {
89 solc: Some(foundry_config::SolcReq::Version(SOLC_VERSION.parse().unwrap())),
90 ..Default::default()
91 });
92
93 let output = Command::new("git")
95 .current_dir(prj.root().join("lib/forge-std"))
96 .args(["checkout", FORGE_STD_REVISION])
97 .output()
98 .expect("failed to checkout forge-std");
99 assert!(output.status.success(), "{output:#?}");
100
101 cmd.forge_fuse().arg("build").assert_success();
103
104 let _ = fs::remove_dir_all(tpath);
106
107 pretty_err(tpath, copy_dir_filtered(prj.root(), tpath));
109
110 write.set_len(0).unwrap();
112 write.seek(std::io::SeekFrom::Start(0)).unwrap();
113 write.write_all(crate::fd_lock::LOCK_TOKEN).unwrap();
114 }
115
116 drop(write);
118 _read = lock.read().unwrap();
119 }
120
121 test_debug!("- copying template dir from {}", tpath.display());
122 pretty_err(target, fs::create_dir_all(target));
123 pretty_err(target, copy_dir_filtered(tpath, target));
124}
125
126pub fn get_compiled(project: &mut Project) -> ProjectCompileOutput {
128 let lock_file_path = project.sources_path().join(".lock");
129 let mut lock = crate::fd_lock::new_lock(&lock_file_path);
132 let read = lock.read().unwrap();
133 let out;
134
135 let mut write = None;
136 if !project.cache_path().exists() || !crate::fd_lock::lock_exists(&lock_file_path) {
137 drop(read);
138 write = Some(lock.write().unwrap());
139 test_debug!("cache miss for {}", lock_file_path.display());
140 } else {
141 test_debug!("cache hit for {}", lock_file_path.display());
142 }
143
144 if project.compiler.vyper.is_none() {
145 project.compiler.vyper = Some(get_vyper());
146 }
147
148 test_debug!("compiling {}", lock_file_path.display());
149 out = project.compile().unwrap();
150 test_debug!("compiled {}", lock_file_path.display());
151
152 if out.has_compiler_errors() {
153 panic!("Compiled with errors:\n{out}");
154 }
155
156 if let Some(write) = &mut write {
157 write.write_all(crate::fd_lock::LOCK_TOKEN).unwrap();
158 }
159
160 out
161}
162
163pub fn get_vyper() -> Vyper {
165 static VYPER: LazyLock<PathBuf> = LazyLock::new(|| std::env::temp_dir().join("vyper"));
166
167 if let Ok(vyper) = Vyper::new("vyper") {
168 return vyper;
169 }
170 if let Ok(vyper) = Vyper::new(&*VYPER) {
171 return vyper;
172 }
173 return RuntimeOrHandle::new().block_on(install());
174
175 async fn install() -> Vyper {
176 #[cfg(target_family = "unix")]
177 use std::{fs::Permissions, os::unix::fs::PermissionsExt};
178
179 let path = VYPER.as_path();
180 let mut file = File::create(path).unwrap();
181 if let Err(e) = file.try_lock() {
182 if let fs::TryLockError::WouldBlock = e {
183 file.lock().unwrap();
184 assert!(path.exists());
185 return Vyper::new(path).unwrap();
186 }
187 file.lock().unwrap();
188 }
189
190 let suffix = match svm::platform() {
191 svm::Platform::MacOsAarch64 => "darwin",
192 svm::Platform::LinuxAmd64 => "linux",
193 svm::Platform::WindowsAmd64 => "windows.exe",
194 platform => panic!(
195 "unsupported platform {platform:?} for installing vyper, \
196 install it manually and add it to $PATH"
197 ),
198 };
199 let url = format!(
200 "https://github.com/vyperlang/vyper/releases/download/v0.4.3/vyper.0.4.3+commit.bff19ea2.{suffix}"
201 );
202
203 test_debug!("downloading vyper from {url}");
204 let res = reqwest::Client::builder().build().unwrap().get(url).send().await.unwrap();
205
206 assert!(res.status().is_success());
207
208 let bytes = res.bytes().await.unwrap();
209
210 file.write_all(&bytes).unwrap();
211
212 #[cfg(target_family = "unix")]
213 file.set_permissions(Permissions::from_mode(0o755)).unwrap();
214
215 Vyper::new(path).unwrap()
216 }
217}
218
219#[track_caller]
220pub fn pretty_err<T, E: std::error::Error>(path: impl AsRef<Path>, res: Result<T, E>) -> T {
221 match res {
222 Ok(t) => t,
223 Err(err) => panic!("{}: {err}", path.as_ref().display()),
224 }
225}
226
227pub fn read_string(path: impl AsRef<Path>) -> String {
228 let path = path.as_ref();
229 pretty_err(path, std::fs::read_to_string(path))
230}
231
232pub fn copy_dir_filtered(src: &Path, dst: &Path) -> std::io::Result<()> {
238 fs::create_dir_all(dst)?;
239 copy_dir_filtered_inner(src, dst, true)
240}
241
242fn copy_dir_filtered_inner(src: &Path, dst: &Path, is_root: bool) -> std::io::Result<()> {
243 for entry in fs::read_dir(src)? {
244 let entry = entry?;
245 let ty = entry.file_type()?;
246 let src_path = entry.path();
247 let dst_path = dst.join(entry.file_name());
248
249 if ty.is_dir() {
250 if is_root
252 && let Some(name) = entry.file_name().to_str()
253 && SKIP_DIRS.contains(&name)
254 {
255 continue;
256 }
257 fs::create_dir_all(&dst_path)?;
258 copy_dir_filtered_inner(&src_path, &dst_path, false)?;
259 } else {
260 fs::copy(&src_path, &dst_path)?;
261 }
262 }
263 Ok(())
264}