1
0
mirror of https://github.com/IBM/fp-go.git synced 2025-12-09 23:11:40 +02:00
Files
fp-go/v2/ioeither/file/read_test.go
Dr. Carsten Leue dbe7102e43 fix: better doc and some helpers
Signed-off-by: Dr. Carsten Leue <carsten.leue@de.ibm.com>
2025-11-26 12:05:31 +01:00

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)
}