From 0381efa9b24709594fdb92ac1cd1ec596f5a0837 Mon Sep 17 00:00:00 2001 From: Jon Johnson <113393155+jonathanj-square@users.noreply.github.com> Date: Thu, 30 May 2024 15:40:30 -0700 Subject: [PATCH] fix: allow integration tests to run from anywhere (#1593) fixes #1384 - exposes DSL steps as public for use in other packages - modify the `integration test` just command to search for dirs containing the integration test tag --- Justfile | 4 +- .../ingress/ingress_integration_test.go | 116 +++++++ .../ingress}/testdata/go/httpingress/ftl.toml | 0 .../ingress}/testdata/go/httpingress/go.mod | 0 .../ingress}/testdata/go/httpingress/go.sum | 0 .../testdata/go/httpingress/httpingress.go | 0 integration/{actions_test.go => actions.go} | 196 +++++------ integration/{harness_test.go => harness.go} | 43 +-- integration/integration_test.go | 326 ++++++------------ integration/{wrapper_test.go => wrapper.go} | 2 +- 10 files changed, 354 insertions(+), 333 deletions(-) create mode 100644 backend/controller/ingress/ingress_integration_test.go rename {integration => backend/controller/ingress}/testdata/go/httpingress/ftl.toml (100%) rename {integration => backend/controller/ingress}/testdata/go/httpingress/go.mod (100%) rename {integration => backend/controller/ingress}/testdata/go/httpingress/go.sum (100%) rename {integration => backend/controller/ingress}/testdata/go/httpingress/httpingress.go (100%) rename integration/{actions_test.go => actions.go} (61%) rename integration/{harness_test.go => harness.go} (80%) rename integration/{wrapper_test.go => wrapper.go} (97%) diff --git a/Justfile b/Justfile index a0ea4692c0..25a739b602 100644 --- a/Justfile +++ b/Justfile @@ -114,7 +114,7 @@ integration-tests *test: #!/bin/bash set -euo pipefail testName=${1:-} - go test -fullpath -count 1 -v -tags integration -run "$testName" ./integration ./backend/controller/cronjobs + go test -fullpath -count 1 -v -tags integration -run "$testName" ./... # Run README doc tests test-readme *args: @@ -138,4 +138,4 @@ lint-frontend: build-frontend # Lint the backend lint-backend: - @golangci-lint run ./... \ No newline at end of file + @golangci-lint run ./... diff --git a/backend/controller/ingress/ingress_integration_test.go b/backend/controller/ingress/ingress_integration_test.go new file mode 100644 index 0000000000..66ce94b6f9 --- /dev/null +++ b/backend/controller/ingress/ingress_integration_test.go @@ -0,0 +1,116 @@ +//go:build integration + +package ingress_test + +import ( + "net/http" + "testing" + + "github.com/alecthomas/assert/v2" + "github.com/alecthomas/repr" + + in "github.com/TBD54566975/ftl/integration" +) + +func TestHttpIngress(t *testing.T) { + in.Run(t, "", + in.CopyModule("httpingress"), + in.Deploy("httpingress"), + in.HttpCall(http.MethodGet, "/users/123/posts/456", in.JsonData(t, in.Obj{}), func(t testing.TB, resp *in.HTTPResponse) { + assert.Equal(t, 200, resp.Status) + assert.Equal(t, []string{"Header from FTL"}, resp.Headers["Get"]) + assert.Equal(t, []string{"application/json; charset=utf-8"}, resp.Headers["Content-Type"]) + + message, ok := resp.JsonBody["msg"].(string) + assert.True(t, ok, "msg is not a string: %s", repr.String(resp.JsonBody)) + assert.Equal(t, "UserID: 123, PostID: 456", message) + + nested, ok := resp.JsonBody["nested"].(map[string]any) + assert.True(t, ok, "nested is not a map: %s", repr.String(resp.JsonBody)) + goodStuff, ok := nested["good_stuff"].(string) + assert.True(t, ok, "good_stuff is not a string: %s", repr.String(resp.JsonBody)) + assert.Equal(t, "This is good stuff", goodStuff) + }), + in.HttpCall(http.MethodPost, "/users", in.JsonData(t, in.Obj{"userId": 123, "postId": 345}), func(t testing.TB, resp *in.HTTPResponse) { + assert.Equal(t, 201, resp.Status) + assert.Equal(t, []string{"Header from FTL"}, resp.Headers["Post"]) + success, ok := resp.JsonBody["success"].(bool) + assert.True(t, ok, "success is not a bool: %s", repr.String(resp.JsonBody)) + assert.True(t, success) + }), + // contains aliased field + in.HttpCall(http.MethodPost, "/users", in.JsonData(t, in.Obj{"user_id": 123, "postId": 345}), func(t testing.TB, resp *in.HTTPResponse) { + assert.Equal(t, 201, resp.Status) + }), + in.HttpCall(http.MethodPut, "/users/123", in.JsonData(t, in.Obj{"postId": "346"}), func(t testing.TB, resp *in.HTTPResponse) { + assert.Equal(t, 200, resp.Status) + assert.Equal(t, []string{"Header from FTL"}, resp.Headers["Put"]) + assert.Equal(t, map[string]any{}, resp.JsonBody) + }), + in.HttpCall(http.MethodDelete, "/users/123", in.JsonData(t, in.Obj{}), func(t testing.TB, resp *in.HTTPResponse) { + assert.Equal(t, 200, resp.Status) + assert.Equal(t, []string{"Header from FTL"}, resp.Headers["Delete"]) + assert.Equal(t, map[string]any{}, resp.JsonBody) + }), + + in.HttpCall(http.MethodGet, "/html", in.JsonData(t, in.Obj{}), func(t testing.TB, resp *in.HTTPResponse) { + assert.Equal(t, 200, resp.Status) + assert.Equal(t, []string{"text/html; charset=utf-8"}, resp.Headers["Content-Type"]) + assert.Equal(t, "

HTML Page From FTL 🚀!

", string(resp.BodyBytes)) + }), + + in.HttpCall(http.MethodPost, "/bytes", []byte("Hello, World!"), func(t testing.TB, resp *in.HTTPResponse) { + assert.Equal(t, 200, resp.Status) + assert.Equal(t, []string{"application/octet-stream"}, resp.Headers["Content-Type"]) + assert.Equal(t, []byte("Hello, World!"), resp.BodyBytes) + }), + + in.HttpCall(http.MethodGet, "/empty", nil, func(t testing.TB, resp *in.HTTPResponse) { + assert.Equal(t, 200, resp.Status) + assert.Equal(t, nil, resp.Headers["Content-Type"]) + assert.Equal(t, nil, resp.BodyBytes) + }), + + in.HttpCall(http.MethodGet, "/string", []byte("Hello, World!"), func(t testing.TB, resp *in.HTTPResponse) { + assert.Equal(t, 200, resp.Status) + assert.Equal(t, []string{"text/plain; charset=utf-8"}, resp.Headers["Content-Type"]) + assert.Equal(t, []byte("Hello, World!"), resp.BodyBytes) + }), + + in.HttpCall(http.MethodGet, "/int", []byte("1234"), func(t testing.TB, resp *in.HTTPResponse) { + assert.Equal(t, 200, resp.Status) + assert.Equal(t, []string{"text/plain; charset=utf-8"}, resp.Headers["Content-Type"]) + assert.Equal(t, []byte("1234"), resp.BodyBytes) + }), + in.HttpCall(http.MethodGet, "/float", []byte("1234.56789"), func(t testing.TB, resp *in.HTTPResponse) { + assert.Equal(t, 200, resp.Status) + assert.Equal(t, []string{"text/plain; charset=utf-8"}, resp.Headers["Content-Type"]) + assert.Equal(t, []byte("1234.56789"), resp.BodyBytes) + }), + in.HttpCall(http.MethodGet, "/bool", []byte("true"), func(t testing.TB, resp *in.HTTPResponse) { + assert.Equal(t, 200, resp.Status) + assert.Equal(t, []string{"text/plain; charset=utf-8"}, resp.Headers["Content-Type"]) + assert.Equal(t, []byte("true"), resp.BodyBytes) + }), + in.HttpCall(http.MethodGet, "/error", nil, func(t testing.TB, resp *in.HTTPResponse) { + assert.Equal(t, 500, resp.Status) + assert.Equal(t, []string{"text/plain; charset=utf-8"}, resp.Headers["Content-Type"]) + assert.Equal(t, []byte("Error from FTL"), resp.BodyBytes) + }), + in.HttpCall(http.MethodGet, "/array/string", in.JsonData(t, []string{"hello", "world"}), func(t testing.TB, resp *in.HTTPResponse) { + assert.Equal(t, 200, resp.Status) + assert.Equal(t, []string{"application/json; charset=utf-8"}, resp.Headers["Content-Type"]) + assert.Equal(t, in.JsonData(t, []string{"hello", "world"}), resp.BodyBytes) + }), + in.HttpCall(http.MethodPost, "/array/data", in.JsonData(t, []in.Obj{{"item": "a"}, {"item": "b"}}), func(t testing.TB, resp *in.HTTPResponse) { + assert.Equal(t, 200, resp.Status) + assert.Equal(t, []string{"application/json; charset=utf-8"}, resp.Headers["Content-Type"]) + assert.Equal(t, in.JsonData(t, []in.Obj{{"item": "a"}, {"item": "b"}}), resp.BodyBytes) + }), + in.HttpCall(http.MethodGet, "/typeenum", in.JsonData(t, in.Obj{"name": "A", "value": "hello"}), func(t testing.TB, resp *in.HTTPResponse) { + assert.Equal(t, 200, resp.Status) + assert.Equal(t, []string{"application/json; charset=utf-8"}, resp.Headers["Content-Type"]) + assert.Equal(t, in.JsonData(t, in.Obj{"name": "A", "value": "hello"}), resp.BodyBytes) + }), + ) +} diff --git a/integration/testdata/go/httpingress/ftl.toml b/backend/controller/ingress/testdata/go/httpingress/ftl.toml similarity index 100% rename from integration/testdata/go/httpingress/ftl.toml rename to backend/controller/ingress/testdata/go/httpingress/ftl.toml diff --git a/integration/testdata/go/httpingress/go.mod b/backend/controller/ingress/testdata/go/httpingress/go.mod similarity index 100% rename from integration/testdata/go/httpingress/go.mod rename to backend/controller/ingress/testdata/go/httpingress/go.mod diff --git a/integration/testdata/go/httpingress/go.sum b/backend/controller/ingress/testdata/go/httpingress/go.sum similarity index 100% rename from integration/testdata/go/httpingress/go.sum rename to backend/controller/ingress/testdata/go/httpingress/go.sum diff --git a/integration/testdata/go/httpingress/httpingress.go b/backend/controller/ingress/testdata/go/httpingress/httpingress.go similarity index 100% rename from integration/testdata/go/httpingress/httpingress.go rename to backend/controller/ingress/testdata/go/httpingress/httpingress.go diff --git a/integration/actions_test.go b/integration/actions.go similarity index 61% rename from integration/actions_test.go rename to integration/actions.go index c12ef51951..0c96b1eadf 100644 --- a/integration/actions_test.go +++ b/integration/actions.go @@ -1,6 +1,6 @@ //go:build integration -package simple_test +package integration import ( "bytes" @@ -29,10 +29,10 @@ import ( "github.com/TBD54566975/scaffolder" ) -// scaffold a directory relative to the testdata directory to a directory relative to the working directory. -func scaffold(src, dest string, tmplCtx any) action { - return func(t testing.TB, ic testContext) { - infof("Scaffolding %s -> %s", src, dest) +// Scaffold a directory relative to the testdata directory to a directory relative to the working directory. +func Scaffold(src, dest string, tmplCtx any) Action { + return func(t testing.TB, ic TestContext) { + Infof("Scaffolding %s -> %s", src, dest) err := scaffolder.Scaffold(filepath.Join(ic.testData, src), filepath.Join(ic.workDir, dest), tmplCtx) assert.NoError(t, err) } @@ -41,10 +41,10 @@ func scaffold(src, dest string, tmplCtx any) action { // Copy a module from the testdata directory to the working directory. // // Ensures that replace directives are correctly handled. -func copyModule(module string) action { - return chain( - copyDir(module, module), - func(t testing.TB, ic testContext) { +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) }, @@ -52,28 +52,28 @@ func copyModule(module string) action { } // Copy a directory from the testdata directory to the working directory. -func copyDir(src, dest string) action { - return func(t testing.TB, ic testContext) { - infof("Copying %s -> %s", src, dest) +func CopyDir(src, dest string) Action { + return func(t testing.TB, ic TestContext) { + Infof("Copying %s -> %s", src, dest) err := copy.Copy(filepath.Join(ic.testData, src), filepath.Join(ic.workDir, dest)) assert.NoError(t, err) } } -// chain multiple actions together. -func chain(actions ...action) action { - return func(t testing.TB, ic testContext) { +// Chain multiple actions together. +func Chain(actions ...Action) Action { + return func(t testing.TB, ic TestContext) { for _, action := range actions { action(t, ic) } } } -// chdir changes the test working directory to the subdirectory for the duration of the action. -func chdir(dir string, a action) action { - return func(t testing.TB, ic testContext) { +// Chdir changes the test working directory to the subdirectory for the duration of the action. +func Chdir(dir string, a Action) Action { + return func(t testing.TB, ic TestContext) { dir := filepath.Join(ic.workDir, dir) - infof("Changing directory to %s", dir) + Infof("Changing directory to %s", dir) cwd, err := os.Getwd() assert.NoError(t, err) ic.workDir = dir @@ -84,37 +84,37 @@ func chdir(dir string, a action) action { } } -// debugShell opens a new Terminal window in the test working directory. -func debugShell() action { - return func(t testing.TB, ic testContext) { - infof("Starting debug shell") +// DebugShell opens a new Terminal window in the test working directory. +func DebugShell() Action { + return func(t testing.TB, ic TestContext) { + Infof("Starting debug shell") err := ftlexec.Command(ic, log.Debug, ic.workDir, "open", "-n", "-W", "-a", "Terminal", ".").RunBuffered(ic) assert.NoError(t, err) } } -// exec runs a command from the test working directory. -func exec(cmd string, args ...string) action { - return func(t testing.TB, ic testContext) { - infof("Executing: %s %s", cmd, shellquote.Join(args...)) +// Exec runs a command from the test working directory. +func Exec(cmd string, args ...string) Action { + return func(t testing.TB, ic TestContext) { + Infof("Executing: %s %s", cmd, shellquote.Join(args...)) err := ftlexec.Command(ic, log.Debug, ic.workDir, cmd, args...).RunBuffered(ic) assert.NoError(t, err) } } -// execWithOutput runs a command from the test working directory. +// ExecWithOutput runs a command from the test working directory. // The output is captured and is returned as part of the error. -func execWithOutput(cmd string, args ...string) action { - return func(t testing.TB, ic testContext) { - infof("Executing: %s %s", cmd, shellquote.Join(args...)) +func ExecWithOutput(cmd string, args ...string) Action { + return func(t testing.TB, ic TestContext) { + Infof("Executing: %s %s", cmd, shellquote.Join(args...)) output, err := ftlexec.Capture(ic, ic.workDir, cmd, args...) assert.NoError(t, err, "%s", string(output)) } } -// expectError wraps an action and expects it to return an error with the given message. -func expectError(action action, expectedErrorMsg string) action { - return func(t testing.TB, ic testContext) { +// ExpectError wraps an action and expects it to return an error with the given message. +func ExpectError(action Action, expectedErrorMsg string) Action { + return func(t testing.TB, ic TestContext) { defer func() { if r := recover(); r != nil { if e, ok := r.(TestingError); ok { @@ -129,29 +129,29 @@ func expectError(action action, expectedErrorMsg string) action { } // Deploy a module from the working directory and wait for it to become available. -func deploy(module string) action { - return chain( - exec("ftl", "deploy", module), - wait(module), +func Deploy(module string) Action { + return Chain( + Exec("ftl", "deploy", module), + Wait(module), ) } // Build modules from the working directory and wait for it to become available. -func build(modules ...string) action { +func Build(modules ...string) Action { args := []string{"build"} args = append(args, modules...) - return exec("ftl", args...) + return Exec("ftl", args...) } -// wait for the given module to deploy. -func wait(module string) action { - return func(t testing.TB, ic testContext) { - infof("Waiting for %s to become ready", module) +// Wait for the given module to deploy. +func Wait(module string) Action { + return func(t testing.TB, ic TestContext) { + Infof("Waiting for %s to become ready", module) // There's a bit of a bug here: wait() is already being retried by the // test harness, so in the error case we'll be waiting N^2 times. This // is fine for now, but we should fix this in the future. - ic.AssertWithRetry(t, func(t testing.TB, ic testContext) { - status, err := ic.controller.Status(ic, connect.NewRequest(&ftlv1.StatusRequest{})) + ic.AssertWithRetry(t, func(t testing.TB, ic TestContext) { + status, err := ic.Controller.Status(ic, connect.NewRequest(&ftlv1.StatusRequest{})) assert.NoError(t, err) for _, deployment := range status.Msg.Deployments { if deployment.Name == module { @@ -163,17 +163,17 @@ func wait(module string) action { } } -func sleep(duration time.Duration) action { - return func(t testing.TB, ic testContext) { - infof("Sleeping for %s", duration) +func Sleep(duration time.Duration) Action { + return func(t testing.TB, ic TestContext) { + Infof("Sleeping for %s", duration) time.Sleep(duration) } } // Assert that a file exists in the working directory. -func fileExists(path string) action { - return func(t testing.TB, ic testContext) { - infof("Checking that %s exists", path) +func FileExists(path string) Action { + return func(t testing.TB, ic TestContext) { + Infof("Checking that %s exists", path) _, err := os.Stat(filepath.Join(ic.workDir, path)) assert.NoError(t, err) } @@ -182,13 +182,13 @@ func fileExists(path string) action { // Assert that a file exists and its content contains the given text. // // If "path" is relative it will be to the working directory. -func fileContains(path, needle string) action { - return func(t testing.TB, ic testContext) { +func FileContains(path, needle string) Action { + return func(t testing.TB, ic TestContext) { absPath := path if !filepath.IsAbs(path) { absPath = filepath.Join(ic.workDir, path) } - infof("Checking that the content of %s is correct", absPath) + Infof("Checking that the content of %s is correct", absPath) data, err := os.ReadFile(absPath) assert.NoError(t, err) actual := string(data) @@ -199,13 +199,13 @@ func fileContains(path, needle string) action { // Assert that a file exists and its content is equal to the given text. // // If "path" is relative it will be to the working directory. -func fileContent(path, expected string) action { - return func(t testing.TB, ic testContext) { +func FileContent(path, expected string) Action { + return func(t testing.TB, ic TestContext) { absPath := path if !filepath.IsAbs(path) { absPath = filepath.Join(ic.workDir, path) } - infof("Checking that the content of %s is correct", absPath) + Infof("Checking that the content of %s is correct", absPath) data, err := os.ReadFile(absPath) assert.NoError(t, err) expected = strings.TrimSpace(expected) @@ -214,22 +214,22 @@ func fileContent(path, expected string) action { } } -type obj map[string]any +type Obj map[string]any // Call a verb. // // "check" may be nil -func call(module, verb string, request obj, check func(t testing.TB, response obj)) action { - return func(t testing.TB, ic testContext) { - infof("Calling %s.%s", module, verb) +func Call(module, verb string, request Obj, check func(t testing.TB, response Obj)) Action { + return func(t testing.TB, ic TestContext) { + Infof("Calling %s.%s", module, verb) data, err := json.Marshal(request) assert.NoError(t, err) - resp, err := ic.verbs.Call(ic, connect.NewRequest(&ftlv1.CallRequest{ + resp, err := ic.Verbs.Call(ic, connect.NewRequest(&ftlv1.CallRequest{ Verb: &schemapb.Ref{Module: module, Name: verb}, Body: data, })) assert.NoError(t, err) - var response obj + var response Obj assert.Zero(t, resp.Msg.GetError(), "verb failed: %s", resp.Msg.GetError().GetMessage()) err = json.Unmarshal(resp.Msg.GetBody(), &response) assert.NoError(t, err) @@ -239,10 +239,10 @@ func call(module, verb string, request obj, check func(t testing.TB, response ob } } -// Fail expects the next action to fail. -func fail(next action, msg string, args ...any) action { - return func(t testing.TB, ic testContext) { - infof("Expecting failure of nested action") +// Fail expects the next action to Fail. +func Fail(next Action, msg string, args ...any) Action { + return func(t testing.TB, ic TestContext) { + Infof("Expecting failure of nested action") panicked := true defer func() { if !panicked { @@ -257,8 +257,8 @@ func fail(next action, msg string, args ...any) action { } // fetched and returns a row's column values -func getRow(t testing.TB, ic testContext, database, query string, fieldCount int) []any { - infof("Querying %s: %s", database, query) +func GetRow(t testing.TB, ic TestContext, database, query string, fieldCount int) []any { + Infof("Querying %s: %s", database, query) db, err := sql.Open("pgx", fmt.Sprintf("postgres://postgres:secret@localhost:54320/%s?sslmode=disable", database)) assert.NoError(t, err) defer db.Close() @@ -275,9 +275,9 @@ func getRow(t testing.TB, ic testContext, database, query string, fieldCount int } // Query a single row from a database. -func queryRow(database string, query string, expected ...interface{}) action { - return func(t testing.TB, ic testContext) { - actual := getRow(t, ic, database, query, len(expected)) +func QueryRow(database string, query string, expected ...interface{}) Action { + return func(t testing.TB, ic TestContext) { + actual := GetRow(t, ic, database, query, len(expected)) for i, a := range actual { assert.Equal(t, a, expected[i]) } @@ -285,18 +285,18 @@ func queryRow(database string, query string, expected ...interface{}) action { } // Create a database for use by a module. -func createDBAction(module, dbName string, isTest bool) action { - return func(t testing.TB, ic testContext) { - createDB(t, module, dbName, isTest) +func CreateDBAction(module, dbName string, isTest bool) Action { + return func(t testing.TB, ic TestContext) { + CreateDB(t, module, dbName, isTest) } } -func createDB(t testing.TB, module, dbName string, isTestDb bool) { +func CreateDB(t testing.TB, module, dbName string, isTestDb bool) { // insert test suffix if needed when actually setting up db if isTestDb { dbName += "_test" } - infof("Creating database %s", dbName) + Infof("Creating database %s", dbName) db, err := sql.Open("pgx", "postgres://postgres:secret@localhost:54320/ftl?sslmode=disable") assert.NoError(t, err, "failed to open database connection") t.Cleanup(func() { @@ -327,31 +327,31 @@ func createDB(t testing.TB, module, dbName string, isTestDb bool) { } // Create a directory in the working directory -func mkdir(dir string) action { - return func(t testing.TB, ic testContext) { - infof("Creating directory %s", dir) +func Mkdir(dir string) Action { + return func(t testing.TB, ic TestContext) { + Infof("Creating directory %s", dir) err := os.MkdirAll(filepath.Join(ic.workDir, dir), 0700) assert.NoError(t, err) } } -type httpResponse struct { - status int - headers map[string][]string - jsonBody map[string]any - bodyBytes []byte +type HTTPResponse struct { + Status int + Headers map[string][]string + JsonBody map[string]any + BodyBytes []byte } -func jsonData(t testing.TB, body interface{}) []byte { +func JsonData(t testing.TB, body interface{}) []byte { b, err := json.Marshal(body) assert.NoError(t, err) return b } -// httpCall makes an HTTP call to the running FTL ingress endpoint. -func httpCall(method string, path string, body []byte, onResponse func(t testing.TB, resp *httpResponse)) action { - return func(t testing.TB, ic testContext) { - infof("HTTP %s %s", method, path) +// HttpCall makes an HTTP call to the running FTL ingress endpoint. +func HttpCall(method string, path string, body []byte, onResponse func(t testing.TB, resp *HTTPResponse)) Action { + return func(t testing.TB, ic TestContext) { + Infof("HTTP %s %s", method, path) baseURL, err := url.Parse(fmt.Sprintf("http://localhost:8892/ingress")) assert.NoError(t, err) @@ -372,16 +372,16 @@ func httpCall(method string, path string, body []byte, onResponse func(t testing // ignore the error here since some responses are just `[]byte`. _ = json.Unmarshal(bodyBytes, &resBody) - onResponse(t, &httpResponse{ - status: resp.StatusCode, - headers: resp.Header, - jsonBody: resBody, - bodyBytes: bodyBytes, + onResponse(t, &HTTPResponse{ + Status: resp.StatusCode, + Headers: resp.Header, + JsonBody: resBody, + BodyBytes: bodyBytes, }) } } // Run "go test" in the given module. -func testModule(module string) action { - return chdir(module, exec("go", "test", "-v", ".")) +func ExecModuleTest(module string) Action { + return Chdir(module, Exec("go", "test", "-v", ".")) } diff --git a/integration/harness_test.go b/integration/harness.go similarity index 80% rename from integration/harness_test.go rename to integration/harness.go index 9cd3c9ac61..07d5f8e301 100644 --- a/integration/harness_test.go +++ b/integration/harness.go @@ -1,6 +1,6 @@ //go:build integration -package simple_test +package integration import ( "bytes" @@ -35,17 +35,20 @@ var integrationTestTimeout = func() time.Duration { return d }() -func infof(format string, args ...any) { +func Infof(format string, args ...any) { fmt.Printf("\033[32m\033[1mINFO: "+format+"\033[0m\n", args...) } var buildOnce sync.Once -// run an integration test. +// Run an integration test. // ftlConfigPath: if FTL_CONFIG should be set for this test, then pass in the relative // -// path from integration/testdata/go/. e.g. "database/ftl-project.toml" -func run(t *testing.T, ftlConfigPath string, actions ...action) { +// 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) { tmpDir := t.TempDir() cwd, err := os.Getwd() @@ -67,7 +70,7 @@ func run(t *testing.T, ftlConfigPath string, actions ...action) { binDir := filepath.Join(rootDir, "build", "release") buildOnce.Do(func() { - infof("Building ftl") + Infof("Building ftl") err = ftlexec.Command(ctx, log.Debug, rootDir, "just", "build", "ftl").RunBuffered(ctx) assert.NoError(t, err) }) @@ -75,33 +78,33 @@ func run(t *testing.T, ftlConfigPath string, actions ...action) { controller := rpc.Dial(ftlv1connect.NewControllerServiceClient, "http://localhost:8892", log.Debug) verbs := rpc.Dial(ftlv1connect.NewVerbServiceClient, "http://localhost:8892", log.Debug) - infof("Starting ftl cluster") + Infof("Starting ftl cluster") ctx = startProcess(ctx, t, filepath.Join(binDir, "ftl"), "serve", "--recreate") - ic := testContext{ + ic := TestContext{ Context: ctx, rootDir: rootDir, testData: filepath.Join(cwd, "testdata", "go"), workDir: tmpDir, binDir: binDir, - controller: controller, - verbs: verbs, + Controller: controller, + Verbs: verbs, } - 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{})) + 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") + Infof("Starting test") for _, action := range actions { ic.AssertWithRetry(t, action) } } -type testContext struct { +type TestContext struct { context.Context // Temporary directory the test is executing in. workDir string @@ -112,12 +115,12 @@ type testContext struct { // Path to the "bin" directory. binDir string - controller ftlv1connect.ControllerServiceClient - verbs ftlv1connect.VerbServiceClient + Controller ftlv1connect.ControllerServiceClient + Verbs ftlv1connect.VerbServiceClient } // AssertWithRetry asserts that the given action passes within the timeout. -func (i testContext) AssertWithRetry(t testing.TB, assertion action) { +func (i TestContext) AssertWithRetry(t testing.TB, assertion Action) { waitCtx, done := context.WithTimeout(i, integrationTestTimeout) defer done() for { @@ -135,7 +138,7 @@ func (i testContext) AssertWithRetry(t testing.TB, assertion action) { } // Run an assertion, wrapping testing.TB in an implementation that panics on failure, propagating the error. -func (i testContext) runAssertionOnce(t testing.TB, assertion action) (err error) { +func (i TestContext) runAssertionOnce(t testing.TB, assertion Action) (err error) { defer func() { switch r := recover().(type) { case TestingError: @@ -152,7 +155,7 @@ func (i testContext) runAssertionOnce(t testing.TB, assertion action) (err error return nil } -type action func(t testing.TB, ic testContext) +type Action func(t testing.TB, ic TestContext) type logWriter struct { mu sync.Mutex diff --git a/integration/integration_test.go b/integration/integration_test.go index e2c67cbbd0..69ceceaef3 100644 --- a/integration/integration_test.go +++ b/integration/integration_test.go @@ -1,6 +1,6 @@ //go:build integration -package simple_test +package integration_test import ( "fmt" @@ -18,6 +18,7 @@ import ( ftlv1 "github.com/TBD54566975/ftl/backend/protos/xyz/block/ftl/v1" schemapb "github.com/TBD54566975/ftl/backend/protos/xyz/block/ftl/v1/schema" + . "github.com/TBD54566975/ftl/integration" ) func TestCron(t *testing.T) { @@ -30,10 +31,10 @@ func TestCron(t *testing.T) { t.Cleanup(func() { _ = os.Remove(tmpFile) }) - run(t, "", - copyModule("cron"), - deploy("cron"), - func(t testing.TB, ic testContext) { + Run(t, "", + CopyModule("cron"), + Deploy("cron"), + func(t testing.TB, ic TestContext) { _, err := os.Stat(tmpFile) assert.NoError(t, err) }, @@ -41,22 +42,22 @@ func TestCron(t *testing.T) { } func TestLifecycle(t *testing.T) { - run(t, "", - exec("ftl", "init", "go", ".", "echo"), - deploy("echo"), - call("echo", "echo", obj{"name": "Bob"}, func(t testing.TB, response obj) { + Run(t, "", + Exec("ftl", "init", "go", ".", "echo"), + Deploy("echo"), + Call("echo", "echo", Obj{"name": "Bob"}, func(t testing.TB, response Obj) { assert.Equal(t, "Hello, Bob!", response["message"]) }), ) } func TestInterModuleCall(t *testing.T) { - run(t, "", - copyModule("echo"), - copyModule("time"), - deploy("time"), - deploy("echo"), - call("echo", "echo", obj{"name": "Bob"}, func(t testing.TB, response obj) { + Run(t, "", + CopyModule("echo"), + CopyModule("time"), + Deploy("time"), + Deploy("echo"), + Call("echo", "echo", Obj{"name": "Bob"}, func(t testing.TB, response Obj) { message, ok := response["message"].(string) assert.True(t, ok, "message is not a string: %s", repr.String(response)) if !strings.HasPrefix(message, "Hello, Bob!!! It is ") { @@ -67,201 +68,102 @@ func TestInterModuleCall(t *testing.T) { } func TestNonExportedDecls(t *testing.T) { - run(t, "", - copyModule("time"), - deploy("time"), - copyModule("echo"), - deploy("echo"), - copyModule("notexportedverb"), - expectError(execWithOutput("ftl", "deploy", "notexportedverb"), "call first argument must be a function but is an unresolved reference to echo.Echo, does it need to be exported?"), + Run(t, "", + CopyModule("time"), + Deploy("time"), + CopyModule("echo"), + Deploy("echo"), + CopyModule("notexportedverb"), + ExpectError( + ExecWithOutput("ftl", "deploy", "notexportedverb"), + "call first argument must be a function but is an unresolved reference to echo.Echo, does it need to be exported?"), ) } func TestUndefinedExportedDecls(t *testing.T) { - run(t, "", - copyModule("time"), - deploy("time"), - copyModule("echo"), - deploy("echo"), - copyModule("undefinedverb"), - expectError(execWithOutput("ftl", "deploy", "undefinedverb"), "call first argument must be a function but is an unresolved reference to echo.Undefined"), + Run(t, "", + CopyModule("time"), + Deploy("time"), + CopyModule("echo"), + Deploy("echo"), + CopyModule("undefinedverb"), + ExpectError( + ExecWithOutput("ftl", "deploy", "undefinedverb"), + "call first argument must be a function but is an unresolved reference to echo.Undefined"), ) } func TestDatabase(t *testing.T) { - run(t, "database/ftl-project.toml", + Run(t, "database/ftl-project.toml", // deploy real module against "testdb" - copyModule("database"), - createDBAction("database", "testdb", false), - deploy("database"), - call("database", "insert", obj{"data": "hello"}, nil), - queryRow("testdb", "SELECT data FROM requests", "hello"), + CopyModule("database"), + CreateDBAction("database", "testdb", false), + Deploy("database"), + Call("database", "insert", Obj{"data": "hello"}, nil), + QueryRow("testdb", "SELECT data FROM requests", "hello"), // run tests which should only affect "testdb_test" - createDBAction("database", "testdb", true), - testModule("database"), - queryRow("testdb", "SELECT data FROM requests", "hello"), + CreateDBAction("database", "testdb", true), + ExecModuleTest("database"), + QueryRow("testdb", "SELECT data FROM requests", "hello"), ) } func TestSchemaGenerate(t *testing.T) { - run(t, "", - copyDir("../schema-generate", "schema-generate"), - mkdir("build/schema-generate"), - exec("ftl", "schema", "generate", "schema-generate", "build/schema-generate"), - fileContains("build/schema-generate/test.txt", "olleh"), + Run(t, "", + CopyDir("../schema-generate", "schema-generate"), + Mkdir("build/schema-generate"), + Exec("ftl", "schema", "generate", "schema-generate", "build/schema-generate"), + FileContains("build/schema-generate/test.txt", "olleh"), ) } func TestHttpEncodeOmitempty(t *testing.T) { - run(t, "", - copyModule("omitempty"), - deploy("omitempty"), - httpCall(http.MethodGet, "/get", jsonData(t, obj{}), func(t testing.TB, resp *httpResponse) { - assert.Equal(t, 200, resp.status) - _, ok := resp.jsonBody["mustset"] + Run(t, "", + CopyModule("omitempty"), + Deploy("omitempty"), + HttpCall(http.MethodGet, "/get", JsonData(t, Obj{}), func(t testing.TB, resp *HTTPResponse) { + assert.Equal(t, 200, resp.Status) + _, ok := resp.JsonBody["mustset"] assert.True(t, ok) - _, ok = resp.jsonBody["error"] + _, ok = resp.JsonBody["error"] assert.False(t, ok) }), ) } -func TestHttpIngress(t *testing.T) { - run(t, "", - copyModule("httpingress"), - deploy("httpingress"), - httpCall(http.MethodGet, "/users/123/posts/456", jsonData(t, obj{}), func(t testing.TB, resp *httpResponse) { - assert.Equal(t, 200, resp.status) - assert.Equal(t, []string{"Header from FTL"}, resp.headers["Get"]) - assert.Equal(t, []string{"application/json; charset=utf-8"}, resp.headers["Content-Type"]) - - message, ok := resp.jsonBody["msg"].(string) - assert.True(t, ok, "msg is not a string: %s", repr.String(resp.jsonBody)) - assert.Equal(t, "UserID: 123, PostID: 456", message) - - nested, ok := resp.jsonBody["nested"].(map[string]any) - assert.True(t, ok, "nested is not a map: %s", repr.String(resp.jsonBody)) - goodStuff, ok := nested["good_stuff"].(string) - assert.True(t, ok, "good_stuff is not a string: %s", repr.String(resp.jsonBody)) - assert.Equal(t, "This is good stuff", goodStuff) - }), - httpCall(http.MethodPost, "/users", jsonData(t, obj{"userId": 123, "postId": 345}), func(t testing.TB, resp *httpResponse) { - assert.Equal(t, 201, resp.status) - assert.Equal(t, []string{"Header from FTL"}, resp.headers["Post"]) - success, ok := resp.jsonBody["success"].(bool) - assert.True(t, ok, "success is not a bool: %s", repr.String(resp.jsonBody)) - assert.True(t, success) - }), - // contains aliased field - httpCall(http.MethodPost, "/users", jsonData(t, obj{"user_id": 123, "postId": 345}), func(t testing.TB, resp *httpResponse) { - assert.Equal(t, 201, resp.status) - }), - httpCall(http.MethodPut, "/users/123", jsonData(t, obj{"postId": "346"}), func(t testing.TB, resp *httpResponse) { - assert.Equal(t, 200, resp.status) - assert.Equal(t, []string{"Header from FTL"}, resp.headers["Put"]) - assert.Equal(t, map[string]any{}, resp.jsonBody) - }), - httpCall(http.MethodDelete, "/users/123", jsonData(t, obj{}), func(t testing.TB, resp *httpResponse) { - assert.Equal(t, 200, resp.status) - assert.Equal(t, []string{"Header from FTL"}, resp.headers["Delete"]) - assert.Equal(t, map[string]any{}, resp.jsonBody) - }), - - httpCall(http.MethodGet, "/html", jsonData(t, obj{}), func(t testing.TB, resp *httpResponse) { - assert.Equal(t, 200, resp.status) - assert.Equal(t, []string{"text/html; charset=utf-8"}, resp.headers["Content-Type"]) - assert.Equal(t, "

HTML Page From FTL 🚀!

", string(resp.bodyBytes)) - }), - - httpCall(http.MethodPost, "/bytes", []byte("Hello, World!"), func(t testing.TB, resp *httpResponse) { - assert.Equal(t, 200, resp.status) - assert.Equal(t, []string{"application/octet-stream"}, resp.headers["Content-Type"]) - assert.Equal(t, []byte("Hello, World!"), resp.bodyBytes) - }), - - httpCall(http.MethodGet, "/empty", nil, func(t testing.TB, resp *httpResponse) { - assert.Equal(t, 200, resp.status) - assert.Equal(t, nil, resp.headers["Content-Type"]) - assert.Equal(t, nil, resp.bodyBytes) - }), - - httpCall(http.MethodGet, "/string", []byte("Hello, World!"), func(t testing.TB, resp *httpResponse) { - assert.Equal(t, 200, resp.status) - assert.Equal(t, []string{"text/plain; charset=utf-8"}, resp.headers["Content-Type"]) - assert.Equal(t, []byte("Hello, World!"), resp.bodyBytes) - }), - - httpCall(http.MethodGet, "/int", []byte("1234"), func(t testing.TB, resp *httpResponse) { - assert.Equal(t, 200, resp.status) - assert.Equal(t, []string{"text/plain; charset=utf-8"}, resp.headers["Content-Type"]) - assert.Equal(t, []byte("1234"), resp.bodyBytes) - }), - httpCall(http.MethodGet, "/float", []byte("1234.56789"), func(t testing.TB, resp *httpResponse) { - assert.Equal(t, 200, resp.status) - assert.Equal(t, []string{"text/plain; charset=utf-8"}, resp.headers["Content-Type"]) - assert.Equal(t, []byte("1234.56789"), resp.bodyBytes) - }), - httpCall(http.MethodGet, "/bool", []byte("true"), func(t testing.TB, resp *httpResponse) { - assert.Equal(t, 200, resp.status) - assert.Equal(t, []string{"text/plain; charset=utf-8"}, resp.headers["Content-Type"]) - assert.Equal(t, []byte("true"), resp.bodyBytes) - }), - httpCall(http.MethodGet, "/error", nil, func(t testing.TB, resp *httpResponse) { - assert.Equal(t, 500, resp.status) - assert.Equal(t, []string{"text/plain; charset=utf-8"}, resp.headers["Content-Type"]) - assert.Equal(t, []byte("Error from FTL"), resp.bodyBytes) - }), - httpCall(http.MethodGet, "/array/string", jsonData(t, []string{"hello", "world"}), func(t testing.TB, resp *httpResponse) { - assert.Equal(t, 200, resp.status) - assert.Equal(t, []string{"application/json; charset=utf-8"}, resp.headers["Content-Type"]) - assert.Equal(t, jsonData(t, []string{"hello", "world"}), resp.bodyBytes) - }), - httpCall(http.MethodPost, "/array/data", jsonData(t, []obj{{"item": "a"}, {"item": "b"}}), func(t testing.TB, resp *httpResponse) { - assert.Equal(t, 200, resp.status) - assert.Equal(t, []string{"application/json; charset=utf-8"}, resp.headers["Content-Type"]) - assert.Equal(t, jsonData(t, []obj{{"item": "a"}, {"item": "b"}}), resp.bodyBytes) - }), - httpCall(http.MethodGet, "/typeenum", jsonData(t, obj{"name": "A", "value": "hello"}), func(t testing.TB, resp *httpResponse) { - assert.Equal(t, 200, resp.status) - assert.Equal(t, []string{"application/json; charset=utf-8"}, resp.headers["Content-Type"]) - assert.Equal(t, jsonData(t, obj{"name": "A", "value": "hello"}), resp.bodyBytes) - }), - ) -} - func TestRuntimeReflection(t *testing.T) { - run(t, "", - copyModule("runtimereflection"), - testModule("runtimereflection"), + Run(t, "", + CopyModule("runtimereflection"), + ExecModuleTest("runtimereflection"), ) } func TestModuleUnitTests(t *testing.T) { - run(t, "", - copyModule("time"), - copyModule("wrapped"), - copyModule("verbtypes"), - build("time", "wrapped", "verbtypes"), - testModule("wrapped"), - testModule("verbtypes"), + Run(t, "", + CopyModule("time"), + CopyModule("wrapped"), + CopyModule("verbtypes"), + Build("time", "wrapped", "verbtypes"), + ExecModuleTest("wrapped"), + ExecModuleTest("verbtypes"), ) } func TestLease(t *testing.T) { - run(t, "", - copyModule("leases"), - build("leases"), + Run(t, "", + CopyModule("leases"), + Build("leases"), // checks if leases work in a unit test environment - testModule("leases"), - deploy("leases"), + ExecModuleTest("leases"), + Deploy("leases"), // checks if it leases work with a real controller - func(t testing.TB, ic testContext) { + func(t testing.TB, ic TestContext) { // Start a lease. wg := errgroup.Group{} wg.Go(func() error { - infof("Acquiring lease") - resp, err := ic.verbs.Call(ic, connect.NewRequest(&ftlv1.CallRequest{ + Infof("Acquiring lease") + resp, err := ic.Verbs.Call(ic, connect.NewRequest(&ftlv1.CallRequest{ Verb: &schemapb.Ref{Module: "leases", Name: "acquire"}, Body: []byte("{}"), })) @@ -273,9 +175,9 @@ func TestLease(t *testing.T) { time.Sleep(time.Second) - infof("Trying to acquire lease again") + Infof("Trying to acquire lease again") // Trying to obtain the lease again should fail. - resp, err := ic.verbs.Call(ic, connect.NewRequest(&ftlv1.CallRequest{ + resp, err := ic.Verbs.Call(ic, connect.NewRequest(&ftlv1.CallRequest{ Verb: &schemapb.Ref{Module: "leases", Name: "acquire"}, Body: []byte("{}"), })) @@ -292,65 +194,65 @@ func TestLease(t *testing.T) { func TestFSMGoTests(t *testing.T) { logFilePath := filepath.Join(t.TempDir(), "fsm.log") t.Setenv("FSM_LOG_FILE", logFilePath) - run(t, "", - copyModule("fsm"), - build("fsm"), - testModule("fsm"), + Run(t, "", + CopyModule("fsm"), + Build("fsm"), + ExecModuleTest("fsm"), ) } func TestFSM(t *testing.T) { logFilePath := filepath.Join(t.TempDir(), "fsm.log") t.Setenv("FSM_LOG_FILE", logFilePath) - fsmInState := func(instance, status, state string) action { - return queryRow("ftl", fmt.Sprintf(` + fsmInState := func(instance, status, state string) Action { + return QueryRow("ftl", fmt.Sprintf(` SELECT status, current_state FROM fsm_instances WHERE fsm = 'fsm.fsm' AND key = '%s' `, instance), status, state) } - run(t, "", - copyModule("fsm"), - deploy("fsm"), - - call("fsm", "sendOne", obj{"instance": "1"}, nil), - call("fsm", "sendOne", obj{"instance": "2"}, nil), - fileContains(logFilePath, "start 1"), - fileContains(logFilePath, "start 2"), + Run(t, "", + CopyModule("fsm"), + Deploy("fsm"), + + Call("fsm", "sendOne", Obj{"instance": "1"}, nil), + Call("fsm", "sendOne", Obj{"instance": "2"}, nil), + FileContains(logFilePath, "start 1"), + FileContains(logFilePath, "start 2"), fsmInState("1", "running", "fsm.start"), fsmInState("2", "running", "fsm.start"), - call("fsm", "sendOne", obj{"instance": "1"}, nil), - fileContains(logFilePath, "middle 1"), + Call("fsm", "sendOne", Obj{"instance": "1"}, nil), + FileContains(logFilePath, "middle 1"), fsmInState("1", "running", "fsm.middle"), - call("fsm", "sendOne", obj{"instance": "1"}, nil), - fileContains(logFilePath, "end 1"), + Call("fsm", "sendOne", Obj{"instance": "1"}, nil), + FileContains(logFilePath, "end 1"), fsmInState("1", "completed", "fsm.end"), - fail(call("fsm", "sendOne", obj{"instance": "1"}, nil), + Fail(Call("fsm", "sendOne", Obj{"instance": "1"}, nil), "FSM instance 1 is already in state fsm.end"), // Invalid state transition - fail(call("fsm", "sendTwo", obj{"instance": "2"}, nil), + Fail(Call("fsm", "sendTwo", Obj{"instance": "2"}, nil), "invalid state transition"), - call("fsm", "sendOne", obj{"instance": "2"}, nil), - fileContains(logFilePath, "middle 2"), + Call("fsm", "sendOne", Obj{"instance": "2"}, nil), + FileContains(logFilePath, "middle 2"), fsmInState("2", "running", "fsm.middle"), // Invalid state transition - fail(call("fsm", "sendTwo", obj{"instance": "2"}, nil), + Fail(Call("fsm", "sendTwo", Obj{"instance": "2"}, nil), "invalid state transition"), ) } func TestFSMRetry(t *testing.T) { - checkRetries := func(origin, verb string, delays []time.Duration) action { - return func(t testing.TB, ic testContext) { + checkRetries := func(origin, verb string, delays []time.Duration) Action { + return func(t testing.TB, ic TestContext) { results := []any{} for i := 0; i < len(delays); i++ { - values := getRow(t, ic, "ftl", fmt.Sprintf("SELECT scheduled_at FROM async_calls WHERE origin = '%s' AND verb = '%s' AND state = 'error' ORDER BY created_at LIMIT 1 OFFSET %d", origin, verb, i), 1) + values := GetRow(t, ic, "ftl", fmt.Sprintf("SELECT scheduled_at FROM async_calls WHERE origin = '%s' AND verb = '%s' AND state = 'error' ORDER BY created_at LIMIT 1 OFFSET %d", origin, verb, i), 1) results = append(results, values[0]) } times := []time.Time{} @@ -367,28 +269,28 @@ func TestFSMRetry(t *testing.T) { } } - run(t, "", - copyModule("fsmretry"), - build("fsmretry"), - deploy("fsmretry"), + Run(t, "", + CopyModule("fsmretry"), + Build("fsmretry"), + Deploy("fsmretry"), // start 2 FSM instances - call("fsmretry", "start", obj{"id": "1"}, func(t testing.TB, response obj) {}), - call("fsmretry", "start", obj{"id": "2"}, func(t testing.TB, response obj) {}), + Call("fsmretry", "start", Obj{"id": "1"}, func(t testing.TB, response Obj) {}), + Call("fsmretry", "start", Obj{"id": "2"}, func(t testing.TB, response Obj) {}), - sleep(2*time.Second), + Sleep(2*time.Second), // transition the FSM, should fail each time. - call("fsmretry", "startTransitionToTwo", obj{"id": "1"}, func(t testing.TB, response obj) {}), - call("fsmretry", "startTransitionToThree", obj{"id": "2"}, func(t testing.TB, response obj) {}), + Call("fsmretry", "startTransitionToTwo", Obj{"id": "1"}, func(t testing.TB, response Obj) {}), + Call("fsmretry", "startTransitionToThree", Obj{"id": "2"}, func(t testing.TB, response Obj) {}), - sleep(8*time.Second), //6s is longest run of retries + Sleep(8*time.Second), //6s is longest run of retries // both FSMs instances should have failed - queryRow("ftl", "SELECT COUNT(*) FROM fsm_instances WHERE status = 'failed'", int64(2)), + QueryRow("ftl", "SELECT COUNT(*) FROM fsm_instances WHERE status = 'failed'", int64(2)), - queryRow("ftl", fmt.Sprintf("SELECT COUNT(*) FROM async_calls WHERE origin = '%s' AND verb = '%s'", "fsm:fsmretry.fsm:1", "fsmretry.state2"), int64(4)), + QueryRow("ftl", fmt.Sprintf("SELECT COUNT(*) FROM async_calls WHERE origin = '%s' AND verb = '%s'", "fsm:fsmretry.fsm:1", "fsmretry.state2"), int64(4)), checkRetries("fsm:fsmretry.fsm:1", "fsmretry.state2", []time.Duration{time.Second, time.Second, time.Second}), - queryRow("ftl", fmt.Sprintf("SELECT COUNT(*) FROM async_calls WHERE origin = '%s' AND verb = '%s'", "fsm:fsmretry.fsm:2", "fsmretry.state3"), int64(4)), + QueryRow("ftl", fmt.Sprintf("SELECT COUNT(*) FROM async_calls WHERE origin = '%s' AND verb = '%s'", "fsm:fsmretry.fsm:2", "fsmretry.state3"), int64(4)), checkRetries("fsm:fsmretry.fsm:2", "fsmretry.state3", []time.Duration{time.Second, 2 * time.Second, 3 * time.Second}), ) } diff --git a/integration/wrapper_test.go b/integration/wrapper.go similarity index 97% rename from integration/wrapper_test.go rename to integration/wrapper.go index ba710eaff9..2492fce706 100644 --- a/integration/wrapper_test.go +++ b/integration/wrapper.go @@ -1,4 +1,4 @@ -package simple_test +package integration import ( "fmt"