/* * MIT License * * Copyright (c) 2024 Nicolas JUHEL * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in all * copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. * * */ // Package ioprogress_test provides comprehensive tests for Reader implementation. // // This file contains 22 test specifications covering all aspects of the Reader interface: // - Constructor and initialization // - Read operations (single, multiple, EOF, empty data) // - Progress tracking with increment callbacks // - EOF callback behavior // - Reset callback for multi-stage operations // - Close operations (single and multiple) // - Combined operations (full read cycle) // - Edge cases (zero-byte reads, large data) // // Test Strategy: Each test validates a specific behavior or edge case using BDD-style // organization. Tests use helper types (closeableReader) to provide controlled test // environments that track state (e.g., closed status). // // Thread Safety: Tests validate that callbacks can be registered concurrently using // atomic operations. The package passes race detection (-race flag) with zero data races. package ioprogress_test import ( "io" "strings" "sync/atomic" . "github.com/nabbar/golib/ioutils/ioprogress" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" ) // Helper types (closeableReader, newCloseableReader) are defined in helper_test.go // to avoid code duplication across test files. var _ = Describe("Reader", func() { Context("Creation", func() { It("should create reader from io.ReadCloser", func() { source := newCloseableReader("test data") reader := NewReadCloser(source) Expect(reader).ToNot(BeNil()) }) }) Context("Read operations", func() { It("should read data", func() { source := newCloseableReader("hello world") reader := NewReadCloser(source) data := make([]byte, 5) n, err := reader.Read(data) Expect(err).ToNot(HaveOccurred()) Expect(n).To(Equal(5)) Expect(string(data)).To(Equal("hello")) }) It("should read multiple times", func() { source := newCloseableReader("abcdefghij") reader := NewReadCloser(source) // First read data1 := make([]byte, 3) n1, err1 := reader.Read(data1) Expect(err1).ToNot(HaveOccurred()) Expect(n1).To(Equal(3)) Expect(string(data1)).To(Equal("abc")) // Second read data2 := make([]byte, 3) n2, err2 := reader.Read(data2) Expect(err2).ToNot(HaveOccurred()) Expect(n2).To(Equal(3)) Expect(string(data2)).To(Equal("def")) }) It("should handle EOF", func() { source := newCloseableReader("short") reader := NewReadCloser(source) // Read all data data := make([]byte, 100) n, err := reader.Read(data) Expect(n).To(Equal(5)) Expect(string(data[:n])).To(Equal("short")) // Second read should give EOF _, err = reader.Read(data) Expect(err).To(Equal(io.EOF)) }) It("should handle empty reader", func() { source := newCloseableReader("") reader := NewReadCloser(source) data := make([]byte, 10) n, err := reader.Read(data) Expect(n).To(Equal(0)) Expect(err).To(Equal(io.EOF)) }) }) Context("Progress tracking with increment callback", func() { It("should call increment callback on each read", func() { source := newCloseableReader("hello world") reader := NewReadCloser(source) var totalIncremented int64 var callCount atomic.Int32 reader.RegisterFctIncrement(func(size int64) { atomic.AddInt64(&totalIncremented, size) callCount.Add(1) }) // Read in chunks data := make([]byte, 5) reader.Read(data) // 5 bytes reader.Read(data) // 5 bytes reader.Read(data) // 1 byte Expect(totalIncremented).To(Equal(int64(11))) Expect(callCount.Load()).To(Equal(int32(3))) }) It("should track total bytes read", func() { source := newCloseableReader("1234567890") reader := NewReadCloser(source) var totalBytes int64 reader.RegisterFctIncrement(func(size int64) { atomic.AddInt64(&totalBytes, size) }) // Read all at once data := make([]byte, 100) n, _ := reader.Read(data) Expect(totalBytes).To(Equal(int64(n))) Expect(totalBytes).To(Equal(int64(10))) }) It("should handle nil increment callback", func() { source := newCloseableReader("test") reader := NewReadCloser(source) // Register nil callback (should not panic) reader.RegisterFctIncrement(nil) data := make([]byte, 4) n, err := reader.Read(data) Expect(err).ToNot(HaveOccurred()) Expect(n).To(Equal(4)) }) It("should allow changing increment callback", func() { source := newCloseableReader("abcdefgh") reader := NewReadCloser(source) var count1 int64 reader.RegisterFctIncrement(func(size int64) { atomic.AddInt64(&count1, size) }) data := make([]byte, 4) reader.Read(data) // 4 bytes with first callback var count2 int64 reader.RegisterFctIncrement(func(size int64) { atomic.AddInt64(&count2, size) }) reader.Read(data) // 4 bytes with second callback Expect(count1).To(Equal(int64(4))) Expect(count2).To(Equal(int64(4))) }) }) Context("EOF callback", func() { It("should call EOF callback when reaching end", func() { source := newCloseableReader("data") reader := NewReadCloser(source) eofCalled := false reader.RegisterFctEOF(func() { eofCalled = true }) // Read all data - may need two reads to trigger EOF data := make([]byte, 100) reader.Read(data) if !eofCalled { // Read again to trigger EOF reader.Read(data) } Expect(eofCalled).To(BeTrue()) }) It("should not call EOF callback on partial reads", func() { source := newCloseableReader("hello world") reader := NewReadCloser(source) eofCalled := false reader.RegisterFctEOF(func() { eofCalled = true }) // Partial read data := make([]byte, 5) reader.Read(data) Expect(eofCalled).To(BeFalse()) }) It("should handle nil EOF callback", func() { source := newCloseableReader("test") reader := NewReadCloser(source) // Register nil callback (should not panic) reader.RegisterFctEOF(nil) data := make([]byte, 100) n, err := reader.Read(data) // First read may or may not return EOF if err != io.EOF { // Read again to get EOF _, err = reader.Read(data) } Expect(err).To(Equal(io.EOF)) Expect(n).To(BeNumerically(">=", 0)) }) }) Context("Reset callback", func() { It("should call reset callback with max and current", func() { source := newCloseableReader("hello") reader := NewReadCloser(source) var resetMax, resetCurrent int64 reader.RegisterFctReset(func(max, current int64) { resetMax = max resetCurrent = current }) // Read some data first data := make([]byte, 3) reader.Read(data) // Call reset reader.Reset(100) Expect(resetMax).To(Equal(int64(100))) Expect(resetCurrent).To(Equal(int64(3))) }) It("should handle nil reset callback", func() { source := newCloseableReader("test") reader := NewReadCloser(source) // Register nil callback (should not panic) reader.RegisterFctReset(nil) // This should not panic reader.Reset(50) }) It("should track current progress correctly", func() { source := newCloseableReader("1234567890") reader := NewReadCloser(source) var progressValues []int64 reader.RegisterFctReset(func(max, current int64) { progressValues = append(progressValues, current) }) // Read incrementally and reset data := make([]byte, 3) reader.Read(data) // 3 bytes reader.Reset(100) reader.Read(data) // 6 bytes total reader.Reset(100) reader.Read(data) // 9 bytes total reader.Reset(100) Expect(progressValues).To(Equal([]int64{3, 6, 9})) }) }) Context("Close operations", func() { It("should close underlying reader", func() { source := newCloseableReader("data") reader := NewReadCloser(source) err := reader.Close() Expect(err).ToNot(HaveOccurred()) Expect(source.closed).To(BeTrue()) }) It("should be safe to close multiple times", func() { source := newCloseableReader("data") reader := NewReadCloser(source) err1 := reader.Close() err2 := reader.Close() Expect(err1).ToNot(HaveOccurred()) Expect(err2).ToNot(HaveOccurred()) }) }) Context("Combined operations", func() { It("should track progress through complete read cycle", func() { source := newCloseableReader("hello world test") reader := NewReadCloser(source) var increments []int64 var eofCalled bool reader.RegisterFctIncrement(func(size int64) { increments = append(increments, size) }) reader.RegisterFctEOF(func() { eofCalled = true }) // Read in chunks until EOF data := make([]byte, 5) for { n, err := reader.Read(data) if err == io.EOF { break } if n == 0 { break } } Expect(len(increments)).To(BeNumerically(">=", 3)) Expect(eofCalled).To(BeTrue()) }) }) Context("Edge cases", func() { It("should handle zero-byte read", func() { source := newCloseableReader("test") reader := NewReadCloser(source) var incrementCalled bool reader.RegisterFctIncrement(func(size int64) { incrementCalled = true }) data := make([]byte, 0) n, err := reader.Read(data) Expect(n).To(Equal(0)) Expect(err).ToNot(HaveOccurred()) Expect(incrementCalled).To(BeTrue()) // Called even with 0 bytes }) It("should handle large data", func() { largeData := strings.Repeat("x", 1024*1024) // 1 MB source := newCloseableReader(largeData) reader := NewReadCloser(source) var totalBytes int64 reader.RegisterFctIncrement(func(size int64) { atomic.AddInt64(&totalBytes, size) }) // Read all data := make([]byte, 1024*1024+100) n, _ := reader.Read(data) Expect(totalBytes).To(Equal(int64(n))) }) }) })