mirror of
https://github.com/IBM/fp-go.git
synced 2025-12-17 23:37:41 +02:00
499 lines
14 KiB
Go
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)
|
|
}
|