diff --git a/go-runtime/ftl/call.go b/go-runtime/ftl/call.go index 57d6e8ded7..c483bab6f1 100644 --- a/go-runtime/ftl/call.go +++ b/go-runtime/ftl/call.go @@ -14,12 +14,7 @@ import ( "github.com/TBD54566975/ftl/internal/rpc" ) -func call[Req, Resp any](ctx context.Context, callee Ref, req Req) (resp Resp, err error) { - reqData, err := encoding.Marshal(req) - if err != nil { - return resp, fmt.Errorf("%s: failed to marshal request: %w", callee, err) - } - +func call[Req, Resp any](ctx context.Context, callee Ref, req Req, inline Verb[Req, Resp]) (resp Resp, err error) { behavior, err := modulecontext.FromContext(ctx).BehaviorForVerb(modulecontext.Ref(callee)) if err != nil { return resp, fmt.Errorf("%s: %w", callee, err) @@ -35,8 +30,17 @@ func call[Req, Resp any](ctx context.Context, callee Ref, req Req) (resp Resp, e } return resp, fmt.Errorf("%s: overridden verb had invalid response type %T, expected %v", callee, uncheckedResp, reflect.TypeFor[Resp]()) case modulecontext.DirectBehavior: - panic("not implemented") + resp, err = inline(ctx, req) + if err != nil { + return resp, fmt.Errorf("%s: %w", callee, err) + } + return resp, nil case modulecontext.StandardBehavior: + reqData, err := encoding.Marshal(req) + if err != nil { + return resp, fmt.Errorf("%s: failed to marshal request: %w", callee, err) + } + client := rpc.ClientFromContext[ftlv1connect.VerbServiceClient](ctx) cresp, err := client.Call(ctx, connect.NewRequest(&ftlv1.CallRequest{Verb: callee.ToProto(), Body: reqData})) if err != nil { @@ -63,22 +67,28 @@ func call[Req, Resp any](ctx context.Context, callee Ref, req Req) (resp Resp, e // Call a Verb through the FTL Controller. func Call[Req, Resp any](ctx context.Context, verb Verb[Req, Resp], req Req) (Resp, error) { - return call[Req, Resp](ctx, FuncRef(verb), req) + return call[Req, Resp](ctx, FuncRef(verb), req, verb) } // CallSink calls a Sink through the FTL controller. func CallSink[Req any](ctx context.Context, sink Sink[Req], req Req) error { - _, err := call[Req, Unit](ctx, FuncRef(sink), req) + _, err := call[Req, Unit](ctx, FuncRef(sink), req, func(ctx context.Context, req Req) (Unit, error) { + return Unit{}, sink(ctx, req) + }) return err } // CallSource calls a Source through the FTL controller. func CallSource[Resp any](ctx context.Context, source Source[Resp]) (Resp, error) { - return call[Unit, Resp](ctx, FuncRef(source), Unit{}) + return call[Unit, Resp](ctx, FuncRef(source), Unit{}, func(ctx context.Context, req Unit) (Resp, error) { + return source(ctx) + }) } // CallEmpty calls a Verb with no request or response through the FTL controller. func CallEmpty(ctx context.Context, empty Empty) error { - _, err := call[Unit, Unit](ctx, FuncRef(empty), Unit{}) + _, err := call[Unit, Unit](ctx, FuncRef(empty), Unit{}, func(ctx context.Context, req Unit) (Unit, error) { + return Unit{}, empty(ctx) + }) return err } diff --git a/go-runtime/ftl/config_test.go b/go-runtime/ftl/config_test.go index 9149703950..145adaa37d 100644 --- a/go-runtime/ftl/config_test.go +++ b/go-runtime/ftl/config_test.go @@ -13,7 +13,7 @@ import ( func TestConfig(t *testing.T) { ctx := log.ContextWithNewDefaultLogger(context.Background()) - moduleCtx := modulecontext.New() + moduleCtx := modulecontext.New("test") ctx = moduleCtx.ApplyToContext(ctx) type C struct { diff --git a/go-runtime/ftl/ftltest/ftltest.go b/go-runtime/ftl/ftltest/ftltest.go index 24349b18be..538169591f 100644 --- a/go-runtime/ftl/ftltest/ftltest.go +++ b/go-runtime/ftl/ftltest/ftltest.go @@ -89,3 +89,13 @@ func WhenVerb[Req any, Resp any](verb ftl.Verb[Req, Resp], fake func(ctx context return nil } } + +// WithCallsAllowedWithinModule allows tests to enable calls to all verbs within the current module +// +// Any overrides provided by calling WhenVerb(...) will take precedence +func WithCallsAllowedWithinModule() func(context.Context) error { + return func(ctx context.Context) error { + modulecontext.FromContext(ctx).AllowDirectVerbBehaviorWithinModule() + return nil + } +} diff --git a/go-runtime/ftl/secrets_test.go b/go-runtime/ftl/secrets_test.go index b1079c856e..72df2ba111 100644 --- a/go-runtime/ftl/secrets_test.go +++ b/go-runtime/ftl/secrets_test.go @@ -13,7 +13,7 @@ import ( func TestSecret(t *testing.T) { ctx := log.ContextWithNewDefaultLogger(context.Background()) - moduleCtx := modulecontext.New() + moduleCtx := modulecontext.New("test") ctx = moduleCtx.ApplyToContext(ctx) type C struct { diff --git a/go-runtime/modulecontext/from_environment.go b/go-runtime/modulecontext/from_environment.go index d3d1a12fcb..972e13df7e 100644 --- a/go-runtime/modulecontext/from_environment.go +++ b/go-runtime/modulecontext/from_environment.go @@ -18,9 +18,9 @@ func FromEnvironment(ctx context.Context, module string, isTesting bool) (*Modul // TODO: split this func into separate purposes: explicitly reading a particular project file, and reading DSNs from environment var moduleCtx *ModuleContext if isTesting { - moduleCtx = NewForTesting() + moduleCtx = NewForTesting(module) } else { - moduleCtx = New() + moduleCtx = New(module) } cm, err := cf.NewDefaultConfigurationManagerFromEnvironment(ctx) diff --git a/go-runtime/modulecontext/from_proto.go b/go-runtime/modulecontext/from_proto.go index aea15e6787..ebf7a1ef63 100644 --- a/go-runtime/modulecontext/from_proto.go +++ b/go-runtime/modulecontext/from_proto.go @@ -5,7 +5,7 @@ import ( ) func FromProto(response *ftlv1.ModuleContextResponse) (*ModuleContext, error) { - moduleCtx := New() + moduleCtx := New(response.Module) for name, data := range response.Configs { moduleCtx.configs[name] = data } diff --git a/go-runtime/modulecontext/module_context.go b/go-runtime/modulecontext/module_context.go index c1a41ae354..fcf7a8be69 100644 --- a/go-runtime/modulecontext/module_context.go +++ b/go-runtime/modulecontext/module_context.go @@ -6,8 +6,10 @@ import ( "encoding/json" "fmt" "strconv" + "strings" ftlv1 "github.com/TBD54566975/ftl/backend/protos/xyz/block/ftl/v1" + _ "github.com/jackc/pgx/v5/stdlib" // SQL driver ) @@ -41,17 +43,21 @@ func (x DBType) String() string { // ModuleContext holds the context needed for a module, including configs, secrets and DSNs type ModuleContext struct { - isTesting bool + module string configs map[string][]byte secrets map[string][]byte databases map[string]dbEntry - mockVerbs map[Ref]MockVerb + + isTesting bool + mockVerbs map[Ref]MockVerb + allowDirectVerbBehavior bool } type contextKeyModuleContext struct{} -func New() *ModuleContext { +func New(module string) *ModuleContext { return &ModuleContext{ + module: module, configs: map[string][]byte{}, secrets: map[string][]byte{}, databases: map[string]dbEntry{}, @@ -59,8 +65,8 @@ func New() *ModuleContext { } } -func NewForTesting() *ModuleContext { - moduleCtx := New() +func NewForTesting(module string) *ModuleContext { + moduleCtx := New(module) moduleCtx.isTesting = true return moduleCtx } @@ -164,10 +170,13 @@ func (m *ModuleContext) GetDatabase(name string, dbType DBType) (*sql.DB, error) func (m *ModuleContext) BehaviorForVerb(ref Ref) (VerbBehavior, error) { if mock, ok := m.mockVerbs[ref]; ok { return MockBehavior{Mock: mock}, nil - } - // TODO: add logic here for when to do direct behavior - if m.isTesting { - return StandardBehavior{}, fmt.Errorf("no mock found") + } else if m.allowDirectVerbBehavior && ref.Module == m.module { + return DirectBehavior{}, nil + } else if m.isTesting { + if ref.Module == m.module { + return StandardBehavior{}, fmt.Errorf("no mock found: provide a mock with ftltest.WhenVerb(%s, ...) or enable all calls within the module with ftltest.WithCallsAllowedWithinModule()", strings.ToUpper(ref.Name[:1])+ref.Name[1:]) + } + return StandardBehavior{}, fmt.Errorf("no mock found: provide a mock with ftltest.WhenVerb(%s.%s, ...)", ref.Module, strings.ToUpper(ref.Name[:1])+ref.Name[1:]) } return StandardBehavior{}, nil } @@ -176,6 +185,10 @@ func (m *ModuleContext) SetMockVerb(ref Ref, mock MockVerb) { m.mockVerbs[ref] = mock } +func (m *ModuleContext) AllowDirectVerbBehaviorWithinModule() { + m.allowDirectVerbBehavior = true +} + // VerbBehavior indicates how to execute a verb // //sumtype:decl diff --git a/go-runtime/modulecontext/module_context_test.go b/go-runtime/modulecontext/module_context_test.go index 9818d418d1..92357c92da 100644 --- a/go-runtime/modulecontext/module_context_test.go +++ b/go-runtime/modulecontext/module_context_test.go @@ -11,7 +11,7 @@ import ( func TestGettingAndSettingFromContext(t *testing.T) { ctx := log.ContextWithNewDefaultLogger(context.Background()) - moduleCtx := New() + moduleCtx := New("test") ctx = moduleCtx.ApplyToContext(ctx) assert.Equal(t, moduleCtx, FromContext(ctx), "module context should be the same when read from context") } diff --git a/integration/actions_test.go b/integration/actions_test.go index 9ed122138f..18e1acd32e 100644 --- a/integration/actions_test.go +++ b/integration/actions_test.go @@ -144,6 +144,13 @@ func deploy(module string) action { ) } +// Build modules from the working directory and wait for it to become available. +func build(modules ...string) action { + args := []string{"build"} + args = append(args, modules...) + return exec("ftl", args...) +} + // wait for the given module to deploy. func wait(module string) action { return func(t testing.TB, ic testContext) error { @@ -346,3 +353,7 @@ func httpCall(method string, path string, body []byte, onResponse func(resp *htt }) } } + +func testModule(module string) action { + return chdir(module, exec("go", "test", "-v", ".")) +} diff --git a/integration/integration_test.go b/integration/integration_test.go index 60c3ae3fff..1670d2741b 100644 --- a/integration/integration_test.go +++ b/integration/integration_test.go @@ -212,6 +212,15 @@ func TestHttpIngress(t *testing.T) { func TestRuntimeReflection(t *testing.T) { run(t, copyModule("runtimereflection"), - chdir("runtimereflection", exec("go", "test", "-v", ".")), + testModule("runtimereflection"), + ) +} + +func TestModuleUnitTests(t *testing.T) { + run(t, + copyModule("time"), + copyModule("wrapped"), + build("time", "wrapped"), + testModule("wrapped"), ) } diff --git a/integration/testdata/go/wrapped/ftl.toml b/integration/testdata/go/wrapped/ftl.toml new file mode 100644 index 0000000000..7f47362c94 --- /dev/null +++ b/integration/testdata/go/wrapped/ftl.toml @@ -0,0 +1,2 @@ +module = "wrapped" +language = "go" diff --git a/integration/testdata/go/wrapped/go.mod b/integration/testdata/go/wrapped/go.mod new file mode 100644 index 0000000000..bcb01a97eb --- /dev/null +++ b/integration/testdata/go/wrapped/go.mod @@ -0,0 +1,51 @@ +module ftl/wrapped + +go 1.22.2 + +require ( + github.com/TBD54566975/ftl v0.193.0 + github.com/alecthomas/assert/v2 v2.9.0 +) + +require ( + connectrpc.com/connect v1.14.0 // indirect + connectrpc.com/grpcreflect v1.2.0 // indirect + connectrpc.com/otelconnect v0.7.0 // indirect + github.com/BurntSushi/toml v1.3.2 // indirect + github.com/TBD54566975/scaffolder v0.8.0 // indirect + github.com/alecthomas/concurrency v0.0.2 // indirect + github.com/alecthomas/kong v0.9.0 // indirect + github.com/alecthomas/participle/v2 v2.1.1 // indirect + github.com/alecthomas/repr v0.4.0 // indirect + github.com/alecthomas/types v0.14.0 // indirect + github.com/alessio/shellescape v1.4.2 // indirect + github.com/danieljoos/wincred v1.2.0 // indirect + github.com/go-logr/logr v1.4.1 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/godbus/dbus/v5 v5.1.0 // indirect + github.com/hexops/gotextdiff v1.0.3 // indirect + github.com/jackc/pgpassfile v1.0.0 // indirect + github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect + github.com/jackc/pgx/v5 v5.5.5 // indirect + github.com/jackc/puddle/v2 v2.2.1 // indirect + github.com/jpillora/backoff v1.0.0 // indirect + github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/multiformats/go-base36 v0.2.0 // indirect + github.com/swaggest/jsonschema-go v0.3.70 // indirect + github.com/swaggest/refl v1.3.0 // indirect + github.com/zalando/go-keyring v0.2.4 // indirect + go.opentelemetry.io/otel v1.26.0 // indirect + go.opentelemetry.io/otel/metric v1.26.0 // indirect + go.opentelemetry.io/otel/trace v1.26.0 // indirect + golang.org/x/crypto v0.22.0 // indirect + golang.org/x/exp v0.0.0-20240222234643-814bf88cf225 // indirect + golang.org/x/mod v0.17.0 // indirect + golang.org/x/net v0.24.0 // indirect + golang.org/x/sync v0.7.0 // indirect + golang.org/x/sys v0.19.0 // indirect + golang.org/x/text v0.14.0 // indirect + google.golang.org/protobuf v1.33.0 // indirect +) + +replace github.com/TBD54566975/ftl => ../../../.. diff --git a/integration/testdata/go/wrapped/go.sum b/integration/testdata/go/wrapped/go.sum new file mode 100644 index 0000000000..da511de1e8 --- /dev/null +++ b/integration/testdata/go/wrapped/go.sum @@ -0,0 +1,142 @@ +connectrpc.com/connect v1.14.0 h1:PDS+J7uoz5Oui2VEOMcfz6Qft7opQM9hPiKvtGC01pA= +connectrpc.com/connect v1.14.0/go.mod h1:uoAq5bmhhn43TwhaKdGKN/bZcGtzPW1v+ngDTn5u+8s= +connectrpc.com/grpcreflect v1.2.0 h1:Q6og1S7HinmtbEuBvARLNwYmTbhEGRpHDhqrPNlmK+U= +connectrpc.com/grpcreflect v1.2.0/go.mod h1:nwSOKmE8nU5u/CidgHtPYk1PFI3U9ignz7iDMxOYkSY= +connectrpc.com/otelconnect v0.7.0 h1:ZH55ZZtcJOTKWWLy3qmL4Pam4RzRWBJFOqTPyAqCXkY= +connectrpc.com/otelconnect v0.7.0/go.mod h1:Bt2ivBymHZHqxvo4HkJ0EwHuUzQN6k2l0oH+mp/8nwc= +github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8= +github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= +github.com/TBD54566975/scaffolder v0.8.0 h1:DWl1K3dWcLsOPAYGQGPQXtffrml6XCB0tF05JdpMqZU= +github.com/TBD54566975/scaffolder v0.8.0/go.mod h1:Ab/jbQ4q8EloYL0nbkdh2DVvkGc4nxr1OcIbdMpTxxg= +github.com/alecthomas/assert/v2 v2.9.0 h1:ZcLG8ccMEtlMLkLW4gwGpBWBb0N8MUCmsy1lYBVd1xQ= +github.com/alecthomas/assert/v2 v2.9.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k= +github.com/alecthomas/concurrency v0.0.2 h1:Q3kGPtLbleMbH9lHX5OBFvJygfyFw29bXZKBg+IEVuo= +github.com/alecthomas/concurrency v0.0.2/go.mod h1:GmuQb/iHX7mbNtPlC/WDzEFxDMB0HYFer2Qda9QTs7w= +github.com/alecthomas/kong v0.9.0 h1:G5diXxc85KvoV2f0ZRVuMsi45IrBgx9zDNGNj165aPA= +github.com/alecthomas/kong v0.9.0/go.mod h1:Y47y5gKfHp1hDc7CH7OeXgLIpp+Q2m1Ni0L5s3bI8Os= +github.com/alecthomas/participle/v2 v2.1.1 h1:hrjKESvSqGHzRb4yW1ciisFJ4p3MGYih6icjJvbsmV8= +github.com/alecthomas/participle/v2 v2.1.1/go.mod h1:Y1+hAs8DHPmc3YUFzqllV+eSQ9ljPTk0ZkPMtEdAx2c= +github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc= +github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= +github.com/alecthomas/types v0.14.0 h1:4pCdEWVctLZQP9dE48fCyXWYkcoQtkf1EAxx9xGfCRY= +github.com/alecthomas/types v0.14.0/go.mod h1:fIOGnLeeUJXe1AAVofQmMaEMWLxY9bK4QxTLGIo30PA= +github.com/alessio/shellescape v1.4.2 h1:MHPfaU+ddJ0/bYWpgIeUnQUqKrlJ1S7BfEYPM4uEoM0= +github.com/alessio/shellescape v1.4.2/go.mod h1:PZAiSCk0LJaZkiCSkPv8qIobYglO3FPpyFjDCtHLS30= +github.com/bool64/dev v0.2.34 h1:P9n315P8LdpxusnYQ0X7MP1CZXwBK5ae5RZrd+GdSZE= +github.com/bool64/dev v0.2.34/go.mod h1:iJbh1y/HkunEPhgebWRNcs8wfGq7sjvJ6W5iabL8ACg= +github.com/bool64/shared v0.1.5 h1:fp3eUhBsrSjNCQPcSdQqZxxh9bBwrYiZ+zOKFkM0/2E= +github.com/bool64/shared v0.1.5/go.mod h1:081yz68YC9jeFB3+Bbmno2RFWvGKv1lPKkMP6MHJlPs= +github.com/danieljoos/wincred v1.2.0 h1:ozqKHaLK0W/ii4KVbbvluM91W2H3Sh0BncbUNPS7jLE= +github.com/danieljoos/wincred v1.2.0/go.mod h1:FzQLLMKBFdvu+osBrnFODiv32YGwCfx0SkRa/eYHgec= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ= +github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk= +github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= +github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= +github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= +github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= +github.com/iancoleman/orderedmap v0.3.0 h1:5cbR2grmZR/DiVt+VJopEhtVs9YGInGIxAoMJn+Ichc= +github.com/iancoleman/orderedmap v0.3.0/go.mod h1:XuLcCUkdL5owUCQeF2Ue9uuw1EptkJDkXXS7VoV7XGE= +github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= +github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= +github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk= +github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= +github.com/jackc/pgx/v5 v5.5.5 h1:amBjrZVmksIdNjxGW/IiIMzxMKZFelXbUoPNb+8sjQw= +github.com/jackc/pgx/v5 v5.5.5/go.mod h1:ez9gk+OAat140fv9ErkZDYFWmXLfV+++K0uAOiwgm1A= +github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk= +github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= +github.com/jpillora/backoff v1.0.0 h1:uvFg412JmmHBHw7iwprIxkPMI+sGQ4kzOWsMeHnm2EA= +github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= +github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs= +github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/multiformats/go-base36 v0.2.0 h1:lFsAbNOGeKtuKozrtBsAkSVhv1p9D0/qedU9rQyccr0= +github.com/multiformats/go-base36 v0.2.0/go.mod h1:qvnKE++v+2MWCfePClUEjE78Z7P2a1UV0xHgWc0hkp4= +github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4= +github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= +github.com/santhosh-tekuri/jsonschema/v5 v5.3.1 h1:lZUw3E0/J3roVtGQ+SCrUrg3ON6NgVqpn3+iol9aGu4= +github.com/santhosh-tekuri/jsonschema/v5 v5.3.1/go.mod h1:uToXkOrWAZ6/Oc07xWQrPOhJotwFIyu2bBVN41fcDUY= +github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8= +github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/swaggest/assertjson v1.9.0 h1:dKu0BfJkIxv/xe//mkCrK5yZbs79jL7OVf9Ija7o2xQ= +github.com/swaggest/assertjson v1.9.0/go.mod h1:b+ZKX2VRiUjxfUIal0HDN85W0nHPAYUbYH5WkkSsFsU= +github.com/swaggest/jsonschema-go v0.3.70 h1:8Vx5nm5t/6DBFw2+WC0/Vp1ZVe9/4mpuA0tuAe0wwCI= +github.com/swaggest/jsonschema-go v0.3.70/go.mod h1:7N43/CwdaWgPUDfYV70K7Qm79tRqe/al7gLSt9YeGIE= +github.com/swaggest/refl v1.3.0 h1:PEUWIku+ZznYfsoyheF97ypSduvMApYyGkYF3nabS0I= +github.com/swaggest/refl v1.3.0/go.mod h1:3Ujvbmh1pfSbDYjC6JGG7nMgPvpG0ehQL4iNonnLNbg= +github.com/yudai/gojsondiff v1.0.0 h1:27cbfqXLVEJ1o8I6v3y9lg8Ydm53EKqHXAOMxEGlCOA= +github.com/yudai/gojsondiff v1.0.0/go.mod h1:AY32+k2cwILAkW1fbgxQ5mUmMiZFgLIV+FBNExI05xg= +github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82 h1:BHyfKlQyqbsFN5p3IfnEUduWvb9is428/nNb5L3U01M= +github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82/go.mod h1:lgjkn3NuSvDfVJdfcVVdX+jpBxNmX4rDAzaS45IcYoM= +github.com/zalando/go-keyring v0.2.4 h1:wi2xxTqdiwMKbM6TWwi+uJCG/Tum2UV0jqaQhCa9/68= +github.com/zalando/go-keyring v0.2.4/go.mod h1:HL4k+OXQfJUWaMnqyuSOc0drfGPX2b51Du6K+MRgZMk= +go.opentelemetry.io/otel v1.26.0 h1:LQwgL5s/1W7YiiRwxf03QGnWLb2HW4pLiAhaA5cZXBs= +go.opentelemetry.io/otel v1.26.0/go.mod h1:UmLkJHUAidDval2EICqBMbnAd0/m2vmpf/dAM+fvFs4= +go.opentelemetry.io/otel/metric v1.26.0 h1:7S39CLuY5Jgg9CrnA9HHiEjGMF/X2VHvoXGgSllRz30= +go.opentelemetry.io/otel/metric v1.26.0/go.mod h1:SY+rHOI4cEawI9a7N1A4nIg/nTQXe1ccCNWYOJUrpX4= +go.opentelemetry.io/otel/sdk v1.26.0 h1:Y7bumHf5tAiDlRYFmGqetNcLaVUZmh4iYfmGxtmz7F8= +go.opentelemetry.io/otel/sdk v1.26.0/go.mod h1:0p8MXpqLeJ0pzcszQQN4F0S5FVjBLgypeGSngLsmirs= +go.opentelemetry.io/otel/sdk/metric v1.26.0 h1:cWSks5tfriHPdWFnl+qpX3P681aAYqlZHcAyHw5aU9Y= +go.opentelemetry.io/otel/sdk/metric v1.26.0/go.mod h1:ClMFFknnThJCksebJwz7KIyEDHO+nTB6gK8obLy8RyE= +go.opentelemetry.io/otel/trace v1.26.0 h1:1ieeAUb4y0TE26jUFrCIXKpTuVK7uJGN9/Z/2LP5sQA= +go.opentelemetry.io/otel/trace v1.26.0/go.mod h1:4iDxvGDQuUkHve82hJJ8UqrwswHYsZuWCBllGV2U2y0= +golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30= +golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M= +golang.org/x/exp v0.0.0-20240222234643-814bf88cf225 h1:LfspQV/FYTatPTr/3HzIcmiUFH7PGP+OQ6mgDYo3yuQ= +golang.org/x/exp v0.0.0-20240222234643-814bf88cf225/go.mod h1:CxmFvTBINI24O/j8iY7H1xHzx2i4OsyguNBmN/uPtqc= +golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA= +golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w= +golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8= +golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= +golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o= +golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= +google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6 h1:5D53IMaUuA5InSeMu9eJtlQXS2NxAhyWQvkKEgXZhHI= +modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6/go.mod h1:Qz0X07sNOR1jWYCrJMEnbW/X55x206Q7Vt4mz6/wHp4= +modernc.org/libc v1.49.3 h1:j2MRCRdwJI2ls/sGbeSk0t2bypOG/uvPZUsGQFDulqg= +modernc.org/libc v1.49.3/go.mod h1:yMZuGkn7pXbKfoT/M35gFJOAEdSKdxL0q64sF7KqCDo= +modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4= +modernc.org/mathutil v1.6.0/go.mod h1:Ui5Q9q1TR2gFm0AQRqQUaBWFLAhQpCwNcuhBOSedWPo= +modernc.org/memory v1.8.0 h1:IqGTL6eFMaDZZhEWwcREgeMXYwmW83LYW8cROZYkg+E= +modernc.org/memory v1.8.0/go.mod h1:XPZ936zp5OMKGWPqbD3JShgd/ZoQ7899TUuQqxY+peU= +modernc.org/sqlite v1.29.8 h1:nGKglNx9K5v0As+zF0/Gcl1kMkmaU1XynYyq92PbsC8= +modernc.org/sqlite v1.29.8/go.mod h1:lQPm27iqa4UNZpmr4Aor0MH0HkCLbt1huYDfWylLZFk= +modernc.org/strutil v1.2.0 h1:agBi9dp1I+eOnxXeiZawM8F4LawKv4NzGWSaLfyeNZA= +modernc.org/strutil v1.2.0/go.mod h1:/mdcBmfOibveCTBxUl5B5l6W+TTH1FXPLHZE6bTosX0= +modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= +modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= diff --git a/integration/testdata/go/wrapped/wrapped.go b/integration/testdata/go/wrapped/wrapped.go new file mode 100644 index 0000000000..c94e54790c --- /dev/null +++ b/integration/testdata/go/wrapped/wrapped.go @@ -0,0 +1,40 @@ +package wrapped + +import ( + "context" + "fmt" + "ftl/time" + + "github.com/TBD54566975/ftl/go-runtime/ftl" // Import the FTL SDK. +) + +// Wrapped module provides 2 verbs: Outer and Inner. +// Outer calls Inner and Inner calls time.Time. +// This module is useful to testing mocking verbs and setting up test config and secrets + +var mySecret = ftl.Secret[string]("secret") +var myConfig = ftl.Config[string]("config") + +type WrappedResponse struct { + Output string `json:"output"` + Secret string `json:"secret"` + Config string `json:"config"` +} + +//ftl:verb +func Outer(ctx context.Context) (WrappedResponse, error) { + return ftl.CallSource(ctx, Inner) +} + +//ftl:verb +func Inner(ctx context.Context) (WrappedResponse, error) { + resp, err := ftl.Call(ctx, time.Time, time.TimeRequest{}) + if err != nil { + return WrappedResponse{}, err + } + return WrappedResponse{ + Output: fmt.Sprintf("%v", resp.Time), + Config: myConfig.Get(ctx), + Secret: mySecret.Get(ctx), + }, nil +} diff --git a/integration/testdata/go/wrapped/wrapped_test.go b/integration/testdata/go/wrapped/wrapped_test.go new file mode 100644 index 0000000000..9df52687e7 --- /dev/null +++ b/integration/testdata/go/wrapped/wrapped_test.go @@ -0,0 +1,62 @@ +package wrapped + +import ( + "context" + "ftl/time" + "testing" + stdtime "time" + + "github.com/TBD54566975/ftl/go-runtime/ftl" + "github.com/TBD54566975/ftl/go-runtime/ftl/ftltest" + "github.com/alecthomas/assert/v2" +) + +func TestWrapped(t *testing.T) { + allOptions := []func(context.Context) error{ + ftltest.WithConfig(myConfig, "helloworld"), + ftltest.WithSecret(mySecret, "shhhhh"), + ftltest.WithCallsAllowedWithinModule(), + ftltest.WhenVerb(time.Time, func(ctx context.Context, req time.TimeRequest) (time.TimeResponse, error) { + return time.TimeResponse{Time: stdtime.Date(2024, 1, 1, 0, 0, 0, 0, stdtime.UTC)}, nil + }), + } + + for _, tt := range []struct { + name string + options []func(context.Context) error + expectedError ftl.Option[string] + }{ + { + name: "OnlyConfigAndSecret", + options: allOptions[:2], + expectedError: ftl.Some("wrapped.inner: no mock found: provide a mock with ftltest.WhenVerb(Inner, ...) or enable all calls within the module with ftltest.WithCallsAllowedWithinModule()"), + }, + { + name: "AllowCallsWithinModule", + options: allOptions[:3], + expectedError: ftl.Some("wrapped.inner: time.time: no mock found: provide a mock with ftltest.WhenVerb(time.Time, ...)"), + }, + { + name: "WithExternalVerbMock", + options: allOptions[:4], + }, + } { + t.Run(tt.name, func(t *testing.T) { + ctx := ftltest.Context( + tt.options..., + ) + myConfig.Get(ctx) + resp, err := Outer(ctx) + + if expected, ok := tt.expectedError.Get(); ok { + assert.EqualError(t, err, expected) + return + } else { + assert.NoError(t, err) + } + assert.Equal(t, "2024-01-01 00:00:00 +0000 UTC", resp.Output) + assert.Equal(t, "helloworld", resp.Config) + assert.Equal(t, "shhhhh", resp.Secret) + }) + } +}