rstango 0.4.0

Implementation of the Tango distributed control system
Documentation
//! The polling of the devices of a server: where its periods come from, what it records
//! and sends, and how a client changes it through the admin device.

mod common;

use std::sync::atomic::{AtomicI32, Ordering};
use std::time::{Duration, Instant};
use std::thread;

use tango::dev::{AttrValueEvent, DevProxy, DevSource};
use tango::server::{AttrDef, ClassDef, CommandDef, Dev};
use tango::error::Result;
use tango::testing::TestContext;

use common::{local_and_wire, test_context, test_server};


/// A motor counting how many times it is read.
struct Motor {
    reads: AtomicI32,
}

impl Motor {

    fn init(_dev: &Dev<Self>) -> Result<Self> {
        Ok(Self { reads: AtomicI32::new(0) })
    }

    fn count(&self, _dev: &Dev<Self>) -> Result<i32> {
        Ok(self.reads.fetch_add(1, Ordering::Relaxed) + 1)
    }

    fn position(&self, _dev: &Dev<Self>) -> Result<f64> {
        Ok(1.5)
    }

    fn set_target(&self, _dev: &Dev<Self>, _target: f64) -> Result<()> {
        Ok(())
    }

    fn get_speed(&self, _dev: &Dev<Self>, _: ()) -> Result<i32> {
        Ok(7)
    }

    fn move_by(&self, _dev: &Dev<Self>, _delta: f64) -> Result<()> {
        Ok(())
    }

}

/// The motor class, which asks for its `Position` and its `GetSpeed` to be polled.
fn motor_def() -> ClassDef<Motor> {
    ClassDef::new("Motor", Motor::init)
        .attr(AttrDef::new("Count")
            .read(Motor::count)
            .periodic_event(Duration::from_millis(50)))
        .attr(AttrDef::new("Position")
            .read(Motor::position)
            .polled(Duration::from_millis(500)))
        .attr(AttrDef::new("Target")
            .write(Motor::set_target))
        .command(CommandDef::new("GetSpeed")
            .exec(Motor::get_speed)
            .polled(Duration::from_millis(500)))
        .command(CommandDef::new("MoveBy")
            .exec(Motor::move_by))
}

/// A context serving the motor `motor/dev/1`, its database holding those properties of it.
fn serve(local: bool, props: &[(&str, &[&str])]) -> TestContext {

    let ctx = test_context(&[("motor/dev/1", "Motor")]);
    for &(name, values) in props {
        let values = values.iter().map(|value| value.to_string()).collect::<Vec<_>>();
        ctx.database().set_dev_prop("motor/dev/1", (name, values)).unwrap();
    }

    ctx.serve(test_server(local)
        .class(motor_def()))
        .unwrap();

    ctx

}

/// Wait for that to hold, failing after a few seconds.
fn wait_until(what: &str, mut cond: impl FnMut() -> bool) {
    let deadline = Instant::now() + Duration::from_secs(5);
    while !cond() {
        assert!(Instant::now() < deadline, "timed out waiting for {what}");
        thread::sleep(Duration::from_millis(10));
    }
}

/// What the database holds of that property of the motor.
fn prop(ctx: &TestContext, name: &str) -> Vec<String> {
    ctx.database().get_dev_prop::<Vec<String>, _>("motor/dev/1", name).unwrap()
}

fn ms(ms: u64) -> Duration {
    Duration::from_millis(ms)
}


local_and_wire! {
    /// The periods a device is polled at: the database first, then its definitions, what
    /// is polled being recorded in its history and read from the cache.
    fn periods(local: bool) {

        let ctx = serve(local, &[
            ("polled_attr", &["count", "20", "Position", "300", "Missing", "100"]),
            ("poll_ring_depth", &["3"]),
        ]);

        let client = ctx.dev("motor/dev/1");
        assert_eq!(client.attr_poll_period("Count").unwrap(), Some(ms(20)));
        assert_eq!(client.attr_poll_period("Position").unwrap(), Some(ms(300)));
        assert_eq!(client.command_poll_period("GetSpeed").unwrap(), Some(ms(500)));
        assert_eq!(client.attr_poll_period("Target").unwrap(), None);
        assert_eq!(client.attr_poll_period("Missing").unwrap(), None);

        wait_until("the history to fill", || client.attr_history::<i32>("Count", 10).is_ok_and(|history| history.len() == 3));
        assert!(client.command_history::<i32>("GetSpeed", 10).is_ok());

        let mut cached = client.clone();
        cached.set_source(DevSource::Cache);
        assert!(cached.read_attr::<i32, _>("Count").unwrap().value.unwrap() >= 3);
        assert_eq!(cached.command::<_, i32>("GetSpeed", ()).unwrap(), 7);
        assert_eq!(cached.read_attr::<f64, _>("Target").unwrap_err().reason(), "API_AttrNotPolled");

        let status = client.polling_status().unwrap();
        assert_eq!(status.len(), 3);
        assert!(status[0].starts_with("Polled command name = GetSpeed"));
        assert!(status.iter().any(|status| status.contains("Polling ring buffer depth = 3")));

    }
}

local_and_wire! {
    /// The polling a client changes through the admin device, which writes it to the
    /// database along, so that a restarted device is polled the same.
    fn admin(local: bool) {

        let ctx = serve(local, &[]);
        let client = ctx.dev("motor/dev/1");
        let admin = client.admin_dev().unwrap();

        client.poll_attr("Count", ms(100)).unwrap();
        assert_eq!(client.attr_poll_period("Count").unwrap(), Some(ms(100)));
        assert_eq!(prop(&ctx, "polled_attr"), ["Count", "100"]);

        // Polled already, which is its period updated.
        client.poll_attr("count", ms(200)).unwrap();
        assert_eq!(client.attr_poll_period("Count").unwrap(), Some(ms(200)));
        assert_eq!(prop(&ctx, "polled_attr"), ["Count", "200"]);

        // A period the definition asks for, written once changed.
        client.poll_command("GetSpeed", ms(250)).unwrap();
        assert_eq!(prop(&ctx, "polled_cmd"), ["GetSpeed", "250"]);

        assert_eq!(admin.command::<_, Vec<String>>("PolledDevice", ()).unwrap(), ["motor/dev/1"]);

        // A restart reads the database again, the device restarted being another one,
        // which a client reaching it directly has to reach anew.
        admin.command::<_, ()>("DevRestart", "motor/dev/1").unwrap();
        let client = ctx.dev("motor/dev/1");
        assert_eq!(client.attr_poll_period("Count").unwrap(), Some(ms(200)));
        assert_eq!(client.command_poll_period("GetSpeed").unwrap(), Some(ms(250)));

        client.stop_poll_attr("Count").unwrap();
        assert_eq!(client.attr_poll_period("Count").unwrap(), None);
        assert!(prop(&ctx, "polled_attr").is_empty());
        assert_eq!(client.stop_poll_attr("Count").unwrap_err().reason(), "API_PollObjNotFound");

        // Neither a write-only attribute nor a command taking an argument can be polled,
        // nor what the device does not have.
        assert_eq!(client.poll_attr("Target", ms(100)).unwrap_err().reason(), "API_NotSupported");
        assert_eq!(client.poll_command("MoveBy", ms(100)).unwrap_err().reason(), "API_NotSupported");
        assert!(client.poll_attr("Missing", ms(100)).is_err());
        assert_eq!(client.poll_attr("Count", Duration::ZERO).unwrap_err().reason(), "API_NotSupported");

    }
}

local_and_wire! {
    /// The periodic events of a polled attribute, sent at the period of the event rather
    /// than at the one of the polling, and refused once it is not polled.
    #[cfg_attr(miri, ignore = "Miri is too slow: the wire variant times out subscribing, before even reaching ZMQ")]
    fn periodic_event(local: bool) {

        let ctx = serve(local, &[("polled_attr", &["Count", "10"])]);
        let client = ctx.dev("motor/dev/1");

        let mut periodic = client.subscribe_attr_value::<i32>("Count", AttrValueEvent::Periodic);
        periodic.ensure_subscribed().unwrap();

        let first = periodic.wait(Duration::from_secs(5)).unwrap().value.unwrap();
        let second = periodic.wait(Duration::from_secs(5)).unwrap().value.unwrap();
        assert!(second > first + 1, "polled at 10 ms, sent at 50 ms: {first} then {second}");

        client.stop_poll_attr("Count").unwrap();
        let refused = client.subscribe_attr_value::<i32>("Count", AttrValueEvent::Periodic).ensure_subscribed();
        assert_eq!(refused.unwrap_err().reason(), "API_AttributePollingNotStarted");

    }
}

local_and_wire! {
    /// Nothing is polled between the `StopPolling` and the `StartPolling` of the admin
    /// device, what is polled being kept.
    fn stop_start(local: bool) {

        let ctx = serve(local, &[("polled_attr", &["Count", "10"])]);
        let client = ctx.dev("motor/dev/1");
        let admin = client.admin_dev().unwrap();

        wait_until("a first poll", || client.attr_history::<i32>("Count", 1).is_ok());
        admin.command::<_, ()>("StopPolling", ()).unwrap();
        thread::sleep(ms(50));

        let latest = |client: &DevProxy| client.attr_history::<i32>("Count", 1).unwrap()[0].value.as_ref().unwrap().value.unwrap();
        let stopped = latest(&client);
        thread::sleep(ms(100));
        assert_eq!(latest(&client), stopped);
        assert_eq!(client.attr_poll_period("Count").unwrap(), Some(ms(10)));

        admin.command::<_, ()>("StartPolling", ()).unwrap();
        wait_until("the polling to start again", || latest(&client) > stopped);

    }
}