Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Testing framework] Implement JSON view functionality for test command #33400

Merged
merged 1 commit into from
Jun 28, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions internal/command/views/json/message_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,4 +28,11 @@ const (
MessageProvisionErrored MessageType = "provision_errored"
MessageRefreshStart MessageType = "refresh_start"
MessageRefreshComplete MessageType = "refresh_complete"

// Test messages
MessageTestAbstract MessageType = "test_abstract"
MessageTestFile MessageType = "test_file"
MessageTestRun MessageType = "test_run"
MessageTestSummary MessageType = "test_summary"
MessageTestCleanup MessageType = "test_cleanup"
)
43 changes: 43 additions & 0 deletions internal/command/views/json/test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package json

import (
"strings"

"github.com/hashicorp/terraform/internal/moduletest"
)

type TestSuiteAbstract map[string][]string

type TestStatus string

type TestFileStatus struct {
Path string `json:"path"`
Status TestStatus `json:"status"`
}

type TestRunStatus struct {
Path string `json:"path"`
Run string `json:"run"`
Status TestStatus `json:"status"`
}

type TestSuiteSummary struct {
Status TestStatus `json:"status"`
Passed int `json:"passed"`
Failed int `json:"failed"`
Errored int `json:"errored"`
Skipped int `json:"skipped"`
}

type TestFileCleanup struct {
FailedResources []TestFailedResource `json:"failed_resources"`
}

type TestFailedResource struct {
Instance string `json:"instance"`
DeposedKey string `json:"deposed_key,omitempty"`
}

func ToTestStatus(status moduletest.Status) TestStatus {
return TestStatus(strings.ToLower(status.String()))
}
19 changes: 8 additions & 11 deletions internal/command/views/json_view.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"fmt"

"github.com/hashicorp/go-hclog"

"github.com/hashicorp/terraform/internal/command/views/json"
"github.com/hashicorp/terraform/internal/tfdiags"
tfversion "github.com/hashicorp/terraform/version"
Expand Down Expand Up @@ -69,23 +70,19 @@ func (v *JSONView) StateDump(state string) {
)
}

func (v *JSONView) Diagnostics(diags tfdiags.Diagnostics) {
func (v *JSONView) Diagnostics(diags tfdiags.Diagnostics, metadata ...interface{}) {
sources := v.view.configSources()
for _, diag := range diags {
diagnostic := json.NewDiagnostic(diag, sources)

args := []interface{}{"type", json.MessageDiagnostic, "diagnostic", diagnostic}
args = append(args, metadata...)

switch diag.Severity() {
case tfdiags.Warning:
v.log.Warn(
fmt.Sprintf("Warning: %s", diag.Description().Summary),
"type", json.MessageDiagnostic,
"diagnostic", diagnostic,
)
v.log.Warn(fmt.Sprintf("Warning: %s", diag.Description().Summary), args...)
default:
v.log.Error(
fmt.Sprintf("Error: %s", diag.Description().Summary),
"type", json.MessageDiagnostic,
"diagnostic", diagnostic,
)
v.log.Error(fmt.Sprintf("Error: %s", diag.Description().Summary), args...)
}
}
}
Expand Down
48 changes: 48 additions & 0 deletions internal/command/views/json_view_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"time"

"github.com/google/go-cmp/cmp"

"github.com/hashicorp/terraform/internal/addrs"
viewsjson "github.com/hashicorp/terraform/internal/command/views/json"
"github.com/hashicorp/terraform/internal/plans"
Expand Down Expand Up @@ -104,6 +105,53 @@ func TestJSONView_Diagnostics(t *testing.T) {
testJSONViewOutputEquals(t, done(t).Stdout(), want)
}

func TestJSONView_DiagnosticsWithMetadata(t *testing.T) {
streams, done := terminal.StreamsForTesting(t)
jv := NewJSONView(NewView(streams))

var diags tfdiags.Diagnostics
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Warning,
`Improper use of "less"`,
`You probably mean "10 buckets or fewer"`,
))
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,
"Unusually stripey cat detected",
"Are you sure this random_pet isn't a cheetah?",
))

jv.Diagnostics(diags, "@meta", "extra_info")

want := []map[string]interface{}{
{
"@level": "warn",
"@message": `Warning: Improper use of "less"`,
"@module": "terraform.ui",
"type": "diagnostic",
"diagnostic": map[string]interface{}{
"severity": "warning",
"summary": `Improper use of "less"`,
"detail": `You probably mean "10 buckets or fewer"`,
},
"@meta": "extra_info",
},
{
"@level": "error",
"@message": "Error: Unusually stripey cat detected",
"@module": "terraform.ui",
"type": "diagnostic",
"diagnostic": map[string]interface{}{
"severity": "error",
"summary": "Unusually stripey cat detected",
"detail": "Are you sure this random_pet isn't a cheetah?",
},
"@meta": "extra_info",
},
}
testJSONViewOutputEquals(t, done(t).Stdout(), want)
}

func TestJSONView_PlannedChange(t *testing.T) {
streams, done := terminal.StreamsForTesting(t)
jv := NewJSONView(NewView(streams))
Expand Down
195 changes: 190 additions & 5 deletions internal/command/views/test.go
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
package views

import (
"bytes"
"fmt"

"github.com/mitchellh/colorstring"

"github.com/hashicorp/terraform/internal/command/arguments"
"github.com/hashicorp/terraform/internal/command/views/json"
"github.com/hashicorp/terraform/internal/moduletest"
"github.com/hashicorp/terraform/internal/states"
"github.com/hashicorp/terraform/internal/tfdiags"
)

// Test renders outputs for test executions.
Expand All @@ -26,14 +30,22 @@ type Test interface {
File(file *moduletest.File)

// Run prints out the summary for a single test run block.
Run(run *moduletest.Run)
Run(run *moduletest.Run, file *moduletest.File)

// DestroySummary prints out the summary of the destroy step of each test
// file. If everything goes well, this should be empty.
DestroySummary(diags tfdiags.Diagnostics, file *moduletest.File, state *states.State)

// Diagnostics prints out the provided diagnostics.
Diagnostics(run *moduletest.Run, file *moduletest.File, diags tfdiags.Diagnostics)
}

func NewTest(vt arguments.ViewType, view *View) Test {
switch vt {
case arguments.ViewJSON:
// TODO(liamcervante): Add support for JSON outputs.
panic("not supported yet")
return &TestJSON{
view: NewJSONView(view),
}
case arguments.ViewHuman:
return &TestHuman{
view: view,
Expand Down Expand Up @@ -93,11 +105,169 @@ func (t *TestHuman) File(file *moduletest.File) {
t.view.streams.Printf("%s... %s\n", file.Name, colorizeTestStatus(file.Status, t.view.colorize))
}

func (t *TestHuman) Run(run *moduletest.Run) {
func (t *TestHuman) Run(run *moduletest.Run, file *moduletest.File) {
t.view.streams.Printf(" run %q... %s\n", run.Name, colorizeTestStatus(run.Status, t.view.colorize))

// Finally we'll print out a summary of the diagnostics from the run.
t.view.Diagnostics(run.Diagnostics)
t.Diagnostics(run, file, run.Diagnostics)
}

func (t *TestHuman) DestroySummary(diags tfdiags.Diagnostics, file *moduletest.File, state *states.State) {
if diags.HasErrors() {
t.view.streams.Eprintf("Terraform encountered an error destroying resources created while executing %s.\n", file.Name)
}
t.Diagnostics(nil, file, diags)

if state.HasManagedResourceInstanceObjects() {
t.view.streams.Eprintf("\nTerraform left the following resources in state after executing %s, they need to be cleaned up manually:\n", file.Name)
for _, resource := range state.AllResourceInstanceObjectAddrs() {
if resource.DeposedKey != states.NotDeposed {
t.view.streams.Eprintf(" - %s (%s)\n", resource.Instance, resource.DeposedKey)
continue
}
t.view.streams.Eprintf(" - %s\n", resource.Instance)
}
}
}

func (t *TestHuman) Diagnostics(_ *moduletest.Run, _ *moduletest.File, diags tfdiags.Diagnostics) {
t.view.Diagnostics(diags)
}

type TestJSON struct {
view *JSONView
}

var _ Test = (*TestJSON)(nil)

func (t TestJSON) Abstract(suite *moduletest.Suite) {
var fileCount, runCount int

abstract := json.TestSuiteAbstract{}
for name, file := range suite.Files {
fileCount++
var runs []string
for _, run := range file.Runs {
runCount++
runs = append(runs, run.Name)
}
abstract[name] = runs
}

files := "files"
runs := "run blocks"

if fileCount == 1 {
files = "file"
}

if runCount == 1 {
runs = "run block"
}

t.view.log.Info(
fmt.Sprintf("Found %d %s and %d %s", fileCount, files, runCount, runs),
"type", json.MessageTestAbstract,
json.MessageTestAbstract, abstract)
}

func (t TestJSON) Conclusion(suite *moduletest.Suite) {
summary := json.TestSuiteSummary{
Status: json.ToTestStatus(suite.Status),
}
for _, file := range suite.Files {
for _, run := range file.Runs {
switch run.Status {
case moduletest.Skip:
summary.Skipped++
case moduletest.Pass:
summary.Passed++
case moduletest.Error:
summary.Errored++
case moduletest.Fail:
summary.Failed++
}
}
}

var message bytes.Buffer
if suite.Status <= moduletest.Skip {
// Then no tests.
message.WriteString("Executed 0 tests")
if summary.Skipped > 0 {
message.WriteString(fmt.Sprintf(", %d skipped.", summary.Skipped))
} else {
message.WriteString(".")
}
} else {
if suite.Status == moduletest.Pass {
message.WriteString("Success!")
} else {
message.WriteString("Failure!")
}

message.WriteString(fmt.Sprintf(" %d passed, %d failed", summary.Passed, summary.Failed+summary.Errored))
if summary.Skipped > 0 {
message.WriteString(fmt.Sprintf(", %d skipped.", summary.Skipped))
} else {
message.WriteString(".")
}
}

t.view.log.Info(
message.String(),
"type", json.MessageTestSummary,
json.MessageTestSummary, summary)
}

func (t TestJSON) File(file *moduletest.File) {
t.view.log.Info(
fmt.Sprintf("%s... %s", file.Name, testStatus(file.Status)),
"type", json.MessageTestFile,
json.MessageTestFile, json.TestFileStatus{file.Name, json.ToTestStatus(file.Status)},
"@testfile", file.Name)
}

func (t TestJSON) Run(run *moduletest.Run, file *moduletest.File) {
t.view.log.Info(
fmt.Sprintf(" %q... %s", run.Name, testStatus(run.Status)),
"type", json.MessageTestRun,
json.MessageTestRun, json.TestRunStatus{file.Name, run.Name, json.ToTestStatus(run.Status)},
"@testfile", file.Name,
"@testrun", run.Name)

t.Diagnostics(run, file, run.Diagnostics)
}

func (t TestJSON) DestroySummary(diags tfdiags.Diagnostics, file *moduletest.File, state *states.State) {
if state.HasManagedResourceInstanceObjects() {
cleanup := json.TestFileCleanup{}
for _, resource := range state.AllResourceInstanceObjectAddrs() {
cleanup.FailedResources = append(cleanup.FailedResources, json.TestFailedResource{
Instance: resource.Instance.String(),
DeposedKey: resource.DeposedKey.String(),
})
}

t.view.log.Error(
fmt.Sprintf("Terraform left some resources in state after executing %s, they need to be cleaned up manually.", file.Name),
"type", json.MessageTestCleanup,
json.MessageTestCleanup, cleanup,
"@testfile", file.Name)
}

t.Diagnostics(nil, file, diags)
}

func (t TestJSON) Diagnostics(run *moduletest.Run, file *moduletest.File, diags tfdiags.Diagnostics) {
var metadata []interface{}
if file != nil {
metadata = append(metadata, "@testfile", file.Name)
}
if run != nil {
metadata = append(metadata, "@testrun", run.Name)
}
t.view.Diagnostics(diags, metadata...)
}

func colorizeTestStatus(status moduletest.Status, color *colorstring.Colorize) string {
Expand All @@ -114,3 +284,18 @@ func colorizeTestStatus(status moduletest.Status, color *colorstring.Colorize) s
panic("unrecognized status: " + status.String())
}
}

func testStatus(status moduletest.Status) string {
switch status {
case moduletest.Error, moduletest.Fail:
return "fail"
case moduletest.Pass:
return "pass"
case moduletest.Skip:
return "skip"
case moduletest.Pending:
return "pending"
default:
panic("unrecognized status: " + status.String())
}
}
Loading