Files
opentf/internal/addrs/parse_target_test.go
Martin Atkins 5b5a285066 Replace github.com/go-test/deep with go-cmp
My original intention was just to reduce our number of dependencies by
standardizing on a single comparison library, but in the process of doing
so I found various examples of the kinds of problems that caused this
codebase to begin adopting go-cmp instead of go-test/deep in the first
place, which make it easy to accidentally write a false-positive test that
doesn't actually check what the author thinks is being checked:

- deep.Equal silently ignores unexported fields, so comparing two values
  that differ only in data in unexported fields succeeds even when it ought
  not to.

  TestContext2Apply_multiVarComprehensive in package tofu was an excellent
  example of this problem: it had various test assertions that were
  actually checking absolutely nothing, despite appearing to compare
  pairs of cty.Value.

- deep.Equal also silently ignores anything below a certain level of
  nesting, and so comparison of deep data structures can appear to succeed
  even though they don't actually match.

  There were a few examples where that problem had already been found and
  fixed by temporarily overriding the package deep global settings, but
  with go-cmp the default behavior already visits everything, or panics
  if it cannot.

This does mean that in a few cases this needed some more elaborate options
to cmp.Diff to align with the previous behavior, which is a little annoying
but overall I think better to be explicit about what each test is relying
on. Perhaps we can rework these tests to need fewer unusual cmp options
in future, but for this commit I want to keep focused on the smallest
possible changes to remove our dependency on github.com/go-test/deep .

Signed-off-by: Martin Atkins <mart@degeneration.co.uk>
2025-10-13 08:17:40 -07:00

546 lines
12 KiB
Go

// Copyright (c) The OpenTofu Authors
// SPDX-License-Identifier: MPL-2.0
// Copyright (c) 2023 HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package addrs
import (
"testing"
"github.com/google/go-cmp/cmp"
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/hclsyntax"
"github.com/opentofu/opentofu/internal/tfdiags"
)
func TestParseTarget(t *testing.T) {
tests := []struct {
Input string
Want *Target
WantErr string
}{
{
`module.foo`,
&Target{
Subject: ModuleInstance{
{
Name: "foo",
},
},
SourceRange: tfdiags.SourceRange{
Start: tfdiags.SourcePos{Line: 1, Column: 1, Byte: 0},
End: tfdiags.SourcePos{Line: 1, Column: 11, Byte: 10},
},
},
``,
},
{
`module.foo[2]`,
&Target{
Subject: ModuleInstance{
{
Name: "foo",
InstanceKey: IntKey(2),
},
},
SourceRange: tfdiags.SourceRange{
Start: tfdiags.SourcePos{Line: 1, Column: 1, Byte: 0},
End: tfdiags.SourcePos{Line: 1, Column: 14, Byte: 13},
},
},
``,
},
{
`module.foo[2].module.bar`,
&Target{
Subject: ModuleInstance{
{
Name: "foo",
InstanceKey: IntKey(2),
},
{
Name: "bar",
},
},
SourceRange: tfdiags.SourceRange{
Start: tfdiags.SourcePos{Line: 1, Column: 1, Byte: 0},
End: tfdiags.SourcePos{Line: 1, Column: 25, Byte: 24},
},
},
``,
},
{
`aws_instance.foo`,
&Target{
Subject: AbsResource{
Resource: Resource{
Mode: ManagedResourceMode,
Type: "aws_instance",
Name: "foo",
},
Module: RootModuleInstance,
},
SourceRange: tfdiags.SourceRange{
Start: tfdiags.SourcePos{Line: 1, Column: 1, Byte: 0},
End: tfdiags.SourcePos{Line: 1, Column: 17, Byte: 16},
},
},
``,
},
{
`aws_instance.foo[1]`,
&Target{
Subject: AbsResourceInstance{
Resource: ResourceInstance{
Resource: Resource{
Mode: ManagedResourceMode,
Type: "aws_instance",
Name: "foo",
},
Key: IntKey(1),
},
Module: RootModuleInstance,
},
SourceRange: tfdiags.SourceRange{
Start: tfdiags.SourcePos{Line: 1, Column: 1, Byte: 0},
End: tfdiags.SourcePos{Line: 1, Column: 20, Byte: 19},
},
},
``,
},
{
`data.aws_instance.foo`,
&Target{
Subject: AbsResource{
Resource: Resource{
Mode: DataResourceMode,
Type: "aws_instance",
Name: "foo",
},
Module: RootModuleInstance,
},
SourceRange: tfdiags.SourceRange{
Start: tfdiags.SourcePos{Line: 1, Column: 1, Byte: 0},
End: tfdiags.SourcePos{Line: 1, Column: 22, Byte: 21},
},
},
``,
},
{
`data.aws_instance.foo[1]`,
&Target{
Subject: AbsResourceInstance{
Resource: ResourceInstance{
Resource: Resource{
Mode: DataResourceMode,
Type: "aws_instance",
Name: "foo",
},
Key: IntKey(1),
},
Module: RootModuleInstance,
},
SourceRange: tfdiags.SourceRange{
Start: tfdiags.SourcePos{Line: 1, Column: 1, Byte: 0},
End: tfdiags.SourcePos{Line: 1, Column: 25, Byte: 24},
},
},
``,
},
{
`module.foo.aws_instance.bar`,
&Target{
Subject: AbsResource{
Resource: Resource{
Mode: ManagedResourceMode,
Type: "aws_instance",
Name: "bar",
},
Module: ModuleInstance{
{Name: "foo"},
},
},
SourceRange: tfdiags.SourceRange{
Start: tfdiags.SourcePos{Line: 1, Column: 1, Byte: 0},
End: tfdiags.SourcePos{Line: 1, Column: 28, Byte: 27},
},
},
``,
},
{
`module.foo.module.bar.aws_instance.baz`,
&Target{
Subject: AbsResource{
Resource: Resource{
Mode: ManagedResourceMode,
Type: "aws_instance",
Name: "baz",
},
Module: ModuleInstance{
{Name: "foo"},
{Name: "bar"},
},
},
SourceRange: tfdiags.SourceRange{
Start: tfdiags.SourcePos{Line: 1, Column: 1, Byte: 0},
End: tfdiags.SourcePos{Line: 1, Column: 39, Byte: 38},
},
},
``,
},
{
`module.foo.module.bar.aws_instance.baz["hello"]`,
&Target{
Subject: AbsResourceInstance{
Resource: ResourceInstance{
Resource: Resource{
Mode: ManagedResourceMode,
Type: "aws_instance",
Name: "baz",
},
Key: StringKey("hello"),
},
Module: ModuleInstance{
{Name: "foo"},
{Name: "bar"},
},
},
SourceRange: tfdiags.SourceRange{
Start: tfdiags.SourcePos{Line: 1, Column: 1, Byte: 0},
End: tfdiags.SourcePos{Line: 1, Column: 48, Byte: 47},
},
},
``,
},
{
`module.foo.data.aws_instance.bar`,
&Target{
Subject: AbsResource{
Resource: Resource{
Mode: DataResourceMode,
Type: "aws_instance",
Name: "bar",
},
Module: ModuleInstance{
{Name: "foo"},
},
},
SourceRange: tfdiags.SourceRange{
Start: tfdiags.SourcePos{Line: 1, Column: 1, Byte: 0},
End: tfdiags.SourcePos{Line: 1, Column: 33, Byte: 32},
},
},
``,
},
{
`module.foo.module.bar.data.aws_instance.baz`,
&Target{
Subject: AbsResource{
Resource: Resource{
Mode: DataResourceMode,
Type: "aws_instance",
Name: "baz",
},
Module: ModuleInstance{
{Name: "foo"},
{Name: "bar"},
},
},
SourceRange: tfdiags.SourceRange{
Start: tfdiags.SourcePos{Line: 1, Column: 1, Byte: 0},
End: tfdiags.SourcePos{Line: 1, Column: 44, Byte: 43},
},
},
``,
},
{
`module.foo.module.bar[0].data.aws_instance.baz`,
&Target{
Subject: AbsResource{
Resource: Resource{
Mode: DataResourceMode,
Type: "aws_instance",
Name: "baz",
},
Module: ModuleInstance{
{Name: "foo", InstanceKey: NoKey},
{Name: "bar", InstanceKey: IntKey(0)},
},
},
SourceRange: tfdiags.SourceRange{
Start: tfdiags.SourcePos{Line: 1, Column: 1, Byte: 0},
End: tfdiags.SourcePos{Line: 1, Column: 47, Byte: 46},
},
},
``,
},
{
`module.foo.module.bar["a"].data.aws_instance.baz["hello"]`,
&Target{
Subject: AbsResourceInstance{
Resource: ResourceInstance{
Resource: Resource{
Mode: DataResourceMode,
Type: "aws_instance",
Name: "baz",
},
Key: StringKey("hello"),
},
Module: ModuleInstance{
{Name: "foo", InstanceKey: NoKey},
{Name: "bar", InstanceKey: StringKey("a")},
},
},
SourceRange: tfdiags.SourceRange{
Start: tfdiags.SourcePos{Line: 1, Column: 1, Byte: 0},
End: tfdiags.SourcePos{Line: 1, Column: 58, Byte: 57},
},
},
``,
},
{
`module.foo.module.bar.data.aws_instance.baz["hello"]`,
&Target{
Subject: AbsResourceInstance{
Resource: ResourceInstance{
Resource: Resource{
Mode: DataResourceMode,
Type: "aws_instance",
Name: "baz",
},
Key: StringKey("hello"),
},
Module: ModuleInstance{
{Name: "foo"},
{Name: "bar"},
},
},
SourceRange: tfdiags.SourceRange{
Start: tfdiags.SourcePos{Line: 1, Column: 1, Byte: 0},
End: tfdiags.SourcePos{Line: 1, Column: 53, Byte: 52},
},
},
``,
},
// ephemeral
{
`ephemeral.aws_instance.baz`,
&Target{
Subject: AbsResource{
Resource: Resource{
Mode: EphemeralResourceMode,
Type: "aws_instance",
Name: "baz",
},
Module: RootModuleInstance,
},
SourceRange: tfdiags.SourceRange{
Start: tfdiags.SourcePos{Line: 1, Column: 1, Byte: 0},
End: tfdiags.SourcePos{Line: 1, Column: 27, Byte: 26},
},
},
``,
},
{
`ephemeral.aws_instance.baz[1]`,
&Target{
Subject: AbsResourceInstance{
Resource: ResourceInstance{
Resource: Resource{
Mode: EphemeralResourceMode,
Type: "aws_instance",
Name: "baz",
},
Key: IntKey(1),
},
Module: RootModuleInstance,
},
SourceRange: tfdiags.SourceRange{
Start: tfdiags.SourcePos{Line: 1, Column: 1, Byte: 0},
End: tfdiags.SourcePos{Line: 1, Column: 30, Byte: 29},
},
},
``,
},
{
`module.foo.ephemeral.aws_instance.baz`,
&Target{
Subject: AbsResource{
Resource: Resource{
Mode: EphemeralResourceMode,
Type: "aws_instance",
Name: "baz",
},
Module: ModuleInstance{
{Name: "foo"},
},
},
SourceRange: tfdiags.SourceRange{
Start: tfdiags.SourcePos{Line: 1, Column: 1, Byte: 0},
End: tfdiags.SourcePos{Line: 1, Column: 38, Byte: 37},
},
},
``,
},
{
`module.foo.module.bar.ephemeral.aws_instance.baz`,
&Target{
Subject: AbsResource{
Resource: Resource{
Mode: EphemeralResourceMode,
Type: "aws_instance",
Name: "baz",
},
Module: ModuleInstance{
{Name: "foo"},
{Name: "bar"},
},
},
SourceRange: tfdiags.SourceRange{
Start: tfdiags.SourcePos{Line: 1, Column: 1, Byte: 0},
End: tfdiags.SourcePos{Line: 1, Column: 49, Byte: 48},
},
},
``,
},
{
`module.foo.module.bar[0].ephemeral.aws_instance.baz`,
&Target{
Subject: AbsResource{
Resource: Resource{
Mode: EphemeralResourceMode,
Type: "aws_instance",
Name: "baz",
},
Module: ModuleInstance{
{Name: "foo", InstanceKey: NoKey},
{Name: "bar", InstanceKey: IntKey(0)},
},
},
SourceRange: tfdiags.SourceRange{
Start: tfdiags.SourcePos{Line: 1, Column: 1, Byte: 0},
End: tfdiags.SourcePos{Line: 1, Column: 52, Byte: 51},
},
},
``,
},
{
`module.foo.module.bar["a"].ephemeral.aws_instance.baz["hello"]`,
&Target{
Subject: AbsResourceInstance{
Resource: ResourceInstance{
Resource: Resource{
Mode: EphemeralResourceMode,
Type: "aws_instance",
Name: "baz",
},
Key: StringKey("hello"),
},
Module: ModuleInstance{
{Name: "foo", InstanceKey: NoKey},
{Name: "bar", InstanceKey: StringKey("a")},
},
},
SourceRange: tfdiags.SourceRange{
Start: tfdiags.SourcePos{Line: 1, Column: 1, Byte: 0},
End: tfdiags.SourcePos{Line: 1, Column: 63, Byte: 62},
},
},
``,
},
{
`module.foo.module.bar.ephemeral.aws_instance.baz["hello"]`,
&Target{
Subject: AbsResourceInstance{
Resource: ResourceInstance{
Resource: Resource{
Mode: EphemeralResourceMode,
Type: "aws_instance",
Name: "baz",
},
Key: StringKey("hello"),
},
Module: ModuleInstance{
{Name: "foo"},
{Name: "bar"},
},
},
SourceRange: tfdiags.SourceRange{
Start: tfdiags.SourcePos{Line: 1, Column: 1, Byte: 0},
End: tfdiags.SourcePos{Line: 1, Column: 58, Byte: 57},
},
},
``,
},
// errors
{
`aws_instance`,
nil,
`Resource specification must include a resource type and name.`,
},
{
`module`,
nil,
`Prefix "module." must be followed by a module name.`,
},
{
`module["baz"]`,
nil,
`Prefix "module." must be followed by a module name.`,
},
{
`module.baz.bar`,
nil,
`Resource specification must include a resource type and name.`,
},
{
`aws_instance.foo.bar`,
nil,
`Resource instance key must be given in square brackets.`,
},
{
`aws_instance.foo[1].baz`,
nil,
`Unexpected extra operators after address.`,
},
}
for _, test := range tests {
t.Run(test.Input, func(t *testing.T) {
traversal, travDiags := hclsyntax.ParseTraversalAbs([]byte(test.Input), "", hcl.Pos{Line: 1, Column: 1})
if travDiags.HasErrors() {
t.Fatal(travDiags.Error())
}
got, diags := ParseTarget(traversal)
switch len(diags) {
case 0:
if test.WantErr != "" {
t.Fatalf("succeeded; want error: %s", test.WantErr)
}
case 1:
if test.WantErr == "" {
t.Fatalf("unexpected diagnostics: %s", diags.Err())
}
if got, want := diags[0].Description().Detail, test.WantErr; got != want {
t.Fatalf("wrong error\ngot: %s\nwant: %s", got, want)
}
default:
t.Fatalf("too many diagnostics: %s", diags.Err())
}
if diags.HasErrors() {
return
}
if diff := cmp.Diff(test.Want, got, CmpOptionsForTesting); diff != "" {
t.Error("wrong result:\n" + diff)
}
})
}
}