diff --git a/testio/LICENSE b/testio/LICENSE new file mode 100644 index 0000000..b228ca4 --- /dev/null +++ b/testio/LICENSE @@ -0,0 +1,13 @@ +Copyright (c) 2014 Kyle Isom + +Permission to use, copy, modify, and distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. diff --git a/testio/README.md b/testio/README.md new file mode 100644 index 0000000..c4d28a4 --- /dev/null +++ b/testio/README.md @@ -0,0 +1,15 @@ +## testio + +This is a collection of various utility io types: + +* BrokenReadWriter +* BrokenWriter +* BufCloser +* BufferConn +* LoggingBuffer + +You can check out the +[godoc](https://godoc.org/github.com/kisom/goutils/testio) for dtails. + +It was imported from [kisom/testio](https://github.com/kisom/testio/). The +original Git directory is preserved in git-hist.tar.xz. diff --git a/testio/git-hist.tar.xz b/testio/git-hist.tar.xz new file mode 100644 index 0000000..2800f5e Binary files /dev/null and b/testio/git-hist.tar.xz differ diff --git a/testio/testio.go b/testio/testio.go new file mode 100644 index 0000000..ce90562 --- /dev/null +++ b/testio/testio.go @@ -0,0 +1,261 @@ +// Package testio implements various io utility types. Included are +// BrokenWriter, which fails after writing a certain number of bytes; +// a BufCloser, which wraps a bytes.Buffer in a Close method; a +// BrokenReadWriter, which fails after writing a certain number of +// bytes and/or reading a certain number of bytes; a LoggingBuffer +// that logs all reads and writes; and a BufferConn, that is designed +// to simulate net.Conn. +package testio + +import ( + "bytes" + "errors" + "fmt" + "io" + "os" +) + +// BrokenWriter implements an io.Writer that fails after a certain +// number of bytes. This can be used to simulate a network connection +// that breaks during write or a file on a filesystem that becomes +// full, for example. A BrokenWriter doesn't actually store any data. +type BrokenWriter struct { + current, limit int +} + +// NewBrokenWriter creates a new BrokenWriter that can store only +// limit bytes. +func NewBrokenWriter(limit int) *BrokenWriter { + return &BrokenWriter{limit: limit} +} + +// Write will write the byte slice to the BrokenWriter, failing if the +// maximum number of bytes has been reached. +func (w *BrokenWriter) Write(p []byte) (int, error) { + if (len(p) + w.current) <= w.limit { + w.current += len(p) + } else { + spill := (len(p) + w.current) - w.limit + w.current = w.limit + return len(p) - spill, errors.New("testio: write failed") + } + + return len(p), nil +} + +// Extend increases the byte limit to allow more data to be written. +func (w *BrokenWriter) Extend(n int) { + w.limit += n +} + +// Reset clears the limit and bytes in the BrokenWriter. Extend needs +// to be called to allow data to be written. +func (w *BrokenWriter) Reset() { + w.limit = 0 + w.current = 0 +} + +// BrokenReadWriter implements a broken reader and writer, backed by a +// bytes.Buffer. +type BrokenReadWriter struct { + rlimit, wlimit int + buf *bytes.Buffer +} + +// NewBrokenReadWriter initialises a new BrokerReadWriter with an empty +// reader and the specified limits. +func NewBrokenReadWriter(wlimit, rlimit int) *BrokenReadWriter { + return &BrokenReadWriter{ + wlimit: wlimit, + rlimit: rlimit, + buf: &bytes.Buffer{}, + } +} + +// Write satisfies the Writer interface. +func (brw *BrokenReadWriter) Write(p []byte) (int, error) { + if (len(p) + brw.buf.Len()) > brw.wlimit { + remain := brw.wlimit - brw.buf.Len() + if remain > 0 { + brw.buf.Write(p[:remain]) + return remain, errors.New("testio: write failed") + } + return 0, errors.New("testio: write failed") + } + return brw.buf.Write(p) +} + +// Read satisfies the Reader interface. +func (brw *BrokenReadWriter) Read(p []byte) (int, error) { + remain := brw.rlimit - brw.buf.Len() + if len(p) > remain { + tmp := make([]byte, len(p)-remain) + n, err := brw.buf.Read(tmp) + if err == nil { + err = io.EOF + } + copy(p, tmp) + return n, err + } + return brw.buf.Read(p) +} + +// Extend increases the BrokenReadWriter limit. +func (brw *BrokenReadWriter) Extend(w, r int) { + brw.rlimit += r + brw.wlimit += w +} + +// Reset clears the internal buffer. It retains its original limit. +func (brw *BrokenReadWriter) Reset() { + brw.buf.Reset() +} + +// BufCloser is a buffer wrapped with a Close method. +type BufCloser struct { + buf *bytes.Buffer +} + +// Write writes the data to the BufCloser. +func (buf *BufCloser) Write(p []byte) (int, error) { + return buf.buf.Write(p) +} + +// Read reads data from the BufCloser. +func (buf *BufCloser) Read(p []byte) (int, error) { + return buf.buf.Read(p) +} + +// Close is a stub function to satisfy the io.Closer interface. +func (buf *BufCloser) Close() error { + return nil +} + +// Reset clears the internal buffer. +func (buf *BufCloser) Reset() { + buf.buf.Reset() +} + +// Bytes returns the contents of the buffer as a byte slice. +func (buf *BufCloser) Bytes() []byte { + return buf.buf.Bytes() +} + +// NewBufCloser creates and initializes a new BufCloser using buf as +// its initial contents. It is intended to prepare a BufCloser to read +// existing data. It can also be used to size the internal buffer for +// writing. To do that, buf should have the desired capacity but a +// length of zero. +func NewBufCloser(buf []byte) *BufCloser { + bc := new(BufCloser) + bc.buf = bytes.NewBuffer(buf) + return bc +} + +// NewBufCloserString creates and initializes a new Buffer using +// string s as its initial contents. It is intended to prepare a +// buffer to read an existing string. +func NewBufCloserString(s string) *BufCloser { + buf := new(BufCloser) + buf.buf = bytes.NewBufferString(s) + return buf +} + +// A LoggingBuffer is an io.ReadWriter that prints the hex value of +// the data for all reads and writes. +type LoggingBuffer struct { + rw io.ReadWriter + w io.Writer + name string +} + +// NewLoggingBuffer creates a logging buffer from an existing +// io.ReadWriter. By default, it will log to standard error. +func NewLoggingBuffer(rw io.ReadWriter) *LoggingBuffer { + return &LoggingBuffer{ + rw: rw, + w: os.Stderr, + } +} + +// LogTo sets the io.Writer that the buffer will write logs to. +func (lb *LoggingBuffer) LogTo(w io.Writer) { + lb.w = w +} + +// SetName gives a name to the logging buffer to help distinguish +// output from this buffer. +func (lb *LoggingBuffer) SetName(name string) { + lb.name = name +} + +// Write writes the data to the logging buffer and writes the data to +// the logging writer. +func (lb *LoggingBuffer) Write(p []byte) (int, error) { + if lb.name != "" { + fmt.Fprintf(lb.w, "[%s] ", lb.name) + } + + fmt.Fprintf(lb.w, "[WRITE] %x\n", p) + return lb.rw.Write(p) +} + +// Read reads the data from the logging buffer and writes the data to +// the logging writer. +func (lb *LoggingBuffer) Read(p []byte) (int, error) { + n, err := lb.rw.Read(p) + if err != nil { + return n, err + } + if lb.name != "" { + fmt.Fprintf(lb.w, "[%s] ", lb.name) + } + + fmt.Fprintf(lb.w, "[READ] %x\n", p) + return n, err +} + +// BufferConn is a type that can be used to simulate network +// connections between a "client" (the code that uses the BufferConn) +// and some simulated "peer". Writes go to a "client" buffer, which is +// used to record the data sent by the caller, which may be read with +// ReadPeer. The peer's responses may be simulated by calling +// WritePeer; when the client reads from the BufferConn, they will see +// this data. +type BufferConn struct { + client, peer *bytes.Buffer +} + +// NewBufferConn initialises a new simulated network connection. +func NewBufferConn() *BufferConn { + return &BufferConn{ + client: &bytes.Buffer{}, + peer: &bytes.Buffer{}, + } +} + +// Write writes to the client buffer. +func (bc *BufferConn) Write(p []byte) (int, error) { + return bc.client.Write(p) +} + +// Read reads from the peer buffer. +func (bc *BufferConn) Read(p []byte) (int, error) { + return bc.peer.Read(p) +} + +// WritePeer writes data to the peer buffer. +func (bc *BufferConn) WritePeer(p []byte) (int, error) { + return bc.peer.Write(p) +} + +// ReadClient reads data from the client buffer. +func (bc *BufferConn) ReadClient(p []byte) (int, error) { + return bc.client.Read(p) +} + +// Close is a dummy operation that allows the BufferConn to be used as +// an io.Closer. +func (bc *BufferConn) Close() error { + return nil +} diff --git a/testio/testio_test.go b/testio/testio_test.go new file mode 100644 index 0000000..d819e62 --- /dev/null +++ b/testio/testio_test.go @@ -0,0 +1,220 @@ +package testio + +import ( + "bytes" + "os" + "testing" +) + +func TestBrokenWriter(t *testing.T) { + buf := NewBrokenWriter(2) + data := []byte{1, 2} + + n, err := buf.Write(data) + if err != nil { + t.Fatalf("%v", err) + } else if n != 2 { + t.Fatalf("expected write size of 2, have %d", n) + } + + _, err = buf.Write(data) + if err == nil { + t.Fatal("expected a write failure") + } + + buf.Reset() + _, err = buf.Write(data) + if err == nil { + t.Fatalf("expected a write failure after reset") + } + + buf.Extend(2) + _, err = buf.Write(data) + if err != nil { + t.Fatalf("%v", err) + } +} + +func TestBufCloser(t *testing.T) { + var data = []byte{1, 2} + var read = make([]byte, 2) + + buf := NewBufCloser(data) + _, err := buf.Read(read) + if err != nil { + t.Fatalf("%v", err) + } + + _, err = buf.Write(data) + if err != nil { + t.Fatalf("%v", err) + } + + buf.Close() + buf.Reset() + + s := "hi" + buf = NewBufCloserString(s) + + read = buf.Bytes() + if string(read) != s { + t.Fatalf("expected %s, have %s", s, read) + } +} + +func TestLoggingBuffer(t *testing.T) { + src := &bytes.Buffer{} + data := []byte("AB") + lb := NewLoggingBuffer(src) + _, err := lb.Write(data) + if err != nil { + t.Fatalf("%v", err) + } + + src.Reset() + lb.SetName("TEST") + out := &bytes.Buffer{} + lb.LogTo(out) + + _, err = lb.Write(data) + if err != nil { + t.Fatalf("%v", err) + } + + expected := "[TEST] [WRITE] 4142\n" + if string(out.Bytes()) != expected { + t.Fatalf("expected '%s', have '%s'", expected, string(out.Bytes())) + } + + out.Reset() + src = bytes.NewBuffer(data) + read := make([]byte, 2) + + _, err = lb.Read(read) + if err != nil { + t.Fatalf("%v", err) + } + + expected = "[TEST] [READ] 4142\n" + if string(out.Bytes()) != expected { + t.Fatalf("expected '%s', have '%s'", expected, string(out.Bytes())) + } + + out.Reset() + lb.SetName("") + lb.LogTo(os.Stderr) + lb.Write([]byte("AB")) + + lb.LogTo(out) + _, err = lb.Read(read) + if err != nil { + t.Fatalf("%v", err) + } + + expected = "[READ] 4142\n" + if string(out.Bytes()) != expected { + t.Fatalf("expected '%s', have '%s'", expected, string(out.Bytes())) + } + + src.Reset() + _, err = lb.Read(read) + if err == nil { + t.Fatal("expected a read failure") + } +} + +func TestBrokenReadWriter(t *testing.T) { + brw := NewBrokenReadWriter(0, 0) + lb := NewLoggingBuffer(brw) + + var p = make([]byte, 2) + var data = []byte("HI") + _, err := lb.Write(data) + if err == nil { + t.Fatal("expected a write failure") + } + + _, err = lb.Read(p) + if err == nil { + t.Fatal("expected a read failure") + } + + brw.Extend(1, 0) + _, err = lb.Write(data) + if err == nil { + t.Fatal("expected a write failure") + } + + brw.Extend(2, 0) + _, err = lb.Write(data) + if err != nil { + t.Fatalf("%v", err) + } + + brw.Extend(4, 1) + _, err = lb.Write(data) + if err != nil { + t.Fatalf("%v", err) + } + + _, err = lb.Read(p) + if err == nil { + t.Fatal("expected a read failure") + } + + brw.Reset() + brw.Extend(10, 2) + _, err = lb.Write(data) + if err != nil { + t.Fatalf("%v", err) + } + + p = make([]byte, 1) + _, err = lb.Read(p) + if err != nil { + t.Fatalf("%v", err) + } +} + +func TestBufferConn(t *testing.T) { + bc := NewBufferConn() + + client := []byte("AB") + peer := []byte("XY") + + _, err := bc.WritePeer(peer) + if err != nil { + t.Fatalf("%v", err) + } + + var p = make([]byte, 2) + _, err = bc.Write(client) + if err != nil { + t.Fatalf("%v", err) + } + + _, err = bc.Read(p) + if err != nil { + t.Fatalf("%v", err) + } + + if !bytes.Equal(p, peer) { + t.Fatalf("client should have read %x, but read %x", + peer, p) + } + + _, err = bc.ReadClient(p) + if err != nil { + t.Fatalf("%v", err) + } + + if !bytes.Equal(client, p) { + t.Fatalf("client should have sent %x, but sent %x", + client, p) + } + + err = bc.Close() + if err != nil { + t.Fatalf("Close should always return nil, but it returned %v", err) + } +}