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