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