Skip to content

Commit

Permalink
pkg/eval/evaltest: Implement value matchers with interface.
Browse files Browse the repository at this point in the history
  • Loading branch information
xiaq committed Aug 14, 2023
1 parent f240df4 commit 95ba078
Show file tree
Hide file tree
Showing 3 changed files with 64 additions and 54 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -2,51 +2,12 @@ package evaltest

import (
"fmt"
"math"
"reflect"
"regexp"

"src.elv.sh/pkg/eval"
"src.elv.sh/pkg/parse"
)

// Anything is a value that can be passed to [Case.Puts] to match any value. It
// is useful when the value contains information that is useful when the test
// fails.
var Anything = anything{}

type anything struct{}

// ApproximatelyThreshold defines the threshold for matching float64 values when
// using Approximately.
const ApproximatelyThreshold = 1e-15

// Approximately returns a value that can be passed to Case.Puts to match a
// float64 within the threshold defined by ApproximatelyThreshold.
func Approximately(f float64) any { return approximately{f} }

type approximately struct{ value float64 }

func matchFloat64(a, b, threshold float64) bool {
if math.IsNaN(a) && math.IsNaN(b) {
return true
}
if math.IsInf(a, 0) && math.IsInf(b, 0) &&
math.Signbit(a) == math.Signbit(b) {
return true
}
return math.Abs(a-b) <= threshold
}

// StringMatching returns a value that can be passed to Case.Puts to match any
// string matching a regexp pattern. If the pattern is not a valid regexp, the
// function panics.
func StringMatching(p string) any {
return stringMatching{regexp.MustCompile(p)}
}

type stringMatching struct{ pattern *regexp.Regexp }

type errorMatcher interface{ matchError(error) bool }

// An errorMatcher for compilation errors.
Expand Down
20 changes: 5 additions & 15 deletions pkg/eval/evaltest/evaltest.go
Original file line number Diff line number Diff line change
Expand Up @@ -259,23 +259,13 @@ func matchOut(want, got []any) bool {
}

func match(got, want any) bool {
if want == Anything {
return true
if matcher, ok := want.(ValueMatcher); ok {
return matcher.matchValue(got)
}
switch got := got.(type) {
case float64:
// Special-case float64 to correctly handle NaN and support
// approximate comparison.
switch want := want.(type) {
case float64:
// Special-case float64 to handle NaNs and infinities.
if got, ok := got.(float64); ok {
if want, ok := want.(float64); ok {
return matchFloat64(got, want, 0)
case approximately:
return matchFloat64(got, want.value, ApproximatelyThreshold)
}
case string:
switch want := want.(type) {
case stringMatching:
return want.pattern.MatchString(got)
}
}
return vals.Equal(got, want)
Expand Down
59 changes: 59 additions & 0 deletions pkg/eval/evaltest/value_matchers.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package evaltest

import (
"math"
"regexp"
)

// ValueMatcher is a value that can be passed to [Case.Puts] and has its own
// matching semantics.
type ValueMatcher interface{ matchValue(any) bool }

// Anything matches anything. It is useful when the value contains information
// that is useful when the test fails.
var Anything ValueMatcher = anything{}

type anything struct{}

func (anything) matchValue(any) bool { return true }

// ApproximatelyThreshold defines the threshold for matching float64 values when
// using [Approximately].
const ApproximatelyThreshold = 1e-15

// Approximately matches a float64 within the threshold defined by
// [ApproximatelyThreshold].
func Approximately(f float64) ValueMatcher { return approximately{f} }

type approximately struct{ value float64 }

func (a approximately) matchValue(value any) bool {
if value, ok := value.(float64); ok {
return matchFloat64(a.value, value, ApproximatelyThreshold)
}
return false
}

func matchFloat64(a, b, threshold float64) bool {
if math.IsNaN(a) && math.IsNaN(b) {
return true
}
if math.IsInf(a, 0) && math.IsInf(b, 0) &&
math.Signbit(a) == math.Signbit(b) {
return true
}
return math.Abs(a-b) <= threshold
}

// [StringMatching] matches any string matching a regexp pattern. If the pattern
// is not a valid regexp, the function panics.
func StringMatching(p string) ValueMatcher { return stringMatching{regexp.MustCompile(p)} }

type stringMatching struct{ pattern *regexp.Regexp }

func (s stringMatching) matchValue(value any) bool {
if value, ok := value.(string); ok {
return s.pattern.MatchString(value)
}
return false
}

0 comments on commit 95ba078

Please sign in to comment.