From cc52d8dc639fdf3e866ad153a7673bbd5eeca563 Mon Sep 17 00:00:00 2001 From: Trey Van Riper Date: Thu, 9 Jun 2022 10:07:00 -0400 Subject: [PATCH] first cut --- README.md | 7 +++ expect.go | 72 +++++++++++++++++++++++ go.mod | 3 + mockio.go | 83 ++++++++++++++++++++++++++ mockio_test.go | 156 +++++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 321 insertions(+) create mode 100644 expect.go create mode 100644 go.mod create mode 100644 mockio.go create mode 100644 mockio_test.go diff --git a/README.md b/README.md index 139ec0d..ad9c653 100644 --- a/README.md +++ b/README.md @@ -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. + diff --git a/expect.go b/expect.go new file mode 100644 index 0000000..8dcc3c2 --- /dev/null +++ b/expect.go @@ -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, + } +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..39e3bd3 --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module github.com/tvanriper/go-mock-io + +go 1.18 diff --git a/mockio.go b/mockio.go new file mode 100644 index 0000000..f784858 --- /dev/null +++ b/mockio.go @@ -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 +} diff --git a/mockio_test.go b/mockio_test.go new file mode 100644 index 0000000..0cdf24f --- /dev/null +++ b/mockio_test.go @@ -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) + } +}