1use crate::prj::{TestCommand, TestProject, clone_remote, setup_forge};
2use foundry_compilers::PathStyle;
3use std::{
4 path::{Path, PathBuf},
5 process::Command,
6};
7
8#[derive(Clone, Debug)]
10#[must_use = "ExtTester does nothing unless you `run` it"]
11pub struct ExtTester {
12 pub org: &'static str,
13 pub name: &'static str,
14 pub rev: &'static str,
15 pub style: PathStyle,
16 pub fork_block: Option<u64>,
17 pub fuzz_runs: u32,
18 pub args: Vec<String>,
19 pub envs: Vec<(String, String)>,
20 pub install_commands: Vec<Vec<String>>,
21 pub python_packages: Vec<String>,
22 pub verbosity: String,
23}
24
25impl ExtTester {
26 pub fn new(org: &'static str, name: &'static str, rev: &'static str) -> Self {
28 Self {
29 org,
30 name,
31 rev,
32 style: PathStyle::Dapptools,
33 fork_block: None,
34 fuzz_runs: 32,
35 args: vec![],
36 envs: vec![],
37 install_commands: vec![],
38 python_packages: vec![],
39 verbosity: "-vvv".to_string(),
40 }
41 }
42
43 pub const fn style(mut self, style: PathStyle) -> Self {
45 self.style = style;
46 self
47 }
48
49 pub const fn fork_block(mut self, fork_block: u64) -> Self {
51 self.fork_block = Some(fork_block);
52 self
53 }
54
55 pub const fn fuzz_runs(mut self, fuzz_runs: u32) -> Self {
57 self.fuzz_runs = fuzz_runs;
58 self
59 }
60
61 pub fn arg(mut self, arg: impl Into<String>) -> Self {
63 self.args.push(arg.into());
64 self
65 }
66
67 pub fn args<I, A>(mut self, args: I) -> Self
69 where
70 I: IntoIterator<Item = A>,
71 A: Into<String>,
72 {
73 self.args.extend(args.into_iter().map(Into::into));
74 self
75 }
76
77 pub fn verbosity(mut self, verbosity: usize) -> Self {
79 self.verbosity = format!("-{}", "v".repeat(verbosity));
80 self
81 }
82
83 pub fn env(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
85 self.envs.push((key.into(), value.into()));
86 self
87 }
88
89 pub fn envs<I, K, V>(mut self, envs: I) -> Self
91 where
92 I: IntoIterator<Item = (K, V)>,
93 K: Into<String>,
94 V: Into<String>,
95 {
96 self.envs.extend(envs.into_iter().map(|(k, v)| (k.into(), v.into())));
97 self
98 }
99
100 pub fn install_command(mut self, command: &[&str]) -> Self {
105 self.install_commands.push(command.iter().map(|s| s.to_string()).collect());
106 self
107 }
108
109 pub fn python_package(mut self, package: impl Into<String>) -> Self {
111 self.python_packages.push(package.into());
112 self
113 }
114
115 pub fn setup_forge_prj(&self, recursive: bool) -> (TestProject, TestCommand) {
116 let (prj, mut test_cmd) = setup_forge(self.name, self.style.clone());
117
118 prj.wipe();
120
121 let repo_url = format!("https://github.com/{}/{}.git", self.org, self.name);
123 let root = prj.root().to_str().unwrap();
124 clone_remote(&repo_url, root, recursive);
125
126 if self.rev.is_empty() {
128 let mut git = Command::new("git");
129 git.current_dir(root).args(["log", "-n", "1"]);
130 test_debug!("$ {git:?}");
131 let output = git.output().unwrap();
132 assert!(output.status.success(), "git log failed: {output:?}");
133 let stdout = String::from_utf8(output.stdout).unwrap();
134 let commit = stdout.lines().next().unwrap().split_whitespace().nth(1).unwrap();
135 panic!("pin to latest commit: {commit}");
136 }
137 let mut git = Command::new("git");
138 git.current_dir(root).args(["checkout", self.rev]);
139 test_debug!("$ {git:?}");
140 let status = git.status().unwrap();
141 assert!(status.success(), "git checkout failed: {status}");
142
143 let mut new_paths = Vec::new();
145 if let Some(python_bin_dir) = self.install_python_packages(root) {
146 new_paths.push(python_bin_dir);
147 }
148 if let Some(vyper) = &prj.inner.project().compiler.vyper {
149 let vyper_dir = vyper.path.parent().expect("vyper path should have a parent");
150 new_paths.push(vyper_dir.to_path_buf());
151 }
152 let forge_bin = prj.foundry_bin_path("forge");
153 let forge_dir = forge_bin.parent().expect("forge path should have a parent");
154 new_paths.push(forge_dir.to_path_buf());
155 let existing_path = std::env::var_os("PATH").unwrap_or_default();
156 new_paths.extend(std::env::split_paths(&existing_path));
157
158 let joined_path = std::env::join_paths(new_paths).expect("failed to join PATH");
159 test_cmd.env("PATH", joined_path);
160
161 (prj, test_cmd)
162 }
163
164 fn install_python_packages(&self, root: &str) -> Option<PathBuf> {
165 if self.python_packages.is_empty() {
166 return None;
167 }
168
169 let venv = Path::new(root).join(".foundry-ext-venv");
170 let mut venv_cmd = Command::new("python3");
171 venv_cmd.args(["-m", "venv"]).arg(&venv);
172 test_debug!("cd {root}; {venv_cmd:?}");
173 let status = venv_cmd.current_dir(root).status().expect("failed to create Python venv");
174 assert!(status.success(), "python venv creation failed: {status}");
175
176 let bin_dir = venv.join(if cfg!(windows) { "Scripts" } else { "bin" });
177 let pip = bin_dir.join(if cfg!(windows) { "pip.exe" } else { "pip" });
178 for package in &self.python_packages {
179 let mut pip_cmd = Command::new(&pip);
180 pip_cmd.args(["install", "--disable-pip-version-check"]).arg(package).current_dir(root);
181 test_debug!("cd {root}; {pip_cmd:?}");
182 let status = pip_cmd.status().expect("failed to install Python package");
183 assert!(status.success(), "Python package install failed: {status}");
184 }
185
186 Some(bin_dir)
187 }
188
189 pub fn run_install_commands(&self, root: &str) {
190 for install_command in &self.install_commands {
191 let mut install_cmd = Command::new(&install_command[0]);
192 install_cmd.args(&install_command[1..]).current_dir(root);
193 test_debug!("cd {root}; {install_cmd:?}");
194 match install_cmd.status() {
195 Ok(s) => {
196 test_debug!("\n\n{install_cmd:?}: {s}");
197 if s.success() {
198 break;
199 }
200 }
201 Err(e) => {
202 eprintln!("\n\n{install_cmd:?}: {e}");
203 }
204 }
205 }
206 }
207
208 pub fn run(&self) {
210 let (prj, mut test_cmd) = self.setup_forge_prj(true);
211
212 self.run_install_commands(prj.root().to_str().unwrap());
214
215 test_cmd.arg("test");
217 test_cmd.args(&self.args);
218 test_cmd.args([
219 format!("--fuzz-runs={}", self.fuzz_runs),
220 "--ffi".to_string(),
221 self.verbosity.clone(),
222 ]);
223
224 test_cmd.envs(self.envs.iter().map(|(k, v)| (k, v)));
225 if let Some(fork_block) = self.fork_block {
226 test_cmd.env("FOUNDRY_ETH_RPC_URL", crate::rpc::next_http_archive_rpc_url());
227 test_cmd.env("FOUNDRY_FORK_BLOCK_NUMBER", fork_block.to_string());
228 }
229 test_cmd.env("FOUNDRY_INVARIANT_DEPTH", "15");
230 test_cmd.env("FOUNDRY_ALLOW_INTERNAL_EXPECT_REVERT", "true");
231
232 test_cmd.assert_success();
233 }
234}