1use crate::{init_tracing, rpc::rpc_endpoints};
2use eyre::{Result, WrapErr};
3use foundry_compilers::{
4 ArtifactOutput, ConfigurableArtifacts, PathStyle, ProjectPathsConfig,
5 artifacts::Contract,
6 cache::CompilerCache,
7 compilers::multi::MultiCompiler,
8 project_util::{TempProject, copy_dir},
9 solc::SolcSettings,
10};
11use foundry_config::Config;
12use parking_lot::Mutex;
13use regex::Regex;
14use snapbox::{Data, IntoData, assert_data_eq, cmd::OutputAssert};
15use std::{
16 env,
17 ffi::OsStr,
18 fs::{self, File},
19 io::{BufWriter, Write},
20 path::{Path, PathBuf},
21 process::{Command, Output, Stdio},
22 sync::{
23 Arc, LazyLock,
24 atomic::{AtomicUsize, Ordering},
25 },
26};
27
28use crate::util::{SOLC_VERSION, pretty_err};
29
30static CURRENT_DIR_LOCK: LazyLock<Mutex<()>> = LazyLock::new(|| Mutex::new(()));
31
32static NEXT_ID: AtomicUsize = AtomicUsize::new(0);
34
35pub fn clone_remote(repo_url: &str, target_dir: &str, recursive: bool) {
37 let mut cmd = Command::new("git");
38 cmd.args(["clone"]);
39 if recursive {
40 cmd.args(["--recursive", "--shallow-submodules"]);
41 } else {
42 cmd.args(["--depth=1", "--no-checkout", "--filter=blob:none", "--no-recurse-submodules"]);
43 }
44 cmd.args([repo_url, target_dir]);
45 test_debug!("{cmd:?}");
46 let status = cmd.status().unwrap();
47 if !status.success() {
48 panic!("git clone failed: {status}");
49 }
50}
51
52#[track_caller]
58pub fn setup_forge(name: &str, style: PathStyle) -> (TestProject, TestCommand) {
59 setup_forge_project(TestProject::new(name, style))
60}
61
62pub fn setup_forge_project(test: TestProject) -> (TestProject, TestCommand) {
63 let cmd = test.forge_command();
64 (test, cmd)
65}
66
67#[derive(Clone, Debug)]
69pub struct RemoteProject {
70 id: String,
71 run_build: bool,
72 run_commands: Vec<Vec<String>>,
73 path_style: PathStyle,
74}
75
76impl RemoteProject {
77 pub fn new(id: impl Into<String>) -> Self {
78 Self {
79 id: id.into(),
80 run_build: true,
81 run_commands: vec![],
82 path_style: PathStyle::Dapptools,
83 }
84 }
85
86 pub fn set_build(mut self, run_build: bool) -> Self {
88 self.run_build = run_build;
89 self
90 }
91
92 pub fn path_style(mut self, path_style: PathStyle) -> Self {
94 self.path_style = path_style;
95 self
96 }
97
98 pub fn cmd(mut self, cmd: impl IntoIterator<Item = impl Into<String>>) -> Self {
100 self.run_commands.push(cmd.into_iter().map(Into::into).collect());
101 self
102 }
103}
104
105impl<T: Into<String>> From<T> for RemoteProject {
106 fn from(id: T) -> Self {
107 Self::new(id)
108 }
109}
110
111pub fn setup_forge_remote(prj: impl Into<RemoteProject>) -> (TestProject, TestCommand) {
122 try_setup_forge_remote(prj).unwrap()
123}
124
125pub fn try_setup_forge_remote(
127 config: impl Into<RemoteProject>,
128) -> Result<(TestProject, TestCommand)> {
129 let config = config.into();
130 let mut tmp = TempProject::checkout(&config.id).wrap_err("failed to checkout project")?;
131 tmp.project_mut().paths = config.path_style.paths(tmp.root())?;
132
133 let prj = TestProject::with_project(tmp);
134 if config.run_build {
135 let mut cmd = prj.forge_command();
136 cmd.arg("build").assert_success();
137 }
138 for addon in config.run_commands {
139 debug_assert!(!addon.is_empty());
140 let mut cmd = Command::new(&addon[0]);
141 if addon.len() > 1 {
142 cmd.args(&addon[1..]);
143 }
144 let status = cmd
145 .current_dir(prj.root())
146 .stdout(Stdio::null())
147 .stderr(Stdio::null())
148 .status()
149 .wrap_err_with(|| format!("Failed to execute {addon:?}"))?;
150 eyre::ensure!(status.success(), "Failed to execute command {:?}", addon);
151 }
152
153 let cmd = prj.forge_command();
154 Ok((prj, cmd))
155}
156
157pub fn setup_cast(name: &str, style: PathStyle) -> (TestProject, TestCommand) {
158 setup_cast_project(TestProject::new(name, style))
159}
160
161pub fn setup_cast_project(test: TestProject) -> (TestProject, TestCommand) {
162 let cmd = test.cast_command();
163 (test, cmd)
164}
165
166#[derive(Clone, Debug)]
170pub struct TestProject<
171 T: ArtifactOutput<CompilerContract = Contract> + Default = ConfigurableArtifacts,
172> {
173 exe_root: PathBuf,
175 pub(crate) inner: Arc<TempProject<MultiCompiler, T>>,
177}
178
179impl TestProject {
180 pub fn new(name: &str, style: PathStyle) -> Self {
184 let id = NEXT_ID.fetch_add(1, Ordering::SeqCst);
185 let project = pretty_err(name, TempProject::with_style(&format!("{name}-{id}"), style));
186 Self::with_project(project)
187 }
188
189 pub fn with_project(project: TempProject) -> Self {
190 init_tracing();
191 let this = env::current_exe().unwrap();
192 let exe_root = canonicalize(this.parent().expect("executable's directory"));
193 Self { exe_root, inner: Arc::new(project) }
194 }
195
196 pub fn root(&self) -> &Path {
198 self.inner.root()
199 }
200
201 pub fn paths(&self) -> &ProjectPathsConfig {
203 self.inner.paths()
204 }
205
206 pub fn config(&self) -> PathBuf {
208 self.root().join(Config::FILE_NAME)
209 }
210
211 pub fn cache(&self) -> &PathBuf {
213 &self.paths().cache
214 }
215
216 pub fn artifacts(&self) -> &PathBuf {
218 &self.paths().artifacts
219 }
220
221 pub fn clear(&self) {
223 self.clear_cache();
224 self.clear_artifacts();
225 }
226
227 pub fn clear_cache(&self) {
229 let _ = fs::remove_file(self.cache());
230 }
231
232 pub fn clear_artifacts(&self) {
234 let _ = fs::remove_dir_all(self.artifacts());
235 }
236
237 pub fn clear_cache_dir(&self) {
239 let _ = fs::remove_dir_all(self.root().join("cache"));
240 }
241
242 pub fn update_config(&self, f: impl FnOnce(&mut Config)) {
244 self._update_config(Box::new(f));
245 }
246
247 fn _update_config(&self, f: Box<dyn FnOnce(&mut Config) + '_>) {
248 let mut config = self
249 .config()
250 .exists()
251 .then_some(())
252 .and_then(|()| Config::load_with_root(self.root()).ok())
253 .unwrap_or_default();
254 config.remappings.clear();
255 f(&mut config);
256 self.write_config(config);
257 }
258
259 #[doc(hidden)] pub fn write_config(&self, config: Config) {
262 let file = self.config();
263 pretty_err(&file, fs::write(&file, config.to_string_pretty().unwrap()));
264 }
265
266 pub fn add_rpc_endpoints(&self) {
268 self.update_config(|config| {
269 config.rpc_endpoints = rpc_endpoints();
270 });
271 }
272
273 pub fn add_source(&self, name: &str, contents: &str) -> PathBuf {
275 self.inner.add_source(name, Self::add_source_prelude(contents)).unwrap()
276 }
277
278 pub fn add_raw_source(&self, name: &str, contents: &str) -> PathBuf {
280 self.inner.add_source(name, contents).unwrap()
281 }
282
283 pub fn add_script(&self, name: &str, contents: &str) -> PathBuf {
285 self.inner.add_script(name, Self::add_source_prelude(contents)).unwrap()
286 }
287
288 pub fn add_raw_script(&self, name: &str, contents: &str) -> PathBuf {
290 self.inner.add_script(name, contents).unwrap()
291 }
292
293 pub fn add_test(&self, name: &str, contents: &str) -> PathBuf {
295 self.inner.add_test(name, Self::add_source_prelude(contents)).unwrap()
296 }
297
298 pub fn add_raw_test(&self, name: &str, contents: &str) -> PathBuf {
300 self.inner.add_test(name, contents).unwrap()
301 }
302
303 pub fn add_lib(&self, name: &str, contents: &str) -> PathBuf {
305 self.inner.add_lib(name, Self::add_source_prelude(contents)).unwrap()
306 }
307
308 pub fn add_raw_lib(&self, name: &str, contents: &str) -> PathBuf {
310 self.inner.add_lib(name, contents).unwrap()
311 }
312
313 fn add_source_prelude(s: &str) -> String {
314 let mut s = s.to_string();
315 if !s.contains("pragma solidity") {
316 s = format!("pragma solidity ={SOLC_VERSION};\n{s}");
317 }
318 if !s.contains("// SPDX") {
319 s = format!("// SPDX-License-Identifier: MIT OR Apache-2.0\n{s}");
320 }
321 s
322 }
323
324 #[track_caller]
326 pub fn assert_config_exists(&self) {
327 assert!(self.config().exists());
328 }
329
330 #[track_caller]
332 pub fn assert_cache_exists(&self) {
333 assert!(self.cache().exists());
334 }
335
336 #[track_caller]
338 pub fn assert_artifacts_dir_exists(&self) {
339 assert!(self.paths().artifacts.exists());
340 }
341
342 #[track_caller]
344 pub fn assert_create_dirs_exists(&self) {
345 self.paths().create_all().unwrap_or_else(|_| panic!("Failed to create project paths"));
346 CompilerCache::<SolcSettings>::default()
347 .write(&self.paths().cache)
348 .expect("Failed to create cache");
349 self.assert_all_paths_exist();
350 }
351
352 #[track_caller]
354 pub fn assert_style_paths_exist(&self, style: PathStyle) {
355 let paths = style.paths(&self.paths().root).unwrap();
356 config_paths_exist(&paths, self.inner.project().cached);
357 }
358
359 #[track_caller]
361 pub fn copy_to(&self, target: impl AsRef<Path>) {
362 let target = target.as_ref();
363 pretty_err(target, fs::create_dir_all(target));
364 pretty_err(target, copy_dir(self.root(), target));
365 }
366
367 pub fn create_file(&self, path: impl AsRef<Path>, contents: &str) -> PathBuf {
370 let path = path.as_ref();
371 if !path.is_relative() {
372 panic!("create_file(): file path is absolute");
373 }
374 let path = self.root().join(path);
375 if let Some(parent) = path.parent() {
376 pretty_err(parent, std::fs::create_dir_all(parent));
377 }
378 let file = pretty_err(&path, File::create(&path));
379 let mut writer = BufWriter::new(file);
380 pretty_err(&path, writer.write_all(contents.as_bytes()));
381 path
382 }
383
384 pub fn insert_ds_test(&self) -> PathBuf {
386 self.add_source("test.sol", include_str!("../../../testdata/utils/DSTest.sol"))
387 }
388
389 pub fn insert_utils(&self) {
391 self.add_test("utils/DSTest.sol", include_str!("../../../testdata/utils/DSTest.sol"));
392 self.add_test("utils/Test.sol", include_str!("../../../testdata/utils/Test.sol"));
393 self.add_test("utils/Vm.sol", include_str!("../../../testdata/utils/Vm.sol"));
394 self.add_test("utils/console.sol", include_str!("../../../testdata/utils/console.sol"));
395 }
396
397 pub fn insert_console(&self) -> PathBuf {
399 let s = include_str!("../../../testdata/utils/console.sol");
400 self.add_source("console.sol", s)
401 }
402
403 pub fn insert_vm(&self) -> PathBuf {
405 let s = include_str!("../../../testdata/utils/Vm.sol");
406 self.add_source("Vm.sol", s)
407 }
408
409 pub fn assert_all_paths_exist(&self) {
415 let paths = self.paths();
416 config_paths_exist(paths, self.inner.project().cached);
417 }
418
419 pub fn assert_cleaned(&self) {
421 let paths = self.paths();
422 assert!(!paths.cache.exists());
423 assert!(!paths.artifacts.exists());
424 }
425
426 #[track_caller]
428 pub fn forge_command(&self) -> TestCommand {
429 let cmd = self.forge_bin();
430 let _lock = CURRENT_DIR_LOCK.lock();
431 TestCommand {
432 project: self.clone(),
433 cmd,
434 current_dir_lock: None,
435 saved_cwd: pretty_err("<current dir>", std::env::current_dir()),
436 stdin: None,
437 redact_output: true,
438 }
439 }
440
441 pub fn cast_command(&self) -> TestCommand {
443 let mut cmd = self.cast_bin();
444 cmd.current_dir(self.inner.root());
445 let _lock = CURRENT_DIR_LOCK.lock();
446 TestCommand {
447 project: self.clone(),
448 cmd,
449 current_dir_lock: None,
450 saved_cwd: pretty_err("<current dir>", std::env::current_dir()),
451 stdin: None,
452 redact_output: true,
453 }
454 }
455
456 pub fn forge_bin(&self) -> Command {
458 let mut cmd = Command::new(self.forge_path());
459 cmd.current_dir(self.inner.root());
460 cmd.env("NO_COLOR", "1");
462 cmd
463 }
464
465 pub(crate) fn forge_path(&self) -> PathBuf {
466 canonicalize(self.exe_root.join(format!("../forge{}", env::consts::EXE_SUFFIX)))
467 }
468
469 pub fn cast_bin(&self) -> Command {
471 let cast = canonicalize(self.exe_root.join(format!("../cast{}", env::consts::EXE_SUFFIX)));
472 let mut cmd = Command::new(cast);
473 cmd.env("NO_COLOR", "1");
475 cmd
476 }
477
478 pub fn config_from_output<I, A>(&self, args: I) -> Config
480 where
481 I: IntoIterator<Item = A>,
482 A: AsRef<OsStr>,
483 {
484 let mut cmd = self.forge_bin();
485 cmd.arg("config").arg("--root").arg(self.root()).args(args).arg("--json");
486 let output = cmd.output().unwrap();
487 let c = lossy_string(&output.stdout);
488 let config: Config = serde_json::from_str(c.as_ref()).unwrap();
489 config.sanitized()
490 }
491
492 pub fn wipe(&self) {
494 pretty_err(self.root(), fs::remove_dir_all(self.root()));
495 pretty_err(self.root(), fs::create_dir_all(self.root()));
496 }
497
498 pub fn wipe_contracts(&self) {
500 fn rm_create(path: &Path) {
501 pretty_err(path, fs::remove_dir_all(path));
502 pretty_err(path, fs::create_dir(path));
503 }
504 rm_create(&self.paths().sources);
505 rm_create(&self.paths().tests);
506 rm_create(&self.paths().scripts);
507 }
508
509 pub fn initialize_default_contracts(&self) {
515 self.add_raw_source(
516 "Counter.sol",
517 include_str!("../../forge/assets/solidity/CounterTemplate.sol"),
518 );
519 self.add_raw_test(
520 "Counter.t.sol",
521 include_str!("../../forge/assets/solidity/CounterTemplate.t.sol"),
522 );
523 self.add_raw_script(
524 "Counter.s.sol",
525 include_str!("../../forge/assets/solidity/CounterTemplate.s.sol"),
526 );
527 }
528}
529
530fn config_paths_exist(paths: &ProjectPathsConfig, cached: bool) {
531 if cached {
532 assert!(paths.cache.exists());
533 }
534 assert!(paths.sources.exists());
535 assert!(paths.artifacts.exists());
536 paths.libraries.iter().for_each(|lib| assert!(lib.exists()));
537}
538
539pub struct TestCommand {
541 saved_cwd: PathBuf,
542 project: TestProject,
544 cmd: Command,
546 current_dir_lock: Option<parking_lot::MutexGuard<'static, ()>>,
548 stdin: Option<Vec<u8>>,
549 redact_output: bool,
551}
552
553impl TestCommand {
554 pub fn cmd(&mut self) -> &mut Command {
556 &mut self.cmd
557 }
558
559 pub fn set_cmd(&mut self, cmd: Command) -> &mut Self {
561 self.cmd = cmd;
562 self
563 }
564
565 pub fn forge_fuse(&mut self) -> &mut Self {
567 self.set_cmd(self.project.forge_bin())
568 }
569
570 pub fn cast_fuse(&mut self) -> &mut Self {
572 self.set_cmd(self.project.cast_bin())
573 }
574
575 pub fn set_current_dir(&mut self, p: impl AsRef<Path>) {
577 drop(self.current_dir_lock.take());
578 let lock = CURRENT_DIR_LOCK.lock();
579 self.current_dir_lock = Some(lock);
580 let p = p.as_ref();
581 pretty_err(p, std::env::set_current_dir(p));
582 }
583
584 pub fn arg<A: AsRef<OsStr>>(&mut self, arg: A) -> &mut Self {
586 self.cmd.arg(arg);
587 self
588 }
589
590 pub fn args<I, A>(&mut self, args: I) -> &mut Self
592 where
593 I: IntoIterator<Item = A>,
594 A: AsRef<OsStr>,
595 {
596 self.cmd.args(args);
597 self
598 }
599
600 pub fn stdin(&mut self, stdin: impl Into<Vec<u8>>) -> &mut Self {
602 self.stdin = Some(stdin.into());
603 self
604 }
605
606 pub fn root_arg(&mut self) -> &mut Self {
608 let root = self.project.root().to_path_buf();
609 self.arg("--root").arg(root)
610 }
611
612 pub fn env(&mut self, k: impl AsRef<OsStr>, v: impl AsRef<OsStr>) {
614 self.cmd.env(k, v);
615 }
616
617 pub fn envs<I, K, V>(&mut self, envs: I)
619 where
620 I: IntoIterator<Item = (K, V)>,
621 K: AsRef<OsStr>,
622 V: AsRef<OsStr>,
623 {
624 self.cmd.envs(envs);
625 }
626
627 pub fn unset_env(&mut self, k: impl AsRef<OsStr>) {
629 self.cmd.env_remove(k);
630 }
631
632 pub fn current_dir<P: AsRef<Path>>(&mut self, dir: P) -> &mut Self {
638 self.cmd.current_dir(dir);
639 self
640 }
641
642 #[track_caller]
644 pub fn config(&mut self) -> Config {
645 self.cmd.args(["config", "--json"]);
646 let output = self.assert().success().get_output().stdout_lossy();
647 self.forge_fuse();
648 serde_json::from_str(output.as_ref()).unwrap()
649 }
650
651 #[track_caller]
653 pub fn git_init(&self) {
654 let mut cmd = Command::new("git");
655 cmd.arg("init").current_dir(self.project.root());
656 let output = OutputAssert::new(cmd.output().unwrap());
657 output.success();
658 }
659
660 #[track_caller]
662 pub fn git_submodule_status(&self) -> Output {
663 let mut cmd = Command::new("git");
664 cmd.arg("submodule").arg("status").current_dir(self.project.root());
665 cmd.output().unwrap()
666 }
667
668 #[track_caller]
670 pub fn git_add(&self) {
671 let mut cmd = Command::new("git");
672 cmd.current_dir(self.project.root());
673 cmd.arg("add").arg(".");
674 let output = OutputAssert::new(cmd.output().unwrap());
675 output.success();
676 }
677
678 #[track_caller]
680 pub fn git_commit(&self, msg: &str) {
681 let mut cmd = Command::new("git");
682 cmd.current_dir(self.project.root());
683 cmd.arg("commit").arg("-m").arg(msg);
684 let output = OutputAssert::new(cmd.output().unwrap());
685 output.success();
686 }
687
688 #[track_caller]
690 pub fn assert_with(&mut self, f: &[RegexRedaction]) -> OutputAssert {
691 let assert = OutputAssert::new(self.execute());
692 if self.redact_output {
693 let mut redactions = test_redactions();
694 insert_redactions(f, &mut redactions);
695 return assert.with_assert(
696 snapbox::Assert::new()
697 .action_env(snapbox::assert::DEFAULT_ACTION_ENV)
698 .redact_with(redactions),
699 );
700 }
701 assert
702 }
703
704 #[track_caller]
706 pub fn assert(&mut self) -> OutputAssert {
707 self.assert_with(&[])
708 }
709
710 #[track_caller]
712 pub fn assert_success(&mut self) -> OutputAssert {
713 self.assert().success()
714 }
715
716 #[track_caller]
718 pub fn assert_json_stdout(&mut self, expected: impl IntoData) {
719 let expected = expected.is(snapbox::data::DataFormat::Json).unordered();
720 let stdout = self.assert_success().get_output().stdout.clone();
721 let actual = stdout.into_data().is(snapbox::data::DataFormat::Json).unordered();
722 assert_data_eq!(actual, expected);
723 }
724
725 #[track_caller]
727 pub fn assert_json_stderr(&mut self, success: bool, expected: impl IntoData) {
728 let expected = expected.is(snapbox::data::DataFormat::Json).unordered();
729 let stderr = if success { self.assert_success() } else { self.assert_failure() }
730 .get_output()
731 .stderr
732 .clone();
733 let actual = stderr.into_data().is(snapbox::data::DataFormat::Json).unordered();
734 assert_data_eq!(actual, expected);
735 }
736
737 #[track_caller]
739 pub fn assert_empty_stdout(&mut self) {
740 self.assert_success().stdout_eq(Data::new());
741 }
742
743 #[track_caller]
745 pub fn assert_failure(&mut self) -> OutputAssert {
746 self.assert().failure()
747 }
748
749 #[track_caller]
751 pub fn assert_code(&mut self, expected: i32) -> OutputAssert {
752 self.assert().code(expected)
753 }
754
755 #[track_caller]
757 pub fn assert_empty_stderr(&mut self) {
758 self.assert_failure().stderr_eq(Data::new());
759 }
760
761 #[track_caller]
764 pub fn assert_file(&mut self, data: impl IntoData) {
765 self.assert_file_with(|this, path| _ = this.arg(path).assert_success(), data);
766 }
767
768 #[track_caller]
771 pub fn assert_file_with(&mut self, f: impl FnOnce(&mut Self, &Path), data: impl IntoData) {
772 let file = tempfile::NamedTempFile::new().expect("couldn't create temporary file");
773 f(self, file.path());
774 assert_data_eq!(Data::read_from(file.path(), None), data);
775 }
776
777 pub fn with_no_redact(&mut self) -> &mut Self {
779 self.redact_output = false;
780 self
781 }
782
783 #[track_caller]
785 pub fn execute(&mut self) -> Output {
786 self.try_execute().unwrap()
787 }
788
789 #[track_caller]
790 pub fn try_execute(&mut self) -> std::io::Result<Output> {
791 test_debug!("executing {:?}", self.cmd);
792 let mut child =
793 self.cmd.stdout(Stdio::piped()).stderr(Stdio::piped()).stdin(Stdio::piped()).spawn()?;
794 if let Some(bytes) = self.stdin.take() {
795 child.stdin.take().unwrap().write_all(&bytes)?;
796 }
797 let output = child.wait_with_output()?;
798 test_debug!("exited with {}", output.status);
799 test_trace!("\n--- stdout ---\n{}\n--- /stdout ---", output.stdout_lossy());
800 test_trace!("\n--- stderr ---\n{}\n--- /stderr ---", output.stderr_lossy());
801 Ok(output)
802 }
803}
804
805impl Drop for TestCommand {
806 fn drop(&mut self) {
807 let _lock = self.current_dir_lock.take().unwrap_or_else(|| CURRENT_DIR_LOCK.lock());
808 if self.saved_cwd.exists() {
809 let _ = std::env::set_current_dir(&self.saved_cwd);
810 }
811 }
812}
813
814fn test_redactions() -> snapbox::Redactions {
815 static REDACTIONS: LazyLock<snapbox::Redactions> = LazyLock::new(|| {
816 make_redactions(&[
817 ("[SOLC_VERSION]", r"Solc( version)? \d+.\d+.\d+"),
818 ("[ELAPSED]", r"(finished )?in \d+(\.\d+)?\w?s( \(.*?s CPU time\))?"),
819 ("[GAS]", r"[Gg]as( used)?: \d+"),
820 ("[GAS_COST]", r"[Gg]as cost\s*\(\d+\)"),
821 ("[GAS_LIMIT]", r"[Gg]as limit\s*\(\d+\)"),
822 ("[AVG_GAS]", r"μ: \d+, ~: \d+"),
823 ("[FILE]", r"-->.*\.sol"),
824 ("[FILE]", r"Location(.|\n)*\.rs(.|\n)*Backtrace"),
825 ("[COMPILING_FILES]", r"Compiling \d+ files?"),
826 ("[TX_HASH]", r"Transaction hash: 0x[0-9A-Fa-f]{64}"),
827 ("[ADDRESS]", r"Address: +0x[0-9A-Fa-f]{40}"),
828 ("[PUBLIC_KEY]", r"Public key: +0x[0-9A-Fa-f]{128}"),
829 ("[PRIVATE_KEY]", r"Private key: +0x[0-9A-Fa-f]{64}"),
830 ("[UPDATING_DEPENDENCIES]", r"Updating dependencies in .*"),
831 ("[SAVED_TRANSACTIONS]", r"Transactions saved to: .*\.json"),
832 ("[SAVED_SENSITIVE_VALUES]", r"Sensitive values saved to: .*\.json"),
833 ("[ESTIMATED_GAS_PRICE]", r"Estimated gas price:\s*(\d+(\.\d+)?)\s*gwei"),
834 ("[ESTIMATED_TOTAL_GAS_USED]", r"Estimated total gas used for script: \d+"),
835 (
836 "[ESTIMATED_AMOUNT_REQUIRED]",
837 r"Estimated amount required:\s*(\d+(\.\d+)?)\s*[A-Z]{3}",
838 ),
839 ])
840 });
841 REDACTIONS.clone()
842}
843
844pub type RegexRedaction = (&'static str, &'static str);
846
847fn make_redactions(redactions: &[RegexRedaction]) -> snapbox::Redactions {
849 let mut r = snapbox::Redactions::new();
850 insert_redactions(redactions, &mut r);
851 r
852}
853
854fn insert_redactions(redactions: &[RegexRedaction], r: &mut snapbox::Redactions) {
855 for &(placeholder, re) in redactions {
856 r.insert(placeholder, Regex::new(re).expect(re)).expect(re);
857 }
858}
859
860pub trait OutputExt {
862 fn stdout_lossy(&self) -> String;
864
865 fn stderr_lossy(&self) -> String;
867}
868
869impl OutputExt for Output {
870 fn stdout_lossy(&self) -> String {
871 lossy_string(&self.stdout)
872 }
873
874 fn stderr_lossy(&self) -> String {
875 lossy_string(&self.stderr)
876 }
877}
878
879pub fn lossy_string(bytes: &[u8]) -> String {
880 String::from_utf8_lossy(bytes).replace("\r\n", "\n")
881}
882
883fn canonicalize(path: impl AsRef<Path>) -> PathBuf {
884 foundry_common::fs::canonicalize_path(path.as_ref())
885 .unwrap_or_else(|_| path.as_ref().to_path_buf())
886}