1
0
mirror of https://github.com/IBM/fp-go.git synced 2025-12-17 23:37:41 +02:00
Files
fp-go/v2/context/readerresult/rec_test.go
Dr. Carsten Leue acb601fc01 fix: reuse some more code
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-12-15 16:30:40 +01:00

499 lines
14 KiB
Go

// Copyright (c) 2023 - 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 readerresult
import (
"context"
"errors"
"fmt"
"testing"
"time"
A "github.com/IBM/fp-go/v2/array"
E "github.com/IBM/fp-go/v2/either"
R "github.com/IBM/fp-go/v2/result"
"github.com/stretchr/testify/assert"
)
// TestTailRecFactorial tests factorial computation with context
func TestTailRecFactorial(t *testing.T) {
type State struct {
n int
acc int
}
factorialStep := func(state State) ReaderResult[E.Either[State, int]] {
return func(ctx context.Context) Result[E.Either[State, int]] {
if state.n <= 0 {
return R.Of(E.Right[State](state.acc))
}
return R.Of(E.Left[int](State{state.n - 1, state.acc * state.n}))
}
}
factorial := TailRec(factorialStep)
result := factorial(State{5, 1})(context.Background())
assert.Equal(t, R.Of(120), result)
}
// TestTailRecFibonacci tests Fibonacci computation
func TestTailRecFibonacci(t *testing.T) {
type State struct {
n int
prev int
curr int
}
fibStep := func(state State) ReaderResult[E.Either[State, int]] {
return func(ctx context.Context) Result[E.Either[State, int]] {
if state.n <= 0 {
return R.Of(E.Right[State](state.curr))
}
return R.Of(E.Left[int](State{state.n - 1, state.curr, state.prev + state.curr}))
}
}
fib := TailRec(fibStep)
result := fib(State{10, 0, 1})(context.Background())
assert.Equal(t, R.Of(89), result) // 10th Fibonacci number
}
// TestTailRecCountdown tests countdown computation
func TestTailRecCountdown(t *testing.T) {
countdownStep := func(n int) ReaderResult[E.Either[int, int]] {
return func(ctx context.Context) Result[E.Either[int, int]] {
if n <= 0 {
return R.Of(E.Right[int](n))
}
return R.Of(E.Left[int](n - 1))
}
}
countdown := TailRec(countdownStep)
result := countdown(10)(context.Background())
assert.Equal(t, R.Of(0), result)
}
// TestTailRecImmediateTermination tests immediate termination (Right on first call)
func TestTailRecImmediateTermination(t *testing.T) {
immediateStep := func(n int) ReaderResult[E.Either[int, int]] {
return func(ctx context.Context) Result[E.Either[int, int]] {
return R.Of(E.Right[int](n * 2))
}
}
immediate := TailRec(immediateStep)
result := immediate(42)(context.Background())
assert.Equal(t, R.Of(84), result)
}
// TestTailRecStackSafety tests that TailRec handles large iterations without stack overflow
func TestTailRecStackSafety(t *testing.T) {
countdownStep := func(n int) ReaderResult[E.Either[int, int]] {
return func(ctx context.Context) Result[E.Either[int, int]] {
if n <= 0 {
return R.Of(E.Right[int](n))
}
return R.Of(E.Left[int](n - 1))
}
}
countdown := TailRec(countdownStep)
result := countdown(10000)(context.Background())
assert.Equal(t, R.Of(0), result)
}
// TestTailRecSumList tests summing a list
func TestTailRecSumList(t *testing.T) {
type State struct {
list []int
sum int
}
sumStep := func(state State) ReaderResult[E.Either[State, int]] {
return func(ctx context.Context) Result[E.Either[State, int]] {
if A.IsEmpty(state.list) {
return R.Of(E.Right[State](state.sum))
}
return R.Of(E.Left[int](State{state.list[1:], state.sum + state.list[0]}))
}
}
sumList := TailRec(sumStep)
result := sumList(State{[]int{1, 2, 3, 4, 5}, 0})(context.Background())
assert.Equal(t, R.Of(15), result)
}
// TestTailRecCollatzConjecture tests the Collatz conjecture
func TestTailRecCollatzConjecture(t *testing.T) {
collatzStep := func(n int) ReaderResult[E.Either[int, int]] {
return func(ctx context.Context) Result[E.Either[int, int]] {
if n <= 1 {
return R.Of(E.Right[int](n))
}
if n%2 == 0 {
return R.Of(E.Left[int](n / 2))
}
return R.Of(E.Left[int](3*n + 1))
}
}
collatz := TailRec(collatzStep)
result := collatz(10)(context.Background())
assert.Equal(t, R.Of(1), result)
}
// TestTailRecGCD tests greatest common divisor
func TestTailRecGCD(t *testing.T) {
type State struct {
a int
b int
}
gcdStep := func(state State) ReaderResult[E.Either[State, int]] {
return func(ctx context.Context) Result[E.Either[State, int]] {
if state.b == 0 {
return R.Of(E.Right[State](state.a))
}
return R.Of(E.Left[int](State{state.b, state.a % state.b}))
}
}
gcd := TailRec(gcdStep)
result := gcd(State{48, 18})(context.Background())
assert.Equal(t, R.Of(6), result)
}
// TestTailRecErrorPropagation tests that errors are properly propagated
func TestTailRecErrorPropagation(t *testing.T) {
expectedErr := errors.New("computation error")
errorStep := func(n int) ReaderResult[E.Either[int, int]] {
return func(ctx context.Context) Result[E.Either[int, int]] {
if n == 5 {
return R.Left[E.Either[int, int]](expectedErr)
}
if n <= 0 {
return R.Of(E.Right[int](n))
}
return R.Of(E.Left[int](n - 1))
}
}
computation := TailRec(errorStep)
result := computation(10)(context.Background())
assert.True(t, R.IsLeft(result))
_, err := R.Unwrap(result)
assert.Equal(t, expectedErr, err)
}
// TestTailRecContextCancellationImmediate tests short circuit when context is already canceled
func TestTailRecContextCancellationImmediate(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
cancel() // Cancel immediately before execution
stepExecuted := false
countdownStep := func(n int) ReaderResult[E.Either[int, int]] {
return func(ctx context.Context) Result[E.Either[int, int]] {
stepExecuted = true
if n <= 0 {
return R.Of(E.Right[int](n))
}
return R.Of(E.Left[int](n - 1))
}
}
countdown := TailRec(countdownStep)
result := countdown(10)(ctx)
// Should short circuit without executing any steps
assert.False(t, stepExecuted, "Step should not be executed when context is already canceled")
assert.True(t, R.IsLeft(result))
_, err := R.Unwrap(result)
assert.Equal(t, context.Canceled, err)
}
// TestTailRecContextCancellationDuringExecution tests short circuit when context is canceled during execution
func TestTailRecContextCancellationDuringExecution(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
executionCount := 0
countdownStep := func(n int) ReaderResult[E.Either[int, int]] {
return func(ctx context.Context) Result[E.Either[int, int]] {
executionCount++
// Cancel after 3 iterations
if executionCount == 3 {
cancel()
}
if n <= 0 {
return R.Of(E.Right[int](n))
}
return R.Of(E.Left[int](n - 1))
}
}
countdown := TailRec(countdownStep)
result := countdown(100)(ctx)
// Should stop after cancellation
assert.True(t, R.IsLeft(result))
assert.LessOrEqual(t, executionCount, 4, "Should stop shortly after cancellation")
_, err := R.Unwrap(result)
assert.Equal(t, context.Canceled, err)
}
// TestTailRecContextWithTimeout tests behavior with timeout context
func TestTailRecContextWithTimeout(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond)
defer cancel()
executionCount := 0
slowStep := func(n int) ReaderResult[E.Either[int, int]] {
return func(ctx context.Context) Result[E.Either[int, int]] {
executionCount++
// Simulate slow computation
time.Sleep(20 * time.Millisecond)
if n <= 0 {
return R.Of(E.Right[int](n))
}
return R.Of(E.Left[int](n - 1))
}
}
computation := TailRec(slowStep)
result := computation(100)(ctx)
// Should timeout and return error
assert.True(t, R.IsLeft(result))
assert.Less(t, executionCount, 100, "Should not complete all iterations due to timeout")
_, err := R.Unwrap(result)
assert.Equal(t, context.DeadlineExceeded, err)
}
// TestTailRecContextWithCause tests that context.Cause is properly returned
func TestTailRecContextWithCause(t *testing.T) {
customErr := errors.New("custom cancellation reason")
ctx, cancel := context.WithCancelCause(context.Background())
cancel(customErr)
countdownStep := func(n int) ReaderResult[E.Either[int, int]] {
return func(ctx context.Context) Result[E.Either[int, int]] {
if n <= 0 {
return R.Of(E.Right[int](n))
}
return R.Of(E.Left[int](n - 1))
}
}
countdown := TailRec(countdownStep)
result := countdown(10)(ctx)
assert.True(t, R.IsLeft(result))
_, err := R.Unwrap(result)
assert.Equal(t, customErr, err)
}
// TestTailRecContextCancellationMultipleIterations tests that cancellation is checked on each iteration
func TestTailRecContextCancellationMultipleIterations(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
executionCount := 0
maxExecutions := 5
countdownStep := func(n int) ReaderResult[E.Either[int, int]] {
return func(ctx context.Context) Result[E.Either[int, int]] {
executionCount++
if executionCount == maxExecutions {
cancel()
}
if n <= 0 {
return R.Of(E.Right[int](n))
}
return R.Of(E.Left[int](n - 1))
}
}
countdown := TailRec(countdownStep)
result := countdown(1000)(ctx)
// Should detect cancellation on next iteration check
assert.True(t, R.IsLeft(result))
// Should stop within 1-2 iterations after cancellation
assert.LessOrEqual(t, executionCount, maxExecutions+2)
_, err := R.Unwrap(result)
assert.Equal(t, context.Canceled, err)
}
// TestTailRecContextNotCanceled tests normal execution when context is not canceled
func TestTailRecContextNotCanceled(t *testing.T) {
ctx := context.Background()
executionCount := 0
countdownStep := func(n int) ReaderResult[E.Either[int, int]] {
return func(ctx context.Context) Result[E.Either[int, int]] {
executionCount++
if n <= 0 {
return R.Of(E.Right[int](n))
}
return R.Of(E.Left[int](n - 1))
}
}
countdown := TailRec(countdownStep)
result := countdown(10)(ctx)
assert.Equal(t, 11, executionCount) // 10, 9, 8, ..., 1, 0
assert.Equal(t, R.Of(0), result)
}
// TestTailRecPowerOfTwo tests computing power of 2
func TestTailRecPowerOfTwo(t *testing.T) {
type State struct {
exponent int
result int
target int
}
powerStep := func(state State) ReaderResult[E.Either[State, int]] {
return func(ctx context.Context) Result[E.Either[State, int]] {
if state.exponent >= state.target {
return R.Of(E.Right[State](state.result))
}
return R.Of(E.Left[int](State{state.exponent + 1, state.result * 2, state.target}))
}
}
power := TailRec(powerStep)
result := power(State{0, 1, 10})(context.Background())
assert.Equal(t, R.Of(1024), result) // 2^10
}
// TestTailRecFindInRange tests finding a value in a range
func TestTailRecFindInRange(t *testing.T) {
type State struct {
current int
max int
target int
}
findStep := func(state State) ReaderResult[E.Either[State, int]] {
return func(ctx context.Context) Result[E.Either[State, int]] {
if state.current >= state.max {
return R.Of(E.Right[State](-1)) // Not found
}
if state.current == state.target {
return R.Of(E.Right[State](state.current)) // Found
}
return R.Of(E.Left[int](State{state.current + 1, state.max, state.target}))
}
}
find := TailRec(findStep)
result := find(State{0, 100, 42})(context.Background())
assert.Equal(t, R.Of(42), result)
}
// TestTailRecFindNotInRange tests finding a value not in range
func TestTailRecFindNotInRange(t *testing.T) {
type State struct {
current int
max int
target int
}
findStep := func(state State) ReaderResult[E.Either[State, int]] {
return func(ctx context.Context) Result[E.Either[State, int]] {
if state.current >= state.max {
return R.Of(E.Right[State](-1)) // Not found
}
if state.current == state.target {
return R.Of(E.Right[State](state.current)) // Found
}
return R.Of(E.Left[int](State{state.current + 1, state.max, state.target}))
}
}
find := TailRec(findStep)
result := find(State{0, 100, 200})(context.Background())
assert.Equal(t, R.Of(-1), result)
}
// TestTailRecWithContextValue tests that context values are accessible
func TestTailRecWithContextValue(t *testing.T) {
type contextKey string
const multiplierKey contextKey = "multiplier"
ctx := context.WithValue(context.Background(), multiplierKey, 3)
countdownStep := func(n int) ReaderResult[E.Either[int, int]] {
return func(ctx context.Context) Result[E.Either[int, int]] {
if n <= 0 {
multiplier := ctx.Value(multiplierKey).(int)
return R.Of(E.Right[int](n * multiplier))
}
return R.Of(E.Left[int](n - 1))
}
}
countdown := TailRec(countdownStep)
result := countdown(5)(ctx)
assert.Equal(t, R.Of(0), result) // 0 * 3 = 0
}
// TestTailRecComplexState tests with complex state structure
func TestTailRecComplexState(t *testing.T) {
type ComplexState struct {
counter int
sum int
product int
completed bool
}
complexStep := func(state ComplexState) ReaderResult[E.Either[ComplexState, string]] {
return func(ctx context.Context) Result[E.Either[ComplexState, string]] {
if state.counter <= 0 || state.completed {
result := fmt.Sprintf("sum=%d, product=%d", state.sum, state.product)
return R.Of(E.Right[ComplexState](result))
}
newState := ComplexState{
counter: state.counter - 1,
sum: state.sum + state.counter,
product: state.product * state.counter,
completed: state.counter == 1,
}
return R.Of(E.Left[string](newState))
}
}
computation := TailRec(complexStep)
result := computation(ComplexState{5, 0, 1, false})(context.Background())
assert.Equal(t, R.Of("sum=15, product=120"), result)
}