1use 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
19pub struct TestNode {
21 child: Child,
22 ws_url: String,
23 _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 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 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 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}