foundry_test_utils/
util.rs

1use crate::{init_tracing, rpc::rpc_endpoints};
2use eyre::{Result, WrapErr};
3use foundry_compilers::{
4    ArtifactOutput, ConfigurableArtifacts, PathStyle, Project, ProjectCompileOutput,
5    ProjectPathsConfig, Vyper,
6    artifacts::Contract,
7    cache::CompilerCache,
8    compilers::multi::MultiCompiler,
9    project_util::{TempProject, copy_dir},
10    solc::SolcSettings,
11    utils::RuntimeOrHandle,
12};
13use foundry_config::Config;
14use parking_lot::Mutex;
15use regex::Regex;
16use snapbox::{Data, IntoData, assert_data_eq, cmd::OutputAssert};
17use std::{
18    env,
19    ffi::OsStr,
20    fs::{self, File},
21    io::{BufWriter, IsTerminal, Read, Seek, Write},
22    path::{Path, PathBuf},
23    process::{Command, Output, Stdio},
24    sync::{
25        Arc, LazyLock,
26        atomic::{AtomicUsize, Ordering},
27    },
28};
29
30static CURRENT_DIR_LOCK: LazyLock<Mutex<()>> = LazyLock::new(|| Mutex::new(()));
31
32/// The commit of forge-std to use.
33pub const FORGE_STD_REVISION: &str = include_str!("../../../testdata/forge-std-rev");
34
35/// Stores whether `stdout` is a tty / terminal.
36pub static IS_TTY: LazyLock<bool> = LazyLock::new(|| std::io::stdout().is_terminal());
37
38/// Global default template path. Contains the global template project from which all other
39/// temp projects are initialized. See [`initialize()`] for more info.
40static TEMPLATE_PATH: LazyLock<PathBuf> =
41    LazyLock::new(|| env::temp_dir().join("foundry-forge-test-template"));
42
43/// Global default template lock. If its contents are not exactly `"1"`, the global template will
44/// be re-initialized. See [`initialize()`] for more info.
45static TEMPLATE_LOCK: LazyLock<PathBuf> =
46    LazyLock::new(|| env::temp_dir().join("foundry-forge-test-template.lock"));
47
48/// Global test identifier.
49static NEXT_ID: AtomicUsize = AtomicUsize::new(0);
50
51/// The default Solc version used when compiling tests.
52pub const SOLC_VERSION: &str = "0.8.30";
53
54/// Another Solc version used when compiling tests.
55///
56/// Necessary to avoid downloading multiple versions.
57pub const OTHER_SOLC_VERSION: &str = "0.8.26";
58
59/// External test builder
60#[derive(Clone, Debug)]
61#[must_use = "ExtTester does nothing unless you `run` it"]
62pub struct ExtTester {
63    pub org: &'static str,
64    pub name: &'static str,
65    pub rev: &'static str,
66    pub style: PathStyle,
67    pub fork_block: Option<u64>,
68    pub args: Vec<String>,
69    pub envs: Vec<(String, String)>,
70    pub install_commands: Vec<Vec<String>>,
71    pub verbosity: String,
72}
73
74impl ExtTester {
75    /// Creates a new external test builder.
76    pub fn new(org: &'static str, name: &'static str, rev: &'static str) -> Self {
77        Self {
78            org,
79            name,
80            rev,
81            style: PathStyle::Dapptools,
82            fork_block: None,
83            args: vec![],
84            envs: vec![],
85            install_commands: vec![],
86            verbosity: "-vvv".to_string(),
87        }
88    }
89
90    /// Sets the path style.
91    pub fn style(mut self, style: PathStyle) -> Self {
92        self.style = style;
93        self
94    }
95
96    /// Sets the fork block.
97    pub fn fork_block(mut self, fork_block: u64) -> Self {
98        self.fork_block = Some(fork_block);
99        self
100    }
101
102    /// Adds an argument to the forge command.
103    pub fn arg(mut self, arg: impl Into<String>) -> Self {
104        self.args.push(arg.into());
105        self
106    }
107
108    /// Adds multiple arguments to the forge command.
109    pub fn args<I, A>(mut self, args: I) -> Self
110    where
111        I: IntoIterator<Item = A>,
112        A: Into<String>,
113    {
114        self.args.extend(args.into_iter().map(Into::into));
115        self
116    }
117
118    /// Sets the verbosity
119    pub fn verbosity(mut self, verbosity: usize) -> Self {
120        self.verbosity = format!("-{}", "v".repeat(verbosity));
121        self
122    }
123
124    /// Adds an environment variable to the forge command.
125    pub fn env(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
126        self.envs.push((key.into(), value.into()));
127        self
128    }
129
130    /// Adds multiple environment variables to the forge command.
131    pub fn envs<I, K, V>(mut self, envs: I) -> Self
132    where
133        I: IntoIterator<Item = (K, V)>,
134        K: Into<String>,
135        V: Into<String>,
136    {
137        self.envs.extend(envs.into_iter().map(|(k, v)| (k.into(), v.into())));
138        self
139    }
140
141    /// Adds a command to run after the project is cloned.
142    ///
143    /// Note that the command is run in the project's root directory, and it won't fail the test if
144    /// it fails.
145    pub fn install_command(mut self, command: &[&str]) -> Self {
146        self.install_commands.push(command.iter().map(|s| s.to_string()).collect());
147        self
148    }
149
150    pub fn setup_forge_prj(&self, recursive: bool) -> (TestProject, TestCommand) {
151        let (prj, mut test_cmd) = setup_forge(self.name, self.style.clone());
152
153        // Export vyper and forge in test command - workaround for snekmate venom tests.
154        if let Some(vyper) = &prj.inner.project().compiler.vyper {
155            let vyper_dir = vyper.path.parent().expect("vyper path should have a parent");
156            let forge_bin = prj.forge_path();
157            let forge_dir = forge_bin.parent().expect("forge path should have a parent");
158
159            let existing_path = std::env::var_os("PATH").unwrap_or_default();
160            let mut new_paths = vec![vyper_dir.to_path_buf(), forge_dir.to_path_buf()];
161            new_paths.extend(std::env::split_paths(&existing_path));
162
163            let joined_path = std::env::join_paths(new_paths).expect("failed to join PATH");
164            test_cmd.env("PATH", joined_path);
165        }
166
167        // Wipe the default structure.
168        prj.wipe();
169
170        // Clone the external repository.
171        let repo_url = format!("https://github.com/{}/{}.git", self.org, self.name);
172        let root = prj.root().to_str().unwrap();
173        clone_remote(&repo_url, root, recursive);
174
175        // Checkout the revision.
176        if self.rev.is_empty() {
177            let mut git = Command::new("git");
178            git.current_dir(root).args(["log", "-n", "1"]);
179            test_debug!("$ {git:?}");
180            let output = git.output().unwrap();
181            if !output.status.success() {
182                panic!("git log failed: {output:?}");
183            }
184            let stdout = String::from_utf8(output.stdout).unwrap();
185            let commit = stdout.lines().next().unwrap().split_whitespace().nth(1).unwrap();
186            panic!("pin to latest commit: {commit}");
187        } else {
188            let mut git = Command::new("git");
189            git.current_dir(root).args(["checkout", self.rev]);
190            test_debug!("$ {git:?}");
191            let status = git.status().unwrap();
192            if !status.success() {
193                panic!("git checkout failed: {status}");
194            }
195        }
196
197        (prj, test_cmd)
198    }
199
200    pub fn run_install_commands(&self, root: &str) {
201        for install_command in &self.install_commands {
202            let mut install_cmd = Command::new(&install_command[0]);
203            install_cmd.args(&install_command[1..]).current_dir(root);
204            test_debug!("cd {root}; {install_cmd:?}");
205            match install_cmd.status() {
206                Ok(s) => {
207                    test_debug!("\n\n{install_cmd:?}: {s}");
208                    if s.success() {
209                        break;
210                    }
211                }
212                Err(e) => {
213                    eprintln!("\n\n{install_cmd:?}: {e}");
214                }
215            }
216        }
217    }
218
219    /// Runs the test.
220    pub fn run(&self) {
221        let (prj, mut test_cmd) = self.setup_forge_prj(true);
222
223        // Run installation command.
224        self.run_install_commands(prj.root().to_str().unwrap());
225
226        // Run the tests.
227        test_cmd.arg("test");
228        test_cmd.args(&self.args);
229        test_cmd.args(["--fuzz-runs=32", "--ffi", &self.verbosity]);
230
231        test_cmd.envs(self.envs.iter().map(|(k, v)| (k, v)));
232        if let Some(fork_block) = self.fork_block {
233            test_cmd.env("FOUNDRY_ETH_RPC_URL", crate::rpc::next_http_archive_rpc_url());
234            test_cmd.env("FOUNDRY_FORK_BLOCK_NUMBER", fork_block.to_string());
235        }
236        test_cmd.env("FOUNDRY_INVARIANT_DEPTH", "15");
237        test_cmd.env("FOUNDRY_ALLOW_INTERNAL_EXPECT_REVERT", "true");
238
239        test_cmd.assert_success();
240    }
241}
242
243/// Initializes a project with `forge init` at the given path from a template directory.
244///
245/// This should be called after an empty project is created like in
246/// [some of this crate's macros](crate::forgetest_init).
247///
248/// ## Note
249///
250/// This doesn't always run `forge init`, instead opting to copy an already-initialized template
251/// project from a global template path. This is done to speed up tests.
252///
253/// This used to use a `static` `Lazy`, but this approach does not with `cargo-nextest` because it
254/// runs each test in a separate process. Instead, we use a global lock file to ensure that only one
255/// test can initialize the template at a time.
256///
257/// This sets the project's solc version to the [`SOLC_VERSION`].
258pub fn initialize(target: &Path) {
259    test_debug!("initializing {}", target.display());
260
261    let tpath = TEMPLATE_PATH.as_path();
262    pretty_err(tpath, fs::create_dir_all(tpath));
263
264    // Initialize the global template if necessary.
265    let mut lock = crate::fd_lock::new_lock(TEMPLATE_LOCK.as_path());
266    let mut _read = lock.read().unwrap();
267    if !crate::fd_lock::lock_exists(TEMPLATE_LOCK.as_path()) {
268        // We are the first to acquire the lock:
269        // - initialize a new empty temp project;
270        // - run `forge init`;
271        // - run `forge build`;
272        // - copy it over to the global template;
273        // Ideally we would be able to initialize a temp project directly in the global template,
274        // but `TempProject` does not currently allow this: https://github.com/foundry-rs/compilers/issues/22
275
276        // Release the read lock and acquire a write lock, initializing the lock file.
277        drop(_read);
278        let mut write = lock.write().unwrap();
279
280        let mut data = Vec::new();
281        write.read_to_end(&mut data).unwrap();
282        if data != crate::fd_lock::LOCK_TOKEN {
283            // Initialize and build.
284            let (prj, mut cmd) = setup_forge("template", foundry_compilers::PathStyle::Dapptools);
285            test_debug!("- initializing template dir in {}", prj.root().display());
286
287            cmd.args(["init", "--force", "--empty"]).assert_success();
288            prj.write_config(Config {
289                solc: Some(foundry_config::SolcReq::Version(SOLC_VERSION.parse().unwrap())),
290                ..Default::default()
291            });
292
293            // Checkout forge-std.
294            let output = Command::new("git")
295                .current_dir(prj.root().join("lib/forge-std"))
296                .args(["checkout", FORGE_STD_REVISION])
297                .output()
298                .expect("failed to checkout forge-std");
299            assert!(output.status.success(), "{output:#?}");
300
301            // Build the project.
302            cmd.forge_fuse().arg("build").assert_success();
303
304            // Remove the existing template, if any.
305            let _ = fs::remove_dir_all(tpath);
306
307            // Copy the template to the global template path.
308            pretty_err(tpath, copy_dir(prj.root(), tpath));
309
310            // Update lockfile to mark that template is initialized.
311            write.set_len(0).unwrap();
312            write.seek(std::io::SeekFrom::Start(0)).unwrap();
313            write.write_all(crate::fd_lock::LOCK_TOKEN).unwrap();
314        }
315
316        // Release the write lock and acquire a new read lock.
317        drop(write);
318        _read = lock.read().unwrap();
319    }
320
321    test_debug!("- copying template dir from {}", tpath.display());
322    pretty_err(target, fs::create_dir_all(target));
323    pretty_err(target, copy_dir(tpath, target));
324}
325
326/// Compile the project with a lock for the cache.
327pub fn get_compiled(project: &mut Project) -> ProjectCompileOutput {
328    let lock_file_path = project.sources_path().join(".lock");
329    // We need to use a file lock because `cargo-nextest` runs tests in different processes.
330    // This is similar to `initialize`, see its comments for more details.
331    let mut lock = crate::fd_lock::new_lock(&lock_file_path);
332    let read = lock.read().unwrap();
333    let out;
334
335    let mut write = None;
336    if !project.cache_path().exists() || !crate::fd_lock::lock_exists(&lock_file_path) {
337        drop(read);
338        write = Some(lock.write().unwrap());
339        test_debug!("cache miss for {}", lock_file_path.display());
340    } else {
341        test_debug!("cache hit for {}", lock_file_path.display());
342    }
343
344    if project.compiler.vyper.is_none() {
345        project.compiler.vyper = Some(get_vyper());
346    }
347
348    test_debug!("compiling {}", lock_file_path.display());
349    out = project.compile().unwrap();
350    test_debug!("compiled {}", lock_file_path.display());
351
352    if out.has_compiler_errors() {
353        panic!("Compiled with errors:\n{out}");
354    }
355
356    if let Some(write) = &mut write {
357        write.write_all(crate::fd_lock::LOCK_TOKEN).unwrap();
358    }
359
360    out
361}
362
363/// Installs Vyper if it's not already present.
364pub fn get_vyper() -> Vyper {
365    static VYPER: LazyLock<PathBuf> = LazyLock::new(|| std::env::temp_dir().join("vyper"));
366
367    if let Ok(vyper) = Vyper::new("vyper") {
368        return vyper;
369    }
370    if let Ok(vyper) = Vyper::new(&*VYPER) {
371        return vyper;
372    }
373    return RuntimeOrHandle::new().block_on(install());
374
375    async fn install() -> Vyper {
376        #[cfg(target_family = "unix")]
377        use std::{fs::Permissions, os::unix::fs::PermissionsExt};
378
379        let path = VYPER.as_path();
380        let mut file = File::create(path).unwrap();
381        if let Err(e) = file.try_lock() {
382            if let fs::TryLockError::WouldBlock = e {
383                file.lock().unwrap();
384                assert!(path.exists());
385                return Vyper::new(path).unwrap();
386            }
387            file.lock().unwrap();
388        }
389
390        let suffix = match svm::platform() {
391            svm::Platform::MacOsAarch64 => "darwin",
392            svm::Platform::LinuxAmd64 => "linux",
393            svm::Platform::WindowsAmd64 => "windows.exe",
394            platform => panic!(
395                "unsupported platform {platform:?} for installing vyper, \
396                 install it manually and add it to $PATH"
397            ),
398        };
399        let url = format!(
400            "https://github.com/vyperlang/vyper/releases/download/v0.4.3/vyper.0.4.3+commit.bff19ea2.{suffix}"
401        );
402
403        test_debug!("downloading vyper from {url}");
404        let res = reqwest::Client::builder().build().unwrap().get(url).send().await.unwrap();
405
406        assert!(res.status().is_success());
407
408        let bytes = res.bytes().await.unwrap();
409
410        file.write_all(&bytes).unwrap();
411
412        #[cfg(target_family = "unix")]
413        file.set_permissions(Permissions::from_mode(0o755)).unwrap();
414
415        Vyper::new(path).unwrap()
416    }
417}
418
419/// Clones a remote repository into the specified directory. Panics if the command fails.
420pub fn clone_remote(repo_url: &str, target_dir: &str, recursive: bool) {
421    let mut cmd = Command::new("git");
422    cmd.args(["clone"]);
423    if recursive {
424        cmd.args(["--recursive", "--shallow-submodules"]);
425    } else {
426        cmd.args(["--depth=1", "--no-checkout", "--filter=blob:none", "--no-recurse-submodules"]);
427    }
428    cmd.args([repo_url, target_dir]);
429    test_debug!("{cmd:?}");
430    let status = cmd.status().unwrap();
431    if !status.success() {
432        panic!("git clone failed: {status}");
433    }
434}
435
436/// Setup an empty test project and return a command pointing to the forge
437/// executable whose CWD is set to the project's root.
438///
439/// The name given will be used to create the directory. Generally, it should
440/// correspond to the test name.
441#[track_caller]
442pub fn setup_forge(name: &str, style: PathStyle) -> (TestProject, TestCommand) {
443    setup_forge_project(TestProject::new(name, style))
444}
445
446pub fn setup_forge_project(test: TestProject) -> (TestProject, TestCommand) {
447    let cmd = test.forge_command();
448    (test, cmd)
449}
450
451/// How to initialize a remote git project
452#[derive(Clone, Debug)]
453pub struct RemoteProject {
454    id: String,
455    run_build: bool,
456    run_commands: Vec<Vec<String>>,
457    path_style: PathStyle,
458}
459
460impl RemoteProject {
461    pub fn new(id: impl Into<String>) -> Self {
462        Self {
463            id: id.into(),
464            run_build: true,
465            run_commands: vec![],
466            path_style: PathStyle::Dapptools,
467        }
468    }
469
470    /// Whether to run `forge build`
471    pub fn set_build(mut self, run_build: bool) -> Self {
472        self.run_build = run_build;
473        self
474    }
475
476    /// Configures the project's pathstyle
477    pub fn path_style(mut self, path_style: PathStyle) -> Self {
478        self.path_style = path_style;
479        self
480    }
481
482    /// Add another command to run after cloning
483    pub fn cmd(mut self, cmd: impl IntoIterator<Item = impl Into<String>>) -> Self {
484        self.run_commands.push(cmd.into_iter().map(Into::into).collect());
485        self
486    }
487}
488
489impl<T: Into<String>> From<T> for RemoteProject {
490    fn from(id: T) -> Self {
491        Self::new(id)
492    }
493}
494
495/// Setups a new local forge project by cloning and initializing the `RemoteProject`
496///
497/// This will
498///   1. clone the prj, like "transmissions1/solmate"
499///   2. run `forge build`, if configured
500///   3. run additional commands
501///
502/// # Panics
503///
504/// If anything goes wrong during, checkout, build, or other commands are unsuccessful
505pub fn setup_forge_remote(prj: impl Into<RemoteProject>) -> (TestProject, TestCommand) {
506    try_setup_forge_remote(prj).unwrap()
507}
508
509/// Same as `setup_forge_remote` but not panicking
510pub fn try_setup_forge_remote(
511    config: impl Into<RemoteProject>,
512) -> Result<(TestProject, TestCommand)> {
513    let config = config.into();
514    let mut tmp = TempProject::checkout(&config.id).wrap_err("failed to checkout project")?;
515    tmp.project_mut().paths = config.path_style.paths(tmp.root())?;
516
517    let prj = TestProject::with_project(tmp);
518    if config.run_build {
519        let mut cmd = prj.forge_command();
520        cmd.arg("build").assert_success();
521    }
522    for addon in config.run_commands {
523        debug_assert!(!addon.is_empty());
524        let mut cmd = Command::new(&addon[0]);
525        if addon.len() > 1 {
526            cmd.args(&addon[1..]);
527        }
528        let status = cmd
529            .current_dir(prj.root())
530            .stdout(Stdio::null())
531            .stderr(Stdio::null())
532            .status()
533            .wrap_err_with(|| format!("Failed to execute {addon:?}"))?;
534        eyre::ensure!(status.success(), "Failed to execute command {:?}", addon);
535    }
536
537    let cmd = prj.forge_command();
538    Ok((prj, cmd))
539}
540
541pub fn setup_cast(name: &str, style: PathStyle) -> (TestProject, TestCommand) {
542    setup_cast_project(TestProject::new(name, style))
543}
544
545pub fn setup_cast_project(test: TestProject) -> (TestProject, TestCommand) {
546    let cmd = test.cast_command();
547    (test, cmd)
548}
549
550/// `TestProject` represents a temporary project to run tests against.
551///
552/// Test projects are created from a global atomic counter to avoid duplicates.
553#[derive(Clone, Debug)]
554pub struct TestProject<
555    T: ArtifactOutput<CompilerContract = Contract> + Default = ConfigurableArtifacts,
556> {
557    /// The directory in which this test executable is running.
558    exe_root: PathBuf,
559    /// The project in which the test should run.
560    inner: Arc<TempProject<MultiCompiler, T>>,
561}
562
563impl TestProject {
564    /// Create a new test project with the given name. The name
565    /// does not need to be distinct for each invocation, but should correspond
566    /// to a logical grouping of tests.
567    pub fn new(name: &str, style: PathStyle) -> Self {
568        let id = NEXT_ID.fetch_add(1, Ordering::SeqCst);
569        let project = pretty_err(name, TempProject::with_style(&format!("{name}-{id}"), style));
570        Self::with_project(project)
571    }
572
573    pub fn with_project(project: TempProject) -> Self {
574        init_tracing();
575        let this = env::current_exe().unwrap();
576        let exe_root = canonicalize(this.parent().expect("executable's directory"));
577        Self { exe_root, inner: Arc::new(project) }
578    }
579
580    /// Returns the root path of the project's workspace.
581    pub fn root(&self) -> &Path {
582        self.inner.root()
583    }
584
585    /// Returns the paths config.
586    pub fn paths(&self) -> &ProjectPathsConfig {
587        self.inner.paths()
588    }
589
590    /// Returns the path to the project's `foundry.toml` file.
591    pub fn config(&self) -> PathBuf {
592        self.root().join(Config::FILE_NAME)
593    }
594
595    /// Returns the path to the project's cache file.
596    pub fn cache(&self) -> &PathBuf {
597        &self.paths().cache
598    }
599
600    /// Returns the path to the project's artifacts directory.
601    pub fn artifacts(&self) -> &PathBuf {
602        &self.paths().artifacts
603    }
604
605    /// Removes the project's cache and artifacts directory.
606    pub fn clear(&self) {
607        self.clear_cache();
608        self.clear_artifacts();
609    }
610
611    /// Removes this project's cache file.
612    pub fn clear_cache(&self) {
613        let _ = fs::remove_file(self.cache());
614    }
615
616    /// Removes this project's artifacts directory.
617    pub fn clear_artifacts(&self) {
618        let _ = fs::remove_dir_all(self.artifacts());
619    }
620
621    /// Removes the entire cache directory (including fuzz, invariant, and test-failures caches).
622    pub fn clear_cache_dir(&self) {
623        let _ = fs::remove_dir_all(self.root().join("cache"));
624    }
625
626    /// Updates the project's config with the given function.
627    pub fn update_config(&self, f: impl FnOnce(&mut Config)) {
628        self._update_config(Box::new(f));
629    }
630
631    fn _update_config(&self, f: Box<dyn FnOnce(&mut Config) + '_>) {
632        let mut config = self
633            .config()
634            .exists()
635            .then_some(())
636            .and_then(|()| Config::load_with_root(self.root()).ok())
637            .unwrap_or_default();
638        config.remappings.clear();
639        f(&mut config);
640        self.write_config(config);
641    }
642
643    /// Writes the given config as toml to `foundry.toml`.
644    #[doc(hidden)] // Prefer `update_config`.
645    pub fn write_config(&self, config: Config) {
646        let file = self.config();
647        pretty_err(&file, fs::write(&file, config.to_string_pretty().unwrap()));
648    }
649
650    /// Writes [`rpc_endpoints`] to the project's config.
651    pub fn add_rpc_endpoints(&self) {
652        self.update_config(|config| {
653            config.rpc_endpoints = rpc_endpoints();
654        });
655    }
656
657    /// Adds a source file to the project.
658    pub fn add_source(&self, name: &str, contents: &str) -> PathBuf {
659        self.inner.add_source(name, Self::add_source_prelude(contents)).unwrap()
660    }
661
662    /// Adds a source file to the project. Prefer using `add_source` instead.
663    pub fn add_raw_source(&self, name: &str, contents: &str) -> PathBuf {
664        self.inner.add_source(name, contents).unwrap()
665    }
666
667    /// Adds a script file to the project.
668    pub fn add_script(&self, name: &str, contents: &str) -> PathBuf {
669        self.inner.add_script(name, Self::add_source_prelude(contents)).unwrap()
670    }
671
672    /// Adds a script file to the project. Prefer using `add_script` instead.
673    pub fn add_raw_script(&self, name: &str, contents: &str) -> PathBuf {
674        self.inner.add_script(name, contents).unwrap()
675    }
676
677    /// Adds a test file to the project.
678    pub fn add_test(&self, name: &str, contents: &str) -> PathBuf {
679        self.inner.add_test(name, Self::add_source_prelude(contents)).unwrap()
680    }
681
682    /// Adds a test file to the project. Prefer using `add_test` instead.
683    pub fn add_raw_test(&self, name: &str, contents: &str) -> PathBuf {
684        self.inner.add_test(name, contents).unwrap()
685    }
686
687    /// Adds a library file to the project.
688    pub fn add_lib(&self, name: &str, contents: &str) -> PathBuf {
689        self.inner.add_lib(name, Self::add_source_prelude(contents)).unwrap()
690    }
691
692    /// Adds a library file to the project. Prefer using `add_lib` instead.
693    pub fn add_raw_lib(&self, name: &str, contents: &str) -> PathBuf {
694        self.inner.add_lib(name, contents).unwrap()
695    }
696
697    fn add_source_prelude(s: &str) -> String {
698        let mut s = s.to_string();
699        if !s.contains("pragma solidity") {
700            s = format!("pragma solidity ={SOLC_VERSION};\n{s}");
701        }
702        if !s.contains("// SPDX") {
703            s = format!("// SPDX-License-Identifier: MIT OR Apache-2.0\n{s}");
704        }
705        s
706    }
707
708    /// Asserts that the `<root>/foundry.toml` file exists.
709    #[track_caller]
710    pub fn assert_config_exists(&self) {
711        assert!(self.config().exists());
712    }
713
714    /// Asserts that the `<root>/cache/sol-files-cache.json` file exists.
715    #[track_caller]
716    pub fn assert_cache_exists(&self) {
717        assert!(self.cache().exists());
718    }
719
720    /// Asserts that the `<root>/out` file exists.
721    #[track_caller]
722    pub fn assert_artifacts_dir_exists(&self) {
723        assert!(self.paths().artifacts.exists());
724    }
725
726    /// Creates all project dirs and ensure they were created
727    #[track_caller]
728    pub fn assert_create_dirs_exists(&self) {
729        self.paths().create_all().unwrap_or_else(|_| panic!("Failed to create project paths"));
730        CompilerCache::<SolcSettings>::default()
731            .write(&self.paths().cache)
732            .expect("Failed to create cache");
733        self.assert_all_paths_exist();
734    }
735
736    /// Ensures that the given layout exists
737    #[track_caller]
738    pub fn assert_style_paths_exist(&self, style: PathStyle) {
739        let paths = style.paths(&self.paths().root).unwrap();
740        config_paths_exist(&paths, self.inner.project().cached);
741    }
742
743    /// Copies the project's root directory to the given target
744    #[track_caller]
745    pub fn copy_to(&self, target: impl AsRef<Path>) {
746        let target = target.as_ref();
747        pretty_err(target, fs::create_dir_all(target));
748        pretty_err(target, copy_dir(self.root(), target));
749    }
750
751    /// Creates a file with contents `contents` in the test project's directory. The
752    /// file will be deleted when the project is dropped.
753    pub fn create_file(&self, path: impl AsRef<Path>, contents: &str) -> PathBuf {
754        let path = path.as_ref();
755        if !path.is_relative() {
756            panic!("create_file(): file path is absolute");
757        }
758        let path = self.root().join(path);
759        if let Some(parent) = path.parent() {
760            pretty_err(parent, std::fs::create_dir_all(parent));
761        }
762        let file = pretty_err(&path, File::create(&path));
763        let mut writer = BufWriter::new(file);
764        pretty_err(&path, writer.write_all(contents.as_bytes()));
765        path
766    }
767
768    /// Adds DSTest as a source under "test.sol"
769    pub fn insert_ds_test(&self) -> PathBuf {
770        self.add_source("test.sol", include_str!("../../../testdata/utils/DSTest.sol"))
771    }
772
773    /// Adds custom test utils under the "test/utils" directory.
774    pub fn insert_utils(&self) {
775        self.add_test("utils/DSTest.sol", include_str!("../../../testdata/utils/DSTest.sol"));
776        self.add_test("utils/Test.sol", include_str!("../../../testdata/utils/Test.sol"));
777        self.add_test("utils/Vm.sol", include_str!("../../../testdata/utils/Vm.sol"));
778        self.add_test("utils/console.sol", include_str!("../../../testdata/utils/console.sol"));
779    }
780
781    /// Adds `console.sol` as a source under "console.sol"
782    pub fn insert_console(&self) -> PathBuf {
783        let s = include_str!("../../../testdata/utils/console.sol");
784        self.add_source("console.sol", s)
785    }
786
787    /// Adds `Vm.sol` as a source under "Vm.sol"
788    pub fn insert_vm(&self) -> PathBuf {
789        let s = include_str!("../../../testdata/utils/Vm.sol");
790        self.add_source("Vm.sol", s)
791    }
792
793    /// Asserts all project paths exist. These are:
794    /// - sources
795    /// - artifacts
796    /// - libs
797    /// - cache
798    pub fn assert_all_paths_exist(&self) {
799        let paths = self.paths();
800        config_paths_exist(paths, self.inner.project().cached);
801    }
802
803    /// Asserts that the artifacts dir and cache don't exist
804    pub fn assert_cleaned(&self) {
805        let paths = self.paths();
806        assert!(!paths.cache.exists());
807        assert!(!paths.artifacts.exists());
808    }
809
810    /// Creates a new command that is set to use the forge executable for this project
811    #[track_caller]
812    pub fn forge_command(&self) -> TestCommand {
813        let cmd = self.forge_bin();
814        let _lock = CURRENT_DIR_LOCK.lock();
815        TestCommand {
816            project: self.clone(),
817            cmd,
818            current_dir_lock: None,
819            saved_cwd: pretty_err("<current dir>", std::env::current_dir()),
820            stdin: None,
821            redact_output: true,
822        }
823    }
824
825    /// Creates a new command that is set to use the cast executable for this project
826    pub fn cast_command(&self) -> TestCommand {
827        let mut cmd = self.cast_bin();
828        cmd.current_dir(self.inner.root());
829        let _lock = CURRENT_DIR_LOCK.lock();
830        TestCommand {
831            project: self.clone(),
832            cmd,
833            current_dir_lock: None,
834            saved_cwd: pretty_err("<current dir>", std::env::current_dir()),
835            stdin: None,
836            redact_output: true,
837        }
838    }
839
840    /// Returns the path to the forge executable.
841    pub fn forge_bin(&self) -> Command {
842        let mut cmd = Command::new(self.forge_path());
843        cmd.current_dir(self.inner.root());
844        // Disable color output for comparisons; can be overridden with `--color always`.
845        cmd.env("NO_COLOR", "1");
846        cmd
847    }
848
849    fn forge_path(&self) -> PathBuf {
850        canonicalize(self.exe_root.join(format!("../forge{}", env::consts::EXE_SUFFIX)))
851    }
852
853    /// Returns the path to the cast executable.
854    pub fn cast_bin(&self) -> Command {
855        let cast = canonicalize(self.exe_root.join(format!("../cast{}", env::consts::EXE_SUFFIX)));
856        let mut cmd = Command::new(cast);
857        // disable color output for comparisons
858        cmd.env("NO_COLOR", "1");
859        cmd
860    }
861
862    /// Returns the `Config` as spit out by `forge config`
863    pub fn config_from_output<I, A>(&self, args: I) -> Config
864    where
865        I: IntoIterator<Item = A>,
866        A: AsRef<OsStr>,
867    {
868        let mut cmd = self.forge_bin();
869        cmd.arg("config").arg("--root").arg(self.root()).args(args).arg("--json");
870        let output = cmd.output().unwrap();
871        let c = lossy_string(&output.stdout);
872        let config: Config = serde_json::from_str(c.as_ref()).unwrap();
873        config.sanitized()
874    }
875
876    /// Removes all files and dirs inside the project's root dir
877    pub fn wipe(&self) {
878        pretty_err(self.root(), fs::remove_dir_all(self.root()));
879        pretty_err(self.root(), fs::create_dir_all(self.root()));
880    }
881
882    /// Removes all contract files from `src`, `test`, `script`
883    pub fn wipe_contracts(&self) {
884        fn rm_create(path: &Path) {
885            pretty_err(path, fs::remove_dir_all(path));
886            pretty_err(path, fs::create_dir(path));
887        }
888        rm_create(&self.paths().sources);
889        rm_create(&self.paths().tests);
890        rm_create(&self.paths().scripts);
891    }
892
893    /// Initializes the default contracts (Counter.sol, Counter.t.sol, Counter.s.sol).
894    ///
895    /// This is useful for tests that need the default contracts created by `forge init`.
896    /// Most tests should not need this method, as the default behavior is to create an empty
897    /// project.
898    pub fn initialize_default_contracts(&self) {
899        self.add_raw_source(
900            "Counter.sol",
901            include_str!("../../forge/assets/solidity/CounterTemplate.sol"),
902        );
903        self.add_raw_test(
904            "Counter.t.sol",
905            include_str!("../../forge/assets/solidity/CounterTemplate.t.sol"),
906        );
907        self.add_raw_script(
908            "Counter.s.sol",
909            include_str!("../../forge/assets/solidity/CounterTemplate.s.sol"),
910        );
911    }
912}
913
914impl Drop for TestCommand {
915    fn drop(&mut self) {
916        let _lock = self.current_dir_lock.take().unwrap_or_else(|| CURRENT_DIR_LOCK.lock());
917        if self.saved_cwd.exists() {
918            let _ = std::env::set_current_dir(&self.saved_cwd);
919        }
920    }
921}
922
923fn config_paths_exist(paths: &ProjectPathsConfig, cached: bool) {
924    if cached {
925        assert!(paths.cache.exists());
926    }
927    assert!(paths.sources.exists());
928    assert!(paths.artifacts.exists());
929    paths.libraries.iter().for_each(|lib| assert!(lib.exists()));
930}
931
932#[track_caller]
933pub fn pretty_err<T, E: std::error::Error>(path: impl AsRef<Path>, res: Result<T, E>) -> T {
934    match res {
935        Ok(t) => t,
936        Err(err) => panic!("{}: {err}", path.as_ref().display()),
937    }
938}
939
940pub fn read_string(path: impl AsRef<Path>) -> String {
941    let path = path.as_ref();
942    pretty_err(path, std::fs::read_to_string(path))
943}
944
945/// A simple wrapper around a Command with some conveniences.
946pub struct TestCommand {
947    saved_cwd: PathBuf,
948    /// The project used to launch this command.
949    project: TestProject,
950    /// The actual command we use to control the process.
951    cmd: Command,
952    // initial: Command,
953    current_dir_lock: Option<parking_lot::MutexGuard<'static, ()>>,
954    stdin: Option<Vec<u8>>,
955    /// If true, command output is redacted.
956    redact_output: bool,
957}
958
959impl TestCommand {
960    /// Returns a mutable reference to the underlying command.
961    pub fn cmd(&mut self) -> &mut Command {
962        &mut self.cmd
963    }
964
965    /// Replaces the underlying command.
966    pub fn set_cmd(&mut self, cmd: Command) -> &mut Self {
967        self.cmd = cmd;
968        self
969    }
970
971    /// Resets the command to the default `forge` command.
972    pub fn forge_fuse(&mut self) -> &mut Self {
973        self.set_cmd(self.project.forge_bin())
974    }
975
976    /// Resets the command to the default `cast` command.
977    pub fn cast_fuse(&mut self) -> &mut Self {
978        self.set_cmd(self.project.cast_bin())
979    }
980
981    /// Sets the current working directory.
982    pub fn set_current_dir(&mut self, p: impl AsRef<Path>) {
983        drop(self.current_dir_lock.take());
984        let lock = CURRENT_DIR_LOCK.lock();
985        self.current_dir_lock = Some(lock);
986        let p = p.as_ref();
987        pretty_err(p, std::env::set_current_dir(p));
988    }
989
990    /// Add an argument to pass to the command.
991    pub fn arg<A: AsRef<OsStr>>(&mut self, arg: A) -> &mut Self {
992        self.cmd.arg(arg);
993        self
994    }
995
996    /// Add any number of arguments to the command.
997    pub fn args<I, A>(&mut self, args: I) -> &mut Self
998    where
999        I: IntoIterator<Item = A>,
1000        A: AsRef<OsStr>,
1001    {
1002        self.cmd.args(args);
1003        self
1004    }
1005
1006    /// Set the stdin bytes for the next command.
1007    pub fn stdin(&mut self, stdin: impl Into<Vec<u8>>) -> &mut Self {
1008        self.stdin = Some(stdin.into());
1009        self
1010    }
1011
1012    /// Convenience function to add `--root project.root()` argument
1013    pub fn root_arg(&mut self) -> &mut Self {
1014        let root = self.project.root().to_path_buf();
1015        self.arg("--root").arg(root)
1016    }
1017
1018    /// Set the environment variable `k` to value `v` for the command.
1019    pub fn env(&mut self, k: impl AsRef<OsStr>, v: impl AsRef<OsStr>) {
1020        self.cmd.env(k, v);
1021    }
1022
1023    /// Set the environment variable `k` to value `v` for the command.
1024    pub fn envs<I, K, V>(&mut self, envs: I)
1025    where
1026        I: IntoIterator<Item = (K, V)>,
1027        K: AsRef<OsStr>,
1028        V: AsRef<OsStr>,
1029    {
1030        self.cmd.envs(envs);
1031    }
1032
1033    /// Unsets the environment variable `k` for the command.
1034    pub fn unset_env(&mut self, k: impl AsRef<OsStr>) {
1035        self.cmd.env_remove(k);
1036    }
1037
1038    /// Set the working directory for this command.
1039    ///
1040    /// Note that this does not need to be called normally, since the creation
1041    /// of this TestCommand causes its working directory to be set to the
1042    /// test's directory automatically.
1043    pub fn current_dir<P: AsRef<Path>>(&mut self, dir: P) -> &mut Self {
1044        self.cmd.current_dir(dir);
1045        self
1046    }
1047
1048    /// Returns the `Config` as spit out by `forge config`
1049    #[track_caller]
1050    pub fn config(&mut self) -> Config {
1051        self.cmd.args(["config", "--json"]);
1052        let output = self.assert().success().get_output().stdout_lossy();
1053        self.forge_fuse();
1054        serde_json::from_str(output.as_ref()).unwrap()
1055    }
1056
1057    /// Runs `git init` inside the project's dir
1058    #[track_caller]
1059    pub fn git_init(&self) {
1060        let mut cmd = Command::new("git");
1061        cmd.arg("init").current_dir(self.project.root());
1062        let output = OutputAssert::new(cmd.output().unwrap());
1063        output.success();
1064    }
1065
1066    /// Runs `git submodule status` inside the project's dir
1067    #[track_caller]
1068    pub fn git_submodule_status(&self) -> Output {
1069        let mut cmd = Command::new("git");
1070        cmd.arg("submodule").arg("status").current_dir(self.project.root());
1071        cmd.output().unwrap()
1072    }
1073
1074    /// Runs `git add .` inside the project's dir
1075    #[track_caller]
1076    pub fn git_add(&self) {
1077        let mut cmd = Command::new("git");
1078        cmd.current_dir(self.project.root());
1079        cmd.arg("add").arg(".");
1080        let output = OutputAssert::new(cmd.output().unwrap());
1081        output.success();
1082    }
1083
1084    /// Runs `git commit .` inside the project's dir
1085    #[track_caller]
1086    pub fn git_commit(&self, msg: &str) {
1087        let mut cmd = Command::new("git");
1088        cmd.current_dir(self.project.root());
1089        cmd.arg("commit").arg("-m").arg(msg);
1090        let output = OutputAssert::new(cmd.output().unwrap());
1091        output.success();
1092    }
1093
1094    /// Runs the command, returning a [`snapbox`] object to assert the command output.
1095    #[track_caller]
1096    pub fn assert_with(&mut self, f: &[RegexRedaction]) -> OutputAssert {
1097        let assert = OutputAssert::new(self.execute());
1098        if self.redact_output {
1099            let mut redactions = test_redactions();
1100            insert_redactions(f, &mut redactions);
1101            return assert.with_assert(
1102                snapbox::Assert::new()
1103                    .action_env(snapbox::assert::DEFAULT_ACTION_ENV)
1104                    .redact_with(redactions),
1105            );
1106        }
1107        assert
1108    }
1109
1110    /// Runs the command, returning a [`snapbox`] object to assert the command output.
1111    #[track_caller]
1112    pub fn assert(&mut self) -> OutputAssert {
1113        self.assert_with(&[])
1114    }
1115
1116    /// Runs the command and asserts that it resulted in success.
1117    #[track_caller]
1118    pub fn assert_success(&mut self) -> OutputAssert {
1119        self.assert().success()
1120    }
1121
1122    /// Runs the command and asserts that it resulted in success, with expected JSON data.
1123    #[track_caller]
1124    pub fn assert_json_stdout(&mut self, expected: impl IntoData) {
1125        let expected = expected.is(snapbox::data::DataFormat::Json).unordered();
1126        let stdout = self.assert_success().get_output().stdout.clone();
1127        let actual = stdout.into_data().is(snapbox::data::DataFormat::Json).unordered();
1128        assert_data_eq!(actual, expected);
1129    }
1130
1131    /// Runs the command and asserts that it resulted in the expected outcome and JSON data.
1132    #[track_caller]
1133    pub fn assert_json_stderr(&mut self, success: bool, expected: impl IntoData) {
1134        let expected = expected.is(snapbox::data::DataFormat::Json).unordered();
1135        let stderr = if success { self.assert_success() } else { self.assert_failure() }
1136            .get_output()
1137            .stderr
1138            .clone();
1139        let actual = stderr.into_data().is(snapbox::data::DataFormat::Json).unordered();
1140        assert_data_eq!(actual, expected);
1141    }
1142
1143    /// Runs the command and asserts that it **succeeded** nothing was printed to stdout.
1144    #[track_caller]
1145    pub fn assert_empty_stdout(&mut self) {
1146        self.assert_success().stdout_eq(Data::new());
1147    }
1148
1149    /// Runs the command and asserts that it failed.
1150    #[track_caller]
1151    pub fn assert_failure(&mut self) -> OutputAssert {
1152        self.assert().failure()
1153    }
1154
1155    /// Runs the command and asserts that the exit code is `expected`.
1156    #[track_caller]
1157    pub fn assert_code(&mut self, expected: i32) -> OutputAssert {
1158        self.assert().code(expected)
1159    }
1160
1161    /// Runs the command and asserts that it **failed** nothing was printed to stderr.
1162    #[track_caller]
1163    pub fn assert_empty_stderr(&mut self) {
1164        self.assert_failure().stderr_eq(Data::new());
1165    }
1166
1167    /// Runs the command with a temporary file argument and asserts that the contents of the file
1168    /// match the given data.
1169    #[track_caller]
1170    pub fn assert_file(&mut self, data: impl IntoData) {
1171        self.assert_file_with(|this, path| _ = this.arg(path).assert_success(), data);
1172    }
1173
1174    /// Creates a temporary file, passes it to `f`, then asserts that the contents of the file match
1175    /// the given data.
1176    #[track_caller]
1177    pub fn assert_file_with(&mut self, f: impl FnOnce(&mut Self, &Path), data: impl IntoData) {
1178        let file = tempfile::NamedTempFile::new().expect("couldn't create temporary file");
1179        f(self, file.path());
1180        assert_data_eq!(Data::read_from(file.path(), None), data);
1181    }
1182
1183    /// Does not apply [`snapbox`] redactions to the command output.
1184    pub fn with_no_redact(&mut self) -> &mut Self {
1185        self.redact_output = false;
1186        self
1187    }
1188
1189    /// Executes command, applies stdin function and returns output
1190    #[track_caller]
1191    pub fn execute(&mut self) -> Output {
1192        self.try_execute().unwrap()
1193    }
1194
1195    #[track_caller]
1196    pub fn try_execute(&mut self) -> std::io::Result<Output> {
1197        test_debug!("executing {:?}", self.cmd);
1198        let mut child =
1199            self.cmd.stdout(Stdio::piped()).stderr(Stdio::piped()).stdin(Stdio::piped()).spawn()?;
1200        if let Some(bytes) = self.stdin.take() {
1201            child.stdin.take().unwrap().write_all(&bytes)?;
1202        }
1203        let output = child.wait_with_output()?;
1204        test_debug!("exited with {}", output.status);
1205        test_trace!("\n--- stdout ---\n{}\n--- /stdout ---", output.stdout_lossy());
1206        test_trace!("\n--- stderr ---\n{}\n--- /stderr ---", output.stderr_lossy());
1207        Ok(output)
1208    }
1209}
1210
1211fn test_redactions() -> snapbox::Redactions {
1212    static REDACTIONS: LazyLock<snapbox::Redactions> = LazyLock::new(|| {
1213        make_redactions(&[
1214            ("[SOLC_VERSION]", r"Solc( version)? \d+.\d+.\d+"),
1215            ("[ELAPSED]", r"(finished )?in \d+(\.\d+)?\w?s( \(.*?s CPU time\))?"),
1216            ("[GAS]", r"[Gg]as( used)?: \d+"),
1217            ("[GAS_COST]", r"[Gg]as cost\s*\(\d+\)"),
1218            ("[GAS_LIMIT]", r"[Gg]as limit\s*\(\d+\)"),
1219            ("[AVG_GAS]", r"μ: \d+, ~: \d+"),
1220            ("[FILE]", r"-->.*\.sol"),
1221            ("[FILE]", r"Location(.|\n)*\.rs(.|\n)*Backtrace"),
1222            ("[COMPILING_FILES]", r"Compiling \d+ files?"),
1223            ("[TX_HASH]", r"Transaction hash: 0x[0-9A-Fa-f]{64}"),
1224            ("[ADDRESS]", r"Address: +0x[0-9A-Fa-f]{40}"),
1225            ("[PUBLIC_KEY]", r"Public key: +0x[0-9A-Fa-f]{128}"),
1226            ("[PRIVATE_KEY]", r"Private key: +0x[0-9A-Fa-f]{64}"),
1227            ("[UPDATING_DEPENDENCIES]", r"Updating dependencies in .*"),
1228            ("[SAVED_TRANSACTIONS]", r"Transactions saved to: .*\.json"),
1229            ("[SAVED_SENSITIVE_VALUES]", r"Sensitive values saved to: .*\.json"),
1230            ("[ESTIMATED_GAS_PRICE]", r"Estimated gas price:\s*(\d+(\.\d+)?)\s*gwei"),
1231            ("[ESTIMATED_TOTAL_GAS_USED]", r"Estimated total gas used for script: \d+"),
1232            (
1233                "[ESTIMATED_AMOUNT_REQUIRED]",
1234                r"Estimated amount required:\s*(\d+(\.\d+)?)\s*[A-Z]{3}",
1235            ),
1236        ])
1237    });
1238    REDACTIONS.clone()
1239}
1240
1241/// A tuple of a placeholder and a regex replacement string.
1242type RegexRedaction = (&'static str, &'static str);
1243
1244/// Creates a [`snapbox`] redactions object from a list of regex redactions.
1245fn make_redactions(redactions: &[RegexRedaction]) -> snapbox::Redactions {
1246    let mut r = snapbox::Redactions::new();
1247    insert_redactions(redactions, &mut r);
1248    r
1249}
1250
1251fn insert_redactions(redactions: &[RegexRedaction], r: &mut snapbox::Redactions) {
1252    for &(placeholder, re) in redactions {
1253        r.insert(placeholder, Regex::new(re).expect(re)).expect(re);
1254    }
1255}
1256
1257/// Extension trait for [`Output`].
1258pub trait OutputExt {
1259    /// Returns the stdout as lossy string
1260    fn stdout_lossy(&self) -> String;
1261
1262    /// Returns the stderr as lossy string
1263    fn stderr_lossy(&self) -> String;
1264}
1265
1266impl OutputExt for Output {
1267    fn stdout_lossy(&self) -> String {
1268        lossy_string(&self.stdout)
1269    }
1270
1271    fn stderr_lossy(&self) -> String {
1272        lossy_string(&self.stderr)
1273    }
1274}
1275
1276pub fn lossy_string(bytes: &[u8]) -> String {
1277    String::from_utf8_lossy(bytes).replace("\r\n", "\n")
1278}
1279
1280fn canonicalize(path: impl AsRef<Path>) -> PathBuf {
1281    foundry_common::fs::canonicalize_path(path.as_ref())
1282        .unwrap_or_else(|_| path.as_ref().to_path_buf())
1283}