tekhsi_rs 0.1.1

High-performance client for Tektronix TekHSI enabled oscilloscopes
Documentation
#[path = "common/config.rs"]
mod common_address;

use std::time::{Duration, Instant};

use common_address::test_address;
use tekhsi_rs::data::Acquisition;
use tekhsi_rs::errors::{SubscriptionError, SubscriptionUpdateError, TekHsiError};
use tekhsi_rs::{SubscribeOptions, TekHsiClient};
use tokio::time::timeout;

fn acquisition_matches_symbols(acquisition: &Acquisition, symbols: &[String]) -> bool {
    if acquisition.data.len() != symbols.len() {
        return false;
    }
    symbols
        .iter()
        .all(|symbol| acquisition.get_by_symbol(symbol.as_str()).is_some())
}

#[tokio::test]
async fn subscribe_rejects_empty_symbols() -> Result<(), Box<dyn std::error::Error>> {
    let client = TekHsiClient::connect(&test_address()).await?;
    let err = client
        .subscribe(Vec::new(), SubscribeOptions::default())
        .expect_err("expected empty symbols to fail");
    assert!(matches!(
        err,
        TekHsiError::Subscription(SubscriptionError::EmptySymbols)
    ));
    client.disconnect().await?;
    Ok(())
}

#[tokio::test]
async fn subscribe_rejects_double_subscription() -> Result<(), Box<dyn std::error::Error>> {
    let client = TekHsiClient::connect(&test_address()).await?;
    let symbols = client.list_available_symbols().await?;
    assert!(!symbols.is_empty(), "test server should return symbols");

    let receiver = client.subscribe(symbols.clone(), SubscribeOptions::default())?;
    let err = client
        .subscribe(symbols, SubscribeOptions::default())
        .expect_err("expected double subscribe to fail");
    assert!(matches!(
        err,
        TekHsiError::Subscription(SubscriptionError::AlreadyActive)
    ));

    drop(receiver);
    client.disconnect().await?;
    Ok(())
}

#[tokio::test]
async fn update_symbols_rejects_empty_symbols() -> Result<(), Box<dyn std::error::Error>> {
    let client = TekHsiClient::connect(&test_address()).await?;
    let err = client
        .update_symbols(Vec::new())
        .expect_err("expected empty symbols to fail");
    assert!(matches!(
        err,
        TekHsiError::SubscriptionUpdate(SubscriptionUpdateError::EmptySymbols)
    ));
    client.disconnect().await?;
    Ok(())
}

#[tokio::test]
async fn update_symbols_requires_active_subscription() -> Result<(), Box<dyn std::error::Error>> {
    let client = TekHsiClient::connect(&test_address()).await?;
    let err = client
        .update_symbols(vec!["ch1".to_string()])
        .expect_err("expected update to fail without subscription");
    assert!(matches!(
        err,
        TekHsiError::SubscriptionUpdate(SubscriptionUpdateError::NotActive)
    ));
    client.disconnect().await?;
    Ok(())
}

#[tokio::test]
async fn update_symbols_applies_to_active_subscription() -> Result<(), Box<dyn std::error::Error>> {
    let client = TekHsiClient::connect(&test_address()).await?;
    let symbols = client.list_available_symbols().await?;
    assert!(!symbols.is_empty(), "test server should return symbols");

    let mut receiver = client.subscribe(symbols.clone(), SubscribeOptions::default())?;

    let _ = timeout(Duration::from_secs(1), receiver.recv())
        .await?
        .map_err(|err| format!("acquisition receive failed: {err}"))?;

    let target_symbols = vec![symbols[0].clone()];
    client.update_symbols(target_symbols.clone())?;

    let deadline = Instant::now() + Duration::from_millis(500);
    loop {
        let now = Instant::now();
        if now >= deadline {
            return Err("timed out waiting for updated symbols".into());
        }

        let remaining = deadline - now;
        let acquisition = timeout(remaining, receiver.recv())
            .await?
            .map_err(|err| format!("acquisition receive failed: {err}"))?;

        if acquisition_matches_symbols(&acquisition, &target_symbols) {
            break;
        }
    }

    drop(receiver);
    client.disconnect().await?;
    Ok(())
}

#[tokio::test]
async fn acquisition_propagates_timing() -> Result<(), Box<dyn std::error::Error>> {
    let client = TekHsiClient::connect(&test_address()).await?;
    let symbols = client.list_available_symbols().await?;
    assert!(!symbols.is_empty(), "test server should return symbols");

    let mut receiver = client.subscribe(symbols, SubscribeOptions::default())?;
    for _ in 0..5 {
        let acquisition = timeout(Duration::from_secs(5), receiver.recv())
            .await?
            .map_err(|err| format!("acquisition receive failed: {err}"))?;

        if acquisition.wait_time == Duration::ZERO {
            return Err("expected non-zero wait_time".into());
        }
        if acquisition.download_time == Duration::ZERO {
            return Err("expected non-zero download_time".into());
        }
        let total = acquisition.wait_time + acquisition.download_time;
        if total > Duration::from_secs(5) {
            return Err(format!("timing exceeded 5 seconds: {:?}", total).into());
        }
    }

    client.disconnect().await?;
    Ok(())
}