mod output;
mod run;
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
use clap::{Parser, ValueEnum};
use howfastly::types::{self, SizePlan, TestConfig};
#[derive(Clone, Copy, Debug, ValueEnum)]
pub enum OutputFormat {
Human,
Json,
Csv,
}
#[derive(Clone, Copy, Debug, ValueEnum)]
pub enum PayloadSize {
#[value(name = "100k")]
K100,
#[value(name = "1m")]
M1,
#[value(name = "10m")]
M10,
#[value(name = "25m")]
M25,
#[value(name = "100m")]
M100,
}
impl PayloadSize {
pub fn bytes(self) -> u64 {
match self {
Self::K100 => 100_000,
Self::M1 => 1_000_000,
Self::M10 => 10_000_000,
Self::M25 => 25_000_000,
Self::M100 => 100_000_000,
}
}
}
#[derive(Debug, Parser)]
#[command(
name = "howfastly",
about = concat!(
"HowFastly ",
env!("CARGO_PKG_VERSION"),
"\nHow fast is your connection to the Fastly network?"
)
)]
pub struct Args {
#[arg(
long,
short = 'U',
env = "HOWFASTLY_URL",
default_value = "https://speed.edgecompute.app"
)]
pub url: String,
#[arg(long, short)]
pub nr_tests: Option<usize>,
#[arg(long, short = 'l', default_value_t = types::LATENCY_SAMPLES)]
pub nr_latency_tests: usize,
#[arg(long, short, value_enum, default_value = "100m")]
pub max_payload_size: PayloadSize,
#[arg(long, short, conflicts_with = "upload_only")]
pub download_only: bool,
#[arg(long, short = 'u')]
pub upload_only: bool,
#[arg(long, value_name = "ADDR", num_args = 0..=1, default_missing_value = "0.0.0.0", conflicts_with = "ipv6")]
pub ipv4: Option<Ipv4Addr>,
#[arg(long, value_name = "ADDR", num_args = 0..=1, default_missing_value = "::")]
pub ipv6: Option<Ipv6Addr>,
#[arg(long, short, value_enum, default_value = "human")]
pub format: OutputFormat,
#[arg(long, short)]
pub verbose: bool,
}
impl Args {
pub fn local_addr(&self) -> Option<IpAddr> {
self.ipv4.map(IpAddr::V4).or(self.ipv6.map(IpAddr::V6))
}
pub fn config(&self) -> TestConfig {
let cap = self.max_payload_size.bytes();
let keep = |plan: &[SizePlan]| {
plan.iter()
.copied()
.filter(|p| p.bytes <= cap)
.map(|p| SizePlan {
iterations: self.nr_tests.unwrap_or(p.iterations),
..p
})
.collect()
};
TestConfig {
latency_samples: self.nr_latency_tests,
download: keep(&types::DOWNLOAD_PLAN),
upload: keep(&types::UPLOAD_PLAN),
time_budget_secs: types::TIME_BUDGET_SECS,
}
}
}
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let args = Args::parse();
let results = run::run(&args).await?;
print!("{}", output::render(&results, args.format)?);
Ok(())
}
#[cfg(test)]
mod tests {
use clap::CommandFactory;
use super::*;
#[test]
fn cli() {
Args::command().debug_assert();
}
#[test]
fn shorts() {
let a = Args::try_parse_from([
"howfastly",
"-U",
"http://x",
"-n",
"3",
"-l",
"5",
"-m",
"1m",
"-d",
"-f",
"json",
])
.unwrap();
assert_eq!(a.url, "http://x");
assert_eq!(a.nr_tests, Some(3));
assert_eq!(a.nr_latency_tests, 5);
assert!(matches!(a.max_payload_size, PayloadSize::M1));
assert!(a.download_only);
assert!(matches!(a.format, OutputFormat::Json));
let a = Args::try_parse_from(["howfastly", "-u"]).unwrap();
assert!(a.upload_only);
assert!(Args::try_parse_from(["howfastly", "-d", "-u"]).is_err());
assert!(Args::try_parse_from(["howfastly", "-f", "json-pretty"]).is_err());
}
#[test]
fn ip_binding() {
let a = Args::try_parse_from(["howfastly"]).unwrap();
assert_eq!(a.local_addr(), None);
let a = Args::try_parse_from(["howfastly", "--ipv4"]).unwrap();
assert_eq!(a.local_addr(), Some(IpAddr::V4(Ipv4Addr::UNSPECIFIED)));
let a = Args::try_parse_from(["howfastly", "--ipv4", "192.0.2.7"]).unwrap();
assert_eq!(
a.local_addr(),
Some(IpAddr::V4(Ipv4Addr::new(192, 0, 2, 7)))
);
let a = Args::try_parse_from(["howfastly", "--ipv6"]).unwrap();
assert_eq!(a.local_addr(), Some(IpAddr::V6(Ipv6Addr::UNSPECIFIED)));
let a = Args::try_parse_from(["howfastly", "--ipv6", "2001:db8::1"]).unwrap();
assert_eq!(a.local_addr(), Some("2001:db8::1".parse().unwrap()));
assert!(Args::try_parse_from(["howfastly", "--ipv4", "--ipv6"]).is_err());
assert!(Args::try_parse_from(["howfastly", "--ipv4", "::1"]).is_err());
assert!(Args::try_parse_from(["howfastly", "--ipv6", "127.0.0.1"]).is_err());
}
}