Merge pull request #19960 from hashicorp/jbardin/resource-test-shims

Resource test shims
This commit is contained in:
James Bardin 2019-01-10 13:30:26 -05:00 committed by GitHub
commit 21aadb0456
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 167 additions and 131 deletions

View File

@ -24,6 +24,14 @@ resource "test_resource_nested" "foo" {
}
}
`),
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttr(
"test_resource_nested.foo", "nested.#", "1",
),
resource.TestCheckResourceAttr(
"test_resource_nested.foo", "nested.1877647874.string", "val",
),
),
},
},
})
@ -31,7 +39,7 @@ resource "test_resource_nested" "foo" {
func TestResourceNested_addRemove(t *testing.T) {
var id string
checkFunc := func(s *terraform.State) error {
idCheck := func(s *terraform.State) error {
root := s.ModuleByPath(addrs.RootModuleInstance)
res := root.Resources["test_resource_nested.foo"]
if res.Primary.ID == id {
@ -49,7 +57,17 @@ func TestResourceNested_addRemove(t *testing.T) {
resource "test_resource_nested" "foo" {
}
`),
Check: checkFunc,
Check: resource.ComposeTestCheckFunc(
idCheck,
resource.TestCheckResourceAttr(
"test_resource_nested.foo", "nested.#", "0",
),
// Checking for a count of 0 and a nonexistent count should
// now be the same operation.
resource.TestCheckNoResourceAttr(
"test_resource_nested.foo", "nested.#",
),
),
},
resource.TestStep{
Config: strings.TrimSpace(`
@ -59,7 +77,12 @@ resource "test_resource_nested" "foo" {
}
}
`),
Check: checkFunc,
Check: resource.ComposeTestCheckFunc(
idCheck,
resource.TestCheckResourceAttr(
"test_resource_nested.foo", "nested.1877647874.string", "val",
),
),
},
resource.TestStep{
Config: strings.TrimSpace(`
@ -70,7 +93,15 @@ resource "test_resource_nested" "foo" {
}
}
`),
Check: checkFunc,
Check: resource.ComposeTestCheckFunc(
idCheck,
resource.TestCheckResourceAttr(
"test_resource_nested.foo", "nested.1877647874.string", "val",
),
resource.TestCheckResourceAttr(
"test_resource_nested.foo", "optional", "true",
),
),
},
resource.TestStep{
Config: strings.TrimSpace(`
@ -80,7 +111,15 @@ resource "test_resource_nested" "foo" {
}
}
`),
Check: checkFunc,
Check: resource.ComposeTestCheckFunc(
idCheck,
resource.TestCheckResourceAttr(
"test_resource_nested.foo", "nested.1877647874.string", "val",
),
resource.TestCheckNoResourceAttr(
"test_resource_nested.foo", "optional",
),
),
},
resource.TestStep{
Config: strings.TrimSpace(`
@ -91,14 +130,27 @@ resource "test_resource_nested" "foo" {
}
}
`),
Check: checkFunc,
Check: resource.ComposeTestCheckFunc(
idCheck,
resource.TestCheckResourceAttr(
"test_resource_nested.foo", "nested.2994502535.string", "val",
),
resource.TestCheckResourceAttr(
"test_resource_nested.foo", "nested.2994502535.optional", "true",
),
),
},
resource.TestStep{
Config: strings.TrimSpace(`
resource "test_resource_nested" "foo" {
}
`),
Check: checkFunc,
Check: resource.ComposeTestCheckFunc(
idCheck,
resource.TestCheckNoResourceAttr(
"test_resource_nested.foo", "nested.#",
),
),
},
},
})
@ -135,14 +187,14 @@ resource "test_resource_nested" "foo" {
got := rs.Primary.Attributes
want := map[string]string{
"nested.#": "2",
"nested.0.string": "a",
"nested.0.optional": "false",
"nested.0.nested_again.#": "1",
"nested.0.nested_again.0.string": "a",
"nested.1.string": "",
"nested.1.optional": "false",
"nested.1.nested_again.#": "0",
"nested.#": "2",
"nested.33842314.string": "a",
"nested.33842314.optional": "false",
"nested.33842314.nested_again.#": "1",
"nested.33842314.nested_again.936590934.string": "a",
"nested.140280279.string": "",
"nested.140280279.optional": "false",
"nested.140280279.nested_again.#": "0",
}
delete(got, "id") // it's random, so not useful for testing

View File

@ -4,25 +4,17 @@ import (
"fmt"
"github.com/hashicorp/terraform/addrs"
"github.com/hashicorp/terraform/configs/configschema"
"github.com/zclconf/go-cty/cty"
"github.com/hashicorp/terraform/config/hcl2shim"
"github.com/hashicorp/terraform/helper/schema"
"github.com/hashicorp/terraform/states"
"github.com/hashicorp/terraform/terraform"
)
func mustShimNewState(newState *states.State, schemas *terraform.Schemas) *terraform.State {
s, err := shimNewState(newState, schemas)
if err != nil {
panic(err)
}
return s
}
// shimState takes a new *states.State and reverts it to a legacy state for the provider ACC tests
func shimNewState(newState *states.State, schemas *terraform.Schemas) (*terraform.State, error) {
func shimNewState(newState *states.State, providers map[string]terraform.ResourceProvider) (*terraform.State, error) {
state := terraform.NewState()
// in the odd case of a nil state, let the helper packages handle it
@ -57,25 +49,10 @@ func shimNewState(newState *states.State, schemas *terraform.Schemas) (*terrafor
resType := res.Addr.Type
providerType := res.ProviderConfig.ProviderConfig.Type
providerSchema := schemas.Providers[providerType]
if providerSchema == nil {
return nil, fmt.Errorf("missing schema for %q", providerType)
}
var resSchema *configschema.Block
switch res.Addr.Mode {
case addrs.ManagedResourceMode:
resSchema = providerSchema.ResourceTypes[resType]
case addrs.DataResourceMode:
resSchema = providerSchema.DataSources[resType]
}
if resSchema == nil {
return nil, fmt.Errorf("missing resource schema for %q in %q", resType, providerType)
}
resource := getResource(providers, providerType, resType)
for key, i := range res.Instances {
flatmap, err := shimmedAttributes(i.Current, resSchema.ImpliedType())
flatmap, err := shimmedAttributes(i.Current, resource)
if err != nil {
return nil, fmt.Errorf("error decoding state for %q: %s", resType, err)
}
@ -114,7 +91,7 @@ func shimNewState(newState *states.State, schemas *terraform.Schemas) (*terrafor
// add any deposed instances
for _, dep := range i.Deposed {
flatmap, err := shimmedAttributes(dep, resSchema.ImpliedType())
flatmap, err := shimmedAttributes(dep, resource)
if err != nil {
return nil, fmt.Errorf("error decoding deposed state for %q: %s", resType, err)
}
@ -139,17 +116,46 @@ func shimNewState(newState *states.State, schemas *terraform.Schemas) (*terrafor
return state, nil
}
func shimmedAttributes(instance *states.ResourceInstanceObjectSrc, ty cty.Type) (map[string]string, error) {
func getResource(providers map[string]terraform.ResourceProvider, providerName, resourceType string) *schema.Resource {
p := providers[providerName]
if p == nil {
panic(fmt.Sprintf("provider %q not found in test step", providerName))
}
// this is only for tests, so should only see schema.Providers
provider := p.(*schema.Provider)
resource := provider.ResourcesMap[resourceType]
if resource != nil {
return resource
}
resource = provider.DataSourcesMap[resourceType]
if resource != nil {
return resource
}
panic(fmt.Sprintf("resource %s not found in test step", resourceType))
}
func shimmedAttributes(instance *states.ResourceInstanceObjectSrc, res *schema.Resource) (map[string]string, error) {
flatmap := instance.AttrsFlat
// if we have json attrs, they need to be decoded
if flatmap == nil {
rio, err := instance.Decode(ty)
if err != nil {
return nil, err
}
flatmap = hcl2shim.FlatmapValueFromHCL2(rio.Value)
if flatmap != nil {
return flatmap, nil
}
return flatmap, nil
// if we have json attrs, they need to be decoded
rio, err := instance.Decode(res.CoreConfigSchema().ImpliedType())
if err != nil {
return nil, err
}
instanceState, err := res.ShimInstanceStateFromValue(rio.Value)
if err != nil {
return nil, err
}
return instanceState.Attributes, nil
}

View File

@ -4,7 +4,7 @@ import (
"testing"
"github.com/hashicorp/terraform/addrs"
"github.com/hashicorp/terraform/configs/configschema"
"github.com/hashicorp/terraform/helper/schema"
"github.com/hashicorp/terraform/states"
"github.com/hashicorp/terraform/terraform"
"github.com/zclconf/go-cty/cty"
@ -286,46 +286,29 @@ func TestStateShim(t *testing.T) {
},
}
schemas := &terraform.Schemas{
Providers: map[string]*terraform.ProviderSchema{
"test": {
ResourceTypes: map[string]*configschema.Block{
"test_thing": &configschema.Block{
Attributes: map[string]*configschema.Attribute{
"id": {
Type: cty.String,
Computed: true,
},
"fizzle": {
Type: cty.String,
Optional: true,
},
"bazzle": {
Type: cty.String,
Optional: true,
},
},
providers := map[string]terraform.ResourceProvider{
"test": &schema.Provider{
ResourcesMap: map[string]*schema.Resource{
"test_thing": &schema.Resource{
Schema: map[string]*schema.Schema{
"id": {Type: schema.TypeString, Computed: true},
"fizzle": {Type: schema.TypeString, Optional: true},
"bazzle": {Type: schema.TypeString, Optional: true},
},
},
DataSources: map[string]*configschema.Block{
"test_data_thing": &configschema.Block{
Attributes: map[string]*configschema.Attribute{
"id": {
Type: cty.String,
Computed: true,
},
"fuzzle": {
Type: cty.String,
Optional: true,
},
},
},
DataSourcesMap: map[string]*schema.Resource{
"test_data_thing": &schema.Resource{
Schema: map[string]*schema.Schema{
"id": {Type: schema.TypeString, Computed: true},
"fuzzle": {Type: schema.TypeString, Optional: true},
},
},
},
},
}
shimmed, err := shimNewState(state, schemas)
shimmed, err := shimNewState(state, providers)
if err != nil {
t.Fatal(err)
}

View File

@ -382,6 +382,10 @@ type TestStep struct {
// be refreshed and don't matter.
ImportStateVerify bool
ImportStateVerifyIgnore []string
// provider s is used internally to maintain a reference to the
// underlying providers during the tests
providers map[string]terraform.ResourceProvider
}
// Set to a file mask in sprintf format where %s is test name
@ -476,6 +480,17 @@ func Test(t TestT, c TestCase) {
c.PreCheck()
}
// get instances of all providers, so we can use the individual
// resources to shim the state during the tests.
providers := make(map[string]terraform.ResourceProvider)
for name, pf := range testProviderFactories(c) {
p, err := pf()
if err != nil {
t.Fatal(err)
}
providers[name] = p
}
providerResolver, err := testProviderResolver(c)
if err != nil {
t.Fatal(err)
@ -491,6 +506,10 @@ func Test(t TestT, c TestCase) {
idRefresh := c.IDRefreshName != ""
errored := false
for i, step := range c.Steps {
// insert the providers into the step so we can get the resources for
// shimming the state
step.providers = providers
var err error
log.Printf("[DEBUG] Test: Executing step %d", i)
@ -600,6 +619,7 @@ func Test(t TestT, c TestCase) {
Destroy: true,
PreventDiskCleanup: lastStep.PreventDiskCleanup,
PreventPostDestroyRefresh: c.PreventPostDestroyRefresh,
providers: providers,
}
log.Printf("[WARN] Test: Executing destroy step")
@ -629,12 +649,10 @@ func testProviderConfig(c TestCase) string {
return strings.Join(lines, "")
}
// testProviderResolver is a helper to build a ResourceProviderResolver
// with pre instantiated ResourceProviders, so that we can reset them for the
// test, while only calling the factory function once.
// Any errors are stored so that they can be returned by the factory in
// terraform to match non-test behavior.
func testProviderResolver(c TestCase) (providers.Resolver, error) {
// testProviderFactories combines the fixed Providers and
// ResourceProviderFactory functions into a single map of
// ResourceProviderFactory functions.
func testProviderFactories(c TestCase) map[string]terraform.ResourceProviderFactory {
ctxProviders := make(map[string]terraform.ResourceProviderFactory)
for k, pf := range c.ProviderFactories {
ctxProviders[k] = pf
@ -644,6 +662,16 @@ func testProviderResolver(c TestCase) (providers.Resolver, error) {
for k, p := range c.Providers {
ctxProviders[k] = terraform.ResourceProviderFactoryFixed(p)
}
return ctxProviders
}
// testProviderResolver is a helper to build a ResourceProviderResolver
// with pre instantiated ResourceProviders, so that we can reset them for the
// test, while only calling the factory function once.
// Any errors are stored so that they can be returned by the factory in
// terraform to match non-test behavior.
func testProviderResolver(c TestCase) (providers.Resolver, error) {
ctxProviders := testProviderFactories(c)
// wrap the old provider factories in the test grpc server so they can be
// called from terraform.
@ -667,32 +695,6 @@ func testProviderResolver(c TestCase) (providers.Resolver, error) {
return providers.ResolverFixed(newProviders), nil
}
// testProviderFactores returns a fixed and reset factories for creating a resolver
func testProviderFactories(c TestCase) (map[string]providers.Factory, error) {
factories := c.ProviderFactories
if factories == nil {
factories = make(map[string]terraform.ResourceProviderFactory)
}
// add any fixed providers
for k, p := range c.Providers {
factories[k] = terraform.ResourceProviderFactoryFixed(p)
}
// wrap the providers to be GRPC mocks rather than legacy terraform.ResourceProvider
newFactories := make(map[string]providers.Factory)
for k, pf := range factories {
newFactories[k] = func() (providers.Interface, error) {
p, err := pf()
if err != nil {
return nil, err
}
return GRPCTestProvider(p), nil
}
}
return newFactories, nil
}
// UnitTest is a helper to force the acceptance testing harness to run in the
// normal unit test suite. This should only be used for resource that don't
// have any external dependencies.

View File

@ -62,14 +62,11 @@ func testStep(opts terraform.ContextOpts, state *terraform.State, step TestStep)
log.Printf("[WARN] Config warnings:\n%s", stepDiags)
}
// We will need access to the schemas in order to shim to the old-style
// testing API.
schemas := ctx.Schemas()
// Refresh!
newState, stepDiags := ctx.Refresh()
// shim the state first so the test can check the state on errors
state, err = shimNewState(newState, schemas)
state, err = shimNewState(newState, step.providers)
if err != nil {
return nil, err
}
@ -95,7 +92,7 @@ func testStep(opts terraform.ContextOpts, state *terraform.State, step TestStep)
// Apply the diff, creating real resources.
newState, stepDiags = ctx.Apply()
// shim the state first so the test can check the state on errors
state, err = shimNewState(newState, schemas)
state, err = shimNewState(newState, step.providers)
if err != nil {
return nil, err
}
@ -139,7 +136,7 @@ func testStep(opts terraform.ContextOpts, state *terraform.State, step TestStep)
return state, newOperationError("follow-up refresh", stepDiags)
}
state, err = shimNewState(newState, schemas)
state, err = shimNewState(newState, step.providers)
if err != nil {
return nil, err
}
@ -190,7 +187,7 @@ func testStep(opts terraform.ContextOpts, state *terraform.State, step TestStep)
//
// This is here only for compatibility with existing tests that predate our
// new plan and state types, and should not be used in new tests. Instead, use
// a library like "cmp" to do a deep equality check and diff on the two
// a library like "cmp" to do a deep equality and diff on the two
// data structures.
func legacyPlanComparisonString(state *states.State, changes *plans.Changes) string {
return fmt.Sprintf(

View File

@ -62,10 +62,6 @@ func testStepImportState(
return state, stepDiags.Err()
}
// We will need access to the schemas in order to shim to the old-style
// testing API.
schemas := ctx.Schemas()
// The test step provides the resource address as a string, so we need
// to parse it to get an addrs.AbsResourceAddress to pass in to the
// import method.
@ -95,7 +91,7 @@ func testStepImportState(
return state, stepDiags.Err()
}
newState, err := shimNewState(importedState, schemas)
newState, err := shimNewState(importedState, step.providers)
if err != nil {
return nil, err
}