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
32pub const FORGE_STD_REVISION: &str = include_str!("../../../testdata/forge-std-rev");
34
35pub static IS_TTY: LazyLock<bool> = LazyLock::new(|| std::io::stdout().is_terminal());
37
38static TEMPLATE_PATH: LazyLock<PathBuf> =
41 LazyLock::new(|| env::temp_dir().join("foundry-forge-test-template"));
42
43static TEMPLATE_LOCK: LazyLock<PathBuf> =
46 LazyLock::new(|| env::temp_dir().join("foundry-forge-test-template.lock"));
47
48static NEXT_ID: AtomicUsize = AtomicUsize::new(0);
50
51pub const SOLC_VERSION: &str = "0.8.30";
53
54pub const OTHER_SOLC_VERSION: &str = "0.8.26";
58
59#[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 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 pub fn style(mut self, style: PathStyle) -> Self {
92 self.style = style;
93 self
94 }
95
96 pub fn fork_block(mut self, fork_block: u64) -> Self {
98 self.fork_block = Some(fork_block);
99 self
100 }
101
102 pub fn arg(mut self, arg: impl Into<String>) -> Self {
104 self.args.push(arg.into());
105 self
106 }
107
108 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 pub fn verbosity(mut self, verbosity: usize) -> Self {
120 self.verbosity = format!("-{}", "v".repeat(verbosity));
121 self
122 }
123
124 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 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 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 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 prj.wipe();
169
170 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 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 pub fn run(&self) {
221 let (prj, mut test_cmd) = self.setup_forge_prj(true);
222
223 self.run_install_commands(prj.root().to_str().unwrap());
225
226 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
243pub 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 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 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 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 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 cmd.forge_fuse().arg("build").assert_success();
303
304 let _ = fs::remove_dir_all(tpath);
306
307 pretty_err(tpath, copy_dir(prj.root(), tpath));
309
310 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 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
326pub fn get_compiled(project: &mut Project) -> ProjectCompileOutput {
328 let lock_file_path = project.sources_path().join(".lock");
329 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
363pub 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
419pub 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#[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#[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 pub fn set_build(mut self, run_build: bool) -> Self {
472 self.run_build = run_build;
473 self
474 }
475
476 pub fn path_style(mut self, path_style: PathStyle) -> Self {
478 self.path_style = path_style;
479 self
480 }
481
482 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
495pub fn setup_forge_remote(prj: impl Into<RemoteProject>) -> (TestProject, TestCommand) {
506 try_setup_forge_remote(prj).unwrap()
507}
508
509pub 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#[derive(Clone, Debug)]
554pub struct TestProject<
555 T: ArtifactOutput<CompilerContract = Contract> + Default = ConfigurableArtifacts,
556> {
557 exe_root: PathBuf,
559 inner: Arc<TempProject<MultiCompiler, T>>,
561}
562
563impl TestProject {
564 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 pub fn root(&self) -> &Path {
582 self.inner.root()
583 }
584
585 pub fn paths(&self) -> &ProjectPathsConfig {
587 self.inner.paths()
588 }
589
590 pub fn config(&self) -> PathBuf {
592 self.root().join(Config::FILE_NAME)
593 }
594
595 pub fn cache(&self) -> &PathBuf {
597 &self.paths().cache
598 }
599
600 pub fn artifacts(&self) -> &PathBuf {
602 &self.paths().artifacts
603 }
604
605 pub fn clear(&self) {
607 self.clear_cache();
608 self.clear_artifacts();
609 }
610
611 pub fn clear_cache(&self) {
613 let _ = fs::remove_file(self.cache());
614 }
615
616 pub fn clear_artifacts(&self) {
618 let _ = fs::remove_dir_all(self.artifacts());
619 }
620
621 pub fn clear_cache_dir(&self) {
623 let _ = fs::remove_dir_all(self.root().join("cache"));
624 }
625
626 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 #[doc(hidden)] 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 pub fn add_rpc_endpoints(&self) {
652 self.update_config(|config| {
653 config.rpc_endpoints = rpc_endpoints();
654 });
655 }
656
657 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 pub fn add_raw_source(&self, name: &str, contents: &str) -> PathBuf {
664 self.inner.add_source(name, contents).unwrap()
665 }
666
667 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 pub fn add_raw_script(&self, name: &str, contents: &str) -> PathBuf {
674 self.inner.add_script(name, contents).unwrap()
675 }
676
677 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 pub fn add_raw_test(&self, name: &str, contents: &str) -> PathBuf {
684 self.inner.add_test(name, contents).unwrap()
685 }
686
687 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 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 #[track_caller]
710 pub fn assert_config_exists(&self) {
711 assert!(self.config().exists());
712 }
713
714 #[track_caller]
716 pub fn assert_cache_exists(&self) {
717 assert!(self.cache().exists());
718 }
719
720 #[track_caller]
722 pub fn assert_artifacts_dir_exists(&self) {
723 assert!(self.paths().artifacts.exists());
724 }
725
726 #[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 #[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 #[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 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 pub fn insert_ds_test(&self) -> PathBuf {
770 self.add_source("test.sol", include_str!("../../../testdata/utils/DSTest.sol"))
771 }
772
773 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 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 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 pub fn assert_all_paths_exist(&self) {
799 let paths = self.paths();
800 config_paths_exist(paths, self.inner.project().cached);
801 }
802
803 pub fn assert_cleaned(&self) {
805 let paths = self.paths();
806 assert!(!paths.cache.exists());
807 assert!(!paths.artifacts.exists());
808 }
809
810 #[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 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 pub fn forge_bin(&self) -> Command {
842 let mut cmd = Command::new(self.forge_path());
843 cmd.current_dir(self.inner.root());
844 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 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 cmd.env("NO_COLOR", "1");
859 cmd
860 }
861
862 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 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 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 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
945pub struct TestCommand {
947 saved_cwd: PathBuf,
948 project: TestProject,
950 cmd: Command,
952 current_dir_lock: Option<parking_lot::MutexGuard<'static, ()>>,
954 stdin: Option<Vec<u8>>,
955 redact_output: bool,
957}
958
959impl TestCommand {
960 pub fn cmd(&mut self) -> &mut Command {
962 &mut self.cmd
963 }
964
965 pub fn set_cmd(&mut self, cmd: Command) -> &mut Self {
967 self.cmd = cmd;
968 self
969 }
970
971 pub fn forge_fuse(&mut self) -> &mut Self {
973 self.set_cmd(self.project.forge_bin())
974 }
975
976 pub fn cast_fuse(&mut self) -> &mut Self {
978 self.set_cmd(self.project.cast_bin())
979 }
980
981 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 pub fn arg<A: AsRef<OsStr>>(&mut self, arg: A) -> &mut Self {
992 self.cmd.arg(arg);
993 self
994 }
995
996 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 pub fn stdin(&mut self, stdin: impl Into<Vec<u8>>) -> &mut Self {
1008 self.stdin = Some(stdin.into());
1009 self
1010 }
1011
1012 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 pub fn env(&mut self, k: impl AsRef<OsStr>, v: impl AsRef<OsStr>) {
1020 self.cmd.env(k, v);
1021 }
1022
1023 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 pub fn unset_env(&mut self, k: impl AsRef<OsStr>) {
1035 self.cmd.env_remove(k);
1036 }
1037
1038 pub fn current_dir<P: AsRef<Path>>(&mut self, dir: P) -> &mut Self {
1044 self.cmd.current_dir(dir);
1045 self
1046 }
1047
1048 #[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 #[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 #[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 #[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 #[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 #[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 #[track_caller]
1112 pub fn assert(&mut self) -> OutputAssert {
1113 self.assert_with(&[])
1114 }
1115
1116 #[track_caller]
1118 pub fn assert_success(&mut self) -> OutputAssert {
1119 self.assert().success()
1120 }
1121
1122 #[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 #[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 #[track_caller]
1145 pub fn assert_empty_stdout(&mut self) {
1146 self.assert_success().stdout_eq(Data::new());
1147 }
1148
1149 #[track_caller]
1151 pub fn assert_failure(&mut self) -> OutputAssert {
1152 self.assert().failure()
1153 }
1154
1155 #[track_caller]
1157 pub fn assert_code(&mut self, expected: i32) -> OutputAssert {
1158 self.assert().code(expected)
1159 }
1160
1161 #[track_caller]
1163 pub fn assert_empty_stderr(&mut self) {
1164 self.assert_failure().stderr_eq(Data::new());
1165 }
1166
1167 #[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 #[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 pub fn with_no_redact(&mut self) -> &mut Self {
1185 self.redact_output = false;
1186 self
1187 }
1188
1189 #[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
1241type RegexRedaction = (&'static str, &'static str);
1243
1244fn 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
1257pub trait OutputExt {
1259 fn stdout_lossy(&self) -> String;
1261
1262 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}