From 4acf5848843b1d1c69f10ab79a6da9176b90698a Mon Sep 17 00:00:00 2001 From: Samsondeen Dare Date: Mon, 23 Feb 2026 10:43:39 +0100 Subject: [PATCH 01/11] Fill computed values of Nested type --- internal/command/test_test.go | 79 +++++++++++++++++++ .../test/override_data_list_attribute/main.tf | 15 ++++ .../main.tftest.hcl | 46 +++++++++++ .../main.tf | 15 ++++ .../main.tftest.hcl | 46 +++++++++++ internal/command/testing/test_provider.go | 27 +++++++ internal/moduletest/mocking/fill.go | 77 ++++++++++-------- 7 files changed, 273 insertions(+), 32 deletions(-) create mode 100644 internal/command/testdata/test/override_data_list_attribute/main.tf create mode 100644 internal/command/testdata/test/override_data_list_attribute/main.tftest.hcl create mode 100644 internal/command/testdata/test/override_data_nested_list_attribute/main.tf create mode 100644 internal/command/testdata/test/override_data_nested_list_attribute/main.tftest.hcl diff --git a/internal/command/test_test.go b/internal/command/test_test.go index 92adc8573874..2960cc5c3859 100644 --- a/internal/command/test_test.go +++ b/internal/command/test_test.go @@ -5674,6 +5674,85 @@ func TestTest_TeardownOrder(t *testing.T) { } } +func TestTest_OverrideDataListAttribute(t *testing.T) { + tcs := map[string]struct { + dir string + code int + desc string + }{ + "plain_list_attribute": { + dir: "override_data_list_attribute", + code: 0, + desc: "override_data with a computed cty.List(cty.Object) attribute", + }, + "nested_list_attribute": { + dir: "override_data_nested_list_attribute", + code: 0, + desc: "override_data with a computed NestedType NestingList attribute", + }, + } + + for name, tc := range tcs { + t.Run(name, func(t *testing.T) { + td := t.TempDir() + testCopyDir(t, testFixturePath(path.Join("test", tc.dir)), td) + t.Chdir(td) + + provider := testing_command.NewProvider(nil) + providerSource, closeFn := newMockProviderSource(t, map[string][]string{ + "test": {"1.0.0"}, + }) + defer closeFn() + + streams, done := terminal.StreamsForTesting(t) + view := views.NewView(streams) + ui := new(cli.MockUi) + + meta := Meta{ + testingOverrides: metaOverridesForProvider(provider.Provider), + Ui: ui, + View: view, + Streams: streams, + ProviderSource: providerSource, + } + + init := &InitCommand{ + Meta: meta, + } + + if code := init.Run(nil); code != 0 { + output := done(t) + t.Fatalf("expected init status code 0 but got %d: %s", code, output.All()) + } + + // Reset the streams for the next command. + streams, done = terminal.StreamsForTesting(t) + meta.Streams = streams + meta.View = views.NewView(streams) + + c := &TestCommand{ + Meta: meta, + } + + code := c.Run([]string{"-no-color"}) + output := done(t) + + if code != tc.code { + t.Errorf("expected status code %d but got %d:\n\n%s", tc.code, code, output.All()) + } + + if tc.code == 0 { + if !strings.Contains(output.Stdout(), "1 passed, 0 failed.") { + t.Errorf("expected passing test output but got:\n\nstdout:\n%s\nstderr:\n%s", output.Stdout(), output.Stderr()) + } + if output.Stderr() != "" { + t.Errorf("unexpected stderr output:\n%s", output.Stderr()) + } + } + }) + } +} + // testModuleInline takes a map of path -> config strings and yields a config // structure with those files loaded from disk func testModuleInline(t *testing.T, sources map[string]string) (*configs.Config, string, func()) { diff --git a/internal/command/testdata/test/override_data_list_attribute/main.tf b/internal/command/testdata/test/override_data_list_attribute/main.tf new file mode 100644 index 000000000000..f9d19bfd7435 --- /dev/null +++ b/internal/command/testdata/test/override_data_list_attribute/main.tf @@ -0,0 +1,15 @@ +terraform { + required_providers { + test = { + source = "hashicorp/test" + } + } +} + +data "test_data_source" "datasource" { + id = "resource" +} + +output "list_value" { + value = data.test_data_source.datasource.list_value +} diff --git a/internal/command/testdata/test/override_data_list_attribute/main.tftest.hcl b/internal/command/testdata/test/override_data_list_attribute/main.tftest.hcl new file mode 100644 index 000000000000..597e98ac4f9f --- /dev/null +++ b/internal/command/testdata/test/override_data_list_attribute/main.tftest.hcl @@ -0,0 +1,46 @@ +provider "test" {} + +override_data { + target = data.test_data_source.datasource + values = { + list_value = [ + { + name = "first" + value = "one" + }, + { + name = "second" + value = "two" + }, + ] + } +} + +run "test_override_data_list_attribute" { + command = plan + + assert { + condition = length(data.test_data_source.datasource.list_value) == 2 + error_message = "Expected list_value to have 2 elements, got ${length(data.test_data_source.datasource.list_value)}" + } + + assert { + condition = data.test_data_source.datasource.list_value[0].name == "first" + error_message = "Expected first element name to be 'first'" + } + + assert { + condition = data.test_data_source.datasource.list_value[0].value == "one" + error_message = "Expected first element value to be 'one'" + } + + assert { + condition = data.test_data_source.datasource.list_value[1].name == "second" + error_message = "Expected second element name to be 'second'" + } + + assert { + condition = data.test_data_source.datasource.list_value[1].value == "two" + error_message = "Expected second element value to be 'two'" + } +} diff --git a/internal/command/testdata/test/override_data_nested_list_attribute/main.tf b/internal/command/testdata/test/override_data_nested_list_attribute/main.tf new file mode 100644 index 000000000000..52cfeb5fa44a --- /dev/null +++ b/internal/command/testdata/test/override_data_nested_list_attribute/main.tf @@ -0,0 +1,15 @@ +terraform { + required_providers { + test = { + source = "hashicorp/test" + } + } +} + +data "test_data_source" "datasource" { + id = "resource" +} + +output "nested_list_value" { + value = data.test_data_source.datasource.nested_list_value +} diff --git a/internal/command/testdata/test/override_data_nested_list_attribute/main.tftest.hcl b/internal/command/testdata/test/override_data_nested_list_attribute/main.tftest.hcl new file mode 100644 index 000000000000..fe5a85019600 --- /dev/null +++ b/internal/command/testdata/test/override_data_nested_list_attribute/main.tftest.hcl @@ -0,0 +1,46 @@ +provider "test" {} + +override_data { + target = data.test_data_source.datasource + values = { + nested_list_value = [ + { + name = "first" + value = "one" + }, + { + name = "second" + value = "two" + }, + ] + } +} + +run "test_override_data_nested_list_attribute" { + command = plan + + assert { + condition = length(data.test_data_source.datasource.nested_list_value) == 2 + error_message = "Expected nested_list_value to have 2 elements, got ${length(data.test_data_source.datasource.nested_list_value)}" + } + + assert { + condition = data.test_data_source.datasource.nested_list_value[0].name == "first" + error_message = "Expected first element name to be 'first'" + } + + assert { + condition = data.test_data_source.datasource.nested_list_value[0].value == "one" + error_message = "Expected first element value to be 'one'" + } + + assert { + condition = data.test_data_source.datasource.nested_list_value[1].name == "second" + error_message = "Expected second element name to be 'second'" + } + + assert { + condition = data.test_data_source.datasource.nested_list_value[1].value == "two" + error_message = "Expected second element value to be 'two'" + } +} diff --git a/internal/command/testing/test_provider.go b/internal/command/testing/test_provider.go index f1a8e95f9751..bd2ed41ac845 100644 --- a/internal/command/testing/test_provider.go +++ b/internal/command/testing/test_provider.go @@ -53,6 +53,33 @@ var ( "value": {Type: cty.String, Computed: true}, "write_only": {Type: cty.String, Optional: true, WriteOnly: true}, + // list_value is a computed attribute of list-of-objects + // type, used to test that override_data can correctly + // override list-type attributes (see GitHub issue #37939). + "list_value": { + Type: cty.List(cty.Object(map[string]cty.Type{ + "name": cty.String, + "value": cty.String, + })), + Computed: true, + }, + + // nested_list_value is a computed attribute using NestedType + // with NestingList, used to test that override_data can correctly + // override nested list-type attributes (see GitHub issue #37939). + // Many real-world providers (e.g. Databricks) define list-of-objects + // attributes this way rather than using a plain cty.List type. + "nested_list_value": { + Computed: true, + NestedType: &configschema.Object{ + Nesting: configschema.NestingList, + Attributes: map[string]*configschema.Attribute{ + "name": {Type: cty.String, Optional: true}, + "value": {Type: cty.String, Optional: true}, + }, + }, + }, + // We never actually reference these values from a data // source, but we have tests that use the same cty.Value // to represent a test_resource and a test_data_source diff --git a/internal/moduletest/mocking/fill.go b/internal/moduletest/mocking/fill.go index f8643aa10b02..e3431eeeb978 100644 --- a/internal/moduletest/mocking/fill.go +++ b/internal/moduletest/mocking/fill.go @@ -24,43 +24,13 @@ func FillAttribute(in cty.Value, attribute *configschema.Attribute) (cty.Value, func fillAttribute(in cty.Value, attribute *configschema.Attribute, path cty.Path) (cty.Value, error) { if attribute.NestedType != nil { - // Then the in value must be an object. - if !in.Type().IsObjectType() { - return cty.NilVal, path.NewErrorf("incompatible types; expected object type, found %s", in.Type().FriendlyName()) - } - switch attribute.NestedType.Nesting { case configschema.NestingSingle, configschema.NestingGroup: - var names []string - for name := range attribute.NestedType.Attributes { - names = append(names, name) - } - if len(names) == 0 { - return cty.EmptyObjectVal, nil - } - - // Make the order we iterate through the attributes deterministic. We - // are generating random strings in here so it's worth making the - // operation repeatable. - sort.Strings(names) - - children := make(map[string]cty.Value) - for _, name := range names { - if in.Type().HasAttribute(name) { - child, err := fillAttribute(in.GetAttr(name), attribute.NestedType.Attributes[name], path.GetAttr(name)) - if err != nil { - return cty.NilVal, err - } - children[name] = child - continue - } - children[name] = GenerateValueForAttribute(attribute.NestedType.Attributes[name]) - } - return cty.ObjectVal(children), nil + return fillObject(in, attribute, path) case configschema.NestingSet: return cty.SetValEmpty(attribute.ImpliedType().ElementType()), nil case configschema.NestingList: - return cty.ListValEmpty(attribute.ImpliedType().ElementType()), nil + return fillIterable(in, attribute, path) case configschema.NestingMap: return cty.MapValEmpty(attribute.ImpliedType().ElementType()), nil default: @@ -71,6 +41,49 @@ func fillAttribute(in cty.Value, attribute *configschema.Attribute, path cty.Pat return fillType(in, attribute.Type, path) } +func fillObject(in cty.Value, attribute *configschema.Attribute, path cty.Path) (cty.Value, error) { + // Then the in value must be an object. + if !in.Type().IsObjectType() { + return cty.NilVal, path.NewErrorf("incompatible types; expected object type, found %s", in.Type().FriendlyName()) + } + + var names []string + for name := range attribute.NestedType.Attributes { + names = append(names, name) + } + if len(names) == 0 { + return cty.EmptyObjectVal, nil + } + + // Make the order we iterate through the attributes deterministic. We + // are generating random strings in here so it's worth making the + // operation repeatable. + sort.Strings(names) + + children := make(map[string]cty.Value) + for _, name := range names { + if in.Type().HasAttribute(name) { + child, err := fillAttribute(in.GetAttr(name), attribute.NestedType.Attributes[name], path.GetAttr(name)) + if err != nil { + return cty.NilVal, err + } + children[name] = child + continue + } + children[name] = GenerateValueForAttribute(attribute.NestedType.Attributes[name]) + } + return cty.ObjectVal(children), nil +} + +func fillIterable(in cty.Value, attribute *configschema.Attribute, path cty.Path) (cty.Value, error) { + ty := attribute.NestedType.ConfigType() + out, err := fillType(in, ty, path) + if err != nil { + return cty.NilVal, err + } + return out, err +} + // FillType makes the input value match the target type by adding attributes // directly to it or to any nested objects. Essentially, this is a "safe" // conversion between two objects. From 08d5e4d07739c816ee03bd20a87bdf8a1ea71966 Mon Sep 17 00:00:00 2001 From: Samsondeen Dare Date: Mon, 23 Feb 2026 11:26:17 +0100 Subject: [PATCH 02/11] more tests --- internal/command/test_test.go | 24 +- .../test/override_data_list_attribute/main.tf | 4 +- .../main.tftest.hcl | 23 +- .../test/override_data_map_attribute/main.tf | 15 + .../main.tftest.hcl | 31 + .../main.tf | 4 +- .../main.tftest.hcl | 23 +- .../main.tf | 15 + .../main.tftest.hcl | 32 + .../main.tf | 15 + .../main.tftest.hcl | 31 + .../test/override_data_set_attribute/main.tf | 15 + .../main.tftest.hcl | 31 + internal/command/testing/test_provider.go | 71 +- internal/moduletest/mocking/fill.go | 10 +- internal/moduletest/mocking/fill_test.go | 859 ++++++++++++++++++ internal/moduletest/mocking/values_test.go | 3 + 17 files changed, 1147 insertions(+), 59 deletions(-) create mode 100644 internal/command/testdata/test/override_data_map_attribute/main.tf create mode 100644 internal/command/testdata/test/override_data_map_attribute/main.tftest.hcl create mode 100644 internal/command/testdata/test/override_data_nested_map_attribute/main.tf create mode 100644 internal/command/testdata/test/override_data_nested_map_attribute/main.tftest.hcl create mode 100644 internal/command/testdata/test/override_data_nested_set_attribute/main.tf create mode 100644 internal/command/testdata/test/override_data_nested_set_attribute/main.tftest.hcl create mode 100644 internal/command/testdata/test/override_data_set_attribute/main.tf create mode 100644 internal/command/testdata/test/override_data_set_attribute/main.tftest.hcl diff --git a/internal/command/test_test.go b/internal/command/test_test.go index 2960cc5c3859..a27a34dc96fc 100644 --- a/internal/command/test_test.go +++ b/internal/command/test_test.go @@ -5674,13 +5674,13 @@ func TestTest_TeardownOrder(t *testing.T) { } } -func TestTest_OverrideDataListAttribute(t *testing.T) { +func TestTest_OverrideData(t *testing.T) { tcs := map[string]struct { dir string code int desc string }{ - "plain_list_attribute": { + "list_attribute": { dir: "override_data_list_attribute", code: 0, desc: "override_data with a computed cty.List(cty.Object) attribute", @@ -5690,6 +5690,26 @@ func TestTest_OverrideDataListAttribute(t *testing.T) { code: 0, desc: "override_data with a computed NestedType NestingList attribute", }, + "set_attribute": { + dir: "override_data_set_attribute", + code: 0, + desc: "override_data with a computed cty.Set(cty.Object) attribute", + }, + "nested_set_attribute": { + dir: "override_data_nested_set_attribute", + code: 0, + desc: "override_data with a computed NestedType NestingSet attribute", + }, + "map_attribute": { + dir: "override_data_map_attribute", + code: 0, + desc: "override_data with a computed cty.Map(cty.Object) attribute", + }, + "nested_map_attribute": { + dir: "override_data_nested_map_attribute", + code: 0, + desc: "override_data with a computed NestedType NestingMap attribute", + }, } for name, tc := range tcs { diff --git a/internal/command/testdata/test/override_data_list_attribute/main.tf b/internal/command/testdata/test/override_data_list_attribute/main.tf index f9d19bfd7435..00330c09aea6 100644 --- a/internal/command/testdata/test/override_data_list_attribute/main.tf +++ b/internal/command/testdata/test/override_data_list_attribute/main.tf @@ -6,10 +6,10 @@ terraform { } } -data "test_data_source" "datasource" { +data "test_complex_data_source" "datasource" { id = "resource" } output "list_value" { - value = data.test_data_source.datasource.list_value + value = data.test_complex_data_source.datasource.list_value } diff --git a/internal/command/testdata/test/override_data_list_attribute/main.tftest.hcl b/internal/command/testdata/test/override_data_list_attribute/main.tftest.hcl index 597e98ac4f9f..a0518942621c 100644 --- a/internal/command/testdata/test/override_data_list_attribute/main.tftest.hcl +++ b/internal/command/testdata/test/override_data_list_attribute/main.tftest.hcl @@ -1,7 +1,7 @@ provider "test" {} override_data { - target = data.test_data_source.datasource + target = data.test_complex_data_source.datasource values = { list_value = [ { @@ -20,27 +20,12 @@ run "test_override_data_list_attribute" { command = plan assert { - condition = length(data.test_data_source.datasource.list_value) == 2 - error_message = "Expected list_value to have 2 elements, got ${length(data.test_data_source.datasource.list_value)}" + condition = length(data.test_complex_data_source.datasource.list_value) == 2 + error_message = "Expected list_value to have 2 elements, got ${length(data.test_complex_data_source.datasource.list_value)}" } assert { - condition = data.test_data_source.datasource.list_value[0].name == "first" + condition = data.test_complex_data_source.datasource.list_value[0].name == "first" error_message = "Expected first element name to be 'first'" } - - assert { - condition = data.test_data_source.datasource.list_value[0].value == "one" - error_message = "Expected first element value to be 'one'" - } - - assert { - condition = data.test_data_source.datasource.list_value[1].name == "second" - error_message = "Expected second element name to be 'second'" - } - - assert { - condition = data.test_data_source.datasource.list_value[1].value == "two" - error_message = "Expected second element value to be 'two'" - } } diff --git a/internal/command/testdata/test/override_data_map_attribute/main.tf b/internal/command/testdata/test/override_data_map_attribute/main.tf new file mode 100644 index 000000000000..520c9e1de8bf --- /dev/null +++ b/internal/command/testdata/test/override_data_map_attribute/main.tf @@ -0,0 +1,15 @@ +terraform { + required_providers { + test = { + source = "hashicorp/test" + } + } +} + +data "test_complex_data_source" "datasource" { + id = "resource" +} + +output "map_value" { + value = data.test_complex_data_source.datasource.map_value +} diff --git a/internal/command/testdata/test/override_data_map_attribute/main.tftest.hcl b/internal/command/testdata/test/override_data_map_attribute/main.tftest.hcl new file mode 100644 index 000000000000..02d7b8311751 --- /dev/null +++ b/internal/command/testdata/test/override_data_map_attribute/main.tftest.hcl @@ -0,0 +1,31 @@ +provider "test" {} + +override_data { + target = data.test_complex_data_source.datasource + values = { + map_value = { + "key1" = { + name = "first" + value = "one" + } + "key2" = { + name = "second" + value = "two" + } + } + } +} + +run "test_override_data_map_attribute" { + command = plan + + assert { + condition = length(data.test_complex_data_source.datasource.map_value) == 2 + error_message = "Expected map_value to have 2 elements, got ${length(data.test_complex_data_source.datasource.map_value)}" + } + + assert { + condition = data.test_complex_data_source.datasource.map_value["key1"].name == "first" + error_message = "Expected key1 name to be 'first'" + } +} diff --git a/internal/command/testdata/test/override_data_nested_list_attribute/main.tf b/internal/command/testdata/test/override_data_nested_list_attribute/main.tf index 52cfeb5fa44a..ea41b36bc2e2 100644 --- a/internal/command/testdata/test/override_data_nested_list_attribute/main.tf +++ b/internal/command/testdata/test/override_data_nested_list_attribute/main.tf @@ -6,10 +6,10 @@ terraform { } } -data "test_data_source" "datasource" { +data "test_complex_data_source" "datasource" { id = "resource" } output "nested_list_value" { - value = data.test_data_source.datasource.nested_list_value + value = data.test_complex_data_source.datasource.nested_list_value } diff --git a/internal/command/testdata/test/override_data_nested_list_attribute/main.tftest.hcl b/internal/command/testdata/test/override_data_nested_list_attribute/main.tftest.hcl index fe5a85019600..a02327798074 100644 --- a/internal/command/testdata/test/override_data_nested_list_attribute/main.tftest.hcl +++ b/internal/command/testdata/test/override_data_nested_list_attribute/main.tftest.hcl @@ -1,7 +1,7 @@ provider "test" {} override_data { - target = data.test_data_source.datasource + target = data.test_complex_data_source.datasource values = { nested_list_value = [ { @@ -20,27 +20,12 @@ run "test_override_data_nested_list_attribute" { command = plan assert { - condition = length(data.test_data_source.datasource.nested_list_value) == 2 - error_message = "Expected nested_list_value to have 2 elements, got ${length(data.test_data_source.datasource.nested_list_value)}" + condition = length(data.test_complex_data_source.datasource.nested_list_value) == 2 + error_message = "Expected nested_list_value to have 2 elements, got ${length(data.test_complex_data_source.datasource.nested_list_value)}" } assert { - condition = data.test_data_source.datasource.nested_list_value[0].name == "first" + condition = data.test_complex_data_source.datasource.nested_list_value[0].name == "first" error_message = "Expected first element name to be 'first'" } - - assert { - condition = data.test_data_source.datasource.nested_list_value[0].value == "one" - error_message = "Expected first element value to be 'one'" - } - - assert { - condition = data.test_data_source.datasource.nested_list_value[1].name == "second" - error_message = "Expected second element name to be 'second'" - } - - assert { - condition = data.test_data_source.datasource.nested_list_value[1].value == "two" - error_message = "Expected second element value to be 'two'" - } } diff --git a/internal/command/testdata/test/override_data_nested_map_attribute/main.tf b/internal/command/testdata/test/override_data_nested_map_attribute/main.tf new file mode 100644 index 000000000000..0df44632df41 --- /dev/null +++ b/internal/command/testdata/test/override_data_nested_map_attribute/main.tf @@ -0,0 +1,15 @@ +terraform { + required_providers { + test = { + source = "hashicorp/test" + } + } +} + +data "test_complex_data_source" "datasource" { + id = "resource" +} + +output "nested_map_value" { + value = data.test_complex_data_source.datasource.nested_map_value +} diff --git a/internal/command/testdata/test/override_data_nested_map_attribute/main.tftest.hcl b/internal/command/testdata/test/override_data_nested_map_attribute/main.tftest.hcl new file mode 100644 index 000000000000..844f18adf686 --- /dev/null +++ b/internal/command/testdata/test/override_data_nested_map_attribute/main.tftest.hcl @@ -0,0 +1,32 @@ +provider "test" {} + +override_data { + target = data.test_complex_data_source.datasource + values = { + nested_map_value = { + "key1" = { + name = "first" + value = "one" + } + "key2" = { + name = "second" + value = "two" + } + } + } +} + +run "test_override_data_nested_map_attribute" { + command = plan + + assert { + condition = length(data.test_complex_data_source.datasource.nested_map_value) == 2 + error_message = "Expected nested_map_value to have 2 elements, got ${length(data.test_complex_data_source.datasource.nested_map_value)}" + } + + assert { + condition = data.test_complex_data_source.datasource.nested_map_value["key1"].name == "first" + error_message = "Expected key1 name to be 'first'" + } + +} diff --git a/internal/command/testdata/test/override_data_nested_set_attribute/main.tf b/internal/command/testdata/test/override_data_nested_set_attribute/main.tf new file mode 100644 index 000000000000..5174b78b954f --- /dev/null +++ b/internal/command/testdata/test/override_data_nested_set_attribute/main.tf @@ -0,0 +1,15 @@ +terraform { + required_providers { + test = { + source = "hashicorp/test" + } + } +} + +data "test_complex_data_source" "datasource" { + id = "resource" +} + +output "nested_set_value" { + value = data.test_complex_data_source.datasource.nested_set_value +} diff --git a/internal/command/testdata/test/override_data_nested_set_attribute/main.tftest.hcl b/internal/command/testdata/test/override_data_nested_set_attribute/main.tftest.hcl new file mode 100644 index 000000000000..ef4dc0638749 --- /dev/null +++ b/internal/command/testdata/test/override_data_nested_set_attribute/main.tftest.hcl @@ -0,0 +1,31 @@ +provider "test" {} + +override_data { + target = data.test_complex_data_source.datasource + values = { + nested_set_value = [ + { + name = "first" + value = "one" + }, + { + name = "second" + value = "two" + }, + ] + } +} + +run "test_override_data_nested_set_attribute" { + command = plan + + assert { + condition = length(data.test_complex_data_source.datasource.nested_set_value) == 2 + error_message = "Expected nested_set_value to have 2 elements, got ${length(data.test_complex_data_source.datasource.nested_set_value)}" + } + + assert { + condition = contains([for item in data.test_complex_data_source.datasource.nested_set_value : item.name], "first") + error_message = "Expected nested_set_value to contain an element with name 'first'" + } +} diff --git a/internal/command/testdata/test/override_data_set_attribute/main.tf b/internal/command/testdata/test/override_data_set_attribute/main.tf new file mode 100644 index 000000000000..bbea87bd356d --- /dev/null +++ b/internal/command/testdata/test/override_data_set_attribute/main.tf @@ -0,0 +1,15 @@ +terraform { + required_providers { + test = { + source = "hashicorp/test" + } + } +} + +data "test_complex_data_source" "datasource" { + id = "resource" +} + +output "set_value" { + value = data.test_complex_data_source.datasource.set_value +} diff --git a/internal/command/testdata/test/override_data_set_attribute/main.tftest.hcl b/internal/command/testdata/test/override_data_set_attribute/main.tftest.hcl new file mode 100644 index 000000000000..cac115aba854 --- /dev/null +++ b/internal/command/testdata/test/override_data_set_attribute/main.tftest.hcl @@ -0,0 +1,31 @@ +provider "test" {} + +override_data { + target = data.test_complex_data_source.datasource + values = { + set_value = [ + { + name = "first" + value = "one" + }, + { + name = "second" + value = "two" + }, + ] + } +} + +run "test_override_data_set_attribute" { + command = plan + + assert { + condition = length(data.test_complex_data_source.datasource.set_value) == 2 + error_message = "Expected set_value to have 2 elements, got ${length(data.test_complex_data_source.datasource.set_value)}" + } + + assert { + condition = contains([for item in data.test_complex_data_source.datasource.set_value : item.name], "first") + error_message = "Expected set_value to contain an element with name 'first'" + } +} diff --git a/internal/command/testing/test_provider.go b/internal/command/testing/test_provider.go index bd2ed41ac845..e40bda8ca231 100644 --- a/internal/command/testing/test_provider.go +++ b/internal/command/testing/test_provider.go @@ -53,24 +53,57 @@ var ( "value": {Type: cty.String, Computed: true}, "write_only": {Type: cty.String, Optional: true, WriteOnly: true}, - // list_value is a computed attribute of list-of-objects - // type, used to test that override_data can correctly - // override list-type attributes (see GitHub issue #37939). + // We never actually reference these values from a data + // source, but we have tests that use the same cty.Value + // to represent a test_resource and a test_data_source + // so the schemas have to match. + + "interrupt_count": {Type: cty.Number, Computed: true}, + "destroy_fail": {Type: cty.Bool, Computed: true}, + "create_wait_seconds": {Type: cty.Number, Computed: true}, + "destroy_wait_seconds": {Type: cty.Number, Computed: true}, + "defer": {Type: cty.Bool, Computed: true}, + }, + }, + }, + "test_complex_data_source": { + Body: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "id": {Type: cty.String, Required: true}, + "value": {Type: cty.String, Computed: true}, + "write_only": {Type: cty.String, Optional: true, WriteOnly: true}, + "list_value": { Type: cty.List(cty.Object(map[string]cty.Type{ "name": cty.String, "value": cty.String, })), Computed: true, + Optional: true, + }, + "set_value": { + Type: cty.Set(cty.Object(map[string]cty.Type{ + "name": cty.String, + "value": cty.String, + })), + Computed: true, + Optional: true, + }, + "map_value": { + Type: cty.Map(cty.Object(map[string]cty.Type{ + "name": cty.String, + "value": cty.String, + })), + Computed: true, + Optional: true, }, - // nested_list_value is a computed attribute using NestedType - // with NestingList, used to test that override_data can correctly - // override nested list-type attributes (see GitHub issue #37939). - // Many real-world providers (e.g. Databricks) define list-of-objects - // attributes this way rather than using a plain cty.List type. + // The below nested_* attributes represent config supporting + // attributes as nested blocks, where terraform interprets the nested blocks + // as attributes within the schema "nested_list_value": { Computed: true, + Optional: true, NestedType: &configschema.Object{ Nesting: configschema.NestingList, Attributes: map[string]*configschema.Attribute{ @@ -79,6 +112,28 @@ var ( }, }, }, + "nested_set_value": { + Computed: true, + Optional: true, + NestedType: &configschema.Object{ + Nesting: configschema.NestingSet, + Attributes: map[string]*configschema.Attribute{ + "name": {Type: cty.String, Optional: true}, + "value": {Type: cty.String, Optional: true}, + }, + }, + }, + "nested_map_value": { + Computed: true, + Optional: true, + NestedType: &configschema.Object{ + Nesting: configschema.NestingMap, + Attributes: map[string]*configschema.Attribute{ + "name": {Type: cty.String, Optional: true}, + "value": {Type: cty.String, Optional: true}, + }, + }, + }, // We never actually reference these values from a data // source, but we have tests that use the same cty.Value diff --git a/internal/moduletest/mocking/fill.go b/internal/moduletest/mocking/fill.go index e3431eeeb978..676bf815893f 100644 --- a/internal/moduletest/mocking/fill.go +++ b/internal/moduletest/mocking/fill.go @@ -28,11 +28,11 @@ func fillAttribute(in cty.Value, attribute *configschema.Attribute, path cty.Pat case configschema.NestingSingle, configschema.NestingGroup: return fillObject(in, attribute, path) case configschema.NestingSet: - return cty.SetValEmpty(attribute.ImpliedType().ElementType()), nil + return fillIterable(in, attribute, path) case configschema.NestingList: return fillIterable(in, attribute, path) case configschema.NestingMap: - return cty.MapValEmpty(attribute.ImpliedType().ElementType()), nil + return fillIterable(in, attribute, path) default: panic(fmt.Errorf("unknown nesting mode: %d", attribute.NestedType.Nesting)) } @@ -77,11 +77,7 @@ func fillObject(in cty.Value, attribute *configschema.Attribute, path cty.Path) func fillIterable(in cty.Value, attribute *configschema.Attribute, path cty.Path) (cty.Value, error) { ty := attribute.NestedType.ConfigType() - out, err := fillType(in, ty, path) - if err != nil { - return cty.NilVal, err - } - return out, err + return fillType(in, ty, path) } // FillType makes the input value match the target type by adding attributes diff --git a/internal/moduletest/mocking/fill_test.go b/internal/moduletest/mocking/fill_test.go index 008c0fbe804c..6701eb6e66d4 100644 --- a/internal/moduletest/mocking/fill_test.go +++ b/internal/moduletest/mocking/fill_test.go @@ -8,8 +8,867 @@ import ( "testing" "github.com/zclconf/go-cty/cty" + + "github.com/hashicorp/terraform/internal/configs/configschema" ) +// TestFillAttribute tests the FillAttribute function which handles +// configschema.Attribute values including NestedType attributes with various +// nesting modes (NestingSingle, NestingGroup, NestingList, NestingSet, NestingMap). +func TestFillAttribute(t *testing.T) { + tcs := map[string]struct { + in cty.Value + attribute *configschema.Attribute + expected cty.Value + }{ + // ===================================================================== + // Plain attributes (no NestedType) - falls through to fillType + // ===================================================================== + + "plain_string_attribute": { + in: cty.StringVal("hello"), + attribute: &configschema.Attribute{ + Type: cty.String, + }, + expected: cty.StringVal("hello"), + }, + + "plain_number_to_string_conversion": { + in: cty.NumberIntVal(42), + attribute: &configschema.Attribute{ + Type: cty.String, + }, + expected: cty.StringVal("42"), + }, + + "plain_list_of_objects_attribute": { + // When the attribute uses Type (not NestedType), it goes through + // fillType directly. This is the "plain" list-of-objects case + // that already worked before the fix. + in: cty.TupleVal([]cty.Value{ + cty.ObjectVal(map[string]cty.Value{ + "name": cty.StringVal("first"), + }), + }), + attribute: &configschema.Attribute{ + Type: cty.List(cty.Object(map[string]cty.Type{ + "name": cty.String, + "value": cty.String, + })), + }, + expected: cty.ListVal([]cty.Value{ + cty.ObjectVal(map[string]cty.Value{ + "name": cty.StringVal("first"), + "value": cty.StringVal("ssnk9qhr"), + }), + }), + }, + + // ===================================================================== + // NestingSingle + // ===================================================================== + + "nesting_single_partial_attrs": { + in: cty.ObjectVal(map[string]cty.Value{ + "name": cty.StringVal("hello"), + }), + attribute: &configschema.Attribute{ + NestedType: &configschema.Object{ + Nesting: configschema.NestingSingle, + Attributes: map[string]*configschema.Attribute{ + "name": {Type: cty.String}, + "value": {Type: cty.String}, + }, + }, + }, + expected: cty.ObjectVal(map[string]cty.Value{ + "name": cty.StringVal("hello"), + "value": cty.StringVal("ssnk9qhr"), + }), + }, + + "nesting_single_all_attrs_present": { + in: cty.ObjectVal(map[string]cty.Value{ + "name": cty.StringVal("hello"), + "value": cty.StringVal("world"), + }), + attribute: &configschema.Attribute{ + NestedType: &configschema.Object{ + Nesting: configschema.NestingSingle, + Attributes: map[string]*configschema.Attribute{ + "name": {Type: cty.String}, + "value": {Type: cty.String}, + }, + }, + }, + expected: cty.ObjectVal(map[string]cty.Value{ + "name": cty.StringVal("hello"), + "value": cty.StringVal("world"), + }), + }, + + "nesting_single_empty_schema": { + in: cty.EmptyObjectVal, + attribute: &configschema.Attribute{ + NestedType: &configschema.Object{ + Nesting: configschema.NestingSingle, + Attributes: map[string]*configschema.Attribute{}, + }, + }, + expected: cty.EmptyObjectVal, + }, + + "nesting_single_extra_attrs_in_input_dropped": { + // Input has attributes not in the schema; they should be dropped. + in: cty.ObjectVal(map[string]cty.Value{ + "name": cty.StringVal("hello"), + "extra": cty.StringVal("should be dropped"), + }), + attribute: &configschema.Attribute{ + NestedType: &configschema.Object{ + Nesting: configschema.NestingSingle, + Attributes: map[string]*configschema.Attribute{ + "name": {Type: cty.String}, + }, + }, + }, + expected: cty.ObjectVal(map[string]cty.Value{ + "name": cty.StringVal("hello"), + }), + }, + + // ===================================================================== + // NestingGroup (behaves same as NestingSingle for fill purposes) + // ===================================================================== + + "nesting_group_partial_attrs": { + in: cty.ObjectVal(map[string]cty.Value{ + "name": cty.StringVal("hello"), + }), + attribute: &configschema.Attribute{ + NestedType: &configschema.Object{ + Nesting: configschema.NestingGroup, + Attributes: map[string]*configschema.Attribute{ + "name": {Type: cty.String}, + "value": {Type: cty.String}, + }, + }, + }, + expected: cty.ObjectVal(map[string]cty.Value{ + "name": cty.StringVal("hello"), + "value": cty.StringVal("ssnk9qhr"), + }), + }, + + // ===================================================================== + // NestingList - the main bug fix (GitHub issue #37939) + // ===================================================================== + + "nesting_list_tuple_input_partial_attrs": { + // This is the core bug scenario: HCL produces a tuple when parsing + // list literals in override_data values. The old code would fail + // or return empty for this input. + in: cty.TupleVal([]cty.Value{ + cty.ObjectVal(map[string]cty.Value{ + "name": cty.StringVal("first"), + }), + }), + attribute: &configschema.Attribute{ + NestedType: &configschema.Object{ + Nesting: configschema.NestingList, + Attributes: map[string]*configschema.Attribute{ + "name": {Type: cty.String}, + "value": {Type: cty.String}, + }, + }, + }, + expected: cty.ListVal([]cty.Value{ + cty.ObjectVal(map[string]cty.Value{ + "name": cty.StringVal("first"), + "value": cty.StringVal("ssnk9qhr"), + }), + }), + }, + + "nesting_list_list_input_partial_attrs": { + // Input is already a proper list (not a tuple). + in: cty.ListVal([]cty.Value{ + cty.ObjectVal(map[string]cty.Value{ + "name": cty.StringVal("first"), + }), + }), + attribute: &configschema.Attribute{ + NestedType: &configschema.Object{ + Nesting: configschema.NestingList, + Attributes: map[string]*configschema.Attribute{ + "name": {Type: cty.String}, + "value": {Type: cty.String}, + }, + }, + }, + expected: cty.ListVal([]cty.Value{ + cty.ObjectVal(map[string]cty.Value{ + "name": cty.StringVal("first"), + "value": cty.StringVal("ssnk9qhr"), + }), + }), + }, + + "nesting_list_multiple_elements": { + in: cty.TupleVal([]cty.Value{ + cty.ObjectVal(map[string]cty.Value{ + "name": cty.StringVal("first"), + }), + cty.ObjectVal(map[string]cty.Value{ + "name": cty.StringVal("second"), + }), + }), + attribute: &configschema.Attribute{ + NestedType: &configschema.Object{ + Nesting: configschema.NestingList, + Attributes: map[string]*configschema.Attribute{ + "name": {Type: cty.String}, + "value": {Type: cty.String}, + }, + }, + }, + expected: cty.ListVal([]cty.Value{ + cty.ObjectVal(map[string]cty.Value{ + "name": cty.StringVal("first"), + "value": cty.StringVal("ssnk9qhr"), + }), + cty.ObjectVal(map[string]cty.Value{ + "name": cty.StringVal("second"), + "value": cty.StringVal("amyllmyg"), + }), + }), + }, + + "nesting_list_all_attrs_present": { + in: cty.TupleVal([]cty.Value{ + cty.ObjectVal(map[string]cty.Value{ + "name": cty.StringVal("first"), + "value": cty.StringVal("one"), + }), + }), + attribute: &configschema.Attribute{ + NestedType: &configschema.Object{ + Nesting: configschema.NestingList, + Attributes: map[string]*configschema.Attribute{ + "name": {Type: cty.String}, + "value": {Type: cty.String}, + }, + }, + }, + expected: cty.ListVal([]cty.Value{ + cty.ObjectVal(map[string]cty.Value{ + "name": cty.StringVal("first"), + "value": cty.StringVal("one"), + }), + }), + }, + + "nesting_list_extra_attrs_in_elements_dropped": { + // Input objects have attributes not defined in the schema. + in: cty.TupleVal([]cty.Value{ + cty.ObjectVal(map[string]cty.Value{ + "name": cty.StringVal("first"), + "value": cty.StringVal("one"), + "extra": cty.StringVal("should be dropped"), + }), + }), + attribute: &configschema.Attribute{ + NestedType: &configschema.Object{ + Nesting: configschema.NestingList, + Attributes: map[string]*configschema.Attribute{ + "name": {Type: cty.String}, + "value": {Type: cty.String}, + }, + }, + }, + expected: cty.ListVal([]cty.Value{ + cty.ObjectVal(map[string]cty.Value{ + "name": cty.StringVal("first"), + "value": cty.StringVal("one"), + }), + }), + }, + + "nesting_list_set_input": { + // A set can also be converted to a list. + in: cty.SetVal([]cty.Value{ + cty.ObjectVal(map[string]cty.Value{ + "name": cty.StringVal("first"), + "value": cty.StringVal("one"), + }), + }), + attribute: &configschema.Attribute{ + NestedType: &configschema.Object{ + Nesting: configschema.NestingList, + Attributes: map[string]*configschema.Attribute{ + "name": {Type: cty.String}, + "value": {Type: cty.String}, + }, + }, + }, + expected: cty.ListVal([]cty.Value{ + cty.ObjectVal(map[string]cty.Value{ + "name": cty.StringVal("first"), + "value": cty.StringVal("one"), + }), + }), + }, + + "nesting_list_element_type_conversion": { + // Elements have attributes that need type conversion (number→string). + in: cty.TupleVal([]cty.Value{ + cty.ObjectVal(map[string]cty.Value{ + "name": cty.StringVal("first"), + "count": cty.NumberIntVal(42), + }), + }), + attribute: &configschema.Attribute{ + NestedType: &configschema.Object{ + Nesting: configschema.NestingList, + Attributes: map[string]*configschema.Attribute{ + "name": {Type: cty.String}, + "count": {Type: cty.String}, + }, + }, + }, + expected: cty.ListVal([]cty.Value{ + cty.ObjectVal(map[string]cty.Value{ + "name": cty.StringVal("first"), + "count": cty.StringVal("42"), + }), + }), + }, + + "nesting_list_three_attrs_partial": { + // Object schema with 3 attributes, only 1 provided. + // Attributes are filled in alphabetical order: "alpha" (provided), + // "beta" (generated=ssnk9qhr), "gamma" (generated=amyllmyg). + in: cty.TupleVal([]cty.Value{ + cty.ObjectVal(map[string]cty.Value{ + "alpha": cty.StringVal("provided"), + }), + }), + attribute: &configschema.Attribute{ + NestedType: &configschema.Object{ + Nesting: configschema.NestingList, + Attributes: map[string]*configschema.Attribute{ + "alpha": {Type: cty.String}, + "beta": {Type: cty.String}, + "gamma": {Type: cty.String}, + }, + }, + }, + expected: cty.ListVal([]cty.Value{ + cty.ObjectVal(map[string]cty.Value{ + "alpha": cty.StringVal("provided"), + "beta": cty.StringVal("ssnk9qhr"), + "gamma": cty.StringVal("amyllmyg"), + }), + }), + }, + + // ===================================================================== + // NestingSet - BUG: current implementation ignores input and always + // returns an empty set. These tests document the CURRENT behavior. + // If NestingSet is fixed to use fillIterable (like NestingList), + // these tests should be updated to expect filled values. + // ===================================================================== + + "nesting_set_returns_empty_ignoring_input": { + // BUG: The current implementation returns an empty set regardless + // of the input. Override values for NestingSet attributes are + // silently dropped, just like the original NestingList bug. + in: cty.SetVal([]cty.Value{ + cty.ObjectVal(map[string]cty.Value{ + "name": cty.StringVal("first"), + "value": cty.StringVal("one"), + }), + }), + attribute: &configschema.Attribute{ + NestedType: &configschema.Object{ + Nesting: configschema.NestingSet, + Attributes: map[string]*configschema.Attribute{ + "name": {Type: cty.String}, + "value": {Type: cty.String}, + }, + }, + }, + // BUG: should be cty.SetVal with the input values preserved, + // but current code returns empty. + expected: cty.SetValEmpty(cty.Object(map[string]cty.Type{ + "name": cty.String, + "value": cty.String, + })), + }, + + // ===================================================================== + // NestingMap - BUG: current implementation ignores input and always + // returns an empty map. These tests document the CURRENT behavior. + // If NestingMap is fixed to use fillIterable (like NestingList), + // these tests should be updated to expect filled values. + // ===================================================================== + + "nesting_map_returns_empty_ignoring_input": { + // BUG: The current implementation returns an empty map regardless + // of the input. Override values for NestingMap attributes are + // silently dropped, just like the original NestingList bug. + in: cty.MapVal(map[string]cty.Value{ + "one": cty.ObjectVal(map[string]cty.Value{ + "name": cty.StringVal("first"), + "value": cty.StringVal("val1"), + }), + }), + attribute: &configschema.Attribute{ + NestedType: &configschema.Object{ + Nesting: configschema.NestingMap, + Attributes: map[string]*configschema.Attribute{ + "name": {Type: cty.String}, + "value": {Type: cty.String}, + }, + }, + }, + // BUG: should be cty.MapVal with the input values preserved, + // but current code returns empty. + expected: cty.MapValEmpty(cty.Object(map[string]cty.Type{ + "name": cty.String, + "value": cty.String, + })), + }, + + // ===================================================================== + // Deep nesting: NestingSingle containing NestingList + // ===================================================================== + + "nesting_single_containing_nesting_list": { + // A single object that has a child attribute which is a NestingList. + // The fillObject function recurses into fillAttribute for children, + // which then calls fillIterable for the NestingList child. + in: cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("myid"), + "items": cty.TupleVal([]cty.Value{ + cty.ObjectVal(map[string]cty.Value{ + "name": cty.StringVal("item1"), + }), + }), + }), + attribute: &configschema.Attribute{ + NestedType: &configschema.Object{ + Nesting: configschema.NestingSingle, + Attributes: map[string]*configschema.Attribute{ + "id": {Type: cty.String}, + "items": { + NestedType: &configschema.Object{ + Nesting: configschema.NestingList, + Attributes: map[string]*configschema.Attribute{ + "name": {Type: cty.String}, + "value": {Type: cty.String}, + }, + }, + }, + }, + }, + }, + expected: cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("myid"), + "items": cty.ListVal([]cty.Value{ + cty.ObjectVal(map[string]cty.Value{ + "name": cty.StringVal("item1"), + "value": cty.StringVal("ssnk9qhr"), + }), + }), + }), + }, + + // ===================================================================== + // Deep nesting: NestingList containing NestingSingle + // ===================================================================== + + "nesting_list_containing_nesting_single": { + // List elements contain a NestingSingle child attribute. + // The fillIterable path goes through fillType which handles + // the list→object recursion, but nested NestedType attributes + // within the objects are handled purely at the cty.Type level + // since fillType doesn't know about configschema.Attribute. + // This works because ConfigType() produces the full type tree. + in: cty.TupleVal([]cty.Value{ + cty.ObjectVal(map[string]cty.Value{ + "name": cty.StringVal("item1"), + "detail": cty.ObjectVal(map[string]cty.Value{ + "key": cty.StringVal("mykey"), + }), + }), + }), + attribute: &configschema.Attribute{ + NestedType: &configschema.Object{ + Nesting: configschema.NestingList, + Attributes: map[string]*configschema.Attribute{ + "name": {Type: cty.String}, + "detail": { + NestedType: &configschema.Object{ + Nesting: configschema.NestingSingle, + Attributes: map[string]*configschema.Attribute{ + "key": {Type: cty.String}, + "value": {Type: cty.String}, + }, + }, + }, + }, + }, + }, + expected: cty.ListVal([]cty.Value{ + cty.ObjectVal(map[string]cty.Value{ + "name": cty.StringVal("item1"), + "detail": cty.ObjectVal(map[string]cty.Value{ + "key": cty.StringVal("mykey"), + "value": cty.StringVal("ssnk9qhr"), + }), + }), + }), + }, + + // ===================================================================== + // Deep nesting: NestingList containing NestingList (3 levels) + // ===================================================================== + + "nesting_list_containing_nesting_list": { + // Three levels: outer list → objects with inner list → objects. + // Tests that the fillIterable→fillType recursion handles nested + // list-of-object types correctly through pure cty.Type handling. + in: cty.TupleVal([]cty.Value{ + cty.ObjectVal(map[string]cty.Value{ + "group": cty.StringVal("g1"), + "members": cty.TupleVal([]cty.Value{ + cty.ObjectVal(map[string]cty.Value{ + "name": cty.StringVal("alice"), + }), + }), + }), + }), + attribute: &configschema.Attribute{ + NestedType: &configschema.Object{ + Nesting: configschema.NestingList, + Attributes: map[string]*configschema.Attribute{ + "group": {Type: cty.String}, + "members": { + NestedType: &configschema.Object{ + Nesting: configschema.NestingList, + Attributes: map[string]*configschema.Attribute{ + "name": {Type: cty.String}, + "role": {Type: cty.String}, + }, + }, + }, + }, + }, + }, + expected: cty.ListVal([]cty.Value{ + cty.ObjectVal(map[string]cty.Value{ + "group": cty.StringVal("g1"), + "members": cty.ListVal([]cty.Value{ + cty.ObjectVal(map[string]cty.Value{ + "name": cty.StringVal("alice"), + "role": cty.StringVal("ssnk9qhr"), + }), + }), + }), + }), + }, + + // ===================================================================== + // Deep nesting: NestingSingle containing NestingSingle (2 levels) + // ===================================================================== + + "nesting_single_containing_nesting_single": { + in: cty.ObjectVal(map[string]cty.Value{ + "outer_name": cty.StringVal("outer"), + "inner": cty.ObjectVal(map[string]cty.Value{ + "inner_name": cty.StringVal("inner"), + }), + }), + attribute: &configschema.Attribute{ + NestedType: &configschema.Object{ + Nesting: configschema.NestingSingle, + Attributes: map[string]*configschema.Attribute{ + "outer_name": {Type: cty.String}, + "inner": { + NestedType: &configschema.Object{ + Nesting: configschema.NestingSingle, + Attributes: map[string]*configschema.Attribute{ + "inner_name": {Type: cty.String}, + "inner_value": {Type: cty.String}, + }, + }, + }, + }, + }, + }, + expected: cty.ObjectVal(map[string]cty.Value{ + "outer_name": cty.StringVal("outer"), + "inner": cty.ObjectVal(map[string]cty.Value{ + "inner_name": cty.StringVal("inner"), + "inner_value": cty.StringVal("ssnk9qhr"), + }), + }), + }, + + // ===================================================================== + // NestingSingle with mixed attribute types inside + // ===================================================================== + + "nesting_single_with_mixed_types": { + // Object with string, number, bool, and list attributes. + in: cty.ObjectVal(map[string]cty.Value{ + "name": cty.StringVal("test"), + "active": cty.True, + }), + attribute: &configschema.Attribute{ + NestedType: &configschema.Object{ + Nesting: configschema.NestingSingle, + Attributes: map[string]*configschema.Attribute{ + "active": {Type: cty.Bool}, + "count": {Type: cty.Number}, + "name": {Type: cty.String}, + }, + }, + }, + // Sorted: active (provided), count (generated=0), name (provided) + expected: cty.ObjectVal(map[string]cty.Value{ + "active": cty.True, + "count": cty.Zero, + "name": cty.StringVal("test"), + }), + }, + + // ===================================================================== + // NestingList with elements that have no missing attributes + // ===================================================================== + + "nesting_list_no_missing_attrs_multiple_elements": { + in: cty.TupleVal([]cty.Value{ + cty.ObjectVal(map[string]cty.Value{ + "name": cty.StringVal("first"), + "value": cty.StringVal("one"), + }), + cty.ObjectVal(map[string]cty.Value{ + "name": cty.StringVal("second"), + "value": cty.StringVal("two"), + }), + }), + attribute: &configschema.Attribute{ + NestedType: &configschema.Object{ + Nesting: configschema.NestingList, + Attributes: map[string]*configschema.Attribute{ + "name": {Type: cty.String}, + "value": {Type: cty.String}, + }, + }, + }, + expected: cty.ListVal([]cty.Value{ + cty.ObjectVal(map[string]cty.Value{ + "name": cty.StringVal("first"), + "value": cty.StringVal("one"), + }), + cty.ObjectVal(map[string]cty.Value{ + "name": cty.StringVal("second"), + "value": cty.StringVal("two"), + }), + }), + }, + + // ===================================================================== + // NestingList with single-attr objects (minimal schema) + // ===================================================================== + + "nesting_list_single_attr_schema": { + in: cty.TupleVal([]cty.Value{ + cty.ObjectVal(map[string]cty.Value{}), + }), + attribute: &configschema.Attribute{ + NestedType: &configschema.Object{ + Nesting: configschema.NestingList, + Attributes: map[string]*configschema.Attribute{ + "id": {Type: cty.String}, + }, + }, + }, + expected: cty.ListVal([]cty.Value{ + cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("ssnk9qhr"), + }), + }), + }, + + // ===================================================================== + // NestingSingle with a child that has a plain list attribute (Type not NestedType) + // ===================================================================== + + "nesting_single_with_plain_list_child": { + in: cty.ObjectVal(map[string]cty.Value{ + "name": cty.StringVal("test"), + "tags": cty.ListVal([]cty.Value{ + cty.StringVal("tag1"), + cty.StringVal("tag2"), + }), + }), + attribute: &configschema.Attribute{ + NestedType: &configschema.Object{ + Nesting: configschema.NestingSingle, + Attributes: map[string]*configschema.Attribute{ + "name": {Type: cty.String}, + "tags": {Type: cty.List(cty.String)}, + }, + }, + }, + expected: cty.ObjectVal(map[string]cty.Value{ + "name": cty.StringVal("test"), + "tags": cty.ListVal([]cty.Value{ + cty.StringVal("tag1"), + cty.StringVal("tag2"), + }), + }), + }, + + // ===================================================================== + // NestingList with bool and number attribute types + // ===================================================================== + + "nesting_list_with_bool_and_number_attrs": { + in: cty.TupleVal([]cty.Value{ + cty.ObjectVal(map[string]cty.Value{ + "name": cty.StringVal("item"), + }), + }), + attribute: &configschema.Attribute{ + NestedType: &configschema.Object{ + Nesting: configschema.NestingList, + Attributes: map[string]*configschema.Attribute{ + "active": {Type: cty.Bool}, + "count": {Type: cty.Number}, + "name": {Type: cty.String}, + }, + }, + }, + // Sorted: active (generated=false), count (generated=0), name (provided) + expected: cty.ListVal([]cty.Value{ + cty.ObjectVal(map[string]cty.Value{ + "active": cty.False, + "count": cty.Zero, + "name": cty.StringVal("item"), + }), + }), + }, + } + + for name, tc := range tcs { + t.Run(name, func(t *testing.T) { + testRand = rand.New(rand.NewSource(0)) + defer func() { + testRand = nil + }() + + actual, err := FillAttribute(tc.in, tc.attribute) + if err != nil { + t.Fatalf("unexpected error: %s", err) + } + + if !tc.expected.RawEquals(actual) { + t.Errorf("\nexpected: %s\nactual: %s", tc.expected.GoString(), actual.GoString()) + } + }) + } +} + +// TestFillAttribute_Errors tests error cases for FillAttribute. +func TestFillAttribute_Errors(t *testing.T) { + tcs := map[string]struct { + in cty.Value + attribute *configschema.Attribute + err string + }{ + "nesting_single_non_object_input": { + // NestingSingle requires an object input; a string should fail. + in: cty.StringVal("not an object"), + attribute: &configschema.Attribute{ + NestedType: &configschema.Object{ + Nesting: configschema.NestingSingle, + Attributes: map[string]*configschema.Attribute{ + "name": {Type: cty.String}, + }, + }, + }, + err: "incompatible types; expected object type, found string", + }, + "nesting_group_non_object_input": { + in: cty.NumberIntVal(42), + attribute: &configschema.Attribute{ + NestedType: &configschema.Object{ + Nesting: configschema.NestingGroup, + Attributes: map[string]*configschema.Attribute{ + "name": {Type: cty.String}, + }, + }, + }, + err: "incompatible types; expected object type, found number", + }, + "nesting_list_incompatible_input": { + // Passing a plain string where a list of objects is expected. + in: cty.StringVal("not a list"), + attribute: &configschema.Attribute{ + NestedType: &configschema.Object{ + Nesting: configschema.NestingList, + Attributes: map[string]*configschema.Attribute{ + "name": {Type: cty.String}, + }, + }, + }, + err: "incompatible types; expected list of object, found string", + }, + "nesting_list_element_type_mismatch": { + // Tuple elements are strings, but the schema expects objects. + in: cty.TupleVal([]cty.Value{ + cty.StringVal("not an object"), + }), + attribute: &configschema.Attribute{ + NestedType: &configschema.Object{ + Nesting: configschema.NestingList, + Attributes: map[string]*configschema.Attribute{ + "name": {Type: cty.String}, + }, + }, + }, + err: "incompatible types; expected object, found string", + }, + "plain_attribute_type_mismatch": { + // Plain attribute with completely incompatible type. + in: cty.ListValEmpty(cty.String), + attribute: &configschema.Attribute{ + Type: cty.Bool, + }, + err: "bool required, but have list of string", + }, + } + + for name, tc := range tcs { + t.Run(name, func(t *testing.T) { + actual, err := FillAttribute(tc.in, tc.attribute) + if err == nil { + t.Fatalf("expected error but got success with value: %s", actual.GoString()) + } + if out := err.Error(); out != tc.err { + t.Errorf("\nexpected error: %s\nactual error: %s", tc.err, out) + } + if actual != cty.NilVal { + t.Errorf("expected cty.NilVal on error but got: %s", actual.GoString()) + } + }) + } +} + func TestFillType(t *testing.T) { tcs := map[string]struct { in cty.Value diff --git a/internal/moduletest/mocking/values_test.go b/internal/moduletest/mocking/values_test.go index 7ec12162b72d..9e4e624615d9 100644 --- a/internal/moduletest/mocking/values_test.go +++ b/internal/moduletest/mocking/values_test.go @@ -565,6 +565,9 @@ func TestComputedValuesForDataSource(t *testing.T) { "value": cty.String, })), }), + expectedFailures: []string{ + `Terraform could not compute a value for the target type list of object with the mocked data defined at :0,0-0 with the attribute ".nested": incompatible types; expected list of object, found object.`, + }, }, "nested_set_attribute": { target: cty.ObjectVal(map[string]cty.Value{ From 320ec2c6895b09a1ab2f4c991b6fa82cbd244b42 Mon Sep 17 00:00:00 2001 From: Samsondeen Dare Date: Mon, 23 Feb 2026 12:03:59 +0100 Subject: [PATCH 03/11] more tests --- internal/command/testing/test_provider.go | 1 + internal/moduletest/mocking/fill_test.go | 859 --------------------- internal/moduletest/mocking/values_test.go | 108 +++ 3 files changed, 109 insertions(+), 859 deletions(-) diff --git a/internal/command/testing/test_provider.go b/internal/command/testing/test_provider.go index e40bda8ca231..2f921975b3db 100644 --- a/internal/command/testing/test_provider.go +++ b/internal/command/testing/test_provider.go @@ -66,6 +66,7 @@ var ( }, }, }, + "test_complex_data_source": { Body: &configschema.Block{ Attributes: map[string]*configschema.Attribute{ diff --git a/internal/moduletest/mocking/fill_test.go b/internal/moduletest/mocking/fill_test.go index 6701eb6e66d4..008c0fbe804c 100644 --- a/internal/moduletest/mocking/fill_test.go +++ b/internal/moduletest/mocking/fill_test.go @@ -8,867 +8,8 @@ import ( "testing" "github.com/zclconf/go-cty/cty" - - "github.com/hashicorp/terraform/internal/configs/configschema" ) -// TestFillAttribute tests the FillAttribute function which handles -// configschema.Attribute values including NestedType attributes with various -// nesting modes (NestingSingle, NestingGroup, NestingList, NestingSet, NestingMap). -func TestFillAttribute(t *testing.T) { - tcs := map[string]struct { - in cty.Value - attribute *configschema.Attribute - expected cty.Value - }{ - // ===================================================================== - // Plain attributes (no NestedType) - falls through to fillType - // ===================================================================== - - "plain_string_attribute": { - in: cty.StringVal("hello"), - attribute: &configschema.Attribute{ - Type: cty.String, - }, - expected: cty.StringVal("hello"), - }, - - "plain_number_to_string_conversion": { - in: cty.NumberIntVal(42), - attribute: &configschema.Attribute{ - Type: cty.String, - }, - expected: cty.StringVal("42"), - }, - - "plain_list_of_objects_attribute": { - // When the attribute uses Type (not NestedType), it goes through - // fillType directly. This is the "plain" list-of-objects case - // that already worked before the fix. - in: cty.TupleVal([]cty.Value{ - cty.ObjectVal(map[string]cty.Value{ - "name": cty.StringVal("first"), - }), - }), - attribute: &configschema.Attribute{ - Type: cty.List(cty.Object(map[string]cty.Type{ - "name": cty.String, - "value": cty.String, - })), - }, - expected: cty.ListVal([]cty.Value{ - cty.ObjectVal(map[string]cty.Value{ - "name": cty.StringVal("first"), - "value": cty.StringVal("ssnk9qhr"), - }), - }), - }, - - // ===================================================================== - // NestingSingle - // ===================================================================== - - "nesting_single_partial_attrs": { - in: cty.ObjectVal(map[string]cty.Value{ - "name": cty.StringVal("hello"), - }), - attribute: &configschema.Attribute{ - NestedType: &configschema.Object{ - Nesting: configschema.NestingSingle, - Attributes: map[string]*configschema.Attribute{ - "name": {Type: cty.String}, - "value": {Type: cty.String}, - }, - }, - }, - expected: cty.ObjectVal(map[string]cty.Value{ - "name": cty.StringVal("hello"), - "value": cty.StringVal("ssnk9qhr"), - }), - }, - - "nesting_single_all_attrs_present": { - in: cty.ObjectVal(map[string]cty.Value{ - "name": cty.StringVal("hello"), - "value": cty.StringVal("world"), - }), - attribute: &configschema.Attribute{ - NestedType: &configschema.Object{ - Nesting: configschema.NestingSingle, - Attributes: map[string]*configschema.Attribute{ - "name": {Type: cty.String}, - "value": {Type: cty.String}, - }, - }, - }, - expected: cty.ObjectVal(map[string]cty.Value{ - "name": cty.StringVal("hello"), - "value": cty.StringVal("world"), - }), - }, - - "nesting_single_empty_schema": { - in: cty.EmptyObjectVal, - attribute: &configschema.Attribute{ - NestedType: &configschema.Object{ - Nesting: configschema.NestingSingle, - Attributes: map[string]*configschema.Attribute{}, - }, - }, - expected: cty.EmptyObjectVal, - }, - - "nesting_single_extra_attrs_in_input_dropped": { - // Input has attributes not in the schema; they should be dropped. - in: cty.ObjectVal(map[string]cty.Value{ - "name": cty.StringVal("hello"), - "extra": cty.StringVal("should be dropped"), - }), - attribute: &configschema.Attribute{ - NestedType: &configschema.Object{ - Nesting: configschema.NestingSingle, - Attributes: map[string]*configschema.Attribute{ - "name": {Type: cty.String}, - }, - }, - }, - expected: cty.ObjectVal(map[string]cty.Value{ - "name": cty.StringVal("hello"), - }), - }, - - // ===================================================================== - // NestingGroup (behaves same as NestingSingle for fill purposes) - // ===================================================================== - - "nesting_group_partial_attrs": { - in: cty.ObjectVal(map[string]cty.Value{ - "name": cty.StringVal("hello"), - }), - attribute: &configschema.Attribute{ - NestedType: &configschema.Object{ - Nesting: configschema.NestingGroup, - Attributes: map[string]*configschema.Attribute{ - "name": {Type: cty.String}, - "value": {Type: cty.String}, - }, - }, - }, - expected: cty.ObjectVal(map[string]cty.Value{ - "name": cty.StringVal("hello"), - "value": cty.StringVal("ssnk9qhr"), - }), - }, - - // ===================================================================== - // NestingList - the main bug fix (GitHub issue #37939) - // ===================================================================== - - "nesting_list_tuple_input_partial_attrs": { - // This is the core bug scenario: HCL produces a tuple when parsing - // list literals in override_data values. The old code would fail - // or return empty for this input. - in: cty.TupleVal([]cty.Value{ - cty.ObjectVal(map[string]cty.Value{ - "name": cty.StringVal("first"), - }), - }), - attribute: &configschema.Attribute{ - NestedType: &configschema.Object{ - Nesting: configschema.NestingList, - Attributes: map[string]*configschema.Attribute{ - "name": {Type: cty.String}, - "value": {Type: cty.String}, - }, - }, - }, - expected: cty.ListVal([]cty.Value{ - cty.ObjectVal(map[string]cty.Value{ - "name": cty.StringVal("first"), - "value": cty.StringVal("ssnk9qhr"), - }), - }), - }, - - "nesting_list_list_input_partial_attrs": { - // Input is already a proper list (not a tuple). - in: cty.ListVal([]cty.Value{ - cty.ObjectVal(map[string]cty.Value{ - "name": cty.StringVal("first"), - }), - }), - attribute: &configschema.Attribute{ - NestedType: &configschema.Object{ - Nesting: configschema.NestingList, - Attributes: map[string]*configschema.Attribute{ - "name": {Type: cty.String}, - "value": {Type: cty.String}, - }, - }, - }, - expected: cty.ListVal([]cty.Value{ - cty.ObjectVal(map[string]cty.Value{ - "name": cty.StringVal("first"), - "value": cty.StringVal("ssnk9qhr"), - }), - }), - }, - - "nesting_list_multiple_elements": { - in: cty.TupleVal([]cty.Value{ - cty.ObjectVal(map[string]cty.Value{ - "name": cty.StringVal("first"), - }), - cty.ObjectVal(map[string]cty.Value{ - "name": cty.StringVal("second"), - }), - }), - attribute: &configschema.Attribute{ - NestedType: &configschema.Object{ - Nesting: configschema.NestingList, - Attributes: map[string]*configschema.Attribute{ - "name": {Type: cty.String}, - "value": {Type: cty.String}, - }, - }, - }, - expected: cty.ListVal([]cty.Value{ - cty.ObjectVal(map[string]cty.Value{ - "name": cty.StringVal("first"), - "value": cty.StringVal("ssnk9qhr"), - }), - cty.ObjectVal(map[string]cty.Value{ - "name": cty.StringVal("second"), - "value": cty.StringVal("amyllmyg"), - }), - }), - }, - - "nesting_list_all_attrs_present": { - in: cty.TupleVal([]cty.Value{ - cty.ObjectVal(map[string]cty.Value{ - "name": cty.StringVal("first"), - "value": cty.StringVal("one"), - }), - }), - attribute: &configschema.Attribute{ - NestedType: &configschema.Object{ - Nesting: configschema.NestingList, - Attributes: map[string]*configschema.Attribute{ - "name": {Type: cty.String}, - "value": {Type: cty.String}, - }, - }, - }, - expected: cty.ListVal([]cty.Value{ - cty.ObjectVal(map[string]cty.Value{ - "name": cty.StringVal("first"), - "value": cty.StringVal("one"), - }), - }), - }, - - "nesting_list_extra_attrs_in_elements_dropped": { - // Input objects have attributes not defined in the schema. - in: cty.TupleVal([]cty.Value{ - cty.ObjectVal(map[string]cty.Value{ - "name": cty.StringVal("first"), - "value": cty.StringVal("one"), - "extra": cty.StringVal("should be dropped"), - }), - }), - attribute: &configschema.Attribute{ - NestedType: &configschema.Object{ - Nesting: configschema.NestingList, - Attributes: map[string]*configschema.Attribute{ - "name": {Type: cty.String}, - "value": {Type: cty.String}, - }, - }, - }, - expected: cty.ListVal([]cty.Value{ - cty.ObjectVal(map[string]cty.Value{ - "name": cty.StringVal("first"), - "value": cty.StringVal("one"), - }), - }), - }, - - "nesting_list_set_input": { - // A set can also be converted to a list. - in: cty.SetVal([]cty.Value{ - cty.ObjectVal(map[string]cty.Value{ - "name": cty.StringVal("first"), - "value": cty.StringVal("one"), - }), - }), - attribute: &configschema.Attribute{ - NestedType: &configschema.Object{ - Nesting: configschema.NestingList, - Attributes: map[string]*configschema.Attribute{ - "name": {Type: cty.String}, - "value": {Type: cty.String}, - }, - }, - }, - expected: cty.ListVal([]cty.Value{ - cty.ObjectVal(map[string]cty.Value{ - "name": cty.StringVal("first"), - "value": cty.StringVal("one"), - }), - }), - }, - - "nesting_list_element_type_conversion": { - // Elements have attributes that need type conversion (number→string). - in: cty.TupleVal([]cty.Value{ - cty.ObjectVal(map[string]cty.Value{ - "name": cty.StringVal("first"), - "count": cty.NumberIntVal(42), - }), - }), - attribute: &configschema.Attribute{ - NestedType: &configschema.Object{ - Nesting: configschema.NestingList, - Attributes: map[string]*configschema.Attribute{ - "name": {Type: cty.String}, - "count": {Type: cty.String}, - }, - }, - }, - expected: cty.ListVal([]cty.Value{ - cty.ObjectVal(map[string]cty.Value{ - "name": cty.StringVal("first"), - "count": cty.StringVal("42"), - }), - }), - }, - - "nesting_list_three_attrs_partial": { - // Object schema with 3 attributes, only 1 provided. - // Attributes are filled in alphabetical order: "alpha" (provided), - // "beta" (generated=ssnk9qhr), "gamma" (generated=amyllmyg). - in: cty.TupleVal([]cty.Value{ - cty.ObjectVal(map[string]cty.Value{ - "alpha": cty.StringVal("provided"), - }), - }), - attribute: &configschema.Attribute{ - NestedType: &configschema.Object{ - Nesting: configschema.NestingList, - Attributes: map[string]*configschema.Attribute{ - "alpha": {Type: cty.String}, - "beta": {Type: cty.String}, - "gamma": {Type: cty.String}, - }, - }, - }, - expected: cty.ListVal([]cty.Value{ - cty.ObjectVal(map[string]cty.Value{ - "alpha": cty.StringVal("provided"), - "beta": cty.StringVal("ssnk9qhr"), - "gamma": cty.StringVal("amyllmyg"), - }), - }), - }, - - // ===================================================================== - // NestingSet - BUG: current implementation ignores input and always - // returns an empty set. These tests document the CURRENT behavior. - // If NestingSet is fixed to use fillIterable (like NestingList), - // these tests should be updated to expect filled values. - // ===================================================================== - - "nesting_set_returns_empty_ignoring_input": { - // BUG: The current implementation returns an empty set regardless - // of the input. Override values for NestingSet attributes are - // silently dropped, just like the original NestingList bug. - in: cty.SetVal([]cty.Value{ - cty.ObjectVal(map[string]cty.Value{ - "name": cty.StringVal("first"), - "value": cty.StringVal("one"), - }), - }), - attribute: &configschema.Attribute{ - NestedType: &configschema.Object{ - Nesting: configschema.NestingSet, - Attributes: map[string]*configschema.Attribute{ - "name": {Type: cty.String}, - "value": {Type: cty.String}, - }, - }, - }, - // BUG: should be cty.SetVal with the input values preserved, - // but current code returns empty. - expected: cty.SetValEmpty(cty.Object(map[string]cty.Type{ - "name": cty.String, - "value": cty.String, - })), - }, - - // ===================================================================== - // NestingMap - BUG: current implementation ignores input and always - // returns an empty map. These tests document the CURRENT behavior. - // If NestingMap is fixed to use fillIterable (like NestingList), - // these tests should be updated to expect filled values. - // ===================================================================== - - "nesting_map_returns_empty_ignoring_input": { - // BUG: The current implementation returns an empty map regardless - // of the input. Override values for NestingMap attributes are - // silently dropped, just like the original NestingList bug. - in: cty.MapVal(map[string]cty.Value{ - "one": cty.ObjectVal(map[string]cty.Value{ - "name": cty.StringVal("first"), - "value": cty.StringVal("val1"), - }), - }), - attribute: &configschema.Attribute{ - NestedType: &configschema.Object{ - Nesting: configschema.NestingMap, - Attributes: map[string]*configschema.Attribute{ - "name": {Type: cty.String}, - "value": {Type: cty.String}, - }, - }, - }, - // BUG: should be cty.MapVal with the input values preserved, - // but current code returns empty. - expected: cty.MapValEmpty(cty.Object(map[string]cty.Type{ - "name": cty.String, - "value": cty.String, - })), - }, - - // ===================================================================== - // Deep nesting: NestingSingle containing NestingList - // ===================================================================== - - "nesting_single_containing_nesting_list": { - // A single object that has a child attribute which is a NestingList. - // The fillObject function recurses into fillAttribute for children, - // which then calls fillIterable for the NestingList child. - in: cty.ObjectVal(map[string]cty.Value{ - "id": cty.StringVal("myid"), - "items": cty.TupleVal([]cty.Value{ - cty.ObjectVal(map[string]cty.Value{ - "name": cty.StringVal("item1"), - }), - }), - }), - attribute: &configschema.Attribute{ - NestedType: &configschema.Object{ - Nesting: configschema.NestingSingle, - Attributes: map[string]*configschema.Attribute{ - "id": {Type: cty.String}, - "items": { - NestedType: &configschema.Object{ - Nesting: configschema.NestingList, - Attributes: map[string]*configschema.Attribute{ - "name": {Type: cty.String}, - "value": {Type: cty.String}, - }, - }, - }, - }, - }, - }, - expected: cty.ObjectVal(map[string]cty.Value{ - "id": cty.StringVal("myid"), - "items": cty.ListVal([]cty.Value{ - cty.ObjectVal(map[string]cty.Value{ - "name": cty.StringVal("item1"), - "value": cty.StringVal("ssnk9qhr"), - }), - }), - }), - }, - - // ===================================================================== - // Deep nesting: NestingList containing NestingSingle - // ===================================================================== - - "nesting_list_containing_nesting_single": { - // List elements contain a NestingSingle child attribute. - // The fillIterable path goes through fillType which handles - // the list→object recursion, but nested NestedType attributes - // within the objects are handled purely at the cty.Type level - // since fillType doesn't know about configschema.Attribute. - // This works because ConfigType() produces the full type tree. - in: cty.TupleVal([]cty.Value{ - cty.ObjectVal(map[string]cty.Value{ - "name": cty.StringVal("item1"), - "detail": cty.ObjectVal(map[string]cty.Value{ - "key": cty.StringVal("mykey"), - }), - }), - }), - attribute: &configschema.Attribute{ - NestedType: &configschema.Object{ - Nesting: configschema.NestingList, - Attributes: map[string]*configschema.Attribute{ - "name": {Type: cty.String}, - "detail": { - NestedType: &configschema.Object{ - Nesting: configschema.NestingSingle, - Attributes: map[string]*configschema.Attribute{ - "key": {Type: cty.String}, - "value": {Type: cty.String}, - }, - }, - }, - }, - }, - }, - expected: cty.ListVal([]cty.Value{ - cty.ObjectVal(map[string]cty.Value{ - "name": cty.StringVal("item1"), - "detail": cty.ObjectVal(map[string]cty.Value{ - "key": cty.StringVal("mykey"), - "value": cty.StringVal("ssnk9qhr"), - }), - }), - }), - }, - - // ===================================================================== - // Deep nesting: NestingList containing NestingList (3 levels) - // ===================================================================== - - "nesting_list_containing_nesting_list": { - // Three levels: outer list → objects with inner list → objects. - // Tests that the fillIterable→fillType recursion handles nested - // list-of-object types correctly through pure cty.Type handling. - in: cty.TupleVal([]cty.Value{ - cty.ObjectVal(map[string]cty.Value{ - "group": cty.StringVal("g1"), - "members": cty.TupleVal([]cty.Value{ - cty.ObjectVal(map[string]cty.Value{ - "name": cty.StringVal("alice"), - }), - }), - }), - }), - attribute: &configschema.Attribute{ - NestedType: &configschema.Object{ - Nesting: configschema.NestingList, - Attributes: map[string]*configschema.Attribute{ - "group": {Type: cty.String}, - "members": { - NestedType: &configschema.Object{ - Nesting: configschema.NestingList, - Attributes: map[string]*configschema.Attribute{ - "name": {Type: cty.String}, - "role": {Type: cty.String}, - }, - }, - }, - }, - }, - }, - expected: cty.ListVal([]cty.Value{ - cty.ObjectVal(map[string]cty.Value{ - "group": cty.StringVal("g1"), - "members": cty.ListVal([]cty.Value{ - cty.ObjectVal(map[string]cty.Value{ - "name": cty.StringVal("alice"), - "role": cty.StringVal("ssnk9qhr"), - }), - }), - }), - }), - }, - - // ===================================================================== - // Deep nesting: NestingSingle containing NestingSingle (2 levels) - // ===================================================================== - - "nesting_single_containing_nesting_single": { - in: cty.ObjectVal(map[string]cty.Value{ - "outer_name": cty.StringVal("outer"), - "inner": cty.ObjectVal(map[string]cty.Value{ - "inner_name": cty.StringVal("inner"), - }), - }), - attribute: &configschema.Attribute{ - NestedType: &configschema.Object{ - Nesting: configschema.NestingSingle, - Attributes: map[string]*configschema.Attribute{ - "outer_name": {Type: cty.String}, - "inner": { - NestedType: &configschema.Object{ - Nesting: configschema.NestingSingle, - Attributes: map[string]*configschema.Attribute{ - "inner_name": {Type: cty.String}, - "inner_value": {Type: cty.String}, - }, - }, - }, - }, - }, - }, - expected: cty.ObjectVal(map[string]cty.Value{ - "outer_name": cty.StringVal("outer"), - "inner": cty.ObjectVal(map[string]cty.Value{ - "inner_name": cty.StringVal("inner"), - "inner_value": cty.StringVal("ssnk9qhr"), - }), - }), - }, - - // ===================================================================== - // NestingSingle with mixed attribute types inside - // ===================================================================== - - "nesting_single_with_mixed_types": { - // Object with string, number, bool, and list attributes. - in: cty.ObjectVal(map[string]cty.Value{ - "name": cty.StringVal("test"), - "active": cty.True, - }), - attribute: &configschema.Attribute{ - NestedType: &configschema.Object{ - Nesting: configschema.NestingSingle, - Attributes: map[string]*configschema.Attribute{ - "active": {Type: cty.Bool}, - "count": {Type: cty.Number}, - "name": {Type: cty.String}, - }, - }, - }, - // Sorted: active (provided), count (generated=0), name (provided) - expected: cty.ObjectVal(map[string]cty.Value{ - "active": cty.True, - "count": cty.Zero, - "name": cty.StringVal("test"), - }), - }, - - // ===================================================================== - // NestingList with elements that have no missing attributes - // ===================================================================== - - "nesting_list_no_missing_attrs_multiple_elements": { - in: cty.TupleVal([]cty.Value{ - cty.ObjectVal(map[string]cty.Value{ - "name": cty.StringVal("first"), - "value": cty.StringVal("one"), - }), - cty.ObjectVal(map[string]cty.Value{ - "name": cty.StringVal("second"), - "value": cty.StringVal("two"), - }), - }), - attribute: &configschema.Attribute{ - NestedType: &configschema.Object{ - Nesting: configschema.NestingList, - Attributes: map[string]*configschema.Attribute{ - "name": {Type: cty.String}, - "value": {Type: cty.String}, - }, - }, - }, - expected: cty.ListVal([]cty.Value{ - cty.ObjectVal(map[string]cty.Value{ - "name": cty.StringVal("first"), - "value": cty.StringVal("one"), - }), - cty.ObjectVal(map[string]cty.Value{ - "name": cty.StringVal("second"), - "value": cty.StringVal("two"), - }), - }), - }, - - // ===================================================================== - // NestingList with single-attr objects (minimal schema) - // ===================================================================== - - "nesting_list_single_attr_schema": { - in: cty.TupleVal([]cty.Value{ - cty.ObjectVal(map[string]cty.Value{}), - }), - attribute: &configschema.Attribute{ - NestedType: &configschema.Object{ - Nesting: configschema.NestingList, - Attributes: map[string]*configschema.Attribute{ - "id": {Type: cty.String}, - }, - }, - }, - expected: cty.ListVal([]cty.Value{ - cty.ObjectVal(map[string]cty.Value{ - "id": cty.StringVal("ssnk9qhr"), - }), - }), - }, - - // ===================================================================== - // NestingSingle with a child that has a plain list attribute (Type not NestedType) - // ===================================================================== - - "nesting_single_with_plain_list_child": { - in: cty.ObjectVal(map[string]cty.Value{ - "name": cty.StringVal("test"), - "tags": cty.ListVal([]cty.Value{ - cty.StringVal("tag1"), - cty.StringVal("tag2"), - }), - }), - attribute: &configschema.Attribute{ - NestedType: &configschema.Object{ - Nesting: configschema.NestingSingle, - Attributes: map[string]*configschema.Attribute{ - "name": {Type: cty.String}, - "tags": {Type: cty.List(cty.String)}, - }, - }, - }, - expected: cty.ObjectVal(map[string]cty.Value{ - "name": cty.StringVal("test"), - "tags": cty.ListVal([]cty.Value{ - cty.StringVal("tag1"), - cty.StringVal("tag2"), - }), - }), - }, - - // ===================================================================== - // NestingList with bool and number attribute types - // ===================================================================== - - "nesting_list_with_bool_and_number_attrs": { - in: cty.TupleVal([]cty.Value{ - cty.ObjectVal(map[string]cty.Value{ - "name": cty.StringVal("item"), - }), - }), - attribute: &configschema.Attribute{ - NestedType: &configschema.Object{ - Nesting: configschema.NestingList, - Attributes: map[string]*configschema.Attribute{ - "active": {Type: cty.Bool}, - "count": {Type: cty.Number}, - "name": {Type: cty.String}, - }, - }, - }, - // Sorted: active (generated=false), count (generated=0), name (provided) - expected: cty.ListVal([]cty.Value{ - cty.ObjectVal(map[string]cty.Value{ - "active": cty.False, - "count": cty.Zero, - "name": cty.StringVal("item"), - }), - }), - }, - } - - for name, tc := range tcs { - t.Run(name, func(t *testing.T) { - testRand = rand.New(rand.NewSource(0)) - defer func() { - testRand = nil - }() - - actual, err := FillAttribute(tc.in, tc.attribute) - if err != nil { - t.Fatalf("unexpected error: %s", err) - } - - if !tc.expected.RawEquals(actual) { - t.Errorf("\nexpected: %s\nactual: %s", tc.expected.GoString(), actual.GoString()) - } - }) - } -} - -// TestFillAttribute_Errors tests error cases for FillAttribute. -func TestFillAttribute_Errors(t *testing.T) { - tcs := map[string]struct { - in cty.Value - attribute *configschema.Attribute - err string - }{ - "nesting_single_non_object_input": { - // NestingSingle requires an object input; a string should fail. - in: cty.StringVal("not an object"), - attribute: &configschema.Attribute{ - NestedType: &configschema.Object{ - Nesting: configschema.NestingSingle, - Attributes: map[string]*configschema.Attribute{ - "name": {Type: cty.String}, - }, - }, - }, - err: "incompatible types; expected object type, found string", - }, - "nesting_group_non_object_input": { - in: cty.NumberIntVal(42), - attribute: &configschema.Attribute{ - NestedType: &configschema.Object{ - Nesting: configschema.NestingGroup, - Attributes: map[string]*configschema.Attribute{ - "name": {Type: cty.String}, - }, - }, - }, - err: "incompatible types; expected object type, found number", - }, - "nesting_list_incompatible_input": { - // Passing a plain string where a list of objects is expected. - in: cty.StringVal("not a list"), - attribute: &configschema.Attribute{ - NestedType: &configschema.Object{ - Nesting: configschema.NestingList, - Attributes: map[string]*configschema.Attribute{ - "name": {Type: cty.String}, - }, - }, - }, - err: "incompatible types; expected list of object, found string", - }, - "nesting_list_element_type_mismatch": { - // Tuple elements are strings, but the schema expects objects. - in: cty.TupleVal([]cty.Value{ - cty.StringVal("not an object"), - }), - attribute: &configschema.Attribute{ - NestedType: &configschema.Object{ - Nesting: configschema.NestingList, - Attributes: map[string]*configschema.Attribute{ - "name": {Type: cty.String}, - }, - }, - }, - err: "incompatible types; expected object, found string", - }, - "plain_attribute_type_mismatch": { - // Plain attribute with completely incompatible type. - in: cty.ListValEmpty(cty.String), - attribute: &configschema.Attribute{ - Type: cty.Bool, - }, - err: "bool required, but have list of string", - }, - } - - for name, tc := range tcs { - t.Run(name, func(t *testing.T) { - actual, err := FillAttribute(tc.in, tc.attribute) - if err == nil { - t.Fatalf("expected error but got success with value: %s", actual.GoString()) - } - if out := err.Error(); out != tc.err { - t.Errorf("\nexpected error: %s\nactual error: %s", tc.err, out) - } - if actual != cty.NilVal { - t.Errorf("expected cty.NilVal on error but got: %s", actual.GoString()) - } - }) - } -} - func TestFillType(t *testing.T) { tcs := map[string]struct { in cty.Value diff --git a/internal/moduletest/mocking/values_test.go b/internal/moduletest/mocking/values_test.go index 9e4e624615d9..1757be11cc51 100644 --- a/internal/moduletest/mocking/values_test.go +++ b/internal/moduletest/mocking/values_test.go @@ -537,6 +537,40 @@ func TestComputedValuesForDataSource(t *testing.T) { }), }, "nested_list_attribute_computed": { + target: cty.ObjectVal(map[string]cty.Value{ + "nested": cty.NullVal(cty.List(cty.Object(map[string]cty.Type{ + "id": cty.String, + "value": cty.String, + }))), + }), + with: cty.ObjectVal(map[string]cty.Value{ + "nested": cty.ListVal([]cty.Value{ + cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("myvalue"), + }), + }), + }), + schema: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "nested": { + NestedType: &configschema.Object{ + Attributes: computedAttributes, + Nesting: configschema.NestingList, + }, + Computed: true, + }, + }, + }, + expected: cty.ObjectVal(map[string]cty.Value{ + "nested": cty.ListVal([]cty.Value{ + cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("myvalue"), + "value": cty.StringVal("ssnk9qhr"), + }), + }), + }), + }, + "nested_list_attribute_computed_err": { target: cty.ObjectVal(map[string]cty.Value{ "nested": cty.NullVal(cty.List(cty.Object(map[string]cty.Type{ "id": cty.String, @@ -637,6 +671,40 @@ func TestComputedValuesForDataSource(t *testing.T) { }), }, "nested_set_attribute_computed": { + target: cty.ObjectVal(map[string]cty.Value{ + "nested": cty.NullVal(cty.Set(cty.Object(map[string]cty.Type{ + "id": cty.String, + "value": cty.String, + }))), + }), + with: cty.ObjectVal(map[string]cty.Value{ + "nested": cty.SetVal([]cty.Value{ + cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("myvalue"), + }), + }), + }), + schema: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "nested": { + NestedType: &configschema.Object{ + Attributes: computedAttributes, + Nesting: configschema.NestingSet, + }, + Computed: true, + }, + }, + }, + expected: cty.ObjectVal(map[string]cty.Value{ + "nested": cty.SetVal([]cty.Value{ + cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("myvalue"), + "value": cty.StringVal("ssnk9qhr"), + }), + }), + }), + }, + "nested_set_attribute_computed_err": { target: cty.ObjectVal(map[string]cty.Value{ "nested": cty.NullVal(cty.Set(cty.Object(map[string]cty.Type{ "id": cty.String, @@ -665,6 +733,9 @@ func TestComputedValuesForDataSource(t *testing.T) { "value": cty.String, })), }), + expectedFailures: []string{ + `Terraform could not compute a value for the target type set of object with the mocked data defined at :0,0-0 with the attribute ".nested": incompatible types; expected set of object, found object.`, + }, }, "nested_map_attribute": { target: cty.ObjectVal(map[string]cty.Value{ @@ -734,6 +805,40 @@ func TestComputedValuesForDataSource(t *testing.T) { }), }, "nested_map_attribute_computed": { + target: cty.ObjectVal(map[string]cty.Value{ + "nested": cty.NullVal(cty.Map(cty.Object(map[string]cty.Type{ + "id": cty.String, + "value": cty.String, + }))), + }), + with: cty.ObjectVal(map[string]cty.Value{ + "nested": cty.MapVal(map[string]cty.Value{ + "key": cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("myvalue"), + }), + }), + }), + schema: &configschema.Block{ + Attributes: map[string]*configschema.Attribute{ + "nested": { + NestedType: &configschema.Object{ + Attributes: computedAttributes, + Nesting: configschema.NestingMap, + }, + Computed: true, + }, + }, + }, + expected: cty.ObjectVal(map[string]cty.Value{ + "nested": cty.MapVal(map[string]cty.Value{ + "key": cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("myvalue"), + "value": cty.StringVal("ssnk9qhr"), + }), + }), + }), + }, + "nested_map_attribute_computed_err": { target: cty.ObjectVal(map[string]cty.Value{ "nested": cty.NullVal(cty.Map(cty.Object(map[string]cty.Type{ "id": cty.String, @@ -762,6 +867,9 @@ func TestComputedValuesForDataSource(t *testing.T) { "value": cty.String, })), }), + expectedFailures: []string{ + `Terraform could not compute a value for the target type map of object with the mocked data defined at :0,0-0 with the attribute ".nested[\"id\"]": incompatible types; expected object, found string.`, + }, }, "invalid_replacement_path": { target: cty.ObjectVal(map[string]cty.Value{ From 7951e69dcdde4268e4c5489d2dc96c69121c8282 Mon Sep 17 00:00:00 2001 From: Samsondeen Dare Date: Mon, 23 Feb 2026 12:43:25 +0100 Subject: [PATCH 04/11] remove fillIterable --- internal/moduletest/mocking/fill.go | 54 ++--------------------------- 1 file changed, 2 insertions(+), 52 deletions(-) diff --git a/internal/moduletest/mocking/fill.go b/internal/moduletest/mocking/fill.go index 676bf815893f..469e2735378b 100644 --- a/internal/moduletest/mocking/fill.go +++ b/internal/moduletest/mocking/fill.go @@ -22,61 +22,11 @@ func FillAttribute(in cty.Value, attribute *configschema.Attribute) (cty.Value, } func fillAttribute(in cty.Value, attribute *configschema.Attribute, path cty.Path) (cty.Value, error) { + ty := attribute.Type if attribute.NestedType != nil { - - switch attribute.NestedType.Nesting { - case configschema.NestingSingle, configschema.NestingGroup: - return fillObject(in, attribute, path) - case configschema.NestingSet: - return fillIterable(in, attribute, path) - case configschema.NestingList: - return fillIterable(in, attribute, path) - case configschema.NestingMap: - return fillIterable(in, attribute, path) - default: - panic(fmt.Errorf("unknown nesting mode: %d", attribute.NestedType.Nesting)) - } - } - - return fillType(in, attribute.Type, path) -} - -func fillObject(in cty.Value, attribute *configschema.Attribute, path cty.Path) (cty.Value, error) { - // Then the in value must be an object. - if !in.Type().IsObjectType() { - return cty.NilVal, path.NewErrorf("incompatible types; expected object type, found %s", in.Type().FriendlyName()) - } - - var names []string - for name := range attribute.NestedType.Attributes { - names = append(names, name) + ty = attribute.NestedType.ConfigType() } - if len(names) == 0 { - return cty.EmptyObjectVal, nil - } - - // Make the order we iterate through the attributes deterministic. We - // are generating random strings in here so it's worth making the - // operation repeatable. - sort.Strings(names) - - children := make(map[string]cty.Value) - for _, name := range names { - if in.Type().HasAttribute(name) { - child, err := fillAttribute(in.GetAttr(name), attribute.NestedType.Attributes[name], path.GetAttr(name)) - if err != nil { - return cty.NilVal, err - } - children[name] = child - continue - } - children[name] = GenerateValueForAttribute(attribute.NestedType.Attributes[name]) - } - return cty.ObjectVal(children), nil -} -func fillIterable(in cty.Value, attribute *configschema.Attribute, path cty.Path) (cty.Value, error) { - ty := attribute.NestedType.ConfigType() return fillType(in, ty, path) } From bfe600de3dee9b460b0394ac52b284ea7ecc0399 Mon Sep 17 00:00:00 2001 From: Samsondeen Dare Date: Mon, 23 Feb 2026 13:07:11 +0100 Subject: [PATCH 05/11] add changelog --- .changes/v1.15/ENHANCEMENTS-20260223-130341.yaml | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changes/v1.15/ENHANCEMENTS-20260223-130341.yaml diff --git a/.changes/v1.15/ENHANCEMENTS-20260223-130341.yaml b/.changes/v1.15/ENHANCEMENTS-20260223-130341.yaml new file mode 100644 index 000000000000..1393968cfc27 --- /dev/null +++ b/.changes/v1.15/ENHANCEMENTS-20260223-130341.yaml @@ -0,0 +1,5 @@ +kind: ENHANCEMENTS +body: "mocking: computed values are now correctly filled for nested list, set, and map attributes when using mock and override data in tests." +time: 2026-02-23T13:03:41.913383+01:00 +custom: + Issue: "37939" From 6089bd986b12164136c1efca12b9efe7812c5160 Mon Sep 17 00:00:00 2001 From: Samsondeen Dare Date: Mon, 23 Feb 2026 13:37:54 +0100 Subject: [PATCH 06/11] merge tests --- internal/command/test_test.go | 129 ++++++------------ .../mocking/tests/nested_mocked.tftest.hcl | 29 ++++ .../list.tftest.hcl} | 0 .../main.tf | 0 .../map.tftest.hcl} | 0 .../nested_list.tftest.hcl} | 0 .../nested_map.tftest.hcl} | 0 .../nested_set.tftest.hcl} | 0 .../set.tftest.hcl} | 0 .../test/override_data_list_attribute/main.tf | 15 -- .../test/override_data_map_attribute/main.tf | 15 -- .../main.tf | 15 -- .../main.tf | 15 -- .../main.tf | 15 -- 14 files changed, 72 insertions(+), 161 deletions(-) create mode 100644 internal/command/testdata/test/mocking/tests/nested_mocked.tftest.hcl rename internal/command/testdata/test/{override_data_list_attribute/main.tftest.hcl => override_data_attribute/list.tftest.hcl} (100%) rename internal/command/testdata/test/{override_data_set_attribute => override_data_attribute}/main.tf (100%) rename internal/command/testdata/test/{override_data_map_attribute/main.tftest.hcl => override_data_attribute/map.tftest.hcl} (100%) rename internal/command/testdata/test/{override_data_nested_list_attribute/main.tftest.hcl => override_data_attribute/nested_list.tftest.hcl} (100%) rename internal/command/testdata/test/{override_data_nested_map_attribute/main.tftest.hcl => override_data_attribute/nested_map.tftest.hcl} (100%) rename internal/command/testdata/test/{override_data_nested_set_attribute/main.tftest.hcl => override_data_attribute/nested_set.tftest.hcl} (100%) rename internal/command/testdata/test/{override_data_set_attribute/main.tftest.hcl => override_data_attribute/set.tftest.hcl} (100%) delete mode 100644 internal/command/testdata/test/override_data_list_attribute/main.tf delete mode 100644 internal/command/testdata/test/override_data_map_attribute/main.tf delete mode 100644 internal/command/testdata/test/override_data_nested_list_attribute/main.tf delete mode 100644 internal/command/testdata/test/override_data_nested_map_attribute/main.tf delete mode 100644 internal/command/testdata/test/override_data_nested_set_attribute/main.tf diff --git a/internal/command/test_test.go b/internal/command/test_test.go index a27a34dc96fc..e163db8380f1 100644 --- a/internal/command/test_test.go +++ b/internal/command/test_test.go @@ -292,7 +292,7 @@ func TestTest_Runs(t *testing.T) { code: 0, }, "mocking": { - expectedOut: []string{"10 passed, 0 failed."}, + expectedOut: []string{"11 passed, 0 failed."}, code: 0, }, "mocking-invalid": { @@ -5675,101 +5675,58 @@ func TestTest_TeardownOrder(t *testing.T) { } func TestTest_OverrideData(t *testing.T) { - tcs := map[string]struct { - dir string - code int - desc string - }{ - "list_attribute": { - dir: "override_data_list_attribute", - code: 0, - desc: "override_data with a computed cty.List(cty.Object) attribute", - }, - "nested_list_attribute": { - dir: "override_data_nested_list_attribute", - code: 0, - desc: "override_data with a computed NestedType NestingList attribute", - }, - "set_attribute": { - dir: "override_data_set_attribute", - code: 0, - desc: "override_data with a computed cty.Set(cty.Object) attribute", - }, - "nested_set_attribute": { - dir: "override_data_nested_set_attribute", - code: 0, - desc: "override_data with a computed NestedType NestingSet attribute", - }, - "map_attribute": { - dir: "override_data_map_attribute", - code: 0, - desc: "override_data with a computed cty.Map(cty.Object) attribute", - }, - "nested_map_attribute": { - dir: "override_data_nested_map_attribute", - code: 0, - desc: "override_data with a computed NestedType NestingMap attribute", - }, - } - - for name, tc := range tcs { - t.Run(name, func(t *testing.T) { - td := t.TempDir() - testCopyDir(t, testFixturePath(path.Join("test", tc.dir)), td) - t.Chdir(td) + td := t.TempDir() + testCopyDir(t, testFixturePath(path.Join("test", "override_data_attribute")), td) + t.Chdir(td) - provider := testing_command.NewProvider(nil) - providerSource, closeFn := newMockProviderSource(t, map[string][]string{ - "test": {"1.0.0"}, - }) - defer closeFn() + provider := testing_command.NewProvider(nil) + providerSource, closeFn := newMockProviderSource(t, map[string][]string{ + "test": {"1.0.0"}, + }) + defer closeFn() - streams, done := terminal.StreamsForTesting(t) - view := views.NewView(streams) - ui := new(cli.MockUi) + streams, done := terminal.StreamsForTesting(t) + view := views.NewView(streams) + ui := new(cli.MockUi) - meta := Meta{ - testingOverrides: metaOverridesForProvider(provider.Provider), - Ui: ui, - View: view, - Streams: streams, - ProviderSource: providerSource, - } + meta := Meta{ + testingOverrides: metaOverridesForProvider(provider.Provider), + Ui: ui, + View: view, + Streams: streams, + ProviderSource: providerSource, + } - init := &InitCommand{ - Meta: meta, - } + init := &InitCommand{ + Meta: meta, + } - if code := init.Run(nil); code != 0 { - output := done(t) - t.Fatalf("expected init status code 0 but got %d: %s", code, output.All()) - } + if code := init.Run(nil); code != 0 { + output := done(t) + t.Fatalf("expected init status code 0 but got %d: %s", code, output.All()) + } - // Reset the streams for the next command. - streams, done = terminal.StreamsForTesting(t) - meta.Streams = streams - meta.View = views.NewView(streams) + // Reset the streams for the next command. + streams, done = terminal.StreamsForTesting(t) + meta.Streams = streams + meta.View = views.NewView(streams) - c := &TestCommand{ - Meta: meta, - } + c := &TestCommand{ + Meta: meta, + } - code := c.Run([]string{"-no-color"}) - output := done(t) + code := c.Run([]string{"-no-color"}) + output := done(t) - if code != tc.code { - t.Errorf("expected status code %d but got %d:\n\n%s", tc.code, code, output.All()) - } + if code != 0 { + t.Errorf("expected status code %d but got %d:\n\n%s", 0, code, output.All()) + } - if tc.code == 0 { - if !strings.Contains(output.Stdout(), "1 passed, 0 failed.") { - t.Errorf("expected passing test output but got:\n\nstdout:\n%s\nstderr:\n%s", output.Stdout(), output.Stderr()) - } - if output.Stderr() != "" { - t.Errorf("unexpected stderr output:\n%s", output.Stderr()) - } - } - }) + if !strings.Contains(output.Stdout(), "6 passed, 0 failed.") { + t.Errorf("expected passing test output but got:\n\nstdout:\n%s\nstderr:\n%s", output.Stdout(), output.Stderr()) + } + if output.Stderr() != "" { + t.Errorf("unexpected stderr output:\n%s", output.Stderr()) } } diff --git a/internal/command/testdata/test/mocking/tests/nested_mocked.tftest.hcl b/internal/command/testdata/test/mocking/tests/nested_mocked.tftest.hcl new file mode 100644 index 000000000000..8c5925ac6210 --- /dev/null +++ b/internal/command/testdata/test/mocking/tests/nested_mocked.tftest.hcl @@ -0,0 +1,29 @@ +mock_provider "test" { + alias = "primary" + + mock_resource "test_resource" { + defaults = { + id = "aaaa" + list_value = [{name = "first"}, {name = "second"}] + nested_list_value = [{name = "first"}, {name = "second"}] + set_value = [{name = "first"}, {name = "second"}] + nested_set_value = [{name = "first"}, {name = "second"}] + map_value = [{name = "first"}, {name = "second"}] + nested_map_value = [{name = "first"}, {name = "second"}] + } + } +} + +variables { + instances = 1 + child_instances = 0 +} + + +run "test" { + + assert { + condition = test_resource.primary[0].id == "aaaa" + error_message = "did not apply mocks" + } +} diff --git a/internal/command/testdata/test/override_data_list_attribute/main.tftest.hcl b/internal/command/testdata/test/override_data_attribute/list.tftest.hcl similarity index 100% rename from internal/command/testdata/test/override_data_list_attribute/main.tftest.hcl rename to internal/command/testdata/test/override_data_attribute/list.tftest.hcl diff --git a/internal/command/testdata/test/override_data_set_attribute/main.tf b/internal/command/testdata/test/override_data_attribute/main.tf similarity index 100% rename from internal/command/testdata/test/override_data_set_attribute/main.tf rename to internal/command/testdata/test/override_data_attribute/main.tf diff --git a/internal/command/testdata/test/override_data_map_attribute/main.tftest.hcl b/internal/command/testdata/test/override_data_attribute/map.tftest.hcl similarity index 100% rename from internal/command/testdata/test/override_data_map_attribute/main.tftest.hcl rename to internal/command/testdata/test/override_data_attribute/map.tftest.hcl diff --git a/internal/command/testdata/test/override_data_nested_list_attribute/main.tftest.hcl b/internal/command/testdata/test/override_data_attribute/nested_list.tftest.hcl similarity index 100% rename from internal/command/testdata/test/override_data_nested_list_attribute/main.tftest.hcl rename to internal/command/testdata/test/override_data_attribute/nested_list.tftest.hcl diff --git a/internal/command/testdata/test/override_data_nested_map_attribute/main.tftest.hcl b/internal/command/testdata/test/override_data_attribute/nested_map.tftest.hcl similarity index 100% rename from internal/command/testdata/test/override_data_nested_map_attribute/main.tftest.hcl rename to internal/command/testdata/test/override_data_attribute/nested_map.tftest.hcl diff --git a/internal/command/testdata/test/override_data_nested_set_attribute/main.tftest.hcl b/internal/command/testdata/test/override_data_attribute/nested_set.tftest.hcl similarity index 100% rename from internal/command/testdata/test/override_data_nested_set_attribute/main.tftest.hcl rename to internal/command/testdata/test/override_data_attribute/nested_set.tftest.hcl diff --git a/internal/command/testdata/test/override_data_set_attribute/main.tftest.hcl b/internal/command/testdata/test/override_data_attribute/set.tftest.hcl similarity index 100% rename from internal/command/testdata/test/override_data_set_attribute/main.tftest.hcl rename to internal/command/testdata/test/override_data_attribute/set.tftest.hcl diff --git a/internal/command/testdata/test/override_data_list_attribute/main.tf b/internal/command/testdata/test/override_data_list_attribute/main.tf deleted file mode 100644 index 00330c09aea6..000000000000 --- a/internal/command/testdata/test/override_data_list_attribute/main.tf +++ /dev/null @@ -1,15 +0,0 @@ -terraform { - required_providers { - test = { - source = "hashicorp/test" - } - } -} - -data "test_complex_data_source" "datasource" { - id = "resource" -} - -output "list_value" { - value = data.test_complex_data_source.datasource.list_value -} diff --git a/internal/command/testdata/test/override_data_map_attribute/main.tf b/internal/command/testdata/test/override_data_map_attribute/main.tf deleted file mode 100644 index 520c9e1de8bf..000000000000 --- a/internal/command/testdata/test/override_data_map_attribute/main.tf +++ /dev/null @@ -1,15 +0,0 @@ -terraform { - required_providers { - test = { - source = "hashicorp/test" - } - } -} - -data "test_complex_data_source" "datasource" { - id = "resource" -} - -output "map_value" { - value = data.test_complex_data_source.datasource.map_value -} diff --git a/internal/command/testdata/test/override_data_nested_list_attribute/main.tf b/internal/command/testdata/test/override_data_nested_list_attribute/main.tf deleted file mode 100644 index ea41b36bc2e2..000000000000 --- a/internal/command/testdata/test/override_data_nested_list_attribute/main.tf +++ /dev/null @@ -1,15 +0,0 @@ -terraform { - required_providers { - test = { - source = "hashicorp/test" - } - } -} - -data "test_complex_data_source" "datasource" { - id = "resource" -} - -output "nested_list_value" { - value = data.test_complex_data_source.datasource.nested_list_value -} diff --git a/internal/command/testdata/test/override_data_nested_map_attribute/main.tf b/internal/command/testdata/test/override_data_nested_map_attribute/main.tf deleted file mode 100644 index 0df44632df41..000000000000 --- a/internal/command/testdata/test/override_data_nested_map_attribute/main.tf +++ /dev/null @@ -1,15 +0,0 @@ -terraform { - required_providers { - test = { - source = "hashicorp/test" - } - } -} - -data "test_complex_data_source" "datasource" { - id = "resource" -} - -output "nested_map_value" { - value = data.test_complex_data_source.datasource.nested_map_value -} diff --git a/internal/command/testdata/test/override_data_nested_set_attribute/main.tf b/internal/command/testdata/test/override_data_nested_set_attribute/main.tf deleted file mode 100644 index 5174b78b954f..000000000000 --- a/internal/command/testdata/test/override_data_nested_set_attribute/main.tf +++ /dev/null @@ -1,15 +0,0 @@ -terraform { - required_providers { - test = { - source = "hashicorp/test" - } - } -} - -data "test_complex_data_source" "datasource" { - id = "resource" -} - -output "nested_set_value" { - value = data.test_complex_data_source.datasource.nested_set_value -} From 2d5f51edd48cf620c39420927e643e253ec01cd1 Mon Sep 17 00:00:00 2001 From: Samsondeen Dare Date: Mon, 23 Feb 2026 14:11:10 +0100 Subject: [PATCH 07/11] add test for mock_resource --- .../command/testdata/test/mocking/main.tf | 11 +- .../mocking/tests/nested_mocked.tftest.hcl | 42 ++++- internal/command/testing/test_provider.go | 151 ++++++++++-------- 3 files changed, 133 insertions(+), 71 deletions(-) diff --git a/internal/command/testdata/test/mocking/main.tf b/internal/command/testdata/test/mocking/main.tf index 49506e06c38f..9d0180ca1b24 100644 --- a/internal/command/testdata/test/mocking/main.tf +++ b/internal/command/testdata/test/mocking/main.tf @@ -24,12 +24,17 @@ variable "child_instances" { resource "test_resource" "primary" { provider = test.primary - count = var.instances + count = var.instances +} + +resource "test_complex_resource" "primary" { + provider = test.primary + count = var.instances } resource "test_resource" "secondary" { provider = test.secondary - count = var.instances + count = var.instances } module "child" { @@ -38,7 +43,7 @@ module "child" { source = "./child" providers = { - test.primary = test.primary + test.primary = test.primary test.secondary = test.secondary } diff --git a/internal/command/testdata/test/mocking/tests/nested_mocked.tftest.hcl b/internal/command/testdata/test/mocking/tests/nested_mocked.tftest.hcl index 8c5925ac6210..28cb1d9be769 100644 --- a/internal/command/testdata/test/mocking/tests/nested_mocked.tftest.hcl +++ b/internal/command/testdata/test/mocking/tests/nested_mocked.tftest.hcl @@ -1,15 +1,29 @@ mock_provider "test" { alias = "primary" - mock_resource "test_resource" { + mock_resource "test_complex_resource" { defaults = { id = "aaaa" list_value = [{name = "first"}, {name = "second"}] nested_list_value = [{name = "first"}, {name = "second"}] set_value = [{name = "first"}, {name = "second"}] nested_set_value = [{name = "first"}, {name = "second"}] - map_value = [{name = "first"}, {name = "second"}] - nested_map_value = [{name = "first"}, {name = "second"}] + map_value = { + "key1": { + name = "first" + }, + "key2": { + name = "third" + } + } + nested_map_value = { + "key1": { + name = "first" + }, + "key2": { + name = "third" + } + } } } } @@ -23,7 +37,27 @@ variables { run "test" { assert { - condition = test_resource.primary[0].id == "aaaa" + condition = test_complex_resource.primary[0].id == "aaaa" + error_message = "did not apply mocks" + } + + assert { + condition = test_complex_resource.primary[0].list_value[0].name == "first" + error_message = "did not apply mocks" + } + + assert { + condition = test_complex_resource.primary[0].nested_list_value[0].name == "first" + error_message = "did not apply mocks" + } + + assert { + condition = test_complex_resource.primary[0].map_value["key1"].name == "first" + error_message = "did not apply mocks" + } + + assert { + condition = test_complex_resource.primary[0].nested_map_value["key1"].name == "first" error_message = "did not apply mocks" } } diff --git a/internal/command/testing/test_provider.go b/internal/command/testing/test_provider.go index 2f921975b3db..3feda5ea9be4 100644 --- a/internal/command/testing/test_provider.go +++ b/internal/command/testing/test_provider.go @@ -5,6 +5,7 @@ package testing import ( "fmt" + "maps" "path" "strings" "sync" @@ -20,6 +21,76 @@ import ( ) var ( + // withBlockTypeAttributes contains additional block type attributes that are used across + // different resources and types, so this function serves as a place to store the shared attributes. + withBlockTypeAttributes = func(original map[string]*configschema.Attribute) map[string]*configschema.Attribute { + attrs := map[string]*configschema.Attribute{ + "list_value": { + Type: cty.List(cty.Object(map[string]cty.Type{ + "name": cty.String, + "value": cty.String, + })), + Computed: true, + Optional: true, + }, + "set_value": { + Type: cty.Set(cty.Object(map[string]cty.Type{ + "name": cty.String, + "value": cty.String, + })), + Computed: true, + Optional: true, + }, + "map_value": { + Type: cty.Map(cty.Object(map[string]cty.Type{ + "name": cty.String, + "value": cty.String, + })), + Computed: true, + Optional: true, + }, + + // The below nested_* attributes represent config supporting + // attributes as nested blocks, where terraform interprets the nested blocks + // as attributes within the schema + "nested_list_value": { + Computed: true, + Optional: true, + NestedType: &configschema.Object{ + Nesting: configschema.NestingList, + Attributes: map[string]*configschema.Attribute{ + "name": {Type: cty.String, Optional: true}, + "value": {Type: cty.String, Optional: true}, + }, + }, + }, + "nested_set_value": { + Computed: true, + Optional: true, + NestedType: &configschema.Object{ + Nesting: configschema.NestingSet, + Attributes: map[string]*configschema.Attribute{ + "name": {Type: cty.String, Optional: true}, + "value": {Type: cty.String, Optional: true}, + }, + }, + }, + "nested_map_value": { + Computed: true, + Optional: true, + NestedType: &configschema.Object{ + Nesting: configschema.NestingMap, + Attributes: map[string]*configschema.Attribute{ + "name": {Type: cty.String, Optional: true}, + "value": {Type: cty.String, Optional: true}, + }, + }, + }, + } + maps.Copy(original, attrs) + return original + } + ProviderSchema = &providers.GetProviderSchemaResponse{ Provider: providers.Schema{ Body: &configschema.Block{ @@ -44,6 +115,20 @@ var ( }, }, }, + "test_complex_resource": { + Body: &configschema.Block{ + Attributes: withBlockTypeAttributes(map[string]*configschema.Attribute{ + "id": {Type: cty.String, Optional: true, Computed: true}, + "value": {Type: cty.String, Optional: true}, + "interrupt_count": {Type: cty.Number, Optional: true}, + "destroy_fail": {Type: cty.Bool, Optional: true, Computed: true}, + "create_wait_seconds": {Type: cty.Number, Optional: true}, + "destroy_wait_seconds": {Type: cty.Number, Optional: true}, + "write_only": {Type: cty.String, Optional: true, WriteOnly: true}, + "defer": {Type: cty.Bool, Optional: true}, + }), + }, + }, }, DataSources: map[string]providers.Schema{ "test_data_source": { @@ -69,73 +154,11 @@ var ( "test_complex_data_source": { Body: &configschema.Block{ - Attributes: map[string]*configschema.Attribute{ + Attributes: withBlockTypeAttributes(map[string]*configschema.Attribute{ "id": {Type: cty.String, Required: true}, "value": {Type: cty.String, Computed: true}, "write_only": {Type: cty.String, Optional: true, WriteOnly: true}, - "list_value": { - Type: cty.List(cty.Object(map[string]cty.Type{ - "name": cty.String, - "value": cty.String, - })), - Computed: true, - Optional: true, - }, - "set_value": { - Type: cty.Set(cty.Object(map[string]cty.Type{ - "name": cty.String, - "value": cty.String, - })), - Computed: true, - Optional: true, - }, - "map_value": { - Type: cty.Map(cty.Object(map[string]cty.Type{ - "name": cty.String, - "value": cty.String, - })), - Computed: true, - Optional: true, - }, - - // The below nested_* attributes represent config supporting - // attributes as nested blocks, where terraform interprets the nested blocks - // as attributes within the schema - "nested_list_value": { - Computed: true, - Optional: true, - NestedType: &configschema.Object{ - Nesting: configschema.NestingList, - Attributes: map[string]*configschema.Attribute{ - "name": {Type: cty.String, Optional: true}, - "value": {Type: cty.String, Optional: true}, - }, - }, - }, - "nested_set_value": { - Computed: true, - Optional: true, - NestedType: &configschema.Object{ - Nesting: configschema.NestingSet, - Attributes: map[string]*configschema.Attribute{ - "name": {Type: cty.String, Optional: true}, - "value": {Type: cty.String, Optional: true}, - }, - }, - }, - "nested_map_value": { - Computed: true, - Optional: true, - NestedType: &configschema.Object{ - Nesting: configschema.NestingMap, - Attributes: map[string]*configschema.Attribute{ - "name": {Type: cty.String, Optional: true}, - "value": {Type: cty.String, Optional: true}, - }, - }, - }, - // We never actually reference these values from a data // source, but we have tests that use the same cty.Value // to represent a test_resource and a test_data_source @@ -146,7 +169,7 @@ var ( "create_wait_seconds": {Type: cty.Number, Computed: true}, "destroy_wait_seconds": {Type: cty.Number, Computed: true}, "defer": {Type: cty.Bool, Computed: true}, - }, + }), }, }, }, From 76440932801161b3f2278356bbe2f88d2e844056 Mon Sep 17 00:00:00 2001 From: Samsondeen Dare Date: Mon, 23 Feb 2026 10:43:39 +0100 Subject: [PATCH 08/11] Fill computed values of Nested type --- internal/command/test_test.go | 109 +++++++++++------- .../test/override_data_list_attribute/main.tf | 15 +++ .../main.tftest.hcl | 46 ++++++++ .../main.tf | 15 +++ .../main.tftest.hcl | 46 ++++++++ internal/command/testing/test_provider.go | 27 +++++ internal/moduletest/mocking/fill.go | 57 ++++++++- 7 files changed, 271 insertions(+), 44 deletions(-) create mode 100644 internal/command/testdata/test/override_data_list_attribute/main.tf create mode 100644 internal/command/testdata/test/override_data_list_attribute/main.tftest.hcl create mode 100644 internal/command/testdata/test/override_data_nested_list_attribute/main.tf create mode 100644 internal/command/testdata/test/override_data_nested_list_attribute/main.tftest.hcl diff --git a/internal/command/test_test.go b/internal/command/test_test.go index e163db8380f1..ee7292b5c223 100644 --- a/internal/command/test_test.go +++ b/internal/command/test_test.go @@ -5674,59 +5674,82 @@ func TestTest_TeardownOrder(t *testing.T) { } } -func TestTest_OverrideData(t *testing.T) { - td := t.TempDir() - testCopyDir(t, testFixturePath(path.Join("test", "override_data_attribute")), td) - t.Chdir(td) +func TestTest_OverrideDataListAttribute(t *testing.T) { + tcs := map[string]struct { + dir string + code int + desc string + }{ + "plain_list_attribute": { + dir: "override_data_list_attribute", + code: 0, + desc: "override_data with a computed cty.List(cty.Object) attribute", + }, + "nested_list_attribute": { + dir: "override_data_nested_list_attribute", + code: 0, + desc: "override_data with a computed NestedType NestingList attribute", + }, + } - provider := testing_command.NewProvider(nil) - providerSource, closeFn := newMockProviderSource(t, map[string][]string{ - "test": {"1.0.0"}, - }) - defer closeFn() + for name, tc := range tcs { + t.Run(name, func(t *testing.T) { + td := t.TempDir() + testCopyDir(t, testFixturePath(path.Join("test", tc.dir)), td) + t.Chdir(td) - streams, done := terminal.StreamsForTesting(t) - view := views.NewView(streams) - ui := new(cli.MockUi) + provider := testing_command.NewProvider(nil) + providerSource, closeFn := newMockProviderSource(t, map[string][]string{ + "test": {"1.0.0"}, + }) + defer closeFn() - meta := Meta{ - testingOverrides: metaOverridesForProvider(provider.Provider), - Ui: ui, - View: view, - Streams: streams, - ProviderSource: providerSource, - } + streams, done := terminal.StreamsForTesting(t) + view := views.NewView(streams) + ui := new(cli.MockUi) - init := &InitCommand{ - Meta: meta, - } + meta := Meta{ + testingOverrides: metaOverridesForProvider(provider.Provider), + Ui: ui, + View: view, + Streams: streams, + ProviderSource: providerSource, + } - if code := init.Run(nil); code != 0 { - output := done(t) - t.Fatalf("expected init status code 0 but got %d: %s", code, output.All()) - } + init := &InitCommand{ + Meta: meta, + } - // Reset the streams for the next command. - streams, done = terminal.StreamsForTesting(t) - meta.Streams = streams - meta.View = views.NewView(streams) + if code := init.Run(nil); code != 0 { + output := done(t) + t.Fatalf("expected init status code 0 but got %d: %s", code, output.All()) + } - c := &TestCommand{ - Meta: meta, - } + // Reset the streams for the next command. + streams, done = terminal.StreamsForTesting(t) + meta.Streams = streams + meta.View = views.NewView(streams) - code := c.Run([]string{"-no-color"}) - output := done(t) + c := &TestCommand{ + Meta: meta, + } - if code != 0 { - t.Errorf("expected status code %d but got %d:\n\n%s", 0, code, output.All()) - } + code := c.Run([]string{"-no-color"}) + output := done(t) - if !strings.Contains(output.Stdout(), "6 passed, 0 failed.") { - t.Errorf("expected passing test output but got:\n\nstdout:\n%s\nstderr:\n%s", output.Stdout(), output.Stderr()) - } - if output.Stderr() != "" { - t.Errorf("unexpected stderr output:\n%s", output.Stderr()) + if code != tc.code { + t.Errorf("expected status code %d but got %d:\n\n%s", tc.code, code, output.All()) + } + + if tc.code == 0 { + if !strings.Contains(output.Stdout(), "1 passed, 0 failed.") { + t.Errorf("expected passing test output but got:\n\nstdout:\n%s\nstderr:\n%s", output.Stdout(), output.Stderr()) + } + if output.Stderr() != "" { + t.Errorf("unexpected stderr output:\n%s", output.Stderr()) + } + } + }) } } diff --git a/internal/command/testdata/test/override_data_list_attribute/main.tf b/internal/command/testdata/test/override_data_list_attribute/main.tf new file mode 100644 index 000000000000..f9d19bfd7435 --- /dev/null +++ b/internal/command/testdata/test/override_data_list_attribute/main.tf @@ -0,0 +1,15 @@ +terraform { + required_providers { + test = { + source = "hashicorp/test" + } + } +} + +data "test_data_source" "datasource" { + id = "resource" +} + +output "list_value" { + value = data.test_data_source.datasource.list_value +} diff --git a/internal/command/testdata/test/override_data_list_attribute/main.tftest.hcl b/internal/command/testdata/test/override_data_list_attribute/main.tftest.hcl new file mode 100644 index 000000000000..597e98ac4f9f --- /dev/null +++ b/internal/command/testdata/test/override_data_list_attribute/main.tftest.hcl @@ -0,0 +1,46 @@ +provider "test" {} + +override_data { + target = data.test_data_source.datasource + values = { + list_value = [ + { + name = "first" + value = "one" + }, + { + name = "second" + value = "two" + }, + ] + } +} + +run "test_override_data_list_attribute" { + command = plan + + assert { + condition = length(data.test_data_source.datasource.list_value) == 2 + error_message = "Expected list_value to have 2 elements, got ${length(data.test_data_source.datasource.list_value)}" + } + + assert { + condition = data.test_data_source.datasource.list_value[0].name == "first" + error_message = "Expected first element name to be 'first'" + } + + assert { + condition = data.test_data_source.datasource.list_value[0].value == "one" + error_message = "Expected first element value to be 'one'" + } + + assert { + condition = data.test_data_source.datasource.list_value[1].name == "second" + error_message = "Expected second element name to be 'second'" + } + + assert { + condition = data.test_data_source.datasource.list_value[1].value == "two" + error_message = "Expected second element value to be 'two'" + } +} diff --git a/internal/command/testdata/test/override_data_nested_list_attribute/main.tf b/internal/command/testdata/test/override_data_nested_list_attribute/main.tf new file mode 100644 index 000000000000..52cfeb5fa44a --- /dev/null +++ b/internal/command/testdata/test/override_data_nested_list_attribute/main.tf @@ -0,0 +1,15 @@ +terraform { + required_providers { + test = { + source = "hashicorp/test" + } + } +} + +data "test_data_source" "datasource" { + id = "resource" +} + +output "nested_list_value" { + value = data.test_data_source.datasource.nested_list_value +} diff --git a/internal/command/testdata/test/override_data_nested_list_attribute/main.tftest.hcl b/internal/command/testdata/test/override_data_nested_list_attribute/main.tftest.hcl new file mode 100644 index 000000000000..fe5a85019600 --- /dev/null +++ b/internal/command/testdata/test/override_data_nested_list_attribute/main.tftest.hcl @@ -0,0 +1,46 @@ +provider "test" {} + +override_data { + target = data.test_data_source.datasource + values = { + nested_list_value = [ + { + name = "first" + value = "one" + }, + { + name = "second" + value = "two" + }, + ] + } +} + +run "test_override_data_nested_list_attribute" { + command = plan + + assert { + condition = length(data.test_data_source.datasource.nested_list_value) == 2 + error_message = "Expected nested_list_value to have 2 elements, got ${length(data.test_data_source.datasource.nested_list_value)}" + } + + assert { + condition = data.test_data_source.datasource.nested_list_value[0].name == "first" + error_message = "Expected first element name to be 'first'" + } + + assert { + condition = data.test_data_source.datasource.nested_list_value[0].value == "one" + error_message = "Expected first element value to be 'one'" + } + + assert { + condition = data.test_data_source.datasource.nested_list_value[1].name == "second" + error_message = "Expected second element name to be 'second'" + } + + assert { + condition = data.test_data_source.datasource.nested_list_value[1].value == "two" + error_message = "Expected second element value to be 'two'" + } +} diff --git a/internal/command/testing/test_provider.go b/internal/command/testing/test_provider.go index 3feda5ea9be4..a2c3390fb2f9 100644 --- a/internal/command/testing/test_provider.go +++ b/internal/command/testing/test_provider.go @@ -138,6 +138,33 @@ var ( "value": {Type: cty.String, Computed: true}, "write_only": {Type: cty.String, Optional: true, WriteOnly: true}, + // list_value is a computed attribute of list-of-objects + // type, used to test that override_data can correctly + // override list-type attributes (see GitHub issue #37939). + "list_value": { + Type: cty.List(cty.Object(map[string]cty.Type{ + "name": cty.String, + "value": cty.String, + })), + Computed: true, + }, + + // nested_list_value is a computed attribute using NestedType + // with NestingList, used to test that override_data can correctly + // override nested list-type attributes (see GitHub issue #37939). + // Many real-world providers (e.g. Databricks) define list-of-objects + // attributes this way rather than using a plain cty.List type. + "nested_list_value": { + Computed: true, + NestedType: &configschema.Object{ + Nesting: configschema.NestingList, + Attributes: map[string]*configschema.Attribute{ + "name": {Type: cty.String, Optional: true}, + "value": {Type: cty.String, Optional: true}, + }, + }, + }, + // We never actually reference these values from a data // source, but we have tests that use the same cty.Value // to represent a test_resource and a test_data_source diff --git a/internal/moduletest/mocking/fill.go b/internal/moduletest/mocking/fill.go index 469e2735378b..6eccf33e214d 100644 --- a/internal/moduletest/mocking/fill.go +++ b/internal/moduletest/mocking/fill.go @@ -24,12 +24,67 @@ func FillAttribute(in cty.Value, attribute *configschema.Attribute) (cty.Value, func fillAttribute(in cty.Value, attribute *configschema.Attribute, path cty.Path) (cty.Value, error) { ty := attribute.Type if attribute.NestedType != nil { - ty = attribute.NestedType.ConfigType() + + switch attribute.NestedType.Nesting { + case configschema.NestingSingle, configschema.NestingGroup: + return fillObject(in, attribute, path) + case configschema.NestingSet: + return cty.SetValEmpty(attribute.ImpliedType().ElementType()), nil + case configschema.NestingList: + return fillIterable(in, attribute, path) + case configschema.NestingMap: + return cty.MapValEmpty(attribute.ImpliedType().ElementType()), nil + default: + panic(fmt.Errorf("unknown nesting mode: %d", attribute.NestedType.Nesting)) + } } return fillType(in, ty, path) } +func fillObject(in cty.Value, attribute *configschema.Attribute, path cty.Path) (cty.Value, error) { + // Then the in value must be an object. + if !in.Type().IsObjectType() { + return cty.NilVal, path.NewErrorf("incompatible types; expected object type, found %s", in.Type().FriendlyName()) + } + + var names []string + for name := range attribute.NestedType.Attributes { + names = append(names, name) + } + if len(names) == 0 { + return cty.EmptyObjectVal, nil + } + + // Make the order we iterate through the attributes deterministic. We + // are generating random strings in here so it's worth making the + // operation repeatable. + sort.Strings(names) + + children := make(map[string]cty.Value) + for _, name := range names { + if in.Type().HasAttribute(name) { + child, err := fillAttribute(in.GetAttr(name), attribute.NestedType.Attributes[name], path.GetAttr(name)) + if err != nil { + return cty.NilVal, err + } + children[name] = child + continue + } + children[name] = GenerateValueForAttribute(attribute.NestedType.Attributes[name]) + } + return cty.ObjectVal(children), nil +} + +func fillIterable(in cty.Value, attribute *configschema.Attribute, path cty.Path) (cty.Value, error) { + ty := attribute.NestedType.ConfigType() + out, err := fillType(in, ty, path) + if err != nil { + return cty.NilVal, err + } + return out, err +} + // FillType makes the input value match the target type by adding attributes // directly to it or to any nested objects. Essentially, this is a "safe" // conversion between two objects. From da4cf6b5c1ad1352d5e92abfcb5951b38aba61cb Mon Sep 17 00:00:00 2001 From: Samsondeen Dare Date: Mon, 13 Apr 2026 12:57:53 +0200 Subject: [PATCH 09/11] retain legacy behaviour of empty collection when given an object as mock --- internal/command/test_test.go | 73 ++++++++++++++----- .../main.tf | 15 ++++ .../main.tftest.hcl | 19 +++++ .../main.tf | 15 ++++ .../main.tftest.hcl | 40 ++++++++++ .../main.tf | 15 ++++ .../main.tftest.hcl | 44 +++++++++++ .../main.tf | 15 ++++ .../main.tftest.hcl | 12 +++ .../main.tf | 15 ++++ .../main.tftest.hcl | 19 +++++ .../main.tf | 15 ++++ .../main.tftest.hcl | 44 +++++++++++ internal/moduletest/mocking/fill.go | 47 ++++++++---- 14 files changed, 352 insertions(+), 36 deletions(-) create mode 100644 internal/command/testdata/test/override_data_complex_nested_set_attribute_object/main.tf create mode 100644 internal/command/testdata/test/override_data_complex_nested_set_attribute_object/main.tftest.hcl create mode 100644 internal/command/testdata/test/override_data_complex_set_attribute_partial_elements/main.tf create mode 100644 internal/command/testdata/test/override_data_complex_set_attribute_partial_elements/main.tftest.hcl create mode 100644 internal/command/testdata/test/override_data_list_attribute_partial_elements/main.tf create mode 100644 internal/command/testdata/test/override_data_list_attribute_partial_elements/main.tftest.hcl create mode 100644 internal/command/testdata/test/override_data_nested_list_attribute_invalid_type/main.tf create mode 100644 internal/command/testdata/test/override_data_nested_list_attribute_invalid_type/main.tftest.hcl create mode 100644 internal/command/testdata/test/override_data_nested_list_attribute_object/main.tf create mode 100644 internal/command/testdata/test/override_data_nested_list_attribute_object/main.tftest.hcl create mode 100644 internal/command/testdata/test/override_data_nested_list_attribute_partial_elements/main.tf create mode 100644 internal/command/testdata/test/override_data_nested_list_attribute_partial_elements/main.tftest.hcl diff --git a/internal/command/test_test.go b/internal/command/test_test.go index ee7292b5c223..d96840e39aa8 100644 --- a/internal/command/test_test.go +++ b/internal/command/test_test.go @@ -5674,21 +5674,52 @@ func TestTest_TeardownOrder(t *testing.T) { } } -func TestTest_OverrideDataListAttribute(t *testing.T) { +func TestTest_OverrideDataMocking(t *testing.T) { tcs := map[string]struct { - dir string - code int - desc string + dir string + expectedCode int + expectedStdout string + expectedStderr string }{ "plain_list_attribute": { - dir: "override_data_list_attribute", - code: 0, - desc: "override_data with a computed cty.List(cty.Object) attribute", + dir: "override_data_list_attribute", + expectedCode: 0, + expectedStdout: "1 passed, 0 failed.", }, "nested_list_attribute": { - dir: "override_data_nested_list_attribute", - code: 0, - desc: "override_data with a computed NestedType NestingList attribute", + dir: "override_data_nested_list_attribute", + expectedCode: 0, + expectedStdout: "1 passed, 0 failed.", + }, + "nested_list_attribute_with_object_value": { + dir: "override_data_nested_list_attribute_object", + expectedCode: 0, + expectedStdout: "1 passed, 0 failed.", + }, + "nested_list_attribute_with_invalid_type_value": { + dir: "override_data_nested_list_attribute_invalid_type", + expectedCode: 1, + expectedStderr: "incompatible types; expected list of object, found", + }, + "list_attribute_with_partial_element_values": { + dir: "override_data_list_attribute_partial_elements", + expectedCode: 0, + expectedStdout: "1 passed, 0 failed.", + }, + "set_attribute_with_partial_element_values": { + dir: "override_data_complex_set_attribute_partial_elements", + expectedCode: 0, + expectedStdout: "1 passed, 0 failed.", + }, + "nested_set_attribute_with_object_value": { + dir: "override_data_complex_nested_set_attribute_object", + expectedCode: 0, + expectedStdout: "1 passed, 0 failed.", + }, + "nested_list_attribute_with_partial_element_values": { + dir: "override_data_nested_list_attribute_partial_elements", + expectedCode: 0, + expectedStdout: "1 passed, 0 failed.", }, } @@ -5725,7 +5756,6 @@ func TestTest_OverrideDataListAttribute(t *testing.T) { t.Fatalf("expected init status code 0 but got %d: %s", code, output.All()) } - // Reset the streams for the next command. streams, done = terminal.StreamsForTesting(t) meta.Streams = streams meta.View = views.NewView(streams) @@ -5737,17 +5767,20 @@ func TestTest_OverrideDataListAttribute(t *testing.T) { code := c.Run([]string{"-no-color"}) output := done(t) - if code != tc.code { - t.Errorf("expected status code %d but got %d:\n\n%s", tc.code, code, output.All()) + if code != tc.expectedCode { + t.Fatalf("expected status code %d but got %d:\n\n%s", tc.expectedCode, code, output.All()) } - if tc.code == 0 { - if !strings.Contains(output.Stdout(), "1 passed, 0 failed.") { - t.Errorf("expected passing test output but got:\n\nstdout:\n%s\nstderr:\n%s", output.Stdout(), output.Stderr()) - } - if output.Stderr() != "" { - t.Errorf("unexpected stderr output:\n%s", output.Stderr()) - } + if tc.expectedStdout != "" && !strings.Contains(output.Stdout(), tc.expectedStdout) { + t.Errorf("expected stdout to contain %q but got:\n\nstdout:\n%s\nstderr:\n%s", tc.expectedStdout, output.Stdout(), output.Stderr()) + } + + if tc.expectedStderr != "" && !strings.Contains(output.Stderr(), tc.expectedStderr) { + t.Errorf("expected stderr to contain %q but got:\n\nstdout:\n%s\nstderr:\n%s", tc.expectedStderr, output.Stdout(), output.Stderr()) + } + + if tc.expectedCode == 0 && output.Stderr() != "" { + t.Errorf("unexpected stderr output:\n%s", output.Stderr()) } }) } diff --git a/internal/command/testdata/test/override_data_complex_nested_set_attribute_object/main.tf b/internal/command/testdata/test/override_data_complex_nested_set_attribute_object/main.tf new file mode 100644 index 000000000000..5174b78b954f --- /dev/null +++ b/internal/command/testdata/test/override_data_complex_nested_set_attribute_object/main.tf @@ -0,0 +1,15 @@ +terraform { + required_providers { + test = { + source = "hashicorp/test" + } + } +} + +data "test_complex_data_source" "datasource" { + id = "resource" +} + +output "nested_set_value" { + value = data.test_complex_data_source.datasource.nested_set_value +} diff --git a/internal/command/testdata/test/override_data_complex_nested_set_attribute_object/main.tftest.hcl b/internal/command/testdata/test/override_data_complex_nested_set_attribute_object/main.tftest.hcl new file mode 100644 index 000000000000..f18d29348530 --- /dev/null +++ b/internal/command/testdata/test/override_data_complex_nested_set_attribute_object/main.tftest.hcl @@ -0,0 +1,19 @@ +provider "test" {} + +override_data { + target = data.test_complex_data_source.datasource + values = { + nested_set_value = { + name = "shared" + } + } +} + +run "test_override_data_complex_nested_set_attribute_object" { + command = plan + + assert { + condition = length(data.test_complex_data_source.datasource.nested_set_value) == 0 + error_message = "Expected nested_set_value to be empty when overridden with an object" + } +} diff --git a/internal/command/testdata/test/override_data_complex_set_attribute_partial_elements/main.tf b/internal/command/testdata/test/override_data_complex_set_attribute_partial_elements/main.tf new file mode 100644 index 000000000000..bbea87bd356d --- /dev/null +++ b/internal/command/testdata/test/override_data_complex_set_attribute_partial_elements/main.tf @@ -0,0 +1,15 @@ +terraform { + required_providers { + test = { + source = "hashicorp/test" + } + } +} + +data "test_complex_data_source" "datasource" { + id = "resource" +} + +output "set_value" { + value = data.test_complex_data_source.datasource.set_value +} diff --git a/internal/command/testdata/test/override_data_complex_set_attribute_partial_elements/main.tftest.hcl b/internal/command/testdata/test/override_data_complex_set_attribute_partial_elements/main.tftest.hcl new file mode 100644 index 000000000000..14f89f2de39c --- /dev/null +++ b/internal/command/testdata/test/override_data_complex_set_attribute_partial_elements/main.tftest.hcl @@ -0,0 +1,40 @@ +provider "test" {} + +override_data { + target = data.test_complex_data_source.datasource + values = { + set_value = [ + { + name = "first" + }, + { + value = "two" + }, + ] + } +} + +run "test_override_data_complex_set_attribute_partial_elements" { + command = plan + + assert { + condition = length(data.test_complex_data_source.datasource.set_value) == 2 + error_message = "Expected set_value to have 2 elements, got ${length(data.test_complex_data_source.datasource.set_value)}" + } + + assert { + condition = length([ + for item in data.test_complex_data_source.datasource.set_value : item + if item.name == "first" && item.value != null + ]) == 1 + error_message = "Expected one set_value element with name 'first' and a filled-in value" + } + + assert { + condition = length([ + for item in data.test_complex_data_source.datasource.set_value : item + if item.value == "two" && item.name != null + ]) == 1 + error_message = "Expected one set_value element with value 'two' and a filled-in name" + } +} diff --git a/internal/command/testdata/test/override_data_list_attribute_partial_elements/main.tf b/internal/command/testdata/test/override_data_list_attribute_partial_elements/main.tf new file mode 100644 index 000000000000..f9d19bfd7435 --- /dev/null +++ b/internal/command/testdata/test/override_data_list_attribute_partial_elements/main.tf @@ -0,0 +1,15 @@ +terraform { + required_providers { + test = { + source = "hashicorp/test" + } + } +} + +data "test_data_source" "datasource" { + id = "resource" +} + +output "list_value" { + value = data.test_data_source.datasource.list_value +} diff --git a/internal/command/testdata/test/override_data_list_attribute_partial_elements/main.tftest.hcl b/internal/command/testdata/test/override_data_list_attribute_partial_elements/main.tftest.hcl new file mode 100644 index 000000000000..66bc4929342a --- /dev/null +++ b/internal/command/testdata/test/override_data_list_attribute_partial_elements/main.tftest.hcl @@ -0,0 +1,44 @@ +provider "test" {} + +override_data { + target = data.test_data_source.datasource + values = { + list_value = [ + { + name = "first" + }, + { + value = "two" + }, + ] + } +} + +run "test_override_data_list_attribute_partial_elements" { + command = plan + + assert { + condition = length(data.test_data_source.datasource.list_value) == 2 + error_message = "Expected list_value to have 2 elements, got ${length(data.test_data_source.datasource.list_value)}" + } + + assert { + condition = data.test_data_source.datasource.list_value[0].name == "first" + error_message = "Expected first element name to be 'first'" + } + + assert { + condition = data.test_data_source.datasource.list_value[0].value != null + error_message = "Expected first element value to be filled in" + } + + assert { + condition = data.test_data_source.datasource.list_value[1].value == "two" + error_message = "Expected second element value to be 'two'" + } + + assert { + condition = data.test_data_source.datasource.list_value[1].name != null + error_message = "Expected second element name to be filled in" + } +} diff --git a/internal/command/testdata/test/override_data_nested_list_attribute_invalid_type/main.tf b/internal/command/testdata/test/override_data_nested_list_attribute_invalid_type/main.tf new file mode 100644 index 000000000000..52cfeb5fa44a --- /dev/null +++ b/internal/command/testdata/test/override_data_nested_list_attribute_invalid_type/main.tf @@ -0,0 +1,15 @@ +terraform { + required_providers { + test = { + source = "hashicorp/test" + } + } +} + +data "test_data_source" "datasource" { + id = "resource" +} + +output "nested_list_value" { + value = data.test_data_source.datasource.nested_list_value +} diff --git a/internal/command/testdata/test/override_data_nested_list_attribute_invalid_type/main.tftest.hcl b/internal/command/testdata/test/override_data_nested_list_attribute_invalid_type/main.tftest.hcl new file mode 100644 index 000000000000..cb3ffaee8884 --- /dev/null +++ b/internal/command/testdata/test/override_data_nested_list_attribute_invalid_type/main.tftest.hcl @@ -0,0 +1,12 @@ +provider "test" {} + +override_data { + target = data.test_data_source.datasource + values = { + nested_list_value = "wrong type" + } +} + +run "test_override_data_nested_list_attribute_invalid_type" { + command = plan +} diff --git a/internal/command/testdata/test/override_data_nested_list_attribute_object/main.tf b/internal/command/testdata/test/override_data_nested_list_attribute_object/main.tf new file mode 100644 index 000000000000..52cfeb5fa44a --- /dev/null +++ b/internal/command/testdata/test/override_data_nested_list_attribute_object/main.tf @@ -0,0 +1,15 @@ +terraform { + required_providers { + test = { + source = "hashicorp/test" + } + } +} + +data "test_data_source" "datasource" { + id = "resource" +} + +output "nested_list_value" { + value = data.test_data_source.datasource.nested_list_value +} diff --git a/internal/command/testdata/test/override_data_nested_list_attribute_object/main.tftest.hcl b/internal/command/testdata/test/override_data_nested_list_attribute_object/main.tftest.hcl new file mode 100644 index 000000000000..125cbc3b94c0 --- /dev/null +++ b/internal/command/testdata/test/override_data_nested_list_attribute_object/main.tftest.hcl @@ -0,0 +1,19 @@ +provider "test" {} + +override_data { + target = data.test_data_source.datasource + values = { + nested_list_value = { + name = "shared" + } + } +} + +run "test_override_data_nested_list_attribute_object" { + command = plan + + assert { + condition = length(data.test_data_source.datasource.nested_list_value) == 0 + error_message = "Expected nested_list_value to be empty when overridden with an object" + } +} diff --git a/internal/command/testdata/test/override_data_nested_list_attribute_partial_elements/main.tf b/internal/command/testdata/test/override_data_nested_list_attribute_partial_elements/main.tf new file mode 100644 index 000000000000..52cfeb5fa44a --- /dev/null +++ b/internal/command/testdata/test/override_data_nested_list_attribute_partial_elements/main.tf @@ -0,0 +1,15 @@ +terraform { + required_providers { + test = { + source = "hashicorp/test" + } + } +} + +data "test_data_source" "datasource" { + id = "resource" +} + +output "nested_list_value" { + value = data.test_data_source.datasource.nested_list_value +} diff --git a/internal/command/testdata/test/override_data_nested_list_attribute_partial_elements/main.tftest.hcl b/internal/command/testdata/test/override_data_nested_list_attribute_partial_elements/main.tftest.hcl new file mode 100644 index 000000000000..ad5c19f05232 --- /dev/null +++ b/internal/command/testdata/test/override_data_nested_list_attribute_partial_elements/main.tftest.hcl @@ -0,0 +1,44 @@ +provider "test" {} + +override_data { + target = data.test_data_source.datasource + values = { + nested_list_value = [ + { + name = "first" + }, + { + value = "two" + }, + ] + } +} + +run "test_override_data_nested_list_attribute_partial_elements" { + command = plan + + assert { + condition = length(data.test_data_source.datasource.nested_list_value) == 2 + error_message = "Expected nested_list_value to have 2 elements, got ${length(data.test_data_source.datasource.nested_list_value)}" + } + + assert { + condition = data.test_data_source.datasource.nested_list_value[0].name == "first" + error_message = "Expected first element name to be 'first'" + } + + assert { + condition = data.test_data_source.datasource.nested_list_value[0].value != null + error_message = "Expected first element value to be filled in" + } + + assert { + condition = data.test_data_source.datasource.nested_list_value[1].value == "two" + error_message = "Expected second element value to be 'two'" + } + + assert { + condition = data.test_data_source.datasource.nested_list_value[1].name != null + error_message = "Expected second element name to be filled in" + } +} diff --git a/internal/moduletest/mocking/fill.go b/internal/moduletest/mocking/fill.go index 6eccf33e214d..8d10be61e5c0 100644 --- a/internal/moduletest/mocking/fill.go +++ b/internal/moduletest/mocking/fill.go @@ -17,35 +17,50 @@ import ( // attributes and/or performing conversions to make the input value correct. // // It is similar to FillType, except it accepts attributes instead of types. -func FillAttribute(in cty.Value, attribute *configschema.Attribute) (cty.Value, error) { - return fillAttribute(in, attribute, cty.Path{}) +func FillAttribute(providedMock cty.Value, attribute *configschema.Attribute) (cty.Value, error) { + return fillAttribute(providedMock, attribute, cty.Path{}) } -func fillAttribute(in cty.Value, attribute *configschema.Attribute, path cty.Path) (cty.Value, error) { +func fillAttribute(providedMock cty.Value, attribute *configschema.Attribute, path cty.Path) (cty.Value, error) { ty := attribute.Type if attribute.NestedType != nil { + // For nested types, the providedMock value is interpreted in two ways: + // - If it's an object, it's treated as a single instance of the nested type, + // and because we can't know how many instances are needed, we return an empty collection. + // - If it's a collection, it's treated as the whole nested type collection, + // and then we update each element of the collection with generated values where possible. + // Note: The collection type must match the attribute's nested type. switch attribute.NestedType.Nesting { case configschema.NestingSingle, configschema.NestingGroup: - return fillObject(in, attribute, path) + return fillObject(providedMock, attribute, path) case configschema.NestingSet: - return cty.SetValEmpty(attribute.ImpliedType().ElementType()), nil + if providedMock.Type().IsObjectType() { + return cty.SetValEmpty(attribute.ImpliedType().ElementType()), nil + } + return fillIterable(providedMock, attribute, path) case configschema.NestingList: - return fillIterable(in, attribute, path) + if providedMock.Type().IsObjectType() { + return cty.ListValEmpty(attribute.ImpliedType().ElementType()), nil + } + return fillIterable(providedMock, attribute, path) case configschema.NestingMap: - return cty.MapValEmpty(attribute.ImpliedType().ElementType()), nil + if providedMock.Type().IsObjectType() { + return cty.MapValEmpty(attribute.ImpliedType().ElementType()), nil + } + return fillIterable(providedMock, attribute, path) default: panic(fmt.Errorf("unknown nesting mode: %d", attribute.NestedType.Nesting)) } } - return fillType(in, ty, path) + return fillType(providedMock, ty, path) } -func fillObject(in cty.Value, attribute *configschema.Attribute, path cty.Path) (cty.Value, error) { - // Then the in value must be an object. - if !in.Type().IsObjectType() { - return cty.NilVal, path.NewErrorf("incompatible types; expected object type, found %s", in.Type().FriendlyName()) +func fillObject(providedMock cty.Value, attribute *configschema.Attribute, path cty.Path) (cty.Value, error) { + // Then the providedMock value must be an object. + if !providedMock.Type().IsObjectType() { + return cty.NilVal, path.NewErrorf("incompatible types; expected object type, found %s", providedMock.Type().FriendlyName()) } var names []string @@ -63,8 +78,8 @@ func fillObject(in cty.Value, attribute *configschema.Attribute, path cty.Path) children := make(map[string]cty.Value) for _, name := range names { - if in.Type().HasAttribute(name) { - child, err := fillAttribute(in.GetAttr(name), attribute.NestedType.Attributes[name], path.GetAttr(name)) + if providedMock.Type().HasAttribute(name) { + child, err := fillAttribute(providedMock.GetAttr(name), attribute.NestedType.Attributes[name], path.GetAttr(name)) if err != nil { return cty.NilVal, err } @@ -76,9 +91,9 @@ func fillObject(in cty.Value, attribute *configschema.Attribute, path cty.Path) return cty.ObjectVal(children), nil } -func fillIterable(in cty.Value, attribute *configschema.Attribute, path cty.Path) (cty.Value, error) { +func fillIterable(providedMock cty.Value, attribute *configschema.Attribute, path cty.Path) (cty.Value, error) { ty := attribute.NestedType.ConfigType() - out, err := fillType(in, ty, path) + out, err := fillType(providedMock, ty, path) if err != nil { return cty.NilVal, err } From da295013b552b80778daec9d9948f0a189d7091e Mon Sep 17 00:00:00 2001 From: Samsondeen Dare Date: Mon, 13 Apr 2026 16:28:43 +0200 Subject: [PATCH 10/11] more tests --- internal/command/test_test.go | 10 +- .../mocking/tests/nested_mocked.tftest.hcl | 8 +- internal/command/testing/test_provider.go | 100 +++++++++--------- internal/moduletest/mocking/fill.go | 5 +- internal/moduletest/mocking/values.go | 15 +++ 5 files changed, 78 insertions(+), 60 deletions(-) diff --git a/internal/command/test_test.go b/internal/command/test_test.go index d96840e39aa8..9dabeb4381d5 100644 --- a/internal/command/test_test.go +++ b/internal/command/test_test.go @@ -377,12 +377,14 @@ func TestTest_Runs(t *testing.T) { code: 0, }, "write-only-attributes-mocked": { - expectedOut: []string{"1 passed, 0 failed."}, - code: 0, + expectedOut: []string{"0 passed, 1 failed."}, + expectedErr: []string{"Test assertion failed", "wrong value"}, + code: 1, }, "write-only-attributes-overridden": { - expectedOut: []string{"1 passed, 0 failed."}, - code: 0, + expectedOut: []string{"0 passed, 1 failed."}, + expectedErr: []string{"Test assertion failed", "wrong value"}, + code: 1, }, "with-default-variables": { args: []string{"-var=input_two=universe"}, diff --git a/internal/command/testdata/test/mocking/tests/nested_mocked.tftest.hcl b/internal/command/testdata/test/mocking/tests/nested_mocked.tftest.hcl index 28cb1d9be769..a59ba8e3c350 100644 --- a/internal/command/testdata/test/mocking/tests/nested_mocked.tftest.hcl +++ b/internal/command/testdata/test/mocking/tests/nested_mocked.tftest.hcl @@ -8,22 +8,22 @@ mock_provider "test" { nested_list_value = [{name = "first"}, {name = "second"}] set_value = [{name = "first"}, {name = "second"}] nested_set_value = [{name = "first"}, {name = "second"}] - map_value = { + map_value = tomap({ "key1": { name = "first" }, "key2": { name = "third" } - } - nested_map_value = { + }) + nested_map_value = tomap({ "key1": { name = "first" }, "key2": { name = "third" } - } + }) } } } diff --git a/internal/command/testing/test_provider.go b/internal/command/testing/test_provider.go index a2c3390fb2f9..d60ceab07ce2 100644 --- a/internal/command/testing/test_provider.go +++ b/internal/command/testing/test_provider.go @@ -133,69 +133,42 @@ var ( DataSources: map[string]providers.Schema{ "test_data_source": { Body: &configschema.Block{ - Attributes: map[string]*configschema.Attribute{ - "id": {Type: cty.String, Required: true}, - "value": {Type: cty.String, Computed: true}, - "write_only": {Type: cty.String, Optional: true, WriteOnly: true}, - - // list_value is a computed attribute of list-of-objects - // type, used to test that override_data can correctly - // override list-type attributes (see GitHub issue #37939). - "list_value": { - Type: cty.List(cty.Object(map[string]cty.Type{ - "name": cty.String, - "value": cty.String, - })), - Computed: true, - }, - - // nested_list_value is a computed attribute using NestedType - // with NestingList, used to test that override_data can correctly - // override nested list-type attributes (see GitHub issue #37939). - // Many real-world providers (e.g. Databricks) define list-of-objects - // attributes this way rather than using a plain cty.List type. - "nested_list_value": { - Computed: true, - NestedType: &configschema.Object{ - Nesting: configschema.NestingList, - Attributes: map[string]*configschema.Attribute{ - "name": {Type: cty.String, Optional: true}, - "value": {Type: cty.String, Optional: true}, - }, - }, - }, + Attributes: withBlockTypeAttributes(map[string]*configschema.Attribute{ + "id": {Type: cty.String, Optional: true, Computed: true}, + "value": {Type: cty.String, Optional: true}, // We never actually reference these values from a data // source, but we have tests that use the same cty.Value // to represent a test_resource and a test_data_source // so the schemas have to match. - - "interrupt_count": {Type: cty.Number, Computed: true}, - "destroy_fail": {Type: cty.Bool, Computed: true}, - "create_wait_seconds": {Type: cty.Number, Computed: true}, - "destroy_wait_seconds": {Type: cty.Number, Computed: true}, - "defer": {Type: cty.Bool, Computed: true}, - }, + // + "interrupt_count": {Type: cty.Number, Optional: true}, + "destroy_fail": {Type: cty.Bool, Optional: true, Computed: true}, + "create_wait_seconds": {Type: cty.Number, Optional: true}, + "destroy_wait_seconds": {Type: cty.Number, Optional: true}, + "write_only": {Type: cty.String, Optional: true, WriteOnly: true}, + "defer": {Type: cty.Bool, Optional: true}, + }), }, }, "test_complex_data_source": { Body: &configschema.Block{ Attributes: withBlockTypeAttributes(map[string]*configschema.Attribute{ - "id": {Type: cty.String, Required: true}, - "value": {Type: cty.String, Computed: true}, - "write_only": {Type: cty.String, Optional: true, WriteOnly: true}, + "id": {Type: cty.String, Optional: true, Computed: true}, + "value": {Type: cty.String, Optional: true}, // We never actually reference these values from a data // source, but we have tests that use the same cty.Value // to represent a test_resource and a test_data_source // so the schemas have to match. - - "interrupt_count": {Type: cty.Number, Computed: true}, - "destroy_fail": {Type: cty.Bool, Computed: true}, - "create_wait_seconds": {Type: cty.Number, Computed: true}, - "destroy_wait_seconds": {Type: cty.Number, Computed: true}, - "defer": {Type: cty.Bool, Computed: true}, + // + "interrupt_count": {Type: cty.Number, Optional: true}, + "destroy_fail": {Type: cty.Bool, Optional: true, Computed: true}, + "create_wait_seconds": {Type: cty.Number, Optional: true}, + "destroy_wait_seconds": {Type: cty.Number, Optional: true}, + "write_only": {Type: cty.String, Optional: true, WriteOnly: true}, + "defer": {Type: cty.Bool, Optional: true}, }), }, }, @@ -498,14 +471,41 @@ func (provider *TestProvider) ReadDataSource(request providers.ReadDataSourceReq resource := provider.Store.Get(provider.GetDataKey(id)) if resource == cty.NilVal { diags = diags.Append(tfdiags.Sourceless(tfdiags.Error, "not found", fmt.Sprintf("%s does not exist", id))) + resource = cty.EmptyObjectVal } - if writeOnly := resource.GetAttr("write_only"); !writeOnly.IsNull() { - vals := resource.AsValueMap() + vals := resource.AsValueMap() + + if writeOnly := request.Config.GetAttr("write_only"); !writeOnly.IsNull() { vals["write_only"] = cty.NullVal(cty.String) - resource = cty.ObjectVal(vals) } + sharedObjectType := cty.Object(map[string]cty.Type{ + "name": cty.String, + "value": cty.String, + }) + + if _, exists := vals["list_value"]; !exists { + vals["list_value"] = cty.ListValEmpty(sharedObjectType) + } + if _, exists := vals["set_value"]; !exists { + vals["set_value"] = cty.SetValEmpty(sharedObjectType) + } + if _, exists := vals["map_value"]; !exists { + vals["map_value"] = cty.MapValEmpty(sharedObjectType) + } + if _, exists := vals["nested_list_value"]; !exists { + vals["nested_list_value"] = cty.ListValEmpty(sharedObjectType) + } + if _, exists := vals["nested_set_value"]; !exists { + vals["nested_set_value"] = cty.SetValEmpty(sharedObjectType) + } + if _, exists := vals["nested_map_value"]; !exists { + vals["nested_map_value"] = cty.MapValEmpty(sharedObjectType) + } + + resource = cty.ObjectVal(vals) + return providers.ReadDataSourceResponse{ State: resource, Diagnostics: diags, diff --git a/internal/moduletest/mocking/fill.go b/internal/moduletest/mocking/fill.go index 8d10be61e5c0..1c468fe2680c 100644 --- a/internal/moduletest/mocking/fill.go +++ b/internal/moduletest/mocking/fill.go @@ -27,8 +27,9 @@ func fillAttribute(providedMock cty.Value, attribute *configschema.Attribute, pa // For nested types, the providedMock value is interpreted in two ways: // - If it's an object, it's treated as a single instance of the nested type, // and because we can't know how many instances are needed, we return an empty collection. - // - If it's a collection, it's treated as the whole nested type collection, - // and then we update each element of the collection with generated values where possible. + // - If it's already a collection, it's treated as the whole nested type + // collection, and then we update each element of the collection with + // generated values where possible. // Note: The collection type must match the attribute's nested type. switch attribute.NestedType.Nesting { diff --git a/internal/moduletest/mocking/values.go b/internal/moduletest/mocking/values.go index 6048aee35888..2b8b77618667 100644 --- a/internal/moduletest/mocking/values.go +++ b/internal/moduletest/mocking/values.go @@ -165,6 +165,21 @@ func populateComputedValues(target cty.Value, with MockedData, schema *configsch }) } + // If there are no errors, we coerce the value to ensure it matches the schema. + // Any errors here would be because we generated an invalid value. + if !diags.HasErrors() { + var err error + value, err = schema.CoerceValue(value) + if err != nil { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Detail: "Failed to coerce value", + Summary: fmt.Sprintf("Terraform failed to coerce a value for a mocked object: %s. This is a bug in Terraform - please report it.", err), + Subject: with.Range.Ptr(), + }) + } + } + return value, diags } From fc4e2d595f41c6809f36e892d3b1be3f7737063f Mon Sep 17 00:00:00 2001 From: Samsondeen Dare Date: Mon, 13 Apr 2026 16:47:45 +0200 Subject: [PATCH 11/11] update tests --- internal/moduletest/mocking/values_test.go | 53 +++++++++++++++++----- 1 file changed, 41 insertions(+), 12 deletions(-) diff --git a/internal/moduletest/mocking/values_test.go b/internal/moduletest/mocking/values_test.go index 1757be11cc51..a75ff15302a1 100644 --- a/internal/moduletest/mocking/values_test.go +++ b/internal/moduletest/mocking/values_test.go @@ -570,7 +570,7 @@ func TestComputedValuesForDataSource(t *testing.T) { }), }), }, - "nested_list_attribute_computed_err": { + "nested_list_attribute_computed_empty": { target: cty.ObjectVal(map[string]cty.Value{ "nested": cty.NullVal(cty.List(cty.Object(map[string]cty.Type{ "id": cty.String, @@ -599,9 +599,7 @@ func TestComputedValuesForDataSource(t *testing.T) { "value": cty.String, })), }), - expectedFailures: []string{ - `Terraform could not compute a value for the target type list of object with the mocked data defined at :0,0-0 with the attribute ".nested": incompatible types; expected list of object, found object.`, - }, + expectedFailures: nil, }, "nested_set_attribute": { target: cty.ObjectVal(map[string]cty.Value{ @@ -704,7 +702,7 @@ func TestComputedValuesForDataSource(t *testing.T) { }), }), }, - "nested_set_attribute_computed_err": { + "nested_set_attribute_computed_empty": { target: cty.ObjectVal(map[string]cty.Value{ "nested": cty.NullVal(cty.Set(cty.Object(map[string]cty.Type{ "id": cty.String, @@ -733,9 +731,7 @@ func TestComputedValuesForDataSource(t *testing.T) { "value": cty.String, })), }), - expectedFailures: []string{ - `Terraform could not compute a value for the target type set of object with the mocked data defined at :0,0-0 with the attribute ".nested": incompatible types; expected set of object, found object.`, - }, + expectedFailures: nil, }, "nested_map_attribute": { target: cty.ObjectVal(map[string]cty.Value{ @@ -838,7 +834,7 @@ func TestComputedValuesForDataSource(t *testing.T) { }), }), }, - "nested_map_attribute_computed_err": { + "nested_map_attribute_computed_empty": { target: cty.ObjectVal(map[string]cty.Value{ "nested": cty.NullVal(cty.Map(cty.Object(map[string]cty.Type{ "id": cty.String, @@ -867,9 +863,7 @@ func TestComputedValuesForDataSource(t *testing.T) { "value": cty.String, })), }), - expectedFailures: []string{ - `Terraform could not compute a value for the target type map of object with the mocked data defined at :0,0-0 with the attribute ".nested[\"id\"]": incompatible types; expected object, found string.`, - }, + expectedFailures: nil, }, "invalid_replacement_path": { target: cty.ObjectVal(map[string]cty.Value{ @@ -1047,6 +1041,41 @@ func TestComputedValuesForDataSource(t *testing.T) { "Terraform could not compute a value for the target type string with the mocked data defined at :0,0-0 with the attribute \".block[0].id\": string required, but have object.", }, }, + + "invalid_replacement_path_nested_list_block": { + target: cty.ObjectVal(map[string]cty.Value{ + "block": cty.ListVal([]cty.Value{ + cty.ObjectVal(map[string]cty.Value{ + "id": cty.NullVal(cty.String), + "value": cty.StringVal("one"), + }), + }), + }), + with: cty.ObjectVal(map[string]cty.Value{ + "block": cty.ListVal([]cty.Value{ + cty.StringVal("Hello, world!"), + }), + }), + schema: &configschema.Block{ + BlockTypes: map[string]*configschema.NestedBlock{ + "block": { + Block: computedBlock, + Nesting: configschema.NestingList, + }, + }, + }, + expected: cty.ObjectVal(map[string]cty.Value{ + "block": cty.ListVal([]cty.Value{ + cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("ssnk9qhr"), + "value": cty.StringVal("one"), + }), + }), + }), + expectedFailures: []string{ + "Terraform expected an object type for attribute \".block[0]\" defined within the mocked data at :0,0-0, but found list of string.", + }, + }, "dynamic_attribute_unset": { target: cty.ObjectVal(map[string]cty.Value{ "dynamic_attribute": cty.NullVal(cty.DynamicPseudoType),