| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110 |
- package reader
- import (
- "context"
- "errors"
- "io"
- "testing"
- )
- func TestFuncReader(t *testing.T) {
- // counter yields 0..total-1 then reports io.EOF.
- newCounter := func(total int) nextFunc[int] {
- i := 0
- return func() (int, error) {
- if i >= total {
- return 0, io.EOF
- }
- v := i
- i++
- return v, nil
- }
- }
- t.Run("returns io.EOF with the final batch, then stays terminal", func(t *testing.T) {
- // counter(3) drained through a 2-slot buffer: reads of 2, 1, 0.
- r := NewFuncReader(newCounter(3), nil)
- dst := make([]int, 2)
- // Exact fill: the buffer fills before next() reports io.EOF, so io.EOF
- // is deferred to the following read (Read cannot know it is exhausted
- // without pulling again).
- if n, err := r.Read(context.Background(), dst); n != 2 || err != nil {
- t.Fatalf("read 1: got (%d, %v), want (2, nil)", n, err)
- }
- // The last item and io.EOF come back together: the terminal error is
- // returned alongside the final data-bearing batch, not on a separate call.
- if n, err := r.Read(context.Background(), dst); n != 1 || !errors.Is(err, io.EOF) {
- t.Fatalf("read 2: got (%d, %v), want (1, io.EOF)", n, err)
- }
- // Terminal state is sticky: io.EOF latched, no further items.
- if n, err := r.Read(context.Background(), dst); n != 0 || !errors.Is(err, io.EOF) {
- t.Fatalf("read 3: got (%d, %v), want (0, io.EOF)", n, err)
- }
- })
- t.Run("io.EOF at batch start yields (0, io.EOF)", func(t *testing.T) {
- r := NewFuncReader(newCounter(0), nil)
- if n, err := r.Read(context.Background(), make([]int, 4)); n != 0 || !errors.Is(err, io.EOF) {
- t.Fatalf("got (%d, %v), want (0, io.EOF)", n, err)
- }
- })
- t.Run("item returned alongside io.EOF is not read", func(t *testing.T) {
- // A nextFunc that violates the contract by returning a real item with
- // io.EOF. io.EOF is authoritative: the item must be dropped, never
- // written to dst. This pins the "Do NOT return an item with io.EOF"
- // rule that lets FuncReader avoid disambiguating zero from placeholder.
- next := func() (int, error) { return 99, io.EOF }
- r := NewFuncReader(next, nil)
- dst := []int{-1, -1}
- n, err := r.Read(context.Background(), dst)
- if n != 0 || !errors.Is(err, io.EOF) {
- t.Fatalf("got (%d, %v), want (0, io.EOF)", n, err)
- }
- if dst[0] != -1 {
- t.Errorf("dst[0] was written despite io.EOF: got %d, want untouched (-1)", dst[0])
- }
- })
- t.Run("error is returned with prefix and is sticky", func(t *testing.T) {
- wantErr := errors.New("boom")
- calls := 0
- next := func() (int, error) {
- calls++
- switch calls {
- case 1:
- return 10, nil
- case 2:
- return 0, wantErr
- default:
- t.Fatalf("next called %d times; should have latched", calls)
- return 0, nil
- }
- }
- r := NewFuncReader(next, nil)
- dst := make([]int, 4)
- n, err := r.Read(context.Background(), dst)
- if n != 1 || !errors.Is(err, wantErr) {
- t.Fatalf("read 1: got (%d, %v), want (1, %v)", n, err, wantErr)
- }
- if dst[0] != 10 {
- t.Errorf("prefix item: got %d, want 10", dst[0])
- }
- // Sticky: no further calls to next, same error returned.
- if n2, err2 := r.Read(context.Background(), dst); n2 != 0 || !errors.Is(err2, wantErr) {
- t.Errorf("read 2: got (%d, %v), want (0, %v)", n2, err2, wantErr)
- }
- })
- t.Run("nil closer Close is a no-op", func(t *testing.T) {
- r := NewFuncReader(newCounter(1), nil)
- if err := r.Close(); err != nil {
- t.Errorf("Close: got %v, want nil", err)
- }
- })
- }
|