mirror of
https://github.com/opentffoundation/opentf.git
synced 2025-12-20 02:09:26 -05:00
The code for matching relevant_attributes against resource_drift entries (a part of the heuristic for deciding whether to show "changes outside of OpenTofu" in the human-oriented plan UI) was previously assuming that paths in resource_drift would always be valid for the associated resource instance object values because in most cases the language runtime will detect invalid references and so fail to generate a plan at all. However, when the reference is to something within a dynamically-typed argument (such as the manifest in kubernetes_manifest) and when it appears only as an argument to either the "try" or "can" functions (so the dynamic error is intentionally suppressed) the language runtime can't catch it and so the incorrect reference will leak out into relevant_attributes, thereby violating assumptions made by the path matcher. Instead then, we'll continue the existing precedent that this "relevant attributes" mechanism is a best-effort heuristic that prefers to succeed with an incomplete result rather than to fail, extending that to the traversals in the plan renderer which will now treat incorrectly-typed steps as not matching rather than causing OpenTofu to crash completely. Since a reference to something that doesn't exist cannot succeed it also cannot possibly _actually_ contribute directly to the final result of the expression it appeared in, so in practice it should be okay to disregard these invalid references for the purposes of deciding which changes outside of OpenTofu seem likely to have caused the actions that OpenTofu is proposing to make during the apply phase. Signed-off-by: Martin Atkins <mart@degeneration.co.uk>
325 lines
7.3 KiB
Go
325 lines
7.3 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 attribute_path
|
|
|
|
import "testing"
|
|
|
|
func TestPathMatcher_FollowsPath(t *testing.T) {
|
|
var matcher Matcher
|
|
|
|
matcher = &PathMatcher{
|
|
Paths: [][]any{
|
|
{
|
|
float64(0),
|
|
"key",
|
|
float64(0),
|
|
},
|
|
},
|
|
}
|
|
|
|
if matcher.Matches() {
|
|
t.Errorf("should not have exact matched at base level")
|
|
}
|
|
if !matcher.MatchesPartial() {
|
|
t.Errorf("should have partial matched at base level")
|
|
}
|
|
|
|
if matcher := matcher.GetChildWithKey("key"); matcher.MatchesPartial() {
|
|
t.Errorf("should not have matched string key in first step (it's actually an index step)")
|
|
}
|
|
|
|
matcher = matcher.GetChildWithIndex(0)
|
|
|
|
if matcher.Matches() {
|
|
t.Errorf("should not have exact matched at first level")
|
|
}
|
|
if !matcher.MatchesPartial() {
|
|
t.Errorf("should have partial matched at first level")
|
|
}
|
|
|
|
matcher = matcher.GetChildWithKey("key")
|
|
|
|
if matcher.Matches() {
|
|
t.Errorf("should not have exact matched at second level")
|
|
}
|
|
if !matcher.MatchesPartial() {
|
|
t.Errorf("should have partial matched at second level")
|
|
}
|
|
|
|
matcher = matcher.GetChildWithIndex(0)
|
|
|
|
if !matcher.Matches() {
|
|
t.Errorf("should have exact matched at leaf level")
|
|
}
|
|
if !matcher.MatchesPartial() {
|
|
t.Errorf("should have partial matched at leaf level")
|
|
}
|
|
}
|
|
|
|
func TestPathMatcher_HandlesListIndexWithString(t *testing.T) {
|
|
t.Run("convertible", func(t *testing.T) {
|
|
var matcher Matcher
|
|
matcher = &PathMatcher{
|
|
Paths: [][]any{
|
|
{
|
|
"0",
|
|
"key",
|
|
},
|
|
},
|
|
}
|
|
|
|
matcher = matcher.GetChildWithIndex(0)
|
|
if matcher.Matches() {
|
|
t.Errorf("should not have exact matched at first level")
|
|
}
|
|
if !matcher.MatchesPartial() {
|
|
t.Errorf("should have partial matched at first level")
|
|
}
|
|
|
|
matcher = matcher.GetChildWithKey("key")
|
|
if !matcher.Matches() {
|
|
t.Errorf("should have exact matched at second level")
|
|
}
|
|
if !matcher.MatchesPartial() {
|
|
t.Errorf("should have partial matched at second level")
|
|
}
|
|
|
|
})
|
|
t.Run("not convertible", func(t *testing.T) {
|
|
var matcher Matcher
|
|
matcher = &PathMatcher{
|
|
Paths: [][]any{
|
|
{
|
|
"not a number",
|
|
"key",
|
|
},
|
|
},
|
|
}
|
|
|
|
// The main thing we're testing here is actually that this
|
|
// succeeds without crashing, since we should treat the non-number
|
|
// step as a non-match. This is to allow for situations where there
|
|
// is an invalid reference to something that can only be
|
|
// dynamically-typechecked from in a "try" or "can" function call,
|
|
// which can cause invalid paths to appear in a matcher even though
|
|
// planning otherwise succeeded.
|
|
matcher = matcher.GetChildWithIndex(0)
|
|
if matcher.Matches() {
|
|
t.Errorf("should not have exact matched at first level")
|
|
}
|
|
if matcher.MatchesPartial() {
|
|
t.Errorf("should not have partial matched at first level")
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestPathMatcher_Propagates(t *testing.T) {
|
|
var matcher Matcher
|
|
|
|
matcher = &PathMatcher{
|
|
Paths: [][]any{
|
|
{
|
|
float64(0),
|
|
"key",
|
|
},
|
|
},
|
|
Propagate: true,
|
|
}
|
|
|
|
if matcher.Matches() {
|
|
t.Errorf("should not have exact matched at base level")
|
|
}
|
|
if !matcher.MatchesPartial() {
|
|
t.Errorf("should have partial matched at base level")
|
|
}
|
|
|
|
if matcher := matcher.GetChildWithKey("key"); matcher.MatchesPartial() {
|
|
t.Errorf("should not have matched string key in first step (it's actually an index step)")
|
|
}
|
|
|
|
matcher = matcher.GetChildWithIndex(0)
|
|
|
|
if matcher.Matches() {
|
|
t.Errorf("should not have exact matched at first level")
|
|
}
|
|
if !matcher.MatchesPartial() {
|
|
t.Errorf("should have partial matched at first level")
|
|
}
|
|
|
|
matcher = matcher.GetChildWithKey("key")
|
|
|
|
if !matcher.Matches() {
|
|
t.Errorf("should have exact matched at second level")
|
|
}
|
|
if !matcher.MatchesPartial() {
|
|
t.Errorf("should have partial matched at second level")
|
|
}
|
|
|
|
matcher = matcher.GetChildWithIndex(0)
|
|
|
|
if !matcher.Matches() {
|
|
t.Errorf("should have exact matched at leaf level")
|
|
}
|
|
if !matcher.MatchesPartial() {
|
|
t.Errorf("should have partial matched at leaf level")
|
|
}
|
|
}
|
|
func TestPathMatcher_DoesNotPropagate(t *testing.T) {
|
|
var matcher Matcher
|
|
|
|
matcher = &PathMatcher{
|
|
Paths: [][]interface{}{
|
|
{
|
|
float64(0),
|
|
"key",
|
|
},
|
|
},
|
|
}
|
|
|
|
if matcher.Matches() {
|
|
t.Errorf("should not have exact matched at base level")
|
|
}
|
|
if !matcher.MatchesPartial() {
|
|
t.Errorf("should have partial matched at base level")
|
|
}
|
|
|
|
matcher = matcher.GetChildWithIndex(0)
|
|
|
|
if matcher.Matches() {
|
|
t.Errorf("should not have exact matched at first level")
|
|
}
|
|
if !matcher.MatchesPartial() {
|
|
t.Errorf("should have partial matched at first level")
|
|
}
|
|
|
|
matcher = matcher.GetChildWithKey("key")
|
|
|
|
if !matcher.Matches() {
|
|
t.Errorf("should have exact matched at second level")
|
|
}
|
|
if !matcher.MatchesPartial() {
|
|
t.Errorf("should have partial matched at second level")
|
|
}
|
|
|
|
matcher = matcher.GetChildWithIndex(0)
|
|
|
|
if matcher.Matches() {
|
|
t.Errorf("should not have exact matched at leaf level")
|
|
}
|
|
if matcher.MatchesPartial() {
|
|
t.Errorf("should not have partial matched at leaf level")
|
|
}
|
|
}
|
|
|
|
func TestPathMatcher_BreaksPath(t *testing.T) {
|
|
var matcher Matcher
|
|
|
|
matcher = &PathMatcher{
|
|
Paths: [][]interface{}{
|
|
{
|
|
float64(0),
|
|
"key",
|
|
float64(0),
|
|
},
|
|
},
|
|
}
|
|
|
|
if matcher.Matches() {
|
|
t.Errorf("should not have exact matched at base level")
|
|
}
|
|
if !matcher.MatchesPartial() {
|
|
t.Errorf("should have partial matched at base level")
|
|
}
|
|
|
|
matcher = matcher.GetChildWithIndex(0)
|
|
|
|
if matcher.Matches() {
|
|
t.Errorf("should not have exact matched at first level")
|
|
}
|
|
if !matcher.MatchesPartial() {
|
|
t.Errorf("should have partial matched at first level")
|
|
}
|
|
|
|
matcher = matcher.GetChildWithKey("invalid")
|
|
|
|
if matcher.Matches() {
|
|
t.Errorf("should not have exact matched at second level")
|
|
}
|
|
if matcher.MatchesPartial() {
|
|
t.Errorf("should not have partial matched at second level")
|
|
|
|
}
|
|
}
|
|
|
|
func TestPathMatcher_MultiplePaths(t *testing.T) {
|
|
var matcher Matcher
|
|
|
|
matcher = &PathMatcher{
|
|
Paths: [][]interface{}{
|
|
{
|
|
float64(0),
|
|
"key",
|
|
float64(0),
|
|
},
|
|
{
|
|
float64(0),
|
|
"key",
|
|
float64(1),
|
|
},
|
|
},
|
|
}
|
|
|
|
if matcher.Matches() {
|
|
t.Errorf("should not have exact matched at base level")
|
|
}
|
|
if !matcher.MatchesPartial() {
|
|
t.Errorf("should have partial matched at base level")
|
|
}
|
|
|
|
matcher = matcher.GetChildWithIndex(0)
|
|
|
|
if matcher.Matches() {
|
|
t.Errorf("should not have exact matched at first level")
|
|
}
|
|
if !matcher.MatchesPartial() {
|
|
t.Errorf("should have partial matched at first level")
|
|
}
|
|
|
|
matcher = matcher.GetChildWithKey("key")
|
|
|
|
if matcher.Matches() {
|
|
t.Errorf("should not have exact matched at second level")
|
|
}
|
|
if !matcher.MatchesPartial() {
|
|
t.Errorf("should have partial matched at second level")
|
|
}
|
|
|
|
validZero := matcher.GetChildWithIndex(0)
|
|
validOne := matcher.GetChildWithIndex(1)
|
|
invalid := matcher.GetChildWithIndex(2)
|
|
|
|
if !validZero.Matches() {
|
|
t.Errorf("should have exact matched at leaf level")
|
|
}
|
|
if !validZero.MatchesPartial() {
|
|
t.Errorf("should have partial matched at leaf level")
|
|
}
|
|
|
|
if !validOne.Matches() {
|
|
t.Errorf("should have exact matched at leaf level")
|
|
}
|
|
if !validOne.MatchesPartial() {
|
|
t.Errorf("should have partial matched at leaf level")
|
|
}
|
|
|
|
if invalid.Matches() {
|
|
t.Errorf("should not have exact matched at leaf level")
|
|
}
|
|
if invalid.MatchesPartial() {
|
|
t.Errorf("should not have partial matched at leaf level")
|
|
}
|
|
}
|