224 lines
5.5 KiB
Go
224 lines
5.5 KiB
Go
package jsonrpc
|
|
|
|
import (
|
|
"bufio"
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"strings"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
// TestMain lets this test binary double as a mock JSON-RPC server subprocess.
|
|
// When JSONRPC_TEST_SERVER is set the process runs the echo server and exits;
|
|
// otherwise it runs the normal test suite. This is the standard Go pattern for
|
|
// exercising a subprocess transport without shipping a separate helper binary.
|
|
func TestMain(m *testing.M) {
|
|
switch os.Getenv("JSONRPC_TEST_SERVER") {
|
|
case "echo":
|
|
runEchoServer()
|
|
return
|
|
case "silent":
|
|
// Read and discard everything, never reply — used for timeout tests.
|
|
sc := bufio.NewScanner(os.Stdin)
|
|
sc.Buffer(make([]byte, 0, 64*1024), 16*1024*1024)
|
|
for sc.Scan() {
|
|
}
|
|
return
|
|
}
|
|
os.Exit(m.Run())
|
|
}
|
|
|
|
// runEchoServer replies to each request: method "echo" returns its params,
|
|
// method "fail" returns a JSON-RPC error, notifications produce no reply.
|
|
func runEchoServer() {
|
|
scanner := bufio.NewScanner(os.Stdin)
|
|
scanner.Buffer(make([]byte, 0, 64*1024), 16*1024*1024)
|
|
w := bufio.NewWriter(os.Stdout)
|
|
for scanner.Scan() {
|
|
var req Request
|
|
if err := json.Unmarshal(scanner.Bytes(), &req); err != nil {
|
|
continue
|
|
}
|
|
if req.ID == nil {
|
|
continue // notification: no response
|
|
}
|
|
var resp Response
|
|
resp.JSONRPC = Version
|
|
resp.ID = req.ID
|
|
switch req.Method {
|
|
case "fail":
|
|
resp.Error = &Error{Code: -32000, Message: "boom"}
|
|
default:
|
|
resp.Result = req.Params
|
|
if resp.Result == nil {
|
|
resp.Result = json.RawMessage(`null`)
|
|
}
|
|
}
|
|
out, _ := json.Marshal(&resp)
|
|
out = append(out, '\n')
|
|
_, _ = w.Write(out)
|
|
_ = w.Flush()
|
|
}
|
|
}
|
|
|
|
// newTestClient starts this test binary as a mock server in the given mode.
|
|
func newTestClient(t *testing.T, mode string) *Client {
|
|
t.Helper()
|
|
exe, err := os.Executable()
|
|
if err != nil {
|
|
t.Fatalf("os.Executable: %v", err)
|
|
}
|
|
c, err := NewClient(Config{
|
|
Command: exe,
|
|
Env: append(os.Environ(), "JSONRPC_TEST_SERVER="+mode),
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("NewClient: %v", err)
|
|
}
|
|
t.Cleanup(func() { _ = c.Close() })
|
|
return c
|
|
}
|
|
|
|
func TestCallEcho(t *testing.T) {
|
|
c := newTestClient(t, "echo")
|
|
ctx := context.Background()
|
|
|
|
raw, err := c.Call(ctx, "echo", map[string]any{"hello": "world"})
|
|
if err != nil {
|
|
t.Fatalf("Call: %v", err)
|
|
}
|
|
var got map[string]string
|
|
if err := json.Unmarshal(raw, &got); err != nil {
|
|
t.Fatalf("unmarshal result: %v", err)
|
|
}
|
|
if got["hello"] != "world" {
|
|
t.Fatalf("got %v, want hello=world", got)
|
|
}
|
|
}
|
|
|
|
func TestCallServerError(t *testing.T) {
|
|
c := newTestClient(t, "echo")
|
|
_, err := c.Call(context.Background(), "fail", nil)
|
|
if err == nil {
|
|
t.Fatal("expected error, got nil")
|
|
}
|
|
var rpcErr *Error
|
|
if !errors.As(err, &rpcErr) {
|
|
t.Fatalf("expected *jsonrpc.Error, got %T: %v", err, err)
|
|
}
|
|
if rpcErr.Code != -32000 || !strings.Contains(rpcErr.Message, "boom") {
|
|
t.Fatalf("unexpected error: %+v", rpcErr)
|
|
}
|
|
}
|
|
|
|
// TestConcurrentCalls verifies responses correlate to the right caller when many
|
|
// requests are in flight at once (id-based correlation).
|
|
func TestConcurrentCalls(t *testing.T) {
|
|
c := newTestClient(t, "echo")
|
|
ctx := context.Background()
|
|
|
|
const n = 50
|
|
var wg sync.WaitGroup
|
|
errs := make([]error, n)
|
|
for i := range n {
|
|
wg.Add(1)
|
|
go func(i int) {
|
|
defer wg.Done()
|
|
raw, err := c.Call(ctx, "echo", map[string]int{"n": i})
|
|
if err != nil {
|
|
errs[i] = err
|
|
return
|
|
}
|
|
var got map[string]int
|
|
if err := json.Unmarshal(raw, &got); err != nil {
|
|
errs[i] = err
|
|
return
|
|
}
|
|
if got["n"] != i {
|
|
errs[i] = fmt.Errorf("call %d got n=%d", i, got["n"])
|
|
}
|
|
}(i)
|
|
}
|
|
wg.Wait()
|
|
for i, err := range errs {
|
|
if err != nil {
|
|
t.Fatalf("call %d: %v", i, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestCallContextTimeout verifies Call returns when the context is cancelled and
|
|
// the server never replies.
|
|
func TestCallContextTimeout(t *testing.T) {
|
|
c := newTestClient(t, "silent")
|
|
ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
|
|
defer cancel()
|
|
|
|
start := time.Now()
|
|
_, err := c.Call(ctx, "echo", nil)
|
|
if err == nil {
|
|
t.Fatal("expected timeout error, got nil")
|
|
}
|
|
if time.Since(start) > 2*time.Second {
|
|
t.Fatalf("Call blocked too long: %v", time.Since(start))
|
|
}
|
|
}
|
|
|
|
func TestNotifyNoResponse(t *testing.T) {
|
|
c := newTestClient(t, "echo")
|
|
if err := c.Notify("ping", map[string]string{"k": "v"}); err != nil {
|
|
t.Fatalf("Notify: %v", err)
|
|
}
|
|
// A subsequent Call must still work (notification produced no stray reply).
|
|
if _, err := c.Call(context.Background(), "echo", nil); err != nil {
|
|
t.Fatalf("Call after Notify: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestCallAfterClose(t *testing.T) {
|
|
c := newTestClient(t, "echo")
|
|
if err := c.Close(); err != nil {
|
|
t.Fatalf("Close: %v", err)
|
|
}
|
|
if _, err := c.Call(context.Background(), "echo", nil); err == nil {
|
|
t.Fatal("expected error calling closed client")
|
|
}
|
|
}
|
|
|
|
func TestNewClientEmptyCommand(t *testing.T) {
|
|
if _, err := NewClient(Config{}); err == nil {
|
|
t.Fatal("expected error for empty command")
|
|
}
|
|
}
|
|
|
|
func TestIDRoundTrip(t *testing.T) {
|
|
for _, tc := range []struct {
|
|
name string
|
|
raw string
|
|
}{
|
|
{"number", `123`},
|
|
{"string", `"abc"`},
|
|
} {
|
|
var id ID
|
|
if err := json.Unmarshal([]byte(tc.raw), &id); err != nil {
|
|
t.Fatalf("%s: unmarshal: %v", tc.name, err)
|
|
}
|
|
out, err := json.Marshal(id)
|
|
if err != nil {
|
|
t.Fatalf("%s: marshal: %v", tc.name, err)
|
|
}
|
|
if string(out) != tc.raw {
|
|
t.Fatalf("%s: round-trip got %s want %s", tc.name, out, tc.raw)
|
|
}
|
|
}
|
|
var bad ID
|
|
if err := json.Unmarshal([]byte(`true`), &bad); err == nil {
|
|
t.Fatal("expected error for boolean id")
|
|
}
|
|
}
|