use async_trait::async_trait;
use endbasic_core::{
ArgSepSyntax, CallError, CallResult, Callable, CallableMetadata, CallableMetadataBuilder,
ExprType, RequiredValueSyntax, Scope, SingularArgSyntax,
};
use std::borrow::Cow;
use std::rc::Rc;
use std::thread;
use std::time::{Duration, Instant};
use crate::MachineBuilder;
pub(crate) const CATEGORY: &str = "Date and time";
#[async_trait(?Send)]
pub trait DateTime {
fn monotonic(&self) -> Duration;
async fn sleep(&self, d: Duration) -> Result<(), String>;
}
pub struct SystemDateTime {
epoch: Instant,
}
impl Default for SystemDateTime {
fn default() -> Self {
Self { epoch: Instant::now() }
}
}
#[async_trait(?Send)]
impl DateTime for SystemDateTime {
fn monotonic(&self) -> Duration {
self.epoch.elapsed()
}
async fn sleep(&self, d: Duration) -> Result<(), String> {
thread::sleep(d);
Ok(())
}
}
pub struct MonotonicFunction {
metadata: Rc<CallableMetadata>,
datetime: Rc<dyn DateTime>,
}
impl MonotonicFunction {
pub fn new(datetime: Rc<dyn DateTime>) -> Rc<Self> {
Rc::from(Self {
metadata: CallableMetadataBuilder::new("MONOTONIC")
.with_return_type(ExprType::Double)
.with_syntax(&[(&[], None)])
.with_category(CATEGORY)
.with_description(
"Returns a monotonic timestamp.
The returned value is the number of seconds since an unspecified reference point. The timestamp \
is only useful to compute elapsed time by subtraction and is not related to the current date or \
wall-clock time.",
)
.build(),
datetime,
})
}
}
#[async_trait(?Send)]
impl Callable for MonotonicFunction {
fn metadata(&self) -> Rc<CallableMetadata> {
self.metadata.clone()
}
fn exec(&self, scope: Scope<'_>) -> CallResult<()> {
debug_assert_eq!(0, scope.nargs());
scope.return_double(self.datetime.monotonic().as_secs_f64())
}
}
pub struct SleepCommand {
metadata: Rc<CallableMetadata>,
datetime: Rc<dyn DateTime>,
}
impl SleepCommand {
pub fn new(datetime: Rc<dyn DateTime>) -> Rc<Self> {
Rc::from(Self {
metadata: CallableMetadataBuilder::new("SLEEP")
.with_async(true)
.with_syntax(&[(
&[SingularArgSyntax::RequiredValue(
RequiredValueSyntax {
name: Cow::Borrowed("seconds"),
vtype: ExprType::Double,
},
ArgSepSyntax::End,
)],
None,
)])
.with_category(CATEGORY)
.with_description(
"Suspends program execution.
Pauses program execution for the given number of seconds, which can be specified either as an \
integer or as a floating point number for finer precision.",
)
.build(),
datetime,
})
}
}
#[async_trait(?Send)]
impl Callable for SleepCommand {
fn metadata(&self) -> Rc<CallableMetadata> {
self.metadata.clone()
}
async fn async_exec(&self, scope: Scope<'_>) -> CallResult<()> {
debug_assert_eq!(1, scope.nargs());
let n = scope.get_double(0);
if n < 0.0 {
return Err(CallError::Syntax(
scope.get_pos(0),
"Sleep time must be positive".to_owned(),
));
}
self.datetime
.sleep(Duration::from_secs_f64(n))
.await
.map_err(|e| CallError::Syntax(scope.get_pos(0), e))
}
}
pub fn add_all(machine: &mut MachineBuilder, datetime: Rc<dyn DateTime>) {
machine.add_callable(MonotonicFunction::new(datetime.clone()));
machine.add_callable(SleepCommand::new(datetime));
}
#[cfg(test)]
mod tests {
use super::*;
use crate::MachineBuilder;
use crate::testutils::*;
use futures_lite::future::block_on;
use std::time::Instant;
#[test]
fn test_monotonic_ok() {
let mut tester = Tester::default();
tester.get_datetime().add_monotonic(Duration::from_millis(12_345));
tester.run("result = MONOTONIC").expect_var("result", 12.345).check();
}
#[test]
fn test_monotonic_errors() {
check_expr_compilation_error("1:10: MONOTONIC expected no arguments", "MONOTONIC()");
}
#[test]
fn test_sleep_ok_int() {
let mut tester = Tester::default();
tester.run("SLEEP 123").check();
assert_eq!(vec![Duration::from_secs(123)], tester.get_datetime().sleeps());
}
#[test]
fn test_sleep_ok_float() {
let mut tester = Tester::default();
tester.run("SLEEP 123.1").check();
let sleeps = tester.get_datetime().sleeps();
assert_eq!(1, sleeps.len());
let ms = sleeps[0].as_millis();
assert!(ms > 123095 && ms < 123105, "Bad {}", ms);
}
#[test]
fn test_sleep_real() {
let before = Instant::now();
let mut machine = MachineBuilder::default().build();
machine.compile(&mut "SLEEP 0.010".as_bytes()).unwrap();
match block_on(machine.exec()) {
Ok(None) => (),
r => panic!("Expected Ok(None) but got {:?}", r),
}
assert!(before.elapsed() >= Duration::from_millis(10));
}
#[test]
fn test_sleep_errors() {
check_stmt_compilation_err("1:1: SLEEP expected seconds#", "SLEEP");
check_stmt_compilation_err("1:1: SLEEP expected seconds#", "SLEEP 2, 3");
check_stmt_compilation_err("1:1: SLEEP expected seconds#", "SLEEP 2; 3");
check_stmt_compilation_err("1:7: STRING is not a number", "SLEEP \"foo\"");
check_stmt_err("1:7: Sleep time must be positive", "SLEEP -1");
check_stmt_err("1:7: Sleep time must be positive", "SLEEP -0.001");
}
}