Merge pull request #12498 from hashicorp/jbardin/test-reset
Add schema.Provider.TestReset to reset StopContext between tests
This commit is contained in:
commit
ecb1944c31
|
@ -22,6 +22,13 @@ import (
|
||||||
|
|
||||||
const TestEnvVar = "TF_ACC"
|
const TestEnvVar = "TF_ACC"
|
||||||
|
|
||||||
|
// TestProvider can be implemented by any ResourceProvider to provide custom
|
||||||
|
// reset functionality at the start of an acceptance test.
|
||||||
|
// The helper/schema Provider implements this interface.
|
||||||
|
type TestProvider interface {
|
||||||
|
TestReset() error
|
||||||
|
}
|
||||||
|
|
||||||
// TestCheckFunc is the callback type used with acceptance tests to check
|
// TestCheckFunc is the callback type used with acceptance tests to check
|
||||||
// the state of a resource. The state passed in is the latest state known,
|
// the state of a resource. The state passed in is the latest state known,
|
||||||
// or in the case of being after a destroy, it is the last known state when
|
// or in the case of being after a destroy, it is the last known state when
|
||||||
|
@ -216,13 +223,9 @@ func Test(t TestT, c TestCase) {
|
||||||
c.PreCheck()
|
c.PreCheck()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build our context options that we can
|
ctxProviders, err := testProviderFactories(c)
|
||||||
ctxProviders := c.ProviderFactories
|
if err != nil {
|
||||||
if ctxProviders == nil {
|
t.Fatal(err)
|
||||||
ctxProviders = make(map[string]terraform.ResourceProviderFactory)
|
|
||||||
for k, p := range c.Providers {
|
|
||||||
ctxProviders[k] = terraform.ResourceProviderFactoryFixed(p)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
opts := terraform.ContextOpts{Providers: ctxProviders}
|
opts := terraform.ContextOpts{Providers: ctxProviders}
|
||||||
|
|
||||||
|
@ -333,6 +336,43 @@ func Test(t TestT, c TestCase) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// testProviderFactories is a helper to build the ResourceProviderFactory map
|
||||||
|
// 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 testProviderFactories(c TestCase) (map[string]terraform.ResourceProviderFactory, error) {
|
||||||
|
ctxProviders := make(map[string]terraform.ResourceProviderFactory)
|
||||||
|
|
||||||
|
// add any fixed providers
|
||||||
|
for k, p := range c.Providers {
|
||||||
|
ctxProviders[k] = terraform.ResourceProviderFactoryFixed(p)
|
||||||
|
}
|
||||||
|
|
||||||
|
// call any factory functions and store the result.
|
||||||
|
for k, pf := range c.ProviderFactories {
|
||||||
|
p, err := pf()
|
||||||
|
ctxProviders[k] = func() (terraform.ResourceProvider, error) {
|
||||||
|
return p, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// reset the providers if needed
|
||||||
|
for k, pf := range ctxProviders {
|
||||||
|
// we can ignore any errors here, if we don't have a provider to reset
|
||||||
|
// the error will be handled later
|
||||||
|
p, _ := pf()
|
||||||
|
if p, ok := p.(TestProvider); ok {
|
||||||
|
err := p.TestReset()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("[ERROR] failed to reset provider %q: %s", k, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ctxProviders, nil
|
||||||
|
}
|
||||||
|
|
||||||
// UnitTest is a helper to force the acceptance testing harness to run in the
|
// 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
|
// normal unit test suite. This should only be used for resource that don't
|
||||||
// have any external dependencies.
|
// have any external dependencies.
|
||||||
|
|
|
@ -4,7 +4,9 @@ import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
@ -25,8 +27,26 @@ func init() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// wrap the mock provider to implement TestProvider
|
||||||
|
type resetProvider struct {
|
||||||
|
*terraform.MockResourceProvider
|
||||||
|
mu sync.Mutex
|
||||||
|
TestResetCalled bool
|
||||||
|
TestResetError error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *resetProvider) TestReset() error {
|
||||||
|
p.mu.Lock()
|
||||||
|
defer p.mu.Unlock()
|
||||||
|
p.TestResetCalled = true
|
||||||
|
return p.TestResetError
|
||||||
|
}
|
||||||
|
|
||||||
func TestTest(t *testing.T) {
|
func TestTest(t *testing.T) {
|
||||||
mp := testProvider()
|
mp := &resetProvider{
|
||||||
|
MockResourceProvider: testProvider(),
|
||||||
|
}
|
||||||
|
|
||||||
mp.DiffReturn = nil
|
mp.DiffReturn = nil
|
||||||
|
|
||||||
mp.ApplyFn = func(
|
mp.ApplyFn = func(
|
||||||
|
@ -95,6 +115,9 @@ func TestTest(t *testing.T) {
|
||||||
if !checkDestroy {
|
if !checkDestroy {
|
||||||
t.Fatal("didn't call check for destroy")
|
t.Fatal("didn't call check for destroy")
|
||||||
}
|
}
|
||||||
|
if !mp.TestResetCalled {
|
||||||
|
t.Fatal("didn't call TestReset")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestTest_idRefresh(t *testing.T) {
|
func TestTest_idRefresh(t *testing.T) {
|
||||||
|
@ -355,6 +378,53 @@ func TestTest_stepError(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestTest_factoryError(t *testing.T) {
|
||||||
|
resourceFactoryError := fmt.Errorf("resource factory error")
|
||||||
|
|
||||||
|
factory := func() (terraform.ResourceProvider, error) {
|
||||||
|
return nil, resourceFactoryError
|
||||||
|
}
|
||||||
|
|
||||||
|
mt := new(mockT)
|
||||||
|
Test(mt, TestCase{
|
||||||
|
ProviderFactories: map[string]terraform.ResourceProviderFactory{
|
||||||
|
"test": factory,
|
||||||
|
},
|
||||||
|
Steps: []TestStep{
|
||||||
|
TestStep{
|
||||||
|
ExpectError: regexp.MustCompile("resource factory error"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
if !mt.failed() {
|
||||||
|
t.Fatal("test should've failed")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTest_resetError(t *testing.T) {
|
||||||
|
mp := &resetProvider{
|
||||||
|
MockResourceProvider: testProvider(),
|
||||||
|
TestResetError: fmt.Errorf("provider reset error"),
|
||||||
|
}
|
||||||
|
|
||||||
|
mt := new(mockT)
|
||||||
|
Test(mt, TestCase{
|
||||||
|
Providers: map[string]terraform.ResourceProvider{
|
||||||
|
"test": mp,
|
||||||
|
},
|
||||||
|
Steps: []TestStep{
|
||||||
|
TestStep{
|
||||||
|
ExpectError: regexp.MustCompile("provider reset error"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
if !mt.failed() {
|
||||||
|
t.Fatal("test should've failed")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestComposeAggregateTestCheckFunc(t *testing.T) {
|
func TestComposeAggregateTestCheckFunc(t *testing.T) {
|
||||||
check1 := func(s *terraform.State) error {
|
check1 := func(s *terraform.State) error {
|
||||||
return errors.New("Error 1")
|
return errors.New("Error 1")
|
||||||
|
|
|
@ -50,8 +50,15 @@ type Provider struct {
|
||||||
// See the ConfigureFunc documentation for more information.
|
// See the ConfigureFunc documentation for more information.
|
||||||
ConfigureFunc ConfigureFunc
|
ConfigureFunc ConfigureFunc
|
||||||
|
|
||||||
|
// MetaReset is called by TestReset to reset any state stored in the meta
|
||||||
|
// interface. This is especially important if the StopContext is stored by
|
||||||
|
// the provider.
|
||||||
|
MetaReset func() error
|
||||||
|
|
||||||
meta interface{}
|
meta interface{}
|
||||||
|
|
||||||
|
// a mutex is required because TestReset can directly repalce the stopCtx
|
||||||
|
stopMu sync.Mutex
|
||||||
stopCtx context.Context
|
stopCtx context.Context
|
||||||
stopCtxCancel context.CancelFunc
|
stopCtxCancel context.CancelFunc
|
||||||
stopOnce sync.Once
|
stopOnce sync.Once
|
||||||
|
@ -124,20 +131,43 @@ func (p *Provider) Stopped() bool {
|
||||||
// StopCh returns a channel that is closed once the provider is stopped.
|
// StopCh returns a channel that is closed once the provider is stopped.
|
||||||
func (p *Provider) StopContext() context.Context {
|
func (p *Provider) StopContext() context.Context {
|
||||||
p.stopOnce.Do(p.stopInit)
|
p.stopOnce.Do(p.stopInit)
|
||||||
|
|
||||||
|
p.stopMu.Lock()
|
||||||
|
defer p.stopMu.Unlock()
|
||||||
|
|
||||||
return p.stopCtx
|
return p.stopCtx
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Provider) stopInit() {
|
func (p *Provider) stopInit() {
|
||||||
|
p.stopMu.Lock()
|
||||||
|
defer p.stopMu.Unlock()
|
||||||
|
|
||||||
p.stopCtx, p.stopCtxCancel = context.WithCancel(context.Background())
|
p.stopCtx, p.stopCtxCancel = context.WithCancel(context.Background())
|
||||||
}
|
}
|
||||||
|
|
||||||
// Stop implementation of terraform.ResourceProvider interface.
|
// Stop implementation of terraform.ResourceProvider interface.
|
||||||
func (p *Provider) Stop() error {
|
func (p *Provider) Stop() error {
|
||||||
p.stopOnce.Do(p.stopInit)
|
p.stopOnce.Do(p.stopInit)
|
||||||
|
|
||||||
|
p.stopMu.Lock()
|
||||||
|
defer p.stopMu.Unlock()
|
||||||
|
|
||||||
p.stopCtxCancel()
|
p.stopCtxCancel()
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestReset resets any state stored in the Provider, and will call TestReset
|
||||||
|
// on Meta if it implements the TestProvider interface.
|
||||||
|
// This may be used to reset the schema.Provider at the start of a test, and is
|
||||||
|
// automatically called by resource.Test.
|
||||||
|
func (p *Provider) TestReset() error {
|
||||||
|
p.stopInit()
|
||||||
|
if p.MetaReset != nil {
|
||||||
|
return p.MetaReset()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// Input implementation of terraform.ResourceProvider interface.
|
// Input implementation of terraform.ResourceProvider interface.
|
||||||
func (p *Provider) Input(
|
func (p *Provider) Input(
|
||||||
input terraform.UIInput,
|
input terraform.UIInput,
|
||||||
|
|
|
@ -381,3 +381,29 @@ func TestProviderStop_stopFirst(t *testing.T) {
|
||||||
t.Fatal("should be stopped")
|
t.Fatal("should be stopped")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestProviderReset(t *testing.T) {
|
||||||
|
var p Provider
|
||||||
|
stopCtx := p.StopContext()
|
||||||
|
p.MetaReset = func() error {
|
||||||
|
stopCtx = p.StopContext()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// cancel the current context
|
||||||
|
p.Stop()
|
||||||
|
|
||||||
|
if err := p.TestReset(); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// the first context should have been replaced
|
||||||
|
if err := stopCtx.Err(); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// we should not get a canceled context here either
|
||||||
|
if err := p.StopContext().Err(); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -781,15 +781,14 @@ func (c *Context) walk(
|
||||||
}
|
}
|
||||||
|
|
||||||
// Watch for a stop so we can call the provider Stop() API.
|
// Watch for a stop so we can call the provider Stop() API.
|
||||||
doneCh := make(chan struct{})
|
watchStop, watchWait := c.watchStop(walker)
|
||||||
stopCh := c.runContext.Done()
|
|
||||||
go c.watchStop(walker, doneCh, stopCh)
|
|
||||||
|
|
||||||
// Walk the real graph, this will block until it completes
|
// Walk the real graph, this will block until it completes
|
||||||
realErr := graph.Walk(walker)
|
realErr := graph.Walk(walker)
|
||||||
|
|
||||||
// Close the done channel so the watcher stops
|
// Close the channel so the watcher stops, and wait for it to return.
|
||||||
close(doneCh)
|
close(watchStop)
|
||||||
|
<-watchWait
|
||||||
|
|
||||||
// If we have a shadow graph and we interrupted the real graph, then
|
// If we have a shadow graph and we interrupted the real graph, then
|
||||||
// we just close the shadow and never verify it. It is non-trivial to
|
// we just close the shadow and never verify it. It is non-trivial to
|
||||||
|
@ -878,52 +877,74 @@ func (c *Context) walk(
|
||||||
return walker, realErr
|
return walker, realErr
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Context) watchStop(walker *ContextGraphWalker, doneCh, stopCh <-chan struct{}) {
|
// watchStop immediately returns a `stop` and a `wait` chan after dispatching
|
||||||
// Wait for a stop or completion
|
// the watchStop goroutine. This will watch the runContext for cancellation and
|
||||||
select {
|
// stop the providers accordingly. When the watch is no longer needed, the
|
||||||
case <-stopCh:
|
// `stop` chan should be closed before waiting on the `wait` chan.
|
||||||
// Stop was triggered. Fall out of the select
|
// The `wait` chan is important, because without synchronizing with the end of
|
||||||
case <-doneCh:
|
// the watchStop goroutine, the runContext may also be closed during the select
|
||||||
// Done, just exit completely
|
// incorrectly causing providers to be stopped. Even if the graph walk is done
|
||||||
return
|
// at that point, stopping a provider permanently cancels its StopContext which
|
||||||
}
|
// can cause later actions to fail.
|
||||||
|
func (c *Context) watchStop(walker *ContextGraphWalker) (chan struct{}, <-chan struct{}) {
|
||||||
|
stop := make(chan struct{})
|
||||||
|
wait := make(chan struct{})
|
||||||
|
|
||||||
// If we're here, we're stopped, trigger the call.
|
// get the runContext cancellation channel now, because releaseRun will
|
||||||
|
// write to the runContext field.
|
||||||
|
done := c.runContext.Done()
|
||||||
|
|
||||||
{
|
go func() {
|
||||||
// Copy the providers so that a misbehaved blocking Stop doesn't
|
defer close(wait)
|
||||||
// completely hang Terraform.
|
// Wait for a stop or completion
|
||||||
walker.providerLock.Lock()
|
select {
|
||||||
ps := make([]ResourceProvider, 0, len(walker.providerCache))
|
case <-done:
|
||||||
for _, p := range walker.providerCache {
|
// done means the context was canceled, so we need to try and stop
|
||||||
ps = append(ps, p)
|
// providers.
|
||||||
|
case <-stop:
|
||||||
|
// our own stop channel was closed.
|
||||||
|
return
|
||||||
}
|
}
|
||||||
defer walker.providerLock.Unlock()
|
|
||||||
|
|
||||||
for _, p := range ps {
|
// If we're here, we're stopped, trigger the call.
|
||||||
// We ignore the error for now since there isn't any reasonable
|
|
||||||
// action to take if there is an error here, since the stop is still
|
|
||||||
// advisory: Terraform will exit once the graph node completes.
|
|
||||||
p.Stop()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
{
|
||||||
// Call stop on all the provisioners
|
// Copy the providers so that a misbehaved blocking Stop doesn't
|
||||||
walker.provisionerLock.Lock()
|
// completely hang Terraform.
|
||||||
ps := make([]ResourceProvisioner, 0, len(walker.provisionerCache))
|
walker.providerLock.Lock()
|
||||||
for _, p := range walker.provisionerCache {
|
ps := make([]ResourceProvider, 0, len(walker.providerCache))
|
||||||
ps = append(ps, p)
|
for _, p := range walker.providerCache {
|
||||||
}
|
ps = append(ps, p)
|
||||||
defer walker.provisionerLock.Unlock()
|
}
|
||||||
|
defer walker.providerLock.Unlock()
|
||||||
|
|
||||||
for _, p := range ps {
|
for _, p := range ps {
|
||||||
// We ignore the error for now since there isn't any reasonable
|
// We ignore the error for now since there isn't any reasonable
|
||||||
// action to take if there is an error here, since the stop is still
|
// action to take if there is an error here, since the stop is still
|
||||||
// advisory: Terraform will exit once the graph node completes.
|
// advisory: Terraform will exit once the graph node completes.
|
||||||
p.Stop()
|
p.Stop()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
{
|
||||||
|
// Call stop on all the provisioners
|
||||||
|
walker.provisionerLock.Lock()
|
||||||
|
ps := make([]ResourceProvisioner, 0, len(walker.provisionerCache))
|
||||||
|
for _, p := range walker.provisionerCache {
|
||||||
|
ps = append(ps, p)
|
||||||
|
}
|
||||||
|
defer walker.provisionerLock.Unlock()
|
||||||
|
|
||||||
|
for _, p := range ps {
|
||||||
|
// We ignore the error for now since there isn't any reasonable
|
||||||
|
// action to take if there is an error here, since the stop is still
|
||||||
|
// advisory: Terraform will exit once the graph node completes.
|
||||||
|
p.Stop()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
return stop, wait
|
||||||
}
|
}
|
||||||
|
|
||||||
// parseVariableAsHCL parses the value of a single variable as would have been specified
|
// parseVariableAsHCL parses the value of a single variable as would have been specified
|
||||||
|
|
Loading…
Reference in New Issue