Skip to content

Commit

Permalink
weavertest now supports fake component implementations. (#357)
Browse files Browse the repository at this point in the history
Some of the components in a Service Weaver application can
now be replaced with fake test-specific implementations when
using weavertest. The test code registers a set of such
fake implementations with weavertest.Runner. When the test
runs, the real component implementations are replaced by
the supplied fakes.
  • Loading branch information
ghemawat committed May 25, 2023
1 parent cd2fb25 commit 89ff04b
Show file tree
Hide file tree
Showing 8 changed files with 95 additions and 16 deletions.
12 changes: 10 additions & 2 deletions internal/private/private.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,17 @@ import (
"reflect"
)

// RunOptions controls a Service Weaver application execution.
type RunOptions struct {
// Fakes holds a mapping from component interface type to the fake
// implementation to use for that component.
Fakes map[reflect.Type]any
}

// Run runs a Service Weaver application rooted at a component with
// interface type rootType and app as the body of the application.
var Run func(ctx context.Context, rootType reflect.Type, app func(context.Context, any) error) error
// interface type rootType, app as the body of the application, and
// the supplied options.
var Run func(ctx context.Context, rootType reflect.Type, options RunOptions, app func(context.Context, any) error) error

// Get fetches the component with type t with root recorded as the requester.
var Get func(root any, t reflect.Type) (any, error)
11 changes: 10 additions & 1 deletion weavelet.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import (
"github.com/ServiceWeaver/weaver/internal/config"
"github.com/ServiceWeaver/weaver/internal/envelope/conn"
"github.com/ServiceWeaver/weaver/internal/net/call"
"github.com/ServiceWeaver/weaver/internal/private"
"github.com/ServiceWeaver/weaver/internal/traceio"
"github.com/ServiceWeaver/weaver/runtime"
"github.com/ServiceWeaver/weaver/runtime/codegen"
Expand Down Expand Up @@ -58,6 +59,7 @@ type weavelet struct {
transport *transport // Transport for cross-weavelet communication
dialAddr string // Address this weavelet is reachable at
tracer trace.Tracer // Tracer for this weavelet
overrides map[reflect.Type]any // Component implementation overrides

componentsByName map[string]*component // component name -> component
componentsByType map[reflect.Type]*component // component type -> component
Expand All @@ -82,9 +84,10 @@ type server struct {
var _ conn.WeaveletHandler = &weavelet{}

// newWeavelet returns a new weavelet.
func newWeavelet(ctx context.Context, componentInfos []*codegen.Registration) (*weavelet, error) {
func newWeavelet(ctx context.Context, options private.RunOptions, componentInfos []*codegen.Registration) (*weavelet, error) {
w := &weavelet{
ctx: ctx,
overrides: options.Fakes,
componentsByName: make(map[string]*component, len(componentInfos)),
componentsByType: make(map[reflect.Type]*component, len(componentInfos)),
}
Expand Down Expand Up @@ -557,6 +560,12 @@ func (w *weavelet) getImpl(c *component) (*componentImpl, error) {
}

func (w *weavelet) createComponent(ctx context.Context, c *component) error {
if obj, ok := w.overrides[c.info.Iface]; ok {
// Use supplied implementation (typically a weavertest fake).
c.impl.impl = obj
return nil
}

// Create the implementation object.
v := reflect.New(c.info.Impl)
obj := v.Interface()
Expand Down
6 changes: 3 additions & 3 deletions weaver.go
Original file line number Diff line number Diff line change
Expand Up @@ -133,11 +133,11 @@ func Run[T InstanceOf[Main]](ctx context.Context, app func(context.Context, T) e
}
return app(ctx, arg)
}
return internalRun(ctx, rootType, rootBody)
return internalRun(ctx, rootType, private.RunOptions{}, rootBody)
}

func internalRun(ctx context.Context, rootType reflect.Type, rootBody func(context.Context, any) error) error {
wlet, err := newWeavelet(ctx, codegen.Registered())
func internalRun(ctx context.Context, rootType reflect.Type, opts private.RunOptions, rootBody func(context.Context, any) error) error {
wlet, err := newWeavelet(ctx, opts, codegen.Registered())
if err != nil {
return fmt.Errorf("error initializating application: %w", err)
}
Expand Down
16 changes: 14 additions & 2 deletions weavertest/deployer.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ type deployer struct {
config *protos.AppConfig // application config
colocation map[string]string // maps component to group
running errgroup.Group // collects errors from goroutines
local map[string]bool // Components that should run locally
log func(*protos.LogEntry) // logs the passed in string

mu sync.Mutex // guards fields below
Expand Down Expand Up @@ -117,8 +118,19 @@ func newDeployer(ctx context.Context, wlet *protos.EnvelopeInfo, config *protos.
config: config,
colocation: colocation,
groups: map[string]*group{},
local: map[string]bool{},
log: logWriter,
}

// Force testMain to be local.
d.local["github.com/ServiceWeaver/weaver/weavertest/testMainInterface"] = true

// Fakes need to be local as well.
for _, fake := range runner.Fakes {
name := fmt.Sprintf("%s/%s", fake.intf.PkgPath(), fake.intf.Name())
d.local[name] = true
}

return d
}

Expand Down Expand Up @@ -381,8 +393,8 @@ func (d *deployer) group(component string) *group {
var name string
if !d.runner.multi {
name = "main" // Everything is in one group.
} else if component == "github.com/ServiceWeaver/weaver/weavertest/testMainInterface" {
name = "main" // Force testMain into main group
} else if d.local[component] {
name = "main" // Run locally
} else if x, ok := d.colocation[component]; ok {
name = x // Use specified group
} else {
Expand Down
32 changes: 29 additions & 3 deletions weavertest/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,12 @@ type Runner struct {
// config file. It can contain application level as well as
// component level configuration.
Config string

// Fakes holds a list of component implementations that should
// be used instead of the implementations registered in the binary.
// The typical use is to override some subset of the application
// code being tested with test-specific component implementations.
Fakes []FakeComponent
}

var (
Expand All @@ -65,6 +71,22 @@ var (
// AllRunners returns a slice of all builtin weavertest runners.
func AllRunners() []Runner { return []Runner{Local, RPC, Multi} }

// FakeComponent records the implementation to use for a specific component type.
type FakeComponent struct {
intf reflect.Type
impl any
}

// Fake arranges to use impl as the implementation for the component type T.
// The result is typically placed in Runner.Fakes.
// REQUIRES: impl must implement T.
func Fake[T any](impl any) FakeComponent {
if _, ok := impl.(T); !ok {
panic(fmt.Sprintf("%T does not implement %v", impl, reflect.TypeOf((*T)(nil)).Elem()))
}
return FakeComponent{intf: reflect.TypeOf((*T)(nil)).Elem(), impl: impl}
}

//go:generate ../cmd/weaver/weaver generate

// testMain is the component implementation used in tests.
Expand Down Expand Up @@ -162,7 +184,7 @@ func (r Runner) sub(t testing.TB, isBench bool, testBody any) {
ctx, cleanup = multiCtx, multiCleanup
}

if err := runWeaver(ctx, runner); err != nil {
if err := runWeaver(ctx, r, runner); err != nil {
t.Fatal(err)
}
}
Expand Down Expand Up @@ -207,8 +229,12 @@ func checkRunFunc(t testing.TB, fn any) (func(context.Context, any) error, error
}, nil
}

func runWeaver(ctx context.Context, body func(context.Context, any) error) error {
return private.Run(ctx, reflect.TypeOf((*testMainInterface)(nil)).Elem(), body)
func runWeaver(ctx context.Context, runner Runner, body func(context.Context, any) error) error {
opts := private.RunOptions{Fakes: map[reflect.Type]any{}}
for _, f := range runner.Fakes {
opts.Fakes[f.intf] = f.impl
}
return private.Run(ctx, reflect.TypeOf((*testMainInterface)(nil)).Elem(), opts, body)
}

// logStacks prints the stacks of live goroutines. This functionality
Expand Down
4 changes: 2 additions & 2 deletions weavertest/internal/diverge/diverge_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ func TestDealiasing(t *testing.T) {
if err != nil {
t.Fatal(err)
}
localCalls := runner == weavertest.Local
localCalls := runner.Name == weavertest.Local.Name
if want, got := localCalls, (pair.X == pair.Y); want != got {
t.Fatalf("expecting aliasing = %v, got %v", want, got)
}
Expand All @@ -45,7 +45,7 @@ func TestCustomErrors(t *testing.T) {
for _, runner := range weavertest.AllRunners() {
runner.Test(t, func(t *testing.T, e Errer) {
err := e.Err(context.Background(), 1)
localCalls := runner == weavertest.Local
localCalls := runner.Name == weavertest.Local.Name
if want, got := localCalls, errors.Is(err, IntError{2}); want != got {
t.Fatalf("expecting Is(IntError{2}) = %v, got %v for error %v", want, got, err)
}
Expand Down
28 changes: 26 additions & 2 deletions weavertest/internal/simple/simple_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ func TestOneComponent(t *testing.T) {
cPid := os.Getpid()
dstPid, _ := dst.Getpid(context.Background())
sameProcess := cPid == dstPid
if runner == weavertest.Multi {
if runner.Name == weavertest.Multi.Name {
if sameProcess {
t.Fatal("the root and the dst components should run in different processes")
}
Expand All @@ -52,6 +52,30 @@ func TestOneComponent(t *testing.T) {
}
}

type fakeDest struct{ file, msg string }

func (f *fakeDest) Getpid(context.Context) (int, error) { return 100, nil }
func (f *fakeDest) GetAll(context.Context, string) ([]string, error) { return nil, nil }
func (f *fakeDest) RoutedRecord(context.Context, string, string) error { return nil }
func (f *fakeDest) Record(ctx context.Context, file, msg string) error {
f.file = file
f.msg = msg
return nil
}

func TestFake(t *testing.T) {
for _, runner := range weavertest.AllRunners() {
fake := &fakeDest{}
runner.Fakes = append(runner.Fakes, weavertest.Fake[simple.Destination](fake))
runner.Test(t, func(t *testing.T, src simple.Source) {
src.Emit(context.Background(), "file", "msg")
if fake.file != "file" || fake.msg != "msg" {
t.Fatal("fake Destination method not called")
}
})
}
}

func TestTwoComponents(t *testing.T) {
// Add a list of items to a component (dst) from another component (src). Verify that
// dst updates the state accordingly.
Expand Down Expand Up @@ -114,7 +138,7 @@ func TestServer(t *testing.T) {
if err != nil {
t.Fatalf("Could not fetch proxy address: %v", err)
}
if runner != weavertest.Multi && proxy != "" {
if runner.Name != weavertest.Multi.Name && proxy != "" {
t.Fatalf("Unexpected proxy %q", proxy)
}

Expand Down
2 changes: 1 addition & 1 deletion weavertest/multi.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ func initMultiProcess(ctx context.Context, name string, isBench bool, runner Run
}
os.Exit(1)
}()
err := runWeaver(ctx, func(context.Context, any) error {
err := runWeaver(ctx, runner, func(context.Context, any) error {
return nil
})
if err != nil {
Expand Down

0 comments on commit 89ff04b

Please sign in to comment.