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

Add ability to test whether warnings are raised during test steps #17

Open
wants to merge 34 commits into
base: main
Choose a base branch
from

Conversation

bendbennett
Copy link
Contributor

Closes: #16

Copy link
Contributor

@bflad bflad left a comment

Choose a reason for hiding this comment

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

Just leaving some breadcrumbs for others from our discussion earlier. 😄 Pretty cool stuff!

)

const stdoutFile = "stdout.txt"
Copy link
Contributor

Choose a reason for hiding this comment

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

We might be able to skip writing out to a file and re-reading the data from the file by using a bytes buffer and grabbing anything we want out of that (e.g. diagnostics).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Have switched over to using a bytes.Buffer.

@@ -28,10 +29,10 @@ func testStepNewConfig(ctx context.Context, t testing.T, c TestCase, wd *plugint
// require a refresh before applying
// failing to do this will result in data sources not being updated
err = runProviderCommand(ctx, t, func() error {
return wd.Refresh(ctx)
return wd.RefreshJSON(ctx, w)
Copy link
Contributor

Choose a reason for hiding this comment

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

To prevent the new JSON handling (only available in Terraform 0.15.3+) from being a breaking change for supported Terraform versions for the testing module, we can rely on checking for tfexec.ErrVersionMismatch to revert back to calling the non-JSON version of the commands. 👍

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Have added in checks for tfexec.ErrVersionMismatch and fallback to non-JSON versions of the commands.

ReadContext: func(_ context.Context, d *schema.ResourceData, _ interface{}) (diags diag.Diagnostics) {
// Only generate diag when ReadContext is called for the second
// time during terraform refresh.
if d.Get("id") == "new_id" {
Copy link
Contributor

Choose a reason for hiding this comment

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

To ensure errors are only being generated when you want, you can migrate the inline providers from the TestCase level to the TestStep level, then do things like raise errors in the second TestStep provider code.

Copy link
Contributor Author

@bendbennett bendbennett Jan 16, 2023

Choose a reason for hiding this comment

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

This all seems to be working as desired with the exception of refresh. Perhaps we can run through that together?

Have added ExpectNonEmptyPlan to TestTest_TestStep_ProviderFactories_ExpectWarningRefresh.

Computed: true,
PlanModifiers: []planmodifier.String{
testplanmodifier.String{
PlanModifyStringMethod: func(ctx context.Context, req planmodifier.StringRequest, resp *planmodifier.StringResponse) {
Copy link
Contributor

Choose a reason for hiding this comment

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

For framework code that should raise diagnostics during planning, it might be easier (maybe?) to implement a ModifyPlan method instead of attribute plan modifier. Not a big deal though!

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Have switched over to using ModifyPlan.

go.mod Outdated
@@ -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.20230116095935-bc76870e2b9b
Copy link
Contributor Author

@bendbennett bendbennett Jan 18, 2023

Choose a reason for hiding this comment

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

Needs to be set to terraform-exec release containing ...JSON() functions once Adding ApplyJSON(), DestroyJSON(), PlanJSON() and RefreshJSON() functions is merged and released.

@bendbennett bendbennett marked this pull request as ready for review January 18, 2023 09:35
@bendbennett bendbennett requested a review from a team as a code owner January 18, 2023 09:35
@bendbennett bendbennett added the enhancement New feature or request label Jan 19, 2023
@bendbennett bendbennett added this to the v1.1.0 milestone Jan 19, 2023
.changie.yaml Outdated
@@ -7,7 +7,7 @@ kindFormat: '{{.Kind}}:'
changeFormat: '* {{.Body}} ([#{{.Custom.Issue}}](https://github.com/hashicorp/terraform-plugin-testing/issues/{{.Custom.Issue}}))'
custom:
- key: Issue
label: Issue/PR Number
Copy link
Contributor

Choose a reason for hiding this comment

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

var _ io.Writer = &Stdout{}
var _ io.Reader = &Stdout{}

type Stdout struct {
Copy link
Contributor

Choose a reason for hiding this comment

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

We'll want to ensure that this type and associated creation function are not exported to the public API, maybe internal/plugintest is a good spot?

I also wonder if we should call this something like "TerraformJSONBuffer" and allow it to store things we care about so the buffer does not need to be rescanned each time? This will then allow us to detach the buffer logic from our check specific logic. e.g.

type TerraformJSONDiagnostics = []tfjson.Diagnostic

func (d TerraformJSONDiagnostics) ContainsRegex(r *regexp.Regexp) bool {
  // Logic like DiagnosticFound
}

// go-cmp friendly
func (d TerraformJSONDiagnostics) Equal(o TerraformJSONDiagnostics) bool {}

type TerraformJSONBuffer struct{
  buf         *bytes.Buffer
  diagnostics TerraformJSONDiagnostics
  parsed      bool // unless there's a good way to check to ensure the buf is emptied
}

func (b *TerraformJSONBuffer) Parse() {
  // logic to scan buffer text, attempt to parse the JSON, and save things like b.diagnostics
  b.parsed = true
}

func (b *TerraformJSONBuffer) Diagnostics() TerraformJSONDiagnostics {
  if !b.parsed {
    b.Parse()
  }

  return b.diagnostics
}
tfJSON := NewTerraformJSONBuffer()
err := tf.ApplyJSON(ctx, tfJSON) 

if tfJSON.Diagnostics().ContainsRegex(r) { /* ... */ }

tfjson "github.com/hashicorp/terraform-json"
)

var stdoutJSON = []string{
Copy link
Contributor

Choose a reason for hiding this comment

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

Would it be easier if we created a testdata directory and saved this as a file? Especially if we get more use cases for this data. I think that would allow you to do things like (pseudo-code) io.Copy(tfJSON, os.Open("testdata/description.txt")) for this specific use case and not worry about making the data (presumably copied text from a real Terraform call) Go code friendly.

Comment on lines 123 to 124
// stdout (io.Writer) is supplied to all terraform commands that are using streaming json output.
stdout := NewStdout()
Copy link
Contributor

Choose a reason for hiding this comment

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

I think we should have each command use its own buffer for a few reasons:

  • Prevents the need for everywhere to pass it and for the business logic to deal with any buffering aspects
  • Prevents any strange cross-command contamination
  • Allows the buffer to be garbage collected, rather than potentially keeping a buffer around that's the largest size from any of the commands it was used in

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Have made a first pass at this. Interested to hear your thoughts.

I've also left a couple of "TODO"s. Would value your input on these.

…nces of NewTerraformJSONBuffer within each command (#16)
buf *bytes.Buffer
diagnostics TerraformJSONDiagnostics
jsonOutput []string
rawOutput string
Copy link
Contributor Author

Choose a reason for hiding this comment

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

rawOutput was added in order to capture any outputs that were not JSON in order to maximise the information being returned to the provider developer for the purposes of debugging.

For example:

{"@level":"info","@message":"Terraform 1.3.2","@module":"terraform.ui","@timestamp":"2023-01-25T13:54:15.322845Z","terraform":"1.3.2","type":"version","ui":"1.0"}
{"@level":"warn","@message":"Warning: Empty or non-existent state","@module":"terraform.ui","@timestamp":"2023-01-25T13:54:15.355523Z","diagnostic":{"severity":"warning","summary":"Empty or non-existent state","detail":"There are currently no remote objects tracked in the state, so there is nothing to refresh."},"type":"diagnostic"}
{"@level":"info","@message":"Outputs: 0","@module":"terraform.ui","@timestamp":"2023-01-25T13:54:15.355853Z","outputs":{},"type":"outputs"}
{"@level":"info","@message":"Terraform 1.3.2","@module":"terraform.ui","@timestamp":"2023-01-25T13:54:15.380962Z","terraform":"1.3.2","type":"version","ui":"1.0"}
{"@level":"warn","@message":"Warning: Provider development overrides are in effect","@module":"terraform.ui","@timestamp":"2023-01-25T13:54:15.381655Z","diagnostic":{"severity":"warning","summary":"Provider development overrides are in effect","detail":"The following provider development overrides are set in the CLI configuration:\n - bendbennett/playground in /Users/bdb/go/bin\n - bendbennett/timeouts in /Users/bdb/go/bin\n\nThe behavior may therefore not match any released version of the provider and applying changes may cause the state to become incompatible with published releases."},"type":"diagnostic"}
{"@level":"info","@message":"example_resource.test: Plan to create","@module":"terraform.ui","@timestamp":"2023-01-25T13:54:15.396541Z","change":{"resource":{"addr":"example_resource.test","module":"","resource":"example_resource.test","implied_provider":"example","resource_type":"example_resource","resource_name":"test","resource_key":null},"action":"create"},"type":"planned_change"}
{"@level":"info","@message":"Plan: 1 to add, 0 to change, 0 to destroy.","@module":"terraform.ui","@timestamp":"2023-01-25T13:54:15.396576Z","changes":{"add":1,"change":0,"remove":0,"operation":"plan"},"type":"change_summary"}

Terraform used the selected providers to generate the following execution
plan. Resource actions are indicated with the following symbols:
  + create

Terraform will perform the following actions:

  # example_resource.test will be created
  + resource "example_resource" "test" {
      + id = (known after apply)
    }

Plan: 1 to add, 0 to change, 0 to destroy.
{"@level":"info","@message":"Terraform 1.3.2","@module":"terraform.ui","@timestamp":"2023-01-25T13:54:15.479418Z","terraform":"1.3.2","type":"version","ui":"1.0"}
{"@level":"warn","@message":"Warning: Provider development overrides are in effect","@module":"terraform.ui","@timestamp":"2023-01-25T13:54:15.480270Z","diagnostic":{"severity":"warning","summary":"Provider development overrides are in effect","detail":"The following provider development overrides are set in the CLI configuration:\n - bendbennett/playground in /Users/bdb/go/bin\n - bendbennett/timeouts in /Users/bdb/go/bin\n\nThe behavior may therefore not match any released version of the provider and applying changes may cause the state to become incompatible with published releases."},"type":"diagnostic"}
{"@level":"info","@message":"example_resource.test: Plan to create","@module":"terraform.ui","@timestamp":"2023-01-25T13:54:15.485951Z","change":{"resource":{"addr":"example_resource.test","module":"","resource":"example_resource.test","implied_provider":"example","resource_type":"example_resource","resource_name":"test","resource_key":null},"action":"create"},"type":"planned_change"}
{"@level":"info","@message":"example_resource.test: Creating...","@module":"terraform.ui","@timestamp":"2023-01-25T13:54:15.491701Z","hook":{"resource":{"addr":"example_resource.test","module":"","resource":"example_resource.test","implied_provider":"example","resource_type":"example_resource","resource_name":"test","resource_key":null},"action":"create"},"type":"apply_start"}
{"@level":"info","@message":"example_resource.test: Creation errored after 0s","@module":"terraform.ui","@timestamp":"2023-01-25T13:54:15.492349Z","hook":{"resource":{"addr":"example_resource.test","module":"","resource":"example_resource.test","implied_provider":"example","resource_type":"example_resource","resource_name":"test","resource_key":null},"action":"create","elapsed_seconds":0},"type":"apply_errored"}
{"@level":"error","@message":"Error: error summary","@module":"terraform.ui","@timestamp":"2023-01-25T13:54:15.503665Z","diagnostic":{"severity":"error","summary":"error summary","detail":"error detail","address":"example_resource.test","range":{"filename":"terraform_plugin_test.tf","start":{"line":1,"column":36,"byte":35},"end":{"line":1,"column":37,"byte":36}},"snippet":{"context":"resource \"example_resource\" \"test\"","code":"resource \"example_resource\" \"test\" { }","start_line":1,"highlight_start_offset":35,"highlight_end_offset":36,"values":[]}},"type":"diagnostic"}

Copy link
Contributor

Choose a reason for hiding this comment

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

Could you expand a little more on when this output might be shown to provider developers? At first glance I'd be worried about showing this level of detail unless its potentially in TRACE logging from the testing code. There are separate environment variables for outputting Terraform core's logging, although they can be problematic sometimes.

I don't think it's necessarily a bad idea to capture this data though in case we do need it for advanced troubleshooting cases.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This output would be displayed when, for instance, an Expect Warning regexp failed to match.

I think it's a fair comment that perhaps this is too much information. I'll update the PR so that only diagnostics from the streaming JSON are returned.

Copy link
Contributor

@bflad bflad left a comment

Choose a reason for hiding this comment

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

Maybe it's easiest to chat through this one early next week? I think its getting closer, but I'm getting a little lost in the details on a Friday. 👍


func (b *TerraformJSONBuffer) Write(p []byte) (n int, err error) {
if b.buf == nil {
log.Fatal("call NewTerraformJSONBuffer to initialise buffer")
Copy link
Contributor

Choose a reason for hiding this comment

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

Should here and below return this as a normal error?

		return 0, fmt.Errorf("cannot write to uninitialized buffer, use NewTerraformJSONBuffer")

}

if err := scanner.Err(); err != nil {
log.Fatal(err)
Copy link
Contributor

Choose a reason for hiding this comment

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

It might be best if this method returned an error instead of immediately exiting 👍

log.Fatal("call NewTerraformJSONBuffer to initialise buffer")
}

scanner := bufio.NewScanner(b.buf)
Copy link
Contributor

Choose a reason for hiding this comment

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

Do we need to worry about the 64KB bufio.MaxScanTokenSize? If Terraform can send lines larger than the default buffer size, we may need to provide our own to match Terraform's largest possible line size.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Have switched to using bufio.NewReader.

Comment on lines 90 to 114
tfJSON := NewTerraformJSONBuffer()

file, err := os.Open("../testdata/terraform-json-output.txt")
if err != nil {
t.Errorf("cannot read file: %s", err)
}
defer file.Close()

var fileEntries []string

scanner := bufio.NewScanner(file)
for scanner.Scan() {
txt := scanner.Text()

_, err := tfJSON.Write([]byte(txt + "\n"))
if err != nil {
t.Errorf("cannot write to tfJSON: %s", err)
}

fileEntries = append(fileEntries, txt)
}

if err := scanner.Err(); err != nil {
t.Errorf("scanner error: %s", err)
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Since os.File is itself an io.Reader, can it just be copied to tfJSON? e.g.

	tfJSON := NewTerraformJSONBuffer()

	file, err := os.Open("../testdata/terraform-json-output.txt")
	if err != nil {
		t.Errorf("cannot read file: %s", err)
	}
	defer file.Close()

	_, err := io.Copy(tfJSON, file)

	if err != nil {
		t.Fatalf("cannot copy file contents to buffer: %s", err)
	}

I think then we could take that a step further if we wanted so you could potentially create the TerraformJSONBuffer using file contents for when we get more test cases:

func NewTerraformJSONBufferFromFile(path string) error {
	tfJSON := NewTerraformJSONBuffer()

	file, err := os.Open(path)

	if err != nil {
		return fmt.Errorf("cannot read file: %w", err)
	}

	defer file.Close()

	_, err := io.Copy(tfJSON, file)

	if err != nil {
		return fmt.Errorf("cannot copy file contents to buffer: %w", err)
	}

	return nil
}

Or conversely it could just be a method such as ReadFile():

func (b TerraformJSONBuffer) ReadFile(path string) error {
	file, err := os.Open(path)

	if err != nil {
		return fmt.Errorf("cannot read file: %w", err)
	}

	defer file.Close()

	_, err := io.Copy(b, file)

	if err != nil {
		return fmt.Errorf("cannot copy file contents to buffer: %w", err)
	}

	return nil
}

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Have incorporated both NewTerraformJSONBufferFromFile and ReadFile. Thanks!

buf *bytes.Buffer
diagnostics TerraformJSONDiagnostics
jsonOutput []string
rawOutput string
Copy link
Contributor

Choose a reason for hiding this comment

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

Could you expand a little more on when this output might be shown to provider developers? At first glance I'd be worried about showing this level of detail unless its potentially in TRACE logging from the testing code. There are separate environment variables for outputting Terraform core's logging, although they can be problematic sometimes.

I don't think it's necessarily a bad idea to capture this data though in case we do need it for advanced troubleshooting cases.

return createPlanResponse, nil
}

stdout, err := wd.SavedPlanRawStdout(ctx)
Copy link
Contributor

Choose a reason for hiding this comment

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

Should this stdout be wired with the CreatePlanResponse stdout? Maybe the overlappting naming is confusing me. 😄

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I have removed the usage of stdout from CreatePlanResponse and the other ...Response(s) as it does not seem to be necessary or desirable to show this level of information to provider developers.

Comment on lines +295 to +300
// 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.
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.

@bflad bflad removed this from the v1.1.0 milestone Feb 14, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Ability to test whether warnings are raised during test steps
2 participants