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