use super::*;
#[tokio::test]
async fn a_lifecycle_verb_renders_the_whole_flock_as_a_table() {
use shep_client::testing::fake_client_on;
use shep_core::protocol::ProcessInfo;
use shep_core::status::ProcStatus;
let dir = tempfile::tempdir().unwrap();
let address = shep_client::testing::control_address(dir.path());
let (client, daemon) = fake_client_on(&address).await;
daemon.reply_to_list(a_flock_with_a_dog());
let mut out = Vec::new();
let mut err = Vec::new();
let touched = vec![ProcessInfo::builder(1, "koji", ProcStatus::Stopped).build()];
let code = {
let mut streams = Streams {
out: &mut out,
err: &mut err,
style: crate::style::Presentation::BARE,
fmt: Format::Table,
};
render_outcome(&client, &mut streams, "stop", FlockRows(touched)).await
};
assert_eq!(code, ExitCode::Success);
let printed = String::from_utf8(out).unwrap();
let (sheep, dogs) = printed
.split_once("\nDogs\n")
.unwrap_or_else(|| panic!("the dogs table needs its own caption: {printed}"));
let sheep_names: Vec<&str> = sheep
.lines()
.filter_map(|line| line.split_whitespace().nth(1))
.filter(|word| *word != "NAME")
.collect();
assert_eq!(
sheep_names,
vec!["golbat", "koji", "rotom"],
"every sheep, not only the one that was stopped, and no dog among \
them: {printed}"
);
let dog_names: Vec<&str> = dogs
.lines()
.filter_map(|line| line.split_whitespace().nth(1))
.filter(|word| *word != "NAME")
.collect();
assert_eq!(
dog_names,
vec!["log-rotate"],
"the dog renders through the dogs table: {printed}"
);
assert!(
dogs.contains("SOURCE") && dogs.contains("adopted"),
"with the SOURCE column the sheep table has not: {printed}"
);
}
#[tokio::test]
async fn the_json_surface_keeps_the_rows_the_verb_touched() {
use shep_client::testing::fake_client_on;
use shep_core::protocol::ProcessInfo;
use shep_core::status::ProcStatus;
let dir = tempfile::tempdir().unwrap();
let address = shep_client::testing::control_address(dir.path());
let (client, daemon) = fake_client_on(&address).await;
daemon.reply_to_list(a_flock_with_a_dog());
let mut out = Vec::new();
let mut err = Vec::new();
let touched = vec![ProcessInfo::builder(1, "koji", ProcStatus::Stopped).build()];
let code = {
let mut streams = Streams {
out: &mut out,
err: &mut err,
style: crate::style::Presentation::BARE,
fmt: Format::Json,
};
render_outcome(&client, &mut streams, "stop", FlockRows(touched)).await
};
assert_eq!(code, ExitCode::Success);
let envelope: serde_json::Value = serde_json::from_slice(&out).unwrap();
let names: Vec<&str> = envelope["data"]
.as_array()
.unwrap()
.iter()
.map(|row| row["name"].as_str().unwrap())
.collect();
assert_eq!(
names,
vec!["koji"],
"only what the verb touched: {envelope}"
);
assert_eq!(
daemon.list_flock_count(),
0,
"and no listing was fetched to build it"
);
}
#[test]
fn a_start_target_walks_the_precedence() {
assert_eq!(
matched_names("koji"),
vec!["koji"],
"tier 1: a sheep by name"
);
assert_eq!(matched_names("1"), vec!["koji"], "tier 1: a sheep by id");
assert_eq!(
matched_names("fold:backed"),
vec!["golbat", "koji"],
"tier 2: a fold, named as one"
);
assert_eq!(
matched_names("backed"),
vec!["golbat", "koji"],
"tier 2: the same fold, named bare"
);
assert!(
matched_names("nosuchthing").is_empty(),
"and a token that is none of those falls through to the file tiers"
);
}
#[test]
fn a_sheep_outranks_a_fold_of_the_same_name() {
use shep_core::protocol::ProcessInfo;
use shep_core::status::ProcStatus;
let flock = vec![
ProcessInfo::builder(0, "backed", ProcStatus::Stopped).build(),
ProcessInfo::builder(1, "koji", ProcStatus::Stopped)
.fold(Some("backed".to_string()))
.build(),
];
let selector = ProcessSelector::parse("backed").unwrap();
let names: Vec<String> = flock_matches(&selector, &flock)
.into_iter()
.map(|info| info.name)
.collect();
assert_eq!(names, vec!["backed"], "the sheep, not the fold it names");
}
#[test]
fn a_wildcard_passes_a_dog_by_and_an_exact_name_reaches_it() {
assert_eq!(
matched_names("all"),
vec!["golbat", "koji", "rotom"],
"no dog in the sweep"
);
assert_eq!(
matched_names("log-rotate"),
vec!["log-rotate"],
"but naming it outright reaches it"
);
}
#[test]
fn an_instance_selector_reaches_a_dog() {
use shep_core::protocol::{DogSource, ProcessInfo};
use shep_core::status::ProcStatus;
let flock = vec![
ProcessInfo::builder(0, "metrics", ProcStatus::Online)
.dog(Some(DogSource::BuiltIn))
.instance(Some(0))
.build(),
ProcessInfo::builder(1, "metrics", ProcStatus::Online)
.dog(Some(DogSource::BuiltIn))
.instance(Some(1))
.build(),
];
let selector = ProcessSelector::parse("metrics:0").unwrap();
let ids: Vec<u32> = flock_matches(&selector, &flock)
.into_iter()
.map(|info| info.id)
.collect();
assert_eq!(ids, vec![0], "the named slot, dog or not");
}
#[test]
fn a_token_with_a_path_separator_is_never_a_name() {
let path = ProcessSelector::parse("./backed").unwrap();
assert!(
!is_reachable_as_a_name(&path),
"./backed can only be a file"
);
let bare = ProcessSelector::parse("backed").unwrap();
assert!(is_reachable_as_a_name(&bare), "backed may be either");
let regex = ProcessSelector::parse("/web/").unwrap();
assert!(
is_reachable_as_a_name(®ex),
"a regex is not a name, so the separator rule does not apply to it"
);
}
#[test]
fn a_selector_that_matched_nothing_is_reported_as_a_selector() {
let miss = |target: &str, flock: &[shep_core::protocol::ProcessInfo]| {
selector_miss(target, &ProcessSelector::parse(target).unwrap(), flock)
};
let empty: [shep_core::protocol::ProcessInfo; 0] = [];
assert_eq!(
miss("fold:typo", &empty).as_deref(),
Some("no sheep is in a fold called typo")
);
assert_eq!(
miss("zz-*", &empty).as_deref(),
Some("no sheep matched zz-*")
);
assert_eq!(
miss("all", &empty).as_deref(),
Some("the flock is empty; there is nothing to start")
);
assert_eq!(
miss("koji", &empty),
None,
"a bare name may still be a file, so the unresolvable message stands"
);
assert_eq!(miss("11", &empty), None, "and so may a bare id");
}
#[test]
fn unique_names_drops_a_duplicate_that_is_not_adjacent() {
use shep_core::status::ProcStatus;
let rows = [
ProcessInfo::builder(0, "web", ProcStatus::Stopped).build(),
ProcessInfo::builder(1, "api", ProcStatus::Stopped).build(),
ProcessInfo::builder(2, "web", ProcStatus::Stopped).build(),
];
let borrowed: Vec<&ProcessInfo> = rows.iter().collect();
assert_eq!(
unique_names(&borrowed),
vec!["web", "api"],
"one entry per name, in the order each was first seen"
);
}
#[test]
fn an_all_that_matched_nothing_counts_sheep_and_not_dogs() {
use shep_core::protocol::{DogSource, ProcessInfo};
use shep_core::status::ProcStatus;
let all = ProcessSelector::parse("all").unwrap();
let dogs_only = [ProcessInfo::builder(0, "log-rotate", ProcStatus::Online)
.dog(Some(DogSource::BuiltIn))
.build()];
let said = selector_miss("all", &all, &dogs_only).expect("a miss is reported");
assert!(
said.starts_with("no sheep in the flock"),
"a flock holding only dogs is not empty: {said}"
);
assert!(
said.contains("`shep dogs`"),
"and it says where the rows an operator can see came from: {said}"
);
let empty: [ProcessInfo; 0] = [];
assert_eq!(
selector_miss("all", &all, &empty).as_deref(),
Some("the flock is empty; there is nothing to start"),
"with nothing registered at all, empty is the honest word"
);
}
#[tokio::test]
async fn a_start_on_an_empty_fold_exits_not_found_without_a_start_request() {
use shep_client::testing::fake_client_on;
let dir = tempfile::tempdir().unwrap();
let address = shep_client::testing::control_address(dir.path());
let (client, daemon) = fake_client_on(&address).await;
daemon.reply_to_list(a_foldable_flock());
let mut out = Vec::new();
let mut err = Vec::new();
let code = {
let mut streams = Streams {
out: &mut out,
err: &mut err,
style: crate::style::Presentation::BARE,
fmt: Format::Table,
};
start(
&client,
&mut streams,
&start_args("fold:typo"),
None,
&BTreeMap::new(),
)
.await
};
assert_eq!(code, ExitCode::NotFound);
let said = String::from_utf8(err).unwrap();
assert!(
said.contains("no sheep is in a fold called typo"),
"the refusal names the fold, not a file: {said}"
);
assert!(
!said.contains("existing path"),
"and never mentions a path nobody asked about: {said}"
);
assert!(out.is_empty(), "stdout stays empty on a failure");
}
#[tokio::test]
async fn a_target_naming_a_running_sheep_leaves_it_alone() {
use shep_client::testing::fake_client_on;
use shep_core::status::ProcStatus;
let dir = tempfile::tempdir().unwrap();
let path = shep_client::testing::control_address(dir.path());
let (client, daemon) = fake_client_on(&path).await;
daemon.reply_to_list(vec![
shep_core::protocol::ProcessInfo::builder(7, "api-auth", ProcStatus::Online).build(),
]);
let mut out = Vec::new();
let mut err = Vec::new();
let code = {
let mut streams = Streams {
out: &mut out,
err: &mut err,
style: crate::style::Presentation::BARE,
fmt: Format::Table,
};
start(
&client,
&mut streams,
&start_args("api-auth"),
None,
&BTreeMap::new(),
)
.await
};
assert_eq!(code, ExitCode::Success);
let said = String::from_utf8_lossy(&err);
assert!(said.contains("already"), "the operator is told: {said}");
assert!(
said.contains("shep restart api-auth"),
"and pointed at the verb that would replace it: {said}"
);
}