pop_common/
test_env.rs

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