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

(WIP) Add tests for DynamicPseudoType behavior #208

Closed
wants to merge 39 commits into from

Conversation

austinvalle
Copy link
Member

@austinvalle austinvalle commented Jan 19, 2024

Ref: hashicorp/terraform-plugin-framework#147

In preparation for introducing dynamic schema attributes and type handling in terraform-plugin-framework, this PR adds tests to verify the behavior of tftypes.DynamicPseudoType, which the plugin framework dynamic code will be abstracting.

I tried to add comments in places where the test isn't self-explanatory ™️, but please ask questions if something looks off or an explanation doesn't describe enough.

The tests currently implemented in this PR covers resource schemas and built-in functions for Protocol v6, some follow-up tests that still need to be introduced:

  • Data source schemas
  • Provider schemas
  • Duplicate as many tests as possible for Protocol v5
    • This will include some implementation for the testsdk for v5 support
  • Provider-defined functions (will probably wait until Terraform 1.8 release?)

PR Notes

@austinvalle austinvalle changed the title (WIP) DynamicPseudoType testing (WIP) Add tests for DynamicPseudoType behavior Jan 30, 2024
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.

Overall super excited to get this level of end-to-end testing! There is a ton of great tests and assertions in here. My comments probably fall more under pedantic than anything.

@@ -96,6 +97,7 @@ jobs:
- run: go mod download
- run: go test -v -cover ./internal/framework6provider/
- run: go test -v -cover ./internal/protocolv6provider/
- run: go test -v -cover ./internal/dynamic6provider/
Copy link
Contributor

Choose a reason for hiding this comment

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

It'd be nice to not expand our packages here and use protocol(v6)provider because it means updating the terraform-plugin-go CI and I'm not sure it offers much benefit over having all the terraform-plugin-go tests in one package per protocol version. Maybe we can introduce the testsdk and update the existing provider in a separate PR, then the dynamic tests are additive to the existing provider?

Copy link
Member Author

@austinvalle austinvalle Jan 31, 2024

Choose a reason for hiding this comment

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

That's a good shout and will make even more sense once framework implements dynamic types and we add tests for that.

I think I can wait for @bendbennett's work on #210 to merge, then will add all my tests to the protocolv6provider + protocolprovider and switch them all to using the testsdk.

Copy link
Contributor

Choose a reason for hiding this comment

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

#210 is almost complete with the exception of dealing with hashicorp/terraform-plugin-framework#914. I'll be looking into that imminently, but don't let me hold you up in terms of merging this PR.

Copy link
Member Author

Choose a reason for hiding this comment

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

I don't plan on opening this PR for review until the dynamic type work in terraform-plugin-framework is nearing completion (which I just started), so I think we still have some time, whoever gets to it first 😆

After that work is done, I want to make sure we also have a discussion with the core team to ensure we aren't exposing dynamics in any way that could be invalid/confusing.

internal/dynamic6provider/dynamic_functions_test.go Outdated Show resolved Hide resolved

func Test_Dynamic_ImportState(t *testing.T) {
r.UnitTest(t, r.TestCase{
Steps: []r.TestStep{
Copy link
Contributor

Choose a reason for hiding this comment

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

One thing to potentially consider with this testing is whether we should make it two TestStep so it can enable ImportStateVerify: true and verify that there are no state differences between an originally applied state and imported state (or create a separate test like that). This is typically how import testing is done for regular providers and I think its a nice benefit even in this case since it will check the imported state contents beyond core's "schema correctness" of the import response.

Copy link
Member Author

Choose a reason for hiding this comment

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

I wasn't 100% sure how to write an import test, but I took a look at the AWS provider and I think I fixed it in 95d52c2

},
},
},
PlanChangeFunc: verifyDynamicTypeByPath(
Copy link
Contributor

Choose a reason for hiding this comment

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

Verifying the type is awesome! Love this. While we're already so close to it, should we just verify the value as well? It could catch bizarre conversion bugs in tftypes, e.g. if the element values got errantly re-arranged for example.

Copy link
Member Author

@austinvalle austinvalle Jan 31, 2024

Choose a reason for hiding this comment

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

Sure! I adjusted the verify method to just accept a tftypes.Value and use the type and value of that to verify. Seemed more ergonomic 😆

I broke them into two separate assertions for different messaging (even though I think (tftypes.Value).Equal also verifies the type.) Does that make sense or should we just stick to one assert?

0fd1b90

internal/dynamic6provider/dynamic_nesting_modes_test.go Outdated Show resolved Hide resolved
Comment on lines +210 to +220
// The type is stored as a tuple, but we can still use `ListExact` to verify state
knownvalue.ListExact([]knownvalue.Check{
knownvalue.StringExact("hey"),
knownvalue.ObjectExact(map[string]knownvalue.Check{
"number": knownvalue.Int64Exact(12345),
}),
knownvalue.ListExact([]knownvalue.Check{
knownvalue.StringExact("there"),
knownvalue.StringExact("tuple"),
}),
}),
Copy link
Member Author

Choose a reason for hiding this comment

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

@bflad @bendbennett I hesitate to suggest we add a TupleExact, but just wanted to point out that this works fine

Copy link
Contributor

Choose a reason for hiding this comment

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

Interesting. If there are legitimate use-cases for TupleExact then perhaps we can add this in going forwards.

Copy link
Contributor

@bendbennett bendbennett left a comment

Choose a reason for hiding this comment

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

This is marvellous!
Great job.

As discussed out-of-band, perhaps we can think about documenting some of the procedures for extracting and examining msgpack data to aid provider developers and practitioners.

@@ -96,6 +97,7 @@ jobs:
- run: go mod download
- run: go test -v -cover ./internal/framework6provider/
- run: go test -v -cover ./internal/protocolv6provider/
- run: go test -v -cover ./internal/dynamic6provider/
Copy link
Contributor

Choose a reason for hiding this comment

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

#210 is almost complete with the exception of dealing with hashicorp/terraform-plugin-framework#914. I'll be looking into that imminently, but don't let me hold you up in terms of merging this PR.

@@ -9,7 +9,7 @@ require (
github.com/hashicorp/terraform-plugin-go v0.21.0
github.com/hashicorp/terraform-plugin-mux v0.14.0
github.com/hashicorp/terraform-plugin-sdk/v2 v2.32.0
github.com/hashicorp/terraform-plugin-testing v1.6.0
github.com/hashicorp/terraform-plugin-testing v1.6.1-0.20240130155516-51777dda582e
Copy link
Contributor

Choose a reason for hiding this comment

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

I'll tag a release once we've finalised and merged hashicorp/terraform-plugin-testing#276

"corner_dynamic_thing": {
SchemaResponse: &resource.SchemaResponse{
Schema: &tfprotov6.Schema{
Block: &tfprotov6.SchemaBlock{
Copy link
Contributor

Choose a reason for hiding this comment

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

Because I spend far more time looking at a schema at the "Framework-level", scanning Block: ... always makes me think, hang on a minute, given the config that's being used this isn't a block, it's an attribute. Always makes me think of Plugin Protocol v6: What is a block, even?.

"corner_dynamic_thing.foo",
tfjsonpath.New("attribute_with_dpt"),
knownvalue.ListExact([]knownvalue.Check{
knownvalue.StringExact("hey"),
Copy link
Contributor

Choose a reason for hiding this comment

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

Might not be appropriate for this PR, or this repo even, but if it's not documented somewhere, is it worth providing some information about the rules that Terraform applies when there is ambiguity in the types specified in configuration, in this instance a string and an integer, and how that is handled in the context of a attributes that use tftypes.DynamicPseudoType? Does Terraform take the first value (i.e., "hey"), determine that it is a string, then determine if all of the subsequent values in the list can be converted to a string?

Copy link
Member Author

@austinvalle austinvalle Feb 1, 2024

Choose a reason for hiding this comment

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

I'm not sure it's documented in the context you're describing, but it is an interesting thought I also share.

Just observing the behavior of Terraform/cty it looks potentially arbitrary, but digging more into cty documentation, I believe it may just be following the only safe conversion route possible: https://github.com/zclconf/go-cty/blob/main/docs/convert.md#conversion-charts

  • The literal representation of this specific test config is known to Terraform as: tuple[string, number]
  • The schema we defined tells Terraform/cty that it needs to be converted to a list, which has a single element type that is not determined (DynamicPseudoType)
  • Terraform needs to determine the element type, so it needs to find a safe route to converge [string, number]. Which the only safe option according to that table is string

If we switch the config around to:

resource "corner_dynamic_thing" "foo" {
  attribute_with_dpt = [12345, "hey"]
}

We receive the same element type as string from the msgpack config:

{
  "attribute_with_dpt": [
    [
      "\"string\"",
      "12345"
    ],
    [
      "\"string\"",
      "hey"
    ]
  ]
}

It would be nice to get some confirmation on this behavior, so I will note this down as something to ask the core team so we can understand a little deeper 👍🏻

Comment on lines +81 to +97
// This may eventually be considered an invalid schema. It currently behaves as expected but may not be exposed to provider developers
// to avoid confusion. While the element type is dynamic, all elements must still have the exact same type, which Terraform will achieve
// by either performing a conversion (like below, converting 12345 to "12345"), or throw an error if conversion is impossible, for example:
//
// Error: Incorrect attribute value type
//
// on terraform_plugin_test.tf line 12, in resource "corner_dynamic_thing" "foo":
// attribute_with_dpt = {
// "key1" = "hey"
// "key2" = {
// number = 12345
// }
// }
//
// Inappropriate value for attribute "attribute_with_dpt": all map elements must have the same type.
//
// Related issue: https://github.com/hashicorp/terraform/issues/34574
Copy link
Contributor

Choose a reason for hiding this comment

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

❤️ the extensive notes/documentation.

Comment on lines +210 to +220
// The type is stored as a tuple, but we can still use `ListExact` to verify state
knownvalue.ListExact([]knownvalue.Check{
knownvalue.StringExact("hey"),
knownvalue.ObjectExact(map[string]knownvalue.Check{
"number": knownvalue.Int64Exact(12345),
}),
knownvalue.ListExact([]knownvalue.Check{
knownvalue.StringExact("there"),
knownvalue.StringExact("tuple"),
}),
}),
Copy link
Contributor

Choose a reason for hiding this comment

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

Interesting. If there are legitimate use-cases for TupleExact then perhaps we can add this in going forwards.

Comment on lines +239 to +240
tftypes.DynamicPseudoType,
tftypes.DynamicPseudoType,
Copy link
Contributor

Choose a reason for hiding this comment

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

Not directly related to this PR, but given that within terraform-plugin-go, tftypes.DynamicPseudoType is defined as a primitive, it seems a little odd that it can be used for any Terraform type.

Copy link
Member Author

Choose a reason for hiding this comment

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

Right, I think the documentation and wording will be important for framework developers to understand. As described by cty, it's a placeholder for a type that will eventually be determined, "pseudo-type": https://github.com/zclconf/go-cty/blob/main/docs/types.md#the-dynamic-pseudo-type.

One consequence of this being a "pseudo-type" is that there is no known, non-null value of this type

})
}

func verifyDynamicAttributeByPath(path *tftypes.AttributePath, expectedVal tftypes.Value) func(context.Context, resource.PlanChangeRequest, *resource.PlanChangeResponse) {
Copy link
Contributor

Choose a reason for hiding this comment

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

❤️ this is really great!

// - https://github.com/hashicorp/terraform-plugin-go/issues/267
// - https://github.com/hashicorp/terraform/issues/34574
//
// In this test, the dynamic `bar` attribute will cause the entire block to be sent over as a DynamicPseudoType, instead of just the bar attribute.
Copy link
Contributor

Choose a reason for hiding this comment

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

Wondering if it's worth noting that the indication that the entire block is a DynamicPseudoType is provided by the "type annotation" that appears prior to the values. Perhaps this is obvious, and maybe we should document this elsewhere, but it could be contrasted with an example where the DynamicPseudoTypes are constrained to, for instance elements, such as the Test_Dynamic_Attribute_ListType you documented:

{
  "attribute_with_dpt": [
    [
      "\"string\"",
      "hey"
    ],
    [
      "\"string\"",
      "12345"
    ]
  ]
}

@bflad
Copy link
Contributor

bflad commented Apr 19, 2024

@austinvalle do we still need this now that #228 is merged?

@austinvalle
Copy link
Member Author

@austinvalle do we still need this now that #228 is merged?

I'm comfortable saying that the important parts of this PR are covered by #228, so I'll close this.

The only tests that aren't covered in #228 are:

  • Testing import
  • Testing Terraform core's type conversion
  • Testing a feature we don't natively support in terraform-plugin-framework (collections w/ dynamic element type)

@austinvalle austinvalle deleted the av/dpt-testing branch April 22, 2024 13:15
Copy link

I'm going to lock this pull request because it has been closed for 30 days ⏳. This helps our maintainers find and focus on the active contributions.
If you have found a problem that seems related to this change, please open a new issue and complete the issue template so we can capture all the details necessary to investigate further.

@github-actions github-actions bot locked as resolved and limited conversation to collaborators May 22, 2024
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants