package installer import ( "context" "fmt" "os" "os/exec" "path/filepath" "runtime" "strings" "testing" "gitgud.io/mike/mpv-manager/pkg/constants" ) // commandRunnerHelper emits exact process output without depending on tools // installed on the host. TestMain dispatches it before creating its sandbox. func commandRunnerHelper() (bool, int) { for index, arg := range os.Args { if arg != "command-runner-helper" || index+1 >= len(os.Args) { continue } switch os.Args[index+1] { case "streams": fmt.Fprintln(os.Stdout, "stdout-line") fmt.Fprintln(os.Stderr, "stderr-line") case "hello": fmt.Fprintln(os.Stdout, "hello") case "fail": return true, 7 default: return true, 2 } return true, 0 } return false, 0 } func commandRunnerHelperArgs(mode string) []string { return []string{"--", "command-runner-helper", mode} } func TestOutputCaptureBoundsUnterminatedLinesAndHandlesCarriageReturns(t *testing.T) { output := make(chan string, 1000) capture := &outputCapture{outputChan: output} chunk := []byte(strings.Repeat("x", 64*1024)) for range 64 { n, err := capture.Write(chunk) if err != nil || n != len(chunk) || len(capture.pending) >= constants.MaxOutputLineBytes { t.Fatalf("capture exceeded partial-line budget: n=%d pending=%d err=%v", n, len(capture.pending), err) } } capture.Flush() for len(output) > 0 { if len(<-output) > constants.MaxOutputLineBytes { t.Fatal("emitted line exceeded byte budget") } } _, _ = capture.Write([]byte("10%\r20%\r30%\r\n")) for _, expected := range []string{"10%", "20%", "30%"} { if actual := <-output; actual != expected { t.Fatalf("got %q, want %q", actual, expected) } } } func TestRunCommandWithOutputStreamsEachLineOnce(t *testing.T) { output := make(chan string, 16) runner := NewCommandRunner(output, nil) cmd := exec.Command(os.Args[0], commandRunnerHelperArgs("streams")...) if err := runner.RunCommandWithOutput(cmd); err != nil { t.Fatalf("RunCommandWithOutput failed: %v", err) } counts := map[string]int{} for { select { case line := <-output: counts[line]++ default: if counts["stdout-line"] != 1 || counts["stderr-line"] != 1 { t.Fatalf("expected each stream line once, got %#v", counts) } return } } } func TestOutputCaptureBuffersArbitraryWriteChunks(t *testing.T) { output := make(chan string, 4) capture := &outputCapture{outputChan: output} if _, err := capture.Write([]byte("partial")); err != nil { t.Fatal(err) } select { case line := <-output: t.Fatalf("unterminated chunk emitted early as %q", line) default: } if _, err := capture.Write([]byte("-line\r\nsecond")); err != nil { t.Fatal(err) } if line := <-output; line != "partial-line" { t.Fatalf("first line = %q", line) } capture.Flush() if line := <-output; line != "second" { t.Fatalf("flushed line = %q", line) } } func TestCommandRunnerRunCommand(t *testing.T) { output := make(chan string, 16) runner := NewCommandRunner(output, nil) if err := runner.RunCommand(os.Args[0], commandRunnerHelperArgs("hello")...); err != nil { t.Fatalf("RunCommand helper failed: %v", err) } errChan := make(chan error, 1) failing := NewCommandRunner(nil, errChan) if err := failing.RunCommand(os.Args[0], commandRunnerHelperArgs("fail")...); err == nil { t.Fatal("RunCommand with a failing child should fail") } select { case err := <-errChan: if err == nil { t.Fatal("expected the failure on the error channel") } default: t.Fatal("expected the failure to be reported on the error channel") } } func TestCommandRunnerRunCommandOutput(t *testing.T) { runner := NewCommandRunner(nil, nil) out, err := runner.RunCommandOutput(os.Args[0], commandRunnerHelperArgs("hello")...) if err != nil { t.Fatalf("RunCommandOutput helper failed: %v", err) } if string(out) != "hello\n" { t.Fatalf("RunCommandOutput helper = %q, want %q", out, "hello\n") } if _, err := runner.RunCommandOutput(os.Args[0], commandRunnerHelperArgs("fail")...); err == nil { t.Fatal("RunCommandOutput with a failing child should fail") } if _, err := runner.RunCommandOutput("mpv-manager-definitely-not-a-command"); err == nil { t.Fatal("RunCommandOutput with a missing binary should fail") } } func TestCommandRunnerRunShellCommandAndRunCommands(t *testing.T) { output := make(chan string, 32) runner := NewCommandRunner(output, nil) failScript := "exit 3" if runtime.GOOS == "windows" { failScript = "exit /b 3" } if err := runner.RunShellCommand("echo shell-ok"); err != nil { t.Fatalf("RunShellCommand failed: %v", err) } if err := runner.RunShellCommand(`echo "quoted shell value"`); err != nil { t.Fatalf("RunShellCommand with embedded quotes failed: %v", err) } helperPath := filepath.Join(t.TempDir(), "command runner", "helper.exe") helperBytes, err := os.ReadFile(os.Args[0]) if err != nil { t.Fatal(err) } if err := os.MkdirAll(filepath.Dir(helperPath), 0755); err != nil { t.Fatal(err) } if err := os.WriteFile(helperPath, helperBytes, 0755); err != nil { t.Fatal(err) } helperScript := "'" + strings.ReplaceAll(helperPath, "'", "'\\''") + "' -- command-runner-helper hello" if runtime.GOOS == "windows" { helperScript = `"` + helperPath + `" -- command-runner-helper hello` } if err := runner.RunShellCommand(helperScript); err != nil { t.Fatalf("RunShellCommand with a quoted executable path failed: %v", err) } if err := runner.RunShellCommand(failScript); err == nil { t.Fatal("RunShellCommand(exit 3) should fail") } if err := runner.RunCommands([]string{"echo one", "echo two"}); err != nil { t.Fatalf("RunCommands failed: %v", err) } if err := runner.RunCommands([]string{"echo before-failure", failScript, "echo after-failure"}); err == nil { t.Fatal("RunCommands with a failing command should fail") } lines := drainOutput(output) quotedOutput := "quoted shell value" if runtime.GOOS == "windows" { quotedOutput = `"quoted shell value"` } for _, expected := range []string{"shell-ok", quotedOutput, "hello", "one", "two", "before-failure"} { found := false for _, line := range lines { if line == expected { found = true } if line == "after-failure" { t.Fatal("RunCommands continued after a failed command") } } if !found { t.Fatalf("shell did not emit %q: %v", expected, lines) } } } func TestCommandRunnerContextAndCancellation(t *testing.T) { runner := NewCommandRunner(nil, nil) if !runner.InteractiveSudoAllowed() { t.Fatal("ordinary command runners should allow an intentional CLI sudo prompt") } runner.SetInteractiveSudoAllowed(false) if runner.InteractiveSudoAllowed() { t.Fatal("SetInteractiveSudoAllowed(false) was ignored") } if runner.IsCancelled() { t.Fatal("fresh runner should not be cancelled") } if err := runner.CheckCancelled(); err != nil { t.Fatalf("CheckCancelled on fresh runner: %v", err) } ctx, cancel := context.WithCancel(context.Background()) runner.SetContext(ctx) if runner.Context() != ctx { t.Fatal("SetContext did not install the context") } cancel() if !runner.IsCancelled() { t.Fatal("runner should report cancelled after cancel()") } if err := runner.CheckCancelled(); err == nil { t.Fatal("CheckCancelled should fail after cancel()") } if err := runner.RunCommands([]string{"echo never-runs"}); err == nil { t.Fatal("RunCommands should stop on a cancelled context") } withCtx := NewCommandRunnerWithContext(ctx, nil, nil) if !withCtx.IsCancelled() { t.Fatal("NewCommandRunnerWithContext should keep the cancelled context") } }