use std::io;
use serde::Serialize;
use shep_core::protocol::{HostUsage, ProcessInfo};
use crate::cli::Format;
use crate::style::Presentation;
use super::{DogRows, FlockRows, SCHEMA_VERSION, rows, table_of};
#[derive(Serialize)]
#[cfg_attr(windows, allow(dead_code))]
struct FlockEnvelope<'a> {
schema_version: u32,
command: &'a str,
data: FlockRows,
#[serde(skip_serializing_if = "Option::is_none")]
host: Option<Option<HostUsage>>,
}
#[cfg_attr(windows, allow(dead_code))]
pub fn emit_flock(
out: &mut dyn io::Write,
fmt: Format,
command: &str,
listing: Vec<ProcessInfo>,
host: Option<Option<HostUsage>>,
style: Presentation,
) -> io::Result<()> {
match fmt {
Format::Json => {
let envelope = FlockEnvelope {
schema_version: SCHEMA_VERSION,
command,
data: FlockRows(listing),
host,
};
serde_json::to_writer(&mut *out, &envelope)?;
writeln!(out)
}
Format::Table => {
let (dogs, sheep): (Vec<ProcessInfo>, Vec<ProcessInfo>) =
listing.into_iter().partition(|p| p.dog.is_some());
write!(out, "{}", table_of(&FlockRows(sheep), style))?;
if dogs.is_empty() {
return Ok(());
}
let pointer = silence_pointer(&dogs);
write!(out, "\nDogs\n")?;
write!(out, "{}", table_of(&DogRows(dogs), style))?;
match pointer {
None => Ok(()),
Some(line) => writeln!(out, "\n{line}"),
}
}
}
}
fn silence_pointer(dogs: &[ProcessInfo]) -> Option<String> {
let silent: Vec<&str> = dogs
.iter()
.filter(|dog| rows::silence_note(dog).is_some())
.map(|dog| dog.name.as_str())
.collect();
match silent.as_slice() {
[] => None,
[only] => Some(format!(
"`{only}` is silent -- its process is up and it has never answered this shepherd. \
Run `shep describe {only}` for what that means and what to do about it."
)),
many => Some(format!(
"these dogs are silent -- their processes are up and they have never answered this \
shepherd: {}. Run `shep describe <name>` for what that means and what to do about \
it.",
many.join(", ")
)),
}
}
#[cfg(test)]
mod tests {
use shep_core::protocol::DogSource;
use crate::output::rows::tests::dog_info;
use super::super::tests::{mixed_listing, sheep_info, silent_dog};
use super::*;
#[test]
fn a_flock_listing_prints_the_dogs_in_their_own_table() {
let mut out = Vec::new();
emit_flock(
&mut out,
Format::Table,
"flock",
mixed_listing(),
None,
Presentation::BARE,
)
.unwrap();
let text = String::from_utf8(out).unwrap();
let (sheep_table, dogs_table) = text.split_once("\nDogs\n").expect("a Dogs caption");
assert!(sheep_table.contains("web"));
assert!(!sheep_table.contains("bark"), "a dog is not a sheep");
assert!(dogs_table.contains("bark"));
assert!(!dogs_table.contains("web"));
assert!(
dogs_table.starts_with("ID"),
"the dogs table leads with ID, as the sheep table does: {dogs_table}"
);
let shared: Vec<&str> = dogs_table
.lines()
.next()
.unwrap()
.split_whitespace()
.take(9)
.collect();
assert_eq!(
shared,
[
"ID", "NAME", "STATUS", "PID", "RESTARTS", "EXIT", "CPU", "MEM", "UPTIME"
],
"the nine shared columns, in the sheep table's own order"
);
assert!(
dogs_table
.lines()
.next()
.unwrap()
.trim_end()
.ends_with("SOURCE"),
"and this table's own column last"
);
}
#[test]
fn a_silent_dog_is_pointed_at_the_view_that_explains_it() {
let mut out = Vec::new();
emit_flock(
&mut out,
Format::Table,
"flock",
vec![sheep_info("web"), silent_dog("log-rotate", Some(true))],
None,
Presentation::BARE,
)
.unwrap();
let text = String::from_utf8(out).unwrap();
assert!(text.contains("silent"), "the cell still says it: {text}");
assert!(
text.contains("shep describe log-rotate"),
"and the pointer names the dog, so it can be typed: {text}"
);
}
#[test]
fn the_silence_pointer_sits_below_the_table_and_changes_no_column() {
let silent = vec![sheep_info("web"), silent_dog("log-rotate", Some(true))];
let mut talking = silent.clone();
talking[1].handshook = Some(true);
talking[1].dog_stale = Some(false);
let render = |listing: Vec<ProcessInfo>| {
let mut out = Vec::new();
emit_flock(
&mut out,
Format::Table,
"flock",
listing,
None,
Presentation::BARE,
)
.unwrap();
String::from_utf8(out).unwrap()
};
let with_pointer = render(silent);
let header = with_pointer
.split_once("\nDogs\n")
.expect("a Dogs caption")
.1
.lines()
.next()
.unwrap()
.to_string();
assert_eq!(
header,
render(talking)
.split_once("\nDogs\n")
.expect("a Dogs caption")
.1
.lines()
.next()
.unwrap(),
"the pointer is prose under the table, never a column in it"
);
assert!(
with_pointer.trim_end().ends_with("what to do about it."),
"and it comes last, after the table it annotates: {with_pointer}"
);
}
#[test]
fn a_flock_with_no_silent_dog_says_nothing_about_silence() {
let mut out = Vec::new();
let mut talking = dog_info("bark", DogSource::BuiltIn);
talking.handshook = Some(true);
talking.dog_stale = Some(false);
emit_flock(
&mut out,
Format::Table,
"flock",
vec![sheep_info("web"), talking],
None,
Presentation::BARE,
)
.unwrap();
let text = String::from_utf8(out).unwrap();
assert!(!text.contains("silent"), "{text}");
assert!(!text.contains("shep describe"), "{text}");
}
#[test]
fn the_json_surface_stays_one_array_of_every_entry() {
let mut out = Vec::new();
emit_flock(
&mut out,
Format::Json,
"flock",
mixed_listing(),
None,
Presentation::BARE,
)
.unwrap();
let json: serde_json::Value = serde_json::from_slice(&out).unwrap();
assert_eq!(json["schema_version"], 1);
assert_eq!(json["data"].as_array().unwrap().len(), 2);
assert_eq!(json["data"][0]["dog"], serde_json::Value::Null);
assert_eq!(json["data"][1]["dog"]["kind"], "built_in");
}
#[test]
fn a_flock_with_no_dogs_prints_one_table_and_no_caption() {
let mut out = Vec::new();
emit_flock(
&mut out,
Format::Table,
"flock",
vec![sheep_info("web")],
None,
Presentation::BARE,
)
.unwrap();
let text = String::from_utf8(out).unwrap();
assert!(!text.contains("Dogs"));
}
}