foundry_test_utils/
ext.rs

1use crate::prj::{TestCommand, TestProject, clone_remote, setup_forge};
2use foundry_compilers::PathStyle;
3use std::process::Command;
4
5/// External test builder
6#[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    /// Creates a new external test builder.
22    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    /// Sets the path style.
37    pub fn style(mut self, style: PathStyle) -> Self {
38        self.style = style;
39        self
40    }
41
42    /// Sets the fork block.
43    pub fn fork_block(mut self, fork_block: u64) -> Self {
44        self.fork_block = Some(fork_block);
45        self
46    }
47
48    /// Adds an argument to the forge command.
49    pub fn arg(mut self, arg: impl Into<String>) -> Self {
50        self.args.push(arg.into());
51        self
52    }
53
54    /// Adds multiple arguments to the forge command.
55    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    /// Sets the verbosity
65    pub fn verbosity(mut self, verbosity: usize) -> Self {
66        self.verbosity = format!("-{}", "v".repeat(verbosity));
67        self
68    }
69
70    /// Adds an environment variable to the forge command.
71    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    /// Adds multiple environment variables to the forge command.
77    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    /// Adds a command to run after the project is cloned.
88    ///
89    /// Note that the command is run in the project's root directory, and it won't fail the test if
90    /// it fails.
91    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        // Export vyper and forge in test command - workaround for snekmate venom tests.
100        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        // Wipe the default structure.
114        prj.wipe();
115
116        // Clone the external repository.
117        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        // Checkout the revision.
122        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    /// Runs the test.
166    pub fn run(&self) {
167        let (prj, mut test_cmd) = self.setup_forge_prj(true);
168
169        // Run installation command.
170        self.run_install_commands(prj.root().to_str().unwrap());
171
172        // Run the tests.
173        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}