Skip to content

Commit

Permalink
Let weavertest test component implementations.
Browse files Browse the repository at this point in the history
Before this PR, you could use a weavertest.Runner to test a set of
component interfaces:

```go
runniner.Test(t, func(t *testing.T, a A, b B, c C) {
    ...
})
```

This PR extends the API to allow for testing of component
implementations. Assuming `a`, `b`, and `c` are the implementing structs
of component interfaces `A`, `B`, and `C`, we can now write:

```go
runniner.Test(t, func(t *testing.T, a *a, b *b, c *c) {
    ...
})
```

You can also combine interfaces and implementations:

```go
runniner.Test(t, func(t *testing.T, a *a, b B, c1 *c, c2 *c, cIntf C) {
    ...
})
```

You can also combine this with fakes. All possible combinations of
interfaces, implementation pointers, and fakes are allowed with one
exception. A fake and implmentation pointer cannot be provided for the
same component.

This functionality is useful for clearbox testing of components. It is
also useful for testing the main HTTP handling code associated with the
main component, as demonstrated in `examples/chat/server_test.go`.
  • Loading branch information
mwhittaker committed Jun 21, 2023
1 parent e0ac135 commit 85730c9
Show file tree
Hide file tree
Showing 12 changed files with 858 additions and 57 deletions.
11 changes: 9 additions & 2 deletions component.go
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,13 @@ type Main interface {
// implementation type instance.
type Implements[T any] struct {
*componentImpl

// Given a component implementation type, there is currently no nice way,
// using reflection, to get the corresponding component interface type [1].
// The component_interface_type field exists to make it possible.
//
// [1]: https://github.com/golang/go/issues/54393.
component_interface_type T //nolint:unused
}

// setInstance is used during component initialization to fill Implements.component.
Expand Down Expand Up @@ -176,8 +183,8 @@ func (r Ref[T]) isRef() {}
// the user can add the following lines to the application config file:
//
// [listeners]
// myListener = {local_address = "localhost:9000"}
// myOtherListener = {local_address = "localhost:9001"}
// myListener = {local_address = "localhost:9000"}
// myOtherListener = {local_address = "localhost:9001"}
//
// Listeners are identified by their field names in the component implementation
// structs (e.g., myListener and myOtherListener).
Expand Down
18 changes: 9 additions & 9 deletions examples/chat/server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,12 @@ import (
"fmt"
"io"
"net/http"
"net/http/httptest"
"regexp"
"sync"
"testing"
"time"

"github.com/ServiceWeaver/weaver"
"github.com/ServiceWeaver/weaver/weavertest"
)

Expand Down Expand Up @@ -77,20 +77,20 @@ func (db *db) GetImage(ctx context.Context, _ string, image ImageID) ([]byte, er
}

func TestServer(t *testing.T) {
t.Skip("TODO(mwhittaker): Enable testing of main.")
for _, r := range weavertest.AllRunners() {
// Use a fake DB since normal implementation does not work with multiple replicas
db := &db{}
r.Fakes = []weavertest.FakeComponent{weavertest.Fake[SQLStore](db)}
r.Test(t, func(t *testing.T, store weaver.Main) {
addr := "TODO(mwhittaker): Enable testing of main"
r.Test(t, func(t *testing.T, main *server) {
server := httptest.NewServer(main)
defer server.Close()

// Login
expect(t, "login", httpGet(t, addr, "/"), "Login")
expect(t, "feed", httpGet(t, addr, "/?name=user"), "/newthread")
expect(t, "login", httpGet(t, server.URL, "/"), "Login")
expect(t, "feed", httpGet(t, server.URL, "/?name=user"), "/newthread")

// Create a new thread
r := httpGet(t, addr, "/newthread?name=user&recipients=bar,baz&message=Hello&image=")
r := httpGet(t, server.URL, "/newthread?name=user&recipients=bar,baz&message=Hello&image=")
expect(t, "new thread", r, "Hello")
m := regexp.MustCompile(`id="tid(\d+)"`).FindStringSubmatch(r)
if m == nil {
Expand All @@ -99,7 +99,7 @@ func TestServer(t *testing.T) {
tid := m[1]

// Reply to thread
r = httpGet(t, addr, fmt.Sprintf("/newpost?name=user&tid=%s&post=Hi!", tid))
r = httpGet(t, server.URL, fmt.Sprintf("/newpost?name=user&tid=%s&post=Hi!", tid))
expect(t, "reply", r, `Hello`)
expect(t, "reply", r, `Hi!`)
})
Expand All @@ -108,7 +108,7 @@ func TestServer(t *testing.T) {

func httpGet(t *testing.T, addr, path string) string {
t.Helper()
response, err := http.Get("http://" + addr + path)
response, err := http.Get(addr + path)
if err != nil {
t.Fatal(err)
}
Expand Down
11 changes: 11 additions & 0 deletions godeps.txt
Original file line number Diff line number Diff line change
Expand Up @@ -938,6 +938,7 @@ github.com/ServiceWeaver/weaver/weavertest
github.com/google/uuid
go.opentelemetry.io/otel/sdk/trace
golang.org/x/exp/maps
golang.org/x/exp/slices
golang.org/x/sync/errgroup
os
reflect
Expand All @@ -947,6 +948,16 @@ github.com/ServiceWeaver/weaver/weavertest
sync
testing
time
github.com/ServiceWeaver/weaver/weavertest/internal/chain
context
errors
github.com/ServiceWeaver/weaver
github.com/ServiceWeaver/weaver/runtime/codegen
go.opentelemetry.io/otel/codes
go.opentelemetry.io/otel/trace
reflect
sync
time
github.com/ServiceWeaver/weaver/weavertest/internal/deploy
context
errors
Expand Down
5 changes: 4 additions & 1 deletion internal/private/private.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,9 @@ type App interface {
// Wait returns when the application has ended.
Wait(context.Context) error

// Get fetches the component with type t from wlet.
// Get fetches the component with interface type t from wlet.
Get(requester string, t reflect.Type) (any, error)

// GetImpl fetches the component implementation with type t from wlet.
GetImpl(requester string, t reflect.Type) (any, error)
}
46 changes: 36 additions & 10 deletions weavelet.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,8 +63,9 @@ type weavelet struct {
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
componentsByName map[string]*component // component name -> component
componentsByType map[reflect.Type]*component // component interface type -> component
componentsByImplType map[reflect.Type]*component // component impl type -> component

listenersMu sync.Mutex
listeners map[string]*listenerState
Expand Down Expand Up @@ -99,10 +100,11 @@ var _ private.App = &weavelet{}
// newWeavelet returns a new weavelet.
func newWeavelet(ctx context.Context, options private.AppOptions, 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)),
ctx: ctx,
overrides: options.Fakes,
componentsByName: make(map[string]*component, len(componentInfos)),
componentsByType: make(map[reflect.Type]*component, len(componentInfos)),
componentsByImplType: make(map[reflect.Type]*component, len(componentInfos)),
}

// TODO(mwhittaker): getEnv starts the WeaveletConn handler which calls
Expand All @@ -129,6 +131,7 @@ func newWeavelet(ctx context.Context, options private.AppOptions, componentInfos
}
w.componentsByName[info.Name] = c
w.componentsByType[info.Iface] = c
w.componentsByImplType[info.Impl] = c
}

if info.Mtls {
Expand Down Expand Up @@ -278,10 +281,19 @@ func (w *weavelet) Get(requester string, compType reflect.Type) (any, error) {
return nil, err
}
result, _, err := w.getInstance(w.ctx, component, requester)
return result, err
}

func (w *weavelet) GetImpl(requester string, compType reflect.Type) (any, error) {
component, err := w.getComponentByImplType(compType)
if err != nil {
return nil, err
}
impl, err := w.getImpl(w.ctx, component)
if err != nil {
return nil, err
}
return result, nil
return impl.impl, nil
}

// logRolodexCard pretty prints a card that includes basic information about
Expand Down Expand Up @@ -556,17 +568,31 @@ func (w *weavelet) getComponent(name string) (*component, error) {
return c, nil
}

// getComponentByType returns the component with the given type.
// getComponentByType returns the component with the given interface type.
func (w *weavelet) getComponentByType(t reflect.Type) (*component, error) {
// Note that we don't need to lock d.byType because, while the components
// referenced by d.byType are modified, d.byType itself is read-only.
// Note that we don't need to lock d.componentsByType because, while the
// components referenced by d.componentsByType are modified,
// d.componentsByType itself is read-only.
c, ok := w.componentsByType[t]
if !ok {
return nil, fmt.Errorf("component of type %v was not registered; maybe you forgot to run weaver generate", t)
}
return c, nil
}

// getComponentByImplType returns the component with the given implementation
// type.
func (w *weavelet) getComponentByImplType(t reflect.Type) (*component, error) {
// Note that we don't need to lock d.componentsByImplType because, while
// the components referenced by d.componentsByImplType are modified,
// d.componentsByImplType itself is read-only.
c, ok := w.componentsByImplType[t]
if !ok {
return nil, fmt.Errorf("component impl of type %v was not registered; maybe you forgot to run weaver generate", t)
}
return c, nil
}

// getImpl returns a component's componentImpl, initializing it if necessary.
func (w *weavelet) getImpl(ctx context.Context, c *component) (*componentImpl, error) {
init := func(c *component) error {
Expand Down
12 changes: 10 additions & 2 deletions weavertest/deployer.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import (
"errors"
"fmt"
"os"
"reflect"
"sync"

"github.com/ServiceWeaver/weaver/internal/envelope/conn"
Expand Down Expand Up @@ -101,8 +102,10 @@ type connection struct {

var _ envelope.EnvelopeHandler = &handler{}

// newDeployer returns a new weavertest multiprocess deployer.
func newDeployer(ctx context.Context, wlet *protos.EnvelopeInfo, config *protos.AppConfig, runner Runner, logWriter func(*protos.LogEntry)) *deployer {
// newDeployer returns a new weavertest multiprocess deployer. locals contains
// components that should be co-located with the main component and not
// replicated.
func newDeployer(ctx context.Context, wlet *protos.EnvelopeInfo, config *protos.AppConfig, runner Runner, locals []reflect.Type, logWriter func(*protos.LogEntry)) *deployer {
colocation := map[string]string{}
for _, group := range config.Colocate {
for _, c := range group.Components {
Expand All @@ -122,6 +125,11 @@ func newDeployer(ctx context.Context, wlet *protos.EnvelopeInfo, config *protos.
log: logWriter,
}

for _, local := range locals {
name := fmt.Sprintf("%s/%s", local.PkgPath(), local.Name())
d.local[name] = true
}

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

0 comments on commit 85730c9

Please sign in to comment.