1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
//! What the table counts: the sheep the name filter leaves, and the flock
//! behind it.
use super::*;
impl App {
/// Every sheep the table's rows are drawn from, in name-then-id order: the
/// whole flock, or whatever the filter leaves of it.
///
/// Tests only. The title band was the last thing that drew from it and now
/// counts [`Self::rows_len`] instead, but a test asserting on what the
/// filter keeps wants the rows themselves, in the order the table would put
/// them in.
#[cfg(test)]
#[must_use]
pub fn rows(&self) -> Vec<&Row> {
let mut visible: Vec<&Row> = self.filtered_rows().collect();
visible.sort_unstable_by(|a, b| {
(a.info.name.as_str(), a.info.id).cmp(&(b.info.name.as_str(), b.info.id))
});
visible
}
/// How many sheep the table would draw: the whole flock, or whatever the
/// name filter leaves of it.
///
/// A sheep count, not [`Self::visible_rows`]'s [`RowKey`] count: a group
/// header is not a sheep. The title band shows this beside
/// [`Self::flock_len`] and is the only caller, so nothing here builds or
/// orders a list.
#[must_use]
pub fn rows_len(&self) -> usize {
self.filtered_rows().count()
}
/// Every sheep the name filter keeps, in whatever order the map holds
/// them. [`Self::rows_len`] counts these.
pub(super) fn filtered_rows(&self) -> impl Iterator<Item = &Row> {
let needle = self.filter.as_str().to_lowercase();
self.flock
.values()
.filter(move |row| needle.is_empty() || row.info.name.to_lowercase().contains(&needle))
}
/// Every sheep the shepherd last reported, in id order, whatever the filter
/// hides.
///
/// The host strip sums this rather than [`Self::filtered_rows`], so a name
/// filter cannot narrow what `flock cpu`/`flock mem` add up to while the
/// label still says `flock`.
#[must_use]
pub fn all_rows(&self) -> Vec<&Row> {
self.flock.values().collect()
}
/// How many sheep the shepherd last reported, whatever the filter hides.
#[must_use]
pub fn flock_len(&self) -> usize {
self.flock.len()
}
/// How the flock table currently gathers its rows, toggled by
/// [`KeyPress::FoldView`].
///
/// `view::mod`'s draw loop reads this to choose between the flat column
/// set and the fold view's own.
#[must_use]
pub fn grouping(&self) -> Grouping {
self.grouping
}
/// Sets the filter directly, bypassing [`Self::set_filter`]'s reseat.
#[cfg(test)]
pub(crate) fn set_filter_for_tests(&mut self, query: &str) {
self.filter = TextBox::from(query);
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::lookout::app::testing::*;
/// The fixture separates the two answers: by id it is `web` 0, `api` 1,
/// `web` 2; by name then id it is `api` 1, `web` 0, `web` 2. The `(name,
/// id)` tiebreak itself is not falsifiable here, since the rows arrive in
/// id order; what this catches is the sort going missing entirely.
#[test]
fn the_table_draws_by_name_then_by_id() {
let t0 = Instant::now();
let mut app = App::new(
Palette::detect(None, None, None),
Control::ReadOnly,
"/home/ada/.shep".to_string(),
t0,
);
app.update(Msg::Snapshot {
rows: vec![
sheep(0, "web", ProcStatus::Online),
sheep(1, "api", ProcStatus::Online),
sheep(2, "web", ProcStatus::Online),
],
at: t0,
});
let drawn: Vec<(&str, u32)> = app
.rows()
.iter()
.map(|row| (row.info.name.as_str(), row.info.id))
.collect();
assert_eq!(drawn, vec![("api", 1), ("web", 0), ("web", 2)]);
}
#[test]
fn a_filter_narrows_the_rows_and_leaves_the_real_size_readable() {
let app = filtered("web");
assert_eq!(app.rows().len(), 2, "api-web and web-worker");
assert_eq!(app.flock_len(), 4, "the flock did not get smaller");
}
/// `ProcessSelector`'s `Name` compares with `==`, so borrowing the CLI's
/// selector grammar would match nothing while `web-worker` is being typed.
#[test]
fn the_filter_matches_a_substring_and_not_a_whole_name() {
assert_eq!(filtered("wor").rows().len(), 1, "web-worker, by its middle");
assert_eq!(filtered("w").rows().len(), 2, "api-web, by its own middle");
}
#[test]
fn the_filter_ignores_case_in_both_directions() {
let t0 = Instant::now();
let mut app = App::new(
Palette::detect(None, None, None),
Control::ReadOnly,
"/home/ada/.shep".to_string(),
t0,
);
app.update(Msg::Snapshot {
rows: vec![sheep(1, "WebEdge", ProcStatus::Online)],
at: t0,
});
app.set_filter(TextBox::from("webedge"));
assert_eq!(
app.rows().len(),
1,
"a lowercase query against a mixed name"
);
app.set_filter(TextBox::from("WEBEDGE"));
assert_eq!(app.rows().len(), 1, "and an uppercase one");
}
#[test]
fn nothing_visible_means_nothing_selected() {
let app = filtered("zzz");
assert_eq!(app.rows().len(), 0);
assert_eq!(app.selected(), None);
assert!(app.selected_row().is_none());
assert_eq!(app.flock_len(), 4, "the flock is still four sheep");
}
#[test]
fn a_filter_survives_the_two_second_snapshot() {
let mut app = filtered("web");
let t1 = Instant::now();
app.update(Msg::Snapshot {
rows: vec![
sheep(1, "api-web", ProcStatus::Online),
sheep(2, "cron", ProcStatus::Online),
sheep(3, "queue", ProcStatus::Online),
sheep(4, "web-worker", ProcStatus::Online),
],
at: t1,
});
assert_eq!(app.filter(), "web", "the snapshot did not clear it");
assert_eq!(app.rows().len(), 2, "and did not widen the table");
assert_eq!(app.flock_len(), 4);
}
#[test]
fn an_empty_query_is_the_same_as_no_filter() {
let mut app = filtered("zzz");
app.set_filter(TextBox::new());
assert_eq!(app.rows().len(), 4);
assert_eq!(app.selected(), Some(RowKey::Sheep(1)), "seated again");
}
}