Skip to content

Commit ea2f2db

Browse files
committed
test: add the infra to run the local node in tests, add counter-contract
test running on the local node
1 parent 74835ac commit ea2f2db

3 files changed

Lines changed: 435 additions & 4 deletions

File tree

Lines changed: 230 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,230 @@
1+
//! Infrastructure for running a local Miden node for integration tests
2+
3+
use std::{
4+
io::BufRead,
5+
path::{Path, PathBuf},
6+
process::{Child, Command, Stdio},
7+
sync::{Arc, Mutex},
8+
thread,
9+
time::Duration,
10+
};
11+
12+
use temp_dir::TempDir;
13+
use tokio::time::sleep;
14+
15+
/// Manages the lifecycle of a local Miden node instance
16+
pub struct LocalMidenNode {
17+
/// Temporary directory containing node data
18+
data_dir: TempDir,
19+
/// The node process handle
20+
node_process: Option<Child>,
21+
/// RPC URL for the node
22+
rpc_url: String,
23+
/// Whether the node has been bootstrapped
24+
bootstrapped: bool,
25+
}
26+
27+
impl LocalMidenNode {
28+
/// Creates a new LocalMidenNode instance
29+
pub fn new() -> Self {
30+
let data_dir = TempDir::new().expect("Failed to create temp directory");
31+
Self {
32+
data_dir,
33+
node_process: None,
34+
rpc_url: "http://127.0.0.1:57291".to_string(),
35+
bootstrapped: false,
36+
}
37+
}
38+
39+
/// Install miden-node binary if not already installed
40+
pub fn ensure_installed(&self) -> Result<(), String> {
41+
// Check if miden-node is already installed
42+
let check = Command::new("miden-node").arg("--version").output();
43+
44+
match check {
45+
Ok(output) if output.status.success() => {
46+
let version = String::from_utf8_lossy(&output.stdout);
47+
eprintln!("miden-node already installed: {}", version.trim());
48+
Ok(())
49+
}
50+
_ => {
51+
eprintln!("Installing miden-node from crates.io...");
52+
let output = Command::new("cargo")
53+
.args(["install", "miden-node", "--locked"])
54+
.output()
55+
.map_err(|e| format!("Failed to run cargo install: {}", e))?;
56+
57+
if !output.status.success() {
58+
let stderr = String::from_utf8_lossy(&output.stderr);
59+
return Err(format!("Failed to install miden-node: {}", stderr));
60+
}
61+
62+
eprintln!("miden-node installed successfully");
63+
Ok(())
64+
}
65+
}
66+
}
67+
68+
/// Bootstrap the node with genesis data
69+
pub fn bootstrap(&mut self) -> Result<(), String> {
70+
if self.bootstrapped {
71+
return Ok(());
72+
}
73+
74+
eprintln!("Bootstrapping miden-node...");
75+
76+
let output = Command::new("miden-node")
77+
.args([
78+
"bundled",
79+
"bootstrap",
80+
"--data-directory",
81+
self.data_dir.path().to_str().unwrap(),
82+
"--accounts-directory",
83+
self.data_dir.path().to_str().unwrap(),
84+
])
85+
.output()
86+
.map_err(|e| format!("Failed to run bootstrap: {}", e))?;
87+
88+
if !output.status.success() {
89+
let stderr = String::from_utf8_lossy(&output.stderr);
90+
return Err(format!("Failed to bootstrap node: {}", stderr));
91+
}
92+
93+
self.bootstrapped = true;
94+
eprintln!("Node bootstrapped successfully");
95+
Ok(())
96+
}
97+
98+
/// Start the node process
99+
pub async fn start(&mut self) -> Result<(), String> {
100+
if self.node_process.is_some() {
101+
return Err("Node is already running".to_string());
102+
}
103+
104+
if !self.bootstrapped {
105+
self.bootstrap()?;
106+
}
107+
108+
eprintln!("Starting miden-node on {}...", self.rpc_url);
109+
110+
let mut child = Command::new("miden-node")
111+
.args([
112+
"bundled",
113+
"start",
114+
"--data-directory",
115+
self.data_dir.path().to_str().unwrap(),
116+
"--rpc.url",
117+
&self.rpc_url,
118+
"--block.interval",
119+
"1", // 1 second block interval for faster tests
120+
])
121+
.stdout(Stdio::piped())
122+
.stderr(Stdio::piped())
123+
.spawn()
124+
.map_err(|e| format!("Failed to start node: {}", e))?;
125+
126+
// Capture output for debugging
127+
let stdout = child.stdout.take().expect("Failed to capture stdout");
128+
let stderr = child.stderr.take().expect("Failed to capture stderr");
129+
130+
// Spawn threads to read and print output
131+
thread::spawn(move || {
132+
let reader = std::io::BufReader::new(stdout);
133+
for line in reader.lines().map_while(Result::ok) {
134+
eprintln!("[node stdout] {}", line);
135+
}
136+
});
137+
138+
thread::spawn(move || {
139+
let reader = std::io::BufReader::new(stderr);
140+
for line in reader.lines().map_while(Result::ok) {
141+
eprintln!("[node stderr] {}", line);
142+
}
143+
});
144+
145+
self.node_process = Some(child);
146+
147+
// Wait for the node to be ready
148+
self.wait_for_ready().await?;
149+
150+
eprintln!("Node started successfully");
151+
Ok(())
152+
}
153+
154+
/// Wait for the node to be ready to accept connections
155+
async fn wait_for_ready(&self) -> Result<(), String> {
156+
// The node doesn't have a health endpoint, so we just wait a bit
157+
// for it to start up. In practice, checking if we can connect to
158+
// the gRPC endpoint would be better, but for now a simple delay works.
159+
eprintln!("Waiting for node to be ready...");
160+
sleep(Duration::from_secs(3)).await;
161+
eprintln!("Node should be ready now");
162+
Ok(())
163+
}
164+
165+
/// Stop the node process
166+
pub fn stop(&mut self) -> Result<(), String> {
167+
if let Some(mut child) = self.node_process.take() {
168+
eprintln!("Stopping miden-node...");
169+
170+
// Kill the process
171+
match child.kill() {
172+
Ok(_) => eprintln!("Kill signal sent to node process"),
173+
Err(e) => eprintln!("Warning: Failed to kill node process: {}", e),
174+
}
175+
176+
// Wait for the process to exit
177+
match child.wait() {
178+
Ok(status) => eprintln!("Node stopped with status: {:?}", status),
179+
Err(e) => eprintln!("Warning: Failed to wait for node exit: {}", e),
180+
}
181+
}
182+
183+
Ok(())
184+
}
185+
186+
/// Get the RPC URL for connecting to the node
187+
pub fn rpc_url(&self) -> &str {
188+
&self.rpc_url
189+
}
190+
}
191+
192+
impl Drop for LocalMidenNode {
193+
fn drop(&mut self) {
194+
if let Err(e) = self.stop() {
195+
eprintln!("Error stopping node during cleanup: {}", e);
196+
}
197+
}
198+
}
199+
200+
/// Create an isolated node instance for tests
201+
pub async fn create_isolated_node() -> Result<LocalMidenNode, String> {
202+
let mut node = LocalMidenNode::new();
203+
node.ensure_installed()?;
204+
node.bootstrap()?;
205+
node.start().await?;
206+
Ok(node)
207+
}
208+
209+
#[cfg(test)]
210+
mod tests {
211+
use super::*;
212+
213+
#[tokio::test]
214+
async fn test_local_node_lifecycle() {
215+
let mut node = LocalMidenNode::new();
216+
217+
// Ensure node is installed
218+
node.ensure_installed().expect("Failed to install node");
219+
220+
// Bootstrap and start the node
221+
node.bootstrap().expect("Failed to bootstrap node");
222+
node.start().await.expect("Failed to start node");
223+
224+
// Verify we can get the RPC URL
225+
assert_eq!(node.rpc_url(), "http://127.0.0.1:57291");
226+
227+
// Stop the node
228+
node.stop().expect("Failed to stop node");
229+
}
230+
}

tests/integration/src/rust_masm_tests/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ mod apps;
1313
mod examples;
1414
mod instructions;
1515
mod intrinsics;
16+
mod local_node;
1617
mod rust_sdk;
1718
mod testnet;
1819
mod types;

0 commit comments

Comments
 (0)