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 exe_root: PathBuf,
170 pub(crate) inner: Arc<TempProject<MultiCompiler, T>>,
172}
173
174impl TestProject {
175 pub fn new(name: &str, style: PathStyle) -> Self {
179 let id = NEXT_ID.fetch_add(1, Ordering::SeqCst);
180 let project = pretty_err(name, TempProject::with_style(&format!("{name}-{id}"), style));
181 Self::with_project(project)
182 }
183
184 pub fn with_project(project: TempProject) -> Self {
185 init_tracing();
186 let this = env::current_exe().unwrap();
187 let exe_root = canonicalize(this.parent().expect("executable's directory"));
188 Self { exe_root, inner: Arc::new(project) }
189 }
190
191 pub fn root(&self) -> &Path {
193 self.inner.root()
194 }
195
196 pub fn paths(&self) -> &ProjectPathsConfig {
198 self.inner.paths()
199 }
200
201 pub fn config(&self) -> PathBuf {
203 self.root().join(Config::FILE_NAME)
204 }
205
206 pub fn cache(&self) -> &PathBuf {
208 &self.paths().cache
209 }
210
211 pub fn artifacts(&self) -> &PathBuf {
213 &self.paths().artifacts
214 }
215
216 pub fn clear(&self) {
218 self.clear_cache();
219 self.clear_artifacts();
220 }
221
222 pub fn clear_cache(&self) {
224 let _ = fs::remove_file(self.cache());
225 }
226
227 pub fn clear_artifacts(&self) {
229 let _ = fs::remove_dir_all(self.artifacts());
230 }
231
232 pub fn clear_cache_dir(&self) {
234 let _ = fs::remove_dir_all(self.root().join("cache"));
235 }
236
237 pub fn update_config(&self, f: impl FnOnce(&mut Config)) {
239 self._update_config(Box::new(f));
240 }
241
242 fn _update_config(&self, f: Box<dyn FnOnce(&mut Config) + '_>) {
243 let mut config = self
244 .config()
245 .exists()
246 .then_some(())
247 .and_then(|()| Config::load_with_root(self.root()).ok())
248 .unwrap_or_default();
249 config.remappings.clear();
250 f(&mut config);
251 self.write_config(config);
252 }
253
254 #[doc(hidden)] pub fn write_config(&self, config: Config) {
257 let file = self.config();
258 pretty_err(&file, fs::write(&file, config.to_string_pretty().unwrap()));
259 }
260
261 pub fn add_rpc_endpoints(&self) {
263 self.update_config(|config| {
264 config.rpc_endpoints = rpc_endpoints();
265 });
266 }
267
268 pub fn add_source(&self, name: &str, contents: &str) -> PathBuf {
270 self.inner.add_source(name, Self::add_source_prelude(contents)).unwrap()
271 }
272
273 pub fn add_raw_source(&self, name: &str, contents: &str) -> PathBuf {
275 self.inner.add_source(name, contents).unwrap()
276 }
277
278 pub fn add_script(&self, name: &str, contents: &str) -> PathBuf {
280 self.inner.add_script(name, Self::add_source_prelude(contents)).unwrap()
281 }
282
283 pub fn add_raw_script(&self, name: &str, contents: &str) -> PathBuf {
285 self.inner.add_script(name, contents).unwrap()
286 }
287
288 pub fn add_test(&self, name: &str, contents: &str) -> PathBuf {
290 self.inner.add_test(name, Self::add_source_prelude(contents)).unwrap()
291 }
292
293 pub fn add_raw_test(&self, name: &str, contents: &str) -> PathBuf {
295 self.inner.add_test(name, contents).unwrap()
296 }
297
298 pub fn add_lib(&self, name: &str, contents: &str) -> PathBuf {
300 self.inner.add_lib(name, Self::add_source_prelude(contents)).unwrap()
301 }
302
303 pub fn add_raw_lib(&self, name: &str, contents: &str) -> PathBuf {
305 self.inner.add_lib(name, contents).unwrap()
306 }
307
308 fn add_source_prelude(s: &str) -> String {
309 let mut s = s.to_string();
310 if !s.contains("pragma solidity") {
311 s = format!("pragma solidity ={SOLC_VERSION};\n{s}");
312 }
313 if !s.contains("// SPDX") {
314 s = format!("// SPDX-License-Identifier: MIT OR Apache-2.0\n{s}");
315 }
316 s
317 }
318
319 #[track_caller]
321 pub fn assert_config_exists(&self) {
322 assert!(self.config().exists());
323 }
324
325 #[track_caller]
327 pub fn assert_cache_exists(&self) {
328 assert!(self.cache().exists());
329 }
330
331 #[track_caller]
333 pub fn assert_artifacts_dir_exists(&self) {
334 assert!(self.paths().artifacts.exists());
335 }
336
337 #[track_caller]
339 pub fn assert_create_dirs_exists(&self) {
340 self.paths().create_all().unwrap_or_else(|_| panic!("Failed to create project paths"));
341 CompilerCache::<SolcSettings>::default()
342 .write(&self.paths().cache)
343 .expect("Failed to create cache");
344 self.assert_all_paths_exist();
345 }
346
347 #[track_caller]
349 pub fn assert_style_paths_exist(&self, style: PathStyle) {
350 let paths = style.paths(&self.paths().root).unwrap();
351 config_paths_exist(&paths, self.inner.project().cached);
352 }
353
354 #[track_caller]
356 pub fn copy_to(&self, target: impl AsRef<Path>) {
357 let target = target.as_ref();
358 pretty_err(target, fs::create_dir_all(target));
359 pretty_err(target, copy_dir_filtered(self.root(), target));
360 }
361
362 pub fn create_file(&self, path: impl AsRef<Path>, contents: &str) -> PathBuf {
365 let path = path.as_ref();
366 assert!(path.is_relative(), "create_file(): file path is absolute");
367 let path = self.root().join(path);
368 if let Some(parent) = path.parent() {
369 pretty_err(parent, std::fs::create_dir_all(parent));
370 }
371 let file = pretty_err(&path, File::create(&path));
372 let mut writer = BufWriter::new(file);
373 pretty_err(&path, writer.write_all(contents.as_bytes()));
374 path
375 }
376
377 pub fn insert_ds_test(&self) -> PathBuf {
379 self.add_source("test.sol", include_str!("../../../testdata/utils/DSTest.sol"))
380 }
381
382 pub fn insert_utils(&self) {
384 self.add_test("utils/DSTest.sol", include_str!("../../../testdata/utils/DSTest.sol"));
385 self.add_test("utils/Test.sol", include_str!("../../../testdata/utils/Test.sol"));
386 self.add_test("utils/Vm.sol", include_str!("../../../testdata/utils/Vm.sol"));
387 self.add_test("utils/console.sol", include_str!("../../../testdata/utils/console.sol"));
388 }
389
390 pub fn insert_console(&self) -> PathBuf {
392 let s = include_str!("../../../testdata/utils/console.sol");
393 self.add_source("console.sol", s)
394 }
395
396 pub fn insert_vm(&self) -> PathBuf {
398 let s = include_str!("../../../testdata/utils/Vm.sol");
399 self.add_source("Vm.sol", s)
400 }
401
402 pub fn assert_all_paths_exist(&self) {
408 let paths = self.paths();
409 config_paths_exist(paths, self.inner.project().cached);
410 }
411
412 pub fn assert_cleaned(&self) {
414 let paths = self.paths();
415 assert!(!paths.cache.exists());
416 assert!(!paths.artifacts.exists());
417 }
418
419 #[track_caller]
421 pub fn forge_command(&self) -> TestCommand {
422 let cmd = self.forge_bin();
423 let _lock = CURRENT_DIR_LOCK.lock();
424 TestCommand {
425 project: self.clone(),
426 cmd,
427 current_dir_lock: None,
428 saved_cwd: pretty_err("<current dir>", std::env::current_dir()),
429 stdin: None,
430 redact_output: true,
431 }
432 }
433
434 pub fn cast_command(&self) -> TestCommand {
436 let mut cmd = self.cast_bin();
437 cmd.current_dir(self.inner.root());
438 let _lock = CURRENT_DIR_LOCK.lock();
439 TestCommand {
440 project: self.clone(),
441 cmd,
442 current_dir_lock: None,
443 saved_cwd: pretty_err("<current dir>", std::env::current_dir()),
444 stdin: None,
445 redact_output: true,
446 }
447 }
448
449 pub fn forge_bin(&self) -> Command {
451 let mut cmd = Command::new(self.foundry_bin_path("forge"));
452 cmd.current_dir(self.inner.root());
453 cmd.env("NO_COLOR", "1");
455 cmd
456 }
457
458 pub fn foundry_bin_path(&self, name: &str) -> PathBuf {
460 canonicalize(self.exe_root.join(format!("../{name}{}", env::consts::EXE_SUFFIX)))
461 }
462
463 pub fn ensure_foundry_bin(&self, name: &str) -> PathBuf {
465 let bin = self.foundry_bin_path(name);
466 if bin.exists() {
467 return bin;
468 }
469
470 let package = format!("{name}@{}", env!("CARGO_PKG_VERSION"));
471 let (target_dir, profile) = cargo_build_target_dir_and_profile(&self.exe_root);
472 let mut cmd = Command::new(env::var_os("CARGO").unwrap_or_else(|| "cargo".into()));
473 cmd.args(["build", "-p", &package, "--bin", name, "--manifest-path"])
474 .arg(Path::new(env!("CARGO_MANIFEST_DIR")).join("../../Cargo.toml"))
475 .arg("--target-dir")
476 .arg(target_dir);
477 if let Some(profile) = profile {
478 cmd.arg("--profile").arg(profile);
479 }
480
481 let output = cmd.output().expect("build Foundry sibling binary");
482 assert!(
483 output.status.success(),
484 "failed to build {name} for CLI test\nstdout:\n{}\nstderr:\n{}",
485 output.stdout_lossy(),
486 output.stderr_lossy(),
487 );
488
489 bin
490 }
491
492 pub fn cast_bin(&self) -> Command {
494 let mut cmd = Command::new(self.foundry_bin_path("cast"));
495 cmd.env("NO_COLOR", "1");
497 cmd
498 }
499
500 pub fn config_from_output<I, A>(&self, args: I) -> Config
502 where
503 I: IntoIterator<Item = A>,
504 A: AsRef<OsStr>,
505 {
506 let mut cmd = self.forge_bin();
507 cmd.arg("config").arg("--root").arg(self.root()).args(args).arg("--json");
508 let output = cmd.output().unwrap();
509 let c = lossy_string(&output.stdout);
510 let config: Config = serde_json::from_str(c.as_ref()).unwrap();
511 config.sanitized()
512 }
513
514 pub fn wipe(&self) {
516 pretty_err(self.root(), fs::remove_dir_all(self.root()));
517 pretty_err(self.root(), fs::create_dir_all(self.root()));
518 }
519
520 pub fn wipe_contracts(&self) {
522 fn rm_create(path: &Path) {
523 pretty_err(path, fs::remove_dir_all(path));
524 pretty_err(path, fs::create_dir(path));
525 }
526 rm_create(&self.paths().sources);
527 rm_create(&self.paths().tests);
528 rm_create(&self.paths().scripts);
529 }
530
531 pub fn initialize_default_contracts(&self) {
537 self.add_raw_source(
538 "Counter.sol",
539 include_str!("../../forge/assets/solidity/CounterTemplate.sol"),
540 );
541 self.add_raw_test(
542 "Counter.t.sol",
543 include_str!("../../forge/assets/solidity/CounterTemplate.t.sol"),
544 );
545 self.add_raw_script(
546 "Counter.s.sol",
547 include_str!("../../forge/assets/solidity/CounterTemplate.s.sol"),
548 );
549 }
550}
551
552fn config_paths_exist(paths: &ProjectPathsConfig, cached: bool) {
553 if cached {
554 assert!(paths.cache.exists());
555 }
556 assert!(paths.sources.exists());
557 assert!(paths.artifacts.exists());
558 paths.libraries.iter().for_each(|lib| assert!(lib.exists()));
559}
560
561pub struct TestCommand {
563 saved_cwd: PathBuf,
564 project: TestProject,
566 cmd: Command,
568 current_dir_lock: Option<parking_lot::MutexGuard<'static, ()>>,
570 stdin: Option<Vec<u8>>,
571 redact_output: bool,
573}
574
575impl TestCommand {
576 pub const fn cmd(&mut self) -> &mut Command {
578 &mut self.cmd
579 }
580
581 pub fn set_cmd(&mut self, cmd: Command) -> &mut Self {
583 self.cmd = cmd;
584 self
585 }
586
587 pub fn forge_fuse(&mut self) -> &mut Self {
589 self.set_cmd(self.project.forge_bin())
590 }
591
592 pub fn cast_fuse(&mut self) -> &mut Self {
594 self.set_cmd(self.project.cast_bin())
595 }
596
597 pub fn set_current_dir(&mut self, p: impl AsRef<Path>) {
599 drop(self.current_dir_lock.take());
600 let lock = CURRENT_DIR_LOCK.lock();
601 self.current_dir_lock = Some(lock);
602 let p = p.as_ref();
603 pretty_err(p, std::env::set_current_dir(p));
604 }
605
606 pub fn arg<A: AsRef<OsStr>>(&mut self, arg: A) -> &mut Self {
608 self.cmd.arg(arg);
609 self
610 }
611
612 pub fn args<I, A>(&mut self, args: I) -> &mut Self
614 where
615 I: IntoIterator<Item = A>,
616 A: AsRef<OsStr>,
617 {
618 self.cmd.args(args);
619 self
620 }
621
622 pub fn stdin(&mut self, stdin: impl Into<Vec<u8>>) -> &mut Self {
624 self.stdin = Some(stdin.into());
625 self
626 }
627
628 pub fn root_arg(&mut self) -> &mut Self {
630 let root = self.project.root().to_path_buf();
631 self.arg("--root").arg(root)
632 }
633
634 pub fn env(&mut self, k: impl AsRef<OsStr>, v: impl AsRef<OsStr>) {
636 self.cmd.env(k, v);
637 }
638
639 pub fn envs<I, K, V>(&mut self, envs: I)
641 where
642 I: IntoIterator<Item = (K, V)>,
643 K: AsRef<OsStr>,
644 V: AsRef<OsStr>,
645 {
646 self.cmd.envs(envs);
647 }
648
649 pub fn unset_env(&mut self, k: impl AsRef<OsStr>) {
651 self.cmd.env_remove(k);
652 }
653
654 pub fn current_dir<P: AsRef<Path>>(&mut self, dir: P) -> &mut Self {
660 self.cmd.current_dir(dir);
661 self
662 }
663
664 #[track_caller]
666 pub fn config(&mut self) -> Config {
667 self.cmd.args(["config", "--json"]);
668 let output = self.assert().success().get_output().stdout_lossy();
669 self.forge_fuse();
670 serde_json::from_str(output.as_ref()).unwrap()
671 }
672
673 #[track_caller]
675 pub fn git_init(&self) {
676 let mut cmd = Command::new("git");
677 cmd.arg("init").current_dir(self.project.root());
678 let output = OutputAssert::new(cmd.output().unwrap());
679 output.success();
680 }
681
682 #[track_caller]
684 pub fn git_submodule_status(&self) -> Output {
685 let mut cmd = Command::new("git");
686 cmd.arg("submodule").arg("status").current_dir(self.project.root());
687 cmd.output().unwrap()
688 }
689
690 #[track_caller]
692 pub fn git_add(&self) {
693 let mut cmd = Command::new("git");
694 cmd.current_dir(self.project.root());
695 cmd.arg("add").arg(".");
696 let output = OutputAssert::new(cmd.output().unwrap());
697 output.success();
698 }
699
700 #[track_caller]
702 pub fn git_commit(&self, msg: &str) {
703 let mut cmd = Command::new("git");
704 cmd.current_dir(self.project.root());
705 cmd.arg("commit").arg("-m").arg(msg);
706 let output = OutputAssert::new(cmd.output().unwrap());
707 output.success();
708 }
709
710 #[track_caller]
712 pub fn assert_with(&mut self, f: &[RegexRedaction]) -> OutputAssert {
713 let assert = OutputAssert::new(self.execute());
714 if self.redact_output {
715 let mut redactions = test_redactions();
716 insert_redactions(f, &mut redactions);
717 return assert.with_assert(
718 snapbox::Assert::new()
719 .action_env(snapbox::assert::DEFAULT_ACTION_ENV)
720 .redact_with(redactions),
721 );
722 }
723 assert
724 }
725
726 #[track_caller]
728 pub fn assert(&mut self) -> OutputAssert {
729 self.assert_with(&[])
730 }
731
732 #[track_caller]
734 pub fn assert_success(&mut self) -> OutputAssert {
735 self.assert().success()
736 }
737
738 #[track_caller]
740 pub fn assert_json_stdout(&mut self, expected: impl IntoData) {
741 let expected = expected.is(snapbox::data::DataFormat::Json).unordered();
742 let stdout = self.assert_success().get_output().stdout.clone();
743 let actual = stdout.into_data().is(snapbox::data::DataFormat::Json).unordered();
744 assert_data_eq!(actual, expected);
745 }
746
747 #[track_caller]
749 pub fn assert_json_stderr(&mut self, success: bool, expected: impl IntoData) {
750 let expected = expected.is(snapbox::data::DataFormat::Json).unordered();
751 let stderr = if success { self.assert_success() } else { self.assert_failure() }
752 .get_output()
753 .stderr
754 .clone();
755 let actual = stderr.into_data().is(snapbox::data::DataFormat::Json).unordered();
756 assert_data_eq!(actual, expected);
757 }
758
759 #[track_caller]
761 pub fn assert_empty_stdout(&mut self) {
762 self.assert_success().stdout_eq(Data::new());
763 }
764
765 #[track_caller]
767 pub fn assert_failure(&mut self) -> OutputAssert {
768 self.assert().failure()
769 }
770
771 #[track_caller]
773 pub fn assert_code(&mut self, expected: i32) -> OutputAssert {
774 self.assert().code(expected)
775 }
776
777 #[track_caller]
779 pub fn assert_empty_stderr(&mut self) {
780 self.assert_failure().stderr_eq(Data::new());
781 }
782
783 #[track_caller]
786 pub fn assert_file(&mut self, data: impl IntoData) {
787 self.assert_file_with(|this, path| _ = this.arg(path).assert_success(), data);
788 }
789
790 #[track_caller]
793 pub fn assert_file_with(&mut self, f: impl FnOnce(&mut Self, &Path), data: impl IntoData) {
794 let file = tempfile::NamedTempFile::new().expect("couldn't create temporary file");
795 f(self, file.path());
796 assert_data_eq!(Data::read_from(file.path(), None), data);
797 }
798
799 pub const fn with_no_redact(&mut self) -> &mut Self {
801 self.redact_output = false;
802 self
803 }
804
805 #[track_caller]
807 pub fn execute(&mut self) -> Output {
808 self.try_execute().unwrap()
809 }
810
811 #[track_caller]
812 pub fn try_execute(&mut self) -> std::io::Result<Output> {
813 test_debug!("executing {:?}", self.cmd);
814 let mut child =
815 self.cmd.stdout(Stdio::piped()).stderr(Stdio::piped()).stdin(Stdio::piped()).spawn()?;
816 if let Some(bytes) = self.stdin.take() {
817 child.stdin.take().unwrap().write_all(&bytes)?;
818 }
819 let output = child.wait_with_output()?;
820 test_debug!("exited with {}", output.status);
821 test_trace!("\n--- stdout ---\n{}\n--- /stdout ---", output.stdout_lossy());
822 test_trace!("\n--- stderr ---\n{}\n--- /stderr ---", output.stderr_lossy());
823 Ok(output)
824 }
825}
826
827impl Drop for TestCommand {
828 fn drop(&mut self) {
829 let _lock = self.current_dir_lock.take().unwrap_or_else(|| CURRENT_DIR_LOCK.lock());
830 if self.saved_cwd.exists() {
831 let _ = std::env::set_current_dir(&self.saved_cwd);
832 }
833 }
834}
835
836fn test_redactions() -> snapbox::Redactions {
837 static REDACTIONS: LazyLock<snapbox::Redactions> = LazyLock::new(|| {
838 make_redactions(&[
839 ("[SOLC_VERSION]", r"Solc( version)? \d+.\d+.\d+"),
840 ("[ELAPSED]", r"(finished )?in (\d+m )?\d+(\.\d+)?\w?s( \(.*?s CPU time\))?"),
841 ("[GAS]", r"[Gg]as( used)?: \d+"),
842 ("[GAS_COST]", r"[Gg]as cost\s*\(\d+\)"),
843 ("[GAS_LIMIT]", r"[Gg]as limit\s*\(\d+\)"),
844 ("[AVG_GAS]", r"μ: \d+, ~: \d+"),
845 ("[FILE]", r"(-->|╭▸).*\.sol"),
846 ("[FILE]", r"Location(.|\n)*\.rs(.|\n)*Backtrace"),
847 ("[COMPILING_FILES]", r"Compiling \d+ files?"),
848 ("[TX_HASH]", r"Transaction hash: 0x[0-9A-Fa-f]{64}"),
849 ("[ADDRESS]", r"Address: +0x[0-9A-Fa-f]{40}"),
850 ("[PUBLIC_KEY]", r"Public key: +0x[0-9A-Fa-f]{128}"),
851 ("[PRIVATE_KEY]", r"Private key: +0x[0-9A-Fa-f]{64}"),
852 ("[UPDATING_DEPENDENCIES]", r"Updating dependencies in .*"),
853 ("[SAVED_TRANSACTIONS]", r"Transactions saved to: .*\.json"),
854 ("[SAVED_SENSITIVE_VALUES]", r"Sensitive values saved to: .*\.json"),
855 ("[ESTIMATED_GAS_PRICE]", r"Estimated gas price:\s*(\d+(\.\d+)?)\s*gwei"),
856 ("[ESTIMATED_TOTAL_GAS_USED]", r"Estimated total gas used for script: \d+"),
857 (
858 "[ESTIMATED_AMOUNT_REQUIRED]",
859 r"Estimated amount required:\s*(\d+(\.\d+)?)\s*[A-Z]{3}",
860 ),
861 ("[SEED]", r"Fuzz seed: 0x[0-9A-Fa-f]+"),
862 ])
863 });
864 REDACTIONS.clone()
865}
866
867pub type RegexRedaction = (&'static str, &'static str);
869
870fn make_redactions(redactions: &[RegexRedaction]) -> snapbox::Redactions {
872 let mut r = snapbox::Redactions::new();
873 insert_redactions(redactions, &mut r);
874 r
875}
876
877fn insert_redactions(redactions: &[RegexRedaction], r: &mut snapbox::Redactions) {
878 for &(placeholder, re) in redactions {
879 r.insert(placeholder, Regex::new(re).expect(re)).expect(re);
880 }
881}
882
883pub trait OutputExt {
885 fn stdout_lossy(&self) -> String;
887
888 fn stderr_lossy(&self) -> String;
890}
891
892impl OutputExt for Output {
893 fn stdout_lossy(&self) -> String {
894 lossy_string(&self.stdout)
895 }
896
897 fn stderr_lossy(&self) -> String {
898 lossy_string(&self.stderr)
899 }
900}
901
902pub fn lossy_string(bytes: &[u8]) -> String {
903 String::from_utf8_lossy(bytes).replace("\r\n", "\n")
904}
905
906fn canonicalize(path: impl AsRef<Path>) -> PathBuf {
907 foundry_common::fs::canonicalize_path(path.as_ref())
908 .unwrap_or_else(|_| path.as_ref().to_path_buf())
909}
910
911fn cargo_build_target_dir_and_profile(exe_root: &Path) -> (&Path, Option<&str>) {
912 let profile_dir = exe_root.parent().expect("test executable profile directory");
913 let target_dir = profile_dir.parent().expect("Cargo target directory");
914 let profile = match profile_dir.file_name().and_then(OsStr::to_str) {
915 Some("debug") => None,
917 Some(profile) => Some(profile),
918 None => panic!("test executable profile directory must be UTF-8"),
919 };
920 (target_dir, profile)
921}