Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
569231b
Piping output from terraform apply into stdout.txt in working directo…
bendbennett Dec 21, 2022
1d91315
Add test to verify that warnings are generated during test step execu…
bendbennett Jan 3, 2023
5c1b67e
Moving defer file.Close outside for loop (#16)
bendbennett Jan 3, 2023
71f0877
Updating defer func to log error and fail test if closing file contai…
bendbennett Jan 3, 2023
1061d08
Inspecting streaming json output for warnings, and error and streamin…
bendbennett Jan 12, 2023
4cac99e
Switching to using streaming json for terraform refresh (#16)
bendbennett Jan 12, 2023
021c2e8
Use framework testprovider files for setup of test using ProtoV5Provi…
bendbennett Jan 12, 2023
5e649d3
Temporarily use terraform-exec sha (#16)
bendbennett Jan 12, 2023
9a77e42
Using CreatePlanJSON() for generating streaming JSON output when runn…
bendbennett Jan 13, 2023
f496bfe
Refactoring stdout related functions (#16)
bendbennett Jan 13, 2023
9a154c6
Merge branch 'main' into bendbennett/issues-16
bendbennett Jan 16, 2023
da0f4e9
Fallback to using standard terraform-exec commands (e.g., Apply() rat…
bendbennett Jan 16, 2023
554cbd9
Updating test coverage for apply, plan, refresh and destroy (#16)
bendbennett Jan 16, 2023
89be0c3
Switching to using bytes.Buffer for terraform-exec streaming json out…
bendbennett Jan 16, 2023
ad2edeb
Adding changelog entry and updating documentation (#16)
bendbennett Jan 18, 2023
28c6676
Adding ExpectNonEmptyPlan to TestTest_TestStep_ProviderFactories_Expe…
bendbennett Jan 18, 2023
3ccd606
Removing unused method (#16)
bendbennett Jan 18, 2023
af3a9dd
Renaming struct from Stdout to TerraformJSONBuffer (#16)
bendbennett Jan 23, 2023
74ea0f6
Adding Parse() function to TerraformJSONBuffer and separating out tfj…
bendbennett Jan 23, 2023
e93c329
Using file for test fixture (#16)
bendbennett Jan 23, 2023
32cb527
Refactored to use command-specific responses and to instantiate insta…
bendbennett Jan 24, 2023
72c60c0
Joining strings for error output (#16)
bendbennett Jan 24, 2023
9357173
Returning response structs for testStepNewRefreshState() and testStep…
bendbennett Jan 24, 2023
a12aaa1
Bumping to latest main for terraform-exec (#16)
bendbennett Jan 25, 2023
2fb79ba
Adding contents of error diagnostics to error supplied to TestCase Er…
bendbennett Jan 25, 2023
ad5ebb1
Supplying stdout to test output (#16)
bendbennett Jan 25, 2023
a30a88f
Renaming vars (#16)
bendbennett Jan 25, 2023
870e540
Return diagnostics rather than the whole of stdout (#16)
bendbennett Jan 30, 2023
25b4681
Only return error diagnostics for errors and warning diagnostics for …
bendbennett Jan 30, 2023
d27b6c2
Return errors rather than log and exit. Remove usage of TFJSON stdout…
bendbennett Jan 30, 2023
1369356
Switching to using bufio.NewReader in order to be able to handle line…
bendbennett Jan 31, 2023
bba2711
Adding NewTerraformJSONBufferFromFile and ReadFile funcs (#16)
bendbennett Jan 31, 2023
d96eb7c
Merge branch 'main' into bendbennett/issues-16
bendbennett Jan 31, 2023
bc8bc6d
Linting (#16)
bendbennett Jan 31, 2023
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
8 changes: 8 additions & 0 deletions .changes/unreleased/FEATURES-20230118-083204.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
kind: FEATURES
body: 'helper/resource: Added `TestStep` type `ExpectWarning` field, which enables
verifying that a warning has been emitted during plan, apply, refresh and destroy.
Requires that Terraform 0.15.3 or later is used as it makes use of the Terraform
[machine-readable UI](https://developer.hashicorp.com/terraform/internals/machine-readable-ui)'
time: 2023-01-18T08:32:04.688764Z
custom:
Issue: "16"
3 changes: 2 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,9 @@ require (
github.com/hashicorp/hc-install v0.4.0
github.com/hashicorp/hcl/v2 v2.15.0
github.com/hashicorp/logutils v1.0.0
github.com/hashicorp/terraform-exec v0.17.3
github.com/hashicorp/terraform-exec v0.17.4-0.20230124151948-5eada031c166
github.com/hashicorp/terraform-json v0.14.0
github.com/hashicorp/terraform-plugin-framework v1.0.1
github.com/hashicorp/terraform-plugin-go v0.14.3
github.com/hashicorp/terraform-plugin-log v0.7.0
github.com/hashicorp/terraform-plugin-sdk/v2 v2.24.1
Expand Down
6 changes: 4 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -93,10 +93,12 @@ github.com/hashicorp/hcl/v2 v2.15.0 h1:CPDXO6+uORPjKflkWCCwoWc9uRp+zSIPcCQ+BrxV7
github.com/hashicorp/hcl/v2 v2.15.0/go.mod h1:JRmR89jycNkrrqnMmvPDMd56n1rQJ2Q6KocSLCMCXng=
github.com/hashicorp/logutils v1.0.0 h1:dLEQVugN8vlakKOUE3ihGLTZJRB4j+M2cdTm/ORI65Y=
github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64=
github.com/hashicorp/terraform-exec v0.17.3 h1:MX14Kvnka/oWGmIkyuyvL6POx25ZmKrjlaclkx3eErU=
github.com/hashicorp/terraform-exec v0.17.3/go.mod h1:+NELG0EqQekJzhvikkeQsOAZpsw0cv/03rbeQJqscAI=
github.com/hashicorp/terraform-exec v0.17.4-0.20230124151948-5eada031c166 h1:WCvC+VknMywOyDXTIzvHHYWknjNBKI0/dmIR9EZd0bw=
github.com/hashicorp/terraform-exec v0.17.4-0.20230124151948-5eada031c166/go.mod h1:5M9hP3RX39/2AWDpAEWf0whTJFibOUN7DfH1E1lCZN8=
github.com/hashicorp/terraform-json v0.14.0 h1:sh9iZ1Y8IFJLx+xQiKHGud6/TSUCM0N8e17dKDpqV7s=
github.com/hashicorp/terraform-json v0.14.0/go.mod h1:5A9HIWPkk4e5aeeXIBbkcOvaZbIYnAIkEyqP2pNSckM=
github.com/hashicorp/terraform-plugin-framework v1.0.1 h1:apX2jtaEKa15+do6H2izBJdl1dEH2w5BPVkDJ3Q3mKA=
github.com/hashicorp/terraform-plugin-framework v1.0.1/go.mod h1:FV97t2BZOARkL7NNlsc/N25c84MyeSSz72uPp7Vq1lg=
github.com/hashicorp/terraform-plugin-go v0.14.3 h1:nlnJ1GXKdMwsC8g1Nh05tK2wsC3+3BL/DBBxFEki+j0=
github.com/hashicorp/terraform-plugin-go v0.14.3/go.mod h1:7ees7DMZ263q8wQ6E4RdIdR6nHHJtrdt4ogX5lPkX1A=
github.com/hashicorp/terraform-plugin-log v0.7.0 h1:SDxJUyT8TwN4l5b5/VkiTIaQgY6R+Y2BQ0sRZftGKQs=
Expand Down
8 changes: 8 additions & 0 deletions helper/resource/testing.go
Original file line number Diff line number Diff line change
Expand Up @@ -510,6 +510,14 @@ type TestStep struct {
// test to pass.
ExpectError *regexp.Regexp

// ExpectWarning requires the use of Terraform 0.15.3 or later as it makes
// use of the Terraform [machine-readable UI](https://developer.hashicorp.com/terraform/internals/machine-readable-ui).
//
// ExpectWarning allows the construction of test cases that we expect to pass
// with a warning. The specified regexp must match against stdout for the
// test to pass.
ExpectWarning *regexp.Regexp

// PlanOnly can be set to only run `plan` with this configuration, and not
// actually apply it. This is useful for ensuring config changes result in
// no-op plans
Expand Down
166 changes: 155 additions & 11 deletions helper/resource/testing_new.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ package resource

import (
"context"
"encoding/json"
"fmt"
"os"
"path/filepath"
Expand Down Expand Up @@ -253,34 +254,107 @@ func runNewTest(ctx context.Context, t testing.T, c TestCase, helper *plugintest
if step.RefreshState {
logging.HelperResourceTrace(ctx, "TestStep is RefreshState mode")

err := testStepNewRefreshState(ctx, t, wd, step, providers)
testStepNewRefreshStateResponse, err := testStepNewRefreshState(ctx, t, wd, step, providers)
refreshTfJSONDiags := testStepNewRefreshStateResponse.tfJSONDiags

if step.ExpectError != nil {
logging.HelperResourceDebug(ctx, "Checking TestStep ExpectError")

if err == nil {
logging.HelperResourceError(ctx,
"Error running refresh: expected an error but got none",
)
t.Fatalf("Step %d/%d error running refresh: expected an error but got none", stepNumber, len(c.Steps))
}
if !step.ExpectError.MatchString(err.Error()) {

errorFound := step.ExpectError.MatchString(err.Error())
jsonErrorFound := refreshTfJSONDiags.Contains(step.ExpectError, tfjson.DiagnosticSeverityError)

errorOutput := []string{err.Error()}

for _, v := range refreshTfJSONDiags.Errors() {
b, err := json.Marshal(v)
if err != nil {
t.Errorf("could not marshal tfjson diagnostic: %s", err)
}

errorOutput = append(errorOutput, string(b))
}

if !errorFound && !jsonErrorFound {
logging.HelperResourceError(ctx,
fmt.Sprintf("Error running refresh: expected an error with pattern (%s)", step.ExpectError.String()),
map[string]interface{}{logging.KeyError: err},
map[string]interface{}{logging.KeyError: strings.Join(errorOutput, "")},
)
t.Fatalf("Step %d/%d error running refresh, expected an error with pattern (%s), no match on: %s", stepNumber, len(c.Steps), step.ExpectError.String(), err)
t.Fatalf("Step %d/%d error running refresh, expected an error with pattern (%s), no match on: %s", stepNumber, len(c.Steps), step.ExpectError.String(), strings.Join(errorOutput, "\n"))
}
} else {
if err != nil && c.ErrorCheck != nil {
logging.HelperResourceDebug(ctx, "Calling TestCase ErrorCheck")

// This is a "best effort" to supply ErrorCheck with the contents of the
// error diagnostics returned from running the Terraform command when
// executed with the -json flag. If ErrorCheck functions are using
// regexp to perform matches that include line breaks then these will
// likely no longer behave as expected and therefore potentially
// represent a breaking change.
Comment on lines +295 to +300
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For others benefit, the TestCase.ErrorCheck functionality is the main wrinkle that prevents us from immediately being able to use Terraform's machine readable output (JSON view/UI).

ErrorCheck functionality, currently used in many popular Terraform provider acceptance tests, was intended to serve these use cases:

  • Skipping tests based on known errors (e.g. an API error indicating that a feature is not supported against an endpoint variant)
  • Rewriting errors for clarity
  • Ignoring known errors

Today the testing code is reliant on terraform-exec's error returns to include the data needed for TestCase.ErrorCheck and TestStep.ExpectError functionality to work. Warning diagnostics are not passed back the same way, since they are not errors and should not stop program execution. In order to capture similar data for warnings, the testing code would either need to perform error-prone (and likely version dependent) scraping of the human-readable output or switch to the machine-readable output. The machine-readable output means that the error from terraform-exec needs to be fundamentally treated differently and trying to convert machine-readable error diagnostics to be functionally equivalent to the prior error would be a non-trivial exercise that's not guaranteed to fully keep backwards compatibility.

An approach to this situation may be to prevent defining both TestCase.ErrorCheck and the new TestStep.ExpectWarning in the same TestCase, and setting up the testing execution to continue using the human-readable output where ErrorCheck is used. This is a non-starter for providers such as the Terraform AWS Provider though, which defines ErrorCheck functionality on all tests, meaning ExpectWarning could never be used.

Definitely open to ideas here, but trying to work around this further is risky at best.

var diagStrings []string

for _, v := range refreshTfJSONDiags.Errors() {
diagStrings = append(diagStrings, "Error: "+v.Summary)
diagStrings = append(diagStrings, v.Detail)
}

err = fmt.Errorf("%s\n"+strings.Join(diagStrings, "\n"), err)

err = c.ErrorCheck(err)

logging.HelperResourceDebug(ctx, "Called TestCase ErrorCheck")
}
if err != nil {
logging.HelperResourceError(ctx,
"Error running refresh",
map[string]interface{}{logging.KeyError: err},
)
t.Fatalf("Step %d/%d error running refresh: %s", stepNumber, len(c.Steps), err)

errorOutput := []string{err.Error()}

for _, v := range refreshTfJSONDiags {
b, err := json.Marshal(v)
if err != nil {
t.Errorf("could not marshal tfjson diagnostic: %s", err)
}

errorOutput = append(errorOutput, string(b))
}

t.Fatalf("Step %d/%d error: %s", stepNumber, len(c.Steps), strings.Join(errorOutput, "\n"))
}
}

if step.ExpectWarning != nil {
logging.HelperResourceDebug(ctx, "Checking TestStep ExpectWarning")

warningFound := refreshTfJSONDiags.Contains(step.ExpectWarning, tfjson.DiagnosticSeverityWarning)

if !warningFound {
logging.HelperResourceError(ctx,
fmt.Sprintf("Expected a warning with pattern (%s)", step.ExpectWarning.String()),
map[string]interface{}{logging.KeyError: err},
)

var warningDiags []string

for _, v := range refreshTfJSONDiags.Warnings() {
b, err := json.Marshal(v)
if err != nil {
t.Errorf("could not marshal tfjson diagnostic: %s", err)
}

warningDiags = append(warningDiags, string(b))
}

t.Errorf("Step %d/%d, expected a warning matching pattern: %q, no match on: %s", stepNumber, len(c.Steps), step.ExpectWarning.String(), strings.Join(warningDiags, "\n"))
}
}

Expand All @@ -292,7 +366,9 @@ func runNewTest(ctx context.Context, t testing.T, c TestCase, helper *plugintest
if step.Config != "" {
logging.HelperResourceTrace(ctx, "TestStep is Config mode")

err := testStepNewConfig(ctx, t, c, wd, step, providers)
testStepNewConfigResponse, err := testStepNewConfig(ctx, t, c, wd, step, providers)
newConfigTfJSONDiags := testStepNewConfigResponse.tfJSONDiags

if step.ExpectError != nil {
logging.HelperResourceDebug(ctx, "Checking TestStep ExpectError")

Expand All @@ -302,17 +378,47 @@ func runNewTest(ctx context.Context, t testing.T, c TestCase, helper *plugintest
)
t.Fatalf("Step %d/%d, expected an error but got none", stepNumber, len(c.Steps))
}
if !step.ExpectError.MatchString(err.Error()) {

errorFound := step.ExpectError.MatchString(err.Error())
jsonErrorFound := newConfigTfJSONDiags.Contains(step.ExpectError, tfjson.DiagnosticSeverityError)

errorOutput := []string{err.Error()}

for _, v := range newConfigTfJSONDiags.Errors() {
b, err := json.Marshal(v)
if err != nil {
t.Errorf("could not marshal tfjson Diagnostic: %s", err)
}

errorOutput = append(errorOutput, string(b))
}

if !errorFound && !jsonErrorFound {
logging.HelperResourceError(ctx,
fmt.Sprintf("Expected an error with pattern (%s)", step.ExpectError.String()),
map[string]interface{}{logging.KeyError: err},
map[string]interface{}{logging.KeyError: strings.Join(errorOutput, "")},
)
t.Fatalf("Step %d/%d, expected an error with pattern, no match on: %s", stepNumber, len(c.Steps), err)
t.Fatalf("Step %d/%d, expected an error matching pattern, no match on: %s", stepNumber, len(c.Steps), strings.Join(errorOutput, ""))
}
} else {
if err != nil && c.ErrorCheck != nil {
logging.HelperResourceDebug(ctx, "Calling TestCase ErrorCheck")

// This is a "best effort" to supply ErrorCheck with the contents of the
// error diagnostics returned from running the Terraform command when
// executed with the -json flag. If ErrorCheck functions are using
// regexp to perform matches that include line breaks then these will
// likely no longer behave as expected and therefore potentially
// represent a breaking change.
var diagStrings []string

for _, v := range newConfigTfJSONDiags.Errors() {
diagStrings = append(diagStrings, "Error: "+v.Summary)
diagStrings = append(diagStrings, v.Detail)
}

err = fmt.Errorf("%s\n"+strings.Join(diagStrings, "\n"), err)

err = c.ErrorCheck(err)

logging.HelperResourceDebug(ctx, "Called TestCase ErrorCheck")
Expand All @@ -322,7 +428,45 @@ func runNewTest(ctx context.Context, t testing.T, c TestCase, helper *plugintest
"Unexpected error",
map[string]interface{}{logging.KeyError: err},
)
t.Fatalf("Step %d/%d error: %s", stepNumber, len(c.Steps), err)

errorOutput := []string{err.Error()}

for _, v := range newConfigTfJSONDiags.Errors() {
b, err := json.Marshal(v)
if err != nil {
t.Errorf("could not marshall tfjson Diagnostic: %s", err)
}

errorOutput = append(errorOutput, string(b))
}

t.Fatalf("Step %d/%d error: %s", stepNumber, len(c.Steps), strings.Join(errorOutput, "\n"))
}
}

if step.ExpectWarning != nil {
logging.HelperResourceDebug(ctx, "Checking TestStep ExpectWarning")

warningFound := newConfigTfJSONDiags.Contains(step.ExpectWarning, tfjson.DiagnosticSeverityWarning)

if !warningFound {
logging.HelperResourceError(ctx,
fmt.Sprintf("Expected a warning with pattern (%s)", step.ExpectWarning.String()),
map[string]interface{}{logging.KeyError: err},
)

var warningDiags []string

for _, v := range newConfigTfJSONDiags.Warnings() {
b, err := json.Marshal(v)
if err != nil {
t.Errorf("could not marshal tfjson diagnostic: %s", err)
}

warningDiags = append(warningDiags, string(b))
}

t.Errorf("Step %d/%d, expected a warning matching pattern: %q, no match on: %s", stepNumber, len(c.Steps), step.ExpectWarning.String(), strings.Join(warningDiags, "\n"))
}
}

Expand Down Expand Up @@ -394,7 +538,7 @@ func testIDRefresh(ctx context.Context, t testing.T, c TestCase, wd *plugintest.

// Refresh!
err = runProviderCommand(ctx, t, func() error {
err = wd.Refresh(ctx)
_, err := wd.Refresh(ctx)
if err != nil {
t.Fatalf("Error running terraform refresh: %s", err)
}
Expand Down
Loading