use serde::Serialize;
use shep_core::protocol::{DogAccess, DogSource, ProcessInfo};
use crate::output::Render;
use crate::style::Presentation;
use super::toolkit::{dog_action_paint, exit_cell, paint, process_info_paint, reported};
const EMPTY: &str = "-";
fn source_cell(source: Option<&DogSource>) -> String {
source.map_or(EMPTY, |source| source.into()).to_owned()
}
fn access_cell(access: Option<&DogAccess>) -> String {
let Some(access) = access.filter(|access| !access.is_clear()) else {
return EMPTY.to_owned();
};
[
(access.undeclared, "undeclared"),
(access.not_reviewed, "not reviewed"),
(!access.waiting.is_empty(), "waiting"),
]
.into_iter()
.filter_map(|(on, word)| on.then_some(word))
.collect::<Vec<_>>()
.join(", ")
}
#[derive(Debug, Serialize)]
#[serde(transparent)]
pub struct DogRows(pub Vec<ProcessInfo>);
impl Render for DogRows {
fn headers() -> &'static [&'static str] {
&[
"ID", "NAME", "STATUS", "PID", "RESTARTS", "EXIT", "CPU", "MEM", "UPTIME", "SOURCE",
"ACCESS",
]
}
fn rows(&self) -> Vec<Vec<String>> {
self.0
.iter()
.map(|p| {
vec![
p.id.to_string(),
p.name.clone(),
reported(p).word(),
p.pid
.map_or_else(|| EMPTY.to_string(), |pid| pid.to_string()),
p.restarts.to_string(),
exit_cell(p.pid, p.last_exit),
p.cpu_percent
.map_or_else(|| EMPTY.to_string(), |cpu| format!("{cpu:.1}%")),
p.memory_bytes
.map_or_else(|| EMPTY.to_string(), crate::output::human_bytes),
crate::output::human_duration(p.uptime_ms),
source_cell(p.dog.as_ref()),
access_cell(p.access.as_ref()),
]
})
.collect()
}
fn rows_for(&self, presentation: Presentation, status_word: bool) -> Vec<Vec<String>> {
paint(
self.rows(),
Self::headers(),
presentation,
status_word,
|header, _cell, index| process_info_paint(header, &self.0[index]),
)
}
#[track_caller]
fn json_key_for(header: &str) -> String {
match header {
"ID" => "id",
"NAME" => "name",
"STATUS" => "status",
"PID" => "pid",
"RESTARTS" => "restarts",
"EXIT" => "last_exit",
"CPU" => "cpu_percent",
"MEM" => "memory_bytes",
"UPTIME" => "uptime_ms",
"SOURCE" => "dog",
"ACCESS" => "access",
other => panic!("DogRows::headers() does not include {other:?}"),
}
.to_owned()
}
const JSON_ONLY: &'static [&'static str] = &[
"fold",
"out_file",
"err_file",
"lambs",
"smit",
"handshook",
"dog_stale",
"instance",
"pending",
"overridden",
"depends_on",
"max_memory",
"cpu_ms",
];
const PRIORITIES: &'static [u8] = &[0, 0, 0, 2, 4, 6, 5, 3, 1, 7, 6];
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DogActionStatus {
NoShepherd(&'static str),
Acted(&'static str),
}
impl DogActionStatus {
#[must_use]
pub const fn text(self) -> &'static str {
match self {
Self::NoShepherd(text) | Self::Acted(text) => text,
}
}
#[must_use]
pub const fn shepherd_acted(self) -> bool {
matches!(self, Self::Acted(..))
}
}
#[derive(Debug, Serialize)]
pub struct DogActionRow {
name: String,
source: Option<DogSource>,
shepherd_acted: bool,
status: &'static str,
}
impl DogActionRow {
#[must_use]
pub fn new(
name: impl Into<String>,
source: impl Into<Option<DogSource>>,
status: DogActionStatus,
) -> Self {
Self {
name: name.into(),
source: source.into(),
shepherd_acted: status.shepherd_acted(),
status: status.text(),
}
}
}
impl Render for DogActionRow {
fn headers() -> &'static [&'static str] {
&["NAME", "SOURCE", "SHEPHERD", "STATUS"]
}
fn rows(&self) -> Vec<Vec<String>> {
vec![vec![
self.name.clone(),
source_cell(self.source.as_ref()),
self.shepherd_acted.to_string(),
self.status.to_owned(),
]]
}
fn rows_for(&self, presentation: Presentation, status_word: bool) -> Vec<Vec<String>> {
paint(
self.rows(),
Self::headers(),
presentation,
status_word,
|header, cell, _index| dog_action_paint(header, cell),
)
}
#[track_caller]
fn json_key_for(header: &str) -> String {
match header {
"NAME" => "name",
"SOURCE" => "source",
"SHEPHERD" => "shepherd_acted",
"STATUS" => "status",
other => panic!("DogActionRow::headers() does not include {other:?}"),
}
.to_owned()
}
const PRIORITIES: &'static [u8] = &[0, 7, 6, 0];
}
#[cfg(test)]
pub(crate) mod tests {
use shep_core::{protocol::DogSource, status::ProcStatus};
use crate::vocabulary::Role;
use super::super::tests::{assert_no_drift, coloured, dog_info, painted, sample_dog};
use super::*;
#[test]
fn the_source_column_names_a_kind_and_leaves_the_path_to_json() {
let rows = DogRows(vec![
dog_info("metrics", DogSource::BuiltIn { grants: None }),
dog_info(
"otel",
DogSource::Adopted {
grants: None,
path: "/usr/local/bin/shep-otel".to_string(),
channel: false,
},
),
]);
let headers = DogRows::headers();
let at = |cells: &[String], h: &str| {
cells[headers.iter().position(|x| *x == h).unwrap()].clone()
};
assert_eq!(at(&rows.rows()[0], "SOURCE"), "built-in");
assert_eq!(at(&rows.rows()[1], "SOURCE"), "adopted");
let json = serde_json::to_value(&rows).unwrap();
assert_eq!(json[1]["dog"]["path"], "/usr/local/bin/shep-otel");
}
#[test]
fn dog_rows_do_not_drift() {
assert_no_drift(
&DogRows(vec![dog_info(
"metrics",
DogSource::BuiltIn { grants: None },
)]),
|j| &j[0],
&["UPTIME", "CPU", "MEM", "SOURCE", "EXIT", "ACCESS"],
);
}
#[test]
fn dog_enabled_row_does_not_drift() {
assert_no_drift(
&DogActionRow::new(
"metrics",
DogSource::BuiltIn { grants: None },
DogActionStatus::Acted("online"),
),
|j| j,
&["SOURCE"],
);
}
#[test]
fn dog_disabled_row_does_not_drift() {
assert_no_drift(
&DogActionRow::new(
"metrics",
DogSource::BuiltIn { grants: None },
DogActionStatus::NoShepherd("not running; will not start with the next shepherd"),
),
|j| j,
&["SOURCE"],
);
}
#[test]
fn dog_adopted_row_does_not_drift() {
assert_no_drift(
&DogActionRow::new(
"otel",
DogSource::Adopted {
grants: None,
path: "/usr/local/bin/shep-otel".to_string(),
channel: false,
},
DogActionStatus::Acted("online"),
),
|j| j,
&["SOURCE"],
);
}
#[test]
fn the_shepherd_cell_and_key_come_from_the_status_variant() {
let acted = DogActionRow::new(
"metrics",
DogSource::BuiltIn { grants: None },
DogActionStatus::Acted("online"),
);
assert_eq!(acted.rows()[0][2], "true");
assert_eq!(acted.rows()[0][3], "online");
assert_eq!(
serde_json::to_value(&acted).unwrap()["shepherd_acted"],
serde_json::json!(true)
);
let absent = DogActionRow::new(
"metrics",
DogSource::BuiltIn { grants: None },
DogActionStatus::NoShepherd("no shepherd running; the config was written"),
);
assert_eq!(absent.rows()[0][2], "false");
assert_eq!(
absent.rows()[0][3],
"no shepherd running; the config was written"
);
assert_eq!(
serde_json::to_value(&absent).unwrap()["shepherd_acted"],
serde_json::json!(false)
);
}
#[test]
fn dog_rehomed_row_does_not_drift_with_or_without_a_source() {
assert_no_drift(
&DogActionRow::new(
"otel",
DogSource::Adopted {
grants: None,
path: "/usr/local/bin/shep-otel".to_string(),
channel: false,
},
DogActionStatus::Acted("stopped"),
),
|j| j,
&["SOURCE"],
);
assert_no_drift(
&DogActionRow::new(
"ghost",
None::<DogSource>,
DogActionStatus::NoShepherd("not running; will not start with the next shepherd"),
),
|j| j,
&["SOURCE"],
);
}
#[test]
fn the_dogs_table_is_coloured_by_the_flock_tables_own_rules() {
let rows =
DogRows(vec![sample_dog(ProcStatus::Online, Some(14_110))]).rows_for(coloured(), true);
let row = &rows[0];
assert_eq!(row[0], painted("9", Role::Ink3), "ID is chrome");
assert_eq!(row[1], "log-rotate", "NAME is plain, as in the flock table");
assert_eq!(
row[2],
painted("(o.o) online", Role::Meadow),
"STATUS takes the face and the role, from vocabulary.rs"
);
assert_eq!(row[3], "14110", "a real PID is left plain");
assert_eq!(row[4], painted("4", Role::Butter), "RESTARTS above zero");
assert_eq!(row[5], painted("-", Role::Ink3), "EXIT: still running");
assert_eq!(row[6], painted("0.0%", Role::Ink3), "idle CPU is not news");
assert_eq!(row[7], painted("3.0M", Role::Meadow), "MEM below the ramp");
assert_eq!(row[8], "41s", "UPTIME is plain, as in the flock table");
assert_eq!(
row[9],
painted("adopted", Role::Butter),
"SOURCE carries the trust distinction, and sits last"
);
}
#[test]
fn a_stopped_dogs_placeholders_are_muted() {
let rows = DogRows(vec![sample_dog(ProcStatus::Stopped, None)]).rows_for(coloured(), true);
let row = &rows[0];
assert_eq!(row[2], painted("(-.-) stopped", Role::Ink3));
assert_eq!(row[3], painted("-", Role::Ink3), "PID");
assert_eq!(row[6], painted("-", Role::Ink3), "CPU");
assert_eq!(row[7], painted("-", Role::Ink3), "MEM");
}
#[test]
fn a_dog_action_row_colours_a_status_and_never_a_sentence() {
let acted = DogActionRow::new(
"log-rotate",
DogSource::Adopted {
grants: None,
path: "/usr/local/bin/shep-log-rotate".to_string(),
channel: false,
},
DogActionStatus::Acted("online"),
);
let row = &acted.rows_for(coloured(), true)[0];
assert_eq!(
row[1],
painted("adopted", Role::Butter),
"SOURCE says this is not shep's own code"
);
assert_eq!(row[3], painted("(o.o) online", Role::Meadow));
let sentence = "no shepherd running; the config was written";
let unacted = DogActionRow::new(
"log-rotate",
DogSource::BuiltIn { grants: None },
DogActionStatus::NoShepherd(sentence),
);
let row = &unacted.rows_for(coloured(), true)[0];
assert_eq!(row[3], sentence, "a sentence is left exactly as it was");
}
#[test]
fn a_dog_action_row_leaves_the_name_and_the_shepherd_column_plain() {
let row = &DogActionRow::new(
"log-rotate",
DogSource::BuiltIn { grants: None },
DogActionStatus::NoShepherd("no shepherd running"),
)
.rows_for(coloured(), true)[0];
assert_eq!(row[0], "log-rotate");
assert_eq!(row[2], "false");
}
#[test]
fn a_rehomed_row_with_nothing_to_forget_still_mutes_its_source() {
let row = &DogActionRow::new(
"metrics",
None::<DogSource>,
DogActionStatus::Acted("stopped"),
)
.rows_for(coloured(), true)[0];
assert_eq!(row[1], painted("-", Role::Ink3));
assert_eq!(row[3], painted("(-.-) stopped", Role::Ink3));
}
#[test]
fn the_dog_action_json_keeps_the_path_the_source_column_drops() {
let adopted = DogActionRow::new(
"otel",
DogSource::Adopted {
grants: None,
path: "/usr/local/bin/shep-otel".to_string(),
channel: false,
},
DogActionStatus::Acted("online"),
);
assert_eq!(adopted.rows()[0][1], "adopted", "the column names a kind");
let json = serde_json::to_value(&adopted).unwrap();
assert_eq!(json["source"]["kind"], "adopted");
assert_eq!(json["source"]["path"], "/usr/local/bin/shep-otel");
assert_eq!(json["status"], "online");
assert_eq!(json["shepherd_acted"], serde_json::json!(true));
let forgotten = DogActionRow::new(
"metrics",
None::<DogSource>,
DogActionStatus::NoShepherd("not running; will not start with the next shepherd"),
);
assert_eq!(forgotten.rows()[0][1], "-");
assert_eq!(
serde_json::to_value(&forgotten).unwrap()["source"],
serde_json::Value::Null
);
}
pub(crate) fn styled(level: crate::style::StyleLevel) -> Presentation {
Presentation::new(
level,
None,
Some(std::ffi::OsStr::new("xterm-256color")),
None,
200,
)
}
fn dog_with_contact(handshook: Option<bool>) -> ProcessInfo {
let mut dog = sample_dog(ProcStatus::Online, Some(208_341));
dog.handshook = handshook;
dog
}
pub(crate) fn cell_of<T: Render>(row: &[String], header: &str) -> String {
row[T::headers().iter().position(|h| *h == header).unwrap()].clone()
}
#[test]
fn a_dog_that_has_never_answered_the_shepherd_does_not_read_as_online() {
let rows = DogRows(vec![dog_with_contact(Some(false))]).rows();
assert_eq!(cell_of::<DogRows>(&rows[0], "STATUS"), "silent");
}
#[test]
fn a_silent_dog_reads_the_same_in_all_three_styles() {
use crate::style::StyleLevel;
let dogs = DogRows(vec![dog_with_contact(Some(false))]);
let full = dogs.rows_for(styled(StyleLevel::Full), true);
assert_eq!(
cell_of::<DogRows>(&full[0], "STATUS"),
painted("(?_?) silent", Role::Butter)
);
let plain = dogs.rows_for(styled(StyleLevel::Plain), true);
assert_eq!(
cell_of::<DogRows>(&plain[0], "STATUS"),
painted("silent", Role::Butter)
);
let bare = dogs.rows();
let cell = cell_of::<DogRows>(&bare[0], "STATUS");
assert_eq!(cell, "silent");
assert!(!cell.contains('\u{1b}'), "bare carries no escape: {cell:?}");
}
#[test]
fn a_dog_that_has_answered_renders_exactly_as_before() {
use crate::style::StyleLevel;
let mut before = sample_dog(ProcStatus::Online, Some(208_341));
before.handshook = None;
let talking = dog_with_contact(Some(true));
assert_eq!(
DogRows(vec![talking.clone()]).rows(),
DogRows(vec![before.clone()]).rows()
);
assert_eq!(
DogRows(vec![talking]).rows_for(styled(StyleLevel::Full), true),
DogRows(vec![before]).rows_for(styled(StyleLevel::Full), true)
);
}
#[test]
fn a_dog_from_a_shepherd_predating_the_field_reads_as_it_always_did() {
use crate::style::StyleLevel;
let rows = DogRows(vec![dog_with_contact(None)]).rows();
assert_eq!(cell_of::<DogRows>(&rows[0], "STATUS"), "online");
let full = DogRows(vec![dog_with_contact(None)]).rows_for(styled(StyleLevel::Full), true);
assert_eq!(
cell_of::<DogRows>(&full[0], "STATUS"),
painted("(o.o) online", Role::Meadow)
);
}
#[test]
fn only_online_is_overridden_by_a_silence() {
for status in [
ProcStatus::Starting,
ProcStatus::Stopping,
ProcStatus::Stopped,
ProcStatus::Errored,
ProcStatus::WaitingRestart,
] {
let mut dog = sample_dog(status, Some(208_341));
dog.handshook = Some(false);
let rows = DogRows(vec![dog]).rows();
assert_eq!(
cell_of::<DogRows>(&rows[0], "STATUS"),
status.to_string(),
"{status} says what it says without help"
);
}
}
#[test]
fn the_json_form_carries_the_handshake_fact() {
let json = serde_json::to_value(DogRows(vec![
dog_with_contact(Some(false)),
dog_with_contact(Some(true)),
dog_with_contact(None),
]))
.unwrap();
assert_eq!(json[0]["handshook"], serde_json::json!(false));
assert_eq!(json[0]["status"], "online");
assert_eq!(json[1]["handshook"], serde_json::json!(true));
assert_eq!(json[2]["handshook"], serde_json::Value::Null);
}
}