foundry_test_utils/
ext.rs1use crate::prj::{TestCommand, TestProject, clone_remote, setup_forge};
2use foundry_compilers::PathStyle;
3use std::process::Command;
4
5#[derive(Clone, Debug)]
7#[must_use = "ExtTester does nothing unless you `run` it"]
8pub struct ExtTester {
9 pub org: &'static str,
10 pub name: &'static str,
11 pub rev: &'static str,
12 pub style: PathStyle,
13 pub fork_block: Option<u64>,
14 pub args: Vec<String>,
15 pub envs: Vec<(String, String)>,
16 pub install_commands: Vec<Vec<String>>,
17 pub verbosity: String,
18}
19
20impl ExtTester {
21 pub fn new(org: &'static str, name: &'static str, rev: &'static str) -> Self {
23 Self {
24 org,
25 name,
26 rev,
27 style: PathStyle::Dapptools,
28 fork_block: None,
29 args: vec![],
30 envs: vec![],
31 install_commands: vec![],
32 verbosity: "-vvv".to_string(),
33 }
34 }
35
36 pub fn style(mut self, style: PathStyle) -> Self {
38 self.style = style;
39 self
40 }
41
42 pub fn fork_block(mut self, fork_block: u64) -> Self {
44 self.fork_block = Some(fork_block);
45 self
46 }
47
48 pub fn arg(mut self, arg: impl Into<String>) -> Self {
50 self.args.push(arg.into());
51 self
52 }
53
54 pub fn args<I, A>(mut self, args: I) -> Self
56 where
57 I: IntoIterator<Item = A>,
58 A: Into<String>,
59 {
60 self.args.extend(args.into_iter().map(Into::into));
61 self
62 }
63
64 pub fn verbosity(mut self, verbosity: usize) -> Self {
66 self.verbosity = format!("-{}", "v".repeat(verbosity));
67 self
68 }
69
70 pub fn env(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
72 self.envs.push((key.into(), value.into()));
73 self
74 }
75
76 pub fn envs<I, K, V>(mut self, envs: I) -> Self
78 where
79 I: IntoIterator<Item = (K, V)>,
80 K: Into<String>,
81 V: Into<String>,
82 {
83 self.envs.extend(envs.into_iter().map(|(k, v)| (k.into(), v.into())));
84 self
85 }
86
87 pub fn install_command(mut self, command: &[&str]) -> Self {
92 self.install_commands.push(command.iter().map(|s| s.to_string()).collect());
93 self
94 }
95
96 pub fn setup_forge_prj(&self, recursive: bool) -> (TestProject, TestCommand) {
97 let (prj, mut test_cmd) = setup_forge(self.name, self.style.clone());
98
99 if let Some(vyper) = &prj.inner.project().compiler.vyper {
101 let vyper_dir = vyper.path.parent().expect("vyper path should have a parent");
102 let forge_bin = prj.forge_path();
103 let forge_dir = forge_bin.parent().expect("forge path should have a parent");
104
105 let existing_path = std::env::var_os("PATH").unwrap_or_default();
106 let mut new_paths = vec![vyper_dir.to_path_buf(), forge_dir.to_path_buf()];
107 new_paths.extend(std::env::split_paths(&existing_path));
108
109 let joined_path = std::env::join_paths(new_paths).expect("failed to join PATH");
110 test_cmd.env("PATH", joined_path);
111 }
112
113 prj.wipe();
115
116 let repo_url = format!("https://github.com/{}/{}.git", self.org, self.name);
118 let root = prj.root().to_str().unwrap();
119 clone_remote(&repo_url, root, recursive);
120
121 if self.rev.is_empty() {
123 let mut git = Command::new("git");
124 git.current_dir(root).args(["log", "-n", "1"]);
125 test_debug!("$ {git:?}");
126 let output = git.output().unwrap();
127 if !output.status.success() {
128 panic!("git log failed: {output:?}");
129 }
130 let stdout = String::from_utf8(output.stdout).unwrap();
131 let commit = stdout.lines().next().unwrap().split_whitespace().nth(1).unwrap();
132 panic!("pin to latest commit: {commit}");
133 } else {
134 let mut git = Command::new("git");
135 git.current_dir(root).args(["checkout", self.rev]);
136 test_debug!("$ {git:?}");
137 let status = git.status().unwrap();
138 if !status.success() {
139 panic!("git checkout failed: {status}");
140 }
141 }
142
143 (prj, test_cmd)
144 }
145
146 pub fn run_install_commands(&self, root: &str) {
147 for install_command in &self.install_commands {
148 let mut install_cmd = Command::new(&install_command[0]);
149 install_cmd.args(&install_command[1..]).current_dir(root);
150 test_debug!("cd {root}; {install_cmd:?}");
151 match install_cmd.status() {
152 Ok(s) => {
153 test_debug!("\n\n{install_cmd:?}: {s}");
154 if s.success() {
155 break;
156 }
157 }
158 Err(e) => {
159 eprintln!("\n\n{install_cmd:?}: {e}");
160 }
161 }
162 }
163 }
164
165 pub fn run(&self) {
167 let (prj, mut test_cmd) = self.setup_forge_prj(true);
168
169 self.run_install_commands(prj.root().to_str().unwrap());
171
172 test_cmd.arg("test");
174 test_cmd.args(&self.args);
175 test_cmd.args(["--fuzz-runs=32", "--ffi", &self.verbosity]);
176
177 test_cmd.envs(self.envs.iter().map(|(k, v)| (k, v)));
178 if let Some(fork_block) = self.fork_block {
179 test_cmd.env("FOUNDRY_ETH_RPC_URL", crate::rpc::next_http_archive_rpc_url());
180 test_cmd.env("FOUNDRY_FORK_BLOCK_NUMBER", fork_block.to_string());
181 }
182 test_cmd.env("FOUNDRY_INVARIANT_DEPTH", "15");
183 test_cmd.env("FOUNDRY_ALLOW_INTERNAL_EXPECT_REVERT", "true");
184
185 test_cmd.assert_success();
186 }
187}