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> {
105 get_provider_builder(config)?.build()
106}
107
108pub fn get_provider_builder(config: &Config) -> Result<ProviderBuilder> {
112 let url = config.get_rpc_url_or_localhost_http()?;
113 let mut builder = ProviderBuilder::new(url.as_ref());
114
115 builder = builder.accept_invalid_certs(config.eth_rpc_accept_invalid_certs);
116
117 if let Ok(chain) = config.chain.unwrap_or_default().try_into() {
118 builder = builder.chain(chain);
119 }
120
121 if let Some(jwt) = config.get_rpc_jwt_secret()? {
122 builder = builder.jwt(jwt.as_ref());
123 }
124
125 if let Some(rpc_timeout) = config.eth_rpc_timeout {
126 builder = builder.timeout(Duration::from_secs(rpc_timeout));
127 }
128
129 if let Some(rpc_headers) = config.eth_rpc_headers.clone() {
130 builder = builder.headers(rpc_headers);
131 }
132
133 Ok(builder)
134}
135
136pub async fn get_chain<P>(chain: Option<Chain>, provider: P) -> Result<Chain>
137where
138 P: Provider<AnyNetwork>,
139{
140 match chain {
141 Some(chain) => Ok(chain),
142 None => Ok(Chain::from_id(provider.get_chain_id().await?)),
143 }
144}
145
146pub fn parse_ether_value(value: &str) -> Result<U256> {
153 Ok(if value.starts_with("0x") {
154 U256::from_str_radix(value, 16)?
155 } else {
156 alloy_dyn_abi::DynSolType::coerce_str(&alloy_dyn_abi::DynSolType::Uint(256), value)?
157 .as_uint()
158 .wrap_err("Could not parse ether value from string")?
159 .0
160 })
161}
162
163pub fn parse_json<T: DeserializeOwned>(value: &str) -> serde_json::Result<T> {
165 serde_json::from_str(value)
166}
167
168pub fn parse_delay(delay: &str) -> Result<Duration> {
170 let delay = if delay.ends_with("ms") {
171 let d: u64 = delay.trim_end_matches("ms").parse()?;
172 Duration::from_millis(d)
173 } else {
174 let d: f64 = delay.parse()?;
175 let delay = (d * 1000.0).round();
176 if delay.is_infinite() || delay.is_nan() || delay.is_sign_negative() {
177 eyre::bail!("delay must be finite and non-negative");
178 }
179
180 Duration::from_millis(delay as u64)
181 };
182 Ok(delay)
183}
184
185pub fn now() -> Duration {
187 SystemTime::now().duration_since(UNIX_EPOCH).expect("time went backwards")
188}
189
190pub fn common_setup() {
192 install_crypto_provider();
193 crate::handler::install();
194 load_dotenv();
195 enable_paint();
196}
197
198pub fn load_dotenv() {
206 let load = |p: &Path| {
207 dotenvy::from_path(p.join(".env")).ok();
208 };
209
210 if let (Ok(cwd), Ok(prj_root)) = (std::env::current_dir(), find_project_root(None)) {
214 load(&prj_root);
215 if cwd != prj_root {
216 load(&cwd);
218 }
219 };
220}
221
222pub fn enable_paint() {
224 let enable = yansi::Condition::os_support() && yansi::Condition::tty_and_color_live();
225 yansi::whenever(yansi::Condition::cached(enable));
226}
227
228pub fn install_crypto_provider() {
239 rustls::crypto::ring::default_provider()
241 .install_default()
242 .expect("Failed to install default rustls crypto provider");
243}
244
245pub async fn fetch_abi_from_etherscan(
247 address: Address,
248 config: &foundry_config::Config,
249) -> Result<Vec<(JsonAbi, String)>> {
250 let chain = config.chain.unwrap_or_default();
251 let api_key = config.get_etherscan_api_key(Some(chain)).unwrap_or_default();
252 let client = foundry_block_explorers::Client::new(chain, api_key)?;
253 let source = client.contract_source_code(address).await?;
254 source.items.into_iter().map(|item| Ok((item.abi()?, item.contract_name))).collect()
255}
256
257pub trait CommandUtils {
259 fn exec(&mut self) -> Result<Output>;
261
262 fn get_stdout_lossy(&mut self) -> Result<String>;
264}
265
266impl CommandUtils for Command {
267 #[track_caller]
268 fn exec(&mut self) -> Result<Output> {
269 trace!(command=?self, "executing");
270
271 let output = self.output()?;
272
273 trace!(code=?output.status.code(), ?output);
274
275 if output.status.success() {
276 Ok(output)
277 } else {
278 let stdout = String::from_utf8_lossy(&output.stdout);
279 let stdout = stdout.trim();
280 let stderr = String::from_utf8_lossy(&output.stderr);
281 let stderr = stderr.trim();
282 let msg = if stdout.is_empty() {
283 stderr.to_string()
284 } else if stderr.is_empty() {
285 stdout.to_string()
286 } else {
287 format!("stdout:\n{stdout}\n\nstderr:\n{stderr}")
288 };
289
290 let mut name = self.get_program().to_string_lossy();
291 if let Some(arg) = self.get_args().next() {
292 let arg = arg.to_string_lossy();
293 if !arg.starts_with('-') {
294 let name = name.to_mut();
295 name.push(' ');
296 name.push_str(&arg);
297 }
298 }
299
300 let mut err = match output.status.code() {
301 Some(code) => format!("{name} exited with code {code}"),
302 None => format!("{name} terminated by a signal"),
303 };
304 if !msg.is_empty() {
305 err.push(':');
306 err.push(if msg.lines().count() == 0 { ' ' } else { '\n' });
307 err.push_str(&msg);
308 }
309 Err(eyre::eyre!(err))
310 }
311 }
312
313 #[track_caller]
314 fn get_stdout_lossy(&mut self) -> Result<String> {
315 let output = self.exec()?;
316 let stdout = String::from_utf8_lossy(&output.stdout);
317 Ok(stdout.trim().into())
318 }
319}
320
321#[derive(Clone, Copy, Debug)]
322pub struct Git<'a> {
323 pub root: &'a Path,
324 pub quiet: bool,
325 pub shallow: bool,
326}
327
328impl<'a> Git<'a> {
329 pub fn new(root: &'a Path) -> Self {
330 Self { root, quiet: shell::is_quiet(), shallow: false }
331 }
332
333 pub fn from_config(config: &'a Config) -> Self {
334 Self::new(config.root.as_path())
335 }
336
337 pub fn root_of(relative_to: &Path) -> Result<PathBuf> {
338 let output = Self::cmd_no_root()
339 .current_dir(relative_to)
340 .args(["rev-parse", "--show-toplevel"])
341 .get_stdout_lossy()?;
342 Ok(PathBuf::from(output))
343 }
344
345 pub fn clone_with_branch(
346 shallow: bool,
347 from: impl AsRef<OsStr>,
348 branch: impl AsRef<OsStr>,
349 to: Option<impl AsRef<OsStr>>,
350 ) -> Result<()> {
351 Self::cmd_no_root()
352 .stderr(Stdio::inherit())
353 .args(["clone", "--recurse-submodules"])
354 .args(shallow.then_some("--depth=1"))
355 .args(shallow.then_some("--shallow-submodules"))
356 .arg("-b")
357 .arg(branch)
358 .arg(from)
359 .args(to)
360 .exec()
361 .map(drop)
362 }
363
364 pub fn clone(
365 shallow: bool,
366 from: impl AsRef<OsStr>,
367 to: Option<impl AsRef<OsStr>>,
368 ) -> Result<()> {
369 Self::cmd_no_root()
370 .stderr(Stdio::inherit())
371 .args(["clone", "--recurse-submodules"])
372 .args(shallow.then_some("--depth=1"))
373 .args(shallow.then_some("--shallow-submodules"))
374 .arg(from)
375 .args(to)
376 .exec()
377 .map(drop)
378 }
379
380 pub fn fetch(
381 self,
382 shallow: bool,
383 remote: impl AsRef<OsStr>,
384 branch: Option<impl AsRef<OsStr>>,
385 ) -> Result<()> {
386 self.cmd()
387 .stderr(Stdio::inherit())
388 .arg("fetch")
389 .args(shallow.then_some("--no-tags"))
390 .args(shallow.then_some("--depth=1"))
391 .arg(remote)
392 .args(branch)
393 .exec()
394 .map(drop)
395 }
396
397 pub fn root(self, root: &Path) -> Git<'_> {
398 Git { root, ..self }
399 }
400
401 pub fn quiet(self, quiet: bool) -> Self {
402 Self { quiet, ..self }
403 }
404
405 pub fn shallow(self, shallow: bool) -> Self {
407 Self { shallow, ..self }
408 }
409
410 pub fn checkout(self, recursive: bool, tag: impl AsRef<OsStr>) -> Result<()> {
411 self.cmd()
412 .arg("checkout")
413 .args(recursive.then_some("--recurse-submodules"))
414 .arg(tag)
415 .exec()
416 .map(drop)
417 }
418
419 pub fn head(self) -> Result<String> {
421 self.cmd().args(["rev-parse", "HEAD"]).get_stdout_lossy()
422 }
423
424 pub fn checkout_at(self, tag: impl AsRef<OsStr>, at: &Path) -> Result<()> {
425 self.cmd_at(at).arg("checkout").arg(tag).exec().map(drop)
426 }
427
428 pub fn init(self) -> Result<()> {
429 self.cmd().arg("init").exec().map(drop)
430 }
431
432 pub fn current_rev_branch(self, at: &Path) -> Result<(String, String)> {
433 let rev = self.cmd_at(at).args(["rev-parse", "HEAD"]).get_stdout_lossy()?;
434 let branch =
435 self.cmd_at(at).args(["rev-parse", "--abbrev-ref", "HEAD"]).get_stdout_lossy()?;
436 Ok((rev, branch))
437 }
438
439 #[expect(clippy::should_implement_trait)] pub fn add<I, S>(self, paths: I) -> Result<()>
441 where
442 I: IntoIterator<Item = S>,
443 S: AsRef<OsStr>,
444 {
445 self.cmd().arg("add").args(paths).exec().map(drop)
446 }
447
448 pub fn reset(self, hard: bool, tree: impl AsRef<OsStr>) -> Result<()> {
449 self.cmd().arg("reset").args(hard.then_some("--hard")).arg(tree).exec().map(drop)
450 }
451
452 pub fn commit_tree(
453 self,
454 tree: impl AsRef<OsStr>,
455 msg: Option<impl AsRef<OsStr>>,
456 ) -> Result<String> {
457 self.cmd()
458 .arg("commit-tree")
459 .arg(tree)
460 .args(msg.as_ref().is_some().then_some("-m"))
461 .args(msg)
462 .get_stdout_lossy()
463 }
464
465 pub fn rm<I, S>(self, force: bool, paths: I) -> Result<()>
466 where
467 I: IntoIterator<Item = S>,
468 S: AsRef<OsStr>,
469 {
470 self.cmd().arg("rm").args(force.then_some("--force")).args(paths).exec().map(drop)
471 }
472
473 pub fn commit(self, msg: &str) -> Result<()> {
474 let output = self
475 .cmd()
476 .args(["commit", "-m", msg])
477 .args(cfg!(any(test, debug_assertions)).then_some("--no-gpg-sign"))
478 .output()?;
479 if !output.status.success() {
480 let stdout = String::from_utf8_lossy(&output.stdout);
481 let stderr = String::from_utf8_lossy(&output.stderr);
482 let msg = "nothing to commit, working tree clean";
484 if !(stdout.contains(msg) || stderr.contains(msg)) {
485 return Err(eyre::eyre!(
486 "failed to commit (code={:?}, stdout={:?}, stderr={:?})",
487 output.status.code(),
488 stdout.trim(),
489 stderr.trim()
490 ));
491 }
492 }
493 Ok(())
494 }
495
496 pub fn is_in_repo(self) -> std::io::Result<bool> {
497 self.cmd().args(["rev-parse", "--is-inside-work-tree"]).status().map(|s| s.success())
498 }
499
500 pub fn is_repo_root(self) -> Result<bool> {
501 self.cmd().args(["rev-parse", "--show-cdup"]).exec().map(|out| out.stdout.is_empty())
502 }
503
504 pub fn is_clean(self) -> Result<bool> {
505 self.cmd().args(["status", "--porcelain"]).exec().map(|out| out.stdout.is_empty())
506 }
507
508 pub fn has_branch(self, branch: impl AsRef<OsStr>, at: &Path) -> Result<bool> {
509 self.cmd_at(at)
510 .args(["branch", "--list", "--no-color"])
511 .arg(branch)
512 .get_stdout_lossy()
513 .map(|stdout| !stdout.is_empty())
514 }
515
516 pub fn has_tag(self, tag: impl AsRef<OsStr>, at: &Path) -> Result<bool> {
517 self.cmd_at(at)
518 .args(["tag", "--list"])
519 .arg(tag)
520 .get_stdout_lossy()
521 .map(|stdout| !stdout.is_empty())
522 }
523
524 pub fn has_rev(self, rev: impl AsRef<OsStr>, at: &Path) -> Result<bool> {
525 self.cmd_at(at)
526 .args(["cat-file", "-t"])
527 .arg(rev)
528 .get_stdout_lossy()
529 .map(|stdout| &stdout == "commit")
530 }
531
532 pub fn get_rev(self, tag_or_branch: impl AsRef<OsStr>, at: &Path) -> Result<String> {
533 self.cmd_at(at).args(["rev-list", "-n", "1"]).arg(tag_or_branch).get_stdout_lossy()
534 }
535
536 pub fn ensure_clean(self) -> Result<()> {
537 if self.is_clean()? {
538 Ok(())
539 } else {
540 Err(eyre::eyre!(
541 "\
542The target directory is a part of or on its own an already initialized git repository,
543and it requires clean working and staging areas, including no untracked files.
544
545Check the current git repository's status with `git status`.
546Then, you can track files with `git add ...` and then commit them with `git commit`,
547ignore them in the `.gitignore` file."
548 ))
549 }
550 }
551
552 pub fn commit_hash(self, short: bool, revision: &str) -> Result<String> {
553 self.cmd()
554 .arg("rev-parse")
555 .args(short.then_some("--short"))
556 .arg(revision)
557 .get_stdout_lossy()
558 }
559
560 pub fn tag(self) -> Result<String> {
561 self.cmd().arg("tag").get_stdout_lossy()
562 }
563
564 pub fn tag_for_commit(self, rev: &str, at: &Path) -> Result<Option<String>> {
572 self.cmd_at(at)
573 .args(["tag", "--contains"])
574 .arg(rev)
575 .get_stdout_lossy()
576 .map(|stdout| stdout.lines().next().map(str::to_string))
577 }
578
579 pub fn read_submodules_with_branch(
587 self,
588 at: &Path,
589 lib: &OsStr,
590 ) -> Result<HashMap<PathBuf, String>> {
591 let gitmodules = foundry_common::fs::read_to_string(at.join(".gitmodules"))?;
593
594 let paths = SUBMODULE_BRANCH_REGEX
595 .captures_iter(&gitmodules)
596 .map(|cap| {
597 let path_str = cap.get(1).unwrap().as_str();
598 let path = PathBuf::from_str(path_str).unwrap();
599 trace!(path = %path.display(), "unstripped path");
600
601 let lib_pos = path.components().find_position(|c| c.as_os_str() == lib);
608 let path = path
609 .components()
610 .skip(lib_pos.map(|(i, _)| i).unwrap_or(0))
611 .collect::<PathBuf>();
612
613 let branch = cap.get(2).unwrap().as_str().to_string();
614 (path, branch)
615 })
616 .collect::<HashMap<_, _>>();
617
618 Ok(paths)
619 }
620
621 pub fn has_missing_dependencies<I, S>(self, paths: I) -> Result<bool>
622 where
623 I: IntoIterator<Item = S>,
624 S: AsRef<OsStr>,
625 {
626 self.cmd()
627 .args(["submodule", "status"])
628 .args(paths)
629 .get_stdout_lossy()
630 .map(|stdout| stdout.lines().any(|line| line.starts_with('-')))
631 }
632
633 pub fn has_submodules<I, S>(self, paths: I) -> Result<bool>
635 where
636 I: IntoIterator<Item = S>,
637 S: AsRef<OsStr>,
638 {
639 self.cmd()
640 .args(["submodule", "status"])
641 .args(paths)
642 .get_stdout_lossy()
643 .map(|stdout| stdout.trim().lines().next().is_some())
644 }
645
646 pub fn submodule_add(
647 self,
648 force: bool,
649 url: impl AsRef<OsStr>,
650 path: impl AsRef<OsStr>,
651 ) -> Result<()> {
652 self.cmd()
653 .stderr(self.stderr())
654 .args(["submodule", "add"])
655 .args(self.shallow.then_some("--depth=1"))
656 .args(force.then_some("--force"))
657 .arg(url)
658 .arg(path)
659 .exec()
660 .map(drop)
661 }
662
663 pub fn submodule_update<I, S>(
664 self,
665 force: bool,
666 remote: bool,
667 no_fetch: bool,
668 recursive: bool,
669 paths: I,
670 ) -> Result<()>
671 where
672 I: IntoIterator<Item = S>,
673 S: AsRef<OsStr>,
674 {
675 self.cmd()
676 .stderr(self.stderr())
677 .args(["submodule", "update", "--progress", "--init"])
678 .args(self.shallow.then_some("--depth=1"))
679 .args(force.then_some("--force"))
680 .args(remote.then_some("--remote"))
681 .args(no_fetch.then_some("--no-fetch"))
682 .args(recursive.then_some("--recursive"))
683 .args(paths)
684 .exec()
685 .map(drop)
686 }
687
688 pub fn submodule_foreach(self, recursive: bool, cmd: impl AsRef<OsStr>) -> Result<()> {
689 self.cmd()
690 .stderr(self.stderr())
691 .args(["submodule", "foreach"])
692 .args(recursive.then_some("--recursive"))
693 .arg(cmd)
694 .exec()
695 .map(drop)
696 }
697
698 pub fn submodules_uninitialized(self) -> Result<bool> {
702 self.cmd()
703 .args(["submodule", "status"])
704 .get_stdout_lossy()
705 .map(|stdout| stdout.lines().any(|line| line.starts_with('-')))
706 }
707
708 pub fn submodule_init(self) -> Result<()> {
710 self.cmd().stderr(self.stderr()).args(["submodule", "init"]).exec().map(drop)
711 }
712
713 pub fn submodules(&self) -> Result<Submodules> {
714 self.cmd().args(["submodule", "status"]).get_stdout_lossy().map(|stdout| stdout.parse())?
715 }
716
717 pub fn submodule_sync(self) -> Result<()> {
718 self.cmd().stderr(self.stderr()).args(["submodule", "sync"]).exec().map(drop)
719 }
720
721 pub fn submodule_url(self, path: &Path) -> Result<Option<String>> {
723 self.cmd()
724 .args(["config", "--get", &format!("submodule.{}.url", path.to_slash_lossy())])
725 .get_stdout_lossy()
726 .map(|url| Some(url.trim().to_string()))
727 }
728
729 pub fn cmd(self) -> Command {
730 let mut cmd = Self::cmd_no_root();
731 cmd.current_dir(self.root);
732 cmd
733 }
734
735 pub fn cmd_at(self, path: &Path) -> Command {
736 let mut cmd = Self::cmd_no_root();
737 cmd.current_dir(path);
738 cmd
739 }
740
741 pub fn cmd_no_root() -> Command {
742 let mut cmd = Command::new("git");
743 cmd.stdout(Stdio::piped()).stderr(Stdio::piped());
744 cmd
745 }
746
747 fn stderr(self) -> Stdio {
749 if self.quiet { Stdio::piped() } else { Stdio::inherit() }
750 }
751}
752
753#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq, Eq)]
755pub struct Submodule {
756 rev: String,
758 path: PathBuf,
760}
761
762impl Submodule {
763 pub fn new(rev: String, path: PathBuf) -> Self {
764 Self { rev, path }
765 }
766
767 pub fn rev(&self) -> &str {
768 &self.rev
769 }
770
771 pub fn path(&self) -> &PathBuf {
772 &self.path
773 }
774}
775
776impl FromStr for Submodule {
777 type Err = eyre::Report;
778
779 fn from_str(s: &str) -> Result<Self> {
780 let caps = SUBMODULE_STATUS_REGEX
781 .captures(s)
782 .ok_or_else(|| eyre::eyre!("Invalid submodule status format"))?;
783
784 Ok(Self {
785 rev: caps.get(1).unwrap().as_str().to_string(),
786 path: PathBuf::from(caps.get(2).unwrap().as_str()),
787 })
788 }
789}
790
791#[derive(Debug, Clone, PartialEq, Eq)]
793pub struct Submodules(pub Vec<Submodule>);
794
795impl Submodules {
796 pub fn len(&self) -> usize {
797 self.0.len()
798 }
799
800 pub fn is_empty(&self) -> bool {
801 self.0.is_empty()
802 }
803}
804
805impl FromStr for Submodules {
806 type Err = eyre::Report;
807
808 fn from_str(s: &str) -> Result<Self> {
809 let subs = s.lines().map(str::parse).collect::<Result<Vec<Submodule>>>()?;
810 Ok(Self(subs))
811 }
812}
813
814impl<'a> IntoIterator for &'a Submodules {
815 type Item = &'a Submodule;
816 type IntoIter = std::slice::Iter<'a, Submodule>;
817
818 fn into_iter(self) -> Self::IntoIter {
819 self.0.iter()
820 }
821}
822#[cfg(test)]
823mod tests {
824 use super::*;
825 use foundry_common::fs;
826 use std::{env, fs::File, io::Write};
827 use tempfile::tempdir;
828
829 #[test]
830 fn parse_submodule_status() {
831 let s = "+8829465a08cac423dcf59852f21e448449c1a1a8 lib/openzeppelin-contracts (v4.8.0-791-g8829465a)";
832 let sub = Submodule::from_str(s).unwrap();
833 assert_eq!(sub.rev(), "8829465a08cac423dcf59852f21e448449c1a1a8");
834 assert_eq!(sub.path(), Path::new("lib/openzeppelin-contracts"));
835
836 let s = "-8829465a08cac423dcf59852f21e448449c1a1a8 lib/openzeppelin-contracts";
837 let sub = Submodule::from_str(s).unwrap();
838 assert_eq!(sub.rev(), "8829465a08cac423dcf59852f21e448449c1a1a8");
839 assert_eq!(sub.path(), Path::new("lib/openzeppelin-contracts"));
840
841 let s = "8829465a08cac423dcf59852f21e448449c1a1a8 lib/openzeppelin-contracts";
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
847 #[test]
848 fn parse_multiline_submodule_status() {
849 let s = r#"+d3db4ef90a72b7d24aa5a2e5c649593eaef7801d lib/forge-std (v1.9.4-6-gd3db4ef)
850+8829465a08cac423dcf59852f21e448449c1a1a8 lib/openzeppelin-contracts (v4.8.0-791-g8829465a)
851"#;
852 let subs = Submodules::from_str(s).unwrap().0;
853 assert_eq!(subs.len(), 2);
854 assert_eq!(subs[0].rev(), "d3db4ef90a72b7d24aa5a2e5c649593eaef7801d");
855 assert_eq!(subs[0].path(), Path::new("lib/forge-std"));
856 assert_eq!(subs[1].rev(), "8829465a08cac423dcf59852f21e448449c1a1a8");
857 assert_eq!(subs[1].path(), Path::new("lib/openzeppelin-contracts"));
858 }
859
860 #[test]
861 fn foundry_path_ext_works() {
862 let p = Path::new("contracts/MyTest.t.sol");
863 assert!(p.is_sol_test());
864 assert!(p.is_sol());
865 let p = Path::new("contracts/Greeter.sol");
866 assert!(!p.is_sol_test());
867 }
868
869 #[test]
871 fn can_load_dotenv() {
872 let temp = tempdir().unwrap();
873 Git::new(temp.path()).init().unwrap();
874 let cwd_env = temp.path().join(".env");
875 fs::create_file(temp.path().join("foundry.toml")).unwrap();
876 let nested = temp.path().join("nested");
877 fs::create_dir(&nested).unwrap();
878
879 let mut cwd_file = File::create(cwd_env).unwrap();
880 let mut prj_file = File::create(nested.join(".env")).unwrap();
881
882 cwd_file.write_all("TESTCWDKEY=cwd_val".as_bytes()).unwrap();
883 cwd_file.sync_all().unwrap();
884
885 prj_file.write_all("TESTPRJKEY=prj_val".as_bytes()).unwrap();
886 prj_file.sync_all().unwrap();
887
888 let cwd = env::current_dir().unwrap();
889 env::set_current_dir(nested).unwrap();
890 load_dotenv();
891 env::set_current_dir(cwd).unwrap();
892
893 assert_eq!(env::var("TESTCWDKEY").unwrap(), "cwd_val");
894 assert_eq!(env::var("TESTPRJKEY").unwrap(), "prj_val");
895 }
896
897 #[test]
898 fn test_read_gitmodules_regex() {
899 let gitmodules = r#"
900 [submodule "lib/solady"]
901 path = lib/solady
902 url = ""
903 branch = v0.1.0
904 [submodule "lib/openzeppelin-contracts"]
905 path = lib/openzeppelin-contracts
906 url = ""
907 branch = v4.8.0-791-g8829465a
908 [submodule "lib/forge-std"]
909 path = lib/forge-std
910 url = ""
911"#;
912
913 let paths = SUBMODULE_BRANCH_REGEX
914 .captures_iter(gitmodules)
915 .map(|cap| {
916 (
917 PathBuf::from_str(cap.get(1).unwrap().as_str()).unwrap(),
918 String::from(cap.get(2).unwrap().as_str()),
919 )
920 })
921 .collect::<HashMap<_, _>>();
922
923 assert_eq!(paths.get(Path::new("lib/solady")).unwrap(), "v0.1.0");
924 assert_eq!(
925 paths.get(Path::new("lib/openzeppelin-contracts")).unwrap(),
926 "v4.8.0-791-g8829465a"
927 );
928
929 let no_branch_gitmodules = r#"
930 [submodule "lib/solady"]
931 path = lib/solady
932 url = ""
933 [submodule "lib/openzeppelin-contracts"]
934 path = lib/openzeppelin-contracts
935 url = ""
936 [submodule "lib/forge-std"]
937 path = lib/forge-std
938 url = ""
939"#;
940 let paths = SUBMODULE_BRANCH_REGEX
941 .captures_iter(no_branch_gitmodules)
942 .map(|cap| {
943 (
944 PathBuf::from_str(cap.get(1).unwrap().as_str()).unwrap(),
945 String::from(cap.get(2).unwrap().as_str()),
946 )
947 })
948 .collect::<HashMap<_, _>>();
949
950 assert!(paths.is_empty());
951
952 let branch_in_between = r#"
953 [submodule "lib/solady"]
954 path = lib/solady
955 url = ""
956 [submodule "lib/openzeppelin-contracts"]
957 path = lib/openzeppelin-contracts
958 url = ""
959 branch = v4.8.0-791-g8829465a
960 [submodule "lib/forge-std"]
961 path = lib/forge-std
962 url = ""
963 "#;
964
965 let paths = SUBMODULE_BRANCH_REGEX
966 .captures_iter(branch_in_between)
967 .map(|cap| {
968 (
969 PathBuf::from_str(cap.get(1).unwrap().as_str()).unwrap(),
970 String::from(cap.get(2).unwrap().as_str()),
971 )
972 })
973 .collect::<HashMap<_, _>>();
974
975 assert_eq!(paths.len(), 1);
976 assert_eq!(
977 paths.get(Path::new("lib/openzeppelin-contracts")).unwrap(),
978 "v4.8.0-791-g8829465a"
979 );
980 }
981}