motorcortex-rust 0.5.0

Motorcortex Rust: a Rust client for the Motorcortex Core real-time control system (async + blocking).
Documentation
//! Smoke tests for `motorcortex_rust::blocking::*` against the
//! vendored `test_server`. Prove the sync façade drives the async
//! core end-to-end — no explicit runtime, no `.await`.

use motorcortex_rust::blocking::{Request, Subscribe};
use motorcortex_rust::core::ConnectionState;
use motorcortex_rust::{ConnectionOptions, StatusCode};

use crate::{CERT_PATH, URL_REQ, URL_SUB};

fn opts() -> ConnectionOptions {
    ConnectionOptions::new(CERT_PATH.to_string(), 5000, 5000)
}

#[test]
fn test_blocking_request_full_lifecycle() {
    // Create → connect → login → tree → set/get → logout → disconnect.
    let req = Request::connect_to(URL_REQ, opts()).expect("connect_to");
    assert_eq!(*req.as_async().state().borrow(), ConnectionState::Connected);

    assert!(matches!(
        req.login("root", "vectioneer").expect("login"),
        StatusCode::Ok | StatusCode::ReadOnlyMode,
    ));

    req.request_parameter_tree().expect("tree");

    let status = req
        .set_parameter("root/Control/dummyDouble", 8.125_f64)
        .expect("set");
    assert_eq!(status, StatusCode::Ok);

    let value: f64 = req
        .get_parameter("root/Control/dummyDouble")
        .expect("get");
    assert_eq!(value, 8.125);

    assert_eq!(req.logout().expect("logout"), StatusCode::Ok);
    req.disconnect().expect("disconnect");
}

#[test]
fn test_blocking_subscribe_callback_fires() {
    // Same shape as the async subscribe callback test, sync body.
    use std::sync::{
        Arc,
        atomic::{AtomicBool, Ordering},
    };

    let req = Request::connect_to(URL_REQ, opts()).expect("req connect_to");
    req.request_parameter_tree().expect("tree");
    let sub = Subscribe::connect_to(URL_SUB, opts()).expect("sub connect_to");

    let subscription = sub
        .subscribe(
            &req,
            ["root/Control/dummyDouble"],
            "blocking-cb",
            1000,
        )
        .expect("subscribe");

    let fired = Arc::new(AtomicBool::new(false));
    let fired_cb = Arc::clone(&fired);
    subscription.notify(move |_s| {
        fired_cb.store(true, Ordering::Relaxed);
    });

    // Nudge the publisher + wait synchronously.
    req.set_parameter("root/Control/dummyDouble", 12.5_f64)
        .expect("set");

    // Busy-wait with a bounded timeout; the publisher cadence is kHz
    // so the callback should fire within a few ms.
    let deadline = std::time::Instant::now() + std::time::Duration::from_secs(3);
    while !fired.load(Ordering::Relaxed) {
        if std::time::Instant::now() > deadline {
            panic!("notify callback did not fire within 3 s");
        }
        std::thread::sleep(std::time::Duration::from_millis(10));
    }

    sub.unsubscribe(&req, subscription).expect("unsubscribe");
    sub.disconnect().expect("sub disconnect");
    req.disconnect().expect("req disconnect");
}

#[test]
fn test_blocking_subscription_iter_sees_newly_set_value() {
    // The blocking iterator over stream(). Values written after the
    // iter is created must eventually show up.
    let req = Request::connect_to(URL_REQ, opts()).expect("req");
    req.request_parameter_tree().expect("tree");
    let sub = Subscribe::connect_to(URL_SUB, opts()).expect("sub");

    let subscription = sub
        .subscribe(
            &req,
            ["root/Control/dummyDouble"],
            "blocking-iter",
            1000,
        )
        .expect("subscribe");

    let mut iter = subscription.iter::<f64>(256);
    req.set_parameter("root/Control/dummyDouble", 55.0_f64)
        .expect("set");

    // Walk the iterator with a per-sample deadline. Accept the first
    // exact match we see, bail on persistent timeout.
    let deadline = std::time::Instant::now() + std::time::Duration::from_secs(3);
    let mut saw = false;
    while std::time::Instant::now() < deadline {
        match iter.next() {
            Some(Ok((_ts, v))) if (v - 55.0).abs() < 1e-9 => {
                saw = true;
                break;
            }
            Some(_) => continue,
            None => break,
        }
    }
    assert!(saw, "blocking iter must surface the value we wrote");

    sub.unsubscribe(&req, subscription).expect("unsubscribe");
    sub.disconnect().expect("sub disconnect");
    req.disconnect().expect("req disconnect");
}

#[test]
fn test_blocking_request_batch_and_tree_apis() {
    // Exercises the blocking wrappers the earlier tests didn't
    // touch: get_parameter_tree_hash, get_parameters,
    // set_parameters, create_group, remove_group, parameter_tree.
    let req = Request::connect_to(URL_REQ, opts()).expect("connect_to");
    req.login("root", "vectioneer").expect("login");
    req.request_parameter_tree().expect("tree");

    // Tree cache is populated; the shared Arc is handed out.
    let tree = req.parameter_tree();
    {
        let guard = tree.read().unwrap();
        assert!(
            guard
                .get_parameter_info("root/Control/dummyDouble")
                .is_some(),
            "tree cache must contain the probe parameter",
        );
    }

    // Tree hash round-trip.
    let hash = req.get_parameter_tree_hash().expect("hash");
    assert_ne!(hash, 0, "populated server should report a non-zero hash");

    // Batch write then batch read.
    let status = req
        .set_parameters(
            &["root/Control/dummyDouble", "root/Control/dummyInt32"],
            (7.5_f64, 42_i32),
        )
        .expect("set_parameters");
    assert_eq!(status, StatusCode::Ok);

    let (d, i): (f64, i32) = req
        .get_parameters(&["root/Control/dummyDouble", "root/Control/dummyInt32"])
        .expect("get_parameters");
    assert_eq!(d, 7.5);
    assert_eq!(i, 42);

    // create_group + remove_group through the blocking façade.
    let group = req
        .create_group(["root/Control/dummyDouble"], "blocking-grp", 1000)
        .expect("create_group");
    assert_eq!(group.alias, "blocking-grp");
    assert_ne!(group.id, 0);

    let remove_status = req.remove_group("blocking-grp").expect("remove_group");
    assert_eq!(remove_status, StatusCode::Ok);

    req.disconnect().expect("disconnect");
}

#[test]
fn test_blocking_subscription_metadata_and_read_api() {
    // Covers the blocking Subscription getters that the other
    // tests didn't touch: id, name, read, read_all, latest, Clone.
    let req = Request::connect_to(URL_REQ, opts()).expect("req");
    req.request_parameter_tree().expect("tree");
    let sub = Subscribe::connect_to(URL_SUB, opts()).expect("sub");

    let subscription = sub
        .subscribe(
            &req,
            ["root/Control/dummyDouble"],
            "blocking-meta",
            1000,
        )
        .expect("subscribe");

    assert_ne!(subscription.id(), 0, "server should assign a non-zero id");
    assert_eq!(subscription.name(), "blocking-meta");

    // Clone shares the inner state — both observe the same id.
    let clone = subscription.clone();
    assert_eq!(subscription.id(), clone.id());

    // Nudge the publisher then read through every sync accessor.
    req.set_parameter("root/Control/dummyDouble", 3.5_f64)
        .expect("set");

    // `latest` awaits the next sample through the hidden runtime.
    let (_ts, v): (_, f64) = subscription.latest().expect("latest");
    assert!(v.is_finite());

    // `read` and `read_all` surface the most-recent payload
    // synchronously.
    let (_ts, rv): (_, f64) = subscription.read().expect("read");
    assert!(rv.is_finite());
    let (_ts, flat): (_, Vec<f64>) = subscription.read_all().expect("read_all");
    assert_eq!(flat.len(), 1);
    assert!(flat[0].is_finite());

    sub.unsubscribe(&req, subscription).expect("unsubscribe");
    sub.disconnect().expect("sub disconnect");
    req.disconnect().expect("req disconnect");
}