1
0
mirror of https://github.com/IBM/fp-go.git synced 2025-11-23 22:14:53 +02:00
Files
fp-go/v2/predicate/predicate_test.go
Dr. Carsten Leue fd0550e71b fix: better test coverage
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-11-12 10:35:53 +01:00

681 lines
20 KiB
Go

// Copyright (c) 2025 IBM Corp.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package predicate
import (
"testing"
"github.com/IBM/fp-go/v2/eq"
F "github.com/IBM/fp-go/v2/function"
"github.com/stretchr/testify/assert"
)
// Test predicates for reuse
var (
isPositive = func(n int) bool { return n > 0 }
isEven = func(n int) bool { return n%2 == 0 }
isNegative = func(n int) bool { return n < 0 }
isGreaterThan10 = func(n int) bool { return n > 10 }
)
// TestNot tests the Not function
func TestNot(t *testing.T) {
t.Run("negates true to false", func(t *testing.T) {
notPositive := Not(isPositive)
assert.False(t, notPositive(5))
assert.False(t, notPositive(1))
})
t.Run("negates false to true", func(t *testing.T) {
notPositive := Not(isPositive)
assert.True(t, notPositive(-5))
assert.True(t, notPositive(0))
})
t.Run("double negation returns original", func(t *testing.T) {
doubleNegated := Not(Not(isPositive))
assert.True(t, doubleNegated(5))
assert.False(t, doubleNegated(-5))
})
}
// TestAnd tests the And function
func TestAnd(t *testing.T) {
t.Run("returns true when both predicates are true", func(t *testing.T) {
isPositiveAndEven := F.Pipe1(isPositive, And(isEven))
assert.True(t, isPositiveAndEven(2))
assert.True(t, isPositiveAndEven(4))
assert.True(t, isPositiveAndEven(100))
})
t.Run("returns false when first predicate is false", func(t *testing.T) {
isPositiveAndEven := F.Pipe1(isPositive, And(isEven))
assert.False(t, isPositiveAndEven(-2))
assert.False(t, isPositiveAndEven(-4))
})
t.Run("returns false when second predicate is false", func(t *testing.T) {
isPositiveAndEven := F.Pipe1(isPositive, And(isEven))
assert.False(t, isPositiveAndEven(1))
assert.False(t, isPositiveAndEven(3))
assert.False(t, isPositiveAndEven(5))
})
t.Run("returns false when both predicates are false", func(t *testing.T) {
isPositiveAndEven := F.Pipe1(isPositive, And(isEven))
assert.False(t, isPositiveAndEven(-1))
assert.False(t, isPositiveAndEven(-3))
})
t.Run("chains multiple And operations", func(t *testing.T) {
isPositiveEvenAndGreaterThan10 := F.Pipe2(
isPositive,
And(isEven),
And(isGreaterThan10),
)
assert.True(t, isPositiveEvenAndGreaterThan10(12))
assert.False(t, isPositiveEvenAndGreaterThan10(8))
assert.False(t, isPositiveEvenAndGreaterThan10(11))
})
}
// TestOr tests the Or function
func TestOr(t *testing.T) {
t.Run("returns true when first predicate is true", func(t *testing.T) {
isPositiveOrEven := F.Pipe1(isPositive, Or(isEven))
assert.True(t, isPositiveOrEven(1))
assert.True(t, isPositiveOrEven(3))
assert.True(t, isPositiveOrEven(5))
})
t.Run("returns true when second predicate is true", func(t *testing.T) {
isPositiveOrEven := F.Pipe1(isPositive, Or(isEven))
assert.True(t, isPositiveOrEven(-2))
assert.True(t, isPositiveOrEven(-4))
assert.True(t, isPositiveOrEven(0))
})
t.Run("returns true when both predicates are true", func(t *testing.T) {
isPositiveOrEven := F.Pipe1(isPositive, Or(isEven))
assert.True(t, isPositiveOrEven(2))
assert.True(t, isPositiveOrEven(4))
assert.True(t, isPositiveOrEven(100))
})
t.Run("returns false when both predicates are false", func(t *testing.T) {
isPositiveOrEven := F.Pipe1(isPositive, Or(isEven))
assert.False(t, isPositiveOrEven(-1))
assert.False(t, isPositiveOrEven(-3))
assert.False(t, isPositiveOrEven(-5))
})
t.Run("chains multiple Or operations", func(t *testing.T) {
isPositiveOrEvenOrNegative := F.Pipe2(
isPositive,
Or(isEven),
Or(isNegative),
)
assert.True(t, isPositiveOrEvenOrNegative(5)) // positive
assert.True(t, isPositiveOrEvenOrNegative(2)) // even
assert.True(t, isPositiveOrEvenOrNegative(-3)) // negative
assert.True(t, isPositiveOrEvenOrNegative(0)) // even
})
}
// TestContraMap tests the ContraMap function
func TestContraMap(t *testing.T) {
type Person struct {
Name string
Age int
}
t.Run("transforms predicate to work with different type", func(t *testing.T) {
isAdult := func(age int) bool { return age >= 18 }
getAge := func(p Person) int { return p.Age }
isPersonAdult := F.Pipe1(isAdult, ContraMap(getAge))
assert.True(t, isPersonAdult(Person{Name: "Alice", Age: 25}))
assert.True(t, isPersonAdult(Person{Name: "Bob", Age: 18}))
assert.False(t, isPersonAdult(Person{Name: "Charlie", Age: 15}))
})
t.Run("works with string length", func(t *testing.T) {
isLongEnough := func(n int) bool { return n >= 5 }
getLength := func(s string) int { return len(s) }
isStringLongEnough := F.Pipe1(isLongEnough, ContraMap(getLength))
assert.True(t, isStringLongEnough("hello"))
assert.True(t, isStringLongEnough("world!"))
assert.False(t, isStringLongEnough("hi"))
assert.False(t, isStringLongEnough(""))
})
t.Run("composes with other operations", func(t *testing.T) {
type Product struct {
Name string
Price int
}
isExpensive := func(price int) bool { return price > 100 }
isCheap := func(price int) bool { return price < 50 }
getPrice := func(p Product) int { return p.Price }
isExpensiveProduct := F.Pipe1(isExpensive, ContraMap(getPrice))
isCheapProduct := F.Pipe1(isCheap, ContraMap(getPrice))
isExtremePrice := F.Pipe1(isExpensiveProduct, Or(isCheapProduct))
assert.True(t, isExtremePrice(Product{Name: "Luxury", Price: 200}))
assert.True(t, isExtremePrice(Product{Name: "Budget", Price: 30}))
assert.False(t, isExtremePrice(Product{Name: "Mid-range", Price: 75}))
})
}
// TestSemigroupAny tests the SemigroupAny function
func TestSemigroupAny(t *testing.T) {
s := SemigroupAny[int]()
t.Run("combines predicates with OR logic", func(t *testing.T) {
combined := s.Concat(isPositive, isEven)
assert.True(t, combined(4)) // both true
assert.True(t, combined(3)) // first true
assert.True(t, combined(-2)) // second true
assert.False(t, combined(-3)) // both false
})
t.Run("is associative", func(t *testing.T) {
// (a OR b) OR c == a OR (b OR c)
left := s.Concat(s.Concat(isPositive, isEven), isNegative)
right := s.Concat(isPositive, s.Concat(isEven, isNegative))
testValues := []int{-5, -2, 0, 1, 2, 5}
for _, v := range testValues {
assert.Equal(t, left(v), right(v), "associativity failed for value %d", v)
}
})
t.Run("combines multiple predicates", func(t *testing.T) {
combined := s.Concat(s.Concat(isPositive, isEven), isGreaterThan10)
assert.True(t, combined(15)) // positive and > 10
assert.True(t, combined(2)) // even
assert.True(t, combined(1)) // positive
assert.False(t, combined(-3)) // none
})
}
// TestSemigroupAll tests the SemigroupAll function
func TestSemigroupAll(t *testing.T) {
s := SemigroupAll[int]()
t.Run("combines predicates with AND logic", func(t *testing.T) {
combined := s.Concat(isPositive, isEven)
assert.True(t, combined(4)) // both true
assert.False(t, combined(3)) // first true only
assert.False(t, combined(-2)) // second true only
assert.False(t, combined(-3)) // both false
})
t.Run("is associative", func(t *testing.T) {
// (a AND b) AND c == a AND (b AND c)
isLessThan100 := func(n int) bool { return n < 100 }
left := s.Concat(s.Concat(isPositive, isEven), isLessThan100)
right := s.Concat(isPositive, s.Concat(isEven, isLessThan100))
testValues := []int{-5, -2, 0, 1, 2, 50, 150}
for _, v := range testValues {
assert.Equal(t, left(v), right(v), "associativity failed for value %d", v)
}
})
t.Run("combines multiple predicates", func(t *testing.T) {
combined := s.Concat(s.Concat(isPositive, isEven), isGreaterThan10)
assert.True(t, combined(12)) // all true
assert.False(t, combined(8)) // not > 10
assert.False(t, combined(11)) // not even
assert.False(t, combined(-2)) // not positive
})
}
// TestMonoidAny tests the MonoidAny function
func TestMonoidAny(t *testing.T) {
m := MonoidAny[int]()
t.Run("has identity element that returns false", func(t *testing.T) {
empty := m.Empty()
assert.False(t, empty(0))
assert.False(t, empty(5))
assert.False(t, empty(-5))
})
t.Run("identity is left identity", func(t *testing.T) {
// empty OR p == p
combined := m.Concat(m.Empty(), isPositive)
assert.True(t, combined(5))
assert.False(t, combined(-5))
})
t.Run("identity is right identity", func(t *testing.T) {
// p OR empty == p
combined := m.Concat(isPositive, m.Empty())
assert.True(t, combined(5))
assert.False(t, combined(-5))
})
t.Run("reduces empty list to identity", func(t *testing.T) {
predicates := []Predicate[int]{}
result := m.Empty()
for _, p := range predicates {
result = m.Concat(result, p)
}
assert.False(t, result(5))
})
t.Run("reduces list of predicates", func(t *testing.T) {
predicates := []Predicate[int]{isPositive, isEven, isGreaterThan10}
result := m.Empty()
for _, p := range predicates {
result = m.Concat(result, p)
}
assert.True(t, result(15)) // positive
assert.True(t, result(2)) // even
assert.True(t, result(11)) // > 10
assert.False(t, result(-3)) // none
})
}
// TestMonoidAll tests the MonoidAll function
func TestMonoidAll(t *testing.T) {
m := MonoidAll[int]()
t.Run("has identity element that returns true", func(t *testing.T) {
empty := m.Empty()
assert.True(t, empty(0))
assert.True(t, empty(5))
assert.True(t, empty(-5))
})
t.Run("identity is left identity", func(t *testing.T) {
// empty AND p == p
combined := m.Concat(m.Empty(), isPositive)
assert.True(t, combined(5))
assert.False(t, combined(-5))
})
t.Run("identity is right identity", func(t *testing.T) {
// p AND empty == p
combined := m.Concat(isPositive, m.Empty())
assert.True(t, combined(5))
assert.False(t, combined(-5))
})
t.Run("reduces empty list to identity", func(t *testing.T) {
predicates := []Predicate[int]{}
result := m.Empty()
for _, p := range predicates {
result = m.Concat(result, p)
}
assert.True(t, result(5))
})
t.Run("reduces list of predicates", func(t *testing.T) {
isLessThan100 := func(n int) bool { return n < 100 }
predicates := []Predicate[int]{isPositive, isEven, isLessThan100}
result := m.Empty()
for _, p := range predicates {
result = m.Concat(result, p)
}
assert.True(t, result(50)) // all true
assert.False(t, result(51)) // not even
assert.False(t, result(-2)) // not positive
assert.False(t, result(150)) // not < 100
})
}
// TestComplexScenarios tests complex combinations of predicates
func TestComplexScenarios(t *testing.T) {
t.Run("complex boolean logic", func(t *testing.T) {
// (positive AND even) OR (negative AND odd)
positiveAndEven := F.Pipe1(isPositive, And(isEven))
isOdd := Not(isEven)
negativeAndOdd := F.Pipe1(isNegative, And(isOdd))
complex := F.Pipe1(positiveAndEven, Or(negativeAndOdd))
assert.True(t, complex(2)) // positive and even
assert.True(t, complex(4)) // positive and even
assert.True(t, complex(-1)) // negative and odd
assert.True(t, complex(-3)) // negative and odd
assert.False(t, complex(1)) // positive but odd
assert.False(t, complex(-2)) // negative but even
assert.False(t, complex(0)) // neither
})
t.Run("contramap with complex predicates", func(t *testing.T) {
type User struct {
Name string
Age int
Score int
}
isAdultAge := func(age int) bool { return age >= 18 }
hasHighScore := func(score int) bool { return score >= 80 }
getAge := func(u User) int { return u.Age }
getScore := func(u User) int { return u.Score }
isAdult := F.Pipe1(isAdultAge, ContraMap(getAge))
hasGoodScore := F.Pipe1(hasHighScore, ContraMap(getScore))
isQualified := F.Pipe1(isAdult, And(hasGoodScore))
assert.True(t, isQualified(User{Name: "Alice", Age: 25, Score: 90}))
assert.False(t, isQualified(User{Name: "Bob", Age: 16, Score: 90}))
assert.False(t, isQualified(User{Name: "Charlie", Age: 25, Score: 70}))
assert.False(t, isQualified(User{Name: "Dave", Age: 16, Score: 70}))
})
t.Run("monoid with contramap", func(t *testing.T) {
type Item struct {
Price int
Stock int
}
m := MonoidAll[Item]()
isAffordable := func(price int) bool { return price < 100 }
isInStock := func(stock int) bool { return stock > 0 }
getPrice := func(i Item) int { return i.Price }
getStock := func(i Item) int { return i.Stock }
isAffordableItem := F.Pipe1(isAffordable, ContraMap(getPrice))
isInStockItem := F.Pipe1(isInStock, ContraMap(getStock))
canBuy := m.Concat(isAffordableItem, isInStockItem)
assert.True(t, canBuy(Item{Price: 50, Stock: 10}))
assert.False(t, canBuy(Item{Price: 150, Stock: 10}))
assert.False(t, canBuy(Item{Price: 50, Stock: 0}))
assert.False(t, canBuy(Item{Price: 150, Stock: 0}))
})
}
// TestIsEqual tests the IsEqual function
func TestIsEqual(t *testing.T) {
t.Run("works with custom equality", func(t *testing.T) {
type Person struct {
Name string
Age int
}
// Custom equality that only compares names
nameEq := eq.FromEquals(func(a, b Person) bool {
return a.Name == b.Name
})
isEqualToPerson := IsEqual(nameEq)
alice := Person{Name: "Alice", Age: 30}
isAlice := isEqualToPerson(alice)
assert.True(t, isAlice(Person{Name: "Alice", Age: 30}))
assert.True(t, isAlice(Person{Name: "Alice", Age: 25})) // Different age, same name
assert.False(t, isAlice(Person{Name: "Bob", Age: 30}))
})
t.Run("works with struct equality", func(t *testing.T) {
type Point struct {
X, Y int
}
pointEq := eq.FromStrictEquals[Point]()
isEqualToPoint := IsEqual(pointEq)
origin := Point{X: 0, Y: 0}
isOrigin := isEqualToPoint(origin)
assert.True(t, isOrigin(Point{X: 0, Y: 0}))
assert.False(t, isOrigin(Point{X: 1, Y: 0}))
assert.False(t, isOrigin(Point{X: 0, Y: 1}))
})
t.Run("can be used with And/Or", func(t *testing.T) {
intEq := eq.FromStrictEquals[int]()
isEqualTo5 := IsEqual(intEq)(5)
isEqualTo10 := IsEqual(intEq)(10)
is5Or10 := F.Pipe1(isEqualTo5, Or(isEqualTo10))
assert.True(t, is5Or10(5))
assert.True(t, is5Or10(10))
assert.False(t, is5Or10(7))
})
}
// TestIsStrictEqual tests the IsStrictEqual function
func TestIsStrictEqual(t *testing.T) {
t.Run("works with integers", func(t *testing.T) {
isEqualTo42 := IsStrictEqual[int]()(42)
assert.True(t, isEqualTo42(42))
assert.False(t, isEqualTo42(0))
assert.False(t, isEqualTo42(-42))
})
t.Run("works with strings", func(t *testing.T) {
isEqualToHello := IsStrictEqual[string]()("hello")
assert.True(t, isEqualToHello("hello"))
assert.False(t, isEqualToHello("Hello"))
assert.False(t, isEqualToHello("world"))
assert.False(t, isEqualToHello(""))
})
t.Run("works with booleans", func(t *testing.T) {
isEqualToTrue := IsStrictEqual[bool]()(true)
assert.True(t, isEqualToTrue(true))
assert.False(t, isEqualToTrue(false))
isEqualToFalse := IsStrictEqual[bool]()(false)
assert.True(t, isEqualToFalse(false))
assert.False(t, isEqualToFalse(true))
})
t.Run("works with floats", func(t *testing.T) {
isEqualTo3Point14 := IsStrictEqual[float64]()(3.14)
assert.True(t, isEqualTo3Point14(3.14))
assert.False(t, isEqualTo3Point14(3.15))
assert.False(t, isEqualTo3Point14(0.0))
})
t.Run("can be combined with other predicates", func(t *testing.T) {
isEqualTo5 := IsStrictEqual[int]()(5)
isNotEqualTo5 := Not(isEqualTo5)
assert.False(t, isNotEqualTo5(5))
assert.True(t, isNotEqualTo5(10))
assert.True(t, isNotEqualTo5(0))
})
}
// TestIsZero tests the IsZero function
func TestIsZero(t *testing.T) {
t.Run("works with integers", func(t *testing.T) {
isZeroInt := IsZero[int]()
assert.True(t, isZeroInt(0))
assert.False(t, isZeroInt(1))
assert.False(t, isZeroInt(-1))
assert.False(t, isZeroInt(100))
})
t.Run("works with strings", func(t *testing.T) {
isZeroString := IsZero[string]()
assert.True(t, isZeroString(""))
assert.False(t, isZeroString("hello"))
assert.False(t, isZeroString(" "))
assert.False(t, isZeroString("0"))
})
t.Run("works with booleans", func(t *testing.T) {
isZeroBool := IsZero[bool]()
assert.True(t, isZeroBool(false))
assert.False(t, isZeroBool(true))
})
t.Run("works with floats", func(t *testing.T) {
isZeroFloat := IsZero[float64]()
assert.True(t, isZeroFloat(0.0))
assert.False(t, isZeroFloat(0.1))
assert.False(t, isZeroFloat(-0.1))
})
t.Run("works with pointers", func(t *testing.T) {
isZeroPtr := IsZero[*int]()
assert.True(t, isZeroPtr(nil))
x := 42
assert.False(t, isZeroPtr(&x))
})
t.Run("works with structs", func(t *testing.T) {
type Point struct {
X, Y int
}
isZeroPoint := IsZero[Point]()
assert.True(t, isZeroPoint(Point{X: 0, Y: 0}))
assert.False(t, isZeroPoint(Point{X: 1, Y: 0}))
assert.False(t, isZeroPoint(Point{X: 0, Y: 1}))
})
t.Run("can be combined with other predicates", func(t *testing.T) {
isZeroInt := IsZero[int]()
isPositiveOrZero := F.Pipe1(isPositive, Or(isZeroInt))
assert.True(t, isPositiveOrZero(5))
assert.True(t, isPositiveOrZero(0))
assert.False(t, isPositiveOrZero(-5))
})
}
// TestIsNonZero tests the IsNonZero function
func TestIsNonZero(t *testing.T) {
t.Run("works with integers", func(t *testing.T) {
isNonZeroInt := IsNonZero[int]()
assert.False(t, isNonZeroInt(0))
assert.True(t, isNonZeroInt(1))
assert.True(t, isNonZeroInt(-1))
assert.True(t, isNonZeroInt(100))
})
t.Run("works with strings", func(t *testing.T) {
isNonZeroString := IsNonZero[string]()
assert.False(t, isNonZeroString(""))
assert.True(t, isNonZeroString("hello"))
assert.True(t, isNonZeroString(" "))
assert.True(t, isNonZeroString("0"))
})
t.Run("works with booleans", func(t *testing.T) {
isNonZeroBool := IsNonZero[bool]()
assert.False(t, isNonZeroBool(false))
assert.True(t, isNonZeroBool(true))
})
t.Run("works with floats", func(t *testing.T) {
isNonZeroFloat := IsNonZero[float64]()
assert.False(t, isNonZeroFloat(0.0))
assert.True(t, isNonZeroFloat(0.1))
assert.True(t, isNonZeroFloat(-0.1))
})
t.Run("works with pointers", func(t *testing.T) {
isNonZeroPtr := IsNonZero[*int]()
assert.False(t, isNonZeroPtr(nil))
x := 42
assert.True(t, isNonZeroPtr(&x))
y := 0
assert.True(t, isNonZeroPtr(&y)) // Pointer itself is non-nil
})
t.Run("is opposite of IsZero", func(t *testing.T) {
isZeroInt := IsZero[int]()
isNonZeroInt := IsNonZero[int]()
testValues := []int{-100, -1, 0, 1, 100}
for _, v := range testValues {
assert.Equal(t, !isZeroInt(v), isNonZeroInt(v), "IsNonZero should be opposite of IsZero for value %d", v)
}
})
t.Run("can be combined with other predicates", func(t *testing.T) {
isNonZeroInt := IsNonZero[int]()
isNonZeroAndPositive := F.Pipe1(isNonZeroInt, And(isPositive))
assert.True(t, isNonZeroAndPositive(5))
assert.False(t, isNonZeroAndPositive(0))
assert.False(t, isNonZeroAndPositive(-5))
})
}
// TestPredicatesIntegration tests integration of predicates.go functions with other predicate operations
func TestPredicatesIntegration(t *testing.T) {
t.Run("filter with IsZero", func(t *testing.T) {
numbers := []int{0, 1, 0, 2, 0, 3}
isZeroInt := IsZero[int]()
var nonZeros []int
for _, n := range numbers {
if !isZeroInt(n) {
nonZeros = append(nonZeros, n)
}
}
assert.Equal(t, []int{1, 2, 3}, nonZeros)
})
t.Run("validation with IsNonZero", func(t *testing.T) {
type Config struct {
Host string
Port int
}
isNonZeroString := IsNonZero[string]()
isNonZeroInt := IsNonZero[int]()
getHost := func(c Config) string { return c.Host }
getPort := func(c Config) int { return c.Port }
hasHost := F.Pipe1(isNonZeroString, ContraMap(getHost))
hasPort := F.Pipe1(isNonZeroInt, ContraMap(getPort))
isValid := F.Pipe1(hasHost, And(hasPort))
assert.True(t, isValid(Config{Host: "localhost", Port: 8080}))
assert.False(t, isValid(Config{Host: "", Port: 8080}))
assert.False(t, isValid(Config{Host: "localhost", Port: 0}))
assert.False(t, isValid(Config{Host: "", Port: 0}))
})
t.Run("equality with monoid", func(t *testing.T) {
m := MonoidAny[int]()
isEqualTo1 := IsStrictEqual[int]()(1)
isEqualTo2 := IsStrictEqual[int]()(2)
isEqualTo3 := IsStrictEqual[int]()(3)
is1Or2Or3 := m.Concat(m.Concat(isEqualTo1, isEqualTo2), isEqualTo3)
assert.True(t, is1Or2Or3(1))
assert.True(t, is1Or2Or3(2))
assert.True(t, is1Or2Or3(3))
assert.False(t, is1Or2Or3(4))
assert.False(t, is1Or2Or3(0))
})
}