diff --git a/.github/workflows/autofmt.yml b/.github/workflows/autofmt.yml index 63098d44c7..ac377c7eac 100644 --- a/.github/workflows/autofmt.yml +++ b/.github/workflows/autofmt.yml @@ -1,6 +1,9 @@ on: pull_request: name: Auto-format +concurrency: + group: ${{ github.ref }}-autofmt + cancel-in-progress: true jobs: format: # Check if the PR is not from a fork diff --git a/.github/workflows/integration.yml b/.github/workflows/integration.yml index d03daa5fb1..2d9c254fee 100644 --- a/.github/workflows/integration.yml +++ b/.github/workflows/integration.yml @@ -1,5 +1,9 @@ on: + pull_request: workflow_call: +concurrency: + group: ${{ github.ref }}-integration + cancel-in-progress: true name: Integration jobs: integration: @@ -15,8 +19,18 @@ jobs: uses: actions/cache@v3 with: path: ~/go/pkg/mod - key: ${{ runner.os }}-go + key: ${{ runner.os }}-go-${{ hashFiles('**/go.mod') }} + restore-keys: | + ${{ runner.os }}-go- + - uses: actions/cache@v3 + with: + path: ~/.m2/repository + key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }} + restore-keys: | + ${{ runner.os }}-maven- - name: Docker Compose run: docker compose up -d --wait + - name: Download Go Modules + run: go mod download - name: Integration tests - run: integration-tests + run: go test -v -tags integration ./integration diff --git a/examples/go.mod b/examples/go.mod index ed6c42abf4..28e096c045 100644 --- a/examples/go.mod +++ b/examples/go.mod @@ -31,11 +31,11 @@ require ( go.opentelemetry.io/otel v1.21.0 // indirect go.opentelemetry.io/otel/metric v1.21.0 // indirect go.opentelemetry.io/otel/trace v1.21.0 // indirect - golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa // indirect + golang.org/x/exp v0.0.0-20231127185646-65229373498e // indirect golang.org/x/mod v0.14.0 // indirect - golang.org/x/net v0.18.0 // indirect + golang.org/x/net v0.19.0 // indirect golang.org/x/sync v0.5.0 // indirect - golang.org/x/sys v0.14.0 // indirect + golang.org/x/sys v0.15.0 // indirect golang.org/x/text v0.14.0 // indirect google.golang.org/protobuf v1.31.0 // indirect ) diff --git a/examples/go.sum b/examples/go.sum index 8850e80284..c230bbad7f 100644 --- a/examples/go.sum +++ b/examples/go.sum @@ -88,21 +88,21 @@ go.opentelemetry.io/otel/sdk/metric v1.21.0 h1:smhI5oD714d6jHE6Tie36fPx4WDFIg+Y6 go.opentelemetry.io/otel/sdk/metric v1.21.0/go.mod h1:FJ8RAsoPGv/wYMgBdUJXOm+6pzFY3YdljnXtv1SBE8Q= go.opentelemetry.io/otel/trace v1.21.0 h1:WD9i5gzvoUPuXIXH24ZNBudiarZDKuekPqi/E8fpfLc= go.opentelemetry.io/otel/trace v1.21.0/go.mod h1:LGbsEB0f9LGjN+OZaQQ26sohbOmiMR+BaslueVtS/qQ= -golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa h1:FRnLl4eNAQl8hwxVVC17teOw8kdjVDVAiFMtgUdTSRQ= -golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa/go.mod h1:zk2irFbV9DP96SEBUUAy67IdHUaZuSnrz1n472HUCLE= +golang.org/x/exp v0.0.0-20231127185646-65229373498e h1:Gvh4YaCaXNs6dKTlfgismwWZKyjVZXwOPfIyUaqU3No= +golang.org/x/exp v0.0.0-20231127185646-65229373498e/go.mod h1:iRJReGqOEeBhDZGkGbynYwcHlctCvnjTYIamk7uXpHI= golang.org/x/mod v0.14.0 h1:dGoOF9QVLYng8IHTm7BAyWqCqSheQ5pYWGhzW00YJr0= golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= -golang.org/x/net v0.18.0 h1:mIYleuAkSbHh0tCv7RvjL3F6ZVbLjq4+R7zbOn3Kokg= -golang.org/x/net v0.18.0/go.mod h1:/czyP5RqHAH4odGYxBJ1qz0+CE5WZ+2j1YgoEo8F2jQ= +golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c= +golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U= golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE= golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.14.0 h1:Vz7Qs629MkJkGyHxUlRHizWJRG2j8fbQKjELVSNhy7Q= -golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= +golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/tools v0.15.0 h1:zdAyfUGbYmuVokhzVmghFl2ZJh5QhcfebBgmVPFYA+8= -golang.org/x/tools v0.15.0/go.mod h1:hpksKq4dtpQWS1uQ61JkdqWM3LscIS6Slf+VVkm+wQk= +golang.org/x/tools v0.16.0 h1:GO788SKMRunPIBCXiQyo2AaexLstOrVhuAL5YwsckQM= +golang.org/x/tools v0.16.0/go.mod h1:kYVVN6I1mBNoB1OX+noeBjbRk4IUEPa7JJ+TJMEooJ0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8= diff --git a/examples/online-boutique/go.mod b/examples/online-boutique/go.mod index 9c24b0808c..4f7e760180 100644 --- a/examples/online-boutique/go.mod +++ b/examples/online-boutique/go.mod @@ -9,7 +9,7 @@ require ( github.com/alecthomas/errors v0.4.0 github.com/google/uuid v1.4.0 github.com/hashicorp/golang-lru/v2 v2.0.5 - golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa + golang.org/x/exp v0.0.0-20231127185646-65229373498e ) require ( @@ -36,9 +36,9 @@ require ( go.opentelemetry.io/otel/metric v1.21.0 // indirect go.opentelemetry.io/otel/trace v1.21.0 // indirect golang.org/x/mod v0.14.0 // indirect - golang.org/x/net v0.18.0 // indirect + golang.org/x/net v0.19.0 // indirect golang.org/x/sync v0.5.0 // indirect - golang.org/x/sys v0.14.0 // indirect + golang.org/x/sys v0.15.0 // indirect golang.org/x/text v0.14.0 // indirect google.golang.org/protobuf v1.31.0 // indirect ) diff --git a/examples/online-boutique/go.sum b/examples/online-boutique/go.sum index e18494e8b7..8e521ebbf6 100644 --- a/examples/online-boutique/go.sum +++ b/examples/online-boutique/go.sum @@ -90,21 +90,21 @@ go.opentelemetry.io/otel/sdk/metric v1.21.0 h1:smhI5oD714d6jHE6Tie36fPx4WDFIg+Y6 go.opentelemetry.io/otel/sdk/metric v1.21.0/go.mod h1:FJ8RAsoPGv/wYMgBdUJXOm+6pzFY3YdljnXtv1SBE8Q= go.opentelemetry.io/otel/trace v1.21.0 h1:WD9i5gzvoUPuXIXH24ZNBudiarZDKuekPqi/E8fpfLc= go.opentelemetry.io/otel/trace v1.21.0/go.mod h1:LGbsEB0f9LGjN+OZaQQ26sohbOmiMR+BaslueVtS/qQ= -golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa h1:FRnLl4eNAQl8hwxVVC17teOw8kdjVDVAiFMtgUdTSRQ= -golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa/go.mod h1:zk2irFbV9DP96SEBUUAy67IdHUaZuSnrz1n472HUCLE= +golang.org/x/exp v0.0.0-20231127185646-65229373498e h1:Gvh4YaCaXNs6dKTlfgismwWZKyjVZXwOPfIyUaqU3No= +golang.org/x/exp v0.0.0-20231127185646-65229373498e/go.mod h1:iRJReGqOEeBhDZGkGbynYwcHlctCvnjTYIamk7uXpHI= golang.org/x/mod v0.14.0 h1:dGoOF9QVLYng8IHTm7BAyWqCqSheQ5pYWGhzW00YJr0= golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= -golang.org/x/net v0.18.0 h1:mIYleuAkSbHh0tCv7RvjL3F6ZVbLjq4+R7zbOn3Kokg= -golang.org/x/net v0.18.0/go.mod h1:/czyP5RqHAH4odGYxBJ1qz0+CE5WZ+2j1YgoEo8F2jQ= +golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c= +golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U= golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE= golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.14.0 h1:Vz7Qs629MkJkGyHxUlRHizWJRG2j8fbQKjELVSNhy7Q= -golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= +golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/tools v0.15.0 h1:zdAyfUGbYmuVokhzVmghFl2ZJh5QhcfebBgmVPFYA+8= -golang.org/x/tools v0.15.0/go.mod h1:hpksKq4dtpQWS1uQ61JkdqWM3LscIS6Slf+VVkm+wQk= +golang.org/x/tools v0.16.0 h1:GO788SKMRunPIBCXiQyo2AaexLstOrVhuAL5YwsckQM= +golang.org/x/tools v0.16.0/go.mod h1:kYVVN6I1mBNoB1OX+noeBjbRk4IUEPa7JJ+TJMEooJ0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8= diff --git a/go.sum b/go.sum index 125d3c917d..36bb473b91 100644 --- a/go.sum +++ b/go.sum @@ -174,31 +174,21 @@ go.uber.org/automaxprocs v1.5.3 h1:kWazyxZUrS3Gs4qUpbwo5kEIMGe/DAvi5Z4tl2NW4j8= go.uber.org/automaxprocs v1.5.3/go.mod h1:eRbA25aqJrxAbsLO0xy5jVwPt7FQnRgjW+efnwa1WM0= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= -golang.org/x/crypto v0.15.0 h1:frVn1TEaCEaZcn3Tmd7Y2b5KKPaZ+I32Q2OA3kYp5TA= -golang.org/x/crypto v0.15.0/go.mod h1:4ChreQoLWfG3xLDer1WdlH5NdlQ3+mwnQq1YTKY+72g= golang.org/x/crypto v0.16.0 h1:mMMrFzRSCF0GvB7Ne27XVtVAaXLrPmgPC7/v0tkwHaY= golang.org/x/crypto v0.16.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= -golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa h1:FRnLl4eNAQl8hwxVVC17teOw8kdjVDVAiFMtgUdTSRQ= -golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa/go.mod h1:zk2irFbV9DP96SEBUUAy67IdHUaZuSnrz1n472HUCLE= golang.org/x/exp v0.0.0-20231127185646-65229373498e h1:Gvh4YaCaXNs6dKTlfgismwWZKyjVZXwOPfIyUaqU3No= golang.org/x/exp v0.0.0-20231127185646-65229373498e/go.mod h1:iRJReGqOEeBhDZGkGbynYwcHlctCvnjTYIamk7uXpHI= golang.org/x/mod v0.14.0 h1:dGoOF9QVLYng8IHTm7BAyWqCqSheQ5pYWGhzW00YJr0= golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= -golang.org/x/net v0.18.0 h1:mIYleuAkSbHh0tCv7RvjL3F6ZVbLjq4+R7zbOn3Kokg= -golang.org/x/net v0.18.0/go.mod h1:/czyP5RqHAH4odGYxBJ1qz0+CE5WZ+2j1YgoEo8F2jQ= golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c= golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U= golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE= golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.14.0 h1:Vz7Qs629MkJkGyHxUlRHizWJRG2j8fbQKjELVSNhy7Q= -golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/tools v0.15.0 h1:zdAyfUGbYmuVokhzVmghFl2ZJh5QhcfebBgmVPFYA+8= -golang.org/x/tools v0.15.0/go.mod h1:hpksKq4dtpQWS1uQ61JkdqWM3LscIS6Slf+VVkm+wQk= golang.org/x/tools v0.16.0 h1:GO788SKMRunPIBCXiQyo2AaexLstOrVhuAL5YwsckQM= golang.org/x/tools v0.16.0/go.mod h1:kYVVN6I1mBNoB1OX+noeBjbRk4IUEPa7JJ+TJMEooJ0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/integration/integration_test.go b/integration/integration_test.go index 044aa7013e..0e46a3fc1a 100644 --- a/integration/integration_test.go +++ b/integration/integration_test.go @@ -3,78 +3,260 @@ package integration import ( + "bytes" "context" + "encoding/json" "os" "path/filepath" - "strconv" + "regexp" "syscall" "testing" + "time" + "connectrpc.com/connect" "github.com/alecthomas/assert/v2" + "github.com/alecthomas/errors" + "golang.org/x/exp/maps" "github.com/TBD54566975/ftl/backend/common/exec" "github.com/TBD54566975/ftl/backend/common/log" "github.com/TBD54566975/ftl/backend/common/rpc" + ftlv1 "github.com/TBD54566975/ftl/protos/xyz/block/ftl/v1" "github.com/TBD54566975/ftl/protos/xyz/block/ftl/v1/ftlv1connect" + schemapb "github.com/TBD54566975/ftl/protos/xyz/block/ftl/v1/schema" ) -var binaries = []string{"ftl-controller", "ftl-runner"} +const integrationTestTimeout = time.Second * 60 -type assertion func(client ftlv1connect.ControllerServiceClient) -type asserts []assertion +func TestIntegration(t *testing.T) { + tmpDir := t.TempDir() -type fixture interface { - up() error - down() error -} -type fixtures []fixture + modulesDir := filepath.Join(tmpDir, "modules") -func TestIntegration(t *testing.T) { - binDir := t.TempDir() - logger := log.Configure(os.Stderr, log.Config{Level: log.Warn}) - ctx := log.ContextWithLogger(context.Background(), logger) - for _, binary := range binaries { - t.Logf("Building %s", binary) - err := exec.Command(ctx, log.Debug, "..", "go", "build", "-trimpath", "-ldflags=-s -w -buildid=", "-o", filepath.Join(binDir, binary), "./cmd/"+binary).Run() - assert.NoError(t, err) - } tests := []struct { - name string - extraRunners int - fixtures fixtures - assertions asserts - }{} + name string + assertions assertions + }{ + {name: "DeployTime", + assertions: assertions{ + run("examples", "ftl-go", "deploy", "time"), + deploymentExists("time"), + }}, + {name: "CallTime", assertions: assertions{ + call("time", "time", obj{}, func(t testing.TB, resp obj) { + assert.Equal(t, maps.Keys(resp), []string{"time"}) + }), + }}, + {name: "DeployEchoKotlin", + assertions: assertions{ + run(".", "ftl", "deploy", "examples/kotlin/ftl-module-echo"), + deploymentExists("echo"), + }}, + {name: "CallEchoKotlin", assertions: assertions{ + call("echo", "echo", obj{"name": "Alice"}, func(t testing.TB, resp obj) { + message, ok := resp["message"].(string) + assert.True(t, ok, "message is not a string") + assert.True(t, regexp.MustCompile(`^Hello, Alice!`).MatchString(message), "%q does not match %q", message, `^Hello, Alice!`) + }), + }}, + {name: "InitNewKotlin", assertions: assertions{ + run(".", "ftl", "init", "kotlin", modulesDir, "echo2"), + run(".", "ftl", "init", "kotlin", modulesDir, "echo3"), + }}, + {name: "DeployNewKotlinEcho2", assertions: assertions{ + run(".", "ftl", "deploy", filepath.Join(modulesDir, "ftl-module-echo2")), + deploymentExists("echo2"), + }}, + {name: "CallEcho2", assertions: assertions{ + call("echo2", "echo", obj{"name": "Alice"}, func(t testing.TB, resp obj) { + message, ok := resp["message"].(string) + assert.True(t, ok, "message is not a string") + assert.True(t, regexp.MustCompile(`^Hello, Alice!`).MatchString(message), "%q does not match %q", message, `^Hello, Alice!`) + }), + }}, + {name: "DeployNewKotlinEcho3", assertions: assertions{ + run(".", "ftl", "deploy", filepath.Join(modulesDir, "ftl-module-echo3")), + deploymentExists("echo3"), + }}, + {name: "CallEcho3", assertions: assertions{ + call("echo3", "echo", obj{"name": "Alice"}, func(t testing.TB, resp obj) { + message, ok := resp["message"].(string) + assert.True(t, ok, "message is not a string") + assert.True(t, regexp.MustCompile(`^Hello, Alice!`).MatchString(message), "%q does not match %q", message, `^Hello, Alice!`) + }), + }}, + } + + cwd, err := os.Getwd() + assert.NoError(t, err) + + rootDir := filepath.Join(cwd, "..") + + // Build FTL binary + logger := log.Configure(&logWriter{logger: t}, log.Config{Level: log.Debug}) + ctx := log.ContextWithLogger(context.Background(), logger) + logger.Infof("Building ftl") + binDir := filepath.Join(rootDir, "build", "release") + err = exec.Command(ctx, log.Debug, rootDir, filepath.Join(rootDir, "bin", "bit"), "build/release/ftl", "**/*.jar").Run() + assert.NoError(t, err) + + controller := rpc.Dial(ftlv1connect.NewControllerServiceClient, "http://localhost:8892", log.Debug) + verbs := rpc.Dial(ftlv1connect.NewVerbServiceClient, "http://localhost:8892", log.Debug) + + ctx = startProcess(t, ctx, filepath.Join(binDir, "ftl"), "serve", "--recreate") + + ic := itContext{Context: ctx, rootDir: rootDir, binDir: binDir, controller: controller, verbs: verbs} + + ic.assertWithRetry(t, func(t testing.TB, ic itContext) error { + _, err := ic.controller.Status(ic, connect.NewRequest(&ftlv1.StatusRequest{})) + return errors.WithStack(err) + }) + for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - run(t, ctx, "ftl-controller") - run(t, ctx, "ftl-runner", "--language=go") - client := rpc.Dial(ftlv1connect.NewControllerServiceClient, "localhost:8893", log.Warn) - for i := 0; i < tt.extraRunners; i++ { - run(t, ctx, "ftl-runner", "--language=go", "--endpoint=http://localhost:"+strconv.Itoa(8893+i)) - } - for _, fixture := range tt.fixtures { - err := fixture.up() - assert.NoError(t, err, "fixture failed") - } for _, assertion := range tt.assertions { - assertion(client) - } - for _, fixture := range tt.fixtures { - err := fixture.down() - assert.NoError(t, err, "fixture failed") + ic.assertWithRetry(t, assertion) } }) } } -func run(t testing.TB, ctx context.Context, args ...string) { +type assertion func(t testing.TB, ic itContext) error +type assertions []assertion + +// Assertions + +// Run a command in "dir" which is relative to the root directory of the project. +func run(dir, cmd string, args ...string) assertion { + return func(t testing.TB, ic itContext) error { + path := os.Getenv("PATH") + path = ic.binDir + ":" + path + cmd := exec.Command(ic, log.Debug, filepath.Join(ic.rootDir, dir), cmd, args...) + cmd.Env = append(cmd.Env, os.Environ()...) + cmd.Env = append(cmd.Env, "PATH="+path) + return cmd.Run() + } +} + +func deploymentExists(name string) assertion { + return status(func(t testing.TB, status *ftlv1.StatusResponse) { + for _, deployment := range status.Deployments { + if deployment.Schema.Name == "time" { + return + } + } + t.Fatal("time deployment not found") + }) +} + +// Check status of the controller. +func status(check func(t testing.TB, status *ftlv1.StatusResponse)) assertion { + return func(t testing.TB, ic itContext) error { + status, err := ic.controller.Status(ic, connect.NewRequest(&ftlv1.StatusRequest{})) + if err != nil { + return errors.WithStack(err) + } + check(t, status.Msg) + return nil + } +} + +type obj map[string]any + +func call[Resp any](module, verb string, req obj, onResponse func(t testing.TB, resp Resp)) assertion { + return func(t testing.TB, ic itContext) error { + jreq, err := json.Marshal(req) + assert.NoError(t, err) + + cresp, err := ic.verbs.Call(ic, connect.NewRequest(&ftlv1.CallRequest{ + Verb: &schemapb.VerbRef{Module: module, Name: verb}, + Body: jreq, + })) + if err != nil { + return errors.WithStack(err) + } + + if cresp.Msg.GetError() != nil { + return errors.New(cresp.Msg.GetError().GetMessage()) + } + + var resp Resp + err = json.Unmarshal(cresp.Msg.GetBody(), &resp) + assert.NoError(t, err) + + onResponse(t, resp) + return nil + } +} + +type itContext struct { + context.Context + rootDir string + binDir string // Where "ftl" binary is located. + controller ftlv1connect.ControllerServiceClient + verbs ftlv1connect.VerbServiceClient +} + +func (i itContext) assertWithRetry(t testing.TB, assertion assertion) { + t.Helper() + waitCtx, done := context.WithTimeout(i, integrationTestTimeout) + defer done() + for { + err := assertion(t, i) + if err == nil { + return + } + select { + case <-waitCtx.Done(): + t.Fatalf("Timed out waiting for assertion to pass: %s", err) + + case <-time.After(time.Millisecond * 200): + } + } +} + +// startProcess runs a binary in the background. +func startProcess(t testing.TB, ctx context.Context, args ...string) context.Context { t.Helper() - binDir := t.TempDir() - cmd := exec.Command(ctx, log.Debug, "..", filepath.Join(binDir, args[0]), args...) + ctx, cancel := context.WithCancel(ctx) + cmd := exec.Command(ctx, log.Info, "..", args[0], args[1:]...) err := cmd.Start() assert.NoError(t, err) + terminated := make(chan bool) + go func() { + err := cmd.Wait() + select { + case <-terminated: + default: + cancel() + assert.NoError(t, err) + } + }() t.Cleanup(func() { + close(terminated) err := cmd.Kill(syscall.SIGTERM) assert.NoError(t, err) + cancel() }) + return ctx +} + +type logWriter struct { + logger interface{ Log(...any) } + buffer []byte +} + +func (l *logWriter) Write(p []byte) (n int, err error) { + for { + index := bytes.IndexByte(p, '\n') + if index == -1 { + l.buffer = append(l.buffer, p...) + return n, nil + } else { + l.buffer = append(l.buffer, p[:index]...) + l.logger.Log(string(l.buffer)) + l.buffer = l.buffer[:0] + p = p[index+1:] + } + } } diff --git a/scripts/integration-tests b/scripts/integration-tests deleted file mode 100755 index bbbda46da8..0000000000 --- a/scripts/integration-tests +++ /dev/null @@ -1,94 +0,0 @@ -#!/bin/bash -set -euo pipefail -export PATH="$PWD/build/release:$PATH" - -info() { - echo -e "\033[1;32m$*\033[0m" -} - -error() { - echo -e "\033[1;31m$*\033[0m" - exit 1 -} - -build_release() { - info "Building release" - bit build/release/ftl \ - kotlin-runtime/ftl-runtime/target/ftl-runtime-1.0-SNAPSHOT.jar \ - kotlin-runtime/ftl-generator/target/ftl-generator-1.0-SNAPSHOT.jar \ - build/template/ftl/jars/ftl-runtime.jar -} - -wait_for() { - info "Waiting for $1" - for _ in {1..240}; do - if eval "$2"; then - info "Success!" - return 0 - fi - sleep 1 - done - error "Timed out waiting for $1" -} - -stop_cluster() { - kill %1 - wait -} - -start_cluster() { - info "Starting cluster" - ftl serve --recreate & - wait_for "cluster to become ready" "ftl status" - trap stop_cluster EXIT INT TERM -} - -deploy_echo_kotlin() ( - info "Deploying echo-kotlin" - ftl deploy examples/kotlin/ftl-module-echo -) - -deploy_fresh_kotlin() ( - info "Deploying newly initialised Kotlin module" - IT_MODULES=build/modules - rm -rf "${IT_MODULES}" - ftl init kotlin "${IT_MODULES}" echo2 - ftl init kotlin "${IT_MODULES}" echo3 - cd "${IT_MODULES}" - ftl deploy ftl-module-echo2 - ftl deploy ftl-module-echo3 -) - -deploy_time_go() ( - info "Deploying time" - cd examples - # Pull a supported platforms from the cluster. - platform="$(ftl status | jq -r '(.runners // [])[].labels | "\(.os)-\(.arch)"' | sort | uniq | head -1)" - ftl-go --os "${platform%-*}" --arch "${platform#*-}" deploy time -) - -wait_for_deploys() { - wait_for "deployments to come up" 'ftl status | jq -r "(.routes // [])[].module" | sort | paste -sd " " - | grep -q "echo echo2 echo3 time"' -} - -build_release -start_cluster - -# Cluster is up, start interacting with it. -deploy_time_go -deploy_echo_kotlin -deploy_fresh_kotlin - -wait_for_deploys - -info "Calling echo" -message="$(ftl call echo.echo '{"name": "Alice"}' | jq -r .message)" -[[ "$message" =~ "Hello, Alice! The time is " ]] || error "Unexpected response from echo: $message" - -message="$(ftl call echo2.echo '{"name": "Alice"}' | jq -r .message)" -[[ "$message" =~ "Hello, Alice!" ]] || error "Unexpected response from echo2: $message" - -message="$(ftl call echo3.echo '{"name": "Alice"}' | jq -r .message)" -[[ "$message" =~ "Hello, Alice!" ]] || error "Unexpected response from echo2: $message" - -info "Success!"