Skip to content

Commit

Permalink
chore: support extending ftltest context with additional options
Browse files Browse the repository at this point in the history
  • Loading branch information
jvmakine committed Aug 29, 2024
1 parent b5c95b3 commit 2b3ef5a
Show file tree
Hide file tree
Showing 4 changed files with 125 additions and 12 deletions.
7 changes: 6 additions & 1 deletion go-runtime/ftl/ftltest/fake.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,10 @@ type subscriber func(context.Context, any) error
type fakeFTL struct {
fsm *fakeFSMManager

// We store the options used to construct this fake, so they can be
// replayed to extend the fake with new options
options []Option

mockMaps map[uintptr]mapImpl
allowMapCalls bool
configValues map[string][]byte
Expand All @@ -64,13 +68,14 @@ type fakeFTL struct {
// type but is not constrained by input/output type like ftl.Map.
type mapImpl func(context.Context) (any, error)

func contextWithFakeFTL(ctx context.Context) context.Context {
func contextWithFakeFTL(ctx context.Context, options ...Option) context.Context {
fake := &fakeFTL{
fsm: newFakeFSMManager(),
mockMaps: map[uintptr]mapImpl{},
allowMapCalls: false,
configValues: map[string][]byte{},
secretValues: map[string][]byte{},
options: options,
}
ctx = internal.WithContext(ctx, fake)
fake.pubSub = newFakePubSub(ctx)
Expand Down
40 changes: 29 additions & 11 deletions go-runtime/ftl/ftltest/ftltest.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ import (
mcu "github.com/TBD54566975/ftl/internal/testutils/modulecontext"
)

// Allows tests to mock module reflection
var moduleGetter = reflection.Module

type OptionsState struct {
databases map[string]modulecontext.Database
mockVerbs map[schema.RefKey]modulecontext.Verb
Expand All @@ -45,14 +48,18 @@ type Option struct {

// Context suitable for use in testing FTL verbs with provided options
func Context(options ...Option) context.Context {
ctx := log.ContextWithNewDefaultLogger(context.Background())
module := moduleGetter()
return newContext(ctx, module, options...)
}

func newContext(ctx context.Context, module string, options ...Option) context.Context {
state := &OptionsState{
databases: make(map[string]modulecontext.Database),
mockVerbs: make(map[schema.RefKey]modulecontext.Verb),
}

ctx := log.ContextWithNewDefaultLogger(context.Background())
ctx = contextWithFakeFTL(ctx)
name := reflection.Module()
ctx = contextWithFakeFTL(ctx, options...)

sort.Slice(options, func(i, j int) bool {
return options[i].rank < options[j].rank
Expand All @@ -65,11 +72,22 @@ func Context(options ...Option) context.Context {
}
}

builder := modulecontext.NewBuilder(name).AddDatabases(state.databases)
builder := modulecontext.NewBuilder(module).AddDatabases(state.databases)
builder = builder.UpdateForTesting(state.mockVerbs, state.allowDirectVerbBehavior, newFakeLeaseClient())

return mcu.MakeDynamic(ctx, builder.Build()).ApplyToContext(ctx)
}

// SubContext applies the given options to the given context, creating a new
// context extending the previous one.
//
// Does not modify the existing context
func SubContext(ctx context.Context, options ...Option) context.Context {
oldFtl := internal.FromContext(ctx).(*fakeFTL) //nolint:forcetypeassert
module := moduleGetter()
return newContext(ctx, module, append(oldFtl.options, options...)...)
}

// WithDefaultProjectFile loads config and secrets from the default project
// file, which is either the FTL_CONFIG environment variable or the
// ftl-project.toml file in the git root.
Expand Down Expand Up @@ -113,7 +131,7 @@ func WithProjectFile(path string) Option {
if err != nil {
return fmt.Errorf("could not set up configs: %w", err)
}
configs, err := cm.MapForModule(ctx, reflection.Module())
configs, err := cm.MapForModule(ctx, moduleGetter())
if err != nil {
return fmt.Errorf("could not read configs: %w", err)
}
Expand All @@ -129,7 +147,7 @@ func WithProjectFile(path string) Option {
if err != nil {
return fmt.Errorf("could not set up secrets: %w", err)
}
secrets, err := sm.MapForModule(ctx, reflection.Module())
secrets, err := sm.MapForModule(ctx, moduleGetter())
if err != nil {
return fmt.Errorf("could not read secrets: %w", err)
}
Expand All @@ -156,8 +174,8 @@ func WithConfig[T ftl.ConfigType](config ftl.ConfigValue[T], value T) Option {
return Option{
rank: other,
apply: func(ctx context.Context, state *OptionsState) error {
if config.Module != reflection.Module() {
return fmt.Errorf("config %v does not match current module %s", config.Module, reflection.Module())
if config.Module != moduleGetter() {
return fmt.Errorf("config %v does not match current module %s", config.Module, moduleGetter())
}
fftl := internal.FromContext(ctx).(*fakeFTL) //nolint:forcetypeassert
if err := fftl.setConfig(config.Name, value); err != nil {
Expand All @@ -180,8 +198,8 @@ func WithSecret[T ftl.SecretType](secret ftl.SecretValue[T], value T) Option {
return Option{
rank: other,
apply: func(ctx context.Context, state *OptionsState) error {
if secret.Module != reflection.Module() {
return fmt.Errorf("secret %v does not match current module %s", secret.Module, reflection.Module())
if secret.Module != moduleGetter() {
return fmt.Errorf("secret %v does not match current module %s", secret.Module, moduleGetter())
}
fftl := internal.FromContext(ctx).(*fakeFTL) //nolint:forcetypeassert
if err := fftl.setSecret(secret.Name, value); err != nil {
Expand All @@ -205,7 +223,7 @@ func WithDatabase(dbHandle ftl.Database) Option {
rank: other,
apply: func(ctx context.Context, state *OptionsState) error {
fftl := internal.FromContext(ctx)
originalDSN, err := getDSNFromSecret(fftl, reflection.Module(), dbHandle.Name)
originalDSN, err := getDSNFromSecret(fftl, moduleGetter(), dbHandle.Name)
if err != nil {
return err
}
Expand Down
67 changes: 67 additions & 0 deletions go-runtime/ftl/ftltest/ftltest_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,11 @@ import (
"context"
"fmt"
"testing"
_ "unsafe"

"github.com/TBD54566975/ftl/go-runtime/ftl"
"github.com/TBD54566975/ftl/go-runtime/ftl/reflection"
"github.com/TBD54566975/ftl/go-runtime/internal"
"github.com/TBD54566975/ftl/internal/log"
"github.com/alecthomas/assert/v2"
)
Expand Down Expand Up @@ -36,3 +39,67 @@ func TestFtlTestProjectNotLoadedInContext(t *testing.T) {
_ = ftl.Config[string]("moo").Get(ctx)
})
}

func TestFtlTextContextExtension(t *testing.T) {
withFakeModule(t, "ftl/test")

t.Run("extends with a new project file", func(t *testing.T) {
original := Context(WithProjectFile("testdata/go/wrapped/ftl-project.toml"))
extended := SubContext(original, WithProjectFile("testdata/go/wrapped/ftl-project-test-1.toml"))

var config string
assert.NoError(t, internal.FromContext(original).(*fakeFTL).GetConfig(original, "config", &config)) //nolint:forcetypeassert
assert.Equal(t, "bazbaz", config, "does not change the original context")
assert.NoError(t, internal.FromContext(extended).(*fakeFTL).GetConfig(extended, "config", &config)) //nolint:forcetypeassert
assert.Equal(t, "foobar", config, "overwrites configuration values from the new file")
})
t.Run("extends with a new config value", func(t *testing.T) {
configA := ftl.ConfigValue[string]{Ref: reflection.Ref{Module: "ftl/test", Name: "configA"}}
configB := ftl.ConfigValue[string]{Ref: reflection.Ref{Module: "ftl/test", Name: "configB"}}

original := Context(WithConfig(configA, "a"), WithConfig(configB, "b"))
extended := SubContext(original, WithConfig(configA, "a.2"))

var config string
assert.NoError(t, internal.FromContext(original).(*fakeFTL).GetConfig(original, "configA", &config)) //nolint:forcetypeassert
assert.Equal(t, "a", config, "does not change the original context")
assert.NoError(t, internal.FromContext(extended).(*fakeFTL).GetConfig(extended, "configA", &config)) //nolint:forcetypeassert
assert.Equal(t, "a.2", config, "overwrites configuration values from the new file")
assert.NoError(t, internal.FromContext(extended).(*fakeFTL).GetConfig(extended, "configB", &config)) //nolint:forcetypeassert
assert.Equal(t, "b", config, "retains other config from the original context")
})
t.Run("extends with a new secret value", func(t *testing.T) {
secretA := ftl.SecretValue[string]{Ref: reflection.Ref{Module: "ftl/test", Name: "secretA"}}
secretB := ftl.SecretValue[string]{Ref: reflection.Ref{Module: "ftl/test", Name: "secretB"}}

original := Context(WithSecret(secretA, "a"), WithSecret(secretB, "b"))
extended := SubContext(original, WithSecret(secretA, "a.2"))

var config string
assert.NoError(t, internal.FromContext(original).(*fakeFTL).GetSecret(original, "secretA", &config)) //nolint:forcetypeassert
assert.Equal(t, "a", config, "does not change the original context")
assert.NoError(t, internal.FromContext(extended).(*fakeFTL).GetSecret(extended, "secretA", &config)) //nolint:forcetypeassert
assert.Equal(t, "a.2", config, "overwrites secret values from the new file")
assert.NoError(t, internal.FromContext(extended).(*fakeFTL).GetSecret(extended, "secretB", &config)) //nolint:forcetypeassert
assert.Equal(t, "b", config, "retains other secret from the original context")
})
t.Run("retains the existing context.Context state", func(t *testing.T) {
type keyType string
original := context.WithValue(Context(), keyType("key"), "value")
extended := SubContext(original, WithProjectFile("testdata/go/wrapped/ftl-project.toml"))

assert.Equal(t, "value", extended.Value(keyType("key")), "keeps context.Context value from the original context")
})
}

// mock out module reflection to make it testable
func withFakeModule(t *testing.T, name string) {
t.Helper()
var previousModuleGetter = moduleGetter
moduleGetter = func() string {
return name
}
t.Cleanup(func() {
moduleGetter = previousModuleGetter
})
}
23 changes: 23 additions & 0 deletions go-runtime/ftl/ftltest/testdata/go/verbtypes/verbtypes_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,29 @@ func TestVerbs(t *testing.T) {
assert.Equal(t, knockOnEffects["empty"], "test")
}

func TestContextExtension(t *testing.T) {
ctx1 := ftltest.Context(
ftltest.WhenSource(Source, func(ctx context.Context) (Response, error) {
return Response{Output: "fake"}, nil
}),
)

ctx2 := ftltest.SubContext(
ctx1,
ftltest.WhenSource(Source, func(ctx context.Context) (Response, error) {
return Response{Output: "another fake"}, nil
}),
)

sourceResp, err := ftl.CallSource(ctx1, Source)
assert.NoError(t, err)
assert.Equal(t, Response{Output: "fake"}, sourceResp)

sourceResp, err = ftl.CallSource(ctx2, Source)
assert.NoError(t, err)
assert.Equal(t, Response{Output: "another fake"}, sourceResp)
}

func TestVerbErrors(t *testing.T) {
ctx := ftltest.Context(
ftltest.WhenVerb(Verb, func(ctx context.Context, req Request) (Response, error) {
Expand Down

0 comments on commit 2b3ef5a

Please sign in to comment.