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