diff options
| author | Paul Buetow <paul@buetow.org> | 2026-05-13 14:28:37 +0300 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2026-05-13 14:28:37 +0300 |
| commit | 27b94f917064948fa33141309a3f08deb40ffde2 (patch) | |
| tree | 0f1c63eba01da1cc89fbbedcfe71cdcb55b06cb0 /internal/tui/probes | |
| parent | 140d6c0fe472f112170022b9831dfe700698f382 (diff) | |
improve unit test coverage to >=60% in probes, common, export, streamrow, pidpicker, tui/export
Before: probes=30%, tui/common=41%, export=0%, streamrow=25%, pidpicker=59%, tui/export=45%
After: probes=89%, tui/common=97%, export=77%, streamrow=100%, pidpicker=73%, tui/export=99%
New test files cover RingBuffer push/wrap/reset, Row accessor methods, nil
Sequencer safety, SnapshotCSV nil and data paths, helper functions snapValue /
snapValueF / trendSummary, all table navigation keys, VisibleTableWindow/
ClampTableCol edge cases, RenderTableHeader/Row, PickerShortHelp, probe modal
navigation/search/toggle/view/error paths, truncateText/sanitizeOneLine,
export modal View rendering, key navigation, status messages, scanAllThreadsFrom,
readThreadInfo guards, formatProcess variants, and clamp helper.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Diffstat (limited to 'internal/tui/probes')
| -rw-r--r-- | internal/tui/probes/probes_extra_test.go | 355 |
1 files changed, 355 insertions, 0 deletions
diff --git a/internal/tui/probes/probes_extra_test.go b/internal/tui/probes/probes_extra_test.go new file mode 100644 index 0000000..c7a7067 --- /dev/null +++ b/internal/tui/probes/probes_extra_test.go @@ -0,0 +1,355 @@ +package probes + +import ( + "strings" + "testing" + + "ior/internal/probemanager" + + tea "charm.land/bubbletea/v2" +) + +// TestVisibleReturnsFalseBeforeOpen verifies that a freshly constructed model +// is not visible. +func TestVisibleReturnsFalseBeforeOpen(t *testing.T) { + m := NewModel(nil) + if m.Visible() { + t.Fatal("expected model to be invisible after NewModel") + } +} + +// TestSetDarkModeDoesNotPanic verifies that SetDarkMode can be called without +// panicking regardless of mode. +func TestSetDarkModeDoesNotPanic(t *testing.T) { + m := NewModel(nil) + m = m.SetDarkMode(false) + m = m.SetDarkMode(true) + _ = m +} + +// TestNavigationKeysMoveCursor verifies that j/k and down/up keys move the +// cursor within the filtered list. +func TestNavigationKeysMoveCursor(t *testing.T) { + fm := &fakeManager{ + states: []probemanager.ProbeState{ + {Syscall: "read", Active: true}, + {Syscall: "write", Active: true}, + {Syscall: "openat", Active: true}, + }, + } + m := NewModel(fm).Open() + + // Move down twice. + m, _ = m.Update(tea.KeyPressMsg{Code: []rune{'j'}[0], Text: "j"}) + m, _ = m.Update(tea.KeyPressMsg{Code: []rune{'j'}[0], Text: "j"}) + if m.cursor != 2 { + t.Fatalf("expected cursor=2 after two j presses, got %d", m.cursor) + } + + // Move up once. + m, _ = m.Update(tea.KeyPressMsg{Code: []rune{'k'}[0], Text: "k"}) + if m.cursor != 1 { + t.Fatalf("expected cursor=1 after k press, got %d", m.cursor) + } + + // down/up aliases. + m, _ = m.Update(tea.KeyPressMsg{Code: tea.KeyDown}) + if m.cursor != 2 { + t.Fatalf("expected cursor=2 after down press, got %d", m.cursor) + } + m, _ = m.Update(tea.KeyPressMsg{Code: tea.KeyUp}) + if m.cursor != 1 { + t.Fatalf("expected cursor=1 after up press, got %d", m.cursor) + } +} + +// TestEscClosesModal verifies that pressing esc returns an invisible model. +func TestEscClosesModal(t *testing.T) { + fm := &fakeManager{ + states: []probemanager.ProbeState{{Syscall: "read", Active: true}}, + } + m := NewModel(fm).Open() + m, _ = m.Update(tea.KeyPressMsg{Code: tea.KeyEsc}) + if m.Visible() { + t.Fatal("expected modal closed after esc") + } +} + +// TestUpdateIgnoredWhenNotVisible verifies that key events are no-ops when the +// model is closed. +func TestUpdateIgnoredWhenNotVisible(t *testing.T) { + fm := &fakeManager{ + states: []probemanager.ProbeState{{Syscall: "read", Active: true}}, + } + m := NewModel(fm) // not opened + next, cmd := m.Update(tea.KeyPressMsg{Code: []rune{'j'}[0], Text: "j"}) + if cmd != nil { + t.Fatal("expected no command when model is closed") + } + if next.cursor != 0 { + t.Fatalf("cursor moved while closed, got %d", next.cursor) + } +} + +// TestSearchFilterNarrowsResults verifies that typing '/' followed by a search +// term filters the visible probe list. +func TestSearchFilterNarrowsResults(t *testing.T) { + fm := &fakeManager{ + states: []probemanager.ProbeState{ + {Syscall: "read", Active: true}, + {Syscall: "write", Active: true}, + {Syscall: "readlink", Active: false}, + }, + } + m := NewModel(fm).Open() + + // Enter search mode. + m, _ = m.Update(tea.KeyPressMsg{Code: []rune{'/'}[0], Text: "/"}) + if !m.searching { + t.Fatal("expected searching=true after /") + } + + // Type "read" via individual rune key presses so the textinput accumulates + // the characters, then confirm with enter. + for _, ch := range "read" { + m, _ = m.Update(tea.KeyPressMsg{Code: ch, Text: string(ch)}) + } + m, _ = m.Update(tea.KeyPressMsg{Code: tea.KeyEnter}) + if m.searching { + t.Fatal("expected searching=false after enter") + } + if m.search != "read" { + t.Fatalf("expected search=read, got %q", m.search) + } + + filtered := m.filtered() + for _, p := range filtered { + if !strings.Contains(p.Syscall, "read") { + t.Fatalf("unexpected probe %q in filtered list", p.Syscall) + } + } +} + +// TestSearchEscCancelsSearch verifies that pressing esc during search exits +// search mode without applying the filter. +func TestSearchEscCancelsSearch(t *testing.T) { + fm := &fakeManager{ + states: []probemanager.ProbeState{ + {Syscall: "read", Active: true}, + {Syscall: "write", Active: true}, + }, + } + m := NewModel(fm).Open() + + m, _ = m.Update(tea.KeyPressMsg{Code: []rune{'/'}[0], Text: "/"}) + m, _ = m.Update(tea.KeyPressMsg{Code: tea.KeyEsc}) + if m.searching { + t.Fatal("expected searching=false after esc") + } + // Filter should not be active. + if len(m.filtered()) != 2 { + t.Fatalf("expected 2 unfiltered probes after esc, got %d", len(m.filtered())) + } +} + +// TestProbeToggledMsgUpdatesState verifies that ProbeToggledMsg triggers a +// reload and clears any previous error. +func TestProbeToggledMsgUpdatesState(t *testing.T) { + fm := &fakeManager{ + states: []probemanager.ProbeState{{Syscall: "read", Active: true}}, + } + m := NewModel(fm).Open() + m.lastErr = "previous error" + + // Add a new probe to the manager so reload picks it up. + fm.states = append(fm.states, probemanager.ProbeState{Syscall: "write", Active: false}) + + m, _ = m.Update(ProbeToggledMsg{Syscall: "read"}) + if m.lastErr != "" { + t.Fatalf("expected lastErr cleared after successful toggle, got %q", m.lastErr) + } + if len(m.probes) != 2 { + t.Fatalf("expected probes refreshed to 2 entries, got %d", len(m.probes)) + } +} + +// TestProbeToggledMsgSetsError verifies that a ProbeToggledMsg carrying an +// error populates lastErr. +func TestProbeToggledMsgSetsError(t *testing.T) { + fm := &fakeManager{ + states: []probemanager.ProbeState{{Syscall: "read", Active: true}}, + } + m := NewModel(fm).Open() + m, _ = m.Update(ProbeToggledMsg{Syscall: "read", Err: &testErr{"boom"}}) + if !strings.Contains(m.lastErr, "boom") { + t.Fatalf("expected lastErr to contain boom, got %q", m.lastErr) + } +} + +// testErr is a minimal error implementation for testing error message capture. +type testErr struct{ msg string } + +func (e *testErr) Error() string { return e.msg } + +// TestViewRendersEmptyProbeList verifies that View returns a non-empty string +// even when the probe list is empty. +func TestViewRendersEmptyProbeList(t *testing.T) { + fm := &fakeManager{states: nil} + m := NewModel(fm).Open() + out := m.View(80, 24) + if out == "" { + t.Fatal("expected non-empty View output") + } + if !strings.Contains(out, "no probes") { + t.Fatalf("expected 'no probes' in view output, got:\n%s", out) + } +} + +// TestViewInvisibleReturnsEmpty verifies that View returns an empty string when +// the model is not visible. +func TestViewInvisibleReturnsEmpty(t *testing.T) { + m := NewModel(nil) // not opened + if out := m.View(80, 24); out != "" { + t.Fatalf("expected empty View when not visible, got %q", out) + } +} + +// TestVisibleRowsDefault verifies the fallback when height is zero. +func TestVisibleRowsDefault(t *testing.T) { + m := NewModel(nil) + m.height = 0 + if got := m.visibleRows(); got != 10 { + t.Fatalf("visibleRows with height=0 = %d, want 10", got) + } +} + +// TestVisibleRowsMinimum verifies that visibleRows never returns less than 3. +func TestVisibleRowsMinimum(t *testing.T) { + m := NewModel(nil) + m.height = 5 + if got := m.visibleRows(); got < 3 { + t.Fatalf("visibleRows = %d, want >= 3", got) + } +} + +// TestSanitizeOneLine verifies embedded control characters are replaced with +// spaces. +func TestSanitizeOneLine(t *testing.T) { + out := sanitizeOneLine("a\nb\rc\td") + if strings.ContainsAny(out, "\n\r\t") { + t.Fatalf("sanitizeOneLine left control chars: %q", out) + } + if out != "a b c d" { + t.Fatalf("sanitizeOneLine = %q, want 'a b c d'", out) + } +} + +// TestTruncateText verifies ellipsis truncation and edge cases. +func TestTruncateText(t *testing.T) { + if got := truncateText("hello", 10); got != "hello" { + t.Fatalf("truncateText no-op = %q, want hello", got) + } + if got := truncateText("hello world", 8); got != "hello..." { + t.Fatalf("truncateText 8 = %q, want hello...", got) + } + if got := truncateText("ab", 2); got != "ab" { + t.Fatalf("truncateText exact = %q, want ab", got) + } + if got := truncateText("abc", 3); got != "abc" { + t.Fatalf("truncateText exact 3 = %q, want abc", got) + } + if got := truncateText("abcde", 3); got != "abc" { + t.Fatalf("truncateText short limit = %q, want abc", got) + } + if got := truncateText("abcde", 0); got != "" { + t.Fatalf("truncateText 0 = %q, want empty", got) + } +} + +// TestToggleCmdNilManager verifies that toggleCmd with a nil manager returns a +// ProbeToggledMsg carrying an error. +func TestToggleCmdNilManager(t *testing.T) { + cmd := toggleCmd(nil, "read") + msg := cmd() + toggled, ok := msg.(ProbeToggledMsg) + if !ok { + t.Fatalf("expected ProbeToggledMsg, got %T", msg) + } + if toggled.Err == nil { + t.Fatal("expected error from nil manager toggle") + } +} + +// TestBulkToggleCmdNilManager verifies that bulkToggleCmd with a nil manager +// returns a ProbeToggledMsg carrying an error. +func TestBulkToggleCmdNilManager(t *testing.T) { + cmd := bulkToggleCmd(nil, nil, false) + msg := cmd() + toggled, ok := msg.(ProbeToggledMsg) + if !ok { + t.Fatalf("expected ProbeToggledMsg, got %T", msg) + } + if toggled.Err == nil { + t.Fatal("expected error from nil manager bulk toggle") + } +} + +// TestEnterKeyTogglesSelectedProbe verifies that pressing enter emits a toggle +// command equivalent to space. +func TestEnterKeyTogglesSelectedProbe(t *testing.T) { + fm := &fakeManager{ + states: []probemanager.ProbeState{{Syscall: "write", Active: false}}, + } + m := NewModel(fm).Open() + _, cmd := m.Update(tea.KeyPressMsg{Code: tea.KeyEnter}) + if cmd == nil { + t.Fatal("expected toggle command on enter") + } + msg := cmd() + toggled, ok := msg.(ProbeToggledMsg) + if !ok { + t.Fatalf("expected ProbeToggledMsg, got %T", msg) + } + if toggled.Err != nil { + t.Fatalf("unexpected toggle error: %v", toggled.Err) + } +} + +// TestFKeySetsSearchMode verifies that pressing 'f' also enters search mode. +func TestFKeySetsSearchMode(t *testing.T) { + fm := &fakeManager{ + states: []probemanager.ProbeState{{Syscall: "read", Active: true}}, + } + m := NewModel(fm).Open() + m, _ = m.Update(tea.KeyPressMsg{Code: []rune{'f'}[0], Text: "f"}) + if !m.searching { + t.Fatal("expected searching=true after 'f'") + } +} + +// TestViewWithProbeErrors verifies that View shows probe error annotations. +func TestViewWithProbeErrors(t *testing.T) { + fm := &fakeManager{ + states: []probemanager.ProbeState{ + {Syscall: "read", Active: true, Error: "attach failed"}, + }, + } + m := NewModel(fm).Open() + out := m.View(80, 24) + if !strings.Contains(out, "attach failed") { + t.Fatalf("expected probe error in view, got:\n%s", out) + } +} + +// TestViewRendersLastError verifies that lastErr is shown in the view. +func TestViewRendersLastError(t *testing.T) { + fm := &fakeManager{ + states: []probemanager.ProbeState{{Syscall: "read", Active: true}}, + } + m := NewModel(fm).Open() + m.lastErr = "something went wrong" + out := m.View(80, 24) + if !strings.Contains(out, "something went wrong") { + t.Fatalf("expected lastErr in view, got:\n%s", out) + } +} |
