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