1use alloy_json_abi::JsonAbi;
2use alloy_primitives::{U256, map::HashMap};
3use alloy_provider::{Provider, network::AnyNetwork};
4use eyre::{ContextCompat, Result};
5use foundry_common::{
6 provider::{ProviderBuilder, RetryProvider},
7 shell,
8};
9use foundry_config::{Chain, Config};
10use itertools::Itertools;
11use regex::Regex;
12use serde::de::DeserializeOwned;
13use std::{
14 ffi::OsStr,
15 path::{Path, PathBuf},
16 process::{Command, Output, Stdio},
17 str::FromStr,
18 sync::LazyLock,
19 time::{Duration, SystemTime, UNIX_EPOCH},
20};
21use tracing_subscriber::prelude::*;
22
23mod cmd;
24pub use cmd::*;
25
26mod suggestions;
27pub use suggestions::*;
28
29mod abi;
30pub use abi::*;
31
32mod allocator;
33pub use allocator::*;
34
35#[doc(hidden)]
37pub use foundry_config::utils::*;
38
39pub const STATIC_FUZZ_SEED: [u8; 32] = [
43 0x01, 0x00, 0xfa, 0x69, 0xa5, 0xf1, 0x71, 0x0a, 0x95, 0xcd, 0xef, 0x94, 0x88, 0x9b, 0x02, 0x84,
44 0x5d, 0x64, 0x0b, 0x19, 0xad, 0xf0, 0xe3, 0x57, 0xb8, 0xd4, 0xbe, 0x7d, 0x49, 0xee, 0x70, 0xe6,
45];
46
47pub static SUBMODULE_BRANCH_REGEX: LazyLock<Regex> =
49 LazyLock::new(|| Regex::new(r#"\[submodule "([^"]+)"\](?:[^\[]*?branch = ([^\s]+))"#).unwrap());
50pub static SUBMODULE_STATUS_REGEX: LazyLock<Regex> =
52 LazyLock::new(|| Regex::new(r"^[\s+-]?([a-f0-9]+)\s+([^\s]+)(?:\s+\([^)]+\))?$").unwrap());
53
54pub trait FoundryPathExt {
56 fn is_sol_test(&self) -> bool;
58
59 fn is_sol(&self) -> bool;
61
62 fn is_yul(&self) -> bool;
64}
65
66impl<T: AsRef<Path>> FoundryPathExt for T {
67 fn is_sol_test(&self) -> bool {
68 self.as_ref()
69 .file_name()
70 .and_then(|s| s.to_str())
71 .map(|s| s.ends_with(".t.sol"))
72 .unwrap_or_default()
73 }
74
75 fn is_sol(&self) -> bool {
76 self.as_ref().extension() == Some(std::ffi::OsStr::new("sol"))
77 }
78
79 fn is_yul(&self) -> bool {
80 self.as_ref().extension() == Some(std::ffi::OsStr::new("yul"))
81 }
82}
83
84pub fn subscriber() {
86 let registry = tracing_subscriber::Registry::default()
87 .with(tracing_subscriber::EnvFilter::from_default_env());
88 #[cfg(feature = "tracy")]
89 let registry = registry.with(tracing_tracy::TracyLayer::default());
90 registry.with(tracing_subscriber::fmt::layer()).init()
91}
92
93pub fn abi_to_solidity(abi: &JsonAbi, name: &str) -> Result<String> {
94 let s = abi.to_sol(name, None);
95 let s = forge_fmt::format(&s)?;
96 Ok(s)
97}
98
99pub fn get_provider(config: &Config) -> Result<RetryProvider> {
102 get_provider_builder(config)?.build()
103}
104
105pub fn get_provider_builder(config: &Config) -> Result<ProviderBuilder> {
109 let url = config.get_rpc_url_or_localhost_http()?;
110 let mut builder = ProviderBuilder::new(url.as_ref());
111
112 builder = builder.accept_invalid_certs(config.eth_rpc_accept_invalid_certs);
113
114 if let Ok(chain) = config.chain.unwrap_or_default().try_into() {
115 builder = builder.chain(chain);
116 }
117
118 if let Some(jwt) = config.get_rpc_jwt_secret()? {
119 builder = builder.jwt(jwt.as_ref());
120 }
121
122 if let Some(rpc_timeout) = config.eth_rpc_timeout {
123 builder = builder.timeout(Duration::from_secs(rpc_timeout));
124 }
125
126 if let Some(rpc_headers) = config.eth_rpc_headers.clone() {
127 builder = builder.headers(rpc_headers);
128 }
129
130 Ok(builder)
131}
132
133pub async fn get_chain<P>(chain: Option<Chain>, provider: P) -> Result<Chain>
134where
135 P: Provider<AnyNetwork>,
136{
137 match chain {
138 Some(chain) => Ok(chain),
139 None => Ok(Chain::from_id(provider.get_chain_id().await?)),
140 }
141}
142
143pub fn parse_ether_value(value: &str) -> Result<U256> {
150 Ok(if value.starts_with("0x") {
151 U256::from_str_radix(value, 16)?
152 } else {
153 alloy_dyn_abi::DynSolType::coerce_str(&alloy_dyn_abi::DynSolType::Uint(256), value)?
154 .as_uint()
155 .wrap_err("Could not parse ether value from string")?
156 .0
157 })
158}
159
160pub fn parse_json<T: DeserializeOwned>(value: &str) -> serde_json::Result<T> {
162 serde_json::from_str(value)
163}
164
165pub fn parse_delay(delay: &str) -> Result<Duration> {
167 let delay = if delay.ends_with("ms") {
168 let d: u64 = delay.trim_end_matches("ms").parse()?;
169 Duration::from_millis(d)
170 } else {
171 let d: f64 = delay.parse()?;
172 let delay = (d * 1000.0).round();
173 if delay.is_infinite() || delay.is_nan() || delay.is_sign_negative() {
174 eyre::bail!("delay must be finite and non-negative");
175 }
176
177 Duration::from_millis(delay as u64)
178 };
179 Ok(delay)
180}
181
182pub fn now() -> Duration {
184 SystemTime::now().duration_since(UNIX_EPOCH).expect("time went backwards")
185}
186
187pub fn block_on<F: Future>(future: F) -> F::Output {
189 let rt = tokio::runtime::Runtime::new().expect("could not start tokio rt");
190 rt.block_on(future)
191}
192
193pub fn load_dotenv() {
201 let load = |p: &Path| {
202 dotenvy::from_path(p.join(".env")).ok();
203 };
204
205 if let (Ok(cwd), Ok(prj_root)) = (std::env::current_dir(), find_project_root(None)) {
209 load(&prj_root);
210 if cwd != prj_root {
211 load(&cwd);
213 }
214 };
215}
216
217pub fn enable_paint() {
219 let enable = yansi::Condition::os_support() && yansi::Condition::tty_and_color_live();
220 yansi::whenever(yansi::Condition::cached(enable));
221}
222
223pub fn install_crypto_provider() {
234 rustls::crypto::ring::default_provider()
236 .install_default()
237 .expect("Failed to install default rustls crypto provider");
238}
239
240pub trait CommandUtils {
242 fn exec(&mut self) -> Result<Output>;
244
245 fn get_stdout_lossy(&mut self) -> Result<String>;
247}
248
249impl CommandUtils for Command {
250 #[track_caller]
251 fn exec(&mut self) -> Result<Output> {
252 trace!(command=?self, "executing");
253
254 let output = self.output()?;
255
256 trace!(code=?output.status.code(), ?output);
257
258 if output.status.success() {
259 Ok(output)
260 } else {
261 let stdout = String::from_utf8_lossy(&output.stdout);
262 let stdout = stdout.trim();
263 let stderr = String::from_utf8_lossy(&output.stderr);
264 let stderr = stderr.trim();
265 let msg = if stdout.is_empty() {
266 stderr.to_string()
267 } else if stderr.is_empty() {
268 stdout.to_string()
269 } else {
270 format!("stdout:\n{stdout}\n\nstderr:\n{stderr}")
271 };
272
273 let mut name = self.get_program().to_string_lossy();
274 if let Some(arg) = self.get_args().next() {
275 let arg = arg.to_string_lossy();
276 if !arg.starts_with('-') {
277 let name = name.to_mut();
278 name.push(' ');
279 name.push_str(&arg);
280 }
281 }
282
283 let mut err = match output.status.code() {
284 Some(code) => format!("{name} exited with code {code}"),
285 None => format!("{name} terminated by a signal"),
286 };
287 if !msg.is_empty() {
288 err.push(':');
289 err.push(if msg.lines().count() == 0 { ' ' } else { '\n' });
290 err.push_str(&msg);
291 }
292 Err(eyre::eyre!(err))
293 }
294 }
295
296 #[track_caller]
297 fn get_stdout_lossy(&mut self) -> Result<String> {
298 let output = self.exec()?;
299 let stdout = String::from_utf8_lossy(&output.stdout);
300 Ok(stdout.trim().into())
301 }
302}
303
304#[derive(Clone, Copy, Debug)]
305pub struct Git<'a> {
306 pub root: &'a Path,
307 pub quiet: bool,
308 pub shallow: bool,
309}
310
311impl<'a> Git<'a> {
312 #[inline]
313 pub fn new(root: &'a Path) -> Self {
314 Self { root, quiet: shell::is_quiet(), shallow: false }
315 }
316
317 #[inline]
318 pub fn from_config(config: &'a Config) -> Self {
319 Self::new(config.root.as_path())
320 }
321
322 pub fn root_of(relative_to: &Path) -> Result<PathBuf> {
323 let output = Self::cmd_no_root()
324 .current_dir(relative_to)
325 .args(["rev-parse", "--show-toplevel"])
326 .get_stdout_lossy()?;
327 Ok(PathBuf::from(output))
328 }
329
330 pub fn clone_with_branch(
331 shallow: bool,
332 from: impl AsRef<OsStr>,
333 branch: impl AsRef<OsStr>,
334 to: Option<impl AsRef<OsStr>>,
335 ) -> Result<()> {
336 Self::cmd_no_root()
337 .stderr(Stdio::inherit())
338 .args(["clone", "--recurse-submodules"])
339 .args(shallow.then_some("--depth=1"))
340 .args(shallow.then_some("--shallow-submodules"))
341 .arg("-b")
342 .arg(branch)
343 .arg(from)
344 .args(to)
345 .exec()
346 .map(drop)
347 }
348
349 pub fn clone(
350 shallow: bool,
351 from: impl AsRef<OsStr>,
352 to: Option<impl AsRef<OsStr>>,
353 ) -> Result<()> {
354 Self::cmd_no_root()
355 .stderr(Stdio::inherit())
356 .args(["clone", "--recurse-submodules"])
357 .args(shallow.then_some("--depth=1"))
358 .args(shallow.then_some("--shallow-submodules"))
359 .arg(from)
360 .args(to)
361 .exec()
362 .map(drop)
363 }
364
365 pub fn fetch(
366 self,
367 shallow: bool,
368 remote: impl AsRef<OsStr>,
369 branch: Option<impl AsRef<OsStr>>,
370 ) -> Result<()> {
371 self.cmd()
372 .stderr(Stdio::inherit())
373 .arg("fetch")
374 .args(shallow.then_some("--no-tags"))
375 .args(shallow.then_some("--depth=1"))
376 .arg(remote)
377 .args(branch)
378 .exec()
379 .map(drop)
380 }
381
382 #[inline]
383 pub fn root(self, root: &Path) -> Git<'_> {
384 Git { root, ..self }
385 }
386
387 #[inline]
388 pub fn quiet(self, quiet: bool) -> Self {
389 Self { quiet, ..self }
390 }
391
392 #[inline]
394 pub fn shallow(self, shallow: bool) -> Self {
395 Self { shallow, ..self }
396 }
397
398 pub fn checkout(self, recursive: bool, tag: impl AsRef<OsStr>) -> Result<()> {
399 self.cmd()
400 .arg("checkout")
401 .args(recursive.then_some("--recurse-submodules"))
402 .arg(tag)
403 .exec()
404 .map(drop)
405 }
406
407 pub fn checkout_at(self, tag: impl AsRef<OsStr>, at: &Path) -> Result<()> {
408 self.cmd_at(at).arg("checkout").arg(tag).exec().map(drop)
409 }
410
411 pub fn init(self) -> Result<()> {
412 self.cmd().arg("init").exec().map(drop)
413 }
414
415 pub fn current_rev_branch(self, at: &Path) -> Result<(String, String)> {
416 let rev = self.cmd_at(at).args(["rev-parse", "HEAD"]).get_stdout_lossy()?;
417 let branch =
418 self.cmd_at(at).args(["rev-parse", "--abbrev-ref", "HEAD"]).get_stdout_lossy()?;
419 Ok((rev, branch))
420 }
421
422 #[expect(clippy::should_implement_trait)] pub fn add<I, S>(self, paths: I) -> Result<()>
424 where
425 I: IntoIterator<Item = S>,
426 S: AsRef<OsStr>,
427 {
428 self.cmd().arg("add").args(paths).exec().map(drop)
429 }
430
431 pub fn reset(self, hard: bool, tree: impl AsRef<OsStr>) -> Result<()> {
432 self.cmd().arg("reset").args(hard.then_some("--hard")).arg(tree).exec().map(drop)
433 }
434
435 pub fn commit_tree(
436 self,
437 tree: impl AsRef<OsStr>,
438 msg: Option<impl AsRef<OsStr>>,
439 ) -> Result<String> {
440 self.cmd()
441 .arg("commit-tree")
442 .arg(tree)
443 .args(msg.as_ref().is_some().then_some("-m"))
444 .args(msg)
445 .get_stdout_lossy()
446 }
447
448 pub fn rm<I, S>(self, force: bool, paths: I) -> Result<()>
449 where
450 I: IntoIterator<Item = S>,
451 S: AsRef<OsStr>,
452 {
453 self.cmd().arg("rm").args(force.then_some("--force")).args(paths).exec().map(drop)
454 }
455
456 pub fn commit(self, msg: &str) -> Result<()> {
457 let output = self
458 .cmd()
459 .args(["commit", "-m", msg])
460 .args(cfg!(any(test, debug_assertions)).then_some("--no-gpg-sign"))
461 .output()?;
462 if !output.status.success() {
463 let stdout = String::from_utf8_lossy(&output.stdout);
464 let stderr = String::from_utf8_lossy(&output.stderr);
465 let msg = "nothing to commit, working tree clean";
467 if !(stdout.contains(msg) || stderr.contains(msg)) {
468 return Err(eyre::eyre!(
469 "failed to commit (code={:?}, stdout={:?}, stderr={:?})",
470 output.status.code(),
471 stdout.trim(),
472 stderr.trim()
473 ));
474 }
475 }
476 Ok(())
477 }
478
479 pub fn is_in_repo(self) -> std::io::Result<bool> {
480 self.cmd().args(["rev-parse", "--is-inside-work-tree"]).status().map(|s| s.success())
481 }
482
483 pub fn is_clean(self) -> Result<bool> {
484 self.cmd().args(["status", "--porcelain"]).exec().map(|out| out.stdout.is_empty())
485 }
486
487 pub fn has_branch(self, branch: impl AsRef<OsStr>, at: &Path) -> Result<bool> {
488 self.cmd_at(at)
489 .args(["branch", "--list", "--no-color"])
490 .arg(branch)
491 .get_stdout_lossy()
492 .map(|stdout| !stdout.is_empty())
493 }
494
495 pub fn has_tag(self, tag: impl AsRef<OsStr>, at: &Path) -> Result<bool> {
496 self.cmd_at(at)
497 .args(["tag", "--list"])
498 .arg(tag)
499 .get_stdout_lossy()
500 .map(|stdout| !stdout.is_empty())
501 }
502
503 pub fn has_rev(self, rev: impl AsRef<OsStr>, at: &Path) -> Result<bool> {
504 self.cmd_at(at)
505 .args(["cat-file", "-t"])
506 .arg(rev)
507 .get_stdout_lossy()
508 .map(|stdout| &stdout == "commit")
509 }
510
511 pub fn get_rev(self, tag_or_branch: impl AsRef<OsStr>, at: &Path) -> Result<String> {
512 self.cmd_at(at).args(["rev-list", "-n", "1"]).arg(tag_or_branch).get_stdout_lossy()
513 }
514
515 pub fn ensure_clean(self) -> Result<()> {
516 if self.is_clean()? {
517 Ok(())
518 } else {
519 Err(eyre::eyre!(
520 "\
521The target directory is a part of or on its own an already initialized git repository,
522and it requires clean working and staging areas, including no untracked files.
523
524Check the current git repository's status with `git status`.
525Then, you can track files with `git add ...` and then commit them with `git commit`,
526ignore them in the `.gitignore` file."
527 ))
528 }
529 }
530
531 pub fn commit_hash(self, short: bool, revision: &str) -> Result<String> {
532 self.cmd()
533 .arg("rev-parse")
534 .args(short.then_some("--short"))
535 .arg(revision)
536 .get_stdout_lossy()
537 }
538
539 pub fn tag(self) -> Result<String> {
540 self.cmd().arg("tag").get_stdout_lossy()
541 }
542
543 pub fn tag_for_commit(self, rev: &str, at: &Path) -> Result<Option<String>> {
551 self.cmd_at(at)
552 .args(["tag", "--contains"])
553 .arg(rev)
554 .get_stdout_lossy()
555 .map(|stdout| stdout.lines().next().map(str::to_string))
556 }
557
558 pub fn read_submodules_with_branch(
566 self,
567 at: &Path,
568 lib: &OsStr,
569 ) -> Result<HashMap<PathBuf, String>> {
570 let gitmodules = foundry_common::fs::read_to_string(at.join(".gitmodules"))?;
572
573 let paths = SUBMODULE_BRANCH_REGEX
574 .captures_iter(&gitmodules)
575 .map(|cap| {
576 let path_str = cap.get(1).unwrap().as_str();
577 let path = PathBuf::from_str(path_str).unwrap();
578 trace!(path = %path.display(), "unstripped path");
579
580 let lib_pos = path.components().find_position(|c| c.as_os_str() == lib);
587 let path = path
588 .components()
589 .skip(lib_pos.map(|(i, _)| i).unwrap_or(0))
590 .collect::<PathBuf>();
591
592 let branch = cap.get(2).unwrap().as_str().to_string();
593 (path, branch)
594 })
595 .collect::<HashMap<_, _>>();
596
597 Ok(paths)
598 }
599
600 pub fn has_missing_dependencies<I, S>(self, paths: I) -> Result<bool>
601 where
602 I: IntoIterator<Item = S>,
603 S: AsRef<OsStr>,
604 {
605 self.cmd()
606 .args(["submodule", "status"])
607 .args(paths)
608 .get_stdout_lossy()
609 .map(|stdout| stdout.lines().any(|line| line.starts_with('-')))
610 }
611
612 pub fn has_submodules<I, S>(self, paths: I) -> Result<bool>
614 where
615 I: IntoIterator<Item = S>,
616 S: AsRef<OsStr>,
617 {
618 self.cmd()
619 .args(["submodule", "status"])
620 .args(paths)
621 .get_stdout_lossy()
622 .map(|stdout| stdout.trim().lines().next().is_some())
623 }
624
625 pub fn submodule_add(
626 self,
627 force: bool,
628 url: impl AsRef<OsStr>,
629 path: impl AsRef<OsStr>,
630 ) -> Result<()> {
631 self.cmd()
632 .stderr(self.stderr())
633 .args(["submodule", "add"])
634 .args(self.shallow.then_some("--depth=1"))
635 .args(force.then_some("--force"))
636 .arg(url)
637 .arg(path)
638 .exec()
639 .map(drop)
640 }
641
642 pub fn submodule_update<I, S>(
643 self,
644 force: bool,
645 remote: bool,
646 no_fetch: bool,
647 recursive: bool,
648 paths: I,
649 ) -> Result<()>
650 where
651 I: IntoIterator<Item = S>,
652 S: AsRef<OsStr>,
653 {
654 self.cmd()
655 .stderr(self.stderr())
656 .args(["submodule", "update", "--progress", "--init"])
657 .args(self.shallow.then_some("--depth=1"))
658 .args(force.then_some("--force"))
659 .args(remote.then_some("--remote"))
660 .args(no_fetch.then_some("--no-fetch"))
661 .args(recursive.then_some("--recursive"))
662 .args(paths)
663 .exec()
664 .map(drop)
665 }
666
667 pub fn submodule_foreach(self, recursive: bool, cmd: impl AsRef<OsStr>) -> Result<()> {
668 self.cmd()
669 .stderr(self.stderr())
670 .args(["submodule", "foreach"])
671 .args(recursive.then_some("--recursive"))
672 .arg(cmd)
673 .exec()
674 .map(drop)
675 }
676
677 pub fn submodules_unintialized(self) -> Result<bool> {
681 self.cmd()
682 .args(["submodule", "status"])
683 .get_stdout_lossy()
684 .map(|stdout| stdout.lines().any(|line| line.starts_with('-')))
685 }
686
687 pub fn submodule_init(self) -> Result<()> {
689 self.cmd().stderr(self.stderr()).args(["submodule", "init"]).exec().map(drop)
690 }
691
692 pub fn submodules(&self) -> Result<Submodules> {
693 self.cmd().args(["submodule", "status"]).get_stdout_lossy().map(|stdout| stdout.parse())?
694 }
695
696 pub fn submodule_sync(self) -> Result<()> {
697 self.cmd().stderr(self.stderr()).args(["submodule", "sync"]).exec().map(drop)
698 }
699
700 pub fn cmd(self) -> Command {
701 let mut cmd = Self::cmd_no_root();
702 cmd.current_dir(self.root);
703 cmd
704 }
705
706 pub fn cmd_at(self, path: &Path) -> Command {
707 let mut cmd = Self::cmd_no_root();
708 cmd.current_dir(path);
709 cmd
710 }
711
712 pub fn cmd_no_root() -> Command {
713 let mut cmd = Command::new("git");
714 cmd.stdout(Stdio::piped()).stderr(Stdio::piped());
715 cmd
716 }
717
718 fn stderr(self) -> Stdio {
720 if self.quiet { Stdio::piped() } else { Stdio::inherit() }
721 }
722}
723
724#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq, Eq)]
726pub struct Submodule {
727 rev: String,
729 path: PathBuf,
731}
732
733impl Submodule {
734 pub fn new(rev: String, path: PathBuf) -> Self {
735 Self { rev, path }
736 }
737
738 pub fn rev(&self) -> &str {
739 &self.rev
740 }
741
742 pub fn path(&self) -> &PathBuf {
743 &self.path
744 }
745}
746
747impl FromStr for Submodule {
748 type Err = eyre::Report;
749
750 fn from_str(s: &str) -> Result<Self> {
751 let caps = SUBMODULE_STATUS_REGEX
752 .captures(s)
753 .ok_or_else(|| eyre::eyre!("Invalid submodule status format"))?;
754
755 Ok(Self {
756 rev: caps.get(1).unwrap().as_str().to_string(),
757 path: PathBuf::from(caps.get(2).unwrap().as_str()),
758 })
759 }
760}
761
762#[derive(Debug, Clone, PartialEq, Eq)]
764pub struct Submodules(pub Vec<Submodule>);
765
766impl Submodules {
767 pub fn len(&self) -> usize {
768 self.0.len()
769 }
770
771 pub fn is_empty(&self) -> bool {
772 self.0.is_empty()
773 }
774}
775
776impl FromStr for Submodules {
777 type Err = eyre::Report;
778
779 fn from_str(s: &str) -> Result<Self> {
780 let subs = s.lines().map(str::parse).collect::<Result<Vec<Submodule>>>()?;
781 Ok(Self(subs))
782 }
783}
784
785impl<'a> IntoIterator for &'a Submodules {
786 type Item = &'a Submodule;
787 type IntoIter = std::slice::Iter<'a, Submodule>;
788
789 fn into_iter(self) -> Self::IntoIter {
790 self.0.iter()
791 }
792}
793#[cfg(test)]
794mod tests {
795 use super::*;
796 use foundry_common::fs;
797 use std::{env, fs::File, io::Write};
798 use tempfile::tempdir;
799
800 #[test]
801 fn parse_submodule_status() {
802 let s = "+8829465a08cac423dcf59852f21e448449c1a1a8 lib/openzeppelin-contracts (v4.8.0-791-g8829465a)";
803 let sub = Submodule::from_str(s).unwrap();
804 assert_eq!(sub.rev(), "8829465a08cac423dcf59852f21e448449c1a1a8");
805 assert_eq!(sub.path(), Path::new("lib/openzeppelin-contracts"));
806
807 let s = "-8829465a08cac423dcf59852f21e448449c1a1a8 lib/openzeppelin-contracts";
808 let sub = Submodule::from_str(s).unwrap();
809 assert_eq!(sub.rev(), "8829465a08cac423dcf59852f21e448449c1a1a8");
810 assert_eq!(sub.path(), Path::new("lib/openzeppelin-contracts"));
811
812 let s = "8829465a08cac423dcf59852f21e448449c1a1a8 lib/openzeppelin-contracts";
813 let sub = Submodule::from_str(s).unwrap();
814 assert_eq!(sub.rev(), "8829465a08cac423dcf59852f21e448449c1a1a8");
815 assert_eq!(sub.path(), Path::new("lib/openzeppelin-contracts"));
816 }
817
818 #[test]
819 fn parse_multiline_submodule_status() {
820 let s = r#"+d3db4ef90a72b7d24aa5a2e5c649593eaef7801d lib/forge-std (v1.9.4-6-gd3db4ef)
821+8829465a08cac423dcf59852f21e448449c1a1a8 lib/openzeppelin-contracts (v4.8.0-791-g8829465a)
822"#;
823 let subs = Submodules::from_str(s).unwrap().0;
824 assert_eq!(subs.len(), 2);
825 assert_eq!(subs[0].rev(), "d3db4ef90a72b7d24aa5a2e5c649593eaef7801d");
826 assert_eq!(subs[0].path(), Path::new("lib/forge-std"));
827 assert_eq!(subs[1].rev(), "8829465a08cac423dcf59852f21e448449c1a1a8");
828 assert_eq!(subs[1].path(), Path::new("lib/openzeppelin-contracts"));
829 }
830
831 #[test]
832 fn foundry_path_ext_works() {
833 let p = Path::new("contracts/MyTest.t.sol");
834 assert!(p.is_sol_test());
835 assert!(p.is_sol());
836 let p = Path::new("contracts/Greeter.sol");
837 assert!(!p.is_sol_test());
838 }
839
840 #[test]
842 fn can_load_dotenv() {
843 let temp = tempdir().unwrap();
844 Git::new(temp.path()).init().unwrap();
845 let cwd_env = temp.path().join(".env");
846 fs::create_file(temp.path().join("foundry.toml")).unwrap();
847 let nested = temp.path().join("nested");
848 fs::create_dir(&nested).unwrap();
849
850 let mut cwd_file = File::create(cwd_env).unwrap();
851 let mut prj_file = File::create(nested.join(".env")).unwrap();
852
853 cwd_file.write_all("TESTCWDKEY=cwd_val".as_bytes()).unwrap();
854 cwd_file.sync_all().unwrap();
855
856 prj_file.write_all("TESTPRJKEY=prj_val".as_bytes()).unwrap();
857 prj_file.sync_all().unwrap();
858
859 let cwd = env::current_dir().unwrap();
860 env::set_current_dir(nested).unwrap();
861 load_dotenv();
862 env::set_current_dir(cwd).unwrap();
863
864 assert_eq!(env::var("TESTCWDKEY").unwrap(), "cwd_val");
865 assert_eq!(env::var("TESTPRJKEY").unwrap(), "prj_val");
866 }
867
868 #[test]
869 fn test_read_gitmodules_regex() {
870 let gitmodules = r#"
871 [submodule "lib/solady"]
872 path = lib/solady
873 url = ""
874 branch = v0.1.0
875 [submodule "lib/openzeppelin-contracts"]
876 path = lib/openzeppelin-contracts
877 url = ""
878 branch = v4.8.0-791-g8829465a
879 [submodule "lib/forge-std"]
880 path = lib/forge-std
881 url = ""
882"#;
883
884 let paths = SUBMODULE_BRANCH_REGEX
885 .captures_iter(gitmodules)
886 .map(|cap| {
887 (
888 PathBuf::from_str(cap.get(1).unwrap().as_str()).unwrap(),
889 String::from(cap.get(2).unwrap().as_str()),
890 )
891 })
892 .collect::<HashMap<_, _>>();
893
894 assert_eq!(paths.get(Path::new("lib/solady")).unwrap(), "v0.1.0");
895 assert_eq!(
896 paths.get(Path::new("lib/openzeppelin-contracts")).unwrap(),
897 "v4.8.0-791-g8829465a"
898 );
899
900 let no_branch_gitmodules = r#"
901 [submodule "lib/solady"]
902 path = lib/solady
903 url = ""
904 [submodule "lib/openzeppelin-contracts"]
905 path = lib/openzeppelin-contracts
906 url = ""
907 [submodule "lib/forge-std"]
908 path = lib/forge-std
909 url = ""
910"#;
911 let paths = SUBMODULE_BRANCH_REGEX
912 .captures_iter(no_branch_gitmodules)
913 .map(|cap| {
914 (
915 PathBuf::from_str(cap.get(1).unwrap().as_str()).unwrap(),
916 String::from(cap.get(2).unwrap().as_str()),
917 )
918 })
919 .collect::<HashMap<_, _>>();
920
921 assert!(paths.is_empty());
922
923 let branch_in_between = r#"
924 [submodule "lib/solady"]
925 path = lib/solady
926 url = ""
927 [submodule "lib/openzeppelin-contracts"]
928 path = lib/openzeppelin-contracts
929 url = ""
930 branch = v4.8.0-791-g8829465a
931 [submodule "lib/forge-std"]
932 path = lib/forge-std
933 url = ""
934 "#;
935
936 let paths = SUBMODULE_BRANCH_REGEX
937 .captures_iter(branch_in_between)
938 .map(|cap| {
939 (
940 PathBuf::from_str(cap.get(1).unwrap().as_str()).unwrap(),
941 String::from(cap.get(2).unwrap().as_str()),
942 )
943 })
944 .collect::<HashMap<_, _>>();
945
946 assert_eq!(paths.len(), 1);
947 assert_eq!(
948 paths.get(Path::new("lib/openzeppelin-contracts")).unwrap(),
949 "v4.8.0-791-g8829465a"
950 );
951 }
952}