169 lines
4.0 KiB
Go
Raw Normal View History

2021-12-04 16:42:11 +00:00
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.Line
// A static test service that returns the same thing each time.
var testService = Static(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(assigner jrpc2.Assigner, stat jrpc2.ServerStatus) {
if _, ok := assigner.(handler.Map); !ok {
t.t.Errorf("Finished assigner: got %+v, want handler.Map", assigner)
}
if !t.init {
t.t.Error("Service finished without being initialized")
}
if !stat.Success() {
t.t.Errorf("Finish unsuccessful: %v", 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.
lst := NetAccepter(lst, newChan)
if err := Loop(lst, newService, nil); 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()
lst.Close()
<-sc
})
}
}