Skip to content

Commit 288e97f

Browse files
authored
feat(anvil): comprehensive documentation and API improvements (#429)
# Improve the anvil module Enhance the anvil module with better documentation, API consistency, and improved test coverage to match repository best practices. ## Changes - Add public ANVIL_PORT constant for API consistency with other modules - Add with_tag() convenience method for ergonomic tag selection - Create comprehensive inline documentation with usage examples - Add detailed builder method documentation clarifying path handling - Create examples/anvil.rs demonstrating practical usage - Add blocking/sync test variant - Add test for latest() method - Update lib.rs module description to be more descriptive - Update outdated testcontainers version references ## Impact - Test coverage: 2 → 4 tests (100% increase) - Documentation: Now includes comprehensive examples and usage patterns - API: Consistent with 27+ other modules that export port constants - User experience: Significantly improved with clearer docs and examples --- Signed off by Joseph Livesey <[email protected]>
1 parent a47c5d6 commit 288e97f

File tree

4 files changed

+195
-12
lines changed

4 files changed

+195
-12
lines changed

Cargo.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,10 @@ azure_storage_blobs = "0.21.0"
140140
azure_storage = "0.21.0"
141141
base64 = "0.22.1"
142142

143+
[[example]]
144+
name = "anvil"
145+
required-features = ["anvil"]
146+
143147
[[example]]
144148
name = "postgres"
145149
required-features = ["postgres"]

examples/anvil.rs

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
use testcontainers::runners::SyncRunner;
2+
use testcontainers_modules::anvil::{AnvilNode, ANVIL_PORT};
3+
4+
fn main() -> Result<(), Box<dyn std::error::Error + 'static>> {
5+
// Start the Anvil node
6+
let node = AnvilNode::default().start()?;
7+
8+
// Get the mapped port for the Anvil JSON-RPC endpoint
9+
let port = node.get_host_port_ipv4(ANVIL_PORT)?;
10+
let rpc_url = format!("http://localhost:{port}");
11+
12+
println!("Anvil node started successfully!");
13+
println!("JSON-RPC endpoint: {rpc_url}");
14+
println!();
15+
println!("You can now connect to this endpoint using your Ethereum tooling:");
16+
println!(" - cast: cast block-number --rpc-url {rpc_url}");
17+
println!(" - web3: new Web3(new Web3.providers.HttpProvider('{rpc_url}'))");
18+
println!(" - alloy: Provider::try_from('{rpc_url}')");
19+
println!();
20+
println!("Press Ctrl+C to stop the container");
21+
22+
// Keep the container running
23+
std::thread::park();
24+
25+
Ok(())
26+
}

src/anvil/mod.rs

Lines changed: 164 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -7,27 +7,71 @@ use testcontainers::{
77

88
const NAME: &str = "ghcr.io/foundry-rs/foundry";
99
const TAG: &str = "v1.1.0";
10-
const PORT: ContainerPort = ContainerPort::Tcp(8545);
10+
11+
/// Port that the [`AnvilNode`] container exposes for JSON-RPC connections.
12+
/// Can be rebound externally via [`testcontainers::core::ImageExt::with_mapped_port`]
13+
///
14+
/// [`AnvilNode`]: https://book.getfoundry.sh/anvil/
15+
pub const ANVIL_PORT: ContainerPort = ContainerPort::Tcp(8545);
1116

1217
/// # Community Testcontainers Implementation for [Foundry Anvil](https://book.getfoundry.sh/anvil/)
1318
///
1419
/// This is a community implementation of the [Testcontainers](https://testcontainers.org/) interface for [Foundry Anvil](https://book.getfoundry.sh/anvil/).
1520
///
16-
/// It is not officially supported by Foundry, but it is a community effort to provide a more user-friendly interface for running Anvil inside a Docker container.
21+
/// Anvil is Foundry's fast local Ethereum node for development and testing. It's an ideal tool for rapid
22+
/// iteration on smart contract development and provides a clean, lightweight alternative to running a full node.
23+
///
24+
/// # Example
25+
///
26+
/// ```rust,no_run
27+
/// use testcontainers_modules::{
28+
/// anvil::{AnvilNode, ANVIL_PORT},
29+
/// testcontainers::runners::AsyncRunner,
30+
/// };
31+
///
32+
/// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
33+
/// // Start an Anvil node
34+
/// let node = AnvilNode::default().start().await?;
35+
///
36+
/// // Get the RPC endpoint URL
37+
/// let host_port = node.get_host_port_ipv4(ANVIL_PORT).await?;
38+
/// let rpc_url = format!("http://localhost:{host_port}");
39+
///
40+
/// // Use with your favorite Ethereum library (alloy, ethers, web3, etc.)
41+
/// // let provider = Provider::try_from(rpc_url)?;
42+
/// # Ok(())
43+
/// # }
44+
/// ```
45+
///
46+
/// # Advanced Configuration
47+
///
48+
/// ```rust,ignore
49+
/// use testcontainers_modules::anvil::AnvilNode;
50+
///
51+
/// // Configure chain ID and forking
52+
/// let node = AnvilNode::default()
53+
/// .with_chain_id(1337)
54+
/// .with_fork_url("https://eth.llamarpc.com")
55+
/// .with_fork_block_number(18_000_000)
56+
/// .start().await?;
57+
/// ```
58+
///
59+
/// # Usage
1760
///
18-
/// The endpoint of the container is intended to be injected into your provider configuration, so that you can easily run tests against a local Anvil instance.
19-
/// See the `test_anvil_node_container` test for an example of how to use this.
61+
/// The endpoint of the container is intended to be injected into your provider configuration, so that you can
62+
/// easily run tests against a local Anvil instance.
2063
///
2164
/// To use the latest Foundry image, you can use the `latest()` method:
2265
///
2366
/// ```rust,ignore
2467
/// let node = AnvilNode::latest().start().await?;
2568
/// ```
2669
///
27-
/// Users can use a specific Foundry image in their code with [`ImageExt::with_tag`](https://docs.rs/testcontainers/0.23.1/testcontainers/core/trait.ImageExt.html#tymethod.with_tag).
70+
/// Users can use a specific Foundry image in their code with [`ImageExt::with_tag`](https://docs.rs/testcontainers/latest/testcontainers/core/trait.ImageExt.html#tymethod.with_tag).
2871
///
2972
/// ```rust,ignore
30-
/// let node = AnvilNode::with_tag("master").start().await?;
73+
/// use testcontainers::core::ImageExt;
74+
/// let node = AnvilNode::default().with_tag("nightly").start().await?;
3175
/// ```
3276
#[derive(Debug, Clone, Default)]
3377
pub struct AnvilNode {
@@ -50,25 +94,65 @@ impl AnvilNode {
5094
}
5195
}
5296

97+
/// Use a specific Foundry image tag
98+
pub fn with_tag(mut self, tag: impl Into<String>) -> Self {
99+
self.tag = Some(tag.into());
100+
self
101+
}
102+
53103
/// Specify the chain ID - this will be Ethereum Mainnet by default
54104
pub fn with_chain_id(mut self, chain_id: u64) -> Self {
55105
self.chain_id = Some(chain_id);
56106
self
57107
}
58108

59-
/// Specify the fork URL
109+
/// Specify the fork URL to fork from a live network
110+
///
111+
/// # Example
112+
/// ```rust,ignore
113+
/// let node = AnvilNode::default()
114+
/// .with_fork_url("https://eth.llamarpc.com")
115+
/// .start().await?;
116+
/// ```
60117
pub fn with_fork_url(mut self, fork_url: impl Into<String>) -> Self {
61118
self.fork_url = Some(fork_url.into());
62119
self
63120
}
64121

65-
/// Specify the fork block number
122+
/// Specify the fork block number (requires `with_fork_url` to be set)
123+
///
124+
/// # Example
125+
/// ```rust,ignore
126+
/// let node = AnvilNode::default()
127+
/// .with_fork_url("https://eth.llamarpc.com")
128+
/// .with_fork_block_number(18_000_000)
129+
/// .start().await?;
130+
/// ```
66131
pub fn with_fork_block_number(mut self, block_number: u64) -> Self {
67132
self.fork_block_number = Some(block_number);
68133
self
69134
}
70135

71136
/// Mount a host directory for anvil state at `/state` inside the container
137+
///
138+
/// Use this method to bind mount a host directory to the container's `/state` directory.
139+
/// This allows you to persist state across container restarts.
140+
///
141+
/// # Arguments
142+
/// * `host_dir` - Path on the host machine to mount (will be mounted to `/state` in container)
143+
///
144+
/// # Note
145+
/// When using this method, specify container paths (starting with `/state/`) in
146+
/// `with_load_state_path` and `with_dump_state_path`.
147+
///
148+
/// # Example
149+
/// ```rust,ignore
150+
/// let temp_dir = std::env::temp_dir().join("anvil-state");
151+
/// let node = AnvilNode::default()
152+
/// .with_state_mount(&temp_dir)
153+
/// .with_dump_state_path("/state/state.json")
154+
/// .start().await?;
155+
/// ```
72156
pub fn with_state_mount(mut self, host_dir: impl AsRef<std::path::Path>) -> Self {
73157
let Some(host_dir_str) = host_dir.as_ref().to_str() else {
74158
return self;
@@ -79,20 +163,51 @@ impl AnvilNode {
79163

80164
/// Configure Anvil to initialize from a previously saved state snapshot.
81165
/// Equivalent to passing `--load-state <PATH>`.
166+
///
167+
/// # Arguments
168+
/// * `path` - Path to the state file (container-internal path, typically `/state/...` if using `with_state_mount`)
169+
///
170+
/// # Example
171+
/// ```rust,ignore
172+
/// let node = AnvilNode::default()
173+
/// .with_state_mount("/host/path/to/state")
174+
/// .with_load_state_path("/state/state.json")
175+
/// .start().await?;
176+
/// ```
82177
pub fn with_load_state_path(mut self, path: impl Into<String>) -> Self {
83178
self.load_state_path = Some(path.into());
84179
self
85180
}
86181

87182
/// Configure Anvil to dump the state on exit to the given file or directory.
88183
/// Equivalent to passing `--dump-state <PATH>`.
184+
///
185+
/// # Arguments
186+
/// * `path` - Path where state should be saved (container-internal path, typically `/state/...` if using `with_state_mount`)
187+
///
188+
/// # Example
189+
/// ```rust,ignore
190+
/// let node = AnvilNode::default()
191+
/// .with_state_mount("/host/path/to/state")
192+
/// .with_dump_state_path("/state/state.json")
193+
/// .start().await?;
194+
/// ```
89195
pub fn with_dump_state_path(mut self, path: impl Into<String>) -> Self {
90196
self.dump_state_path = Some(path.into());
91197
self
92198
}
93199

94200
/// Configure periodic state persistence interval in seconds.
95201
/// Equivalent to passing `--state-interval <SECONDS>`.
202+
///
203+
/// # Example
204+
/// ```rust,ignore
205+
/// let node = AnvilNode::default()
206+
/// .with_state_mount("/host/path/to/state")
207+
/// .with_dump_state_path("/state/state.json")
208+
/// .with_state_interval(30) // Save every 30 seconds
209+
/// .start().await?;
210+
/// ```
96211
pub fn with_state_interval(mut self, seconds: u64) -> Self {
97212
self.state_interval_secs = Some(seconds);
98213
self
@@ -151,7 +266,7 @@ impl Image for AnvilNode {
151266
}
152267

153268
fn expose_ports(&self) -> &[ContainerPort] {
154-
&[PORT]
269+
&[ANVIL_PORT]
155270
}
156271

157272
fn name(&self) -> &str {
@@ -171,16 +286,54 @@ impl Image for AnvilNode {
171286
mod tests {
172287
use alloy_network::AnyNetwork;
173288
use alloy_provider::{Provider, RootProvider};
174-
use testcontainers::runners::AsyncRunner;
175289

176290
use super::*;
177291

178292
#[tokio::test]
179293
async fn test_anvil_node_container() {
294+
use testcontainers::runners::AsyncRunner;
295+
180296
let _ = pretty_env_logger::try_init();
181297

182298
let node = AnvilNode::default().start().await.unwrap();
183-
let port = node.get_host_port_ipv4(PORT).await.unwrap();
299+
let port = node.get_host_port_ipv4(ANVIL_PORT).await.unwrap();
300+
301+
let provider: RootProvider<AnyNetwork> =
302+
RootProvider::new_http(format!("http://localhost:{port}").parse().unwrap());
303+
304+
let block_number = provider.get_block_number().await.unwrap();
305+
306+
assert_eq!(block_number, 0);
307+
}
308+
309+
#[test]
310+
fn test_anvil_node_container_sync() {
311+
use testcontainers::runners::SyncRunner;
312+
313+
let _ = pretty_env_logger::try_init();
314+
315+
let node = AnvilNode::default().start().unwrap();
316+
let port = node.get_host_port_ipv4(ANVIL_PORT).unwrap();
317+
318+
let provider: RootProvider<AnyNetwork> =
319+
RootProvider::new_http(format!("http://localhost:{port}").parse().unwrap());
320+
321+
let block_number = tokio::runtime::Runtime::new()
322+
.unwrap()
323+
.block_on(provider.get_block_number())
324+
.unwrap();
325+
326+
assert_eq!(block_number, 0);
327+
}
328+
329+
#[tokio::test]
330+
async fn test_anvil_latest() {
331+
use testcontainers::runners::AsyncRunner;
332+
333+
let _ = pretty_env_logger::try_init();
334+
335+
let node = AnvilNode::latest().start().await.unwrap();
336+
let port = node.get_host_port_ipv4(ANVIL_PORT).await.unwrap();
184337

185338
let provider: RootProvider<AnyNetwork> =
186339
RootProvider::new_http(format!("http://localhost:{port}").parse().unwrap());

src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
1111
#[cfg(feature = "anvil")]
1212
#[cfg_attr(docsrs, doc(cfg(feature = "anvil")))]
13-
/// **Anvil** (local blockchain emulator for EVM-compatible development) testcontainer
13+
/// **Anvil** (Foundry's fast local Ethereum node for development and testing) testcontainer
1414
pub mod anvil;
1515
#[cfg(feature = "arrow_flightsql")]
1616
#[cfg_attr(docsrs, doc(cfg(feature = "arrow_flightsql")))]

0 commit comments

Comments
 (0)