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