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 1d4f9eab4e..97e0a66e54 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 b3d1baa882..22513f3610 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 62d9984c5c..5824acbbf5 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 6ae5a00e83..2fee9701c3 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 e5474d314e..4679ce16a0 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 002fb62f64..3af001258c 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 bc8dbdd0b9..879db011d1 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 57e355beb2..a82cdb84b1 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 015fa07e23..75b19e426e 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 2e79d26e31..f7b46df8dc 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 567eca4cc3..23091bbfd4 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 d8157ba364..35cfa1586e 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 06afc15ae5..1dff86c46d 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 d53a8cf0d8..5eb88500d6 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 155156a76a..e96d0ff0f9 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 e858feefc3..c91c9fdef2 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 9c7f07b566..5f2bca228b 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 ebfe57d400..7875fd7429 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 5a9281ec55..b9a192ecc2 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 4b0c35d50e..d56462e10c 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 d64f14854a..0680b4989f 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"),