pop_common/
test_env.rs

1// SPDX-License-Identifier: GPL-3.0
2
3use crate::{
4	find_free_port,
5	polkadot_sdk::sort_by_latest_semantic_version,
6	set_executable_permission,
7	sourcing::{ArchiveFileSpec, Binary, GitHub::ReleaseArchive, Source::GitHub},
8	Error,
9};
10
11use serde_json::json;
12use std::{
13	env::consts::{ARCH, OS},
14	process::{Child, Command, Stdio},
15	time::Duration,
16};
17use tokio::time::sleep;
18
19/// Represents a temporary test node process, running locally for testing.
20pub struct TestNode {
21	child: Child,
22	ws_url: String,
23	// Needed to be kept alive to avoid deleting the temporaory directory.
24	_temp_dir: tempfile::TempDir,
25}
26
27impl Drop for TestNode {
28	fn drop(&mut self) {
29		let _ = self.child.kill();
30	}
31}
32
33impl TestNode {
34	async fn wait_for_node_availability(host: &str, port: u16) -> anyhow::Result<()> {
35		let mut attempts = 0;
36		let url = format!("http://{host}:{port}");
37		let client = reqwest::Client::new();
38		let payload = json!({
39			"jsonrpc": "2.0",
40			"id": 1,
41			"method": "system_health",
42			"params": []
43		});
44
45		loop {
46			sleep(Duration::from_secs(2)).await;
47			match client.post(&url).json(&payload).send().await {
48				Ok(resp) => {
49					let text = resp.text().await?;
50					if !text.is_empty() {
51						return Ok(());
52					}
53				},
54				Err(_) => {
55					attempts += 1;
56					if attempts > 10 {
57						return Err(anyhow::anyhow!("Node could not be started"));
58					}
59				},
60			}
61		}
62	}
63
64	/// Spawns a local ink! node and waits until it's ready.
65	pub async fn spawn() -> anyhow::Result<Self> {
66		let temp_dir = tempfile::tempdir()?;
67		let random_port = find_free_port(None);
68		let cache = temp_dir.path().to_path_buf();
69
70		let binary = Binary::Source {
71			name: "ink-node".to_string(),
72			source: GitHub(ReleaseArchive {
73				owner: "use-ink".into(),
74				repository: "ink-node".into(),
75				tag: None,
76				tag_pattern: Some("v{version}".into()),
77				prerelease: false,
78				version_comparator: sort_by_latest_semantic_version,
79				fallback: "v0.43.0".to_string(),
80				archive: archive_name_by_target()?,
81				contents: release_directory_by_target("ink-node")?,
82				latest: None,
83			})
84			.into(),
85			cache: cache.to_path_buf(),
86		};
87		binary.source(false, &(), true).await?;
88		set_executable_permission(binary.path())?;
89
90		let mut command = Command::new(binary.path());
91		command.arg("--dev");
92		command.arg(format!("--rpc-port={}", random_port));
93		command.stderr(Stdio::null());
94		command.stdout(Stdio::null());
95
96		let child = command.spawn()?;
97		let host = "127.0.0.1";
98
99		// Wait until the node is ready
100		Self::wait_for_node_availability(host, random_port).await?;
101
102		let ws_url = format!("ws://{host}:{random_port}");
103
104		Ok(Self { child, ws_url, _temp_dir: temp_dir })
105	}
106
107	/// Returns the WebSocket URL of the running test node.
108	pub fn ws_url(&self) -> &str {
109		&self.ws_url
110	}
111}
112
113fn archive_name_by_target() -> Result<String, Error> {
114	match OS {
115		"macos" => Ok("ink-node-mac-universal.tar.gz".to_string()),
116		"linux" => Ok("ink-node-linux.tar.gz".to_string()),
117		_ => Err(Error::UnsupportedPlatform { arch: ARCH, os: OS }),
118	}
119}
120
121fn release_directory_by_target(binary: &str) -> Result<Vec<ArchiveFileSpec>, Error> {
122	match OS {
123		"macos" => Ok("ink-node-mac/ink-node"),
124		"linux" => Ok("ink-node-linux/ink-node"),
125		_ => Err(Error::UnsupportedPlatform { arch: ARCH, os: OS }),
126	}
127	.map(|name| vec![ArchiveFileSpec::new(name.into(), Some(binary.into()), true)])
128}