diff options
| -rw-r--r-- | internal/ior_mode_test.go | 60 | ||||
| -rw-r--r-- | internal/streamrow/row_test.go | 52 | ||||
| -rw-r--r-- | internal/tui/tui_test.go | 83 |
3 files changed, 195 insertions, 0 deletions
diff --git a/internal/ior_mode_test.go b/internal/ior_mode_test.go index ced80fc..a7fcab1 100644 --- a/internal/ior_mode_test.go +++ b/internal/ior_mode_test.go @@ -449,6 +449,22 @@ func TestValidateRunConfigRejectsParquetWithContentFilters(t *testing.T) { } } +func TestValidateRunConfigRejectsParquetWithGlobalFilter(t *testing.T) { + cfg := flags.Config{ + ParquetPath: "trace.parquet", + GlobalFilter: globalfilter.Filter{ + Syscall: &globalfilter.StringFilter{Pattern: "read"}, + }, + } + err := validateRunConfig(cfg) + if err == nil { + t.Fatalf("expected error for -parquet with global filter") + } + if err.Error() != "-parquet cannot be combined with content filters (-comm, -path, -pid, -tid)" { + t.Fatalf("unexpected error: %v", err) + } +} + func TestBuildTestFlamesRuntimeSeedsLiveTrie(t *testing.T) { cfg := flags.NewFlags() _, streamBuf, liveTrie := buildTestFlamesRuntime(cfg) @@ -681,6 +697,50 @@ func TestHeadlessParquetTraceConfigClearsContentFilters(t *testing.T) { } } +func TestHeadlessParquetSinkRecordsRows(t *testing.T) { + recorder := parquet.NewRecorder(parquet.RecorderConfig{ + BatchSize: 1, + FlushInterval: time.Hour, + }) + path := filepath.Join(t.TempDir(), "headless.parquet") + if err := recorder.Start(path, parquet.StartOptions{ + Metadata: parquet.FileMetadata{Mode: "headless"}, + }); err != nil { + t.Fatalf("recorder.Start() error = %v", err) + } + + _, cancel := context.WithCancel(context.Background()) + defer cancel() + + sink := newHeadlessParquetSink(recorder, cancel) + el := &eventLoop{} + sink.configure(el) + + el.printCb(testTracePair(1, "keep")) + el.printCb(testTracePair(2, "keep")) + + if err := recorder.Stop(); err != nil { + t.Fatalf("recorder.Stop() error = %v", err) + } + if err := sink.err(); err != nil { + t.Fatalf("sink.err() = %v, want nil", err) + } + + rows := readRecordedParquet(t, path) + if len(rows) != 2 { + t.Fatalf("recorded rows = %d, want 2", len(rows)) + } + if rows[0].Seq != 1 || rows[1].Seq != 2 { + t.Fatalf("recorded seq = %d,%d, want 1,2", rows[0].Seq, rows[1].Seq) + } + if rows[0].FilterEpoch != 0 || rows[1].FilterEpoch != 0 { + t.Fatalf("recorded filter epochs = %d,%d, want 0,0", rows[0].FilterEpoch, rows[1].FilterEpoch) + } + if rows[0].Comm != "keep" || rows[1].Syscall != "openat" { + t.Fatalf("unexpected recorded rows: %+v %+v", rows[0], rows[1]) + } +} + func TestTuiTraceStarterFromRunTracePersistsRecorderAcrossRestarts(t *testing.T) { recorder := parquet.NewRecorder(parquet.RecorderConfig{ BatchSize: 1, diff --git a/internal/streamrow/row_test.go b/internal/streamrow/row_test.go index 729ba94..17d6c40 100644 --- a/internal/streamrow/row_test.go +++ b/internal/streamrow/row_test.go @@ -3,6 +3,10 @@ package streamrow import ( "sync" "testing" + + "ior/internal/event" + "ior/internal/file" + "ior/internal/types" ) func TestSequencerStartsAfterSeed(t *testing.T) { @@ -46,3 +50,51 @@ func TestSequencerIsMonotonicUnderConcurrency(t *testing.T) { t.Fatalf("unique sequence count = %d, want %d", got, want) } } + +func TestNewPopulatesFieldsFromPair(t *testing.T) { + enter := &types.OpenEvent{TraceId: types.SYS_ENTER_OPENAT, Time: 1234, Pid: 42, Tid: 84} + exit := &types.RetEvent{TraceId: types.SYS_EXIT_OPENAT, Time: 1300, Ret: -2, Pid: 42, Tid: 84} + pair := event.NewPair(enter) + pair.ExitEv = exit + pair.File = file.NewFd(7, "/tmp/test.txt", 0) + pair.Comm = "cat" + pair.Duration = 66 + pair.DurationToPrev = 19 + pair.Bytes = 512 + + got := New(9, pair) + if got.Seq != 9 || got.TimeNs != 1234 { + t.Fatalf("Seq/TimeNs = %d/%d, want 9/1234", got.Seq, got.TimeNs) + } + if got.Syscall != "openat" || got.Comm != "cat" { + t.Fatalf("Syscall/Comm = %q/%q, want openat/cat", got.Syscall, got.Comm) + } + if got.PID != 42 || got.TID != 84 { + t.Fatalf("PID/TID = %d/%d, want 42/84", got.PID, got.TID) + } + if got.FileName != "/tmp/test.txt" || got.FD != 7 { + t.Fatalf("FileName/FD = %q/%d, want /tmp/test.txt/7", got.FileName, got.FD) + } + if got.DurationNs != 66 || got.GapNs != 19 || got.Bytes != 512 { + t.Fatalf("DurationNs/GapNs/Bytes = %d/%d/%d, want 66/19/512", got.DurationNs, got.GapNs, got.Bytes) + } + if got.RetVal != -2 || !got.IsError { + t.Fatalf("RetVal/IsError = %d/%v, want -2/true", got.RetVal, got.IsError) + } +} + +func TestNewWarningPopulatesSyntheticWarningFields(t *testing.T) { + got := NewWarning(7, "Dropped malformed event") + if got.Seq != 7 || got.TimeNs == 0 { + t.Fatalf("Seq/TimeNs = %d/%d, want 7/non-zero", got.Seq, got.TimeNs) + } + if got.Syscall != "warning" || got.Comm != "ior" { + t.Fatalf("Syscall/Comm = %q/%q, want warning/ior", got.Syscall, got.Comm) + } + if got.FileName != "Dropped malformed event" || got.FD != UnknownFD { + t.Fatalf("FileName/FD = %q/%d, want warning text/%d", got.FileName, got.FD, UnknownFD) + } + if got.RetVal != -1 || !got.IsError { + t.Fatalf("RetVal/IsError = %d/%v, want -1/true", got.RetVal, got.IsError) + } +} diff --git a/internal/tui/tui_test.go b/internal/tui/tui_test.go index 70552fb..6ce16e6 100644 --- a/internal/tui/tui_test.go +++ b/internal/tui/tui_test.go @@ -722,6 +722,52 @@ func TestRecordKeyOpensRecordingModalOnDashboard(t *testing.T) { } } +func TestRecordModalSubmitStartsRecording(t *testing.T) { + m := NewModel(-1, func(context.Context) error { return nil }) + m.screen = ScreenDashboard + m.attaching = false + + path := filepath.Join(t.TempDir(), "capture.parquet") + m.recordModal = m.recordModal.Open(path) + + next, _ := m.Update(tea.KeyPressMsg{Code: tea.KeyEnter}) + updated := next.(Model) + if updated.recordModal.Visible() { + t.Fatalf("expected recording modal to close after submit") + } + status := updated.runtime.Recorder().Status() + if !status.Active { + t.Fatalf("expected recorder to be active after modal submit") + } + t.Cleanup(func() { + if err := updated.stopRecording(); err != nil { + t.Fatalf("stopRecording() cleanup error = %v", err) + } + }) + if status.Path != path { + t.Fatalf("recording path = %q, want %q", status.Path, path) + } +} + +func TestRecordModalRejectsBlankFilename(t *testing.T) { + m := NewModel(-1, func(context.Context) error { return nil }) + m.screen = ScreenDashboard + m.attaching = false + m.recordModal = m.recordModal.Open(" ") + + next, _ := m.Update(tea.KeyPressMsg{Code: tea.KeyEnter}) + updated := next.(Model) + if !updated.recordModal.Visible() { + t.Fatalf("expected recording modal to stay open on blank filename") + } + if updated.runtime.Recorder().Status().Active { + t.Fatalf("expected blank filename submit not to start recorder") + } + if !strings.Contains(updated.recordModal.View(120, 30), "filename is required") { + t.Fatalf("expected blank filename error to be visible") + } +} + func TestStartRecordingUpdatesDashboardStatus(t *testing.T) { m := NewModel(-1, func(context.Context) error { return nil }) m.screen = ScreenDashboard @@ -810,6 +856,43 @@ func TestSelectPIDStopsActiveRecording(t *testing.T) { } } +func TestGlobalFilterApplyKeepsActiveRecordingAcrossRestart(t *testing.T) { + m := NewModelWithConfig(flags.Config{PidFilter: -1, TidFilter: -1, TUIExportEnable: true}, -1, func(context.Context) error { return nil }) + m.screen = ScreenDashboard + m.attaching = false + + path := filepath.Join(t.TempDir(), "capture.parquet") + if err := m.startRecording(path); err != nil { + t.Fatalf("startRecording() error = %v", err) + } + t.Cleanup(func() { + if err := m.stopRecording(); err != nil { + t.Fatalf("stopRecording() cleanup error = %v", err) + } + }) + + initialRecorder := m.runtime.Recorder() + + next, _ := m.Update(tea.KeyPressMsg{Code: []rune{'f'}[0], Text: string([]rune{'f'})}) + m = next.(Model) + next, _ = m.Update(tea.KeyPressMsg{Code: tea.KeyEnter}) + m = next.(Model) + next, _ = m.Update(tea.KeyPressMsg{Code: []rune("read")[0], Text: string([]rune("read"))}) + m = next.(Model) + next, _ = m.Update(tea.KeyPressMsg{Code: tea.KeyEsc}) + m = next.(Model) + + if got := m.runtime.FilterEpoch(); got != 1 { + t.Fatalf("filter epoch after apply = %d, want 1", got) + } + if got := m.runtime.Recorder(); got != initialRecorder { + t.Fatalf("expected runtime recorder to survive filter restart") + } + if !m.runtime.Recorder().Status().Active { + t.Fatalf("expected active recording to survive filter restart") + } +} + func TestFlamePauseKeyDoesNotTriggerPIDReselect(t *testing.T) { m := NewModel(-1, func(context.Context) error { return nil }) m.screen = ScreenDashboard |
