1use crate::{init_tracing, rpc::rpc_endpoints};
2use eyre::{Result, WrapErr};
3use foundry_compilers::{
4 ArtifactOutput, ConfigurableArtifacts, PathStyle, ProjectPathsConfig, artifacts::Contract,
5 cache::CompilerCache, compilers::multi::MultiCompiler, project_util::TempProject,
6 solc::SolcSettings,
7};
8use foundry_config::Config;
9use parking_lot::Mutex;
10use regex::Regex;
11use snapbox::{Data, IntoData, assert_data_eq, cmd::OutputAssert};
12use std::{
13 env,
14 ffi::OsStr,
15 fs::{self, File},
16 io::{BufWriter, Write},
17 path::{Path, PathBuf},
18 process::{Command, Output, Stdio},
19 sync::{
20 Arc, LazyLock,
21 atomic::{AtomicUsize, Ordering},
22 },
23};
24
25use crate::util::{SOLC_VERSION, copy_dir_filtered, pretty_err};
26
27static CURRENT_DIR_LOCK: LazyLock<Mutex<()>> = LazyLock::new(|| Mutex::new(()));
28
29static NEXT_ID: AtomicUsize = AtomicUsize::new(0);
31
32pub fn clone_remote(repo_url: &str, target_dir: &str, recursive: bool) {
34 let mut cmd = Command::new("git");
35 cmd.args(["clone"]);
36 if recursive {
37 cmd.args(["--recursive", "--shallow-submodules"]);
38 } else {
39 cmd.args(["--depth=1", "--no-checkout", "--filter=blob:none", "--no-recurse-submodules"]);
40 }
41 cmd.args([repo_url, target_dir]);
42 test_debug!("{cmd:?}");
43 let status = cmd.status().unwrap();
44 assert!(status.success(), "git clone failed: {status}")
45}
46
47#[track_caller]
53pub fn setup_forge(name: &str, style: PathStyle) -> (TestProject, TestCommand) {
54 setup_forge_project(TestProject::new(name, style))
55}
56
57pub fn setup_forge_project(test: TestProject) -> (TestProject, TestCommand) {
58 let cmd = test.forge_command();
59 (test, cmd)
60}
61
62#[derive(Clone, Debug)]
64pub struct RemoteProject {
65 id: String,
66 run_build: bool,
67 run_commands: Vec<Vec<String>>,
68 path_style: PathStyle,
69}
70
71impl RemoteProject {
72 pub fn new(id: impl Into<String>) -> Self {
73 Self {
74 id: id.into(),
75 run_build: true,
76 run_commands: vec![],
77 path_style: PathStyle::Dapptools,
78 }
79 }
80
81 pub const fn set_build(mut self, run_build: bool) -> Self {
83 self.run_build = run_build;
84 self
85 }
86
87 pub const fn path_style(mut self, path_style: PathStyle) -> Self {
89 self.path_style = path_style;
90 self
91 }
92
93 pub fn cmd(mut self, cmd: impl IntoIterator<Item = impl Into<String>>) -> Self {
95 self.run_commands.push(cmd.into_iter().map(Into::into).collect());
96 self
97 }
98}
99
100impl<T: Into<String>> From<T> for RemoteProject {
101 fn from(id: T) -> Self {
102 Self::new(id)
103 }
104}
105
106pub fn setup_forge_remote(prj: impl Into<RemoteProject>) -> (TestProject, TestCommand) {
117 try_setup_forge_remote(prj).unwrap()
118}
119
120pub fn try_setup_forge_remote(
122 config: impl Into<RemoteProject>,
123) -> Result<(TestProject, TestCommand)> {
124 let config = config.into();
125 let mut tmp = TempProject::checkout(&config.id).wrap_err("failed to checkout project")?;
126 tmp.project_mut().paths = config.path_style.paths(tmp.root())?;
127
128 let prj = TestProject::with_project(tmp);
129 if config.run_build {
130 let mut cmd = prj.forge_command();
131 cmd.arg("build").assert_success();
132 }
133 for addon in config.run_commands {
134 debug_assert!(!addon.is_empty());
135 let mut cmd = Command::new(&addon[0]);
136 if addon.len() > 1 {
137 cmd.args(&addon[1..]);
138 }
139 let status = cmd
140 .current_dir(prj.root())
141 .stdout(Stdio::null())
142 .stderr(Stdio::null())
143 .status()
144 .wrap_err_with(|| format!("Failed to execute {addon:?}"))?;
145 eyre::ensure!(status.success(), "Failed to execute command {:?}", addon);
146 }
147
148 let cmd = prj.forge_command();
149 Ok((prj, cmd))
150}
151
152pub fn setup_cast(name: &str, style: PathStyle) -> (TestProject, TestCommand) {
153 setup_cast_project(TestProject::new(name, style))
154}
155
156pub fn setup_cast_project(test: TestProject) -> (TestProject, TestCommand) {
157 let cmd = test.cast_command();
158 (test, cmd)
159}
160
161#[derive(Clone, Debug)]
165pub struct TestProject<
166 T: ArtifactOutput<CompilerContract = Contract> + Default = ConfigurableArtifacts,
167> {
168 profile_dir: PathBuf,
171 pub(crate) inner: Arc<TempProject<MultiCompiler, T>>,
173}
174
175impl TestProject {
176 pub fn new(name: &str, style: PathStyle) -> Self {
180 let id = NEXT_ID.fetch_add(1, Ordering::SeqCst);
181 let project = pretty_err(name, TempProject::with_style(&format!("{name}-{id}"), style));
182 Self::with_project(project)
183 }
184
185 pub fn with_project(project: TempProject) -> Self {
186 init_tracing();
187 Self { profile_dir: cargo_profile_dir(), inner: Arc::new(project) }
188 }
189
190 pub fn root(&self) -> &Path {
192 self.inner.root()
193 }
194
195 pub fn paths(&self) -> &ProjectPathsConfig {
197 self.inner.paths()
198 }
199
200 pub fn config(&self) -> PathBuf {
202 self.root().join(Config::FILE_NAME)
203 }
204
205 pub fn cache(&self) -> &PathBuf {
207 &self.paths().cache
208 }
209
210 pub fn artifacts(&self) -> &PathBuf {
212 &self.paths().artifacts
213 }
214
215 pub fn clear(&self) {
217 self.clear_cache();
218 self.clear_artifacts();
219 }
220
221 pub fn clear_cache(&self) {
223 let _ = fs::remove_file(self.cache());
224 }
225
226 pub fn clear_artifacts(&self) {
228 let _ = fs::remove_dir_all(self.artifacts());
229 }
230
231 pub fn clear_cache_dir(&self) {
233 let _ = fs::remove_dir_all(self.root().join("cache"));
234 }
235
236 pub fn update_config(&self, f: impl FnOnce(&mut Config)) {
238 self._update_config(Box::new(f));
239 }
240
241 fn _update_config(&self, f: Box<dyn FnOnce(&mut Config) + '_>) {
242 let mut config = self
243 .config()
244 .exists()
245 .then_some(())
246 .and_then(|()| Config::load_with_root(self.root()).ok())
247 .unwrap_or_default();
248 config.remappings.clear();
249 f(&mut config);
250 self.write_config(config);
251 }
252
253 #[doc(hidden)] pub fn write_config(&self, config: Config) {
256 let file = self.config();
257 pretty_err(&file, fs::write(&file, config.to_string_pretty().unwrap()));
258 }
259
260 pub fn add_rpc_endpoints(&self) {
262 self.update_config(|config| {
263 config.rpc_endpoints = rpc_endpoints();
264 });
265 }
266
267 pub fn add_source(&self, name: &str, contents: &str) -> PathBuf {
269 self.inner.add_source(name, Self::add_source_prelude(contents)).unwrap()
270 }
271
272 pub fn add_raw_source(&self, name: &str, contents: &str) -> PathBuf {
274 self.inner.add_source(name, contents).unwrap()
275 }
276
277 pub fn add_script(&self, name: &str, contents: &str) -> PathBuf {
279 self.inner.add_script(name, Self::add_source_prelude(contents)).unwrap()
280 }
281
282 pub fn add_raw_script(&self, name: &str, contents: &str) -> PathBuf {
284 self.inner.add_script(name, contents).unwrap()
285 }
286
287 pub fn add_test(&self, name: &str, contents: &str) -> PathBuf {
289 self.inner.add_test(name, Self::add_source_prelude(contents)).unwrap()
290 }
291
292 pub fn add_raw_test(&self, name: &str, contents: &str) -> PathBuf {
294 self.inner.add_test(name, contents).unwrap()
295 }
296
297 pub fn add_lib(&self, name: &str, contents: &str) -> PathBuf {
299 self.inner.add_lib(name, Self::add_source_prelude(contents)).unwrap()
300 }
301
302 pub fn add_raw_lib(&self, name: &str, contents: &str) -> PathBuf {
304 self.inner.add_lib(name, contents).unwrap()
305 }
306
307 fn add_source_prelude(s: &str) -> String {
308 let mut s = s.to_string();
309 if !s.contains("pragma solidity") {
310 s = format!("pragma solidity ={SOLC_VERSION};\n{s}");
311 }
312 if !s.contains("// SPDX") {
313 s = format!("// SPDX-License-Identifier: MIT OR Apache-2.0\n{s}");
314 }
315 s
316 }
317
318 #[track_caller]
320 pub fn assert_config_exists(&self) {
321 assert!(self.config().exists());
322 }
323
324 #[track_caller]
326 pub fn assert_cache_exists(&self) {
327 assert!(self.cache().exists());
328 }
329
330 #[track_caller]
332 pub fn assert_artifacts_dir_exists(&self) {
333 assert!(self.paths().artifacts.exists());
334 }
335
336 #[track_caller]
338 pub fn assert_create_dirs_exists(&self) {
339 self.paths().create_all().unwrap_or_else(|_| panic!("Failed to create project paths"));
340 CompilerCache::<SolcSettings>::default()
341 .write(&self.paths().cache)
342 .expect("Failed to create cache");
343 self.assert_all_paths_exist();
344 }
345
346 #[track_caller]
348 pub fn assert_style_paths_exist(&self, style: PathStyle) {
349 let paths = style.paths(&self.paths().root).unwrap();
350 config_paths_exist(&paths, self.inner.project().cached);
351 }
352
353 #[track_caller]
355 pub fn copy_to(&self, target: impl AsRef<Path>) {
356 let target = target.as_ref();
357 pretty_err(target, fs::create_dir_all(target));
358 pretty_err(target, copy_dir_filtered(self.root(), target));
359 }
360
361 pub fn create_file(&self, path: impl AsRef<Path>, contents: &str) -> PathBuf {
364 let path = path.as_ref();
365 assert!(path.is_relative(), "create_file(): file path is absolute");
366 let path = self.root().join(path);
367 if let Some(parent) = path.parent() {
368 pretty_err(parent, std::fs::create_dir_all(parent));
369 }
370 let file = pretty_err(&path, File::create(&path));
371 let mut writer = BufWriter::new(file);
372 pretty_err(&path, writer.write_all(contents.as_bytes()));
373 path
374 }
375
376 pub fn insert_ds_test(&self) -> PathBuf {
378 self.add_source("test.sol", include_str!("../../../testdata/utils/DSTest.sol"))
379 }
380
381 pub fn insert_utils(&self) {
383 self.add_test("utils/DSTest.sol", include_str!("../../../testdata/utils/DSTest.sol"));
384 self.add_test("utils/Test.sol", include_str!("../../../testdata/utils/Test.sol"));
385 self.add_test("utils/Vm.sol", include_str!("../../../testdata/utils/Vm.sol"));
386 self.add_test("utils/console.sol", include_str!("../../../testdata/utils/console.sol"));
387 }
388
389 pub fn insert_console(&self) -> PathBuf {
391 let s = include_str!("../../../testdata/utils/console.sol");
392 self.add_source("console.sol", s)
393 }
394
395 pub fn insert_vm(&self) -> PathBuf {
397 let s = include_str!("../../../testdata/utils/Vm.sol");
398 self.add_source("Vm.sol", s)
399 }
400
401 pub fn assert_all_paths_exist(&self) {
407 let paths = self.paths();
408 config_paths_exist(paths, self.inner.project().cached);
409 }
410
411 pub fn assert_cleaned(&self) {
413 let paths = self.paths();
414 assert!(!paths.cache.exists());
415 assert!(!paths.artifacts.exists());
416 }
417
418 #[track_caller]
420 pub fn forge_command(&self) -> TestCommand {
421 let cmd = self.forge_bin();
422 let _lock = CURRENT_DIR_LOCK.lock();
423 TestCommand {
424 project: self.clone(),
425 cmd,
426 current_dir_lock: None,
427 saved_cwd: pretty_err("<current dir>", std::env::current_dir()),
428 stdin: None,
429 redact_output: true,
430 }
431 }
432
433 pub fn cast_command(&self) -> TestCommand {
435 let mut cmd = self.cast_bin();
436 cmd.current_dir(self.inner.root());
437 let _lock = CURRENT_DIR_LOCK.lock();
438 TestCommand {
439 project: self.clone(),
440 cmd,
441 current_dir_lock: None,
442 saved_cwd: pretty_err("<current dir>", std::env::current_dir()),
443 stdin: None,
444 redact_output: true,
445 }
446 }
447
448 pub fn forge_bin(&self) -> Command {
450 let mut cmd = Command::new(self.foundry_bin_path("forge"));
451 cmd.current_dir(self.inner.root());
452 cmd.env("NO_COLOR", "1");
454 cmd
455 }
456
457 pub fn foundry_bin_path(&self, name: &str) -> PathBuf {
459 canonicalize(self.profile_dir.join(format!("{name}{}", env::consts::EXE_SUFFIX)))
460 }
461
462 pub fn ensure_foundry_bin(&self, name: &str) -> PathBuf {
464 let bin = self.foundry_bin_path(name);
465 if bin.exists() {
466 return bin;
467 }
468
469 let package = format!("{name}@{}", env!("CARGO_PKG_VERSION"));
470 let (target_dir, profile) = cargo_build_target_dir_and_profile(&self.profile_dir);
471 let mut cmd = Command::new(env::var_os("CARGO").unwrap_or_else(|| "cargo".into()));
472 cmd.args(["build", "-p", &package, "--bin", name, "--manifest-path"])
473 .arg(Path::new(env!("CARGO_MANIFEST_DIR")).join("../../Cargo.toml"))
474 .arg("--target-dir")
475 .arg(target_dir);
476 if let Some(profile) = profile {
477 cmd.arg("--profile").arg(profile);
478 }
479
480 let output = cmd.output().expect("build Foundry sibling binary");
481 assert!(
482 output.status.success(),
483 "failed to build {name} for CLI test\nstdout:\n{}\nstderr:\n{}",
484 output.stdout_lossy(),
485 output.stderr_lossy(),
486 );
487
488 bin
489 }
490
491 pub fn cast_bin(&self) -> Command {
493 let mut cmd = Command::new(self.foundry_bin_path("cast"));
494 cmd.env("NO_COLOR", "1");
496 cmd
497 }
498
499 pub fn config_from_output<I, A>(&self, args: I) -> Config
501 where
502 I: IntoIterator<Item = A>,
503 A: AsRef<OsStr>,
504 {
505 let mut cmd = self.forge_bin();
506 cmd.arg("config").arg("--root").arg(self.root()).args(args).arg("--json");
507 let output = cmd.output().unwrap();
508 let c = lossy_string(&output.stdout);
509 let config: Config = serde_json::from_str(c.as_ref()).unwrap();
510 config.sanitized()
511 }
512
513 pub fn wipe(&self) {
515 pretty_err(self.root(), fs::remove_dir_all(self.root()));
516 pretty_err(self.root(), fs::create_dir_all(self.root()));
517 }
518
519 pub fn wipe_contracts(&self) {
521 fn rm_create(path: &Path) {
522 pretty_err(path, fs::remove_dir_all(path));
523 pretty_err(path, fs::create_dir(path));
524 }
525 rm_create(&self.paths().sources);
526 rm_create(&self.paths().tests);
527 rm_create(&self.paths().scripts);
528 }
529
530 pub fn initialize_default_contracts(&self) {
536 self.add_raw_source(
537 "Counter.sol",
538 include_str!("../../forge/assets/solidity/CounterTemplate.sol"),
539 );
540 self.add_raw_test(
541 "Counter.t.sol",
542 include_str!("../../forge/assets/solidity/CounterTemplate.t.sol"),
543 );
544 self.add_raw_script(
545 "Counter.s.sol",
546 include_str!("../../forge/assets/solidity/CounterTemplate.s.sol"),
547 );
548 }
549}
550
551fn config_paths_exist(paths: &ProjectPathsConfig, cached: bool) {
552 if cached {
553 assert!(paths.cache.exists());
554 }
555 assert!(paths.sources.exists());
556 assert!(paths.artifacts.exists());
557 paths.libraries.iter().for_each(|lib| assert!(lib.exists()));
558}
559
560pub struct TestCommand {
562 saved_cwd: PathBuf,
563 project: TestProject,
565 cmd: Command,
567 current_dir_lock: Option<parking_lot::MutexGuard<'static, ()>>,
569 stdin: Option<Vec<u8>>,
570 redact_output: bool,
572}
573
574impl TestCommand {
575 pub const fn cmd(&mut self) -> &mut Command {
577 &mut self.cmd
578 }
579
580 pub fn set_cmd(&mut self, cmd: Command) -> &mut Self {
582 self.cmd = cmd;
583 self
584 }
585
586 pub fn forge_fuse(&mut self) -> &mut Self {
588 self.set_cmd(self.project.forge_bin())
589 }
590
591 pub fn cast_fuse(&mut self) -> &mut Self {
593 self.set_cmd(self.project.cast_bin())
594 }
595
596 pub fn set_current_dir(&mut self, p: impl AsRef<Path>) {
598 drop(self.current_dir_lock.take());
599 let lock = CURRENT_DIR_LOCK.lock();
600 self.current_dir_lock = Some(lock);
601 let p = p.as_ref();
602 pretty_err(p, std::env::set_current_dir(p));
603 }
604
605 pub fn arg<A: AsRef<OsStr>>(&mut self, arg: A) -> &mut Self {
607 self.cmd.arg(arg);
608 self
609 }
610
611 pub fn args<I, A>(&mut self, args: I) -> &mut Self
613 where
614 I: IntoIterator<Item = A>,
615 A: AsRef<OsStr>,
616 {
617 self.cmd.args(args);
618 self
619 }
620
621 pub fn stdin(&mut self, stdin: impl Into<Vec<u8>>) -> &mut Self {
623 self.stdin = Some(stdin.into());
624 self
625 }
626
627 pub fn root_arg(&mut self) -> &mut Self {
629 let root = self.project.root().to_path_buf();
630 self.arg("--root").arg(root)
631 }
632
633 pub fn env(&mut self, k: impl AsRef<OsStr>, v: impl AsRef<OsStr>) {
635 self.cmd.env(k, v);
636 }
637
638 pub fn envs<I, K, V>(&mut self, envs: I)
640 where
641 I: IntoIterator<Item = (K, V)>,
642 K: AsRef<OsStr>,
643 V: AsRef<OsStr>,
644 {
645 self.cmd.envs(envs);
646 }
647
648 pub fn unset_env(&mut self, k: impl AsRef<OsStr>) {
650 self.cmd.env_remove(k);
651 }
652
653 pub fn current_dir<P: AsRef<Path>>(&mut self, dir: P) -> &mut Self {
659 self.cmd.current_dir(dir);
660 self
661 }
662
663 #[track_caller]
665 pub fn config(&mut self) -> Config {
666 self.cmd.args(["config", "--json"]);
667 let output = self.assert().success().get_output().stdout_lossy();
668 self.forge_fuse();
669 serde_json::from_str(output.as_ref()).unwrap()
670 }
671
672 #[track_caller]
674 pub fn git_init(&self) {
675 let mut cmd = Command::new("git");
676 cmd.arg("init").current_dir(self.project.root());
677 let output = OutputAssert::new(cmd.output().unwrap());
678 output.success();
679 }
680
681 #[track_caller]
683 pub fn git_submodule_status(&self) -> Output {
684 let mut cmd = Command::new("git");
685 cmd.arg("submodule").arg("status").current_dir(self.project.root());
686 cmd.output().unwrap()
687 }
688
689 #[track_caller]
691 pub fn git_add(&self) {
692 let mut cmd = Command::new("git");
693 cmd.current_dir(self.project.root());
694 cmd.arg("add").arg(".");
695 let output = OutputAssert::new(cmd.output().unwrap());
696 output.success();
697 }
698
699 #[track_caller]
701 pub fn git_commit(&self, msg: &str) {
702 let mut cmd = Command::new("git");
703 cmd.current_dir(self.project.root());
704 cmd.arg("commit").arg("-m").arg(msg);
705 let output = OutputAssert::new(cmd.output().unwrap());
706 output.success();
707 }
708
709 #[track_caller]
711 pub fn assert_with(&mut self, f: &[RegexRedaction]) -> OutputAssert {
712 let assert = OutputAssert::new(self.execute());
713 if self.redact_output {
714 let mut redactions = test_redactions();
715 insert_redactions(f, &mut redactions);
716 return assert.with_assert(
717 snapbox::Assert::new()
718 .action_env(snapbox::assert::DEFAULT_ACTION_ENV)
719 .redact_with(redactions),
720 );
721 }
722 assert
723 }
724
725 #[track_caller]
727 pub fn assert(&mut self) -> OutputAssert {
728 self.assert_with(&[])
729 }
730
731 #[track_caller]
733 pub fn assert_success(&mut self) -> OutputAssert {
734 self.assert().success()
735 }
736
737 #[track_caller]
739 pub fn assert_json_stdout(&mut self, expected: impl IntoData) {
740 self.assert_json_stdout_with_status(true, expected);
741 }
742
743 #[track_caller]
746 pub fn assert_json_stdout_with_status(
747 &mut self,
748 success: bool,
749 expected: impl IntoData,
750 ) -> OutputAssert {
751 let expected = expected.is(snapbox::data::DataFormat::Json).unordered();
752 let assert = if success { self.assert_success() } else { self.assert_failure() };
753 let stdout = assert.get_output().stdout.clone();
754 let actual = stdout.into_data().is(snapbox::data::DataFormat::Json).unordered();
755 assert_data_eq!(actual, expected);
756 assert
757 }
758
759 #[track_caller]
761 pub fn assert_json_stderr(&mut self, success: bool, expected: impl IntoData) {
762 let expected = expected.is(snapbox::data::DataFormat::Json).unordered();
763 let stderr = if success { self.assert_success() } else { self.assert_failure() }
764 .get_output()
765 .stderr
766 .clone();
767 let actual = stderr.into_data().is(snapbox::data::DataFormat::Json).unordered();
768 assert_data_eq!(actual, expected);
769 }
770
771 #[track_caller]
773 pub fn assert_empty_stdout(&mut self) {
774 self.assert_success().stdout_eq(Data::new());
775 }
776
777 #[track_caller]
779 pub fn assert_failure(&mut self) -> OutputAssert {
780 self.assert().failure()
781 }
782
783 #[track_caller]
785 pub fn assert_code(&mut self, expected: i32) -> OutputAssert {
786 self.assert().code(expected)
787 }
788
789 #[track_caller]
791 pub fn assert_empty_stderr(&mut self) {
792 self.assert_failure().stderr_eq(Data::new());
793 }
794
795 #[track_caller]
798 pub fn assert_file(&mut self, data: impl IntoData) {
799 self.assert_file_with(|this, path| _ = this.arg(path).assert_success(), data);
800 }
801
802 #[track_caller]
805 pub fn assert_file_with(&mut self, f: impl FnOnce(&mut Self, &Path), data: impl IntoData) {
806 let file = tempfile::NamedTempFile::new().expect("couldn't create temporary file");
807 f(self, file.path());
808 assert_data_eq!(Data::read_from(file.path(), None), data);
809 }
810
811 pub const fn with_no_redact(&mut self) -> &mut Self {
813 self.redact_output = false;
814 self
815 }
816
817 #[track_caller]
819 pub fn execute(&mut self) -> Output {
820 self.try_execute().unwrap()
821 }
822
823 #[track_caller]
824 pub fn try_execute(&mut self) -> std::io::Result<Output> {
825 test_debug!("executing {:?}", self.cmd);
826 let mut child =
827 self.cmd.stdout(Stdio::piped()).stderr(Stdio::piped()).stdin(Stdio::piped()).spawn()?;
828 if let Some(bytes) = self.stdin.take() {
829 child.stdin.take().unwrap().write_all(&bytes)?;
830 }
831 let output = child.wait_with_output()?;
832 test_debug!("exited with {}", output.status);
833 test_trace!("\n--- stdout ---\n{}\n--- /stdout ---", output.stdout_lossy());
834 test_trace!("\n--- stderr ---\n{}\n--- /stderr ---", output.stderr_lossy());
835 Ok(output)
836 }
837}
838
839impl Drop for TestCommand {
840 fn drop(&mut self) {
841 let _lock = self.current_dir_lock.take().unwrap_or_else(|| CURRENT_DIR_LOCK.lock());
842 if self.saved_cwd.exists() {
843 let _ = std::env::set_current_dir(&self.saved_cwd);
844 }
845 }
846}
847
848fn test_redactions() -> snapbox::Redactions {
849 static REDACTIONS: LazyLock<snapbox::Redactions> = LazyLock::new(|| {
850 make_redactions(&[
851 ("[SOLC_VERSION]", r"Solc( version)? \d+.\d+.\d+"),
852 ("[ELAPSED]", r"(finished )?in (\d+m )?\d+(\.\d+)?\w?s( \(.*?s CPU time\))?"),
853 ("[GAS]", r"[Gg]as( used)?: \d+"),
854 ("[GAS_COST]", r"[Gg]as cost\s*\(\d+\)"),
855 ("[GAS_LIMIT]", r"[Gg]as limit\s*\(\d+\)"),
856 ("[AVG_GAS]", r"μ: \d+, ~: \d+"),
857 ("[FILE]", r"(-->|╭▸).*\.sol"),
858 ("[FILE]", r"Location(.|\n)*\.rs(.|\n)*Backtrace"),
859 ("[COMPILING_FILES]", r"Compiling \d+ files?"),
860 ("[TX_HASH]", r"Transaction hash: 0x[0-9A-Fa-f]{64}"),
861 ("[ADDRESS]", r"Address: +0x[0-9A-Fa-f]{40}"),
862 ("[PUBLIC_KEY]", r"Public key: +0x[0-9A-Fa-f]{128}"),
863 ("[PRIVATE_KEY]", r"Private key: +0x[0-9A-Fa-f]{64}"),
864 ("[UPDATING_DEPENDENCIES]", r"Updating dependencies in .*"),
865 ("[SAVED_TRANSACTIONS]", r"Transactions saved to: .*\.json"),
866 ("[SAVED_SENSITIVE_VALUES]", r"Sensitive values saved to: .*\.json"),
867 ("[ESTIMATED_GAS_PRICE]", r"Estimated gas price:\s*(\d+(\.\d+)?)\s*gwei"),
868 ("[ESTIMATED_MAX_FEE_PER_GAS]", r"Estimated max fee per gas:\s*(\d+(\.\d+)?)\s*gwei"),
869 ("[ESTIMATED_BASE_FEE_PER_GAS]", r"Estimated base fee per gas:\s*(\d+(\.\d+)?)\s*gwei"),
870 (
871 "[ESTIMATED_PRIORITY_FEE_PER_GAS]",
872 r"Estimated max priority fee per gas:\s*(\d+(\.\d+)?)\s*gwei",
873 ),
874 ("[ESTIMATED_TOTAL_GAS_USED]", r"Estimated total gas used for script: \d+"),
875 (
876 "[ESTIMATED_AMOUNT_REQUIRED]",
877 r"Estimated amount required:\s*(\d+(\.\d+)?)\s*[A-Z]{3}",
878 ),
879 ("[SEED]", r"Fuzz seed: 0x[0-9A-Fa-f]+"),
880 ])
881 });
882 REDACTIONS.clone()
883}
884
885pub type RegexRedaction = (&'static str, &'static str);
887
888fn make_redactions(redactions: &[RegexRedaction]) -> snapbox::Redactions {
890 let mut r = snapbox::Redactions::new();
891 insert_redactions(redactions, &mut r);
892 r
893}
894
895fn insert_redactions(redactions: &[RegexRedaction], r: &mut snapbox::Redactions) {
896 for &(placeholder, re) in redactions {
897 r.insert(placeholder, Regex::new(re).expect(re)).expect(re);
898 }
899}
900
901pub trait OutputExt {
903 fn stdout_lossy(&self) -> String;
905
906 fn stderr_lossy(&self) -> String;
908}
909
910impl OutputExt for Output {
911 fn stdout_lossy(&self) -> String {
912 lossy_string(&self.stdout)
913 }
914
915 fn stderr_lossy(&self) -> String {
916 lossy_string(&self.stderr)
917 }
918}
919
920pub fn lossy_string(bytes: &[u8]) -> String {
921 String::from_utf8_lossy(bytes).replace("\r\n", "\n")
922}
923
924fn canonicalize(path: impl AsRef<Path>) -> PathBuf {
925 foundry_common::fs::canonicalize_path(path.as_ref())
926 .unwrap_or_else(|_| path.as_ref().to_path_buf())
927}
928
929fn cargo_build_target_dir_and_profile(profile_dir: &Path) -> (&Path, Option<&str>) {
930 let target_dir = profile_dir.parent().expect("Cargo target directory");
931 let profile = match profile_dir.file_name().and_then(OsStr::to_str) {
932 Some("debug") => None,
934 Some(profile) => Some(profile),
935 None => panic!("test executable profile directory must be UTF-8"),
936 };
937 (target_dir, profile)
938}
939
940pub fn cargo_profile_dir() -> PathBuf {
948 let exe = env::current_exe().expect("test executable path");
949 let exe_dir = canonicalize(exe.parent().expect("executable's directory"));
950 let mut dir = exe_dir.as_path();
951 while let Some(parent) = dir.parent() {
952 if matches!(dir.file_name().and_then(OsStr::to_str), Some("deps" | "build")) {
953 return parent.to_path_buf();
954 }
955 dir = parent;
956 }
957 exe_dir
958}