mirror of
https://github.com/opentofu/opentofu.git
synced 2025-01-11 00:22:32 -06:00
add mock providers for testing framework (#1772)
Signed-off-by: ollevche <ollevche@gmail.com>
This commit is contained in:
parent
2c5c8a5f72
commit
9d9a7aab06
@ -7,6 +7,7 @@ NEW FEATURES:
|
||||
* Added support for `override_resource`, `override_data` and `override_module` blocks in testing framework. ([#1499](https://github.com/opentofu/opentofu/pull/1499))
|
||||
* Variables and Locals allowed in module sources and backend configurations (with limitations) ([#1718](https://github.com/opentofu/opentofu/pull/1718))
|
||||
* Added support to new .tofu extensions to allow tofu-specific overrides of .tf files ([#1738](https://github.com/opentofu/opentofu/pull/1738))
|
||||
* Added support for `mock_provider`, `mock_resource` and `mock_data` blocks in testing framework. ([#1772](https://github.com/opentofu/opentofu/pull/1772))
|
||||
|
||||
ENHANCEMENTS:
|
||||
* Added `tofu test -json` types to website Machine-Readable UI documentation. ([#1408](https://github.com/opentofu/opentofu/issues/1408))
|
||||
|
@ -52,8 +52,8 @@ func TestMultipleRunBlocks(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestOverrides(t *testing.T) {
|
||||
// This test fetches "local" and "random" providers.
|
||||
func TestMocksAndOverrides(t *testing.T) {
|
||||
// This test fetches providers from registry.
|
||||
skipIfCannotAccessNetwork(t)
|
||||
|
||||
tf := e2e.NewBinary(t, tofuBin, filepath.Join("testdata", "overrides-in-tests"))
|
||||
@ -76,7 +76,7 @@ func TestOverrides(t *testing.T) {
|
||||
if stderr != "" {
|
||||
t.Errorf("unexpected stderr output on 'test':\n%s", stderr)
|
||||
}
|
||||
if !strings.Contains(stdout, "11 passed, 0 failed") {
|
||||
if !strings.Contains(stdout, "12 passed, 0 failed") {
|
||||
t.Errorf("output doesn't have expected success string:\n%s", stdout)
|
||||
}
|
||||
}
|
||||
|
@ -57,3 +57,28 @@ module "rand_count" {
|
||||
|
||||
source = "./rand"
|
||||
}
|
||||
|
||||
resource "aws_s3_bucket" "test" {
|
||||
bucket = "must not be used anyway"
|
||||
}
|
||||
|
||||
data "aws_s3_bucket" "test" {
|
||||
bucket = "must not be used anyway"
|
||||
}
|
||||
|
||||
provider "local" {
|
||||
alias = "aliased"
|
||||
}
|
||||
|
||||
resource "local_file" "mocked" {
|
||||
provider = local.aliased
|
||||
filename = "mocked.txt"
|
||||
content = "I am mocked file, do not create me please"
|
||||
}
|
||||
|
||||
data "local_file" "maintf" {
|
||||
provider = local.aliased
|
||||
filename = "main.tf"
|
||||
}
|
||||
|
||||
resource "random_pet" "cat" {}
|
||||
|
@ -221,3 +221,67 @@ run "check_for_each_n_count_overridden" {
|
||||
error_message = "Mocked random integer should be 101"
|
||||
}
|
||||
}
|
||||
|
||||
# ensures non-aliased provider is mocked by default
|
||||
mock_provider "aws" {
|
||||
mock_resource "aws_s3_bucket" {
|
||||
defaults = {
|
||||
arn = "arn:aws:s3:::mocked"
|
||||
}
|
||||
}
|
||||
|
||||
mock_data "aws_s3_bucket" {
|
||||
defaults = {
|
||||
bucket_domain_name = "mocked.com"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# ensures non-aliased provider works as intended
|
||||
# and aliased one is mocked
|
||||
mock_provider "local" {
|
||||
alias = "aliased"
|
||||
}
|
||||
|
||||
# ensures we can use this provider in run's providers block
|
||||
# to use mocked one only for a specific test
|
||||
mock_provider "random" {
|
||||
alias = "for_pets"
|
||||
|
||||
mock_resource "random_pet" {
|
||||
defaults = {
|
||||
id = "my lovely cat"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
run "check_mock_providers" {
|
||||
assert {
|
||||
condition = resource.aws_s3_bucket.test.arn == "arn:aws:s3:::mocked"
|
||||
error_message = "aws s3 bucket resource doesn't have mocked values"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = data.aws_s3_bucket.test.bucket_domain_name == "mocked.com"
|
||||
error_message = "aws s3 bucket data doesn't have mocked values"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = !fileexists(local_file.mocked.filename)
|
||||
error_message = "file should not be created due to provider being mocked"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = data.local_file.maintf.content != file("main.tf")
|
||||
error_message = "file should not be read due to provider being mocked"
|
||||
}
|
||||
|
||||
providers = {
|
||||
random = random.for_pets
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = resource.random_pet.cat.id == "my lovely cat"
|
||||
error_message = "providers block in run should allow replacing real providers by mocked"
|
||||
}
|
||||
}
|
||||
|
@ -928,6 +928,8 @@ func (c *Config) transformProviderConfigsForTest(run *TestRun, file *TestFile) (
|
||||
// 3b. If the run has no override configuration, we copy all the providers
|
||||
// from the test file into `next`, overriding all providers with name
|
||||
// collisions from the original config.
|
||||
// 3c. Copy all mock providers from the test file to the `next`, overriding
|
||||
// providers with name collisions from the original config.
|
||||
// 4. We then modify the original configuration so that the providers it
|
||||
// holds are the combination specified by the original config, the test
|
||||
// file and the run file.
|
||||
@ -951,7 +953,7 @@ func (c *Config) transformProviderConfigsForTest(run *TestRun, file *TestFile) (
|
||||
|
||||
for _, ref := range run.Providers {
|
||||
|
||||
testProvider, ok := file.Providers[ref.InParent.String()]
|
||||
testProvider, ok := file.getTestProviderOrMock(ref.InParent.String())
|
||||
if !ok {
|
||||
// Then this reference was invalid as we didn't have the
|
||||
// specified provider in the parent. This should have been
|
||||
@ -966,13 +968,15 @@ func (c *Config) transformProviderConfigsForTest(run *TestRun, file *TestFile) (
|
||||
}
|
||||
|
||||
next[ref.InChild.String()] = &Provider{
|
||||
Name: ref.InChild.Name,
|
||||
NameRange: ref.InChild.NameRange,
|
||||
Alias: ref.InChild.Alias,
|
||||
AliasRange: ref.InChild.AliasRange,
|
||||
Version: testProvider.Version,
|
||||
Config: testProvider.Config,
|
||||
DeclRange: testProvider.DeclRange,
|
||||
Name: ref.InChild.Name,
|
||||
NameRange: ref.InChild.NameRange,
|
||||
Alias: ref.InChild.Alias,
|
||||
AliasRange: ref.InChild.AliasRange,
|
||||
Version: testProvider.Version,
|
||||
Config: testProvider.Config,
|
||||
DeclRange: testProvider.DeclRange,
|
||||
IsMocked: testProvider.IsMocked,
|
||||
MockResources: testProvider.MockResources,
|
||||
}
|
||||
|
||||
}
|
||||
@ -984,6 +988,18 @@ func (c *Config) transformProviderConfigsForTest(run *TestRun, file *TestFile) (
|
||||
}
|
||||
}
|
||||
|
||||
for _, mp := range file.MockProviders {
|
||||
next[mp.moduleUniqueKey()] = &Provider{
|
||||
Name: mp.Name,
|
||||
NameRange: mp.NameRange,
|
||||
Alias: mp.Alias,
|
||||
AliasRange: mp.AliasRange,
|
||||
DeclRange: mp.DeclRange,
|
||||
IsMocked: true,
|
||||
MockResources: mp.MockResources,
|
||||
}
|
||||
}
|
||||
|
||||
c.Module.ProviderConfigs = next
|
||||
|
||||
return func() {
|
||||
|
@ -1,8 +1,10 @@
|
||||
package hcl2shim
|
||||
|
||||
import (
|
||||
"cmp"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"github.com/opentofu/opentofu/internal/configs/configschema"
|
||||
@ -10,30 +12,27 @@ import (
|
||||
"github.com/zclconf/go-cty/cty"
|
||||
)
|
||||
|
||||
// ComposeMockValueBySchema composes mock value based on schema configuration. It uses
|
||||
// configuration value as a baseline and populates null values with provided defaults.
|
||||
// If the provided defaults doesn't contain needed fields, ComposeMockValueBySchema uses
|
||||
// its own defaults. ComposeMockValueBySchema fails if schema contains dynamic types.
|
||||
func ComposeMockValueBySchema(schema *configschema.Block, config cty.Value, defaults map[string]cty.Value) (
|
||||
cty.Value, tfdiags.Diagnostics) {
|
||||
return mockValueComposer{}.composeMockValueBySchema(schema, config, defaults)
|
||||
// MockValueComposer provides different ways to generate mock values based on
|
||||
// config schema, attributes, blocks and cty types in general.
|
||||
type MockValueComposer struct {
|
||||
rand *rand.Rand
|
||||
}
|
||||
|
||||
type mockValueComposer struct {
|
||||
getMockStringOverride func() string
|
||||
}
|
||||
|
||||
func (mvc mockValueComposer) getMockString() string {
|
||||
f := getRandomAlphaNumString
|
||||
|
||||
if mvc.getMockStringOverride != nil {
|
||||
f = mvc.getMockStringOverride
|
||||
func NewMockValueComposer(seed int64) MockValueComposer {
|
||||
return MockValueComposer{
|
||||
rand: rand.New(rand.NewSource(seed)), //nolint:gosec // It doesn't need to be secure.
|
||||
}
|
||||
|
||||
return f()
|
||||
}
|
||||
|
||||
func (mvc mockValueComposer) composeMockValueBySchema(schema *configschema.Block, config cty.Value, defaults map[string]cty.Value) (cty.Value, tfdiags.Diagnostics) {
|
||||
// ComposeBySchema composes mock value based on schema configuration. It uses
|
||||
// configuration value as a baseline and populates null values with provided defaults.
|
||||
// If the provided defaults doesn't contain needed fields, ComposeBySchema uses
|
||||
// its own defaults. ComposeBySchema fails if schema contains dynamic types.
|
||||
// ComposeBySchema produces the same result with the given input values (seed and func arguments).
|
||||
// It does so by traversing schema attributes, blocks and data structure elements / fields
|
||||
// in a stable way by sorting keys or elements beforehand. Then, randomized values match
|
||||
// between multiple ComposeBySchema calls, because seed and random sequences are the same.
|
||||
func (mvc MockValueComposer) ComposeBySchema(schema *configschema.Block, config cty.Value, defaults map[string]cty.Value) (cty.Value, tfdiags.Diagnostics) {
|
||||
var configMap map[string]cty.Value
|
||||
var diags tfdiags.Diagnostics
|
||||
|
||||
@ -73,7 +72,7 @@ func (mvc mockValueComposer) composeMockValueBySchema(schema *configschema.Block
|
||||
return cty.ObjectVal(mockValues), diags
|
||||
}
|
||||
|
||||
func (mvc mockValueComposer) composeMockValueForAttributes(schema *configschema.Block, configMap map[string]cty.Value, defaults map[string]cty.Value) (map[string]cty.Value, tfdiags.Diagnostics) {
|
||||
func (mvc MockValueComposer) composeMockValueForAttributes(schema *configschema.Block, configMap map[string]cty.Value, defaults map[string]cty.Value) (map[string]cty.Value, tfdiags.Diagnostics) {
|
||||
var diags tfdiags.Diagnostics
|
||||
|
||||
addPotentialDefaultsWarning := func(key, description string) {
|
||||
@ -90,7 +89,10 @@ func (mvc mockValueComposer) composeMockValueForAttributes(schema *configschema.
|
||||
|
||||
impliedTypes := schema.ImpliedType().AttributeTypes()
|
||||
|
||||
for k, attr := range schema.Attributes {
|
||||
// Stable order is important here so random values match its fields between function calls.
|
||||
for _, kv := range mapToSortedSlice(schema.Attributes) {
|
||||
k, attr := kv.k, kv.v
|
||||
|
||||
// If the value present in configuration - just use it.
|
||||
if cv, ok := configMap[k]; ok && !cv.IsNull() {
|
||||
mockAttrs[k] = cv
|
||||
@ -141,14 +143,17 @@ func (mvc mockValueComposer) composeMockValueForAttributes(schema *configschema.
|
||||
return mockAttrs, diags
|
||||
}
|
||||
|
||||
func (mvc mockValueComposer) composeMockValueForBlocks(schema *configschema.Block, configMap map[string]cty.Value, defaults map[string]cty.Value) (map[string]cty.Value, tfdiags.Diagnostics) {
|
||||
func (mvc MockValueComposer) composeMockValueForBlocks(schema *configschema.Block, configMap map[string]cty.Value, defaults map[string]cty.Value) (map[string]cty.Value, tfdiags.Diagnostics) {
|
||||
var diags tfdiags.Diagnostics
|
||||
|
||||
mockBlocks := make(map[string]cty.Value)
|
||||
|
||||
impliedTypes := schema.ImpliedType().AttributeTypes()
|
||||
|
||||
for k, block := range schema.BlockTypes {
|
||||
// Stable order is important here so random values match its fields between function calls.
|
||||
for _, kv := range mapToSortedSlice(schema.BlockTypes) {
|
||||
k, block := kv.k, kv.v
|
||||
|
||||
// Checking if the config value really present for the block.
|
||||
// It should be non-null and non-empty collection.
|
||||
|
||||
@ -213,12 +218,12 @@ func (mvc mockValueComposer) composeMockValueForBlocks(schema *configschema.Bloc
|
||||
// to compose each value from the block's inner collection. It recursevily calls
|
||||
// composeMockValueBySchema to proceed with all the inner attributes and blocks
|
||||
// the same way so all the nested blocks follow the same logic.
|
||||
func (mvc mockValueComposer) getMockValueForBlock(targetType cty.Type, configVal cty.Value, block *configschema.Block, defaults map[string]cty.Value) (cty.Value, tfdiags.Diagnostics) {
|
||||
func (mvc MockValueComposer) getMockValueForBlock(targetType cty.Type, configVal cty.Value, block *configschema.Block, defaults map[string]cty.Value) (cty.Value, tfdiags.Diagnostics) {
|
||||
var diags tfdiags.Diagnostics
|
||||
|
||||
switch {
|
||||
case targetType.IsObjectType():
|
||||
mockBlockVal, moreDiags := mvc.composeMockValueBySchema(block, configVal, defaults)
|
||||
mockBlockVal, moreDiags := mvc.ComposeBySchema(block, configVal, defaults)
|
||||
diags = diags.Append(moreDiags)
|
||||
if moreDiags.HasErrors() {
|
||||
return cty.NilVal, diags
|
||||
@ -231,10 +236,11 @@ func (mvc mockValueComposer) getMockValueForBlock(targetType cty.Type, configVal
|
||||
|
||||
var iterator = configVal.ElementIterator()
|
||||
|
||||
// Stable order is important here so random values match its fields between function calls.
|
||||
for iterator.Next() {
|
||||
_, blockConfigV := iterator.Element()
|
||||
|
||||
mockBlockVal, moreDiags := mvc.composeMockValueBySchema(block, blockConfigV, defaults)
|
||||
mockBlockVal, moreDiags := mvc.ComposeBySchema(block, blockConfigV, defaults)
|
||||
diags = diags.Append(moreDiags)
|
||||
if moreDiags.HasErrors() {
|
||||
return cty.NilVal, diags
|
||||
@ -254,10 +260,11 @@ func (mvc mockValueComposer) getMockValueForBlock(targetType cty.Type, configVal
|
||||
|
||||
var iterator = configVal.ElementIterator()
|
||||
|
||||
// Stable order is important here so random values match its fields between function calls.
|
||||
for iterator.Next() {
|
||||
blockConfigK, blockConfigV := iterator.Element()
|
||||
|
||||
mockBlockVal, moreDiags := mvc.composeMockValueBySchema(block, blockConfigV, defaults)
|
||||
mockBlockVal, moreDiags := mvc.ComposeBySchema(block, blockConfigV, defaults)
|
||||
diags = diags.Append(moreDiags)
|
||||
if moreDiags.HasErrors() {
|
||||
return cty.NilVal, diags
|
||||
@ -280,7 +287,7 @@ func (mvc mockValueComposer) getMockValueForBlock(targetType cty.Type, configVal
|
||||
|
||||
// getMockValueByType tries to generate mock cty.Value based on provided cty.Type.
|
||||
// It will return non-ok response if it encounters dynamic type.
|
||||
func (mvc mockValueComposer) getMockValueByType(t cty.Type) (cty.Value, bool) {
|
||||
func (mvc MockValueComposer) getMockValueByType(t cty.Type) (cty.Value, bool) {
|
||||
var v cty.Value
|
||||
|
||||
// just to be sure for cases when the logic below misses something
|
||||
@ -309,8 +316,11 @@ func (mvc mockValueComposer) getMockValueByType(t cty.Type) (cty.Value, bool) {
|
||||
case t.IsObjectType():
|
||||
objVals := make(map[string]cty.Value)
|
||||
|
||||
// populate the object with mock values
|
||||
for k, at := range t.AttributeTypes() {
|
||||
// Populate the object with mock values. Stable order is important here
|
||||
// so random values match its fields between function calls.
|
||||
for _, kv := range mapToSortedSlice(t.AttributeTypes()) {
|
||||
k, at := kv.k, kv.v
|
||||
|
||||
if t.AttributeOptional(k) {
|
||||
continue
|
||||
}
|
||||
@ -335,19 +345,41 @@ func (mvc mockValueComposer) getMockValueByType(t cty.Type) (cty.Value, bool) {
|
||||
return v, true
|
||||
}
|
||||
|
||||
func getRandomAlphaNumString() string {
|
||||
func (mvc MockValueComposer) getMockString() string {
|
||||
const chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890"
|
||||
|
||||
const minLength, maxLength = 4, 16
|
||||
|
||||
length := rand.Intn(maxLength-minLength) + minLength //nolint:gosec // It doesn't need to be secure.
|
||||
length := mvc.rand.Intn(maxLength-minLength) + minLength
|
||||
|
||||
b := strings.Builder{}
|
||||
b.Grow(length)
|
||||
|
||||
for i := 0; i < length; i++ {
|
||||
b.WriteByte(chars[rand.Intn(len(chars))]) //nolint:gosec // It doesn't need to be secure.
|
||||
b.WriteByte(chars[mvc.rand.Intn(len(chars))])
|
||||
}
|
||||
|
||||
return b.String()
|
||||
}
|
||||
|
||||
type keyValue[K cmp.Ordered, V any] struct {
|
||||
k K
|
||||
v V
|
||||
}
|
||||
|
||||
// mapToSortedSlice makes it possible to iterate over map in a stable manner.
|
||||
func mapToSortedSlice[K cmp.Ordered, V any](m map[K]V) []keyValue[K, V] {
|
||||
keys := make([]K, 0, len(m))
|
||||
for k := range m {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
|
||||
slices.Sort(keys)
|
||||
|
||||
s := make([]keyValue[K, V], 0, len(m))
|
||||
for _, k := range keys {
|
||||
s = append(s, keyValue[K, V]{k, m[k]})
|
||||
}
|
||||
|
||||
return s
|
||||
}
|
||||
|
@ -1,13 +1,15 @@
|
||||
package hcl2shim
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/opentofu/opentofu/internal/configs/configschema"
|
||||
"github.com/zclconf/go-cty/cty"
|
||||
)
|
||||
|
||||
// TestComposeMockValueBySchema ensures different configschema.Block values
|
||||
// processed correctly (lists, maps, objects, etc). Also, it should ensure that
|
||||
// the resulting values are equal given the same set of inputs (seed, schema, etc).
|
||||
func TestComposeMockValueBySchema(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
@ -83,13 +85,13 @@ func TestComposeMockValueBySchema(t *testing.T) {
|
||||
config: cty.NilVal,
|
||||
wantVal: cty.ObjectVal(map[string]cty.Value{
|
||||
"required-only": cty.NullVal(cty.String),
|
||||
"required-computed": cty.StringVal("aaaaaaaa"),
|
||||
"required-computed": cty.StringVal("xNmGyAVmNkB4"),
|
||||
"optional": cty.NullVal(cty.String),
|
||||
"optional-computed": cty.StringVal("aaaaaaaa"),
|
||||
"computed-only": cty.StringVal("aaaaaaaa"),
|
||||
"optional-computed": cty.StringVal("6zQu0"),
|
||||
"computed-only": cty.StringVal("l3INvNSQT"),
|
||||
"sensitive-optional": cty.NullVal(cty.String),
|
||||
"sensitive-required": cty.NullVal(cty.String),
|
||||
"sensitive-computed": cty.StringVal("aaaaaaaa"),
|
||||
"sensitive-computed": cty.StringVal("ionwj3qrsh4xyC9"),
|
||||
}),
|
||||
},
|
||||
"diff-props-in-single-block-attributes": {
|
||||
@ -166,13 +168,13 @@ func TestComposeMockValueBySchema(t *testing.T) {
|
||||
wantVal: cty.ObjectVal(map[string]cty.Value{
|
||||
"nested": cty.ObjectVal(map[string]cty.Value{
|
||||
"required-only": cty.NullVal(cty.String),
|
||||
"required-computed": cty.StringVal("aaaaaaaa"),
|
||||
"required-computed": cty.StringVal("xNmGyAVmNkB4"),
|
||||
"optional": cty.NullVal(cty.String),
|
||||
"optional-computed": cty.StringVal("aaaaaaaa"),
|
||||
"computed-only": cty.StringVal("aaaaaaaa"),
|
||||
"optional-computed": cty.StringVal("6zQu0"),
|
||||
"computed-only": cty.StringVal("l3INvNSQT"),
|
||||
"sensitive-optional": cty.NullVal(cty.String),
|
||||
"sensitive-required": cty.NullVal(cty.String),
|
||||
"sensitive-computed": cty.StringVal("aaaaaaaa"),
|
||||
"sensitive-computed": cty.StringVal("ionwj3qrsh4xyC9"),
|
||||
}),
|
||||
}),
|
||||
},
|
||||
@ -208,10 +210,18 @@ func TestComposeMockValueBySchema(t *testing.T) {
|
||||
Nesting: configschema.NestingList,
|
||||
Block: configschema.Block{
|
||||
Attributes: map[string]*configschema.Attribute{
|
||||
"field": {
|
||||
"num": {
|
||||
Type: cty.Number,
|
||||
Computed: true,
|
||||
},
|
||||
"str1": {
|
||||
Type: cty.String,
|
||||
Computed: true,
|
||||
},
|
||||
"str2": {
|
||||
Type: cty.String,
|
||||
Computed: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
@ -223,7 +233,9 @@ func TestComposeMockValueBySchema(t *testing.T) {
|
||||
wantVal: cty.ObjectVal(map[string]cty.Value{
|
||||
"nested": cty.ListVal([]cty.Value{
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
"field": cty.NumberIntVal(0),
|
||||
"num": cty.NumberIntVal(0),
|
||||
"str1": cty.StringVal("l3INvNSQT"),
|
||||
"str2": cty.StringVal("6zQu0"),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
@ -235,10 +247,18 @@ func TestComposeMockValueBySchema(t *testing.T) {
|
||||
Nesting: configschema.NestingSet,
|
||||
Block: configschema.Block{
|
||||
Attributes: map[string]*configschema.Attribute{
|
||||
"field": {
|
||||
"num": {
|
||||
Type: cty.Number,
|
||||
Computed: true,
|
||||
},
|
||||
"str1": {
|
||||
Type: cty.String,
|
||||
Computed: true,
|
||||
},
|
||||
"str2": {
|
||||
Type: cty.String,
|
||||
Computed: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
@ -250,7 +270,9 @@ func TestComposeMockValueBySchema(t *testing.T) {
|
||||
wantVal: cty.ObjectVal(map[string]cty.Value{
|
||||
"nested": cty.SetVal([]cty.Value{
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
"field": cty.NumberIntVal(0),
|
||||
"num": cty.NumberIntVal(0),
|
||||
"str1": cty.StringVal("l3INvNSQT"),
|
||||
"str2": cty.StringVal("6zQu0"),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
@ -262,10 +284,18 @@ func TestComposeMockValueBySchema(t *testing.T) {
|
||||
Nesting: configschema.NestingMap,
|
||||
Block: configschema.Block{
|
||||
Attributes: map[string]*configschema.Attribute{
|
||||
"field": {
|
||||
"num": {
|
||||
Type: cty.Number,
|
||||
Computed: true,
|
||||
},
|
||||
"str1": {
|
||||
Type: cty.String,
|
||||
Computed: true,
|
||||
},
|
||||
"str2": {
|
||||
Type: cty.String,
|
||||
Computed: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
@ -279,7 +309,9 @@ func TestComposeMockValueBySchema(t *testing.T) {
|
||||
wantVal: cty.ObjectVal(map[string]cty.Value{
|
||||
"nested": cty.MapVal(map[string]cty.Value{
|
||||
"somelabel": cty.ObjectVal(map[string]cty.Value{
|
||||
"field": cty.NumberIntVal(0),
|
||||
"num": cty.NumberIntVal(0),
|
||||
"str1": cty.StringVal("l3INvNSQT"),
|
||||
"str2": cty.StringVal("6zQu0"),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
@ -304,8 +336,9 @@ func TestComposeMockValueBySchema(t *testing.T) {
|
||||
},
|
||||
"obj": {
|
||||
Type: cty.Object(map[string]cty.Type{
|
||||
"fieldNum": cty.Number,
|
||||
"fieldStr": cty.String,
|
||||
"fieldNum": cty.Number,
|
||||
"fieldStr1": cty.String,
|
||||
"fieldStr2": cty.String,
|
||||
}),
|
||||
Computed: true,
|
||||
Optional: true,
|
||||
@ -326,7 +359,12 @@ func TestComposeMockValueBySchema(t *testing.T) {
|
||||
Computed: true,
|
||||
Optional: true,
|
||||
},
|
||||
"str": {
|
||||
"str1": {
|
||||
Type: cty.String,
|
||||
Computed: true,
|
||||
Optional: true,
|
||||
},
|
||||
"str2": {
|
||||
Type: cty.String,
|
||||
Computed: true,
|
||||
Optional: true,
|
||||
@ -359,21 +397,23 @@ func TestComposeMockValueBySchema(t *testing.T) {
|
||||
}),
|
||||
wantVal: cty.ObjectVal(map[string]cty.Value{
|
||||
"num": cty.NumberIntVal(0),
|
||||
"str": cty.StringVal("aaaaaaaa"),
|
||||
"str": cty.StringVal("xNmGyAVmNkB4"),
|
||||
"bool": cty.False,
|
||||
"obj": cty.ObjectVal(map[string]cty.Value{
|
||||
"fieldNum": cty.NumberIntVal(0),
|
||||
"fieldStr": cty.StringVal("aaaaaaaa"),
|
||||
"fieldNum": cty.NumberIntVal(0),
|
||||
"fieldStr1": cty.StringVal("l3INvNSQT"),
|
||||
"fieldStr2": cty.StringVal("6zQu0"),
|
||||
}),
|
||||
"list": cty.ListValEmpty(cty.String),
|
||||
"nested": cty.ListVal([]cty.Value{
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
"num": cty.NumberIntVal(0),
|
||||
"str": cty.StringVal("aaaaaaaa"),
|
||||
"str1": cty.StringVal("mCp2gObD"),
|
||||
"str2": cty.StringVal("iOtQNQsLiFD5"),
|
||||
"bool": cty.False,
|
||||
"obj": cty.ObjectVal(map[string]cty.Value{
|
||||
"fieldNum": cty.NumberIntVal(0),
|
||||
"fieldStr": cty.StringVal("aaaaaaaa"),
|
||||
"fieldStr": cty.StringVal("ionwj3qrsh4xyC9"),
|
||||
}),
|
||||
"list": cty.ListValEmpty(cty.String),
|
||||
}),
|
||||
@ -443,12 +483,12 @@ func TestComposeMockValueBySchema(t *testing.T) {
|
||||
wantVal: cty.ObjectVal(map[string]cty.Value{
|
||||
"useConfigValue": cty.StringVal("iAmFromConfig"),
|
||||
"useDefaultsValue": cty.StringVal("iAmFromDefaults"),
|
||||
"generateMockValue": cty.StringVal("aaaaaaaa"),
|
||||
"generateMockValue": cty.StringVal("l3INvNSQT"),
|
||||
"nested": cty.ListVal([]cty.Value{
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
"useConfigValue": cty.StringVal("iAmFromConfig"),
|
||||
"useDefaultsValue": cty.StringVal("iAmFromDefaults"),
|
||||
"generateMockValue": cty.StringVal("aaaaaaaa"),
|
||||
"generateMockValue": cty.StringVal("6zQu0"),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
@ -456,21 +496,13 @@ func TestComposeMockValueBySchema(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
const mockStringLength = 8
|
||||
|
||||
mvc := mockValueComposer{
|
||||
getMockStringOverride: func() string {
|
||||
return strings.Repeat("a", mockStringLength)
|
||||
},
|
||||
}
|
||||
|
||||
for name, test := range tests {
|
||||
test := test
|
||||
|
||||
t.Run(name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
gotVal, gotDiags := mvc.composeMockValueBySchema(test.schema, test.config, test.defaults)
|
||||
gotVal, gotDiags := NewMockValueComposer(42).ComposeBySchema(test.schema, test.config, test.defaults)
|
||||
switch {
|
||||
case test.wantError && !gotDiags.HasErrors():
|
||||
t.Fatalf("Expected error in diags, but none returned")
|
||||
|
@ -68,6 +68,13 @@ type Module struct {
|
||||
StaticEvaluator *StaticEvaluator
|
||||
}
|
||||
|
||||
// GetProviderConfig uses name and alias to find the respective Provider configuration.
|
||||
func (m *Module) GetProviderConfig(name, alias string) (*Provider, bool) {
|
||||
tp := &Provider{Name: name, Alias: alias}
|
||||
p, ok := m.ProviderConfigs[tp.moduleUniqueKey()]
|
||||
return p, ok
|
||||
}
|
||||
|
||||
// File describes the contents of a single configuration file.
|
||||
//
|
||||
// Individual files are not usually used alone, but rather combined together
|
||||
|
@ -37,6 +37,11 @@ type Provider struct {
|
||||
// export this so providers don't need to be re-resolved.
|
||||
// This same field is also added to the ProviderConfigRef struct.
|
||||
providerType addrs.Provider
|
||||
|
||||
// IsMocked indicates if this provider has been mocked. It is used in
|
||||
// testing framework to instantiate test provider wrapper.
|
||||
IsMocked bool
|
||||
MockResources []*MockResource
|
||||
}
|
||||
|
||||
func decodeProviderBlock(block *hcl.Block) (*Provider, hcl.Diagnostics) {
|
||||
|
@ -71,6 +71,10 @@ type TestFile struct {
|
||||
// Underlying modules shouldn't be called.
|
||||
OverrideModules []*OverrideModule
|
||||
|
||||
// MockProviders is a map of providers that should be mocked. It is merged
|
||||
// with Providers map to use later when instantiating provider instance.
|
||||
MockProviders map[string]*MockProvider
|
||||
|
||||
VariablesDeclRange hcl.Range
|
||||
}
|
||||
|
||||
@ -87,6 +91,30 @@ func (file *TestFile) Validate() tfdiags.Diagnostics {
|
||||
return diags
|
||||
}
|
||||
|
||||
func (file *TestFile) getTestProviderOrMock(addr string) (*Provider, bool) {
|
||||
testProvider, ok := file.Providers[addr]
|
||||
if ok {
|
||||
return testProvider, true
|
||||
}
|
||||
|
||||
mockProvider, ok := file.MockProviders[addr]
|
||||
if ok {
|
||||
p := &Provider{
|
||||
Name: mockProvider.Name,
|
||||
NameRange: mockProvider.NameRange,
|
||||
Alias: mockProvider.Alias,
|
||||
AliasRange: mockProvider.AliasRange,
|
||||
DeclRange: mockProvider.DeclRange,
|
||||
IsMocked: true,
|
||||
MockResources: mockProvider.MockResources,
|
||||
}
|
||||
|
||||
return p, true
|
||||
}
|
||||
|
||||
return nil, false
|
||||
}
|
||||
|
||||
// TestRun represents a single run block within a test file.
|
||||
//
|
||||
// Each run block represents a single OpenTofu command to be executed and a set
|
||||
@ -254,9 +282,9 @@ func (r OverrideResource) getBlockName() string {
|
||||
case addrs.DataResourceMode:
|
||||
return blockNameOverrideData
|
||||
case addrs.InvalidResourceMode:
|
||||
return "invalid"
|
||||
panic("BUG: invalid resource mode in override resource")
|
||||
default:
|
||||
return "invalid"
|
||||
panic("BUG: undefined resource mode in override resource: " + r.Mode.String())
|
||||
}
|
||||
}
|
||||
|
||||
@ -273,6 +301,60 @@ type OverrideModule struct {
|
||||
Outputs map[string]cty.Value
|
||||
}
|
||||
|
||||
const blockNameMockProvider = "mock_provider"
|
||||
|
||||
// MockProvider represents mocked provider block. It partially matches
|
||||
// the Provider configuration block (name, alias) and includes additional
|
||||
// mocking data (mock resources).
|
||||
type MockProvider struct {
|
||||
// Fields below are copied from configs.Provider struct:
|
||||
|
||||
Name string
|
||||
NameRange hcl.Range
|
||||
Alias string
|
||||
AliasRange *hcl.Range // nil if no alias set
|
||||
|
||||
DeclRange hcl.Range
|
||||
|
||||
// Fields below are specific to configs.MockProvider:
|
||||
|
||||
MockResources []*MockResource
|
||||
}
|
||||
|
||||
// moduleUniqueKey is copied from Provider.moduleUniqueKey
|
||||
func (p *MockProvider) moduleUniqueKey() string {
|
||||
if p.Alias != "" {
|
||||
return fmt.Sprintf("%s.%s", p.Name, p.Alias)
|
||||
}
|
||||
return p.Name
|
||||
}
|
||||
|
||||
const (
|
||||
blockNameMockResource = "mock_resource"
|
||||
blockNameMockData = "mock_data"
|
||||
)
|
||||
|
||||
// MockResource represents mocked resource. It is similar to OverrideResource,
|
||||
// except all the resources with the same type should be overridden (mocked).
|
||||
type MockResource struct {
|
||||
Mode addrs.ResourceMode
|
||||
Type string
|
||||
Defaults map[string]cty.Value
|
||||
}
|
||||
|
||||
func (r MockResource) getBlockName() string {
|
||||
switch r.Mode {
|
||||
case addrs.ManagedResourceMode:
|
||||
return blockNameMockResource
|
||||
case addrs.DataResourceMode:
|
||||
return blockNameMockData
|
||||
case addrs.InvalidResourceMode:
|
||||
panic("BUG: invalid resource mode in mock resource")
|
||||
default:
|
||||
panic("BUG: undefined resource mode in mock resource: " + r.Mode.String())
|
||||
}
|
||||
}
|
||||
|
||||
func loadTestFile(body hcl.Body) (*TestFile, hcl.Diagnostics) {
|
||||
var diags hcl.Diagnostics
|
||||
|
||||
@ -280,7 +362,8 @@ func loadTestFile(body hcl.Body) (*TestFile, hcl.Diagnostics) {
|
||||
diags = append(diags, contentDiags...)
|
||||
|
||||
tf := TestFile{
|
||||
Providers: make(map[string]*Provider),
|
||||
Providers: make(map[string]*Provider),
|
||||
MockProviders: make(map[string]*MockProvider),
|
||||
}
|
||||
|
||||
for _, block := range content.Blocks {
|
||||
@ -340,6 +423,24 @@ func loadTestFile(body hcl.Body) (*TestFile, hcl.Diagnostics) {
|
||||
tf.OverrideModules = append(tf.OverrideModules, overrideMod)
|
||||
}
|
||||
|
||||
case blockNameMockProvider:
|
||||
mockProvider, mockProviderDiags := decodeMockProviderBlock(block)
|
||||
diags = append(diags, mockProviderDiags...)
|
||||
|
||||
if !mockProviderDiags.HasErrors() {
|
||||
k := mockProvider.moduleUniqueKey()
|
||||
|
||||
if _, ok := tf.MockProviders[k]; ok {
|
||||
diags = diags.Append(&hcl.Diagnostic{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: "Duplicated `mock_provider` block",
|
||||
Detail: fmt.Sprintf("It is not allowed to have multiple `mock_provider` blocks with the same address: `%v`.", k),
|
||||
Subject: mockProvider.DeclRange.Ptr(),
|
||||
})
|
||||
} else {
|
||||
tf.MockProviders[k] = mockProvider
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -734,6 +835,108 @@ func decodeOverrideModuleBlock(block *hcl.Block) (*OverrideModule, hcl.Diagnosti
|
||||
return mod, diags
|
||||
}
|
||||
|
||||
// Some code of decodeMockProviderBlock function was copied from decodeProviderBlock.
|
||||
func decodeMockProviderBlock(block *hcl.Block) (*MockProvider, hcl.Diagnostics) {
|
||||
var diags hcl.Diagnostics
|
||||
|
||||
content, moreDiags := block.Body.Content(mockProviderBlockSchema)
|
||||
diags = append(diags, moreDiags...)
|
||||
|
||||
// Provider names must be localized. Produce an error with a message
|
||||
// indicating the action the user can take to fix this message if the local
|
||||
// name is not localized.
|
||||
name := block.Labels[0]
|
||||
nameDiags := checkProviderNameNormalized(name, block.DefRange)
|
||||
diags = append(diags, nameDiags...)
|
||||
if nameDiags.HasErrors() {
|
||||
// If the name is invalid then we mustn't produce a result because
|
||||
// downstreams could try to use it as a provider type and then crash.
|
||||
return nil, diags
|
||||
}
|
||||
|
||||
provider := &MockProvider{
|
||||
Name: name,
|
||||
NameRange: block.LabelRanges[0],
|
||||
DeclRange: block.DefRange,
|
||||
}
|
||||
|
||||
if attr, exists := content.Attributes["alias"]; exists {
|
||||
valDiags := gohcl.DecodeExpression(attr.Expr, nil, &provider.Alias)
|
||||
diags = append(diags, valDiags...)
|
||||
provider.AliasRange = attr.Expr.Range().Ptr()
|
||||
|
||||
if !hclsyntax.ValidIdentifier(provider.Alias) {
|
||||
diags = append(diags, &hcl.Diagnostic{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: "Invalid mock provider configuration alias",
|
||||
Detail: fmt.Sprintf("An alias must be a valid name. %s", badIdentifierDetail),
|
||||
Subject: provider.AliasRange,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
var (
|
||||
managedResources = make(map[string]struct{})
|
||||
dataResources = make(map[string]struct{})
|
||||
)
|
||||
|
||||
for _, block := range content.Blocks {
|
||||
res, resDiags := decodeMockResourceBlock(block)
|
||||
diags = append(diags, resDiags...)
|
||||
if resDiags.HasErrors() {
|
||||
continue
|
||||
}
|
||||
|
||||
resources := managedResources
|
||||
if res.Mode == addrs.DataResourceMode {
|
||||
resources = dataResources
|
||||
}
|
||||
|
||||
if _, ok := resources[res.Type]; ok {
|
||||
diags = append(diags, &hcl.Diagnostic{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: fmt.Sprintf("Duplicated `%v` block", res.getBlockName()),
|
||||
Detail: fmt.Sprintf("`%v.%v` is already defined in `mock_provider` block.", res.getBlockName(), res.Type),
|
||||
Subject: provider.DeclRange.Ptr(),
|
||||
})
|
||||
continue
|
||||
}
|
||||
|
||||
resources[res.Type] = struct{}{}
|
||||
|
||||
provider.MockResources = append(provider.MockResources, res)
|
||||
}
|
||||
|
||||
return provider, diags
|
||||
}
|
||||
|
||||
func decodeMockResourceBlock(block *hcl.Block) (*MockResource, hcl.Diagnostics) {
|
||||
var mode addrs.ResourceMode
|
||||
|
||||
switch block.Type {
|
||||
case blockNameMockResource:
|
||||
mode = addrs.ManagedResourceMode
|
||||
case blockNameMockData:
|
||||
mode = addrs.DataResourceMode
|
||||
default:
|
||||
panic("BUG: unsupported block type for mock resource: " + block.Type)
|
||||
}
|
||||
|
||||
res := &MockResource{
|
||||
Mode: mode,
|
||||
Type: block.Labels[0],
|
||||
}
|
||||
|
||||
content, diags := block.Body.Content(mockResourceBlockSchema)
|
||||
|
||||
if attr, exists := content.Attributes["defaults"]; exists {
|
||||
v, moreDiags := parseObjectAttrWithNoVariables(attr)
|
||||
res.Defaults, diags = v, append(diags, moreDiags...)
|
||||
}
|
||||
|
||||
return res, diags
|
||||
}
|
||||
|
||||
func parseObjectAttrWithNoVariables(attr *hcl.Attribute) (map[string]cty.Value, hcl.Diagnostics) {
|
||||
attrVal, valDiags := attr.Expr.Value(nil)
|
||||
diags := valDiags
|
||||
@ -821,6 +1024,10 @@ var testFileSchema = &hcl.BodySchema{
|
||||
{
|
||||
Type: blockNameOverrideModule,
|
||||
},
|
||||
{
|
||||
Type: blockNameMockProvider,
|
||||
LabelNames: []string{"name"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@ -898,3 +1105,32 @@ var overrideModuleBlockSchema = &hcl.BodySchema{
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
//nolint:gochecknoglobals // To follow existing code style.
|
||||
var mockProviderBlockSchema = &hcl.BodySchema{
|
||||
Attributes: []hcl.AttributeSchema{
|
||||
{
|
||||
Name: "alias",
|
||||
Required: false,
|
||||
},
|
||||
},
|
||||
Blocks: []hcl.BlockHeaderSchema{
|
||||
{
|
||||
Type: blockNameMockResource,
|
||||
LabelNames: []string{"type"},
|
||||
},
|
||||
{
|
||||
Type: blockNameMockData,
|
||||
LabelNames: []string{"type"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
//nolint:gochecknoglobals // To follow existing code style.
|
||||
var mockResourceBlockSchema = &hcl.BodySchema{
|
||||
Attributes: []hcl.AttributeSchema{
|
||||
{
|
||||
Name: "defaults",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
@ -145,6 +145,18 @@ func (ctx *BuiltinEvalContext) InitProvider(addr addrs.AbsProviderConfig) (provi
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if ctx.Evaluator != nil && ctx.Evaluator.Config != nil && ctx.Evaluator.Config.Module != nil {
|
||||
// If an aliased provider is mocked, we use providerForTest wrapper.
|
||||
// We cannot wrap providers.Factory itself, because factories don't support aliases.
|
||||
pc, ok := ctx.Evaluator.Config.Module.GetProviderConfig(addr.Provider.Type, addr.Alias)
|
||||
if ok && pc.IsMocked {
|
||||
p, err = newProviderForTest(p, pc.MockResources)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
log.Printf("[TRACE] BuiltinEvalContext: Initialized %q provider for %s", addr.String(), addr)
|
||||
ctx.ProviderCache[key] = p
|
||||
|
||||
|
@ -681,7 +681,7 @@ func (n *NodeAbstractResourceInstance) plan(
|
||||
var keyData instances.RepetitionData
|
||||
|
||||
resource := n.Addr.Resource.Resource
|
||||
provider, providerSchema, err := n.getProviderWithPlannedChange(ctx, n.ResolvedProvider, plannedChange)
|
||||
provider, providerSchema, err := n.getProvider(ctx, n.ResolvedProvider)
|
||||
if err != nil {
|
||||
return nil, nil, keyData, diags.Append(err)
|
||||
}
|
||||
@ -2573,10 +2573,6 @@ func resourceInstancePrevRunAddr(ctx EvalContext, currentAddr addrs.AbsResourceI
|
||||
}
|
||||
|
||||
func (n *NodeAbstractResourceInstance) getProvider(ctx EvalContext, addr addrs.AbsProviderConfig) (providers.Interface, providers.ProviderSchema, error) {
|
||||
return n.getProviderWithPlannedChange(ctx, addr, nil)
|
||||
}
|
||||
|
||||
func (n *NodeAbstractResourceInstance) getProviderWithPlannedChange(ctx EvalContext, addr addrs.AbsProviderConfig, plannedChange *plans.ResourceInstanceChange) (providers.Interface, providers.ProviderSchema, error) {
|
||||
underlyingProvider, schema, err := getProvider(ctx, addr)
|
||||
if err != nil {
|
||||
return nil, providers.ProviderSchema{}, err
|
||||
@ -2586,15 +2582,9 @@ func (n *NodeAbstractResourceInstance) getProviderWithPlannedChange(ctx EvalCont
|
||||
return underlyingProvider, schema, nil
|
||||
}
|
||||
|
||||
providerForTest := providerForTest{
|
||||
internal: underlyingProvider,
|
||||
schema: schema,
|
||||
overrideValues: n.Config.OverrideValues,
|
||||
}
|
||||
providerForTest := newProviderForTestWithSchema(underlyingProvider, schema)
|
||||
|
||||
if plannedChange != nil {
|
||||
providerForTest.plannedChange = &plannedChange.After
|
||||
}
|
||||
providerForTest.setSingleResource(n.Addr.Resource.Resource, n.Config.OverrideValues)
|
||||
|
||||
return providerForTest, schema, nil
|
||||
}
|
||||
|
@ -6,126 +6,202 @@
|
||||
package tofu
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"hash/fnv"
|
||||
|
||||
"github.com/opentofu/opentofu/internal/addrs"
|
||||
"github.com/opentofu/opentofu/internal/configs"
|
||||
"github.com/opentofu/opentofu/internal/configs/hcl2shim"
|
||||
"github.com/opentofu/opentofu/internal/providers"
|
||||
"github.com/zclconf/go-cty/cty"
|
||||
)
|
||||
|
||||
var _ providers.Interface = providerForTest{}
|
||||
var _ providers.Interface = &providerForTest{}
|
||||
|
||||
// providerForTest is a wrapper around real provider to allow certain resources to be overridden
|
||||
// for testing framework. Currently, it's used in NodeAbstractResourceInstance only in a format
|
||||
// of one time use. It handles overrideValues and plannedChange for a single resource instance
|
||||
// (i.e. by certain address).
|
||||
// TODO: providerForTest should be extended to handle mock providers implementation with per-type
|
||||
// mocking. It will allow providerForTest to be used for both overrides and full mocking.
|
||||
// In such scenario, overrideValues should be extended to handle per-type values and plannedChange
|
||||
// should contain per PlanResourceChangeRequest cache to produce the same plan result
|
||||
// for the same PlanResourceChangeRequest.
|
||||
// providerForTest is a wrapper around a real provider to allow certain resources to be overridden
|
||||
// (by address) or mocked (by provider and resource type) for testing framework.
|
||||
type providerForTest struct {
|
||||
// It's not embedded to make it safer to extend providers.Interface
|
||||
// without silently breaking providerForTest functionality.
|
||||
// providers.Interface is not embedded to make it safer to extend
|
||||
// the interface without silently breaking providerForTest functionality.
|
||||
internal providers.Interface
|
||||
schema providers.ProviderSchema
|
||||
|
||||
overrideValues map[string]cty.Value
|
||||
plannedChange *cty.Value
|
||||
managedResources resourceForTestByType
|
||||
dataResources resourceForTestByType
|
||||
}
|
||||
|
||||
func (p providerForTest) ReadResource(r providers.ReadResourceRequest) providers.ReadResourceResponse {
|
||||
func newProviderForTestWithSchema(internal providers.Interface, schema providers.ProviderSchema) *providerForTest {
|
||||
return &providerForTest{
|
||||
internal: internal,
|
||||
schema: schema,
|
||||
managedResources: make(resourceForTestByType),
|
||||
dataResources: make(resourceForTestByType),
|
||||
}
|
||||
}
|
||||
|
||||
func newProviderForTest(internal providers.Interface, res []*configs.MockResource) (providers.Interface, error) {
|
||||
schema := internal.GetProviderSchema()
|
||||
if schema.Diagnostics.HasErrors() {
|
||||
return nil, fmt.Errorf("getting provider schema for test wrapper: %w", schema.Diagnostics.Err())
|
||||
}
|
||||
|
||||
p := newProviderForTestWithSchema(internal, schema)
|
||||
|
||||
p.addMockResources(res)
|
||||
|
||||
return p, nil
|
||||
}
|
||||
|
||||
func (p *providerForTest) ReadResource(r providers.ReadResourceRequest) providers.ReadResourceResponse {
|
||||
var resp providers.ReadResourceResponse
|
||||
|
||||
resSchema, _ := p.schema.SchemaForResourceType(addrs.ManagedResourceMode, r.TypeName)
|
||||
|
||||
resp.NewState, resp.Diagnostics = hcl2shim.ComposeMockValueBySchema(resSchema, r.ProviderMeta, p.overrideValues)
|
||||
overrideValues := p.managedResources.getOverrideValues(r.TypeName)
|
||||
|
||||
resp.NewState, resp.Diagnostics = newMockValueComposer(r.TypeName).
|
||||
ComposeBySchema(resSchema, r.ProviderMeta, overrideValues)
|
||||
|
||||
return resp
|
||||
}
|
||||
|
||||
func (p providerForTest) PlanResourceChange(r providers.PlanResourceChangeRequest) providers.PlanResourceChangeResponse {
|
||||
func (p *providerForTest) PlanResourceChange(r providers.PlanResourceChangeRequest) providers.PlanResourceChangeResponse {
|
||||
if r.Config.IsNull() {
|
||||
return providers.PlanResourceChangeResponse{
|
||||
PlannedState: r.ProposedNewState, // null
|
||||
}
|
||||
}
|
||||
|
||||
if p.plannedChange != nil {
|
||||
return providers.PlanResourceChangeResponse{
|
||||
PlannedState: *p.plannedChange,
|
||||
}
|
||||
}
|
||||
|
||||
resSchema, _ := p.schema.SchemaForResourceType(addrs.ManagedResourceMode, r.TypeName)
|
||||
|
||||
overrideValues := p.managedResources.getOverrideValues(r.TypeName)
|
||||
|
||||
var resp providers.PlanResourceChangeResponse
|
||||
|
||||
resp.PlannedState, resp.Diagnostics = hcl2shim.ComposeMockValueBySchema(resSchema, r.Config, p.overrideValues)
|
||||
resp.PlannedState, resp.Diagnostics = newMockValueComposer(r.TypeName).
|
||||
ComposeBySchema(resSchema, r.Config, overrideValues)
|
||||
|
||||
return resp
|
||||
}
|
||||
|
||||
func (p providerForTest) ApplyResourceChange(r providers.ApplyResourceChangeRequest) providers.ApplyResourceChangeResponse {
|
||||
func (p *providerForTest) ApplyResourceChange(r providers.ApplyResourceChangeRequest) providers.ApplyResourceChangeResponse {
|
||||
return providers.ApplyResourceChangeResponse{
|
||||
NewState: r.PlannedState,
|
||||
}
|
||||
}
|
||||
|
||||
func (p providerForTest) ReadDataSource(r providers.ReadDataSourceRequest) providers.ReadDataSourceResponse {
|
||||
func (p *providerForTest) ReadDataSource(r providers.ReadDataSourceRequest) providers.ReadDataSourceResponse {
|
||||
resSchema, _ := p.schema.SchemaForResourceType(addrs.DataResourceMode, r.TypeName)
|
||||
|
||||
var resp providers.ReadDataSourceResponse
|
||||
|
||||
resp.State, resp.Diagnostics = hcl2shim.ComposeMockValueBySchema(resSchema, r.Config, p.overrideValues)
|
||||
overrideValues := p.dataResources.getOverrideValues(r.TypeName)
|
||||
|
||||
resp.State, resp.Diagnostics = newMockValueComposer(r.TypeName).
|
||||
ComposeBySchema(resSchema, r.Config, overrideValues)
|
||||
|
||||
return resp
|
||||
}
|
||||
|
||||
// Calling the internal provider ensures providerForTest has the same behaviour as if
|
||||
// it wasn't overridden. Some of these functions should be changed in the future to
|
||||
// support mock_provider (e.g. ConfigureProvider should do nothing), mock_resource and
|
||||
// mock_data. The only exception is ImportResourceState, which panics if called via providerForTest
|
||||
// because importing is not supported in testing framework.
|
||||
// it wasn't overridden or mocked. The only exception is ImportResourceState, which panics
|
||||
// if called via providerForTest because importing is not supported in testing framework.
|
||||
|
||||
func (p providerForTest) GetProviderSchema() providers.GetProviderSchemaResponse {
|
||||
func (p *providerForTest) GetProviderSchema() providers.GetProviderSchemaResponse {
|
||||
return p.internal.GetProviderSchema()
|
||||
}
|
||||
|
||||
func (p providerForTest) ValidateProviderConfig(r providers.ValidateProviderConfigRequest) providers.ValidateProviderConfigResponse {
|
||||
func (p *providerForTest) ValidateProviderConfig(r providers.ValidateProviderConfigRequest) providers.ValidateProviderConfigResponse {
|
||||
return p.internal.ValidateProviderConfig(r)
|
||||
}
|
||||
|
||||
func (p providerForTest) ValidateResourceConfig(r providers.ValidateResourceConfigRequest) providers.ValidateResourceConfigResponse {
|
||||
func (p *providerForTest) ValidateResourceConfig(r providers.ValidateResourceConfigRequest) providers.ValidateResourceConfigResponse {
|
||||
return p.internal.ValidateResourceConfig(r)
|
||||
}
|
||||
|
||||
func (p providerForTest) ValidateDataResourceConfig(r providers.ValidateDataResourceConfigRequest) providers.ValidateDataResourceConfigResponse {
|
||||
func (p *providerForTest) ValidateDataResourceConfig(r providers.ValidateDataResourceConfigRequest) providers.ValidateDataResourceConfigResponse {
|
||||
return p.internal.ValidateDataResourceConfig(r)
|
||||
}
|
||||
|
||||
func (p providerForTest) UpgradeResourceState(r providers.UpgradeResourceStateRequest) providers.UpgradeResourceStateResponse {
|
||||
func (p *providerForTest) UpgradeResourceState(r providers.UpgradeResourceStateRequest) providers.UpgradeResourceStateResponse {
|
||||
return p.internal.UpgradeResourceState(r)
|
||||
}
|
||||
|
||||
func (p providerForTest) ConfigureProvider(r providers.ConfigureProviderRequest) providers.ConfigureProviderResponse {
|
||||
return p.internal.ConfigureProvider(r)
|
||||
// providerForTest doesn't configure its internal provider because it is mocked.
|
||||
func (p *providerForTest) ConfigureProvider(_ providers.ConfigureProviderRequest) providers.ConfigureProviderResponse {
|
||||
return providers.ConfigureProviderResponse{}
|
||||
}
|
||||
|
||||
func (p providerForTest) Stop() error {
|
||||
func (p *providerForTest) Stop() error {
|
||||
return p.internal.Stop()
|
||||
}
|
||||
|
||||
func (p providerForTest) GetFunctions() providers.GetFunctionsResponse {
|
||||
func (p *providerForTest) GetFunctions() providers.GetFunctionsResponse {
|
||||
return p.internal.GetFunctions()
|
||||
}
|
||||
|
||||
func (p providerForTest) CallFunction(r providers.CallFunctionRequest) providers.CallFunctionResponse {
|
||||
func (p *providerForTest) CallFunction(r providers.CallFunctionRequest) providers.CallFunctionResponse {
|
||||
return p.internal.CallFunction(r)
|
||||
}
|
||||
|
||||
func (p providerForTest) Close() error {
|
||||
func (p *providerForTest) Close() error {
|
||||
return p.internal.Close()
|
||||
}
|
||||
|
||||
func (p providerForTest) ImportResourceState(providers.ImportResourceStateRequest) providers.ImportResourceStateResponse {
|
||||
func (p *providerForTest) ImportResourceState(providers.ImportResourceStateRequest) providers.ImportResourceStateResponse {
|
||||
panic("Importing is not supported in testing context. providerForTest must not be used to call ImportResourceState")
|
||||
}
|
||||
|
||||
func (p *providerForTest) setSingleResource(addr addrs.Resource, overrides map[string]cty.Value) {
|
||||
res := resourceForTest{
|
||||
overrideValues: overrides,
|
||||
}
|
||||
|
||||
switch addr.Mode {
|
||||
case addrs.ManagedResourceMode:
|
||||
p.managedResources[addr.Type] = res
|
||||
case addrs.DataResourceMode:
|
||||
p.dataResources[addr.Type] = res
|
||||
case addrs.InvalidResourceMode:
|
||||
panic("BUG: invalid mock resource mode")
|
||||
default:
|
||||
panic("BUG: unsupported resource mode: " + addr.Mode.String())
|
||||
}
|
||||
}
|
||||
|
||||
func (p *providerForTest) addMockResources(mockResources []*configs.MockResource) {
|
||||
for _, mockRes := range mockResources {
|
||||
var resources resourceForTestByType
|
||||
|
||||
switch mockRes.Mode {
|
||||
case addrs.ManagedResourceMode:
|
||||
resources = p.managedResources
|
||||
case addrs.DataResourceMode:
|
||||
resources = p.dataResources
|
||||
case addrs.InvalidResourceMode:
|
||||
panic("BUG: invalid mock resource mode")
|
||||
default:
|
||||
panic("BUG: unsupported mock resource mode: " + mockRes.Mode.String())
|
||||
}
|
||||
|
||||
resources[mockRes.Type] = resourceForTest{
|
||||
overrideValues: mockRes.Defaults,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type resourceForTest struct {
|
||||
overrideValues map[string]cty.Value
|
||||
}
|
||||
|
||||
type resourceForTestByType map[string]resourceForTest
|
||||
|
||||
func (m resourceForTestByType) getOverrideValues(typeName string) map[string]cty.Value {
|
||||
return m[typeName].overrideValues
|
||||
}
|
||||
|
||||
func newMockValueComposer(typeName string) hcl2shim.MockValueComposer {
|
||||
hash := fnv.New64()
|
||||
hash.Write([]byte(typeName))
|
||||
return hcl2shim.NewMockValueComposer(int64(hash.Sum64()))
|
||||
}
|
||||
|
@ -0,0 +1,11 @@
|
||||
data "local_file" "bucket_name" {
|
||||
filename = "bucket_name.txt"
|
||||
}
|
||||
|
||||
provider "aws" {
|
||||
region = "us-east-2"
|
||||
}
|
||||
|
||||
resource "aws_s3_bucket" "test" {
|
||||
bucket = data.local_file.bucket_name.content
|
||||
}
|
@ -0,0 +1,29 @@
|
||||
// All resources and data sources provided by `aws.mock` provider
|
||||
// will be mocked. Their values will be automatically generated.
|
||||
mock_provider "aws" {
|
||||
alias = "mock"
|
||||
}
|
||||
|
||||
// The same goes for `local` provider. Also, every `local_file`
|
||||
// data source will have its `content` set to `test`.
|
||||
mock_provider "local" {
|
||||
mock_data "local_file" {
|
||||
defaults = {
|
||||
content = "test"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Test if the bucket name is correctly passed to the aws_s3_bucket
|
||||
// resource from the local file.
|
||||
run "test" {
|
||||
// Use `aws.mock` provider for this test run only.
|
||||
providers = {
|
||||
aws = aws.mock
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = aws_s3_bucket.test.bucket == "test"
|
||||
error_message = "Incorrect bucket name: ${aws_s3_bucket.test.bucket}"
|
||||
}
|
||||
}
|
@ -29,6 +29,8 @@ import ExpectFailureResourcesMain from '!!raw-loader!./examples/expect_failures_
|
||||
import ExpectFailureResourcesTest from '!!raw-loader!./examples/expect_failures_resources/main.tftest.hcl'
|
||||
import OverrideResourceMain from '!!raw-loader!./examples/override_resource/main.tf'
|
||||
import OverrideResourceTest from '!!raw-loader!./examples/override_resource/main.tftest.hcl'
|
||||
import MockProviderMain from '!!raw-loader!./examples/mock_provider/main.tf'
|
||||
import MockProviderTest from '!!raw-loader!./examples/mock_provider/main.tftest.hcl'
|
||||
import OverrideModuleMain from '!!raw-loader!./examples/override_module/main.tf'
|
||||
import OverrideModuleTest from '!!raw-loader!./examples/override_module/main.tftest.hcl'
|
||||
import OverrideModuleBucketMeta from '!!raw-loader!./examples/override_module/bucket_meta/main.tf'
|
||||
@ -132,9 +134,10 @@ A test file consists of:
|
||||
* A **[`variables` block](#the-variables-and-runvariables-blocks)** (optional): define variables for all tests in the
|
||||
current file.
|
||||
* The **[`provider` blocks](#the-providers-block)** (optional): define the providers to be used for the tests.
|
||||
* The **[`override_resource` block](#the-override_resource-and-override_data-blocks)** (optional): defines a resource to be overridden.
|
||||
* The **[`override_data` block](#the-override_resource-and-override_data-blocks)** (optional): defines a data source to be overridden.
|
||||
* The **[`override_module` block](#the-override_module-block)** (optional): defines a module call to be overridden.
|
||||
* The **[`mock_provider` blocks](#the-mock-providers-blocks)** (optional): define the providers to be mocked.
|
||||
* The **[`override_resource` blocks](#the-override_resource-and-override_data-blocks)** (optional): define the resources to be overridden.
|
||||
* The **[`override_data` blocks](#the-override_resource-and-override_data-blocks)** (optional): define the data sources to be overridden.
|
||||
* The **[`override_module` blocks](#the-override_module-block)** (optional): define the module calls to be overridden.
|
||||
|
||||
### The `run` block
|
||||
|
||||
@ -386,12 +389,46 @@ of the file.
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
### The `mock_provider` blocks
|
||||
|
||||
A `mock_provider` block allows you to replace provider configuration by a mocked one. In such scenario,
|
||||
creation and retrieval of provider resources and data sources will be skipped. Instead, OpenTofu
|
||||
will automatically generate all computed attributes and blocks to be used in tests.
|
||||
|
||||
:::tip Note
|
||||
|
||||
Learn more on how OpenTofu produces [automatically generated values](#automatically-generated-values).
|
||||
|
||||
:::
|
||||
|
||||
Mock providers also support `alias` field as well as `mock_resource` and `mock_data` blocks.
|
||||
In some cases, you may want to use default values instead of automatically generated ones by passing them
|
||||
inside `defaults` field of `mock_resource` or `mock_data` blocks.
|
||||
|
||||
In the example below, we test if the bucket name is correctly passed to the resource
|
||||
without actually creating it:
|
||||
|
||||
<Tabs>
|
||||
<TabItem value={"test"} label={"main.tftest.hcl"} default>
|
||||
<CodeBlock language={"hcl"}>{MockProviderTest}</CodeBlock>
|
||||
</TabItem>
|
||||
<TabItem value={"main"} label={"main.tf"}>
|
||||
<CodeBlock language={"hcl"}>{MockProviderMain}</CodeBlock>
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
### The `override_resource` and `override_data` blocks
|
||||
|
||||
In some cases you may want to test your infrastructure with certain resources or data sources being overridden.
|
||||
You can use the `override_resource` or `override_data` blocks to skip creation and retrieval of these resources or data sources using the real provider.
|
||||
Instead, OpenTofu will automatically generate all computed attributes and blocks to be used in tests.
|
||||
|
||||
:::tip Note
|
||||
|
||||
Learn more on how OpenTofu produces [automatically generated values](#automatically-generated-values).
|
||||
|
||||
:::
|
||||
|
||||
These blocks consist of the following elements:
|
||||
|
||||
| Name | Type | Description |
|
||||
@ -419,9 +456,9 @@ Each instance of a resource or data source must be overridden.
|
||||
|
||||
:::
|
||||
|
||||
#### Automatically generated values
|
||||
### Automatically generated values
|
||||
|
||||
Overriding resources and data sources requires OpenTofu to automatically generate computed attributes without calling respective providers.
|
||||
Mocking resources and data sources requires OpenTofu to automatically generate computed attributes without calling respective providers.
|
||||
When generating these values, OpenTofu cannot follow custom provider logic, so it uses simple rules based on value type:
|
||||
|
||||
| Attribute type | Generated value |
|
||||
@ -437,7 +474,7 @@ When generating these values, OpenTofu cannot follow custom provider logic, so i
|
||||
|
||||
:::tip Note
|
||||
|
||||
You can set custom values to use instead of automatically generated ones via `values` field in both `override_resource` and `override_data` blocks.
|
||||
You can set custom values to use instead of automatically generated ones via respective mock or override fields.
|
||||
Keep in mind, it's only possible for computed attributes and configuration values cannot be changed.
|
||||
|
||||
:::
|
||||
|
Loading…
Reference in New Issue
Block a user