1use crate::{
4 Error,
5 polkadot_sdk::{sort_by_latest_semantic_version, sort_by_latest_stable_version},
6 resolve_port, set_executable_permission,
7 sourcing::{ArchiveFileSpec, Binary, GitHub::ReleaseArchive, Source::GitHub},
8 target,
9};
10
11use serde_json::json;
12use std::{
13 env::consts::{ARCH, OS},
14 process::{Child, Command, Stdio},
15 time::Duration,
16};
17use tokio::{sync::OnceCell, time::sleep};
18
19struct NodeProcess {
21 child: Child,
22 ws_url: String,
23 _temp_dir: tempfile::TempDir,
24 _cache_dir: Option<tempfile::TempDir>,
25}
26
27impl Drop for NodeProcess {
28 fn drop(&mut self) {
29 let _ = self.child.kill();
30 }
31}
32
33impl NodeProcess {
34 async fn wait_for_availability(host: &str, port: u16) -> anyhow::Result<()> {
36 let mut attempts = 0;
37 let url = format!("http://{host}:{port}");
38 let client = reqwest::Client::new();
39 let payload = json!({
40 "jsonrpc": "2.0",
41 "id": 1,
42 "method": "system_health",
43 "params": []
44 });
45
46 loop {
47 sleep(Duration::from_secs(2)).await;
48 match client.post(&url).json(&payload).send().await {
49 Ok(resp) => {
50 let text = resp.text().await?;
51 if !text.is_empty() {
52 return Ok(());
53 }
54 },
55 Err(_) => {
56 attempts += 1;
57 if attempts > 10 {
58 return Err(anyhow::anyhow!("Node could not be started"));
59 }
60 },
61 }
62 }
63 }
64
65 async fn spawn(binary: Binary, cache_dir: Option<tempfile::TempDir>) -> anyhow::Result<Self> {
70 let temp_dir = tempfile::tempdir()?;
71 let random_port = resolve_port(None);
72
73 binary.source(false, &(), true).await?;
74 set_executable_permission(binary.path())?;
75
76 let mut command = Command::new(binary.path());
77 command.arg("--dev");
78 command.arg(format!("--rpc-port={}", random_port));
79 command.stderr(Stdio::null());
80 command.stdout(Stdio::null());
81
82 let child = command.spawn()?;
83 let host = "127.0.0.1";
84
85 Self::wait_for_availability(host, random_port).await?;
86
87 let ws_url = format!("ws://{host}:{random_port}");
88
89 Ok(Self { child, ws_url, _temp_dir: temp_dir, _cache_dir: cache_dir })
90 }
91
92 fn ws_url(&self) -> &str {
93 &self.ws_url
94 }
95}
96
97pub struct InkTestNode(NodeProcess);
102
103impl InkTestNode {
104 pub async fn spawn() -> anyhow::Result<Self> {
106 let temp_dir = tempfile::tempdir()?;
107 let cache = temp_dir.path().to_path_buf();
108
109 let binary = Binary::Source {
110 name: "ink-node".to_string(),
111 source: GitHub(ReleaseArchive {
112 owner: "use-ink".into(),
113 repository: "ink-node".into(),
114 tag: None,
115 tag_pattern: Some("v{version}".into()),
116 prerelease: false,
117 version_comparator: sort_by_latest_semantic_version,
118 fallback: "v0.47.0".to_string(),
119 archive: ink_node_archive()?,
120 contents: ink_node_contents()?,
121 latest: None,
122 })
123 .into(),
124 cache: cache.to_path_buf(),
125 };
126
127 NodeProcess::spawn(binary, Some(temp_dir)).await.map(Self)
128 }
129
130 pub fn ws_url(&self) -> &str {
132 self.0.ws_url()
133 }
134}
135
136pub struct SubstrateTestNode(NodeProcess);
146
147impl SubstrateTestNode {
148 pub async fn spawn() -> anyhow::Result<Self> {
153 let temp_dir = tempfile::tempdir()?;
154 let cache = temp_dir.path().to_path_buf();
155
156 let binary = Binary::Source {
157 name: "substrate-node".to_string(),
158 source: GitHub(ReleaseArchive {
159 owner: "r0gue-io".into(),
160 repository: "polkadot".into(),
161 tag: Some("polkadot-stable2512-1".into()),
162 tag_pattern: Some("polkadot-{version}".into()),
163 prerelease: false,
164 version_comparator: sort_by_latest_stable_version,
165 fallback: "stable2512-1".to_string(),
166 archive: format!("substrate-node-{}.tar.gz", target()?),
167 contents: vec![ArchiveFileSpec::new("substrate-node".into(), None, true)],
168 latest: None,
169 })
170 .into(),
171 cache: cache.to_path_buf(),
172 };
173
174 NodeProcess::spawn(binary, Some(temp_dir)).await.map(Self)
175 }
176
177 pub fn ws_url(&self) -> &str {
179 self.0.ws_url()
180 }
181}
182
183fn ink_node_archive() -> Result<String, Error> {
184 match OS {
185 "macos" => Ok("ink-node-mac-universal.tar.gz".to_string()),
186 "linux" => Ok("ink-node-linux.tar.gz".to_string()),
187 _ => Err(Error::UnsupportedPlatform { arch: ARCH, os: OS }),
188 }
189}
190
191fn ink_node_contents() -> Result<Vec<ArchiveFileSpec>, Error> {
192 match OS {
193 "macos" => Ok("ink-node-mac/ink-node"),
194 "linux" => Ok("ink-node-linux/ink-node"),
195 _ => Err(Error::UnsupportedPlatform { arch: ARCH, os: OS }),
196 }
197 .map(|name| vec![ArchiveFileSpec::new(name.into(), Some("ink-node".into()), true)])
198}
199
200static SHARED_INK_NODE_WS_URL: OnceCell<String> = OnceCell::const_new();
201static SHARED_SUBSTRATE_NODE_WS_URL: OnceCell<String> = OnceCell::const_new();
202
203pub async fn shared_ink_ws_url() -> String {
208 SHARED_INK_NODE_WS_URL
209 .get_or_init(|| async {
210 if let Ok(url) = std::env::var("POP_TEST_INK_NODE_WS_URL") {
211 return url;
212 }
213 let node = InkTestNode::spawn().await.expect("Failed to spawn shared InkTestNode");
214 let ws_url = node.ws_url().to_string();
215 let _ = Box::leak(Box::new(node));
216 ws_url
217 })
218 .await
219 .clone()
220}
221
222pub async fn shared_substrate_ws_url() -> String {
227 SHARED_SUBSTRATE_NODE_WS_URL
228 .get_or_init(|| async {
229 if let Ok(url) = std::env::var("POP_TEST_SUBSTRATE_NODE_WS_URL") {
230 return url;
231 }
232 let node = SubstrateTestNode::spawn()
233 .await
234 .expect("Failed to spawn shared SubstrateTestNode");
235 let ws_url = node.ws_url().to_string();
236 let _ = Box::leak(Box::new(node));
237 ws_url
238 })
239 .await
240 .clone()
241}