use super::*;
impl App {
#[must_use]
pub(in crate::lookout) fn over_a_tree(&self) -> bool {
self.config_pane().is_some_and(ConfigPane::is_tree)
}
pub(super) fn on_dog_pane(
&mut self,
name: String,
adopted_path: Option<PathBuf>,
result: Result<serde_json::Value, String>,
) -> Effect {
match result {
Ok(schema) => {
self.dog_target = Some(DogProbe {
name: name.clone(),
adopted_path,
schema,
});
Effect::Send(Sent::DogSection { name })
}
Err(message) => {
self.notice = Some(Notice {
text: message,
grave: true,
});
Effect::None
}
}
}
pub(super) fn on_dog_section(
&mut self,
name: &str,
result: Result<Response, RequestError>,
) -> Effect {
let Some(probe) = self.dog_target.clone().filter(|probe| probe.name == name) else {
return Effect::None;
};
self.config_read_in_flight = false;
if self.closing {
return self.on_close_reread(name, result);
}
match result {
Ok(Response::DogSection { toml }) => {
let dropped = self.rebuild_dog_pane(probe, toml.as_str());
if dropped > 0 {
self.notice = Some(Notice {
text: format!("{name}: {}", moved_entries(dropped)),
grave: true,
});
}
}
Ok(_unrecognised) => {
self.notice = Some(Notice {
text: format!(
"{name}: the shepherd answered something this lookout does not understand"
),
grave: true,
});
}
Err(RequestError::Rpc(err)) => {
self.notice = Some(Notice {
text: format!("{name}: {}", err.message),
grave: true,
});
}
Err(other) => {
self.notice = Some(Notice {
text: format!("{name}: {other}"),
grave: true,
});
}
}
Effect::None
}
pub(super) fn rebuild_dog_pane(&mut self, probe: DogProbe, toml: &str) -> usize {
let carried = self.config_pane().map(|pane| pane.view().clone());
let carried_list = self
.config_pane()
.and_then(ConfigPane::list)
.map(|list| (list.key().to_owned(), list.view().clone()));
let carried_open = self.config_pane().map(|pane| pane.open_tables().clone());
let mut pane = ConfigPane::dog(
probe.name,
probe.adopted_path,
probe.schema,
toml.to_owned(),
);
let dropped = self
.config_pane()
.map_or(0, |previous| pane.adopt_edits_from(previous));
if let Some(open) = carried_open {
pane.adopt_open_tables(&open);
}
if let Some(carried) = carried {
pane.adopt_view(carried);
}
if let Some((key, carried)) = carried_list {
pane.adopt_list_view(&key, carried);
}
self.body = Body::ConfigPane(pane);
self.release_text_mode_if_unowned();
self.keymap_open = false;
dropped
}
pub(super) fn on_dog_section_set(
&mut self,
name: &str,
result: Result<Response, RequestError>,
) -> Effect {
match result {
Ok(Response::DogConfigSet { .. }) => {
self.notice = Some(Notice {
text: format!("{name}: its config is written, and {name} is told"),
grave: false,
});
Effect::Send(Sent::DogSection {
name: name.to_owned(),
})
}
Ok(_unrecognised) => {
self.notice = Some(Notice {
text: format!(
"{name}: the shepherd answered something this lookout does not understand"
),
grave: true,
});
Effect::None
}
Err(RequestError::Rpc(err)) => {
self.notice = Some(Notice {
text: format!("{name}: {}", err.message),
grave: true,
});
Effect::None
}
Err(other) => {
self.notice = Some(Notice {
text: format!("{name}: {other}"),
grave: true,
});
Effect::None
}
}
}
}
pub(super) fn moved_entries(dropped: usize) -> String {
if dropped == 1 {
"1 edit under an entry that moved in the file was dropped".to_owned()
} else {
format!("{dropped} edits under entries that moved in the file were dropped")
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::lookout::app::testing::*;
use shep_core::protocol::RpcError;
use shep_core::protocol::RpcErrorCode;
use std::path::Path;
#[test]
fn e_on_a_dog_row_opens_its_pane_instead_of_refusing() {
let mut app = fixtures::app_with_a_dog_selected_and_control();
let effect = app.update(Msg::Key(KeyPress::Edit));
let Effect::LoadDogPane { name, adopted_path } = effect else {
panic!("expected a dog pane, got {effect:?}");
};
assert_eq!(name, "otel");
assert_eq!(adopted_path.as_deref(), Some(Path::new("/opt/otel")));
assert!(app.notice().is_none(), "{:?}", app.notice());
}
#[test]
fn e_on_a_built_in_dog_opens_a_pane_with_no_path() {
let mut app = fixtures::app_with_a_built_in_dog_selected_and_control();
let effect = app.update(Msg::Key(KeyPress::Edit));
let Effect::LoadDogPane { adopted_path, .. } = effect else {
panic!("expected a dog pane, got {effect:?}");
};
assert_eq!(adopted_path, None);
}
#[test]
fn a_refusal_that_names_nothing_still_reaches_the_operator_named() {
let mut app = fixtures::app_in_sheep_pane();
app.update(Msg::Replied {
sent: Sent::SheepConfig {
name: "web".to_string(),
},
result: Err(RequestError::Rpc(RpcError {
code: RpcErrorCode::Internal,
message: "the supervisor engine has stopped".to_string(),
daemon_version: None,
})),
});
let said = app.notice().map(ToString::to_string).unwrap_or_default();
assert_eq!(said, "web: the supervisor engine has stopped");
assert!(app.notice().unwrap().is_grave());
}
#[test]
fn a_config_reply_for_a_pane_nobody_wants_is_dropped() {
let mut app =
fixtures::with_selection(ProcessInfo::builder(9, "web", ProcStatus::Online).build());
let reply = || Msg::Replied {
sent: Sent::SheepConfig {
name: "web".to_string(),
},
result: Ok(Response::SheepConfig(Box::new(
fixtures::sheep_config_view(),
))),
};
app.update(Msg::Key(KeyPress::Edit));
app.update(Msg::Key(KeyPress::Edit));
app.update(reply());
assert!(app.config_pane().is_some(), "the first answer opens it");
app.update(Msg::Key(KeyPress::Escape));
assert!(app.config_pane().is_none());
app.update(reply());
assert!(
app.config_pane().is_none(),
"the second answer lands on a dashboard and is dropped"
);
assert!(app.notice().is_none(), "silently: nothing went wrong");
}
#[test]
fn e_still_opens_a_pane_after_one_was_closed() {
let mut app = fixtures::app_in_sheep_pane();
app.update(Msg::Key(KeyPress::Escape));
app.update(Msg::Key(KeyPress::Edit));
app.update(Msg::Replied {
sent: Sent::SheepConfig {
name: "web".to_string(),
},
result: Ok(Response::SheepConfig(Box::new(
fixtures::sheep_config_view(),
))),
});
assert!(app.config_pane().is_some());
}
#[test]
fn e_on_a_settings_dog_row_probes_that_dogs_own_binary() {
let mut app = fixtures::app_in_settings_on_dog("otel");
assert_eq!(
app.update(Msg::Key(KeyPress::Edit)),
Effect::LoadDogPane {
name: "otel".to_string(),
adopted_path: Some(std::path::PathBuf::from("/usr/local/bin/shep-otel")),
}
);
assert!(
app.config_pane().is_none(),
"the pane opens on the answer, never on the keypress"
);
assert!(
app.settings().is_some(),
"and the screen stays up until it does"
);
}
#[test]
fn e_on_a_settings_scalar_row_does_nothing_at_all() {
let mut app = fixtures::app_in_settings();
assert_eq!(app.update(Msg::Key(KeyPress::Edit)), Effect::None);
assert!(app.config_pane().is_none());
assert!(app.notice().is_none(), "and says nothing about it");
}
#[test]
fn a_dog_with_no_schema_gets_no_pane_and_is_told_where_to_edit() {
let mut app = fixtures::app_in_settings_on_dog("otel");
app.update(Msg::Key(KeyPress::Edit));
assert_eq!(
app.update(Msg::DogPane {
name: "otel".to_string(),
adopted_path: None,
result: Err("otel publishes no schema; edit dogs.toml with $EDITOR".to_string()),
}),
Effect::None
);
assert!(app.config_pane().is_none());
assert!(
app.settings().is_some(),
"the screen the operator pressed `e` on is still there"
);
let notice = app.notice().expect("a refusal is reported").to_string();
assert!(notice.contains("dogs.toml"), "{notice}");
assert!(notice.contains("$EDITOR"), "{notice}");
}
#[test]
fn a_schema_asks_for_the_section_and_the_section_opens_the_pane() {
let mut app = fixtures::app_in_settings_on_dog("metrics");
app.update(Msg::Key(KeyPress::Edit));
assert_eq!(
app.update(Msg::DogPane {
name: "metrics".to_string(),
adopted_path: None,
result: Ok(crate::dog::builtin_schema("metrics").expect("a built-in")),
}),
Effect::Send(Sent::DogSection {
name: "metrics".to_string()
})
);
assert!(app.config_pane().is_none(), "one half is not a pane");
app.update(Msg::Replied {
sent: Sent::DogSection {
name: "metrics".to_string(),
},
result: Ok(Response::DogSection {
toml: "bind = \"0.0.0.0:9615\"\n".to_string().into(),
}),
});
let pane = app.config_pane().expect("both halves are a pane");
assert_eq!(pane.target().name(), "metrics");
assert_eq!(pane.value("bind"), "0.0.0.0:9615");
assert!(
app.settings().is_none(),
"the settings screen closes once there is something to look at"
);
}
#[test]
fn a_dog_sections_reply_that_lands_with_the_overlay_up_closes_it() {
let mut app = fixtures::app_in_settings_on_dog("metrics");
app.update(Msg::Key(KeyPress::Edit));
app.update(Msg::DogPane {
name: "metrics".to_string(),
adopted_path: None,
result: Ok(crate::dog::builtin_schema("metrics").expect("a built-in")),
});
assert!(app.config_pane().is_none(), "one half is not a pane yet");
let _ = app.update(Msg::Key(KeyPress::Help));
assert!(app.keymap_open(), "the overlay did not open");
app.update(Msg::Replied {
sent: Sent::DogSection {
name: "metrics".to_string(),
},
result: Ok(Response::DogSection {
toml: "bind = \"0.0.0.0:9615\"\n".to_string().into(),
}),
});
assert!(
app.config_pane().is_some(),
"the reply must still open the pane"
);
assert!(
!app.keymap_open(),
"the overlay survived a body change under it"
);
}
#[test]
fn a_section_for_a_dog_nobody_is_waiting_for_is_dropped() {
let mut app = fixtures::app_in_dog_pane();
app.update(Msg::Key(KeyPress::Escape));
assert!(app.config_pane().is_none());
app.update(Msg::Replied {
sent: Sent::DogSection {
name: "bark".to_string(),
},
result: Ok(Response::DogSection {
toml: fixtures::dog_section().into(),
}),
});
assert!(app.config_pane().is_none(), "a late reply re-opens nothing");
}
#[test]
fn a_section_for_a_different_dog_does_not_replace_the_pane() {
let mut app = fixtures::app_in_dog_pane();
app.update(Msg::Replied {
sent: Sent::DogSection {
name: "metrics".to_string(),
},
result: Ok(Response::DogSection {
toml: "bind = \"0.0.0.0:9615\"\n".to_string().into(),
}),
});
let pane = app.config_pane().expect("the bark pane is still open");
assert_eq!(pane.target().name(), "bark");
assert_eq!(
pane.value("poll"),
"60s",
"and still holds bark's own section"
);
}
#[test]
fn a_dog_pane_write_carries_the_whole_edited_section() {
let mut app = fixtures::app_in_dog_pane();
let index = app
.config_pane()
.expect("the pane is open")
.fields()
.fields()
.iter()
.position(|field| field.key == "poll")
.expect("poll is a bark field");
app.update(Msg::Key(KeyPress::SelectFirst));
for _ in 0..index {
app.update(Msg::Key(KeyPress::SelectDown));
}
app.update(Msg::Key(KeyPress::Confirm));
for _ in 0..3 {
app.update(Msg::Key(KeyPress::TextBackspace));
}
for typed in "30s".chars() {
app.update(Msg::Key(KeyPress::TextChar(typed)));
}
app.update(Msg::Key(KeyPress::TextApply));
let batch = wire_batch(close_bark(&mut app));
let [Sent::SetDogSection { name, toml, .. }] = batch.as_slice() else {
panic!("closing the pane sends the section: {batch:?}");
};
assert_eq!(name, "bark");
assert!(
toml.as_str().contains("poll = \"30s\""),
"{}",
toml.as_str()
);
assert!(
toml.as_str().contains("# how often"),
"a comment shep did not write survives: {}",
toml.as_str()
);
}
#[test]
fn closing_a_dog_pane_sends_one_write_for_two_edits() {
let mut app = fixtures::app_in_dog_pane_with_two_edits();
let Effect::SendAll(sent) = close_bark(&mut app) else {
panic!("wanted a batch");
};
assert_eq!(sent.len(), 1, "a dog takes one section write, not two");
let [Sent::SetDogSection { name, toml, .. }] = sent.as_slice() else {
panic!("closing the pane sends the section: {sent:?}");
};
assert_eq!(name, "bark");
assert!(
toml.as_str().contains("poll = \"45s\""),
"{}",
toml.as_str()
);
assert!(
toml.as_str().contains("history_bytes = 8192"),
"{}",
toml.as_str()
);
}
#[test]
fn a_landed_dog_write_re_reads_the_section_and_promises_nothing_more() {
let mut app = fixtures::app_in_dog_pane();
assert_eq!(
app.update(Msg::Replied {
sent: Sent::DogSection {
name: "bark".to_string(),
},
result: Ok(Response::DogConfigSet {
name: "bark".to_string()
}),
}),
Effect::None,
"a reply routed by its own request, and this one is not the write"
);
let Effect::Send(Sent::DogSection { name }) = app.update(Msg::Replied {
sent: Sent::SetDogSection {
name: "bark".to_string(),
ticket: 0,
toml: fixtures::dog_section().into(),
authority: WriteAuthority::granted(&app).expect("the fixture opens the gate"),
},
result: Ok(Response::DogConfigSet {
name: "bark".to_string(),
}),
}) else {
panic!("a landed write re-reads");
};
assert_eq!(name, "bark");
let notice = app
.notice()
.expect("a landed write is reported")
.to_string();
assert!(notice.contains("bark is told"), "{notice}");
}
#[test]
fn r_in_a_dog_pane_re_reads_the_section_and_never_re_probes() {
let mut app = fixtures::app_in_dog_pane();
assert_eq!(
app.update(Msg::Key(KeyPress::Refresh)),
Effect::Send(Sent::DogSection {
name: "bark".to_string()
})
);
}
fn nested_cursor_to(app: &mut App, row: usize) {
app.update(Msg::Key(KeyPress::SelectFirst));
for _ in 0..row {
app.update(Msg::Key(KeyPress::SelectDown));
}
}
fn nested_rows(app: &App) -> usize {
app.config_pane().expect("the pane is open").rows().len()
}
#[test]
fn right_and_left_open_and_close_the_table_under_the_cursor() {
let mut app = fixtures::app_in_dog_pane_with_a_nested_table();
assert_eq!(nested_rows(&app), 3);
nested_cursor_to(&mut app, 1);
app.update(Msg::Key(KeyPress::TabNext));
assert_eq!(nested_rows(&app), 5, "worker opened onto model and token");
app.update(Msg::Key(KeyPress::TabPrev));
assert_eq!(nested_rows(&app), 3);
app.update(Msg::Key(KeyPress::Confirm));
assert_eq!(nested_rows(&app), 5, "enter toggles it open");
app.update(Msg::Key(KeyPress::Confirm));
assert_eq!(nested_rows(&app), 3, "and closed");
}
#[test]
fn e_space_and_d_on_a_table_row_do_nothing() {
for key in [KeyPress::Edit, KeyPress::Cycle, KeyPress::Remove] {
let mut app = fixtures::app_in_dog_pane_with_a_nested_table();
nested_cursor_to(&mut app, 1);
let before = app.config_pane().expect("the pane is open").cursor();
assert!(matches!(before, Some(PaneRow::Table(_))), "{before:?}");
let effect = app.update(Msg::Key(key));
assert_eq!(effect, Effect::None, "{key:?}");
let pane = app.config_pane().expect("the pane is open");
assert!(pane.edits().is_empty(), "{key:?} filed something");
assert!(pane.typing().is_none(), "{key:?} opened an editor");
assert_eq!(pane.cursor(), before, "{key:?}");
assert_eq!(pane.rows().len(), 3, "{key:?} toggled the table");
}
}
#[test]
fn u_on_a_table_row_undoes_the_newest_edit() {
let mut app = fixtures::app_in_dog_pane_with_a_nested_table();
nested_cursor_to(&mut app, 2);
app.update(Msg::Key(KeyPress::Edit));
app.update(Msg::Key(KeyPress::TextChar('x')));
app.update(Msg::Key(KeyPress::TextApply));
assert!(!app.config_pane().expect("open").edits().is_empty());
app.update(Msg::Key(KeyPress::SelectUp));
let on = app.config_pane().expect("open").cursor();
assert!(matches!(on, Some(PaneRow::Table(_))), "{on:?}");
app.update(Msg::Key(KeyPress::Undo));
assert!(app.config_pane().expect("open").edits().is_empty());
}
#[test]
fn a_re_read_keeps_the_open_tables_and_the_cursor() {
let mut app = fixtures::app_in_dog_pane_with_a_nested_table();
nested_cursor_to(&mut app, 1);
app.update(Msg::Key(KeyPress::TabNext));
app.update(Msg::Key(KeyPress::SelectDown));
let cursor = app.config_pane().expect("the pane is open").cursor();
let Effect::Send(sent) = app.update(Msg::Key(KeyPress::Refresh)) else {
panic!("r re-reads the section");
};
app.update(Msg::Replied {
sent,
result: Ok(Response::DogSection {
toml: fixtures::nested_dog_section().into(),
}),
});
let pane = app.config_pane().expect("still open");
assert_eq!(pane.rows().len(), 5);
assert!(pane.open_tables().contains("models.worker"));
assert_eq!(pane.cursor(), cursor);
}
#[test]
fn left_and_right_do_nothing_on_a_sheep_pane() {
let mut app = fixtures::app_in_sheep_pane();
let before = app.config_pane().expect("the pane is open").cursor();
let effect = app.update(Msg::Key(KeyPress::TabNext));
assert_eq!(effect, Effect::None);
let effect = app.update(Msg::Key(KeyPress::TabPrev));
assert_eq!(effect, Effect::None);
assert_eq!(app.config_pane().expect("open").cursor(), before);
}
#[test]
fn a_re_read_keeps_an_open_list_at_its_cursor_with_the_filed_elements() {
let mut app = fixtures::app_in_dog_pane_with_lists();
pane_to(&mut app, "hosts");
app.update(Msg::Key(KeyPress::Confirm));
app.update(Msg::Key(KeyPress::SelectLast));
app.update(Msg::Key(KeyPress::Edit));
for typed in "c.example".chars() {
app.update(Msg::Key(KeyPress::TextChar(typed)));
}
app.update(Msg::Key(KeyPress::TextApply));
app.update(Msg::Key(KeyPress::SelectUp));
let Effect::Send(sent) = app.update(Msg::Key(KeyPress::Refresh)) else {
panic!("r re-reads the section");
};
app.update(Msg::Replied {
sent,
result: Ok(Response::DogSection {
toml: fixtures::list_dog_section().into(),
}),
});
let list = app
.config_pane()
.expect("still open")
.list()
.expect("the sub-screen rides across the refresh");
assert_eq!(list.key(), "hosts");
assert_eq!(
list.elements(),
["a.example", "b.example", "c.example"],
"the filed array, not the section's"
);
assert_eq!(list.cursor(), Some(crate::lookout::pane::ListRow::Item(1)));
}
}