mirror of
https://github.com/IBM/fp-go.git
synced 2025-12-09 23:11:40 +02:00
355 lines
9.8 KiB
Go
355 lines
9.8 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 file
|
|
|
|
import (
|
|
"errors"
|
|
"io"
|
|
"os"
|
|
"strings"
|
|
"testing"
|
|
|
|
E "github.com/IBM/fp-go/v2/either"
|
|
F "github.com/IBM/fp-go/v2/function"
|
|
"github.com/IBM/fp-go/v2/ioeither"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
// mockReadCloser is a mock implementation of io.ReadCloser for testing
|
|
type mockReadCloser struct {
|
|
data []byte
|
|
readErr error
|
|
closeErr error
|
|
readPos int
|
|
closeCalled bool
|
|
}
|
|
|
|
func (m *mockReadCloser) Read(p []byte) (n int, err error) {
|
|
if m.readErr != nil {
|
|
return 0, m.readErr
|
|
}
|
|
if m.readPos >= len(m.data) {
|
|
return 0, io.EOF
|
|
}
|
|
n = copy(p, m.data[m.readPos:])
|
|
m.readPos += n
|
|
if m.readPos >= len(m.data) {
|
|
return n, io.EOF
|
|
}
|
|
return n, nil
|
|
}
|
|
|
|
func (m *mockReadCloser) Close() error {
|
|
m.closeCalled = true
|
|
return m.closeErr
|
|
}
|
|
|
|
// TestReadSuccessfulRead tests reading data successfully from a ReadCloser
|
|
func TestReadSuccessfulRead(t *testing.T) {
|
|
testData := []byte("Hello, World!")
|
|
mock := &mockReadCloser{data: testData}
|
|
|
|
// Create an acquire function that returns our mock
|
|
acquire := ioeither.Of[error](mock)
|
|
|
|
// Create a reader function that reads all data
|
|
reader := func(rc *mockReadCloser) IOEither[error, []byte] {
|
|
return ioeither.TryCatchError(func() ([]byte, error) {
|
|
return io.ReadAll(rc)
|
|
})
|
|
}
|
|
|
|
// Execute the Read operation
|
|
result := Read[[]byte](acquire)(reader)
|
|
either := result()
|
|
|
|
// Assertions
|
|
assert.True(t, E.IsRight(either))
|
|
data := E.GetOrElse(func(error) []byte { return nil })(either)
|
|
assert.Equal(t, testData, data)
|
|
assert.True(t, mock.closeCalled, "Close should have been called")
|
|
}
|
|
|
|
// TestReadWithRealFile tests reading from an actual file
|
|
func TestReadWithRealFile(t *testing.T) {
|
|
// Create a temporary file
|
|
tmpFile, err := os.CreateTemp("", "read_test_*.txt")
|
|
require.NoError(t, err)
|
|
defer os.Remove(tmpFile.Name())
|
|
|
|
testContent := []byte("Test file content for Read function")
|
|
_, err = tmpFile.Write(testContent)
|
|
require.NoError(t, err)
|
|
tmpFile.Close()
|
|
|
|
// Use Read to read the file
|
|
reader := func(f *os.File) IOEither[error, []byte] {
|
|
return ioeither.TryCatchError(func() ([]byte, error) {
|
|
return io.ReadAll(f)
|
|
})
|
|
}
|
|
|
|
result := Read[[]byte](Open(tmpFile.Name()))(reader)
|
|
either := result()
|
|
|
|
assert.True(t, E.IsRight(either))
|
|
data := E.GetOrElse(func(error) []byte { return nil })(either)
|
|
assert.Equal(t, testContent, data)
|
|
}
|
|
|
|
// TestReadPartialRead tests reading only part of the data
|
|
func TestReadPartialRead(t *testing.T) {
|
|
testData := []byte("Hello, World! This is a longer message.")
|
|
mock := &mockReadCloser{data: testData}
|
|
|
|
acquire := ioeither.Of[error](mock)
|
|
|
|
// Reader that only reads first 13 bytes
|
|
reader := func(rc *mockReadCloser) IOEither[error, []byte] {
|
|
return ioeither.TryCatchError(func() ([]byte, error) {
|
|
buf := make([]byte, 13)
|
|
n, err := rc.Read(buf)
|
|
if err != nil && err != io.EOF {
|
|
return nil, err
|
|
}
|
|
return buf[:n], nil
|
|
})
|
|
}
|
|
|
|
result := Read[[]byte](acquire)(reader)
|
|
either := result()
|
|
|
|
assert.True(t, E.IsRight(either))
|
|
data := E.GetOrElse(func(error) []byte { return nil })(either)
|
|
assert.Equal(t, []byte("Hello, World!"), data)
|
|
assert.True(t, mock.closeCalled, "Close should have been called")
|
|
}
|
|
|
|
// TestReadErrorDuringRead tests that errors during reading are propagated
|
|
func TestReadErrorDuringRead(t *testing.T) {
|
|
readError := errors.New("read error")
|
|
mock := &mockReadCloser{
|
|
data: []byte("data"),
|
|
readErr: readError,
|
|
}
|
|
|
|
acquire := ioeither.Of[error](mock)
|
|
|
|
reader := func(rc *mockReadCloser) IOEither[error, []byte] {
|
|
return ioeither.TryCatchError(func() ([]byte, error) {
|
|
return io.ReadAll(rc)
|
|
})
|
|
}
|
|
|
|
result := Read[[]byte](acquire)(reader)
|
|
either := result()
|
|
|
|
assert.True(t, E.IsLeft(either))
|
|
err := E.Fold(func(e error) error { return e }, func([]byte) error { return nil })(either)
|
|
assert.Equal(t, readError, err)
|
|
assert.True(t, mock.closeCalled, "Close should be called even on read error")
|
|
}
|
|
|
|
// TestReadErrorDuringClose tests that errors during close are handled
|
|
func TestReadErrorDuringClose(t *testing.T) {
|
|
closeError := errors.New("close error")
|
|
mock := &mockReadCloser{
|
|
data: []byte("Hello"),
|
|
closeErr: closeError,
|
|
}
|
|
|
|
acquire := ioeither.Of[error](mock)
|
|
|
|
reader := func(rc *mockReadCloser) IOEither[error, []byte] {
|
|
return ioeither.TryCatchError(func() ([]byte, error) {
|
|
return io.ReadAll(rc)
|
|
})
|
|
}
|
|
|
|
result := Read[[]byte](acquire)(reader)
|
|
either := result()
|
|
|
|
// The close error should be propagated
|
|
assert.True(t, E.IsLeft(either))
|
|
assert.True(t, mock.closeCalled, "Close should have been called")
|
|
}
|
|
|
|
// TestReadErrorDuringAcquire tests that errors during resource acquisition are propagated
|
|
func TestReadErrorDuringAcquire(t *testing.T) {
|
|
acquireError := errors.New("acquire error")
|
|
acquire := ioeither.Left[*mockReadCloser](acquireError)
|
|
|
|
reader := func(rc *mockReadCloser) IOEither[error, []byte] {
|
|
return ioeither.TryCatchError(func() ([]byte, error) {
|
|
return io.ReadAll(rc)
|
|
})
|
|
}
|
|
|
|
result := Read[[]byte](acquire)(reader)
|
|
either := result()
|
|
|
|
assert.True(t, E.IsLeft(either))
|
|
err := E.Fold(func(e error) error { return e }, func([]byte) error { return nil })(either)
|
|
assert.Equal(t, acquireError, err)
|
|
}
|
|
|
|
// TestReadWithStringReader tests reading and transforming to a different type
|
|
func TestReadWithStringReader(t *testing.T) {
|
|
testData := []byte("Hello, World!")
|
|
mock := &mockReadCloser{data: testData}
|
|
|
|
acquire := ioeither.Of[error](mock)
|
|
|
|
// Reader that converts bytes to uppercase string
|
|
reader := func(rc *mockReadCloser) IOEither[error, string] {
|
|
return ioeither.TryCatchError(func() (string, error) {
|
|
data, err := io.ReadAll(rc)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return strings.ToUpper(string(data)), nil
|
|
})
|
|
}
|
|
|
|
result := Read[string](acquire)(reader)
|
|
either := result()
|
|
|
|
assert.True(t, E.IsRight(either))
|
|
str := E.GetOrElse(func(error) string { return "" })(either)
|
|
assert.Equal(t, "HELLO, WORLD!", str)
|
|
assert.True(t, mock.closeCalled, "Close should have been called")
|
|
}
|
|
|
|
// TestReadComposition tests composing Read with other operations
|
|
func TestReadComposition(t *testing.T) {
|
|
testData := []byte("42")
|
|
mock := &mockReadCloser{data: testData}
|
|
|
|
acquire := ioeither.Of[error](mock)
|
|
|
|
// Reader that parses the content as an integer
|
|
reader := func(rc *mockReadCloser) IOEither[error, int] {
|
|
return ioeither.TryCatchError(func() (int, error) {
|
|
data, err := io.ReadAll(rc)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
var num int
|
|
// Simple parsing
|
|
num = int(data[0]-'0')*10 + int(data[1]-'0')
|
|
return num, nil
|
|
})
|
|
}
|
|
|
|
result := F.Pipe1(
|
|
acquire,
|
|
Read[int],
|
|
)(reader)
|
|
|
|
either := result()
|
|
|
|
assert.True(t, E.IsRight(either))
|
|
num := E.GetOrElse(func(error) int { return 0 })(either)
|
|
assert.Equal(t, 42, num)
|
|
assert.True(t, mock.closeCalled, "Close should have been called")
|
|
}
|
|
|
|
// TestReadMultipleOperations tests that Read can be used multiple times
|
|
func TestReadMultipleOperations(t *testing.T) {
|
|
// Create a function that creates a new mock each time
|
|
createMock := func() *mockReadCloser {
|
|
return &mockReadCloser{data: []byte("test data")}
|
|
}
|
|
|
|
reader := func(rc *mockReadCloser) IOEither[error, []byte] {
|
|
return ioeither.TryCatchError(func() ([]byte, error) {
|
|
return io.ReadAll(rc)
|
|
})
|
|
}
|
|
|
|
// First read
|
|
mock1 := createMock()
|
|
result1 := Read[[]byte](ioeither.Of[error](mock1))(reader)
|
|
either1 := result1()
|
|
|
|
assert.True(t, E.IsRight(either1))
|
|
data1 := E.GetOrElse(func(error) []byte { return nil })(either1)
|
|
assert.Equal(t, []byte("test data"), data1)
|
|
assert.True(t, mock1.closeCalled)
|
|
|
|
// Second read with a new mock
|
|
mock2 := createMock()
|
|
result2 := Read[[]byte](ioeither.Of[error](mock2))(reader)
|
|
either2 := result2()
|
|
|
|
assert.True(t, E.IsRight(either2))
|
|
data2 := E.GetOrElse(func(error) []byte { return nil })(either2)
|
|
assert.Equal(t, []byte("test data"), data2)
|
|
assert.True(t, mock2.closeCalled)
|
|
}
|
|
|
|
// TestReadEnsuresCloseOnPanic tests that Close is called even if reader panics
|
|
// Note: This is more of a conceptual test as the actual panic handling depends on
|
|
// the implementation of WithResource
|
|
func TestReadWithEmptyData(t *testing.T) {
|
|
mock := &mockReadCloser{data: []byte{}}
|
|
|
|
acquire := ioeither.Of[error](mock)
|
|
|
|
reader := func(rc *mockReadCloser) IOEither[error, []byte] {
|
|
return ioeither.TryCatchError(func() ([]byte, error) {
|
|
return io.ReadAll(rc)
|
|
})
|
|
}
|
|
|
|
result := Read[[]byte](acquire)(reader)
|
|
either := result()
|
|
|
|
assert.True(t, E.IsRight(either))
|
|
data := E.GetOrElse(func(error) []byte { return nil })(either)
|
|
assert.Empty(t, data)
|
|
assert.True(t, mock.closeCalled, "Close should be called even with empty data")
|
|
}
|
|
|
|
// TestReadIntegrationWithEither tests integration with Either operations
|
|
func TestReadIntegrationWithEither(t *testing.T) {
|
|
testData := []byte("integration test")
|
|
mock := &mockReadCloser{data: testData}
|
|
|
|
acquire := ioeither.Of[error](mock)
|
|
|
|
reader := func(rc *mockReadCloser) IOEither[error, []byte] {
|
|
return ioeither.TryCatchError(func() ([]byte, error) {
|
|
return io.ReadAll(rc)
|
|
})
|
|
}
|
|
|
|
result := Read[[]byte](acquire)(reader)
|
|
either := result()
|
|
|
|
// Test with Either operations
|
|
assert.True(t, E.IsRight(either))
|
|
|
|
folded := E.Fold(
|
|
func(err error) string { return "error: " + err.Error() },
|
|
func(data []byte) string { return "success: " + string(data) },
|
|
)(either)
|
|
|
|
assert.Equal(t, "success: integration test", folded)
|
|
assert.True(t, mock.closeCalled)
|
|
}
|