Skip to content

Commit 09a236a

Browse files
authored
helper/resource: Support TestStep provider handling (#972)
Reference: #253 Reference: #628 Reference: #779 Provider developers can now select whether to configure providers for acceptance testing at the `TestCase` or `TestStep` level. Only one level may be used in this current implementation, however it may be possible to allow merged `TestCase` and `TestStep` configuration with additional validation logic to ensure a single provider is not specified multiple times across the merge result of all fields. This change also introduces some upfront `TestCase` and `TestStep` configuration validation when calling any of the `Test` functions, failing the test early if a problem is detected. There are other validations that are possible, however these are considered out of scope.
1 parent f11cd84 commit 09a236a

17 files changed

+2077
-238
lines changed

.changelog/972.txt

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
```release-note:note
2+
helper/resource: Provider references or external installation can now be handled at either the `TestCase` or `TestStep` level. Using the `TestStep` handling, advanced use cases are now enabled such as state upgrade acceptance testing.
3+
```
4+
5+
```release-note:enhancement
6+
helper/resource: Added `TestStep` type `ExternalProviders`, `ProtoV5ProviderFactories`, `ProtoV6ProviderFactories`, and `ProviderFactories` fields
7+
```
8+
9+
```release-note:bug
10+
helper/resource: Removed extraneous `terraform state show` command when not using the `TestStep` type `Taint` field
11+
```

helper/resource/plugin.go

Lines changed: 95 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,17 +19,108 @@ import (
1919
testing "github.com/mitchellh/go-testing-interface"
2020
)
2121

22+
// protov5ProviderFactory is a function which is called to start a protocol
23+
// version 5 provider server.
24+
type protov5ProviderFactory func() (tfprotov5.ProviderServer, error)
25+
26+
// protov5ProviderFactories is a mapping of provider addresses to provider
27+
// factory for protocol version 5 provider servers.
28+
type protov5ProviderFactories map[string]func() (tfprotov5.ProviderServer, error)
29+
30+
// merge combines provider factories.
31+
//
32+
// In case of an overlapping entry, the later entry will overwrite the previous
33+
// value.
34+
func (pf protov5ProviderFactories) merge(otherPfs ...protov5ProviderFactories) protov5ProviderFactories {
35+
result := make(protov5ProviderFactories)
36+
37+
for name, providerFactory := range pf {
38+
result[name] = providerFactory
39+
}
40+
41+
for _, otherPf := range otherPfs {
42+
for name, providerFactory := range otherPf {
43+
result[name] = providerFactory
44+
}
45+
}
46+
47+
return result
48+
}
49+
50+
// protov6ProviderFactory is a function which is called to start a protocol
51+
// version 6 provider server.
52+
type protov6ProviderFactory func() (tfprotov6.ProviderServer, error)
53+
54+
// protov6ProviderFactories is a mapping of provider addresses to provider
55+
// factory for protocol version 6 provider servers.
56+
type protov6ProviderFactories map[string]func() (tfprotov6.ProviderServer, error)
57+
58+
// merge combines provider factories.
59+
//
60+
// In case of an overlapping entry, the later entry will overwrite the previous
61+
// value.
62+
func (pf protov6ProviderFactories) merge(otherPfs ...protov6ProviderFactories) protov6ProviderFactories {
63+
result := make(protov6ProviderFactories)
64+
65+
for name, providerFactory := range pf {
66+
result[name] = providerFactory
67+
}
68+
69+
for _, otherPf := range otherPfs {
70+
for name, providerFactory := range otherPf {
71+
result[name] = providerFactory
72+
}
73+
}
74+
75+
return result
76+
}
77+
78+
// sdkProviderFactory is a function which is called to start a SDK provider
79+
// server.
80+
type sdkProviderFactory func() (*schema.Provider, error)
81+
82+
// protov6ProviderFactories is a mapping of provider addresses to provider
83+
// factory for protocol version 6 provider servers.
84+
type sdkProviderFactories map[string]func() (*schema.Provider, error)
85+
86+
// merge combines provider factories.
87+
//
88+
// In case of an overlapping entry, the later entry will overwrite the previous
89+
// value.
90+
func (pf sdkProviderFactories) merge(otherPfs ...sdkProviderFactories) sdkProviderFactories {
91+
result := make(sdkProviderFactories)
92+
93+
for name, providerFactory := range pf {
94+
result[name] = providerFactory
95+
}
96+
97+
for _, otherPf := range otherPfs {
98+
for name, providerFactory := range otherPf {
99+
result[name] = providerFactory
100+
}
101+
}
102+
103+
return result
104+
}
105+
22106
type providerFactories struct {
23-
legacy map[string]func() (*schema.Provider, error)
24-
protov5 map[string]func() (tfprotov5.ProviderServer, error)
25-
protov6 map[string]func() (tfprotov6.ProviderServer, error)
107+
legacy sdkProviderFactories
108+
protov5 protov5ProviderFactories
109+
protov6 protov6ProviderFactories
26110
}
27111

28-
func runProviderCommand(ctx context.Context, t testing.T, f func() error, wd *plugintest.WorkingDir, factories providerFactories) error {
112+
func runProviderCommand(ctx context.Context, t testing.T, f func() error, wd *plugintest.WorkingDir, factories *providerFactories) error {
29113
// don't point to this as a test failure location
30114
// point to whatever called it
31115
t.Helper()
32116

117+
// This should not happen, but prevent panics just in case.
118+
if factories == nil {
119+
err := fmt.Errorf("Provider factories are missing to run Terraform command. Please report this bug in the testing framework.")
120+
logging.HelperResourceError(ctx, err.Error())
121+
return err
122+
}
123+
33124
// Run the providers in the same process as the test runner using the
34125
// reattach behavior in Terraform. This ensures we get test coverage
35126
// and enables the use of delve as a debugger.

helper/resource/plugin_test.go

Lines changed: 236 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,236 @@
1+
package resource
2+
3+
import (
4+
"fmt"
5+
"testing"
6+
7+
"github.com/google/go-cmp/cmp"
8+
"github.com/hashicorp/terraform-plugin-go/tfprotov5"
9+
"github.com/hashicorp/terraform-plugin-go/tfprotov6"
10+
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
11+
)
12+
13+
func TestProtoV5ProviderFactoriesMerge(t *testing.T) {
14+
t.Parallel()
15+
16+
testProviderFactory1 := func() (tfprotov5.ProviderServer, error) {
17+
return nil, nil
18+
}
19+
testProviderFactory2 := func() (tfprotov5.ProviderServer, error) {
20+
return nil, nil
21+
}
22+
23+
// Function pointers do not play well with go-cmp, so convert these
24+
// into their stringified address for comparison.
25+
transformer := cmp.Transformer(
26+
"protov5ProviderFactory",
27+
func(pf protov5ProviderFactory) string {
28+
return fmt.Sprintf("%v", pf)
29+
},
30+
)
31+
32+
testCases := map[string]struct {
33+
pf protov5ProviderFactories
34+
others []protov5ProviderFactories
35+
expected protov5ProviderFactories
36+
}{
37+
"no-overlap": {
38+
pf: protov5ProviderFactories{
39+
"test1": testProviderFactory1,
40+
},
41+
others: []protov5ProviderFactories{
42+
{
43+
"test2": testProviderFactory1,
44+
},
45+
{
46+
"test3": testProviderFactory1,
47+
},
48+
},
49+
expected: protov5ProviderFactories{
50+
"test1": testProviderFactory1,
51+
"test2": testProviderFactory1,
52+
"test3": testProviderFactory1,
53+
},
54+
},
55+
"overlap": {
56+
pf: protov5ProviderFactories{
57+
"test": testProviderFactory1,
58+
},
59+
others: []protov5ProviderFactories{
60+
{
61+
"test": testProviderFactory1,
62+
},
63+
{
64+
"test": testProviderFactory2,
65+
},
66+
},
67+
expected: protov5ProviderFactories{
68+
"test": testProviderFactory2,
69+
},
70+
},
71+
}
72+
73+
for name, testCase := range testCases {
74+
name, testCase := name, testCase
75+
76+
t.Run(name, func(t *testing.T) {
77+
t.Parallel()
78+
79+
got := testCase.pf.merge(testCase.others...)
80+
81+
if diff := cmp.Diff(got, testCase.expected, transformer); diff != "" {
82+
t.Errorf("unexpected difference: %s", diff)
83+
}
84+
})
85+
}
86+
}
87+
88+
func TestProtoV6ProviderFactoriesMerge(t *testing.T) {
89+
t.Parallel()
90+
91+
testProviderFactory1 := func() (tfprotov6.ProviderServer, error) {
92+
return nil, nil
93+
}
94+
testProviderFactory2 := func() (tfprotov6.ProviderServer, error) {
95+
return nil, nil
96+
}
97+
98+
// Function pointers do not play well with go-cmp, so convert these
99+
// into their stringified address for comparison.
100+
transformer := cmp.Transformer(
101+
"protov6ProviderFactory",
102+
func(pf protov6ProviderFactory) string {
103+
return fmt.Sprintf("%v", pf)
104+
},
105+
)
106+
107+
testCases := map[string]struct {
108+
pf protov6ProviderFactories
109+
others []protov6ProviderFactories
110+
expected protov6ProviderFactories
111+
}{
112+
"no-overlap": {
113+
pf: protov6ProviderFactories{
114+
"test1": testProviderFactory1,
115+
},
116+
others: []protov6ProviderFactories{
117+
{
118+
"test2": testProviderFactory1,
119+
},
120+
{
121+
"test3": testProviderFactory1,
122+
},
123+
},
124+
expected: protov6ProviderFactories{
125+
"test1": testProviderFactory1,
126+
"test2": testProviderFactory1,
127+
"test3": testProviderFactory1,
128+
},
129+
},
130+
"overlap": {
131+
pf: protov6ProviderFactories{
132+
"test": testProviderFactory1,
133+
},
134+
others: []protov6ProviderFactories{
135+
{
136+
"test": testProviderFactory1,
137+
},
138+
{
139+
"test": testProviderFactory2,
140+
},
141+
},
142+
expected: protov6ProviderFactories{
143+
"test": testProviderFactory2,
144+
},
145+
},
146+
}
147+
148+
for name, testCase := range testCases {
149+
name, testCase := name, testCase
150+
151+
t.Run(name, func(t *testing.T) {
152+
t.Parallel()
153+
154+
got := testCase.pf.merge(testCase.others...)
155+
156+
if diff := cmp.Diff(got, testCase.expected, transformer); diff != "" {
157+
t.Errorf("unexpected difference: %s", diff)
158+
}
159+
})
160+
}
161+
}
162+
163+
func TestSdkProviderFactoriesMerge(t *testing.T) {
164+
t.Parallel()
165+
166+
testProviderFactory1 := func() (*schema.Provider, error) {
167+
return nil, nil
168+
}
169+
testProviderFactory2 := func() (*schema.Provider, error) {
170+
return nil, nil
171+
}
172+
173+
// Function pointers do not play well with go-cmp, so convert these
174+
// into their stringified address for comparison.
175+
transformer := cmp.Transformer(
176+
"sdkProviderFactory",
177+
func(pf sdkProviderFactory) string {
178+
return fmt.Sprintf("%v", pf)
179+
},
180+
)
181+
182+
testCases := map[string]struct {
183+
pf sdkProviderFactories
184+
others []sdkProviderFactories
185+
expected sdkProviderFactories
186+
}{
187+
"no-overlap": {
188+
pf: sdkProviderFactories{
189+
"test1": testProviderFactory1,
190+
},
191+
others: []sdkProviderFactories{
192+
{
193+
"test2": testProviderFactory1,
194+
},
195+
{
196+
"test3": testProviderFactory1,
197+
},
198+
},
199+
expected: sdkProviderFactories{
200+
"test1": testProviderFactory1,
201+
"test2": testProviderFactory1,
202+
"test3": testProviderFactory1,
203+
},
204+
},
205+
"overlap": {
206+
pf: sdkProviderFactories{
207+
"test": testProviderFactory1,
208+
},
209+
others: []sdkProviderFactories{
210+
{
211+
"test": testProviderFactory1,
212+
},
213+
{
214+
"test": testProviderFactory2,
215+
},
216+
},
217+
expected: sdkProviderFactories{
218+
"test": testProviderFactory2,
219+
},
220+
},
221+
}
222+
223+
for name, testCase := range testCases {
224+
name, testCase := name, testCase
225+
226+
t.Run(name, func(t *testing.T) {
227+
t.Parallel()
228+
229+
got := testCase.pf.merge(testCase.others...)
230+
231+
if diff := cmp.Diff(got, testCase.expected, transformer); diff != "" {
232+
t.Errorf("unexpected difference: %s", diff)
233+
}
234+
})
235+
}
236+
}

0 commit comments

Comments
 (0)