1#![allow(clippy::result_large_err)]
2use std::{
3 collections::{HashMap, HashSet},
4 path::{Path, PathBuf},
5 sync::{
6 atomic::{AtomicU32, Ordering},
7 Arc,
8 },
9 time::Duration,
10};
11
12use anyhow::anyhow;
13use configuration::types::{Arg, AssetLocation};
14use prom_metrics_parser::MetricMap;
15use provider::{
16 types::{ExecutionResult, RunScriptOptions},
17 DynNode,
18};
19use serde::{Deserialize, Serialize, Serializer};
20use subxt::{backend::rpc::RpcClient, OnlineClient, PolkadotConfig};
21use support::net::{skip_err_while_waiting, wait_ws_ready};
22use thiserror::Error;
23use tokio::sync::RwLock;
24use tracing::{debug, trace, warn};
25
26use self::core::NodeCore;
27use crate::{
28 generators::{generate_node_command, generate_node_command_cumulus, GenCmdOptions},
29 network::NodeContext,
30 network_spec::node::NodeSpec,
31 shared::{constants::PROCESS_START_TIME_METRIC, types::NodeSnapshot},
32 tx_helper::client::get_client_from_url,
33};
34
35pub mod core;
36pub mod jam;
37pub mod spawned;
38
39pub use self::{
40 jam::JamNetworkNode,
41 spawned::{NodeKind, SpawnedNode},
42};
43
44type BoxedClosure = Box<dyn Fn(&str) -> Result<bool, anyhow::Error> + Send + Sync>;
45
46#[derive(Error, Debug)]
47pub enum NetworkNodeError {
48 #[error("metric '{0}' not found!")]
49 MetricNotFound(String),
50}
51
52const MONITOR_TARGET: &str = "zombie_monitor";
54
55#[derive(Clone, Serialize)]
62pub struct NetworkNode {
63 #[serde(flatten)]
64 pub(crate) core: NodeCore,
65 pub(crate) spec: NodeSpec,
67 pub(crate) ws_uri: String,
68 pub(crate) multiaddr: String,
69 pub(crate) prometheus_uri: String,
70 pub(crate) cmd_generator_opts: GenCmdOptions,
74 pub(crate) context: NodeContext,
76 #[serde(skip)]
77 metrics_cache: Arc<RwLock<MetricMap>>,
78}
79#[derive(Deserialize)]
80pub(crate) struct RawNetworkNode {
81 pub(crate) name: String,
82 pub(crate) ws_uri: String,
83 pub(crate) prometheus_uri: String,
84 pub(crate) multiaddr: String,
85 pub(crate) spec: NodeSpec,
86 pub(crate) cmd_generator_opts: GenCmdOptions,
87 pub(crate) context: NodeContext,
88 #[serde(default)]
89 pub(crate) inner: serde_json::Value,
90}
91
92#[derive(Debug, Clone, Copy, PartialEq, Eq)]
102pub enum WaitCount {
103 TargetReached(u32),
104 TargetFailed(u32),
105}
106
107impl WaitCount {
108 pub fn success(&self) -> bool {
109 match self {
110 Self::TargetReached(..) => true,
111 Self::TargetFailed(..) => false,
112 }
113 }
114}
115
116pub type LogLineCount = WaitCount;
117
118#[derive(Clone)]
131pub struct CountOptions {
132 pub predicate: Arc<dyn Fn(u32) -> bool + Send + Sync>,
133 pub timeout: Duration,
134 pub wait_until_timeout_elapses: bool,
135}
136
137impl CountOptions {
138 pub fn new(
139 predicate: impl Fn(u32) -> bool + 'static + Send + Sync,
140 timeout: Duration,
141 wait_until_timeout_elapses: bool,
142 ) -> Self {
143 Self {
144 predicate: Arc::new(predicate),
145 timeout,
146 wait_until_timeout_elapses,
147 }
148 }
149
150 pub fn no_occurences_within_timeout(timeout: Duration) -> Self {
151 Self::new(|n| n == 0, timeout, true)
152 }
153
154 pub fn at_least_once(timeout: Duration) -> Self {
155 Self::new(|count| count >= 1, timeout, false)
156 }
157
158 pub fn at_least(target: u32, timeout: Duration) -> Self {
159 Self::new(move |count| count >= target, timeout, false)
160 }
161
162 pub fn exactly_once(timeout: Duration) -> Self {
163 Self::new(|count| count == 1, timeout, false)
164 }
165}
166
167pub type LogLineCountOptions = CountOptions;
168
169impl NetworkNode {
170 #[allow(clippy::too_many_arguments)]
172 pub(crate) fn new<T: Into<String>>(
173 name: T,
174 ws_uri: T,
175 prometheus_uri: T,
176 multiaddr: T,
177 spec: NodeSpec,
178 inner: DynNode,
179 cmd_generator_opts: GenCmdOptions,
180 context: NodeContext,
181 ) -> Self {
182 Self {
183 core: NodeCore::new(name, inner, NodeKind::Substrate),
184 ws_uri: ws_uri.into(),
185 prometheus_uri: prometheus_uri.into(),
186 spec,
187 cmd_generator_opts,
188 context,
189 multiaddr: multiaddr.into(),
190 metrics_cache: Arc::new(Default::default()),
191 }
192 }
193
194 pub fn core(&self) -> &NodeCore {
196 &self.core
197 }
198
199 pub fn is_running(&self) -> bool {
206 self.core.is_running()
207 }
208
209 pub fn last_start_ts(&self) -> u64 {
211 self.core.last_start_ts()
212 }
213
214 pub(crate) fn set_is_running(&self, is_running: bool) {
215 self.core.set_is_running(is_running);
216 }
217
218 pub(crate) fn set_last_start_ts(&self, ts: u64) {
220 self.core.set_last_start_ts(ts);
221 }
222
223 pub fn name(&self) -> &str {
224 self.core.name()
225 }
226
227 pub fn args(&self) -> Vec<&str> {
230 self.core.args()
231 }
232
233 pub fn base_dir(&self) -> &PathBuf {
237 self.core.base_dir()
238 }
239
240 pub async fn pause(&self) -> Result<(), anyhow::Error> {
246 self.core.pause().await
247 }
248
249 pub async fn resume(&self) -> Result<(), anyhow::Error> {
255 self.core.resume().await
256 }
257
258 pub async fn restart(&self, after: Option<Duration>) -> Result<(), anyhow::Error> {
263 self.core.restart(after).await
264 }
265
266 pub async fn run_script(
273 &self,
274 options: RunScriptOptions,
275 ) -> Result<ExecutionResult, anyhow::Error> {
276 self.core.run_script(options).await
277 }
278
279 pub async fn logs(&self) -> Result<String, anyhow::Error> {
281 self.core.logs().await
282 }
283
284 pub async fn wait_log_line_count(
286 &self,
287 pattern: impl Into<String>,
288 is_glob: bool,
289 count: usize,
290 ) -> Result<(), anyhow::Error> {
291 self.core.wait_log_line_count(pattern, is_glob, count).await
292 }
293
294 pub async fn wait_log_line_count_with_timeout(
299 &self,
300 substring: impl Into<String>,
301 is_glob: bool,
302 options: LogLineCountOptions,
303 ) -> Result<LogLineCount, anyhow::Error> {
304 self.core
305 .wait_log_line_count_with_timeout(substring, is_glob, options)
306 .await
307 }
308
309 pub async fn snapshot_db(
315 &self,
316 out_path: impl AsRef<Path>,
317 ) -> Result<NodeSnapshot, anyhow::Error> {
318 let is_cumulus_based = matches!(
319 self.context,
320 NodeContext::Para {
321 is_cumulus_based: true,
322 ..
323 }
324 );
325
326 self.core.snapshot_db(out_path, is_cumulus_based).await
327 }
328
329 pub async fn is_responsive(&self) -> bool {
336 tokio::time::timeout(Duration::from_secs(2), wait_ws_ready(self.ws_uri()))
337 .await
338 .is_ok()
339 }
340
341 pub fn user_args(&self) -> Vec<String> {
344 self.spec
345 .args
346 .iter()
347 .fold(vec![], |acc, arg| [acc, arg.to_vec()].concat())
348 }
349
350 pub fn spec(&self) -> &NodeSpec {
351 &self.spec
352 }
353
354 pub fn ws_uri(&self) -> &str {
355 &self.ws_uri
356 }
357
358 pub fn multiaddr(&self) -> &str {
359 self.multiaddr.as_ref()
360 }
361
362 pub async fn rpc(&self) -> Result<RpcClient, subxt::Error> {
366 get_client_from_url(&self.ws_uri).await
367 }
368
369 #[deprecated = "Use `wait_client` instead."]
371 pub async fn client<Config: subxt::Config>(
372 &self,
373 ) -> Result<OnlineClient<Config>, subxt::Error> {
374 self.try_client().await
375 }
376
377 pub async fn try_client<Config: subxt::Config>(
386 &self,
387 ) -> Result<OnlineClient<Config>, subxt::Error> {
388 get_client_from_url(&self.ws_uri).await
389 }
390
391 pub async fn wait_client<Config: subxt::Config>(
393 &self,
394 ) -> Result<OnlineClient<Config>, anyhow::Error> {
395 debug!("wait_client ws_uri: {}", self.ws_uri());
396 wait_ws_ready(self.ws_uri())
397 .await
398 .map_err(|e| anyhow!("Error awaiting http_client to ws be ready, err: {e}"))?;
399
400 self.try_client()
401 .await
402 .map_err(|e| anyhow!("Can't create a subxt client, err: {e}"))
403 }
404
405 pub async fn wait_client_with_timeout<Config: subxt::Config>(
407 &self,
408 timeout_secs: impl Into<u64>,
409 ) -> Result<OnlineClient<Config>, anyhow::Error> {
410 debug!("waiting until subxt client is ready");
411 tokio::time::timeout(
412 Duration::from_secs(timeout_secs.into()),
413 self.wait_client::<Config>(),
414 )
415 .await?
416 }
417
418 pub async fn restart_with(
430 &self,
431 assets: Vec<AssetLocation>,
432 program: Option<String>,
433 args: Option<Vec<Arg>>,
434 after: Option<Duration>,
435 ) -> Result<(), anyhow::Error> {
436 let mut spec_cloned = self.spec.clone();
437
438 if let Some(args) = args {
439 spec_cloned.args = args;
440 }
441 if let Some(program) = program {
442 spec_cloned.command = program.as_str().try_into()?;
443 }
444
445 let (program, args) = match self.context {
446 NodeContext::Rc
447 | NodeContext::Para {
448 is_cumulus_based: false,
449 ..
450 } => generate_node_command(&spec_cloned, self.cmd_generator_opts.clone(), None),
451 NodeContext::Para {
452 para_id,
453 is_cumulus_based: true,
454 } => generate_node_command_cumulus(
455 &spec_cloned,
456 self.cmd_generator_opts.clone(),
457 para_id,
458 ),
459 NodeContext::Jam => {
460 return Err(anyhow!(
461 "[{}] is a JAM node held as a substrate `NetworkNode`, {}",
462 self.name(),
463 support::constants::THIS_IS_A_BUG
464 ))
465 },
466 };
467
468 self.core
469 .restart_with(&assets, &program, &args, after)
470 .await
471 }
472
473 pub async fn reports(&self, metric_name: impl Into<String>) -> Result<f64, anyhow::Error> {
481 let metric_name = metric_name.into();
482 self.fetch_metrics().await?;
484 self.metric(&metric_name, true).await
486 }
487
488 pub async fn assert(
497 &self,
498 metric_name: impl Into<String>,
499 value: impl Into<f64>,
500 ) -> Result<bool, anyhow::Error> {
501 let value: f64 = value.into();
502 self.assert_with(metric_name, |v| v == value).await
503 }
504
505 pub async fn assert_with(
508 &self,
509 metric_name: impl Into<String>,
510 predicate: impl Fn(f64) -> bool,
511 ) -> Result<bool, anyhow::Error> {
512 let metric_name = metric_name.into();
513 self.fetch_metrics().await?;
515 let val = self.metric(&metric_name, true).await?;
516 let log_msg = format!("🔎 Current value {val} passed to the predicated?");
517 if metric_name == PROCESS_START_TIME_METRIC {
518 trace!(target: MONITOR_TARGET, "{log_msg}");
519 } else {
520 trace!("{log_msg}");
521 }
522 Ok(predicate(val))
523 }
524
525 pub async fn wait_metric(
529 &self,
530 metric_name: impl Into<String>,
531 predicate: impl Fn(f64) -> bool,
532 ) -> Result<(), anyhow::Error> {
533 let metric_name = metric_name.into();
534 let log_msg = format!(
535 "[{}] waiting until metric {metric_name} pass the predicate",
536 self.name()
537 );
538 if metric_name == PROCESS_START_TIME_METRIC {
539 trace!(target: MONITOR_TARGET, "{log_msg}");
540 } else {
541 trace!("{log_msg}");
542 }
543
544 loop {
545 let res = self.assert_with(&metric_name, &predicate).await;
546 let log_msg = format!("res: {res:?}");
547 match res {
548 Ok(res) => {
549 if res {
550 return Ok(());
551 }
552 },
553 Err(e) => match e.downcast::<reqwest::Error>() {
554 Ok(io_err) => {
555 if !skip_err_while_waiting(&io_err) {
556 return Err(io_err.into());
557 }
558 },
559 Err(other) => {
560 match other.downcast::<NetworkNodeError>() {
561 Ok(node_err) => {
562 if !matches!(node_err, NetworkNodeError::MetricNotFound(_)) {
563 return Err(node_err.into());
564 }
565 },
566 Err(other) => return Err(other),
567 };
568 },
569 },
570 }
571
572 if metric_name == PROCESS_START_TIME_METRIC {
573 trace!(target: MONITOR_TARGET, "{log_msg}");
574 } else {
575 trace!("{log_msg}");
576 }
577
578 tokio::time::sleep(Duration::from_secs(1)).await;
580 }
581 }
582
583 pub async fn wait_metric_with_timeout(
586 &self,
587 metric_name: impl Into<String>,
588 predicate: impl Fn(f64) -> bool,
589 timeout_secs: impl Into<u64>,
590 ) -> Result<(), anyhow::Error> {
591 let metric_name = metric_name.into();
592 let secs = timeout_secs.into();
593 let log_msg = format!(
594 "[{}] waiting until metric {metric_name} pass the predicate for {secs}s",
595 self.name()
596 );
597
598 if metric_name == PROCESS_START_TIME_METRIC {
599 trace!(target: MONITOR_TARGET, "{log_msg}");
600 } else {
601 debug!("{log_msg}");
602 }
603
604 let res = tokio::time::timeout(
605 Duration::from_secs(secs),
606 self.wait_metric(&metric_name, predicate),
607 )
608 .await;
609
610 if let Ok(inner_res) = res {
611 match inner_res {
612 Ok(_) => Ok(()),
613 Err(e) => Err(anyhow!("Error waiting for metric: {e}")),
614 }
615 } else {
616 Err(anyhow!(
618 "Timeout ({secs}), waiting for metric {metric_name} pass the predicate"
619 ))
620 }
621 }
622
623 pub async fn wait_event_count_with_timeout(
669 &self,
670 pallet: impl Into<String>,
671 variant: impl Into<String>,
672 options: CountOptions,
673 ) -> Result<WaitCount, anyhow::Error> {
674 let pallet = pallet.into();
675 let variant = variant.into();
676 debug!(
677 "waiting until match event ({pallet} {variant}) count within {} seconds",
678 options.timeout.as_secs_f64()
679 );
680
681 let init_value = Arc::new(AtomicU32::new(0));
682
683 let res = tokio::time::timeout(
684 options.timeout,
685 self.wait_event_count(&pallet, &variant, &options, init_value.clone()),
686 )
687 .await;
688
689 let q = init_value.load(Ordering::Relaxed);
690 if let Ok(inner_res) = res {
691 match inner_res {
692 Ok(_) => Ok(WaitCount::TargetReached(q)),
693 Err(e) => Err(anyhow!("Error waiting for counter: {e}")),
694 }
695 } else {
696 if options.wait_until_timeout_elapses {
698 let q = init_value.load(Ordering::Relaxed);
699 if (options.predicate)(q) {
700 Ok(LogLineCount::TargetReached(q))
701 } else {
702 Ok(LogLineCount::TargetFailed(q))
703 }
704 } else {
705 Err(anyhow!(
706 "Timeout ({}), waiting for counter",
707 options.timeout.as_secs()
708 ))
709 }
710 }
711 }
712
713 async fn wait_event_count(
715 &self,
716 pallet: &str,
717 variant: &str,
718 options: &CountOptions,
719 init_count: Arc<AtomicU32>,
720 ) -> Result<(), anyhow::Error> {
721 let client: OnlineClient<PolkadotConfig> = self.wait_client().await?;
722 let mut blocks_sub: subxt::backend::StreamOf<
723 Result<
724 subxt::blocks::Block<PolkadotConfig, OnlineClient<PolkadotConfig>>,
725 subxt::Error,
726 >,
727 > = client.blocks().subscribe_finalized().await?;
728 while let Some(block) = blocks_sub.next().await {
729 let events = block?.events().await?;
730 for event in events.iter() {
731 let evt = event?;
732 if evt.pallet_name() == pallet && evt.variant_name() == variant {
733 let old_value = init_count.fetch_add(1, Ordering::Relaxed);
734 if !options.wait_until_timeout_elapses && (options.predicate)(old_value + 1) {
735 return Ok(());
736 }
737 }
738 }
739 }
740
741 Ok(())
742 }
743
744 async fn fetch_metrics(&self) -> Result<(), anyhow::Error> {
745 let response = reqwest::get(&self.prometheus_uri).await?;
746 let metrics = prom_metrics_parser::parse(&response.text().await?)?;
747 let mut cache = self.metrics_cache.write().await;
748 *cache = metrics;
749 Ok(())
750 }
751
752 async fn metric(
754 &self,
755 metric_name: &str,
756 treat_not_found_as_zero: bool,
757 ) -> Result<f64, anyhow::Error> {
758 let mut metrics_map = self.metrics_cache.read().await;
759 if metrics_map.is_empty() {
760 drop(metrics_map);
762 self.fetch_metrics().await?;
763 metrics_map = self.metrics_cache.read().await;
764 }
765
766 if let Some(val) = metrics_map.get(metric_name) {
767 Ok(*val)
768 } else if treat_not_found_as_zero {
769 Ok(0_f64)
770 } else {
771 Err(NetworkNodeError::MetricNotFound(metric_name.into()).into())
772 }
773 }
774
775 pub async fn get_histogram_buckets(
795 &self,
796 metric_name: impl AsRef<str>,
797 label_filters: Option<HashMap<String, String>>,
798 ) -> Result<HashMap<String, u64>, anyhow::Error> {
799 let metric_name = metric_name.as_ref();
800
801 let response = reqwest::get(&self.prometheus_uri).await?;
803 let metrics = prom_metrics_parser::parse(&response.text().await?)?;
804
805 let resolved_metric_name = if metric_name.contains("_bucket") {
807 metric_name.to_string()
808 } else {
809 format!("{metric_name}_bucket")
810 };
811
812 let mut metric_entries: Vec<(String, HashMap<String, String>, u64)> = Vec::new();
816
817 for (key, &value) in metrics.iter() {
818 if !key.starts_with(&resolved_metric_name) {
819 continue;
820 }
821
822 let remaining = &key[resolved_metric_name.len()..];
823
824 let labels_str = &remaining[1..remaining.len() - 1];
825 let parsed_labels = Self::parse_label_string(labels_str);
826
827 if !parsed_labels.contains_key("le") {
829 continue;
830 }
831
832 if let Some(ref filters) = label_filters {
834 let mut all_match = true;
835 for (filter_key, filter_value) in filters {
836 if parsed_labels.get(filter_key) != Some(filter_value) {
837 all_match = false;
838 break;
839 }
840 }
841 if !all_match {
842 continue;
843 }
844 }
845
846 metric_entries.push((key.clone(), parsed_labels, value as u64));
847 }
848
849 let max_label_count = metric_entries
852 .iter()
853 .map(|(_, labels, _)| labels.iter().filter(|(k, _)| k.as_str() != "le").count())
854 .max()
855 .unwrap_or(0);
856
857 let mut raw_buckets: Vec<(String, u64)> = Vec::new();
859 let mut seen_le_values = HashSet::new();
860 let mut active_series: Option<Vec<(String, String)>> = None;
861
862 for (_, parsed_labels, value) in metric_entries {
863 let le_value = parsed_labels.get("le").unwrap().clone();
864
865 let mut non_le_labels: Vec<(String, String)> = parsed_labels
867 .iter()
868 .filter(|(k, _)| k.as_str() != "le")
869 .map(|(k, v)| (k.clone(), v.clone()))
870 .collect();
871 non_le_labels.sort();
872
873 if non_le_labels.len() < max_label_count {
876 continue;
877 }
878
879 if let Some(ref prev_series) = active_series {
881 if prev_series != &non_le_labels {
882 if !raw_buckets.is_empty() {
883 break; }
885 active_series = Some(non_le_labels.clone());
886 seen_le_values.clear();
887 }
888 } else {
889 active_series = Some(non_le_labels.clone());
890 }
891
892 if !seen_le_values.insert(le_value.clone()) {
894 continue;
895 }
896
897 trace!("{} le:{} {}", resolved_metric_name, &le_value, value);
898 raw_buckets.push((le_value, value));
899 }
900
901 raw_buckets.sort_by(|a, b| Self::compare_le_values(&a.0, &b.0));
903
904 let mut buckets = HashMap::new();
906 let mut previous_value = 0_u64;
907 for (le, cumulative_count) in raw_buckets {
908 if cumulative_count < previous_value {
909 warn!(
910 "Warning: bucket count decreased from {} to {} at le={}",
911 previous_value, cumulative_count, le
912 );
913 }
914 let delta = cumulative_count.saturating_sub(previous_value);
915 buckets.insert(le, delta);
916 previous_value = cumulative_count;
917 }
918
919 Ok(buckets)
920 }
921
922 fn parse_label_string(labels_str: &str) -> HashMap<String, String> {
928 let mut labels = HashMap::new();
929 let mut current_key = String::new();
930 let mut current_value = String::new();
931 let mut in_value = false;
932 let mut in_quotes = false;
933
934 for ch in labels_str.chars() {
935 match ch {
936 '=' if !in_quotes && !in_value => {
937 in_value = true;
938 },
939 '"' if in_value => {
940 in_quotes = !in_quotes;
941 },
942 ',' if !in_quotes => {
943 if !current_key.is_empty() {
945 labels.insert(
946 current_key.trim().to_string(),
947 current_value.trim().to_string(),
948 );
949 current_key.clear();
950 current_value.clear();
951 in_value = false;
952 }
953 },
954 _ => {
955 if in_value {
956 current_value.push(ch);
957 } else {
958 current_key.push(ch);
959 }
960 },
961 }
962 }
963
964 if !current_key.is_empty() {
966 labels.insert(
967 current_key.trim().to_string(),
968 current_value.trim().to_string(),
969 );
970 }
971
972 labels
973 }
974
975 fn compare_le_values(a: &str, b: &str) -> std::cmp::Ordering {
979 use std::cmp::Ordering;
980
981 match (a, b) {
983 ("+Inf", "+Inf") => Ordering::Equal,
984 ("+Inf", _) => Ordering::Greater,
985 (_, "+Inf") => Ordering::Less,
986 _ => {
987 match (a.parse::<f64>(), b.parse::<f64>()) {
989 (Ok(a_val), Ok(b_val)) => a_val.partial_cmp(&b_val).unwrap_or(Ordering::Equal),
990 _ => a.cmp(b),
992 }
993 },
994 }
995 }
996
997 pub async fn wait_until_is_up(
1009 &self,
1010 timeout_secs: impl Into<u64>,
1011 ) -> Result<(), anyhow::Error> {
1012 self.wait_metric_with_timeout(PROCESS_START_TIME_METRIC, |b| b >= 1.0, timeout_secs)
1013 .await
1014 .map_err(|err| anyhow::anyhow!("{}: {:?}", self.name(), err))
1015 }
1016}
1017
1018#[async_trait::async_trait]
1019impl SpawnedNode for NetworkNode {
1020 fn core(&self) -> &NodeCore {
1021 &self.core
1022 }
1023
1024 async fn wait_until_is_up(&self, timeout_secs: u64) -> Result<(), anyhow::Error> {
1025 NetworkNode::wait_until_is_up(self, timeout_secs).await
1026 }
1027}
1028
1029impl std::fmt::Debug for NetworkNode {
1030 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1031 f.debug_struct("NetworkNode")
1032 .field("inner", &"inner_skipped")
1033 .field("spec", &self.spec)
1034 .field("name", &self.name())
1035 .field("ws_uri", &self.ws_uri)
1036 .field("prometheus_uri", &self.prometheus_uri)
1037 .finish()
1038 }
1039}
1040
1041fn serialize_provider_node<S>(node: &DynNode, serializer: S) -> Result<S::Ok, S::Error>
1042where
1043 S: Serializer,
1044{
1045 erased_serde::serialize(node.as_ref(), serializer)
1046}
1047
1048#[cfg(test)]
1050mod tests {
1051 use std::{
1052 path::{Path, PathBuf},
1053 sync::{Arc, Mutex},
1054 };
1055
1056 use async_trait::async_trait;
1057 use provider::{types::*, ProviderError, ProviderNode};
1058
1059 use super::*;
1060
1061 #[derive(Serialize)]
1062 struct MockNode {
1063 logs: Arc<Mutex<Vec<String>>>,
1064 }
1065
1066 impl MockNode {
1067 fn new() -> Self {
1068 Self {
1069 logs: Arc::new(Mutex::new(vec![])),
1070 }
1071 }
1072
1073 fn logs_push(&self, lines: Vec<impl Into<String>>) {
1074 self.logs
1075 .lock()
1076 .unwrap()
1077 .extend(lines.into_iter().map(|l| l.into()));
1078 }
1079 }
1080
1081 #[async_trait]
1082 impl ProviderNode for MockNode {
1083 fn name(&self) -> &str {
1084 todo!()
1085 }
1086
1087 fn args(&self) -> Vec<&str> {
1088 todo!()
1089 }
1090
1091 fn base_dir(&self) -> &PathBuf {
1092 todo!()
1093 }
1094
1095 fn config_dir(&self) -> &PathBuf {
1096 todo!()
1097 }
1098
1099 fn data_dir(&self) -> &PathBuf {
1100 todo!()
1101 }
1102
1103 fn relay_data_dir(&self) -> &PathBuf {
1104 todo!()
1105 }
1106
1107 fn scripts_dir(&self) -> &PathBuf {
1108 todo!()
1109 }
1110
1111 fn log_path(&self) -> &PathBuf {
1112 todo!()
1113 }
1114
1115 fn log_cmd(&self) -> String {
1116 todo!()
1117 }
1118
1119 fn path_in_node(&self, _file: &Path) -> PathBuf {
1120 todo!()
1121 }
1122
1123 async fn logs(&self) -> Result<String, ProviderError> {
1124 Ok(self.logs.lock().unwrap().join("\n"))
1125 }
1126
1127 async fn dump_logs(&self, _local_dest: PathBuf) -> Result<(), ProviderError> {
1128 todo!()
1129 }
1130
1131 async fn run_command(
1132 &self,
1133 _options: RunCommandOptions,
1134 ) -> Result<ExecutionResult, ProviderError> {
1135 todo!()
1136 }
1137
1138 async fn run_script(
1139 &self,
1140 _options: RunScriptOptions,
1141 ) -> Result<ExecutionResult, ProviderError> {
1142 todo!()
1143 }
1144
1145 async fn send_file(
1146 &self,
1147 _local_file_path: &Path,
1148 _remote_file_path: &Path,
1149 _mode: &str,
1150 ) -> Result<(), ProviderError> {
1151 todo!()
1152 }
1153
1154 async fn receive_file(
1155 &self,
1156 _remote_file_path: &Path,
1157 _local_file_path: &Path,
1158 ) -> Result<(), ProviderError> {
1159 todo!()
1160 }
1161
1162 async fn pause(&self) -> Result<(), ProviderError> {
1163 todo!()
1164 }
1165
1166 async fn resume(&self) -> Result<(), ProviderError> {
1167 todo!()
1168 }
1169
1170 async fn restart(&self, _after: Option<Duration>) -> Result<(), ProviderError> {
1171 todo!()
1172 }
1173
1174 async fn restart_with(
1175 &self,
1176 _assets: &[AssetLocation],
1177 _cmd: &str,
1178 _args: &[String],
1179 _after: Option<Duration>,
1180 ) -> Result<(), ProviderError> {
1181 todo!()
1182 }
1183
1184 async fn destroy(&self) -> Result<(), ProviderError> {
1185 todo!()
1186 }
1187
1188 async fn snapshot_db(&self, _: bool) -> Result<InnerSnapshotDb, ProviderError> {
1189 todo!()
1190 }
1191 }
1192
1193 #[tokio::test(flavor = "multi_thread")]
1194 async fn test_wait_log_count_target_reached_immediately() -> Result<(), anyhow::Error> {
1195 let mock_provider = Arc::new(MockNode::new());
1196 let mock_node = NetworkNode::new(
1197 "node1",
1198 "ws_uri",
1199 "prometheus_uri",
1200 "multiaddr",
1201 NodeSpec::default(),
1202 mock_provider.clone(),
1203 GenCmdOptions::default(),
1204 NodeContext::Rc,
1205 );
1206
1207 mock_provider.logs_push(vec![
1208 "system booting",
1209 "stub line 1",
1210 "stub line 2",
1211 "system ready",
1212 ]);
1213
1214 let options = LogLineCountOptions {
1216 predicate: Arc::new(|n| n == 1),
1217 timeout: Duration::from_secs(10),
1218 wait_until_timeout_elapses: false,
1219 };
1220
1221 let log_line_count = mock_node
1222 .wait_log_line_count_with_timeout("system ready", false, options)
1223 .await?;
1224
1225 assert!(matches!(log_line_count, LogLineCount::TargetReached(1)));
1226
1227 Ok(())
1228 }
1229
1230 #[tokio::test(flavor = "multi_thread")]
1231 async fn test_wait_log_count_target_reached_after_delay() -> Result<(), anyhow::Error> {
1232 let mock_provider = Arc::new(MockNode::new());
1233 let mock_node = NetworkNode::new(
1234 "node1",
1235 "ws_uri",
1236 "prometheus_uri",
1237 "multiaddr",
1238 NodeSpec::default(),
1239 mock_provider.clone(),
1240 GenCmdOptions::default(),
1241 NodeContext::Rc,
1242 );
1243
1244 mock_provider.logs_push(vec![
1245 "system booting",
1246 "stub line 1",
1247 "stub line 2",
1248 "system ready",
1249 ]);
1250
1251 let options = LogLineCountOptions {
1253 predicate: Arc::new(|n| n == 2),
1254 timeout: Duration::from_secs(4),
1255 wait_until_timeout_elapses: false,
1256 };
1257
1258 let task = tokio::spawn({
1259 async move {
1260 mock_node
1261 .wait_log_line_count_with_timeout("system ready", false, options)
1262 .await
1263 .unwrap()
1264 }
1265 });
1266
1267 tokio::time::sleep(Duration::from_secs(2)).await;
1268
1269 mock_provider.logs_push(vec!["system ready"]);
1270
1271 let log_line_count = task.await?;
1272
1273 assert!(matches!(log_line_count, LogLineCount::TargetReached(2)));
1274
1275 Ok(())
1276 }
1277
1278 #[tokio::test(flavor = "multi_thread")]
1279 async fn test_wait_log_count_target_failed_timeout() -> Result<(), anyhow::Error> {
1280 let mock_provider = Arc::new(MockNode::new());
1281 let mock_node = NetworkNode::new(
1282 "node1",
1283 "ws_uri",
1284 "prometheus_uri",
1285 "multiaddr",
1286 NodeSpec::default(),
1287 mock_provider.clone(),
1288 GenCmdOptions::default(),
1289 NodeContext::Rc,
1290 );
1291
1292 mock_provider.logs_push(vec![
1293 "system booting",
1294 "stub line 1",
1295 "stub line 2",
1296 "system ready",
1297 ]);
1298
1299 let options = LogLineCountOptions {
1301 predicate: Arc::new(|n| n == 2),
1302 timeout: Duration::from_secs(2),
1303 wait_until_timeout_elapses: false,
1304 };
1305
1306 let log_line_count = mock_node
1307 .wait_log_line_count_with_timeout("system ready", false, options)
1308 .await?;
1309
1310 assert!(matches!(log_line_count, LogLineCount::TargetFailed(1)));
1311
1312 Ok(())
1313 }
1314
1315 #[tokio::test(flavor = "multi_thread")]
1316 async fn test_wait_log_count_target_failed_exceeded() -> Result<(), anyhow::Error> {
1317 let mock_provider = Arc::new(MockNode::new());
1318 let mock_node = NetworkNode::new(
1319 "node1",
1320 "ws_uri",
1321 "prometheus_uri",
1322 "multiaddr",
1323 NodeSpec::default(),
1324 mock_provider.clone(),
1325 GenCmdOptions::default(),
1326 NodeContext::Rc,
1327 );
1328
1329 mock_provider.logs_push(vec![
1330 "system booting",
1331 "stub line 1",
1332 "stub line 2",
1333 "system ready",
1334 ]);
1335
1336 let options = LogLineCountOptions {
1338 predicate: Arc::new(|n| n == 2),
1339 timeout: Duration::from_secs(2),
1340 wait_until_timeout_elapses: true,
1341 };
1342
1343 let task = tokio::spawn({
1344 async move {
1345 mock_node
1346 .wait_log_line_count_with_timeout("system ready", false, options)
1347 .await
1348 .unwrap()
1349 }
1350 });
1351
1352 tokio::time::sleep(Duration::from_secs(1)).await;
1353
1354 mock_provider.logs_push(vec!["system ready"]);
1355 mock_provider.logs_push(vec!["system ready"]);
1356
1357 let log_line_count = task.await?;
1358
1359 assert!(matches!(log_line_count, LogLineCount::TargetFailed(3)));
1360
1361 Ok(())
1362 }
1363
1364 #[tokio::test(flavor = "multi_thread")]
1365 async fn test_wait_log_count_target_reached_no_occurences() -> Result<(), anyhow::Error> {
1366 let mock_provider = Arc::new(MockNode::new());
1367 let mock_node = NetworkNode::new(
1368 "node1",
1369 "ws_uri",
1370 "prometheus_uri",
1371 "multiaddr",
1372 NodeSpec::default(),
1373 mock_provider.clone(),
1374 GenCmdOptions::default(),
1375 NodeContext::Rc,
1376 );
1377
1378 mock_provider.logs_push(vec!["system booting", "stub line 1", "stub line 2"]);
1379
1380 let task = tokio::spawn({
1381 async move {
1382 mock_node
1383 .wait_log_line_count_with_timeout(
1384 "system ready",
1385 false,
1386 LogLineCountOptions::no_occurences_within_timeout(Duration::from_secs(2)),
1388 )
1389 .await
1390 .unwrap()
1391 }
1392 });
1393
1394 tokio::time::sleep(Duration::from_secs(1)).await;
1395
1396 mock_provider.logs_push(vec!["stub line 3"]);
1397
1398 assert!(task.await?.success());
1399
1400 Ok(())
1401 }
1402
1403 #[tokio::test(flavor = "multi_thread")]
1404 async fn test_wait_log_count_target_reached_in_range() -> Result<(), anyhow::Error> {
1405 let mock_provider = Arc::new(MockNode::new());
1406 let mock_node = NetworkNode::new(
1407 "node1",
1408 "ws_uri",
1409 "prometheus_uri",
1410 "multiaddr",
1411 NodeSpec::default(),
1412 mock_provider.clone(),
1413 GenCmdOptions::default(),
1414 NodeContext::Rc,
1415 );
1416
1417 mock_provider.logs_push(vec!["system booting", "stub line 1", "stub line 2"]);
1418
1419 let options = LogLineCountOptions {
1421 predicate: Arc::new(|n| (2..=5).contains(&n)),
1422 timeout: Duration::from_secs(2),
1423 wait_until_timeout_elapses: true,
1424 };
1425
1426 let task = tokio::spawn({
1427 async move {
1428 mock_node
1429 .wait_log_line_count_with_timeout("system ready", false, options)
1430 .await
1431 .unwrap()
1432 }
1433 });
1434
1435 tokio::time::sleep(Duration::from_secs(1)).await;
1436
1437 mock_provider.logs_push(vec!["system ready", "system ready", "system ready"]);
1438
1439 assert!(task.await?.success());
1440
1441 Ok(())
1442 }
1443
1444 #[tokio::test(flavor = "multi_thread")]
1445 async fn test_wait_log_count_with_timeout_with_lookahead_regex() -> Result<(), anyhow::Error> {
1446 let mock_provider = Arc::new(MockNode::new());
1447 let mock_node = NetworkNode::new(
1448 "node1",
1449 "ws_uri",
1450 "prometheus_uri",
1451 "multiaddr",
1452 NodeSpec::default(),
1453 mock_provider.clone(),
1454 GenCmdOptions::default(),
1455 NodeContext::Rc,
1456 );
1457
1458 mock_provider.logs_push(vec![
1459 "system booting",
1460 "stub line 1",
1461 "Error importing block 0xfd66e545c446b1c01205503130b816af0ec2c0e504a8472808e6ff4a644ce1fa: block has an unknown parent",
1463 "stub line 2"
1464 ]);
1465
1466 let options = LogLineCountOptions {
1467 predicate: Arc::new(|n| n == 1),
1468 timeout: Duration::from_secs(3),
1469 wait_until_timeout_elapses: true,
1470 };
1471
1472 let task = tokio::spawn({
1473 async move {
1474 mock_node
1475 .wait_log_line_count_with_timeout(
1476 "error(?! importing block .*: block has an unknown parent)",
1477 false,
1478 options,
1479 )
1480 .await
1481 .unwrap()
1482 }
1483 });
1484
1485 tokio::time::sleep(Duration::from_secs(1)).await;
1486
1487 mock_provider.logs_push(vec![
1488 "system ready",
1489 "system error",
1491 "system ready",
1492 ]);
1493
1494 assert!(task.await?.success());
1495
1496 Ok(())
1497 }
1498
1499 #[tokio::test(flavor = "multi_thread")]
1500 async fn test_wait_log_count_with_timeout_with_lookahead_regex_fails(
1501 ) -> Result<(), anyhow::Error> {
1502 let mock_provider = Arc::new(MockNode::new());
1503 let mock_node = NetworkNode::new(
1504 "node1",
1505 "ws_uri",
1506 "prometheus_uri",
1507 "multiaddr",
1508 NodeSpec::default(),
1509 mock_provider.clone(),
1510 GenCmdOptions::default(),
1511 NodeContext::Rc,
1512 );
1513
1514 mock_provider.logs_push(vec![
1515 "system booting",
1516 "stub line 1",
1517 "Error importing block 0xfd66e545c446b1c01205503130b816af0ec2c0e504a8472808e6ff4a644ce1fa: block has an unknown parent",
1519 "stub line 2"
1520 ]);
1521
1522 let options = LogLineCountOptions {
1523 predicate: Arc::new(|n| n == 1),
1524 timeout: Duration::from_secs(6),
1525 wait_until_timeout_elapses: true,
1526 };
1527
1528 let task = tokio::spawn({
1529 async move {
1530 mock_node
1531 .wait_log_line_count_with_timeout(
1532 "error(?! importing block .*: block has an unknown parent)",
1533 false,
1534 options,
1535 )
1536 .await
1537 .unwrap()
1538 }
1539 });
1540
1541 tokio::time::sleep(Duration::from_secs(1)).await;
1542
1543 mock_provider.logs_push(vec!["system ready", "system ready"]);
1544
1545 assert!(!task.await?.success());
1546
1547 Ok(())
1548 }
1549
1550 #[tokio::test(flavor = "multi_thread")]
1551 async fn test_wait_log_count_with_lockahead_regex() -> Result<(), anyhow::Error> {
1552 let mock_provider = Arc::new(MockNode::new());
1553 let mock_node = NetworkNode::new(
1554 "node1",
1555 "ws_uri",
1556 "prometheus_uri",
1557 "multiaddr",
1558 NodeSpec::default(),
1559 mock_provider.clone(),
1560 GenCmdOptions::default(),
1561 NodeContext::Rc,
1562 );
1563
1564 mock_provider.logs_push(vec![
1565 "system booting",
1566 "stub line 1",
1567 "Error importing block 0xfd66e545c446b1c01205503130b816af0ec2c0e504a8472808e6ff4a644ce1fa: block has an unknown parent",
1569 "stub line 2"
1570 ]);
1571
1572 let task = tokio::spawn({
1573 async move {
1574 mock_node
1575 .wait_log_line_count(
1576 "error(?! importing block .*: block has an unknown parent)",
1577 false,
1578 1,
1579 )
1580 .await
1581 .unwrap()
1582 }
1583 });
1584
1585 tokio::time::sleep(Duration::from_secs(1)).await;
1586
1587 mock_provider.logs_push(vec![
1588 "system ready",
1589 "system error",
1591 "system ready",
1592 ]);
1593
1594 assert!(task.await.is_ok());
1595
1596 Ok(())
1597 }
1598
1599 #[tokio::test(flavor = "multi_thread")]
1600 async fn test_wait_log_count_with_lookahead_regex_fails() -> Result<(), anyhow::Error> {
1601 let mock_provider = Arc::new(MockNode::new());
1602 let mock_node = NetworkNode::new(
1603 "node1",
1604 "ws_uri",
1605 "prometheus_uri",
1606 "multiaddr",
1607 NodeSpec::default(),
1608 mock_provider.clone(),
1609 GenCmdOptions::default(),
1610 NodeContext::Rc,
1611 );
1612
1613 mock_provider.logs_push(vec![
1614 "system booting",
1615 "stub line 1",
1616 "Error importing block 0xfd66e545c446b1c01205503130b816af0ec2c0e504a8472808e6ff4a644ce1fa: block has an unknown parent",
1618 "stub line 2"
1619 ]);
1620
1621 let options = LogLineCountOptions {
1622 predicate: Arc::new(|count| count == 1),
1623 timeout: Duration::from_secs(2),
1624 wait_until_timeout_elapses: true,
1625 };
1626
1627 let task = tokio::spawn({
1628 async move {
1629 mock_node
1631 .wait_log_line_count_with_timeout(
1632 "error(?! importing block .*: block has an unknown parent)",
1633 false,
1634 options,
1635 )
1636 .await
1637 .unwrap()
1638 }
1639 });
1640
1641 tokio::time::sleep(Duration::from_secs(1)).await;
1642
1643 mock_provider.logs_push(vec!["system ready", "system ready"]);
1644
1645 assert!(!task.await?.success());
1646
1647 Ok(())
1648 }
1649
1650 #[tokio::test]
1651 async fn test_get_histogram_buckets_parsing() -> Result<(), anyhow::Error> {
1652 use std::sync::Arc;
1654
1655 let mock_metrics = concat!(
1657 "# HELP substrate_block_verification_time Time taken to verify blocks\n",
1658 "# TYPE substrate_block_verification_time histogram\n",
1659 "substrate_block_verification_time_bucket{chain=\"rococo_local_testnet\",le=\"0.1\"} 10\n",
1660 "substrate_block_verification_time_bucket{chain=\"rococo_local_testnet\",le=\"0.5\"} 25\n",
1661 "substrate_block_verification_time_bucket{chain=\"rococo_local_testnet\",le=\"1.0\"} 35\n",
1662 "substrate_block_verification_time_bucket{chain=\"rococo_local_testnet\",le=\"2.5\"} 40\n",
1663 "substrate_block_verification_time_bucket{chain=\"rococo_local_testnet\",le=\"+Inf\"} 42\n",
1664 "substrate_block_verification_time_sum{chain=\"rococo_local_testnet\"} 45.5\n",
1665 "substrate_block_verification_time_count{chain=\"rococo_local_testnet\"} 42\n",
1666 );
1667
1668 let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await?;
1670 let addr = listener.local_addr()?;
1671 let metrics = Arc::new(mock_metrics.to_string());
1672
1673 tokio::spawn({
1674 let metrics = metrics.clone();
1675 async move {
1676 loop {
1677 if let Ok((mut socket, _)) = listener.accept().await {
1678 let metrics = metrics.clone();
1679 tokio::spawn(async move {
1680 use tokio::io::{AsyncReadExt, AsyncWriteExt};
1681 let mut buffer = [0; 1024];
1682 let _ = socket.read(&mut buffer).await;
1683
1684 let response = format!(
1685 "HTTP/1.1 200 OK\r\nContent-Length: {}\r\n\r\n{}",
1686 metrics.len(),
1687 metrics
1688 );
1689 let _ = socket.write_all(response.as_bytes()).await;
1690 });
1691 }
1692 }
1693 }
1694 });
1695
1696 let mock_provider = Arc::new(MockNode::new());
1698 let mock_node = NetworkNode::new(
1699 "test_node",
1700 "ws://localhost:9944",
1701 &format!("http://127.0.0.1:{}/metrics", addr.port()),
1702 "/ip4/127.0.0.1/tcp/30333",
1703 NodeSpec::default(),
1704 mock_provider,
1705 GenCmdOptions::default(),
1706 NodeContext::Rc,
1707 );
1708
1709 let mut label_filters = HashMap::new();
1711 label_filters.insert("chain".to_string(), "rococo_local_testnet".to_string());
1712 let buckets = mock_node
1713 .get_histogram_buckets("substrate_block_verification_time", Some(label_filters))
1714 .await?;
1715
1716 assert_eq!(buckets.get("0.1"), Some(&10));
1718 assert_eq!(buckets.get("0.5"), Some(&15)); assert_eq!(buckets.get("1.0"), Some(&10)); assert_eq!(buckets.get("2.5"), Some(&5)); assert_eq!(buckets.get("+Inf"), Some(&2)); let mut label_filters = std::collections::HashMap::new();
1725 label_filters.insert("chain".to_string(), "rococo_local_testnet".to_string());
1726
1727 let buckets_filtered = mock_node
1728 .get_histogram_buckets("substrate_block_verification_time", Some(label_filters))
1729 .await?;
1730
1731 assert_eq!(buckets_filtered.get("0.1"), Some(&10));
1732 assert_eq!(buckets_filtered.get("0.5"), Some(&15));
1733
1734 let buckets_with_suffix = mock_node
1736 .get_histogram_buckets("substrate_block_verification_time_bucket", None)
1737 .await?;
1738
1739 assert_eq!(buckets_with_suffix.get("0.1"), Some(&10));
1740
1741 Ok(())
1742 }
1743
1744 #[tokio::test]
1745 async fn test_get_histogram_buckets_unordered() -> Result<(), anyhow::Error> {
1746 use std::sync::Arc;
1748
1749 let mock_metrics = concat!(
1750 "# HELP test_metric A test metric\n",
1751 "# TYPE test_metric histogram\n",
1752 "test_metric_bucket{le=\"2.5\"} 40\n",
1753 "test_metric_bucket{le=\"0.1\"} 10\n",
1754 "test_metric_bucket{le=\"+Inf\"} 42\n",
1755 "test_metric_bucket{le=\"1.0\"} 35\n",
1756 "test_metric_bucket{le=\"0.5\"} 25\n",
1757 );
1758
1759 let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await?;
1760 let addr = listener.local_addr()?;
1761 let metrics = Arc::new(mock_metrics.to_string());
1762
1763 tokio::spawn({
1764 let metrics = metrics.clone();
1765 async move {
1766 loop {
1767 if let Ok((mut socket, _)) = listener.accept().await {
1768 let metrics = metrics.clone();
1769 tokio::spawn(async move {
1770 use tokio::io::{AsyncReadExt, AsyncWriteExt};
1771 let mut buffer = [0; 1024];
1772 let _ = socket.read(&mut buffer).await;
1773 let response = format!(
1774 "HTTP/1.1 200 OK\r\nContent-Length: {}\r\n\r\n{}",
1775 metrics.len(),
1776 metrics
1777 );
1778 let _ = socket.write_all(response.as_bytes()).await;
1779 });
1780 }
1781 }
1782 }
1783 });
1784
1785 let mock_provider = Arc::new(MockNode::new());
1786 let mock_node = NetworkNode::new(
1787 "test_node",
1788 "ws://localhost:9944",
1789 &format!("http://127.0.0.1:{}/metrics", addr.port()),
1790 "/ip4/127.0.0.1/tcp/30333",
1791 NodeSpec::default(),
1792 mock_provider,
1793 GenCmdOptions::default(),
1794 NodeContext::Rc,
1795 );
1796
1797 let buckets = mock_node.get_histogram_buckets("test_metric", None).await?;
1798
1799 assert_eq!(buckets.get("0.1"), Some(&10)); assert_eq!(buckets.get("0.5"), Some(&15)); assert_eq!(buckets.get("1.0"), Some(&10)); assert_eq!(buckets.get("2.5"), Some(&5)); assert_eq!(buckets.get("+Inf"), Some(&2)); Ok(())
1807 }
1808
1809 #[tokio::test]
1810 async fn test_get_histogram_buckets_complex_labels() -> Result<(), anyhow::Error> {
1811 use std::sync::Arc;
1813
1814 let mock_metrics = concat!(
1815 "# HELP test_metric A test metric\n",
1816 "# TYPE test_metric histogram\n",
1817 "test_metric_bucket{method=\"GET,POST\",path=\"/api/test\",le=\"0.1\"} 5\n",
1818 "test_metric_bucket{method=\"GET,POST\",path=\"/api/test\",le=\"0.5\"} 15\n",
1819 "test_metric_bucket{method=\"GET,POST\",path=\"/api/test\",le=\"+Inf\"} 20\n",
1820 );
1821
1822 let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await?;
1823 let addr = listener.local_addr()?;
1824 let metrics = Arc::new(mock_metrics.to_string());
1825
1826 tokio::spawn({
1827 let metrics = metrics.clone();
1828 async move {
1829 loop {
1830 if let Ok((mut socket, _)) = listener.accept().await {
1831 let metrics = metrics.clone();
1832 tokio::spawn(async move {
1833 use tokio::io::{AsyncReadExt, AsyncWriteExt};
1834 let mut buffer = [0; 1024];
1835 let _ = socket.read(&mut buffer).await;
1836 let response = format!(
1837 "HTTP/1.1 200 OK\r\nContent-Length: {}\r\n\r\n{}",
1838 metrics.len(),
1839 metrics
1840 );
1841 let _ = socket.write_all(response.as_bytes()).await;
1842 });
1843 }
1844 }
1845 }
1846 });
1847
1848 let mock_provider = Arc::new(MockNode::new());
1849 let mock_node = NetworkNode::new(
1850 "test_node",
1851 "ws://localhost:9944",
1852 &format!("http://127.0.0.1:{}/metrics", addr.port()),
1853 "/ip4/127.0.0.1/tcp/30333",
1854 NodeSpec::default(),
1855 mock_provider,
1856 GenCmdOptions::default(),
1857 NodeContext::Rc,
1858 );
1859
1860 let buckets = mock_node.get_histogram_buckets("test_metric", None).await?;
1862 assert_eq!(buckets.get("0.1"), Some(&5));
1863 assert_eq!(buckets.get("0.5"), Some(&10)); assert_eq!(buckets.get("+Inf"), Some(&5)); let mut label_filters = std::collections::HashMap::new();
1868 label_filters.insert("method".to_string(), "GET,POST".to_string());
1869
1870 let buckets_filtered = mock_node
1871 .get_histogram_buckets("test_metric", Some(label_filters))
1872 .await?;
1873
1874 assert_eq!(buckets_filtered.get("0.1"), Some(&5));
1875 assert_eq!(buckets_filtered.get("0.5"), Some(&10));
1876
1877 Ok(())
1878 }
1879
1880 #[test]
1881 fn test_compare_le_values() {
1882 use std::cmp::Ordering;
1883
1884 use crate::network::node::NetworkNode;
1885
1886 assert_eq!(NetworkNode::compare_le_values("0.1", "0.5"), Ordering::Less);
1888 assert_eq!(
1889 NetworkNode::compare_le_values("1.0", "0.5"),
1890 Ordering::Greater
1891 );
1892 assert_eq!(
1893 NetworkNode::compare_le_values("1.0", "1.0"),
1894 Ordering::Equal
1895 );
1896
1897 assert_eq!(
1899 NetworkNode::compare_le_values("+Inf", "999"),
1900 Ordering::Greater
1901 );
1902 assert_eq!(
1903 NetworkNode::compare_le_values("0.1", "+Inf"),
1904 Ordering::Less
1905 );
1906 assert_eq!(
1907 NetworkNode::compare_le_values("+Inf", "+Inf"),
1908 Ordering::Equal
1909 );
1910
1911 assert_eq!(NetworkNode::compare_le_values("10", "100"), Ordering::Less);
1913 assert_eq!(
1914 NetworkNode::compare_le_values("1000", "999"),
1915 Ordering::Greater
1916 );
1917 }
1918
1919 fn mock_network_node(name: &str) -> NetworkNode {
1920 NetworkNode::new(
1921 name,
1922 "ws://127.0.0.1:9944",
1923 "http://127.0.0.1:9615/metrics",
1924 "/ip4/127.0.0.1/tcp/30333",
1925 NodeSpec::default(),
1926 Arc::new(MockNode::new()),
1927 GenCmdOptions::default(),
1928 NodeContext::Rc,
1929 )
1930 }
1931
1932 fn mock_jam_node(name: &str) -> JamNetworkNode {
1933 JamNetworkNode::new(
1934 name,
1935 Arc::new(MockNode::new()),
1936 crate::network_spec::jamnode::JamNodeSpec::default(),
1937 std::net::IpAddr::from([127, 0, 0, 1]),
1938 GenCmdOptions::default(),
1939 )
1940 }
1941
1942 #[test]
1946 fn test_network_node_serializes_flat() -> Result<(), anyhow::Error> {
1947 let value = serde_json::to_value(mock_network_node("alice"))?;
1948
1949 let obj = value.as_object().expect("node serializes as a map");
1950 assert_eq!(obj["name"], "alice");
1951 assert_eq!(obj["ws_uri"], "ws://127.0.0.1:9944");
1952 assert_eq!(obj["kind"], "substrate");
1953 assert!(obj.contains_key("inner"), "inner should be flattened in");
1954
1955 let raw: RawNetworkNode = serde_json::from_value(value)?;
1957 assert_eq!(raw.name, "alice");
1958 assert_eq!(raw.ws_uri, "ws://127.0.0.1:9944");
1959
1960 Ok(())
1961 }
1962
1963 #[test]
1964 fn test_jam_node_serializes_flat() -> Result<(), anyhow::Error> {
1965 let value = serde_json::to_value(mock_jam_node("jam-1"))?;
1966
1967 let obj = value.as_object().expect("node serializes as a map");
1968 assert_eq!(obj["name"], "jam-1");
1969 assert_eq!(obj["kind"], "jam");
1970
1971 let raw: jam::RawJamNetworkNode = serde_json::from_value(value)?;
1972 assert_eq!(raw.name, "jam-1");
1973
1974 Ok(())
1975 }
1976
1977 #[test]
1980 fn test_registry_holds_both_kinds_and_downcasts() {
1981 let mut registry: HashMap<String, Arc<dyn SpawnedNode>> = HashMap::new();
1982 registry.insert("alice".into(), Arc::new(mock_network_node("alice")));
1983 registry.insert("jam-1".into(), Arc::new(mock_jam_node("jam-1")));
1984
1985 let downcast = |name: &str| -> &dyn std::any::Any { registry[name].as_ref() };
1986
1987 let alice = downcast("alice")
1989 .downcast_ref::<NetworkNode>()
1990 .expect("alice is a substrate node");
1991 assert_eq!(alice.ws_uri(), "ws://127.0.0.1:9944");
1992
1993 let jam = downcast("jam-1")
1994 .downcast_ref::<JamNetworkNode>()
1995 .expect("jam-1 is a jam node");
1996 assert_eq!(jam.peer_addr(), "@127.0.0.1:0");
1997
1998 assert!(downcast("alice").downcast_ref::<JamNetworkNode>().is_none());
2000 assert!(downcast("jam-1").downcast_ref::<NetworkNode>().is_none());
2001 assert_eq!(registry["alice"].kind(), NodeKind::Substrate);
2002 assert_eq!(registry["jam-1"].kind(), NodeKind::Jam);
2003
2004 assert_eq!(registry["jam-1"].name(), "jam-1");
2006 assert!(!registry["jam-1"].is_running());
2007 }
2008}