ink_e2e/
node_proc.rs

1// Copyright (C) Use Ink (UK) Ltd.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use sp_keyring::Sr25519Keyring;
16use std::{
17    ffi::{
18        OsStr,
19        OsString,
20    },
21    io::{
22        BufRead,
23        BufReader,
24        Read,
25    },
26    process,
27};
28use subxt::{
29    backend::rpc::RpcClient,
30    Config,
31    OnlineClient,
32};
33
34/// Spawn a local substrate node for testing.
35pub struct TestNodeProcess<R: Config> {
36    proc: process::Child,
37    rpc: RpcClient,
38    client: OnlineClient<R>,
39    url: String,
40}
41
42impl<R> Drop for TestNodeProcess<R>
43where
44    R: Config,
45{
46    fn drop(&mut self) {
47        let _ = self.kill();
48    }
49}
50
51impl<R> TestNodeProcess<R>
52where
53    R: Config,
54{
55    /// Construct a builder for spawning a test node process.
56    pub fn build<S>(program: S) -> TestNodeProcessBuilder<R>
57    where
58        S: AsRef<OsStr> + Clone,
59    {
60        TestNodeProcessBuilder::new(program)
61    }
62
63    /// Construct a builder for spawning a test node process, using the environment
64    /// variable `CONTRACTS_NODE`, otherwise using the default contracts node.
65    pub fn build_with_env_or_default() -> TestNodeProcessBuilder<R> {
66        const DEFAULT_CONTRACTS_NODE: &str = "ink-node";
67
68        // Use the user supplied `CONTRACTS_NODE` or default to `DEFAULT_CONTRACTS_NODE`.
69        let contracts_node =
70            std::env::var("CONTRACTS_NODE").unwrap_or(DEFAULT_CONTRACTS_NODE.to_owned());
71
72        // Check the specified contracts node.
73        if which::which(&contracts_node).is_err() {
74            if contracts_node == DEFAULT_CONTRACTS_NODE {
75                panic!(
76                    "The '{DEFAULT_CONTRACTS_NODE}' executable was not found. Install '{DEFAULT_CONTRACTS_NODE}' on the PATH, \
77                    or specify the `CONTRACTS_NODE` environment variable.",
78                )
79            } else {
80                panic!("The contracts node executable '{contracts_node}' was not found.")
81            }
82        }
83        Self::build(contracts_node)
84    }
85
86    /// Attempt to kill the running substrate process.
87    pub fn kill(&mut self) -> Result<(), String> {
88        tracing::info!("Killing node process {}", self.proc.id());
89        if let Err(err) = self.proc.kill() {
90            let err = format!("Error killing node process {}: {}", self.proc.id(), err);
91            tracing::error!("{}", err);
92            return Err(err)
93        }
94        Ok(())
95    }
96
97    /// Returns the `subxt` RPC client connected to the running node.
98    pub fn rpc(&self) -> RpcClient {
99        self.rpc.clone()
100    }
101
102    /// Returns the `subxt` client connected to the running node.
103    pub fn client(&self) -> OnlineClient<R> {
104        self.client.clone()
105    }
106
107    /// Returns the URL of the running node.
108    pub fn url(&self) -> &str {
109        &self.url
110    }
111}
112
113/// Construct a test node process.
114pub struct TestNodeProcessBuilder<R> {
115    node_path: OsString,
116    authority: Option<Sr25519Keyring>,
117    marker: std::marker::PhantomData<R>,
118}
119
120impl<R> TestNodeProcessBuilder<R>
121where
122    R: Config,
123{
124    pub fn new<P>(node_path: P) -> TestNodeProcessBuilder<R>
125    where
126        P: AsRef<OsStr>,
127    {
128        Self {
129            node_path: node_path.as_ref().into(),
130            authority: None,
131            marker: Default::default(),
132        }
133    }
134
135    /// Set the authority development account for a node in validator mode e.g. --alice.
136    pub fn with_authority(&mut self, account: Sr25519Keyring) -> &mut Self {
137        self.authority = Some(account);
138        self
139    }
140
141    /// Spawn the substrate node at the given path, and wait for RPC to be initialized.
142    pub async fn spawn(&self) -> Result<TestNodeProcess<R>, String> {
143        let mut cmd = process::Command::new(&self.node_path);
144        cmd.env("RUST_LOG", "info")
145            .arg("--dev")
146            .stdout(process::Stdio::piped())
147            .stderr(process::Stdio::piped())
148            .arg("--port=0")
149            .arg("--rpc-port=0")
150            .arg("-lruntime::revive=debug");
151
152        if let Some(authority) = self.authority {
153            let authority = format!("{authority:?}");
154            let arg = format!("--{}", authority.as_str().to_lowercase());
155            cmd.arg(arg);
156        }
157
158        let mut proc = cmd.spawn().map_err(|e| {
159            format!(
160                "Error spawning substrate node '{}': {}",
161                self.node_path.to_string_lossy(),
162                e
163            )
164        })?;
165
166        // Wait for RPC port to be logged (it's logged to stderr):
167        let stderr = proc.stderr.take().unwrap();
168        let port = find_substrate_port_from_output(stderr);
169        let url = format!("ws://127.0.0.1:{port}");
170
171        // Connect to the node with a `subxt` client:
172        let rpc = RpcClient::from_url(url.clone())
173            .await
174            .map_err(|err| format!("Error initializing rpc client: {}", err))?;
175        let client = OnlineClient::from_url(url.clone()).await;
176        match client {
177            Ok(client) => {
178                Ok(TestNodeProcess {
179                    proc,
180                    rpc,
181                    client,
182                    url: url.clone(),
183                })
184            }
185            Err(err) => {
186                let err = format!("Failed to connect to node rpc at {url}: {err}");
187                tracing::error!("{}", err);
188                proc.kill().map_err(|e| {
189                    format!("Error killing substrate process '{}': {}", proc.id(), e)
190                })?;
191                Err(err)
192            }
193        }
194    }
195}
196
197// Consume a stderr reader from a spawned substrate command and
198// locate the port number that is logged out to it.
199fn find_substrate_port_from_output(r: impl Read + Send + 'static) -> u16 {
200    BufReader::new(r)
201        .lines()
202        .find_map(|line| {
203            let line =
204                line.expect("failed to obtain next line from stdout for port discovery");
205
206            // does the line contain our port (we expect this specific output from
207            // substrate).
208            let line_end = line
209                .rsplit_once("Listening for new connections on 127.0.0.1:")
210                .or_else(|| {
211                    line.rsplit_once("Running JSON-RPC WS server: addr=127.0.0.1:")
212                })
213                .or_else(|| line.rsplit_once("Running JSON-RPC server: addr=127.0.0.1:"))
214                .map(|(_, port_str)| port_str)?;
215
216            // match the first group of digits
217            let re = regex::Regex::new(r"^\d+").expect("regex creation failed");
218            let port_capture = re
219                .captures(line_end)
220                .unwrap_or_else(|| panic!("unable to extract port from '{}'", line_end));
221            assert!(
222                port_capture.len() == 1,
223                "captured more than one port from '{}'",
224                line_end
225            );
226            let port_str = &port_capture[0];
227
228            // expect to have a number here (the chars after '127.0.0.1:') and parse them
229            // into a u16.
230            let port_num = port_str.parse().unwrap_or_else(|_| {
231                panic!("valid port expected for tracing line, got '{port_str}'")
232            });
233
234            Some(port_num)
235        })
236        .expect("We should find a port before the reader ends")
237}
238
239#[cfg(test)]
240mod tests {
241    use super::*;
242    use subxt::{
243        backend::legacy::LegacyRpcMethods,
244        PolkadotConfig as SubxtConfig,
245    };
246
247    #[tokio::test]
248    #[allow(unused_assignments)]
249    async fn spawning_and_killing_nodes_works() {
250        let mut client1: Option<LegacyRpcMethods<SubxtConfig>> = None;
251        let mut client2: Option<LegacyRpcMethods<SubxtConfig>> = None;
252
253        {
254            let node_proc1 = TestNodeProcess::<SubxtConfig>::build("ink-node")
255                .spawn()
256                .await
257                .unwrap();
258            client1 = Some(LegacyRpcMethods::new(node_proc1.rpc()));
259
260            let node_proc2 = TestNodeProcess::<SubxtConfig>::build("ink-node")
261                .spawn()
262                .await
263                .unwrap();
264            client2 = Some(LegacyRpcMethods::new(node_proc2.rpc()));
265
266            let res1 = client1.clone().unwrap().chain_get_block_hash(None).await;
267            let res2 = client2.clone().unwrap().chain_get_block_hash(None).await;
268
269            assert!(res1.is_ok());
270            assert!(res2.is_ok());
271        }
272
273        // node processes should have been killed by `Drop` in the above block.
274        let res1 = client1.unwrap().chain_get_block_hash(None).await;
275        let res2 = client2.unwrap().chain_get_block_hash(None).await;
276
277        assert!(res1.is_err());
278        assert!(res2.is_err());
279    }
280
281    #[test]
282    fn parse_port_from_node_output() {
283        let log = "2024-12-04 10:57:03.893  INFO main sc_rpc_server: Running JSON-RPC server: addr=127.0.0.1:9944,[::1]:9944  ";
284        let port = find_substrate_port_from_output(log.as_bytes());
285        assert_eq!(port, 9944);
286
287        let log = "2024-12-04 10:57:03.893  INFO main sc_rpc_server: Running JSON-RPC server: addr=127.0.0.1:9944  ";
288        let port = find_substrate_port_from_output(log.as_bytes());
289        assert_eq!(port, 9944);
290
291        let log = r#"2024-12-04 11:02:24.637  INFO main sc_cli::runner: 👤 Role: AUTHORITY
2922024-12-04 11:02:24.637  INFO main sc_cli::runner: 💾 Database: RocksDb at /var/folders/s5/5gcp8ck95k39z006fj059_0c0000gn/T/substrateHZoRbb/chains/dev/db/full
2932024-12-04 11:02:25.324  WARN main sc_service::config: Using default protocol ID "sup" because none is configured in the chain specs
2942024-12-04 11:02:25.327  INFO main sc_rpc_server: Running JSON-RPC server: addr=127.0.0.1:9944,[::1]:9944
2952024-12-04 11:02:24.637  INFO main sc_cli::runner: 💾 Database: RocksDb at /var/folders/s5/5gcp8ck95k39z006fj059_0c0000gn/T/substrateHZoRbb/chains/dev/db/full
2962024-12-04 11:02:24.637  INFO main sc_cli::runner: 💾 Database: RocksDb at /var/folders/s5/5gcp8ck95k39z006fj059_0c0000gn/T/substrateHZoRbb/chains/dev/db/full
297"#;
298        let port = find_substrate_port_from_output(log.as_bytes());
299        assert_eq!(port, 9944);
300    }
301}