udp_prague 0.1.0

A Rust implementation of the Prague congestion control protocol for UDP-based applications.
Documentation
use std::env;
use std::fs;
use std::path::PathBuf;
use std::time::{SystemTime, UNIX_EPOCH};

use super::*;

fn temp_json_path(label: &str) -> PathBuf {
    let unique = SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .expect("system time")
        .as_nanos();
    env::temp_dir().join(format!("udp_prague_{label}_{unique}.jsonl"))
}

#[test]
fn rfc8888_negative_timing_samples_are_clamped_for_reporting() {
    let args = vec!["udp_prague_sender".to_string()];
    let mut app = AppStuff::new(true, &args).expect("app args");

    app.LogRecvRFC8888ACK(&crate::core::PragueRecvRfc8888AckEvent {
        now: 1_000,
        seqnr: 1,
        bytes_received: 26,
        begin_seq: 1,
        num_reports: 1,
        num_rtt: 1,
        rtts: &[-1],
        counters: crate::core::PragueAckCounters {
            packets_received: 8,
            packets_ce: 0,
            packets_lost: 0,
            error_l4s: false,
        },
        transport: crate::core::PraguePacketWindowMetrics {
            pacing_rate: 12_500,
            packet_window: 2,
            packet_burst: 1,
            packet_inflight: 0,
            packet_inburst: 1,
            next_send: 1_100,
        },
        frames: crate::core::PragueFrameWindowMetrics {
            frame_window: 0,
            frame_inflight: 0,
            frame_sending: false,
            sent_frame: 0,
            lost_frame: 0,
            recv_frame: 0,
        },
    });

    assert_eq!(app.acc_rtts, 0);
    assert_eq!(app.count_rtts, 1);
}

#[test]
fn startup_timeout_flag_populates_runner_config() {
    let args = vec![
        "udp_prague_sender".to_string(),
        "--startuptimeout".to_string(),
        "250000".to_string(),
    ];
    let app = AppStuff::new(true, &args).expect("app args");

    assert_eq!(app.runner_config().startup_wait_timeout_us, Some(250_000));
}

#[test]
fn json_dump_failure_is_latched() {
    let args = vec!["udp_prague_sender".to_string()];
    let mut app = AppStuff::new(true, &args).expect("app args");

    app.json_output = true;
    app.jw.reset();
    app.jw.field_str("name", "sender");
    app.jw.finalize();

    app.dump_json_report();
    assert!(app.json_output_failed);

    app.dump_json_report();
    assert!(app.json_output_failed);
}

#[test]
fn json_filename_rejects_path_separators_for_cxx_cli_parity() {
    let args = vec![
        "udp_prague_sender".to_string(),
        "-j".to_string(),
        "subdir/out.jsonl".to_string(),
    ];

    assert!(matches!(
        AppStuff::new(true, &args),
        Err(AppError::InvalidValue("json filename"))
    ));
}

#[test]
fn sender_rt_json_report_keeps_frame_fields_and_cxx_float_format() {
    let args = vec!["udp_prague_sender".to_string()];
    let mut app = AppStuff::new(true, &args).expect("app args");
    let path = temp_json_path("sender_rt");

    app.json_output = true;
    app.rt_mode = true;
    app.jw
        .init(path.to_str().expect("utf8 path"), false)
        .expect("init json writer");
    app.acc_bytes_sent = 125;
    app.acc_bytes_rcvd = 25;
    app.acc_rtts = 50;
    app.count_rtts = 1;

    app.PrintSender(1_000_000, 10, 0, 0, 250_000, 7, 3, 4, 2, 5, 1);

    assert!(app.jw.buf.contains("\"sent_rate\":\"0.001000\""));
    assert!(app.jw.buf.contains("\"rtt\":\"0.050000\""));
    assert!(app.jw.buf.contains("\"frame_inflight\":\"1\""));
    assert!(app.jw.buf.contains("\"frame_window\":\"5\""));
    assert!(app.jw.buf.contains("\"pkt_inflight\":\"4\""));

    let frame_pos = app.jw.buf.find("\"frame_inflight\"").expect("frame field");
    let packet_pos = app.jw.buf.find("\"pkt_inflight\"").expect("packet field");
    assert!(frame_pos < packet_pos);

    let _ = fs::remove_file(path);
}

#[test]
fn receiver_rfc8888_json_report_uses_ato_key() {
    let args = vec!["udp_prague_receiver".to_string()];
    let mut app = AppStuff::new(false, &args).expect("app args");
    let path = temp_json_path("receiver_rfc8888");

    app.json_output = true;
    app.rfc8888_ack = true;
    app.jw
        .init(path.to_str().expect("utf8 path"), false)
        .expect("init json writer");
    app.acc_bytes_rcvd = 250;
    app.acc_bytes_sent = 25;
    app.acc_rtts = 25_000;
    app.count_rtts = 1;
    app.prev_pkts = 8;
    app.prev_marks = 1;
    app.prev_losts = 2;

    app.PrintReceiver(1_000_000, 0, 0, 0);

    assert!(app.jw.buf.contains("\"ATO\":"), "{}", app.jw.buf);
    assert!(!app.jw.buf.contains("\"RTT\":"));
    assert!(app.jw.buf.contains("\"pkt_rcvd\":\"8\""));
    assert!(app.jw.buf.contains("\"pkt_mark\":\"1\""));
    assert!(app.jw.buf.contains("\"pkt_lost\":\"2\""));

    let _ = fs::remove_file(path);
}