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};
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(())
}
}
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))
}
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
}
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));
}
}
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! {
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! {
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"]);
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"]);
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"]);
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");
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! {
#[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! {
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);
}
}