use std::error::Error;
use std::fmt;
use std::time::Duration;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AutoRequest {
On,
Off,
Status,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct AutoReply {
pub mode: AutoMode,
pub changed: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AutoMode {
On,
Off,
}
impl AutoMode {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::On => "on",
Self::Off => "off",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SupervisorAction {
Start,
Stop,
Status,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SupervisorReply {
pub state: SupervisorState,
pub socket: Option<String>,
pub pid: Option<u32>,
pub changed: bool,
pub uptime: Option<Duration>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum SupervisorState {
Running,
Stopped,
Failed(String),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum SupervisorError {
Unavailable(String),
Io(String),
Refused(String),
}
impl fmt::Display for SupervisorError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Unavailable(s) => write!(f, "supervisor unavailable: {s}"),
Self::Io(s) => write!(f, "supervisor IO error: {s}"),
Self::Refused(s) => write!(f, "supervisor refused: {s}"),
}
}
}
impl Error for SupervisorError {}
pub trait SupervisorSource: Send + Sync + 'static {
fn act(&self, action: SupervisorAction) -> Result<SupervisorReply, SupervisorError>;
fn tear_down_socket(&self) -> Result<bool, SupervisorError>;
}
pub trait AutoSource: Send + Sync + 'static {
fn act(&self, action: AutoRequest) -> Result<AutoReply, SupervisorError>;
}
#[derive(Debug)]
pub struct MockAutoSource {
mode: std::sync::Mutex<AutoMode>,
}
impl MockAutoSource {
#[must_use]
pub fn new(initial: AutoMode) -> Self {
Self {
mode: std::sync::Mutex::new(initial),
}
}
#[must_use]
pub fn current(&self) -> AutoMode {
*self
.mode
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
}
}
impl Default for MockAutoSource {
fn default() -> Self {
Self::new(AutoMode::Off)
}
}
impl AutoSource for MockAutoSource {
fn act(&self, action: AutoRequest) -> Result<AutoReply, SupervisorError> {
let mut guard = self
.mode
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
let prior = *guard;
let (mode, changed) = match action {
AutoRequest::On => (AutoMode::On, prior != AutoMode::On),
AutoRequest::Off => (AutoMode::Off, prior != AutoMode::Off),
AutoRequest::Status => (prior, false),
};
*guard = mode;
Ok(AutoReply { mode, changed })
}
}
#[derive(Debug)]
pub struct MockSupervisorSource {
inner: std::sync::Mutex<MockSupervisorInner>,
}
#[derive(Debug)]
struct MockSupervisorInner {
running: bool,
socket: String,
pid: u32,
started_at: std::time::Instant,
socket_torn_down: bool,
}
impl MockSupervisorSource {
#[must_use]
pub fn new(running: bool) -> Self {
Self {
inner: std::sync::Mutex::new(MockSupervisorInner {
running,
socket: "<operator-socket>".to_owned(),
pid: 4242,
started_at: std::time::Instant::now(),
socket_torn_down: false,
}),
}
}
#[must_use]
pub fn is_running(&self) -> bool {
self.inner
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.running
}
#[must_use]
pub fn socket_torn_down(&self) -> bool {
self.inner
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.socket_torn_down
}
}
impl Default for MockSupervisorSource {
fn default() -> Self {
Self::new(false)
}
}
impl SupervisorSource for MockSupervisorSource {
fn act(&self, action: SupervisorAction) -> Result<SupervisorReply, SupervisorError> {
let mut inner = self
.inner
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
match action {
SupervisorAction::Start => {
let changed = !inner.running;
if changed {
inner.running = true;
inner.started_at = std::time::Instant::now();
inner.socket_torn_down = false;
}
Ok(SupervisorReply {
state: SupervisorState::Running,
socket: Some(inner.socket.clone()),
pid: Some(inner.pid),
changed,
uptime: Some(inner.started_at.elapsed()),
})
}
SupervisorAction::Stop => {
let changed = inner.running;
inner.running = false;
Ok(SupervisorReply {
state: SupervisorState::Stopped,
socket: None,
pid: None,
changed,
uptime: None,
})
}
SupervisorAction::Status => {
if inner.running {
Ok(SupervisorReply {
state: SupervisorState::Running,
socket: Some(inner.socket.clone()),
pid: Some(inner.pid),
changed: false,
uptime: Some(inner.started_at.elapsed()),
})
} else {
Ok(SupervisorReply {
state: SupervisorState::Stopped,
socket: None,
pid: None,
changed: false,
uptime: None,
})
}
}
}
}
fn tear_down_socket(&self) -> Result<bool, SupervisorError> {
let mut inner = self
.inner
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
if inner.running {
inner.running = false;
inner.socket_torn_down = true;
Ok(true)
} else {
Ok(false)
}
}
}
#[cfg(test)]
mod tests {
use super::{
AutoMode, AutoRequest, AutoSource, MockAutoSource, MockSupervisorSource, SupervisorAction,
SupervisorSource, SupervisorState,
};
#[test]
fn mock_auto_flips_on_then_is_idempotent() {
let src = MockAutoSource::new(AutoMode::Off);
let first = src.act(AutoRequest::On).unwrap();
assert!(first.changed);
assert_eq!(first.mode, AutoMode::On);
let again = src.act(AutoRequest::On).unwrap();
assert!(!again.changed);
assert_eq!(again.mode, AutoMode::On);
}
#[test]
fn mock_auto_status_is_pure() {
let src = MockAutoSource::new(AutoMode::On);
let reply = src.act(AutoRequest::Status).unwrap();
assert!(!reply.changed);
assert_eq!(reply.mode, AutoMode::On);
assert_eq!(src.current(), AutoMode::On);
}
#[test]
fn mock_supervisor_start_then_status_reports_running() {
let src = MockSupervisorSource::new(false);
let started = src.act(SupervisorAction::Start).unwrap();
assert!(started.changed);
assert_eq!(started.state, SupervisorState::Running);
let status = src.act(SupervisorAction::Status).unwrap();
assert!(!status.changed);
assert_eq!(status.state, SupervisorState::Running);
assert_eq!(status.socket.as_deref(), Some("<operator-socket>"));
}
#[test]
fn mock_supervisor_tear_down_only_when_running() {
let src = MockSupervisorSource::new(true);
assert!(src.tear_down_socket().unwrap());
assert!(!src.is_running());
assert!(src.socket_torn_down());
assert!(!src.tear_down_socket().unwrap());
}
}