From f6cde8d9122cfb8a413b8091ac5e06d6f16fb535 Mon Sep 17 00:00:00 2001 From: Will McCutchen <will@mccutch.org> Date: Fri, 16 Jun 2017 16:47:57 -0700 Subject: [PATCH] Fix and test synthetic byte stream --- httpbin/handlers.go | 7 ++-- httpbin/helpers.go | 46 +++++++++++++++------ httpbin/helpers_test.go | 89 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 125 insertions(+), 17 deletions(-) diff --git a/httpbin/handlers.go b/httpbin/handlers.go index 701211c..159e403 100644 --- a/httpbin/handlers.go +++ b/httpbin/handlers.go @@ -559,10 +559,9 @@ func (h *HTTPBin) Range(w http.ResponseWriter, r *http.Request) { return } - content := &syntheticReadSeeker{ - numBytes: numBytes, - byteFactory: func(offset int64) byte { return byte(97 + (offset % 26)) }, - } + content := newSyntheticByteStream(numBytes, func(offset int64) byte { + return byte(97 + (offset % 26)) + }) var modtime time.Time http.ServeContent(w, r, "", modtime, content) } diff --git a/httpbin/helpers.go b/httpbin/helpers.go index e1d94fd..2fa7a92 100644 --- a/httpbin/helpers.go +++ b/httpbin/helpers.go @@ -11,6 +11,7 @@ import ( "net/url" "strconv" "strings" + "sync" "time" ) @@ -140,41 +141,60 @@ func parseBoundedDuration(input string, min, max time.Duration) (time.Duration, return d, err } -// syntheticReadSeeker implements the ReadSeeker interface to allow reading +// syntheticByteStream implements the ReadSeeker interface to allow reading // arbitrary subsets of bytes up to a maximum size given a function for // generating the byte at a given offset. -type syntheticReadSeeker struct { - numBytes int64 - offset int64 - byteFactory func(int64) byte +type syntheticByteStream struct { + mu sync.Mutex + + size int64 + offset int64 + factory func(int64) byte +} + +// newSyntheticByteStream returns a new stream of bytes of a specific size, +// given a factory function for generating the byte at a given offset. +func newSyntheticByteStream(size int64, factory func(int64) byte) io.ReadSeeker { + return &syntheticByteStream{ + size: size, + factory: factory, + } } -// Read implements the Reader interface for syntheticReadSeeker -func (s *syntheticReadSeeker) Read(p []byte) (int, error) { +// Read implements the Reader interface for syntheticByteStream +func (s *syntheticByteStream) Read(p []byte) (int, error) { + s.mu.Lock() + defer s.mu.Unlock() + start := s.offset end := start + int64(len(p)) var err error - if end > s.numBytes { + if end >= s.size { err = io.EOF - end = s.numBytes - start + end = s.size } for idx := start; idx < end; idx++ { - p[idx-start] = s.byteFactory(idx) + p[idx-start] = s.factory(idx) } + s.offset = end + return int(end - start), err } -// Seek implements the Seeker interface for syntheticReadSeeker -func (s *syntheticReadSeeker) Seek(offset int64, whence int) (int64, error) { +// Seek implements the Seeker interface for syntheticByteStream +func (s *syntheticByteStream) Seek(offset int64, whence int) (int64, error) { + s.mu.Lock() + defer s.mu.Unlock() + switch whence { case io.SeekStart: s.offset = offset case io.SeekCurrent: s.offset += offset case io.SeekEnd: - s.offset = s.numBytes - offset + s.offset = s.size - offset default: return 0, errors.New("Seek: invalid whence") } diff --git a/httpbin/helpers_test.go b/httpbin/helpers_test.go index 1422279..283d9b7 100644 --- a/httpbin/helpers_test.go +++ b/httpbin/helpers_test.go @@ -2,10 +2,36 @@ package httpbin import ( "fmt" + "io" + "reflect" "testing" "time" ) +func assertNil(t *testing.T, v interface{}) { + if v != nil { + t.Errorf("expected nil, got %#v", v) + } +} + +func assertIntEqual(t *testing.T, a, b int) { + if a != b { + t.Errorf("expected %v == %v", a, b) + } +} + +func assertBytesEqual(t *testing.T, a, b []byte) { + if !reflect.DeepEqual(a, b) { + t.Errorf("expected %v == %v", a, b) + } +} + +func assertError(t *testing.T, got, expected error) { + if got != expected { + t.Errorf("expected error %v, got %v", expected, got) + } +} + func TestParseDuration(t *testing.T) { var okTests = []struct { input string @@ -53,3 +79,66 @@ func TestParseDuration(t *testing.T) { }) } } + +func TestSyntheticByteStream(t *testing.T) { + factory := func(offset int64) byte { + return byte(offset) + } + + t.Run("read", func(t *testing.T) { + s := newSyntheticByteStream(10, factory) + + // read first half + p := make([]byte, 5) + count, err := s.Read(p) + assertNil(t, err) + assertIntEqual(t, count, 5) + assertBytesEqual(t, p, []byte{0, 1, 2, 3, 4}) + + // read second half + p = make([]byte, 5) + count, err = s.Read(p) + assertError(t, err, io.EOF) + assertIntEqual(t, count, 5) + assertBytesEqual(t, p, []byte{5, 6, 7, 8, 9}) + + // can't read any more + p = make([]byte, 5) + count, err = s.Read(p) + assertError(t, err, io.EOF) + assertIntEqual(t, count, 0) + assertBytesEqual(t, p, []byte{0, 0, 0, 0, 0}) + }) + + t.Run("read into too-large buffer", func(t *testing.T) { + s := newSyntheticByteStream(5, factory) + p := make([]byte, 10) + count, err := s.Read(p) + assertError(t, err, io.EOF) + assertIntEqual(t, count, 5) + assertBytesEqual(t, p, []byte{0, 1, 2, 3, 4, 0, 0, 0, 0, 0}) + }) + + t.Run("seek", func(t *testing.T) { + s := newSyntheticByteStream(100, factory) + + p := make([]byte, 5) + s.Seek(10, io.SeekStart) + count, err := s.Read(p) + assertNil(t, err) + assertIntEqual(t, count, 5) + assertBytesEqual(t, p, []byte{10, 11, 12, 13, 14}) + + s.Seek(10, io.SeekCurrent) + count, err = s.Read(p) + assertNil(t, err) + assertIntEqual(t, count, 5) + assertBytesEqual(t, p, []byte{25, 26, 27, 28, 29}) + + s.Seek(10, io.SeekEnd) + count, err = s.Read(p) + assertNil(t, err) + assertIntEqual(t, count, 5) + assertBytesEqual(t, p, []byte{90, 91, 92, 93, 94}) + }) +} -- GitLab