From 39fddf0b4936ab6c5faa409633e78b3a1f9cd3b2 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 12 Nov 2025 22:53:05 +0000 Subject: [PATCH 1/9] Initial plan From ddc732d063ef198c30011c92399f04ff89cd157a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 12 Nov 2025 22:59:56 +0000 Subject: [PATCH 2/9] Fix infinite loop when running sift without input - Add stdin terminal detection to exit early with helpful error message - Add signal handling (SIGINT/SIGTERM) for graceful shutdown - Add test for isStdinTerminal function Co-authored-by: timtatt <10578290+timtatt@users.noreply.github.com> --- internal/sift/sift.go | 24 ++++++++++++++++++++++++ internal/sift/sift_test.go | 15 +++++++++++++++ 2 files changed, 39 insertions(+) create mode 100644 internal/sift/sift_test.go diff --git a/internal/sift/sift.go b/internal/sift/sift.go index b92dc11..7e97c27 100644 --- a/internal/sift/sift.go +++ b/internal/sift/sift.go @@ -8,6 +8,8 @@ import ( "fmt" "log/slog" "os" + "os/signal" + "syscall" "time" tea "github.com/charmbracelet/bubbletea" @@ -20,6 +22,15 @@ type sift struct { model *siftModel } +// isStdinTerminal checks if stdin is a terminal (no piped input) +func isStdinTerminal() bool { + stat, err := os.Stdin.Stat() + if err != nil { + return false + } + return (stat.Mode() & os.ModeCharDevice) != 0 +} + func (s *sift) ScanStdin() error { scanner := bufio.NewScanner(os.Stdin) @@ -92,9 +103,22 @@ func Run(ctx context.Context, opts SiftOptions) error { slog.DebugContext(ctx, "starting sift", "options", opts) } + // Check if stdin is a terminal (no piped input) + if isStdinTerminal() { + return errors.New("no input provided. pipe test results to sift:\n go test ./... -v -json | sift") + } + ctx, cancel := context.WithCancel(ctx) defer cancel() + // Set up signal handling for graceful shutdown + sigChan := make(chan os.Signal, 1) + signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM) + go func() { + <-sigChan + cancel() + }() + fps := 120 g, ctx := errgroup.WithContext(ctx) diff --git a/internal/sift/sift_test.go b/internal/sift/sift_test.go new file mode 100644 index 0000000..cdbbf52 --- /dev/null +++ b/internal/sift/sift_test.go @@ -0,0 +1,15 @@ +package sift + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestIsStdinTerminal(t *testing.T) { + // This test validates that isStdinTerminal returns a boolean value + // The actual value depends on how the test is run (piped or not) + // We just verify it doesn't panic and returns a value + result := isStdinTerminal() + assert.IsType(t, true, result, "isStdinTerminal should return a boolean") +} From b0297f1b7c61ae34ce4bfd694e9ffaac46015753 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 12 Nov 2025 23:15:06 +0000 Subject: [PATCH 3/9] Show warning and help text when no stdin provided - Export IsStdinTerminal function for use in main - Handle no-stdin case in main.go to print warning and help - Remove stdin check from sift.Run() to avoid duplication - Update test to use exported function name Co-authored-by: timtatt <10578290+timtatt@users.noreply.github.com> --- internal/sift/sift.go | 9 ++------- internal/sift/sift_test.go | 7 ++----- main.go | 12 ++++++++++++ 3 files changed, 16 insertions(+), 12 deletions(-) diff --git a/internal/sift/sift.go b/internal/sift/sift.go index 7e97c27..3003ee9 100644 --- a/internal/sift/sift.go +++ b/internal/sift/sift.go @@ -22,8 +22,8 @@ type sift struct { model *siftModel } -// isStdinTerminal checks if stdin is a terminal (no piped input) -func isStdinTerminal() bool { +// IsStdinTerminal checks if stdin is a terminal (no piped input) +func IsStdinTerminal() bool { stat, err := os.Stdin.Stat() if err != nil { return false @@ -103,11 +103,6 @@ func Run(ctx context.Context, opts SiftOptions) error { slog.DebugContext(ctx, "starting sift", "options", opts) } - // Check if stdin is a terminal (no piped input) - if isStdinTerminal() { - return errors.New("no input provided. pipe test results to sift:\n go test ./... -v -json | sift") - } - ctx, cancel := context.WithCancel(ctx) defer cancel() diff --git a/internal/sift/sift_test.go b/internal/sift/sift_test.go index cdbbf52..b91d837 100644 --- a/internal/sift/sift_test.go +++ b/internal/sift/sift_test.go @@ -7,9 +7,6 @@ import ( ) func TestIsStdinTerminal(t *testing.T) { - // This test validates that isStdinTerminal returns a boolean value - // The actual value depends on how the test is run (piped or not) - // We just verify it doesn't panic and returns a value - result := isStdinTerminal() - assert.IsType(t, true, result, "isStdinTerminal should return a boolean") + result := IsStdinTerminal() + assert.IsType(t, true, result, "IsStdinTerminal should return a boolean") } diff --git a/main.go b/main.go index 92f5d88..c3b106d 100644 --- a/main.go +++ b/main.go @@ -1,14 +1,26 @@ package main import ( + "fmt" + "os" + "github.com/alecthomas/kong" "github.com/timtatt/sift/cmd" + "github.com/timtatt/sift/internal/sift" ) func main() { var cli cmd.CLI ctx := kong.Parse(&cli) + + if sift.IsStdinTerminal() { + fmt.Fprintln(os.Stderr, "Warning: no input provided") + fmt.Fprintln(os.Stderr) + ctx.PrintUsage(false) + os.Exit(1) + } + err := ctx.Run() ctx.FatalIfErrorf(err) } From ef89f2bc4076288a283b055b4f63f952ea1a8bf4 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 12 Nov 2025 23:21:03 +0000 Subject: [PATCH 4/9] Use signal.NotifyContext instead of signal channel Replace manual signal channel handling with signal.NotifyContext for cleaner signal handling. Co-authored-by: timtatt <10578290+timtatt@users.noreply.github.com> --- internal/sift/sift.go | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/internal/sift/sift.go b/internal/sift/sift.go index 3003ee9..ee1ea27 100644 --- a/internal/sift/sift.go +++ b/internal/sift/sift.go @@ -103,17 +103,9 @@ func Run(ctx context.Context, opts SiftOptions) error { slog.DebugContext(ctx, "starting sift", "options", opts) } - ctx, cancel := context.WithCancel(ctx) + ctx, cancel := signal.NotifyContext(ctx, os.Interrupt, syscall.SIGTERM) defer cancel() - // Set up signal handling for graceful shutdown - sigChan := make(chan os.Signal, 1) - signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM) - go func() { - <-sigChan - cancel() - }() - fps := 120 g, ctx := errgroup.WithContext(ctx) From 3a4c57cb78399c31dd66b95758ca69c1fca896a1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 12 Nov 2025 23:41:01 +0000 Subject: [PATCH 5/9] Separate q and ctrl+c behavior for proper exit handling - Add ForceQuit key binding for ctrl+c to exit immediately - Keep Quit key binding for q to switch to inline mode - Handle ctrl+c in search mode to force quit - Preserves existing q behavior while making ctrl+c work as expected Co-authored-by: timtatt <10578290+timtatt@users.noreply.github.com> --- internal/sift/keys.go | 7 ++++++- internal/sift/view.go | 6 ++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/internal/sift/keys.go b/internal/sift/keys.go index 82db8c7..1c4513d 100644 --- a/internal/sift/keys.go +++ b/internal/sift/keys.go @@ -24,6 +24,7 @@ type keyMap struct { ClearSearch key.Binding Help key.Binding Quit key.Binding + ForceQuit key.Binding ChangeMode key.Binding } @@ -131,8 +132,12 @@ var ( key.WithHelp("?", "toggle help"), ), Quit: key.NewBinding( - key.WithKeys("q", "ctrl+c"), + key.WithKeys("q"), key.WithHelp("q", "quit"), ), + ForceQuit: key.NewBinding( + key.WithKeys("ctrl+c"), + key.WithHelp("ctrl+c", "force quit"), + ), } ) diff --git a/internal/sift/view.go b/internal/sift/view.go index 58a72a8..eec97af 100644 --- a/internal/sift/view.go +++ b/internal/sift/view.go @@ -452,6 +452,9 @@ func (m *siftModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { if m.searchInput.Focused() { switch { + case msg.String() == "ctrl+c": + m.quitting = true + return m, tea.Quit case msg.String() == "esc": // Exit search mode and clear query m.searchInput.Blur() @@ -575,6 +578,9 @@ func (m *siftModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { case key.Matches(msg, keys.Help): m.help.ShowAll = !m.help.ShowAll + case key.Matches(msg, keys.ForceQuit): + m.quitting = true + return m, tea.Quit case key.Matches(msg, keys.Quit): if m.mode == viewModeAlternate { m.mode = viewModeInline From 38969a2983eb889ce7f1f030eef5516db0a624cc Mon Sep 17 00:00:00 2001 From: Tim Tattersall Date: Mon, 17 Nov 2025 22:21:41 +1100 Subject: [PATCH 6/9] fix: handle force quit in the scanner --- cmd/cmd.go | 1 + internal/sift/sift.go | 63 ++++++++++++++++++++++++++++++------------- internal/sift/view.go | 5 +++- main.go | 1 + 4 files changed, 51 insertions(+), 19 deletions(-) diff --git a/cmd/cmd.go b/cmd/cmd.go index 452b7e2..03059df 100644 --- a/cmd/cmd.go +++ b/cmd/cmd.go @@ -28,4 +28,5 @@ func (c *CLI) Run() error { NonInteractive: c.NonInteractive, PrettifyLogs: !c.RawLogs, }) + } diff --git a/internal/sift/sift.go b/internal/sift/sift.go index ee1ea27..50be9eb 100644 --- a/internal/sift/sift.go +++ b/internal/sift/sift.go @@ -4,7 +4,6 @@ import ( "bufio" "context" "encoding/json" - "errors" "fmt" "log/slog" "os" @@ -31,28 +30,56 @@ func IsStdinTerminal() bool { return (stat.Mode() & os.ModeCharDevice) != 0 } -func (s *sift) ScanStdin() error { - scanner := bufio.NewScanner(os.Stdin) +func (s *sift) ScanStdin(ctx context.Context) error { - for scanner.Scan() { - var line tests.TestOutputLine + lines := make(chan []byte) + errChan := make(chan error) - err := json.Unmarshal(scanner.Bytes(), &line) - if err != nil { - // TODO: write to a temp dir log - return errors.New("unable to parse json input. ensure to use the `-json` flag when running go tests") + // scans in a separate channel to allow context cancellation + go func() { + scanner := bufio.NewScanner(os.Stdin) + + for scanner.Scan() { + // without a copy, the underlying array changes whilst it is being processed by the consumer + lineCopy := make([]byte, len(scanner.Bytes())) + copy(lineCopy, scanner.Bytes()) + lines <- lineCopy } - s.model.testManager.AddTestOutput(line) - } + if err := scanner.Err(); err != nil { + errChan <- fmt.Errorf("failed to scan stdin: %w", err) + } - if err := scanner.Err(); err != nil { - return fmt.Errorf("failed to scan stdin: %w", err) - } + close(lines) + close(errChan) + }() + + for { + select { + // exit early if context is cancelled + case <-ctx.Done(): + return nil + case err := <-errChan: + return err + case line, ok := <-lines: - s.model.endTime = time.Now() + // channel closed, finished processing + if !ok { + s.model.endTime = time.Now() + return nil + } - return nil + var testOutputLine tests.TestOutputLine + + err := json.Unmarshal(line, &testOutputLine) + if err != nil { + // TODO: write to a temp dir log + return fmt.Errorf("unable to parse json input. ensure to use the `-json` flag when running go tests: %s", err) + } + + s.model.testManager.AddTestOutput(testOutputLine) + } + } } type FrameMsg struct{} @@ -129,7 +156,7 @@ func Run(ctx context.Context, opts SiftOptions) error { } g.Go(func() error { - if err := sift.ScanStdin(); err != nil { + if err := sift.ScanStdin(ctx); err != nil { return err } @@ -157,6 +184,6 @@ func Run(ctx context.Context, opts SiftOptions) error { } m.quitting = false - fmt.Print(m.View()) + fmt.Println(m.View()) return nil } diff --git a/internal/sift/view.go b/internal/sift/view.go index eec97af..f8e64f8 100644 --- a/internal/sift/view.go +++ b/internal/sift/view.go @@ -579,8 +579,11 @@ func (m *siftModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { case key.Matches(msg, keys.Help): m.help.ShowAll = !m.help.ShowAll case key.Matches(msg, keys.ForceQuit): + // ensure running inline mode for the final print + m.mode = viewModeInline m.quitting = true - return m, tea.Quit + + return m, nil case key.Matches(msg, keys.Quit): if m.mode == viewModeAlternate { m.mode = viewModeInline diff --git a/main.go b/main.go index c3b106d..4cff9ba 100644 --- a/main.go +++ b/main.go @@ -22,5 +22,6 @@ func main() { } err := ctx.Run() + ctx.FatalIfErrorf(err) } From 6bfe2116b901bc50f3342d601dbbea4f4b3dca6f Mon Sep 17 00:00:00 2001 From: Tim Tattersall Date: Mon, 17 Nov 2025 22:56:30 +1100 Subject: [PATCH 7/9] fix: error if no tests received from go test --- README.md | 14 +++++++------- internal/sift/sift.go | 17 +++++++++++++---- internal/sift/styles.go | 2 +- 3 files changed, 21 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 45a281c..3f31f6f 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ sift is a lightweight terminal UI for displaying Go test results. It allows deve ## Installation ```bash -go install github.com/timtatt/sift@v0.11.0 +go install github.com/timtatt/sift@v0.12.1 ``` ## Try it out! @@ -23,7 +23,7 @@ You can try a demo of sift with the sample tests provided in the `samples` folde git clone github.com/timtatt/sift.git # Run sift -go test ./samples/... -v -json | sift +go test ./samples/... -json | sift ``` ## Usage @@ -31,10 +31,10 @@ go test ./samples/... -v -json | sift `sift` works by consuming the verbose json output from the `go test` command. The easiest way to use it is to pipe `|` the output straight into `sift` ```bash -go test {your-go-package} -v -json | sift +go test {your-go-package} -json | sift # eg. -go test ./... -v -json | sift +go test ./... -json | sift ``` ## Demo (v0.9.0) @@ -53,13 +53,13 @@ go test ./... -v -json | sift ```bash # Run in non-interactive mode (inline output) -go test ./... -v -json | sift -n +go test ./... -json | sift -n # Enable debug view -go test ./... -v -json | sift --debug +go test ./... -json | sift --debug # Disable log prettification -go test ./... -v -json | sift --raw +go test ./... -json | sift --raw ``` ### Keymaps diff --git a/internal/sift/sift.go b/internal/sift/sift.go index 50be9eb..a54559f 100644 --- a/internal/sift/sift.go +++ b/internal/sift/sift.go @@ -4,6 +4,7 @@ import ( "bufio" "context" "encoding/json" + "errors" "fmt" "log/slog" "os" @@ -54,6 +55,7 @@ func (s *sift) ScanStdin(ctx context.Context) error { close(errChan) }() +out: for { select { // exit early if context is cancelled @@ -65,21 +67,28 @@ func (s *sift) ScanStdin(ctx context.Context) error { // channel closed, finished processing if !ok { - s.model.endTime = time.Now() - return nil + break out } var testOutputLine tests.TestOutputLine err := json.Unmarshal(line, &testOutputLine) if err != nil { - // TODO: write to a temp dir log - return fmt.Errorf("unable to parse json input. ensure to use the `-json` flag when running go tests: %s", err) + slog.ErrorContext(ctx, "unable to parse json input", "err", err) + return errors.New("unable to parse json input. ensure to use the `-json` flag when running go tests") } s.model.testManager.AddTestOutput(testOutputLine) } } + + s.model.endTime = time.Now() + + if s.model.testManager.GetTestCount() == 0 { + return errors.New("no tests received, ensure to specify a package to run `go test` with") + } + + return nil } type FrameMsg struct{} diff --git a/internal/sift/styles.go b/internal/sift/styles.go index 6d6fbd3..79e90ff 100644 --- a/internal/sift/styles.go +++ b/internal/sift/styles.go @@ -10,7 +10,7 @@ import ( var ( colorGreen = lipgloss.AdaptiveColor{ Light: "#2D7F1E", - Dark: "#5FD700", + Dark: "#4b9c09", } colorRed = lipgloss.AdaptiveColor{ Light: "#C41E3A", From 339b4671121e1356ecc5e77e0438baaa37bc082f Mon Sep 17 00:00:00 2001 From: Tim Tattersall Date: Mon, 17 Nov 2025 23:16:14 +1100 Subject: [PATCH 8/9] refactor: move scanstdin to the testmanager --- cmd/cmd.go | 2 +- internal/sift/sift.go | 102 +++++++-------------------------- internal/sift/view.go | 23 +++++--- internal/sift/view_test.go | 18 +++++- internal/tests/test_manager.go | 64 +++++++++++++++++++++ 5 files changed, 118 insertions(+), 91 deletions(-) diff --git a/cmd/cmd.go b/cmd/cmd.go index 03059df..6f95564 100644 --- a/cmd/cmd.go +++ b/cmd/cmd.go @@ -23,7 +23,7 @@ func (c *CLI) Run() error { os.Exit(0) } - return sift.Run(ctx, sift.SiftOptions{ + return sift.Run(ctx, sift.ProgramOptions{ Debug: c.Debug, NonInteractive: c.NonInteractive, PrettifyLogs: !c.RawLogs, diff --git a/internal/sift/sift.go b/internal/sift/sift.go index a54559f..42d0528 100644 --- a/internal/sift/sift.go +++ b/internal/sift/sift.go @@ -1,10 +1,7 @@ package sift import ( - "bufio" "context" - "encoding/json" - "errors" "fmt" "log/slog" "os" @@ -17,11 +14,6 @@ import ( "golang.org/x/sync/errgroup" ) -type sift struct { - program *tea.Program - model *siftModel -} - // IsStdinTerminal checks if stdin is a terminal (no piped input) func IsStdinTerminal() bool { stat, err := os.Stdin.Stat() @@ -31,70 +23,10 @@ func IsStdinTerminal() bool { return (stat.Mode() & os.ModeCharDevice) != 0 } -func (s *sift) ScanStdin(ctx context.Context) error { - - lines := make(chan []byte) - errChan := make(chan error) - - // scans in a separate channel to allow context cancellation - go func() { - scanner := bufio.NewScanner(os.Stdin) - - for scanner.Scan() { - // without a copy, the underlying array changes whilst it is being processed by the consumer - lineCopy := make([]byte, len(scanner.Bytes())) - copy(lineCopy, scanner.Bytes()) - lines <- lineCopy - } - - if err := scanner.Err(); err != nil { - errChan <- fmt.Errorf("failed to scan stdin: %w", err) - } - - close(lines) - close(errChan) - }() - -out: - for { - select { - // exit early if context is cancelled - case <-ctx.Done(): - return nil - case err := <-errChan: - return err - case line, ok := <-lines: - - // channel closed, finished processing - if !ok { - break out - } - - var testOutputLine tests.TestOutputLine - - err := json.Unmarshal(line, &testOutputLine) - if err != nil { - slog.ErrorContext(ctx, "unable to parse json input", "err", err) - return errors.New("unable to parse json input. ensure to use the `-json` flag when running go tests") - } - - s.model.testManager.AddTestOutput(testOutputLine) - } - } - - s.model.endTime = time.Now() - - if s.model.testManager.GetTestCount() == 0 { - return errors.New("no tests received, ensure to specify a package to run `go test` with") - } - - return nil -} - type FrameMsg struct{} // sends a msg to bubbletea model on an interval to ensure the view is being updated according to framerate -func (s *sift) Frame(ctx context.Context, tps int) { +func FrameTicker(ctx context.Context, program *tea.Program, tps int) { tick := time.NewTicker(time.Second / time.Duration(tps)) defer tick.Stop() @@ -103,12 +35,12 @@ func (s *sift) Frame(ctx context.Context, tps int) { case <-ctx.Done(): return case <-tick.C: - s.program.Send(FrameMsg{}) + program.Send(FrameMsg{}) } } } -type SiftOptions struct { +type ProgramOptions struct { Debug bool NonInteractive bool PrettifyLogs bool @@ -130,7 +62,7 @@ func initLogging() error { return nil } -func Run(ctx context.Context, opts SiftOptions) error { +func Run(ctx context.Context, opts ProgramOptions) error { if opts.Debug { if err := initLogging(); err != nil { @@ -146,7 +78,18 @@ func Run(ctx context.Context, opts SiftOptions) error { g, ctx := errgroup.WithContext(ctx) - m := NewSiftModel(opts) + testManager := tests.NewTestManager(tests.TestManagerOpts{ + ParseLogs: opts.PrettifyLogs, + }) + + m, err := NewSiftModel(SiftModelOptions{ + ProgramOptions: opts, + TestManager: testManager, + }) + + if err != nil { + return fmt.Errorf("unable to create sift model: %w", err) + } programOpts := []tea.ProgramOption{ tea.WithFPS(fps), @@ -159,16 +102,13 @@ func Run(ctx context.Context, opts SiftOptions) error { p := tea.NewProgram(m, programOpts...) - sift := &sift{ - model: m, - program: p, - } - g.Go(func() error { - if err := sift.ScanStdin(ctx); err != nil { + if err := testManager.ScanStdin(ctx, os.Stdin); err != nil { return err } + m.endTime = time.Now() + return nil }) @@ -182,12 +122,12 @@ func Run(ctx context.Context, opts SiftOptions) error { }) g.Go(func() error { - sift.Frame(ctx, fps) + FrameTicker(ctx, p, fps) return nil }) - err := g.Wait() + err = g.Wait() if err != nil { return err } diff --git a/internal/sift/view.go b/internal/sift/view.go index f8e64f8..ea061a4 100644 --- a/internal/sift/view.go +++ b/internal/sift/view.go @@ -1,6 +1,7 @@ package sift import ( + "errors" "strings" "time" @@ -27,7 +28,7 @@ const ( ) type siftModel struct { - opts SiftOptions + opts ProgramOptions testManager *tests.TestManager testState map[tests.TestReference]*testState @@ -61,7 +62,17 @@ type cursor struct { log int // tracks the cursor log line } -func NewSiftModel(opts SiftOptions) *siftModel { +type SiftModelOptions struct { + ProgramOptions + TestManager *tests.TestManager +} + +func NewSiftModel(opts SiftModelOptions) (*siftModel, error) { + + if opts.TestManager == nil { + return nil, errors.New("missing test manager") + } + ti := textinput.New() ti.Placeholder = "search for tests" ti.PlaceholderStyle = styleSecondary @@ -74,10 +85,8 @@ func NewSiftModel(opts SiftOptions) *siftModel { } return &siftModel{ - opts: opts, - testManager: tests.NewTestManager(tests.TestManagerOpts{ - ParseLogs: opts.PrettifyLogs, - }), + opts: opts.ProgramOptions, + testManager: opts.TestManager, testState: make(map[tests.TestReference]*testState), autoToggleMode: false, compileSpinner: spinner.New(spinner.WithSpinner(spinner.Dot)), @@ -89,7 +98,7 @@ func NewSiftModel(opts SiftOptions) *siftModel { }, searchInput: ti, mode: mode, - } + }, nil } // normalizeSearchQuery removes spaces from the search query since Go replaces diff --git a/internal/sift/view_test.go b/internal/sift/view_test.go index 2e349ff..00a6c7b 100644 --- a/internal/sift/view_test.go +++ b/internal/sift/view_test.go @@ -1,11 +1,13 @@ package sift import ( + "fmt" "testing" "github.com/charmbracelet/bubbles/key" tea "github.com/charmbracelet/bubbletea" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "github.com/timtatt/sift/internal/tests" ) @@ -800,7 +802,14 @@ type testModelOpts struct { } func createTestModel(opts testModelOpts) *siftModel { - m := NewSiftModel(SiftOptions{}) + m, err := NewSiftModel(SiftModelOptions{ + TestManager: tests.NewTestManager(tests.TestManagerOpts{}), + }) + + if err != nil { + fmt.Println(err) + } + m.autoToggleMode = opts.autoToggleMode testCount := opts.testCount @@ -947,7 +956,12 @@ func TestIsTestVisible_SpaceHandling(t *testing.T) { for _, tt := range testCases { t.Run(tt.name, func(t *testing.T) { - m := NewSiftModel(SiftOptions{}) + m, err := NewSiftModel(SiftModelOptions{ + TestManager: tests.NewTestManager(tests.TestManagerOpts{}), + }) + + require.NoError(t, err) + testRef := tests.TestReference{ Package: "test/package", Test: tt.testName, diff --git a/internal/tests/test_manager.go b/internal/tests/test_manager.go index 72007ce..459dd80 100644 --- a/internal/tests/test_manager.go +++ b/internal/tests/test_manager.go @@ -1,8 +1,14 @@ package tests import ( + "bufio" "cmp" + "context" + "encoding/json" + "errors" "fmt" + "io" + "log/slog" "slices" "strings" "sync" @@ -219,3 +225,61 @@ func (tm *TestManager) GetLogs(testRef TestReference) []logparse.LogEntry { return nil } + +func (tm *TestManager) ScanStdin(ctx context.Context, reader io.Reader) error { + + lines := make(chan []byte) + errChan := make(chan error) + + // scans in a separate channel to allow context cancellation + go func() { + scanner := bufio.NewScanner(reader) + + for scanner.Scan() { + // without a copy, the underlying array changes whilst it is being processed by the consumer + lineCopy := make([]byte, len(scanner.Bytes())) + copy(lineCopy, scanner.Bytes()) + lines <- lineCopy + } + + if err := scanner.Err(); err != nil { + errChan <- fmt.Errorf("failed to scan stdin: %w", err) + } + + close(lines) + close(errChan) + }() + +out: + for { + select { + // exit early if context is cancelled + case <-ctx.Done(): + return nil + case err := <-errChan: + return err + case line, ok := <-lines: + + // channel closed, finished processing + if !ok { + break out + } + + var testOutputLine TestOutputLine + + err := json.Unmarshal(line, &testOutputLine) + if err != nil { + slog.ErrorContext(ctx, "unable to parse json input", "err", err) + return errors.New("unable to parse json input. ensure to use the `-json` flag when running go tests") + } + + tm.AddTestOutput(testOutputLine) + } + } + + if tm.GetTestCount() == 0 { + return errors.New("no tests received, ensure to specify a package to run `go test` with") + } + + return nil +} From d29c1e2f7e2342cba65912b8b8eaa8fd3b8c6408 Mon Sep 17 00:00:00 2001 From: Tim Tattersall Date: Mon, 17 Nov 2025 23:16:59 +1100 Subject: [PATCH 9/9] feat: bump version to 0.12.2 --- internal/sift/version.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/sift/version.go b/internal/sift/version.go index f78195c..ce40638 100644 --- a/internal/sift/version.go +++ b/internal/sift/version.go @@ -1,3 +1,3 @@ package sift -var Version = "v0.12.1" +var Version = "v0.12.2"