referrerpolicy=no-referrer-when-downgrade

substrate_cli_test_utils/
lib.rs

1// This file is part of Substrate.
2
3// Copyright (C) Parity Technologies (UK) Ltd.
4// SPDX-License-Identifier: Apache-2.0
5
6// Licensed under the Apache License, Version 2.0 (the "License");
7// you may not use this file except in compliance with the License.
8// You may obtain a copy of the License at
9//
10// 	http://www.apache.org/licenses/LICENSE-2.0
11//
12// Unless required by applicable law or agreed to in writing, software
13// distributed under the License is distributed on an "AS IS" BASIS,
14// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15// See the License for the specific language governing permissions and
16// limitations under the License.
17
18#![cfg(unix)]
19
20use assert_cmd::cargo::cargo_bin;
21use nix::{
22	sys::signal::{kill, Signal, Signal::SIGINT},
23	unistd::Pid,
24};
25use node_primitives::{Hash, Header};
26use regex::Regex;
27use sp_rpc::{list::ListOrValue, number::NumberOrHex};
28use std::{
29	io::{BufRead, BufReader, Read},
30	ops::{Deref, DerefMut},
31	path::{Path, PathBuf},
32	process::{self, Child, Command},
33	time::Duration,
34};
35use tokio::io::{AsyncBufReadExt, AsyncRead};
36
37/// Similar to [`crate::start_node`] spawns a node, but works in environments where the substrate
38/// binary is not accessible with `cargo_bin("substrate-node")`, and allows customising the args
39/// passed in.
40///
41/// Helpful if you need a Substrate dev node running in the background of a project external to
42/// `substrate`.
43///
44/// The downside compared to using [`crate::start_node`] is that this method is blocking rather than
45/// returning a [`Child`]. Therefore, you may want to call this method inside a new thread.
46///
47/// # Example
48/// ```ignore
49/// // Spawn a dev node.
50/// let _ = std::thread::spawn(move || {
51///     match common::start_node_inline(vec!["--dev", "--rpc-port=12345"]) {
52///         Ok(_) => {}
53///         Err(e) => {
54///             panic!("Node exited with error: {}", e);
55///         }
56///     }
57/// });
58/// ```
59pub fn start_node_inline(args: Vec<&str>) -> Result<(), sc_service::error::Error> {
60	use sc_cli::SubstrateCli;
61
62	// Prepend the args with some dummy value because the first arg is skipped.
63	let cli_call = std::iter::once("node-template").chain(args);
64	let cli = node_cli::Cli::from_iter(cli_call);
65	let runner = cli.create_runner(&cli.run).unwrap();
66	runner.run_node_until_exit(|config| async move { node_cli::service::new_full(config, cli) })
67}
68
69/// Starts a new Substrate node in development mode with a temporary chain.
70///
71/// This function creates a new Substrate node using the `substrate` binary.
72/// It configures the node to run in development mode (`--dev`) with a temporary chain (`--tmp`),
73/// sets the WebSocket port to 45789 (`--ws-port=45789`).
74///
75/// # Returns
76///
77/// A [`Child`] process representing the spawned Substrate node.
78///
79/// # Panics
80///
81/// This function will panic if the `substrate` binary is not found or if the node fails to start.
82///
83/// # Examples
84///
85/// ```ignore
86/// use my_crate::start_node;
87///
88/// let child = start_node();
89/// // Interact with the Substrate node using the WebSocket port 45789.
90/// // When done, the node will be killed when the `child` is dropped.
91/// ```
92///
93/// [`Child`]: std::process::Child
94pub fn start_node() -> Child {
95	Command::new(cargo_bin("substrate-node"))
96		.stdout(process::Stdio::piped())
97		.stderr(process::Stdio::piped())
98		.args(&["--dev", "--tmp", "--rpc-port=45789", "--no-hardware-benchmarks"])
99		.spawn()
100		.unwrap()
101}
102
103/// Builds the Substrate project using the provided arguments.
104///
105/// This function reads the CARGO_MANIFEST_DIR environment variable to find the root workspace
106/// directory. It then runs the `cargo b` command in the root directory with the specified
107/// arguments.
108///
109/// This can be useful for building the Substrate binary with a desired set of features prior
110/// to using the binary in a CLI test.
111///
112/// # Arguments
113///
114/// * `args: &[&str]` - A slice of string references representing the arguments to pass to the
115///   `cargo b` command.
116///
117/// # Panics
118///
119/// This function will panic if:
120///
121/// * The CARGO_MANIFEST_DIR environment variable is not set.
122/// * The root workspace directory cannot be determined.
123/// * The 'cargo b' command fails to execute.
124/// * The 'cargo b' command returns a non-successful status.
125///
126/// # Examples
127///
128/// ```ignore
129/// build_substrate(&["--features=try-runtime"]);
130/// ```
131pub fn build_substrate(args: &[&str]) {
132	let is_release_build = !cfg!(build_profile = "debug");
133
134	// Get the root workspace directory from the CARGO_MANIFEST_DIR environment variable
135	let mut cmd = Command::new("cargo");
136
137	cmd.arg("build").arg("-p=staging-node-cli");
138
139	if is_release_build {
140		cmd.arg("--release");
141	}
142
143	let output = cmd
144		.args(args)
145		.output()
146		.expect(format!("Failed to execute 'cargo b' with args {:?}'", args).as_str());
147
148	if !output.status.success() {
149		panic!(
150			"Failed to execute 'cargo b' with args {:?}': \n{}",
151			args,
152			String::from_utf8_lossy(&output.stderr)
153		);
154	}
155}
156
157/// Takes a readable tokio stream (e.g. from a child process `ChildStderr` or `ChildStdout`) and
158/// a `Regex` pattern, and checks each line against the given pattern as it is produced.
159/// The function returns OK(()) as soon as a line matching the pattern is found, or an Err if
160/// the stream ends without any lines matching the pattern.
161///
162/// # Arguments
163///
164/// * `child_stream` - An async tokio stream, e.g. from a child process `ChildStderr` or
165///   `ChildStdout`.
166/// * `re` - A `Regex` pattern to search for in the stream.
167///
168/// # Returns
169///
170/// * `Ok(())` if a line matching the pattern is found.
171/// * `Err(String)` if the stream ends without any lines matching the pattern.
172///
173/// # Examples
174///
175/// ```ignore
176/// use regex::Regex;
177/// use tokio::process::Command;
178/// use tokio::io::AsyncRead;
179///
180/// # async fn run() {
181/// let child = Command::new("some-command").stderr(std::process::Stdio::piped()).spawn().unwrap();
182/// let stderr = child.stderr.unwrap();
183/// let re = Regex::new("error:").unwrap();
184///
185/// match wait_for_pattern_match_in_stream(stderr, re).await {
186///     Ok(()) => println!("Error found in stderr"),
187///     Err(e) => println!("Error: {}", e),
188/// }
189/// # }
190/// ```
191pub async fn wait_for_stream_pattern_match<R>(stream: R, re: Regex) -> Result<(), String>
192where
193	R: AsyncRead + Unpin,
194{
195	let mut stdio_reader = tokio::io::BufReader::new(stream).lines();
196	while let Ok(Some(line)) = stdio_reader.next_line().await {
197		match re.find(line.as_str()) {
198			Some(_) => return Ok(()),
199			None => (),
200		}
201	}
202	Err(String::from("Stream closed without any lines matching the regex."))
203}
204
205/// Run the given `future` and panic if the `timeout` is hit.
206pub async fn run_with_timeout(timeout: Duration, future: impl futures::Future<Output = ()>) {
207	tokio::time::timeout(timeout, future).await.expect("Hit timeout");
208}
209
210/// Wait for at least n blocks to be finalized from a specified node
211pub async fn wait_n_finalized_blocks(n: usize, url: &str) {
212	use substrate_rpc_client::{ws_client, ChainApi};
213
214	let mut built_blocks = std::collections::HashSet::new();
215	let block_duration = Duration::from_secs(2);
216	let mut interval = tokio::time::interval(block_duration);
217	let rpc = ws_client(url).await.unwrap();
218
219	loop {
220		if let Ok(block) = ChainApi::<(), Hash, Header, ()>::finalized_head(&rpc).await {
221			built_blocks.insert(block);
222			if built_blocks.len() > n {
223				break
224			}
225		};
226		interval.tick().await;
227	}
228}
229
230/// Run the node for a while (3 blocks)
231pub async fn run_node_for_a_while(base_path: &Path, args: &[&str]) {
232	run_with_timeout(Duration::from_secs(60 * 10), async move {
233		let mut cmd = Command::new(cargo_bin("substrate-node"))
234			.stdout(process::Stdio::piped())
235			.stderr(process::Stdio::piped())
236			.args(args)
237			.arg("-d")
238			.arg(base_path)
239			.spawn()
240			.unwrap();
241
242		let stderr = cmd.stderr.take().unwrap();
243
244		let mut child = KillChildOnDrop(cmd);
245
246		let ws_url = extract_info_from_output(stderr).0.ws_url;
247
248		// Let it produce some blocks.
249		wait_n_finalized_blocks(3, &ws_url).await;
250
251		child.assert_still_running();
252
253		// Stop the process
254		child.stop();
255	})
256	.await
257}
258
259pub async fn block_hash(block_number: u64, url: &str) -> Result<Hash, String> {
260	use substrate_rpc_client::{ws_client, ChainApi};
261
262	let rpc = ws_client(url).await.unwrap();
263
264	let result = ChainApi::<(), Hash, Header, ()>::block_hash(
265		&rpc,
266		Some(ListOrValue::Value(NumberOrHex::Number(block_number))),
267	)
268	.await
269	.map_err(|_| "Couldn't get block hash".to_string())?;
270
271	match result {
272		ListOrValue::Value(maybe_block_hash) if maybe_block_hash.is_some() =>
273			Ok(maybe_block_hash.unwrap()),
274		_ => Err("Couldn't get block hash".to_string()),
275	}
276}
277
278pub struct KillChildOnDrop(pub Child);
279
280impl KillChildOnDrop {
281	/// Stop the child and wait until it is finished.
282	///
283	/// Asserts if the exit status isn't success.
284	pub fn stop(&mut self) {
285		self.stop_with_signal(SIGINT);
286	}
287
288	/// Same as [`Self::stop`] but takes the `signal` that is sent to stop the child.
289	pub fn stop_with_signal(&mut self, signal: Signal) {
290		kill(Pid::from_raw(self.id().try_into().unwrap()), signal).unwrap();
291		assert!(self.wait().unwrap().success());
292	}
293
294	/// Asserts that the child is still running.
295	pub fn assert_still_running(&mut self) {
296		assert!(self.try_wait().unwrap().is_none(), "the process should still be running");
297	}
298}
299
300impl Drop for KillChildOnDrop {
301	fn drop(&mut self) {
302		let _ = self.0.kill();
303	}
304}
305
306impl Deref for KillChildOnDrop {
307	type Target = Child;
308
309	fn deref(&self) -> &Self::Target {
310		&self.0
311	}
312}
313
314impl DerefMut for KillChildOnDrop {
315	fn deref_mut(&mut self) -> &mut Self::Target {
316		&mut self.0
317	}
318}
319
320/// Information extracted from a running node.
321pub struct NodeInfo {
322	pub ws_url: String,
323	pub db_path: PathBuf,
324}
325
326/// Extract [`NodeInfo`] from a running node by parsing its output.
327///
328/// Returns the [`NodeInfo`] and all the read data.
329pub fn extract_info_from_output(read: impl Read + Send) -> (NodeInfo, String) {
330	let mut data = String::new();
331
332	let ws_url = BufReader::new(read)
333		.lines()
334		.find_map(|line| {
335			let line = line.expect("failed to obtain next line while extracting node info");
336			data.push_str(&line);
337			data.push_str("\n");
338
339			// does the line contain our port (we expect this specific output from substrate).
340			let sock_addr = match line.split_once("Running JSON-RPC server: addr=") {
341				None => return None,
342				Some((_, after)) => after.split_once(",").unwrap().0,
343			};
344
345			Some(format!("ws://{}", sock_addr))
346		})
347		.unwrap_or_else(|| {
348			eprintln!("Observed node output:\n{}", data);
349			panic!("We should get a WebSocket address")
350		});
351
352	// Database path is printed before the ws url!
353	let re = Regex::new(r"Database: .+ at (\S+)").unwrap();
354	let db_path = PathBuf::from(re.captures(data.as_str()).unwrap().get(1).unwrap().as_str());
355
356	(NodeInfo { ws_url, db_path }, data)
357}