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 fn set_build(mut self, run_build: bool) -> Self {
83 self.run_build = run_build;
84 self
85 }
86
87 pub 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.forge_path());
452 cmd.current_dir(self.inner.root());
453 cmd.env("NO_COLOR", "1");
455 cmd
456 }
457
458 pub(crate) fn forge_path(&self) -> PathBuf {
459 canonicalize(self.exe_root.join(format!("../forge{}", env::consts::EXE_SUFFIX)))
460 }
461
462 pub fn cast_bin(&self) -> Command {
464 let cast = canonicalize(self.exe_root.join(format!("../cast{}", env::consts::EXE_SUFFIX)));
465 let mut cmd = Command::new(cast);
466 cmd.env("NO_COLOR", "1");
468 cmd
469 }
470
471 pub fn config_from_output<I, A>(&self, args: I) -> Config
473 where
474 I: IntoIterator<Item = A>,
475 A: AsRef<OsStr>,
476 {
477 let mut cmd = self.forge_bin();
478 cmd.arg("config").arg("--root").arg(self.root()).args(args).arg("--json");
479 let output = cmd.output().unwrap();
480 let c = lossy_string(&output.stdout);
481 let config: Config = serde_json::from_str(c.as_ref()).unwrap();
482 config.sanitized()
483 }
484
485 pub fn wipe(&self) {
487 pretty_err(self.root(), fs::remove_dir_all(self.root()));
488 pretty_err(self.root(), fs::create_dir_all(self.root()));
489 }
490
491 pub fn wipe_contracts(&self) {
493 fn rm_create(path: &Path) {
494 pretty_err(path, fs::remove_dir_all(path));
495 pretty_err(path, fs::create_dir(path));
496 }
497 rm_create(&self.paths().sources);
498 rm_create(&self.paths().tests);
499 rm_create(&self.paths().scripts);
500 }
501
502 pub fn initialize_default_contracts(&self) {
508 self.add_raw_source(
509 "Counter.sol",
510 include_str!("../../forge/assets/solidity/CounterTemplate.sol"),
511 );
512 self.add_raw_test(
513 "Counter.t.sol",
514 include_str!("../../forge/assets/solidity/CounterTemplate.t.sol"),
515 );
516 self.add_raw_script(
517 "Counter.s.sol",
518 include_str!("../../forge/assets/solidity/CounterTemplate.s.sol"),
519 );
520 }
521}
522
523fn config_paths_exist(paths: &ProjectPathsConfig, cached: bool) {
524 if cached {
525 assert!(paths.cache.exists());
526 }
527 assert!(paths.sources.exists());
528 assert!(paths.artifacts.exists());
529 paths.libraries.iter().for_each(|lib| assert!(lib.exists()));
530}
531
532pub struct TestCommand {
534 saved_cwd: PathBuf,
535 project: TestProject,
537 cmd: Command,
539 current_dir_lock: Option<parking_lot::MutexGuard<'static, ()>>,
541 stdin: Option<Vec<u8>>,
542 redact_output: bool,
544}
545
546impl TestCommand {
547 pub fn cmd(&mut self) -> &mut Command {
549 &mut self.cmd
550 }
551
552 pub fn set_cmd(&mut self, cmd: Command) -> &mut Self {
554 self.cmd = cmd;
555 self
556 }
557
558 pub fn forge_fuse(&mut self) -> &mut Self {
560 self.set_cmd(self.project.forge_bin())
561 }
562
563 pub fn cast_fuse(&mut self) -> &mut Self {
565 self.set_cmd(self.project.cast_bin())
566 }
567
568 pub fn set_current_dir(&mut self, p: impl AsRef<Path>) {
570 drop(self.current_dir_lock.take());
571 let lock = CURRENT_DIR_LOCK.lock();
572 self.current_dir_lock = Some(lock);
573 let p = p.as_ref();
574 pretty_err(p, std::env::set_current_dir(p));
575 }
576
577 pub fn arg<A: AsRef<OsStr>>(&mut self, arg: A) -> &mut Self {
579 self.cmd.arg(arg);
580 self
581 }
582
583 pub fn args<I, A>(&mut self, args: I) -> &mut Self
585 where
586 I: IntoIterator<Item = A>,
587 A: AsRef<OsStr>,
588 {
589 self.cmd.args(args);
590 self
591 }
592
593 pub fn stdin(&mut self, stdin: impl Into<Vec<u8>>) -> &mut Self {
595 self.stdin = Some(stdin.into());
596 self
597 }
598
599 pub fn root_arg(&mut self) -> &mut Self {
601 let root = self.project.root().to_path_buf();
602 self.arg("--root").arg(root)
603 }
604
605 pub fn env(&mut self, k: impl AsRef<OsStr>, v: impl AsRef<OsStr>) {
607 self.cmd.env(k, v);
608 }
609
610 pub fn envs<I, K, V>(&mut self, envs: I)
612 where
613 I: IntoIterator<Item = (K, V)>,
614 K: AsRef<OsStr>,
615 V: AsRef<OsStr>,
616 {
617 self.cmd.envs(envs);
618 }
619
620 pub fn unset_env(&mut self, k: impl AsRef<OsStr>) {
622 self.cmd.env_remove(k);
623 }
624
625 pub fn current_dir<P: AsRef<Path>>(&mut self, dir: P) -> &mut Self {
631 self.cmd.current_dir(dir);
632 self
633 }
634
635 #[track_caller]
637 pub fn config(&mut self) -> Config {
638 self.cmd.args(["config", "--json"]);
639 let output = self.assert().success().get_output().stdout_lossy();
640 self.forge_fuse();
641 serde_json::from_str(output.as_ref()).unwrap()
642 }
643
644 #[track_caller]
646 pub fn git_init(&self) {
647 let mut cmd = Command::new("git");
648 cmd.arg("init").current_dir(self.project.root());
649 let output = OutputAssert::new(cmd.output().unwrap());
650 output.success();
651 }
652
653 #[track_caller]
655 pub fn git_submodule_status(&self) -> Output {
656 let mut cmd = Command::new("git");
657 cmd.arg("submodule").arg("status").current_dir(self.project.root());
658 cmd.output().unwrap()
659 }
660
661 #[track_caller]
663 pub fn git_add(&self) {
664 let mut cmd = Command::new("git");
665 cmd.current_dir(self.project.root());
666 cmd.arg("add").arg(".");
667 let output = OutputAssert::new(cmd.output().unwrap());
668 output.success();
669 }
670
671 #[track_caller]
673 pub fn git_commit(&self, msg: &str) {
674 let mut cmd = Command::new("git");
675 cmd.current_dir(self.project.root());
676 cmd.arg("commit").arg("-m").arg(msg);
677 let output = OutputAssert::new(cmd.output().unwrap());
678 output.success();
679 }
680
681 #[track_caller]
683 pub fn assert_with(&mut self, f: &[RegexRedaction]) -> OutputAssert {
684 let assert = OutputAssert::new(self.execute());
685 if self.redact_output {
686 let mut redactions = test_redactions();
687 insert_redactions(f, &mut redactions);
688 return assert.with_assert(
689 snapbox::Assert::new()
690 .action_env(snapbox::assert::DEFAULT_ACTION_ENV)
691 .redact_with(redactions),
692 );
693 }
694 assert
695 }
696
697 #[track_caller]
699 pub fn assert(&mut self) -> OutputAssert {
700 self.assert_with(&[])
701 }
702
703 #[track_caller]
705 pub fn assert_success(&mut self) -> OutputAssert {
706 self.assert().success()
707 }
708
709 #[track_caller]
711 pub fn assert_json_stdout(&mut self, expected: impl IntoData) {
712 let expected = expected.is(snapbox::data::DataFormat::Json).unordered();
713 let stdout = self.assert_success().get_output().stdout.clone();
714 let actual = stdout.into_data().is(snapbox::data::DataFormat::Json).unordered();
715 assert_data_eq!(actual, expected);
716 }
717
718 #[track_caller]
720 pub fn assert_json_stderr(&mut self, success: bool, expected: impl IntoData) {
721 let expected = expected.is(snapbox::data::DataFormat::Json).unordered();
722 let stderr = if success { self.assert_success() } else { self.assert_failure() }
723 .get_output()
724 .stderr
725 .clone();
726 let actual = stderr.into_data().is(snapbox::data::DataFormat::Json).unordered();
727 assert_data_eq!(actual, expected);
728 }
729
730 #[track_caller]
732 pub fn assert_empty_stdout(&mut self) {
733 self.assert_success().stdout_eq(Data::new());
734 }
735
736 #[track_caller]
738 pub fn assert_failure(&mut self) -> OutputAssert {
739 self.assert().failure()
740 }
741
742 #[track_caller]
744 pub fn assert_code(&mut self, expected: i32) -> OutputAssert {
745 self.assert().code(expected)
746 }
747
748 #[track_caller]
750 pub fn assert_empty_stderr(&mut self) {
751 self.assert_failure().stderr_eq(Data::new());
752 }
753
754 #[track_caller]
757 pub fn assert_file(&mut self, data: impl IntoData) {
758 self.assert_file_with(|this, path| _ = this.arg(path).assert_success(), data);
759 }
760
761 #[track_caller]
764 pub fn assert_file_with(&mut self, f: impl FnOnce(&mut Self, &Path), data: impl IntoData) {
765 let file = tempfile::NamedTempFile::new().expect("couldn't create temporary file");
766 f(self, file.path());
767 assert_data_eq!(Data::read_from(file.path(), None), data);
768 }
769
770 pub fn with_no_redact(&mut self) -> &mut Self {
772 self.redact_output = false;
773 self
774 }
775
776 #[track_caller]
778 pub fn execute(&mut self) -> Output {
779 self.try_execute().unwrap()
780 }
781
782 #[track_caller]
783 pub fn try_execute(&mut self) -> std::io::Result<Output> {
784 test_debug!("executing {:?}", self.cmd);
785 let mut child =
786 self.cmd.stdout(Stdio::piped()).stderr(Stdio::piped()).stdin(Stdio::piped()).spawn()?;
787 if let Some(bytes) = self.stdin.take() {
788 child.stdin.take().unwrap().write_all(&bytes)?;
789 }
790 let output = child.wait_with_output()?;
791 test_debug!("exited with {}", output.status);
792 test_trace!("\n--- stdout ---\n{}\n--- /stdout ---", output.stdout_lossy());
793 test_trace!("\n--- stderr ---\n{}\n--- /stderr ---", output.stderr_lossy());
794 Ok(output)
795 }
796}
797
798impl Drop for TestCommand {
799 fn drop(&mut self) {
800 let _lock = self.current_dir_lock.take().unwrap_or_else(|| CURRENT_DIR_LOCK.lock());
801 if self.saved_cwd.exists() {
802 let _ = std::env::set_current_dir(&self.saved_cwd);
803 }
804 }
805}
806
807fn test_redactions() -> snapbox::Redactions {
808 static REDACTIONS: LazyLock<snapbox::Redactions> = LazyLock::new(|| {
809 make_redactions(&[
810 ("[SOLC_VERSION]", r"Solc( version)? \d+.\d+.\d+"),
811 ("[ELAPSED]", r"(finished )?in \d+(\.\d+)?\w?s( \(.*?s CPU time\))?"),
812 ("[GAS]", r"[Gg]as( used)?: \d+"),
813 ("[GAS_COST]", r"[Gg]as cost\s*\(\d+\)"),
814 ("[GAS_LIMIT]", r"[Gg]as limit\s*\(\d+\)"),
815 ("[AVG_GAS]", r"μ: \d+, ~: \d+"),
816 ("[FILE]", r"(-->|╭▸).*\.sol"),
817 ("[FILE]", r"Location(.|\n)*\.rs(.|\n)*Backtrace"),
818 ("[COMPILING_FILES]", r"Compiling \d+ files?"),
819 ("[TX_HASH]", r"Transaction hash: 0x[0-9A-Fa-f]{64}"),
820 ("[ADDRESS]", r"Address: +0x[0-9A-Fa-f]{40}"),
821 ("[PUBLIC_KEY]", r"Public key: +0x[0-9A-Fa-f]{128}"),
822 ("[PRIVATE_KEY]", r"Private key: +0x[0-9A-Fa-f]{64}"),
823 ("[UPDATING_DEPENDENCIES]", r"Updating dependencies in .*"),
824 ("[SAVED_TRANSACTIONS]", r"Transactions saved to: .*\.json"),
825 ("[SAVED_SENSITIVE_VALUES]", r"Sensitive values saved to: .*\.json"),
826 ("[ESTIMATED_GAS_PRICE]", r"Estimated gas price:\s*(\d+(\.\d+)?)\s*gwei"),
827 ("[ESTIMATED_TOTAL_GAS_USED]", r"Estimated total gas used for script: \d+"),
828 (
829 "[ESTIMATED_AMOUNT_REQUIRED]",
830 r"Estimated amount required:\s*(\d+(\.\d+)?)\s*[A-Z]{3}",
831 ),
832 ("[SEED]", r"Fuzz seed: 0x[0-9A-Fa-f]+"),
833 ])
834 });
835 REDACTIONS.clone()
836}
837
838pub type RegexRedaction = (&'static str, &'static str);
840
841fn make_redactions(redactions: &[RegexRedaction]) -> snapbox::Redactions {
843 let mut r = snapbox::Redactions::new();
844 insert_redactions(redactions, &mut r);
845 r
846}
847
848fn insert_redactions(redactions: &[RegexRedaction], r: &mut snapbox::Redactions) {
849 for &(placeholder, re) in redactions {
850 r.insert(placeholder, Regex::new(re).expect(re)).expect(re);
851 }
852}
853
854pub trait OutputExt {
856 fn stdout_lossy(&self) -> String;
858
859 fn stderr_lossy(&self) -> String;
861}
862
863impl OutputExt for Output {
864 fn stdout_lossy(&self) -> String {
865 lossy_string(&self.stdout)
866 }
867
868 fn stderr_lossy(&self) -> String {
869 lossy_string(&self.stderr)
870 }
871}
872
873pub fn lossy_string(bytes: &[u8]) -> String {
874 String::from_utf8_lossy(bytes).replace("\r\n", "\n")
875}
876
877fn canonicalize(path: impl AsRef<Path>) -> PathBuf {
878 foundry_common::fs::canonicalize_path(path.as_ref())
879 .unwrap_or_else(|_| path.as_ref().to_path_buf())
880}