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 if !status.success() {
45 panic!("git clone failed: {status}");
46 }
47}
48
49#[track_caller]
55pub fn setup_forge(name: &str, style: PathStyle) -> (TestProject, TestCommand) {
56 setup_forge_project(TestProject::new(name, style))
57}
58
59pub fn setup_forge_project(test: TestProject) -> (TestProject, TestCommand) {
60 let cmd = test.forge_command();
61 (test, cmd)
62}
63
64#[derive(Clone, Debug)]
66pub struct RemoteProject {
67 id: String,
68 run_build: bool,
69 run_commands: Vec<Vec<String>>,
70 path_style: PathStyle,
71}
72
73impl RemoteProject {
74 pub fn new(id: impl Into<String>) -> Self {
75 Self {
76 id: id.into(),
77 run_build: true,
78 run_commands: vec![],
79 path_style: PathStyle::Dapptools,
80 }
81 }
82
83 pub fn set_build(mut self, run_build: bool) -> Self {
85 self.run_build = run_build;
86 self
87 }
88
89 pub fn path_style(mut self, path_style: PathStyle) -> Self {
91 self.path_style = path_style;
92 self
93 }
94
95 pub fn cmd(mut self, cmd: impl IntoIterator<Item = impl Into<String>>) -> Self {
97 self.run_commands.push(cmd.into_iter().map(Into::into).collect());
98 self
99 }
100}
101
102impl<T: Into<String>> From<T> for RemoteProject {
103 fn from(id: T) -> Self {
104 Self::new(id)
105 }
106}
107
108pub fn setup_forge_remote(prj: impl Into<RemoteProject>) -> (TestProject, TestCommand) {
119 try_setup_forge_remote(prj).unwrap()
120}
121
122pub fn try_setup_forge_remote(
124 config: impl Into<RemoteProject>,
125) -> Result<(TestProject, TestCommand)> {
126 let config = config.into();
127 let mut tmp = TempProject::checkout(&config.id).wrap_err("failed to checkout project")?;
128 tmp.project_mut().paths = config.path_style.paths(tmp.root())?;
129
130 let prj = TestProject::with_project(tmp);
131 if config.run_build {
132 let mut cmd = prj.forge_command();
133 cmd.arg("build").assert_success();
134 }
135 for addon in config.run_commands {
136 debug_assert!(!addon.is_empty());
137 let mut cmd = Command::new(&addon[0]);
138 if addon.len() > 1 {
139 cmd.args(&addon[1..]);
140 }
141 let status = cmd
142 .current_dir(prj.root())
143 .stdout(Stdio::null())
144 .stderr(Stdio::null())
145 .status()
146 .wrap_err_with(|| format!("Failed to execute {addon:?}"))?;
147 eyre::ensure!(status.success(), "Failed to execute command {:?}", addon);
148 }
149
150 let cmd = prj.forge_command();
151 Ok((prj, cmd))
152}
153
154pub fn setup_cast(name: &str, style: PathStyle) -> (TestProject, TestCommand) {
155 setup_cast_project(TestProject::new(name, style))
156}
157
158pub fn setup_cast_project(test: TestProject) -> (TestProject, TestCommand) {
159 let cmd = test.cast_command();
160 (test, cmd)
161}
162
163#[derive(Clone, Debug)]
167pub struct TestProject<
168 T: ArtifactOutput<CompilerContract = Contract> + Default = ConfigurableArtifacts,
169> {
170 exe_root: PathBuf,
172 pub(crate) inner: Arc<TempProject<MultiCompiler, T>>,
174}
175
176impl TestProject {
177 pub fn new(name: &str, style: PathStyle) -> Self {
181 let id = NEXT_ID.fetch_add(1, Ordering::SeqCst);
182 let project = pretty_err(name, TempProject::with_style(&format!("{name}-{id}"), style));
183 Self::with_project(project)
184 }
185
186 pub fn with_project(project: TempProject) -> Self {
187 init_tracing();
188 let this = env::current_exe().unwrap();
189 let exe_root = canonicalize(this.parent().expect("executable's directory"));
190 Self { exe_root, inner: Arc::new(project) }
191 }
192
193 pub fn root(&self) -> &Path {
195 self.inner.root()
196 }
197
198 pub fn paths(&self) -> &ProjectPathsConfig {
200 self.inner.paths()
201 }
202
203 pub fn config(&self) -> PathBuf {
205 self.root().join(Config::FILE_NAME)
206 }
207
208 pub fn cache(&self) -> &PathBuf {
210 &self.paths().cache
211 }
212
213 pub fn artifacts(&self) -> &PathBuf {
215 &self.paths().artifacts
216 }
217
218 pub fn clear(&self) {
220 self.clear_cache();
221 self.clear_artifacts();
222 }
223
224 pub fn clear_cache(&self) {
226 let _ = fs::remove_file(self.cache());
227 }
228
229 pub fn clear_artifacts(&self) {
231 let _ = fs::remove_dir_all(self.artifacts());
232 }
233
234 pub fn clear_cache_dir(&self) {
236 let _ = fs::remove_dir_all(self.root().join("cache"));
237 }
238
239 pub fn update_config(&self, f: impl FnOnce(&mut Config)) {
241 self._update_config(Box::new(f));
242 }
243
244 fn _update_config(&self, f: Box<dyn FnOnce(&mut Config) + '_>) {
245 let mut config = self
246 .config()
247 .exists()
248 .then_some(())
249 .and_then(|()| Config::load_with_root(self.root()).ok())
250 .unwrap_or_default();
251 config.remappings.clear();
252 f(&mut config);
253 self.write_config(config);
254 }
255
256 #[doc(hidden)] pub fn write_config(&self, config: Config) {
259 let file = self.config();
260 pretty_err(&file, fs::write(&file, config.to_string_pretty().unwrap()));
261 }
262
263 pub fn add_rpc_endpoints(&self) {
265 self.update_config(|config| {
266 config.rpc_endpoints = rpc_endpoints();
267 });
268 }
269
270 pub fn add_source(&self, name: &str, contents: &str) -> PathBuf {
272 self.inner.add_source(name, Self::add_source_prelude(contents)).unwrap()
273 }
274
275 pub fn add_raw_source(&self, name: &str, contents: &str) -> PathBuf {
277 self.inner.add_source(name, contents).unwrap()
278 }
279
280 pub fn add_script(&self, name: &str, contents: &str) -> PathBuf {
282 self.inner.add_script(name, Self::add_source_prelude(contents)).unwrap()
283 }
284
285 pub fn add_raw_script(&self, name: &str, contents: &str) -> PathBuf {
287 self.inner.add_script(name, contents).unwrap()
288 }
289
290 pub fn add_test(&self, name: &str, contents: &str) -> PathBuf {
292 self.inner.add_test(name, Self::add_source_prelude(contents)).unwrap()
293 }
294
295 pub fn add_raw_test(&self, name: &str, contents: &str) -> PathBuf {
297 self.inner.add_test(name, contents).unwrap()
298 }
299
300 pub fn add_lib(&self, name: &str, contents: &str) -> PathBuf {
302 self.inner.add_lib(name, Self::add_source_prelude(contents)).unwrap()
303 }
304
305 pub fn add_raw_lib(&self, name: &str, contents: &str) -> PathBuf {
307 self.inner.add_lib(name, contents).unwrap()
308 }
309
310 fn add_source_prelude(s: &str) -> String {
311 let mut s = s.to_string();
312 if !s.contains("pragma solidity") {
313 s = format!("pragma solidity ={SOLC_VERSION};\n{s}");
314 }
315 if !s.contains("// SPDX") {
316 s = format!("// SPDX-License-Identifier: MIT OR Apache-2.0\n{s}");
317 }
318 s
319 }
320
321 #[track_caller]
323 pub fn assert_config_exists(&self) {
324 assert!(self.config().exists());
325 }
326
327 #[track_caller]
329 pub fn assert_cache_exists(&self) {
330 assert!(self.cache().exists());
331 }
332
333 #[track_caller]
335 pub fn assert_artifacts_dir_exists(&self) {
336 assert!(self.paths().artifacts.exists());
337 }
338
339 #[track_caller]
341 pub fn assert_create_dirs_exists(&self) {
342 self.paths().create_all().unwrap_or_else(|_| panic!("Failed to create project paths"));
343 CompilerCache::<SolcSettings>::default()
344 .write(&self.paths().cache)
345 .expect("Failed to create cache");
346 self.assert_all_paths_exist();
347 }
348
349 #[track_caller]
351 pub fn assert_style_paths_exist(&self, style: PathStyle) {
352 let paths = style.paths(&self.paths().root).unwrap();
353 config_paths_exist(&paths, self.inner.project().cached);
354 }
355
356 #[track_caller]
358 pub fn copy_to(&self, target: impl AsRef<Path>) {
359 let target = target.as_ref();
360 pretty_err(target, fs::create_dir_all(target));
361 pretty_err(target, copy_dir_filtered(self.root(), target));
362 }
363
364 pub fn create_file(&self, path: impl AsRef<Path>, contents: &str) -> PathBuf {
367 let path = path.as_ref();
368 if !path.is_relative() {
369 panic!("create_file(): file path is absolute");
370 }
371 let path = self.root().join(path);
372 if let Some(parent) = path.parent() {
373 pretty_err(parent, std::fs::create_dir_all(parent));
374 }
375 let file = pretty_err(&path, File::create(&path));
376 let mut writer = BufWriter::new(file);
377 pretty_err(&path, writer.write_all(contents.as_bytes()));
378 path
379 }
380
381 pub fn insert_ds_test(&self) -> PathBuf {
383 self.add_source("test.sol", include_str!("../../../testdata/utils/DSTest.sol"))
384 }
385
386 pub fn insert_utils(&self) {
388 self.add_test("utils/DSTest.sol", include_str!("../../../testdata/utils/DSTest.sol"));
389 self.add_test("utils/Test.sol", include_str!("../../../testdata/utils/Test.sol"));
390 self.add_test("utils/Vm.sol", include_str!("../../../testdata/utils/Vm.sol"));
391 self.add_test("utils/console.sol", include_str!("../../../testdata/utils/console.sol"));
392 }
393
394 pub fn insert_console(&self) -> PathBuf {
396 let s = include_str!("../../../testdata/utils/console.sol");
397 self.add_source("console.sol", s)
398 }
399
400 pub fn insert_vm(&self) -> PathBuf {
402 let s = include_str!("../../../testdata/utils/Vm.sol");
403 self.add_source("Vm.sol", s)
404 }
405
406 pub fn assert_all_paths_exist(&self) {
412 let paths = self.paths();
413 config_paths_exist(paths, self.inner.project().cached);
414 }
415
416 pub fn assert_cleaned(&self) {
418 let paths = self.paths();
419 assert!(!paths.cache.exists());
420 assert!(!paths.artifacts.exists());
421 }
422
423 #[track_caller]
425 pub fn forge_command(&self) -> TestCommand {
426 let cmd = self.forge_bin();
427 let _lock = CURRENT_DIR_LOCK.lock();
428 TestCommand {
429 project: self.clone(),
430 cmd,
431 current_dir_lock: None,
432 saved_cwd: pretty_err("<current dir>", std::env::current_dir()),
433 stdin: None,
434 redact_output: true,
435 }
436 }
437
438 pub fn cast_command(&self) -> TestCommand {
440 let mut cmd = self.cast_bin();
441 cmd.current_dir(self.inner.root());
442 let _lock = CURRENT_DIR_LOCK.lock();
443 TestCommand {
444 project: self.clone(),
445 cmd,
446 current_dir_lock: None,
447 saved_cwd: pretty_err("<current dir>", std::env::current_dir()),
448 stdin: None,
449 redact_output: true,
450 }
451 }
452
453 pub fn forge_bin(&self) -> Command {
455 let mut cmd = Command::new(self.forge_path());
456 cmd.current_dir(self.inner.root());
457 cmd.env("NO_COLOR", "1");
459 cmd
460 }
461
462 pub(crate) fn forge_path(&self) -> PathBuf {
463 canonicalize(self.exe_root.join(format!("../forge{}", env::consts::EXE_SUFFIX)))
464 }
465
466 pub fn cast_bin(&self) -> Command {
468 let cast = canonicalize(self.exe_root.join(format!("../cast{}", env::consts::EXE_SUFFIX)));
469 let mut cmd = Command::new(cast);
470 cmd.env("NO_COLOR", "1");
472 cmd
473 }
474
475 pub fn config_from_output<I, A>(&self, args: I) -> Config
477 where
478 I: IntoIterator<Item = A>,
479 A: AsRef<OsStr>,
480 {
481 let mut cmd = self.forge_bin();
482 cmd.arg("config").arg("--root").arg(self.root()).args(args).arg("--json");
483 let output = cmd.output().unwrap();
484 let c = lossy_string(&output.stdout);
485 let config: Config = serde_json::from_str(c.as_ref()).unwrap();
486 config.sanitized()
487 }
488
489 pub fn wipe(&self) {
491 pretty_err(self.root(), fs::remove_dir_all(self.root()));
492 pretty_err(self.root(), fs::create_dir_all(self.root()));
493 }
494
495 pub fn wipe_contracts(&self) {
497 fn rm_create(path: &Path) {
498 pretty_err(path, fs::remove_dir_all(path));
499 pretty_err(path, fs::create_dir(path));
500 }
501 rm_create(&self.paths().sources);
502 rm_create(&self.paths().tests);
503 rm_create(&self.paths().scripts);
504 }
505
506 pub fn initialize_default_contracts(&self) {
512 self.add_raw_source(
513 "Counter.sol",
514 include_str!("../../forge/assets/solidity/CounterTemplate.sol"),
515 );
516 self.add_raw_test(
517 "Counter.t.sol",
518 include_str!("../../forge/assets/solidity/CounterTemplate.t.sol"),
519 );
520 self.add_raw_script(
521 "Counter.s.sol",
522 include_str!("../../forge/assets/solidity/CounterTemplate.s.sol"),
523 );
524 }
525}
526
527fn config_paths_exist(paths: &ProjectPathsConfig, cached: bool) {
528 if cached {
529 assert!(paths.cache.exists());
530 }
531 assert!(paths.sources.exists());
532 assert!(paths.artifacts.exists());
533 paths.libraries.iter().for_each(|lib| assert!(lib.exists()));
534}
535
536pub struct TestCommand {
538 saved_cwd: PathBuf,
539 project: TestProject,
541 cmd: Command,
543 current_dir_lock: Option<parking_lot::MutexGuard<'static, ()>>,
545 stdin: Option<Vec<u8>>,
546 redact_output: bool,
548}
549
550impl TestCommand {
551 pub fn cmd(&mut self) -> &mut Command {
553 &mut self.cmd
554 }
555
556 pub fn set_cmd(&mut self, cmd: Command) -> &mut Self {
558 self.cmd = cmd;
559 self
560 }
561
562 pub fn forge_fuse(&mut self) -> &mut Self {
564 self.set_cmd(self.project.forge_bin())
565 }
566
567 pub fn cast_fuse(&mut self) -> &mut Self {
569 self.set_cmd(self.project.cast_bin())
570 }
571
572 pub fn set_current_dir(&mut self, p: impl AsRef<Path>) {
574 drop(self.current_dir_lock.take());
575 let lock = CURRENT_DIR_LOCK.lock();
576 self.current_dir_lock = Some(lock);
577 let p = p.as_ref();
578 pretty_err(p, std::env::set_current_dir(p));
579 }
580
581 pub fn arg<A: AsRef<OsStr>>(&mut self, arg: A) -> &mut Self {
583 self.cmd.arg(arg);
584 self
585 }
586
587 pub fn args<I, A>(&mut self, args: I) -> &mut Self
589 where
590 I: IntoIterator<Item = A>,
591 A: AsRef<OsStr>,
592 {
593 self.cmd.args(args);
594 self
595 }
596
597 pub fn stdin(&mut self, stdin: impl Into<Vec<u8>>) -> &mut Self {
599 self.stdin = Some(stdin.into());
600 self
601 }
602
603 pub fn root_arg(&mut self) -> &mut Self {
605 let root = self.project.root().to_path_buf();
606 self.arg("--root").arg(root)
607 }
608
609 pub fn env(&mut self, k: impl AsRef<OsStr>, v: impl AsRef<OsStr>) {
611 self.cmd.env(k, v);
612 }
613
614 pub fn envs<I, K, V>(&mut self, envs: I)
616 where
617 I: IntoIterator<Item = (K, V)>,
618 K: AsRef<OsStr>,
619 V: AsRef<OsStr>,
620 {
621 self.cmd.envs(envs);
622 }
623
624 pub fn unset_env(&mut self, k: impl AsRef<OsStr>) {
626 self.cmd.env_remove(k);
627 }
628
629 pub fn current_dir<P: AsRef<Path>>(&mut self, dir: P) -> &mut Self {
635 self.cmd.current_dir(dir);
636 self
637 }
638
639 #[track_caller]
641 pub fn config(&mut self) -> Config {
642 self.cmd.args(["config", "--json"]);
643 let output = self.assert().success().get_output().stdout_lossy();
644 self.forge_fuse();
645 serde_json::from_str(output.as_ref()).unwrap()
646 }
647
648 #[track_caller]
650 pub fn git_init(&self) {
651 let mut cmd = Command::new("git");
652 cmd.arg("init").current_dir(self.project.root());
653 let output = OutputAssert::new(cmd.output().unwrap());
654 output.success();
655 }
656
657 #[track_caller]
659 pub fn git_submodule_status(&self) -> Output {
660 let mut cmd = Command::new("git");
661 cmd.arg("submodule").arg("status").current_dir(self.project.root());
662 cmd.output().unwrap()
663 }
664
665 #[track_caller]
667 pub fn git_add(&self) {
668 let mut cmd = Command::new("git");
669 cmd.current_dir(self.project.root());
670 cmd.arg("add").arg(".");
671 let output = OutputAssert::new(cmd.output().unwrap());
672 output.success();
673 }
674
675 #[track_caller]
677 pub fn git_commit(&self, msg: &str) {
678 let mut cmd = Command::new("git");
679 cmd.current_dir(self.project.root());
680 cmd.arg("commit").arg("-m").arg(msg);
681 let output = OutputAssert::new(cmd.output().unwrap());
682 output.success();
683 }
684
685 #[track_caller]
687 pub fn assert_with(&mut self, f: &[RegexRedaction]) -> OutputAssert {
688 let assert = OutputAssert::new(self.execute());
689 if self.redact_output {
690 let mut redactions = test_redactions();
691 insert_redactions(f, &mut redactions);
692 return assert.with_assert(
693 snapbox::Assert::new()
694 .action_env(snapbox::assert::DEFAULT_ACTION_ENV)
695 .redact_with(redactions),
696 );
697 }
698 assert
699 }
700
701 #[track_caller]
703 pub fn assert(&mut self) -> OutputAssert {
704 self.assert_with(&[])
705 }
706
707 #[track_caller]
709 pub fn assert_success(&mut self) -> OutputAssert {
710 self.assert().success()
711 }
712
713 #[track_caller]
715 pub fn assert_json_stdout(&mut self, expected: impl IntoData) {
716 let expected = expected.is(snapbox::data::DataFormat::Json).unordered();
717 let stdout = self.assert_success().get_output().stdout.clone();
718 let actual = stdout.into_data().is(snapbox::data::DataFormat::Json).unordered();
719 assert_data_eq!(actual, expected);
720 }
721
722 #[track_caller]
724 pub fn assert_json_stderr(&mut self, success: bool, expected: impl IntoData) {
725 let expected = expected.is(snapbox::data::DataFormat::Json).unordered();
726 let stderr = if success { self.assert_success() } else { self.assert_failure() }
727 .get_output()
728 .stderr
729 .clone();
730 let actual = stderr.into_data().is(snapbox::data::DataFormat::Json).unordered();
731 assert_data_eq!(actual, expected);
732 }
733
734 #[track_caller]
736 pub fn assert_empty_stdout(&mut self) {
737 self.assert_success().stdout_eq(Data::new());
738 }
739
740 #[track_caller]
742 pub fn assert_failure(&mut self) -> OutputAssert {
743 self.assert().failure()
744 }
745
746 #[track_caller]
748 pub fn assert_code(&mut self, expected: i32) -> OutputAssert {
749 self.assert().code(expected)
750 }
751
752 #[track_caller]
754 pub fn assert_empty_stderr(&mut self) {
755 self.assert_failure().stderr_eq(Data::new());
756 }
757
758 #[track_caller]
761 pub fn assert_file(&mut self, data: impl IntoData) {
762 self.assert_file_with(|this, path| _ = this.arg(path).assert_success(), data);
763 }
764
765 #[track_caller]
768 pub fn assert_file_with(&mut self, f: impl FnOnce(&mut Self, &Path), data: impl IntoData) {
769 let file = tempfile::NamedTempFile::new().expect("couldn't create temporary file");
770 f(self, file.path());
771 assert_data_eq!(Data::read_from(file.path(), None), data);
772 }
773
774 pub fn with_no_redact(&mut self) -> &mut Self {
776 self.redact_output = false;
777 self
778 }
779
780 #[track_caller]
782 pub fn execute(&mut self) -> Output {
783 self.try_execute().unwrap()
784 }
785
786 #[track_caller]
787 pub fn try_execute(&mut self) -> std::io::Result<Output> {
788 test_debug!("executing {:?}", self.cmd);
789 let mut child =
790 self.cmd.stdout(Stdio::piped()).stderr(Stdio::piped()).stdin(Stdio::piped()).spawn()?;
791 if let Some(bytes) = self.stdin.take() {
792 child.stdin.take().unwrap().write_all(&bytes)?;
793 }
794 let output = child.wait_with_output()?;
795 test_debug!("exited with {}", output.status);
796 test_trace!("\n--- stdout ---\n{}\n--- /stdout ---", output.stdout_lossy());
797 test_trace!("\n--- stderr ---\n{}\n--- /stderr ---", output.stderr_lossy());
798 Ok(output)
799 }
800}
801
802impl Drop for TestCommand {
803 fn drop(&mut self) {
804 let _lock = self.current_dir_lock.take().unwrap_or_else(|| CURRENT_DIR_LOCK.lock());
805 if self.saved_cwd.exists() {
806 let _ = std::env::set_current_dir(&self.saved_cwd);
807 }
808 }
809}
810
811fn test_redactions() -> snapbox::Redactions {
812 static REDACTIONS: LazyLock<snapbox::Redactions> = LazyLock::new(|| {
813 make_redactions(&[
814 ("[SOLC_VERSION]", r"Solc( version)? \d+.\d+.\d+"),
815 ("[ELAPSED]", r"(finished )?in \d+(\.\d+)?\w?s( \(.*?s CPU time\))?"),
816 ("[GAS]", r"[Gg]as( used)?: \d+"),
817 ("[GAS_COST]", r"[Gg]as cost\s*\(\d+\)"),
818 ("[GAS_LIMIT]", r"[Gg]as limit\s*\(\d+\)"),
819 ("[AVG_GAS]", r"μ: \d+, ~: \d+"),
820 ("[FILE]", r"(-->|╭▸).*\.sol"),
821 ("[FILE]", r"Location(.|\n)*\.rs(.|\n)*Backtrace"),
822 ("[COMPILING_FILES]", r"Compiling \d+ files?"),
823 ("[TX_HASH]", r"Transaction hash: 0x[0-9A-Fa-f]{64}"),
824 ("[ADDRESS]", r"Address: +0x[0-9A-Fa-f]{40}"),
825 ("[PUBLIC_KEY]", r"Public key: +0x[0-9A-Fa-f]{128}"),
826 ("[PRIVATE_KEY]", r"Private key: +0x[0-9A-Fa-f]{64}"),
827 ("[UPDATING_DEPENDENCIES]", r"Updating dependencies in .*"),
828 ("[SAVED_TRANSACTIONS]", r"Transactions saved to: .*\.json"),
829 ("[SAVED_SENSITIVE_VALUES]", r"Sensitive values saved to: .*\.json"),
830 ("[ESTIMATED_GAS_PRICE]", r"Estimated gas price:\s*(\d+(\.\d+)?)\s*gwei"),
831 ("[ESTIMATED_TOTAL_GAS_USED]", r"Estimated total gas used for script: \d+"),
832 (
833 "[ESTIMATED_AMOUNT_REQUIRED]",
834 r"Estimated amount required:\s*(\d+(\.\d+)?)\s*[A-Z]{3}",
835 ),
836 ("[SEED]", r"Fuzz seed: 0x[0-9A-Fa-f]+"),
837 ])
838 });
839 REDACTIONS.clone()
840}
841
842pub type RegexRedaction = (&'static str, &'static str);
844
845fn make_redactions(redactions: &[RegexRedaction]) -> snapbox::Redactions {
847 let mut r = snapbox::Redactions::new();
848 insert_redactions(redactions, &mut r);
849 r
850}
851
852fn insert_redactions(redactions: &[RegexRedaction], r: &mut snapbox::Redactions) {
853 for &(placeholder, re) in redactions {
854 r.insert(placeholder, Regex::new(re).expect(re)).expect(re);
855 }
856}
857
858pub trait OutputExt {
860 fn stdout_lossy(&self) -> String;
862
863 fn stderr_lossy(&self) -> String;
865}
866
867impl OutputExt for Output {
868 fn stdout_lossy(&self) -> String {
869 lossy_string(&self.stdout)
870 }
871
872 fn stderr_lossy(&self) -> String {
873 lossy_string(&self.stderr)
874 }
875}
876
877pub fn lossy_string(bytes: &[u8]) -> String {
878 String::from_utf8_lossy(bytes).replace("\r\n", "\n")
879}
880
881fn canonicalize(path: impl AsRef<Path>) -> PathBuf {
882 foundry_common::fs::canonicalize_path(path.as_ref())
883 .unwrap_or_else(|_| path.as_ref().to_path_buf())
884}