Skip to content
This repository was archived by the owner on Jan 30, 2025. It is now read-only.

Commit b704a91

Browse files
Ivan Mirićinancgumus
authored andcommitted
Wait for browser process to exit before returning first error from stderr
This ensures we don't return on the first error we see, since they could be non-fatal, but actually wait for the browser process to exit. Fixes #614
1 parent 1f4392b commit b704a91

File tree

2 files changed

+224
-27
lines changed

2 files changed

+224
-27
lines changed

common/browser_process.go

Lines changed: 37 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -179,41 +179,51 @@ func execute(
179179
return command{cmd, done, stdout, stderr}, nil
180180
}
181181

182-
// parseDevToolsURL grabs the websocket address from chrome's output and returns it.
183-
func parseDevToolsURL(ctx context.Context, cmd command) (wsURL string, _ error) {
184-
type result struct {
185-
devToolsURL string
186-
err error
187-
}
188-
c := make(chan result, 1)
182+
// parseDevToolsURL grabs the WebSocket address from Chrome's output and returns
183+
// it. If the process ends abruptly, it will return the first error from stderr.
184+
func parseDevToolsURL(ctx context.Context, cmd command) (string, error) { //nolint: cyclop
185+
var (
186+
lines = make(chan string)
187+
errCh = make(chan error)
188+
)
189189
go func() {
190-
const urlPrefix = "DevTools listening on "
191190
scanner := bufio.NewScanner(cmd.stderr)
192-
193191
for scanner.Scan() {
194-
line := scanner.Text()
192+
lines <- scanner.Text()
193+
}
194+
if err := scanner.Err(); err != nil {
195+
errCh <- err
196+
}
197+
}()
198+
199+
const urlPrefix = "DevTools listening on "
200+
var stdErr error
201+
202+
preferStdErr := func(e error) error {
203+
if stdErr != nil {
204+
return stdErr
205+
}
206+
return e
207+
}
208+
209+
for {
210+
select {
211+
case line := <-lines:
195212
if strings.HasPrefix(line, urlPrefix) {
196-
c <- result{
197-
strings.TrimPrefix(strings.TrimSpace(line), urlPrefix),
198-
nil,
199-
}
200-
return
213+
return strings.TrimPrefix(strings.TrimSpace(line), urlPrefix), nil
201214
}
202-
if strings.Contains(line, ":ERROR:") {
215+
// TODO: Capture more than one error?
216+
if stdErr == nil && strings.Contains(line, ":ERROR:") {
203217
if i := strings.Index(line, "] "); i > 0 {
204-
c <- result{"", errors.New(line[i+2:])}
205-
return
218+
stdErr = errors.New(line[i+2:])
206219
}
207220
}
221+
case err := <-errCh:
222+
return "", preferStdErr(err)
223+
case <-cmd.done:
224+
return "", preferStdErr(errors.New("browser process ended unexpectedly"))
225+
case <-ctx.Done():
226+
return "", preferStdErr(fmt.Errorf("%w", ctx.Err()))
208227
}
209-
if err := scanner.Err(); err != nil {
210-
c <- result{"", err}
211-
}
212-
}()
213-
select {
214-
case r := <-c:
215-
return r.devToolsURL, r.err
216-
case <-ctx.Done():
217-
return "", fmt.Errorf("%w", ctx.Err())
218228
}
219229
}

common/browser_process_test.go

Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
package common
2+
3+
import (
4+
"context"
5+
"io"
6+
"testing"
7+
"time"
8+
9+
"github.com/stretchr/testify/assert"
10+
"github.com/stretchr/testify/require"
11+
)
12+
13+
type mockCommand struct {
14+
*command
15+
cancelFn context.CancelFunc
16+
}
17+
18+
type mockReader struct {
19+
lines []string
20+
hook func()
21+
err error
22+
}
23+
24+
func (r *mockReader) Read(p []byte) (n int, err error) {
25+
if r.hook != nil {
26+
// Allow some time for the read to be processed
27+
time.AfterFunc(100*time.Millisecond, r.hook)
28+
r.hook = nil // Ensure the hook only runs once
29+
}
30+
if len(r.lines) == 0 {
31+
return 0, io.EOF
32+
}
33+
n = copy(p, []byte(r.lines[0]+"\n"))
34+
r.lines = r.lines[1:]
35+
36+
return n, r.err
37+
}
38+
39+
func TestParseDevToolsURL(t *testing.T) {
40+
t.Parallel()
41+
42+
testCases := []struct {
43+
name string
44+
stderr []string
45+
readErr error
46+
readHook func(c *mockCommand)
47+
assert func(t *testing.T, wsURL string, err error)
48+
}{
49+
{
50+
name: "ok/no_error",
51+
stderr: []string{
52+
`DevTools listening on ws://127.0.0.1:41315/devtools/browser/d1d3f8eb-b362-4f12-9370-bd25778d0da7`,
53+
},
54+
assert: func(t *testing.T, wsURL string, err error) {
55+
t.Helper()
56+
require.NoError(t, err)
57+
assert.Equal(t, "ws://127.0.0.1:41315/devtools/browser/d1d3f8eb-b362-4f12-9370-bd25778d0da7", wsURL)
58+
},
59+
},
60+
{
61+
name: "ok/non-fatal_error",
62+
stderr: []string{
63+
`[23400:23418:1028/115455.877614:ERROR:bus.cc(399)] Failed to ` +
64+
`connect to the bus: Could not parse server address: ` +
65+
`Unknown address type (examples of valid types are "tcp" ` +
66+
`and on UNIX "unix")`,
67+
"",
68+
`DevTools listening on ws://127.0.0.1:41315/devtools/browser/d1d3f8eb-b362-4f12-9370-bd25778d0da7`,
69+
},
70+
assert: func(t *testing.T, wsURL string, err error) {
71+
t.Helper()
72+
require.NoError(t, err)
73+
assert.Equal(t, "ws://127.0.0.1:41315/devtools/browser/d1d3f8eb-b362-4f12-9370-bd25778d0da7", wsURL)
74+
},
75+
},
76+
{
77+
name: "err/fatal-eof",
78+
stderr: []string{
79+
`[6497:6497:1013/103521.932979:ERROR:ozone_platform_x11` +
80+
`.cc(247)] Missing X server or $DISPLAY` + "\n",
81+
},
82+
readErr: io.ErrUnexpectedEOF,
83+
assert: func(t *testing.T, wsURL string, err error) {
84+
t.Helper()
85+
require.Empty(t, wsURL)
86+
assert.EqualError(t, err, "Missing X server or $DISPLAY")
87+
},
88+
},
89+
{
90+
name: "err/fatal-eof-no_stderr",
91+
stderr: []string{""},
92+
readErr: io.ErrUnexpectedEOF,
93+
assert: func(t *testing.T, wsURL string, err error) {
94+
t.Helper()
95+
require.Empty(t, wsURL)
96+
assert.EqualError(t, err, "unexpected EOF")
97+
},
98+
},
99+
{
100+
// Ensure any error found on stderr is returned first.
101+
name: "err/fatal-premature_cmd_done-stderr",
102+
stderr: []string{
103+
`[6497:6497:1013/103521.932979:ERROR:ozone_platform_x11` +
104+
`.cc(247)] Missing X server or $DISPLAY` + "\n",
105+
},
106+
readHook: func(c *mockCommand) { close(c.done) },
107+
assert: func(t *testing.T, wsURL string, err error) {
108+
t.Helper()
109+
require.Empty(t, wsURL)
110+
assert.EqualError(t, err, "Missing X server or $DISPLAY")
111+
},
112+
},
113+
{
114+
// If there's no error on stderr, return a generic error.
115+
name: "err/fatal-premature_cmd_done-no_stderr",
116+
stderr: []string{""},
117+
readHook: func(c *mockCommand) { close(c.done) },
118+
assert: func(t *testing.T, wsURL string, err error) {
119+
t.Helper()
120+
require.Empty(t, wsURL)
121+
assert.EqualError(t, err, "browser process ended unexpectedly")
122+
},
123+
},
124+
{
125+
name: "err/fatal-premature_ctx_cancel-stderr",
126+
stderr: []string{
127+
`[6497:6497:1013/103521.932979:ERROR:ozone_platform_x11` +
128+
`.cc(247)] Missing X server or $DISPLAY` + "\n",
129+
},
130+
readHook: func(c *mockCommand) { c.cancelFn() },
131+
assert: func(t *testing.T, wsURL string, err error) {
132+
t.Helper()
133+
require.Empty(t, wsURL)
134+
assert.EqualError(t, err, "Missing X server or $DISPLAY")
135+
},
136+
},
137+
{
138+
name: "err/fatal-premature_ctx_cancel-no_stderr",
139+
stderr: []string{""},
140+
readHook: func(c *mockCommand) { c.cancelFn() },
141+
assert: func(t *testing.T, wsURL string, err error) {
142+
t.Helper()
143+
require.Empty(t, wsURL)
144+
assert.EqualError(t, err, "context canceled")
145+
},
146+
},
147+
}
148+
149+
for _, tc := range testCases {
150+
tc := tc
151+
t.Run(tc.name, func(t *testing.T) {
152+
t.Parallel()
153+
154+
ctx, cancel := context.WithCancel(context.Background())
155+
t.Cleanup(cancel)
156+
157+
var cmd *mockCommand
158+
mr := mockReader{lines: tc.stderr, err: tc.readErr}
159+
if tc.readHook != nil {
160+
mr.hook = func() { tc.readHook(cmd) }
161+
}
162+
cmd = &mockCommand{&command{done: make(chan struct{}), stderr: &mr}, cancel}
163+
164+
timeout := time.Second
165+
timer := time.NewTimer(timeout)
166+
t.Cleanup(func() { _ = timer.Stop() })
167+
168+
var (
169+
done = make(chan struct{})
170+
wsURL string
171+
err error
172+
)
173+
174+
go func() {
175+
wsURL, err = parseDevToolsURL(ctx, *cmd.command)
176+
close(done)
177+
}()
178+
179+
select {
180+
case <-done:
181+
tc.assert(t, wsURL, err)
182+
case <-timer.C:
183+
t.Errorf("test timed out after %s", timeout)
184+
}
185+
})
186+
}
187+
}

0 commit comments

Comments
 (0)