Skip to content

chore: Uses NewUnknownReplacements meant to replace schemafunc.CopyUnknowns logic and schemafunc.NewAttributeChanges #3192

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 43 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 31 commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
b04314c
test: Support testing PlanModifier
EspenAlbert Mar 21, 2025
ed85e27
fix broken unit test
EspenAlbert Mar 21, 2025
9ffd580
chore: Adds NewUnknownReplacements meant to replace `schemafunc.CopyU…
EspenAlbert Mar 21, 2025
10d8a82
refactor: Handle unknownReplacement logic for `mongo_db_version` usin…
EspenAlbert Mar 21, 2025
19a9f90
test: Support testing root level plan modifier changes
EspenAlbert Mar 21, 2025
a2bac7b
PR suggestions
EspenAlbert Mar 21, 2025
cbd65fb
Merge branch 'master' into CLOUDP-307851_common_plan_modifier_logic
EspenAlbert Mar 24, 2025
7e4004b
refactor: Simplify plan check tests by consolidating test case execution
EspenAlbert Mar 24, 2025
3e8b7d0
refactor test checks to map
EspenAlbert Mar 24, 2025
3cc4e05
Merge branch 'master' into CLOUDP-307851_common_plan_modifier_logic
EspenAlbert Mar 24, 2025
77eb77b
refactor: Address PR comments and remove unused code
EspenAlbert Mar 25, 2025
dda909b
refactor: expose ResourceSchema to allow usage in tests
EspenAlbert Mar 25, 2025
b728367
test: Initial support for wrapping a resource to test the plan modifier
EspenAlbert Mar 25, 2025
d9c59d4
test: Add unit tests to the unknown replacement logic
EspenAlbert Mar 25, 2025
ce1e5c5
chore: rename test
EspenAlbert Mar 25, 2025
9d3f769
test: Add panic test for duplicate replacement names in UnknownReplac…
EspenAlbert Mar 25, 2025
cbf31d9
Merge branch 'master' into CLOUDP-307851_common_plan_modifier_logic
EspenAlbert Mar 26, 2025
4d70f84
chore: Add function to check if diagnostics are non-empty
EspenAlbert Mar 26, 2025
2233dc5
chore: Add diagnostics field to UnknownReplacementRequest struct
EspenAlbert Mar 26, 2025
ec64b97
feat: Support reading list values from plan
EspenAlbert Mar 26, 2025
9684396
chore: Add functions for parent path retrieval
EspenAlbert Mar 26, 2025
938c6b4
style: fmt
EspenAlbert Mar 26, 2025
7e22345
feat: PR comments and enhancements
EspenAlbert Mar 27, 2025
8d9093b
chore: revert changes to tpf package
EspenAlbert Mar 27, 2025
d710412
chore: Add test files and refactor
EspenAlbert Mar 27, 2025
3bbec61
review comments
EspenAlbert Mar 27, 2025
9e37725
refactor: Replace ParsedAttrValue with attr.Value in custom plan modi…
EspenAlbert Mar 27, 2025
c8ce3cd
refactor: Simplify trimLastIndexPath function and introduce AddKeepUn…
EspenAlbert Mar 27, 2025
c1ac25b
refactor: Replace attributeNameEquals function with direct comparison…
EspenAlbert Mar 27, 2025
19d305b
refactor: Remove unused AsUnknownValue function from conversion package
EspenAlbert Mar 27, 2025
7229da8
pr comments
EspenAlbert Mar 28, 2025
0fe2780
doc: Specify ResourceInfo usage
EspenAlbert Mar 28, 2025
8d87c23
pr suggestions and clarifications
EspenAlbert Mar 28, 2025
a714bdb
Merge branch 'master' into CLOUDP-307851_common_plan_modifier_logic
EspenAlbert Mar 28, 2025
47aa17f
chore: lint fixes and formatting
EspenAlbert Mar 28, 2025
df15b83
cherry-pick changes: efed2ea2e19abe31e01229fc1d418afd3149a078
EspenAlbert Apr 1, 2025
13825e5
refactor: Update AncestorPath functions to return diagnostics alongsi…
EspenAlbert Mar 31, 2025
bc6aeb1
refactor: Modify AncestorPath functions to return diagnostics alongsi…
EspenAlbert Mar 31, 2025
02cae8f
refactor: Enhance error logging in plan modification functions instea…
EspenAlbert Mar 31, 2025
e958361
refactor: Clarify comments in ApplyReplacements function regarding an…
EspenAlbert Mar 31, 2025
10ebaf4
doc: Add docstring to more public functions
EspenAlbert Apr 1, 2025
d7deeb3
test: fix broken unit test
EspenAlbert Apr 1, 2025
40a66a9
refactor: sync changes from stack top
EspenAlbert Apr 1, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
152 changes: 152 additions & 0 deletions internal/common/conversion/path_converter.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
package conversion

import (
"context"
"fmt"

"github.com/hashicorp/terraform-plugin-framework/attr"
"github.com/hashicorp/terraform-plugin-framework/diag"
"github.com/hashicorp/terraform-plugin-framework/path"
"github.com/hashicorp/terraform-plugin-go/tftypes"
)

type TPFSchema interface {
TypeAtTerraformPath(context.Context, *tftypes.AttributePath) (attr.Type, error)
}

type TPFSrc interface {
GetAttribute(context.Context, path.Path, any) diag.Diagnostics
}

// AttributePathValue retrieves the value for src (state/plan/config) @ attributePath with converted path.Path, schema is needed to get the correct types.XXX (String/Object/etc.)
func AttributePathValue(ctx context.Context, diags *diag.Diagnostics, attributePath *tftypes.AttributePath, src TPFSrc, schema TPFSchema) (attr.Value, path.Path) {
convertedPath, localDiags := AttributePath(ctx, attributePath, schema)
diags.Append(localDiags...)
if diags.HasError() {
return nil, convertedPath
}
attrType, err := schema.TypeAtTerraformPath(ctx, attributePath)
if err != nil {
diags.AddError("Unable to get type for attribute path", fmt.Sprintf("%s: %s", attributePath.String(), err))
return nil, convertedPath
}
attrValue := attrType.ValueType(ctx)
if localDiags := src.GetAttribute(ctx, convertedPath, &attrValue); localDiags.HasError() {
diags.Append(localDiags...)
return nil, convertedPath
}
return attrValue, convertedPath
}

// AttributePath similar to the internal function in TPF, but simpler interface as argument and less logging.
// AttributePath in TPF repo is internal and cannot be used: https://github.com/hashicorp/terraform-plugin-framework/blob/e09ec9d169c581d2606372ecdfb0113be7e3b34f/internal/fromtftypes/attribute_path.go#L17
func AttributePath(ctx context.Context, tfType *tftypes.AttributePath, schema TPFSchema) (path.Path, diag.Diagnostics) {
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I haven't gone deep, but it smells a lot we're copying internal methods. Also, what is the impact on the policy?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did we get any additional information here?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point. Waiting on reply.

fwPath := path.Empty()
for tfTypeStepIndex, tfTypeStep := range tfType.Steps() {
currentTfTypeSteps := tfType.Steps()[:tfTypeStepIndex+1]
currentTfTypePath := tftypes.NewAttributePathWithSteps(currentTfTypeSteps)
attrType, err := schema.TypeAtTerraformPath(ctx, currentTfTypePath)

if err != nil {
return path.Empty(), diag.Diagnostics{
diag.NewErrorDiagnostic(
"Unable to Convert Attribute Path",
"An unexpected error occurred while trying to convert an attribute path. "+
"This is an error in terraform-plugin-framework used by the provider. "+
"Please report the following to the provider developers.\n\n"+
// Since this is an error with the attribute path
// conversion, we cannot return a protocol path-based
// diagnostic. Returning a framework human-readable
// representation seems like the next best thing to do.
fmt.Sprintf("Attribute Path: %s\n", currentTfTypePath.String())+
fmt.Sprintf("Original Error: %s", err),
),
}
}

fwStep, err := AttributePathStep(ctx, tfTypeStep, attrType)

if err != nil {
return path.Empty(), diag.Diagnostics{
diag.NewErrorDiagnostic(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

consider having a helper func for the 3 errors in the func

"Unable to Convert Attribute Path",
"An unexpected error occurred while trying to convert an attribute path. "+
"This is either an error in terraform-plugin-framework or a custom attribute type used by the provider. "+
"Please report the following to the provider developers.\n\n"+
// Since this is an error with the attribute path
// conversion, we cannot return a protocol path-based
// diagnostic. Returning a framework human-readable
// representation seems like the next best thing to do.
fmt.Sprintf("Attribute Path: %s\n", currentTfTypePath.String())+
fmt.Sprintf("Original Error: %s", err),
),
}
}

// In lieu of creating a path.NewPathFromSteps function, this path
// building logic is inlined to not expand the path package API.
switch fwStep := fwStep.(type) {
case path.PathStepAttributeName:
fwPath = fwPath.AtName(string(fwStep))
case path.PathStepElementKeyInt:
fwPath = fwPath.AtListIndex(int(fwStep))
case path.PathStepElementKeyString:
fwPath = fwPath.AtMapKey(string(fwStep))
case path.PathStepElementKeyValue:
fwPath = fwPath.AtSetValue(fwStep.Value)
default:
return fwPath, diag.Diagnostics{
diag.NewErrorDiagnostic(
"Unable to Convert Attribute Path",
"An unexpected error occurred while trying to convert an attribute path. "+
"This is an error in terraform-plugin-framework used by the provider. "+
"Please report the following to the provider developers.\n\n"+
// Since this is an error with the attribute path
// conversion, we cannot return a protocol path-based
// diagnostic. Returning a framework human-readable
// representation seems like the next best thing to do.
fmt.Sprintf("Attribute Path: %s\n", currentTfTypePath.String())+
fmt.Sprintf("Original Error: unknown path.PathStep type: %#v", fwStep),
),
}
}
}

return fwPath, nil
}

// AttributePathStep in TPF repo is internal and cannot be used: https://github.com/hashicorp/terraform-plugin-framework/blob/e09ec9d169c581d2606372ecdfb0113be7e3b34f/internal/fromtftypes/attribute_path_step.go#L19
func AttributePathStep(ctx context.Context, tfType tftypes.AttributePathStep, attrType attr.Type) (path.PathStep, error) {
switch tfType := tfType.(type) {
case tftypes.AttributeName:
return path.PathStepAttributeName(string(tfType)), nil
case tftypes.ElementKeyInt:
return path.PathStepElementKeyInt(int64(tfType)), nil
case tftypes.ElementKeyString:
return path.PathStepElementKeyString(string(tfType)), nil
case tftypes.ElementKeyValue:
attrValue, err := Value(ctx, tftypes.Value(tfType), attrType)

if err != nil {
return nil, fmt.Errorf("unable to create PathStepElementKeyValue from tftypes.Value: %w", err)
}

return path.PathStepElementKeyValue{Value: attrValue}, nil
default:
return nil, fmt.Errorf("unknown tftypes.AttributePathStep: %#v", tfType)
}
}

func Value(ctx context.Context, tfType tftypes.Value, attrType attr.Type) (attr.Value, error) {
if attrType == nil {
return nil, fmt.Errorf("unable to convert tftypes.Value (%s) to attr.Value: missing attr.Type", tfType.String())
}

attrValue, err := attrType.ValueFromTerraform(ctx, tfType)

if err != nil {
return nil, fmt.Errorf("unable to convert tftypes.Value (%s) to attr.Value: %w", tfType.String(), err)
}

return attrValue, nil
}
114 changes: 114 additions & 0 deletions internal/common/conversion/path_helpers.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
package conversion

import (
"fmt"
"strings"

"github.com/hashicorp/terraform-plugin-framework/diag"
"github.com/hashicorp/terraform-plugin-framework/path"
)

func IsListIndex(p path.Path) bool {
lastPart := lastPart(p)
if IsMapIndex(p) || IsSetIndex(p) {
return false
}
return strings.HasSuffix(lastPart, "]")
}

func IsMapIndex(p path.Path) bool {
lastPart := lastPart(p)
return strings.HasSuffix(lastPart, "\"]")
}

func IsSetIndex(p path.Path) bool {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you tell more about IsSetIndex and why is different from the rest, here we do Contains instead of HasSuffix. Can you give an example of set index to see how it looks like and how the suffix is?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Example:
"advanced_configuration.custom_openssl_cipher_config_tls12[Value(\"ECDHE-RSA-AES256-GCM-SHA384\")]"

Want to separate IsListIndex from IsMapIndex and IsSetIndex.
See new docstrings in plan_modify_differ.go#L140

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thanks, so I don't still understand why we want to differentiate IsSetIndex and IsMapIndex, can we just have IsMapIndex? what is a use case where we want to treat them differently?

(also note IsMapIndex is not correct bc you have to do something like in IsListIndex impl., say if IsSetIndex(p) return false)

Copy link
Collaborator Author

@EspenAlbert EspenAlbert Mar 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is a subtle difference:

  • "replication_specs[Value(\"myKey\")]" ends with )], close parenthesis+square bracket
  • "replication_specs[\"myKey\"]" ends with "] quote+square bracket

See also test case TestIndexMethods

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oh, I missed that different end, thanks. and when do we want to treat them differently?
I was considering index when it's a number like [number], else set or map index

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So far set and map are used with primitive values string in this case. While the ListIndex is used with nested objects. We don't need to check for SetLenChanges or MapLenChanges only ListLenChanges so far

lastPart := lastPart(p)
return strings.Contains(lastPart, "[Value(")
}

func HasAncestor(p, ancestor path.Path) bool {
prefixString := ancestor.String()
pString := p.String()
return strings.HasPrefix(pString, prefixString)
}

func AttributeName(p path.Path) string {
noIndex := trimLastIndex(p)
parts := strings.Split(noIndex, ".")
return parts[len(parts)-1]
}

// AsAddedIndex returns "" if the path is not an index otherwise it adds `+` before the index
func AsAddedIndex(p path.Path) string {
if !isIndexValue(p) {
return ""
}
lastPart := lastPart(p)
indexWithSign := strings.Replace(lastPart, "[", "[+", 1)
everythingExceptLast, _ := strings.CutSuffix(p.String(), lastPart)
return everythingExceptLast + indexWithSign
}

// AsRemovedIndex returns "" if the path is not an index otherwise it adds `-` before the index
func AsRemovedIndex(p path.Path) string {
if !isIndexValue(p) {
return ""
}
lastPart := lastPart(p)
lastPartWithRemoveIndex := strings.Replace(lastPart, "[", "[-", 1)
everythingExceptLast, _ := strings.CutSuffix(p.String(), lastPart)
return everythingExceptLast + lastPartWithRemoveIndex
}

func AncestorPathWithIndex(p path.Path, attributeName string, diags *diag.Diagnostics) path.Path {
for {
p = p.ParentPath()
if p.Equal(path.Empty()) {
diags.AddError("Parent path not found", fmt.Sprintf("Parent attribute %s not found in path %s", attributeName, p.String()))
return p
}
if AttributeName(p) == attributeName {
return p
}
}
}

func AncestorPathNoIndex(p path.Path, attributeName string, diags *diag.Diagnostics) path.Path {
parent := AncestorPathWithIndex(p, attributeName, diags)
if diags.HasError() {
return parent
}
return trimLastIndexPath(parent)
}

func AncestorPaths(p path.Path) []path.Path {
ancestors := []path.Path{}
for {
ancestor := p.ParentPath()
if ancestor.Equal(path.Empty()) {
return ancestors
}
ancestors = append(ancestors, ancestor)
p = ancestor
}
}

func lastPart(p path.Path) string {
parts := strings.Split(p.String(), ".")
return parts[len(parts)-1]
}

func isIndexValue(p path.Path) bool {
return IsMapIndex(p) || IsListIndex(p) || IsSetIndex(p)
}

func trimLastIndex(p path.Path) string {
return trimLastIndexPath(p).String()
}

func trimLastIndexPath(p path.Path) path.Path {
if isIndexValue(p) {
return p.ParentPath()
}
return p
}
113 changes: 113 additions & 0 deletions internal/common/conversion/path_helpers_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
package conversion_test

import (
"testing"

"github.com/hashicorp/terraform-plugin-framework/diag"
"github.com/hashicorp/terraform-plugin-framework/path"
"github.com/hashicorp/terraform-plugin-framework/types"
"github.com/mongodb/terraform-provider-mongodbatlas/internal/common/conversion"
"github.com/stretchr/testify/assert"
)

func TestIsIndexTypes(t *testing.T) {
listIndexPath := path.Root("replication_specs").AtListIndex(0)
mapIndexPath := path.Root("replication_specs").AtMapKey("myKey")
setIndexPath := path.Root("replication_specs").AtSetValue(types.StringValue("myKey"))
assert.True(t, conversion.IsListIndex(listIndexPath))
assert.False(t, conversion.IsListIndex(setIndexPath))
assert.False(t, conversion.IsListIndex(mapIndexPath))

assert.True(t, conversion.IsSetIndex(setIndexPath))
assert.False(t, conversion.IsSetIndex(mapIndexPath))
assert.False(t, conversion.IsSetIndex(listIndexPath))

assert.True(t, conversion.IsMapIndex(mapIndexPath))
assert.False(t, conversion.IsMapIndex(setIndexPath))
assert.False(t, conversion.IsMapIndex(listIndexPath))
}

func TestIndexMethods(t *testing.T) {
assert.True(t, conversion.IsListIndex(path.Root("replication_specs").AtListIndex(0)))
assert.False(t, conversion.IsListIndex(path.Root("replication_specs").AtName("region_configs")))
assert.False(t, conversion.IsListIndex(path.Root("replication_specs").AtMapKey("region_configs")))
assert.Equal(t, "replication_specs[+0]", conversion.AsAddedIndex(path.Root("replication_specs").AtListIndex(0)))
assert.Equal(t, "replication_specs[0].region_configs[+1]", conversion.AsAddedIndex(path.Root("replication_specs").AtListIndex(0).AtName("region_configs").AtListIndex(1)))
assert.Equal(t, "replication_specs[+\"myKey\"]", conversion.AsAddedIndex(path.Root("replication_specs").AtMapKey("myKey")))
assert.Equal(t, "replication_specs[+Value(\"myKey\")]", conversion.AsAddedIndex(path.Root("replication_specs").AtSetValue(types.StringValue("myKey"))))
assert.Equal(t, "replication_specs[-1]", conversion.AsRemovedIndex(path.Root("replication_specs").AtListIndex(1)))
assert.Equal(t, "replication_specs[0].region_configs[-1]", conversion.AsRemovedIndex(path.Root("replication_specs").AtListIndex(0).AtName("region_configs").AtListIndex(1)))
assert.Equal(t, "replication_specs[-\"myKey\"]", conversion.AsRemovedIndex(path.Root("replication_specs").AtMapKey("myKey")))
assert.Equal(t, "replication_specs[-Value(\"myKey\")]", conversion.AsRemovedIndex(path.Root("replication_specs").AtSetValue(types.StringValue("myKey"))))
setIndex := path.Root("advanced_configuration").AtName("custom_openssl_cipher_config_tls12").AtSetValue(types.StringValue("TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384"))
assert.Equal(t, "advanced_configuration.custom_openssl_cipher_config_tls12[-Value(\"TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384\")]", conversion.AsRemovedIndex(setIndex))
assert.Equal(t, "advanced_configuration.custom_openssl_cipher_config_tls12", conversion.AncestorPathNoIndex(setIndex, "custom_openssl_cipher_config_tls12", new(diag.Diagnostics)).String())
assert.Equal(t, "", conversion.AsRemovedIndex(path.Root("replication_specs")))
}

func TestHasAncestor(t *testing.T) {
prefix := path.Root("replication_specs").AtListIndex(0)
assert.True(t, conversion.HasAncestor(path.Root("replication_specs").AtListIndex(0), prefix))
assert.True(t, conversion.HasAncestor(path.Root("replication_specs").AtListIndex(0).AtName("region_configs"), prefix))
assert.False(t, conversion.HasAncestor(path.Root("replication_specs").AtListIndex(1), prefix))
assert.True(t, conversion.HasAncestor(path.Root("replication_specs").AtListIndex(0).AtName("region_configs").AtListIndex(1), path.Empty()))
}

func TestParentPathWithIndex_Found(t *testing.T) {
diags := new(diag.Diagnostics)
// Build a nested path: resource -> parent -> child
basePath := path.Root("resource")
parentPath := basePath.AtName("parent")
childPath := parentPath.AtName("child")

assert.Equal(t, parentPath.String(), conversion.AncestorPathWithIndex(childPath, "parent", diags).String())
assert.Equal(t, basePath.String(), conversion.AncestorPathWithIndex(childPath, "resource", diags).String())
assert.Empty(t, diags, "Diagnostics should not have errors")
}

func TestParentPathWithIndex_FoundIncludesIndex(t *testing.T) {
diags := new(diag.Diagnostics)
// Build a nested path: resource[0] -> parent[0] -> child
basePath := path.Root("resource")
parentPath := basePath.AtListIndex(0).AtName("parent")
childPath := parentPath.AtListIndex(0).AtName("child")
assert.Equal(t, "resource[0].parent[0].child", childPath.String())

assert.Equal(t, parentPath.AtListIndex(0).String(), conversion.AncestorPathWithIndex(childPath, "parent", diags).String())
assert.Equal(t, basePath.AtListIndex(0).String(), conversion.AncestorPathWithIndex(childPath, "resource", diags).String())
assert.Empty(t, diags, "Diagnostics should not have errors")
}

func TestParentPathNoIndex_RemovesIndex(t *testing.T) {
diags := new(diag.Diagnostics)
// Build a nested path: resource[0] -> parent[0] -> child
basePath := path.Root("resource")
parentPath := basePath.AtListIndex(0).AtName("parent")
childPath := parentPath.AtListIndex(0).AtName("child")
assert.Equal(t, "resource[0].parent[0].child", childPath.String())

assert.Equal(t, parentPath.String(), conversion.AncestorPathNoIndex(childPath, "parent", diags).String())
assert.Equal(t, basePath.String(), conversion.AncestorPathNoIndex(childPath, "resource", diags).String())
assert.Empty(t, diags, "Diagnostics should not have errors")
}

func TestParentPathWithIndex_NotFound(t *testing.T) {
diags := new(diag.Diagnostics)
// Build a path: resource -> child
basePath := path.Root("resource")
childPath := basePath.AtName("child")

result := conversion.AncestorPathWithIndex(childPath, "nonexistent", diags)
// The function should traverse to path.Empty() and add an error.
assert.True(t, result.Equal(path.Empty()), "Expected result to be empty if parent not found")
assert.True(t, diags.HasError(), "Diagnostics should have an error when parent attribute is missing")
}

func TestParentPathWithIndex_EmptyPath(t *testing.T) {
diags := new(diag.Diagnostics)
emptyPath := path.Empty()
result := conversion.AncestorPathWithIndex(emptyPath, "any", diags)
// Since the path is empty, it should immediately return empty and add error.
assert.True(t, result.Equal(path.Empty()), "Expected empty path as result from an empty input path")
assert.True(t, diags.HasError(), "Diagnostics should have an error for empty input path")
}
Loading
Loading