internal/moduletest: Experimental module testing helpers

As part of ongoing research into Terraform testing we'd like to use an
experimental feature to validate our current understanding that expressing
tests as part of the Terraform language, as opposed to in some other
language run alongside, is a good and viable way to write practical
module integration tests.

This initial experimental incarnation of that idea is implemented as a
provider, just because that's an easier extension point for research
purposes than a first-class language feature would be. Whether this would
ultimately emerge as a provider similar to this or as custom language
constructs will be a matter for future research, if this first
experiment confirms that tests written in the Terraform language are the
best direction to take.

The previous incarnation of this experiment was an externally-developed
provider apparentlymart/testing, listed on the Terraform Registry. That
helped with showing that there are some useful tests that we can write
in the Terraform language, but integrating such a provider into Terraform
will allow us to make use of it in the also-experimental "terraform test"
command, which will follow in subsequent commits, to see how this might
fit into a development workflow.
This commit is contained in:
Martin Atkins 2021-02-04 16:08:51 -08:00
parent 56b756cfd9
commit 8330f8e991
6 changed files with 741 additions and 0 deletions

1
go.mod
View File

@ -114,6 +114,7 @@ require (
github.com/xiang90/probing v0.0.0-20160813154853-07dd2e8dfe18 // indirect
github.com/xlab/treeprint v0.0.0-20161029104018-1d6e34225557
github.com/zclconf/go-cty v1.7.1
github.com/zclconf/go-cty-debug v0.0.0-20191215020915-b22d67c1ba0b
github.com/zclconf/go-cty-yaml v1.0.2
go.uber.org/atomic v1.3.2 // indirect
go.uber.org/multierr v1.1.0 // indirect

2
go.sum
View File

@ -617,6 +617,8 @@ github.com/zclconf/go-cty v1.1.0/go.mod h1:xnAOWiHeOqg2nWS62VtQ7pbOu17FtxJNW8RLE
github.com/zclconf/go-cty v1.2.0/go.mod h1:hOPWgoHbaTUnI5k4D2ld+GRpFJSCe6bCM7m1q/N4PQ8=
github.com/zclconf/go-cty v1.7.1 h1:AvsC01GMhMLFL8CgEYdHGM+yLnnDOwhPAYcgTkeF0Gw=
github.com/zclconf/go-cty v1.7.1/go.mod h1:VDR4+I79ubFBGm1uJac1226K5yANQFHeauxPBoP54+o=
github.com/zclconf/go-cty-debug v0.0.0-20191215020915-b22d67c1ba0b h1:FosyBZYxY34Wul7O/MSKey3txpPYyCqVO5ZyceuQJEI=
github.com/zclconf/go-cty-debug v0.0.0-20191215020915-b22d67c1ba0b/go.mod h1:ZRKQfBXbGkpdV6QMzT3rU1kSTAnfu1dO8dPKjYprgj8=
github.com/zclconf/go-cty-yaml v1.0.2 h1:dNyg4QLTrv2IfJpm7Wtxi55ed5gLGOlPrZ6kMd51hY0=
github.com/zclconf/go-cty-yaml v1.0.2/go.mod h1:IP3Ylp0wQpYm50IHK8OZWKMu6sPJIUgKa8XhiVHura0=
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=

View File

@ -0,0 +1,52 @@
package moduletest
import (
"github.com/hashicorp/terraform/tfdiags"
)
// Assertion is the description of a single test assertion, whether
// successful or unsuccessful.
type Assertion struct {
Outcome Status
// Description is a user-provided, human-readable description of what
// this assertion represents.
Description string
// Message is typically relevant only for TestFailed or TestError
// assertions, giving a human-readable description of the problem,
// formatted in the way our format package expects to receive paragraphs
// for terminal word wrapping.
Message string
// Diagnostics includes diagnostics specific to the current test assertion,
// if available.
Diagnostics tfdiags.Diagnostics
}
// Component represents a component being tested, each of which can have
// several associated test assertions.
type Component struct {
Assertions map[string]*Assertion
}
// Status is an enumeration of possible outcomes of a test assertion.
type Status rune
const (
// Pending indicates that the test was registered (during planning)
// but didn't register an outcome during apply, perhaps due to being
// blocked by some other upstream failure.
Pending Status = '?'
// Passed indicates that the test condition succeeded.
Passed Status = 'P'
// Failed indicates that the test condition was valid but did not
// succeed.
Failed Status = 'F'
// Error indicates that the test condition was invalid or that the
// test report failed in some other way.
Error Status = 'E'
)

View File

@ -0,0 +1,8 @@
// Package moduletest contains the support code for some experimental features
// we're using to evaluate strategies for having an opinionated approach to
// testing of Terraform modules.
//
// At the moment nothing in this module is considered stable, so any features
// that are usable by end-users ought to emit experiment warnings saying that
// everything is subject to change even in patch releases.
package moduletest

View File

@ -0,0 +1,523 @@
package moduletest
import (
"fmt"
"sync"
"github.com/zclconf/go-cty/cty"
"github.com/zclconf/go-cty/cty/gocty"
ctyjson "github.com/zclconf/go-cty/cty/json"
"github.com/hashicorp/hcl/v2/hclsyntax"
"github.com/hashicorp/terraform/configs/configschema"
"github.com/hashicorp/terraform/providers"
"github.com/hashicorp/terraform/repl"
"github.com/hashicorp/terraform/tfdiags"
)
// Provider is an implementation of providers.Interface which we're
// using as a likely-only-temporary vehicle for research on an opinionated
// module testing workflow in Terraform.
//
// We expose this to configuration as "terraform.io/builtin/test", but
// any attempt to configure it will emit a warning that it is experimental
// and likely to change or be removed entirely in future Terraform CLI
// releases.
//
// The testing provider exists to gather up test results during a Terraform
// apply operation. Its "test_results" managed resource type doesn't have any
// user-visible effect on its own, but when used in conjunction with the
// "terraform test" experimental command it is the intermediary that holds
// the test results while the test runs, so that the test command can then
// report them.
//
// For correct behavior of the assertion tracking, the "terraform test"
// command must be sure to use the same instance of Provider for both the
// plan and apply steps, so that the assertions that were planned can still
// be tracked during apply. For other commands that don't explicitly support
// test assertions, the provider will still succeed but the assertions data
// may not be complete if the apply step fails.
type Provider struct {
// components tracks all of the "component" names that have been
// used in test assertions resources so far. Each resource must have
// a unique component name.
components map[string]*Component
// Must lock mutex in order to interact with the components map, because
// test assertions can potentially run concurrently.
mutex sync.RWMutex
}
var _ providers.Interface = (*Provider)(nil)
// NewProvider returns a new instance of the test provider.
func NewProvider() *Provider {
return &Provider{
components: make(map[string]*Component),
}
}
// TestResults returns the current record of test results tracked inside the
// provider.
//
// The result is a direct reference to the internal state of the provider,
// so the caller mustn't modify it nor store it across calls to provider
// operations.
func (p *Provider) TestResults() map[string]*Component {
return p.components
}
// GetSchema returns the complete schema for the provider.
func (p *Provider) GetSchema() providers.GetSchemaResponse {
return providers.GetSchemaResponse{
ResourceTypes: map[string]providers.Schema{
"test_assertions": testAssertionsSchema,
},
}
}
// PrepareProviderConfig is used to tweak the configuration values.
func (p *Provider) PrepareProviderConfig(req providers.PrepareProviderConfigRequest) providers.PrepareProviderConfigResponse {
// This provider has no configurable settings.
var res providers.PrepareProviderConfigResponse
res.PreparedConfig = req.Config
return res
}
// Configure configures and initializes the provider.
func (p *Provider) Configure(providers.ConfigureRequest) providers.ConfigureResponse {
// This provider has no configurable settings, but we use the configure
// request as an opportunity to generate a warning about it being
// experimental.
var res providers.ConfigureResponse
res.Diagnostics = res.Diagnostics.Append(tfdiags.AttributeValue(
tfdiags.Warning,
"The test provider is experimental",
"The Terraform team is using the test provider (terraform.io/builtin/test) as part of ongoing research about declarative testing of Terraform modules.\n\nThe availability and behavior of this provider is expected to change significantly even in patch releases, so we recommend using this provider only in test configurations and constraining your test configurations to an exact Terraform version.",
nil,
))
return res
}
// ValidateResourceTypeConfig is used to validate configuration values for a resource.
func (p *Provider) ValidateResourceTypeConfig(req providers.ValidateResourceTypeConfigRequest) providers.ValidateResourceTypeConfigResponse {
var res providers.ValidateResourceTypeConfigResponse
if req.TypeName != "test_assertions" { // we only have one resource type
res.Diagnostics = res.Diagnostics.Append(fmt.Errorf("unsupported resource type %s", req.TypeName))
return res
}
config := req.Config
if !config.GetAttr("component").IsKnown() {
res.Diagnostics = res.Diagnostics.Append(tfdiags.AttributeValue(
tfdiags.Error,
"Invalid component expression",
"The component name must be a static value given in the configuration, and may not be derived from a resource type attribute that will only be known during the apply step.",
cty.GetAttrPath("component"),
))
}
if !hclsyntax.ValidIdentifier(config.GetAttr("component").AsString()) {
res.Diagnostics = res.Diagnostics.Append(tfdiags.AttributeValue(
tfdiags.Error,
"Invalid component name",
"The component name must be a valid identifier, starting with a letter followed by zero or more letters, digits, and underscores.",
cty.GetAttrPath("component"),
))
}
for it := config.GetAttr("equal").ElementIterator(); it.Next(); {
k, obj := it.Element()
if !hclsyntax.ValidIdentifier(k.AsString()) {
res.Diagnostics = res.Diagnostics.Append(tfdiags.AttributeValue(
tfdiags.Error,
"Invalid assertion name",
"An assertion name must be a valid identifier, starting with a letter followed by zero or more letters, digits, and underscores.",
cty.GetAttrPath("equal").Index(k),
))
}
if !obj.GetAttr("description").IsKnown() {
res.Diagnostics = res.Diagnostics.Append(tfdiags.AttributeValue(
tfdiags.Error,
"Invalid description expression",
"The description must be a static value given in the configuration, and may not be derived from a resource type attribute that will only be known during the apply step.",
cty.GetAttrPath("equal").Index(k).GetAttr("description"),
))
}
}
for it := config.GetAttr("check").ElementIterator(); it.Next(); {
k, obj := it.Element()
if !hclsyntax.ValidIdentifier(k.AsString()) {
res.Diagnostics = res.Diagnostics.Append(tfdiags.AttributeValue(
tfdiags.Error,
"Invalid assertion name",
"An assertion name must be a valid identifier, starting with a letter followed by zero or more letters, digits, and underscores.",
cty.GetAttrPath("check").Index(k),
))
}
if !obj.GetAttr("description").IsKnown() {
res.Diagnostics = res.Diagnostics.Append(tfdiags.AttributeValue(
tfdiags.Error,
"Invalid description expression",
"The description must be a static value given in the configuration, and may not be derived from a resource type attribute that will only be known during the apply step.",
cty.GetAttrPath("equal").Index(k).GetAttr("description"),
))
}
}
return res
}
// ReadResource refreshes a resource and returns its current state.
func (p *Provider) ReadResource(req providers.ReadResourceRequest) providers.ReadResourceResponse {
var res providers.ReadResourceResponse
if req.TypeName != "test_assertions" { // we only have one resource type
res.Diagnostics = res.Diagnostics.Append(fmt.Errorf("unsupported resource type %s", req.TypeName))
return res
}
// Test assertions are not a real remote object, so there isn't actually
// anything to refresh here.
res.NewState = req.PriorState
return res
}
// UpgradeResourceState is called to allow the provider to adapt the raw value
// stored in the state in case the schema has changed since it was originally
// written.
func (p *Provider) UpgradeResourceState(req providers.UpgradeResourceStateRequest) providers.UpgradeResourceStateResponse {
var res providers.UpgradeResourceStateResponse
if req.TypeName != "test_assertions" { // we only have one resource type
res.Diagnostics = res.Diagnostics.Append(fmt.Errorf("unsupported resource type %s", req.TypeName))
return res
}
// We assume here that there can never be a flatmap version of this
// resource type's data, because this provider was never included in a
// version of Terraform that used flatmap and this provider's schema
// contains attributes that are not flatmap-compatible anyway.
if len(req.RawStateFlatmap) != 0 {
res.Diagnostics = res.Diagnostics.Append(fmt.Errorf("can't upgrade a flatmap state for %q", req.TypeName))
return res
}
if req.Version != 0 {
res.Diagnostics = res.Diagnostics.Append(fmt.Errorf("the state for this %s was created by a newer version of the provider", req.TypeName))
return res
}
v, err := ctyjson.Unmarshal(req.RawStateJSON, testAssertionsSchema.Block.ImpliedType())
if err != nil {
res.Diagnostics = res.Diagnostics.Append(fmt.Errorf("failed to decode state for %s: %s", req.TypeName, err))
return res
}
res.UpgradedState = v
return res
}
// PlanResourceChange takes the current state and proposed state of a
// resource, and returns the planned final state.
func (p *Provider) PlanResourceChange(req providers.PlanResourceChangeRequest) providers.PlanResourceChangeResponse {
var res providers.PlanResourceChangeResponse
if req.TypeName != "test_assertions" { // we only have one resource type
res.Diagnostics = res.Diagnostics.Append(fmt.Errorf("unsupported resource type %s", req.TypeName))
return res
}
// During planning, our job is to gather up all of the planned test
// assertions marked as pending, which will then allow us to include
// all of them in test results even if there's a failure during apply
// that prevents the full completion of the graph walk.
//
// In a sense our plan phase is similar to the compile step for a
// test program written in another language. Planning itself can fail,
// which means we won't be able to form a complete test plan at all,
// but if we succeed in planning then subsequent problems can be treated
// as test failures at "runtime", while still keeping a full manifest
// of all of the tests that ought to have run if the apply had run to
// completion.
proposed := req.ProposedNewState
res.PlannedState = proposed
componentName := proposed.GetAttr("component").AsString() // proven known during validate
p.mutex.Lock()
defer p.mutex.Unlock()
if _, exists := p.components[componentName]; exists {
res.Diagnostics = res.Diagnostics.Append(tfdiags.AttributeValue(
tfdiags.Error,
"Duplicate test component",
fmt.Sprintf("Another test_assertions resource already declared assertions for the component name %q.", componentName),
cty.GetAttrPath("component"),
))
return res
}
component := Component{
Assertions: make(map[string]*Assertion),
}
for it := proposed.GetAttr("equal").ElementIterator(); it.Next(); {
k, obj := it.Element()
name := k.AsString()
if _, exists := component.Assertions[name]; exists {
// We can't actually get here in practice because so far we've
// only been pulling keys from one map, and so any duplicates
// would've been caught during config decoding, but this is here
// just to make these two blocks symmetrical to avoid mishaps in
// future refactoring/reorganization.
res.Diagnostics = res.Diagnostics.Append(tfdiags.AttributeValue(
tfdiags.Error,
"Duplicate test assertion",
fmt.Sprintf("Another assertion block in this resource already declared an assertion named %q.", name),
cty.GetAttrPath("equal").Index(k),
))
continue
}
var desc string
descVal := obj.GetAttr("description")
if descVal.IsNull() {
descVal = cty.StringVal("")
}
err := gocty.FromCtyValue(descVal, &desc)
if err != nil {
// We shouldn't get here because we've already validated everything
// that would make FromCtyValue fail above and during validate.
res.Diagnostics = res.Diagnostics.Append(err)
}
component.Assertions[name] = &Assertion{
Outcome: Pending,
Description: desc,
}
}
for it := proposed.GetAttr("check").ElementIterator(); it.Next(); {
k, obj := it.Element()
name := k.AsString()
if _, exists := component.Assertions[name]; exists {
res.Diagnostics = res.Diagnostics.Append(tfdiags.AttributeValue(
tfdiags.Error,
"Duplicate test assertion",
fmt.Sprintf("Another assertion block in this resource already declared an assertion named %q.", name),
cty.GetAttrPath("check").Index(k),
))
continue
}
var desc string
descVal := obj.GetAttr("description")
if descVal.IsNull() {
descVal = cty.StringVal("")
}
err := gocty.FromCtyValue(descVal, &desc)
if err != nil {
// We shouldn't get here because we've already validated everything
// that would make FromCtyValue fail above and during validate.
res.Diagnostics = res.Diagnostics.Append(err)
}
component.Assertions[name] = &Assertion{
Outcome: Pending,
Description: desc,
}
}
p.components[componentName] = &component
return res
}
// ApplyResourceChange takes the planned state for a resource, which may
// yet contain unknown computed values, and applies the changes returning
// the final state.
func (p *Provider) ApplyResourceChange(req providers.ApplyResourceChangeRequest) providers.ApplyResourceChangeResponse {
var res providers.ApplyResourceChangeResponse
if req.TypeName != "test_assertions" { // we only have one resource type
res.Diagnostics = res.Diagnostics.Append(fmt.Errorf("unsupported resource type %s", req.TypeName))
return res
}
// During apply we actually check the assertions and record the results.
// An assertion failure isn't reflected as an error from the apply call
// because if possible we'd like to continue exercising other objects
// downstream in case that allows us to gather more information to report.
// (If something downstream returns an error then that could prevent us
// from completing other assertions, though.)
planned := req.PlannedState
res.NewState = planned
componentName := planned.GetAttr("component").AsString() // proven known during validate
p.mutex.Lock()
defer p.mutex.Unlock()
component := p.components[componentName]
if component == nil {
// We might get here when using this provider outside of the
// "terraform test" command, where there won't be any mechanism to
// preserve the test provider instance between the plan and apply
// phases. In that case, we assume that nobody will come looking to
// collect the results anyway, and so we can just silently skip
// checking.
return res
}
for it := planned.GetAttr("equal").ElementIterator(); it.Next(); {
k, obj := it.Element()
name := k.AsString()
var desc string
if plan, exists := component.Assertions[name]; exists {
desc = plan.Description
}
assert := &Assertion{
Outcome: Pending,
Description: desc,
}
gotVal := obj.GetAttr("got")
wantVal := obj.GetAttr("want")
switch {
case wantVal.RawEquals(gotVal):
assert.Outcome = Passed
gotStr := repl.FormatValue(gotVal, 4)
assert.Message = fmt.Sprintf("correct value\n got: %s\n", gotStr)
default:
assert.Outcome = Failed
gotStr := repl.FormatValue(gotVal, 4)
wantStr := repl.FormatValue(wantVal, 4)
assert.Message = fmt.Sprintf("wrong value\n got: %s\n want: %s\n", gotStr, wantStr)
}
component.Assertions[name] = assert
}
for it := planned.GetAttr("check").ElementIterator(); it.Next(); {
k, obj := it.Element()
name := k.AsString()
var desc string
if plan, exists := component.Assertions[name]; exists {
desc = plan.Description
}
assert := &Assertion{
Outcome: Pending,
Description: desc,
}
condVal := obj.GetAttr("condition")
switch {
case condVal.IsNull():
res.Diagnostics = res.Diagnostics.Append(tfdiags.AttributeValue(
tfdiags.Error,
"Invalid check condition",
"The condition value must be a boolean expression, not null.",
cty.GetAttrPath("check").Index(k).GetAttr("condition"),
))
continue
case condVal.True():
assert.Outcome = Passed
assert.Message = "condition passed"
default:
assert.Outcome = Failed
// For "check" we can't really return a decent error message
// because we've lost all of the context by the time we get here.
// "equal" will be better for most tests for that reason, and also
// this is one reason why in the long run it would be better for
// test assertions to be a first-class language feature rather than
// just a provider-based concept.
assert.Message = "condition failed"
}
component.Assertions[name] = assert
}
return res
}
// ImportResourceState requests that the given resource be imported.
func (p *Provider) ImportResourceState(req providers.ImportResourceStateRequest) providers.ImportResourceStateResponse {
var res providers.ImportResourceStateResponse
res.Diagnostics = res.Diagnostics.Append(fmt.Errorf("%s is not importable", req.TypeName))
return res
}
// ValidateDataSourceConfig is used to to validate the resource configuration values.
func (p *Provider) ValidateDataSourceConfig(req providers.ValidateDataSourceConfigRequest) providers.ValidateDataSourceConfigResponse {
// This provider has no data resouce types at all.
var res providers.ValidateDataSourceConfigResponse
res.Diagnostics = res.Diagnostics.Append(fmt.Errorf("unsupported data source %s", req.TypeName))
return res
}
// ReadDataSource returns the data source's current state.
func (p *Provider) ReadDataSource(req providers.ReadDataSourceRequest) providers.ReadDataSourceResponse {
// This provider has no data resouce types at all.
var res providers.ReadDataSourceResponse
res.Diagnostics = res.Diagnostics.Append(fmt.Errorf("unsupported data source %s", req.TypeName))
return res
}
// Stop is called when the provider should halt any in-flight actions.
func (p *Provider) Stop() error {
// This provider doesn't do anything that can be cancelled.
return nil
}
// Close is a noop for this provider, since it's run in-process.
func (p *Provider) Close() error {
return nil
}
var testAssertionsSchema = providers.Schema{
Block: &configschema.Block{
Attributes: map[string]*configschema.Attribute{
"component": {
Type: cty.String,
Description: "The name of the component being tested. This is just for namespacing assertions in a result report.",
DescriptionKind: configschema.StringPlain,
Required: true,
},
},
BlockTypes: map[string]*configschema.NestedBlock{
"equal": {
Nesting: configschema.NestingMap,
Block: configschema.Block{
Attributes: map[string]*configschema.Attribute{
"description": {
Type: cty.String,
Description: "An optional human-readable description of what's being tested by this assertion.",
DescriptionKind: configschema.StringPlain,
Required: true,
},
"got": {
Type: cty.DynamicPseudoType,
Description: "The actual result value generated by the relevant component.",
DescriptionKind: configschema.StringPlain,
Required: true,
},
"want": {
Type: cty.DynamicPseudoType,
Description: "The value that the component is expected to have generated.",
DescriptionKind: configschema.StringPlain,
Required: true,
},
},
},
},
"check": {
Nesting: configschema.NestingMap,
Block: configschema.Block{
Attributes: map[string]*configschema.Attribute{
"description": {
Type: cty.String,
Description: "An optional (but strongly recommended) human-readable description of what's being tested by this assertion.",
DescriptionKind: configschema.StringPlain,
Required: true,
},
"condition": {
Type: cty.Bool,
Description: "An expression that must be true in order for the test to pass.",
DescriptionKind: configschema.StringPlain,
Required: true,
},
},
},
},
},
},
}

View File

@ -0,0 +1,155 @@
package moduletest
import (
"testing"
"github.com/google/go-cmp/cmp"
"github.com/hashicorp/terraform/providers"
"github.com/zclconf/go-cty-debug/ctydebug"
"github.com/zclconf/go-cty/cty"
)
func TestProvider(t *testing.T) {
assertionConfig := cty.ObjectVal(map[string]cty.Value{
"component": cty.StringVal("spline_reticulator"),
"equal": cty.MapVal(map[string]cty.Value{
"match": cty.ObjectVal(map[string]cty.Value{
"description": cty.StringVal("this should match"),
"got": cty.StringVal("a"),
"want": cty.StringVal("a"),
}),
"unmatch": cty.ObjectVal(map[string]cty.Value{
"description": cty.StringVal("this should not match"),
"got": cty.StringVal("a"),
"want": cty.StringVal("b"),
}),
}),
"check": cty.MapVal(map[string]cty.Value{
"pass": cty.ObjectVal(map[string]cty.Value{
"description": cty.StringVal("this should pass"),
"condition": cty.True,
}),
"fail": cty.ObjectVal(map[string]cty.Value{
"description": cty.StringVal("this should fail"),
"condition": cty.False,
}),
}),
})
// The provider code expects to receive an object that was decoded from
// HCL using the schema, so to make sure we're testing a more realistic
// situation here we'll require the config to conform to the schema. If
// this fails, it's a bug in the configuration definition above rather
// than in the provider itself.
for _, err := range assertionConfig.Type().TestConformance(testAssertionsSchema.Block.ImpliedType()) {
t.Error(err)
}
p := NewProvider()
configureResp := p.Configure(providers.ConfigureRequest{
Config: cty.EmptyObjectVal,
})
if got, want := len(configureResp.Diagnostics), 1; got != want {
t.Fatalf("got %d Configure diagnostics, but want %d", got, want)
}
if got, want := configureResp.Diagnostics[0].Description().Summary, "The test provider is experimental"; got != want {
t.Fatalf("wrong diagnostic message\ngot: %s\nwant: %s", got, want)
}
validateResp := p.ValidateResourceTypeConfig(providers.ValidateResourceTypeConfigRequest{
TypeName: "test_assertions",
Config: assertionConfig,
})
if got, want := len(validateResp.Diagnostics), 0; got != want {
t.Fatalf("got %d ValidateResourceTypeConfig diagnostics, but want %d", got, want)
}
planResp := p.PlanResourceChange(providers.PlanResourceChangeRequest{
TypeName: "test_assertions",
Config: assertionConfig,
PriorState: cty.NullVal(assertionConfig.Type()),
ProposedNewState: assertionConfig,
})
if got, want := len(planResp.Diagnostics), 0; got != want {
t.Fatalf("got %d PlanResourceChange diagnostics, but want %d", got, want)
}
planned := planResp.PlannedState
if got, want := planned, assertionConfig; !want.RawEquals(got) {
t.Fatalf("wrong planned new value\n%s", ctydebug.DiffValues(want, got))
}
gotComponents := p.TestResults()
wantComponents := map[string]*Component{
"spline_reticulator": {
Assertions: map[string]*Assertion{
"pass": {
Outcome: Pending,
Description: "this should pass",
},
"fail": {
Outcome: Pending,
Description: "this should fail",
},
"match": {
Outcome: Pending,
Description: "this should match",
},
"unmatch": {
Outcome: Pending,
Description: "this should not match",
},
},
},
}
if diff := cmp.Diff(wantComponents, gotComponents); diff != "" {
t.Fatalf("wrong test results after planning\n%s", diff)
}
applyResp := p.ApplyResourceChange(providers.ApplyResourceChangeRequest{
TypeName: "test_assertions",
Config: assertionConfig,
PriorState: cty.NullVal(assertionConfig.Type()),
PlannedState: planned,
})
if got, want := len(applyResp.Diagnostics), 0; got != want {
t.Fatalf("got %d ApplyResourceChange diagnostics, but want %d", got, want)
}
final := applyResp.NewState
if got, want := final, assertionConfig; !want.RawEquals(got) {
t.Fatalf("wrong new value\n%s", ctydebug.DiffValues(want, got))
}
gotComponents = p.TestResults()
wantComponents = map[string]*Component{
"spline_reticulator": {
Assertions: map[string]*Assertion{
"pass": {
Outcome: Passed,
Description: "this should pass",
Message: "condition passed",
},
"fail": {
Outcome: Failed,
Description: "this should fail",
Message: "condition failed",
},
"match": {
Outcome: Passed,
Description: "this should match",
Message: "correct value\n got: \"a\"\n",
},
"unmatch": {
Outcome: Failed,
Description: "this should not match",
Message: "wrong value\n got: \"a\"\n want: \"b\"\n",
},
},
},
}
if diff := cmp.Diff(wantComponents, gotComponents); diff != "" {
t.Fatalf("wrong test results after applying\n%s", diff)
}
}