fix(summer): send a delegate's stderr to the root's error writer through bonfire.ErrWriter (IN-13, quick-261028-rqd)

This commit is contained in:
Jakub Zych
2026-10-28 20:50:39 +01:00
parent f3f87f3cc4
commit 4914cade5a
8 changed files with 145 additions and 29 deletions

View File

@@ -150,13 +150,14 @@ func runAppBinary(ctx context.Context, argv []string, out bonfire.Output) error
// streams, so it sees the terminal: it asks the production question, prints
// styled summaries and animates its widgets. Otherwise its stdout goes
// through out (tests and bonfire.Call roots capture it) and its stderr to
// summer's stderr, so log lines and error blocks never mix into data such
// as --json output.
// the root's error stream (bonfire.ErrWriter), which is os.Stderr for the
// real summer, so log lines and error blocks never mix into data such as
// --json output, and a root built on buffers captures them.
func appStreams(out bonfire.Output, terminal bool) (stdout, stderr io.Writer) {
if terminal {
return os.Stdout, os.Stderr
}
return out, os.Stderr
return out, bonfire.ErrWriter(out)
}
// runAppCmd runs cmd, the app binary started with argv, forwarding SIGINT

View File

@@ -346,6 +346,11 @@ func TestDelegateExitCodePassthrough(t *testing.T) {
if code != 2 {
t.Errorf("exit %d, want the binary's 2", code)
}
// IN-13: the binary's own error block reaches the root's error
// writer.
if n := strings.Count(errOut, "ERROR the app binary failed"); n != 1 {
t.Errorf("the binary's error block reached the root's error writer %d times, want 1:\nstdout %q\nstderr %q", n, out, errOut)
}
for _, s := range []string{out, errOut} {
if strings.Contains(s, " ERROR ") || strings.Contains(s, "summer: migrate") {
t.Errorf("summer printed an error block of its own:\nstdout %q\nstderr %q", out, errOut)
@@ -419,9 +424,9 @@ func TestRunAppCmdMasksSecrets(t *testing.T) {
})
}
// TestDelegateStreams (CR-03): off a terminal the app binary's stdout goes
// through summer's Output and its stderr to summer's stderr, never into
// summer's stdout.
// TestDelegateStreams (CR-03, IN-13): off a terminal the app binary's
// stdout goes through summer's Output and its stderr to the root's error
// writer (bonfire.ErrWriter), never into summer's stdout.
func TestDelegateStreams(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("the fake app binary is a shell script")
@@ -438,23 +443,42 @@ func TestDelegateStreams(t *testing.T) {
t.Fatal(err)
}
t.Chdir(app)
out, err := runSummer(t, "route:list", "--json")
var stdout, stderr syncBuffer
root, err := bonfire.NewRootIO("summer", toolCommands(), strings.NewReader(""), &stdout, &stderr)
if err != nil {
t.Fatalf("route:list: %v\n%s", err, out)
t.Fatal(err)
}
if !strings.Contains(out, "data-line") {
t.Errorf("the binary's stdout did not reach summer's Output: %q", out)
if code := bonfire.Execute(context.Background(), root, []string{"route:list", "--json"}); code != bonfire.ExitOK {
t.Fatalf("route:list: exit %d\nstdout %q\nstderr %q", code, stdout.String(), stderr.String())
}
if strings.Contains(out, "log-line") {
t.Errorf("the binary's stderr landed in summer's stdout: %q", out)
if !strings.Contains(stdout.String(), "data-line") {
t.Errorf("the binary's stdout did not reach summer's Output: %q", stdout.String())
}
if strings.Contains(stdout.String(), "log-line") {
t.Errorf("the binary's stderr landed in summer's stdout: %q", stdout.String())
}
if !strings.Contains(stderr.String(), "log-line") {
t.Errorf("the binary's stderr did not reach the root's error writer: %q", stderr.String())
}
}
// TestAppStreams (CR-03): on a terminal the app binary gets summer's own
// stdout and stderr; off a terminal its stdout goes through the Output and
// its stderr to summer's stderr.
// TestAppStreams (CR-03, IN-13): on a terminal the app binary gets
// summer's own stdout and stderr; off a terminal its stdout goes through the
// Output and its stderr to the Output's error writer (bonfire.ErrWriter),
// which is os.Stderr for the real summer.
func TestAppStreams(t *testing.T) {
var buf bytes.Buffer
var buf, errBuf bytes.Buffer
sep := bonfire.NewOutput(strings.NewReader(""), &buf, &errBuf)
if _, stderr := appStreams(sep, false); stderr != io.Writer(&errBuf) {
t.Errorf("not a terminal: stderr %T, want the Output's error writer", stderr)
}
real, err := bonfire.NewRoot("summer", nil, os.Stdout)
if err != nil {
t.Fatal(err)
}
if real.ErrOrStderr() != os.Stderr {
t.Errorf("the real summer root's error stream is %v, want os.Stderr", real.ErrOrStderr())
}
out := bonfire.NewOutput(strings.NewReader(""), &buf, &buf)
if bonfire.IsTerminal(out) {
t.Fatal("a buffer console reports a terminal")
@@ -464,7 +488,7 @@ func TestAppStreams(t *testing.T) {
t.Errorf("terminal: streams %v, %v; want os.Stdout, os.Stderr", stdout, stderr)
}
stdout, stderr = appStreams(out, false)
if stdout != io.Writer(out) || stderr != os.Stderr {
t.Errorf("not a terminal: streams %v, %v; want the Output, os.Stderr", stdout, stderr)
if stdout != io.Writer(out) || stderr != io.Writer(&buf) {
t.Errorf("not a terminal: streams %T, %T; want the Output and its error writer", stdout, stderr)
}
}