bldr/resource/routed-client_test.go
Rama master · réplica de lectura del repositorio original.
package resource
import (
"context"
strconv "strconv"
"sync"
"testing"
"github.com/aperturerobotics/starpc/srpc"
)
// recordingInvoker records InvokeMethod calls and returns configurable results.
type recordingInvoker struct {
mu sync.Mutex
calls []invokerCall
retFound bool
retErr error
}
type invokerCall struct {
serviceID string
methodID string
}
func (r *recordingInvoker) InvokeMethod(serviceID, methodID string, strm srpc.Stream) (bool, error) {
r.mu.Lock()
r.calls = append(r.calls, invokerCall{serviceID: serviceID, methodID: methodID})
r.mu.Unlock()
return r.retFound, r.retErr
}
func (r *recordingInvoker) getCalls() []invokerCall {
// Copy the recording invoker call log while holding its mutex.
r.mu.Lock()
defer r.mu.Unlock()
out := make([]invokerCall, len(r.calls))
copy(out, r.calls)
return out
}
// recordingClient records ExecCall and NewStream calls with their service/method IDs.
type recordingClient struct {
mu sync.Mutex
calls []clientCall
retErr error
retStrm srpc.Stream
strmErr error
}
type clientCall struct {
op string
service string
method string
}
func (c *recordingClient) ExecCall(ctx context.Context, service, method string, in, out srpc.Message) error {
c.mu.Lock()
c.calls = append(c.calls, clientCall{op: "exec", service: service, method: method})
c.mu.Unlock()
return c.retErr
}
func (c *recordingClient) NewStream(ctx context.Context, service, method string, firstMsg srpc.Message) (srpc.Stream, error) {
c.mu.Lock()
c.calls = append(c.calls, clientCall{op: "stream", service: service, method: method})
c.mu.Unlock()
return c.retStrm, c.strmErr
}
func (c *recordingClient) getCalls() []clientCall {
// Copy the recording client call log while holding its mutex.
c.mu.Lock()
defer c.mu.Unlock()
out := make([]clientCall, len(c.calls))
copy(out, c.calls)
return out
}
// --- RoutedInvoker tests ---
func TestRoutedInvoker_HappyPath(t *testing.T) {
// Set up a mux with a recording invoker.
ri := NewRoutedInvoker()
inv := &recordingInvoker{retFound: true}
ri.SetMux(42, inv)
// Invoke the mux through a resource-prefixed service ID.
found, err := ri.InvokeMethod("42/foo.Service", "Bar", nil)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if !found {
t.Fatal("expected found=true")
}
// Inspect the call recorded by the mux invoker.
calls := inv.getCalls()
if len(calls) != 1 {
t.Fatalf("expected 1 call, got %d", len(calls))
}
if calls[0].serviceID != "foo.Service" {
t.Fatalf("got serviceID %q, want %q", calls[0].serviceID, "foo.Service")
}
if calls[0].methodID != "Bar" {
t.Fatalf("got methodID %q, want %q", calls[0].methodID, "Bar")
}
}
func TestRoutedInvoker_NoSlashReturnsFalse(t *testing.T) {
// Set up a mux for a service ID without a slash.
ri := NewRoutedInvoker()
inv := &recordingInvoker{retFound: true}
ri.SetMux(1, inv)
// Invoke the router with a service ID that has no mux prefix.
found, err := ri.InvokeMethod("no-slash-here", "Method", nil)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if found {
t.Fatal("expected found=false for service ID without slash")
}
if len(inv.getCalls()) != 0 {
t.Fatal("invoker should not have been called")
}
}
func TestRoutedInvoker_NonNumericPrefixReturnsFalse(t *testing.T) {
// Set up a mux for a nonnumeric resource prefix.
ri := NewRoutedInvoker()
inv := &recordingInvoker{retFound: true}
ri.SetMux(1, inv)
// Invoke the router with a nonnumeric resource prefix.
found, err := ri.InvokeMethod("abc/foo.Service", "Method", nil)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if found {
t.Fatal("expected found=false for non-numeric prefix")
}
if len(inv.getCalls()) != 0 {
t.Fatal("invoker should not have been called")
}
}
func TestRoutedInvoker_MuxNotFoundReturnsError(t *testing.T) {
ri := NewRoutedInvoker()
found, err := ri.InvokeMethod("99/foo.Service", "Method", nil)
if found {
t.Fatal("expected found=false for missing mux")
}
if err != ErrResourceNotFound {
t.Fatalf("got error %v, want %v", err, ErrResourceNotFound)
}
}
func TestRoutedInvoker_SetMuxRegisters(t *testing.T) {
// Create a router before registering its mux.
ri := NewRoutedInvoker()
// Before SetMux, the mux should not be found.
found, err := ri.InvokeMethod("7/svc", "m", nil)
if found {
t.Fatal("expected found=false before SetMux")
}
if err != ErrResourceNotFound {
t.Fatalf("got error %v, want %v", err, ErrResourceNotFound)
}
// Register the recording invoker for resource 7.
inv := &recordingInvoker{retFound: true}
ri.SetMux(7, inv)
// Invoke the route after registering its mux.
found, err = ri.InvokeMethod("7/svc", "m", nil)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if !found {
t.Fatal("expected found=true after SetMux")
}
if len(inv.getCalls()) != 1 {
t.Fatalf("expected 1 call, got %d", len(inv.getCalls()))
}
}
func TestRoutedInvoker_RemoveMux(t *testing.T) {
// Set up a registered mux before removing it.
ri := NewRoutedInvoker()
inv := &recordingInvoker{retFound: true}
ri.SetMux(5, inv)
// Verify it works before removal.
found, err := ri.InvokeMethod("5/svc", "m", nil)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if !found {
t.Fatal("expected found=true before RemoveMux")
}
// Remove resource 5 from the routed invoker.
ri.RemoveMux(5)
// Invoke the route after removing its mux.
found, err = ri.InvokeMethod("5/svc", "m", nil)
if found {
t.Fatal("expected found=false after RemoveMux")
}
if err != ErrResourceNotFound {
t.Fatalf("got error %v, want %v", err, ErrResourceNotFound)
}
}
func TestRoutedInvoker_SetMuxReplaces(t *testing.T) {
// Prepare old and replacement invokers for one resource ID.
ri := NewRoutedInvoker()
inv1 := &recordingInvoker{retFound: true}
inv2 := &recordingInvoker{retFound: true}
// Replace the invoker registered for resource 3.
ri.SetMux(3, inv1)
ri.SetMux(3, inv2)
// Invoke resource 3 after replacing its mux.
found, err := ri.InvokeMethod("3/svc", "m", nil)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if !found {
t.Fatal("expected found=true")
}
// Verify that the replaced invoker received no call.
if len(inv1.getCalls()) != 0 {
t.Fatal("old invoker should not have been called")
}
if len(inv2.getCalls()) != 1 {
t.Fatalf("new invoker expected 1 call, got %d", len(inv2.getCalls()))
}
}
func TestRoutedInvoker_MultipleMuxes(t *testing.T) {
// Prepare recording invokers for two resource IDs.
ri := NewRoutedInvoker()
inv1 := &recordingInvoker{retFound: true}
inv2 := &recordingInvoker{retFound: true}
// Register both resource IDs with the routed invoker.
ri.SetMux(10, inv1)
ri.SetMux(20, inv2)
// Invoke the first resource and check its route.
found, err := ri.InvokeMethod("10/svcA", "methodA", nil)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if !found {
t.Fatal("expected found=true for mux 10")
}
// Invoke the second resource and check its route.
found, err = ri.InvokeMethod("20/svcB", "methodB", nil)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if !found {
t.Fatal("expected found=true for mux 20")
}
// Collect each invoker call log for the routing assertions.
calls1 := inv1.getCalls()
calls2 := inv2.getCalls()
if len(calls1) != 1 || calls1[0].serviceID != "svcA" {
t.Fatalf("mux 10: got calls %+v, want serviceID=svcA", calls1)
}
if len(calls2) != 1 || calls2[0].serviceID != "svcB" {
t.Fatalf("mux 20: got calls %+v, want serviceID=svcB", calls2)
}
}
func TestRoutedInvoker_PropagatesMuxError(t *testing.T) {
// Configure a mux to return a resource error.
ri := NewRoutedInvoker()
muxErr := ErrInvalidResourceID
inv := &recordingInvoker{retFound: true, retErr: muxErr}
ri.SetMux(1, inv)
// Invoke the mux and capture its returned result.
found, err := ri.InvokeMethod("1/svc", "m", nil)
if !found {
t.Fatal("expected found=true (propagated from mux)")
}
if err != muxErr {
t.Fatalf("got error %v, want %v", err, muxErr)
}
}
func TestRoutedInvoker_EmptyServiceAfterSlash(t *testing.T) {
// Set up a mux to handle a service with an empty suffix.
ri := NewRoutedInvoker()
inv := &recordingInvoker{retFound: true}
ri.SetMux(1, inv)
// Invoke the route with an empty service suffix.
found, err := ri.InvokeMethod("1/", "m", nil)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if !found {
t.Fatal("expected found=true")
}
calls := inv.getCalls()
if len(calls) != 1 || calls[0].serviceID != "" {
t.Fatalf("expected empty serviceID, got %q", calls[0].serviceID)
}
}
func TestRoutedInvoker_NestedSlash(t *testing.T) {
// Set up a mux for a service containing nested slashes.
ri := NewRoutedInvoker()
inv := &recordingInvoker{retFound: true}
ri.SetMux(1, inv)
// Invoke the route with a nested service path.
found, err := ri.InvokeMethod("1/foo/bar", "m", nil)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if !found {
t.Fatal("expected found=true")
}
calls := inv.getCalls()
if len(calls) != 1 || calls[0].serviceID != "foo/bar" {
t.Fatalf("expected serviceID %q, got %q", "foo/bar", calls[0].serviceID)
}
}
func TestRoutedInvoker_ConcurrentSetRemove(t *testing.T) {
ri := NewRoutedInvoker()
var wg sync.WaitGroup
for i := range 100 {
id := uint32(i)
wg.Add(2)
go func() {
defer wg.Done()
ri.SetMux(id, &recordingInvoker{retFound: true})
}()
go func() {
defer wg.Done()
ri.RemoveMux(id)
}()
}
wg.Wait()
}
func TestRoutedInvoker_ConcurrentInvoke(t *testing.T) {
// Register resource IDs before issuing concurrent routed calls.
ri := NewRoutedInvoker()
for i := range 10 {
ri.SetMux(uint32(i), &recordingInvoker{retFound: true})
}
// Run routed calls concurrently and wait for every caller.
var wg sync.WaitGroup
for i := range 10 {
id := uint32(i)
wg.Go(func() {
svc := strconv.FormatUint(uint64(id), 10) + "/svc"
ri.InvokeMethod(svc, "m", nil)
})
}
wg.Wait()
}
// --- NewRoutedClient tests ---
func TestRoutedClient_ExecCallPrependsPrefix(t *testing.T) {
// Create a recording client for the prefixed ExecCall.
rec := &recordingClient{}
rc := NewRoutedClient(rec, 42)
// Execute the call through the routed client.
err := rc.ExecCall(context.Background(), "foo.Service", "Bar", nil, nil)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
// Inspect the service and method recorded by the client.
calls := rec.getCalls()
if len(calls) != 1 {
t.Fatalf("expected 1 call, got %d", len(calls))
}
if calls[0].op != "exec" {
t.Fatalf("expected op=exec, got %q", calls[0].op)
}
if calls[0].service != "42/foo.Service" {
t.Fatalf("got service %q, want %q", calls[0].service, "42/foo.Service")
}
if calls[0].method != "Bar" {
t.Fatalf("got method %q, want %q", calls[0].method, "Bar")
}
}
func TestRoutedClient_NewStreamPrependsPrefix(t *testing.T) {
// Create a recording client for the prefixed stream.
rec := &recordingClient{}
rc := NewRoutedClient(rec, 7)
// Open a stream through the routed client.
_, err := rc.NewStream(context.Background(), "my.Svc", "DoThing", nil)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
// Inspect the service and method recorded by the client.
calls := rec.getCalls()
if len(calls) != 1 {
t.Fatalf("expected 1 call, got %d", len(calls))
}
if calls[0].op != "stream" {
t.Fatalf("expected op=stream, got %q", calls[0].op)
}
if calls[0].service != "7/my.Svc" {
t.Fatalf("got service %q, want %q", calls[0].service, "7/my.Svc")
}
if calls[0].method != "DoThing" {
t.Fatalf("got method %q, want %q", calls[0].method, "DoThing")
}
}
func TestRoutedClient_MethodPassesThrough(t *testing.T) {
// Create a recording client for both pass-through methods.
rec := &recordingClient{}
rc := NewRoutedClient(rec, 1)
// Invoke ExecCall and NewStream with their original methods.
rc.ExecCall(context.Background(), "svc", "MyMethod", nil, nil)
rc.NewStream(context.Background(), "svc", "OtherMethod", nil)
// Inspect both method names recorded by the client.
calls := rec.getCalls()
if len(calls) != 2 {
t.Fatalf("expected 2 calls, got %d", len(calls))
}
if calls[0].method != "MyMethod" {
t.Fatalf("exec: got method %q, want %q", calls[0].method, "MyMethod")
}
if calls[1].method != "OtherMethod" {
t.Fatalf("stream: got method %q, want %q", calls[1].method, "OtherMethod")
}
}
func TestRoutedClient_ZeroResourceID(t *testing.T) {
// Create a routed client with resource ID zero.
rec := &recordingClient{}
rc := NewRoutedClient(rec, 0)
// Execute a call through the zero-ID routed client.
rc.ExecCall(context.Background(), "svc", "m", nil, nil)
// Inspect the service ID produced for resource zero.
calls := rec.getCalls()
if len(calls) != 1 {
t.Fatalf("expected 1 call, got %d", len(calls))
}
if calls[0].service != "0/svc" {
t.Fatalf("got service %q, want %q", calls[0].service, "0/svc")
}
}
func TestRoutedClient_LargeResourceID(t *testing.T) {
// Create a routed client with the largest uint32 resource ID.
rec := &recordingClient{}
rc := NewRoutedClient(rec, 4294967295) // max uint32
// Execute a call through the maximum-ID routed client.
rc.ExecCall(context.Background(), "svc", "m", nil, nil)
// Inspect the service ID produced for the maximum resource ID.
calls := rec.getCalls()
if len(calls) != 1 {
t.Fatalf("expected 1 call, got %d", len(calls))
}
if calls[0].service != "4294967295/svc" {
t.Fatalf("got service %q, want %q", calls[0].service, "4294967295/svc")
}
}
func TestRoutedClient_PropagatesExecError(t *testing.T) {
rec := &recordingClient{retErr: ErrClientReleased}
rc := NewRoutedClient(rec, 1)
err := rc.ExecCall(context.Background(), "svc", "m", nil, nil)
if err != ErrClientReleased {
t.Fatalf("got error %v, want %v", err, ErrClientReleased)
}
}
func TestRoutedClient_PropagatesStreamError(t *testing.T) {
rec := &recordingClient{strmErr: ErrClientReleased}
rc := NewRoutedClient(rec, 1)
_, err := rc.NewStream(context.Background(), "svc", "m", nil)
if err != ErrClientReleased {
t.Fatalf("got error %v, want %v", err, ErrClientReleased)
}
}
// --- Roundtrip: NewRoutedClient -> RoutedInvoker ---
func TestRoutedRoundtrip(t *testing.T) {
// Register the invoker used to test a complete routed roundtrip.
ri := NewRoutedInvoker()
inv := &recordingInvoker{retFound: true}
ri.SetMux(99, inv)
// Simulate what NewRoutedClient produces being consumed by RoutedInvoker.
// The client prefixes "99/" to the service ID; the invoker strips it.
prefixed := "99/" + "my.Package.Service"
found, err := ri.InvokeMethod(prefixed, "Execute", nil)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if !found {
t.Fatal("expected found=true")
}
calls := inv.getCalls()
if len(calls) != 1 {
t.Fatalf("expected 1 call, got %d", len(calls))
}
if calls[0].serviceID != "my.Package.Service" {
t.Fatalf("got serviceID %q, want %q", calls[0].serviceID, "my.Package.Service")
}
if calls[0].methodID != "Execute" {
t.Fatalf("got methodID %q, want %q", calls[0].methodID, "Execute")
}
}