//go:build !windows package tui import ( "bufio" "context" "errors" "fmt" "io" "os" "os/exec" "os/signal" "syscall" "testing" "time" tea "charm.land/bubbletea/v2" "gitgud.io/mike/mpv-manager/pkg/config" "gitgud.io/mike/mpv-manager/pkg/constants" "gitgud.io/mike/mpv-manager/pkg/installer" "github.com/stretchr/testify/require" ) func TestProgramSIGTERMDrainsCommittedWorker(t *testing.T) { if os.Getenv("MPV_TUI_SIGNAL_CHILD") == "1" { sandboxConfigHome(t) m := NewModel() m.state, m.installing, m.installOperationType = StateInstalling, true, OpTypeUIChange m.selectedUIApp = &config.InstalledApp{ID: "signal-app", AppName: "MPV", InstallMethod: constants.MethodMPVBinary} m.releaseInfoLoader = func() (*installer.ReleaseInfo, error) { return nil, errors.New("offline fixture") } ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGTERM) defer stop() workerCtx, cancelWorker := context.WithCancel(context.Background()) defer cancelWorker() output, result := make(chan string), make(chan error, 1) entered := make(chan struct{}) guard := launchOperationWorker(workerCtx, output, result, func(ctx context.Context, cr *installer.CommandRunner) error { if err := cr.BeginCommit(); err != nil { return err } close(entered) var byte [1]byte if _, err := io.ReadFull(os.Stdin, byte[:]); err != nil { return err } if err := ctx.Err(); err != nil { return err } output <- "committed signal worker drained" return nil }) m.ownOperation(saveChannelsMsg{outputChan: output, installDone: result, cancel: cancelWorker, commitGuard: guard}) <-entered fmt.Println("SIGNAL_READY") err := m.Run(ctx, tea.WithInput(nil), tea.WithOutput(io.Discard), tea.WithoutRenderer()) require.ErrorIs(t, err, context.Canceled) path, err := jobHistoryFilePath() require.NoError(t, err) records, err := loadJobHistoryRecords(path) require.NoError(t, err) require.Len(t, records, 1) require.Equal(t, "complete", records[0].Status) require.Contains(t, m.installOutput, "committed signal worker drained") return } child := exec.Command(os.Args[0], "-test.run=^TestProgramSIGTERMDrainsCommittedWorker$", "-test.timeout=20s") child.Env = append(os.Environ(), "MPV_TUI_SIGNAL_CHILD=1") stdout, err := child.StdoutPipe() require.NoError(t, err) stdin, err := child.StdinPipe() require.NoError(t, err) child.Stderr = os.Stderr require.NoError(t, child.Start()) t.Cleanup(func() { _ = child.Process.Kill() }) ready := make(chan bool, 1) go func() { scanner := bufio.NewScanner(stdout) for scanner.Scan() { if scanner.Text() == "SIGNAL_READY" { ready <- true } } ready <- false }() select { case ok := <-ready: require.True(t, ok) case <-time.After(10 * time.Second): t.Fatal("child never reached commit") } done := make(chan error, 1) go func() { done <- child.Wait() }() require.NoError(t, child.Process.Signal(syscall.SIGTERM)) select { case err := <-done: t.Fatalf("signal abandoned worker: %v", err) case <-time.After(150 * time.Millisecond): } _, err = stdin.Write([]byte("\n")) require.NoError(t, err) _ = stdin.Close() select { case err := <-done: require.NoError(t, err) case <-time.After(10 * time.Second): t.Fatal("child failed to drain") } }