use std::time::Duration;
use crate::degrade::{self, Priority};
use crate::elapsed::format_elapsed;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum WorktreesHiddenBy {
Preference,
Toggle,
}
pub(crate) struct HeaderContent {
pub(crate) entity_count: usize,
pub(crate) run_progress: Option<(usize, usize)>,
pub(crate) filter_match_count: Option<usize>,
pub(crate) worktrees_note: Option<(usize, WorktreesHiddenBy)>,
pub(crate) ignored_note: Option<usize>,
pub(crate) elapsed: Option<Duration>,
}
const SEPARATOR: &str = " · ";
const ELLIPSIS: &str = " ...";
pub(crate) fn trailing_items(content: &HeaderContent) -> Vec<degrade::Item<String>> {
let mut items = Vec::new();
if let Some((done, total)) = content.run_progress {
items.push(degrade::Item {
content: format!("run {done}/{total}"),
priority: Priority::Drop(4),
});
}
if let Some(count) = content.filter_match_count {
items.push(degrade::Item {
content: format!("filter: {count} matches"),
priority: Priority::Drop(3),
});
}
if let Some((count, reason)) = content.worktrees_note {
let reason = match reason {
WorktreesHiddenBy::Preference => "preference off",
WorktreesHiddenBy::Toggle => "toggled off",
};
items.push(degrade::Item {
content: format!("worktrees: {count} ({reason})"),
priority: Priority::Drop(2),
});
}
if let Some(count) = content.ignored_note {
items.push(degrade::Item {
content: format!("ignored: {count} (i shows)"),
priority: Priority::Drop(2),
});
}
if let Some(elapsed) = content.elapsed {
items.push(degrade::Item {
content: format_elapsed(elapsed),
priority: Priority::Drop(1),
});
}
debug_assert!(
items.iter().all(|item| item.content.is_ascii()),
"a header item must be ASCII, or the char-count width in degrade::budget is wrong"
);
items
}
fn items(content: &HeaderContent) -> Vec<degrade::Item<String>> {
let mut items = vec![degrade::Item {
content: format!("{} entities", content.entity_count),
priority: Priority::Pinned,
}];
items.extend(trailing_items(content));
items
}
#[allow(dead_code)] pub(crate) fn render(content: &HeaderContent, width: u16) -> String {
let items = items(content);
degrade::budget(&items, width as usize, SEPARATOR, ELLIPSIS).render(SEPARATOR, ELLIPSIS)
}
#[cfg(test)]
mod tests {
use super::*;
fn sample_content() -> HeaderContent {
HeaderContent {
entity_count: 242,
run_progress: Some((7, 12)),
filter_match_count: Some(12),
worktrees_note: Some((161, WorktreesHiddenBy::Preference)),
ignored_note: None,
elapsed: Some(Duration::from_millis(12000)),
}
}
#[test]
fn header_width_checks_the_first_item_not_only_later_ones() {
let content = HeaderContent {
entity_count: 403,
run_progress: None,
filter_match_count: None,
worktrees_note: None,
ignored_note: None,
elapsed: None,
};
let rendered = render(&content, 5);
assert!(
rendered.chars().count() <= 5,
"must never overrun the given width, got {rendered:?}"
);
assert_eq!(rendered, "");
}
#[test]
fn header_never_overruns_its_budget_at_any_width_from_zero_to_full() {
let content = sample_content();
let full_width = items(&content)
.iter()
.map(|item| item.content.clone())
.collect::<Vec<_>>()
.join(SEPARATOR)
.chars()
.count();
for width in 0..=full_width {
let rendered = render(&content, width as u16);
assert!(
rendered.chars().count() <= width,
"width {width}: rendered {rendered:?} is {} columns",
rendered.chars().count()
);
}
}
#[test]
fn the_worktrees_note_reads_toggled_off_rather_than_preference_off_when_the_toggle_is_why() {
let content = HeaderContent {
entity_count: 403,
run_progress: None,
filter_match_count: None,
worktrees_note: Some((161, WorktreesHiddenBy::Toggle)),
ignored_note: None,
elapsed: None,
};
let rendered = render(&content, 200);
assert!(
rendered.contains("worktrees: 161 (toggled off)"),
"the toggle, not config.toml, hid these rows: {rendered:?}"
);
assert!(
!rendered.contains("preference off"),
"must never claim config.toml said so when the toggle is why: {rendered:?}"
);
}
#[test]
fn the_ignored_note_names_how_many_rows_are_hidden_and_the_key_that_shows_them() {
let content = HeaderContent {
entity_count: 12,
run_progress: None,
filter_match_count: None,
worktrees_note: None,
ignored_note: Some(3),
elapsed: None,
};
let rendered = render(&content, 200);
assert!(
rendered.contains("ignored: 3 (i shows)"),
"a row that vanished on an ignore must be accounted for: {rendered:?}"
);
}
#[test]
fn header_reserves_the_ellipsis_inside_the_budget_rather_than_appending_it_after_a_fit_check() {
let content = HeaderContent {
entity_count: 403,
run_progress: Some((7, 12)),
filter_match_count: None,
worktrees_note: None,
ignored_note: None,
elapsed: None,
};
for width in 8u16..12 {
let rendered = render(&content, width);
assert!(
rendered.chars().count() <= width as usize,
"width {width}: rendered {rendered:?} overruns"
);
assert!(
!rendered.contains("run 7/12"),
"width {width}: run progress should have been dropped to make room for its \
own ellipsis, got {rendered:?}"
);
}
}
fn elapsed_only(elapsed: Duration) -> HeaderContent {
HeaderContent {
entity_count: 403,
run_progress: None,
filter_match_count: None,
worktrees_note: None,
ignored_note: None,
elapsed: Some(elapsed),
}
}
#[test]
fn the_run_timer_moves_up_a_unit_as_it_crosses_one() {
let cases = [
(Duration::from_millis(0), "0ms"),
(Duration::from_millis(168), "168ms"),
(Duration::from_millis(999), "999ms"),
(Duration::from_millis(1000), "1.0s"),
(Duration::from_millis(12000), "12.0s"),
(Duration::from_secs(59), "59.0s"),
(Duration::from_secs(60), "1m00s"),
(Duration::from_secs(168), "2m48s"),
];
for (elapsed, expected) in cases {
let content = elapsed_only(elapsed);
let rendered = render(&content, 999);
assert!(
rendered.ends_with(expected),
"elapsed {elapsed:?}: expected the timer to end with {expected:?}, got {rendered:?}"
);
}
}
struct Row {
width: u16,
expected: String,
}
const SET_NAME_PREFIX: &str = "work ";
fn parse_header_ladder(spec: &str, after: &str) -> Vec<Row> {
let start = spec
.find(after)
.unwrap_or_else(|| panic!("actions.md no longer contains {after:?}"));
let rest = &spec[start..];
let fence_start = rest
.find("```\n")
.expect("a fenced code block must follow the marker");
let after_fence = &rest[fence_start + 4..];
let fence_end = after_fence
.find("```")
.expect("the fenced code block must close");
let block = &after_fence[..fence_end];
block
.lines()
.filter(|line| !line.trim().is_empty())
.map(|line| {
let trimmed = line.trim_start();
let (width_text, rendered) = trimmed
.split_once(" ")
.unwrap_or_else(|| panic!("ladder row is not `<width> <text>`: {line:?}"));
let width: u16 = width_text
.trim()
.parse()
.unwrap_or_else(|_| panic!("ladder row has no numeric width: {line:?}"));
let rendered = rendered.trim_end();
let header_text = rendered.strip_prefix(SET_NAME_PREFIX).unwrap_or_else(|| {
panic!(
"ladder row does not start with {SET_NAME_PREFIX:?}: {line:?}; this \
module owns only what follows the active Set name"
)
});
Row {
width: width - SET_NAME_PREFIX.chars().count() as u16,
expected: header_text.to_string(),
}
})
.collect()
}
fn read_spec() -> String {
let manifest_dir = std::path::Path::new(env!("CARGO_MANIFEST_DIR"));
std::fs::read_to_string(manifest_dir.join("../../docs/spec/actions.md"))
.expect("read the actions spec")
}
#[test]
fn header_matches_the_documented_ladder_at_every_named_width() {
let spec = read_spec();
let rows = parse_header_ladder(
&spec,
"The ladder for the header's own five items, with no warning outstanding.",
);
assert!(!rows.is_empty(), "expected at least one documented width");
for row in &rows {
assert_eq!(
render(&sample_content(), row.width),
row.expected,
"header mismatch at width {}",
row.width
);
}
}
#[test]
fn each_adjacent_priority_pair_has_a_documented_width_that_discriminates_them() {
let spec = read_spec();
let rows = parse_header_ladder(
&spec,
"The ladder for the header's own five items, with no warning outstanding.",
);
let pairs = [
("worktrees: 161 (preference off)", "12.0s"),
("filter: 12 matches", "worktrees: 161 (preference off)"),
("run 7/12", "filter: 12 matches"),
("242 entities", "run 7/12"),
];
for (present, absent) in pairs {
let row = rows
.iter()
.find(|row| row.expected.contains(present) && !row.expected.contains(absent))
.unwrap_or_else(|| {
panic!("no documented row shows {present:?} without {absent:?}")
});
assert_eq!(render(&sample_content(), row.width), row.expected);
}
}
#[test]
fn header_never_reintroduces_the_first_item_exemption_guard() {
let banned = [
format!("{} {} 0", "i", ">"),
format!("{} {} 0", "index", ">"),
format!("{}(1)", ".skip"),
];
let source = crate::test_support::production_source(include_str!("header.rs"));
let offending: Vec<&str> = source
.lines()
.filter(|line| !line.trim_start().starts_with("//"))
.filter(|line| banned.iter().any(|needle| line.contains(needle.as_str())))
.collect();
assert!(
offending.is_empty(),
"found a first-item exemption guard: {offending:?}"
);
}
}