mirror of
https://github.com/sourcegraph/jsonrpc2.git
synced 2026-06-16 04:04:56 +02:00
304 lines
7.3 KiB
Go
304 lines
7.3 KiB
Go
package jsonrpc2_test
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"log"
|
|
"net"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/sourcegraph/jsonrpc2"
|
|
)
|
|
|
|
func TestConn(t *testing.T) {
|
|
|
|
t.Run("closes when context is done", func(t *testing.T) {
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
|
|
connA, connB := Pipe(ctx, noopHandler{}, noopHandler{})
|
|
defer connA.Close()
|
|
defer connB.Close()
|
|
|
|
cancel()
|
|
<-connA.DisconnectNotify()
|
|
|
|
got := connA.Close()
|
|
want := jsonrpc2.ErrClosed
|
|
if got != want {
|
|
t.Fatalf("got %v, want %v", got, want)
|
|
}
|
|
})
|
|
|
|
t.Run("cancels context when closed", func(t *testing.T) {
|
|
ctxCanceled := make(chan struct{})
|
|
|
|
handler := handlerFunc(func(ctx context.Context, c *jsonrpc2.Conn, r *jsonrpc2.Request) {
|
|
// Block until the context is canceled.
|
|
<-ctx.Done()
|
|
close(ctxCanceled)
|
|
})
|
|
|
|
connA, connB := Pipe(context.Background(), noopHandler{}, jsonrpc2.AsyncHandler(handler))
|
|
defer connA.Close()
|
|
defer connB.Close()
|
|
|
|
// Send a notification from connA to connB to trigger connB's handler
|
|
// function.
|
|
if err := connA.Notify(context.Background(), "foo", nil, nil); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Disconnect connA from connB.
|
|
if err := connA.Close(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
select {
|
|
case <-ctxCanceled:
|
|
// Test passed, the handler's context was canceled.
|
|
case <-time.After(time.Second):
|
|
t.Fatal("context not canceled")
|
|
}
|
|
})
|
|
}
|
|
|
|
var paramsTests = []struct {
|
|
sendParams interface{}
|
|
wantParams *json.RawMessage
|
|
}{
|
|
{
|
|
sendParams: nil,
|
|
wantParams: nil,
|
|
},
|
|
{
|
|
sendParams: jsonNull,
|
|
wantParams: &jsonNull,
|
|
},
|
|
{
|
|
sendParams: false,
|
|
wantParams: rawJSONMessage("false"),
|
|
},
|
|
{
|
|
sendParams: 0,
|
|
wantParams: rawJSONMessage("0"),
|
|
},
|
|
{
|
|
sendParams: "",
|
|
wantParams: rawJSONMessage(`""`),
|
|
},
|
|
{
|
|
sendParams: rawJSONMessage(`{"foo":"bar"}`),
|
|
wantParams: rawJSONMessage(`{"foo":"bar"}`),
|
|
},
|
|
}
|
|
|
|
func TestConn_DispatchCall(t *testing.T) {
|
|
for _, test := range paramsTests {
|
|
t.Run(fmt.Sprintf("%s", test.sendParams), func(t *testing.T) {
|
|
testParams(t, test.wantParams, func(c *jsonrpc2.Conn) error {
|
|
_, err := c.DispatchCall(context.Background(), "f", test.sendParams)
|
|
return err
|
|
})
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestConn_Notify(t *testing.T) {
|
|
for _, test := range paramsTests {
|
|
t.Run(fmt.Sprintf("%s", test.sendParams), func(t *testing.T) {
|
|
testParams(t, test.wantParams, func(c *jsonrpc2.Conn) error {
|
|
return c.Notify(context.Background(), "f", test.sendParams)
|
|
})
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestConn_DisconnectNotify(t *testing.T) {
|
|
|
|
t.Run("EOF", func(t *testing.T) {
|
|
connA, connB := net.Pipe()
|
|
c := jsonrpc2.NewConn(context.Background(), jsonrpc2.NewPlainObjectStream(connB), nil)
|
|
// By closing connA, connB receives io.EOF
|
|
if err := connA.Close(); err != nil {
|
|
t.Error(err)
|
|
}
|
|
assertDisconnect(t, c, connB)
|
|
})
|
|
|
|
t.Run("Close", func(t *testing.T) {
|
|
_, connB := net.Pipe()
|
|
c := jsonrpc2.NewConn(context.Background(), jsonrpc2.NewPlainObjectStream(connB), nil)
|
|
if err := c.Close(); err != nil {
|
|
t.Error(err)
|
|
}
|
|
assertDisconnect(t, c, connB)
|
|
})
|
|
|
|
t.Run("Close async", func(t *testing.T) {
|
|
done := make(chan struct{})
|
|
_, connB := net.Pipe()
|
|
c := jsonrpc2.NewConn(context.Background(), jsonrpc2.NewPlainObjectStream(connB), nil)
|
|
go func() {
|
|
if err := c.Close(); err != nil && err != jsonrpc2.ErrClosed {
|
|
t.Error(err)
|
|
}
|
|
close(done)
|
|
}()
|
|
assertDisconnect(t, c, connB)
|
|
<-done
|
|
})
|
|
|
|
t.Run("protocol error", func(t *testing.T) {
|
|
connA, connB := net.Pipe()
|
|
c := jsonrpc2.NewConn(
|
|
context.Background(),
|
|
jsonrpc2.NewPlainObjectStream(connB),
|
|
noopHandler{},
|
|
// Suppress log message. This connection receives an invalid JSON
|
|
// message that causes an error to be written to the logger. We
|
|
// don't want this expected error to appear in os.Stderr though when
|
|
// running tests in verbose mode or when other tests fail.
|
|
jsonrpc2.SetLogger(log.New(io.Discard, "", 0)),
|
|
)
|
|
connA.Write([]byte("invalid json"))
|
|
assertDisconnect(t, c, connB)
|
|
})
|
|
}
|
|
|
|
func TestConn_Close(t *testing.T) {
|
|
cases := []struct {
|
|
name string
|
|
run func(*testing.T, context.Context, *jsonrpc2.Conn)
|
|
}{{
|
|
name: "during Call",
|
|
run: func(t *testing.T, ctx context.Context, conn *jsonrpc2.Conn) {
|
|
ready := make(chan struct{})
|
|
done := make(chan struct{})
|
|
go func() {
|
|
close(ready)
|
|
err := conn.Call(ctx, "m", nil, nil)
|
|
if err != jsonrpc2.ErrClosed {
|
|
t.Errorf("got error %v, want %v", err, jsonrpc2.ErrClosed)
|
|
}
|
|
close(done)
|
|
}()
|
|
// Wait for the request to be sent before we close the connection.
|
|
<-ready
|
|
if err := conn.Close(); err != nil && err != jsonrpc2.ErrClosed {
|
|
t.Error(err)
|
|
}
|
|
<-done
|
|
},
|
|
}, {
|
|
name: "during Wait",
|
|
run: func(t *testing.T, ctx context.Context, conn *jsonrpc2.Conn) {
|
|
call, err := conn.DispatchCall(ctx, "m", nil, nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := conn.Close(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := call.Wait(ctx, nil); err != jsonrpc2.ErrClosed {
|
|
t.Fatal(err)
|
|
}
|
|
},
|
|
}, {
|
|
name: "during Dispatch",
|
|
run: func(t *testing.T, ctx context.Context, conn *jsonrpc2.Conn) {
|
|
if err := conn.Close(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := conn.DispatchCall(ctx, "m", nil, nil); err != jsonrpc2.ErrClosed {
|
|
t.Fatal(err)
|
|
}
|
|
},
|
|
}}
|
|
for _, tc := range cases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
defer cancel()
|
|
|
|
connA, connB := net.Pipe()
|
|
nodeA := jsonrpc2.NewConn(
|
|
ctx,
|
|
jsonrpc2.NewPlainObjectStream(connA), noopHandler{},
|
|
)
|
|
defer nodeA.Close()
|
|
nodeB := jsonrpc2.NewConn(
|
|
ctx,
|
|
jsonrpc2.NewPlainObjectStream(connB),
|
|
noopHandler{},
|
|
)
|
|
defer nodeB.Close()
|
|
|
|
tc.run(t, ctx, nodeB)
|
|
|
|
assertDisconnect(t, nodeB, connB)
|
|
})
|
|
}
|
|
}
|
|
|
|
func testParams(t *testing.T, want *json.RawMessage, fn func(c *jsonrpc2.Conn) error) {
|
|
wg := &sync.WaitGroup{}
|
|
handler := handlerFunc(func(ctx context.Context, conn *jsonrpc2.Conn, r *jsonrpc2.Request) {
|
|
assertRawJSONMessage(t, r.Params, want)
|
|
wg.Done()
|
|
})
|
|
|
|
connA, connB := Pipe(context.Background(), noopHandler{}, handler)
|
|
defer connA.Close()
|
|
defer connB.Close()
|
|
|
|
wg.Add(1)
|
|
if err := fn(connA); err != nil {
|
|
t.Error(err)
|
|
}
|
|
wg.Wait()
|
|
}
|
|
|
|
func assertDisconnect(t *testing.T, c *jsonrpc2.Conn, conn io.Writer) {
|
|
select {
|
|
case <-c.DisconnectNotify():
|
|
case <-time.After(200 * time.Millisecond):
|
|
t.Error("no disconnect notification")
|
|
return
|
|
}
|
|
// Assert that conn is closed by trying to write to it.
|
|
_, got := conn.Write(nil)
|
|
want := io.ErrClosedPipe
|
|
if got != want {
|
|
t.Errorf("got %s, want %s", got, want)
|
|
}
|
|
}
|
|
|
|
func assertRawJSONMessage(t *testing.T, got *json.RawMessage, want *json.RawMessage) {
|
|
// Assert pointers.
|
|
if got == nil || want == nil {
|
|
if got != want {
|
|
t.Errorf("pointer: got %s, want %s", got, want)
|
|
}
|
|
return
|
|
}
|
|
{
|
|
// If pointers are not nil, then assert values.
|
|
got := string(*got)
|
|
want := string(*want)
|
|
if got != want {
|
|
t.Errorf("value: got %q, want %q", got, want)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Pipe returns two jsonrpc2.Conn, connected via a synchronous, in-memory, full
|
|
// duplex network connection.
|
|
func Pipe(ctx context.Context, handlerA, handlerB jsonrpc2.Handler) (connA *jsonrpc2.Conn, connB *jsonrpc2.Conn) {
|
|
a, b := net.Pipe()
|
|
connA = jsonrpc2.NewConn(ctx, jsonrpc2.NewPlainObjectStream(a), handlerA)
|
|
connB = jsonrpc2.NewConn(ctx, jsonrpc2.NewPlainObjectStream(b), handlerB)
|
|
return connA, connB
|
|
}
|