Also make (*Connection).Close safe to call from Bind. A jsonrpc2_v2.Server has no good way to report an error from Bind. If the Server saves the error to return from its own Wait method, that might not ever actually happen: Wait waits for in-flight connections to complete, but if some existing connection stays up then Wait will not return. If the Server goes ahead with establishing the connection and installs its own Handler, that Handler needs to decide whether to serve the error from Bind or something more opaque, and at that point Bind may as well return a handler that makes that choice more precisely. If the Server merely logs the error and closes the Connection, then the Bind method itself may as well do that directly too. It seems to me that the only winning move is not to play. Only Bind is in a position to decide how to handle its errors appropriately, so it should not return them to the Server. Updates golang/go#56281. Change-Id: I07dc43ddf31253ce23da21a92d2b6c0f8d4b3afe Reviewed-on: https://go-review.googlesource.com/c/tools/+/443677 Run-TryBot: Bryan Mills <bcmills@google.com> Auto-Submit: Bryan Mills <bcmills@google.com> TryBot-Result: Gopher Robot <gobot@golang.org> Reviewed-by: Alan Donovan <adonovan@google.com> gopls-CI: kokoro <noreply+kokoro@google.com>
391 lines
10 KiB
Go
391 lines
10 KiB
Go
// Copyright 2018 The Go MCP SDK Authors. All rights reserved.
|
|
// Use of this source code is governed by an MIT-style
|
|
// license that can be found in the LICENSE file.
|
|
|
|
package jsonrpc2_test
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"path"
|
|
"reflect"
|
|
"testing"
|
|
|
|
"golang.org/x/tools/internal/event/export/eventtest"
|
|
"github.com/modelcontextprotocol/go-sdk/internal/jsonrpc2"
|
|
"golang.org/x/tools/internal/stack/stacktest"
|
|
)
|
|
|
|
var callTests = []invoker{
|
|
call{"no_args", nil, true},
|
|
call{"one_string", "fish", "got:fish"},
|
|
call{"one_number", 10, "got:10"},
|
|
call{"join", []string{"a", "b", "c"}, "a/b/c"},
|
|
sequence{"notify", []invoker{
|
|
notify{"set", 3},
|
|
notify{"add", 5},
|
|
call{"get", nil, 8},
|
|
}},
|
|
sequence{"preempt", []invoker{
|
|
async{"a", "wait", "a"},
|
|
notify{"unblock", "a"},
|
|
collect{"a", true, false},
|
|
}},
|
|
sequence{"basic cancel", []invoker{
|
|
async{"b", "wait", "b"},
|
|
cancel{"b"},
|
|
collect{"b", nil, true},
|
|
}},
|
|
sequence{"queue", []invoker{
|
|
async{"a", "wait", "a"},
|
|
notify{"set", 1},
|
|
notify{"add", 2},
|
|
notify{"add", 3},
|
|
notify{"add", 4},
|
|
call{"peek", nil, 0}, // accumulator will not have any adds yet
|
|
notify{"unblock", "a"},
|
|
collect{"a", true, false},
|
|
call{"get", nil, 10}, // accumulator now has all the adds
|
|
}},
|
|
sequence{"fork", []invoker{
|
|
async{"a", "fork", "a"},
|
|
notify{"set", 1},
|
|
notify{"add", 2},
|
|
notify{"add", 3},
|
|
notify{"add", 4},
|
|
call{"get", nil, 10}, // fork will not have blocked the adds
|
|
notify{"unblock", "a"},
|
|
collect{"a", true, false},
|
|
}},
|
|
sequence{"concurrent", []invoker{
|
|
async{"a", "fork", "a"},
|
|
notify{"unblock", "a"},
|
|
async{"b", "fork", "b"},
|
|
notify{"unblock", "b"},
|
|
collect{"a", true, false},
|
|
collect{"b", true, false},
|
|
}},
|
|
}
|
|
|
|
type binder struct {
|
|
framer jsonrpc2.Framer
|
|
runTest func(*handler)
|
|
}
|
|
|
|
type handler struct {
|
|
conn *jsonrpc2.Connection
|
|
accumulator int
|
|
waiters chan map[string]chan struct{}
|
|
calls map[string]*jsonrpc2.AsyncCall
|
|
}
|
|
|
|
type invoker interface {
|
|
Name() string
|
|
Invoke(t *testing.T, ctx context.Context, h *handler)
|
|
}
|
|
|
|
type notify struct {
|
|
method string
|
|
params interface{}
|
|
}
|
|
|
|
type call struct {
|
|
method string
|
|
params interface{}
|
|
expect interface{}
|
|
}
|
|
|
|
type async struct {
|
|
name string
|
|
method string
|
|
params interface{}
|
|
}
|
|
|
|
type collect struct {
|
|
name string
|
|
expect interface{}
|
|
fails bool
|
|
}
|
|
|
|
type cancel struct {
|
|
name string
|
|
}
|
|
|
|
type sequence struct {
|
|
name string
|
|
tests []invoker
|
|
}
|
|
|
|
type echo call
|
|
|
|
type cancelParams struct{ ID int64 }
|
|
|
|
func TestConnectionRaw(t *testing.T) {
|
|
testConnection(t, jsonrpc2.RawFramer())
|
|
}
|
|
|
|
func TestConnectionHeader(t *testing.T) {
|
|
testConnection(t, jsonrpc2.HeaderFramer())
|
|
}
|
|
|
|
func testConnection(t *testing.T, framer jsonrpc2.Framer) {
|
|
stacktest.NoLeak(t)
|
|
ctx := eventtest.NewContext(context.Background(), t)
|
|
listener, err := jsonrpc2.NetPipeListener(ctx)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
server, err := jsonrpc2.Serve(ctx, listener, binder{framer, nil})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer func() {
|
|
listener.Close()
|
|
server.Wait()
|
|
}()
|
|
|
|
for _, test := range callTests {
|
|
t.Run(test.Name(), func(t *testing.T) {
|
|
client, err := jsonrpc2.Dial(ctx,
|
|
listener.Dialer(), binder{framer, func(h *handler) {
|
|
defer h.conn.Close()
|
|
ctx := eventtest.NewContext(ctx, t)
|
|
test.Invoke(t, ctx, h)
|
|
if call, ok := test.(*call); ok {
|
|
// also run all simple call tests in echo mode
|
|
(*echo)(call).Invoke(t, ctx, h)
|
|
}
|
|
}})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
client.Wait()
|
|
})
|
|
}
|
|
}
|
|
|
|
func (test notify) Name() string { return test.method }
|
|
func (test notify) Invoke(t *testing.T, ctx context.Context, h *handler) {
|
|
if err := h.conn.Notify(ctx, test.method, test.params); err != nil {
|
|
t.Fatalf("%v:Notify failed: %v", test.method, err)
|
|
}
|
|
}
|
|
|
|
func (test call) Name() string { return test.method }
|
|
func (test call) Invoke(t *testing.T, ctx context.Context, h *handler) {
|
|
results := newResults(test.expect)
|
|
if err := h.conn.Call(ctx, test.method, test.params).Await(ctx, results); err != nil {
|
|
t.Fatalf("%v:Call failed: %v", test.method, err)
|
|
}
|
|
verifyResults(t, test.method, results, test.expect)
|
|
}
|
|
|
|
func (test echo) Invoke(t *testing.T, ctx context.Context, h *handler) {
|
|
results := newResults(test.expect)
|
|
if err := h.conn.Call(ctx, "echo", []interface{}{test.method, test.params}).Await(ctx, results); err != nil {
|
|
t.Fatalf("%v:Echo failed: %v", test.method, err)
|
|
}
|
|
verifyResults(t, test.method, results, test.expect)
|
|
}
|
|
|
|
func (test async) Name() string { return test.name }
|
|
func (test async) Invoke(t *testing.T, ctx context.Context, h *handler) {
|
|
h.calls[test.name] = h.conn.Call(ctx, test.method, test.params)
|
|
}
|
|
|
|
func (test collect) Name() string { return test.name }
|
|
func (test collect) Invoke(t *testing.T, ctx context.Context, h *handler) {
|
|
o := h.calls[test.name]
|
|
results := newResults(test.expect)
|
|
err := o.Await(ctx, results)
|
|
switch {
|
|
case test.fails && err == nil:
|
|
t.Fatalf("%v:Collect was supposed to fail", test.name)
|
|
case !test.fails && err != nil:
|
|
t.Fatalf("%v:Collect failed: %v", test.name, err)
|
|
}
|
|
verifyResults(t, test.name, results, test.expect)
|
|
}
|
|
|
|
func (test cancel) Name() string { return test.name }
|
|
func (test cancel) Invoke(t *testing.T, ctx context.Context, h *handler) {
|
|
o := h.calls[test.name]
|
|
if err := h.conn.Notify(ctx, "cancel", &cancelParams{o.ID().Raw().(int64)}); err != nil {
|
|
t.Fatalf("%v:Collect failed: %v", test.name, err)
|
|
}
|
|
}
|
|
|
|
func (test sequence) Name() string { return test.name }
|
|
func (test sequence) Invoke(t *testing.T, ctx context.Context, h *handler) {
|
|
for _, child := range test.tests {
|
|
child.Invoke(t, ctx, h)
|
|
}
|
|
}
|
|
|
|
// newResults makes a new empty copy of the expected type to put the results into
|
|
func newResults(expect interface{}) interface{} {
|
|
switch e := expect.(type) {
|
|
case []interface{}:
|
|
var r []interface{}
|
|
for _, v := range e {
|
|
r = append(r, reflect.New(reflect.TypeOf(v)).Interface())
|
|
}
|
|
return r
|
|
case nil:
|
|
return nil
|
|
default:
|
|
return reflect.New(reflect.TypeOf(expect)).Interface()
|
|
}
|
|
}
|
|
|
|
// verifyResults compares the results to the expected values
|
|
func verifyResults(t *testing.T, method string, results interface{}, expect interface{}) {
|
|
if expect == nil {
|
|
if results != nil {
|
|
t.Errorf("%v:Got results %+v where none expeted", method, expect)
|
|
}
|
|
return
|
|
}
|
|
val := reflect.Indirect(reflect.ValueOf(results)).Interface()
|
|
if !reflect.DeepEqual(val, expect) {
|
|
t.Errorf("%v:Results are incorrect, got %+v expect %+v", method, val, expect)
|
|
}
|
|
}
|
|
|
|
func (b binder) Bind(ctx context.Context, conn *jsonrpc2.Connection) jsonrpc2.ConnectionOptions {
|
|
h := &handler{
|
|
conn: conn,
|
|
waiters: make(chan map[string]chan struct{}, 1),
|
|
calls: make(map[string]*jsonrpc2.AsyncCall),
|
|
}
|
|
h.waiters <- make(map[string]chan struct{})
|
|
if b.runTest != nil {
|
|
go b.runTest(h)
|
|
}
|
|
return jsonrpc2.ConnectionOptions{
|
|
Framer: b.framer,
|
|
Preempter: h,
|
|
Handler: h,
|
|
}
|
|
}
|
|
|
|
func (h *handler) waiter(name string) chan struct{} {
|
|
waiters := <-h.waiters
|
|
defer func() { h.waiters <- waiters }()
|
|
waiter, found := waiters[name]
|
|
if !found {
|
|
waiter = make(chan struct{})
|
|
waiters[name] = waiter
|
|
}
|
|
return waiter
|
|
}
|
|
|
|
func (h *handler) Preempt(ctx context.Context, req *jsonrpc2.Request) (interface{}, error) {
|
|
switch req.Method {
|
|
case "unblock":
|
|
var name string
|
|
if err := json.Unmarshal(req.Params, &name); err != nil {
|
|
return nil, fmt.Errorf("%w: %s", jsonrpc2.ErrParse, err)
|
|
}
|
|
close(h.waiter(name))
|
|
return nil, nil
|
|
case "peek":
|
|
if len(req.Params) > 0 {
|
|
return nil, fmt.Errorf("%w: expected no params", jsonrpc2.ErrInvalidParams)
|
|
}
|
|
return h.accumulator, nil
|
|
case "cancel":
|
|
var params cancelParams
|
|
if err := json.Unmarshal(req.Params, ¶ms); err != nil {
|
|
return nil, fmt.Errorf("%w: %s", jsonrpc2.ErrParse, err)
|
|
}
|
|
h.conn.Cancel(jsonrpc2.Int64ID(params.ID))
|
|
return nil, nil
|
|
default:
|
|
return nil, jsonrpc2.ErrNotHandled
|
|
}
|
|
}
|
|
|
|
func (h *handler) Handle(ctx context.Context, req *jsonrpc2.Request) (interface{}, error) {
|
|
switch req.Method {
|
|
case "no_args":
|
|
if len(req.Params) > 0 {
|
|
return nil, fmt.Errorf("%w: expected no params", jsonrpc2.ErrInvalidParams)
|
|
}
|
|
return true, nil
|
|
case "one_string":
|
|
var v string
|
|
if err := json.Unmarshal(req.Params, &v); err != nil {
|
|
return nil, fmt.Errorf("%w: %s", jsonrpc2.ErrParse, err)
|
|
}
|
|
return "got:" + v, nil
|
|
case "one_number":
|
|
var v int
|
|
if err := json.Unmarshal(req.Params, &v); err != nil {
|
|
return nil, fmt.Errorf("%w: %s", jsonrpc2.ErrParse, err)
|
|
}
|
|
return fmt.Sprintf("got:%d", v), nil
|
|
case "set":
|
|
var v int
|
|
if err := json.Unmarshal(req.Params, &v); err != nil {
|
|
return nil, fmt.Errorf("%w: %s", jsonrpc2.ErrParse, err)
|
|
}
|
|
h.accumulator = v
|
|
return nil, nil
|
|
case "add":
|
|
var v int
|
|
if err := json.Unmarshal(req.Params, &v); err != nil {
|
|
return nil, fmt.Errorf("%w: %s", jsonrpc2.ErrParse, err)
|
|
}
|
|
h.accumulator += v
|
|
return nil, nil
|
|
case "get":
|
|
if len(req.Params) > 0 {
|
|
return nil, fmt.Errorf("%w: expected no params", jsonrpc2.ErrInvalidParams)
|
|
}
|
|
return h.accumulator, nil
|
|
case "join":
|
|
var v []string
|
|
if err := json.Unmarshal(req.Params, &v); err != nil {
|
|
return nil, fmt.Errorf("%w: %s", jsonrpc2.ErrParse, err)
|
|
}
|
|
return path.Join(v...), nil
|
|
case "echo":
|
|
var v []interface{}
|
|
if err := json.Unmarshal(req.Params, &v); err != nil {
|
|
return nil, fmt.Errorf("%w: %s", jsonrpc2.ErrParse, err)
|
|
}
|
|
var result interface{}
|
|
err := h.conn.Call(ctx, v[0].(string), v[1]).Await(ctx, &result)
|
|
return result, err
|
|
case "wait":
|
|
var name string
|
|
if err := json.Unmarshal(req.Params, &name); err != nil {
|
|
return nil, fmt.Errorf("%w: %s", jsonrpc2.ErrParse, err)
|
|
}
|
|
select {
|
|
case <-h.waiter(name):
|
|
return true, nil
|
|
case <-ctx.Done():
|
|
return nil, ctx.Err()
|
|
}
|
|
case "fork":
|
|
var name string
|
|
if err := json.Unmarshal(req.Params, &name); err != nil {
|
|
return nil, fmt.Errorf("%w: %s", jsonrpc2.ErrParse, err)
|
|
}
|
|
waitFor := h.waiter(name)
|
|
go func() {
|
|
select {
|
|
case <-waitFor:
|
|
h.conn.Respond(req.ID, true, nil)
|
|
case <-ctx.Done():
|
|
h.conn.Respond(req.ID, nil, ctx.Err())
|
|
}
|
|
}()
|
|
return nil, jsonrpc2.ErrAsyncResponse
|
|
default:
|
|
return nil, jsonrpc2.ErrNotHandled
|
|
}
|
|
}
|