Skip to content

Commit 99745a5

Browse files
support plain output when streams are redirected
1 parent 5712ad5 commit 99745a5

3 files changed

Lines changed: 99 additions & 3 deletions

File tree

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ require (
66
github.com/charmbracelet/bubbles v0.21.0
77
github.com/charmbracelet/bubbletea v1.3.10
88
github.com/charmbracelet/lipgloss v1.1.0
9+
github.com/charmbracelet/x/ansi v0.11.4
910
github.com/goccy/go-json v0.10.5
1011
github.com/itchyny/gojq v0.12.18
1112
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c
@@ -19,7 +20,6 @@ require (
1920
require (
2021
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
2122
github.com/charmbracelet/colorprofile v0.4.1 // indirect
22-
github.com/charmbracelet/x/ansi v0.11.4 // indirect
2323
github.com/charmbracelet/x/cellbuf v0.0.14 // indirect
2424
github.com/charmbracelet/x/term v0.2.2 // indirect
2525
github.com/clipperhouse/displaywidth v0.7.0 // indirect

main_test.go

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
package main
2+
3+
import (
4+
"bytes"
5+
"context"
6+
"fmt"
7+
"os"
8+
"os/exec"
9+
"path/filepath"
10+
"runtime"
11+
"strings"
12+
"testing"
13+
"time"
14+
)
15+
16+
func TestLocalTestRedirectedOutput(t *testing.T) {
17+
dir := t.TempDir()
18+
binary := filepath.Join(dir, "bootdev.exe")
19+
if output, err := exec.Command("go", "build", "-o", binary, ".").CombinedOutput(); err != nil {
20+
t.Fatalf("build CLI: %v\n%s", err, output)
21+
}
22+
config := filepath.Join(dir, "config.yaml")
23+
if err := os.WriteFile(config, []byte("{}\n"), 0o600); err != nil {
24+
t.Fatal(err)
25+
}
26+
27+
command := `printf '\033[31mstdout-marker\033[0m\n'; printf 'stderr-marker\n' >&2`
28+
if runtime.GOOS == "windows" {
29+
command = `[Console]::Out.WriteLine([char]27 + '[31mstdout-marker' + [char]27 + '[0m'); [Console]::Error.WriteLine('stderr-marker')`
30+
}
31+
for _, tt := range []struct {
32+
name string
33+
fail bool
34+
verbose bool
35+
}{
36+
{"pass verbose", false, true},
37+
{"fail verbose", true, true},
38+
{"pass compact", false, false},
39+
{"fail compact", true, false},
40+
} {
41+
t.Run(tt.name, func(t *testing.T) {
42+
wantExit := 0
43+
if tt.fail {
44+
wantExit = 1
45+
}
46+
manifest := filepath.Join(t.TempDir(), "cli.yaml")
47+
data := fmt.Sprintf("allowedOperatingSystems: [%s]\nsteps:\n - description: Output check\n cliCommand:\n command: %s\n tests:\n - exitCode: %d\n", runtime.GOOS, command, wantExit)
48+
if err := os.WriteFile(manifest, []byte(data), 0o600); err != nil {
49+
t.Fatal(err)
50+
}
51+
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
52+
defer cancel()
53+
args := []string{"--config", config, "local-test", manifest}
54+
if tt.verbose {
55+
args = append(args, "--verbose")
56+
}
57+
cmd := exec.CommandContext(ctx, binary, args...)
58+
var stdout, stderr bytes.Buffer
59+
cmd.Stdout, cmd.Stderr = &stdout, &stderr
60+
err := cmd.Run()
61+
if cmd.ProcessState == nil || cmd.ProcessState.ExitCode() != wantExit {
62+
t.Fatalf("exit status: %v; stdout: %s; stderr: %s", err, &stdout, &stderr)
63+
}
64+
output := stdout.String()
65+
if strings.Contains(output+stderr.String(), "\x1b") || strings.Contains(stderr.String(), "TTY") {
66+
t.Fatalf("terminal output leaked: stdout=%q stderr=%q", output, stderr.String())
67+
}
68+
if strings.Count(output, "Output check") != 1 {
69+
t.Fatalf("expected one final report: %s", output)
70+
}
71+
for _, diagnostic := range []string{"Command stdout:\n\nstdout-marker", "Command stderr:\n\nstderr-marker"} {
72+
if got, want := strings.Contains(output, diagnostic), tt.verbose || tt.fail; got != want {
73+
t.Fatalf("diagnostic %q present = %t, want %t; output: %s", diagnostic, got, want, output)
74+
}
75+
}
76+
if tt.fail {
77+
if !strings.Contains(stderr.String(), "local checks failed") {
78+
t.Fatalf("missing failure error: %s", &stderr)
79+
}
80+
} else if stderr.Len() != 0 || !strings.Contains(output, "All tests passed!") {
81+
t.Fatalf("unexpected success output: stdout=%q stderr=%q", output, stderr.String())
82+
}
83+
})
84+
}
85+
}

render/render.go

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,9 @@ import (
99
"github.com/bootdotdev/bootdev/messages"
1010
tea "github.com/charmbracelet/bubbletea"
1111
"github.com/charmbracelet/lipgloss"
12+
"github.com/charmbracelet/x/ansi"
1213
"github.com/spf13/viper"
14+
"golang.org/x/term"
1315
)
1416

1517
var (
@@ -107,7 +109,12 @@ func (m rootModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
107109
func StartRenderer(isSubmit bool, verbose bool, showOmitLessonIDTip bool) (func(tea.Msg), func(api.LessonSubmissionEvent)) {
108110
m := initModel(isSubmit, verbose)
109111
m.showOmitLessonIDTip = showOmitLessonIDTip
110-
p := tea.NewProgram(m, tea.WithoutSignalHandler())
112+
interactive := term.IsTerminal(int(os.Stdin.Fd())) && term.IsTerminal(int(os.Stdout.Fd()))
113+
options := []tea.ProgramOption{tea.WithoutSignalHandler(), tea.WithInput(nil)}
114+
if !interactive {
115+
options = append(options, tea.WithoutRenderer())
116+
}
117+
p := tea.NewProgram(m, options...)
111118
done := make(chan struct{})
112119

113120
go func() {
@@ -117,7 +124,11 @@ func StartRenderer(isSubmit bool, verbose bool, showOmitLessonIDTip bool) (func(
117124
} else if r, ok := model.(rootModel); ok {
118125
r.clear = false
119126
r.finalized = true
120-
fmt.Fprint(os.Stdout, r.View())
127+
output := r.View()
128+
if !interactive {
129+
output = ansi.Strip(output)
130+
}
131+
fmt.Fprint(os.Stdout, output)
121132
}
122133
}()
123134

0 commit comments

Comments
 (0)