speedtest-rs 0.1.1

Speedtest.net testing utility and crate
Documentation
use crate::{distance::EarthLocation, error::Error};
use std::{net::Ipv4Addr, time::Duration};

pub struct SpeedTestClientConfig {
    pub ip: Ipv4Addr,
    pub isp: String,
}

impl Default for SpeedTestClientConfig {
    fn default() -> Self {
        SpeedTestClientConfig {
            ip: Ipv4Addr::new(127, 0, 0, 1),
            isp: String::default(),
        }
    }
}

#[derive(Default)]
pub struct SpeedTestSizeConfig {
    pub upload: Vec<usize>,
    pub download: Vec<usize>,
}

#[derive(Default)]
pub struct SpeedTestCountsConfig {
    pub upload: usize,
    pub download: usize,
}

#[derive(Default)]
pub struct SpeedTestThreadsConfig {
    pub upload: usize,
    pub download: usize,
}

pub struct SpeedTestLengthConfig {
    pub upload: Duration,
    pub download: Duration,
}

impl Default for SpeedTestLengthConfig {
    fn default() -> Self {
        SpeedTestLengthConfig {
            upload: Duration::from_secs(10),
            download: Duration::from_secs(10),
        }
    }
}

#[derive(Default)]
pub struct SpeedTestConfig {
    pub client: SpeedTestClientConfig,
    pub ignore_servers: Vec<u32>,
    pub sizes: SpeedTestSizeConfig,
    pub counts: SpeedTestCountsConfig,
    pub threads: SpeedTestThreadsConfig,
    pub length: SpeedTestLengthConfig,
    pub upload_max: usize,
    pub location: EarthLocation,
}

impl SpeedTestConfig {
    pub fn parse(config_xml: &str) -> Result<SpeedTestConfig, Error> {
        let document = roxmltree::Document::parse(config_xml)?;

        let server_config_node = document
            .descendants()
            .find(|n| n.has_tag_name("server-config"))
            .ok_or(Error::ConfigParseError)?;
        let download_node = document
            .descendants()
            .find(|n| n.has_tag_name("download"))
            .ok_or(Error::ConfigParseError)?;
        let upload_node = document
            .descendants()
            .find(|n| n.has_tag_name("upload"))
            .ok_or(Error::ConfigParseError)?;
        let client_node = document
            .descendants()
            .find(|n| n.has_tag_name("client"))
            .ok_or(Error::ConfigParseError)?;

        let ignore_servers: Vec<u32> = server_config_node
            .attribute("ignoreids")
            .ok_or(Error::ConfigParseError)?
            .split(',')
            .map(|s| s.parse::<u32>())
            .collect::<Result<Vec<u32>, _>>()?;

        let ratio = upload_node
            .attribute("ratio")
            .ok_or(Error::ConfigParseError)?
            .parse::<usize>()?;

        let upload_max = upload_node
            .attribute("maxchunkcount")
            .ok_or(Error::ConfigParseError)?
            .parse::<usize>()?;

        let up_sizes = [32768usize, 65536, 131072, 262144, 524288, 1048576, 7340032];

        let sizes = SpeedTestSizeConfig {
            upload: up_sizes
                .get(ratio - 1..)
                .ok_or(Error::ConfigParseError)?
                .to_vec(),
            download: vec![350usize, 500, 750, 1000, 1500, 2000, 2500, 3000, 3500, 4000],
        };

        let size_count = sizes.upload.len();

        let upload_count = (upload_max as f32 / size_count as f32).ceil() as usize;

        let counts = SpeedTestCountsConfig {
            upload: upload_count,
            download: download_node
                .attribute("threadsperurl")
                .ok_or(Error::ConfigParseError)?
                .parse::<usize>()?,
        };

        let threads = SpeedTestThreadsConfig {
            upload: upload_node
                .attribute("threads")
                .ok_or(Error::ConfigParseError)?
                .parse::<usize>()?,
            download: server_config_node
                .attribute("threadcount")
                .ok_or(Error::ConfigParseError)?
                .parse::<usize>()?
                * 2,
        };

        let length = SpeedTestLengthConfig {
            upload: upload_node
                .attribute("testlength")
                .ok_or(Error::ConfigParseError)?
                .parse::<u64>()
                .map(Duration::from_secs)?,
            download: download_node
                .attribute("testlength")
                .ok_or(Error::ConfigParseError)?
                .parse::<u64>()
                .map(Duration::from_secs)?,
        };

        let client = SpeedTestClientConfig {
            ip: client_node
                .attribute("ip")
                .ok_or(Error::ConfigParseError)?
                .parse()?,
            isp: client_node
                .attribute("isp")
                .ok_or(Error::ConfigParseError)?
                .to_string(),
        };

        Ok(SpeedTestConfig {
            client,
            ignore_servers,
            sizes,
            counts,
            threads,
            length,
            upload_max,
            location: EarthLocation {
                latitude: client_node
                    .attribute("lat")
                    .ok_or(Error::ConfigParseError)?
                    .parse()?,
                longitude: client_node
                    .attribute("lon")
                    .ok_or(Error::ConfigParseError)?
                    .parse()?,
            },
        })
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_parse_config_xml() {
        let config =
            SpeedTestConfig::parse(include_str!("../tests/config/config.php.xml")).unwrap();
        assert_eq!("174.79.12.26", config.client.ip.to_string());
        assert_eq!(
            EarthLocation {
                latitude: 32.9954,
                longitude: -117.0753,
            },
            config.location
        );
        assert_eq!("Cox Communications", config.client.isp);
    }
}