-
Notifications
You must be signed in to change notification settings - Fork 11
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
base: main
Are you sure you want to change the base?
Conversation
d09c78c
to
5e649d3
Compare
There was a problem hiding this 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!
helper/resource/json.go
Outdated
) | ||
|
||
const stdoutFile = "stdout.txt" |
There was a problem hiding this comment.
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).
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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. 👍
There was a problem hiding this comment.
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" { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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) { |
There was a problem hiding this comment.
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!
There was a problem hiding this comment.
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
.
…her than ApplyJSON()) if running commands with -json flag raises an ErrVersionMismatch error (#16)
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 |
There was a problem hiding this comment.
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.
757ea2e
to
ad2edeb
Compare
.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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Additional context on the /PR part: https://hashicorp.slack.com/archives/C030RBBA63U/p1673297961927049
helper/resource/stdout.go
Outdated
var _ io.Writer = &Stdout{} | ||
var _ io.Reader = &Stdout{} | ||
|
||
type Stdout struct { |
There was a problem hiding this comment.
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) { /* ... */ }
helper/resource/stdout_test.go
Outdated
tfjson "github.com/hashicorp/terraform-json" | ||
) | ||
|
||
var stdoutJSON = []string{ |
There was a problem hiding this comment.
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.
helper/resource/testing_new.go
Outdated
// stdout (io.Writer) is supplied to all terraform commands that are using streaming json output. | ||
stdout := NewStdout() |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
…son.Diagnostic into a separate TerraformJSONDiagnostics type (#16)
…nces of NewTerraformJSONBuffer within each command (#16)
buf *bytes.Buffer | ||
diagnostics TerraformJSONDiagnostics | ||
jsonOutput []string | ||
rawOutput string |
There was a problem hiding this comment.
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"}
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this 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") |
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
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) | ||
} |
There was a problem hiding this comment.
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
}
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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. 😄
There was a problem hiding this comment.
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.
…s exceeding 64kb (#16)
// 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. |
There was a problem hiding this comment.
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.
Closes: #16