diff --git a/source.go b/source.go index 8705685..c7f4151 100644 --- a/source.go +++ b/source.go @@ -3,6 +3,7 @@ package peco import ( "bufio" "context" + "fmt" "io" "sync" "time" @@ -109,6 +110,12 @@ func (s *Source) Setup(ctx context.Context, state *Peco) { } }() + // scanErr captures any I/O error from the scanner goroutine. + // The goroutine writes to scanErr before closing the lines channel, + // and the outer loop reads it after lines is closed, so no mutex + // is needed. + var scanErr error + lines := make(chan string) go func() { var scanned int @@ -129,6 +136,7 @@ func (s *Source) Setup(ctx context.Context, state *Peco) { } scanned++ } + scanErr = scanner.Err() }() state.Hub().SendStatusMsg(ctx, "Waiting for input...", 0) @@ -156,6 +164,10 @@ func (s *Source) Setup(ctx context.Context, state *Peco) { } } + if scanErr != nil { + state.Hub().SendStatusMsg(ctx, fmt.Sprintf("Error reading input: %s", scanErr), 0) + } + if pdebug.Enabled { pdebug.Printf("Read all %d lines from source", readCount) } diff --git a/source_test.go b/source_test.go index 55e43f3..fe6f93f 100644 --- a/source_test.go +++ b/source_test.go @@ -1,12 +1,15 @@ package peco import ( + "errors" "io" "strings" "sync" "testing" "time" + "context" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -77,3 +80,66 @@ func TestSource(t *testing.T) { } } } + +// errorAfterReader returns data from the underlying reader, then once +// the underlying reader is exhausted it returns a specified error. +type errorAfterReader struct { + io.Reader + err error + hitEOF bool +} + +func (r *errorAfterReader) Read(p []byte) (int, error) { + if r.hitEOF { + return 0, r.err + } + n, err := r.Reader.Read(p) + if err == io.EOF { + r.hitEOF = true + if n > 0 { + return n, nil + } + return 0, r.err + } + return n, err +} + +func TestSourceScannerErr(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + ig := newIDGen() + go ig.Run(ctx) + + simulatedErr := errors.New("simulated I/O error") + r := &errorAfterReader{ + Reader: strings.NewReader("line1\nline2\n"), + err: simulatedErr, + } + + s := NewSource("-", r, false, ig, 0, false, false) + p := New() + rh := &recordingHub{} + p.hub = rh + s.Setup(ctx, p) + + // Verify lines read before the error are present + require.Equal(t, 2, s.Size(), "should have read 2 lines before the error") + l0, err := s.LineAt(0) + require.NoError(t, err) + require.Equal(t, "line1", l0.DisplayString()) + l1, err := s.LineAt(1) + require.NoError(t, err) + require.Equal(t, "line2", l1.DisplayString()) + + // Verify the scanner error was reported via SendStatusMsg + msgs := rh.getStatusMsgs() + found := false + for _, msg := range msgs { + if strings.Contains(msg, "simulated I/O error") { + found = true + break + } + } + require.True(t, found, "expected scanner error in status messages, got: %v", msgs) +}