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