Skip to content

Commit

Permalink
first cut
Browse files Browse the repository at this point in the history
  • Loading branch information
tvanriper committed Jun 9, 2022
1 parent b910e9e commit cc52d8d
Show file tree
Hide file tree
Showing 5 changed files with 321 additions and 0 deletions.
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,9 @@
# go-mock-io
Test your socket i/o in pure golang

I needed a library to help me test working with a serial connection. Unfortunately, I didn't see one readily available in Golang that served my needs. So, I figured, I'll have to make one.

## Basic concept

Create a mock io.ReadWriteCloser, and give it certain expectations. When someone writing to the mock stream meets any of those expectations, the stream responds with that expectation's response.

72 changes: 72 additions & 0 deletions expect.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
package mockio

// Expect provides an interface for the mock io stream to use when matching incoming
// information.
type Expect interface {
Match(b []byte) (response []byte, count int, ok bool)
}

// ExpectBytes provides a kind of Expect that precisely matches a sequence of bytes to
// respond with another sequence of bytes.
type ExpectBytes struct {
Expect []byte
Respond []byte
}

// Match implements the Expect interface for ExpectBytes.
func (e *ExpectBytes) Match(b []byte) (response []byte, count int, ok bool) {
l := len(e.Expect)
ok = true
for i := 0; i < len(b); i++ {
if i == l {
break
}
if b[i] != e.Expect[i] {
ok = false
break
}
}

if ok {
response = append(response, e.Respond...)
count = l
}
return response, count, ok
}

// NewExpectBytes provides a convenience constructor for ExpectBytes.
func NewExpectBytes(expect []byte, respond []byte) *ExpectBytes {
return &ExpectBytes{
Expect: expect,
Respond: respond,
}
}

// ExpectFuncTest describes a test that matches a sequence of bytes written to the mock
// io stream.
// Always start the match from b[0]. Use count to indicate how many bytes in b were
// consumed in the test. Set ok to true if there was a successful match.
type ExpectFuncTest func(b []byte) (count int, ok bool)

// ExpectFunc provides a kind of Expect that tests a sequence of bytes with a function.
type ExpectFunc struct {
Test ExpectFuncTest
Respond []byte
}

// Match implements the Expect interface for ExpectFunc.
func (e *ExpectFunc) Match(b []byte) (response []byte, count int, ok bool) {
count, ok = e.Test(b)
if ok {
response = append(response, e.Respond...)
}
return response, count, ok
}

// NewExpectFunc provides a convenience constructor for ExpectFunc.
func NewExpectFunc(fn ExpectFuncTest, response []byte) *ExpectFunc {
return &ExpectFunc{
Test: fn,
Respond: response,
}
}
3 changes: 3 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
module github.com/tvanriper/go-mock-io

go 1.18
83 changes: 83 additions & 0 deletions mockio.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
/*
Package mockio provides a mock I/O ReadWriteCloser stream for testing your software's i/o stream handling.
Example usage:
func main() {
s := NewMockIO()
s.Expect(NewExpectBytes([]byte{1,2},[]byte{3,4}))
s.Write([]byte{1,2})
b := make([]byte,2)
n, err := s.Read(b)
if err != nil {
panic(err)
}
fmt.Printf("n: %d, b: %#v", n, b)
}
which should print:
n: 2, b: []byte{0x3, 0x4}
You can use this to test things like serial I/O, or perhaps console I/O, without having
to open a serial port or a console, allowing for automated testing in a controlled fashion.
*/
package mockio

import "bytes"

// MockIO provides a mock I/O ReadWriteCloser to test your software's i/o stream handling.
type MockIO struct {
buffer *bytes.Buffer
holding []byte
Expects []Expect
}

// NewMockIO constructs a new MockIO.
func NewMockIO() *MockIO {
return &MockIO{
buffer: bytes.NewBuffer([]byte{}),
holding: []byte{},
Expects: []Expect{},
}
}

// Read implements the Reader interface for the MockIO stream.
// Use this with your software to test that it reads correctly.
func (m *MockIO) Read(data []byte) (n int, err error) {
return m.buffer.Read(data)
}

// Write implements the Writer interface for the MockIO stream.
// Use this with your software to test that it writes correctly.
func (m *MockIO) Write(data []byte) (n int, err error) {
m.holding = append(m.holding, data...)
respond := []byte{}
for _, test := range m.Expects {
response, count, ok := test.Match(m.holding)
if !ok {
continue
}
respond = append(respond, response...)
m.holding = m.holding[count:]
break
}
m.buffer.Write(respond)
return len(data), nil
}

// Expect adds a new Expect item to a list of things for the MockIO Write to expect.
func (m *MockIO) Expect(exp Expect) {
m.Expects = append(m.Expects, exp)
}

// ClearExpectations removes all items stored in MockIO's Expect buffer.
func (m *MockIO) ClearExpectations() {
m.Expects = []Expect{}
}

// Close implements the Closer interface for the MockIO stream.
func (m *MockIO) Close() (err error) {
m.buffer.Reset()
m.holding = []byte{}
return err
}
156 changes: 156 additions & 0 deletions mockio_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
package mockio

import (
"io"
"log"
"testing"
)

func MatchBytes(l []byte, r []byte) bool {
ll := len(l)
lr := len(r)
if ll != lr {
return false
}
for i := 0; i < ll; i++ {
if l[i] != r[i] {
return false
}
}
return true
}

func TestMockIO(t *testing.T) {
m := NewMockIO()
exp := []byte{3, 4}
m.Expect(NewExpectBytes([]byte{1, 2}, exp))
m.Write([]byte{1, 2})
b := make([]byte, 2)
n, err := m.Read(b)
if err != nil {
t.Error(err)
}
if n != 2 {
t.Errorf("expected %d, received %d", len(exp), n)
}

if !MatchBytes(b, exp) {
t.Errorf("expected %#v but received %#v", exp, b)
}

m.Write([]byte{1, 2, 1, 2})

b = make([]byte, 2)
n, err = m.Read(b)
if err != nil {
t.Error(err)
}
if n != 2 {
t.Errorf("expected %d, received %d", len(exp), n)
}

if !MatchBytes(b, exp) {
t.Errorf("expected %#v but received %#v", exp, b)
}

l := len(m.holding)
if l != 2 {
t.Errorf("expected 2 bytes holding, but found %d", l)
}
}

func TestMockIOFunc(t *testing.T) {
m := NewMockIO()
test := func(b []byte) (count int, ok bool) {
if len(b) != 2 {
return 0, false
}
if b[0] != 0 && b[1] != 1 {
return 0, false
}
return 2, true
}
exp := []byte{3, 4}
m.Expect(NewExpectFunc(test, exp))
m.Write([]byte{0, 1})
b := make([]byte, 2)
n, err := m.Read(b)
if err != nil {
t.Error(err)
}
if n != 2 {
t.Errorf("expected %d, received %d", len(exp), n)
}

if !MatchBytes(b, exp) {
t.Errorf("expected %#v but received %#v", exp, b)
}

log.Printf("received: %#v\n", b)
}

func TestMockIOFail(t *testing.T) {
m := NewMockIO()
exp := []byte{3, 4}
m.Expect(NewExpectBytes([]byte{1, 2}, exp))
m.Write([]byte{0, 2})
b := make([]byte, 2)
n, err := m.Read(b)
if err == nil {
t.Errorf("Expected failure")
}
if err != io.EOF {
t.Errorf("expected EOF but got: %s", err)
}
if n != 0 {
t.Errorf("expected no data, received %d", n)
}
l := len(m.buffer.Bytes())
if l != 0 {
t.Errorf("expected no bytes in buffer, but found %d: %#v", l, m.buffer.Bytes())
}

m.Write([]byte{1, 4})
b = make([]byte, 2)
n, err = m.Read(b)
if err == nil {
t.Errorf("Expected failure")
}
if err != io.EOF {
t.Errorf("expected EOF but got: %s", err)
}
if n != 0 {
t.Errorf("expected no data, received %d", n)
}
l = len(m.buffer.Bytes())
if l != 0 {
t.Errorf("expected no bytes in buffer, but found %d: %#v", l, m.buffer.Bytes())
}
}

func TestMockClear(t *testing.T) {
m := NewMockIO()
exp := []byte{3, 4}
m.Expect(NewExpectBytes([]byte{1, 2}, exp))
l := len(m.Expects)
if l != 1 {
t.Errorf("expected 1 item, but found %d", l)
}
m.ClearExpectations()
l = len(m.Expects)
if l != 0 {
t.Errorf("expected 0 items, but found %d", l)
}

m.Expect(NewExpectBytes([]byte{1, 2}, exp))
m.Write([]byte{3, 4})
l = len(m.holding)
if l != 2 {
t.Errorf("holding should have 2 items, but found %d", l)
}
m.Close()
l = len(m.holding)
if l != 0 {
t.Errorf("holding should have no items, but found %d", l)
}
}

0 comments on commit cc52d8d

Please sign in to comment.