From f54cb415fcd89e6d2087e28ff07a0492cff75274 Mon Sep 17 00:00:00 2001 From: Alec Thomas Date: Wed, 14 Aug 2024 18:50:44 +1000 Subject: [PATCH] refactor: extend the integration testing framework to support multiple languages Previously we had full test coverage in each language, so we would just run them all, but as we're building up support for other languages for now, this change allows multiple languages to be supported per-test. --- backend/controller/admin/local_client_test.go | 13 +- .../console/console_integration_test.go | 5 +- .../cronjobs/cronjobs_integration_test.go | 2 +- .../controller/dal/fsm_integration_test.go | 9 +- .../ingress/ingress_integration_test.go | 4 +- .../leases/lease_integration_test.go | 2 +- backend/controller/pubsub/integration_test.go | 8 +- .../sql/database_integration_test.go | 6 +- cmd/ftl/integration_test.go | 30 +-- common/projectconfig/integration_test.go | 13 +- .../compile/compile_integration_test.go | 8 +- .../encoding/encoding_integration_test.go | 5 +- go-runtime/ftl/ftl_integration_test.go | 11 +- .../ftl/ftltest/ftltest_integration_test.go | 4 +- go-runtime/ftl/integration_test.go | 2 +- .../reflection/reflection_integration_test.go | 2 +- go-runtime/internal/integration_test.go | 2 +- integration/actions.go | 11 +- integration/harness.go | 200 +++++++++++------- internal/encryption/integration_test.go | 13 +- java-runtime/java_integration_test.go | 3 +- 21 files changed, 221 insertions(+), 132 deletions(-) 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"),