Skip to content

Commit

Permalink
Merge pull request #106 from golingon/terragen-compile-test
Browse files Browse the repository at this point in the history
add compile checks to terragen tests
  • Loading branch information
veggiemonk committed Sep 28, 2024
2 parents 31feb97 + 5239084 commit 992efb9
Show file tree
Hide file tree
Showing 2 changed files with 267 additions and 93 deletions.
291 changes: 198 additions & 93 deletions pkg/terragen/gowrapper_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,18 +7,22 @@ import (
"context"
"encoding/json"
"flag"
"fmt"
"log/slog"
"os"
"os/exec"
"path/filepath"
"slices"
"testing"
"time"

"github.com/golingon/lingon/pkg/internal/terrajen"
tu "github.com/golingon/lingon/pkg/testutil"
tfjson "github.com/hashicorp/terraform-json"
"golang.org/x/tools/txtar"
)

var update = flag.Bool("update", false, "update golden files")
var goldenTestDir = filepath.Join("testdata", "golden")

type ProviderTestCase struct {
Name string
Expand All @@ -31,116 +35,139 @@ type ProviderTestCase struct {
FilterDataSources []string
}

// TestGenerateProvider tests the generation of Terraform provider schemas into
// Go code.
//
// When using the -update flag, it updates the golden files which are used as a
// baseline for detecting drift in the generated code.
// It is quite challenging to verify the correctness of the generated code,
// and it is out of scope of this pkg and test.
func TestGenerateProvider(t *testing.T) {
goldenTestDir := filepath.Join("testdata", "golden")
var providerTests = []ProviderTestCase{
{
Name: "aws_emr_cluster",
ProviderName: "aws",
ProviderSource: "hashicorp/aws",
ProviderVersion: "5.44.0",

tests := []ProviderTestCase{
{
Name: "aws_emr_cluster",
ProviderName: "aws",
ProviderSource: "hashicorp/aws",
ProviderVersion: "5.44.0",
FilterResources: []string{"aws_emr_cluster"},
FilterDataSources: []string{"aws_emr_cluster"},
},
{
Name: "aws_iam_role",
ProviderName: "aws",
ProviderSource: "hashicorp/aws",
ProviderVersion: "5.44.0",

FilterResources: []string{"aws_emr_cluster"},
FilterDataSources: []string{"aws_emr_cluster"},
},
{
Name: "aws_iam_role",
ProviderName: "aws",
ProviderSource: "hashicorp/aws",
ProviderVersion: "5.44.0",
FilterResources: []string{"aws_iam_role"},
FilterDataSources: []string{"aws_iam_role"},
},
{
Name: "aws_securitylake_subscriber",
ProviderName: "aws",
ProviderSource: "hashicorp/aws",
ProviderVersion: "5.44.0",

FilterResources: []string{"aws_iam_role"},
FilterDataSources: []string{"aws_iam_role"},
},
{
Name: "aws_securitylake_subscriber",
ProviderName: "aws",
ProviderSource: "hashicorp/aws",
ProviderVersion: "5.44.0",
FilterResources: []string{"aws_securitylake_subscriber"},
FilterDataSources: []string{"aws_securitylake_subscriber"},
},
{
Name: "aws_globalaccelerator_cross_account_attachment",
ProviderName: "aws",
ProviderSource: "hashicorp/aws",
ProviderVersion: "5.47.0",

FilterResources: []string{"aws_securitylake_subscriber"},
FilterDataSources: []string{"aws_securitylake_subscriber"},
FilterResources: []string{
"aws_globalaccelerator_cross_account_attachment",
},
{
Name: "aws_globalaccelerator_cross_account_attachment",
ProviderName: "aws",
ProviderSource: "hashicorp/aws",
ProviderVersion: "5.47.0",

FilterResources: []string{
"aws_globalaccelerator_cross_account_attachment",
},
FilterDataSources: []string{
"aws_globalaccelerator_cross_account_attachment",
},
FilterDataSources: []string{
"aws_globalaccelerator_cross_account_attachment",
},
}
},
}

func TestMain(m *testing.M) {
update := flag.Bool("update", false, "update golden files")
flag.Parse()
if *update {
t.Log("running update")
// Generate "golden" files. Start be deleting the directory.
err := os.RemoveAll(goldenTestDir)
tu.AssertNoError(t, err, "removing golden test dir")

for _, test := range tests {
ctx := context.Background()
ps, err := GenerateProviderSchema(
ctx, Provider{
Name: test.ProviderName,
Source: test.ProviderSource,
Version: test.ProviderVersion,
},
)
tu.AssertNoError(t, err, "generating provider schema:", test.Name)
slog.Info("updating golden files and skipping tests")
if err := generatedGoldenFiles(); err != nil {
slog.Error("generating golden files", "error", err)
os.Exit(1)
}
// Skip running tests if updating golden files.
return
}
os.Exit(m.Run())
}

// Filter resources and data sources.
for rName := range ps.ResourceSchemas {
if !slices.Contains(test.FilterResources, rName) {
delete(ps.ResourceSchemas, rName)
}
}
for dName := range ps.DataSourceSchemas {
if !slices.Contains(test.FilterDataSources, dName) {
delete(ps.DataSourceSchemas, dName)
}
}
func generatedGoldenFiles() error {
if err := os.RemoveAll(goldenTestDir); err != nil {
return fmt.Errorf("removing golden test dir: %w", err)
}

providerGenerator := terrajen.ProviderGenerator{
GeneratedPackageLocation: "out",
ProviderName: test.ProviderName,
ProviderSource: test.ProviderSource,
ProviderVersion: test.ProviderVersion,
for _, test := range providerTests {
ctx := context.Background()
ps, err := GenerateProviderSchema(
ctx, Provider{
Name: test.ProviderName,
Source: test.ProviderSource,
Version: test.ProviderVersion,
},
)
if err != nil {
return fmt.Errorf("generating provider schema: %w", err)
}

// Filter resources and data sources.
for rName := range ps.ResourceSchemas {
if !slices.Contains(test.FilterResources, rName) {
delete(ps.ResourceSchemas, rName)
}
}
for dName := range ps.DataSourceSchemas {
if !slices.Contains(test.FilterDataSources, dName) {
delete(ps.DataSourceSchemas, dName)
}
}

ar, err := generateProviderTxtar(providerGenerator, ps)
tu.AssertNoError(t, err, "generating provider txtar")
providerGenerator := terrajen.ProviderGenerator{
GeneratedPackageLocation: "out",
ProviderName: test.ProviderName,
ProviderSource: test.ProviderSource,
ProviderVersion: test.ProviderVersion,
}

testDir := filepath.Join(goldenTestDir, test.Name)
err = os.MkdirAll(testDir, os.ModePerm)
tu.AssertNoError(t, err, "creating golden test dir: ", testDir)
ar, err := generateProviderTxtar(providerGenerator, ps)
if err != nil {
return fmt.Errorf("generating provider txtar: %w", err)
}

schemaPath := filepath.Join(testDir, "schema.json")
schemaFile, err := os.Create(schemaPath)
tu.AssertNoError(t, err, "creating schema file")
err = json.NewEncoder(schemaFile).Encode(ps)
tu.AssertNoError(t, err, "writing schema file")
testDir := filepath.Join(goldenTestDir, test.Name)
if err := os.MkdirAll(testDir, os.ModePerm); err != nil {
return fmt.Errorf("creating golden test dir: %w", err)
}

txtarPath := filepath.Join(testDir, "provider.txtar")
err = os.WriteFile(txtarPath, txtar.Format(ar), 0o644)
tu.AssertNoError(t, err, "writing txtar file")
schemaPath := filepath.Join(testDir, "schema.json")
schemaFile, err := os.Create(schemaPath)
if err != nil {
return fmt.Errorf("creating schema file: %w", err)
}
if err := json.NewEncoder(schemaFile).Encode(ps); err != nil {
return fmt.Errorf("writing schema file: %w", err)
}

t.SkipNow()
txtarPath := filepath.Join(testDir, "provider.txtar")
if err := os.WriteFile(txtarPath, txtar.Format(ar), 0o644); err != nil {
return fmt.Errorf("writing txtar file: %w", err)
}
}
return nil
}

for _, test := range tests {
// TestGenerateProvider tests that the generated Go code for a Terraform
// provider matches the golden tests.
// This is to ensure that the generated code is consistent and does not change,
// unless we want it to.
//
// It is quite challenging to verify the correctness of the generated code,
// and it is out of scope of this pkg and test.
func TestGenerateProvider(t *testing.T) {
goldenTestDir := filepath.Join("testdata", "golden")

for _, test := range providerTests {
t.Run(test.Name, func(t *testing.T) {
schemaPath := filepath.Join(goldenTestDir, test.Name, "schema.json")
schemaFile, err := os.Open(schemaPath)
Expand Down Expand Up @@ -224,3 +251,81 @@ func TestParseProvider(t *testing.T) {
)
}
}

var (
// gotype is for linting code
gotypeCmd = "golang.org/x/tools/cmd/gotype"
gotypeVersion = "@v0.25.0"
gotype = gotypeCmd + gotypeVersion
)

// TestCompileGenGoCode tests that the generated Go code compiles.
// This test is slow because compiles the generated Go code using `gotype`.
// https://pkg.go.dev/golang.org/x/tools/cmd/gotype
//
// This test has a dependency on `gotype`. It uses `go run ...` to execute
// `gotype`.
func TestCompileGenGoCode(t *testing.T) {
ctx := tu.WithTimeout(t, context.Background(), time.Minute*10)
for _, test := range providerTests {
t.Run(test.Name, func(t *testing.T) {
outDir := filepath.Join("out", test.Name)
schemaPath := filepath.Join(goldenTestDir, test.Name, "schema.json")
schemaFile, err := os.Open(schemaPath)
tu.AssertNoError(t, err, "opening schema file")
var providerSchema tfjson.ProviderSchema
err = json.NewDecoder(schemaFile).Decode(&providerSchema)
tu.AssertNoError(t, err, "decoding schema file")

if err := GenerateGoCode(
GenerateGoArgs{
ProviderName: test.ProviderName,
ProviderSource: test.ProviderSource,
ProviderVersion: test.ProviderVersion,
OutDir: outDir,
Force: false,
Clean: true,
},
&providerSchema,
); err != nil {
tu.AssertNoError(t, err)
}
dirs := dirsForProvider(outDir, &providerSchema)
for _, dir := range dirs {
t.Logf("running gotype on %s", dir)
goTypeExec(t, ctx, dir)
}
})
}
}

// goTypeExec executes `gotype` in the given directory.
func goTypeExec(t *testing.T, ctx context.Context, dir string) {
cmd := exec.CommandContext(ctx, "go", "run", gotype, "-v", ".")
cmd.Dir = dir
out, err := cmd.CombinedOutput()
if err != nil {
t.Logf("gotype output:\n%s", string(out))
tu.AssertNoError(t, err)
}
}

// dirsForProvider returns a list of directories that contain generated Go code
// based on a terraform provider.
func dirsForProvider(root string, schema *tfjson.ProviderSchema) []string {
dirsMap := map[string]struct{}{
// Include root directory because it contains the provider.go file.
root: {},
}
for key := range schema.ResourceSchemas {
dirsMap[filepath.Join(root, key)] = struct{}{}
}
for key := range schema.DataSourceSchemas {
dirsMap[filepath.Join(root, key)] = struct{}{}
}
dirs := make([]string, 0, len(dirsMap))
for dir := range dirsMap {
dirs = append(dirs, dir)
}
return dirs
}
Loading

0 comments on commit 992efb9

Please sign in to comment.