#![allow(clippy::unwrap_used, clippy::expect_used)]
use std::path::PathBuf;
use std::sync::Mutex;
use std::time::Duration;
use anyhow::Result;
use async_trait::async_trait;
use serde_json::Value;
use tempfile::TempDir;
use tokio::task::JoinHandle;
use super::service::{DaemonService, MenuSnapshot, ServiceStatus};
pub(crate) struct StubService {
name: &'static str,
delay: Duration,
credited: Mutex<Vec<Duration>>,
}
impl StubService {
pub(crate) fn new(name: &'static str) -> Self {
Self::slow(name, Duration::ZERO)
}
pub(crate) fn slow(name: &'static str, delay: Duration) -> Self {
Self {
name,
delay,
credited: Mutex::new(Vec::new()),
}
}
pub(crate) fn credited(&self) -> Vec<Duration> {
self.credited.lock().unwrap().clone()
}
}
#[async_trait]
impl DaemonService for StubService {
fn name(&self) -> &'static str {
self.name
}
async fn handle(&self, _op: &str, _payload: Value) -> Result<Value> {
tokio::time::sleep(self.delay).await;
Ok(serde_json::json!({ "done": true }))
}
fn menu(&self) -> MenuSnapshot {
MenuSnapshot {
title: self.name.to_string(),
items: vec![],
}
}
async fn menu_action(&self, _action_id: &str) -> Result<()> {
Ok(())
}
async fn status(&self) -> ServiceStatus {
ServiceStatus {
name: self.name.to_string(),
healthy: true,
summary: String::new(),
detail: Value::Null,
}
}
async fn shutdown(&self) {}
fn credit_accept_outage(&self, outage: Duration) {
self.credited.lock().unwrap().push(outage);
}
}
pub(crate) fn fake_daemon_reply(reply: Value) -> (TempDir, PathBuf, JoinHandle<()>) {
fake_daemon_replies(vec![reply])
}
pub(crate) fn fake_daemon_replies(replies: Vec<Value>) -> (TempDir, PathBuf, JoinHandle<()>) {
use futures::{SinkExt, StreamExt};
use tokio::net::UnixListener;
use tokio_util::codec::{Framed, LinesCodec};
let dir = tempfile::tempdir_in("/tmp").unwrap();
let sock = dir.path().join("d.sock");
let listener = UnixListener::bind(&sock).unwrap();
let server = tokio::spawn(async move {
for reply in replies {
let (stream, _) = listener.accept().await.unwrap();
let mut framed = Framed::new(stream, LinesCodec::new());
let _req = framed.next().await.unwrap().unwrap();
framed
.send(serde_json::to_string(&reply).unwrap())
.await
.unwrap();
}
});
(dir, sock, server)
}
pub(crate) fn fake_daemon_stream(replies: Vec<Value>) -> (TempDir, PathBuf, JoinHandle<()>) {
use futures::{SinkExt, StreamExt};
use tokio::net::UnixListener;
use tokio_util::codec::{Framed, LinesCodec};
let dir = tempfile::tempdir_in("/tmp").unwrap();
let sock = dir.path().join("d.sock");
let listener = UnixListener::bind(&sock).unwrap();
let server = tokio::spawn(async move {
let (stream, _) = listener.accept().await.unwrap();
let mut framed = Framed::new(stream, LinesCodec::new());
let _req = framed.next().await.unwrap().unwrap();
for reply in replies {
framed
.send(serde_json::to_string(&reply).unwrap())
.await
.unwrap();
}
});
(dir, sock, server)
}
pub(crate) fn fake_daemon_stream_hold_open(
replies: Vec<Value>,
) -> (
TempDir,
PathBuf,
tokio::sync::oneshot::Sender<()>,
JoinHandle<()>,
) {
use futures::{SinkExt, StreamExt};
use tokio::net::UnixListener;
use tokio::sync::oneshot;
use tokio_util::codec::{Framed, LinesCodec};
let dir = tempfile::tempdir_in("/tmp").unwrap();
let sock = dir.path().join("d.sock");
let listener = UnixListener::bind(&sock).unwrap();
let (close_tx, close_rx) = oneshot::channel();
let server = tokio::spawn(async move {
let (stream, _) = listener.accept().await.unwrap();
let mut framed = Framed::new(stream, LinesCodec::new());
let _req = framed.next().await.unwrap().unwrap();
for reply in replies {
framed
.send(serde_json::to_string(&reply).unwrap())
.await
.unwrap();
}
let _ = close_rx.await;
});
(dir, sock, close_tx, server)
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn a_stub_service_answers_at_once_and_serves_nothing_else() {
let stub = StubService::new("stub");
assert_eq!(stub.name(), "stub");
let reply = stub.handle("anything", Value::Null).await.unwrap();
assert_eq!(reply, serde_json::json!({ "done": true }));
assert_eq!(stub.menu().title, "stub");
assert!(stub.menu().items.is_empty());
stub.menu_action("anything").await.unwrap();
let status = stub.status().await;
assert_eq!(status.name, "stub");
assert!(status.healthy);
stub.shutdown().await;
}
#[tokio::test]
async fn a_slow_stub_service_takes_its_delay_to_answer() {
let stub = StubService::slow("slow", Duration::from_millis(40));
let started = std::time::Instant::now();
stub.handle("work", Value::Null).await.unwrap();
assert!(started.elapsed() >= Duration::from_millis(40));
}
#[test]
fn a_stub_service_records_each_credited_outage_in_order() {
let stub = StubService::new("stub");
assert!(stub.credited().is_empty());
stub.credit_accept_outage(Duration::from_secs(1));
stub.credit_accept_outage(Duration::from_secs(2));
assert_eq!(
stub.credited(),
vec![Duration::from_secs(1), Duration::from_secs(2)]
);
}
}