use super::*;
use crate::tui_sessions::SessionState;
fn terminal_restore_was_observed(action: impl FnOnce()) -> bool {
TERMINAL_RESTORE_OBSERVED.with(|observed| observed.set(false));
action();
TERMINAL_RESTORE_OBSERVED.with(std::cell::Cell::get)
}
#[test]
fn terminal_restore_function_runs_the_cleanup_path() {
assert!(terminal_restore_was_observed(restore_terminal));
}
#[test]
fn terminal_restore_guard_runs_on_drop() {
assert!(terminal_restore_was_observed(|| {
let _guard = TerminalModeGuard;
}));
}
#[test]
fn test_tab_needs_entries_only_for_store() {
assert!(!tab_needs_entries(Tab::Now));
assert!(!tab_needs_entries(Tab::Build));
assert!(!tab_needs_entries(Tab::Projects));
assert!(tab_needs_entries(Tab::Store));
assert!(!tab_needs_entries(Tab::Transfer));
assert!(!tab_needs_entries(Tab::Why));
}
#[test]
fn now_leads_and_transfer_stays_last() {
assert_eq!(Tab::ORDER[0], Tab::Now);
assert_eq!(*Tab::ORDER.last().unwrap(), Tab::Transfer);
assert_eq!(Tab::Now.next(), Tab::Build);
assert_eq!(Tab::Build.previous(), Tab::Now);
}
#[test]
fn event_presentation_surfaces_the_action_not_just_disposition() {
let (_, status, action, color) = event_presentation(EventResult::Miss);
assert_eq!(status, "miss");
assert_eq!(action, "built + cached");
assert_ne!(color, Color::Red);
assert_eq!(event_presentation(EventResult::Dup).2, "built + deduped");
assert_eq!(event_presentation(EventResult::LocalHit).2, "restored");
assert_eq!(event_presentation(EventResult::RemoteHit).2, "downloaded");
assert_eq!(
event_presentation(EventResult::Passthrough).2,
"built (not cached)"
);
assert_eq!(event_presentation(EventResult::Error).3, Color::Red);
}
#[test]
fn fmt_duration_ms_blanks_zero_and_scales() {
assert_eq!(fmt_duration_ms(0), "");
assert_eq!(fmt_duration_ms(250), "250ms");
assert_eq!(fmt_duration_ms(1500), "1.5s");
}
#[test]
fn format_speed_scales_units() {
assert_eq!(format_speed(0.0), "0 B/s");
assert_eq!(format_speed(500.0), "500 B/s");
assert_eq!(format_speed(2_000.0), "2 KB/s");
assert_eq!(format_speed(5_000_000.0), "5.0 MB/s");
}
#[test]
fn sort_mode_next_cycles_through_all_modes() {
let mut m = SortMode::Size;
let mut labels = vec![m.label().to_string()];
for _ in 0..4 {
m = m.next();
labels.push(m.label().to_string());
}
assert_eq!(labels, ["size", "hits", "age", "name", "size"]);
}
#[test]
fn shorten_home_replaces_home_prefix() {
if let Some(home) = dirs::home_dir() {
let p = home.join("projects/x");
assert_eq!(shorten_home(&p), "~/projects/x");
}
let outside = std::path::Path::new("/opt/elsewhere");
assert_eq!(shorten_home(outside), "/opt/elsewhere");
}
fn test_config() -> Config {
use crate::config::{
DEFAULT_DAEMON_IDLE_TIMEOUT_SECS, DEFAULT_REMOTE_NEGATIVE_TTL_SECS,
DEFAULT_REMOTE_RESTORE_TIMEOUT_SECS, DEFAULT_S3_POOL_IDLE_SECS,
};
Config {
readonly_store: None,
fallback: None,
key_salt: None,
cc_extra_allowlist_flags: Vec::new(),
local_only: false,
remote_readonly: false,
pull_request_prefix: None,
modified_input_guard: false,
input_predictions: false,
record_sessions: false,
volume_stores: Vec::new(),
windows_hardlink: false,
shared_hardlink_restores: false,
deferred_discovery: true,
out_dir_alias: true,
deferred_durability: false,
daemon_publish: false,
project_rules: crate::config::ProjectRules::default(),
auto_gc: true,
index_auto_compact: true,
auto_clean_orphaned_targets: false,
auto_share_target_files: false,
auto_clean_idle_targets_days: 0,
auto_recover_min_free_bytes: 0,
auto_recover_targets: false,
scheduler_memory_pressure: true,
auto_clean_unused_units_days: 0,
target_liveness: false,
seed_new_targets: false,
build_script_hermetic: false,
gc_evict_shared: false,
storage_layout_advice: true,
heartbeat_secs: 30,
explain_miss: false,
scheduler: true,
test_lease: None,
path_only_env_vars: Vec::new(),
incremental_crates: Vec::new(),
key_env_vars: Vec::new(),
base_dirs: Vec::new(),
cache_dir: std::env::temp_dir().join("kache-tui-test"),
runtime_dir: std::env::temp_dir().join("kache-tui-test"),
max_size: 1024 * 1024,
remote: None,
remote_error: None,
socket_path_override: None,
disabled: false,
cache_executables: false,
cache_cc_links: false,
trust_codegen_backends: false,
clean_incremental: true,
preserve_incremental: false,
adaptive_incremental: true,
event_log_max_size: 1024 * 1024,
event_log_keep_lines: 1000,
compression_level: 3,
s3_concurrency: 16,
prefetch_enabled: crate::config::DEFAULT_PREFETCH_ENABLED,
remote_key_listing: false,
remote_key_cache_refresh_secs: crate::config::DEFAULT_REMOTE_KEY_CACHE_REFRESH_SECS,
prefetch_max_keys: crate::config::DEFAULT_PREFETCH_MAX_KEYS,
prefetch_max_bytes: crate::config::DEFAULT_PREFETCH_MAX_BYTES,
prefetch_deadline_secs: crate::config::DEFAULT_PREFETCH_DEADLINE_SECS,
min_store_compile_ms: crate::config::DEFAULT_MIN_STORE_COMPILE_MS,
gc_max_age_hours: crate::config::DEFAULT_GC_MAX_AGE_HOURS,
daemon_idle_timeout_secs: DEFAULT_DAEMON_IDLE_TIMEOUT_SECS,
s3_pool_idle_secs: DEFAULT_S3_POOL_IDLE_SECS,
remote_restore_timeout_secs: DEFAULT_REMOTE_RESTORE_TIMEOUT_SECS,
remote_negative_ttl_secs: DEFAULT_REMOTE_NEGATIVE_TTL_SECS,
}
}
fn test_state() -> AppState {
let config = test_config();
AppState {
tailer: EventTailer::new(config.event_log_path()),
config,
active_tab: Tab::Build,
events: Vec::new(),
live_heartbeats: std::collections::HashMap::new(),
build_scroll: Viewport::new(ScrollAnchor::Bottom),
build_filter: String::new(),
store_filter: String::new(),
why_filter: String::new(),
filter_active: false,
sort_mode: SortMode::Size,
store_scroll: Viewport::new(ScrollAnchor::Top),
stats_snapshot: StatsSnapshot::default(),
stats_loaded: false,
last_stats_fetch: Instant::now(),
project_scan: Arc::new(Mutex::new(ProjectScanData::default())),
last_project_refresh: Instant::now(),
project_scroll: Viewport::new(ScrollAnchor::Top),
selected_project: 0,
project_detail: false,
sessions: Vec::new(),
grouped_len: 0,
selected_session: None,
shown: None,
why_cache: None,
transfer_scroll: Viewport::new(ScrollAnchor::Top),
why_scroll: Viewport::new(ScrollAnchor::Top),
prev_bytes_uploaded: 0,
prev_bytes_downloaded: 0,
upload_speed_bps: 0.0,
download_speed_bps: 0.0,
rustc_version_slot: Arc::new(Mutex::new(None)),
stats_result_slot: Arc::new(Mutex::new(None)),
stats_fetch_in_flight: false,
stats_fetch_requested_entries: false,
should_quit: false,
paused: false,
spark_window: SPARK_WINDOW,
rustc_version: "test".to_string(),
wrapper_status: "test".to_string(),
}
}
#[test]
fn project_scan_status_distinguishes_loading_idle_and_unscanned() {
assert_eq!(project_scan_status(false, false, false), "calculating");
assert_eq!(project_scan_status(true, true, true), "calculating");
assert_eq!(project_scan_status(true, false, true), "idle");
assert_eq!(project_scan_status(true, false, false), "not scanned");
assert!(project_scan_can_start(false));
assert!(!project_scan_can_start(true));
}
#[test]
fn tui_uses_the_daemon_remote_status() {
let config = test_config();
let daemon_cache_dir =
std::path::absolute(std::env::temp_dir().join("kache-tui-daemon-a")).unwrap();
let snapshot = StatsSnapshot {
daemon_connected: true,
daemon_effective_config: Some(crate::daemon::EffectiveConfig {
max_size: config.max_size,
cache_dir: daemon_cache_dir.to_string_lossy().into_owned(),
runtime_dir: "/shared/runtime".to_string(),
config_path: "/daemon-a/config.toml".to_string(),
config_fingerprint: Some("daemon-a-fingerprint".to_string()),
prefetch_enabled: true,
remote_key_listing: false,
remote_description: Some("s3://daemon-a/cache".to_string()),
local_only: false,
remote_readonly: Some(false),
remote_error: None,
remote_key_cache_refresh_secs: 60,
socket_path: "/shared/daemon.sock".to_string(),
started_at_ms: 1,
auto_recover_min_free_bytes: None,
auto_recover_targets: None,
}),
..StatsSnapshot::default()
};
assert_eq!(
effective_remote_status(&config, &snapshot),
"s3://daemon-a/cache"
);
}
#[test]
fn project_scan_removes_stale_entries() {
let dir = tempfile::tempdir().unwrap();
let stats = Arc::new(Mutex::new(ProjectScanData {
project_targets: vec![cli::TargetEntry {
path: dir.path().join("removed-target"),
size: 1,
cached_bytes: 0,
estimated_reclaimable_bytes: 1,
scan_identity: None,
profiles: Vec::new(),
breakdown: cli::CategoryBreakdown::default(),
stale: false,
}],
..ProjectScanData::default()
}));
spawn_project_scan(Arc::clone(&stats), dir.path().to_path_buf())
.join()
.unwrap();
let scan = stats.lock().unwrap();
assert!(scan.scanned);
assert!(!scan.scanning);
assert!(scan.project_targets.is_empty());
}
#[test]
fn viewport_scroll_to_max_then_stop() {
let mut top = Viewport::new(ScrollAnchor::Top);
top.visible_range(10, 5);
assert_eq!(top.max_offset, 5);
for _ in 0..20 {
top.scroll_down();
}
assert_eq!(top.offset, 5);
top.scroll_up();
assert_eq!(top.offset, 4);
let mut bottom = Viewport::new(ScrollAnchor::Bottom);
bottom.visible_range(10, 5);
assert_eq!(bottom.max_offset, 5);
for _ in 0..20 {
bottom.scroll_up();
}
assert_eq!(bottom.offset, 5);
bottom.scroll_down();
assert_eq!(bottom.offset, 4);
}
#[test]
fn viewport_visible_range_shrink_and_expand() {
let mut v = Viewport::new(ScrollAnchor::Top);
v.visible_range(100, 10);
for _ in 0..50 {
v.scroll_down();
}
assert_eq!(v.offset, 50);
assert_eq!(v.max_offset, 90);
v.visible_range(20, 10);
assert_eq!(v.offset, 10);
assert_eq!(v.max_offset, 10);
v.visible_range(100, 10);
assert_eq!(v.offset, 10);
assert_eq!(v.max_offset, 90);
}
#[test]
fn handle_key_number_keys_switch_tabs() {
let mut s = test_state();
handle_key(&mut s, KeyCode::Char('1'));
assert_eq!(s.active_tab, Tab::Now);
handle_key(&mut s, KeyCode::Char('2'));
assert_eq!(s.active_tab, Tab::Build);
handle_key(&mut s, KeyCode::Char('3'));
assert_eq!(s.active_tab, Tab::Why);
handle_key(&mut s, KeyCode::Char('4'));
assert_eq!(s.active_tab, Tab::Projects);
handle_key(&mut s, KeyCode::Char('5'));
assert_eq!(s.active_tab, Tab::Store);
handle_key(&mut s, KeyCode::Char('6'));
assert_eq!(s.active_tab, Tab::Transfer);
}
#[test]
fn switching_to_a_fetching_tab_forces_an_immediate_refresh() {
let mut s = test_state();
let fresh = Instant::now();
s.last_project_refresh = fresh;
s.last_stats_fetch = fresh;
switch_tab(&mut s, Tab::Projects);
assert!(
s.last_project_refresh.elapsed() >= PROJECT_REFRESH_INTERVAL,
"Projects must be due for refresh on arrival"
);
switch_tab(&mut s, Tab::Store);
assert!(
s.last_stats_fetch.elapsed() >= SNAPSHOT_REFRESH_INTERVAL,
"Store must be due for a stats fetch on arrival"
);
let before_project = s.last_project_refresh;
let before_stats = s.last_stats_fetch;
switch_tab(&mut s, Tab::Transfer);
assert_eq!(s.last_project_refresh, before_project);
assert_eq!(s.last_stats_fetch, before_stats);
}
#[test]
fn handle_key_tab_cycles_forward_and_wraps() {
let mut s = test_state();
let order = [
Tab::Why,
Tab::Projects,
Tab::Store,
Tab::Transfer,
Tab::Now,
Tab::Build,
];
for expected in order {
handle_key(&mut s, KeyCode::Tab);
assert_eq!(s.active_tab, expected);
}
}
#[test]
fn handle_key_q_sets_should_quit() {
let mut s = test_state();
handle_key(&mut s, KeyCode::Char('q'));
assert!(s.should_quit);
}
#[test]
fn ctrl_c_quits_and_keeps_the_events() {
let mut s = test_state();
s.active_tab = Tab::Build;
s.events
.push(sample_build_event("serde", EventResult::Miss, 10, 1));
handle_key_event(
&mut s,
KeyEvent::new(KeyCode::Char('c'), KeyModifiers::CONTROL),
);
assert!(s.should_quit, "Ctrl+C must quit");
assert_eq!(s.events.len(), 1, "and must not clear the Build tab");
let mut s = test_state();
s.active_tab = Tab::Build;
s.events
.push(sample_build_event("serde", EventResult::Miss, 10, 1));
handle_key_event(
&mut s,
KeyEvent::new(KeyCode::Char('c'), KeyModifiers::NONE),
);
assert!(!s.should_quit);
assert!(s.events.is_empty());
}
#[test]
fn page_and_edge_keys_move_the_active_viewport() {
let mut s = test_state();
s.active_tab = Tab::Store;
s.store_scroll.visible_range(100, 10);
handle_key(&mut s, KeyCode::PageDown);
assert_eq!(s.store_scroll.offset, 10, "a page is what the panel showed");
handle_key(&mut s, KeyCode::End);
assert_eq!(s.store_scroll.offset, 90);
handle_key(&mut s, KeyCode::PageUp);
assert_eq!(s.store_scroll.offset, 80);
handle_key(&mut s, KeyCode::Home);
assert_eq!(s.store_scroll.offset, 0);
handle_key(&mut s, KeyCode::Char('j'));
assert_eq!(s.store_scroll.offset, 1);
handle_key(&mut s, KeyCode::Char('k'));
assert_eq!(s.store_scroll.offset, 0);
s.active_tab = Tab::Build;
s.build_scroll.visible_range(100, 10);
handle_key(&mut s, KeyCode::PageUp);
assert_eq!(s.build_scroll.offset, 10);
assert!(!s.build_scroll.at_anchor());
handle_key(&mut s, KeyCode::Home);
assert_eq!(s.build_scroll.offset, 90);
handle_key(&mut s, KeyCode::End);
assert_eq!(s.build_scroll.offset, 0);
assert!(s.build_scroll.at_anchor());
}
#[test]
fn arriving_rows_do_not_move_a_scrolled_reader() {
let mut s = test_state();
for i in 0..20 {
s.push_event(session_event(
&format!("c{i}"),
EventResult::Miss,
"/w",
"s1",
100,
));
}
s.refresh_sessions(chrono::Utc::now());
let range = s.build_scroll.visible_range(20, 5);
assert_eq!(range, 15..20, "following the newest");
s.build_scroll.scroll_up_by(10);
let range = s.build_scroll.visible_range(20, 5);
assert_eq!(range, 5..10);
for i in 20..23 {
s.push_event(session_event(
&format!("c{i}"),
EventResult::Miss,
"/w",
"s1",
50,
));
}
s.refresh_sessions(chrono::Utc::now());
let range = s.build_scroll.visible_range(23, 5);
assert_eq!(range, 5..10, "same rows after three arrivals");
s.push_event(session_event("x", EventResult::Miss, "/v", "s2", 400));
s.refresh_sessions(chrono::Utc::now());
assert_eq!(s.selected_session().unwrap().key, "id:s1");
let range = s.build_scroll.visible_range(23, 5);
assert_eq!(range, 5..10);
s.build_scroll.end();
s.push_event(session_event("c23", EventResult::Miss, "/w", "s1", 30));
s.refresh_sessions(chrono::Utc::now());
let range = s.build_scroll.visible_range(24, 5);
assert_eq!(range, 19..24);
s.build_scroll.scroll_up_by(10);
s.build_filter = "zzz".to_string();
s.push_event(session_event("c24", EventResult::Miss, "/w", "s1", 20));
s.refresh_sessions(chrono::Utc::now());
assert_eq!(s.build_scroll.offset, 10);
s.build_filter = "c2".to_string();
s.push_event(session_event("c25", EventResult::Miss, "/w", "s1", 10));
s.refresh_sessions(chrono::Utc::now());
assert_eq!(s.build_scroll.offset, 11, "a matching filter still counts");
}
#[test]
fn pause_toggles_and_is_announced() {
let mut s = test_state();
assert!(!s.paused);
handle_key(&mut s, KeyCode::Char('p'));
assert!(s.paused);
assert!(rendered_tab(&mut s, Tab::Build).contains("PAUSED"));
handle_key(&mut s, KeyCode::Char('p'));
assert!(!s.paused);
assert!(!rendered_tab(&mut s, Tab::Build).contains("PAUSED"));
}
#[test]
fn lookup_series_buckets_by_time_and_keeps_idle_time_flat() {
use chrono::Duration as ChronoDuration;
let now = chrono::Utc::now();
let window = Duration::from_secs(300);
let at = |secs_ago: i64, result: EventResult| {
let mut event = sample_build_event("x", result, 1, 1);
event.ts = now - ChronoDuration::seconds(secs_ago);
event
};
let events = vec![
at(10, EventResult::LocalHit),
at(10, EventResult::RemoteHit),
at(10, EventResult::Miss),
at(150, EventResult::Dup),
at(290, EventResult::PrefetchHit),
at(290, EventResult::Passthrough),
at(400, EventResult::LocalHit),
at(-5, EventResult::LocalHit),
];
let (hits, misses) = lookup_series(&events, now, window, 3);
assert_eq!(
hits,
vec![1, 0, 2],
"oldest bucket first; 400s ago and the future are out"
);
assert_eq!(
misses,
vec![0, 1, 1],
"dup counts as a compile, passthrough as neither"
);
assert_eq!(lookup_series(&events, now, window, 0), (vec![], vec![]));
}
#[test]
fn fmt_window_picks_the_largest_exact_unit() {
assert_eq!(fmt_window(Duration::from_secs(300)), "5m");
assert_eq!(fmt_window(Duration::from_secs(7200)), "2h");
assert_eq!(fmt_window(Duration::from_secs(86_400 * 7)), "7d");
assert_eq!(fmt_window(Duration::from_secs(90)), "90s");
}
#[test]
fn tab_bar_click_targets_match_the_drawn_labels() {
let titles = tab_titles();
for (tab, label, start) in titles {
assert_eq!(tab_at_column(start), Some(tab), "first column of {label:?}");
assert_eq!(
tab_at_column(start + label.len() as u16 - 1),
Some(tab),
"last column of {label:?}"
);
}
let (_, first, start) = titles[0];
assert_eq!(tab_at_column(start + first.len() as u16), None);
assert_eq!(tab_at_column(999), None);
let mut s = test_state();
let area = Rect::new(0, 0, 120, 40);
let (_, _, store_x) = titles
.into_iter()
.find(|(tab, _, _)| *tab == Tab::Store)
.unwrap();
handle_mouse(
&mut s,
MouseEvent {
kind: MouseEventKind::Down(MouseButton::Left),
column: store_x + 1,
row: 0,
modifiers: KeyModifiers::NONE,
},
area,
);
assert_eq!(s.active_tab, Tab::Store);
s.store_scroll.visible_range(100, 10);
handle_mouse(
&mut s,
MouseEvent {
kind: MouseEventKind::ScrollDown,
column: 40,
row: 20,
modifiers: KeyModifiers::NONE,
},
area,
);
assert_eq!(s.store_scroll.offset, 3);
}
#[test]
fn too_small_terminal_says_so_instead_of_drawing_garbage() {
use ratatui::Terminal;
use ratatui::backend::TestBackend;
let mut state = test_state();
let mut terminal = Terminal::new(TestBackend::new(40, 10)).unwrap();
terminal.draw(|frame| draw_ui(frame, &mut state)).unwrap();
let rendered: String = terminal
.backend()
.buffer()
.content()
.iter()
.map(|c| c.symbol())
.collect();
assert!(rendered.contains("40×10"), "{rendered}");
assert!(rendered.contains("60×16"), "{rendered}");
assert!(rendered.contains("kache stats"), "{rendered}");
assert!(
!rendered.contains("[1] Build"),
"no tab bar in the guard screen"
);
}
fn rendered_lines(state: &mut AppState, tab: Tab, width: u16, height: u16) -> Vec<String> {
use ratatui::Terminal;
use ratatui::backend::TestBackend;
state.active_tab = tab;
let mut terminal = Terminal::new(TestBackend::new(width, height)).unwrap();
terminal
.draw(|frame| draw_ui(frame, state))
.expect("draw should succeed");
let buffer = terminal.backend().buffer();
(0..height)
.map(|y| (0..width).map(|x| buffer[(x, y)].symbol()).collect())
.collect()
}
#[test]
fn packed_list_and_cancellation_have_distinct_transfer_labels() {
let mut state = populated_state();
let transfer = &mut state.stats_snapshot.recent_transfers[0];
transfer.direction = daemon::TransferDirection::Download;
transfer.ok = false;
transfer.outcome = "cancelled".to_owned();
transfer.accounting = Some(kache_core::timeline::PrefetchAccounting {
operation: kache_core::timeline::PrefetchOperation::List,
..Default::default()
});
let screen = rendered_lines(&mut state, Tab::Transfer, 120, 40).join("\n");
assert!(screen.contains("STOP"), "{screen}");
assert!(!screen.contains("FAIL"), "{screen}");
let row = screen.lines().find(|line| line.contains("serde")).unwrap();
assert!(
row.trim_start_matches('│').trim_start().starts_with("L "),
"LIST row: {row}"
);
}
#[test]
fn not_found_transfers_render_as_misses() {
let mut state = populated_state();
let transfer = &mut state.stats_snapshot.recent_transfers[0];
transfer.direction = daemon::TransferDirection::Download;
transfer.ok = false;
transfer.outcome = "not_found".to_string();
let screen = rendered_lines(&mut state, Tab::Transfer, 120, 40).join("\n");
assert!(screen.contains("MISS"), "{screen}");
assert!(!screen.contains("FAIL"), "{screen}");
state.stats_snapshot.recent_transfers[0].direction = daemon::TransferDirection::Upload;
let screen = rendered_lines(&mut state, Tab::Transfer, 120, 40).join("\n");
assert!(screen.contains("FAIL"), "{screen}");
state.stats_snapshot.recent_transfers[0].direction = daemon::TransferDirection::Download;
state.stats_snapshot.recent_transfers[0].outcome = "error".to_string();
let screen = rendered_lines(&mut state, Tab::Transfer, 120, 40).join("\n");
assert!(screen.contains("FAIL"), "{screen}");
state.stats_snapshot.recent_transfers[0].outcome = "skipped".to_string();
state.stats_snapshot.recent_transfers[0].request_count = 0;
let screen = rendered_lines(&mut state, Tab::Transfer, 120, 40).join("\n");
assert!(screen.contains("SKIP"), "{screen}");
assert!(!screen.contains("FAIL"), "{screen}");
}
fn populated_state() -> AppState {
let mut state = test_state();
state.events = vec![
sample_build_event("serde", EventResult::Miss, 4200, 2_000_000),
sample_build_event("tokio", EventResult::LocalHit, 30, 1_500_000),
sample_build_event("build.rs", EventResult::Passthrough, 80, 0),
];
state.stats_snapshot.entries = vec![
sample_stats_entry("serde", 2_000_000, 5),
sample_stats_entry("tokio", 1_500_000, 2),
];
state.stats_snapshot.entry_count = 2;
state.stats_snapshot.total_size = 3_500_000;
state.stats_loaded = true;
state.stats_snapshot.recent_transfers = vec![daemon::TransferEvent {
accounting: None,
prefetch: None,
outcome: String::new(),
schema: 3,
crate_name: "serde".to_string(),
direction: daemon::TransferDirection::Upload,
format: "tar.zst".to_string(),
cache_key: "serde-key".to_string(),
object_key: "prefix/serde".to_string(),
compressed_bytes: 1000,
started_at_unix_ms: 0,
finished_at_unix_ms: 0,
elapsed_ms: 12,
network_ms: 6,
semaphore_wait_ms: 0,
head_ms: 0,
request_ms: 2,
body_ms: 4,
request_count: 1,
original_bytes: 3000,
decompress_ms: 0,
extract_ms: 0,
disk_io_ms: 0,
import_lock_wait_ms: 0,
import_ms: 0,
compression_ms: 0,
head_checks_ms: 0,
blobs_skipped: 0,
blobs_total: 1,
ok: true,
timestamp: 0,
}];
{
let mut scan = state.project_scan.lock().unwrap();
scan.project_targets = vec![cli::TargetEntry {
path: std::path::PathBuf::from("/work/myproj/target"),
size: 5_000_000,
cached_bytes: 3_000_000,
estimated_reclaimable_bytes: 2_000_000,
scan_identity: None,
profiles: vec!["debug".to_string()],
breakdown: cli::CategoryBreakdown::default(),
stale: false,
}];
scan.scanned = true;
}
state.refresh_sessions(chrono::Utc::now());
state
}
fn session_event(
crate_name: &str,
result: EventResult,
root: &str,
session: &str,
secs_ago: i64,
) -> BuildEvent {
let mut event = sample_build_event(crate_name, result, 100, 1);
event.cache_key = format!("{crate_name}-{session}");
event.root = root.to_string();
event.session_id = session.to_string();
event.ts = chrono::Utc::now() - chrono::Duration::seconds(secs_ago);
event.compile_time_ms = 2_000;
event
}
#[test]
fn arrows_pick_a_build_and_enter_opens_why() {
let mut s = test_state();
s.events = vec![
session_event("old_a", EventResult::Miss, "/w/old", "s-old", 900),
session_event("old_b", EventResult::LocalHit, "/w/old", "s-old", 899),
session_event("new_a", EventResult::Miss, "/w/new", "s-new", 5),
];
s.refresh_sessions(chrono::Utc::now());
assert_eq!(s.sessions.len(), 2);
assert_eq!(s.sessions[0].key, "id:s-new", "newest on top");
assert!(s.selected_session.is_none(), "following by default");
assert_eq!(s.selected_session().unwrap().key, "id:s-new");
let now = rendered_tab(&mut s, Tab::Now);
assert!(now.contains("following the top build"), "{now}");
let screen = rendered_tab(&mut s, Tab::Build);
assert!(screen.contains("new_a"), "the event panel is the top build");
assert!(!screen.contains("old_a"), "other builds' events stay out");
handle_key(&mut s, KeyCode::Down);
assert_eq!(s.selected_session.as_deref(), Some("id:s-old"));
let screen = rendered_tab(&mut s, Tab::Build);
assert!(
screen.contains("old_a") && !screen.contains("new_a"),
"{screen}"
);
assert!(!screen.contains("following the top build"));
handle_key(&mut s, KeyCode::Up);
assert_eq!(s.selected_session.as_deref(), Some("id:s-new"));
handle_key(&mut s, KeyCode::Up);
assert!(s.selected_session.is_none());
handle_key(&mut s, KeyCode::Down);
handle_key(&mut s, KeyCode::Enter);
assert_eq!(s.active_tab, Tab::Why);
let screen = rendered_tab(&mut s, Tab::Why);
assert!(screen.contains("Why · old"), "{screen}");
s.active_tab = Tab::Build;
handle_key(&mut s, KeyCode::Char('c'));
assert!(s.sessions.is_empty() && s.selected_session.is_none());
}
#[test]
fn a_picked_build_stays_picked_when_rows_reorder() {
let mut s = test_state();
s.events = vec![
session_event("a", EventResult::Miss, "/w/one", "s1", 500),
session_event("b", EventResult::Miss, "/w/two", "s2", 400),
];
s.refresh_sessions(chrono::Utc::now());
assert_eq!(s.sessions[0].key, "id:s2");
handle_key(&mut s, KeyCode::Down);
assert_eq!(s.selected_session.as_deref(), Some("id:s1"));
s.push_event(session_event("c", EventResult::Miss, "/w/one", "s1", 1));
s.refresh_sessions(chrono::Utc::now());
assert_eq!(s.sessions[0].key, "id:s1");
assert_eq!(s.selected_session().unwrap().key, "id:s1");
assert_eq!(s.selected_index(), Some(0));
}
#[test]
fn why_tab_collapses_a_cascade_to_its_root() {
let externs = |mut e: BuildEvent, digest: &str| {
e.key_externs = [("leaf".to_string(), digest.to_string())]
.into_iter()
.collect();
e.key_externs_recorded = true;
e
};
let leaf = |mut e: BuildEvent, sources: &str| {
e.key_externs_recorded = true;
e.key_fields = [("sources".to_string(), sources.to_string())]
.into_iter()
.collect();
e
};
let mut events = vec![leaf(
session_event("leaf", EventResult::LocalHit, "/w", "before", 1000),
"1111",
)];
for i in 0..40 {
events.push(externs(
session_event(
&format!("app{i}"),
EventResult::LocalHit,
"/w",
"before",
999,
),
"aaaa",
));
}
let mut leaf_miss = leaf(
session_event("leaf", EventResult::Miss, "/w", "now", 10),
"2222",
);
leaf_miss.key_diff = vec!["sources".to_string()];
events.push(leaf_miss);
for i in 0..40 {
events.push(externs(
session_event(&format!("app{i}"), EventResult::Miss, "/w", "now", 9),
"bbbb",
));
}
let mut s = test_state();
s.events = events;
s.refresh_sessions(chrono::Utc::now());
assert_eq!(s.selected_session().unwrap().key, "id:now");
let lines: Vec<String> = why_lines(&mut s, 118)
.iter()
.map(|line| line.to_string())
.collect();
let text = lines.join("\n");
assert!(text.contains("41 misses"), "{text}");
let downstream = lines
.iter()
.find(|l| l.contains("downstream of leaf"))
.unwrap_or_else(|| panic!("{text}"));
assert!(downstream.contains("40"), "{downstream}");
assert!(downstream.contains("app0, app1, app2 +37"), "{downstream}");
assert!(
downstream.contains("1m20s"),
"40 x 2s of compile: {downstream}"
);
assert!(
lines
.iter()
.any(|l| l.contains("own inputs changed: sources")),
"{text}"
);
assert!(
!text.contains("explain_miss"),
"digests were recorded, so no hint to enable them: {text}"
);
assert!(text.contains("saved "), "cost strip: {text}");
let screen = rendered_tab(&mut s, Tab::Why);
assert!(screen.contains("downstream of leaf"), "{screen}");
let hidden = why_text(&mut s, 62).join("\n");
assert!(hidden.contains("downstream of leaf"), "{hidden}");
assert!(hidden.contains("1m20s"), "{hidden}");
assert!(!hidden.contains("app0"), "{hidden}");
let shown = why_text(&mut s, 63).join("\n");
assert!(shown.contains("app0"), "{shown}");
let narrower = why_text(&mut s, 55).join("\n");
assert!(narrower.contains("downstream of leaf"), "{narrower}");
assert!(!narrower.contains("app0"), "{narrower}");
}
#[test]
fn why_tab_says_when_it_cannot_explain_and_how_to_fix_that() {
let mut s = test_state();
s.events = vec![
session_event("x", EventResult::LocalHit, "/w", "before", 500),
session_event("x", EventResult::Miss, "/w", "now", 5),
session_event("y", EventResult::Miss, "/w", "now", 4),
];
s.refresh_sessions(chrono::Utc::now());
let text: Vec<String> = why_lines(&mut s, 100)
.iter()
.map(|line| line.to_string())
.collect();
let text = text.join("\n");
assert!(text.contains("unexplained"), "{text}");
assert!(
text.contains("no earlier compile in the loaded history"),
"{text}"
);
assert!(text.contains("explain_miss = true"), "{text}");
assert!(
text.contains("the 1 have no earlier compile to diff"),
"{text}"
);
assert!(
text.contains("cannot name the crate that changed"),
"{text}"
);
s.config.explain_miss = true;
s.why_cache = None;
let text: Vec<String> = why_lines(&mut s, 100)
.iter()
.map(|line| line.to_string())
.collect();
let text = text.join("\n");
assert!(
!text.contains("explain_miss = true"),
"already on, so no hint: {text}"
);
assert!(
!text.contains("cannot name the crate that changed"),
"{text}"
);
}
#[test]
fn why_tab_without_builds_says_so() {
let mut s = test_state();
let screen = rendered_tab(&mut s, Tab::Why);
assert!(screen.contains("No builds recorded yet"), "{screen}");
}
#[test]
fn share_bar_never_hides_a_present_cause() {
assert_eq!(share_bar(0, 10, 4), "░░░░");
assert_eq!(share_bar(1, 1000, 4), "█░░░");
assert_eq!(share_bar(10, 10, 4), "████");
assert_eq!(share_bar(3, 0, 2), " ");
assert_eq!(clip("abcdef", 4), "abc…");
assert_eq!(clip("abc", 4), "abc");
assert_eq!(fmt_saved_ms(0), "0s");
assert_eq!(fmt_saved_ms(750), "750ms");
assert_eq!(fmt_saved_ms(80_000), "1m20s");
}
#[test]
fn every_tab_fits_narrow_and_wide_terminals() {
for (tab, seeded, wide_only) in [
(Tab::Now, "(unknown root)", ""),
(Tab::Build, "serde", "Size"),
(Tab::Store, "serde", "Created"),
(Tab::Projects, "myproj", ""),
(Tab::Transfer, "serde", ""),
(Tab::Why, "build.rs", "direct"),
] {
for (width, height) in [(80u16, 24u16), (120, 40)] {
let mut state = populated_state();
let lines = rendered_lines(&mut state, tab, width, height);
let screen = lines.join("\n");
assert!(
screen.contains(seeded),
"{tab:?} at {width}x{height} must show {seeded:?}:\n{screen}"
);
assert!(
screen.contains("q: quit"),
"{tab:?} at {width}x{height} must keep its help bar:\n{screen}"
);
if !wide_only.is_empty() {
assert_eq!(
screen.contains(wide_only),
width >= 120,
"{tab:?} at {width}: column {wide_only:?} is wide-only:\n{screen}"
);
}
}
}
let mut state = populated_state();
let screen = rendered_lines(&mut state, Tab::Now, 120, 40).join("\n");
assert!(
screen.contains("(unknown root) ~"),
"an inferred build keeps the mark: {screen}"
);
}
#[test]
fn handle_key_filter_mode_captures_text() {
let mut s = test_state();
s.active_tab = Tab::Build;
handle_key(&mut s, KeyCode::Char('f'));
assert!(s.filter_active);
for c in "abc".chars() {
handle_key(&mut s, KeyCode::Char(c));
}
assert_eq!(s.filter(), "abc");
handle_key(&mut s, KeyCode::Backspace);
assert_eq!(s.filter(), "ab");
handle_key(&mut s, KeyCode::Enter);
assert!(!s.filter_active);
assert_eq!(s.filter(), "ab");
handle_key(&mut s, KeyCode::Char('q'));
assert!(s.should_quit);
}
#[test]
fn esc_clears_the_filter_instead_of_quitting() {
let mut s = test_state();
s.active_tab = Tab::Build;
handle_key(&mut s, KeyCode::Char('f'));
for c in "serde".chars() {
handle_key(&mut s, KeyCode::Char(c));
}
handle_key(&mut s, KeyCode::Esc);
assert!(!s.filter_active);
assert_eq!(s.filter(), "", "Esc cancels rather than committing");
assert!(!s.should_quit, "Esc must never quit");
handle_key(&mut s, KeyCode::Char('f'));
for c in "tokio".chars() {
handle_key(&mut s, KeyCode::Char(c));
}
handle_key(&mut s, KeyCode::Enter);
assert_eq!(s.filter(), "tokio");
handle_key(&mut s, KeyCode::Esc);
assert_eq!(s.filter(), "");
assert!(!s.should_quit);
handle_key(&mut s, KeyCode::Esc);
assert!(!s.should_quit);
}
#[test]
fn filters_do_not_leak_between_tabs() {
let mut s = test_state();
s.active_tab = Tab::Build;
handle_key(&mut s, KeyCode::Char('f'));
for c in "serde".chars() {
handle_key(&mut s, KeyCode::Char(c));
}
handle_key(&mut s, KeyCode::Enter);
s.active_tab = Tab::Store;
assert_eq!(s.filter(), "", "Store keeps its own filter");
s.active_tab = Tab::Why;
assert_eq!(s.filter(), "", "Passthrough keeps its own filter");
s.active_tab = Tab::Projects;
assert_eq!(s.filter(), "");
handle_key(&mut s, KeyCode::Char('f'));
assert!(!s.filter_active, "`f` is inert where nothing is filterable");
s.active_tab = Tab::Build;
assert_eq!(
s.filter(),
"serde",
"Build's filter survived the round trip"
);
}
#[test]
fn shift_tab_walks_backwards() {
let mut s = test_state();
s.active_tab = Tab::Build;
handle_key(&mut s, KeyCode::Tab);
assert_eq!(s.active_tab, Tab::Why);
handle_key(&mut s, KeyCode::BackTab);
assert_eq!(s.active_tab, Tab::Build);
handle_key(&mut s, KeyCode::BackTab);
assert_eq!(s.active_tab, Tab::Now);
handle_key(&mut s, KeyCode::BackTab);
assert_eq!(s.active_tab, Tab::Transfer);
handle_key(&mut s, KeyCode::Tab);
assert_eq!(s.active_tab, Tab::Now);
}
fn rendered_tab(state: &mut AppState, tab: Tab) -> String {
use ratatui::Terminal;
use ratatui::backend::TestBackend;
state.active_tab = tab;
let mut terminal = Terminal::new(TestBackend::new(120, 40)).unwrap();
terminal
.draw(|frame| draw_ui(frame, state))
.expect("draw should succeed");
terminal
.backend()
.buffer()
.content()
.iter()
.map(|c| c.symbol())
.collect()
}
#[test]
fn every_tab_renders_its_help_keys() {
for (tab, expected) in [
(Tab::Build, "c: clear"),
(Tab::Store, "s: sort"),
(Tab::Why, "f: filter"),
] {
let mut state = test_state();
let rendered = rendered_tab(&mut state, tab);
assert!(
rendered.contains("q: quit"),
"tab {tab:?} must show how to quit"
);
assert!(
rendered.contains(expected),
"tab {tab:?} must show its own key {expected:?}"
);
}
}
#[test]
fn help_bar_offers_the_way_out_of_a_filter() {
let mut state = test_state();
assert!(
!rendered_tab(&mut state, Tab::Build).contains("Esc: clear filter"),
"no filter, no clear hint"
);
state.active_tab = Tab::Build;
handle_key(&mut state, KeyCode::Char('f'));
for c in "serde".chars() {
handle_key(&mut state, KeyCode::Char(c));
}
handle_key(&mut state, KeyCode::Enter);
let rendered = rendered_tab(&mut state, Tab::Build);
assert!(
rendered.contains("Esc: clear filter"),
"a committed filter must advertise how to clear it"
);
assert!(
rendered.contains("[filter: serde]"),
"and the title must name it"
);
}
#[test]
fn committed_filter_is_announced_in_title_and_help() {
let mut s = test_state();
s.active_tab = Tab::Build;
assert_eq!(filtered_title(" Live Build ", s.filter()), " Live Build ");
assert!(
!help_line(&s, "q: quit").contains("Esc"),
"nothing to clear, so no clear hint"
);
handle_key(&mut s, KeyCode::Char('f'));
for c in "serde".chars() {
handle_key(&mut s, KeyCode::Char(c));
}
handle_key(&mut s, KeyCode::Enter);
assert_eq!(
filtered_title(" Live Build ", s.filter()),
" Live Build [filter: serde] "
);
let help = help_line(&s, "q: quit s: sort");
assert!(
help.contains("s: sort"),
"tab keys survive a filter: {help}"
);
assert!(
help.contains("Esc: clear"),
"and it says how to clear: {help}"
);
}
#[test]
fn handle_key_scroll_is_per_tab() {
let mut s = test_state();
s.active_tab = Tab::Store;
s.store_scroll.max_offset = 10;
handle_key(&mut s, KeyCode::Down);
handle_key(&mut s, KeyCode::Down);
assert_eq!(s.store_scroll.offset, 2);
handle_key(&mut s, KeyCode::Up);
assert_eq!(s.store_scroll.offset, 1);
s.active_tab = Tab::Transfer;
s.transfer_scroll.max_offset = 10;
handle_key(&mut s, KeyCode::Down);
assert_eq!(s.transfer_scroll.offset, 1);
assert_eq!(s.store_scroll.offset, 1, "store offset is untouched");
}
#[test]
fn handle_key_clear_resets_build_offset() {
let mut s = test_state();
s.active_tab = Tab::Build;
s.build_scroll.max_offset = 10;
handle_key(&mut s, KeyCode::PageUp);
assert_eq!(s.build_scroll.offset, 1);
handle_key(&mut s, KeyCode::Char('c'));
assert!(s.events.is_empty());
assert_eq!(s.build_scroll.offset, 0);
}
#[test]
fn handle_key_store_sort_cycles_and_resets_offset() {
let mut s = test_state();
s.active_tab = Tab::Store;
s.store_scroll.max_offset = 5;
assert_eq!(s.sort_mode.label(), "size");
for _ in 0..3 {
handle_key(&mut s, KeyCode::Down);
}
assert_eq!(s.store_scroll.offset, 3);
handle_key(&mut s, KeyCode::Char('s'));
assert_eq!(s.sort_mode.label(), "hits");
assert_eq!(s.store_scroll.offset, 0);
}
#[test]
fn handle_key_filter_input_resets_affected_viewports() {
let mut s = test_state();
s.active_tab = Tab::Store;
s.store_scroll.max_offset = 5;
s.store_scroll.offset = 3;
handle_key(&mut s, KeyCode::Char('f'));
handle_key(&mut s, KeyCode::Char('s'));
assert_eq!(s.store_scroll.offset, 0);
s.store_scroll.offset = 3;
handle_key(&mut s, KeyCode::Backspace);
assert_eq!(s.store_scroll.offset, 0);
}
#[test]
fn draw_ui_renders_every_tab_without_panicking() {
use ratatui::Terminal;
use ratatui::backend::TestBackend;
for tab in [
Tab::Now,
Tab::Build,
Tab::Projects,
Tab::Store,
Tab::Transfer,
Tab::Why,
] {
let mut state = test_state();
state.active_tab = tab;
let mut terminal = Terminal::new(TestBackend::new(120, 40)).unwrap();
terminal
.draw(|frame| draw_ui(frame, &mut state))
.expect("draw should succeed");
let buffer = terminal.backend().buffer().clone();
let rendered: String = buffer.content().iter().map(|c| c.symbol()).collect();
assert!(
rendered.trim().chars().any(|c| !c.is_whitespace()),
"tab {tab:?} should render visible content"
);
}
}
#[test]
fn load_history_keeps_recent_builds_once_and_tails_from_there() {
let dir = tempfile::tempdir().unwrap();
let log = dir.path().join("events.jsonl");
let cutoff = chrono::Utc::now() - chrono::Duration::hours(1);
let mut old = sample_build_event("old", EventResult::Miss, 1, 1);
old.ts = cutoff - chrono::Duration::seconds(1);
let mut edge = sample_build_event("edge", EventResult::LocalHit, 1, 1);
edge.ts = cutoff;
let recent = sample_build_event("recent", EventResult::LocalHit, 1, 1);
for event in [&old, &edge, &recent] {
events::log_event(&log, event).unwrap();
}
let beat = HeartbeatEvent {
schema: 1,
event: "heartbeat".to_string(),
ts: chrono::Utc::now(),
eta_s: None,
crate_name: "gone".to_string(),
root: "/w".to_string(),
pid: 1,
elapsed_s: 1,
typical_s: None,
};
events::log_heartbeat(&log, &beat).unwrap();
let (mut tailer, history) = load_history(log.clone(), cutoff);
let names: Vec<&str> = history.iter().map(|e| e.crate_name.as_str()).collect();
assert_eq!(names, ["edge", "recent"]);
assert!(tailer.poll().unwrap().is_empty());
let later = sample_build_event("later", EventResult::Miss, 1, 1);
events::log_event(&log, &later).unwrap();
let tailed = tailer.poll().unwrap();
assert_eq!(tailed.len(), 1);
assert_eq!(tailed[0].crate_name, "later");
}
#[test]
fn monitor_history_defaults_to_one_hour() {
assert_eq!(MONITOR_HISTORY.secs(), 3600);
assert_eq!(SinceWindow::whole_hours(3).secs(), 3 * 3600);
}
fn sample_build_event(
crate_name: &str,
result: events::EventResult,
elapsed_ms: u64,
size: u64,
) -> events::BuildEvent {
events::BuildEvent {
ts: chrono::Utc::now(),
session_id: String::new(),
demands: Vec::new(),
rebuilt_package: None,
rebuilt_fingerprint: None,
rebuilt_path: None,
rebuilt_paths: Vec::new(),
crate_name: crate_name.to_string(),
version: "0.1.0".to_string(),
result,
elapsed_ms,
compile_time_ms: elapsed_ms,
size,
cache_key: "0123456789abcdef".to_string(),
schema: 8,
key_ms: 0,
key_hash_hits: 0,
key_hash_misses: 0,
key_hash_bytes: 0,
lookup_ms: 0,
restore_ms: 0,
store_ms: 0,
startup_ms: 0,
dep_info_ms: 0,
dep_info_runs: 0,
prediction_mismatches: 0,
flight_wait_ms: 0,
permit_wait_ms: 0,
store_output_blobs: 0,
store_duplicate_blobs: 0,
store_new_blobs: 0,
compiler_runs: 0,
preprocessor_runs: 0,
probe_runs: 0,
reflinked_bytes: 0,
hardlinked_bytes: 0,
copied_bytes: 0,
store_reflinked_bytes: 0,
store_hardlinked_bytes: 0,
store_copied_bytes: 0,
store_copy_cross_device_bytes: 0,
store_copy_permission_bytes: 0,
store_copy_ineligible_bytes: 0,
store_copy_other_bytes: 0,
restore_copy_cross_device_bytes: 0,
restore_copy_permission_bytes: 0,
restore_copy_exclusive_bytes: 0,
restore_copy_other_bytes: 0,
root: String::new(),
passthrough_reason: "linker invocation".to_string(),
skip_reason: String::new(),
store_error: String::new(),
store_handed_off: false,
daemon_store_ms: 0,
lookup_rejection: String::new(),
verify_compare: String::new(),
fallback: false,
fallback_attempt: None,
exit_code: Some(0),
key_fields: Default::default(),
key_diff: Vec::new(),
key_externs: Default::default(),
key_env_deps: None,
key_externs_recorded: false,
unit_id: String::new(),
extern_units: Default::default(),
miss_reason: events::MissReason::None,
object_output: String::new(),
package: String::new(),
}
}
fn sample_stats_entry(crate_name: &str, size: u64, hits: u64) -> daemon::StatsEntry {
daemon::StatsEntry {
store_dirs: Vec::new(),
cache_key: "0123456789abcdef".to_string(),
crate_name: crate_name.to_string(),
crate_type: "lib".to_string(),
profile: "debug".to_string(),
size,
hit_count: hits,
created_at: "2025-01-01 00:00:00".to_string(),
last_accessed: "2025-01-01 00:00:00".to_string(),
content_hash: None,
}
}
#[test]
fn store_names_the_daemon_and_now_leaves_the_version_off() {
let mut state = test_state();
state.config.remote = Some(crate::config::RemoteConfig::test_s3("b", "p"));
state.stats_loaded = true;
let snap = &mut state.stats_snapshot;
snap.daemon_connected = true;
snap.daemon_version = "9.9.9".to_string();
snap.daemon_build_epoch = crate::daemon::build_epoch().wrapping_add(1);
snap.max_size = 10_000_000;
snap.total_size = 4_000_000;
snap.event_stats.local_hits = 7;
snap.event_stats.prefetch_hits = 1;
snap.event_stats.remote_hits = 2;
snap.event_stats.dups = 1;
snap.event_stats.misses = 3;
snap.event_stats.total_elapsed_ms = 5000;
snap.event_stats.miss_elapsed_ms = 3000;
snap.daemon_effective_config = Some(crate::daemon::EffectiveConfig {
max_size: snap.max_size,
cache_dir: "/cache".to_string(),
runtime_dir: "/runtime".to_string(),
config_path: "/daemon/config.toml".to_string(),
config_fingerprint: None,
prefetch_enabled: false,
remote_key_listing: false,
remote_description: None,
local_only: false,
remote_readonly: Some(false),
remote_error: None,
remote_key_cache_refresh_secs: 60,
socket_path: "/daemon.sock".to_string(),
started_at_ms: 1,
auto_recover_min_free_bytes: None,
auto_recover_targets: None,
});
let now = rendered_tab(&mut state, Tab::Now);
assert!(now.contains("71%"), "{now}");
assert!(now.contains("60% of the clock was a miss"), "{now}");
assert!(now.contains("no compile time recorded yet"), "{now}");
assert!(now.contains("remote is off"), "{now}");
assert!(!now.contains("9.9.9"), "version stays on Store: {now}");
assert!(!now.contains("stats are still loading"), "{now}");
state.stats_snapshot.event_stats.hit_compile_time_ms = 50;
state.stats_snapshot.event_stats.miss_compile_time_ms = 50;
let now = rendered_tab(&mut state, Tab::Now);
assert!(
now.contains("50% of compile time came back from the cache"),
"{now}"
);
state.stats_snapshot.event_stats.total_elapsed_ms = 0;
state.stats_snapshot.event_stats.miss_elapsed_ms = 0;
let now = rendered_tab(&mut state, Tab::Now);
assert!(now.contains("no lookup time recorded yet"), "{now}");
let store = rendered_tab(&mut state, Tab::Store);
assert!(store.contains("9.9.9"), "{store}");
assert!(!store.contains("upgrade available"), "{store}");
state.stats_snapshot.daemon_version = "0.0.1".into();
let store = rendered_tab(&mut state, Tab::Store);
assert!(store.contains("upgrade available"), "{store}");
state.stats_snapshot.daemon_version = "9.9.9".into();
state.stats_snapshot.daemon_build_epoch = crate::daemon::build_epoch();
let store = rendered_tab(&mut state, Tab::Store);
assert!(store.contains("daemon v9.9.9"), "{store}");
assert!(!store.contains("restart pending"), "{store}");
state.stats_snapshot.daemon_connected = false;
let store = rendered_tab(&mut state, Tab::Store);
assert!(store.contains("daemon offline"), "{store}");
state.stats_loaded = false;
let now = rendered_tab(&mut state, Tab::Now);
assert!(now.contains("stats are still loading"), "{now}");
assert!(now.contains('\u{2026}'), "{now}");
let store = rendered_tab(&mut state, Tab::Store);
assert!(store.contains("daemon: checking"), "{store}");
}
#[test]
fn draw_ui_renders_populated_tabs_without_panicking() {
use events::EventResult;
use ratatui::Terminal;
use ratatui::backend::TestBackend;
for tab in [Tab::Build, Tab::Store, Tab::Why, Tab::Transfer] {
let mut state = test_state();
state.active_tab = tab;
state.events = vec![
sample_build_event("serde", EventResult::Miss, 4200, 2_000_000),
sample_build_event("tokio", EventResult::LocalHit, 30, 1_500_000),
sample_build_event("build.rs", EventResult::Passthrough, 80, 0),
];
state.stats_snapshot.entries = vec![
sample_stats_entry("serde", 2_000_000, 5),
sample_stats_entry("tokio", 1_500_000, 2),
];
state.stats_snapshot.entry_count = 2;
state.stats_snapshot.total_size = 3_500_000;
state.stats_loaded = true;
state.refresh_sessions(chrono::Utc::now());
state.stats_snapshot.uploads_completed = 3;
state.upload_speed_bps = 2_500_000.0;
state.download_speed_bps = 800_000.0;
let mut terminal = Terminal::new(TestBackend::new(120, 40)).unwrap();
terminal
.draw(|frame| draw_ui(frame, &mut state))
.expect("populated draw should succeed");
let buffer = terminal.backend().buffer().clone();
let rendered: String = buffer.content().iter().map(|c| c.symbol()).collect();
if matches!(tab, Tab::Build | Tab::Store | Tab::Why) {
assert!(
rendered.contains("serde")
|| rendered.contains("tokio")
|| rendered.contains("build.rs"),
"tab {tab:?} should render seeded data"
);
}
}
}
#[test]
fn draw_projects_tab_renders_populated_scan() {
use ratatui::Terminal;
use ratatui::backend::TestBackend;
let mut state = test_state();
state.active_tab = Tab::Projects;
{
let mut scan = state.project_scan.lock().unwrap();
scan.project_targets = vec![
cli::TargetEntry {
path: std::path::PathBuf::from("/work/myproj/target"),
size: 5_000_000,
cached_bytes: 3_000_000,
estimated_reclaimable_bytes: 2_000_000,
scan_identity: None,
profiles: vec!["debug".to_string(), "release".to_string()],
breakdown: cli::CategoryBreakdown {
incremental: 1_000,
deps_local: 2_000_000,
..cli::CategoryBreakdown::default()
},
stale: false,
},
cli::TargetEntry {
path: std::path::PathBuf::from("/work/other/target"),
size: 100,
cached_bytes: 0,
estimated_reclaimable_bytes: 100,
scan_identity: None,
profiles: Vec::new(),
breakdown: cli::CategoryBreakdown::default(),
stale: true,
},
];
scan.scanning = true;
scan.scanned = true;
}
let mut terminal = Terminal::new(TestBackend::new(120, 40)).unwrap();
terminal
.draw(|frame| draw_ui(frame, &mut state))
.expect("projects draw should succeed");
let buffer = terminal.backend().buffer().clone();
let rendered: String = buffer.content().iter().map(|c| c.symbol()).collect();
assert!(rendered.contains("in 2 trees"), "{rendered}");
assert!(rendered.contains("already cached"), "{rendered}");
assert!(rendered.contains("myproj"), "{rendered}");
assert!(rendered.contains("of this tree is cached"), "{rendered}");
assert!(rendered.contains("incremental is"), "{rendered}");
assert!(rendered.contains('█'), "deps get a bar: {rendered}");
assert!(rendered.contains("~ other"), "{rendered}");
assert!(rendered.contains("nothing cached"), "{rendered}");
assert!(rendered.contains("scanning"), "{rendered}");
assert!(
!rendered.contains("[debug]"),
"profiles wait for Enter: {rendered}"
);
}
#[test]
fn tab_titles_advance_by_label_width_plus_gutter() {
let titles = tab_titles();
assert_eq!(titles[0].2, 0);
for pair in titles.windows(2) {
let (_, label, start) = pair[0];
assert_eq!(pair[1].2, start + label.len() as u16 + 2, "{label:?}");
}
}
#[test]
fn ingest_respects_pause_and_settles_heartbeats() {
let dir = tempfile::tempdir().unwrap();
let log = dir.path().join("events.jsonl");
let mut s = test_state();
s.tailer = EventTailer::from_start(log.clone());
let beat = HeartbeatEvent {
schema: 1,
event: "heartbeat".to_string(),
ts: chrono::Utc::now(),
eta_s: Some(60),
crate_name: "gkrust".to_string(),
root: "/w".to_string(),
pid: 4242,
elapsed_s: 30,
typical_s: Some(90),
};
events::log_heartbeat(&log, &beat).unwrap();
s.paused = true;
s.ingest_tailed_records();
assert!(s.live_heartbeats.is_empty(), "paused: nothing is read");
assert!(s.in_flight_view().is_empty());
s.paused = false;
s.ingest_tailed_records();
assert_eq!(s.live_heartbeats.len(), 1, "resumed: the beat was replayed");
assert_eq!(s.in_flight_view()[0].crate_name, "gkrust");
let elsewhere = HeartbeatEvent {
pid: 4343,
root: "/other".to_string(),
..beat.clone()
};
events::log_heartbeat(&log, &elsewhere).unwrap();
s.ingest_tailed_records();
assert_eq!(s.live_heartbeats.len(), 2);
let mut done = sample_build_event("gkrust", EventResult::Miss, 100, 1);
done.root = "/w".to_string();
events::log_event(&log, &done).unwrap();
s.ingest_tailed_records();
assert_eq!(s.events.len(), 1);
assert_eq!(
s.live_heartbeats.len(),
1,
"completion ends this tree's in-flight row only"
);
assert_eq!(s.live_heartbeats[&4343].1.root, "/other");
}
#[test]
fn background_work_is_due_only_when_unpaused_idle_and_on_schedule() {
let mut s = test_state();
s.last_stats_fetch = Instant::now() - SNAPSHOT_REFRESH_INTERVAL;
s.last_project_refresh = Instant::now() - PROJECT_REFRESH_INTERVAL;
s.active_tab = Tab::Projects;
assert!(s.stats_fetch_due());
assert!(s.project_scan_due());
s.paused = true;
assert!(!s.stats_fetch_due(), "paused starts nothing");
assert!(!s.project_scan_due());
s.paused = false;
s.stats_fetch_in_flight = true;
assert!(!s.stats_fetch_due(), "one fetch at a time");
s.stats_fetch_in_flight = false;
s.last_stats_fetch = Instant::now();
assert!(!s.stats_fetch_due(), "not before the interval");
s.active_tab = Tab::Build;
assert!(
!s.project_scan_due(),
"scans only while Projects is showing"
);
s.active_tab = Tab::Projects;
s.last_project_refresh = Instant::now();
assert!(!s.project_scan_due());
}
#[test]
fn terminal_events_dispatch_presses_and_mouse_only() {
let area = Rect::new(0, 0, 120, 40);
let press = |kind| KeyEvent::new_with_kind(KeyCode::Char('q'), KeyModifiers::NONE, kind);
let mut s = test_state();
handle_terminal_event(&mut s, Event::Key(press(KeyEventKind::Release)), area);
assert!(!s.should_quit, "a release is not a keystroke");
handle_terminal_event(&mut s, Event::Key(press(KeyEventKind::Repeat)), area);
assert!(!s.should_quit);
handle_terminal_event(&mut s, Event::Resize(80, 24), area);
assert!(!s.should_quit);
handle_terminal_event(&mut s, Event::Key(press(KeyEventKind::Press)), area);
assert!(s.should_quit);
let mut s = test_state();
let (_, _, store_x) = tab_titles()
.into_iter()
.find(|(tab, _, _)| *tab == Tab::Store)
.unwrap();
handle_terminal_event(
&mut s,
Event::Mouse(MouseEvent {
kind: MouseEventKind::Down(MouseButton::Left),
column: store_x,
row: 0,
modifiers: KeyModifiers::NONE,
}),
area,
);
assert_eq!(s.active_tab, Tab::Store, "mouse events reach the handler");
}
#[test]
fn mouse_clicks_off_the_tab_row_do_nothing_and_the_wheel_goes_both_ways() {
let area = Rect::new(0, 0, 120, 40);
let mut s = test_state();
let (_, _, store_x) = tab_titles()[2];
handle_mouse(
&mut s,
MouseEvent {
kind: MouseEventKind::Down(MouseButton::Left),
column: store_x,
row: 5,
modifiers: KeyModifiers::NONE,
},
area,
);
assert_eq!(
s.active_tab,
Tab::Build,
"a click in the body is not a tab click"
);
s.active_tab = Tab::Store;
s.store_scroll.visible_range(100, 10);
let wheel = |kind| MouseEvent {
kind,
column: 40,
row: 20,
modifiers: KeyModifiers::NONE,
};
handle_mouse(&mut s, wheel(MouseEventKind::ScrollDown), area);
handle_mouse(&mut s, wheel(MouseEventKind::ScrollDown), area);
assert_eq!(s.store_scroll.offset, 6);
handle_mouse(&mut s, wheel(MouseEventKind::ScrollUp), area);
assert_eq!(s.store_scroll.offset, 3);
s.active_tab = Tab::Build;
handle_mouse(
&mut s,
MouseEvent {
kind: MouseEventKind::Down(MouseButton::Left),
column: store_x,
row: 0,
modifiers: KeyModifiers::NONE,
},
Rect::new(0, 0, 40, 10),
);
assert_eq!(s.active_tab, Tab::Build);
}
#[test]
fn terminal_size_guard_is_exact_on_both_axes() {
assert!(!terminal_too_small(Rect::new(0, 0, MIN_WIDTH, MIN_HEIGHT)));
assert!(terminal_too_small(Rect::new(
0,
0,
MIN_WIDTH - 1,
MIN_HEIGHT
)));
assert!(terminal_too_small(Rect::new(
0,
0,
MIN_WIDTH,
MIN_HEIGHT - 1
)));
assert!(terminal_too_small(Rect::new(
0,
0,
MIN_WIDTH - 1,
MIN_HEIGHT - 1
)));
assert!(!terminal_too_small(Rect::new(0, 0, 200, 60)));
}
#[test]
fn lookup_series_counts_an_event_from_this_instant_in_the_newest_column() {
let now = chrono::Utc::now();
let mut event = sample_build_event("x", EventResult::LocalHit, 1, 1);
event.ts = now;
let (hits, misses) = lookup_series(&[event], now, Duration::from_secs(300), 4);
assert_eq!(hits, vec![0, 0, 0, 1]);
assert_eq!(misses, vec![0; 4]);
}
#[test]
fn fmt_window_falls_back_when_the_unit_does_not_divide() {
assert_eq!(fmt_window(Duration::from_secs(45_000)), "750m");
assert_eq!(fmt_window(Duration::from_secs(129_600)), "36h");
assert_eq!(fmt_window(SPARK_WINDOW), "5m");
}
fn rendered_sparkline(state: &AppState, width: u16, height: u16) -> String {
use ratatui::Terminal;
use ratatui::backend::TestBackend;
let mut terminal = Terminal::new(TestBackend::new(width, height)).unwrap();
terminal
.draw(|frame| draw_sparkline(frame, state, frame.area()))
.unwrap();
let buffer = terminal.backend().buffer();
(0..height)
.map(|y| {
(0..width)
.map(|x| buffer[(x, y)].symbol())
.collect::<String>()
})
.collect::<Vec<_>>()
.join("\n")
}
#[test]
fn sparkline_labels_its_axis_and_says_when_nothing_happened() {
let mut s = test_state();
let screen = rendered_sparkline(&s, 80, 5);
assert!(screen.contains("Lookups · last 5m"), "{screen}");
assert!(screen.contains("5m ago"), "{screen}");
assert!(screen.contains("now →"), "{screen}");
assert!(
screen.contains("hit 0") && screen.contains("miss 0"),
"{screen}"
);
assert!(screen.contains("no lookups in the last 5m"), "{screen}");
s.events = vec![sample_build_event("a", EventResult::Miss, 1, 1)];
let screen = rendered_sparkline(&s, 80, 5);
assert!(!screen.contains("no lookups"), "{screen}");
assert!(screen.contains("miss 1"), "{screen}");
s.events
.push(sample_build_event("b", EventResult::LocalHit, 1, 1));
let screen = rendered_sparkline(&s, 80, 5);
assert!(!screen.contains("no lookups"), "{screen}");
assert!(screen.contains("hit 1"), "{screen}");
let screen = rendered_tab(&mut s, Tab::Build);
assert!(screen.contains("Lookups · last 5m"), "{screen}");
}
#[test]
fn sparkline_needs_three_rows_and_twelve_columns_inside_the_frame() {
let s = test_state();
for (width, height, drawn) in [
(14u16, 5u16, true),
(13, 5, false),
(14, 4, false),
(13, 4, false),
(80, 5, true),
] {
let screen = rendered_sparkline(&s, width, height);
assert_eq!(
screen.contains("hit"),
drawn,
"{width}x{height} should draw={drawn}:\n{screen}"
);
}
}
#[test]
fn mid_priority_columns_have_their_own_thresholds() {
let mut state = populated_state();
for (tab, column, min_width) in [(Tab::Build, "Compile", 78u16), (Tab::Store, "Profile", 84)] {
for width in [min_width - 1, min_width, 120] {
let screen = rendered_lines(&mut state, tab, width, 40).join("\n");
assert_eq!(
screen.contains(column),
width >= min_width,
"{tab:?} at {width}: {column:?}\n{screen}"
);
}
}
let screen = rendered_lines(&mut state, Tab::Projects, 120, 40).join("\n");
assert!(screen.contains("in 1 tree"), "{screen}");
assert!(!screen.contains("trees"), "{screen}");
assert!(
!screen.contains("[debug]"),
"profiles wait for Enter: {screen}"
);
assert!(!screen.contains("Fprint"), "{screen}");
state.active_tab = Tab::Projects;
handle_key(&mut state, KeyCode::Enter);
let screen = rendered_lines(&mut state, Tab::Projects, 80, 40).join("\n");
assert!(screen.contains("[debug]"), "{screen}");
assert!(
!screen.contains("fingerprints"),
"a zero category stays off: {screen}"
);
}
#[test]
fn changing_build_resets_both_panels_and_a_lone_build_can_be_pinned() {
let mut s = test_state();
s.push_event(session_event("a", EventResult::Miss, "/w/one", "s1", 500));
s.refresh_sessions(chrono::Utc::now());
assert!(s.selected_session.is_none());
handle_key(&mut s, KeyCode::Down);
assert_eq!(s.selected_session.as_deref(), Some("id:s1"));
s.build_scroll.visible_range(50, 10);
s.build_scroll.scroll_up_by(5);
s.why_scroll.visible_range(50, 10);
s.why_scroll.scroll_down_by(7);
s.push_event(session_event("b", EventResult::Miss, "/w/two", "s2", 1));
s.refresh_sessions(chrono::Utc::now());
assert_eq!(s.sessions[0].key, "id:s2");
assert_eq!(s.selected_session().unwrap().key, "id:s1");
assert_eq!((s.build_scroll.offset, s.why_scroll.offset), (5, 7));
handle_key(&mut s, KeyCode::Up);
assert_eq!(s.selected_session().unwrap().key, "id:s2");
assert_eq!((s.build_scroll.offset, s.why_scroll.offset), (0, 0));
handle_key(&mut s, KeyCode::Up);
assert!(s.selected_session.is_none());
s.build_scroll.visible_range(50, 10);
s.build_scroll.scroll_up_by(3);
s.push_event(session_event("c", EventResult::Miss, "/w/three", "s3", 0));
s.refresh_sessions(chrono::Utc::now());
assert_eq!(s.selected_session().unwrap().key, "id:s3");
assert_eq!(s.build_scroll.offset, 0);
}
fn why_text(s: &mut AppState, width: u16) -> Vec<String> {
why_lines(s, width)
.iter()
.map(|line| line.to_string())
.collect()
}
#[test]
fn build_tab_layout_thresholds() {
let mut s = test_state();
let screen = rendered_tab(&mut s, Tab::Build);
assert!(!screen.contains("Builds ·"), "{screen}");
assert!(!screen.contains("In flight"), "{screen}");
let mut s = populated_state();
let tall = rendered_lines(&mut s, Tab::Build, 100, 31).join("\n");
assert!(tall.contains("Lookups · last"), "{tall}");
let short = rendered_lines(&mut s, Tab::Build, 100, 30).join("\n");
assert!(!short.contains("Lookups · last"), "{short}");
assert!(short.contains("serde"), "the event panel stays: {short}");
assert!(
rendered_lines(&mut s, Tab::Build, 100, 31)
.join("\n")
.contains("serde"),
"and on the tall screen too"
);
let wide = rendered_lines(&mut s, Tab::Build, 100, 40).join("\n");
assert!(
wide.contains("Enter: why") && wide.contains("PgUp PgDn End"),
"{wide}"
);
let narrow = rendered_lines(&mut s, Tab::Build, 99, 40).join("\n");
assert!(
narrow.contains("⏎: why") && !narrow.contains("Enter: why"),
"{narrow}"
);
}
#[test]
fn why_tab_marks_its_scroll_position_only_when_there_is_more() {
let mut s = test_state();
s.events = vec![session_event("a", EventResult::Miss, "/w", "s1", 5)];
s.refresh_sessions(chrono::Utc::now());
let screen = rendered_tab(&mut s, Tab::Why);
assert!(!screen.contains('–'), "everything fits: {screen}");
for i in 0..30 {
let mut e = session_event(&format!("cc{i}"), EventResult::Passthrough, "/w", "s1", 4);
e.passthrough_reason = "unsupported|flag".to_string();
s.push_event(e);
}
s.refresh_sessions(chrono::Utc::now());
let screen = rendered_lines(&mut s, Tab::Why, 100, 20).join("\n");
assert!(screen.contains("1–16 of"), "{screen}");
assert!(screen.contains("PgUp PgDn"), "{screen}");
s.active_tab = Tab::Why;
handle_key(&mut s, KeyCode::End);
let screen = rendered_lines(&mut s, Tab::Why, 100, 20).join("\n");
let total = why_lines(&mut s, 98).len();
assert!(
screen.contains(&format!("{total} of {total}")),
"scrolled to the end: {screen}"
);
}
#[test]
fn why_cost_strip_mentions_copies_only_when_there_are_any() {
let mut s = test_state();
let mut hit = session_event("a", EventResult::LocalHit, "/w", "s1", 5);
hit.reflinked_bytes = 1000;
s.events = vec![hit];
s.refresh_sessions(chrono::Utc::now());
let text = why_text(&mut s, 120).join("\n");
assert!(text.contains("restored 1000 B"), "{text}");
assert!(!text.contains("by copy"), "{text}");
assert!(text.contains("none: every lookup hit"), "{text}");
assert!(!text.contains("explain_miss"), "no misses, no hint: {text}");
let mut hit = session_event("b", EventResult::LocalHit, "/w", "s1", 4);
hit.copied_bytes = 1000;
s.push_event(hit);
s.refresh_sessions(chrono::Utc::now());
let text = why_text(&mut s, 120).join("\n");
assert!(text.contains("50% by copy"), "{text}");
let mut s = test_state();
let mut pt = session_event("cc", EventResult::Passthrough, "/w", "s2", 3);
pt.passthrough_reason = "unsupported|flag".to_string();
s.events = vec![pt];
s.refresh_sessions(chrono::Utc::now());
let text = why_text(&mut s, 120).join("\n");
assert!(text.contains("none: nothing was looked up"), "{text}");
assert!(text.contains("no lookups"), "{text}");
}
#[test]
fn why_counts_misses_in_english_and_says_when_capped() {
let mut s = test_state();
s.events = vec![session_event("a", EventResult::Miss, "/w", "s1", 5)];
s.refresh_sessions(chrono::Utc::now());
let text = why_text(&mut s, 120).join("\n");
assert!(text.contains("Misses by cause · 1 miss\n"), "{text}");
assert!(!text.contains("Misses by cause · 1 misses"), "{text}");
assert!(!text.contains("analyzed)"), "{text}");
assert!(!text.contains("older, not analyzed"), "{text}");
assert!(!text.contains("none:"), "there is a miss: {text}");
assert!(
!text.contains("Passthroughs by reason"),
"none to group: {text}"
);
assert!(!text.contains("Chronic misses"), "none: {text}");
assert!(
!text.contains("Passthroughs in this build"),
"an empty list stays off: {text}"
);
for i in 0..(tui_sessions::MAX_ANALYZED_MISSES + 9) {
s.push_event(session_event(
&format!("m{i}"),
EventResult::Miss,
"/w",
"s1",
4,
));
}
s.refresh_sessions(chrono::Utc::now());
let text = why_text(&mut s, 120).join("\n");
assert!(
text.contains(&format!(
"{} misses (newest {} of {} analyzed)",
tui_sessions::MAX_ANALYZED_MISSES + 10,
tui_sessions::MAX_ANALYZED_MISSES,
tui_sessions::MAX_ANALYZED_MISSES + 10
)),
"{text}"
);
assert!(text.contains("older, not analyzed"), "{text}");
}
fn fit_state() -> AppState {
let mut s = test_state();
let long =
"unsupported|cc flag -march=native -mtune=native -fno-omit-frame-pointer -Wl,--as-needed";
for i in 0..3 {
let mut e = session_event(
&format!("a_rather_long_crate_name_{i}"),
EventResult::Passthrough,
"/w",
"s1",
5,
);
e.passthrough_reason = long.to_string();
s.push_event(e);
}
let mut probe = session_event("rustc", EventResult::Passthrough, "/w", "s1", 4);
probe.passthrough_reason =
"not-a-compile|--print cfg with a long tail of arguments, and then some more".to_string();
s.push_event(probe);
s.push_event(session_event("m", EventResult::Miss, "/w", "s1", 3));
s.refresh_sessions(chrono::Utc::now());
s
}
#[test]
fn why_lines_fit_the_width_they_are_given() {
let mut s = fit_state();
for width in [50u16, 60, 78, 100] {
for line in why_lines(&mut s, width) {
let text = line.to_string();
let laid_out = text.contains('░')
|| text.contains('█')
|| text.trim_start().starts_with(|c: char| c.is_ascii_digit());
if laid_out {
assert!(
line.width() <= width as usize,
"{width}: {} cells: {text:?}",
line.width()
);
}
}
}
let at_60 = why_text(&mut s, 60).join("\n");
assert!(
at_60.contains(" a_rather_long_… unsupport… cc flag"),
"{at_60}"
);
let at_88 = why_text(&mut s, 88).join("\n");
assert!(
at_88.contains(" a_rather_long_crate_n… unsupported cc flag"),
"{at_88}"
);
let group_line = why_lines(&mut s, 100)
.iter()
.map(|line| line.to_string())
.find(|line| line.contains("(queries, not compiles)"))
.unwrap();
assert_eq!(
Line::from(group_line.as_str()).width(),
99,
"{group_line:?}"
);
let text = why_text(&mut s, 100).join("\n");
assert!(text.contains("(queries, not compiles)"), "{text}");
assert!(text.contains("not-a-compile: --print cfg with"), "{text}");
s.why_filter = "rustc".to_string();
s.active_tab = Tab::Why;
let text = why_text(&mut s, 100).join("\n");
assert!(
text.contains("Passthroughs in this build · 1 matching \"rustc\""),
"{text}"
);
assert!(
text.contains(" rustc") && !text.contains("a_rather_long"),
"{text}"
);
s.why_filter = "-march".to_string();
let text = why_text(&mut s, 100).join("\n");
assert!(text.contains("· 3 matching"), "reason matches too: {text}");
s.why_filter = "zzz".to_string();
let text = why_text(&mut s, 100).join("\n");
assert!(text.contains("none match the filter"), "{text}");
}
#[test]
fn enter_opens_why_from_now_and_build() {
let mut s = test_state();
s.active_tab = Tab::Store;
handle_key(&mut s, KeyCode::Enter);
assert_eq!(s.active_tab, Tab::Store);
s.active_tab = Tab::Now;
handle_key(&mut s, KeyCode::Enter);
assert_eq!(s.active_tab, Tab::Why);
s.active_tab = Tab::Build;
handle_key(&mut s, KeyCode::Enter);
assert_eq!(s.active_tab, Tab::Why);
s.active_tab = Tab::Projects;
assert!(!s.project_detail);
handle_key(&mut s, KeyCode::Enter);
assert!(s.project_detail);
assert_eq!(s.active_tab, Tab::Projects);
handle_key(&mut s, KeyCode::Enter);
assert!(!s.project_detail);
}
#[test]
fn fmt_saved_ms_switches_units_at_one_second() {
assert_eq!(fmt_saved_ms(999), "999ms");
assert_eq!(fmt_saved_ms(1000), "1s");
}
#[test]
fn in_flight_compile_keeps_its_build_running() {
let mut s = test_state();
s.events = vec![session_event("a", EventResult::Miss, "/w", "s1", 300)];
s.refresh_sessions(chrono::Utc::now());
assert_eq!(s.sessions[0].state, SessionState::Finished);
let beat = HeartbeatEvent {
schema: 1,
event: "heartbeat".to_string(),
ts: chrono::Utc::now(),
crate_name: "b".to_string(),
root: "/w".to_string(),
pid: 7,
elapsed_s: 5,
typical_s: None,
eta_s: None,
};
s.live_heartbeats.insert(7, (Instant::now(), beat));
s.refresh_sessions(chrono::Utc::now());
assert_eq!(s.sessions[0].state, SessionState::Live);
let screen = rendered_tab(&mut s, Tab::Build);
assert!(screen.contains("running"), "{screen}");
assert!(screen.contains("5s elapsed"), "{screen}");
}
fn rendered_buffer(state: &mut AppState, tab: Tab, width: u16, height: u16) -> Buffer {
use ratatui::Terminal;
use ratatui::backend::TestBackend;
state.active_tab = tab;
let mut terminal = Terminal::new(TestBackend::new(width, height)).unwrap();
terminal
.draw(|frame| draw_ui(frame, state))
.expect("draw should succeed");
terminal.backend().buffer().clone()
}
fn row_text(buffer: &Buffer, y: u16) -> String {
(0..buffer.area.width)
.map(|x| buffer[(x, y)].symbol())
.collect()
}
#[test]
fn now_list_layout_and_styling() {
let mut s = test_state();
s.events = vec![
session_event("a", EventResult::Miss, "/w/one", "s1", 5),
session_event("b", EventResult::LocalHit, "/w/two", "s2", 400),
];
s.refresh_sessions(chrono::Utc::now());
let buffer = rendered_buffer(&mut s, Tab::Now, 100, 40);
let rows: Vec<String> = (0..40).map(|y| row_text(&buffer, y)).collect();
let header_y = rows
.iter()
.position(|row| row.contains("Build") && row.contains("Misses") && row.contains("Saved"))
.unwrap_or_else(|| panic!("{}", rows.join("\n")));
let selected_y = header_y + 1;
assert!(rows[selected_y].contains("▸"), "{}", rows[selected_y]);
assert!(rows[selected_y].contains("one"), "{}", rows[selected_y]);
let mark = rows[selected_y].find('▸').unwrap() as u16;
let style = &buffer[(mark, selected_y as u16)];
assert!(style.modifier.contains(Modifier::REVERSED), "{style:?}");
assert!(style.modifier.contains(Modifier::BOLD), "{style:?}");
let other_y = selected_y + 1;
assert!(rows[other_y].contains("two"), "{}", rows[other_y]);
let mut saw_zero = false;
let mut saw_saved = false;
for x in 0..buffer.area.width {
let cell = &buffer[(x, other_y as u16)];
if cell.symbol() == "0" {
assert_eq!(cell.fg, Color::DarkGray, "misses at {x}: {cell:?}");
saw_zero = true;
}
if cell.symbol() == "2" {
assert_eq!(cell.fg, Color::Green, "saved at {x}: {cell:?}");
saw_saved = true;
}
}
assert!(saw_zero && saw_saved, "{}", rows[other_y]);
let mut saw_miss = false;
for x in 0..buffer.area.width {
let cell = &buffer[(x, selected_y as u16)];
if cell.symbol() == "1" {
assert_eq!(cell.fg, Color::White, "misses at {x}: {cell:?}");
saw_miss = true;
}
}
assert!(saw_miss, "{}", rows[selected_y]);
let narrow = rendered_lines(&mut s, Tab::Now, 70, 40);
let header = narrow
.iter()
.find(|row| row.contains("Misses"))
.unwrap_or_else(|| panic!("{}", narrow.join("\n")));
assert!(header.contains("Saved"), "{header:?}");
let mut s = test_state();
s.events = vec![session_event("a", EventResult::Miss, "/w/one", "s1", 5)];
s.refresh_sessions(chrono::Utc::now());
let screen = rendered_tab(&mut s, Tab::Now);
assert!(screen.contains("one"), "{screen}");
assert!(screen.contains("following the top build"), "{screen}");
}
#[test]
fn share_pct_rounds_to_the_nearest_percent() {
assert_eq!(share_pct(0, 0), " \u{2014}");
assert_eq!(share_pct(2, 3), " 67%");
assert_eq!(share_pct(147, 788), " 19%");
assert_eq!(share_pct(1, 1), "100%");
}
#[test]
fn now_copy_uses_the_rates_the_daemon_already_has() {
assert_eq!(weighted_sentence(None), "no compile time recorded yet");
assert_eq!(
weighted_sentence(Some(50.0)),
"50% of compile time came back from the cache"
);
assert_eq!(miss_clock_sentence(None), "no lookup time recorded yet");
assert_eq!(
miss_clock_sentence(Some(78.0)),
"78% of the clock was a miss"
);
assert_eq!(remote_sentence("not configured"), "remote is off");
assert_eq!(remote_sentence("local-only"), "remote is off");
assert_eq!(remote_sentence("misconfigured"), "remote: misconfigured");
assert_eq!(figure_saved(0), "0s");
assert_eq!(figure_saved(3_600_000), "1.0h");
let label = |result| {
let mut s = test_state();
s.events = vec![session_event("c", result, "/w", "s", 1)];
s.refresh_sessions(chrono::Utc::now());
misses_label(&s.sessions[0].tally)
};
assert_eq!(label(EventResult::Passthrough), "\u{2014}");
assert_eq!(label(EventResult::LocalHit), "0");
assert_eq!(label(EventResult::Miss), "1");
}
#[test]
fn tree_name_uses_the_project_above_target() {
assert_eq!(
tree_name(std::path::Path::new("/work/myproj/target")),
"myproj"
);
assert_eq!(tree_name(std::path::Path::new("target")), "target");
assert_eq!(
tree_name(std::path::Path::new("/work/myproj/debug")),
"debug"
);
}
#[test]
fn now_pages_the_build_list_and_the_wheel_moves_three() {
let mut s = test_state();
for i in 0..8 {
s.push_event(session_event(
"a",
EventResult::Miss,
&format!("/w/n{i}"),
&format!("s{i}"),
100 - i,
));
}
s.refresh_sessions(chrono::Utc::now());
let screen = rendered_tab(&mut s, Tab::Now);
assert!(screen.contains("following the top build"), "{screen}");
assert!(screen.contains("3 more, older"), "{screen}");
assert!(screen.contains("n7"), "{screen}");
assert!(!screen.contains("n0"), "{screen}");
assert!(!screen.contains("newer"), "{screen}");
s.active_tab = Tab::Now;
handle_key(&mut s, KeyCode::Char('j'));
assert_eq!(s.selected_session.as_deref(), Some("id:s6"));
let pinned = rendered_tab(&mut s, Tab::Now);
assert!(!pinned.contains("following"), "{pinned}");
handle_key(&mut s, KeyCode::Char('k'));
assert_eq!(s.selected_session.as_deref(), Some("id:s7"));
handle_key(&mut s, KeyCode::Char('k'));
assert!(s.selected_session.is_none());
handle_key(&mut s, KeyCode::PageDown);
assert_eq!(s.selected_session.as_deref(), Some("id:s2"));
handle_key(&mut s, KeyCode::Home);
assert!(s.selected_session.is_none());
handle_key(&mut s, KeyCode::End);
assert_eq!(s.selected_session.as_deref(), Some("id:s0"));
handle_key(&mut s, KeyCode::PageUp);
assert_eq!(s.selected_session.as_deref(), Some("id:s5"));
handle_key(&mut s, KeyCode::End);
assert_eq!(s.selected_session.as_deref(), Some("id:s0"));
let screen = rendered_tab(&mut s, Tab::Now);
assert!(screen.contains("3 newer"), "{screen}");
assert!(screen.contains("n0"), "{screen}");
assert!(!screen.contains("n7"), "{screen}");
assert!(!screen.contains("more, older"), "{screen}");
handle_key(&mut s, KeyCode::Home);
let area = Rect::new(0, 0, 120, 40);
let wheel = |kind| MouseEvent {
kind,
column: 10,
row: 10,
modifiers: KeyModifiers::NONE,
};
handle_mouse(&mut s, wheel(MouseEventKind::ScrollDown), area);
assert_eq!(s.selected_session.as_deref(), Some("id:s4"));
handle_mouse(&mut s, wheel(MouseEventKind::ScrollUp), area);
assert_eq!(s.selected_session.as_deref(), Some("id:s7"));
handle_mouse(&mut s, wheel(MouseEventKind::ScrollUp), area);
assert!(s.selected_session.is_none());
}
fn project_tree(path: &str, size: u64) -> cli::TargetEntry {
cli::TargetEntry {
path: std::path::PathBuf::from(path),
size,
cached_bytes: 0,
estimated_reclaimable_bytes: 0,
scan_identity: None,
profiles: Vec::new(),
breakdown: cli::CategoryBreakdown::default(),
stale: false,
}
}
#[test]
fn project_keys_move_between_trees() {
let mut s = test_state();
s.active_tab = Tab::Projects;
s.selected_project = 4;
handle_key(&mut s, KeyCode::Down);
assert_eq!(s.selected_project, 0);
{
let mut scan = s.project_scan.lock().unwrap();
scan.project_targets = vec![
project_tree("/work/one/target", 10),
project_tree("/work/two/target", 20),
];
}
handle_key(&mut s, KeyCode::Up);
assert_eq!(s.selected_project, 0);
handle_key(&mut s, KeyCode::Char('j'));
assert_eq!(s.selected_project, 1);
handle_key(&mut s, KeyCode::Down);
assert_eq!(s.selected_project, 1);
handle_key(&mut s, KeyCode::Char('k'));
assert_eq!(s.selected_project, 0);
s.selected_project = 100;
s.step_project(-1);
assert_eq!(s.selected_project, 0);
}
fn quiet_passthrough(name: &str, secs_ago: i64) -> BuildEvent {
session_event(
name,
EventResult::Passthrough,
"/w/quiet",
"quiet",
secs_ago,
)
}
#[test]
fn unnamed_passthroughs_collapse_and_point_at_the_build_that_missed() {
let mut s = test_state();
s.push_event(quiet_passthrough("", 5));
s.push_event(quiet_passthrough("unknown", 4));
s.push_event(session_event(
"a",
EventResult::Miss,
"/w/busy",
"busy",
400,
));
s.refresh_sessions(chrono::Utc::now());
let screen = rendered_tab(&mut s, Tab::Build);
assert!(screen.contains("passed through"), "{screen}");
assert!(
screen.contains("the crate name was not recorded"),
"{screen}"
);
assert!(
screen.contains("busy is the build with 1 miss and nothing saved"),
"{screen}"
);
assert!(!screen.contains("Status"), "no event table: {screen}");
let mut s = test_state();
s.push_event(quiet_passthrough("unknown", 5));
s.push_event(session_event(
"a",
EventResult::Miss,
"/w/warm",
"warm",
800,
));
s.push_event(session_event(
"b",
EventResult::Miss,
"/w/warm",
"warm",
799,
));
s.push_event(session_event(
"h",
EventResult::LocalHit,
"/w/warm",
"warm",
798,
));
s.refresh_sessions(chrono::Utc::now());
let screen = rendered_tab(&mut s, Tab::Build);
assert!(
screen.contains("warm is the build with 2 misses and 2s saved"),
"{screen}"
);
s.build_filter = "nope".to_string();
let screen = rendered_tab(&mut s, Tab::Build);
assert!(!screen.contains("passed through"), "{screen}");
}
#[test]
fn a_named_or_missed_event_keeps_the_build_table() {
let mut s = test_state();
s.push_event(quiet_passthrough("unknown", 5));
s.push_event(session_event(
"build.rs",
EventResult::Passthrough,
"/w/quiet",
"quiet",
4,
));
s.refresh_sessions(chrono::Utc::now());
let screen = rendered_tab(&mut s, Tab::Build);
assert!(screen.contains("build.rs"), "{screen}");
assert!(!screen.contains("passed through"), "{screen}");
let mut s = test_state();
s.push_event(quiet_passthrough("", 5));
s.push_event(session_event(
"leaf",
EventResult::Miss,
"/w/quiet",
"quiet",
4,
));
s.refresh_sessions(chrono::Utc::now());
let screen = rendered_tab(&mut s, Tab::Build);
assert!(screen.contains("leaf"), "{screen}");
assert!(
!screen.contains("the crate name was not recorded"),
"{screen}"
);
}
#[test]
fn a_cold_miss_with_no_history_does_not_mention_explain_miss() {
let mut s = test_state();
s.events = vec![session_event("leaf", EventResult::Miss, "/w/app", "s1", 5)];
s.refresh_sessions(chrono::Utc::now());
let text = why_text(&mut s, 100).join("\n");
assert!(text.contains("cold 0 of 1 hit"), "{text}");
assert!(
text.contains("no earlier compile in the loaded history"),
"{text}"
);
assert!(
text.contains("the 1 have no earlier compile to diff"),
"{text}"
);
assert!(!text.contains("explain_miss"), "{text}");
assert!(!text.contains("Passthroughs in this build"), "{text}");
}
fn event_stats(
local_hits: usize,
prefetch_hits: usize,
remote_hits: usize,
dups: usize,
misses: usize,
) -> daemon::EventStatsResponse {
daemon::EventStatsResponse {
local_hits,
prefetch_hits,
remote_hits,
dups,
misses,
errors: 0,
total_elapsed_ms: 0,
hit_elapsed_ms: 0,
miss_elapsed_ms: 0,
hit_compile_time_ms: 0,
miss_compile_time_ms: 0,
store_output_blobs: 0,
store_duplicate_blobs: 0,
store_new_blobs: 0,
}
}
#[test]
fn count_hit_figure_is_blank_only_when_every_lookup_counter_is_zero() {
assert_eq!(count_hit_figure(&event_stats(0, 0, 0, 0, 0)), "\u{2014}");
assert_eq!(count_hit_figure(&event_stats(2, 0, 0, 0, 0)), "100%");
assert_eq!(count_hit_figure(&event_stats(0, 3, 0, 0, 0)), "100%");
assert_eq!(count_hit_figure(&event_stats(0, 0, 3, 0, 0)), "100%");
assert_eq!(count_hit_figure(&event_stats(0, 0, 0, 3, 0)), "0%");
assert_eq!(count_hit_figure(&event_stats(3, 3, 0, 0, 0)), "100%");
assert_eq!(count_hit_figure(&event_stats(3, 0, 3, 0, 0)), "100%");
assert_eq!(count_hit_figure(&event_stats(3, 0, 0, 3, 0)), "50%");
assert_eq!(count_hit_figure(&event_stats(3, 0, 0, 0, 3)), "50%");
}
#[test]
fn now_window_keeps_the_selection_away_from_the_edges() {
assert_eq!(now_window_start(10, 5), 3);
assert_eq!(now_window_start(4, 3), 0);
}
#[test]
fn now_title_follows_only_while_a_build_is_on_screen() {
let mut s = test_state();
let screen = rendered_tab(&mut s, Tab::Now);
assert!(!screen.contains("following"), "{screen}");
}
#[test]
fn a_hit_only_build_is_not_the_one_with_misses() {
let mut s = test_state();
s.push_event(quiet_passthrough("", 5));
s.push_event(session_event(
"a",
EventResult::LocalHit,
"/w/warm",
"warm",
400,
));
s.refresh_sessions(chrono::Utc::now());
let screen = rendered_tab(&mut s, Tab::Build);
assert!(screen.contains("passed through"), "{screen}");
assert!(!screen.contains("is the build with"), "{screen}");
}
#[test]
fn project_scroll_offset_keeps_the_selection_on_screen() {
assert_eq!(project_scroll_offset(10, 2, 5), 2);
assert_eq!(project_scroll_offset(5, 6, 10), 5);
assert_eq!(project_scroll_offset(0, 5, 5), 1);
assert_eq!(project_scroll_offset(2, 8, 5), 4);
assert_eq!(project_scroll_offset(0, 2, 10), 0);
}
#[test]
fn project_selection_clamps_only_past_the_end() {
let mut s = test_state();
{
let mut scan = s.project_scan.lock().unwrap();
scan.project_targets = vec![
project_tree("/work/one/target", 10),
project_tree("/work/two/target", 20),
];
}
s.selected_project = 0;
rendered_tab(&mut s, Tab::Projects);
assert_eq!(s.selected_project, 0);
s.selected_project = 99;
rendered_tab(&mut s, Tab::Projects);
assert_eq!(s.selected_project, 1);
}
#[test]
fn project_lines_mark_and_anchor_the_selected_tree() {
let s = test_state();
{
let mut scan = s.project_scan.lock().unwrap();
scan.project_targets = vec![
project_tree("/work/alpha/target", 10),
project_tree("/work/beta/target", 20),
];
}
let (lines, anchor) = project_lines(&s, 80);
let text: Vec<String> = lines.iter().map(|line| line.to_string()).collect();
let marked = text
.iter()
.position(|line| line.contains('▸'))
.unwrap_or_else(|| panic!("{}", text.join("\n")));
assert_eq!(anchor, marked);
assert!(text[marked].contains("alpha"), "{}", text[marked]);
assert!(!text[marked].contains("beta"), "{}", text[marked]);
}
#[test]
fn project_row_reserves_the_size_and_clips_the_name() {
let s = test_state();
{
let mut scan = s.project_scan.lock().unwrap();
scan.project_targets = vec![project_tree("/work/abcdefghijklmnopqrstuvwxyz/target", 10)];
}
let (lines, _) = project_lines(&s, 30);
let row = lines
.iter()
.map(|line| line.to_string())
.find(|line| line.contains('▸'))
.unwrap();
assert_eq!(row, "▸ abcdefghijklmnopqrstuv… 10 B");
}
#[test]
fn a_zero_deps_bar_stays_empty() {
let s = test_state();
{
let mut scan = s.project_scan.lock().unwrap();
scan.project_targets = vec![project_tree("/work/tiny/target", 4)];
}
let (lines, _) = project_lines(&s, 40);
let deps = lines
.iter()
.map(|line| line.to_string())
.find(|line| line.contains("deps"))
.unwrap();
assert!(deps.contains('░'), "{deps}");
assert!(!deps.contains('█'), "{deps}");
}
#[test]
fn tree_detail_lists_only_categories_that_hold_bytes() {
let mut tree = project_tree("/work/tiny/target", 100);
tree.breakdown.incremental = 40;
let text = tree_detail_lines(&tree).join("\n");
assert!(text.contains("incremental"), "{text}");
assert!(!text.contains("fingerprints"), "{text}");
}