terraform/internal/lang/function_results_test.go
James Bardin 2227441f7d don't cache unknown function results
The CheckPrior function results validation did not take into account
unknown values, because unknown values were not originally allowed in
the plugin protocol from which this check was adapted.

The test is simple, in that we can just reverse the check which
originally didn't allow this change in value. Provider function return
values types are defined by the protocol, and should be validated at
that call site. The CheckPrior code paths should only be used for the
exact purpose of detecting invalid changes in the return value between
successive phases of execution.
2025-10-24 09:52:37 -04:00

190 lines
4.4 KiB
Go

// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package lang
import (
"fmt"
"testing"
"github.com/hashicorp/terraform/internal/addrs"
"github.com/zclconf/go-cty/cty"
// set the correct global logger for tests
_ "github.com/hashicorp/terraform/internal/logging"
)
func TestFunctionCache(t *testing.T) {
testAddr := addrs.NewDefaultProvider("test")
type testCall struct {
provider addrs.Provider
name string
args []cty.Value
result cty.Value
}
tests := []struct {
first, second testCall
expectErr bool
}{
{
first: testCall{
provider: testAddr,
name: "fun",
args: []cty.Value{cty.StringVal("ok")},
result: cty.True,
},
second: testCall{
provider: testAddr,
name: "fun",
args: []cty.Value{cty.StringVal("ok")},
result: cty.True,
},
},
{
first: testCall{
provider: testAddr,
name: "fun",
args: []cty.Value{cty.StringVal("ok")},
result: cty.True,
},
second: testCall{
provider: testAddr,
name: "fun",
args: []cty.Value{cty.StringVal("ok")},
result: cty.False,
},
// result changed from true => false
expectErr: true,
},
{
first: testCall{
provider: testAddr,
name: "fun",
args: []cty.Value{cty.StringVal("ok")},
result: cty.UnknownVal(cty.Bool),
},
second: testCall{
provider: testAddr,
name: "fun",
args: []cty.Value{cty.StringVal("ok")},
result: cty.False,
},
// result changed from unknown => false
expectErr: false,
},
{
first: testCall{
provider: testAddr,
name: "fun",
args: []cty.Value{cty.StringVal("ok")},
result: cty.True,
},
second: testCall{
provider: addrs.NewDefaultProvider("fake"),
name: "fun",
args: []cty.Value{cty.StringVal("ok")},
result: cty.False,
},
// OK because provider changed
},
{
first: testCall{
provider: testAddr,
name: "fun",
args: []cty.Value{cty.StringVal("ok")},
result: cty.True,
},
second: testCall{
provider: testAddr,
name: "func",
args: []cty.Value{cty.StringVal("ok")},
result: cty.False,
},
// OK because function name changed
},
{
first: testCall{
provider: testAddr,
name: "fun",
args: []cty.Value{cty.StringVal("ok")},
result: cty.True,
},
second: testCall{
provider: testAddr,
name: "fun",
args: []cty.Value{cty.StringVal("ok"), cty.StringVal("ok")},
result: cty.False,
},
// OK because args changed
},
{
first: testCall{
provider: testAddr,
name: "fun",
args: []cty.Value{cty.ObjectVal(map[string]cty.Value{
"attr": cty.NumberIntVal(1),
})},
result: cty.True,
},
second: testCall{
provider: testAddr,
name: "fun",
args: []cty.Value{cty.ObjectVal(map[string]cty.Value{
"attr": cty.NumberIntVal(2),
})},
result: cty.False,
},
// OK because args changed
},
{
first: testCall{
provider: testAddr,
name: "fun",
args: []cty.Value{cty.UnknownVal(cty.Object(map[string]cty.Type{
"attr": cty.Number,
}))},
result: cty.UnknownVal(cty.Bool),
},
second: testCall{
provider: testAddr,
name: "fun",
args: []cty.Value{cty.ObjectVal(map[string]cty.Value{
"attr": cty.NumberIntVal(2),
})},
result: cty.False,
},
// OK because args changed from unknown to known
},
}
for i, test := range tests {
t.Run(fmt.Sprint(i), func(t *testing.T) {
results := NewFunctionResultsTable(nil)
err := results.CheckPriorProvider(test.first.provider, test.first.name, test.first.args, test.first.result)
if err != nil {
t.Fatal("error on first call!", err)
}
err = results.CheckPriorProvider(test.second.provider, test.second.name, test.second.args, test.second.result)
if err != nil && !test.expectErr {
t.Fatal(err)
}
if err == nil && test.expectErr {
t.Fatal("expected error")
}
// reload the data to ensure we validate identically
newResults := NewFunctionResultsTable(results.GetHashes())
originalErr := err != nil
reloadedErr := newResults.CheckPriorProvider(test.second.provider, test.second.name, test.second.args, test.second.result) != nil
if originalErr != reloadedErr {
t.Fatalf("original check returned err:%t, reloaded check returned err:%t", originalErr, reloadedErr)
}
})
}
}