use std::collections::BTreeMap;
use std::sync::mpsc::{Receiver, RecvTimeoutError, Sender};
use std::time::Duration;
use chrono::{DateTime, TimeDelta, Utc};
use ratatui::crossterm::event::KeyEvent;
use crate::app::{Asked, Awaited, Wanted};
use crate::collect::changes::Reported;
use crate::collect::panes::Answer;
use crate::model::snapshot::Snapshot;
use crate::view::{Action, Motion, Notch, Typing};
use super::armed::{Armed, Arming};
use super::keys::{action, typing};
use super::reload::{Reload, Reloaded};
#[cfg_attr(test, derive(Debug, PartialEq))]
pub(super) enum Event {
Key(KeyEvent),
Clicked(u16),
Scrolled(Notch),
Resize,
Changed(Wanted),
Collected(Box<Snapshot>),
Tailed(Answer),
Signalled,
}
impl From<Answer> for Event {
fn from(answer: Answer) -> Self {
Event::Tailed(answer)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(super) enum Showing {
Forest,
Bindings,
Bead,
Searching,
}
#[derive(Clone, Copy)]
#[cfg_attr(test, derive(Debug, PartialEq))]
pub(super) enum Landed {
Followed,
Nothing,
Away,
}
pub(super) trait View {
fn collected(&mut self, snapshot: Snapshot);
fn collecting(&mut self, awaited: &[Awaited]) -> bool;
fn holds_for(&self, drawn_at: DateTime<Utc>) -> Option<Duration>;
fn tailed(&mut self, answer: Answer, now: DateTime<Utc>) -> bool;
fn reread(&mut self, now: DateTime<Utc>);
fn rereads_in(&self, now: DateTime<Utc>) -> Option<Duration>;
fn reloaded(&mut self, reloaded: Reloaded<'_>, now: DateTime<Utc>) -> bool;
fn pressed(&mut self) -> bool;
fn apply(&mut self, action: Action) -> bool;
fn typing(&mut self, typing: Typing) -> bool;
fn scroll(&mut self, motion: Motion) -> bool;
fn scrolled(&mut self, notch: Notch) -> bool;
fn scrolled_bead(&mut self, notch: Notch) -> bool;
fn bead_still_shown(&self) -> bool;
fn follow(&mut self) -> bool;
fn retrace(&mut self) -> bool;
fn clicked(&mut self, row: u16) -> bool;
fn clicked_bead(&mut self, row: u16) -> Landed;
fn draw(&mut self, showing: Showing, now: DateTime<Utc>) -> anyhow::Result<()>;
}
enum Waited {
Event(Event),
Aged,
}
fn wait(events: &Receiver<Event>, holds_for: Option<Duration>) -> Option<Waited> {
let Some(holds_for) = holds_for else {
return events.recv().ok().map(Waited::Event);
};
match events.recv_timeout(holds_for) {
Ok(event) => Some(Waited::Event(event)),
Err(RecvTimeoutError::Timeout) => Some(Waited::Aged),
Err(RecvTimeoutError::Disconnected) => None,
}
}
pub(super) fn drive(
view: &mut dyn View,
events: &Receiver<Event>,
ask: &Sender<Asked>,
mut outstanding: Outstanding,
mut reading: Reading,
arms: &Arming,
mut reload: Option<Reload>,
) -> anyhow::Result<()> {
let mut showing = Showing::Forest;
let mut drawn_at = Utc::now();
view.draw(showing, drawn_at)?;
while let Some(waited) = wait(
events,
sleeps_for(
view,
&outstanding,
&reading.polling,
reload.as_ref(),
drawn_at,
Utc::now(),
),
) {
let woken = match waited {
Waited::Aged => ran_out(view, drawn_at, Utc::now()),
Waited::Event(event) => {
let Some(changed) = answered(
view,
&mut outstanding,
&mut reading.polling,
&mut showing,
event,
) else {
return Ok(());
};
changed
}
};
let now = Utc::now();
let told = asks_for_what_is_due(view, &mut outstanding, &mut reading.polling, now);
outstanding.sends(ask, now);
view.reread(now);
let noticed = looked_at(
view,
reload.as_mut(),
&mut reading,
arms,
ask,
&mut outstanding,
now,
);
if woken || told || noticed {
drawn_at = now;
view.draw(showing, drawn_at)?;
}
}
Ok(())
}
fn looked_at(
view: &mut dyn View,
reload: Option<&mut Reload>,
reading: &mut Reading,
arms: &Arming,
ask: &Sender<Asked>,
outstanding: &mut Outstanding,
now: DateTime<Utc>,
) -> bool {
let Some(reload) = reload else {
return view.reloaded(Reloaded::Untouched, now);
};
let reloaded = reload.checks(now);
let noticed = view.reloaded(reloaded, now);
let Reloaded::Fresh(written) = reloaded else {
return noticed;
};
reading.now_reading(arms(written));
outstanding.waits_out(written.tui.unanswered_after());
if ask
.send(Asked::Reloaded(Box::new(written.clone())))
.is_err()
{
return noticed;
}
let told = asked_for(view, outstanding, Wanted::Everything);
told || noticed
}
fn still_armed(standing: Vec<Armed>, named: Vec<Armed>) -> Vec<Armed> {
let mut standing: BTreeMap<String, Armed> = standing
.into_iter()
.map(|project| (project.project().to_string(), project))
.collect();
named
.into_iter()
.map(|named| match standing.remove(named.project()) {
Some(standing) => standing.still_due(named),
None => named,
})
.collect()
}
pub(super) struct Reading {
polling: Vec<Armed>,
accepted: Reported,
}
impl Reading {
pub(super) fn of(polling: Vec<Armed>, accepted: Reported) -> Self {
Self { polling, accepted }
}
fn now_reading(&mut self, named: Vec<Armed>) {
let polling = still_armed(std::mem::take(&mut self.polling), named);
self.accepted
.now_watching(polling.iter().map(|project| project.project().to_string()));
self.polling = polling;
}
}
fn asks_for_what_is_due(
view: &mut dyn View,
outstanding: &mut Outstanding,
armed: &mut [Armed],
now: DateTime<Utc>,
) -> bool {
let mut told = false;
for wanted in armed.iter_mut().filter_map(|project| project.asks(now)) {
told |= asked_for(view, outstanding, wanted);
}
told
}
fn ran_out(view: &dyn View, drawn_at: DateTime<Utc>, now: DateTime<Utc>) -> bool {
let since_drawn = (now - drawn_at).to_std().unwrap_or_default();
view.holds_for(drawn_at)
.is_some_and(|held| held <= since_drawn)
}
fn sleeps_for(
view: &dyn View,
outstanding: &Outstanding,
armed: &[Armed],
reload: Option<&Reload>,
drawn_at: DateTime<Utc>,
now: DateTime<Utc>,
) -> Option<Duration> {
let since_drawn = (now - drawn_at).to_std().unwrap_or_default();
let holds_for = view
.holds_for(drawn_at)
.map(|held| held.saturating_sub(since_drawn));
[
holds_for,
outstanding.sends_in(now),
view.rereads_in(now),
reload.and_then(|reload| reload.checks_in(now)),
]
.into_iter()
.chain(armed.iter().map(|project| project.asks_in(now)))
.flatten()
.min()
}
fn answered(
view: &mut dyn View,
outstanding: &mut Outstanding,
armed: &mut [Armed],
showing: &mut Showing,
event: Event,
) -> Option<bool> {
let pressed = matches!(
event,
Event::Key(_) | Event::Clicked(_) | Event::Scrolled(_)
) && view.pressed();
let changed = match event {
Event::Key(_) if *showing == Showing::Bindings => {
*showing = Showing::Forest;
true
}
Event::Key(key) if *showing == Showing::Bead => match action(key) {
Some(Action::Back) => {
if !view.retrace() {
*showing = Showing::Forest;
}
true
}
Some(Action::Quit) => {
*showing = Showing::Forest;
true
}
Some(Action::Move(motion)) => view.scroll(motion),
Some(Action::NextRelated) => view.apply(Action::NextRelated),
Some(Action::ShowBead) => view.follow() || view.apply(Action::Focus),
Some(Action::Focus) => view.apply(Action::Focus),
Some(Action::CopyId) => view.apply(Action::CopyId),
Some(Action::ShowBindings) => {
*showing = Showing::Bindings;
true
}
Some(Action::Refresh) => asked_for(view, outstanding, Wanted::Everything),
Some(
Action::CollapseOrParent
| Action::ExpandOrChild
| Action::ToggleFold
| Action::ExpandSubtree
| Action::CollapseSubtree
| Action::RestoreSubtree
| Action::ExpandForest
| Action::CollapseForest
| Action::RestoreDefault
| Action::CycleSpine
| Action::CycleSpineForest
| Action::ToggleFilter
| Action::FocusForest
| Action::Search
| Action::NextMatch
| Action::PreviousMatch,
)
| None => false,
},
Event::Key(key) if *showing == Showing::Searching => match typing(key) {
Some(step @ (Typing::Sought | Typing::Abandoned)) => {
*showing = Showing::Forest;
view.typing(step);
true
}
Some(step) => view.typing(step),
None => match action(key) {
Some(Action::Quit) => return None,
_ => false,
},
},
Event::Key(key) => match action(key) {
Some(Action::Quit) => return None,
Some(Action::ShowBindings) => {
*showing = Showing::Bindings;
true
}
Some(Action::ShowBead) => {
let opened = view.apply(Action::ShowBead);
if opened {
*showing = Showing::Bead;
}
opened
}
Some(Action::Refresh) => asked_for(view, outstanding, Wanted::Everything),
Some(Action::Search) => {
*showing = Showing::Searching;
view.apply(Action::Search)
}
Some(Action::NextRelated) => false,
Some(action) => view.apply(action),
None => false,
},
Event::Clicked(_) | Event::Scrolled(_) if *showing == Showing::Bindings => {
*showing = Showing::Forest;
true
}
Event::Clicked(_) | Event::Scrolled(_) if *showing == Showing::Searching => {
*showing = Showing::Forest;
view.typing(Typing::Abandoned);
true
}
Event::Clicked(row) if *showing == Showing::Bead => match view.clicked_bead(row) {
Landed::Followed => true,
Landed::Nothing => false,
Landed::Away => {
*showing = Showing::Forest;
true
}
},
Event::Scrolled(notch) if *showing == Showing::Bead => view.scrolled_bead(notch),
Event::Clicked(row) => view.clicked(row),
Event::Scrolled(notch) => view.scrolled(notch),
Event::Resize => true,
Event::Changed(wanted) => asked_for(view, outstanding, wanted),
Event::Collected(snapshot) => {
let now = Utc::now();
if let Some(read) = outstanding.came_back() {
for project in armed.iter_mut() {
project.came_back(&read, now);
}
}
view.collected(*snapshot);
if *showing == Showing::Bead && !view.bead_still_shown() {
*showing = Showing::Forest;
}
view.collecting(outstanding.awaited());
true
}
Event::Tailed(answer) => view.tailed(answer, Utc::now()),
Event::Signalled => return None,
};
Some(pressed || changed)
}
fn asked_for(view: &mut dyn View, outstanding: &mut Outstanding, wanted: Wanted) -> bool {
outstanding.ask(wanted, Utc::now()) && view.collecting(outstanding.awaited())
}
const WINDOW: TimeDelta = TimeDelta::milliseconds(200);
pub(super) struct Outstanding {
awaited: Vec<Awaited>,
patience: TimeDelta,
window: TimeDelta,
sent: bool,
}
impl Outstanding {
pub(super) fn for_a_run(patience: TimeDelta) -> Self {
Self::waiting(patience, WINDOW)
}
pub(super) fn waits_out(&mut self, patience: TimeDelta) {
self.patience = patience;
}
pub(super) fn waiting(patience: TimeDelta, window: TimeDelta) -> Self {
Self {
awaited: Vec::new(),
patience,
window,
sent: false,
}
}
pub(super) fn ask(&mut self, wanted: Wanted, now: DateTime<Utc>) -> bool {
if self.awaited.is_empty() {
self.awaited.push(self.stamped(wanted, now));
return true;
}
self.queue(wanted, now)
}
pub(super) fn sends(&mut self, ask: &Sender<Asked>, now: DateTime<Utc>) {
if self.sent {
return;
}
let Some(next) = self.awaited.first() else {
return;
};
if now < next.asked_at + self.window {
return;
}
if ask.send(Asked::Read(next.wanted.clone())).is_err() {
self.awaited.clear();
return;
}
self.sent = true;
}
pub(super) fn sends_in(&self, now: DateTime<Utc>) -> Option<Duration> {
if self.sent {
return None;
}
self.awaited.first().map(|next| {
(next.asked_at + self.window - now)
.to_std()
.unwrap_or_default()
})
}
fn queue(&mut self, wanted: Wanted, now: DateTime<Utc>) -> bool {
match wanted {
Wanted::Everything => {
if self.queued().any(|it| it.wanted == Wanted::Everything) {
return false;
}
self.awaited.truncate(self.in_flight());
self.awaited.push(self.stamped(Wanted::Everything, now));
true
}
Wanted::Project(project) => {
if self.queued().any(|it| it.wanted.names(&project)) {
return false;
}
self.awaited
.push(self.stamped(Wanted::Project(project), now));
true
}
}
}
fn queued(&self) -> impl Iterator<Item = &Awaited> {
self.awaited.iter().skip(self.in_flight())
}
fn in_flight(&self) -> usize {
usize::from(self.sent)
}
fn stamped(&self, wanted: Wanted, asked_at: DateTime<Utc>) -> Awaited {
Awaited {
wanted,
asked_at,
patience: self.patience,
}
}
fn came_back(&mut self) -> Option<Wanted> {
if self.awaited.is_empty() {
return None;
}
self.sent = false;
Some(self.awaited.remove(0).wanted)
}
pub(super) fn awaited(&self) -> &[Awaited] {
&self.awaited
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::config::Config;
use crate::tui::fixtures::{a_snapshot, arkham, ferry, reading, A_MOMENT, PATIENCE};
use crate::tui::keys::tests::{control, key};
use crate::tui::wire::collector;
use crate::view::phrase;
use ratatui::crossterm::event::KeyCode;
use std::cell::RefCell;
use std::sync::mpsc;
use std::thread;
use std::time::Instant;
#[derive(Default)]
struct Recorder {
applied: Vec<Action>,
scrolled: Vec<Motion>,
notched: Vec<Notch>,
notched_bead: Vec<Notch>,
clicked: Vec<u16>,
typed: Vec<Typing>,
clicked_bead: Vec<u16>,
lands_on: Option<Landed>,
not_a_bead: bool,
on_a_reference: bool,
somewhere_to_go_back_to: bool,
followed: usize,
retraced: usize,
collection_moves_the_selection: bool,
collected: usize,
awaited: Vec<Vec<Awaited>>,
drawn_at: Vec<DateTime<Utc>>,
measured_at: RefCell<Vec<DateTime<Utc>>>,
showing: Vec<Showing>,
nothing_under_the_pointer: bool,
pressing_changes: bool,
pressed_after: Vec<usize>,
rereads_in: Option<Duration>,
reread_at: Vec<DateTime<Utc>>,
went_round: Option<Sender<DateTime<Utc>>>,
}
impl Recorder {
fn drawn(&self) -> usize {
self.drawn_at.len()
}
fn collecting(&self) -> Vec<Vec<Wanted>> {
self.awaited
.iter()
.map(|told| told.iter().map(|it| it.wanted.clone()).collect())
.collect()
}
fn asked_at(&self) -> Vec<chrono::DateTime<Utc>> {
self.awaited
.iter()
.flat_map(|told| told.iter().map(|it| it.asked_at))
.collect()
}
}
impl View for Recorder {
fn collected(&mut self, _snapshot: Snapshot) {
self.collected += 1;
}
fn collecting(&mut self, awaited: &[Awaited]) -> bool {
self.awaited.push(awaited.to_vec());
true
}
fn reloaded(&mut self, _reloaded: Reloaded<'_>, _now: DateTime<Utc>) -> bool {
false
}
fn holds_for(&self, drawn_at: DateTime<Utc>) -> Option<Duration> {
self.measured_at.borrow_mut().push(drawn_at);
let told = self.awaited.last()?;
(!told.is_empty()).then_some(phrase::FRAME)
}
fn tailed(&mut self, _answer: Answer, _now: DateTime<Utc>) -> bool {
true
}
fn reread(&mut self, now: DateTime<Utc>) {
self.reread_at.push(now);
if let Some(went_round) = &self.went_round {
let _ = went_round.send(now);
}
}
fn rereads_in(&self, _now: DateTime<Utc>) -> Option<Duration> {
self.rereads_in
}
fn typing(&mut self, typing: Typing) -> bool {
self.typed.push(typing);
true
}
fn pressed(&mut self) -> bool {
self.pressed_after
.push(self.applied.len() + self.clicked.len() + self.notched.len());
self.pressing_changes
}
fn apply(&mut self, action: Action) -> bool {
self.applied.push(action);
!(action == Action::ShowBead && self.not_a_bead)
}
fn scroll(&mut self, motion: Motion) -> bool {
self.scrolled.push(motion);
true
}
fn scrolled(&mut self, notch: Notch) -> bool {
self.notched.push(notch);
true
}
fn scrolled_bead(&mut self, notch: Notch) -> bool {
self.notched_bead.push(notch);
true
}
fn bead_still_shown(&self) -> bool {
!(self.collection_moves_the_selection && self.collected > 0)
}
fn follow(&mut self) -> bool {
self.followed += 1;
self.on_a_reference
}
fn retrace(&mut self) -> bool {
self.retraced += 1;
self.somewhere_to_go_back_to
}
fn clicked(&mut self, row: u16) -> bool {
self.clicked.push(row);
!self.nothing_under_the_pointer
}
fn clicked_bead(&mut self, row: u16) -> Landed {
self.clicked_bead.push(row);
self.lands_on.unwrap_or(Landed::Nothing)
}
fn draw(&mut self, showing: Showing, now: DateTime<Utc>) -> anyhow::Result<()> {
self.drawn_at.push(now);
self.showing.push(showing);
Ok(())
}
}
fn at_once() -> Outstanding {
Outstanding::waiting(PATIENCE, TimeDelta::zero())
}
fn nothing_armed() -> Vec<Armed> {
Vec::new()
}
fn nothing_watched() -> Option<Reload> {
None
}
fn nothing_reported() -> Reported {
Reported::default()
}
fn a_run_reading(polling: Vec<Armed>) -> Reading {
Reading::of(polling, nothing_reported())
}
fn reads(asked: impl IntoIterator<Item = Asked>) -> Vec<Wanted> {
asked.into_iter().map(read).collect()
}
fn read(asked: Asked) -> Wanted {
match asked {
Asked::Read(wanted) => wanted,
Asked::Reloaded(cfg) => {
panic!("nobody wrote a config, and the collector was sent {cfg:?}")
}
}
}
fn polling_every_interval() -> Arming {
Box::new(|cfg: &Config| {
cfg.read()
.map(|project| {
Armed::polling(project.name.clone(), project.poll.then_some(AN_INTERVAL))
})
.collect()
})
}
fn waiting(events: Vec<Event>) -> Receiver<Event> {
let (to, from) = mpsc::channel();
for event in events {
to.send(event)
.expect("the loop's end of the channel is open");
}
from
}
fn pressing<const N: usize>(keys: [KeyEvent; N]) -> Receiver<Event> {
waiting(keys.into_iter().map(Event::Key).collect())
}
fn going_round(view: &mut Recorder, passes: usize, first: Vec<Event>) -> Receiver<Event> {
let (to, from) = mpsc::channel();
for event in first {
to.send(event)
.expect("the loop's end of the channel is open");
}
let (went_round, rounds) = mpsc::channel();
view.went_round = Some(went_round);
thread::spawn(move || {
for _ in 0..passes {
if rounds.recv_timeout(A_MOMENT).is_err() {
break;
}
}
drop(to);
});
from
}
#[test]
fn a_keypress_reaches_the_view_as_the_action_it_is_bound_to() {
let mut view = Recorder::default();
let (ask, _asked) = mpsc::channel();
let events = pressing([
key(KeyCode::Char('j')),
key(KeyCode::Char(' ')),
key(KeyCode::Char('q')),
key(KeyCode::Char('k')),
]);
drive(
&mut view,
&events,
&ask,
at_once(),
a_run_reading(nothing_armed()),
&polling_every_interval(),
nothing_watched(),
)
.expect("the loop runs");
assert_eq!(
view.applied,
[Action::Move(Motion::NextRow), Action::ToggleFold],
"q ends the loop, so nothing after it is applied"
);
}
#[test]
fn a_question_mark_shows_the_bindings_and_the_next_key_takes_them_away() {
let mut view = Recorder::default();
let (ask, _asked) = mpsc::channel();
let events = pressing([
key(KeyCode::Char('?')),
key(KeyCode::Char('z')),
key(KeyCode::Char('?')),
key(KeyCode::Char('j')),
]);
drive(
&mut view,
&events,
&ask,
at_once(),
a_run_reading(nothing_armed()),
&polling_every_interval(),
nothing_watched(),
)
.expect("the loop runs");
assert_eq!(
view.showing,
[
Showing::Forest,
Showing::Bindings,
Showing::Forest,
Showing::Bindings,
Showing::Forest,
]
);
assert!(
view.applied.is_empty(),
"j closed the bindings rather than moving the selection: {:?}",
view.applied
);
}
#[test]
fn while_the_prompt_is_up_a_key_is_a_character_of_an_id_and_not_its_binding() {
let mut view = Recorder::default();
let (ask, _asked) = mpsc::channel();
let events = pressing([
key(KeyCode::Char('/')),
key(KeyCode::Char('a')),
key(KeyCode::Char('j')),
key(KeyCode::Enter),
key(KeyCode::Char('q')),
]);
drive(
&mut view,
&events,
&ask,
at_once(),
a_run_reading(nothing_armed()),
&polling_every_interval(),
nothing_watched(),
)
.expect("the loop runs");
assert_eq!(
view.typed,
[
Typing::Character('a'),
Typing::Character('j'),
Typing::Sought
]
);
assert_eq!(
view.applied,
[Action::Search],
"a key typed into the prompt reached the forest"
);
assert_eq!(
view.showing,
[
Showing::Forest,
Showing::Searching,
Showing::Searching,
Showing::Searching,
Showing::Forest,
]
);
}
#[test]
fn esc_leaves_the_prompt_without_asking_for_what_was_typed() {
let mut view = Recorder::default();
let (ask, _asked) = mpsc::channel();
let events = pressing([
key(KeyCode::Char('/')),
key(KeyCode::Char('x')),
key(KeyCode::Esc),
key(KeyCode::Char('q')),
]);
drive(
&mut view,
&events,
&ask,
at_once(),
a_run_reading(nothing_armed()),
&polling_every_interval(),
nothing_watched(),
)
.expect("the loop runs");
assert_eq!(
view.typed,
[Typing::Character('x'), Typing::Abandoned],
"Esc asked for the id it was leaving behind"
);
assert_eq!(
view.showing,
[
Showing::Forest,
Showing::Searching,
Showing::Searching,
Showing::Forest,
]
);
}
#[test]
fn control_c_leaves_bdi_from_the_prompt_where_q_is_a_letter_of_the_id() {
let mut view = Recorder::default();
let (ask, _asked) = mpsc::channel();
let events = pressing([
key(KeyCode::Char('/')),
key(KeyCode::Char('q')),
control('c'),
key(KeyCode::Char('j')),
]);
drive(
&mut view,
&events,
&ask,
at_once(),
a_run_reading(nothing_armed()),
&polling_every_interval(),
nothing_watched(),
)
.expect("the loop runs");
assert_eq!(view.typed, [Typing::Character('q')]);
assert_eq!(
view.showing,
[Showing::Forest, Showing::Searching, Showing::Searching],
"^C ended the run, so nothing after it was drawn"
);
}
#[test]
fn a_click_leaves_the_prompt_and_asks_for_nothing() {
let mut view = Recorder::default();
let (ask, _asked) = mpsc::channel();
let events = waiting(vec![
Event::Key(key(KeyCode::Char('/'))),
Event::Clicked(3),
Event::Key(key(KeyCode::Char('q'))),
]);
drive(
&mut view,
&events,
&ask,
at_once(),
a_run_reading(nothing_armed()),
&polling_every_interval(),
nothing_watched(),
)
.expect("the loop runs");
assert_eq!(view.typed, [Typing::Abandoned]);
assert!(
view.clicked.is_empty(),
"the click selected a row under the prompt: {:?}",
view.clicked
);
}
#[test]
fn quitting_from_the_bindings_takes_two_presses_and_the_first_is_not_lost() {
let mut view = Recorder::default();
let (ask, _asked) = mpsc::channel();
let events = pressing([
key(KeyCode::Char('?')),
key(KeyCode::Char('q')),
key(KeyCode::Char('q')),
key(KeyCode::Char('j')),
]);
drive(
&mut view,
&events,
&ask,
at_once(),
a_run_reading(nothing_armed()),
&polling_every_interval(),
nothing_watched(),
)
.expect("the loop runs");
assert_eq!(
view.showing,
[Showing::Forest, Showing::Bindings, Showing::Forest],
"the second q ended the loop, so nothing was drawn after it"
);
}
#[test]
fn a_collection_arriving_behind_the_bindings_leaves_them_up() {
let mut view = Recorder::default();
let (ask, _asked) = mpsc::channel();
let events = waiting(vec![
Event::Key(key(KeyCode::Char('?'))),
Event::Collected(Box::new(a_snapshot())),
]);
drive(
&mut view,
&events,
&ask,
at_once(),
a_run_reading(nothing_armed()),
&polling_every_interval(),
nothing_watched(),
)
.expect("the loop runs");
assert_eq!(view.collected, 1);
assert_eq!(
view.showing,
[Showing::Forest, Showing::Bindings, Showing::Bindings]
);
}
#[test]
fn enter_shows_the_bead_and_esc_goes_back_to_the_forest() {
let mut view = Recorder::default();
let (ask, _asked) = mpsc::channel();
let events = pressing([
key(KeyCode::Enter),
key(KeyCode::Esc),
key(KeyCode::Char('j')),
key(KeyCode::Char('q')),
]);
drive(
&mut view,
&events,
&ask,
at_once(),
a_run_reading(nothing_armed()),
&polling_every_interval(),
nothing_watched(),
)
.expect("the loop runs");
assert_eq!(
view.showing,
[
Showing::Forest,
Showing::Bead,
Showing::Forest,
Showing::Forest
]
);
assert_eq!(
view.applied,
[Action::ShowBead, Action::Move(Motion::NextRow)],
"j after Esc moved the selection, so the forest was back"
);
}
#[test]
fn enter_on_a_row_that_is_not_a_bead_leaves_the_forest_up() {
let mut view = Recorder {
not_a_bead: true,
..Recorder::default()
};
let (ask, _asked) = mpsc::channel();
let events = pressing([
key(KeyCode::Enter),
key(KeyCode::Char('j')),
key(KeyCode::Char('q')),
key(KeyCode::Char('k')),
]);
drive(
&mut view,
&events,
&ask,
at_once(),
a_run_reading(nothing_armed()),
&polling_every_interval(),
nothing_watched(),
)
.expect("the loop runs");
assert_eq!(
view.showing,
[Showing::Forest, Showing::Forest],
"nothing was drawn for the Enter, and j drew the forest"
);
assert_eq!(
view.applied,
[Action::ShowBead, Action::Move(Motion::NextRow)]
);
assert!(view.scrolled.is_empty(), "{:?}", view.scrolled);
}
#[test]
fn a_motion_in_the_bead_view_scrolls_the_bead_and_not_the_forest() {
let mut view = Recorder::default();
let (ask, _asked) = mpsc::channel();
let events = pressing([
key(KeyCode::Enter),
key(KeyCode::Char('j')),
control('d'),
key(KeyCode::Esc),
]);
drive(
&mut view,
&events,
&ask,
at_once(),
a_run_reading(nothing_armed()),
&polling_every_interval(),
nothing_watched(),
)
.expect("the loop runs");
assert_eq!(view.scrolled, [Motion::NextRow, Motion::HalfScreenDown]);
assert_eq!(
view.applied,
[Action::ShowBead],
"no motion reached the forest"
);
}
#[test]
fn enter_f_and_y_in_the_bead_view_act_on_the_bead_and_leave_the_view_up() {
let mut view = Recorder::default();
let (ask, _asked) = mpsc::channel();
let events = waiting(vec![
Event::Key(key(KeyCode::Enter)),
Event::Key(key(KeyCode::Enter)),
Event::Key(key(KeyCode::Char('f'))),
Event::Key(key(KeyCode::Char('y'))),
Event::Key(key(KeyCode::Char('q'))),
]);
drive(
&mut view,
&events,
&ask,
at_once(),
a_run_reading(nothing_armed()),
&polling_every_interval(),
nothing_watched(),
)
.expect("the loop runs");
assert_eq!(
view.applied,
[
Action::ShowBead,
Action::Focus,
Action::Focus,
Action::CopyId
]
);
assert_eq!(
view.showing,
[
Showing::Forest,
Showing::Bead,
Showing::Bead,
Showing::Bead,
Showing::Bead,
Showing::Forest
]
);
}
#[test]
fn enter_in_the_bead_view_focuses_the_pane_where_there_is_nothing_to_follow() {
let mut view = Recorder::default();
let (ask, _asked) = mpsc::channel();
let events = waiting(vec![
Event::Key(key(KeyCode::Enter)),
Event::Key(key(KeyCode::Enter)),
]);
drive(
&mut view,
&events,
&ask,
at_once(),
a_run_reading(nothing_armed()),
&polling_every_interval(),
nothing_watched(),
)
.expect("the loop runs");
assert_eq!(view.followed, 1, "the follow was not tried");
assert_eq!(view.applied, [Action::ShowBead, Action::Focus]);
}
#[test]
fn enter_on_a_bead_the_window_names_follows_it_and_focuses_nothing() {
let mut view = Recorder {
on_a_reference: true,
..Recorder::default()
};
let (ask, _asked) = mpsc::channel();
let events = waiting(vec![
Event::Key(key(KeyCode::Enter)),
Event::Key(key(KeyCode::Enter)),
]);
drive(
&mut view,
&events,
&ask,
at_once(),
a_run_reading(nothing_armed()),
&polling_every_interval(),
nothing_watched(),
)
.expect("the loop runs");
assert_eq!(view.followed, 1);
assert_eq!(
view.applied,
[Action::ShowBead],
"the pane was focused on a press that went somewhere"
);
assert_eq!(
view.showing,
[Showing::Forest, Showing::Bead, Showing::Bead],
"following a bead left the view"
);
}
#[test]
fn esc_goes_back_a_bead_before_it_leaves_the_view() {
let mut view = Recorder {
somewhere_to_go_back_to: true,
..Recorder::default()
};
let (ask, _asked) = mpsc::channel();
let events = waiting(vec![
Event::Key(key(KeyCode::Enter)),
Event::Key(key(KeyCode::Esc)),
]);
drive(
&mut view,
&events,
&ask,
at_once(),
a_run_reading(nothing_armed()),
&polling_every_interval(),
nothing_watched(),
)
.expect("the loop runs");
assert_eq!(view.retraced, 1);
assert_eq!(
view.showing,
[Showing::Forest, Showing::Bead, Showing::Bead],
"Esc left the view with a bead still to go back to"
);
}
#[test]
fn esc_leaves_the_view_where_there_is_no_bead_to_go_back_to() {
let mut view = Recorder::default();
let (ask, _asked) = mpsc::channel();
let events = waiting(vec![
Event::Key(key(KeyCode::Enter)),
Event::Key(key(KeyCode::Esc)),
]);
drive(
&mut view,
&events,
&ask,
at_once(),
a_run_reading(nothing_armed()),
&polling_every_interval(),
nothing_watched(),
)
.expect("the loop runs");
assert_eq!(view.retraced, 1, "the way back was not tried");
assert_eq!(
view.showing,
[Showing::Forest, Showing::Bead, Showing::Forest]
);
}
#[test]
fn q_leaves_the_bead_view_whatever_there_is_to_go_back_to() {
let mut view = Recorder {
somewhere_to_go_back_to: true,
..Recorder::default()
};
let (ask, _asked) = mpsc::channel();
let events = waiting(vec![
Event::Key(key(KeyCode::Enter)),
Event::Key(key(KeyCode::Char('q'))),
]);
drive(
&mut view,
&events,
&ask,
at_once(),
a_run_reading(nothing_armed()),
&polling_every_interval(),
nothing_watched(),
)
.expect("the loop runs");
assert_eq!(view.retraced, 0, "q asked for a step back");
assert_eq!(
view.showing,
[Showing::Forest, Showing::Bead, Showing::Forest]
);
}
#[test]
fn tab_steps_the_ring_in_the_bead_view_and_does_nothing_in_the_forest() {
let mut view = Recorder::default();
let (ask, _asked) = mpsc::channel();
let events = pressing([
key(KeyCode::Tab),
key(KeyCode::Enter),
key(KeyCode::Tab),
key(KeyCode::Tab),
]);
drive(
&mut view,
&events,
&ask,
at_once(),
a_run_reading(nothing_armed()),
&polling_every_interval(),
nothing_watched(),
)
.expect("the loop runs");
assert_eq!(
view.applied,
[Action::ShowBead, Action::NextRelated, Action::NextRelated],
"the Tab pressed in the forest reached the view"
);
}
#[test]
fn quitting_from_the_bead_view_takes_two_presses_and_the_first_goes_back() {
let mut view = Recorder::default();
let (ask, _asked) = mpsc::channel();
let events = pressing([
key(KeyCode::Enter),
key(KeyCode::Char('q')),
key(KeyCode::Char('q')),
key(KeyCode::Char('j')),
]);
drive(
&mut view,
&events,
&ask,
at_once(),
a_run_reading(nothing_armed()),
&polling_every_interval(),
nothing_watched(),
)
.expect("the loop runs");
assert_eq!(
view.showing,
[Showing::Forest, Showing::Bead, Showing::Forest],
"the second q ended the loop, so nothing was drawn after it"
);
assert_eq!(view.applied, [Action::ShowBead]);
}
#[test]
fn a_collection_arriving_behind_the_bead_view_leaves_it_up() {
let mut view = Recorder::default();
let (ask, _asked) = mpsc::channel();
let events = waiting(vec![
Event::Key(key(KeyCode::Enter)),
Event::Collected(Box::new(a_snapshot())),
]);
drive(
&mut view,
&events,
&ask,
at_once(),
a_run_reading(nothing_armed()),
&polling_every_interval(),
nothing_watched(),
)
.expect("the loop runs");
assert_eq!(view.collected, 1);
assert_eq!(
view.showing,
[Showing::Forest, Showing::Bead, Showing::Bead]
);
}
#[test]
fn a_collection_that_moves_the_selection_off_the_bead_takes_the_view_down() {
let mut view = Recorder {
collection_moves_the_selection: true,
..Recorder::default()
};
let (ask, _asked) = mpsc::channel();
let events = waiting(vec![
Event::Key(key(KeyCode::Enter)),
Event::Collected(Box::new(a_snapshot())),
Event::Key(key(KeyCode::Char('j'))),
]);
drive(
&mut view,
&events,
&ask,
at_once(),
a_run_reading(nothing_armed()),
&polling_every_interval(),
nothing_watched(),
)
.expect("the loop runs");
assert_eq!(
view.showing,
[
Showing::Forest,
Showing::Bead,
Showing::Forest,
Showing::Forest
]
);
assert_eq!(
view.applied,
[Action::ShowBead, Action::Move(Motion::NextRow)],
"j after the collection moved the selection rather than the bead"
);
assert!(view.scrolled.is_empty(), "{:?}", view.scrolled);
}
#[test]
fn a_notch_over_the_bead_view_scrolls_it() {
let mut view = Recorder::default();
let (ask, _asked) = mpsc::channel();
let events = waiting(vec![
Event::Key(key(KeyCode::Enter)),
Event::Scrolled(Notch::Down),
Event::Key(key(KeyCode::Char('q'))),
]);
drive(
&mut view,
&events,
&ask,
at_once(),
a_run_reading(nothing_armed()),
&polling_every_interval(),
nothing_watched(),
)
.expect("the loop runs");
assert_eq!(view.notched_bead, [Notch::Down]);
assert!(
view.applied
.iter()
.all(|action| *action == Action::ShowBead),
"the notch moved the selection: {:?}",
view.applied
);
}
#[test]
fn a_click_over_the_bead_view_asks_the_window_and_not_the_forest() {
let view = a_click_over_the_bead_view(Landed::Nothing);
assert_eq!(view.clicked_bead, [3]);
assert!(
view.clicked.is_empty(),
"the click reached the forest as well: {:?}",
view.clicked
);
}
#[test]
fn a_click_that_followed_a_reference_leaves_the_window_up() {
let view = a_click_over_the_bead_view(Landed::Followed);
assert_eq!(
view.showing,
[Showing::Forest, Showing::Bead, Showing::Bead, Showing::Bead]
);
assert_eq!(view.notched_bead, [Notch::Down], "{:?}", view.applied);
}
#[test]
fn a_click_that_went_nowhere_leaves_the_window_up_and_draws_nothing() {
let view = a_click_over_the_bead_view(Landed::Nothing);
assert_eq!(
view.showing,
[Showing::Forest, Showing::Bead, Showing::Bead]
);
assert_eq!(view.notched_bead, [Notch::Down], "{:?}", view.applied);
}
#[test]
fn a_click_off_the_page_takes_the_window_away() {
let view = a_click_over_the_bead_view(Landed::Away);
assert_eq!(
view.showing,
[
Showing::Forest,
Showing::Bead,
Showing::Forest,
Showing::Forest
]
);
assert!(view.notched_bead.is_empty(), "{:?}", view.notched_bead);
assert_eq!(view.notched, [Notch::Down]);
assert_eq!(view.applied, [Action::ShowBead]);
}
fn a_click_over_the_bead_view(lands_on: Landed) -> Recorder {
let mut view = Recorder {
lands_on: Some(lands_on),
..Recorder::default()
};
let (ask, _asked) = mpsc::channel();
let events = waiting(vec![
Event::Key(key(KeyCode::Enter)),
Event::Clicked(3),
Event::Scrolled(Notch::Down),
]);
drive(
&mut view,
&events,
&ask,
at_once(),
a_run_reading(nothing_armed()),
&polling_every_interval(),
nothing_watched(),
)
.expect("the loop runs");
view
}
#[test]
fn a_forced_refresh_asks_for_a_collection_and_the_loop_reads_on() {
let mut view = Recorder::default();
let (ask, asked) = mpsc::channel();
let events = waiting(vec![
Event::Key(control('r')),
Event::Key(key(KeyCode::Char('j'))),
]);
drive(
&mut view,
&events,
&ask,
at_once(),
a_run_reading(nothing_armed()),
&polling_every_interval(),
nothing_watched(),
)
.expect("the loop runs");
assert_eq!(reads(asked.try_iter()), [Wanted::Everything]);
assert_eq!(
view.applied,
[Action::Move(Motion::NextRow)],
"the loop read on rather than waiting for the collection"
);
}
#[test]
fn the_refresh_key_puts_something_on_the_screen_before_the_snapshot_lands() {
let mut view = Recorder::default();
let (ask, _asked) = mpsc::channel();
let events = waiting(vec![Event::Key(control('r'))]);
drive(
&mut view,
&events,
&ask,
at_once(),
a_run_reading(nothing_armed()),
&polling_every_interval(),
nothing_watched(),
)
.expect("the loop runs");
assert_eq!(
view.collecting(),
[vec![Wanted::Everything]],
"the view was told a collection began, and over which projects"
);
assert_eq!(
view.drawn(),
2,
"the first frame, and one for the keystroke"
);
}
const A_LONG_WINDOW: TimeDelta = TimeDelta::seconds(30);
fn gathering(window: TimeDelta) -> Outstanding {
Outstanding::waiting(PATIENCE, window)
}
#[test]
fn a_read_is_said_on_the_screen_before_its_window_lets_it_go() {
let mut view = Recorder::default();
let (ask, asked) = mpsc::channel();
let events = waiting(vec![Event::Key(control('r'))]);
drive(
&mut view,
&events,
&ask,
gathering(A_LONG_WINDOW),
a_run_reading(nothing_armed()),
&polling_every_interval(),
nothing_watched(),
)
.expect("the loop runs");
assert_eq!(
view.collecting(),
[vec![Wanted::Everything]],
"the mark belongs to the ask"
);
assert!(
asked.try_iter().next().is_none(),
"and the read had not gone: marked before sent, not after"
);
}
#[test]
fn a_burst_about_one_project_inside_the_window_costs_one_read() {
let mut view = Recorder::default();
let (ask, _asked) = mpsc::channel();
let events = waiting((0..100).map(|_| Event::Changed(arkham())).collect());
drive(
&mut view,
&events,
&ask,
gathering(A_LONG_WINDOW),
a_run_reading(nothing_armed()),
&polling_every_interval(),
nothing_watched(),
)
.expect("the loop runs");
assert_eq!(
view.collecting(),
[vec![arkham()]],
"a hundred messages, one read, and the screen told once"
);
}
#[test]
fn a_loop_with_nothing_due_sleeps_until_something_happens() {
assert_eq!(
sleeps_for(
&Recorder::default(),
&at_once(),
¬hing_armed(),
None,
Utc::now(),
Utc::now()
),
None
);
}
#[test]
fn the_loop_sleeps_until_the_soonest_of_what_it_is_waiting_for() {
let now = Utc::now();
let mut outstanding = gathering(A_LONG_WINDOW);
outstanding.ask(arkham(), now);
let mut sooner = Armed::polling("ferry".to_string(), Some(AN_INTERVAL));
sooner.came_back(&ferry(), now);
assert_eq!(
sleeps_for(
&Recorder::default(),
&outstanding,
&[sooner],
None,
now,
now
),
Some(AN_INTERVAL),
"the poll comes round long before the window is out"
);
assert_eq!(
sleeps_for(
&Recorder::default(),
&outstanding,
¬hing_armed(),
None,
now,
now
),
A_LONG_WINDOW.to_std().ok(),
"and with nothing armed, the window is what is left to wait for"
);
}
#[test]
fn the_loop_sleeps_until_the_band_is_due_to_read_its_pane_again() {
let now = Utc::now();
let mut outstanding = gathering(A_LONG_WINDOW);
outstanding.ask(arkham(), now);
let view = Recorder {
rereads_in: Some(AN_INTERVAL),
..Recorder::default()
};
assert_eq!(
sleeps_for(&view, &outstanding, ¬hing_armed(), None, now, now),
Some(AN_INTERVAL),
"the pane falls due long before the window is out"
);
}
#[test]
fn a_loop_with_nothing_else_due_still_wakes_to_look_at_the_config() {
let now = Utc::now();
let reload = a_config_looked_at_every(AN_INTERVAL, now);
assert_eq!(
sleeps_for(
&Recorder::default(),
&at_once(),
¬hing_armed(),
Some(&reload),
now,
now
),
Some(AN_INTERVAL),
"nothing else is going to wake it"
);
}
fn a_config_looked_at_every(every: Duration, now: DateTime<Utc>) -> Reload {
Reload::watching(
std::path::PathBuf::from("/a/config/nothing/here/opens"),
every,
crate::config::Config::naming(Vec::new()),
Box::new(crate::config::Config::from_toml),
now,
)
}
const ARKHAM_ALONE: &str = "[[projects]]\nname = \"arkham\"\npath = \"/srv/work/arkham\"\n";
const FERRY_ALONE: &str = "[[projects]]\nname = \"ferry\"\npath = \"/srv/work/ferry\"\n";
const ARKHAM_AND_FERRY: &str = "[[projects]]\nname = \"arkham\"\npath = \"/srv/work/arkham\"\n\n[[projects]]\nname = \"ferry\"\npath = \"/srv/work/ferry\"\n";
const ARKHAM_TOLD_INSTEAD: &str = "[[projects]]\nname = \"arkham\"\npath = \"/srv/work/arkham\"\npoll = false\n\n[[projects]]\nname = \"ferry\"\npath = \"/srv/work/ferry\"\n";
fn a_config(text: &str) -> Config {
Config::from_toml(text).expect("the fixture parses")
}
fn a_config_file_saying(named: &str, written: &str, in_force: &str) -> Reload {
let path =
std::env::temp_dir().join(format!("bdi-drive-{named}-{}.toml", std::process::id()));
std::fs::write(&path, written).expect("the config is ours to write");
Reload::watching(
path,
AN_INTERVAL,
a_config(in_force),
Box::new(Config::from_toml),
Utc::now(),
)
}
#[test]
fn the_collector_is_told_the_config_the_reader_has_written() {
let mut view = Recorder::default();
let (ask, asked) = mpsc::channel();
let events = going_round(&mut view, A_FEW_PASSES, Vec::new());
drive(
&mut view,
&events,
&ask,
at_once(),
a_run_reading(nothing_armed()),
&polling_every_interval(),
Some(a_config_file_saying(
"gained",
ARKHAM_AND_FERRY,
ARKHAM_ALONE,
)),
)
.expect("the loop runs");
assert_eq!(
asked.try_iter().collect::<Vec<_>>(),
[
Asked::Reloaded(Box::new(a_config(ARKHAM_AND_FERRY))),
Asked::Read(Wanted::Everything)
],
"the collector was handed the config the reader wrote, and then \
asked to read every project under it"
);
}
#[test]
fn the_inbound_channel_is_told_the_config_the_reader_has_written() {
let mut view = Recorder::default();
let (ask, _asked) = mpsc::channel();
let events = going_round(&mut view, A_FEW_PASSES, Vec::new());
let reported = Reported::watching(["arkham".to_string()]);
drive(
&mut view,
&events,
&ask,
at_once(),
Reading::of(nothing_armed(), reported.clone()),
&polling_every_interval(),
Some(a_config_file_saying("accepts", FERRY_ALONE, ARKHAM_ALONE)),
)
.expect("the loop runs");
assert_eq!(
(reported.take("ferry"), reported.take("arkham")),
(
crate::collect::changes::Answer::Watched("ferry".to_string()),
crate::collect::changes::Answer::Unwatched("arkham".to_string())
),
"the channel accepts the project the reader added and refuses \
the one they took out, without the run being started again"
);
}
fn until_a_project_asks_for_itself(
named: &str,
armed: Vec<Armed>,
in_force: &str,
written: &str,
) -> Vec<Asked> {
let mut view = Recorder::default();
let (ask, asked) = mpsc::channel();
let (send, events) = mpsc::channel();
let holding = thread::spawn(move || {
let mut reached = Vec::new();
while let Ok(one) = asked.recv_timeout(A_MOMENT) {
let arms_them = one == Asked::Read(Wanted::Everything);
let polled = matches!(one, Asked::Read(Wanted::Project(_)));
reached.push(one);
if arms_them {
send.send(Event::Collected(Box::new(a_snapshot())))
.expect("the loop's end of the channel is open");
}
if polled {
break;
}
}
drop(send);
reached
});
drive(
&mut view,
&events,
&ask,
at_once(),
a_run_reading(armed),
&polling_every_interval(),
Some(a_config_file_saying(named, written, in_force)),
)
.expect("the loop runs");
holding.join().expect("the thread ran")
}
#[test]
fn the_projects_that_poll_are_the_ones_the_config_now_names() {
let reached = until_a_project_asks_for_itself(
"swapped",
vec![Armed::polling("arkham".to_string(), Some(AN_INTERVAL))],
ARKHAM_ALONE,
FERRY_ALONE,
);
assert_eq!(
reached.last(),
Some(&Asked::Read(ferry())),
"the project the config gained asked for itself once its read \
came back"
);
assert!(
!reached.contains(&Asked::Read(arkham())),
"and the project the config lost asked for nothing: {reached:?}"
);
}
#[test]
fn a_project_the_reader_has_stopped_polling_asks_no_more() {
let reached = until_a_project_asks_for_itself(
"told-instead",
vec![
Armed::polling("arkham".to_string(), Some(AN_INTERVAL)),
Armed::polling("ferry".to_string(), Some(AN_INTERVAL)),
],
ARKHAM_AND_FERRY,
ARKHAM_TOLD_INSTEAD,
);
assert_eq!(
reached.last(),
Some(&Asked::Read(ferry())),
"the project the edit left alone asked for itself"
);
assert!(
!reached.contains(&Asked::Read(arkham())),
"and the one it stopped polling asked for nothing, though it is \
still read: {reached:?}"
);
}
#[test]
fn the_band_is_asked_to_read_its_pane_again_once_its_interval_is_up() {
let mut view = Recorder {
rereads_in: Some(AN_INTERVAL),
..Recorder::default()
};
let (ask, _asked) = mpsc::channel();
let events = going_round(&mut view, 1, Vec::new());
let started = Utc::now();
drive(
&mut view,
&events,
&ask,
at_once(),
a_run_reading(nothing_armed()),
&polling_every_interval(),
nothing_watched(),
)
.expect("the loop runs");
let first = view
.reread_at
.first()
.expect("the loop went round before it was let go");
assert!(
*first - started >= TimeDelta::from_std(AN_INTERVAL).expect("a short interval"),
"the band was asked at {first}, before its interval was up from {started}"
);
}
#[test]
fn a_wake_for_the_bands_read_alone_draws_nothing() {
let mut view = Recorder {
rereads_in: Some(AN_INTERVAL),
..Recorder::default()
};
let (ask, _asked) = mpsc::channel();
let events = going_round(&mut view, A_FEW_PASSES, Vec::new());
drive(
&mut view,
&events,
&ask,
at_once(),
a_run_reading(nothing_armed()),
&polling_every_interval(),
nothing_watched(),
)
.expect("the loop runs");
assert!(
view.reread_at.len() >= A_FEW_PASSES,
"the loop was let go after {} passes, so the frames below say nothing about repeated wakes",
view.reread_at.len()
);
assert_eq!(view.drawn(), 1, "the first frame, and no other");
}
#[test]
fn a_deadline_decided_after_the_frame_is_what_is_left_of_it() {
let now = Utc::now();
let mut view = Recorder::default();
View::collecting(&mut view, &[reading(arkham(), now)]);
let drawn_at = now - TimeDelta::milliseconds(30);
assert_eq!(
sleeps_for(&view, &at_once(), ¬hing_armed(), None, drawn_at, now),
Some(phrase::FRAME - Duration::from_millis(30)),
);
let drawn_at = now - TimeDelta::milliseconds(100);
assert_eq!(
sleeps_for(&view, &at_once(), ¬hing_armed(), None, drawn_at, now),
Some(Duration::ZERO),
"the frame ran out before the loop asked, so it wakes at once"
);
}
#[test]
fn a_poll_falling_beside_a_keystroke_that_changed_nothing_still_redraws() {
let mut view = Recorder::default();
let (ask, _asked) = mpsc::channel();
let events = waiting(vec![Event::Key(key(KeyCode::Char('x')))]);
let mut overdue = Armed::polling("arkham".to_string(), Some(AN_INTERVAL));
overdue.came_back(&arkham(), Utc::now() - TimeDelta::hours(1));
drive(
&mut view,
&events,
&ask,
at_once(),
a_run_reading(vec![overdue]),
&polling_every_interval(),
nothing_watched(),
)
.expect("the loop runs");
assert_eq!(
view.collecting(),
[vec![arkham()]],
"the poll asked, and said so"
);
assert_eq!(
view.drawn(),
2,
"the first frame, and one for the read the keystroke landed beside"
);
}
fn until_arkham_asks_for_itself(view: &mut Recorder) -> Option<Wanted> {
let (ask, asked) = mpsc::channel();
let (send, events) = mpsc::channel();
send.send(Event::Collected(Box::new(a_snapshot())))
.expect("the loop's end of the channel is open");
let holding = thread::spawn(move || {
let asked_for = asked.recv_timeout(A_MOMENT);
drop(send);
asked_for
});
drive(
view,
&events,
&ask,
started(),
a_run_reading(vec![Armed::polling(
"arkham".to_string(),
Some(AN_INTERVAL),
)]),
&polling_every_interval(),
nothing_watched(),
)
.expect("the loop runs");
holding.join().expect("the thread ran").ok().map(read)
}
#[test]
fn a_project_asks_for_itself_again_once_its_read_comes_back() {
let mut view = Recorder::default();
let asked_for = until_arkham_asks_for_itself(&mut view);
assert_eq!(
asked_for,
Some(arkham()),
"the read that came back armed arkham, and its interval came round"
);
}
#[test]
fn a_project_that_asked_for_itself_says_so_on_the_screen() {
let mut view = Recorder::default();
until_arkham_asks_for_itself(&mut view);
assert_eq!(
view.collecting().last(),
Some(&vec![arkham()]),
"the poll's own read is said on the screen like any other"
);
}
#[test]
fn a_project_whose_ask_is_never_answered_asks_no_more() {
let mut view = Recorder::default();
let (ask, asked) = mpsc::channel();
let (send, events) = mpsc::channel();
let (went_round, rounds) = mpsc::channel();
view.went_round = Some(went_round);
send.send(Event::Collected(Box::new(a_snapshot())))
.expect("the loop's end of the channel is open");
let holding = thread::spawn(move || {
let first = asked.recv_timeout(A_MOMENT).ok();
let comes_due_again = Utc::now() + an_interval();
while rounds
.recv_timeout(A_MOMENT)
.is_ok_and(|looked_at| looked_at < comes_due_again)
{}
drop(send);
(first, asked)
});
drive(
&mut view,
&events,
&ask,
started(),
a_run_reading(vec![Armed::polling(
"arkham".to_string(),
Some(AN_INTERVAL),
)]),
&polling_every_interval(),
nothing_watched(),
)
.expect("the loop runs");
let (first, asked) = holding.join().expect("the thread ran");
let reached_the_collector = reads(first.into_iter().chain(asked.try_iter()));
let asked_at = *view.asked_at().first().expect("arkham asked once");
let last_looked = *view.reread_at.last().expect("the loop went round");
assert!(
last_looked - asked_at >= an_interval(),
"the loop last looked at what was due {} after the ask, so arkham \
never came due again while that ask stood",
last_looked - asked_at
);
assert_eq!(
reached_the_collector,
[arkham()],
"arkham came due again and was declined: nothing had answered it"
);
}
fn an_interval() -> TimeDelta {
TimeDelta::from_std(AN_INTERVAL).expect("a short interval")
}
const AN_INTERVAL: Duration = Duration::from_millis(20);
const A_FEW_PASSES: usize = 5;
fn started() -> Outstanding {
let mut outstanding = at_once();
outstanding.ask(Wanted::Everything, Utc::now());
outstanding
}
#[test]
fn a_collection_nobody_asked_for_is_still_said_on_the_screen() {
let mut view = Recorder::default();
let (ask, _asked) = mpsc::channel();
let events = waiting(vec![Event::Changed(arkham())]);
drive(
&mut view,
&events,
&ask,
at_once(),
a_run_reading(nothing_armed()),
&polling_every_interval(),
nothing_watched(),
)
.expect("the loop runs");
assert_eq!(view.collecting(), [vec![arkham()]]);
assert_eq!(view.drawn(), 2);
}
#[test]
fn a_wait_gives_up_when_what_is_drawn_stops_being_true() {
let (send, events) = mpsc::channel();
thread::spawn(move || {
thread::sleep(phrase::FRAME * 8);
let _ = send.send(Event::Resize);
});
let began = Instant::now();
let waited = wait(&events, Some(phrase::FRAME));
assert!(matches!(waited, Some(Waited::Aged)));
assert!(began.elapsed() >= phrase::FRAME, "{:?}", began.elapsed());
}
#[test]
fn a_wait_on_a_screen_nothing_can_stale_sleeps_until_an_event() {
let (send, events) = mpsc::channel();
thread::spawn(move || {
thread::sleep(phrase::FRAME * 3);
let _ = send.send(Event::Resize);
});
assert!(matches!(
wait(&events, None),
Some(Waited::Event(Event::Resize))
));
}
#[test]
fn a_frame_running_out_redraws_the_screen_and_nothing_else() {
let mut view = Recorder::default();
let (ask, _asked) = mpsc::channel();
let events = going_round(&mut view, 2, vec![Event::Changed(arkham())]);
drive(
&mut view,
&events,
&ask,
at_once(),
a_run_reading(nothing_armed()),
&polling_every_interval(),
nothing_watched(),
)
.expect("the loop runs");
assert!(
view.drawn() > 2,
"the first frame, the collection starting, and the mark turning: {}",
view.drawn()
);
assert_eq!(view.collecting(), [vec![arkham()]], "no second collection");
assert_eq!(view.applied, [], "and no action for a frame running out");
}
#[test]
fn the_collection_that_starts_behind_another_is_said_too() {
let mut view = Recorder::default();
let (ask, _asked) = mpsc::channel();
let events = waiting(vec![
Event::Changed(arkham()),
Event::Changed(ferry()),
Event::Collected(Box::new(a_snapshot())),
]);
drive(
&mut view,
&events,
&ask,
at_once(),
a_run_reading(nothing_armed()),
&polling_every_interval(),
nothing_watched(),
)
.expect("the loop runs");
assert_eq!(
view.collecting(),
[vec![arkham()], vec![arkham(), ferry()], vec![ferry()]],
"the one the event asked for, then it with ferry waiting behind \
it, then ferry alone once the first came back"
);
assert_eq!(view.collected, 1);
}
#[test]
fn a_collection_is_stamped_with_the_moment_it_was_asked_for() {
let mut view = Recorder::default();
let (ask, _asked) = mpsc::channel();
let before = Utc::now();
let events = waiting(vec![Event::Changed(arkham())]);
drive(
&mut view,
&events,
&ask,
at_once(),
a_run_reading(nothing_armed()),
&polling_every_interval(),
nothing_watched(),
)
.expect("the loop runs");
let asked_at = view.asked_at();
assert_eq!(asked_at.len(), 1, "one collection was started");
assert!(
(before..=Utc::now()).contains(&asked_at[0]),
"the stamp is the moment of the ask: {before} .. {} .. {}",
asked_at[0],
Utc::now()
);
}
#[test]
fn a_project_reported_for_again_while_it_is_read_starts_a_wait_of_its_own() {
let mut view = Recorder::default();
let (ask, _asked) = mpsc::channel();
let events = waiting(vec![
Event::Changed(arkham()),
Event::Changed(arkham()),
Event::Collected(Box::new(a_snapshot())),
]);
drive(
&mut view,
&events,
&ask,
at_once(),
a_run_reading(nothing_armed()),
&polling_every_interval(),
nothing_watched(),
)
.expect("the loop runs");
assert_eq!(
view.collecting(),
[vec![arkham()], vec![arkham(), arkham()], vec![arkham()]],
"the one in flight, then it with the second waiting, then the \
second alone"
);
let told = &view.awaited[1];
assert!(
told[1].asked_at > told[0].asked_at,
"the waiting one kept the running one's stamp: {told:?}"
);
}
#[test]
fn a_collection_with_nothing_behind_it_leaves_the_view_resting() {
let mut view = Recorder::default();
let (ask, _asked) = mpsc::channel();
let events = waiting(vec![
Event::Changed(arkham()),
Event::Collected(Box::new(a_snapshot())),
]);
drive(
&mut view,
&events,
&ask,
at_once(),
a_run_reading(nothing_armed()),
&polling_every_interval(),
nothing_watched(),
)
.expect("the loop runs");
assert_eq!(
view.collecting(),
[vec![arkham()], vec![]],
"the one the event asked for, and nothing once it landed"
);
}
#[test]
fn a_request_waiting_its_turn_is_said_beside_the_one_in_flight() {
let mut view = Recorder::default();
let (ask, _asked) = mpsc::channel();
let events = waiting(vec![Event::Changed(arkham()), Event::Changed(ferry())]);
drive(
&mut view,
&events,
&ask,
at_once(),
a_run_reading(nothing_armed()),
&polling_every_interval(),
nothing_watched(),
)
.expect("the loop runs");
assert_eq!(
view.collecting(),
[vec![arkham()], vec![arkham(), ferry()]],
"the one in flight, then it and the one behind it"
);
assert_eq!(view.drawn(), 3, "and the screen changed for it");
}
#[test]
fn only_one_collection_runs_at_a_time() {
let mut view = Recorder::default();
let (ask, asked) = mpsc::channel();
let events = waiting(vec![
Event::Changed(arkham()),
Event::Changed(ferry()),
Event::Key(control('r')),
]);
drive(
&mut view,
&events,
&ask,
at_once(),
a_run_reading(nothing_armed()),
&polling_every_interval(),
nothing_watched(),
)
.expect("the loop runs");
assert_eq!(
reads(asked.try_iter()),
[arkham()],
"the two behind it wait for the one in flight to come back"
);
}
#[test]
fn a_change_that_arrived_mid_collection_is_asked_for_when_it_comes_back() {
let mut view = Recorder::default();
let (ask, asked) = mpsc::channel();
let events = waiting(vec![
Event::Changed(arkham()),
Event::Changed(ferry()),
Event::Collected(Box::new(a_snapshot())),
]);
drive(
&mut view,
&events,
&ask,
at_once(),
a_run_reading(nothing_armed()),
&polling_every_interval(),
nothing_watched(),
)
.expect("the loop runs");
assert_eq!(reads(asked.try_iter()), [arkham(), ferry()]);
}
#[test]
fn a_project_reported_for_twice_is_read_again_rather_than_deduped() {
let mut view = Recorder::default();
let (ask, asked) = mpsc::channel();
let events = waiting(vec![
Event::Changed(arkham()),
Event::Changed(arkham()),
Event::Collected(Box::new(a_snapshot())),
]);
drive(
&mut view,
&events,
&ask,
at_once(),
a_run_reading(nothing_armed()),
&polling_every_interval(),
nothing_watched(),
)
.expect("the loop runs");
assert_eq!(reads(asked.try_iter()), [arkham(), arkham()]);
}
#[test]
fn a_whole_collection_absorbs_the_projects_waiting_beside_it() {
let mut view = Recorder::default();
let (ask, asked) = mpsc::channel();
let events = waiting(vec![
Event::Changed(arkham()),
Event::Changed(ferry()),
Event::Key(control('r')),
Event::Changed(ferry()),
Event::Collected(Box::new(a_snapshot())),
Event::Collected(Box::new(a_snapshot())),
]);
drive(
&mut view,
&events,
&ask,
at_once(),
a_run_reading(nothing_armed()),
&polling_every_interval(),
nothing_watched(),
)
.expect("the loop runs");
assert_eq!(
reads(asked.try_iter()),
[arkham(), Wanted::Everything],
"ferry was going to be read by the whole collection anyway"
);
}
#[test]
fn the_intervals_passing_during_a_collection_cost_one_collection_between_them() {
let mut view = Recorder::default();
let (ask, asked) = mpsc::channel();
let events = waiting(vec![
Event::Changed(Wanted::Everything),
Event::Changed(Wanted::Everything),
Event::Changed(Wanted::Everything),
Event::Collected(Box::new(a_snapshot())),
Event::Collected(Box::new(a_snapshot())),
]);
drive(
&mut view,
&events,
&ask,
at_once(),
a_run_reading(nothing_armed()),
&polling_every_interval(),
nothing_watched(),
)
.expect("the loop runs");
assert_eq!(
reads(asked.try_iter()),
[Wanted::Everything, Wanted::Everything],
"three ticks over one collection asked for one more, not two"
);
}
#[test]
fn a_collection_that_comes_back_reaches_the_view() {
let mut view = Recorder::default();
let (ask, asked) = mpsc::channel();
let events = waiting(vec![
Event::Changed(arkham()),
Event::Collected(Box::new(a_snapshot())),
Event::Changed(ferry()),
]);
drive(
&mut view,
&events,
&ask,
at_once(),
a_run_reading(nothing_armed()),
&polling_every_interval(),
nothing_watched(),
)
.expect("the loop runs");
assert_eq!(view.collected, 1);
assert_eq!(
reads(asked.try_iter()),
[arkham(), ferry()],
"the collection was over, so the second change asked for its own"
);
}
#[test]
fn a_collection_that_takes_its_time_does_not_hold_up_the_loop() {
let (to_the_loop, events) = mpsc::channel();
let (ask, asked) = mpsc::channel();
let (release, held) = mpsc::channel::<()>();
let collecting = to_the_loop.clone();
let worker = thread::spawn(move || {
collector(
Box::new(move |_| {
let _ = held.recv();
Some(a_snapshot())
}),
&asked,
&collecting,
);
});
for event in [
Event::Key(control('r')),
Event::Key(key(KeyCode::Char('j'))),
Event::Key(key(KeyCode::Char('q'))),
] {
to_the_loop.send(event).expect("the loop is listening");
}
let (finished, ended) = mpsc::channel();
let driving = thread::spawn(move || {
let mut view = Recorder::default();
let outcome = drive(
&mut view,
&events,
&ask,
at_once(),
a_run_reading(nothing_armed()),
&polling_every_interval(),
nothing_watched(),
);
let _ = finished.send(());
(view, outcome)
});
ended
.recv_timeout(A_MOMENT)
.expect("the loop ended on q with the collection still outstanding");
let (view, outcome) = driving.join().expect("the loop's thread ends");
outcome.expect("the loop runs");
assert_eq!(view.applied, [Action::Move(Motion::NextRow)]);
assert_eq!(view.collected, 0, "the collection is still outstanding");
drop(release);
worker.join().expect("the collector ends with its channels");
}
#[test]
fn a_loop_whose_events_run_out_ends() {
let mut view = Recorder::default();
let (ask, _asked) = mpsc::channel();
drive(
&mut view,
&waiting(Vec::new()),
&ask,
at_once(),
a_run_reading(nothing_armed()),
&polling_every_interval(),
nothing_watched(),
)
.expect("the loop runs");
assert!(view.applied.is_empty());
}
#[test]
fn a_signal_ends_the_loop() {
let mut view = Recorder::default();
let (ask, _asked) = mpsc::channel();
drive(
&mut view,
&waiting(vec![Event::Signalled, Event::Resize]),
&ask,
at_once(),
a_run_reading(nothing_armed()),
&polling_every_interval(),
nothing_watched(),
)
.expect("the loop runs");
assert_eq!(
view.drawn(),
1,
"the first draw and no other: the loop returned rather than \
going on to the resize behind the signal"
);
}
#[test]
fn a_signal_ends_the_loop_with_the_bindings_up() {
let mut view = Recorder::default();
let (ask, _asked) = mpsc::channel();
drive(
&mut view,
&waiting(vec![
Event::Key(key(KeyCode::Char('?'))),
Event::Signalled,
Event::Resize,
]),
&ask,
at_once(),
a_run_reading(nothing_armed()),
&polling_every_interval(),
nothing_watched(),
)
.expect("the loop runs");
assert_eq!(
view.drawn(),
2,
"the first draw and the bindings: the signal ended the run rather \
than closing the window"
);
}
#[test]
fn a_resize_redraws_and_nothing_else() {
let mut view = Recorder::default();
let (ask, asked) = mpsc::channel();
drive(
&mut view,
&waiting(vec![Event::Resize]),
&ask,
at_once(),
a_run_reading(nothing_armed()),
&polling_every_interval(),
nothing_watched(),
)
.expect("the loop runs");
assert!(view.applied.is_empty());
assert_eq!(asked.try_iter().count(), 0);
assert_eq!(view.drawn(), 2, "the first draw, and the resize");
}
#[test]
fn a_key_bound_to_nothing_does_not_redraw() {
let mut view = Recorder::default();
let (ask, _asked) = mpsc::channel();
drive(
&mut view,
&waiting(vec![Event::Key(key(KeyCode::Char('z')))]),
&ask,
at_once(),
a_run_reading(nothing_armed()),
&polling_every_interval(),
nothing_watched(),
)
.expect("the loop runs");
assert_eq!(view.drawn(), 1, "the first draw and no other");
}
#[test]
fn a_key_bound_to_nothing_redraws_where_the_press_alone_changed_the_screen() {
let mut view = Recorder {
pressing_changes: true,
..Recorder::default()
};
let (ask, _asked) = mpsc::channel();
drive(
&mut view,
&waiting(vec![Event::Key(key(KeyCode::Char('z')))]),
&ask,
at_once(),
a_run_reading(nothing_armed()),
&polling_every_interval(),
nothing_watched(),
)
.expect("the loop runs");
assert_eq!(view.drawn(), 2, "the first draw and one for the press");
}
#[test]
fn the_view_hears_a_press_before_it_hears_what_the_press_means() {
let mut view = Recorder::default();
let (ask, _asked) = mpsc::channel();
drive(
&mut view,
&waiting(vec![
Event::Key(key(KeyCode::Char('y'))),
Event::Clicked(3),
Event::Scrolled(Notch::Down),
Event::Key(key(KeyCode::Char('?'))),
]),
&ask,
at_once(),
a_run_reading(nothing_armed()),
&polling_every_interval(),
nothing_watched(),
)
.expect("the loop runs");
assert_eq!(view.applied, [Action::CopyId]);
assert_eq!(view.clicked, [3]);
assert_eq!(view.notched, [Notch::Down]);
assert_eq!(
view.pressed_after,
[0, 1, 2, 3],
"a press is heard before the action it turns out to be"
);
}
#[test]
fn a_deadline_is_measured_from_the_instant_the_frame_on_the_screen_was_drawn_at() {
let mut view = Recorder::default();
let (ask, _asked) = mpsc::channel();
drive(
&mut view,
&waiting(vec![Event::Resize, Event::Key(key(KeyCode::Char('z')))]),
&ask,
at_once(),
a_run_reading(nothing_armed()),
&polling_every_interval(),
nothing_watched(),
)
.expect("the loop runs");
let [first, second] = view.drawn_at[..] else {
panic!(
"the first frame and one for the resize: {:?}",
view.drawn_at
);
};
assert_eq!(
*view.measured_at.borrow(),
[first, second, second],
"one deadline after each frame, and one after the key that drew nothing"
);
}
#[test]
fn a_click_reaches_the_view_as_the_row_it_landed_on() {
let mut view = Recorder::default();
let (ask, _asked) = mpsc::channel();
drive(
&mut view,
&waiting(vec![Event::Clicked(9)]),
&ask,
at_once(),
a_run_reading(nothing_armed()),
&polling_every_interval(),
nothing_watched(),
)
.expect("the loop runs");
assert_eq!(view.clicked, [9]);
assert!(view.applied.is_empty(), "a click asks for no action");
assert_eq!(view.drawn(), 2, "the first draw, and the click");
}
#[test]
fn a_click_that_lands_on_no_row_does_not_redraw() {
let mut view = Recorder {
nothing_under_the_pointer: true,
..Recorder::default()
};
let (ask, _asked) = mpsc::channel();
drive(
&mut view,
&waiting(vec![Event::Clicked(21)]),
&ask,
at_once(),
a_run_reading(nothing_armed()),
&polling_every_interval(),
nothing_watched(),
)
.expect("the loop runs");
assert_eq!(view.clicked, [21]);
assert_eq!(view.drawn(), 1, "the first draw and no other");
}
#[test]
fn a_wheel_notch_moves_the_forests_view_and_not_its_selection() {
let mut view = Recorder::default();
let (ask, _asked) = mpsc::channel();
drive(
&mut view,
&waiting(vec![
Event::Scrolled(Notch::Up),
Event::Scrolled(Notch::Down),
]),
&ask,
at_once(),
a_run_reading(nothing_armed()),
&polling_every_interval(),
nothing_watched(),
)
.expect("the loop runs");
assert_eq!(view.notched, [Notch::Up, Notch::Down]);
assert!(view.applied.is_empty(), "a wheel notch is not an action");
assert!(view.clicked.is_empty(), "a wheel notch points at no row");
}
#[test]
fn a_click_over_the_bindings_window_closes_it_and_selects_nothing() {
let mut view = Recorder::default();
let (ask, _asked) = mpsc::channel();
drive(
&mut view,
&waiting(vec![
Event::Key(key(KeyCode::Char('?'))),
Event::Clicked(9),
Event::Clicked(9),
]),
&ask,
at_once(),
a_run_reading(nothing_armed()),
&polling_every_interval(),
nothing_watched(),
)
.expect("the loop runs");
assert_eq!(
view.clicked,
[9],
"the first click took the window away; only the second reached the forest"
);
assert_eq!(
view.showing,
[
Showing::Forest,
Showing::Bindings,
Showing::Forest,
Showing::Forest
]
);
}
#[test]
fn a_wheel_notch_over_the_bindings_window_closes_it_and_moves_nothing() {
let mut view = Recorder::default();
let (ask, _asked) = mpsc::channel();
drive(
&mut view,
&waiting(vec![
Event::Key(key(KeyCode::Char('?'))),
Event::Scrolled(Notch::Down),
]),
&ask,
at_once(),
a_run_reading(nothing_armed()),
&polling_every_interval(),
nothing_watched(),
)
.expect("the loop runs");
assert!(view.applied.is_empty());
assert!(view.notched.is_empty());
assert_eq!(
view.showing,
[Showing::Forest, Showing::Bindings, Showing::Forest]
);
}
mod what_waits {
use super::*;
use crate::config::Tui;
use chrono::TimeZone;
use pretty_assertions::assert_eq;
fn at(second: i64) -> chrono::DateTime<Utc> {
Utc.with_ymd_and_hms(2026, 9, 1, 10, 0, 0).unwrap() + TimeDelta::seconds(second)
}
fn hung_on(project: Wanted) -> (Outstanding, Sender<Asked>, Receiver<Asked>) {
let (ask, asked) = mpsc::channel();
let mut outstanding = at_once();
outstanding.ask(project, at(0));
outstanding.sends(&ask, at(0));
(outstanding, ask, asked)
}
fn stamps(outstanding: &Outstanding) -> Vec<(Wanted, chrono::DateTime<Utc>)> {
outstanding
.awaited()
.iter()
.map(|it| (it.wanted.clone(), it.asked_at))
.collect()
}
#[test]
fn a_read_that_cannot_be_sent_yet_is_stamped_when_it_joins_the_queue() {
let (mut outstanding, _ask, _asked) = hung_on(arkham());
outstanding.ask(ferry(), at(5));
assert_eq!(stamps(&outstanding), [(arkham(), at(0)), (ferry(), at(5))]);
}
#[test]
fn a_project_reported_for_again_while_it_waits_keeps_the_wait_it_has() {
let (mut outstanding, _ask, _asked) = hung_on(arkham());
outstanding.ask(ferry(), at(5));
outstanding.ask(ferry(), at(35));
assert_eq!(stamps(&outstanding), [(arkham(), at(0)), (ferry(), at(5))]);
}
#[test]
fn a_whole_collection_absorbing_what_waits_starts_a_wait_of_its_own() {
let (mut outstanding, _ask, _asked) = hung_on(arkham());
outstanding.ask(ferry(), at(5));
outstanding.ask(Wanted::Everything, at(40));
assert_eq!(
stamps(&outstanding),
[(arkham(), at(0)), (Wanted::Everything, at(40))],
"not :05, which would be every other project's wait too"
);
}
#[test]
fn a_whole_collection_absorbs_a_read_that_has_not_been_sent() {
let mut outstanding = Outstanding::waiting(PATIENCE, A_LONG_WINDOW);
outstanding.ask(arkham(), at(0));
outstanding.ask(Wanted::Everything, at(1));
assert_eq!(
stamps(&outstanding),
[(Wanted::Everything, at(1))],
"the whole collection reads arkham anyway, and nothing had gone yet"
);
}
#[test]
fn a_whole_collection_keeps_the_read_already_in_flight() {
let (mut outstanding, _ask, _asked) = hung_on(arkham());
outstanding.ask(Wanted::Everything, at(1));
assert_eq!(
stamps(&outstanding),
[(arkham(), at(0)), (Wanted::Everything, at(1))]
);
}
#[test]
fn a_window_is_not_pushed_back_by_the_notifications_that_arrive_in_it() {
let mut outstanding = Outstanding::waiting(PATIENCE, TimeDelta::seconds(2));
let (ask, asked) = mpsc::channel();
outstanding.ask(Wanted::Everything, at(0));
for pressed in 1..=10 {
outstanding.ask(Wanted::Everything, at(pressed));
outstanding.sends(&ask, at(pressed));
}
assert_eq!(
reads(asked.try_iter()),
[Wanted::Everything],
"sent two seconds after the first press, not two after the last"
);
}
#[test]
fn a_read_goes_before_its_project_can_be_said_to_have_stopped_being_read() {
let shortest = Tui {
unanswered_after_seconds: 1,
..Tui::default()
}
.unanswered_after();
let (ask, asked) = mpsc::channel();
let mut outstanding = Outstanding::for_a_run(shortest);
outstanding.ask(arkham(), at(0));
let held_for = outstanding
.sends_in(at(0))
.expect("the read just asked for is waiting out its window");
let out = at(0)
+ TimeDelta::from_std(held_for).expect("a window is a length chrono can carry");
outstanding.sends(&ask, out);
assert_eq!(
reads(asked.try_iter()),
[arkham()],
"the window was out, so the read went"
);
assert!(
!outstanding.awaited()[0].unanswered_at(out),
"and arkham was not yet said to have stopped being read"
);
}
#[test]
fn a_read_that_reaches_the_front_keeps_the_stamp_it_queued_at() {
let (mut outstanding, _ask, _asked) = hung_on(arkham());
outstanding.ask(ferry(), at(5));
outstanding.came_back();
assert_eq!(stamps(&outstanding), [(ferry(), at(5))]);
}
#[test]
fn the_reads_are_sent_in_the_order_they_were_asked_for() {
let (mut outstanding, ask, asked) = hung_on(arkham());
outstanding.ask(ferry(), at(5));
outstanding.ask(Wanted::Project("harbour".to_string()), at(6));
outstanding.came_back();
outstanding.sends(&ask, at(7));
outstanding.came_back();
outstanding.sends(&ask, at(8));
assert_eq!(
reads(asked.try_iter()),
[arkham(), ferry(), Wanted::Project("harbour".to_string())]
);
}
#[test]
fn a_collection_nothing_asked_for_leaves_the_queue_alone() {
let (ask, _asked) = mpsc::channel();
let mut outstanding = at_once();
assert_eq!(outstanding.came_back(), None);
outstanding.sends(&ask, at(0));
assert_eq!(stamps(&outstanding), []);
}
}
}