use crate::{
Error,
polkadot_sdk::{sort_by_latest_semantic_version, sort_by_latest_stable_version},
resolve_port, set_executable_permission,
sourcing::{ArchiveFileSpec, Binary, GitHub::ReleaseArchive, Source::GitHub},
target,
};
use serde_json::json;
use std::{
env::consts::{ARCH, OS},
process::{Child, Command, Stdio},
time::Duration,
};
use tokio::{sync::OnceCell, time::sleep};
struct NodeProcess {
child: Child,
ws_url: String,
_temp_dir: tempfile::TempDir,
_cache_dir: Option<tempfile::TempDir>,
}
impl Drop for NodeProcess {
fn drop(&mut self) {
let _ = self.child.kill();
}
}
impl NodeProcess {
async fn wait_for_availability(host: &str, port: u16) -> anyhow::Result<()> {
let mut attempts = 0;
let url = format!("http://{host}:{port}");
let client = reqwest::Client::new();
let payload = json!({
"jsonrpc": "2.0",
"id": 1,
"method": "system_health",
"params": []
});
loop {
sleep(Duration::from_secs(2)).await;
match client.post(&url).json(&payload).send().await {
Ok(resp) => {
let text = resp.text().await?;
if !text.is_empty() {
return Ok(());
}
},
Err(_) => {
attempts += 1;
if attempts > 10 {
return Err(anyhow::anyhow!("Node could not be started"));
}
},
}
}
}
async fn spawn(binary: Binary, cache_dir: Option<tempfile::TempDir>) -> anyhow::Result<Self> {
let temp_dir = tempfile::tempdir()?;
let random_port = resolve_port(None);
binary.source(false, &(), true).await?;
set_executable_permission(binary.path())?;
let mut command = Command::new(binary.path());
command.arg("--dev");
command.arg(format!("--rpc-port={}", random_port));
command.stderr(Stdio::null());
command.stdout(Stdio::null());
let child = command.spawn()?;
let host = "127.0.0.1";
Self::wait_for_availability(host, random_port).await?;
let ws_url = format!("ws://{host}:{random_port}");
Ok(Self { child, ws_url, _temp_dir: temp_dir, _cache_dir: cache_dir })
}
fn ws_url(&self) -> &str {
&self.ws_url
}
}
pub struct InkTestNode(NodeProcess);
impl InkTestNode {
pub async fn spawn() -> anyhow::Result<Self> {
let temp_dir = tempfile::tempdir()?;
let cache = temp_dir.path().to_path_buf();
let binary = Binary::Source {
name: "ink-node".to_string(),
source: GitHub(ReleaseArchive {
owner: "use-ink".into(),
repository: "ink-node".into(),
tag: None,
tag_pattern: Some("v{version}".into()),
prerelease: false,
version_comparator: sort_by_latest_semantic_version,
fallback: "v0.47.0".to_string(),
archive: ink_node_archive()?,
contents: ink_node_contents()?,
latest: None,
})
.into(),
cache: cache.to_path_buf(),
};
NodeProcess::spawn(binary, Some(temp_dir)).await.map(Self)
}
pub fn ws_url(&self) -> &str {
self.0.ws_url()
}
}
pub struct SubstrateTestNode(NodeProcess);
impl SubstrateTestNode {
pub async fn spawn() -> anyhow::Result<Self> {
let temp_dir = tempfile::tempdir()?;
let cache = temp_dir.path().to_path_buf();
let binary = Binary::Source {
name: "substrate-node".to_string(),
source: GitHub(ReleaseArchive {
owner: "r0gue-io".into(),
repository: "polkadot".into(),
tag: Some("polkadot-stable2512-1".into()),
tag_pattern: Some("polkadot-{version}".into()),
prerelease: false,
version_comparator: sort_by_latest_stable_version,
fallback: "stable2512-1".to_string(),
archive: format!("substrate-node-{}.tar.gz", target()?),
contents: vec![ArchiveFileSpec::new("substrate-node".into(), None, true)],
latest: None,
})
.into(),
cache: cache.to_path_buf(),
};
NodeProcess::spawn(binary, Some(temp_dir)).await.map(Self)
}
pub fn ws_url(&self) -> &str {
self.0.ws_url()
}
}
fn ink_node_archive() -> Result<String, Error> {
match OS {
"macos" => Ok("ink-node-mac-universal.tar.gz".to_string()),
"linux" => Ok("ink-node-linux.tar.gz".to_string()),
_ => Err(Error::UnsupportedPlatform { arch: ARCH, os: OS }),
}
}
fn ink_node_contents() -> Result<Vec<ArchiveFileSpec>, Error> {
match OS {
"macos" => Ok("ink-node-mac/ink-node"),
"linux" => Ok("ink-node-linux/ink-node"),
_ => Err(Error::UnsupportedPlatform { arch: ARCH, os: OS }),
}
.map(|name| vec![ArchiveFileSpec::new(name.into(), Some("ink-node".into()), true)])
}
static SHARED_INK_NODE_WS_URL: OnceCell<String> = OnceCell::const_new();
static SHARED_SUBSTRATE_NODE_WS_URL: OnceCell<String> = OnceCell::const_new();
pub async fn shared_ink_ws_url() -> String {
SHARED_INK_NODE_WS_URL
.get_or_init(|| async {
if let Ok(url) = std::env::var("POP_TEST_INK_NODE_WS_URL") {
return url;
}
let node = InkTestNode::spawn().await.expect("Failed to spawn shared InkTestNode");
let ws_url = node.ws_url().to_string();
let _ = Box::leak(Box::new(node));
ws_url
})
.await
.clone()
}
pub async fn shared_substrate_ws_url() -> String {
SHARED_SUBSTRATE_NODE_WS_URL
.get_or_init(|| async {
if let Ok(url) = std::env::var("POP_TEST_SUBSTRATE_NODE_WS_URL") {
return url;
}
let node = SubstrateTestNode::spawn()
.await
.expect("Failed to spawn shared SubstrateTestNode");
let ws_url = node.ws_url().to_string();
let _ = Box::leak(Box::new(node));
ws_url
})
.await
.clone()
}