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