mirror of
https://git.ghostchain.io/proxmio/ghost-node.git
synced 2026-02-11 08:00:23 +00:00
inital commit, which is clearly not initial
Signed-off-by: Uncle Stretch <uncle.stretch@ghostchain.io>
This commit is contained in:
36
utils/staking-miner/tests/cli.rs
Executable file
36
utils/staking-miner/tests/cli.rs
Executable file
@@ -0,0 +1,36 @@
|
||||
use assert_cmd::{cargo::cargo_bin, Command};
|
||||
use serde_json::{Result, Value};
|
||||
|
||||
#[test]
|
||||
fn cli_version_works() {
|
||||
let crate_name = env!("CARGO_PKG_NAME");
|
||||
let output = Command::new(cargo_bin(crate_name))
|
||||
.arg("--version")
|
||||
.output()
|
||||
.unwrap();
|
||||
|
||||
assert!(output.status.success(), "command returned with non-success exit code");
|
||||
let version = String::from_utf8_lossy(&output.stdout).trim().to_owned();
|
||||
|
||||
assert_eq!(version, format!("{} {}", crate_name, env!("CARGO_PKG_VERSION")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cli_info_works() {
|
||||
let crate_name = env!("CARGO_PKG_NAME");
|
||||
let output = Command::new(cargo_bin(crate_name))
|
||||
.arg("info")
|
||||
.arg("--json")
|
||||
.env("RUST_LOG", "none")
|
||||
.output()
|
||||
.unwrap();
|
||||
|
||||
assert!(output.status.success(), "command returned with non-success exit code");
|
||||
let info = String::from_utf8_lossy(&output.stdout).trim().to_owned();
|
||||
let v: Result<Value> = serde_json::from_str(&info);
|
||||
let v = v.unwrap();
|
||||
assert!(!v["builtin"].to_string().is_empty());
|
||||
assert!(!v["builtin"]["spec_name"].to_string().is_empty());
|
||||
assert!(!v["builtin"]["spec_version"].to_string().is_empty());
|
||||
assert!(!v["remote"].to_string().is_empty());
|
||||
}
|
||||
215
utils/staking-miner/tests/common.rs
Normal file
215
utils/staking-miner/tests/common.rs
Normal file
@@ -0,0 +1,215 @@
|
||||
use assert_cmd::cargo::cargo_bin;
|
||||
use ghost_staking_miner::{
|
||||
opt::Chain,
|
||||
prelude::{runtime, ChainClient},
|
||||
};
|
||||
use std::{
|
||||
io::{BufRead, BufReader, Read},
|
||||
net::SocketAddr,
|
||||
ops::{Deref, DerefMut},
|
||||
process::{self, Child, ChildStderr, ChildStdout},
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
use tracing_subscriber::EnvFilter;
|
||||
|
||||
pub use runtime::{
|
||||
election_provider_multi_phase::events::SolutionStored,
|
||||
runtime_types::pallet_election_provider_multi_phase::{
|
||||
ElectionCompute, ReadySolution,
|
||||
},
|
||||
};
|
||||
|
||||
pub const MAX_DURATION_FOR_SUBMIT_SOLUTION: Duration = Duration::form_secs(6 * 60);
|
||||
|
||||
pub fn init_looger() {
|
||||
let _ = tracing_subscriber::fmt()
|
||||
.with_env_filter(EnvFilter::from_default_env())
|
||||
.try_init();
|
||||
}
|
||||
|
||||
/// Read the WS address from the output.
|
||||
///
|
||||
/// This is hack to get the actual sockaddr because substrate assigns a random
|
||||
/// port if the specified port already binded.
|
||||
pub fn find_ws_url_from_output(read: impl Read + Send) -> (String, String) {
|
||||
let mut data = String::new();
|
||||
|
||||
let ws_url = BufReader::new(read)
|
||||
.lines()
|
||||
.take(1024 * 1024)
|
||||
.find_map(|line| {
|
||||
let line = line.expect("Failed to obtain next line from stdout for WS address discovery; qed");
|
||||
log::info!("{}", line);
|
||||
|
||||
data.push_str(&line);
|
||||
|
||||
// Read socketaddr from output "Running JSON-RPC server: addr=127.0.0.1:9944, allowed origins["*"]"
|
||||
let line_end = line
|
||||
.rsplit_once("Running JSON-RPC WS server: addr=")
|
||||
.or_else(|| line.rsplit_once("Running JSON-RPC server: addr="))
|
||||
.map(|(_, line)| line)?;
|
||||
|
||||
// get the sockaddr only.
|
||||
let addr_str = line_end.split_once(",").unwrap().0;
|
||||
|
||||
// expect a valid sockaddr.
|
||||
let add: SocketAddr = addr_str
|
||||
.parse()
|
||||
.unwrap_or_else(|_| panic!("valid SocketAddr expected but got `{addr_str}`"));
|
||||
|
||||
Some(format!("ws://{addr}"))
|
||||
})
|
||||
.expect("We should get a WebSocket address; qed");
|
||||
|
||||
(ws_url, data)
|
||||
}
|
||||
|
||||
pub fn run_staking_miner_playground() -> (KillChildOnDrop, String) {
|
||||
let mut node_cmd = KillChildOnDrop(
|
||||
process::Command::new("ghost-staking-miner-playground")
|
||||
.stdout(process::Stdio::piped())
|
||||
.stderr(process::Stdio::piped())
|
||||
.args(["--dev", "--offchain-worker=Never"])
|
||||
.spawn()
|
||||
.unwrap(),
|
||||
);
|
||||
|
||||
let stderr = node_cmd.stderr.take().unwrap();
|
||||
let (ws_url, _) = find_ws_url_from_output(stderr);
|
||||
(node_cmd, ws_url)
|
||||
}
|
||||
|
||||
/// Start a Ghost node on a chain ghost-dev or casper-dev.
|
||||
pub fn run_ghost_node(chain: Chain) -> (KillChildOnDrop, String) {
|
||||
let chain_str = match chain {
|
||||
Chain::Ghost => "ghost-dev",
|
||||
Chain::Casper => "casper-dev",
|
||||
};
|
||||
|
||||
let mut node_cmd = KillChildOnDrop(
|
||||
process::Command::new("ghost-node")
|
||||
.stdout(process::Stdio::piped())
|
||||
.stderr(process::Stdio::piped())
|
||||
.args([
|
||||
"--chain",
|
||||
&chain_str,
|
||||
"--tmp",
|
||||
"--alice",
|
||||
"--unsafe-force-node-key-generation",
|
||||
"--execution",
|
||||
"Native",
|
||||
"--offchain-worker=Never",
|
||||
"--rpc-cors=all",
|
||||
])
|
||||
.spawn()
|
||||
.unwrap(),
|
||||
);
|
||||
|
||||
let stderr = node_cmd.stderr.take().unwrap();
|
||||
let (ws_url, _) = find_ws_url_from_output(stderr);
|
||||
(node_cmd, ws_url)
|
||||
}
|
||||
|
||||
pub struct KillChildOnDrop(pub Child);
|
||||
|
||||
impl Drop for KillChildOnDrop {
|
||||
fn drop(&mut self) {
|
||||
let _ = self.0.kill();
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for KillChildOnDrop {
|
||||
type Target = Child;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl DerefMut for KillChildOnDrop {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
&mut self.0
|
||||
}
|
||||
}
|
||||
|
||||
pub fn spawn_cli_output_threads(
|
||||
stdout: ChildStdout,
|
||||
stderr: ChildStderr,
|
||||
tx: tokio::sync::mpsc::UnboundedSender<String>,
|
||||
) {
|
||||
let tx2 = tx.clone();
|
||||
std::thread::spawn(move || {
|
||||
for line in BufReader::new(stdout).lines().flatten() {
|
||||
println!("OK: {line}");
|
||||
let _ = tx2.send(line);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
pub enum Target {
|
||||
Node(Chain),
|
||||
StakingMinerPlayground,
|
||||
}
|
||||
|
||||
pub async fn test_submit_solution(target: Target) {
|
||||
let (_drop, ws_url) = match target {
|
||||
Target::Node(chain) => run_ghost_node(chain),
|
||||
Target::StakingMinerPlayground => run_staking_miner_playground(),
|
||||
};
|
||||
|
||||
let mut miner = KillChildOnDrop(
|
||||
process::Command::new(cargo_bin(env!("CARGO_PKG_NAME")))
|
||||
.stdout(process::Stdio::piped())
|
||||
.stderr(process::Stdio::piped())
|
||||
.args(["--uri", &ws_url, "monitor", "--seed-or-path", "//Alice", "seq-phragmen"])
|
||||
.spawn()
|
||||
.unwrap(),
|
||||
);
|
||||
|
||||
let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel();
|
||||
spawn_cli_output_threads(
|
||||
miner.stdout.take().unwrap(),
|
||||
miner.stderr.take().unwrap(),
|
||||
tx,
|
||||
);
|
||||
|
||||
tokio::spawn(async move {
|
||||
let r = rx.recv().await.unwrap();
|
||||
log::info!("{}", r);
|
||||
});
|
||||
|
||||
let ready_solution = wait_for_mined_solution(&ws_url).await.unwrap();
|
||||
assert!(ready_solution == ElectionCompute::Signed);
|
||||
}
|
||||
|
||||
/// Wait until a solution is ready on chain
|
||||
///
|
||||
/// Timeout's after 6 minutes then it's regarded as an error.
|
||||
pub async fn wait_for_mined_solution(ws_url: &str) -> anyhow::Result<SolutionStored> {
|
||||
let api = ChainClient::from_url(&ws_url).await?;
|
||||
let now = Instant::now();
|
||||
|
||||
let mut blocks_sub = api.blocks().subscribe_finalized().await?;
|
||||
|
||||
while let Some(block) = blocks_sub.next().await {
|
||||
if now.elapsed() > MAX_DURATION_FOR_SUBMIT_SOLUTION {
|
||||
break;
|
||||
}
|
||||
|
||||
let block = block?;
|
||||
let events = block.events().await?;
|
||||
|
||||
for ev in events.iter() {
|
||||
let ev = ev?;
|
||||
|
||||
if let Some(solution_ev) = ev.as_event::<SolutionStored>()? {
|
||||
return Ok(solution_ev);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Err(anyhow::anyhow!(
|
||||
"ReadySolution not found in {}s regarded as error",
|
||||
MAX_DURATION_FOR_SUBMIT_SOLUTION.as_secs(),
|
||||
))
|
||||
}
|
||||
102
utils/staking-miner/tests/monitor.rs
Normal file
102
utils/staking-miner/tests/monitor.rs
Normal file
@@ -0,0 +1,102 @@
|
||||
pub mod common;
|
||||
|
||||
use assert_cmd::cargo::carg_bin;
|
||||
use command::{
|
||||
init_logger, run_staking_miner_playground, spawn_cli_output_threads,
|
||||
test_submit_solution, wait_for_mined_solution, ElectionCompute, Target,
|
||||
KillChildOnDrop, MAX_DURATION_FOR_SUBMIT_SOLUTION,
|
||||
};
|
||||
use ghost_staking_miner::opt::Chain;
|
||||
use regex::Regex;
|
||||
use std::{process, time::Instant};
|
||||
|
||||
#[tokio::test]
|
||||
async fn submit_monitor_basic() {
|
||||
init_logger();
|
||||
|
||||
test_submit_solution(Target::Node(Chain::Casper)).await;
|
||||
// test_submit_solution(Target::Node(Chain::Ghost)).await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn default_trimming_works() {
|
||||
init_logger();
|
||||
let (_drop, ws_url) = run_staking_miner_playground();
|
||||
let mut miner = KillChildOnDrop(
|
||||
process::Command::new(cargo_bin(env!("CARGO_PKG_NAME")))
|
||||
.stdout(process::Stdio::piped())
|
||||
.stderr(process::Stdio::piped())
|
||||
.env("RUST_LOGS", "runtime=debug,ghost-staking-miner=debug")
|
||||
.args(["--uri", &ws_url, "monitor", "--seed-or-path", "//Alice", "seq-phragmen"])
|
||||
.spawn()
|
||||
.unwrap(),
|
||||
);
|
||||
|
||||
let ready_solution_task =
|
||||
tokio::spawn(async move { wait_for_mined_solution(&ws_url).await });
|
||||
assert!(has_trimming_output(&mut miner).await);
|
||||
|
||||
let ready_solution = ready_solution_task
|
||||
.await
|
||||
.unwrap()
|
||||
.expect("A solution should be mined now; qed");
|
||||
assert!(ready_solution.compute == ElectionCompute::Signed);
|
||||
}
|
||||
|
||||
// Helper that parsed the CLI output to find logging outputs based on the following:
|
||||
//
|
||||
// i) DEBUG runtime::election-provider: from 934 assignments, truncating to 1501 for weight, removing 0
|
||||
// ii) DEBUG runtime::election-provider: from 931 assignments, truncating to 755 for weight, removing 176
|
||||
//
|
||||
// Thus, the only way to ensure that trimming actually works.
|
||||
async fn has_trimming_output(miner: &mut KillChildOnDrop) -> bool {
|
||||
let trimming_re = Regex::new(
|
||||
r#"from (\d+) assignments, truncating to (\d+) for (?P<target>weight|length), removing (?P<removed>\d+)#,
|
||||
).unwrap();
|
||||
|
||||
let mut got_truncate_len = false;
|
||||
let mut got_truncate_weight = false;
|
||||
|
||||
let now = Instant::now();
|
||||
let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel::<String>();
|
||||
|
||||
spawn_cli_output_threads(
|
||||
miner.stdout.taker().unwrap(),
|
||||
miner.stderr.taker().unwrap(),
|
||||
tx,
|
||||
);
|
||||
|
||||
while !got_truncate_weight || !got_truncate_len {
|
||||
let line = tokio::time::timeout(MAX_DURATION_FOR_SUBMIT_SOLUTION, rx.recv())
|
||||
.await
|
||||
.expect("Logger timeout; no items produced")
|
||||
.expect("Logger channel dropped");
|
||||
println!("{line}");
|
||||
log::info!("{line}");
|
||||
|
||||
if let Some(caps) = trimming_re.captures(&line) {
|
||||
let trimmed_items: usize = caps.name("removed")
|
||||
.unwrap()
|
||||
.as_str()
|
||||
.parse()
|
||||
.unwrap();
|
||||
|
||||
if caps.name("target").unwrap().as_str() == "weight" && trimmed_items > 0 {
|
||||
got_truncate_weight = true;
|
||||
}
|
||||
|
||||
if caps.name("target").unwrap().as_str() == "length" && trimmed_items > 0 {
|
||||
got_truncate_len = true;
|
||||
}
|
||||
}
|
||||
|
||||
if now.elapsed() > MAX_DURATION_FOR_SUBMIT_SOLUTION {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
assert!(got_truncate_weight, "Trimming weight logs were not found");
|
||||
assert!(got_truncate_len, "Trimming length logs were not found");
|
||||
|
||||
got_truncate_len && got_truncate_weight
|
||||
}
|
||||
Reference in New Issue
Block a user