use std::net;
use std::time::{Duration, Instant};
use anyhow::Result;
use clap::{Parser, ValueEnum};
use url::Url;
mod scenarios;
#[derive(Parser, Clone)]
#[command(name = "moq-test-client")]
#[command(about = "MoQT Interoperability Test Client", long_about = None)]
pub struct Args {
#[arg(
short,
long,
default_value = "https://localhost:4443",
env = "RELAY_URL"
)]
pub relay: Url,
#[arg(short, long, env = "TESTCASE")]
pub test: Option<TestCase>,
#[arg(short, long)]
pub list: bool,
#[arg(long, default_value = "[::]:0")]
pub bind: net::SocketAddr,
#[command(flatten)]
pub tls: moq_native_ietf::tls::Args,
#[arg(short, long, env = "VERBOSE")]
pub verbose: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
pub enum TestCase {
SetupOnly,
AnnounceOnly,
SubscribeError,
AnnounceSubscribe,
SubscribeBeforeAnnounce,
PublishNamespaceDone,
}
impl TestCase {
fn all() -> Vec<TestCase> {
vec![
TestCase::SetupOnly,
TestCase::AnnounceOnly,
TestCase::SubscribeError,
TestCase::AnnounceSubscribe,
TestCase::SubscribeBeforeAnnounce,
TestCase::PublishNamespaceDone,
]
}
fn name(&self) -> &'static str {
match self {
TestCase::SetupOnly => "setup-only",
TestCase::AnnounceOnly => "announce-only",
TestCase::SubscribeError => "subscribe-error",
TestCase::AnnounceSubscribe => "announce-subscribe",
TestCase::SubscribeBeforeAnnounce => "subscribe-before-announce",
TestCase::PublishNamespaceDone => "publish-namespace-done",
}
}
}
#[derive(Debug)]
pub struct TestResult {
pub test_case: TestCase,
pub passed: bool,
pub duration: Duration,
pub message: Option<String>,
pub cids: Vec<String>,
}
impl TestResult {
fn success(test_case: TestCase, duration: Duration, cids: Vec<String>) -> Self {
Self {
test_case,
passed: true,
duration,
message: None,
cids,
}
}
fn failure(test_case: TestCase, duration: Duration, message: String) -> Self {
Self {
test_case,
passed: false,
duration,
message: Some(message),
cids: Vec::new(),
}
}
}
async fn run_test(args: &Args, test_case: TestCase) -> TestResult {
let start = Instant::now();
let result = match test_case {
TestCase::SetupOnly => scenarios::test_setup_only(args).await,
TestCase::AnnounceOnly => scenarios::test_announce_only(args).await,
TestCase::SubscribeError => scenarios::test_subscribe_error(args).await,
TestCase::AnnounceSubscribe => scenarios::test_announce_subscribe(args).await,
TestCase::SubscribeBeforeAnnounce => scenarios::test_subscribe_before_announce(args).await,
TestCase::PublishNamespaceDone => scenarios::test_publish_namespace_done(args).await,
};
let duration = start.elapsed();
match result {
Ok(cids) => TestResult::success(test_case, duration, cids.cids),
Err(e) => TestResult::failure(test_case, duration, format!("{:#}", e)),
}
}
fn print_tap_result(test_number: usize, result: &TestResult, verbose: bool) {
let status = if result.passed { "ok" } else { "not ok" };
let name = result.test_case.name();
println!("{} {} - {}", status, test_number, name);
println!(" ---");
println!(" duration_ms: {}", result.duration.as_millis());
match result.cids.len() {
0 => {}
1 => println!(" connection_id: {}", result.cids[0]),
2 => {
if result.test_case == TestCase::SubscribeBeforeAnnounce {
println!(" subscriber_connection_id: {}", result.cids[0]);
println!(" publisher_connection_id: {}", result.cids[1]);
} else {
println!(" publisher_connection_id: {}", result.cids[0]);
println!(" subscriber_connection_id: {}", result.cids[1]);
}
}
_ => {
for (i, cid) in result.cids.iter().enumerate() {
println!(" connection_id_{}: {}", i + 1, cid);
}
}
}
if let Some(ref msg) = result.message {
let escaped = if verbose {
msg.replace('\\', "\\\\")
.replace('"', "\\\"")
.replace('\n', "\\n")
} else {
msg.lines()
.next()
.unwrap_or(msg)
.replace('\\', "\\\\")
.replace('"', "\\\"")
};
println!(" message: \"{}\"", escaped);
}
println!(" ...");
}
#[tokio::main]
async fn main() -> Result<()> {
tracing_subscriber::fmt()
.with_env_filter(
tracing_subscriber::EnvFilter::try_from_default_env()
.unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("info,quinn=warn")),
)
.init();
let args = Args::parse();
if args.list {
for tc in TestCase::all() {
println!("{}", tc.name());
}
return Ok(());
}
let tests_to_run = match args.test {
Some(tc) => vec![tc],
None => TestCase::all(),
};
println!("TAP version 14");
println!("# moq-test-client v{}", env!("CARGO_PKG_VERSION"));
println!("# Relay: {}", args.relay);
println!("1..{}", tests_to_run.len());
let mut failed = 0;
for (i, test_case) in tests_to_run.iter().enumerate() {
let result = run_test(&args, *test_case).await;
print_tap_result(i + 1, &result, args.verbose);
if !result.passed {
failed += 1;
}
}
if failed == 0 {
Ok(())
} else {
std::process::exit(1);
}
}