Refactor remaining tests to subtest style

This commit is contained in:
Marius 2016-10-13 18:08:34 +02:00
parent 5be9dedae2
commit c2bb17b947
7 changed files with 343 additions and 417 deletions

View File

@ -8,37 +8,42 @@ import (
) )
func TestCORS(t *testing.T) { func TestCORS(t *testing.T) {
store := NewStoreComposer() SubTest(t, "Preflight", func(t *testing.T, store *MockFullDataStore) {
store.UseCore(zeroStore{}) handler, _ := NewHandler(Config{
handler, _ := NewHandler(Config{ DataStore: store,
StoreComposer: store, })
(&httpTest{
Method: "OPTIONS",
ReqHeader: map[string]string{
"Origin": "tus.io",
},
Code: http.StatusOK,
ResHeader: map[string]string{
"Access-Control-Allow-Headers": "Origin, X-Requested-With, Content-Type, Upload-Length, Upload-Offset, Tus-Resumable, Upload-Metadata",
"Access-Control-Allow-Methods": "POST, GET, HEAD, PATCH, DELETE, OPTIONS",
"Access-Control-Max-Age": "86400",
"Access-Control-Allow-Origin": "tus.io",
},
}).Run(handler, t)
}) })
(&httpTest{ SubTest(t, "Request", func(t *testing.T, store *MockFullDataStore) {
Name: "Preflight request", handler, _ := NewHandler(Config{
Method: "OPTIONS", DataStore: store,
ReqHeader: map[string]string{ })
"Origin": "tus.io",
},
Code: http.StatusOK,
ResHeader: map[string]string{
"Access-Control-Allow-Headers": "Origin, X-Requested-With, Content-Type, Upload-Length, Upload-Offset, Tus-Resumable, Upload-Metadata",
"Access-Control-Allow-Methods": "POST, GET, HEAD, PATCH, DELETE, OPTIONS",
"Access-Control-Max-Age": "86400",
"Access-Control-Allow-Origin": "tus.io",
},
}).Run(handler, t)
(&httpTest{ (&httpTest{
Name: "Actual request", Name: "Actual request",
Method: "GET", Method: "GET",
ReqHeader: map[string]string{ ReqHeader: map[string]string{
"Origin": "tus.io", "Origin": "tus.io",
}, },
Code: http.StatusMethodNotAllowed, Code: http.StatusMethodNotAllowed,
ResHeader: map[string]string{ ResHeader: map[string]string{
"Access-Control-Expose-Headers": "Upload-Offset, Location, Upload-Length, Tus-Version, Tus-Resumable, Tus-Max-Size, Tus-Extension, Upload-Metadata", "Access-Control-Expose-Headers": "Upload-Offset, Location, Upload-Length, Tus-Version, Tus-Resumable, Tus-Max-Size, Tus-Extension, Upload-Metadata",
"Access-Control-Allow-Origin": "tus.io", "Access-Control-Allow-Origin": "tus.io",
}, },
}).Run(handler, t) }).Run(handler, t)
})
} }

View File

@ -19,12 +19,12 @@ func (reader *closingStringReader) Close() error {
return nil return nil
} }
var reader = &closingStringReader{
Reader: strings.NewReader("hello"),
}
func TestGet(t *testing.T) { func TestGet(t *testing.T) {
SubTest(t, "Download", func(t *testing.T, store *MockFullDataStore) { SubTest(t, "Download", func(t *testing.T, store *MockFullDataStore) {
reader := &closingStringReader{
Reader: strings.NewReader("hello"),
}
gomock.InOrder( gomock.InOrder(
store.EXPECT().GetInfo("yes").Return(FileInfo{ store.EXPECT().GetInfo("yes").Return(FileInfo{
Offset: 5, Offset: 5,

View File

@ -1,5 +1,5 @@
// Automatically generated by MockGen. DO NOT EDIT! // Automatically generated by MockGen. DO NOT EDIT!
// Source: handler_test.go // Source: utils_test.go
package tusd_test package tusd_test
@ -94,3 +94,44 @@ func (_m *MockFullDataStore) GetReader(id string) (io.Reader, error) {
func (_mr *_MockFullDataStoreRecorder) GetReader(arg0 interface{}) *gomock.Call { func (_mr *_MockFullDataStoreRecorder) GetReader(arg0 interface{}) *gomock.Call {
return _mr.mock.ctrl.RecordCall(_mr.mock, "GetReader", arg0) return _mr.mock.ctrl.RecordCall(_mr.mock, "GetReader", arg0)
} }
// Mock of Locker interface
type MockLocker struct {
ctrl *gomock.Controller
recorder *_MockLockerRecorder
}
// Recorder for MockLocker (not exported)
type _MockLockerRecorder struct {
mock *MockLocker
}
func NewMockLocker(ctrl *gomock.Controller) *MockLocker {
mock := &MockLocker{ctrl: ctrl}
mock.recorder = &_MockLockerRecorder{mock}
return mock
}
func (_m *MockLocker) EXPECT() *_MockLockerRecorder {
return _m.recorder
}
func (_m *MockLocker) LockUpload(id string) error {
ret := _m.ctrl.Call(_m, "LockUpload", id)
ret0, _ := ret[0].(error)
return ret0
}
func (_mr *_MockLockerRecorder) LockUpload(arg0 interface{}) *gomock.Call {
return _mr.mock.ctrl.RecordCall(_mr.mock, "LockUpload", arg0)
}
func (_m *MockLocker) UnlockUpload(id string) error {
ret := _m.ctrl.Call(_m, "UnlockUpload", id)
ret0, _ := ret[0].(error)
return ret0
}
func (_mr *_MockLockerRecorder) UnlockUpload(arg0 interface{}) *gomock.Call {
return _mr.mock.ctrl.RecordCall(_mr.mock, "UnlockUpload", arg0)
}

View File

@ -1,45 +0,0 @@
package tusd_test
import (
"net/http"
"strings"
"testing"
"github.com/golang/mock/gomock"
. "github.com/tus/tusd"
)
func TestMethodOverride(t *testing.T) {
mockCtrl := gomock.NewController(t)
defer mockCtrl.Finish()
store := NewMockFullDataStore(mockCtrl)
store.EXPECT().GetInfo("yes").Return(FileInfo{
Offset: 5,
Size: 20,
}, nil)
store.EXPECT().WriteChunk("yes", int64(5), NewReaderMatcher("hello")).Return(int64(5), nil)
handler, _ := NewHandler(Config{
DataStore: store,
})
(&httpTest{
Name: "Successful request",
Method: "POST",
URL: "yes",
ReqHeader: map[string]string{
"Tus-Resumable": "1.0.0",
"Upload-Offset": "5",
"Content-Type": "application/offset+octet-stream",
"X-HTTP-Method-Override": "PATCH",
},
ReqBody: strings.NewReader("hello"),
Code: http.StatusNoContent,
ResHeader: map[string]string{
"Upload-Offset": "10",
},
}).Run(handler, t)
}

View File

@ -8,31 +8,38 @@ import (
) )
func TestOptions(t *testing.T) { func TestOptions(t *testing.T) {
store := NewStoreComposer() SubTest(t, "Discovery", func(t *testing.T, store *MockFullDataStore) {
store.UseCore(NewMockFullDataStore(nil)) composer := NewStoreComposer()
handler, _ := NewHandler(Config{ composer.UseCore(store)
StoreComposer: store,
MaxSize: 400, handler, _ := NewHandler(Config{
StoreComposer: composer,
MaxSize: 400,
})
(&httpTest{
Method: "OPTIONS",
ResHeader: map[string]string{
"Tus-Extension": "creation,creation-with-upload",
"Tus-Version": "1.0.0",
"Tus-Resumable": "1.0.0",
"Tus-Max-Size": "400",
},
Code: http.StatusOK,
}).Run(handler, t)
}) })
(&httpTest{ SubTest(t, "InvalidVersion", func(t *testing.T, store *MockFullDataStore) {
Name: "Successful request", handler, _ := NewHandler(Config{
Method: "OPTIONS", DataStore: store,
Code: http.StatusOK, })
ResHeader: map[string]string{
"Tus-Extension": "creation,creation-with-upload",
"Tus-Version": "1.0.0",
"Tus-Resumable": "1.0.0",
"Tus-Max-Size": "400",
},
}).Run(handler, t)
(&httpTest{ (&httpTest{
Name: "Invalid or unsupported version", Method: "POST",
Method: "POST", ReqHeader: map[string]string{
ReqHeader: map[string]string{ "Tus-Resumable": "foo",
"Tus-Resumable": "foo", },
}, Code: http.StatusPreconditionFailed,
Code: http.StatusPreconditionFailed, }).Run(handler, t)
}).Run(handler, t) })
} }

View File

@ -1,326 +1,254 @@
package tusd_test package tusd_test
import ( import (
"io"
"io/ioutil" "io/ioutil"
"net/http" "net/http"
"os" "os"
"strings" "strings"
"testing" "testing"
"github.com/golang/mock/gomock"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
. "github.com/tus/tusd" . "github.com/tus/tusd"
) )
type patchStore struct {
zeroStore
t *assert.Assertions
called bool
}
func (s patchStore) GetInfo(id string) (FileInfo, error) {
if id != "yes" {
return FileInfo{}, os.ErrNotExist
}
return FileInfo{
ID: id,
Offset: 5,
Size: 10,
}, nil
}
func (s patchStore) WriteChunk(id string, offset int64, src io.Reader) (int64, error) {
s.t.False(s.called, "WriteChunk must be called only once")
s.called = true
s.t.Equal(int64(5), offset)
data, err := ioutil.ReadAll(src)
s.t.Nil(err)
s.t.Equal("hello", string(data))
return 5, nil
}
func TestPatch(t *testing.T) { func TestPatch(t *testing.T) {
a := assert.New(t) SubTest(t, "UploadChunk", func(t *testing.T, store *MockFullDataStore) {
gomock.InOrder(
store.EXPECT().GetInfo("yes").Return(FileInfo{
ID: "yes",
Offset: 5,
Size: 10,
}, nil),
store.EXPECT().WriteChunk("yes", int64(5), NewReaderMatcher("hello")).Return(int64(5), nil),
)
handler, _ := NewHandler(Config{ handler, _ := NewHandler(Config{
MaxSize: 100, DataStore: store,
DataStore: patchStore{ NotifyCompleteUploads: true,
t: a, })
},
NotifyCompleteUploads: true, c := make(chan FileInfo, 1)
handler.CompleteUploads = c
(&httpTest{
Method: "PATCH",
URL: "yes",
ReqHeader: map[string]string{
"Tus-Resumable": "1.0.0",
"Content-Type": "application/offset+octet-stream",
"Upload-Offset": "5",
},
ReqBody: strings.NewReader("hello"),
Code: http.StatusNoContent,
ResHeader: map[string]string{
"Upload-Offset": "10",
},
}).Run(handler, t)
a := assert.New(t)
info := <-c
a.Equal("yes", info.ID)
a.EqualValues(int64(10), info.Size)
a.Equal(int64(10), info.Offset)
}) })
c := make(chan FileInfo, 1) SubTest(t, "MethodOverriding", func(t *testing.T, store *MockFullDataStore) {
handler.CompleteUploads = c gomock.InOrder(
store.EXPECT().GetInfo("yes").Return(FileInfo{
ID: "yes",
Offset: 5,
Size: 10,
}, nil),
store.EXPECT().WriteChunk("yes", int64(5), NewReaderMatcher("hello")).Return(int64(5), nil),
)
(&httpTest{ handler, _ := NewHandler(Config{
Name: "Successful request", DataStore: store,
Method: "PATCH", })
URL: "yes",
ReqHeader: map[string]string{
"Tus-Resumable": "1.0.0",
"Content-Type": "application/offset+octet-stream",
"Upload-Offset": "5",
},
ReqBody: strings.NewReader("hello"),
Code: http.StatusNoContent,
ResHeader: map[string]string{
"Upload-Offset": "10",
},
}).Run(handler, t)
info := <-c (&httpTest{
a.Equal("yes", info.ID) Method: "POST",
a.Equal(int64(10), info.Size) URL: "yes",
a.Equal(int64(10), info.Offset) ReqHeader: map[string]string{
"Tus-Resumable": "1.0.0",
(&httpTest{ "Upload-Offset": "5",
Name: "Non-existing file", "Content-Type": "application/offset+octet-stream",
Method: "PATCH", "X-HTTP-Method-Override": "PATCH",
URL: "no", },
ReqHeader: map[string]string{ ReqBody: strings.NewReader("hello"),
"Tus-Resumable": "1.0.0", Code: http.StatusNoContent,
"Content-Type": "application/offset+octet-stream", ResHeader: map[string]string{
"Upload-Offset": "5", "Upload-Offset": "10",
}, },
Code: http.StatusNotFound, }).Run(handler, t)
}).Run(handler, t)
(&httpTest{
Name: "Wrong offset",
Method: "PATCH",
URL: "yes",
ReqHeader: map[string]string{
"Tus-Resumable": "1.0.0",
"Content-Type": "application/offset+octet-stream",
"Upload-Offset": "4",
},
Code: http.StatusConflict,
}).Run(handler, t)
(&httpTest{
Name: "Exceeding file size",
Method: "PATCH",
URL: "yes",
ReqHeader: map[string]string{
"Tus-Resumable": "1.0.0",
"Content-Type": "application/offset+octet-stream",
"Upload-Offset": "5",
},
ReqBody: strings.NewReader("hellothisismorethan15bytes"),
Code: http.StatusRequestEntityTooLarge,
}).Run(handler, t)
}
type overflowPatchStore struct {
zeroStore
t *assert.Assertions
called bool
}
func (s overflowPatchStore) GetInfo(id string) (FileInfo, error) {
if id != "yes" {
return FileInfo{}, os.ErrNotExist
}
return FileInfo{
Offset: 5,
Size: 20,
}, nil
}
func (s overflowPatchStore) WriteChunk(id string, offset int64, src io.Reader) (int64, error) {
s.t.False(s.called, "WriteChunk must be called only once")
s.called = true
s.t.Equal(int64(5), offset)
data, err := ioutil.ReadAll(src)
s.t.Nil(err)
s.t.Equal("hellothisismore", string(data))
return 15, nil
}
// noEOFReader implements io.Reader, io.Writer, io.Closer but does not return
// an io.EOF when the internal buffer is empty. This way we can simulate slow
// networks.
type noEOFReader struct {
closed bool
buffer []byte
}
func (r *noEOFReader) Read(dst []byte) (int, error) {
if r.closed && len(r.buffer) == 0 {
return 0, io.EOF
}
n := copy(dst, r.buffer)
r.buffer = r.buffer[n:]
return n, nil
}
func (r *noEOFReader) Close() error {
r.closed = true
return nil
}
func (r *noEOFReader) Write(src []byte) (int, error) {
r.buffer = append(r.buffer, src...)
return len(src), nil
}
func TestPatchOverflow(t *testing.T) {
handler, _ := NewHandler(Config{
MaxSize: 100,
DataStore: overflowPatchStore{
t: assert.New(t),
},
}) })
body := &noEOFReader{} SubTest(t, "UploadChunkToFinished", func(t *testing.T, store *MockFullDataStore) {
store.EXPECT().GetInfo("yes").Return(FileInfo{
Offset: 20,
Size: 20,
}, nil)
body.Write([]byte("hellothisismorethan15bytes")) handler, _ := NewHandler(Config{
body.Close() DataStore: store,
})
(&httpTest{ (&httpTest{
Name: "Too big body exceeding file size", Method: "PATCH",
Method: "PATCH", URL: "yes",
URL: "yes", ReqHeader: map[string]string{
ReqHeader: map[string]string{ "Tus-Resumable": "1.0.0",
"Tus-Resumable": "1.0.0", "Content-Type": "application/offset+octet-stream",
"Content-Type": "application/offset+octet-stream", "Upload-Offset": "20",
"Upload-Offset": "5", },
"Content-Length": "3", ReqBody: strings.NewReader(""),
}, Code: http.StatusNoContent,
ReqBody: body, ResHeader: map[string]string{
Code: http.StatusNoContent, "Upload-Offset": "20",
}).Run(handler, t) },
} }).Run(handler, t)
const (
LOCK = iota
INFO
WRITE
UNLOCK
END
)
type lockingPatchStore struct {
zeroStore
callOrder chan int
}
func (s lockingPatchStore) GetInfo(id string) (FileInfo, error) {
s.callOrder <- INFO
return FileInfo{
Offset: 0,
Size: 20,
}, nil
}
func (s lockingPatchStore) WriteChunk(id string, offset int64, src io.Reader) (int64, error) {
s.callOrder <- WRITE
return 5, nil
}
func (s lockingPatchStore) LockUpload(id string) error {
s.callOrder <- LOCK
return nil
}
func (s lockingPatchStore) UnlockUpload(id string) error {
s.callOrder <- UNLOCK
return nil
}
func TestLockingPatch(t *testing.T) {
callOrder := make(chan int, 10)
handler, _ := NewHandler(Config{
DataStore: lockingPatchStore{
callOrder: callOrder,
},
}) })
(&httpTest{ SubTest(t, "UploadNotFoundFail", func(t *testing.T, store *MockFullDataStore) {
Name: "Uploading to locking store", store.EXPECT().GetInfo("no").Return(FileInfo{}, os.ErrNotExist)
Method: "PATCH",
URL: "yes",
ReqHeader: map[string]string{
"Tus-Resumable": "1.0.0",
"Content-Type": "application/offset+octet-stream",
"Upload-Offset": "0",
},
ReqBody: strings.NewReader("hello"),
Code: http.StatusNoContent,
}).Run(handler, t)
callOrder <- END handler, _ := NewHandler(Config{
close(callOrder) DataStore: store,
})
if <-callOrder != LOCK { (&httpTest{
t.Error("expected call to LockUpload") Method: "PATCH",
} URL: "no",
ReqHeader: map[string]string{
if <-callOrder != INFO { "Tus-Resumable": "1.0.0",
t.Error("expected call to GetInfo") "Content-Type": "application/offset+octet-stream",
} "Upload-Offset": "5",
},
if <-callOrder != WRITE { Code: http.StatusNotFound,
t.Error("expected call to WriteChunk") }).Run(handler, t)
}
if <-callOrder != UNLOCK {
t.Error("expected call to UnlockUpload")
}
if <-callOrder != END {
t.Error("expected no more calls to happen")
}
}
type finishedPatchStore struct {
zeroStore
}
func (s finishedPatchStore) GetInfo(id string) (FileInfo, error) {
return FileInfo{
Offset: 20,
Size: 20,
}, nil
}
func (s finishedPatchStore) WriteChunk(id string, offset int64, src io.Reader) (int64, error) {
panic("WriteChunk must not be called")
}
func TestFinishedPatch(t *testing.T) {
handler, _ := NewHandler(Config{
DataStore: finishedPatchStore{},
}) })
(&httpTest{ SubTest(t, "MissmatchingOffsetFail", func(t *testing.T, store *MockFullDataStore) {
Name: "Uploading to finished upload", store.EXPECT().GetInfo("yes").Return(FileInfo{
Method: "PATCH", Offset: 5,
URL: "yes", }, nil)
ReqHeader: map[string]string{
"Tus-Resumable": "1.0.0", handler, _ := NewHandler(Config{
"Content-Type": "application/offset+octet-stream", DataStore: store,
"Upload-Offset": "20", })
},
ReqBody: strings.NewReader(""), (&httpTest{
Code: http.StatusNoContent, Method: "PATCH",
ResHeader: map[string]string{ URL: "yes",
"Upload-Offset": "20", ReqHeader: map[string]string{
}, "Tus-Resumable": "1.0.0",
}).Run(handler, t) "Content-Type": "application/offset+octet-stream",
"Upload-Offset": "4",
},
Code: http.StatusConflict,
}).Run(handler, t)
})
SubTest(t, "ExceedingMaxSizeFail", func(t *testing.T, store *MockFullDataStore) {
store.EXPECT().GetInfo("yes").Return(FileInfo{
Offset: 5,
Size: 10,
}, nil)
handler, _ := NewHandler(Config{
DataStore: store,
})
(&httpTest{
Method: "PATCH",
URL: "yes",
ReqHeader: map[string]string{
"Tus-Resumable": "1.0.0",
"Content-Type": "application/offset+octet-stream",
"Upload-Offset": "5",
},
ReqBody: strings.NewReader("hellothisismorethan15bytes"),
Code: http.StatusRequestEntityTooLarge,
}).Run(handler, t)
})
SubTest(t, "OverflowWithoutLength", func(t *testing.T, store *MockFullDataStore) {
// In this test we attempt to upload more than 15 bytes to an upload
// which has only space for 15 bytes (offset of 5 and size of 20).
// The request does not contain the Content-Length header and the handler
// therefore does not know the chunk's size before. The wanted behavior
// is that even if the uploader supplies more than 15 bytes, we only
// pass 15 bytes to the data store and ignore the rest.
gomock.InOrder(
store.EXPECT().GetInfo("yes").Return(FileInfo{
Offset: 5,
Size: 20,
}, nil),
store.EXPECT().WriteChunk("yes", int64(5), NewReaderMatcher("hellothisismore")).Return(int64(15), nil),
)
handler, _ := NewHandler(Config{
DataStore: store,
})
// Wrap the string.Reader in a NopCloser to hide its type. else
// http.NewRequest() will detect the we supply a strings.Reader as body
// and use this information to set the Content-Length header which we
// explicitly do not want (see comment above for reason).
body := ioutil.NopCloser(strings.NewReader("hellothisismorethan15bytes"))
(&httpTest{
Method: "PATCH",
URL: "yes",
ReqHeader: map[string]string{
"Tus-Resumable": "1.0.0",
"Content-Type": "application/offset+octet-stream",
"Upload-Offset": "5",
},
ReqBody: body,
Code: http.StatusNoContent,
ResHeader: map[string]string{
"Upload-Offset": "20",
},
}).Run(handler, t)
})
SubTest(t, "Locker", func(t *testing.T, store *MockFullDataStore) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
locker := NewMockLocker(ctrl)
gomock.InOrder(
locker.EXPECT().LockUpload("yes").Return(nil),
store.EXPECT().GetInfo("yes").Return(FileInfo{
Offset: 0,
Size: 20,
}, nil),
store.EXPECT().WriteChunk("yes", int64(0), NewReaderMatcher("hello")).Return(int64(5), nil),
locker.EXPECT().UnlockUpload("yes").Return(nil),
)
composer := NewStoreComposer()
composer.UseCore(store)
composer.UseLocker(locker)
handler, _ := NewHandler(Config{
StoreComposer: composer,
})
(&httpTest{
Method: "PATCH",
URL: "yes",
ReqHeader: map[string]string{
"Tus-Resumable": "1.0.0",
"Content-Type": "application/offset+octet-stream",
"Upload-Offset": "0",
},
ReqBody: strings.NewReader("hello"),
Code: http.StatusNoContent,
}).Run(handler, t)
})
} }

View File

@ -11,22 +11,8 @@ import (
"github.com/golang/mock/gomock" "github.com/golang/mock/gomock"
"github.com/tus/tusd" "github.com/tus/tusd"
. "github.com/tus/tusd"
) )
type zeroStore struct{}
func (store zeroStore) NewUpload(info FileInfo) (string, error) {
return "", nil
}
func (store zeroStore) WriteChunk(id string, offset int64, src io.Reader) (int64, error) {
return 0, nil
}
func (store zeroStore) GetInfo(id string) (FileInfo, error) {
return FileInfo{}, nil
}
type FullDataStore interface { type FullDataStore interface {
tusd.DataStore tusd.DataStore
tusd.TerminaterDataStore tusd.TerminaterDataStore
@ -34,6 +20,10 @@ type FullDataStore interface {
tusd.GetReaderDataStore tusd.GetReaderDataStore
} }
type Locker interface {
tusd.LockerDataStore
}
type httpTest struct { type httpTest struct {
Name string Name string
@ -79,17 +69,17 @@ func (test *httpTest) Run(handler http.Handler, t *testing.T) *httptest.Response
return w return w
} }
type ReaderMatcher struct { type readerMatcher struct {
expect string expect string
} }
func NewReaderMatcher(expect string) gomock.Matcher { func NewReaderMatcher(expect string) gomock.Matcher {
return ReaderMatcher{ return readerMatcher{
expect: expect, expect: expect,
} }
} }
func (m ReaderMatcher) Matches(x interface{}) bool { func (m readerMatcher) Matches(x interface{}) bool {
input, ok := x.(io.Reader) input, ok := x.(io.Reader)
if !ok { if !ok {
return false return false
@ -104,6 +94,6 @@ func (m ReaderMatcher) Matches(x interface{}) bool {
return reflect.DeepEqual(m.expect, readStr) return reflect.DeepEqual(m.expect, readStr)
} }
func (m ReaderMatcher) String() string { func (m readerMatcher) String() string {
return fmt.Sprintf("reads to %s", m.expect) return fmt.Sprintf("reads to %s", m.expect)
} }