1/* 2Package gbytes provides a buffer that supports incrementally detecting input. 3 4You use gbytes.Buffer with the gbytes.Say matcher. When Say finds a match, it fastforwards the buffer's read cursor to the end of that match. 5 6Subsequent matches against the buffer will only operate against data that appears *after* the read cursor. 7 8The read cursor is an opaque implementation detail that you cannot access. You should use the Say matcher to sift through the buffer. You can always 9access the entire buffer's contents with Contents(). 10 11*/ 12package gbytes 13 14import ( 15 "errors" 16 "fmt" 17 "io" 18 "regexp" 19 "sync" 20 "time" 21) 22 23/* 24gbytes.Buffer implements an io.Writer and can be used with the gbytes.Say matcher. 25 26You should only use a gbytes.Buffer in test code. It stores all writes in an in-memory buffer - behavior that is inappropriate for production code! 27*/ 28type Buffer struct { 29 contents []byte 30 readCursor uint64 31 lock *sync.Mutex 32 detectCloser chan interface{} 33 closed bool 34} 35 36/* 37NewBuffer returns a new gbytes.Buffer 38*/ 39func NewBuffer() *Buffer { 40 return &Buffer{ 41 lock: &sync.Mutex{}, 42 } 43} 44 45/* 46BufferWithBytes returns a new gbytes.Buffer seeded with the passed in bytes 47*/ 48func BufferWithBytes(bytes []byte) *Buffer { 49 return &Buffer{ 50 lock: &sync.Mutex{}, 51 contents: bytes, 52 } 53} 54 55/* 56BufferReader returns a new gbytes.Buffer that wraps a reader. The reader's contents are read into 57the Buffer via io.Copy 58*/ 59func BufferReader(reader io.Reader) *Buffer { 60 b := &Buffer{ 61 lock: &sync.Mutex{}, 62 } 63 64 go func() { 65 io.Copy(b, reader) 66 b.Close() 67 }() 68 69 return b 70} 71 72/* 73Write implements the io.Writer interface 74*/ 75func (b *Buffer) Write(p []byte) (n int, err error) { 76 b.lock.Lock() 77 defer b.lock.Unlock() 78 79 if b.closed { 80 return 0, errors.New("attempt to write to closed buffer") 81 } 82 83 b.contents = append(b.contents, p...) 84 return len(p), nil 85} 86 87/* 88Read implements the io.Reader interface. It advances the 89cursor as it reads. 90 91Returns an error if called after Close. 92*/ 93func (b *Buffer) Read(d []byte) (int, error) { 94 b.lock.Lock() 95 defer b.lock.Unlock() 96 97 if b.closed { 98 return 0, errors.New("attempt to read from closed buffer") 99 } 100 101 if uint64(len(b.contents)) <= b.readCursor { 102 return 0, io.EOF 103 } 104 105 n := copy(d, b.contents[b.readCursor:]) 106 b.readCursor += uint64(n) 107 108 return n, nil 109} 110 111/* 112Close signifies that the buffer will no longer be written to 113*/ 114func (b *Buffer) Close() error { 115 b.lock.Lock() 116 defer b.lock.Unlock() 117 118 b.closed = true 119 120 return nil 121} 122 123/* 124Closed returns true if the buffer has been closed 125*/ 126func (b *Buffer) Closed() bool { 127 b.lock.Lock() 128 defer b.lock.Unlock() 129 130 return b.closed 131} 132 133/* 134Contents returns all data ever written to the buffer. 135*/ 136func (b *Buffer) Contents() []byte { 137 b.lock.Lock() 138 defer b.lock.Unlock() 139 140 contents := make([]byte, len(b.contents)) 141 copy(contents, b.contents) 142 return contents 143} 144 145/* 146Detect takes a regular expression and returns a channel. 147 148The channel will receive true the first time data matching the regular expression is written to the buffer. 149The channel is subsequently closed and the buffer's read-cursor is fast-forwarded to just after the matching region. 150 151You typically don't need to use Detect and should use the ghttp.Say matcher instead. Detect is useful, however, in cases where your code must 152be branch and handle different outputs written to the buffer. 153 154For example, consider a buffer hooked up to the stdout of a client library. You may (or may not, depending on state outside of your control) need to authenticate the client library. 155 156You could do something like: 157 158select { 159case <-buffer.Detect("You are not logged in"): 160 //log in 161case <-buffer.Detect("Success"): 162 //carry on 163case <-time.After(time.Second): 164 //welp 165} 166buffer.CancelDetects() 167 168You should always call CancelDetects after using Detect. This will close any channels that have not detected and clean up the goroutines that were spawned to support them. 169 170Finally, you can pass detect a format string followed by variadic arguments. This will construct the regexp using fmt.Sprintf. 171*/ 172func (b *Buffer) Detect(desired string, args ...interface{}) chan bool { 173 formattedRegexp := desired 174 if len(args) > 0 { 175 formattedRegexp = fmt.Sprintf(desired, args...) 176 } 177 re := regexp.MustCompile(formattedRegexp) 178 179 b.lock.Lock() 180 defer b.lock.Unlock() 181 182 if b.detectCloser == nil { 183 b.detectCloser = make(chan interface{}) 184 } 185 186 closer := b.detectCloser 187 response := make(chan bool) 188 go func() { 189 ticker := time.NewTicker(10 * time.Millisecond) 190 defer ticker.Stop() 191 defer close(response) 192 for { 193 select { 194 case <-ticker.C: 195 b.lock.Lock() 196 data, cursor := b.contents[b.readCursor:], b.readCursor 197 loc := re.FindIndex(data) 198 b.lock.Unlock() 199 200 if loc != nil { 201 response <- true 202 b.lock.Lock() 203 newCursorPosition := cursor + uint64(loc[1]) 204 if newCursorPosition >= b.readCursor { 205 b.readCursor = newCursorPosition 206 } 207 b.lock.Unlock() 208 return 209 } 210 case <-closer: 211 return 212 } 213 } 214 }() 215 216 return response 217} 218 219/* 220CancelDetects cancels any pending detects and cleans up their goroutines. You should always call this when you're done with a set of Detect channels. 221*/ 222func (b *Buffer) CancelDetects() { 223 b.lock.Lock() 224 defer b.lock.Unlock() 225 226 close(b.detectCloser) 227 b.detectCloser = nil 228} 229 230func (b *Buffer) didSay(re *regexp.Regexp) (bool, []byte) { 231 b.lock.Lock() 232 defer b.lock.Unlock() 233 234 unreadBytes := b.contents[b.readCursor:] 235 copyOfUnreadBytes := make([]byte, len(unreadBytes)) 236 copy(copyOfUnreadBytes, unreadBytes) 237 238 loc := re.FindIndex(unreadBytes) 239 240 if loc != nil { 241 b.readCursor += uint64(loc[1]) 242 return true, copyOfUnreadBytes 243 } 244 return false, copyOfUnreadBytes 245} 246