diff --git a/cmp_test.go b/assert_test.go similarity index 55% rename from cmp_test.go rename to assert_test.go index ad4cd75a07fa2b61b49e101b938f00f1bcfa4a70..2f05337e5cc220d540a87be6aa8cad67d4c37c68 100644 --- a/cmp_test.go +++ b/assert_test.go @@ -1,9 +1,14 @@ package websocket_test import ( + "context" + "fmt" "reflect" "github.com/google/go-cmp/cmp" + + "nhooyr.io/websocket" + "nhooyr.io/websocket/wsjson" ) // https://github.com/google/go-cmp/issues/40#issuecomment-328615283 @@ -51,3 +56,44 @@ func structTypes(v reflect.Value, m map[reflect.Type]struct{}) { } } } + +func assertEqualf(exp, act interface{}, f string, v ...interface{}) error { + if diff := cmpDiff(exp, act); diff != "" { + return fmt.Errorf(f+": %v", append(v, diff)...) + } + return nil +} + +func assertJSONEcho(ctx context.Context, c *websocket.Conn, n int) error { + exp := randString(n) + err := wsjson.Write(ctx, c, exp) + if err != nil { + return err + } + + var act interface{} + err = wsjson.Read(ctx, c, &act) + if err != nil { + return err + } + + return assertEqualf(exp, act, "unexpected JSON") +} + +func assertJSONRead(ctx context.Context, c *websocket.Conn, exp interface{}) error { + var act interface{} + err := wsjson.Read(ctx, c, &act) + if err != nil { + return err + } + + return assertEqualf(exp, act, "unexpected JSON") +} + +func randBytes(n int) []byte { + return make([]byte, n) +} + +func randString(n int) string { + return string(randBytes(n)) +} diff --git a/ci/run.sh b/ci/run.sh index 9e47d29162a605692a31a85c8e07a8836b1d9dd6..1e386ff139bc4c850b52fe7e7572a4d56c691b43 100755 --- a/ci/run.sh +++ b/ci/run.sh @@ -6,7 +6,14 @@ set -euo pipefail cd "$(dirname "${0}")" cd "$(git rev-parse --show-toplevel)" +echo "--- fmt" ./ci/fmt.sh + +echo "--- lint" ./ci/lint.sh + +echo "--- test" ./ci/test.sh + +echo "--- wasm" ./ci/wasm.sh diff --git a/ci/test.sh b/ci/test.sh index 81d6f462a70e22aba61e55c291b91066fa8ab225..1f5b5102bda6001d322a9bbfc6b819ccfa84e72e 100755 --- a/ci/test.sh +++ b/ci/test.sh @@ -16,9 +16,13 @@ if [[ ${CI-} ]]; then ) fi -argv+=( - "$@" -) +if [[ $# -gt 0 ]]; then + argv+=( + "$@" + ) +else + argv+=(./...) +fi mkdir -p ci/out/websocket "${argv[@]}" diff --git a/ci/wasm.sh b/ci/wasm.sh index 9894fca69f29ba5f9239a592cdcb2e91d90d2a64..2870365f63adaa311852252a63b18fb604fd3504 100755 --- a/ci/wasm.sh +++ b/ci/wasm.sh @@ -4,7 +4,13 @@ set -euo pipefail cd "$(dirname "${0}")" cd "$(git rev-parse --show-toplevel)" +stdout="$(mktemp -d)/stdout" +mkfifo "$stdout" +go run ./internal/wsecho/cmd > "$stdout" & + +WS_ECHO_SERVER_URL="$(head -n 1 "$stdout")" + GOOS=js GOARCH=wasm go vet ./... go install golang.org/x/lint/golint GOOS=js GOARCH=wasm golint -set_exit_status ./... -GOOS=js GOARCH=wasm go test ./... +GOOS=js GOARCH=wasm go test ./... -args "$WS_ECHO_SERVER_URL" diff --git a/internal/echoserver/echoserver.go b/internal/echoserver/echoserver.go deleted file mode 100644 index 905ede2b5f8d73622b7ad8df4ee05b79b5aa9d35..0000000000000000000000000000000000000000 --- a/internal/echoserver/echoserver.go +++ /dev/null @@ -1,11 +0,0 @@ -package echoserver - -import ( - "net/http" -) - -// EchoServer provides a streaming WebSocket echo server -// for use in tests. -func EchoServer(w http.ResponseWriter, r *http.Request) { - -} diff --git a/internal/wsecho/cmd/main.go b/internal/wsecho/cmd/main.go new file mode 100644 index 0000000000000000000000000000000000000000..9d9dc82bc6cbe64a60be4e6680f2374d6f85d5fb --- /dev/null +++ b/internal/wsecho/cmd/main.go @@ -0,0 +1,21 @@ +// +build !js + +package main + +import ( + "fmt" + "net/http" + "net/http/httptest" + "runtime" + "strings" + + "nhooyr.io/websocket/internal/wsecho" +) + +func main() { + s := httptest.NewServer(http.HandlerFunc(wsecho.Serve)) + wsURL := strings.Replace(s.URL, "http", "ws", 1) + fmt.Printf("%v\n", wsURL) + + runtime.Goexit() +} diff --git a/internal/wsecho/wsecho.go b/internal/wsecho/wsecho.go new file mode 100644 index 0000000000000000000000000000000000000000..1792d0e0b5bdead6e1905b3a1132fcade8f99c47 --- /dev/null +++ b/internal/wsecho/wsecho.go @@ -0,0 +1,73 @@ +// +build !js + +package wsecho + +import ( + "context" + "io" + "log" + "net/http" + "time" + + "nhooyr.io/websocket" +) + +// Serve provides a streaming WebSocket echo server +// for use in tests. +func Serve(w http.ResponseWriter, r *http.Request) { + c, err := websocket.Accept(w, r, &websocket.AcceptOptions{ + Subprotocols: []string{"echo"}, + InsecureSkipVerify: true, + }) + if err != nil { + log.Printf("echo server: failed to accept: %+v", err) + return + } + defer c.Close(websocket.StatusInternalError, "") + + Loop(r.Context(), c) +} + +// Loop echos every msg received from c until an error +// occurs or the context expires. +// The read limit is set to 1 << 40. +func Loop(ctx context.Context, c *websocket.Conn) { + defer c.Close(websocket.StatusInternalError, "") + + c.SetReadLimit(1 << 40) + + ctx, cancel := context.WithTimeout(ctx, time.Minute) + defer cancel() + + b := make([]byte, 32768) + echo := func() error { + typ, r, err := c.Reader(ctx) + if err != nil { + return err + } + + w, err := c.Writer(ctx, typ) + if err != nil { + return err + } + + _, err = io.CopyBuffer(w, r, b) + if err != nil { + return err + } + + err = w.Close() + if err != nil { + return err + } + + return nil + } + + for { + err := echo() + if err != nil { + return + } + } +} diff --git a/websocket_autobahn_python_test.go b/websocket_autobahn_python_test.go index 4e8b588e6a567e3f31c8b1270accc3c30b6f342e..62aa3f8e1ee26446735faf6ea45d8fa5642b55ea 100644 --- a/websocket_autobahn_python_test.go +++ b/websocket_autobahn_python_test.go @@ -20,6 +20,8 @@ import ( "strings" "testing" "time" + + "nhooyr.io/websocket/internal/wsecho" ) // https://github.com/crossbario/autobahn-python/tree/master/wstest @@ -34,7 +36,7 @@ func TestPythonAutobahnServer(t *testing.T) { t.Logf("server handshake failed: %+v", err) return } - echoLoop(r.Context(), c) + wsecho.Loop(r.Context(), c) })) defer s.Close() @@ -186,7 +188,7 @@ func TestPythonAutobahnClientOld(t *testing.T) { if err != nil { t.Fatal(err) } - echoLoop(ctx, c) + wsecho.Loop(ctx, c) }() } diff --git a/websocket_bench_test.go b/websocket_bench_test.go index 9598e87339af996106355cbeb34dd6bc78eadb66..ff2fd70416da5243cf90fb68678e914e2b645836 100644 --- a/websocket_bench_test.go +++ b/websocket_bench_test.go @@ -13,6 +13,7 @@ import ( "time" "nhooyr.io/websocket" + "nhooyr.io/websocket/internal/wsecho" ) func BenchmarkConn(b *testing.B) { @@ -54,7 +55,7 @@ func benchConn(b *testing.B, echo, stream bool, size int) { return err } if echo { - echoLoop(r.Context(), c) + wsecho.Loop(r.Context(), c) } else { discardLoop(r.Context(), c) } diff --git a/websocket_js.go b/websocket_js.go index aab104945d2b4d4e9de7bd0bc5f75a0a4b45abe5..a83dc87219336f451d4e584e2cca71eb65cbefa3 100644 --- a/websocket_js.go +++ b/websocket_js.go @@ -59,7 +59,7 @@ func (c *Conn) init() { }) runtime.SetFinalizer(c, func(c *Conn) { - c.ws.Close(int(StatusInternalError), "internal error") + c.ws.Close(int(StatusInternalError), "") c.close(errors.New("connection garbage collected")) }) } @@ -133,12 +133,10 @@ func (c *Conn) Close(code StatusCode, reason string) error { return fmt.Errorf("already closed: %w", c.closeErr) } - cerr := CloseError{ + err := fmt.Errorf("sent close frame: %v", CloseError{ Code: code, Reason: reason, - } - - err := fmt.Errorf("sent close frame: %v", cerr) + }) err2 := c.ws.Close(int(code), reason) if err2 != nil { @@ -146,7 +144,7 @@ func (c *Conn) Close(code StatusCode, reason string) error { } c.close(err) - if !xerrors.Is(c.closeErr, cerr) { + if !xerrors.Is(c.closeErr, err) { return xerrors.Errorf("failed to close websocket: %w", err) } diff --git a/websocket_js_test.go b/websocket_js_test.go index 332c962815ff5c23850a4df4c5631a362e53ea7d..56058cee49d6e12e55a3586087cde5a1742e5aa8 100644 --- a/websocket_js_test.go +++ b/websocket_js_test.go @@ -2,19 +2,40 @@ package websocket_test import ( "context" + "net/http" + "os" "testing" "time" "nhooyr.io/websocket" ) +var wsEchoServerURL = os.Args[1] + func TestWebSocket(t *testing.T) { t.Parallel() - _, _, err := websocket.Dial(context.Background(), "ws://localhost:8081", nil) + ctx, cancel := context.WithTimeout(context.Background(), time.Second*10) + defer cancel() + + c, resp, err := websocket.Dial(ctx, wsEchoServerURL, nil) if err != nil { t.Fatal(err) } + defer c.Close(websocket.StatusInternalError, "") - time.Sleep(time.Second) + err = assertEqualf(&http.Response{}, resp, "unexpected http response") + if err != nil { + t.Fatal(err) + } + + err = assertJSONEcho(ctx, c, 4096) + if err != nil { + t.Fatal(err) + } + + err = c.Close(websocket.StatusNormalClosure, "") + if err != nil { + t.Fatal(err) + } } diff --git a/websocket_test.go b/websocket_test.go index eedef845d3ca9b12e065522fd0f5b11865df07d4..838eb8e7c64c4750519609913e5de8c66a8c0ebb 100644 --- a/websocket_test.go +++ b/websocket_test.go @@ -29,6 +29,7 @@ import ( "go.uber.org/multierr" "nhooyr.io/websocket" + "nhooyr.io/websocket/internal/wsecho" "nhooyr.io/websocket/wsjson" "nhooyr.io/websocket/wspb" ) @@ -966,7 +967,7 @@ func TestAutobahn(t *testing.T) { ctx := r.Context() if testingClient { - echoLoop(r.Context(), c) + wsecho.Loop(r.Context(), c) return nil } @@ -1007,7 +1008,7 @@ func TestAutobahn(t *testing.T) { return } - echoLoop(ctx, c) + wsecho.Loop(ctx, c) } t.Run(name, func(t *testing.T) { t.Parallel() @@ -1849,47 +1850,6 @@ func TestAutobahn(t *testing.T) { }) } -func echoLoop(ctx context.Context, c *websocket.Conn) { - defer c.Close(websocket.StatusInternalError, "") - - c.SetReadLimit(1 << 40) - - ctx, cancel := context.WithTimeout(ctx, time.Minute) - defer cancel() - - b := make([]byte, 32768) - echo := func() error { - typ, r, err := c.Reader(ctx) - if err != nil { - return err - } - - w, err := c.Writer(ctx, typ) - if err != nil { - return err - } - - _, err = io.CopyBuffer(w, r, b) - if err != nil { - return err - } - - err = w.Close() - if err != nil { - return err - } - - return nil - } - - for { - err := echo() - if err != nil { - return - } - } -} - func assertCloseStatus(err error, code websocket.StatusCode) error { var cerr websocket.CloseError if !errors.As(err, &cerr) { @@ -1898,24 +1858,6 @@ func assertCloseStatus(err error, code websocket.StatusCode) error { return assertEqualf(code, cerr.Code, "unexpected status code") } -func assertJSONRead(ctx context.Context, c *websocket.Conn, exp interface{}) (err error) { - var act interface{} - err = wsjson.Read(ctx, c, &act) - if err != nil { - return err - } - - return assertEqualf(exp, act, "unexpected JSON") -} - -func randBytes(n int) []byte { - return make([]byte, n) -} - -func randString(n int) string { - return string(randBytes(n)) -} - func assertEcho(ctx context.Context, c *websocket.Conn, typ websocket.MessageType, n int) error { p := randBytes(n) err := c.Write(ctx, typ, p) @@ -1949,13 +1891,6 @@ func assertSubprotocol(c *websocket.Conn, exp string) error { return assertEqualf(exp, c.Subprotocol(), "unexpected subprotocol") } -func assertEqualf(exp, act interface{}, f string, v ...interface{}) error { - if diff := cmpDiff(exp, act); diff != "" { - return fmt.Errorf(f+": %v", append(v, diff)...) - } - return nil -} - func assertNetConnRead(r io.Reader, exp string) error { act := make([]byte, len(exp)) _, err := r.Read(act) diff --git a/wsjson/wsjson_js.go b/wsjson/wsjson_js.go index 2e6074ad5270935773d2f0b6a83d04595dbf40cd..5b88ce3ba5e58113e56e19f3967f3319b7c042a4 100644 --- a/wsjson/wsjson_js.go +++ b/wsjson/wsjson_js.go @@ -54,5 +54,5 @@ func write(ctx context.Context, c *websocket.Conn, v interface{}) error { return err } - return c.Write(ctx, websocket.MessageBinary, b) + return c.Write(ctx, websocket.MessageText, b) }