1use crate::init_tracing;
2use eyre::{Result, WrapErr};
3use foundry_compilers::{
4 artifacts::Contract,
5 cache::CompilerCache,
6 compilers::multi::MultiCompiler,
7 error::Result as SolcResult,
8 project_util::{copy_dir, TempProject},
9 solc::SolcSettings,
10 ArtifactOutput, ConfigurableArtifacts, PathStyle, ProjectPathsConfig,
11};
12use foundry_config::Config;
13use parking_lot::Mutex;
14use regex::Regex;
15use snapbox::{assert_data_eq, cmd::OutputAssert, Data, IntoData};
16use std::{
17 env,
18 ffi::OsStr,
19 fs::{self, File},
20 io::{BufWriter, IsTerminal, Read, Seek, Write},
21 path::{Path, PathBuf},
22 process::{ChildStdin, Command, Output, Stdio},
23 sync::{
24 atomic::{AtomicUsize, Ordering},
25 Arc, LazyLock,
26 },
27};
28
29static CURRENT_DIR_LOCK: LazyLock<Mutex<()>> = LazyLock::new(|| Mutex::new(()));
30
31const FORGE_STD_REVISION: &str = include_str!("../../../testdata/forge-std-rev");
33
34pub static IS_TTY: LazyLock<bool> = LazyLock::new(|| std::io::stdout().is_terminal());
36
37static TEMPLATE_PATH: LazyLock<PathBuf> =
40 LazyLock::new(|| env::temp_dir().join("foundry-forge-test-template"));
41
42static TEMPLATE_LOCK: LazyLock<PathBuf> =
45 LazyLock::new(|| env::temp_dir().join("foundry-forge-test-template.lock"));
46
47static NEXT_ID: AtomicUsize = AtomicUsize::new(0);
49
50pub const SOLC_VERSION: &str = "0.8.27";
52
53pub const OTHER_SOLC_VERSION: &str = "0.8.26";
57
58#[derive(Clone, Debug)]
60#[must_use = "ExtTester does nothing unless you `run` it"]
61pub struct ExtTester {
62 pub org: &'static str,
63 pub name: &'static str,
64 pub rev: &'static str,
65 pub style: PathStyle,
66 pub fork_block: Option<u64>,
67 pub args: Vec<String>,
68 pub envs: Vec<(String, String)>,
69 pub install_commands: Vec<Vec<String>>,
70}
71
72impl ExtTester {
73 pub fn new(org: &'static str, name: &'static str, rev: &'static str) -> Self {
75 Self {
76 org,
77 name,
78 rev,
79 style: PathStyle::Dapptools,
80 fork_block: None,
81 args: vec![],
82 envs: vec![],
83 install_commands: vec![],
84 }
85 }
86
87 pub fn style(mut self, style: PathStyle) -> Self {
89 self.style = style;
90 self
91 }
92
93 pub fn fork_block(mut self, fork_block: u64) -> Self {
95 self.fork_block = Some(fork_block);
96 self
97 }
98
99 pub fn arg(mut self, arg: impl Into<String>) -> Self {
101 self.args.push(arg.into());
102 self
103 }
104
105 pub fn args<I, A>(mut self, args: I) -> Self
107 where
108 I: IntoIterator<Item = A>,
109 A: Into<String>,
110 {
111 self.args.extend(args.into_iter().map(Into::into));
112 self
113 }
114
115 pub fn env(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
117 self.envs.push((key.into(), value.into()));
118 self
119 }
120
121 pub fn envs<I, K, V>(mut self, envs: I) -> Self
123 where
124 I: IntoIterator<Item = (K, V)>,
125 K: Into<String>,
126 V: Into<String>,
127 {
128 self.envs.extend(envs.into_iter().map(|(k, v)| (k.into(), v.into())));
129 self
130 }
131
132 pub fn install_command(mut self, command: &[&str]) -> Self {
137 self.install_commands.push(command.iter().map(|s| s.to_string()).collect());
138 self
139 }
140
141 pub fn run(&self) {
143 if self.fork_block.is_some() && std::env::var_os("ETH_RPC_URL").is_none() {
145 eprintln!("ETH_RPC_URL is not set; skipping");
146 return;
147 }
148
149 let (prj, mut test_cmd) = setup_forge(self.name, self.style.clone());
150
151 prj.wipe();
153
154 let repo_url = format!("https://github.com/{}/{}.git", self.org, self.name);
156 let root = prj.root().to_str().unwrap();
157 clone_remote(&repo_url, root);
158
159 if self.rev.is_empty() {
161 let mut git = Command::new("git");
162 git.current_dir(root).args(["log", "-n", "1"]);
163 println!("$ {git:?}");
164 let output = git.output().unwrap();
165 if !output.status.success() {
166 panic!("git log failed: {output:?}");
167 }
168 let stdout = String::from_utf8(output.stdout).unwrap();
169 let commit = stdout.lines().next().unwrap().split_whitespace().nth(1).unwrap();
170 panic!("pin to latest commit: {commit}");
171 } else {
172 let mut git = Command::new("git");
173 git.current_dir(root).args(["checkout", self.rev]);
174 println!("$ {git:?}");
175 let status = git.status().unwrap();
176 if !status.success() {
177 panic!("git checkout failed: {status}");
178 }
179 }
180
181 for install_command in &self.install_commands {
183 let mut install_cmd = Command::new(&install_command[0]);
184 install_cmd.args(&install_command[1..]).current_dir(root);
185 println!("cd {root}; {install_cmd:?}");
186 match install_cmd.status() {
187 Ok(s) => {
188 println!("\n\n{install_cmd:?}: {s}");
189 if s.success() {
190 break;
191 }
192 }
193 Err(e) => {
194 eprintln!("\n\n{install_cmd:?}: {e}");
195 }
196 }
197 }
198
199 test_cmd.arg("test");
201 test_cmd.args(&self.args);
202 test_cmd.args(["--fuzz-runs=32", "--ffi", "-vvv"]);
203
204 test_cmd.envs(self.envs.iter().map(|(k, v)| (k, v)));
205 if let Some(fork_block) = self.fork_block {
206 test_cmd.env("FOUNDRY_ETH_RPC_URL", crate::rpc::next_http_archive_rpc_url());
207 test_cmd.env("FOUNDRY_FORK_BLOCK_NUMBER", fork_block.to_string());
208 }
209 test_cmd.env("FOUNDRY_INVARIANT_DEPTH", "15");
210 test_cmd.env("FOUNDRY_ALLOW_INTERNAL_EXPECT_REVERT", "true");
211
212 test_cmd.assert_success();
213 }
214}
215
216#[expect(clippy::disallowed_macros)]
232pub fn initialize(target: &Path) {
233 println!("initializing {}", target.display());
234
235 let tpath = TEMPLATE_PATH.as_path();
236 pretty_err(tpath, fs::create_dir_all(tpath));
237
238 let mut lock = crate::fd_lock::new_lock(TEMPLATE_LOCK.as_path());
240 let mut _read = Some(lock.read().unwrap());
241 if fs::read(&*TEMPLATE_LOCK).unwrap() != b"1" {
242 _read = None;
252
253 let mut write = lock.write().unwrap();
254
255 let mut data = String::new();
256 write.read_to_string(&mut data).unwrap();
257
258 if data != "1" {
259 let (prj, mut cmd) = setup_forge("template", foundry_compilers::PathStyle::Dapptools);
261 println!("- initializing template dir in {}", prj.root().display());
262
263 cmd.args(["init", "--force"]).assert_success();
264 prj.write_config(Config {
265 solc: Some(foundry_config::SolcReq::Version(SOLC_VERSION.parse().unwrap())),
266 ..Default::default()
267 });
268
269 let output = Command::new("git")
271 .current_dir(prj.root().join("lib/forge-std"))
272 .args(["checkout", FORGE_STD_REVISION])
273 .output()
274 .expect("failed to checkout forge-std");
275 assert!(output.status.success(), "{output:#?}");
276
277 cmd.forge_fuse().arg("build").assert_success();
279
280 let _ = fs::remove_dir_all(tpath);
282
283 pretty_err(tpath, copy_dir(prj.root(), tpath));
285
286 write.set_len(0).unwrap();
288 write.seek(std::io::SeekFrom::Start(0)).unwrap();
289 write.write_all(b"1").unwrap();
290 }
291
292 drop(write);
294 _read = Some(lock.read().unwrap());
295 }
296
297 println!("- copying template dir from {}", tpath.display());
298 pretty_err(target, fs::create_dir_all(target));
299 pretty_err(target, copy_dir(tpath, target));
300}
301
302pub fn clone_remote(repo_url: &str, target_dir: &str) {
304 let mut cmd = Command::new("git");
305 cmd.args(["clone", "--no-tags", "--recursive", "--shallow-submodules"]);
306 cmd.args([repo_url, target_dir]);
307 println!("{cmd:?}");
308 let status = cmd.status().unwrap();
309 if !status.success() {
310 panic!("git clone failed: {status}");
311 }
312 println!();
313}
314
315#[track_caller]
321pub fn setup_forge(name: &str, style: PathStyle) -> (TestProject, TestCommand) {
322 setup_forge_project(TestProject::new(name, style))
323}
324
325pub fn setup_forge_project(test: TestProject) -> (TestProject, TestCommand) {
326 let cmd = test.forge_command();
327 (test, cmd)
328}
329
330#[derive(Clone, Debug)]
332pub struct RemoteProject {
333 id: String,
334 run_build: bool,
335 run_commands: Vec<Vec<String>>,
336 path_style: PathStyle,
337}
338
339impl RemoteProject {
340 pub fn new(id: impl Into<String>) -> Self {
341 Self {
342 id: id.into(),
343 run_build: true,
344 run_commands: vec![],
345 path_style: PathStyle::Dapptools,
346 }
347 }
348
349 pub fn set_build(mut self, run_build: bool) -> Self {
351 self.run_build = run_build;
352 self
353 }
354
355 pub fn path_style(mut self, path_style: PathStyle) -> Self {
357 self.path_style = path_style;
358 self
359 }
360
361 pub fn cmd(mut self, cmd: impl IntoIterator<Item = impl Into<String>>) -> Self {
363 self.run_commands.push(cmd.into_iter().map(Into::into).collect());
364 self
365 }
366}
367
368impl<T: Into<String>> From<T> for RemoteProject {
369 fn from(id: T) -> Self {
370 Self::new(id)
371 }
372}
373
374pub fn setup_forge_remote(prj: impl Into<RemoteProject>) -> (TestProject, TestCommand) {
385 try_setup_forge_remote(prj).unwrap()
386}
387
388pub fn try_setup_forge_remote(
390 config: impl Into<RemoteProject>,
391) -> Result<(TestProject, TestCommand)> {
392 let config = config.into();
393 let mut tmp = TempProject::checkout(&config.id).wrap_err("failed to checkout project")?;
394 tmp.project_mut().paths = config.path_style.paths(tmp.root())?;
395
396 let prj = TestProject::with_project(tmp);
397 if config.run_build {
398 let mut cmd = prj.forge_command();
399 cmd.arg("build").assert_success();
400 }
401 for addon in config.run_commands {
402 debug_assert!(!addon.is_empty());
403 let mut cmd = Command::new(&addon[0]);
404 if addon.len() > 1 {
405 cmd.args(&addon[1..]);
406 }
407 let status = cmd
408 .current_dir(prj.root())
409 .stdout(Stdio::null())
410 .stderr(Stdio::null())
411 .status()
412 .wrap_err_with(|| format!("Failed to execute {addon:?}"))?;
413 eyre::ensure!(status.success(), "Failed to execute command {:?}", addon);
414 }
415
416 let cmd = prj.forge_command();
417 Ok((prj, cmd))
418}
419
420pub fn setup_cast(name: &str, style: PathStyle) -> (TestProject, TestCommand) {
421 setup_cast_project(TestProject::new(name, style))
422}
423
424pub fn setup_cast_project(test: TestProject) -> (TestProject, TestCommand) {
425 let cmd = test.cast_command();
426 (test, cmd)
427}
428
429#[derive(Clone, Debug)]
433pub struct TestProject<
434 T: ArtifactOutput<CompilerContract = Contract> + Default = ConfigurableArtifacts,
435> {
436 exe_root: PathBuf,
438 inner: Arc<TempProject<MultiCompiler, T>>,
440}
441
442impl TestProject {
443 pub fn new(name: &str, style: PathStyle) -> Self {
447 let id = NEXT_ID.fetch_add(1, Ordering::SeqCst);
448 let project = pretty_err(name, TempProject::with_style(&format!("{name}-{id}"), style));
449 Self::with_project(project)
450 }
451
452 pub fn with_project(project: TempProject) -> Self {
453 init_tracing();
454 let this = env::current_exe().unwrap();
455 let exe_root = this.parent().expect("executable's directory").to_path_buf();
456 Self { exe_root, inner: Arc::new(project) }
457 }
458
459 pub fn root(&self) -> &Path {
461 self.inner.root()
462 }
463
464 pub fn paths(&self) -> &ProjectPathsConfig {
466 self.inner.paths()
467 }
468
469 pub fn config(&self) -> PathBuf {
471 self.root().join(Config::FILE_NAME)
472 }
473
474 pub fn cache(&self) -> &PathBuf {
476 &self.paths().cache
477 }
478
479 pub fn artifacts(&self) -> &PathBuf {
481 &self.paths().artifacts
482 }
483
484 pub fn clear(&self) {
486 self.clear_cache();
487 self.clear_artifacts();
488 }
489
490 pub fn clear_cache(&self) {
492 let _ = fs::remove_file(self.cache());
493 }
494
495 pub fn clear_artifacts(&self) {
497 let _ = fs::remove_dir_all(self.artifacts());
498 }
499
500 pub fn update_config(&self, f: impl FnOnce(&mut Config)) {
502 self._update_config(Box::new(f));
503 }
504
505 fn _update_config(&self, f: Box<dyn FnOnce(&mut Config) + '_>) {
506 let mut config = self
507 .config()
508 .exists()
509 .then_some(())
510 .and_then(|()| Config::load_with_root(self.root()).ok())
511 .unwrap_or_default();
512 config.remappings.clear();
513 f(&mut config);
514 self.write_config(config);
515 }
516
517 #[doc(hidden)] pub fn write_config(&self, config: Config) {
520 let file = self.config();
521 pretty_err(&file, fs::write(&file, config.to_string_pretty().unwrap()));
522 }
523
524 pub fn add_source(&self, name: &str, contents: &str) -> SolcResult<PathBuf> {
526 self.inner.add_source(name, Self::add_source_prelude(contents))
527 }
528
529 pub fn add_raw_source(&self, name: &str, contents: &str) -> SolcResult<PathBuf> {
531 self.inner.add_source(name, contents)
532 }
533
534 pub fn add_script(&self, name: &str, contents: &str) -> SolcResult<PathBuf> {
536 self.inner.add_script(name, Self::add_source_prelude(contents))
537 }
538
539 pub fn add_test(&self, name: &str, contents: &str) -> SolcResult<PathBuf> {
541 self.inner.add_test(name, Self::add_source_prelude(contents))
542 }
543
544 pub fn add_lib(&self, name: &str, contents: &str) -> SolcResult<PathBuf> {
546 self.inner.add_lib(name, Self::add_source_prelude(contents))
547 }
548
549 fn add_source_prelude(s: &str) -> String {
550 let mut s = s.to_string();
551 if !s.contains("pragma solidity") {
552 s = format!("pragma solidity ={SOLC_VERSION};\n{s}");
553 }
554 if !s.contains("// SPDX") {
555 s = format!("// SPDX-License-Identifier: MIT OR Apache-2.0\n{s}");
556 }
557 s
558 }
559
560 #[track_caller]
562 pub fn assert_config_exists(&self) {
563 assert!(self.config().exists());
564 }
565
566 #[track_caller]
568 pub fn assert_cache_exists(&self) {
569 assert!(self.cache().exists());
570 }
571
572 #[track_caller]
574 pub fn assert_artifacts_dir_exists(&self) {
575 assert!(self.paths().artifacts.exists());
576 }
577
578 #[track_caller]
580 pub fn assert_create_dirs_exists(&self) {
581 self.paths().create_all().unwrap_or_else(|_| panic!("Failed to create project paths"));
582 CompilerCache::<SolcSettings>::default()
583 .write(&self.paths().cache)
584 .expect("Failed to create cache");
585 self.assert_all_paths_exist();
586 }
587
588 #[track_caller]
590 pub fn assert_style_paths_exist(&self, style: PathStyle) {
591 let paths = style.paths(&self.paths().root).unwrap();
592 config_paths_exist(&paths, self.inner.project().cached);
593 }
594
595 #[track_caller]
597 pub fn copy_to(&self, target: impl AsRef<Path>) {
598 let target = target.as_ref();
599 pretty_err(target, fs::create_dir_all(target));
600 pretty_err(target, copy_dir(self.root(), target));
601 }
602
603 pub fn create_file(&self, path: impl AsRef<Path>, contents: &str) -> PathBuf {
606 let path = path.as_ref();
607 if !path.is_relative() {
608 panic!("create_file(): file path is absolute");
609 }
610 let path = self.root().join(path);
611 if let Some(parent) = path.parent() {
612 pretty_err(parent, std::fs::create_dir_all(parent));
613 }
614 let file = pretty_err(&path, File::create(&path));
615 let mut writer = BufWriter::new(file);
616 pretty_err(&path, writer.write_all(contents.as_bytes()));
617 path
618 }
619
620 pub fn insert_ds_test(&self) -> PathBuf {
622 let s = include_str!("../../../testdata/lib/ds-test/src/test.sol");
623 self.add_source("test.sol", s).unwrap()
624 }
625
626 pub fn insert_console(&self) -> PathBuf {
628 let s = include_str!("../../../testdata/default/logs/console.sol");
629 self.add_source("console.sol", s).unwrap()
630 }
631
632 pub fn insert_vm(&self) -> PathBuf {
634 let s = include_str!("../../../testdata/cheats/Vm.sol");
635 self.add_source("Vm.sol", s).unwrap()
636 }
637
638 pub fn assert_all_paths_exist(&self) {
644 let paths = self.paths();
645 config_paths_exist(paths, self.inner.project().cached);
646 }
647
648 pub fn assert_cleaned(&self) {
650 let paths = self.paths();
651 assert!(!paths.cache.exists());
652 assert!(!paths.artifacts.exists());
653 }
654
655 #[track_caller]
657 pub fn forge_command(&self) -> TestCommand {
658 let cmd = self.forge_bin();
659 let _lock = CURRENT_DIR_LOCK.lock();
660 TestCommand {
661 project: self.clone(),
662 cmd,
663 current_dir_lock: None,
664 saved_cwd: pretty_err("<current dir>", std::env::current_dir()),
665 stdin_fun: None,
666 redact_output: true,
667 }
668 }
669
670 pub fn cast_command(&self) -> TestCommand {
672 let mut cmd = self.cast_bin();
673 cmd.current_dir(self.inner.root());
674 let _lock = CURRENT_DIR_LOCK.lock();
675 TestCommand {
676 project: self.clone(),
677 cmd,
678 current_dir_lock: None,
679 saved_cwd: pretty_err("<current dir>", std::env::current_dir()),
680 stdin_fun: None,
681 redact_output: true,
682 }
683 }
684
685 pub fn forge_bin(&self) -> Command {
687 let forge = self.exe_root.join(format!("../forge{}", env::consts::EXE_SUFFIX));
688 let forge = forge.canonicalize().unwrap_or_else(|_| forge.clone());
689 let mut cmd = Command::new(forge);
690 cmd.current_dir(self.inner.root());
691 cmd.env("NO_COLOR", "1");
693 cmd
694 }
695
696 pub fn cast_bin(&self) -> Command {
698 let cast = self.exe_root.join(format!("../cast{}", env::consts::EXE_SUFFIX));
699 let cast = cast.canonicalize().unwrap_or_else(|_| cast.clone());
700 let mut cmd = Command::new(cast);
701 cmd.env("NO_COLOR", "1");
703 cmd
704 }
705
706 pub fn config_from_output<I, A>(&self, args: I) -> Config
708 where
709 I: IntoIterator<Item = A>,
710 A: AsRef<OsStr>,
711 {
712 let mut cmd = self.forge_bin();
713 cmd.arg("config").arg("--root").arg(self.root()).args(args).arg("--json");
714 let output = cmd.output().unwrap();
715 let c = lossy_string(&output.stdout);
716 let config: Config = serde_json::from_str(c.as_ref()).unwrap();
717 config.sanitized()
718 }
719
720 pub fn wipe(&self) {
722 pretty_err(self.root(), fs::remove_dir_all(self.root()));
723 pretty_err(self.root(), fs::create_dir_all(self.root()));
724 }
725
726 pub fn wipe_contracts(&self) {
728 fn rm_create(path: &Path) {
729 pretty_err(path, fs::remove_dir_all(path));
730 pretty_err(path, fs::create_dir(path));
731 }
732 rm_create(&self.paths().sources);
733 rm_create(&self.paths().tests);
734 rm_create(&self.paths().scripts);
735 }
736}
737
738impl Drop for TestCommand {
739 fn drop(&mut self) {
740 let _lock = self.current_dir_lock.take().unwrap_or_else(|| CURRENT_DIR_LOCK.lock());
741 if self.saved_cwd.exists() {
742 let _ = std::env::set_current_dir(&self.saved_cwd);
743 }
744 }
745}
746
747fn config_paths_exist(paths: &ProjectPathsConfig, cached: bool) {
748 if cached {
749 assert!(paths.cache.exists());
750 }
751 assert!(paths.sources.exists());
752 assert!(paths.artifacts.exists());
753 paths.libraries.iter().for_each(|lib| assert!(lib.exists()));
754}
755
756#[track_caller]
757pub fn pretty_err<T, E: std::error::Error>(path: impl AsRef<Path>, res: Result<T, E>) -> T {
758 match res {
759 Ok(t) => t,
760 Err(err) => panic!("{}: {err}", path.as_ref().display()),
761 }
762}
763
764pub fn read_string(path: impl AsRef<Path>) -> String {
765 let path = path.as_ref();
766 pretty_err(path, std::fs::read_to_string(path))
767}
768
769pub struct TestCommand {
771 saved_cwd: PathBuf,
772 project: TestProject,
774 cmd: Command,
776 current_dir_lock: Option<parking_lot::lock_api::MutexGuard<'static, parking_lot::RawMutex, ()>>,
778 stdin_fun: Option<Box<dyn FnOnce(ChildStdin)>>,
779 redact_output: bool,
781}
782
783impl TestCommand {
784 pub fn cmd(&mut self) -> &mut Command {
786 &mut self.cmd
787 }
788
789 pub fn set_cmd(&mut self, cmd: Command) -> &mut Self {
791 self.cmd = cmd;
792 self
793 }
794
795 pub fn forge_fuse(&mut self) -> &mut Self {
797 self.set_cmd(self.project.forge_bin())
798 }
799
800 pub fn cast_fuse(&mut self) -> &mut Self {
802 self.set_cmd(self.project.cast_bin())
803 }
804
805 pub fn set_current_dir(&mut self, p: impl AsRef<Path>) {
807 drop(self.current_dir_lock.take());
808 let lock = CURRENT_DIR_LOCK.lock();
809 self.current_dir_lock = Some(lock);
810 let p = p.as_ref();
811 pretty_err(p, std::env::set_current_dir(p));
812 }
813
814 pub fn arg<A: AsRef<OsStr>>(&mut self, arg: A) -> &mut Self {
816 self.cmd.arg(arg);
817 self
818 }
819
820 pub fn args<I, A>(&mut self, args: I) -> &mut Self
822 where
823 I: IntoIterator<Item = A>,
824 A: AsRef<OsStr>,
825 {
826 self.cmd.args(args);
827 self
828 }
829
830 pub fn stdin(&mut self, fun: impl FnOnce(ChildStdin) + 'static) -> &mut Self {
831 self.stdin_fun = Some(Box::new(fun));
832 self
833 }
834
835 pub fn root_arg(&mut self) -> &mut Self {
837 let root = self.project.root().to_path_buf();
838 self.arg("--root").arg(root)
839 }
840
841 pub fn env(&mut self, k: impl AsRef<OsStr>, v: impl AsRef<OsStr>) {
843 self.cmd.env(k, v);
844 }
845
846 pub fn envs<I, K, V>(&mut self, envs: I)
848 where
849 I: IntoIterator<Item = (K, V)>,
850 K: AsRef<OsStr>,
851 V: AsRef<OsStr>,
852 {
853 self.cmd.envs(envs);
854 }
855
856 pub fn unset_env(&mut self, k: impl AsRef<OsStr>) {
858 self.cmd.env_remove(k);
859 }
860
861 pub fn current_dir<P: AsRef<Path>>(&mut self, dir: P) -> &mut Self {
867 self.cmd.current_dir(dir);
868 self
869 }
870
871 #[track_caller]
873 pub fn config(&mut self) -> Config {
874 self.cmd.args(["config", "--json"]);
875 let output = self.assert().success().get_output().stdout_lossy();
876 self.forge_fuse();
877 serde_json::from_str(output.as_ref()).unwrap()
878 }
879
880 #[track_caller]
882 pub fn git_init(&self) {
883 let mut cmd = Command::new("git");
884 cmd.arg("init").current_dir(self.project.root());
885 let output = OutputAssert::new(cmd.output().unwrap());
886 output.success();
887 }
888
889 #[track_caller]
891 pub fn git_add(&self) {
892 let mut cmd = Command::new("git");
893 cmd.current_dir(self.project.root());
894 cmd.arg("add").arg(".");
895 let output = OutputAssert::new(cmd.output().unwrap());
896 output.success();
897 }
898
899 #[track_caller]
901 pub fn git_commit(&self, msg: &str) {
902 let mut cmd = Command::new("git");
903 cmd.current_dir(self.project.root());
904 cmd.arg("commit").arg("-m").arg(msg);
905 let output = OutputAssert::new(cmd.output().unwrap());
906 output.success();
907 }
908
909 #[track_caller]
911 pub fn assert(&mut self) -> OutputAssert {
912 let assert = OutputAssert::new(self.execute());
913 if self.redact_output {
914 return assert.with_assert(test_assert());
915 }
916 assert
917 }
918
919 #[track_caller]
921 pub fn assert_success(&mut self) -> OutputAssert {
922 self.assert().success()
923 }
924
925 #[track_caller]
927 pub fn assert_json_stdout(&mut self, expected: impl IntoData) {
928 let expected = expected.is(snapbox::data::DataFormat::Json).unordered();
929 let stdout = self.assert_success().get_output().stdout.clone();
930 let actual = stdout.into_data().is(snapbox::data::DataFormat::Json).unordered();
931 assert_data_eq!(actual, expected);
932 }
933
934 #[track_caller]
936 pub fn assert_empty_stdout(&mut self) {
937 self.assert_success().stdout_eq(Data::new());
938 }
939
940 #[track_caller]
942 pub fn assert_failure(&mut self) -> OutputAssert {
943 self.assert().failure()
944 }
945
946 #[track_caller]
948 pub fn assert_code(&mut self, expected: i32) -> OutputAssert {
949 self.assert().code(expected)
950 }
951
952 #[track_caller]
954 pub fn assert_empty_stderr(&mut self) {
955 self.assert_failure().stderr_eq(Data::new());
956 }
957
958 #[track_caller]
961 pub fn assert_file(&mut self, data: impl IntoData) {
962 self.assert_file_with(|this, path| _ = this.arg(path).assert_success(), data);
963 }
964
965 #[track_caller]
968 pub fn assert_file_with(&mut self, f: impl FnOnce(&mut Self, &Path), data: impl IntoData) {
969 let file = tempfile::NamedTempFile::new().expect("couldn't create temporary file");
970 f(self, file.path());
971 assert_data_eq!(Data::read_from(file.path(), None), data);
972 }
973
974 pub fn with_no_redact(&mut self) -> &mut Self {
976 self.redact_output = false;
977 self
978 }
979
980 #[track_caller]
982 pub fn execute(&mut self) -> Output {
983 self.try_execute().unwrap()
984 }
985
986 #[track_caller]
987 pub fn try_execute(&mut self) -> std::io::Result<Output> {
988 println!("executing {:?}", self.cmd);
989 let mut child =
990 self.cmd.stdout(Stdio::piped()).stderr(Stdio::piped()).stdin(Stdio::piped()).spawn()?;
991 if let Some(fun) = self.stdin_fun.take() {
992 fun(child.stdin.take().unwrap());
993 }
994 child.wait_with_output()
995 }
996}
997
998fn test_assert() -> snapbox::Assert {
999 snapbox::Assert::new()
1000 .action_env(snapbox::assert::DEFAULT_ACTION_ENV)
1001 .redact_with(test_redactions())
1002}
1003
1004fn test_redactions() -> snapbox::Redactions {
1005 static REDACTIONS: LazyLock<snapbox::Redactions> = LazyLock::new(|| {
1006 let mut r = snapbox::Redactions::new();
1007 let redactions = [
1008 ("[SOLC_VERSION]", r"Solc( version)? \d+.\d+.\d+"),
1009 ("[ELAPSED]", r"(finished )?in \d+(\.\d+)?\w?s( \(.*?s CPU time\))?"),
1010 ("[GAS]", r"[Gg]as( used)?: \d+"),
1011 ("[AVG_GAS]", r"μ: \d+, ~: \d+"),
1012 ("[FILE]", r"-->.*\.sol"),
1013 ("[FILE]", r"Location(.|\n)*\.rs(.|\n)*Backtrace"),
1014 ("[COMPILING_FILES]", r"Compiling \d+ files?"),
1015 ("[TX_HASH]", r"Transaction hash: 0x[0-9A-Fa-f]{64}"),
1016 ("[ADDRESS]", r"Address: 0x[0-9A-Fa-f]{40}"),
1017 ("[UPDATING_DEPENDENCIES]", r"Updating dependencies in .*"),
1018 ("[SAVED_TRANSACTIONS]", r"Transactions saved to: .*\.json"),
1019 ("[SAVED_SENSITIVE_VALUES]", r"Sensitive values saved to: .*\.json"),
1020 ("[ESTIMATED_GAS_PRICE]", r"Estimated gas price:\s*(\d+(\.\d+)?)\s*gwei"),
1021 ("[ESTIMATED_TOTAL_GAS_USED]", r"Estimated total gas used for script: \d+"),
1022 (
1023 "[ESTIMATED_AMOUNT_REQUIRED]",
1024 r"Estimated amount required:\s*(\d+(\.\d+)?)\s*[A-Z]{3}",
1025 ),
1026 ];
1027 for (placeholder, re) in redactions {
1028 r.insert(placeholder, Regex::new(re).expect(re)).expect(re);
1029 }
1030 r
1031 });
1032 REDACTIONS.clone()
1033}
1034
1035pub trait OutputExt {
1037 fn stdout_lossy(&self) -> String;
1039}
1040
1041impl OutputExt for Output {
1042 fn stdout_lossy(&self) -> String {
1043 lossy_string(&self.stdout)
1044 }
1045}
1046
1047pub fn lossy_string(bytes: &[u8]) -> String {
1048 String::from_utf8_lossy(bytes).replace("\r\n", "\n")
1049}