forked from NationTech/harmony
feat: Improve output of tui. From p-r tui-score-info (#11)
WIP: formatted score debug print into a table with a name header and the score information below Co-authored-by: Jean-Gabriel Gill-Couture <jg@nationtech.io> Reviewed-on: https://git.nationtech.io/NationTech/harmony/pulls/11 Reviewed-by: johnride <jg@nationtech.io> Co-authored-by: Willem <wrolleman@nationtech.io> Co-committed-by: Willem <wrolleman@nationtech.io>
This commit is contained in:
parent
abd20b96a2
commit
eeafa086f3
3
Cargo.lock
generated
3
Cargo.lock
generated
@ -941,6 +941,7 @@ dependencies = [
|
|||||||
"env_logger",
|
"env_logger",
|
||||||
"harmony",
|
"harmony",
|
||||||
"harmony_macros",
|
"harmony_macros",
|
||||||
|
"harmony_tui",
|
||||||
"harmony_types",
|
"harmony_types",
|
||||||
"log",
|
"log",
|
||||||
"tokio",
|
"tokio",
|
||||||
@ -1339,6 +1340,8 @@ dependencies = [
|
|||||||
"log",
|
"log",
|
||||||
"log-panics",
|
"log-panics",
|
||||||
"ratatui",
|
"ratatui",
|
||||||
|
"serde-value",
|
||||||
|
"serde_json",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tokio-stream",
|
"tokio-stream",
|
||||||
"tui-logger",
|
"tui-logger",
|
||||||
|
@ -8,7 +8,7 @@ publish = false
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
harmony = { path = "../../harmony" }
|
harmony = { path = "../../harmony" }
|
||||||
#harmony_tui = { path = "../../harmony_tui" }
|
harmony_tui = { path = "../../harmony_tui" }
|
||||||
harmony_types = { path = "../../harmony_types" }
|
harmony_types = { path = "../../harmony_types" }
|
||||||
cidr = { workspace = true }
|
cidr = { workspace = true }
|
||||||
tokio = { workspace = true }
|
tokio = { workspace = true }
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
use harmony::{
|
use harmony::{
|
||||||
data::Version,
|
data::Version,
|
||||||
|
inventory::Inventory,
|
||||||
maestro::Maestro,
|
maestro::Maestro,
|
||||||
modules::lamp::{LAMPConfig, LAMPScore},
|
modules::lamp::{LAMPConfig, LAMPScore},
|
||||||
topology::{HAClusterTopology, Url},
|
topology::{HAClusterTopology, Url},
|
||||||
@ -17,8 +18,9 @@ async fn main() {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
Maestro::<HAClusterTopology>::load_from_env()
|
let inventory = Inventory::autoload();
|
||||||
.interpret(Box::new(lamp_stack))
|
let topology = HAClusterTopology::autoload();
|
||||||
.await
|
let mut maestro = Maestro::new(inventory, topology);
|
||||||
.unwrap();
|
maestro.register_all(vec![Box::new(lamp_stack)]);
|
||||||
|
harmony_tui::init(maestro).await.unwrap();
|
||||||
}
|
}
|
||||||
|
@ -1,9 +1,20 @@
|
|||||||
|
use std::net::{SocketAddr, SocketAddrV4};
|
||||||
|
|
||||||
use harmony::{
|
use harmony::{
|
||||||
inventory::Inventory,
|
inventory::Inventory,
|
||||||
maestro::Maestro,
|
maestro::Maestro,
|
||||||
modules::dummy::{ErrorScore, PanicScore, SuccessScore},
|
modules::{
|
||||||
topology::HAClusterTopology,
|
dns::DnsScore,
|
||||||
|
dummy::{ErrorScore, PanicScore, SuccessScore},
|
||||||
|
load_balancer::LoadBalancerScore,
|
||||||
|
okd::load_balancer::OKDLoadBalancerScore,
|
||||||
|
},
|
||||||
|
topology::{
|
||||||
|
BackendServer, HAClusterTopology, HealthCheck, HttpMethod, HttpStatusCode,
|
||||||
|
LoadBalancerService,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
use harmony_macros::ipv4;
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() {
|
async fn main() {
|
||||||
@ -11,10 +22,52 @@ async fn main() {
|
|||||||
let topology = HAClusterTopology::autoload();
|
let topology = HAClusterTopology::autoload();
|
||||||
let mut maestro = Maestro::new(inventory, topology);
|
let mut maestro = Maestro::new(inventory, topology);
|
||||||
|
|
||||||
|
|
||||||
maestro.register_all(vec![
|
maestro.register_all(vec![
|
||||||
Box::new(SuccessScore {}),
|
Box::new(SuccessScore {}),
|
||||||
Box::new(ErrorScore {}),
|
Box::new(ErrorScore {}),
|
||||||
Box::new(PanicScore {}),
|
Box::new(PanicScore {}),
|
||||||
|
Box::new(DnsScore::new(vec![], None)),
|
||||||
|
Box::new(build_large_score()),
|
||||||
]);
|
]);
|
||||||
harmony_tui::init(maestro).await.unwrap();
|
harmony_tui::init(maestro).await.unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn build_large_score() -> LoadBalancerScore {
|
||||||
|
let backend_server = BackendServer {
|
||||||
|
address: "192.168.0.0".to_string(),
|
||||||
|
port: 342,
|
||||||
|
};
|
||||||
|
let lb_service = LoadBalancerService {
|
||||||
|
backend_servers: vec![
|
||||||
|
backend_server.clone(),
|
||||||
|
backend_server.clone(),
|
||||||
|
backend_server.clone(),
|
||||||
|
],
|
||||||
|
listening_port: SocketAddr::V4(SocketAddrV4::new(ipv4!("192.168.0.0"), 49387)),
|
||||||
|
health_check: Some(HealthCheck::HTTP(
|
||||||
|
"/some_long_ass_path_to_see_how_it_is_displayed_but_it_has_to_be_even_longer"
|
||||||
|
.to_string(),
|
||||||
|
HttpMethod::GET,
|
||||||
|
HttpStatusCode::Success2xx,
|
||||||
|
)),
|
||||||
|
};
|
||||||
|
LoadBalancerScore {
|
||||||
|
public_services: vec![
|
||||||
|
lb_service.clone(),
|
||||||
|
lb_service.clone(),
|
||||||
|
lb_service.clone(),
|
||||||
|
lb_service.clone(),
|
||||||
|
lb_service.clone(),
|
||||||
|
lb_service.clone(),
|
||||||
|
],
|
||||||
|
private_services: vec![
|
||||||
|
lb_service.clone(),
|
||||||
|
lb_service.clone(),
|
||||||
|
lb_service.clone(),
|
||||||
|
lb_service.clone(),
|
||||||
|
lb_service.clone(),
|
||||||
|
lb_service.clone(),
|
||||||
|
],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -1,10 +1,12 @@
|
|||||||
|
use std::collections::BTreeMap;
|
||||||
|
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use serde_value::Value;
|
use serde_value::Value;
|
||||||
|
|
||||||
use super::{interpret::Interpret, topology::Topology};
|
use super::{interpret::Interpret, topology::Topology};
|
||||||
|
|
||||||
pub trait Score<T: Topology>:
|
pub trait Score<T: Topology>:
|
||||||
std::fmt::Debug + Send + Sync + CloneBoxScore<T> + SerializeScore<T>
|
std::fmt::Debug + ScoreToString<T> + Send + Sync + CloneBoxScore<T> + SerializeScore<T>
|
||||||
{
|
{
|
||||||
fn create_interpret(&self) -> Box<dyn Interpret<T>>;
|
fn create_interpret(&self) -> Box<dyn Interpret<T>>;
|
||||||
fn name(&self) -> String;
|
fn name(&self) -> String;
|
||||||
@ -39,3 +41,191 @@ where
|
|||||||
Box::new(self.clone())
|
Box::new(self.clone())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub trait ScoreToString<T: Topology> {
|
||||||
|
fn print_score_details(&self) -> String;
|
||||||
|
fn format_value_as_string(&self, val: &Value, indent: usize) -> String;
|
||||||
|
fn format_map(&self, map: &BTreeMap<Value, Value>, indent: usize) -> String;
|
||||||
|
fn wrap_or_truncate(&self, s: &str, width: usize) -> Vec<String>;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<S, T> ScoreToString<T> for S
|
||||||
|
where
|
||||||
|
T: Topology,
|
||||||
|
S: Score<T> + 'static,
|
||||||
|
{
|
||||||
|
fn print_score_details(&self) -> String {
|
||||||
|
let mut output = String::new();
|
||||||
|
output += "\n";
|
||||||
|
output += &self.format_value_as_string(&self.serialize(), 0);
|
||||||
|
output += "\n";
|
||||||
|
output
|
||||||
|
}
|
||||||
|
fn format_map(&self, map: &BTreeMap<Value, Value>, indent: usize) -> String {
|
||||||
|
let pad = " ".repeat(indent * 2);
|
||||||
|
let mut output = String::new();
|
||||||
|
|
||||||
|
output += &format!(
|
||||||
|
"{}+--------------------------+--------------------------------------------------+\n",
|
||||||
|
pad
|
||||||
|
);
|
||||||
|
output += &format!("{}| {:<24} | {:<48} |\n", pad, "score_name", self.name());
|
||||||
|
output += &format!(
|
||||||
|
"{}+--------------------------+--------------------------------------------------+\n",
|
||||||
|
pad
|
||||||
|
);
|
||||||
|
|
||||||
|
for (k, v) in map {
|
||||||
|
let key_str = match k {
|
||||||
|
Value::String(s) => s.clone(),
|
||||||
|
other => format!("{:?}", other),
|
||||||
|
};
|
||||||
|
|
||||||
|
let formatted_val = self.format_value_as_string(v, indent + 1);
|
||||||
|
let mut lines = formatted_val.lines().map(|line| line.trim_start());
|
||||||
|
|
||||||
|
let wrapped_lines: Vec<_> = lines
|
||||||
|
.flat_map(|line| self.wrap_or_truncate(line.trim_start(), 48))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
if let Some(first) = wrapped_lines.first() {
|
||||||
|
output += &format!("{}| {:<24} | {:<48} |\n", pad, key_str, first);
|
||||||
|
for line in &wrapped_lines[1..] {
|
||||||
|
output += &format!("{}| {:<24} | {:<48} |\n", pad, "", line);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// let first_line = lines.next().unwrap_or("");
|
||||||
|
// output += &format!("{}| {:<24} | {:<48} |\n", pad, key_str, first_line);
|
||||||
|
//
|
||||||
|
// for line in lines {
|
||||||
|
// output += &format!("{}| {:<24} | {:<48} |\n", pad, "", line);
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
|
||||||
|
output += &format!(
|
||||||
|
"{}+--------------------------+--------------------------------------------------+\n\n",
|
||||||
|
pad
|
||||||
|
);
|
||||||
|
|
||||||
|
output
|
||||||
|
}
|
||||||
|
|
||||||
|
fn wrap_or_truncate(&self, s: &str, width: usize) -> Vec<String> {
|
||||||
|
let mut lines = Vec::new();
|
||||||
|
let mut current = s;
|
||||||
|
|
||||||
|
while !current.is_empty() {
|
||||||
|
if current.len() <= width {
|
||||||
|
lines.push(current.to_string());
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to wrap at whitespace if possible
|
||||||
|
let mut split_index = current[..width].rfind(' ').unwrap_or(width);
|
||||||
|
if split_index == 0 {
|
||||||
|
split_index = width;
|
||||||
|
}
|
||||||
|
|
||||||
|
lines.push(current[..split_index].trim_end().to_string());
|
||||||
|
current = current[split_index..].trim_start();
|
||||||
|
}
|
||||||
|
|
||||||
|
lines
|
||||||
|
}
|
||||||
|
|
||||||
|
fn format_value_as_string(&self, val: &Value, indent: usize) -> String {
|
||||||
|
let pad = " ".repeat(indent * 2);
|
||||||
|
let mut output = String::new();
|
||||||
|
|
||||||
|
match val {
|
||||||
|
Value::Bool(b) => output += &format!("{}{}\n", pad, b),
|
||||||
|
Value::U8(u) => output += &format!("{}{}\n", pad, u),
|
||||||
|
Value::U16(u) => output += &format!("{}{}\n", pad, u),
|
||||||
|
Value::U32(u) => output += &format!("{}{}\n", pad, u),
|
||||||
|
Value::U64(u) => output += &format!("{}{}\n", pad, u),
|
||||||
|
Value::I8(i) => output += &format!("{}{}\n", pad, i),
|
||||||
|
Value::I16(i) => output += &format!("{}{}\n", pad, i),
|
||||||
|
Value::I32(i) => output += &format!("{}{}\n", pad, i),
|
||||||
|
Value::I64(i) => output += &format!("{}{}\n", pad, i),
|
||||||
|
Value::F32(f) => output += &format!("{}{}\n", pad, f),
|
||||||
|
Value::F64(f) => output += &format!("{}{}\n", pad, f),
|
||||||
|
Value::Char(c) => output += &format!("{}{}\n", pad, c),
|
||||||
|
Value::String(s) => output += &format!("{}{:<48}\n", pad, s),
|
||||||
|
Value::Unit => output += &format!("{}<unit>\n", pad),
|
||||||
|
Value::Bytes(bytes) => output += &format!("{}{:?}\n", pad, bytes),
|
||||||
|
|
||||||
|
Value::Option(opt) => match opt {
|
||||||
|
Some(inner) => {
|
||||||
|
output += &format!("{}Option:\n", pad);
|
||||||
|
output += &self.format_value_as_string(inner, indent + 1);
|
||||||
|
}
|
||||||
|
None => output += &format!("{}None\n", pad),
|
||||||
|
},
|
||||||
|
|
||||||
|
Value::Newtype(inner) => {
|
||||||
|
output += &format!("{}Newtype:\n", pad);
|
||||||
|
output += &self.format_value_as_string(inner, indent + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
Value::Seq(seq) => {
|
||||||
|
if seq.is_empty() {
|
||||||
|
output += &format!("{}[]\n", pad);
|
||||||
|
} else {
|
||||||
|
output += &format!("{}[\n", pad);
|
||||||
|
for item in seq {
|
||||||
|
output += &self.format_value_as_string(item, indent + 1);
|
||||||
|
}
|
||||||
|
output += &format!("{}]\n", pad);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Value::Map(map) => {
|
||||||
|
if map.is_empty() {
|
||||||
|
output += &format!("{}<empty map>\n", pad);
|
||||||
|
} else if indent == 0 {
|
||||||
|
output += &self.format_map(map, indent);
|
||||||
|
} else {
|
||||||
|
for (k, v) in map {
|
||||||
|
let key_str = match k {
|
||||||
|
Value::String(s) => s.clone(),
|
||||||
|
other => format!("{:?}", other),
|
||||||
|
};
|
||||||
|
|
||||||
|
let val_str = self
|
||||||
|
.format_value_as_string(v, indent + 1)
|
||||||
|
.trim()
|
||||||
|
.to_string();
|
||||||
|
let val_lines: Vec<_> = val_str.lines().collect();
|
||||||
|
|
||||||
|
output +=
|
||||||
|
&format!("{}{}: {}\n", pad, key_str, val_lines.first().unwrap_or(&""));
|
||||||
|
for line in val_lines.iter().skip(1) {
|
||||||
|
output += &format!("{} {}\n", pad, line);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
output
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//TODO write test to check that the output is what it should be
|
||||||
|
//
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::modules::dns::DnsScore;
|
||||||
|
use crate::topology::{self, HAClusterTopology};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_format_values_as_string() {
|
||||||
|
let dns_score = Box::new(DnsScore::new(vec![], None));
|
||||||
|
let print_score_output =
|
||||||
|
<DnsScore as ScoreToString<HAClusterTopology>>::print_score_details(&dns_score);
|
||||||
|
let expected_empty_dns_score_table = "\n+--------------------------+--------------------------------------------------+\n| score_name | DnsScore |\n+--------------------------+--------------------------------------------------+\n| dns_entries | [] |\n| register_dhcp_leases | None |\n+--------------------------+--------------------------------------------------+\n\n\n";
|
||||||
|
assert_eq!(print_score_output, expected_empty_dns_score_table);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -46,6 +46,7 @@ pub struct LoadBalancerService {
|
|||||||
|
|
||||||
#[derive(Debug, PartialEq, Clone, Serialize)]
|
#[derive(Debug, PartialEq, Clone, Serialize)]
|
||||||
pub struct BackendServer {
|
pub struct BackendServer {
|
||||||
|
// TODO should not be a string, probably IPAddress
|
||||||
pub address: String,
|
pub address: String,
|
||||||
pub port: u16,
|
pub port: u16,
|
||||||
}
|
}
|
||||||
|
@ -16,3 +16,5 @@ color-eyre = "0.6.3"
|
|||||||
tokio-stream = "0.1.17"
|
tokio-stream = "0.1.17"
|
||||||
tui-logger = "0.14.1"
|
tui-logger = "0.14.1"
|
||||||
log-panics = "2.1.0"
|
log-panics = "2.1.0"
|
||||||
|
serde-value.workspace = true
|
||||||
|
serde_json = "1.0.140"
|
||||||
|
@ -159,12 +159,13 @@ impl<T: Topology + std::fmt::Debug + Send + Sync + 'static> HarmonyTUI<T> {
|
|||||||
frame.render_widget(&help_block, help_area);
|
frame.render_widget(&help_block, help_area);
|
||||||
frame.render_widget(HelpWidget::new(), help_block.inner(help_area));
|
frame.render_widget(HelpWidget::new(), help_block.inner(help_area));
|
||||||
|
|
||||||
let [list_area, output_area] =
|
let [list_area, logger_area] =
|
||||||
Layout::horizontal([Constraint::Min(30), Constraint::Percentage(100)]).areas(app_area);
|
Layout::horizontal([Constraint::Min(30), Constraint::Percentage(100)]).areas(app_area);
|
||||||
|
|
||||||
let block = Block::default().borders(Borders::RIGHT);
|
let block = Block::default().borders(Borders::RIGHT);
|
||||||
frame.render_widget(&block, list_area);
|
frame.render_widget(&block, list_area);
|
||||||
self.score.render(list_area, frame);
|
self.score.render(list_area, frame);
|
||||||
|
|
||||||
let tui_logger = tui_logger::TuiLoggerWidget::default()
|
let tui_logger = tui_logger::TuiLoggerWidget::default()
|
||||||
.style_error(Style::default().fg(Color::Red))
|
.style_error(Style::default().fg(Color::Red))
|
||||||
.style_warn(Style::default().fg(Color::LightRed))
|
.style_warn(Style::default().fg(Color::LightRed))
|
||||||
@ -172,9 +173,9 @@ impl<T: Topology + std::fmt::Debug + Send + Sync + 'static> HarmonyTUI<T> {
|
|||||||
.style_debug(Style::default().fg(Color::Gray))
|
.style_debug(Style::default().fg(Color::Gray))
|
||||||
.style_trace(Style::default().fg(Color::Gray))
|
.style_trace(Style::default().fg(Color::Gray))
|
||||||
.state(&self.tui_state);
|
.state(&self.tui_state);
|
||||||
frame.render_widget(tui_logger, output_area)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
frame.render_widget(tui_logger, logger_area);
|
||||||
|
}
|
||||||
fn scores_list(maestro: &Maestro<T>) -> Vec<Box<dyn Score<T>>> {
|
fn scores_list(maestro: &Maestro<T>) -> Vec<Box<dyn Score<T>>> {
|
||||||
let scores = maestro.scores();
|
let scores = maestro.scores();
|
||||||
let scores_read = scores.read().expect("Should be able to read scores");
|
let scores_read = scores.read().expect("Should be able to read scores");
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
use std::sync::{Arc, RwLock};
|
use std::sync::{Arc, RwLock};
|
||||||
|
|
||||||
|
use crate::HarmonyTuiEvent;
|
||||||
use crossterm::event::{Event, KeyCode, KeyEventKind};
|
use crossterm::event::{Event, KeyCode, KeyEventKind};
|
||||||
use harmony::{score::Score, topology::Topology};
|
use harmony::{score::Score, topology::Topology};
|
||||||
use log::{info, warn};
|
use log::{info, warn};
|
||||||
@ -11,8 +12,6 @@ use ratatui::{
|
|||||||
};
|
};
|
||||||
use tokio::sync::mpsc;
|
use tokio::sync::mpsc;
|
||||||
|
|
||||||
use crate::HarmonyTuiEvent;
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
enum ExecutionState {
|
enum ExecutionState {
|
||||||
INITIATED,
|
INITIATED,
|
||||||
@ -53,23 +52,27 @@ impl<T: Topology + std::fmt::Debug> ScoreListWidget<T> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn launch_execution(&mut self) {
|
pub(crate) fn launch_execution(&mut self) {
|
||||||
let list_read = self.list_state.read().unwrap();
|
if let Some(score) = self.get_selected_score() {
|
||||||
if let Some(index) = list_read.selected() {
|
|
||||||
let score = self
|
|
||||||
.scores
|
|
||||||
.get(index)
|
|
||||||
.expect("List state should always match with internal Vec");
|
|
||||||
|
|
||||||
self.execution = Some(Execution {
|
self.execution = Some(Execution {
|
||||||
state: ExecutionState::INITIATED,
|
state: ExecutionState::INITIATED,
|
||||||
score: score.clone_box(),
|
score: score.clone_box(),
|
||||||
});
|
});
|
||||||
info!("{:#?}\n\nConfirm Execution (Press y/n)", score);
|
info!("{}\n\nConfirm Execution (Press y/n)", score.name());
|
||||||
|
info!("{}", score.print_score_details());
|
||||||
} else {
|
} else {
|
||||||
warn!("No Score selected, nothing to launch");
|
warn!("No Score selected, nothing to launch");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn get_selected_score(&self) -> Option<Box<dyn Score<T>>> {
|
||||||
|
let list_read = self.list_state.read().unwrap();
|
||||||
|
if let Some(index) = list_read.selected() {
|
||||||
|
self.scores.get(index).map(|s| s.clone_box())
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) fn scroll_down(&self) {
|
pub(crate) fn scroll_down(&self) {
|
||||||
self.list_state.write().unwrap().scroll_down_by(1);
|
self.list_state.write().unwrap().scroll_down_by(1);
|
||||||
}
|
}
|
||||||
|
@ -12,6 +12,7 @@ mod test {
|
|||||||
use crate::Config;
|
use crate::Config;
|
||||||
use pretty_assertions::assert_eq;
|
use pretty_assertions::assert_eq;
|
||||||
|
|
||||||
|
#[cfg(opnsenseendtoend)]
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_public_sdk() {
|
async fn test_public_sdk() {
|
||||||
let mac = "11:22:33:44:55:66";
|
let mac = "11:22:33:44:55:66";
|
||||||
|
Loading…
Reference in New Issue
Block a user