func_test.go 3.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110
  1. package reader
  2. import (
  3. "context"
  4. "errors"
  5. "io"
  6. "testing"
  7. )
  8. func TestFuncReader(t *testing.T) {
  9. // counter yields 0..total-1 then reports io.EOF.
  10. newCounter := func(total int) nextFunc[int] {
  11. i := 0
  12. return func() (int, error) {
  13. if i >= total {
  14. return 0, io.EOF
  15. }
  16. v := i
  17. i++
  18. return v, nil
  19. }
  20. }
  21. t.Run("returns io.EOF with the final batch, then stays terminal", func(t *testing.T) {
  22. // counter(3) drained through a 2-slot buffer: reads of 2, 1, 0.
  23. r := NewFuncReader(newCounter(3), nil)
  24. dst := make([]int, 2)
  25. // Exact fill: the buffer fills before next() reports io.EOF, so io.EOF
  26. // is deferred to the following read (Read cannot know it is exhausted
  27. // without pulling again).
  28. if n, err := r.Read(context.Background(), dst); n != 2 || err != nil {
  29. t.Fatalf("read 1: got (%d, %v), want (2, nil)", n, err)
  30. }
  31. // The last item and io.EOF come back together: the terminal error is
  32. // returned alongside the final data-bearing batch, not on a separate call.
  33. if n, err := r.Read(context.Background(), dst); n != 1 || !errors.Is(err, io.EOF) {
  34. t.Fatalf("read 2: got (%d, %v), want (1, io.EOF)", n, err)
  35. }
  36. // Terminal state is sticky: io.EOF latched, no further items.
  37. if n, err := r.Read(context.Background(), dst); n != 0 || !errors.Is(err, io.EOF) {
  38. t.Fatalf("read 3: got (%d, %v), want (0, io.EOF)", n, err)
  39. }
  40. })
  41. t.Run("io.EOF at batch start yields (0, io.EOF)", func(t *testing.T) {
  42. r := NewFuncReader(newCounter(0), nil)
  43. if n, err := r.Read(context.Background(), make([]int, 4)); n != 0 || !errors.Is(err, io.EOF) {
  44. t.Fatalf("got (%d, %v), want (0, io.EOF)", n, err)
  45. }
  46. })
  47. t.Run("item returned alongside io.EOF is not read", func(t *testing.T) {
  48. // A nextFunc that violates the contract by returning a real item with
  49. // io.EOF. io.EOF is authoritative: the item must be dropped, never
  50. // written to dst. This pins the "Do NOT return an item with io.EOF"
  51. // rule that lets FuncReader avoid disambiguating zero from placeholder.
  52. next := func() (int, error) { return 99, io.EOF }
  53. r := NewFuncReader(next, nil)
  54. dst := []int{-1, -1}
  55. n, err := r.Read(context.Background(), dst)
  56. if n != 0 || !errors.Is(err, io.EOF) {
  57. t.Fatalf("got (%d, %v), want (0, io.EOF)", n, err)
  58. }
  59. if dst[0] != -1 {
  60. t.Errorf("dst[0] was written despite io.EOF: got %d, want untouched (-1)", dst[0])
  61. }
  62. })
  63. t.Run("error is returned with prefix and is sticky", func(t *testing.T) {
  64. wantErr := errors.New("boom")
  65. calls := 0
  66. next := func() (int, error) {
  67. calls++
  68. switch calls {
  69. case 1:
  70. return 10, nil
  71. case 2:
  72. return 0, wantErr
  73. default:
  74. t.Fatalf("next called %d times; should have latched", calls)
  75. return 0, nil
  76. }
  77. }
  78. r := NewFuncReader(next, nil)
  79. dst := make([]int, 4)
  80. n, err := r.Read(context.Background(), dst)
  81. if n != 1 || !errors.Is(err, wantErr) {
  82. t.Fatalf("read 1: got (%d, %v), want (1, %v)", n, err, wantErr)
  83. }
  84. if dst[0] != 10 {
  85. t.Errorf("prefix item: got %d, want 10", dst[0])
  86. }
  87. // Sticky: no further calls to next, same error returned.
  88. if n2, err2 := r.Read(context.Background(), dst); n2 != 0 || !errors.Is(err2, wantErr) {
  89. t.Errorf("read 2: got (%d, %v), want (0, %v)", n2, err2, wantErr)
  90. }
  91. })
  92. t.Run("nil closer Close is a no-op", func(t *testing.T) {
  93. r := NewFuncReader(newCounter(1), nil)
  94. if err := r.Close(); err != nil {
  95. t.Errorf("Close: got %v, want nil", err)
  96. }
  97. })
  98. }