added more helper func, cfg validation and testing
Release / Build and Release (push) Successful in 1m4s
Release / Build and Release (push) Successful in 1m4s
This commit is contained in:
+147
-1
@@ -1,6 +1,8 @@
|
||||
use regex::Regex;
|
||||
use serde::Deserialize;
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::LazyLock;
|
||||
use tracing::{debug, info, warn};
|
||||
|
||||
#[derive(Deserialize, Default)]
|
||||
@@ -224,6 +226,80 @@ impl Default for GameConfig {
|
||||
}
|
||||
}
|
||||
|
||||
static TOKEN_RE: LazyLock<Regex> =
|
||||
LazyLock::new(|| Regex::new(r"\{([a-zA-Z0-9_]+)(?::([<>\^])?(\d+)?(?:\.(\d+))?)?\}").unwrap());
|
||||
|
||||
fn extract_tokens(format_str: &str) -> Vec<String> {
|
||||
TOKEN_RE
|
||||
.captures_iter(format_str)
|
||||
.map(|cap| cap[1].to_string())
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn validate_format(label: &str, format_str: &str, known_tokens: &[&str]) {
|
||||
for token in extract_tokens(format_str) {
|
||||
if !known_tokens.contains(&token.as_str()) {
|
||||
warn!(
|
||||
"Config [{}]: unknown token '{{{}}}' in format string. Known tokens: {:?}",
|
||||
label, token, known_tokens
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Config {
|
||||
pub fn validate(&self) {
|
||||
validate_format(
|
||||
"network",
|
||||
&self.network.format,
|
||||
&["interface", "ip", "rx", "tx"],
|
||||
);
|
||||
validate_format("cpu", &self.cpu.format, &["usage", "temp"]);
|
||||
validate_format("memory", &self.memory.format, &["used", "total"]);
|
||||
validate_format(
|
||||
"gpu.amd",
|
||||
&self.gpu.format_amd,
|
||||
&["usage", "vram_used", "vram_total", "temp"],
|
||||
);
|
||||
validate_format("gpu.intel", &self.gpu.format_intel, &["usage", "freq"]);
|
||||
validate_format(
|
||||
"gpu.nvidia",
|
||||
&self.gpu.format_nvidia,
|
||||
&["usage", "vram_used", "vram_total", "temp"],
|
||||
);
|
||||
validate_format(
|
||||
"sys",
|
||||
&self.sys.format,
|
||||
&["uptime", "load1", "load5", "load15", "procs"],
|
||||
);
|
||||
validate_format("disk", &self.disk.format, &["mount", "used", "total"]);
|
||||
validate_format("pool", &self.pool.format, &["used", "total"]);
|
||||
validate_format("power", &self.power.format, &["percentage", "icon"]);
|
||||
validate_format("buds", &self.buds.format, &["left", "right", "anc"]);
|
||||
validate_format(
|
||||
"audio.sink_unmuted",
|
||||
&self.audio.format_sink_unmuted,
|
||||
&["name", "icon", "volume"],
|
||||
);
|
||||
validate_format(
|
||||
"audio.sink_muted",
|
||||
&self.audio.format_sink_muted,
|
||||
&["name", "icon"],
|
||||
);
|
||||
validate_format(
|
||||
"audio.source_unmuted",
|
||||
&self.audio.format_source_unmuted,
|
||||
&["name", "icon", "volume"],
|
||||
);
|
||||
validate_format(
|
||||
"audio.source_muted",
|
||||
&self.audio.format_source_muted,
|
||||
&["name", "icon"],
|
||||
);
|
||||
validate_format("bt.connected", &self.bt.format_connected, &["alias"]);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn load_config(custom_path: Option<PathBuf>) -> Config {
|
||||
let config_path = custom_path.unwrap_or_else(|| {
|
||||
let config_dir = std::env::var("XDG_CONFIG_HOME")
|
||||
@@ -236,9 +312,10 @@ pub fn load_config(custom_path: Option<PathBuf>) -> Config {
|
||||
});
|
||||
|
||||
if let Ok(content) = fs::read_to_string(&config_path) {
|
||||
match toml::from_str(&content) {
|
||||
match toml::from_str::<Config>(&content) {
|
||||
Ok(cfg) => {
|
||||
info!("Successfully loaded configuration from {:?}", config_path);
|
||||
cfg.validate();
|
||||
cfg
|
||||
}
|
||||
Err(e) => {
|
||||
@@ -255,3 +332,72 @@ pub fn load_config(custom_path: Option<PathBuf>) -> Config {
|
||||
Config::default()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::io::Write;
|
||||
|
||||
#[test]
|
||||
fn test_default_config() {
|
||||
let config = Config::default();
|
||||
assert_eq!(
|
||||
config.general.menu_command,
|
||||
"fuzzel --dmenu --prompt '{prompt}'"
|
||||
);
|
||||
assert!(config.cpu.format.contains("usage"));
|
||||
assert!(config.cpu.format.contains("temp"));
|
||||
assert!(config.memory.format.contains("used"));
|
||||
assert!(config.memory.format.contains("total"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_load_missing_config() {
|
||||
let config = load_config(Some(PathBuf::from("/nonexistent/config.toml")));
|
||||
// Should fallback to defaults without panicking
|
||||
assert_eq!(
|
||||
config.general.menu_command,
|
||||
"fuzzel --dmenu --prompt '{prompt}'"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_load_valid_partial_config() {
|
||||
let mut tmpfile = tempfile::NamedTempFile::new().unwrap();
|
||||
// In TOML, braces have no special meaning in strings
|
||||
writeln!(tmpfile, "[cpu]").unwrap();
|
||||
writeln!(tmpfile, "format = \"custom: {{usage}}\"").unwrap();
|
||||
|
||||
let config = load_config(Some(tmpfile.path().to_path_buf()));
|
||||
// TOML treats {{ as literal {{ (no escape), so the value is "custom: {{usage}}"
|
||||
assert!(config.cpu.format.contains("usage"));
|
||||
// Other sections still have defaults
|
||||
assert!(config.memory.format.contains("used"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_load_invalid_toml() {
|
||||
let mut tmpfile = tempfile::NamedTempFile::new().unwrap();
|
||||
write!(tmpfile, "this is not valid toml {{{{").unwrap();
|
||||
|
||||
let config = load_config(Some(tmpfile.path().to_path_buf()));
|
||||
// Should fallback to defaults
|
||||
assert_eq!(
|
||||
config.general.menu_command,
|
||||
"fuzzel --dmenu --prompt '{prompt}'"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_load_empty_config() {
|
||||
let tmpfile = tempfile::NamedTempFile::new().unwrap();
|
||||
// Empty file is valid TOML, all sections default
|
||||
|
||||
let config = load_config(Some(tmpfile.path().to_path_buf()));
|
||||
assert_eq!(
|
||||
config.general.menu_command,
|
||||
"fuzzel --dmenu --prompt '{prompt}'"
|
||||
);
|
||||
assert!(config.cpu.format.contains("usage"));
|
||||
}
|
||||
}
|
||||
|
||||
+19
-16
@@ -27,14 +27,15 @@ impl WaybarModule for BtModule {
|
||||
}
|
||||
|
||||
if let Ok(stdout) = run_command("bluetoothctl", &["show"])
|
||||
&& stdout.contains("Powered: no") {
|
||||
return Ok(WaybarOutput {
|
||||
text: config.bt.format_disabled.clone(),
|
||||
tooltip: Some("Bluetooth Disabled".to_string()),
|
||||
class: Some("disabled".to_string()),
|
||||
percentage: None,
|
||||
});
|
||||
}
|
||||
&& stdout.contains("Powered: no")
|
||||
{
|
||||
return Ok(WaybarOutput {
|
||||
text: config.bt.format_disabled.clone(),
|
||||
tooltip: Some("Bluetooth Disabled".to_string()),
|
||||
class: Some("disabled".to_string()),
|
||||
percentage: None,
|
||||
});
|
||||
}
|
||||
|
||||
if let Some(mac) = find_audio_device() {
|
||||
let info = run_command("bluetoothctl", &["info", &mac])?;
|
||||
@@ -90,12 +91,13 @@ impl WaybarModule for BtModule {
|
||||
|
||||
fn find_audio_device() -> Option<String> {
|
||||
if let Ok(sink) = run_command("pactl", &["get-default-sink"])
|
||||
&& sink.starts_with("bluez_output.") {
|
||||
let parts: Vec<&str> = sink.split('.').collect();
|
||||
if parts.len() >= 2 {
|
||||
return Some(parts[1].replace('_', ":"));
|
||||
}
|
||||
&& sink.starts_with("bluez_output.")
|
||||
{
|
||||
let parts: Vec<&str> = sink.split('.').collect();
|
||||
if parts.len() >= 2 {
|
||||
return Some(parts[1].replace('_', ":"));
|
||||
}
|
||||
}
|
||||
|
||||
if let Ok(stdout) = run_command("bluetoothctl", &["devices", "Connected"]) {
|
||||
for line in stdout.lines() {
|
||||
@@ -104,9 +106,10 @@ fn find_audio_device() -> Option<String> {
|
||||
if parts.len() >= 2 {
|
||||
let mac = parts[1];
|
||||
if let Ok(info_str) = run_command("bluetoothctl", &["info", mac])
|
||||
&& info_str.contains("0000110b-0000-1000-8000-00805f9b34fb") {
|
||||
return Some(mac.to_string());
|
||||
}
|
||||
&& info_str.contains("0000110b-0000-1000-8000-00805f9b34fb")
|
||||
{
|
||||
return Some(mac.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+18
-28
@@ -2,9 +2,8 @@ use crate::config::Config;
|
||||
use crate::modules::WaybarModule;
|
||||
use crate::output::WaybarOutput;
|
||||
use crate::state::SharedState;
|
||||
use crate::utils::{TokenValue, format_template};
|
||||
use crate::utils::{TokenValue, format_template, run_command};
|
||||
use anyhow::Result;
|
||||
use std::process::Command;
|
||||
|
||||
pub struct BudsModule;
|
||||
|
||||
@@ -15,8 +14,7 @@ impl WaybarModule for BudsModule {
|
||||
|
||||
match *action {
|
||||
"cycle_anc" => {
|
||||
let output = Command::new("pbpctrl").args(["get", "anc"]).output()?;
|
||||
let current_mode = String::from_utf8_lossy(&output.stdout).trim().to_string();
|
||||
let current_mode = run_command("pbpctrl", &["get", "anc"])?;
|
||||
|
||||
let next_mode = match current_mode.as_str() {
|
||||
"active" => "aware",
|
||||
@@ -24,9 +22,7 @@ impl WaybarModule for BudsModule {
|
||||
_ => "active",
|
||||
};
|
||||
|
||||
Command::new("pbpctrl")
|
||||
.args(["set", "anc", next_mode])
|
||||
.status()?;
|
||||
let _ = run_command("pbpctrl", &["set", "anc", next_mode]);
|
||||
return Ok(WaybarOutput {
|
||||
text: String::new(),
|
||||
tooltip: None,
|
||||
@@ -35,9 +31,7 @@ impl WaybarModule for BudsModule {
|
||||
});
|
||||
}
|
||||
"connect" => {
|
||||
Command::new("bluetoothctl")
|
||||
.args(["connect", mac])
|
||||
.status()?;
|
||||
let _ = run_command("bluetoothctl", &["connect", mac]);
|
||||
return Ok(WaybarOutput {
|
||||
text: String::new(),
|
||||
tooltip: None,
|
||||
@@ -46,9 +40,7 @@ impl WaybarModule for BudsModule {
|
||||
});
|
||||
}
|
||||
"disconnect" => {
|
||||
Command::new("bluetoothctl")
|
||||
.args(["disconnect", mac])
|
||||
.status()?;
|
||||
let _ = run_command("bluetoothctl", &["disconnect", mac]);
|
||||
return Ok(WaybarOutput {
|
||||
text: String::new(),
|
||||
tooltip: None,
|
||||
@@ -62,8 +54,7 @@ impl WaybarModule for BudsModule {
|
||||
}
|
||||
}
|
||||
|
||||
let bt_info = Command::new("bluetoothctl").args(["info", mac]).output()?;
|
||||
let bt_str = String::from_utf8_lossy(&bt_info.stdout);
|
||||
let bt_str = run_command("bluetoothctl", &["info", mac])?;
|
||||
|
||||
if !bt_str.contains("Connected: yes") {
|
||||
return Ok(WaybarOutput {
|
||||
@@ -74,18 +65,18 @@ impl WaybarModule for BudsModule {
|
||||
});
|
||||
}
|
||||
|
||||
let bat_cmd = Command::new("pbpctrl").args(["show", "battery"]).output();
|
||||
if bat_cmd.is_err() || !bat_cmd.as_ref().unwrap().status.success() {
|
||||
return Ok(WaybarOutput {
|
||||
text: config.buds.format_disconnected.clone(),
|
||||
tooltip: Some("Pixel Buds Pro 2 connected (No Data)".to_string()),
|
||||
class: Some("disconnected".to_string()),
|
||||
percentage: None,
|
||||
});
|
||||
}
|
||||
let bat_output = match run_command("pbpctrl", &["show", "battery"]) {
|
||||
Ok(output) => output,
|
||||
Err(_) => {
|
||||
return Ok(WaybarOutput {
|
||||
text: config.buds.format_disconnected.clone(),
|
||||
tooltip: Some("Pixel Buds Pro 2 connected (No Data)".to_string()),
|
||||
class: Some("disconnected".to_string()),
|
||||
percentage: None,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
let bat_result = bat_cmd.unwrap();
|
||||
let bat_output = String::from_utf8_lossy(&bat_result.stdout);
|
||||
let mut left_bud = "unknown";
|
||||
let mut right_bud = "unknown";
|
||||
|
||||
@@ -117,8 +108,7 @@ impl WaybarModule for BudsModule {
|
||||
format!("{}%", right_bud)
|
||||
};
|
||||
|
||||
let anc_cmd = Command::new("pbpctrl").args(["get", "anc"]).output()?;
|
||||
let current_mode = String::from_utf8_lossy(&anc_cmd.stdout).trim().to_string();
|
||||
let current_mode = run_command("pbpctrl", &["get", "anc"]).unwrap_or_default();
|
||||
|
||||
let (anc_icon, class) = match current_mode.as_str() {
|
||||
"active" => ("ANC", "anc-active"),
|
||||
|
||||
@@ -45,3 +45,58 @@ impl WaybarModule for CpuModule {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::state::{AppState, CpuState, mock_state};
|
||||
|
||||
#[test]
|
||||
fn test_cpu_normal() {
|
||||
let state = mock_state(AppState {
|
||||
cpu: CpuState {
|
||||
usage: 25.0,
|
||||
temp: 45.0,
|
||||
model: "Test CPU".into(),
|
||||
},
|
||||
..Default::default()
|
||||
});
|
||||
let config = Config::default();
|
||||
let output = CpuModule.run(&config, &state, &[]).unwrap();
|
||||
assert!(output.text.contains("25.0"));
|
||||
assert!(output.text.contains("45.0"));
|
||||
assert_eq!(output.class.as_deref(), Some("normal"));
|
||||
assert_eq!(output.percentage, Some(25));
|
||||
assert_eq!(output.tooltip.as_deref(), Some("Test CPU"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_cpu_high() {
|
||||
let state = mock_state(AppState {
|
||||
cpu: CpuState {
|
||||
usage: 80.0,
|
||||
temp: 70.0,
|
||||
model: "Test".into(),
|
||||
},
|
||||
..Default::default()
|
||||
});
|
||||
let config = Config::default();
|
||||
let output = CpuModule.run(&config, &state, &[]).unwrap();
|
||||
assert_eq!(output.class.as_deref(), Some("high"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_cpu_max() {
|
||||
let state = mock_state(AppState {
|
||||
cpu: CpuState {
|
||||
usage: 99.0,
|
||||
temp: 95.0,
|
||||
model: "Test".into(),
|
||||
},
|
||||
..Default::default()
|
||||
});
|
||||
let config = Config::default();
|
||||
let output = CpuModule.run(&config, &state, &[]).unwrap();
|
||||
assert_eq!(output.class.as_deref(), Some("max"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,3 +65,48 @@ impl WaybarModule for DiskModule {
|
||||
Err(anyhow::anyhow!("Mountpoint {} not found", mountpoint))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::state::{AppState, DiskInfo, mock_state};
|
||||
|
||||
fn state_with_disk(mount: &str, total: u64, available: u64) -> crate::state::SharedState {
|
||||
mock_state(AppState {
|
||||
disks: vec![DiskInfo {
|
||||
mount_point: mount.to_string(),
|
||||
filesystem: "ext4".to_string(),
|
||||
total_bytes: total,
|
||||
available_bytes: available,
|
||||
}],
|
||||
..Default::default()
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_disk_found() {
|
||||
let gb = 1024 * 1024 * 1024;
|
||||
let state = state_with_disk("/", 100 * gb, 60 * gb);
|
||||
let config = Config::default();
|
||||
let output = DiskModule.run(&config, &state, &["/"]).unwrap();
|
||||
assert_eq!(output.class.as_deref(), Some("normal"));
|
||||
assert_eq!(output.percentage, Some(40)); // 40% used
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_disk_high() {
|
||||
let gb = 1024 * 1024 * 1024;
|
||||
let state = state_with_disk("/", 100 * gb, 15 * gb);
|
||||
let config = Config::default();
|
||||
let output = DiskModule.run(&config, &state, &["/"]).unwrap();
|
||||
assert_eq!(output.class.as_deref(), Some("high")); // 85% used
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_disk_not_found() {
|
||||
let state = mock_state(AppState::default());
|
||||
let config = Config::default();
|
||||
let result = DiskModule.run(&config, &state, &["/nonexistent"]);
|
||||
assert!(result.is_err());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,3 +47,55 @@ impl WaybarModule for MemoryModule {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::state::{AppState, MemoryState, mock_state};
|
||||
|
||||
#[test]
|
||||
fn test_memory_normal() {
|
||||
let state = mock_state(AppState {
|
||||
memory: MemoryState {
|
||||
used_gb: 8.0,
|
||||
total_gb: 32.0,
|
||||
},
|
||||
..Default::default()
|
||||
});
|
||||
let config = Config::default();
|
||||
let output = MemoryModule.run(&config, &state, &[]).unwrap();
|
||||
assert!(output.text.contains("8.00"));
|
||||
assert!(output.text.contains("32.00"));
|
||||
assert_eq!(output.class.as_deref(), Some("normal"));
|
||||
assert_eq!(output.percentage, Some(25)); // 8/32 = 25%
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_memory_high() {
|
||||
let state = mock_state(AppState {
|
||||
memory: MemoryState {
|
||||
used_gb: 26.0,
|
||||
total_gb: 32.0,
|
||||
},
|
||||
..Default::default()
|
||||
});
|
||||
let config = Config::default();
|
||||
let output = MemoryModule.run(&config, &state, &[]).unwrap();
|
||||
assert_eq!(output.class.as_deref(), Some("high")); // 81%
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_memory_zero_total() {
|
||||
let state = mock_state(AppState {
|
||||
memory: MemoryState {
|
||||
used_gb: 0.0,
|
||||
total_gb: 0.0,
|
||||
},
|
||||
..Default::default()
|
||||
});
|
||||
let config = Config::default();
|
||||
let output = MemoryModule.run(&config, &state, &[]).unwrap();
|
||||
assert_eq!(output.class.as_deref(), Some("normal"));
|
||||
assert_eq!(output.percentage, Some(0));
|
||||
}
|
||||
}
|
||||
|
||||
+68
-10
@@ -2,7 +2,7 @@ use crate::config::Config;
|
||||
use crate::modules::WaybarModule;
|
||||
use crate::output::WaybarOutput;
|
||||
use crate::state::SharedState;
|
||||
use crate::utils::{TokenValue, format_template};
|
||||
use crate::utils::{TokenValue, format_template, run_command};
|
||||
use anyhow::Result;
|
||||
use std::fs;
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
@@ -148,10 +148,7 @@ impl WaybarModule for NetworkModule {
|
||||
}
|
||||
|
||||
fn get_primary_interface() -> Result<String> {
|
||||
let output = std::process::Command::new("ip")
|
||||
.args(["route", "list"])
|
||||
.output()?;
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
let stdout = run_command("ip", &["route", "list"])?;
|
||||
|
||||
let mut defaults = Vec::new();
|
||||
for line in stdout.lines() {
|
||||
@@ -182,11 +179,7 @@ fn get_primary_interface() -> Result<String> {
|
||||
}
|
||||
|
||||
fn get_ip_address(interface: &str) -> Option<String> {
|
||||
let output = std::process::Command::new("ip")
|
||||
.args(["-4", "addr", "show", interface])
|
||||
.output()
|
||||
.ok()?;
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
let stdout = run_command("ip", &["-4", "addr", "show", interface]).ok()?;
|
||||
for line in stdout.lines() {
|
||||
if line.trim().starts_with("inet ") {
|
||||
let parts: Vec<&str> = line.split_whitespace().collect();
|
||||
@@ -217,3 +210,68 @@ fn get_bytes(interface: &str) -> Result<(u64, u64)> {
|
||||
|
||||
Ok((rx, tx))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::state::{AppState, NetworkState, mock_state};
|
||||
|
||||
#[test]
|
||||
fn test_network_no_connection() {
|
||||
let state = mock_state(AppState::default());
|
||||
let config = Config::default();
|
||||
let output = NetworkModule.run(&config, &state, &[]).unwrap();
|
||||
assert_eq!(output.text, "No connection");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_network_connected() {
|
||||
let state = mock_state(AppState {
|
||||
network: NetworkState {
|
||||
rx_mbps: 1.5,
|
||||
tx_mbps: 0.3,
|
||||
interface: "eth0".to_string(),
|
||||
ip: "192.168.1.100".to_string(),
|
||||
},
|
||||
..Default::default()
|
||||
});
|
||||
let config = Config::default();
|
||||
let output = NetworkModule.run(&config, &state, &[]).unwrap();
|
||||
assert!(output.text.contains("eth0"));
|
||||
assert!(output.text.contains("192.168.1.100"));
|
||||
assert!(output.text.contains("1.50"));
|
||||
assert_eq!(output.class.as_deref(), Some("eth0"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_network_vpn_prefix() {
|
||||
let state = mock_state(AppState {
|
||||
network: NetworkState {
|
||||
rx_mbps: 0.0,
|
||||
tx_mbps: 0.0,
|
||||
interface: "wg0".to_string(),
|
||||
ip: "10.0.0.1".to_string(),
|
||||
},
|
||||
..Default::default()
|
||||
});
|
||||
let config = Config::default();
|
||||
let output = NetworkModule.run(&config, &state, &[]).unwrap();
|
||||
assert!(output.text.starts_with(" "));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_network_no_ip() {
|
||||
let state = mock_state(AppState {
|
||||
network: NetworkState {
|
||||
rx_mbps: 0.0,
|
||||
tx_mbps: 0.0,
|
||||
interface: "eth0".to_string(),
|
||||
ip: String::new(),
|
||||
},
|
||||
..Default::default()
|
||||
});
|
||||
let config = Config::default();
|
||||
let output = NetworkModule.run(&config, &state, &[]).unwrap();
|
||||
assert!(output.text.contains("No IP"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,3 +10,55 @@ pub struct WaybarOutput {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub percentage: Option<u8>,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_full_output_serialization() {
|
||||
let output = WaybarOutput {
|
||||
text: "CPU: 50%".to_string(),
|
||||
tooltip: Some("Details".to_string()),
|
||||
class: Some("normal".to_string()),
|
||||
percentage: Some(50),
|
||||
};
|
||||
let json = serde_json::to_string(&output).unwrap();
|
||||
let val: serde_json::Value = serde_json::from_str(&json).unwrap();
|
||||
assert_eq!(val["text"], "CPU: 50%");
|
||||
assert_eq!(val["tooltip"], "Details");
|
||||
assert_eq!(val["class"], "normal");
|
||||
assert_eq!(val["percentage"], 50);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_optional_fields_omitted() {
|
||||
let output = WaybarOutput {
|
||||
text: "test".to_string(),
|
||||
tooltip: None,
|
||||
class: None,
|
||||
percentage: None,
|
||||
};
|
||||
let json = serde_json::to_string(&output).unwrap();
|
||||
let val: serde_json::Value = serde_json::from_str(&json).unwrap();
|
||||
assert_eq!(val["text"], "test");
|
||||
assert!(val.get("tooltip").is_none());
|
||||
assert!(val.get("class").is_none());
|
||||
assert!(val.get("percentage").is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_partial_optional_fields() {
|
||||
let output = WaybarOutput {
|
||||
text: "test".to_string(),
|
||||
tooltip: Some("tip".to_string()),
|
||||
class: None,
|
||||
percentage: Some(75),
|
||||
};
|
||||
let json = serde_json::to_string(&output).unwrap();
|
||||
let val: serde_json::Value = serde_json::from_str(&json).unwrap();
|
||||
assert_eq!(val["tooltip"], "tip");
|
||||
assert!(val.get("class").is_none());
|
||||
assert_eq!(val["percentage"], 75);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -84,3 +84,8 @@ impl Default for GpuState {
|
||||
}
|
||||
|
||||
pub type SharedState = Arc<RwLock<AppState>>;
|
||||
|
||||
#[cfg(test)]
|
||||
pub fn mock_state(state: AppState) -> SharedState {
|
||||
Arc::new(RwLock::new(state))
|
||||
}
|
||||
|
||||
+106
@@ -114,3 +114,109 @@ fn format_str(s: &str, align: &str, width: usize) -> String {
|
||||
_ => s.to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_simple_string_token() {
|
||||
let result = format_template("{name}", &[("name", TokenValue::String("hello"))]);
|
||||
assert_eq!(result, "hello");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_simple_float_token() {
|
||||
let result = format_template("{val}", &[("val", TokenValue::Float(3.15))]);
|
||||
assert_eq!(result, "3.15");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_simple_int_token() {
|
||||
let result = format_template("{count}", &[("count", TokenValue::Int(42))]);
|
||||
assert_eq!(result, "42");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_float_right_align_with_precision() {
|
||||
let result = format_template("{val:>8.2}", &[("val", TokenValue::Float(3.15))]);
|
||||
assert_eq!(result, " 3.15");
|
||||
assert_eq!(result.len(), 8);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_float_left_align_with_precision() {
|
||||
let result = format_template("{val:<8.2}", &[("val", TokenValue::Float(3.15))]);
|
||||
assert_eq!(result, "3.15 ");
|
||||
assert_eq!(result.len(), 8);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_float_center_align_with_precision() {
|
||||
let result = format_template("{val:^8.2}", &[("val", TokenValue::Float(3.15))]);
|
||||
assert_eq!(result, " 3.15 ");
|
||||
assert_eq!(result.len(), 8);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_int_right_align() {
|
||||
let result = format_template("{val:>5}", &[("val", TokenValue::Int(42))]);
|
||||
assert_eq!(result, " 42");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_string_left_align() {
|
||||
let result = format_template("{val:<10}", &[("val", TokenValue::String("hi"))]);
|
||||
assert_eq!(result, "hi ");
|
||||
assert_eq!(result.len(), 10);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_unknown_token_preserved() {
|
||||
let result = format_template("{unknown}", &[("name", TokenValue::String("test"))]);
|
||||
assert_eq!(result, "{unknown}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_multiple_tokens() {
|
||||
let result = format_template(
|
||||
"CPU: {usage:>4.1}% {temp:>4.1}C",
|
||||
&[
|
||||
("usage", TokenValue::Float(55.3)),
|
||||
("temp", TokenValue::Float(65.0)),
|
||||
],
|
||||
);
|
||||
assert_eq!(result, "CPU: 55.3% 65.0C");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_no_tokens() {
|
||||
let result = format_template("plain text", &[]);
|
||||
assert_eq!(result, "plain text");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_empty_template() {
|
||||
let result = format_template("", &[("x", TokenValue::Int(1))]);
|
||||
assert_eq!(result, "");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_mixed_token_types() {
|
||||
let result = format_template(
|
||||
"{name} ({ip}): {rx:>5.2} MB/s",
|
||||
&[
|
||||
("name", TokenValue::String("eth0")),
|
||||
("ip", TokenValue::String("10.0.0.1")),
|
||||
("rx", TokenValue::Float(1.5)),
|
||||
],
|
||||
);
|
||||
assert_eq!(result, "eth0 (10.0.0.1): 1.50 MB/s");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_float_precision_zero() {
|
||||
let result = format_template("{val:>3.0}", &[("val", TokenValue::Float(99.7))]);
|
||||
assert_eq!(result, "100");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user