diff --git a/internal/buildengine/build_go_integration_test.go b/internal/buildengine/build_go_integration_test.go new file mode 100644 index 0000000000..a47505a10a --- /dev/null +++ b/internal/buildengine/build_go_integration_test.go @@ -0,0 +1,56 @@ +//go:build integration + +package buildengine + +import ( + "os" + "testing" + + "github.com/alecthomas/assert/v2" + + in "github.com/TBD54566975/ftl/internal/integration" +) + +func TestGoBuildClearsBuildDir(t *testing.T) { + file := "another/.ftl/test-clear-build.tmp" + in.Run(t, + in.WithTestDataDir("testdata"), + in.CopyModule("another"), + in.WriteFile(file, []byte{1}), + in.FileExists(file), + in.Build("another"), + in.ExpectError(in.FileExists(file), "no such file"), + ) +} + +func TestExternalType(t *testing.T) { + in.Run(t, + in.WithTestDataDir("testdata"), + in.CopyModule("external"), + in.ExpectError(in.Build("external"), + `unsupported type "time.Month" for field "Month"`, + `unsupported external type "time.Month"; see FTL docs on using external types: tbd54566975.github.io/ftl/docs/reference/externaltypes/`, + `unsupported response type "ftl/external.ExternalResponse"`, + ), + ) +} + +func TestGeneratedTypeRegistry(t *testing.T) { + expected, err := os.ReadFile("testdata/type_registry_main.go") + assert.NoError(t, err) + + file := "other/.ftl/go/main/main.go" + + in.Run(t, + in.WithTestDataDir("testdata"), + // Deploy dependency + in.CopyModule("another"), + in.Deploy("another"), + // Build the module under test + in.CopyModule("other"), + in.ExpectError(in.FileExists(file), "no such file"), + in.Build("other"), + // Validate the generated main.go + in.FileContent(file, string(expected)), + ) +} diff --git a/internal/buildengine/build_go_test.go b/internal/buildengine/build_go_test.go deleted file mode 100644 index 2a5ffe06a4..0000000000 --- a/internal/buildengine/build_go_test.go +++ /dev/null @@ -1,91 +0,0 @@ -package buildengine - -import ( - "os" - "testing" - - "github.com/alecthomas/assert/v2" - - "github.com/TBD54566975/ftl/backend/schema" -) - -func TestGoBuildClearsBuildDir(t *testing.T) { - if testing.Short() { - t.SkipNow() - } - sch := &schema.Schema{ - Modules: []*schema.Module{ - schema.Builtins(), - {Name: "test"}, - }, - } - bctx := buildContext{ - moduleDir: "testdata/another", - buildDir: ".ftl", - sch: sch, - } - testBuildClearsBuildDir(t, bctx) -} - -func TestExternalType(t *testing.T) { - if testing.Short() { - t.SkipNow() - } - bctx := buildContext{ - moduleDir: "testdata/external", - buildDir: ".ftl", - sch: &schema.Schema{}, - } - testBuild(t, bctx, "", "unsupported external type", []assertion{ - assertBuildProtoErrors( - `unsupported type "time.Month" for field "Month"`, - `unsupported external type "time.Month"; see FTL docs on using external types: tbd54566975.github.io/ftl/docs/reference/externaltypes/`, - `unsupported response type "ftl/external.ExternalResponse"`, - ), - }) -} - -func TestGeneratedTypeRegistry(t *testing.T) { - if testing.Short() { - t.SkipNow() - } - sch := &schema.Schema{ - Modules: []*schema.Module{ - {Name: "another", Decls: []schema.Decl{ - &schema.Enum{ - Name: "TypeEnum", - Export: true, - Variants: []*schema.EnumVariant{ - {Name: "A", Value: &schema.TypeValue{Value: &schema.Int{}}}, - {Name: "B", Value: &schema.TypeValue{Value: &schema.String{}}}, - }, - }, - &schema.Enum{ - Name: "SecondTypeEnum", - Export: true, - Variants: []*schema.EnumVariant{ - {Name: "One", Value: &schema.TypeValue{Value: &schema.Int{}}}, - {Name: "Two", Value: &schema.TypeValue{Value: &schema.String{}}}, - }, - }, - &schema.Data{ - Name: "TransitiveTypeEnum", - Export: true, - Fields: []*schema.Field{ - {Name: "TypeEnumRef", Type: &schema.Ref{Name: "SecondTypeEnum", Module: "another"}}, - }, - }, - }}, - }, - } - expected, err := os.ReadFile("testdata/type_registry_main.go") - assert.NoError(t, err) - bctx := buildContext{ - moduleDir: "testdata/other", - buildDir: ".ftl", - sch: sch, - } - testBuild(t, bctx, "", "", []assertion{ - assertGeneratedMain(string(expected)), - }) -} diff --git a/internal/buildengine/build_test.go b/internal/buildengine/build_test.go deleted file mode 100644 index e28a4ee598..0000000000 --- a/internal/buildengine/build_test.go +++ /dev/null @@ -1,167 +0,0 @@ -package buildengine - -import ( - "context" - "os" - "path/filepath" - "testing" - - "github.com/alecthomas/assert/v2" - - "github.com/TBD54566975/ftl/backend/schema" - "github.com/TBD54566975/ftl/internal/log" - "github.com/TBD54566975/ftl/internal/moduleconfig" -) - -type buildContext struct { - moduleDir string - buildDir string - sch *schema.Schema -} - -type assertion func(t testing.TB, bctx buildContext) error - -type mockModifyFilesTransaction struct{} - -func (t *mockModifyFilesTransaction) Begin() error { - return nil -} - -func (t *mockModifyFilesTransaction) ModifiedFiles(paths ...string) error { - return nil -} - -func (t *mockModifyFilesTransaction) End() error { - return nil -} - -func testBuild( - t *testing.T, - bctx buildContext, - expectedGeneratStubsErrMsg string, // emptystr if no error expected - expectedBuildErrMsg string, // emptystr if no error expected - assertions []assertion, -) { - t.Helper() - ctx := log.ContextWithLogger(context.Background(), log.Configure(os.Stderr, log.Config{})) - abs, err := filepath.Abs(bctx.moduleDir) - assert.NoError(t, err, "Error getting absolute path for module directory") - module, err := LoadModule(abs) - assert.NoError(t, err) - - projectRootDir := t.TempDir() - - configs := []moduleconfig.ModuleConfig{} - if bctx.moduleDir != "" { - config, err := moduleconfig.LoadModuleConfig(bctx.moduleDir) - assert.NoError(t, err, "Error loading project config") - configs = append(configs, config) - } - - // generate stubs to create the shared modules directory - err = GenerateStubs(ctx, projectRootDir, bctx.sch.Modules, configs) - if len(expectedGeneratStubsErrMsg) > 0 { - assert.Error(t, err) - assert.Contains(t, err.Error(), expectedGeneratStubsErrMsg) - } else { - assert.NoError(t, err) - - err = Build(ctx, projectRootDir, bctx.sch, module, &mockModifyFilesTransaction{}) - if len(expectedBuildErrMsg) > 0 { - assert.Error(t, err) - assert.Contains(t, err.Error(), expectedBuildErrMsg) - } else { - assert.NoError(t, err) - } - } - - for _, a := range assertions { - err = a(t, bctx) - assert.NoError(t, err) - } - - err = os.RemoveAll(filepath.Join(bctx.moduleDir, bctx.buildDir)) - assert.NoError(t, err, "Error removing build directory") -} - -func testBuildClearsBuildDir(t *testing.T, bctx buildContext) { - t.Helper() - ctx := log.ContextWithLogger(context.Background(), log.Configure(os.Stderr, log.Config{})) - abs, err := filepath.Abs(bctx.moduleDir) - assert.NoError(t, err, "Error getting absolute path for module directory") - - projectRoot := t.TempDir() - - // generate stubs to create the shared modules directory - err = GenerateStubs(ctx, projectRoot, bctx.sch.Modules, []moduleconfig.ModuleConfig{{Dir: bctx.moduleDir, Language: "go"}}) - assert.NoError(t, err) - - // build to generate the build directory - module, err := LoadModule(abs) - assert.NoError(t, err) - err = Build(ctx, projectRoot, bctx.sch, module, &mockModifyFilesTransaction{}) - assert.NoError(t, err) - - // create a temporary file in the build directory - buildDir := filepath.Join(bctx.moduleDir, bctx.buildDir) - tempFile, err := os.Create(filepath.Join(buildDir, "test-clear-build.tmp")) - assert.NoError(t, err, "Error creating temporary file in module directory") - tempFile.Close() - - // build to clear the old build directory - module, err = LoadModule(abs) - assert.NoError(t, err) - err = Build(ctx, projectRoot, bctx.sch, module, &mockModifyFilesTransaction{}) - assert.NoError(t, err) - - // ensure the temporary file was removed - _, err = os.Stat(filepath.Join(buildDir, "test-clear-build.tmp")) - assert.Error(t, err, "Build directory was not removed") -} - -func assertGeneratedModule(generatedModulePath string, expectedContent string) assertion { - return func(t testing.TB, bctx buildContext) error { - t.Helper() - target := filepath.Join(bctx.moduleDir, bctx.buildDir) - output := filepath.Join(target, generatedModulePath) - - fileContent, err := os.ReadFile(output) - assert.NoError(t, err) - assert.Equal(t, expectedContent, string(fileContent)) - return nil - } -} - -func assertGeneratedMain(expectedContent string) assertion { - return func(t testing.TB, bctx buildContext) error { - t.Helper() - output := filepath.Join(bctx.moduleDir, bctx.buildDir, "go/main/main.go") - fileContent, err := os.ReadFile(output) - assert.NoError(t, err) - assert.Equal(t, expectedContent, string(fileContent)) - return nil - } -} - -func assertBuildProtoErrors(msgs ...string) assertion { - return func(t testing.TB, bctx buildContext) error { - t.Helper() - config, err := moduleconfig.LoadModuleConfig(bctx.moduleDir) - assert.NoError(t, err, "Error loading module config") - errorList, err := loadProtoErrors(config.Abs()) - assert.NoError(t, err, "Error loading proto errors") - - expected := make([]*schema.Error, 0, len(msgs)) - for _, msg := range msgs { - expected = append(expected, &schema.Error{Msg: msg, Level: schema.ERROR}) - } - - // normalize results - for _, e := range errorList.Errors { - e.EndColumn = 0 - } - - assert.Equal(t, errorList.Errors, expected, assert.Exclude[schema.Position]()) - return nil - } -} diff --git a/internal/buildengine/testdata/type_registry_main.go b/internal/buildengine/testdata/type_registry_main.go index 61a8d30ab8..0eb77e2b08 100644 --- a/internal/buildengine/testdata/type_registry_main.go +++ b/internal/buildengine/testdata/type_registry_main.go @@ -35,7 +35,7 @@ func init() { } func main() { - verbConstructor := server.NewUserVerbServer("ftl", "other", + verbConstructor := server.NewUserVerbServer("integration", "other", server.HandleCall(other.Echo), ) plugin.Start(context.Background(), "other", verbConstructor, ftlv1connect.VerbServiceName, ftlv1connect.NewVerbServiceHandler)