170 lines
4.1 KiB
Go
170 lines
4.1 KiB
Go
|
package server
|
||
|
|
||
|
import (
|
||
|
"context"
|
||
|
"math/rand"
|
||
|
"net"
|
||
|
"sync"
|
||
|
"testing"
|
||
|
"time"
|
||
|
|
||
|
"github.com/creachadair/jrpc2"
|
||
|
"github.com/creachadair/jrpc2/channel"
|
||
|
"github.com/creachadair/jrpc2/handler"
|
||
|
)
|
||
|
|
||
|
var newChan = channel.Varint
|
||
|
|
||
|
// A static test service that returns the same thing each time.
|
||
|
var testService = NewStatic(handler.Map{
|
||
|
"Test": handler.New(func(context.Context) (string, error) {
|
||
|
return "OK", nil
|
||
|
}),
|
||
|
})
|
||
|
|
||
|
// A service with session state, to exercise start/finish plumbing.
|
||
|
type testSession struct {
|
||
|
t *testing.T
|
||
|
init bool
|
||
|
nCall int
|
||
|
}
|
||
|
|
||
|
func newTestSession(t *testing.T) func() Service {
|
||
|
return func() Service { t.Helper(); return &testSession{t: t} }
|
||
|
}
|
||
|
|
||
|
func (t *testSession) Assigner() (jrpc2.Assigner, error) {
|
||
|
if t.init {
|
||
|
t.t.Error("Service has already been initialized")
|
||
|
}
|
||
|
t.init = true
|
||
|
return handler.Map{
|
||
|
"Test": handler.New(func(context.Context) (string, error) {
|
||
|
if !t.init {
|
||
|
t.t.Error("Handler called before service initialized")
|
||
|
}
|
||
|
t.nCall++
|
||
|
return "OK", nil
|
||
|
}),
|
||
|
}, nil
|
||
|
}
|
||
|
|
||
|
func (t *testSession) Finish(stat jrpc2.ServerStatus) {
|
||
|
if !t.init {
|
||
|
t.t.Error("Service finished without being initialized")
|
||
|
}
|
||
|
if !stat.Success() {
|
||
|
t.t.Errorf("Finish unsuccessful: %v", stat.Err)
|
||
|
}
|
||
|
t.t.Logf("Server finished after %d calls, err=%v", t.nCall, stat.Err)
|
||
|
}
|
||
|
|
||
|
func mustListen(t *testing.T) net.Listener {
|
||
|
t.Helper()
|
||
|
|
||
|
lst, err := net.Listen("tcp", "127.0.0.1:0")
|
||
|
if err != nil {
|
||
|
t.Fatalf("Listen: %v", err)
|
||
|
}
|
||
|
return lst
|
||
|
}
|
||
|
|
||
|
func mustDial(t *testing.T, addr string) *jrpc2.Client {
|
||
|
t.Helper()
|
||
|
|
||
|
conn, err := net.Dial("tcp", addr)
|
||
|
if err != nil {
|
||
|
t.Fatalf("Dial %q: %v", addr, err)
|
||
|
}
|
||
|
return jrpc2.NewClient(newChan(conn, conn), nil)
|
||
|
}
|
||
|
|
||
|
func mustServe(t *testing.T, lst net.Listener, newService func() Service) <-chan struct{} {
|
||
|
t.Helper()
|
||
|
|
||
|
sc := make(chan struct{})
|
||
|
go func() {
|
||
|
defer close(sc)
|
||
|
// Start a server loop to accept connections from the clients. This should
|
||
|
// exit cleanly once all the clients have finished and the listener closes.
|
||
|
if err := Loop(lst, newService, &LoopOptions{
|
||
|
Framing: newChan,
|
||
|
}); err != nil {
|
||
|
t.Errorf("Loop: unexpected failure: %v", err)
|
||
|
}
|
||
|
}()
|
||
|
return sc
|
||
|
}
|
||
|
|
||
|
// Test that sequential clients against the same server work sanely.
|
||
|
func TestSeq(t *testing.T) {
|
||
|
lst := mustListen(t)
|
||
|
addr := lst.Addr().String()
|
||
|
sc := mustServe(t, lst, testService)
|
||
|
|
||
|
for i := 0; i < 5; i++ {
|
||
|
cli := mustDial(t, addr)
|
||
|
var rsp string
|
||
|
if err := cli.CallResult(context.Background(), "Test", nil, &rsp); err != nil {
|
||
|
t.Errorf("[client %d] Test call: unexpected error: %v", i, err)
|
||
|
} else if rsp != "OK" {
|
||
|
t.Errorf("[client %d]: Test call: got %q, want OK", i, rsp)
|
||
|
}
|
||
|
cli.Close()
|
||
|
}
|
||
|
lst.Close()
|
||
|
<-sc
|
||
|
}
|
||
|
|
||
|
// Test that concurrent clients against the same server work sanely.
|
||
|
func TestLoop(t *testing.T) {
|
||
|
tests := []struct {
|
||
|
desc string
|
||
|
cons func() Service
|
||
|
}{
|
||
|
{"StaticService", testService},
|
||
|
{"SessionStateService", newTestSession(t)},
|
||
|
}
|
||
|
const numClients = 5
|
||
|
const numCalls = 5
|
||
|
|
||
|
for _, test := range tests {
|
||
|
t.Run(test.desc, func(t *testing.T) {
|
||
|
lst := mustListen(t)
|
||
|
addr := lst.Addr().String()
|
||
|
sc := mustServe(t, lst, test.cons)
|
||
|
|
||
|
// Start a bunch of clients, each of which will dial the server and make
|
||
|
// some calls at random intervals to tickle the race detector.
|
||
|
var wg sync.WaitGroup
|
||
|
for i := 0; i < numClients; i++ {
|
||
|
wg.Add(1)
|
||
|
i := i
|
||
|
go func() {
|
||
|
defer wg.Done()
|
||
|
cli := mustDial(t, addr)
|
||
|
defer cli.Close()
|
||
|
|
||
|
for j := 0; j < numCalls; j++ {
|
||
|
time.Sleep(time.Duration(rand.Intn(10)) * time.Millisecond)
|
||
|
var rsp string
|
||
|
if err := cli.CallResult(context.Background(), "Test", nil, &rsp); err != nil {
|
||
|
t.Errorf("[client %d]: Test call %d: unexpected error: %v", i, j+1, err)
|
||
|
} else if rsp != "OK" {
|
||
|
t.Errorf("[client %d]: Test call %d: got %q, want OK", i, j+1, rsp)
|
||
|
}
|
||
|
}
|
||
|
}()
|
||
|
}
|
||
|
|
||
|
// Wait for the clients to be finished and then close the listener so that
|
||
|
// the service loop will stop.
|
||
|
wg.Wait()
|
||
|
t.Log("Clients are finished; stopping listener")
|
||
|
lst.Close()
|
||
|
<-sc
|
||
|
t.Log("Server is finished")
|
||
|
})
|
||
|
}
|
||
|
}
|