Skip to content

Commit 11a36f9

Browse files
committed
chore: avoid cloning CompileOutput to parse inline config
1 parent b536f51 commit 11a36f9

File tree

4 files changed

+110
-127
lines changed

4 files changed

+110
-127
lines changed

crates/config/src/inline/natspec.rs

Lines changed: 56 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
1-
use std::{collections::BTreeMap, path::Path};
1+
use std::{
2+
collections::BTreeMap,
3+
path::{Path, PathBuf},
4+
};
25

36
use ethers_solc::{
47
artifacts::{ast::NodeType, Node},
5-
ProjectCompileOutput,
8+
ArtifactId, ArtifactOutput, Artifacts, ConfigurableArtifacts, ProjectCompileOutput,
69
};
710
use serde_json::Value;
811

@@ -26,21 +29,21 @@ impl NatSpec {
2629
/// Factory function that extracts a vector of [`NatSpec`] instances from
2730
/// a solc compiler output. The root path is to express contract base dirs.
2831
/// That is essential to match per-test configs at runtime.
29-
pub fn parse<P>(output: &ProjectCompileOutput, root: &P) -> Vec<Self>
30-
where
31-
P: AsRef<Path>,
32-
{
32+
pub fn parse(output: &ProjectCompileOutput, root: &Path) -> Vec<Self> {
3333
let mut natspecs: Vec<Self> = vec![];
3434

35-
let output = output.clone();
36-
for artifact in output.with_stripped_file_prefixes(root).into_artifacts() {
37-
if let Some(ast) = artifact.1.ast.as_ref() {
38-
let contract: String = artifact.0.identifier();
39-
if let Some(node) = contract_root_node(&ast.nodes, &contract) {
40-
apply(&mut natspecs, &contract, node)
41-
}
42-
}
35+
let artifacts = artifacts::<_, ConfigurableArtifacts>(output.cached_artifacts())
36+
.chain(artifacts::<_, ConfigurableArtifacts>(output.compiled_artifacts()));
37+
for (id, artifact) in artifacts {
38+
let Some(ast) = &artifact.ast else { continue };
39+
let path = id.source.as_path();
40+
let path = path.strip_prefix(root).unwrap_or(path);
41+
// id.identifier
42+
let contract = format!("{}:{}", path.display(), id.name);
43+
let Some(node) = contract_root_node(&ast.nodes, &contract) else { continue };
44+
apply(&mut natspecs, &contract, node)
4345
}
46+
4447
natspecs
4548
}
4649

@@ -52,24 +55,21 @@ impl NatSpec {
5255
}
5356

5457
/// Returns a list of configuration lines that match the current profile
55-
pub fn current_profile_configs(&self) -> Vec<String> {
56-
let prefix: &str = INLINE_CONFIG_PREFIX_SELECTED_PROFILE.as_ref();
57-
self.config_lines_with_prefix(prefix)
58+
pub fn current_profile_configs(&self) -> impl Iterator<Item = String> + '_ {
59+
self.config_lines_with_prefix(INLINE_CONFIG_PREFIX_SELECTED_PROFILE.as_str())
5860
}
5961

6062
/// Returns a list of configuration lines that match a specific string prefix
61-
pub fn config_lines_with_prefix<S: Into<String>>(&self, prefix: S) -> Vec<String> {
62-
let prefix: String = prefix.into();
63-
self.config_lines().into_iter().filter(|l| l.starts_with(&prefix)).collect()
63+
pub fn config_lines_with_prefix<'a>(
64+
&'a self,
65+
prefix: &'a str,
66+
) -> impl Iterator<Item = String> + 'a {
67+
self.config_lines().into_iter().filter(move |l| l.starts_with(prefix))
6468
}
6569

6670
/// Returns a list of all the configuration lines available in the natspec
67-
pub fn config_lines(&self) -> Vec<String> {
68-
self.docs
69-
.split('\n')
70-
.map(remove_whitespaces)
71-
.filter(|line| line.contains(INLINE_CONFIG_PREFIX))
72-
.collect::<Vec<String>>()
71+
pub fn config_lines(&self) -> impl Iterator<Item = String> + '_ {
72+
self.docs.lines().map(remove_whitespaces).filter(|line| line.contains(INLINE_CONFIG_PREFIX))
7373
}
7474
}
7575

@@ -89,6 +89,31 @@ fn contract_root_node<'a>(nodes: &'a [Node], contract_id: &'a str) -> Option<&'a
8989
None
9090
}
9191

92+
// borrowed version of `Artifacts::into_artifacts`
93+
fn artifacts<'a, T, O: ArtifactOutput<Artifact = T>>(
94+
a: &'a Artifacts<T>,
95+
) -> impl Iterator<Item = (ArtifactId, &'a T)> + 'a {
96+
a.0.iter().flat_map(|(file, contract_artifacts)| {
97+
contract_artifacts.iter().flat_map(move |(_contract_name, artifacts)| {
98+
let source = PathBuf::from(file.clone());
99+
artifacts.iter().filter_map(move |artifact| {
100+
O::contract_name(&artifact.file).map(|name| {
101+
(
102+
ArtifactId {
103+
path: PathBuf::from(&artifact.file),
104+
name,
105+
source: source.clone(),
106+
version: artifact.version.clone(),
107+
}
108+
.with_slashed_paths(),
109+
&artifact.artifact,
110+
)
111+
})
112+
})
113+
})
114+
})
115+
}
116+
92117
/// Implements a DFS over a compiler output node and its children.
93118
/// If a natspec is found it is added to `natspecs`
94119
fn apply(natspecs: &mut Vec<NatSpec>, contract: &str, node: &Node) {
@@ -137,7 +162,7 @@ fn get_fn_docs(fn_data: &BTreeMap<String, Value>) -> Option<(String, String)> {
137162
let mut src_line = fn_docs
138163
.get("src")
139164
.map(|src| src.to_string())
140-
.unwrap_or(String::from("<no-src-line-available>"));
165+
.unwrap_or_else(|| String::from("<no-src-line-available>"));
141166

142167
src_line.retain(|c| c != '"');
143168
return Some((comment.into(), src_line))
@@ -158,7 +183,7 @@ mod tests {
158183
let natspec = natspec();
159184
let config_lines = natspec.config_lines();
160185
assert_eq!(
161-
config_lines,
186+
config_lines.collect::<Vec<_>>(),
162187
vec![
163188
"forge-config:default.fuzz.runs=600".to_string(),
164189
"forge-config:ci.fuzz.runs=500".to_string(),
@@ -173,7 +198,7 @@ mod tests {
173198
let config_lines = natspec.current_profile_configs();
174199

175200
assert_eq!(
176-
config_lines,
201+
config_lines.collect::<Vec<_>>(),
177202
vec![
178203
"forge-config:default.fuzz.runs=600".to_string(),
179204
"forge-config:default.invariant.runs=1".to_string()
@@ -186,9 +211,9 @@ mod tests {
186211
use super::INLINE_CONFIG_PREFIX;
187212
let natspec = natspec();
188213
let prefix = format!("{INLINE_CONFIG_PREFIX}:default");
189-
let config_lines = natspec.config_lines_with_prefix(prefix);
214+
let config_lines = natspec.config_lines_with_prefix(&prefix);
190215
assert_eq!(
191-
config_lines,
216+
config_lines.collect::<Vec<_>>(),
192217
vec![
193218
"forge-config:default.fuzz.runs=600".to_string(),
194219
"forge-config:default.invariant.runs=1".to_string()

crates/forge/bin/cmd/test/mod.rs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -165,9 +165,8 @@ impl TestArgs {
165165
let test_options: TestOptions = TestOptionsBuilder::default()
166166
.fuzz(config.fuzz)
167167
.invariant(config.invariant)
168-
.compile_output(&output)
169168
.profiles(profiles)
170-
.build(project_root)?;
169+
.build(&output, project_root)?;
171170

172171
// Determine print verbosity and executor verbosity
173172
let verbosity = evm_opts.verbosity;

crates/forge/src/lib.rs

Lines changed: 50 additions & 88 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,47 @@ pub struct TestOptions {
4747
}
4848

4949
impl TestOptions {
50+
/// Tries to create a new instance by detecting inline configurations from the project compile
51+
/// output.
52+
pub fn new(
53+
output: &ProjectCompileOutput,
54+
root: &Path,
55+
profiles: Vec<String>,
56+
base_fuzz: FuzzConfig,
57+
base_invariant: InvariantConfig,
58+
) -> Result<Self, InlineConfigError> {
59+
let natspecs: Vec<NatSpec> = NatSpec::parse(output, root);
60+
let mut inline_invariant = InlineConfig::<InvariantConfig>::default();
61+
let mut inline_fuzz = InlineConfig::<FuzzConfig>::default();
62+
63+
for natspec in natspecs {
64+
// Perform general validation
65+
validate_profiles(&natspec, &profiles)?;
66+
FuzzConfig::validate_configs(&natspec)?;
67+
InvariantConfig::validate_configs(&natspec)?;
68+
69+
// Apply in-line configurations for the current profile
70+
let configs: Vec<String> = natspec.current_profile_configs().collect();
71+
let c: &str = &natspec.contract;
72+
let f: &str = &natspec.function;
73+
let line: String = natspec.debug_context();
74+
75+
match base_fuzz.try_merge(&configs) {
76+
Ok(Some(conf)) => inline_fuzz.insert(c, f, conf),
77+
Ok(None) => { /* No inline config found, do nothing */ }
78+
Err(e) => Err(InlineConfigError { line: line.clone(), source: e })?,
79+
}
80+
81+
match base_invariant.try_merge(&configs) {
82+
Ok(Some(conf)) => inline_invariant.insert(c, f, conf),
83+
Ok(None) => { /* No inline config found, do nothing */ }
84+
Err(e) => Err(InlineConfigError { line: line.clone(), source: e })?,
85+
}
86+
}
87+
88+
Ok(Self { fuzz: base_fuzz, invariant: base_invariant, inline_fuzz, inline_invariant })
89+
}
90+
5091
/// Returns a "fuzz" test runner instance. Parameters are used to select tight scoped fuzz
5192
/// configs that apply for a contract-function pair. A fallback configuration is applied
5293
/// if no specific setup is found for a given input.
@@ -127,83 +168,23 @@ impl TestOptions {
127168
}
128169
}
129170

130-
impl<'a, P> TryFrom<(&'a ProjectCompileOutput, &'a P, Vec<String>, FuzzConfig, InvariantConfig)>
131-
for TestOptions
132-
where
133-
P: AsRef<Path>,
134-
{
135-
type Error = InlineConfigError;
136-
137-
/// Tries to create an instance of `Self`, detecting inline configurations from the project
138-
/// compile output.
139-
///
140-
/// Param is a tuple, whose elements are:
141-
/// 1. Solidity compiler output, essential to extract natspec test configs.
142-
/// 2. Root path to express contract base dirs. This is essential to match inline configs at
143-
/// runtime. 3. List of available configuration profiles
144-
/// 4. Reference to a fuzz base configuration.
145-
/// 5. Reference to an invariant base configuration.
146-
fn try_from(
147-
value: (&'a ProjectCompileOutput, &'a P, Vec<String>, FuzzConfig, InvariantConfig),
148-
) -> Result<Self, Self::Error> {
149-
let output = value.0;
150-
let root = value.1;
151-
let profiles = &value.2;
152-
let base_fuzz: FuzzConfig = value.3;
153-
let base_invariant: InvariantConfig = value.4;
154-
155-
let natspecs: Vec<NatSpec> = NatSpec::parse(output, root);
156-
let mut inline_invariant = InlineConfig::<InvariantConfig>::default();
157-
let mut inline_fuzz = InlineConfig::<FuzzConfig>::default();
158-
159-
for natspec in natspecs {
160-
// Perform general validation
161-
validate_profiles(&natspec, profiles)?;
162-
FuzzConfig::validate_configs(&natspec)?;
163-
InvariantConfig::validate_configs(&natspec)?;
164-
165-
// Apply in-line configurations for the current profile
166-
let configs: Vec<String> = natspec.current_profile_configs();
167-
let c: &str = &natspec.contract;
168-
let f: &str = &natspec.function;
169-
let line: String = natspec.debug_context();
170-
171-
match base_fuzz.try_merge(&configs) {
172-
Ok(Some(conf)) => inline_fuzz.insert(c, f, conf),
173-
Err(e) => Err(InlineConfigError { line: line.clone(), source: e })?,
174-
_ => { /* No inline config found, do nothing */ }
175-
}
176-
177-
match base_invariant.try_merge(&configs) {
178-
Ok(Some(conf)) => inline_invariant.insert(c, f, conf),
179-
Err(e) => Err(InlineConfigError { line: line.clone(), source: e })?,
180-
_ => { /* No inline config found, do nothing */ }
181-
}
182-
}
183-
184-
Ok(Self { fuzz: base_fuzz, invariant: base_invariant, inline_fuzz, inline_invariant })
185-
}
186-
}
187-
188171
/// Builder utility to create a [`TestOptions`] instance.
189172
#[derive(Default)]
173+
#[must_use = "builders do nothing unless you call `build` on them"]
190174
pub struct TestOptionsBuilder {
191175
fuzz: Option<FuzzConfig>,
192176
invariant: Option<InvariantConfig>,
193177
profiles: Option<Vec<String>>,
194-
output: Option<ProjectCompileOutput>,
195178
}
196179

197180
impl TestOptionsBuilder {
198181
/// Sets a [`FuzzConfig`] to be used as base "fuzz" configuration.
199-
#[must_use = "A base 'fuzz' config must be provided"]
200182
pub fn fuzz(mut self, conf: FuzzConfig) -> Self {
201183
self.fuzz = Some(conf);
202184
self
203185
}
204186

205187
/// Sets a [`InvariantConfig`] to be used as base "invariant" configuration.
206-
#[must_use = "A base 'invariant' config must be provided"]
207188
pub fn invariant(mut self, conf: InvariantConfig) -> Self {
208189
self.invariant = Some(conf);
209190
self
@@ -216,40 +197,21 @@ impl TestOptionsBuilder {
216197
self
217198
}
218199

219-
/// Sets a project compiler output instance. This is used to extract
220-
/// inline test configurations that override `self.fuzz` and `self.invariant`
221-
/// specs when necessary.
222-
pub fn compile_output(mut self, output: &ProjectCompileOutput) -> Self {
223-
self.output = Some(output.clone());
224-
self
225-
}
226-
227200
/// Creates an instance of [`TestOptions`]. This takes care of creating "fuzz" and
228201
/// "invariant" fallbacks, and extracting all inline test configs, if available.
229202
///
230203
/// `root` is a reference to the user's project root dir. This is essential
231204
/// to determine the base path of generated contract identifiers. This is to provide correct
232205
/// matchers for inline test configs.
233-
pub fn build(self, root: impl AsRef<Path>) -> Result<TestOptions, InlineConfigError> {
234-
let default_profiles = vec![Config::selected_profile().into()];
235-
let profiles: Vec<String> = self.profiles.unwrap_or(default_profiles);
206+
pub fn build(
207+
self,
208+
output: &ProjectCompileOutput,
209+
root: &Path,
210+
) -> Result<TestOptions, InlineConfigError> {
211+
let profiles: Vec<String> =
212+
self.profiles.unwrap_or_else(|| vec![Config::selected_profile().into()]);
236213
let base_fuzz = self.fuzz.unwrap_or_default();
237214
let base_invariant = self.invariant.unwrap_or_default();
238-
239-
match self.output {
240-
Some(compile_output) => Ok(TestOptions::try_from((
241-
&compile_output,
242-
&root,
243-
profiles,
244-
base_fuzz,
245-
base_invariant,
246-
))?),
247-
None => Ok(TestOptions {
248-
fuzz: base_fuzz,
249-
invariant: base_invariant,
250-
inline_fuzz: InlineConfig::default(),
251-
inline_invariant: InlineConfig::default(),
252-
}),
253-
}
215+
TestOptions::new(output, &root, profiles, base_fuzz, base_invariant)
254216
}
255217
}

crates/forge/tests/it/inline.rs

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -76,9 +76,8 @@ fn build_test_options() {
7676
let build_result = TestOptionsBuilder::default()
7777
.fuzz(FuzzConfig::default())
7878
.invariant(InvariantConfig::default())
79-
.compile_output(&COMPILED)
8079
.profiles(profiles)
81-
.build(root);
80+
.build(&COMPILED, root);
8281

8382
assert!(build_result.is_ok());
8483
}
@@ -90,9 +89,8 @@ fn build_test_options_just_one_valid_profile() {
9089
let build_result = TestOptionsBuilder::default()
9190
.fuzz(FuzzConfig::default())
9291
.invariant(InvariantConfig::default())
93-
.compile_output(&COMPILED)
9492
.profiles(valid_profiles)
95-
.build(root);
93+
.build(&COMPILED, root);
9694

9795
// We expect an error, since COMPILED contains in-line
9896
// per-test configs for "default" and "ci" profiles
@@ -104,7 +102,6 @@ fn test_options() -> TestOptions {
104102
TestOptionsBuilder::default()
105103
.fuzz(FuzzConfig::default())
106104
.invariant(InvariantConfig::default())
107-
.compile_output(&COMPILED)
108-
.build(root)
105+
.build(&COMPILED, root)
109106
.expect("Config loaded")
110107
}

0 commit comments

Comments
 (0)