Skip to content

Commit 6e6341b

Browse files
authored
fix(coverage): contracts by artifact from linked contracts (#11440)
1 parent 4e59f42 commit 6e6341b

File tree

3 files changed

+117
-29
lines changed

3 files changed

+117
-29
lines changed

crates/common/src/contracts.rs

Lines changed: 55 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,9 @@ use eyre::{OptionExt, Result};
88
use foundry_compilers::{
99
ArtifactId, Project, ProjectCompileOutput,
1010
artifacts::{
11-
BytecodeObject, CompactBytecode, CompactContractBytecode, CompactDeployedBytecode,
12-
ConfigurableContractArtifact, ContractBytecodeSome, Offsets, StorageLayout,
11+
BytecodeObject, CompactBytecode, CompactContractBytecode, CompactContractBytecodeCow,
12+
CompactDeployedBytecode, ConfigurableContractArtifact, ContractBytecodeSome, Offsets,
13+
StorageLayout,
1314
},
1415
utils::canonicalized,
1516
};
@@ -101,6 +102,58 @@ impl ContractData {
101102
}
102103
}
103104

105+
/// Builder for creating a `ContractsByArtifact` instance, optionally including storage layouts
106+
/// from project compile output.
107+
pub struct ContractsByArtifactBuilder<'a> {
108+
/// All compiled artifact bytecodes (borrowed).
109+
artifacts: BTreeMap<ArtifactId, CompactContractBytecodeCow<'a>>,
110+
/// Optionally collected storage layouts for matching artifact IDs.
111+
storage_layouts: BTreeMap<ArtifactId, StorageLayout>,
112+
}
113+
114+
impl<'a> ContractsByArtifactBuilder<'a> {
115+
/// Creates a new builder from artifacts with present bytecode iterator.
116+
pub fn new(
117+
artifacts: impl IntoIterator<Item = (ArtifactId, CompactContractBytecodeCow<'a>)>,
118+
) -> Self {
119+
Self { artifacts: artifacts.into_iter().collect(), storage_layouts: BTreeMap::new() }
120+
}
121+
122+
/// Adds storage layouts from `ProjectCompileOutput` to known artifacts.
123+
pub fn with_storage_layouts(mut self, output: ProjectCompileOutput) -> Self {
124+
self.storage_layouts = output
125+
.into_artifacts()
126+
.filter_map(|(id, artifact)| artifact.storage_layout.map(|layout| (id, layout)))
127+
.collect();
128+
self
129+
}
130+
131+
/// Builds `ContractsByArtifact`.
132+
pub fn build(self) -> ContractsByArtifact {
133+
let map = self
134+
.artifacts
135+
.into_iter()
136+
.filter_map(|(id, artifact)| {
137+
let name = id.name.clone();
138+
let CompactContractBytecodeCow { abi, bytecode, deployed_bytecode } = artifact;
139+
140+
Some((
141+
id.clone(),
142+
ContractData {
143+
name,
144+
abi: abi?.into_owned(),
145+
bytecode: bytecode.map(|b| b.into_owned().into()),
146+
deployed_bytecode: deployed_bytecode.map(|b| b.into_owned().into()),
147+
storage_layout: self.storage_layouts.get(&id).map(|l| Arc::new(l.clone())),
148+
},
149+
))
150+
})
151+
.collect();
152+
153+
ContractsByArtifact(Arc::new(map))
154+
}
155+
}
156+
104157
type ArtifactWithContractRef<'a> = (&'a ArtifactId, &'a ContractData);
105158

106159
/// Wrapper type that maps an artifact to a contract ABI and bytecode.
@@ -130,28 +183,6 @@ impl ContractsByArtifact {
130183
Self(Arc::new(map))
131184
}
132185

133-
/// Creates a new instance from project compile output, preserving storage layouts.
134-
pub fn with_storage_layout(output: ProjectCompileOutput) -> Self {
135-
let map = output
136-
.into_artifacts()
137-
.filter_map(|(id, artifact)| {
138-
let name = id.name.clone();
139-
let abi = artifact.abi?;
140-
Some((
141-
id,
142-
ContractData {
143-
name,
144-
abi,
145-
bytecode: artifact.bytecode.map(Into::into),
146-
deployed_bytecode: artifact.deployed_bytecode.map(Into::into),
147-
storage_layout: artifact.storage_layout.map(Arc::new),
148-
},
149-
))
150-
})
151-
.collect();
152-
Self(Arc::new(map))
153-
}
154-
155186
/// Clears all contracts.
156187
pub fn clear(&mut self) {
157188
*self = Self::default();

crates/forge/src/multi_runner.rs

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,10 @@ use crate::{
77
use alloy_json_abi::{Function, JsonAbi};
88
use alloy_primitives::{Address, Bytes, U256};
99
use eyre::Result;
10-
use foundry_common::{ContractsByArtifact, TestFunctionExt, get_contract_name, shell::verbosity};
10+
use foundry_common::{
11+
ContractsByArtifact, ContractsByArtifactBuilder, TestFunctionExt, get_contract_name,
12+
shell::verbosity,
13+
};
1114
use foundry_compilers::{
1215
Artifact, ArtifactId, ProjectCompileOutput,
1316
artifacts::{Contract, Libraries},
@@ -531,10 +534,10 @@ impl MultiContractRunnerBuilder {
531534
}
532535
}
533536

534-
// Create known contracts with storage layout information
535-
let known_contracts = ContractsByArtifact::with_storage_layout(
536-
output.clone().with_stripped_file_prefixes(root),
537-
);
537+
// Create known contracts from linked contracts and storage layout information (if any).
538+
let known_contracts = ContractsByArtifactBuilder::new(linked_contracts)
539+
.with_storage_layouts(output.clone().with_stripped_file_prefixes(root))
540+
.build();
538541

539542
Ok(MultiContractRunner {
540543
contracts: deployable_contracts,

crates/forge/tests/cli/coverage.rs

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1850,6 +1850,60 @@ contract ArrayConditionTest is DSTest {
18501850
"#]]);
18511851
});
18521852

1853+
// https://github.com/foundry-rs/foundry/issues/11432
1854+
// Test coverage for linked libraries.
1855+
forgetest!(linked_library, |prj, cmd| {
1856+
prj.insert_ds_test();
1857+
prj.add_source(
1858+
"Counter.sol",
1859+
r#"
1860+
library LibCounter {
1861+
function increment(uint256 number) external returns (uint256) {
1862+
return number + 1;
1863+
}
1864+
}
1865+
1866+
contract Counter {
1867+
uint256 public number;
1868+
1869+
function increment() public {
1870+
number = LibCounter.increment(number);
1871+
}
1872+
}
1873+
"#,
1874+
)
1875+
.unwrap();
1876+
1877+
prj.add_source(
1878+
"CounterTest.sol",
1879+
r#"
1880+
import "./test.sol";
1881+
import {Counter} from "./Counter.sol";
1882+
1883+
contract CounterTest is DSTest {
1884+
function testIncrement() public {
1885+
Counter counter = new Counter();
1886+
counter.increment();
1887+
}
1888+
}
1889+
"#,
1890+
)
1891+
.unwrap();
1892+
1893+
// Assert 100% coverage for linked libraries.
1894+
cmd.arg("coverage").assert_success().stdout_eq(str![[r#"
1895+
...
1896+
╭-----------------+---------------+---------------+---------------+---------------╮
1897+
| File | % Lines | % Statements | % Branches | % Funcs |
1898+
+=================================================================================+
1899+
| src/Counter.sol | 100.00% (4/4) | 100.00% (3/3) | 100.00% (0/0) | 100.00% (2/2) |
1900+
|-----------------+---------------+---------------+---------------+---------------|
1901+
| Total | 100.00% (4/4) | 100.00% (3/3) | 100.00% (0/0) | 100.00% (2/2) |
1902+
╰-----------------+---------------+---------------+---------------+---------------╯
1903+
...
1904+
"#]]);
1905+
});
1906+
18531907
// <https://github.com/foundry-rs/foundry/issues/10422>
18541908
// Test that line hits are properly recorded in lcov report.
18551909
forgetest!(do_while_lcov, |prj, cmd| {

0 commit comments

Comments
 (0)