tests and time support
This commit is contained in:
		@@ -6,16 +6,20 @@ use log::warn;
 | 
			
		||||
use serde::{self, Deserialize, Serialize};
 | 
			
		||||
use std::env;
 | 
			
		||||
use std::path::{Path, PathBuf};
 | 
			
		||||
use std::time::Duration;
 | 
			
		||||
use tokio::{fs, io::AsyncReadExt};
 | 
			
		||||
 | 
			
		||||
use crate::PROGRAM_NAME;
 | 
			
		||||
 | 
			
		||||
#[derive(Deserialize, Serialize, Debug)]
 | 
			
		||||
#[derive(Debug, Deserialize, Serialize)]
 | 
			
		||||
pub struct Config {
 | 
			
		||||
    pub zone_id: Box<str>,
 | 
			
		||||
    pub api_key: Box<str>,
 | 
			
		||||
    pub zone_id: String,
 | 
			
		||||
    pub api_key: String,
 | 
			
		||||
    pub domains: Vec<Box<str>>,
 | 
			
		||||
    #[serde(default)]
 | 
			
		||||
    pub max_errors_in_row: Option<usize>,
 | 
			
		||||
    #[serde(with = "duration_format", default)]
 | 
			
		||||
    pub max_duration: Option<Duration>,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
pub async fn get_config_path() -> Result<PathBuf, Vec<PathBuf>> {
 | 
			
		||||
@@ -67,3 +71,88 @@ pub async fn read_config<P: AsRef<Path>>(path: &P) -> anyhow::Result<Config> {
 | 
			
		||||
    file.read_to_string(&mut buf).await?;
 | 
			
		||||
    Ok(toml::from_str(&buf)?)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
mod duration_format {
 | 
			
		||||
    use super::*;
 | 
			
		||||
    use serde::{self, Deserialize, Deserializer, Serializer};
 | 
			
		||||
    use std::time::Duration;
 | 
			
		||||
 | 
			
		||||
    pub fn serialize<S>(duration: &Option<Duration>, serializer: S) -> Result<S::Ok, S::Error>
 | 
			
		||||
    where
 | 
			
		||||
        S: Serializer,
 | 
			
		||||
    {
 | 
			
		||||
        let duration = if let Some(aux) = duration.as_ref() {
 | 
			
		||||
            aux
 | 
			
		||||
        } else {
 | 
			
		||||
            return serializer.serialize_none();
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        let mut secs = duration.as_secs();
 | 
			
		||||
        let nanos = duration.subsec_nanos();
 | 
			
		||||
 | 
			
		||||
        let days = secs / 86400;
 | 
			
		||||
        secs %= 86400;
 | 
			
		||||
        let hours = secs / 3600;
 | 
			
		||||
        secs %= 3600;
 | 
			
		||||
        let minutes = secs / 60;
 | 
			
		||||
        secs %= 60;
 | 
			
		||||
 | 
			
		||||
        let mut formatted = String::new();
 | 
			
		||||
        if days > 0 {
 | 
			
		||||
            formatted.push_str(&format!("{}d ", days));
 | 
			
		||||
        }
 | 
			
		||||
        if hours > 0 {
 | 
			
		||||
            formatted.push_str(&format!("{}h ", hours));
 | 
			
		||||
        }
 | 
			
		||||
        if minutes > 0 {
 | 
			
		||||
            formatted.push_str(&format!("{}m ", minutes));
 | 
			
		||||
        }
 | 
			
		||||
        if secs > 0 {
 | 
			
		||||
            formatted.push_str(&format!("{}s ", secs));
 | 
			
		||||
        }
 | 
			
		||||
        if nanos > 0 {
 | 
			
		||||
            formatted.push_str(&format!("{}ns", nanos));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        serializer.serialize_str(formatted.trim())
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn deserialize<'de, D>(deserializer: D) -> Result<Option<Duration>, D::Error>
 | 
			
		||||
    where
 | 
			
		||||
        D: Deserializer<'de>,
 | 
			
		||||
    {
 | 
			
		||||
        Option::<Box<str>>::deserialize(deserializer)?.map_or(Ok(None), |s| {
 | 
			
		||||
            parse_duration(&s)
 | 
			
		||||
                .map(Some)
 | 
			
		||||
                .map_err(serde::de::Error::custom)
 | 
			
		||||
        })
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn parse_duration(s: &str) -> Result<Duration, String> {
 | 
			
		||||
        let mut total_duration = Duration::new(0, 0);
 | 
			
		||||
        let units = [("d", 86400), ("h", 3600), ("m", 60), ("s", 1)];
 | 
			
		||||
        let mut remainder = s;
 | 
			
		||||
 | 
			
		||||
        for &(unit, factor) in &units {
 | 
			
		||||
            if let Some(idx) = remainder.find(unit) {
 | 
			
		||||
                let (value, rest) = remainder.split_at(idx);
 | 
			
		||||
                let value: u64 = value.trim().parse().map_err(|_| "Invalid number")?;
 | 
			
		||||
                total_duration += Duration::from_secs(value * factor);
 | 
			
		||||
                remainder = &rest[unit.len()..];
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if let Some(idx) = remainder.find("ns") {
 | 
			
		||||
            let (value, rest) = remainder.split_at(idx);
 | 
			
		||||
            let value: u32 = value.trim().parse().map_err(|_| "Invalid number")?;
 | 
			
		||||
            total_duration += Duration::new(0, value);
 | 
			
		||||
            remainder = &rest["ns".len()..];
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if !remainder.trim().is_empty() {
 | 
			
		||||
            return Err("Invalid duration format".to_string());
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        Ok(total_duration)
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -3,15 +3,16 @@
 | 
			
		||||
mod cloudflare;
 | 
			
		||||
mod config;
 | 
			
		||||
mod logging;
 | 
			
		||||
mod public_ip;
 | 
			
		||||
mod message_handler;
 | 
			
		||||
mod public_ip;
 | 
			
		||||
mod tests;
 | 
			
		||||
pub mod utils;
 | 
			
		||||
 | 
			
		||||
pub use cloudflare::CloudflareClient;
 | 
			
		||||
pub use config::{get_config_path, read_config, Config};
 | 
			
		||||
pub use logging::init_logger;
 | 
			
		||||
pub use public_ip::get_current_public_ipv4;
 | 
			
		||||
pub use message_handler::MessageHandler;
 | 
			
		||||
pub use public_ip::get_current_public_ipv4;
 | 
			
		||||
 | 
			
		||||
pub const PROGRAM_NAME: &'static str = "dynip-cloudflare";
 | 
			
		||||
pub const MAX_ERORS_IN_ROW_DEFAULT: usize = 10;
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										75
									
								
								src/tests/config_serialization.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										75
									
								
								src/tests/config_serialization.rs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,75 @@
 | 
			
		||||
use std::time::Duration;
 | 
			
		||||
use toml;
 | 
			
		||||
use crate::Config;
 | 
			
		||||
 | 
			
		||||
const TOML_STR_ONE: &str = r#"
 | 
			
		||||
zone_id = ""
 | 
			
		||||
api_key = ""
 | 
			
		||||
domains = [""]
 | 
			
		||||
max_duration = "1d 2h 30m 45s 500000000ns"
 | 
			
		||||
"#;
 | 
			
		||||
 | 
			
		||||
#[test]
 | 
			
		||||
fn test_deserialize() {
 | 
			
		||||
    let config: Config = toml::from_str(TOML_STR_ONE).unwrap();
 | 
			
		||||
    assert_eq!(config.max_duration, Some(Duration::new(95445, 500000000)));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[test]
 | 
			
		||||
fn test_serialize() {
 | 
			
		||||
    let config = Config {
 | 
			
		||||
        zone_id: "".into(),
 | 
			
		||||
        api_key: "".into(),
 | 
			
		||||
        domains: vec!["".into()],
 | 
			
		||||
        max_errors_in_row: None,
 | 
			
		||||
        max_duration: Some(Duration::new(95445, 500000000)),
 | 
			
		||||
    };
 | 
			
		||||
    let toml_str = toml::to_string(&config).unwrap();
 | 
			
		||||
    assert_eq!(TOML_STR_ONE.trim(), toml_str.trim());
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[test]
 | 
			
		||||
fn test_deserialize_none() {
 | 
			
		||||
    let toml_str = r#"
 | 
			
		||||
zone_id = ""
 | 
			
		||||
api_key = ""
 | 
			
		||||
domains = [""]
 | 
			
		||||
max_errors_in_row = 5
 | 
			
		||||
"#;
 | 
			
		||||
    let config: Config = toml::from_str(toml_str).unwrap();
 | 
			
		||||
    assert_eq!(config.max_duration, None);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[test]
 | 
			
		||||
fn test_serialize_none() {
 | 
			
		||||
    let toml_to_be = r#"
 | 
			
		||||
zone_id = ""
 | 
			
		||||
api_key = ""
 | 
			
		||||
domains = [""]
 | 
			
		||||
"#;
 | 
			
		||||
    let config = Config {
 | 
			
		||||
        zone_id: "".into(),
 | 
			
		||||
        api_key: "".into(),
 | 
			
		||||
        domains: vec!["".into()],
 | 
			
		||||
        max_errors_in_row: None,
 | 
			
		||||
        max_duration: None,
 | 
			
		||||
    };
 | 
			
		||||
    let toml_str = toml::to_string(&config).unwrap();
 | 
			
		||||
    assert_eq!(toml_to_be.trim(), toml_str.trim());
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fn main() {
 | 
			
		||||
    let toml_str = r#"
 | 
			
		||||
zone_id = ""
 | 
			
		||||
api_key = ""
 | 
			
		||||
domains = [""]
 | 
			
		||||
max_errors_in_row = 5
 | 
			
		||||
max_duration = "1d 2h 30m 45s 500000000ns"
 | 
			
		||||
"#;
 | 
			
		||||
 | 
			
		||||
    let config: Config = toml::from_str(toml_str).unwrap();
 | 
			
		||||
    println!("{:?}", config);
 | 
			
		||||
 | 
			
		||||
    let toml_out = toml::to_string(&config).unwrap();
 | 
			
		||||
    println!("{}", toml_out);
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										2
									
								
								src/tests/mod.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								src/tests/mod.rs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,2 @@
 | 
			
		||||
#[cfg(test)]
 | 
			
		||||
mod config_serialization;
 | 
			
		||||
		Reference in New Issue
	
	Block a user