diff --git a/backend/controller/admin/local_client_test.go b/backend/controller/admin/local_client_test.go index 1d4f9eab4..97e0a66e5 100644 --- a/backend/controller/admin/local_client_test.go +++ b/backend/controller/admin/local_client_test.go @@ -6,15 +6,18 @@ import ( "context" "testing" + "github.com/alecthomas/assert/v2" + "github.com/alecthomas/types/optional" + cf "github.com/TBD54566975/ftl/common/configuration" in "github.com/TBD54566975/ftl/integration" "github.com/TBD54566975/ftl/internal/log" - "github.com/alecthomas/assert/v2" - "github.com/alecthomas/types/optional" ) func TestDiskSchemaRetrieverWithBuildArtefact(t *testing.T) { - in.RunWithoutController(t, "ftl-project-dr.toml", + in.Run(t, + in.WithFTLConfig("ftl-project-dr.toml"), + in.WithoutController(), in.CopyModule("dischema"), in.Build("dischema"), func(t testing.TB, ic in.TestContext) { @@ -30,7 +33,9 @@ func TestDiskSchemaRetrieverWithBuildArtefact(t *testing.T) { } func TestDiskSchemaRetrieverWithNoSchema(t *testing.T) { - in.RunWithoutController(t, "ftl-project-dr.toml", + in.Run(t, + in.WithFTLConfig("ftl-project-dr.toml"), + in.WithoutController(), in.CopyModule("dischema"), func(t testing.TB, ic in.TestContext) { dsr := &diskSchemaRetriever{} diff --git a/backend/controller/console/console_integration_test.go b/backend/controller/console/console_integration_test.go index b3d1baa88..22513f361 100644 --- a/backend/controller/console/console_integration_test.go +++ b/backend/controller/console/console_integration_test.go @@ -6,9 +6,10 @@ import ( "testing" "connectrpc.com/connect" + "github.com/alecthomas/assert/v2" + pbconsole "github.com/TBD54566975/ftl/backend/protos/xyz/block/ftl/v1/console" in "github.com/TBD54566975/ftl/integration" - "github.com/alecthomas/assert/v2" ) // GetModules calls console service GetModules and returns the response. @@ -24,7 +25,7 @@ func GetModules(onResponse func(t testing.TB, resp *connect.Response[pbconsole.G } func TestConsoleGetModules(t *testing.T) { - in.Run(t, "", + in.Run(t, in.CopyModule("console"), in.Deploy("console"), GetModules(func(t testing.TB, resp *connect.Response[pbconsole.GetModulesResponse]) { diff --git a/backend/controller/cronjobs/cronjobs_integration_test.go b/backend/controller/cronjobs/cronjobs_integration_test.go index 62d9984c5..5824acbbf 100644 --- a/backend/controller/cronjobs/cronjobs_integration_test.go +++ b/backend/controller/cronjobs/cronjobs_integration_test.go @@ -53,7 +53,7 @@ func TestCron(t *testing.T) { t.Cleanup(func() { _ = os.Remove(tmpFile) }) - in.Run(t, "", + in.Run(t, in.CopyModule("cron"), in.Deploy("cron"), func(t testing.TB, ic in.TestContext) { diff --git a/backend/controller/dal/fsm_integration_test.go b/backend/controller/dal/fsm_integration_test.go index 6ae5a00e8..2fee9701c 100644 --- a/backend/controller/dal/fsm_integration_test.go +++ b/backend/controller/dal/fsm_integration_test.go @@ -8,8 +8,9 @@ import ( "testing" "time" - in "github.com/TBD54566975/ftl/integration" "github.com/alecthomas/assert/v2" + + in "github.com/TBD54566975/ftl/integration" ) func TestFSM(t *testing.T) { @@ -22,7 +23,7 @@ func TestFSM(t *testing.T) { WHERE fsm = 'fsm.fsm' AND key = '%s' `, instance), status, state) } - in.Run(t, "", + in.Run(t, in.CopyModule("fsm"), in.Deploy("fsm"), @@ -81,7 +82,7 @@ func TestFSMRetry(t *testing.T) { } } - in.Run(t, "", + in.Run(t, in.CopyModule("fsmretry"), in.Build("fsmretry"), in.Deploy("fsmretry"), @@ -127,7 +128,7 @@ func TestFSMRetry(t *testing.T) { func TestFSMGoTests(t *testing.T) { logFilePath := filepath.Join(t.TempDir(), "fsm.log") t.Setenv("FSM_LOG_FILE", logFilePath) - in.Run(t, "", + in.Run(t, in.CopyModule("fsm"), in.Build("fsm"), in.ExecModuleTest("fsm"), diff --git a/backend/controller/ingress/ingress_integration_test.go b/backend/controller/ingress/ingress_integration_test.go index e5474d314..4679ce16a 100644 --- a/backend/controller/ingress/ingress_integration_test.go +++ b/backend/controller/ingress/ingress_integration_test.go @@ -14,7 +14,7 @@ import ( ) func TestHttpIngress(t *testing.T) { - in.Run(t, "", + in.Run(t, in.CopyModule("httpingress"), in.Deploy("httpingress"), in.HttpCall(http.MethodGet, "/users/123/posts/456", nil, in.JsonData(t, in.Obj{}), func(t testing.TB, resp *in.HTTPResponse) { @@ -152,7 +152,7 @@ func TestHttpIngress(t *testing.T) { func TestHttpIngressWithCors(t *testing.T) { os.Setenv("FTL_CONTROLLER_ALLOW_ORIGIN", "http://localhost:8892") os.Setenv("FTL_CONTROLLER_ALLOW_HEADERS", "x-forwarded-capabilities") - in.Run(t, "", + in.Run(t, in.CopyModule("httpingress"), in.Deploy("httpingress"), // A correct CORS preflight request diff --git a/backend/controller/leases/lease_integration_test.go b/backend/controller/leases/lease_integration_test.go index 002fb62f6..3af001258 100644 --- a/backend/controller/leases/lease_integration_test.go +++ b/backend/controller/leases/lease_integration_test.go @@ -18,7 +18,7 @@ import ( ) func TestLease(t *testing.T) { - in.Run(t, "", + in.Run(t, in.CopyModule("leases"), in.Build("leases"), // checks if leases work in a unit test environment diff --git a/backend/controller/pubsub/integration_test.go b/backend/controller/pubsub/integration_test.go index bc8dbdd0b..879db011d 100644 --- a/backend/controller/pubsub/integration_test.go +++ b/backend/controller/pubsub/integration_test.go @@ -15,7 +15,7 @@ import ( func TestPubSub(t *testing.T) { calls := 20 events := calls * 10 - in.Run(t, "", + in.Run(t, in.CopyModule("publisher"), in.CopyModule("subscriber"), in.Deploy("publisher"), @@ -40,7 +40,7 @@ func TestPubSub(t *testing.T) { } func TestConsumptionDelay(t *testing.T) { - in.Run(t, "", + in.Run(t, in.CopyModule("publisher"), in.CopyModule("subscriber"), in.Deploy("publisher"), @@ -83,7 +83,7 @@ func TestConsumptionDelay(t *testing.T) { func TestRetry(t *testing.T) { retriesPerCall := 2 - in.Run(t, "", + in.Run(t, in.CopyModule("publisher"), in.CopyModule("subscriber"), in.Deploy("publisher"), @@ -135,7 +135,7 @@ func TestRetry(t *testing.T) { } func TestExternalPublishRuntimeCheck(t *testing.T) { - in.Run(t, "", + in.Run(t, in.CopyModule("publisher"), in.CopyModule("subscriber"), in.Deploy("publisher"), diff --git a/backend/controller/sql/database_integration_test.go b/backend/controller/sql/database_integration_test.go index 57e355beb..a82cdb84b 100644 --- a/backend/controller/sql/database_integration_test.go +++ b/backend/controller/sql/database_integration_test.go @@ -10,7 +10,8 @@ import ( ) func TestDatabase(t *testing.T) { - in.Run(t, "database/ftl-project.toml", + in.Run(t, + in.WithFTLConfig("database/ftl-project.toml"), // deploy real module against "testdb" in.CopyModule("database"), in.CreateDBAction("database", "testdb", false), @@ -33,7 +34,8 @@ func TestMigrate(t *testing.T) { return in.QueryRow(dbName, "SELECT version FROM schema_migrations WHERE version = '20240704103403'", "20240704103403") } - in.RunWithoutController(t, "", + in.Run(t, + in.WithoutController(), in.DropDBAction(t, dbName), in.Fail(q(), "Should fail because the database does not exist."), in.Exec("ftl", "migrate", "--dsn", dbUri), diff --git a/cmd/ftl/integration_test.go b/cmd/ftl/integration_test.go index 015fa07e2..75b19e426 100644 --- a/cmd/ftl/integration_test.go +++ b/cmd/ftl/integration_test.go @@ -24,7 +24,8 @@ func TestBox(t *testing.T) { ctx := log.ContextWithNewDefaultLogger(context.Background()) err := exec.Command(ctx, log.Debug, "../..", "docker", "build", "-t", "ftl0/ftl-box:latest", "--progress=plain", "--platform=linux/amd64", "-f", "Dockerfile.box", ".").Run() assert.NoError(t, err) - RunWithoutController(t, "", + Run(t, + WithoutController(), CopyModule("time"), CopyModule("echo"), Exec("ftl", "box", "echo", "--compose=echo-compose.yml"), @@ -35,17 +36,17 @@ func TestBox(t *testing.T) { } func TestConfigsWithController(t *testing.T) { - Run(t, "", configActions(t)...) + Run(t, configActions(t)...) } func TestConfigsWithoutController(t *testing.T) { - RunWithoutController(t, "", configActions(t)...) + Run(t, configActions(t, WithoutController())...) } -func configActions(t *testing.T) []Action { +func configActions(t *testing.T, prepend ...ActionOrOption) []ActionOrOption { t.Helper() - return []Action{ + return append(prepend, // test setting value without --json flag Exec("ftl", "config", "set", "test.one", "hello world", "--inline"), ExecWithExpectedOutput("\"hello world\"\n", "ftl", "config", "get", "test.one"), @@ -58,18 +59,18 @@ func configActions(t *testing.T) []Action { ExecWithOutput("ftl", []string{"config", "get", "test.one"}, func(output string) {}), "failed to get from config manager: not found", ), - } + ) } func TestSecretsWithController(t *testing.T) { - Run(t, "", secretActions(t)...) + Run(t, secretActions(t)...) } func TestSecretsWithoutController(t *testing.T) { - RunWithoutController(t, "", secretActions(t)...) + Run(t, secretActions(t, WithoutController())...) } -func secretActions(t *testing.T) []Action { +func secretActions(t *testing.T, prepend ...ActionOrOption) []ActionOrOption { t.Helper() // can not easily use Exec() to enter secure text, using secret import instead @@ -78,7 +79,7 @@ func secretActions(t *testing.T) []Action { secretsPath2, err := filepath.Abs("testdata/secrets2.json") assert.NoError(t, err) - return []Action{ + return append(prepend, // test setting secret without --json flag Exec("ftl", "secret", "import", "--inline", secretsPath1), ExecWithExpectedOutput("\"hello world\"\n", "ftl", "secret", "get", "test.one"), @@ -91,7 +92,7 @@ func secretActions(t *testing.T) []Action { ExecWithOutput("ftl", []string{"secret", "get", "test.one"}, func(output string) {}), "failed to get from secret manager: not found", ), - } + ) } func TestSecretImportExport(t *testing.T) { @@ -116,7 +117,8 @@ func testImportExport(t *testing.T, object string) { blank := "" exported := &blank - RunWithoutController(t, "", + Run(t, + WithoutController(), // duplicate project file in the temp directory Exec("cp", firstProjFile, secondProjFile), // import into first project file @@ -152,7 +154,7 @@ func NewFunction(ctx context.Context, req TimeRequest) (TimeResponse, error) { return TimeResponse{Time: time.Now()}, nil } ` - Run(t, "", + Run(t, CopyModule("time"), Deploy("time"), ExecWithOutput("ftl", []string{"schema", "diff"}, func(output string) { @@ -199,7 +201,7 @@ func TestResetSubscription(t *testing.T) { `, module, subscription), cursor) } - Run(t, "", + Run(t, CopyModule("time"), CopyModule("echo"), Deploy("time"), diff --git a/common/projectconfig/integration_test.go b/common/projectconfig/integration_test.go index 2e79d26e3..f7b46df8d 100644 --- a/common/projectconfig/integration_test.go +++ b/common/projectconfig/integration_test.go @@ -14,7 +14,8 @@ import ( ) func TestDefaultToRootWhenModuleDirsMissing(t *testing.T) { - in.Run(t, "no-module-dirs-ftl-project.toml", + in.Run(t, + in.WithFTLConfig("no-module-dirs-ftl-project.toml"), in.CopyModule("echo"), in.Exec("ftl", "build"), // Needs to be `ftl build`, not `ftl build echo` in.Deploy("echo"), @@ -25,7 +26,9 @@ func TestDefaultToRootWhenModuleDirsMissing(t *testing.T) { } func TestConfigCmdWithoutController(t *testing.T) { - in.RunWithoutController(t, "configs-ftl-project.toml", + in.Run(t, + in.WithFTLConfig("configs-ftl-project.toml"), + in.WithoutController(), in.ExecWithExpectedOutput("\"value\"\n", "ftl", "config", "get", "key"), ) } @@ -49,7 +52,8 @@ func TestFindConfig(t *testing.T) { assert.Equal(t, "test = \"test\"\n", string(output)) } } - in.RunWithoutController(t, "", + in.Run(t, + in.WithoutController(), in.CopyModule("findconfig"), checkConfig("findconfig"), checkConfig("findconfig/subdir"), @@ -61,7 +65,8 @@ func TestFindConfig(t *testing.T) { } func TestConfigValidation(t *testing.T) { - in.Run(t, "./validateconfig/ftl-project.toml", + in.Run(t, + in.WithFTLConfig("./validateconfig/ftl-project.toml"), in.CopyModule("validateconfig"), // Global sets never error. diff --git a/go-runtime/compile/compile_integration_test.go b/go-runtime/compile/compile_integration_test.go index 567eca4cc..23091bbfd 100644 --- a/go-runtime/compile/compile_integration_test.go +++ b/go-runtime/compile/compile_integration_test.go @@ -12,7 +12,7 @@ import ( ) func TestNonExportedDecls(t *testing.T) { - in.Run(t, "", + in.Run(t, in.CopyModule("time"), in.Deploy("time"), in.CopyModule("echo"), @@ -26,7 +26,7 @@ func TestNonExportedDecls(t *testing.T) { } func TestUndefinedExportedDecls(t *testing.T) { - in.Run(t, "", + in.Run(t, in.CopyModule("time"), in.Deploy("time"), in.CopyModule("echo"), @@ -40,7 +40,7 @@ func TestUndefinedExportedDecls(t *testing.T) { } func TestNonFTLTypes(t *testing.T) { - in.Run(t, "", + in.Run(t, in.CopyModule("external"), in.Deploy("external"), in.Call("external", "echo", in.Obj{"message": "hello"}, func(t testing.TB, response in.Obj) { @@ -50,7 +50,7 @@ func TestNonFTLTypes(t *testing.T) { } func TestNonStructRequestResponse(t *testing.T) { - in.Run(t, "", + in.Run(t, in.CopyModule("two"), in.Deploy("two"), in.CopyModule("one"), diff --git a/go-runtime/encoding/encoding_integration_test.go b/go-runtime/encoding/encoding_integration_test.go index d8157ba36..35cfa1586 100644 --- a/go-runtime/encoding/encoding_integration_test.go +++ b/go-runtime/encoding/encoding_integration_test.go @@ -6,12 +6,13 @@ import ( "net/http" "testing" - in "github.com/TBD54566975/ftl/integration" "github.com/alecthomas/assert/v2" + + in "github.com/TBD54566975/ftl/integration" ) func TestHttpEncodeOmitempty(t *testing.T) { - in.Run(t, "", + in.Run(t, in.CopyModule("omitempty"), in.Deploy("omitempty"), in.HttpCall(http.MethodGet, "/get", nil, in.JsonData(t, in.Obj{}), func(t testing.TB, resp *in.HTTPResponse) { diff --git a/go-runtime/ftl/ftl_integration_test.go b/go-runtime/ftl/ftl_integration_test.go index 06afc15ae..1dff86c46 100644 --- a/go-runtime/ftl/ftl_integration_test.go +++ b/go-runtime/ftl/ftl_integration_test.go @@ -6,14 +6,15 @@ import ( "strings" "testing" - in "github.com/TBD54566975/ftl/integration" "github.com/alecthomas/assert/v2" + in "github.com/TBD54566975/ftl/integration" + "github.com/alecthomas/repr" ) func TestLifecycle(t *testing.T) { - in.Run(t, "", + in.Run(t, in.GitInit(), in.Exec("rm", "ftl-project.toml"), in.Exec("ftl", "init", "test", "."), @@ -26,7 +27,7 @@ func TestLifecycle(t *testing.T) { } func TestInterModuleCall(t *testing.T) { - in.Run(t, "", + in.Run(t, in.CopyModule("echo"), in.CopyModule("time"), in.Deploy("time"), @@ -42,7 +43,7 @@ func TestInterModuleCall(t *testing.T) { } func TestSchemaGenerate(t *testing.T) { - in.Run(t, "", + in.Run(t, in.CopyDir("../schema-generate", "schema-generate"), in.Mkdir("build/schema-generate"), in.Exec("ftl", "schema", "generate", "schema-generate", "build/schema-generate"), @@ -51,7 +52,7 @@ func TestSchemaGenerate(t *testing.T) { } func TestTypeRegistryUnitTest(t *testing.T) { - in.Run(t, "", + in.Run(t, in.CopyModule("typeregistry"), in.Deploy("typeregistry"), in.ExecModuleTest("typeregistry"), diff --git a/go-runtime/ftl/ftltest/ftltest_integration_test.go b/go-runtime/ftl/ftltest/ftltest_integration_test.go index d53a8cf0d..5eb88500d 100644 --- a/go-runtime/ftl/ftltest/ftltest_integration_test.go +++ b/go-runtime/ftl/ftltest/ftltest_integration_test.go @@ -9,7 +9,9 @@ import ( ) func TestModuleUnitTests(t *testing.T) { - in.RunWithoutController(t, "wrapped/ftl-project.toml", + in.Run(t, + in.WithFTLConfig("wrapped/ftl-project.toml"), + in.WithoutController(), in.GitInit(), in.CopyModule("time"), in.CopyModule("wrapped"), diff --git a/go-runtime/ftl/integration_test.go b/go-runtime/ftl/integration_test.go index 155156a76..e96d0ff0f 100644 --- a/go-runtime/ftl/integration_test.go +++ b/go-runtime/ftl/integration_test.go @@ -9,7 +9,7 @@ import ( ) func TestFTLMap(t *testing.T) { - in.Run(t, "", + in.Run(t, in.CopyModule("mapper"), in.Build("mapper"), in.ExecModuleTest("mapper"), diff --git a/go-runtime/ftl/reflection/reflection_integration_test.go b/go-runtime/ftl/reflection/reflection_integration_test.go index e858feefc..c91c9fdef 100644 --- a/go-runtime/ftl/reflection/reflection_integration_test.go +++ b/go-runtime/ftl/reflection/reflection_integration_test.go @@ -9,7 +9,7 @@ import ( ) func TestRuntimeReflection(t *testing.T) { - in.Run(t, "", + in.Run(t, in.CopyModule("runtimereflection"), in.ExecModuleTest("runtimereflection"), ) diff --git a/go-runtime/internal/integration_test.go b/go-runtime/internal/integration_test.go index 9c7f07b56..5f2bca228 100644 --- a/go-runtime/internal/integration_test.go +++ b/go-runtime/internal/integration_test.go @@ -11,7 +11,7 @@ import ( ) func TestRealMap(t *testing.T) { - Run(t, "", + Run(t, CopyModule("mapper"), Deploy("mapper"), Call("mapper", "get", Obj{}, func(t testing.TB, response Obj) { diff --git a/integration/actions.go b/integration/actions.go index ebfe57d40..7875fd742 100644 --- a/integration/actions.go +++ b/integration/actions.go @@ -51,13 +51,18 @@ func GitInit() Action { // Copy a module from the testdata directory to the working directory. // -// Ensures that replace directives are correctly handled. +// Ensures that any language-specific local modifications are made correctly, +// such as Go module file replace directives for FTL. func CopyModule(module string) Action { return Chain( CopyDir(module, module), func(t testing.TB, ic TestContext) { - err := ftlexec.Command(ic, log.Debug, filepath.Join(ic.workDir, module), "go", "mod", "edit", "-replace", "github.com/TBD54566975/ftl="+ic.RootDir).RunBuffered(ic) - assert.NoError(t, err) + root := filepath.Join(ic.workDir, module) + // TODO: Load the module configuration from the module itself and use that to determine the language-specific stuff. + if _, err := os.Stat(filepath.Join(root, "go.mod")); err == nil { + err := ftlexec.Command(ic, log.Debug, root, "go", "mod", "edit", "-replace", "github.com/TBD54566975/ftl="+ic.RootDir).RunBuffered(ic) + assert.NoError(t, err) + } }, ) } diff --git a/integration/harness.go b/integration/harness.go index 5a9281ec5..b9a192ecc 100644 --- a/integration/harness.go +++ b/integration/harness.go @@ -43,47 +43,98 @@ func Infof(format string, args ...any) { var buildOnce sync.Once -// Run an integration test. -// ftlConfigPath: if FTL_CONFIG should be set for this test, then pass in the relative +// An Option for configuring the integration test harness. +type Option func(*options) + +// ActionOrOption is a type that can be either an Action or an Option. +type ActionOrOption any + +// WithLanguages is a Run* option that specifies the languages to test. // -// path based on ./testdata/go/ where "." denotes the directory containing the -// integration test (e.g. for "integration/harness_test.go" supplying -// "database/ftl-project.toml" would set FTL_CONFIG to -// "integration/testdata/go/database/ftl-project.toml"). -func Run(t *testing.T, ftlConfigPath string, actions ...Action) { - run(t, ftlConfigPath, true, false, actions...) +// Defaults to "go" if not provided. +func WithLanguages(languages ...string) Option { + return func(o *options) { + o.languages = languages + } } -// RunWithJava runs an integration test after building the Java runtime. -// ftlConfigPath: if FTL_CONFIG should be set for this test, then pass in the relative +// WithFTLConfig is a Run* option that specifies the FTL config to use. // -// path based on ./testdata/go/ where "." denotes the directory containing the -// integration test (e.g. for "integration/harness_test.go" supplying -// "database/ftl-project.toml" would set FTL_CONFIG to -// "integration/testdata/go/database/ftl-project.toml"). -func RunWithJava(t *testing.T, ftlConfigPath string, actions ...Action) { - run(t, ftlConfigPath, true, true, actions...) +// This will set FTL_CONFIG for this test, then pass in the relative +// path based on ./testdata/go/ where "." denotes the directory containing the +// integration test (e.g. for "integration/harness_test.go" supplying +// "database/ftl-project.toml" would set FTL_CONFIG to +// "integration/testdata/go/database/ftl-project.toml"). +func WithFTLConfig(path string) Option { + return func(o *options) { + o.ftlConfigPath = path + } } -// RunWithoutController runs an integration test without starting the controller. -// ftlConfigPath: if FTL_CONFIG should be set for this test, then pass in the relative -// -// path based on ./testdata/go/ where "." denotes the directory containing the -// integration test (e.g. for "integration/harness_test.go" supplying -// "database/ftl-project.toml" would set FTL_CONFIG to -// "integration/testdata/go/database/ftl-project.toml"). -func RunWithoutController(t *testing.T, ftlConfigPath string, actions ...Action) { - run(t, ftlConfigPath, false, false, actions...) +// WithEnvar is a Run* option that specifies an environment variable to set. +func WithEnvar(key, value string) Option { + return func(o *options) { + o.envars[key] = value + } } -func RunWithEncryption(t *testing.T, ftlConfigPath string, actions ...Action) { - uri := "fake-kms://CKbvh_ILElQKSAowdHlwZS5nb29nbGVhcGlzLmNvbS9nb29nbGUuY3J5cHRvLnRpbmsuQWVzR2NtS2V5EhIaEE6tD2yE5AWYOirhmkY-r3sYARABGKbvh_ILIAE" - t.Setenv("FTL_KMS_URI", uri) +// WithJava is a Run* option that ensures the Java runtime is built. +func WithJava() Option { + return func(o *options) { + o.requireJava = true + } +} - run(t, ftlConfigPath, true, false, actions...) +// WithoutController is a Run* option that disables starting the controller. +func WithoutController() Option { + return func(o *options) { + o.startController = false + } } -func run(t *testing.T, ftlConfigPath string, startController bool, requireJava bool, actions ...Action) { +type options struct { + languages []string + ftlConfigPath string + startController bool + requireJava bool + envars map[string]string +} + +// Run an integration test. +func Run(t *testing.T, actionsOrOptions ...ActionOrOption) { + run(t, actionsOrOptions...) +} + +func run(t *testing.T, actionsOrOptions ...ActionOrOption) { + opts := options{ + startController: true, + languages: []string{"go"}, + envars: map[string]string{}, + } + actions := []Action{} + for _, opt := range actionsOrOptions { + switch o := opt.(type) { + case Action: + actions = append(actions, o) + + case func(t testing.TB, ic TestContext): + actions = append(actions, Action(o)) + + case Option: + o(&opts) + + case func(*options): + o(&opts) + + default: + panic(fmt.Sprintf("expected Option or Action, not %T", opt)) + } + } + + for key, value := range opts.envars { + t.Setenv(key, value) + } + tmpDir := t.TempDir() cwd, err := os.Getwd() @@ -92,12 +143,13 @@ func run(t *testing.T, ftlConfigPath string, startController bool, requireJava b rootDir, ok := internal.GitRoot("").Get() assert.True(t, ok) - if ftlConfigPath != "" { - ftlConfigPath = filepath.Join(cwd, "testdata", "go", ftlConfigPath) + if opts.ftlConfigPath != "" { + // TODO: We shouldn't be copying the shared config from the "go" testdata... + opts.ftlConfigPath = filepath.Join(cwd, "testdata", "go", opts.ftlConfigPath) projectPath := filepath.Join(tmpDir, "ftl-project.toml") // Copy the specified FTL config to the temporary directory. - err = copy.Copy(ftlConfigPath, projectPath) + err = copy.Copy(opts.ftlConfigPath, projectPath) if err == nil { t.Setenv("FTL_CONFIG", projectPath) } else { @@ -106,8 +158,8 @@ func run(t *testing.T, ftlConfigPath string, startController bool, requireJava b // can't be loaded until the module is copied over, and the config itself // is used by FTL during startup. // Some tests still rely on this behavior, so we can't remove it entirely. - t.Logf("Failed to copy %s to %s: %s", ftlConfigPath, projectPath, err) - t.Setenv("FTL_CONFIG", ftlConfigPath) + t.Logf("Failed to copy %s to %s: %s", opts.ftlConfigPath, projectPath, err) + t.Setenv("FTL_CONFIG", opts.ftlConfigPath) } } else { @@ -124,49 +176,53 @@ func run(t *testing.T, ftlConfigPath string, startController bool, requireJava b Infof("Building ftl") err = ftlexec.Command(ctx, log.Debug, rootDir, "just", "build", "ftl").RunBuffered(ctx) assert.NoError(t, err) - if requireJava { + if opts.requireJava { err = ftlexec.Command(ctx, log.Debug, rootDir, "just", "build-java").RunBuffered(ctx) assert.NoError(t, err) } }) - verbs := rpc.Dial(ftlv1connect.NewVerbServiceClient, "http://localhost:8892", log.Debug) - - var controller ftlv1connect.ControllerServiceClient - var console pbconsoleconnect.ConsoleServiceClient - if startController { - controller = rpc.Dial(ftlv1connect.NewControllerServiceClient, "http://localhost:8892", log.Debug) - console = rpc.Dial(pbconsoleconnect.NewConsoleServiceClient, "http://localhost:8892", log.Debug) - - Infof("Starting ftl cluster") - ctx = startProcess(ctx, t, filepath.Join(binDir, "ftl"), "serve", "--recreate") - } - - ic := TestContext{ - Context: ctx, - RootDir: rootDir, - testData: filepath.Join(cwd, "testdata", "go"), - workDir: tmpDir, - binDir: binDir, - Verbs: verbs, - } - - if startController { - ic.Controller = controller - ic.Console = console - - Infof("Waiting for controller to be ready") - ic.AssertWithRetry(t, func(t testing.TB, ic TestContext) { - _, err := ic.Controller.Status(ic, connect.NewRequest(&ftlv1.StatusRequest{})) - assert.NoError(t, err) + for _, language := range opts.languages { + t.Run(language, func(t *testing.T) { + verbs := rpc.Dial(ftlv1connect.NewVerbServiceClient, "http://localhost:8892", log.Debug) + + var controller ftlv1connect.ControllerServiceClient + var console pbconsoleconnect.ConsoleServiceClient + if opts.startController { + controller = rpc.Dial(ftlv1connect.NewControllerServiceClient, "http://localhost:8892", log.Debug) + console = rpc.Dial(pbconsoleconnect.NewConsoleServiceClient, "http://localhost:8892", log.Debug) + + Infof("Starting ftl cluster") + ctx = startProcess(ctx, t, filepath.Join(binDir, "ftl"), "serve", "--recreate") + } + + ic := TestContext{ + Context: ctx, + RootDir: rootDir, + testData: filepath.Join(cwd, "testdata", language), + workDir: tmpDir, + binDir: binDir, + Verbs: verbs, + } + + if opts.startController { + ic.Controller = controller + ic.Console = console + + Infof("Waiting for controller to be ready") + ic.AssertWithRetry(t, func(t testing.TB, ic TestContext) { + _, err := ic.Controller.Status(ic, connect.NewRequest(&ftlv1.StatusRequest{})) + assert.NoError(t, err) + }) + } + + Infof("Starting test") + + for _, action := range actions { + ic.AssertWithRetry(t, action) + } }) } - - Infof("Starting test") - - for _, action := range actions { - ic.AssertWithRetry(t, action) - } } type TestContext struct { @@ -249,7 +305,7 @@ func (l *logWriter) Write(p []byte) (n int, err error) { } } -// startProcess runs a binary in the background. +// startProcess runs a binary in the background and terminates it when the test completes. func startProcess(ctx context.Context, t testing.TB, args ...string) context.Context { t.Helper() ctx, cancel := context.WithCancel(ctx) diff --git a/internal/encryption/integration_test.go b/internal/encryption/integration_test.go index 4b0c35d50..d56462e10 100644 --- a/internal/encryption/integration_test.go +++ b/internal/encryption/integration_test.go @@ -24,8 +24,13 @@ import ( awsv1kms "github.com/aws/aws-sdk-go/service/kms" ) +func WithEncryption() in.Option { + return in.WithEnvar("FTL_KMS_URI", "fake-kms://CKbvh_ILElQKSAowdHlwZS5nb29nbGVhcGlzLmNvbS9nb29nbGUuY3J5cHRvLnRpbmsuQWVzR2NtS2V5EhIaEE6tD2yE5AWYOirhmkY-r3sYARABGKbvh_ILIAE") +} + func TestEncryptionForLogs(t *testing.T) { - in.RunWithEncryption(t, "", + in.Run(t, + WithEncryption(), in.CopyModule("encryption"), in.Deploy("encryption"), in.Call[map[string]interface{}, any]("encryption", "echo", map[string]interface{}{"name": "Alice"}, nil), @@ -61,7 +66,8 @@ func TestEncryptionForLogs(t *testing.T) { } func TestEncryptionForPubSub(t *testing.T) { - in.RunWithEncryption(t, "", + in.Run(t, + WithEncryption(), in.CopyModule("encryption"), in.Deploy("encryption"), in.Call[map[string]interface{}, any]("encryption", "publish", map[string]interface{}{"name": "AliceInWonderland"}, nil), @@ -81,7 +87,8 @@ func TestEncryptionForPubSub(t *testing.T) { } func TestEncryptionForFSM(t *testing.T) { - in.RunWithEncryption(t, "", + in.Run(t, + WithEncryption(), in.CopyModule("encryption"), in.Deploy("encryption"), in.Call[map[string]interface{}, any]("encryption", "beginFsm", map[string]interface{}{"name": "Rosebud"}, nil), diff --git a/java-runtime/java_integration_test.go b/java-runtime/java_integration_test.go index d64f14854..0680b4989 100644 --- a/java-runtime/java_integration_test.go +++ b/java-runtime/java_integration_test.go @@ -14,7 +14,8 @@ import ( ) func TestJavaToGoCall(t *testing.T) { - in.RunWithJava(t, "", + in.Run(t, + in.WithJava(), in.CopyModule("gomodule"), in.CopyDir("javamodule", "javamodule"), in.Deploy("gomodule"),