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 assert!(output.status.success(), "git log failed: {output:?}");
128 let stdout = String::from_utf8(output.stdout).unwrap();
129 let commit = stdout.lines().next().unwrap().split_whitespace().nth(1).unwrap();
130 panic!("pin to latest commit: {commit}");
131 } else {
132 let mut git = Command::new("git");
133 git.current_dir(root).args(["checkout", self.rev]);
134 test_debug!("$ {git:?}");
135 let status = git.status().unwrap();
136 assert!(status.success(), "git checkout failed: {status}")
137 }
138
139 (prj, test_cmd)
140 }
141
142 pub fn run_install_commands(&self, root: &str) {
143 for install_command in &self.install_commands {
144 let mut install_cmd = Command::new(&install_command[0]);
145 install_cmd.args(&install_command[1..]).current_dir(root);
146 test_debug!("cd {root}; {install_cmd:?}");
147 match install_cmd.status() {
148 Ok(s) => {
149 test_debug!("\n\n{install_cmd:?}: {s}");
150 if s.success() {
151 break;
152 }
153 }
154 Err(e) => {
155 eprintln!("\n\n{install_cmd:?}: {e}");
156 }
157 }
158 }
159 }
160
161 pub fn run(&self) {
163 let (prj, mut test_cmd) = self.setup_forge_prj(true);
164
165 self.run_install_commands(prj.root().to_str().unwrap());
167
168 test_cmd.arg("test");
170 test_cmd.args(&self.args);
171 test_cmd.args(["--fuzz-runs=32", "--ffi", &self.verbosity]);
172
173 test_cmd.envs(self.envs.iter().map(|(k, v)| (k, v)));
174 if let Some(fork_block) = self.fork_block {
175 test_cmd.env("FOUNDRY_ETH_RPC_URL", crate::rpc::next_http_archive_rpc_url());
176 test_cmd.env("FOUNDRY_FORK_BLOCK_NUMBER", fork_block.to_string());
177 }
178 test_cmd.env("FOUNDRY_INVARIANT_DEPTH", "15");
179 test_cmd.env("FOUNDRY_ALLOW_INTERNAL_EXPECT_REVERT", "true");
180
181 test_cmd.assert_success();
182 }
183}