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"