Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
5 changes: 5 additions & 0 deletions .changes/v1.16/BUG FIXES-20260929-165130.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
kind: BUG FIXES
body: 'funcs: The `log` and `pow` functions now raise errors when they return +/- Infinity, as this result is not usable in downstream code.'
time: 2026-09-29T16:51:30.773193+01:00
custom:
Issue: "39304"
4 changes: 2 additions & 2 deletions internal/lang/funcs/number.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ var LogFunc = function.New(&function.Spec{
}

result := math.Log(num) / math.Log(base)
if math.IsNaN(result) {
if math.IsNaN(result) || math.IsInf(result, 0) {
return cty.UnknownVal(cty.String), fmt.Errorf("result is not a number")
}
Comment on lines +41 to 43

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Reviewer: Should +/- infinity return a different error? I could update this to return a different fmt.Errorf("result is not a finite number") or something.


Expand Down Expand Up @@ -72,7 +72,7 @@ var PowFunc = function.New(&function.Spec{
}

result := math.Pow(num, power)
if math.IsNaN(result) {
if math.IsNaN(result) || math.IsInf(result, 0) {
return cty.UnknownVal(cty.String), fmt.Errorf("result is not a number")
}

Expand Down
128 changes: 79 additions & 49 deletions internal/lang/funcs/number_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,146 +10,139 @@ import (
"github.com/zclconf/go-cty/cty"
)

func TestLog(t *testing.T) {
func TestLog_valid(t *testing.T) {
tests := []struct {
Num cty.Value
Base cty.Value
Want cty.Value
Err bool
}{
{
cty.NumberFloatVal(1),
cty.NumberFloatVal(10),
cty.NumberFloatVal(0),
false,
},
{
cty.NumberFloatVal(10),
cty.NumberFloatVal(10),
cty.NumberFloatVal(1),
false,
},

{
cty.NumberFloatVal(0),
cty.NumberFloatVal(10),
cty.NegativeInfinity,
false,
Comment on lines -34 to -37

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

This old test case shows infinity being returned without an error, but I don't think this was part of an explicit decision to state that infinite values were valid returned values so we shouldn't be held back by it.

For me, the fact downstream code cannot process this value is a clear sign that raising an error instead is the correct behaviour.

},
{
cty.NumberFloatVal(10),
cty.NumberFloatVal(0),
cty.NumberFloatVal(-0),
false,
},
}

for _, test := range tests {
t.Run(fmt.Sprintf("log(%#v, %#v)", test.Num, test.Base), func(t *testing.T) {
got, err := LogFunc.Call([]cty.Value{test.Num, test.Base})
if err != nil {
t.Fatalf("unexpected error: %s", err)
}

if !got.RawEquals(test.Want) {
t.Errorf("wrong result\ngot: %#v\nwant: %#v", got, test.Want)
}
})
}
}

func TestLog_invalid(t *testing.T) {
tests := []struct {
Num cty.Value
Base cty.Value
}{
{
// NaN result
cty.NumberFloatVal(1),
cty.NumberFloatVal(1),
cty.NumberFloatVal(1),
true,
},
{
// NaN result
cty.NumberFloatVal(-1),
cty.NumberFloatVal(10),
cty.NumberFloatVal(1),
true,
},
{
// Negative infinity result
cty.NumberFloatVal(0),
cty.NumberFloatVal(10),
},
{
// Positive infinity result
cty.PositiveInfinity,
cty.NumberFloatVal(2),
},
}

for _, test := range tests {
t.Run(fmt.Sprintf("log(%#v, %#v)", test.Num, test.Base), func(t *testing.T) {
got, err := LogFunc.Call([]cty.Value{test.Num, test.Base})
_, err := LogFunc.Call([]cty.Value{test.Num, test.Base})

if test.Err {
if err == nil {
t.Fatal("succeeded; want error")
}
return
} else if err != nil {
t.Fatalf("unexpected error: %s", err)
if err == nil {
t.Fatal("expected error but got none")
}

if !got.RawEquals(test.Want) {
t.Errorf("wrong result\ngot: %#v\nwant: %#v", got, test.Want)
expectedErr := "result is not a number"
if err.Error() != expectedErr {
t.Fatal(err)
}
})
}
}

func TestPow(t *testing.T) {
func TestPow_valid(t *testing.T) {
tests := []struct {
Num cty.Value
Power cty.Value
Want cty.Value
Err bool
}{
{
cty.NumberFloatVal(1),
cty.NumberFloatVal(0),
cty.NumberFloatVal(1),
false,
},
{
cty.NumberFloatVal(1),
cty.NumberFloatVal(1),
cty.NumberFloatVal(1),
false,
},

{
cty.NumberFloatVal(2),
cty.NumberFloatVal(0),
cty.NumberFloatVal(1),
false,
},
{
cty.NumberFloatVal(2),
cty.NumberFloatVal(1),
cty.NumberFloatVal(2),
false,
},
{
cty.NumberFloatVal(3),
cty.NumberFloatVal(2),
cty.NumberFloatVal(9),
false,
},
{
cty.NumberFloatVal(-3),
cty.NumberFloatVal(2),
cty.NumberFloatVal(9),
false,
},
{
cty.NumberFloatVal(2),
cty.NumberFloatVal(-2),
cty.NumberFloatVal(0.25),
false,
},
{
cty.NumberFloatVal(0),
cty.NumberFloatVal(2),
cty.NumberFloatVal(0),
false,
},
{
cty.NumberFloatVal(-2),
cty.NumberFloatVal(0.5),
cty.NumberFloatVal(0),
true,
},
}

for _, test := range tests {
t.Run(fmt.Sprintf("pow(%#v, %#v)", test.Num, test.Power), func(t *testing.T) {
got, err := PowFunc.Call([]cty.Value{test.Num, test.Power})

if test.Err {
if err == nil {
t.Fatal("succeeded; want error")
}
return
} else if err != nil {
if err != nil {
t.Fatalf("unexpected error: %s", err)
}

Expand All @@ -159,3 +152,40 @@ func TestPow(t *testing.T) {
})
}
}

func TestPow_invalid(t *testing.T) {
tests := []struct {
Num cty.Value
Power cty.Value
}{
{
// NaN result
cty.NumberFloatVal(-2),
cty.NumberFloatVal(0.5),
},
{
// Positive Infinity result
cty.NumberFloatVal(10),
cty.NumberFloatVal(1000),
},
{
// Negative Infinity result
cty.NegativeInfinity,
cty.NumberFloatVal(1),
},
}

for _, test := range tests {
t.Run(fmt.Sprintf("pow(%#v, %#v)", test.Num, test.Power), func(t *testing.T) {
_, err := PowFunc.Call([]cty.Value{test.Num, test.Power})
if err == nil {
t.Fatal("succeeded; want error")
}

expectedErr := "result is not a number"
if err.Error() != expectedErr {
t.Fatal(err)
}
})
}
}
Loading