use shep_core::status::ProcStatus;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum Role {
Meadow,
Butter,
Bark,
Ink3,
Sky,
}
pub(crate) const fn role_of(status: ProcStatus) -> Role {
match status {
ProcStatus::Online => Role::Meadow,
ProcStatus::Starting | ProcStatus::WaitingRestart => Role::Butter,
ProcStatus::Errored => Role::Bark,
ProcStatus::Stopping | ProcStatus::Stopped => Role::Ink3,
_ => Role::Butter,
}
}
pub(crate) const fn face(status: ProcStatus) -> &'static str {
match status {
ProcStatus::Online => "(o.o)",
ProcStatus::Starting => "(o~o)",
ProcStatus::WaitingRestart => "(>_<)",
ProcStatus::Stopping | ProcStatus::Stopped => "(-.-)",
ProcStatus::Errored => "(x.x)",
_ => "(?.?)",
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum Reported {
Live(ProcStatus),
Silent,
Disabled,
}
impl Reported {
pub(crate) const fn of(status: ProcStatus, handshook: Option<bool>) -> Self {
match (status, handshook) {
(ProcStatus::Online, Some(false)) => Self::Silent,
_ => Self::Live(status),
}
}
pub(crate) fn word(self) -> String {
match self {
Self::Live(status) => status.to_string(),
Self::Silent => "silent".to_string(),
Self::Disabled => "disabled".to_string(),
}
}
pub(crate) const fn face(self) -> &'static str {
match self {
Self::Live(status) => face(status),
Self::Silent => "(?_?)",
Self::Disabled => face(ProcStatus::Stopped),
}
}
pub(crate) const fn role(self) -> Role {
match self {
Self::Live(status) => role_of(status),
Self::Silent => Role::Butter,
Self::Disabled => role_of(ProcStatus::Stopped),
}
}
}
pub(crate) fn silence_note(
name: &str,
reported: Reported,
dog_stale: Option<bool>,
) -> Option<String> {
if reported != Reported::Silent {
return None;
}
let budget = shep_daemon::dogs::DOG_SILENCE_BUDGET.as_secs();
Some(match dog_stale {
Some(false) => format!(
"silent `{name}`'s process is up and it has never answered this shepherd. \
After {budget}s of that, shep restarts a dog once from the binary on disk; if \
the restarted dog stays silent shep gives up and says so here. \
`shep bleats {name}` shows what the dog itself says about connecting."
),
Some(true) => format!(
"silent `{name}`'s process is up and this shepherd has GIVEN UP on it: the one \
restart it earned did not help, so it will not be restarted again and nothing \
more will happen on its own. shep wrote what it saw at that moment into this \
dog's own log -- run `shep bleats {name}` and read it, because that line names \
what shep observed and this listing cannot. Three different faults arrive here \
looking identical: a dog that never reached the socket, one that reaches it and \
never names itself, and one this shepherd turned away on protocol skew. What to \
do about them differs, so the log is the surface that says."
),
None => format!(
"silent `{name}`'s process is up and it has never answered this shepherd. The \
shepherd answering this listing is too old to say whether it has given up on the \
dog or is still waiting for it -- run `shep bleats {name}` for the dog's own \
account, and `shep daemon reload` to bring the running shepherd up to the shep \
that is installed, which can answer."
),
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn every_status_has_a_five_column_face() {
for status in [
ProcStatus::Online,
ProcStatus::Starting,
ProcStatus::WaitingRestart,
ProcStatus::Stopping,
ProcStatus::Stopped,
ProcStatus::Errored,
ProcStatus::Unrecognized,
] {
let face = Reported::Live(status).face();
assert_eq!(
face.chars().count(),
5,
"{status} face {face:?} must be 5 columns; the table budget assumes it"
);
assert!(
face.is_ascii(),
"{status} face {face:?} must be ASCII: an emoji is \
double-width, inconsistently so, and cannot take a colour"
);
}
let silent = Reported::Silent.face();
assert_eq!(silent.chars().count(), 5, "silent face {silent:?}");
assert!(silent.is_ascii(), "silent face {silent:?}");
}
#[test]
fn an_unrecognized_status_looks_like_no_other() {
let unrecognized = Reported::Live(ProcStatus::Unrecognized);
let others = [
Reported::Live(ProcStatus::Online),
Reported::Live(ProcStatus::Starting),
Reported::Live(ProcStatus::WaitingRestart),
Reported::Live(ProcStatus::Stopping),
Reported::Live(ProcStatus::Stopped),
Reported::Live(ProcStatus::Errored),
Reported::Silent,
Reported::Disabled,
];
for other in others {
assert_ne!(unrecognized.face(), other.face(), "{other:?} face");
assert_ne!(unrecognized.word(), other.word(), "{other:?} word");
}
assert_eq!(unrecognized.face(), "(?.?)");
assert_eq!(unrecognized.word(), "unrecognized");
}
#[test]
fn a_silence_overrides_online_and_nothing_else() {
assert_eq!(
Reported::of(ProcStatus::Online, Some(false)),
Reported::Silent
);
for status in [
ProcStatus::Starting,
ProcStatus::Stopping,
ProcStatus::Stopped,
ProcStatus::Errored,
ProcStatus::WaitingRestart,
] {
assert_eq!(Reported::of(status, Some(false)), Reported::Live(status));
}
assert_eq!(
Reported::of(ProcStatus::Online, Some(true)),
Reported::Live(ProcStatus::Online)
);
assert_eq!(
Reported::of(ProcStatus::Online, None),
Reported::Live(ProcStatus::Online)
);
}
#[test]
fn a_silent_row_says_silent_in_butter() {
assert_eq!(Reported::Silent.word(), "silent");
assert_eq!(Reported::Silent.role(), Role::Butter);
assert_eq!(
Reported::Live(ProcStatus::Online).word(),
ProcStatus::Online.to_string(),
"a live row still spells its status exactly as the wire does"
);
}
#[test]
fn only_a_silent_row_gets_a_note() {
for status in [
ProcStatus::Online,
ProcStatus::Starting,
ProcStatus::WaitingRestart,
ProcStatus::Stopping,
ProcStatus::Stopped,
ProcStatus::Errored,
] {
for stale in [None, Some(false), Some(true)] {
assert_eq!(
silence_note("web", Reported::Live(status), stale),
None,
"status={status:?} dog_stale={stale:?}"
);
}
}
assert!(silence_note("log-rotate", Reported::Silent, Some(false)).is_some());
}
#[test]
fn the_three_silences_read_differently_and_all_lead_somewhere() {
let notes: Vec<String> = [None, Some(false), Some(true)]
.into_iter()
.map(|stale| silence_note("log-rotate", Reported::Silent, stale).expect("a silent row"))
.collect();
for note in ¬es {
assert!(note.contains("log-rotate"), "names the dog: {note}");
assert!(
note.contains("shep bleats log-rotate"),
"and sends the reader to the one file that holds the evidence: {note}"
);
assert!(
note.starts_with("silent"),
"labelled with the word it explains: {note}"
);
}
let mut distinct = notes.clone();
distinct.sort_unstable();
distinct.dedup();
assert_eq!(
distinct.len(),
notes.len(),
"three states, three sentences: {notes:?}"
);
}
#[test]
fn the_given_up_note_says_the_shepherd_has_stopped_trying() {
let note = silence_note("log-rotate", Reported::Silent, Some(true)).expect("a silent row");
assert!(note.contains("GIVEN UP"), "{note}");
assert!(note.contains("will not be restarted again"), "{note}");
assert!(note.contains("shep bleats log-rotate"), "{note}");
assert!(
!note.contains("never answered"),
"a refused dog did answer: {note}"
);
assert!(
!note.contains("reinstalling"),
"this surface cannot say whether reinstalling helps: {note}"
);
}
#[test]
fn the_roles_match_what_lookout_already_showed() {
assert_eq!(role_of(ProcStatus::Online), Role::Meadow);
assert_eq!(role_of(ProcStatus::Starting), Role::Butter);
assert_eq!(role_of(ProcStatus::WaitingRestart), Role::Butter);
assert_eq!(role_of(ProcStatus::Errored), Role::Bark);
assert_eq!(role_of(ProcStatus::Stopping), Role::Ink3);
assert_eq!(role_of(ProcStatus::Stopped), Role::Ink3);
}
#[test]
fn the_faces_are_distinct_from_one_another() {
let faces = [
face(ProcStatus::Online),
face(ProcStatus::Starting),
face(ProcStatus::WaitingRestart),
face(ProcStatus::Stopped),
face(ProcStatus::Errored),
Reported::Silent.face(),
];
let mut seen = faces.to_vec();
seen.sort_unstable();
seen.dedup();
assert_eq!(
seen.len(),
faces.len(),
"each state needs its own face: {faces:?}"
);
}
}