summaryrefslogtreecommitdiff
path: root/internal
diff options
context:
space:
mode:
Diffstat (limited to 'internal')
-rw-r--r--internal/ior_mode_test.go60
-rw-r--r--internal/streamrow/row_test.go52
-rw-r--r--internal/tui/tui_test.go83
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