From fbef88da4599ff05540c77f4987e426567e2da7f Mon Sep 17 00:00:00 2001 From: alpharush <0xalpharush@protonmail.com> Date: Sat, 1 Jul 2023 23:15:30 -0500 Subject: [PATCH 1/3] feat(forge): implement glob pattern for forge build --skip --- Cargo.lock | 1 + cli/Cargo.toml | 2 +- cli/src/cmd/forge/test/filter.rs | 67 +------------------ cli/tests/fixtures/can_build_skip_glob.stdout | 3 + cli/tests/it/cmd.rs | 23 +++++++ common/Cargo.toml | 1 + common/src/compile.rs | 10 ++- common/src/glob.rs | 64 ++++++++++++++++++ common/src/lib.rs | 1 + 9 files changed, 104 insertions(+), 68 deletions(-) create mode 100644 cli/tests/fixtures/can_build_skip_glob.stdout create mode 100644 common/src/glob.rs diff --git a/Cargo.lock b/Cargo.lock index 0bdc861d6f4e..908bd829f281 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2380,6 +2380,7 @@ dependencies = [ "eyre", "foundry-config", "foundry-macros", + "globset", "is-terminal", "once_cell", "regex", diff --git a/cli/Cargo.toml b/cli/Cargo.toml index f5e6dd7698d7..7376d9da7525 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -55,7 +55,6 @@ async-trait = "0.1" # disk / paths walkdir = "2" dunce = "1" -globset = "0.4" path-slash = "0.2" tempfile = "3" @@ -89,6 +88,7 @@ toml = "0.7" serial_test = "2" criterion = "0.4" svm = { package = "svm-rs", version = "0.2", default-features = false, features = ["rustls"] } +globset = "0.4" [features] default = ["rustls"] diff --git a/cli/src/cmd/forge/test/filter.rs b/cli/src/cmd/forge/test/filter.rs index f0b2f94f3279..455be5a11e27 100644 --- a/cli/src/cmd/forge/test/filter.rs +++ b/cli/src/cmd/forge/test/filter.rs @@ -2,8 +2,9 @@ use crate::utils::FoundryPathExt; use clap::Parser; use ethers::solc::{FileFilter, ProjectPathsConfig}; use forge::TestFilter; +use foundry_common::glob::GlobMatcher; use foundry_config::Config; -use std::{fmt, path::Path, str::FromStr}; +use std::{fmt, path::Path}; /// The filter to use during testing. /// @@ -214,67 +215,3 @@ impl fmt::Display for ProjectPathsAwareFilter { self.args_filter.fmt(f) } } - -/// A `globset::Glob` that creates its `globset::GlobMatcher` when its created, so it doesn't need -/// to be compiled when the filter functions `TestFilter` functions are called. -#[derive(Debug, Clone)] -pub struct GlobMatcher { - /// The parsed glob - pub glob: globset::Glob, - /// The compiled `glob` - pub matcher: globset::GlobMatcher, -} - -// === impl GlobMatcher === - -impl GlobMatcher { - /// Tests whether the given path matches this pattern or not. - /// - /// The glob `./test/*` won't match absolute paths like `test/Contract.sol`, which is common - /// format here, so we also handle this case here - pub fn is_match(&self, path: &str) -> bool { - let mut matches = self.matcher.is_match(path); - if !matches && !path.starts_with("./") && self.as_str().starts_with("./") { - matches = self.matcher.is_match(format!("./{path}")); - } - matches - } - - /// Returns the `Glob` string used to compile this matcher. - pub fn as_str(&self) -> &str { - self.glob.glob() - } -} - -impl fmt::Display for GlobMatcher { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - self.glob.fmt(f) - } -} - -impl FromStr for GlobMatcher { - type Err = globset::Error; - - fn from_str(s: &str) -> Result { - s.parse::().map(Into::into) - } -} - -impl From for GlobMatcher { - fn from(glob: globset::Glob) -> Self { - let matcher = glob.compile_matcher(); - Self { glob, matcher } - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn can_match_glob_paths() { - let matcher: GlobMatcher = "./test/*".parse().unwrap(); - assert!(matcher.is_match("test/Contract.sol")); - assert!(matcher.is_match("./test/Contract.sol")); - } -} diff --git a/cli/tests/fixtures/can_build_skip_glob.stdout b/cli/tests/fixtures/can_build_skip_glob.stdout new file mode 100644 index 000000000000..522beb3e2b6c --- /dev/null +++ b/cli/tests/fixtures/can_build_skip_glob.stdout @@ -0,0 +1,3 @@ +Compiling 1 files with 0.8.17 +Solc 0.8.17 finished in 33.25ms +Compiler run successful! diff --git a/cli/tests/it/cmd.rs b/cli/tests/it/cmd.rs index a3b16540cc65..4f72b89b6af2 100644 --- a/cli/tests/it/cmd.rs +++ b/cli/tests/it/cmd.rs @@ -1575,6 +1575,29 @@ forgetest_init!(can_build_skip_contracts, |prj: TestProject, mut cmd: TestComman assert!(out.trim().contains("No files changed, compilation skipped"), "{}", out); }); +forgetest_init!(can_build_skip_glob, |prj: TestProject, mut cmd: TestCommand| { + // explicitly set to run with 0.8.17 for consistent output + let config = Config { solc: Some("0.8.17".into()), ..Default::default() }; + prj.write_config(config); + prj.inner() + .add_test( + "Foo", + r#" +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.17; +contract TestDemo { +function test_run() external {} +}"#, + ) + .unwrap(); + // only builds the single template contract `src/*` even if `*.t.sol` or `.s.sol` is absent + cmd.args(["build", "--skip", "*/test/**", "--skip", "*/script/**"]); + + cmd.unchecked_output().stdout_matches_path( + PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/can_build_skip_glob.stdout"), + ); +}); + // checks that build --sizes includes all contracts even if unchanged forgetest_init!(can_build_sizes_repeatedly, |_prj: TestProject, mut cmd: TestCommand| { cmd.args(["build", "--sizes"]); diff --git a/common/Cargo.toml b/common/Cargo.toml index 07c38049f56c..4e1ecb46ed90 100644 --- a/common/Cargo.toml +++ b/common/Cargo.toml @@ -41,6 +41,7 @@ semver = "1" once_cell = "1" dunce = "1" regex = "1" +globset = "0.4" [dev-dependencies] tokio = { version = "1", features = ["rt-multi-thread", "macros"] } diff --git a/common/src/compile.rs b/common/src/compile.rs index 9b6d658651ef..04f6cdc5bc22 100644 --- a/common/src/compile.rs +++ b/common/src/compile.rs @@ -1,5 +1,5 @@ //! Support for compiling [ethers::solc::Project] -use crate::{term, TestFunctionExt}; +use crate::{glob::GlobMatcher, term, TestFunctionExt}; use comfy_table::{presets::ASCII_MARKDOWN, *}; use ethers_etherscan::contract::Metadata; use ethers_solc::{ @@ -528,8 +528,9 @@ impl FileFilter for SkipBuildFilter { /// This is returns the inverse of `file.name.contains(pattern)` fn is_match(&self, file: &Path) -> bool { fn exclude(file: &Path, pattern: &str) -> Option { + let matcher: GlobMatcher = pattern.parse().unwrap(); let file_name = file.file_name()?.to_str()?; - Some(file_name.contains(pattern)) + Some(file_name.contains(pattern) || matcher.is_match(file.as_os_str().to_str()?)) } !exclude(file, self.file_pattern()).unwrap_or_default() @@ -551,5 +552,10 @@ mod tests { assert!(SkipBuildFilter::Tests.is_match(file)); assert!(!SkipBuildFilter::Scripts.is_match(file)); assert!(!SkipBuildFilter::Custom("A.s".to_string()).is_match(file)); + + let file = Path::new("/private/var/folders/test/Foo.sol"); + assert!(!SkipBuildFilter::Custom("*/test/**".to_string()).is_match(file)); + let file = Path::new("script/Contract.sol"); + assert!(!SkipBuildFilter::Custom("*/script/**".to_string()).is_match(file)); } } diff --git a/common/src/glob.rs b/common/src/glob.rs new file mode 100644 index 000000000000..200063afc477 --- /dev/null +++ b/common/src/glob.rs @@ -0,0 +1,64 @@ +//! Contains `globset::Glob` wrapper functions used for filtering +use std::{fmt, str::FromStr}; +/// A `globset::Glob` that creates its `globset::GlobMatcher` when its created, so it doesn't need +/// to be compiled when the filter functions `TestFilter` functions are called. +#[derive(Debug, Clone)] +pub struct GlobMatcher { + /// The parsed glob + pub glob: globset::Glob, + /// The compiled `glob` + pub matcher: globset::GlobMatcher, +} + +// === impl GlobMatcher === + +impl GlobMatcher { + /// Tests whether the given path matches this pattern or not. + /// + /// The glob `./test/*` won't match absolute paths like `test/Contract.sol`, which is common + /// format here, so we also handle this case here + pub fn is_match(&self, path: &str) -> bool { + let mut matches = self.matcher.is_match(path); + if !matches && !path.starts_with("./") && self.as_str().starts_with("./") { + matches = self.matcher.is_match(format!("./{path}")); + } + matches + } + + /// Returns the `Glob` string used to compile this matcher. + pub fn as_str(&self) -> &str { + self.glob.glob() + } +} + +impl fmt::Display for GlobMatcher { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.glob.fmt(f) + } +} + +impl FromStr for GlobMatcher { + type Err = globset::Error; + + fn from_str(s: &str) -> Result { + s.parse::().map(Into::into) + } +} + +impl From for GlobMatcher { + fn from(glob: globset::Glob) -> Self { + let matcher = glob.compile_matcher(); + Self { glob, matcher } + } +} +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn can_match_glob_paths() { + let matcher: GlobMatcher = "./test/*".parse().unwrap(); + assert!(matcher.is_match("test/Contract.sol")); + assert!(matcher.is_match("./test/Contract.sol")); + } +} diff --git a/common/src/lib.rs b/common/src/lib.rs index b5e76e0c184c..19aa0ef3dcf5 100644 --- a/common/src/lib.rs +++ b/common/src/lib.rs @@ -11,6 +11,7 @@ pub mod errors; pub mod evm; pub mod fmt; pub mod fs; +pub mod glob; pub mod provider; pub mod selectors; pub mod shell; From 1b35170e007f129ca2b2ad8e5828040512afa470 Mon Sep 17 00:00:00 2001 From: alpharush <0xalpharush@protonmail.com> Date: Mon, 3 Jul 2023 22:00:29 -0500 Subject: [PATCH 2/3] update tests and docs --- common/src/compile.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/common/src/compile.rs b/common/src/compile.rs index 04f6cdc5bc22..6bba07a8736a 100644 --- a/common/src/compile.rs +++ b/common/src/compile.rs @@ -525,7 +525,7 @@ impl FromStr for SkipBuildFilter { impl FileFilter for SkipBuildFilter { /// Matches file only if the filter does not apply /// - /// This is returns the inverse of `file.name.contains(pattern)` + /// This is returns the inverse of `file.name.contains(pattern) || matcher.is_match(file)` fn is_match(&self, file: &Path) -> bool { fn exclude(file: &Path, pattern: &str) -> Option { let matcher: GlobMatcher = pattern.parse().unwrap(); @@ -553,9 +553,9 @@ mod tests { assert!(!SkipBuildFilter::Scripts.is_match(file)); assert!(!SkipBuildFilter::Custom("A.s".to_string()).is_match(file)); - let file = Path::new("/private/var/folders/test/Foo.sol"); + let file = Path::new("/home/test/Foo.sol"); assert!(!SkipBuildFilter::Custom("*/test/**".to_string()).is_match(file)); - let file = Path::new("script/Contract.sol"); + let file = Path::new("/home/script/Contract.sol"); assert!(!SkipBuildFilter::Custom("*/script/**".to_string()).is_match(file)); } } From e14bc3e11f0b989577ab55b79fcb963343490a64 Mon Sep 17 00:00:00 2001 From: Matthias Seitz Date: Tue, 4 Jul 2023 14:42:40 +0200 Subject: [PATCH 3/3] Update common/src/glob.rs Co-authored-by: evalir --- common/src/glob.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/src/glob.rs b/common/src/glob.rs index 200063afc477..c6bff0176ee8 100644 --- a/common/src/glob.rs +++ b/common/src/glob.rs @@ -6,7 +6,7 @@ use std::{fmt, str::FromStr}; pub struct GlobMatcher { /// The parsed glob pub glob: globset::Glob, - /// The compiled `glob` + /// The compiled glob pub matcher: globset::GlobMatcher, }