good morning!!!!

Skip to content
Snippets Groups Projects
suites.go 6.76 KiB
Newer Older
a's avatar
a committed
package jrpctest

import (
a's avatar
a committed
	"context"
a's avatar
a committed
	"embed"
a's avatar
a committed
	"math/rand"
a's avatar
a committed
	"reflect"
a's avatar
a committed
	"sync"
a's avatar
a committed
	"testing"
a's avatar
a committed
	"time"
a's avatar
a committed

a's avatar
a committed
	"gfx.cafe/open/jrpc/pkg/codec"
a's avatar
a committed
	"gfx.cafe/open/jrpc/pkg/server"

a's avatar
a committed
	"github.com/stretchr/testify/assert"
a's avatar
a committed
	"github.com/stretchr/testify/require"
)

a's avatar
a committed
type ClientMaker func() codec.Conn
type ServerMaker func() (*server.Server, ClientMaker, func())
a's avatar
a committed

type BasicTestSuiteArgs struct {
	ServerMaker ServerMaker
}

a's avatar
a committed
type TestContext func(t *testing.T, server *server.Server, client codec.Conn)
a's avatar
ok  
a committed
type BenchContext func(t *testing.B, server *server.Server, client codec.Conn)
a's avatar
a committed

a's avatar
ok  
a committed
func TestExecutor(sm ServerMaker) func(t *testing.T, c TestContext) {
	return func(t *testing.T, c TestContext) {
		server, dialer, cn := sm()
a's avatar
a committed
		defer cn()
		defer server.Stop()
		client := dialer()
		defer client.Close()
		c(t, server, client)
	}
a's avatar
ok  
a committed
}
func BenchExecutor(sm ServerMaker) func(t *testing.B, c BenchContext) {
	return func(t *testing.B, c BenchContext) {
		server, dialer, cn := sm()
		defer cn()
		defer server.Stop()
		client := dialer()
		defer client.Close()
		c(t, server, client)
	}
}

// go:embed testdata/
var testData embed.FS

func RunBasicTestSuite(t *testing.T, args BasicTestSuiteArgs) {
	var executeTest = TestExecutor(args.ServerMaker)
a's avatar
a committed

a's avatar
a committed
	var makeTest = func(name string, fm TestContext) {
a's avatar
a committed
		t.Run(name, func(t *testing.T) {
			executeTest(t, fm)
		})
	}
a's avatar
a committed

	t.Parallel()
a's avatar
a committed
	makeTest("Request", func(t *testing.T, server *server.Server, client codec.Conn) {
a's avatar
a committed
		var resp EchoResult
		err := client.Do(nil, &resp, "test_echo", []any{"hello", 10, &EchoArgs{"world"}})
		require.NoError(t, err)
		if !reflect.DeepEqual(resp, EchoResult{"hello", 10, &EchoArgs{"world"}}) {
			t.Errorf("incorrect result %#v", resp)
		}
	})
a's avatar
a committed

a's avatar
a committed
	makeTest("ResponseType", func(t *testing.T, server *server.Server, client codec.Conn) {
a's avatar
ok  
a committed
		err := codec.CallInto(nil, client, nil, "test_echo", "hello", 10, &EchoArgs{"world"})
		assert.NoErrorf(t, err, "passing nil as result should be ok")
a's avatar
a committed
		var resultVar EchoResult
		// Note: passing the var, not a ref
a's avatar
ok  
a committed
		err = codec.CallInto(nil, client, resultVar, "test_echo", "hello", 10, &EchoArgs{"world"})
		assert.Error(t, err, "passing var as nil gives error")
a's avatar
a committed
	})

a's avatar
a committed
	makeTest("BatchRequest", func(t *testing.T, server *server.Server, client codec.Conn) {
		batch := []*codec.BatchElem{
a's avatar
a committed
			{
				Method: "test_echo",
				Params: []any{"hello", 10, &EchoArgs{"world"}},
				Result: new(EchoResult),
			},
			{
				Method: "test_echo",
				Params: []any{"hello2", 11, &EchoArgs{"world"}},
				Result: new(EchoResult),
			},
Garet Halliday's avatar
Garet Halliday committed
			{
				Method:         "test_echo",
				Params:         []any{"hello3", 12, &EchoArgs{"world"}},
				IsNotification: true,
			},
a's avatar
a committed
			{
a's avatar
a committed
				Method: "no/such/method",
a's avatar
a committed
				Params: []any{1, 2, 3},
				Result: new(int),
			},
		}
		if err := client.BatchCall(nil, batch...); err != nil {
			t.Fatal(err)
		}
a's avatar
a committed
		wantResult := []*codec.BatchElem{
a's avatar
a committed
			{
				Method: "test_echo",
				Params: []any{"hello", 10, &EchoArgs{"world"}},
				Result: &EchoResult{"hello", 10, &EchoArgs{"world"}},
			},
			{
				Method: "test_echo",
				Params: []any{"hello2", 11, &EchoArgs{"world"}},
				Result: &EchoResult{"hello2", 11, &EchoArgs{"world"}},
			},
Garet Halliday's avatar
Garet Halliday committed
			{
				Method: "test_echo",
				Params: []any{"hello3", 12, &EchoArgs{"world"}},
			},
a's avatar
a committed
			{
a's avatar
a committed
				Method: "no/such/method",
a's avatar
a committed
				Params: []any{1, 2, 3},
				Result: new(int),
a's avatar
a committed
				Error:  &codec.JsonError{Code: -32601, Message: "the method no/such/method does not exist/is not available"},
a's avatar
a committed
			},
		}
		require.EqualValues(t, len(batch), len(wantResult))
		for i := range batch {
			a := batch[i]
Garet Halliday's avatar
Garet Halliday committed
			b := wantResult[i]
a's avatar
a committed
			assert.EqualValuesf(t, a.Method, b.Method, "item %d", i)
			assert.EqualValuesf(t, a.Result, b.Result, "item %d", i)
			assert.EqualValuesf(t, a.Params, b.Params, "item %d", i)
Garet Halliday's avatar
Garet Halliday committed
			assert.EqualValuesf(t, a.Error, b.Error, "item %d", i)
a's avatar
a committed
		}
	})

a's avatar
a committed
	makeTest("ResposeType2", func(t *testing.T, server *server.Server, client codec.Conn) {
a's avatar
a committed
		if err := codec.CallInto(nil, client, nil, "test_echo", "hello", 10, &EchoArgs{"world"}); err != nil {
a's avatar
a committed
			t.Errorf("Passing nil as result should be fine, but got an error: %v", err)
		}
		var resultVar EchoResult
		// Note: passing the var, not a ref
a's avatar
a committed
		err := codec.CallInto(nil, client, resultVar, "test_echo", "hello", 10, &EchoArgs{"world"})
a's avatar
a committed
		if err == nil {
			t.Error("Passing a var as result should be an error")
		}
	})

a's avatar
a committed
	makeTest("ErrorReturnType", func(t *testing.T, server *server.Server, client codec.Conn) {
a's avatar
a committed
		var resp any
a's avatar
a committed
		err := codec.CallInto(nil, client, &resp, "test_returnError")
a's avatar
a committed
		require.Error(t, err)

		// Check code.
		if e, ok := err.(codec.Error); !ok {
			t.Fatalf("client did not return rpc.Error, got %#v", e)
		} else if e.ErrorCode() != (testError{}.ErrorCode()) {
			t.Fatalf("wrong error code %d, want %d", e.ErrorCode(), testError{}.ErrorCode())
		}
		// Check data.
		if e, ok := err.(codec.DataError); !ok {
			t.Fatalf("client did not return rpc.DataError, got %#v", e)
		} else if e.ErrorData() != (testError{}.ErrorData()) {
			t.Fatalf("wrong error data %#v, want %#v", e.ErrorData(), testError{}.ErrorData())
		}
	})
a's avatar
a committed
	makeTest("Notify", func(t *testing.T, server *server.Server, client codec.Conn) {
Garet Halliday's avatar
Garet Halliday committed
		if c, ok := client.(codec.StreamingConn); ok {
			if err := c.Notify(context.Background(), "test_echo", []any{"hello", 10, &EchoArgs{"world"}}); err != nil {
				t.Fatal(err)
			}
a's avatar
a committed
		}
	})
a's avatar
a committed

a's avatar
a committed
	makeTest("context cancel", func(t *testing.T, server *server.Server, client codec.Conn) {
a's avatar
a committed
		maxContextCancelTimeout := 300 * time.Millisecond
		// The actual test starts here.
		var (
			wg       sync.WaitGroup
			nreqs    = 10
			ncallers = 10
		)
		caller := func(index int) {
			defer wg.Done()
			for i := 0; i < nreqs; i++ {
				var (
					ctx     context.Context
					cancel  func()
					timeout = time.Duration(rand.Int63n(int64(maxContextCancelTimeout)))
				)
				if index < ncallers/2 {
					// For half of the callers, create a context without deadline
					// and cancel it later.
					ctx, cancel = context.WithCancel(context.Background())
					time.AfterFunc(timeout, cancel)
				} else {
					// For the other half, create a context with a deadline instead. This is
					// different because the context deadline is used to set the socket write
					// deadline.
					ctx, cancel = context.WithTimeout(context.Background(), timeout)
				}

				// Now perform a call with the context.
				// The key thing here is that no call will ever complete successfully.
a's avatar
a committed
				err := codec.CallInto(ctx, client, nil, "test_block")
a's avatar
a committed
				switch {
				case err == nil:
					_, hasDeadline := ctx.Deadline()
					t.Errorf("no error for call with %v wait time (deadline: %v)", timeout, hasDeadline)
					// default:
					// 	t.Logf("got expected error with %v wait time: %v", timeout, err)
				}
				cancel()
			}
		}
		wg.Add(ncallers)
		for i := 0; i < ncallers; i++ {
			go caller(i)
		}
		wg.Wait()
	})
a's avatar
a committed
	makeTest("", func(t *testing.T, server *server.Server, client codec.Conn) {
	})
a's avatar
a committed
}