diff --git a/internal/command/test_test.go b/internal/command/test_test.go index ee7292b5c2..d96840e39a 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 0000000000..5174b78b95 --- /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 0000000000..f18d293485 --- /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 0000000000..bbea87bd35 --- /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 0000000000..14f89f2de3 --- /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 0000000000..f9d19bfd74 --- /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 0000000000..66bc492934 --- /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 0000000000..52cfeb5fa4 --- /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 0000000000..cb3ffaee88 --- /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 0000000000..52cfeb5fa4 --- /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 0000000000..125cbc3b94 --- /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 0000000000..52cfeb5fa4 --- /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 0000000000..ad5c19f052 --- /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 6eccf33e21..8d10be61e5 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 }