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