diff --git a/internal/command/apply_test.go b/internal/command/apply_test.go index dba6599479..ef4484c401 100644 --- a/internal/command/apply_test.go +++ b/internal/command/apply_test.go @@ -3,11 +3,9 @@ package command import ( "bytes" "context" - "encoding/json" "fmt" "io/ioutil" "os" - "path" "path/filepath" "reflect" "strings" @@ -21,7 +19,6 @@ import ( "github.com/zclconf/go-cty/cty" "github.com/hashicorp/terraform/internal/addrs" - "github.com/hashicorp/terraform/internal/command/views" "github.com/hashicorp/terraform/internal/configs/configschema" "github.com/hashicorp/terraform/internal/plans" "github.com/hashicorp/terraform/internal/providers" @@ -29,7 +26,6 @@ import ( "github.com/hashicorp/terraform/internal/states/statemgr" "github.com/hashicorp/terraform/internal/terraform" "github.com/hashicorp/terraform/internal/tfdiags" - tfversion "github.com/hashicorp/terraform/version" ) func TestApply(t *testing.T) { @@ -2055,81 +2051,7 @@ func TestApply_jsonGoldenReference(t *testing.T) { t.Fatal("state should not be nil") } - // Load the golden reference fixture - wantFile, err := os.Open(path.Join(testFixturePath("apply"), "output.jsonlog")) - if err != nil { - t.Fatalf("failed to open output file: %s", err) - } - defer wantFile.Close() - wantBytes, err := ioutil.ReadAll(wantFile) - if err != nil { - t.Fatalf("failed to read output file: %s", err) - } - want := string(wantBytes) - - got := output.Stdout() - - // Split the output and the reference into lines so that we can compare - // messages - got = strings.TrimSuffix(got, "\n") - gotLines := strings.Split(got, "\n") - - want = strings.TrimSuffix(want, "\n") - wantLines := strings.Split(want, "\n") - - if len(gotLines) != len(wantLines) { - t.Errorf("unexpected number of log lines: got %d, want %d", len(gotLines), len(wantLines)) - } - - // Verify that the log starts with a version message - type versionMessage struct { - Level string `json:"@level"` - Message string `json:"@message"` - Type string `json:"type"` - Terraform string `json:"terraform"` - UI string `json:"ui"` - } - var gotVersion versionMessage - if err := json.Unmarshal([]byte(gotLines[0]), &gotVersion); err != nil { - t.Errorf("failed to unmarshal version line: %s\n%s", err, gotLines[0]) - } - wantVersion := versionMessage{ - "info", - fmt.Sprintf("Terraform %s", tfversion.String()), - "version", - tfversion.String(), - views.JSON_UI_VERSION, - } - if !cmp.Equal(wantVersion, gotVersion) { - t.Errorf("unexpected first message:\n%s", cmp.Diff(wantVersion, gotVersion)) - } - - // Compare the rest of the lines against the golden reference - var gotLineMaps []map[string]interface{} - for i, line := range gotLines[1:] { - index := i + 1 - var gotMap map[string]interface{} - if err := json.Unmarshal([]byte(line), &gotMap); err != nil { - t.Errorf("failed to unmarshal got line %d: %s\n%s", index, err, gotLines[index]) - } - if _, ok := gotMap["@timestamp"]; !ok { - t.Errorf("missing @timestamp field in log: %s", gotLines[index]) - } - delete(gotMap, "@timestamp") - gotLineMaps = append(gotLineMaps, gotMap) - } - var wantLineMaps []map[string]interface{} - for i, line := range wantLines[1:] { - index := i + 1 - var wantMap map[string]interface{} - if err := json.Unmarshal([]byte(line), &wantMap); err != nil { - t.Errorf("failed to unmarshal want line %d: %s\n%s", index, err, gotLines[index]) - } - wantLineMaps = append(wantLineMaps, wantMap) - } - if diff := cmp.Diff(wantLineMaps, gotLineMaps); diff != "" { - t.Errorf("wrong output lines\n%s", diff) - } + checkGoldenReference(t, output, "apply") } func TestApply_warnings(t *testing.T) { diff --git a/internal/command/command_test.go b/internal/command/command_test.go index 50ab641cb3..7fb593d4a0 100644 --- a/internal/command/command_test.go +++ b/internal/command/command_test.go @@ -7,12 +7,14 @@ import ( "encoding/base64" "encoding/json" "fmt" + "github.com/google/go-cmp/cmp" "io" "io/ioutil" "net/http" "net/http/httptest" "os" "os/exec" + "path" "path/filepath" "strings" "syscall" @@ -1052,3 +1054,92 @@ func testView(t *testing.T) (*views.View, func(*testing.T) *terminal.TestOutput) streams, done := terminal.StreamsForTesting(t) return views.NewView(streams), done } + +// checkGoldenReference compares the given test output with a known "golden" output log +// located under the specified fixture path. +// +// If any of these tests fail, please communicate with Terraform Cloud folks before resolving, +// as changes to UI output may also affect the behavior of Terraform Cloud's structured run output. +func checkGoldenReference(t *testing.T, output *terminal.TestOutput, fixturePathName string) { + t.Helper() + + // Load the golden reference fixture + wantFile, err := os.Open(path.Join(testFixturePath(fixturePathName), "output.jsonlog")) + if err != nil { + t.Fatalf("failed to open output file: %s", err) + } + defer wantFile.Close() + wantBytes, err := ioutil.ReadAll(wantFile) + if err != nil { + t.Fatalf("failed to read output file: %s", err) + } + want := string(wantBytes) + + got := output.Stdout() + + // Split the output and the reference into lines so that we can compare + // messages + got = strings.TrimSuffix(got, "\n") + gotLines := strings.Split(got, "\n") + + want = strings.TrimSuffix(want, "\n") + wantLines := strings.Split(want, "\n") + + if len(gotLines) != len(wantLines) { + t.Errorf("unexpected number of log lines: got %d, want %d\n"+ + "NOTE: This failure may indicate a UI change affecting the behavior of structured run output on TFC.\n"+ + "Please communicate with Terraform Cloud team before resolving", len(gotLines), len(wantLines)) + } + + // Verify that the log starts with a version message + type versionMessage struct { + Level string `json:"@level"` + Message string `json:"@message"` + Type string `json:"type"` + Terraform string `json:"terraform"` + UI string `json:"ui"` + } + var gotVersion versionMessage + if err := json.Unmarshal([]byte(gotLines[0]), &gotVersion); err != nil { + t.Errorf("failed to unmarshal version line: %s\n%s", err, gotLines[0]) + } + wantVersion := versionMessage{ + "info", + fmt.Sprintf("Terraform %s", version.String()), + "version", + version.String(), + views.JSON_UI_VERSION, + } + if !cmp.Equal(wantVersion, gotVersion) { + t.Errorf("unexpected first message:\n%s", cmp.Diff(wantVersion, gotVersion)) + } + + // Compare the rest of the lines against the golden reference + var gotLineMaps []map[string]interface{} + for i, line := range gotLines[1:] { + index := i + 1 + var gotMap map[string]interface{} + if err := json.Unmarshal([]byte(line), &gotMap); err != nil { + t.Errorf("failed to unmarshal got line %d: %s\n%s", index, err, gotLines[index]) + } + if _, ok := gotMap["@timestamp"]; !ok { + t.Errorf("missing @timestamp field in log: %s", gotLines[index]) + } + delete(gotMap, "@timestamp") + gotLineMaps = append(gotLineMaps, gotMap) + } + var wantLineMaps []map[string]interface{} + for i, line := range wantLines[1:] { + index := i + 1 + var wantMap map[string]interface{} + if err := json.Unmarshal([]byte(line), &wantMap); err != nil { + t.Errorf("failed to unmarshal want line %d: %s\n%s", index, err, gotLines[index]) + } + wantLineMaps = append(wantLineMaps, wantMap) + } + if diff := cmp.Diff(wantLineMaps, gotLineMaps); diff != "" { + t.Errorf("wrong output lines\n%s\n"+ + "NOTE: This failure may indicate a UI change affecting the behavior of structured run output on TFC.\n"+ + "Please communicate with Terraform Cloud team before resolving", diff) + } +} diff --git a/internal/command/plan_test.go b/internal/command/plan_test.go index 555771aba3..9e145e6bf3 100644 --- a/internal/command/plan_test.go +++ b/internal/command/plan_test.go @@ -700,6 +700,22 @@ func TestPlan_providerArgumentUnset(t *testing.T) { }, }, }, + DataSources: map[string]providers.Schema{ + "test_data_source": { + Block: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "id": { + Type: cty.String, + Required: true, + }, + "valid": { + Type: cty.Bool, + Computed: true, + }, + }, + }, + }, + }, } view, done := testView(t) c := &PlanCommand{ @@ -1382,6 +1398,33 @@ func TestPlan_warnings(t *testing.T) { }) } +func TestPlan_jsonGoldenReference(t *testing.T) { + // Create a temporary working directory that is empty + td := t.TempDir() + testCopyDir(t, testFixturePath("plan"), td) + defer testChdir(t, td)() + + p := planFixtureProvider() + view, done := testView(t) + c := &PlanCommand{ + Meta: Meta{ + testingOverrides: metaOverridesForProvider(p), + View: view, + }, + } + + args := []string{ + "-json", + } + code := c.Run(args) + output := done(t) + if code != 0 { + t.Fatalf("bad: %d\n\n%s", code, output.Stderr()) + } + + checkGoldenReference(t, output, "plan") +} + // planFixtureSchema returns a schema suitable for processing the // configuration in testdata/plan . This schema should be // assigned to a mock provider named "test". @@ -1408,6 +1451,22 @@ func planFixtureSchema() *providers.GetProviderSchemaResponse { }, }, }, + DataSources: map[string]providers.Schema{ + "test_data_source": { + Block: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "id": { + Type: cty.String, + Required: true, + }, + "valid": { + Type: cty.Bool, + Computed: true, + }, + }, + }, + }, + }, } } @@ -1423,6 +1482,14 @@ func planFixtureProvider() *terraform.MockProvider { PlannedState: req.ProposedNewState, } } + p.ReadDataSourceFn = func(req providers.ReadDataSourceRequest) providers.ReadDataSourceResponse { + return providers.ReadDataSourceResponse{ + State: cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("zzzzz"), + "valid": cty.BoolVal(true), + }), + } + } return p } @@ -1456,6 +1523,14 @@ func planVarsFixtureProvider() *terraform.MockProvider { PlannedState: req.ProposedNewState, } } + p.ReadDataSourceFn = func(req providers.ReadDataSourceRequest) providers.ReadDataSourceResponse { + return providers.ReadDataSourceResponse{ + State: cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("zzzzz"), + "valid": cty.BoolVal(true), + }), + } + } return p } @@ -1475,6 +1550,14 @@ func planWarningsFixtureProvider() *terraform.MockProvider { PlannedState: req.ProposedNewState, } } + p.ReadDataSourceFn = func(req providers.ReadDataSourceRequest) providers.ReadDataSourceResponse { + return providers.ReadDataSourceResponse{ + State: cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("zzzzz"), + "valid": cty.BoolVal(true), + }), + } + } return p } diff --git a/internal/command/testdata/plan/main.tf b/internal/command/testdata/plan/main.tf index 070388113e..7b30915731 100644 --- a/internal/command/testdata/plan/main.tf +++ b/internal/command/testdata/plan/main.tf @@ -4,6 +4,10 @@ resource "test_instance" "foo" { # This is here because at some point it caused a test failure network_interface { device_index = 0 - description = "Main network interface" + description = "Main network interface" } } + +data "test_data_source" "a" { + id = "zzzzz" +} diff --git a/internal/command/testdata/plan/output.jsonlog b/internal/command/testdata/plan/output.jsonlog new file mode 100644 index 0000000000..d823fbf29c --- /dev/null +++ b/internal/command/testdata/plan/output.jsonlog @@ -0,0 +1,5 @@ +{"@level":"info","@message":"Terraform 1.3.0-dev","@module":"terraform.ui","terraform":"1.3.0-dev","type":"version","ui":"1.0"} +{"@level":"info","@message":"data.test_data_source.a: Refreshing...","@module":"terraform.ui","hook":{"resource":{"addr":"data.test_data_source.a","module":"","resource":"data.test_data_source.a","implied_provider":"test","resource_type":"test_data_source","resource_name":"a","resource_key":null},"action":"read"},"type":"apply_start"} +{"@level":"info","@message":"data.test_data_source.a: Refresh complete after 0s [id=zzzzz]","@module":"terraform.ui","hook":{"resource":{"addr":"data.test_data_source.a","module":"","resource":"data.test_data_source.a","implied_provider":"test","resource_type":"test_data_source","resource_name":"a","resource_key":null},"action":"read","id_key":"id","id_value":"zzzzz","elapsed_seconds":0},"type":"apply_complete"} +{"@level":"info","@message":"test_instance.foo: Plan to create","@module":"terraform.ui","change":{"resource":{"addr":"test_instance.foo","module":"","resource":"test_instance.foo","implied_provider":"test","resource_type":"test_instance","resource_name":"foo","resource_key":null},"action":"create"},"type":"planned_change"} +{"@level":"info","@message":"Plan: 1 to add, 0 to change, 0 to destroy.","@module":"terraform.ui","changes":{"add":1,"change":0,"remove":0,"operation":"plan"},"type":"change_summary"}