use std::collections::BTreeMap;
use std::sync::{Arc, mpsc};
use std::time::{Duration, Instant};
use super::*;
use crate::commands::dogs::declared_review;
use crate::dog_index::{AvailableDog, DogSourceKind};
use crate::lookout::app::modal::{CUT_MARK, OUTPUT_CAP};
use crate::lookout::view::fixtures;
fn app_with_row(dog: AvailableDog, state: DogState) -> App {
app_with_version(dog, "0.2.4", state)
}
fn app_with_version(dog: AvailableDog, version: &str, state: DogState) -> App {
let t0 = Instant::now();
let mut app = App::new(
crate::lookout::theme::Palette::detect(None, None, None),
Control::Allowed,
"/home/ada/.shep".to_string(),
t0,
);
app.update(Msg::Snapshot {
rows: fixtures::flock_of(2, 1),
at: t0,
});
app.open_catalogue();
app.update(Msg::Catalogue {
result: Ok(Box::new(CatalogueRead {
rows: vec![CatalogueRow {
dog,
version: version.to_owned(),
state,
}],
refreshing: false,
})),
});
app
}
fn cargo_dog(name: &str) -> AvailableDog {
AvailableDog {
name: name.to_owned(),
package: format!("shep-{name}"),
adopt_as: name.to_owned(),
description: format!("{name} watches the flock and says so."),
repo: format!("https://github.com/shep-pm/shep-{name}"),
license: "MIT".to_owned(),
category: "metrics".to_owned(),
source: DogSourceKind::Cargo {
version: Some("0.2.4".to_owned()),
sha256: BTreeMap::new(),
},
permissions: None,
needs: Vec::new(),
}
}
pub(super) fn installing() -> (App, Instant) {
let t0 = Instant::now();
let mut app = App::new(
crate::lookout::theme::Palette::detect(None, None, None),
Control::Allowed,
"/home/ada/.shep".to_string(),
t0,
);
assert!(matches!(
app.start_install("otel"),
Effect::StartDogJob { .. }
));
(app, t0)
}
fn prompt(name: &str) -> Prompt {
Prompt::for_review(
name,
"install",
&declared_review(&[("flock.read", true, "reads the flock to report on it")]),
None,
)
.expect("a declared permission is a question")
}
pub(super) fn grant_tag(app: &App) -> Option<u64> {
match app.modal() {
Some(Modal::Grant { tag, .. }) => Some(*tag),
_ => None,
}
}
#[track_caller]
pub(super) fn grant_question(app: &App) -> String {
match app.modal() {
Some(Modal::Grant { prompt, .. }) => prompt.question().to_owned(),
other => panic!("no grant modal is open: {other:?}"),
}
}
#[track_caller]
fn open_tag(app: &App) -> u64 {
app.modal().map(Modal::tag).expect("a modal is open")
}
#[test]
fn an_adopted_entry_offers_install_but_never_upgrade() {
let mut app = app_with_version(cargo_dog("otel"), "0.2.1 \u{2192} 0.2.4", DogState::Adopted);
assert!(app.can_install(&app.catalogue_rows()[0]));
assert!(!app.can_upgrade(&app.catalogue_rows()[0]));
app.update(Msg::Key(KeyPress::Confirm));
assert!(matches!(
app.update(Msg::Key(KeyPress::Confirm)),
Effect::StartDogJob {
job: Job::Install { .. },
..
}
));
app.set_control_for_tests(Control::ReadOnly);
assert!(!app.can_install(&app.catalogue_rows()[0]));
}
#[test]
fn enter_on_an_installable_entry_starts_the_job_and_a_second_waits_for_it() {
let mut app = app_with_row(cargo_dog("otel"), DogState::NotInstalled);
app.update(Msg::Key(KeyPress::Confirm));
assert!(matches!(app.body(), Body::Catalogue(pane) if pane.entry.as_deref() == Some("otel")));
let authority = WriteAuthority::granted(&app).expect("the gate is open");
assert_eq!(
app.update(Msg::Key(KeyPress::Confirm)),
Effect::StartDogJob {
job: Job::Install {
adopt_as: "otel".to_owned(),
},
authority,
}
);
assert_eq!(
app.dog_job_line().as_deref(),
Some("installing otel starting 0s")
);
assert_eq!(app.start_install("rotate"), Effect::None, "nothing started");
match app.modal() {
Some(Modal::Output { title, text, .. }) => {
assert_eq!(title.as_str(), "installing");
assert!(
text.as_str().contains("otel is still installing"),
"{text:?}"
);
}
other => panic!("expected the waiting modal, got {other:?}"),
}
}
#[test]
fn an_installed_or_uninstallable_entry_starts_nothing() {
let mut installed = app_with_row(cargo_dog("otel"), DogState::Installed);
installed.update(Msg::Key(KeyPress::Confirm));
assert_eq!(installed.update(Msg::Key(KeyPress::Confirm)), Effect::None);
assert_eq!(installed.dog_job_line(), None);
let mut manual = app_with_row(
AvailableDog {
source: DogSourceKind::Manual {
instructions: "Download a release from the repo.".to_owned(),
},
..cargo_dog("otel")
},
DogState::NotInstalled,
);
manual.update(Msg::Key(KeyPress::Confirm));
assert_eq!(manual.update(Msg::Key(KeyPress::Confirm)), Effect::None);
assert_eq!(manual.dog_job_line(), None);
}
#[test]
fn enter_on_a_row_behind_the_index_starts_an_upgrade_and_on_a_current_one_nothing() {
let mut behind = app_with_version(cargo_dog("otel"), "0.2.1 \u{2192} 0.2.4", DogState::Enabled);
behind.update(Msg::Key(KeyPress::Confirm));
let authority = WriteAuthority::granted(&behind).expect("the gate is open");
assert_eq!(
behind.update(Msg::Key(KeyPress::Confirm)),
Effect::StartDogJob {
job: Job::Upgrade {
name: "otel".to_owned(),
},
authority,
}
);
assert_eq!(
behind.dog_job_line().as_deref(),
Some("upgrading otel starting 0s")
);
let mut current = app_with_version(cargo_dog("otel"), "0.2.4", DogState::Installed);
current.update(Msg::Key(KeyPress::Confirm));
assert_eq!(current.update(Msg::Key(KeyPress::Confirm)), Effect::None);
assert_eq!(current.dog_job_line(), None);
}
fn installed_entry() -> App {
let mut app = app_with_version(cargo_dog("otel"), "0.2.4", DogState::Enabled);
app.update(Msg::Key(KeyPress::Confirm));
app
}
#[test]
fn d_on_an_installed_entry_asks_and_a_yes_starts_the_delete() {
let mut app = installed_entry();
assert_eq!(app.update(Msg::Key(KeyPress::ArmDelete)), Effect::None);
match app.modal() {
Some(Modal::Confirm { question, .. }) => assert_eq!(
question.as_str(),
"Delete otel? An adopted dog with no receipt keeps its file."
),
other => panic!("expected the delete question, got {other:?}"),
}
assert_eq!(app.dog_job_line(), None, "nothing runs before the answer");
app.note_modal_reach(Some(0));
let authority = WriteAuthority::granted(&app).expect("the gate is open");
assert_eq!(
app.update(Msg::Key(KeyPress::Confirm)),
Effect::StartDogJob {
job: Job::Delete {
name: "otel".to_owned(),
},
authority,
}
);
assert_eq!(
app.dog_job_line().as_deref(),
Some("deleting otel starting 0s")
);
}
#[test]
fn a_no_to_the_delete_question_starts_nothing() {
let mut app = installed_entry();
app.update(Msg::Key(KeyPress::ArmDelete));
app.note_modal_reach(Some(0));
assert_eq!(app.update(Msg::Key(KeyPress::Escape)), Effect::None);
assert!(app.modal().is_none(), "the question closed");
assert_eq!(app.dog_job_line(), None);
}
#[test]
fn d_on_an_uninstalled_entry_or_in_the_list_asks_nothing() {
let mut uninstalled = app_with_row(cargo_dog("otel"), DogState::NotInstalled);
uninstalled.update(Msg::Key(KeyPress::Confirm));
assert_eq!(
uninstalled.update(Msg::Key(KeyPress::ArmDelete)),
Effect::None
);
assert!(uninstalled.modal().is_none());
let mut list = app_with_row(cargo_dog("otel"), DogState::Installed);
assert_eq!(list.update(Msg::Key(KeyPress::ArmDelete)), Effect::None);
assert!(list.modal().is_none(), "the list has no delete");
}
#[test]
fn a_built_in_dogs_entry_has_no_delete_action() {
let mut app = app_with_row(cargo_dog("metrics"), DogState::Enabled);
app.update(Msg::Key(KeyPress::Confirm));
assert_eq!(app.update(Msg::Key(KeyPress::ArmDelete)), Effect::None);
assert!(app.modal().is_none(), "no question for a built-in dog");
}
#[test]
fn a_closed_gate_refuses_the_upgrade_and_the_delete() {
let mut app = app_with_version(
cargo_dog("otel"),
"0.2.1 \u{2192} 0.2.4",
DogState::Installed,
);
app.set_control_for_tests(Control::ReadOnly);
app.update(Msg::Key(KeyPress::Confirm));
assert_eq!(app.update(Msg::Key(KeyPress::Confirm)), Effect::None);
assert_eq!(app.update(Msg::Key(KeyPress::ArmDelete)), Effect::None);
assert!(app.modal().is_none());
assert_eq!(app.dog_job_line(), None);
}
#[test]
fn a_delete_asked_while_a_job_runs_waits_for_it() {
let mut app = installed_entry();
assert!(matches!(
app.start_install("rotate"),
Effect::StartDogJob { .. }
));
app.update(Msg::Key(KeyPress::ArmDelete));
app.note_modal_reach(Some(0));
assert_eq!(app.update(Msg::Key(KeyPress::Confirm)), Effect::None);
match app.modal() {
Some(Modal::Output { title, text, .. }) => {
assert_eq!(title.as_str(), "deleting");
assert!(
text.as_str().contains("rotate is still installing"),
"{text:?}"
);
}
other => panic!("expected the waiting modal, got {other:?}"),
}
}
#[test]
fn a_landed_upgrade_or_delete_names_what_it_did_and_rereads_the_rows() {
let behind = "0.2.1 \u{2192} 0.2.4";
for (job, started, said, version, state) in [
(
Job::Upgrade {
name: "otel".to_owned(),
},
"upgrading otel starting 0s",
"upgraded otel",
"0.2.4",
DogState::Enabled,
),
(
Job::Delete {
name: "otel".to_owned(),
},
"deleting otel starting 0s",
"deleted otel",
"0.2.4",
DogState::NotInstalled,
),
] {
let mut app = app_with_version(cargo_dog("otel"), behind, DogState::Enabled);
app.update(Msg::Key(KeyPress::Confirm));
let Effect::StartDogJob { .. } = app.start_job(job) else {
panic!("the job did not start");
};
assert_eq!(app.dog_job_line().as_deref(), Some(started));
let effect = app.update(Msg::DogJobDone {
name: "otel".to_owned(),
ok: true,
output: String::new(),
});
assert_eq!(effect, Effect::LoadCatalogue { fetch: false }, "{said}");
assert_eq!(app.dog_job_line().as_deref(), Some(said));
assert_eq!(
app.catalogue_rows().first().map(|row| row.version.as_str()),
Some(behind),
"{said}: the rows are the old ones until the re-read lands"
);
app.update(Msg::Catalogue {
result: Ok(Box::new(CatalogueRead {
rows: vec![CatalogueRow {
dog: cargo_dog("otel"),
version: version.to_owned(),
state,
}],
refreshing: false,
})),
});
let row = app
.catalogue_rows()
.first()
.expect("the row is still there");
assert_eq!(
(row.version.as_str(), row.state, row.is_behind()),
(version, state, false),
"{said}"
);
app.update(Msg::Tick {
now: Instant::now() + DONE_SHOWS,
});
let effect = app.update(Msg::Key(KeyPress::Confirm));
assert!(
!matches!(
effect,
Effect::StartDogJob {
job: Job::Upgrade { .. },
..
}
),
"{said}: nothing is left to upgrade: {effect:?}"
);
}
}
#[test]
fn a_failed_upgrade_opens_the_failure_modal_with_its_own_title() {
let mut app = installed_entry();
app.start_job(Job::Upgrade {
name: "otel".to_owned(),
});
app.update(Msg::DogJobDone {
name: "otel".to_owned(),
ok: false,
output: "the new version asks for a permission".to_owned(),
});
assert!(
matches!(app.modal(), Some(Modal::Output { title, .. }) if title.as_str() == "upgrading otel"),
"{:?}",
app.modal()
);
assert_eq!(app.dog_job_line(), None);
}
#[test]
fn a_closed_gate_refuses_the_install_and_names_the_gate() {
let mut app = app_with_row(cargo_dog("otel"), DogState::NotInstalled);
app.set_control_for_tests(Control::ReadOnly);
app.update(Msg::Key(KeyPress::Confirm));
assert_eq!(app.update(Msg::Key(KeyPress::Confirm)), Effect::None);
assert_eq!(
app.dog_job_line(),
None,
"no entry action behind a closed gate"
);
assert_eq!(app.start_install("otel"), Effect::None);
assert_eq!(
app.notice().map(ToString::to_string).as_deref(),
Some(READ_ONLY_REFUSAL)
);
assert_eq!(app.dog_job_line(), None);
}
#[test]
fn a_step_moves_the_bar_and_a_landed_job_clears_it_and_rereads_the_rows() {
let (mut app, t0) = installing();
assert_eq!(
app.update(Msg::DogStep {
name: "otel".to_owned(),
step: Step::Downloading,
}),
Effect::None
);
assert_eq!(
app.dog_job_line().as_deref(),
Some("installing otel downloading 0s")
);
app.update(Msg::Tick {
now: t0 + Duration::from_secs(7),
});
assert_eq!(
app.dog_job_line().as_deref(),
Some("installing otel downloading 7s")
);
app.update(Msg::DogStep {
name: "otel".to_owned(),
step: Step::Building,
});
assert_eq!(
app.dog_job_line().as_deref(),
Some("installing otel building with cargo 7s")
);
let effect = app.update(Msg::DogJobDone {
name: "otel".to_owned(),
ok: true,
output: "installed otel".to_owned(),
});
assert_eq!(effect, Effect::LoadCatalogue { fetch: false });
assert_eq!(app.dog_job_line().as_deref(), Some("installed otel"));
app.update(Msg::Tick {
now: t0 + Duration::from_secs(7) + DONE_SHOWS + Duration::from_secs(1),
});
assert_eq!(app.dog_job_line(), None);
assert!(matches!(
app.start_install("otel"),
Effect::StartDogJob { .. }
));
}
#[test]
fn a_step_for_another_dog_moves_nothing() {
let (mut app, _) = installing();
app.update(Msg::DogStep {
name: "rotate".to_owned(),
step: Step::Downloading,
});
assert_eq!(
app.dog_job_line().as_deref(),
Some("installing otel starting 0s")
);
}
#[test]
fn a_failed_job_opens_a_modal_with_the_output_capped() {
let (mut app, _) = installing();
let output = "x".repeat(OUTPUT_CAP + 500);
assert_eq!(
app.update(Msg::DogJobDone {
name: "otel".to_owned(),
ok: false,
output,
}),
Effect::None
);
match app.modal() {
Some(Modal::Output { title, text, .. }) => {
assert_eq!(title.as_str(), "installing otel");
assert_eq!(text.as_str().len(), OUTPUT_CAP + CUT_MARK.len());
assert!(text.as_str().ends_with(CUT_MARK), "not capped: {text:?}");
}
other => panic!("expected the failure modal, got {other:?}"),
}
assert_eq!(app.dog_job_line(), None, "the bar is clear");
}
#[test]
fn a_grant_modal_answers_the_callback_that_asked_for_it() {
let (mut app, _) = installing();
app.note_modal_reach(Some(0));
let (yes_tx, yes_rx) = mpsc::channel();
let (no_tx, no_rx) = mpsc::channel();
app.update(Msg::DogGrant {
name: "otel".to_owned(),
prompt: prompt("otel"),
reply: Arc::new(GrantReply(yes_tx)),
});
assert!(
matches!(app.modal(), Some(Modal::Grant { prompt, .. }) if prompt.question().contains("otel")),
"{:?}",
app.modal()
);
let tag = open_tag(&app);
assert_eq!(
app.update(Msg::Key(KeyPress::Confirm)),
Effect::ModalAnswered { tag, yes: true },
"the modal is on screen and answers"
);
app.answer_grant(tag, true);
assert_eq!(yes_rx.try_recv(), Ok(true), "Enter answers yes");
app.update(Msg::DogGrant {
name: "otel".to_owned(),
prompt: prompt("otel"),
reply: Arc::new(GrantReply(no_tx)),
});
app.note_modal_reach(Some(0));
let tag = open_tag(&app);
assert_eq!(
app.update(Msg::Key(KeyPress::Escape)),
Effect::ModalAnswered { tag, yes: false }
);
app.answer_grant(tag, false);
assert_eq!(no_rx.try_recv(), Ok(false), "any other key answers no");
}
#[test]
fn a_grant_for_a_job_that_has_ended_answers_no_on_its_own() {
let (mut app, _) = installing();
app.note_modal_reach(Some(0));
let (tx, rx) = mpsc::channel();
app.update(Msg::DogJobDone {
name: "otel".to_owned(),
ok: true,
output: String::new(),
});
app.update(Msg::DogGrant {
name: "otel".to_owned(),
prompt: prompt("otel"),
reply: Arc::new(GrantReply(tx)),
});
assert!(app.modal().is_none(), "no modal for a job that is gone");
assert_eq!(
rx.try_recv(),
Err(mpsc::TryRecvError::Disconnected),
"the dropped reply reads as a no"
);
}
#[test]
fn quitting_mid_job_asks_first_and_a_no_keeps_the_job() {
let (mut app, _) = installing();
app.note_modal_reach(Some(0));
assert_eq!(app.update(Msg::Key(KeyPress::Quit)), Effect::None);
match app.modal() {
Some(Modal::Confirm { question, .. }) => {
assert!(question.contains("installing otel"), "{question:?}");
}
other => panic!("expected the quit question, got {other:?}"),
}
open_tag(&app);
app.note_modal_reach(Some(0));
assert_eq!(app.update(Msg::Key(KeyPress::Escape)), Effect::None);
assert!(app.modal().is_none());
assert_eq!(
app.dog_job_line().as_deref(),
Some("installing otel starting 0s"),
"the job is still running"
);
assert_eq!(app.update(Msg::Key(KeyPress::Quit)), Effect::None);
assert!(matches!(app.modal(), Some(Modal::Confirm { .. })));
app.note_modal_reach(Some(0));
assert_eq!(app.update(Msg::Key(KeyPress::Confirm)), Effect::Quit);
}
#[test]
fn a_yes_to_quitting_after_the_job_lands_still_quits() {
let (mut app, _) = installing();
app.note_modal_reach(Some(0));
assert_eq!(app.update(Msg::Key(KeyPress::Quit)), Effect::None);
let tag = open_tag(&app);
app.update(Msg::DogJobDone {
name: "otel".to_owned(),
ok: true,
output: String::new(),
});
assert_eq!(app.dog_job_line().as_deref(), Some("installed otel"));
assert_eq!(open_tag(&app), tag, "the question is still open");
assert_eq!(app.update(Msg::Key(KeyPress::Confirm)), Effect::Quit);
}
#[test]
fn a_no_to_quitting_after_the_job_lands_leaves_the_dashboard_open() {
let (mut app, _) = installing();
app.note_modal_reach(Some(0));
assert_eq!(app.update(Msg::Key(KeyPress::Quit)), Effect::None);
app.update(Msg::DogJobDone {
name: "otel".to_owned(),
ok: true,
output: String::new(),
});
assert_eq!(app.update(Msg::Key(KeyPress::Escape)), Effect::None);
assert!(app.modal().is_none(), "the question closed");
assert_eq!(app.update(Msg::Key(KeyPress::Quit)), Effect::Quit);
}
#[test]
fn quitting_with_no_job_running_quits_straight_away() {
let mut app = app_with_row(cargo_dog("otel"), DogState::NotInstalled);
assert_eq!(app.update(Msg::Key(KeyPress::Quit)), Effect::Quit);
}