go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/logdog/common/fetcher/fetcher_test.go (about) 1 // Copyright 2015 The LUCI Authors. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package fetcher 16 17 import ( 18 "context" 19 "errors" 20 "io" 21 "sync" 22 "testing" 23 "time" 24 25 "github.com/golang/protobuf/proto" 26 . "github.com/smartystreets/goconvey/convey" 27 "go.chromium.org/luci/common/clock" 28 "go.chromium.org/luci/common/clock/testclock" 29 "go.chromium.org/luci/common/testing/assertions" 30 "go.chromium.org/luci/logdog/api/logpb" 31 "go.chromium.org/luci/logdog/common/types" 32 ) 33 34 type testSourceCommand struct { 35 logEntries []*logpb.LogEntry 36 37 err error 38 panic bool 39 40 tidx types.MessageIndex 41 tidxSet bool 42 } 43 44 // addLogs adds a text log entry for each named indices. The text entry contains 45 // a single line, "#x", where "x" is the log index. 46 func (cmd *testSourceCommand) logs(indices ...int64) *testSourceCommand { 47 for _, idx := range indices { 48 cmd.logEntries = append(cmd.logEntries, &logpb.LogEntry{ 49 StreamIndex: uint64(idx), 50 }) 51 } 52 return cmd 53 } 54 55 func (cmd *testSourceCommand) terminalIndex(v types.MessageIndex) *testSourceCommand { 56 cmd.tidx, cmd.tidxSet = v, true 57 return cmd 58 } 59 60 func (cmd *testSourceCommand) error(err error, panic bool) *testSourceCommand { 61 cmd.err, cmd.panic = err, panic 62 return cmd 63 } 64 65 type testSource struct { 66 sync.Mutex 67 68 logs map[types.MessageIndex]*logpb.LogEntry 69 err error 70 panic bool 71 terminal types.MessageIndex 72 73 history []int 74 } 75 76 func newTestSource() *testSource { 77 return &testSource{ 78 terminal: -1, 79 logs: make(map[types.MessageIndex]*logpb.LogEntry), 80 } 81 } 82 83 func (ts *testSource) Descriptor() *logpb.LogStreamDescriptor { return nil } 84 85 func (ts *testSource) LogEntries(c context.Context, req *LogRequest) ([]*logpb.LogEntry, types.MessageIndex, error) { 86 ts.Lock() 87 defer ts.Unlock() 88 89 if ts.err != nil { 90 if ts.panic { 91 panic(ts.err) 92 } 93 return nil, 0, ts.err 94 } 95 96 // We have at least our next log. Build our return value. 97 maxCount := req.Count 98 if maxCount <= 0 { 99 maxCount = len(ts.logs) 100 } 101 maxBytes := req.Bytes 102 103 var logs []*logpb.LogEntry 104 bytes := int64(0) 105 index := req.Index 106 for { 107 if len(logs) >= maxCount { 108 break 109 } 110 111 log, ok := ts.logs[index] 112 if !ok { 113 break 114 } 115 116 size := int64(5) // We've rigged all logs to have size 5. 117 if len(logs) > 0 && maxBytes > 0 && (bytes+size) > maxBytes { 118 break 119 } 120 logs = append(logs, log) 121 index++ 122 bytes += size 123 } 124 ts.history = append(ts.history, len(logs)) 125 return logs, ts.terminal, nil 126 } 127 128 func (ts *testSource) send(cmd *testSourceCommand) { 129 ts.Lock() 130 defer ts.Unlock() 131 132 if cmd.err != nil { 133 ts.err, ts.panic = cmd.err, cmd.panic 134 } 135 if cmd.tidxSet { 136 ts.terminal = cmd.tidx 137 } 138 for _, le := range cmd.logEntries { 139 ts.logs[types.MessageIndex(le.StreamIndex)] = le 140 } 141 } 142 143 func (ts *testSource) getHistory() []int { 144 ts.Lock() 145 defer ts.Unlock() 146 147 h := make([]int, len(ts.history)) 148 copy(h, ts.history) 149 return h 150 } 151 152 func loadLogs(f *Fetcher, count int) (result []types.MessageIndex, err error) { 153 for { 154 // Specific limit, hit that limit. 155 if count > 0 && len(result) >= count { 156 return 157 } 158 159 var le *logpb.LogEntry 160 le, err = f.NextLogEntry() 161 if le != nil { 162 result = append(result, types.MessageIndex(le.StreamIndex)) 163 } 164 if err != nil { 165 return 166 } 167 } 168 } 169 170 func TestFetcher(t *testing.T) { 171 t.Parallel() 172 173 Convey(`A testing log Source`, t, func() { 174 c, tc := testclock.UseTime(context.Background(), testclock.TestTimeLocal) 175 c, cancelFunc := context.WithCancel(c) 176 177 ts := newTestSource() 178 o := Options{ 179 Source: ts, 180 181 // All message byte sizes will be 5. 182 sizeFunc: func(proto.Message) int { 183 return 5 184 }, 185 } 186 187 newFetcher := func() *Fetcher { return New(c, o) } 188 reap := func(f *Fetcher) { 189 cancelFunc() 190 loadLogs(f, 0) 191 } 192 193 Convey(`Uses defaults values when not overridden, and stops when cancelled.`, func() { 194 f := newFetcher() 195 defer reap(f) 196 197 So(f.o.BufferCount, ShouldEqual, 0) 198 So(f.o.BufferBytes, ShouldEqual, DefaultBufferBytes) 199 So(f.o.PrefetchFactor, ShouldEqual, 1) 200 }) 201 202 Convey(`With a Count limit of 3.`, func() { 203 o.BufferCount = 3 204 205 Convey(`Will pull 6 sequential log records.`, func() { 206 var cmd testSourceCommand 207 ts.send(cmd.logs(0, 1, 2, 3, 4, 5).terminalIndex(5)) 208 209 f := newFetcher() 210 defer reap(f) 211 212 logs, err := loadLogs(f, 0) 213 So(err, ShouldEqual, io.EOF) 214 So(logs, ShouldResemble, []types.MessageIndex{0, 1, 2, 3, 4, 5}) 215 }) 216 217 Convey(`Will immediately bail out if RequireCompleteStream is set`, func() { 218 var cmd testSourceCommand 219 ts.send(cmd.logs(0, 1, 2, 3, 4, 5).terminalIndex(-1)) 220 221 o.RequireCompleteStream = true 222 f := newFetcher() 223 defer reap(f) 224 225 logs, err := loadLogs(f, 0) 226 So(err, ShouldEqual, ErrIncompleteStream) 227 So(logs, ShouldBeNil) 228 }) 229 230 Convey(`Can read two log records and be cancelled.`, func() { 231 var cmd testSourceCommand 232 ts.send(cmd.logs(0, 1, 2, 3, 4, 5)) 233 234 f := newFetcher() 235 defer reap(f) 236 237 logs, err := loadLogs(f, 2) 238 So(err, ShouldBeNil) 239 So(logs, ShouldResemble, []types.MessageIndex{0, 1}) 240 241 cancelFunc() 242 _, err = loadLogs(f, 0) 243 So(err, ShouldEqual, context.Canceled) 244 }) 245 246 Convey(`Will delay for more log records if none are available.`, func() { 247 delayed := false 248 tc.SetTimerCallback(func(d time.Duration, t clock.Timer) { 249 // Add the remaining logs. 250 delayed = true 251 252 var cmd testSourceCommand 253 ts.send(cmd.logs(1, 2).terminalIndex(2)) 254 255 tc.Add(d) 256 }) 257 258 var cmd testSourceCommand 259 ts.send(cmd.logs(0)) 260 261 f := newFetcher() 262 defer reap(f) 263 264 logs, err := loadLogs(f, 1) 265 So(err, ShouldBeNil) 266 So(logs, ShouldResemble, []types.MessageIndex{0}) 267 268 logs, err = loadLogs(f, 0) 269 So(err, ShouldEqual, io.EOF) 270 So(logs, ShouldResemble, []types.MessageIndex{1, 2}) 271 So(delayed, ShouldBeTrue) 272 }) 273 274 Convey(`When an error is countered getting the terminal index, returns the error.`, func() { 275 var cmd testSourceCommand 276 ts.send(cmd.error(errors.New("test error"), false)) 277 278 f := newFetcher() 279 defer reap(f) 280 281 _, err := loadLogs(f, 0) 282 So(err, assertions.ShouldErrLike, "test error") 283 }) 284 285 Convey(`When an error is countered fetching logs, returns the error.`, func() { 286 var cmd testSourceCommand 287 ts.send(cmd.logs(0, 1, 2).error(errors.New("test error"), false)) 288 289 f := newFetcher() 290 defer reap(f) 291 292 _, err := loadLogs(f, 0) 293 So(err, assertions.ShouldErrLike, "test error") 294 }) 295 296 Convey(`If the source panics, it is caught and returned as an error.`, func() { 297 var cmd testSourceCommand 298 ts.send(cmd.error(errors.New("test error"), true)) 299 300 f := newFetcher() 301 defer reap(f) 302 303 _, err := loadLogs(f, 0) 304 So(err, assertions.ShouldErrLike, "panic during fetch") 305 }) 306 }) 307 308 Convey(`With a byte limit of 15`, func() { 309 o.BufferBytes = 15 310 o.PrefetchFactor = 2 311 312 var cmd testSourceCommand 313 ts.send(cmd.logs(0, 1, 2, 3, 4, 5, 6).terminalIndex(6)) 314 315 f := newFetcher() 316 defer reap(f) 317 318 // First fetch should have asked for 30 bytes (2*15), so 6 logs. After 319 // first log was kicked, there is a deficit of one log. 320 logs, err := loadLogs(f, 0) 321 So(err, ShouldEqual, io.EOF) 322 So(logs, ShouldResemble, []types.MessageIndex{0, 1, 2, 3, 4, 5, 6}) 323 324 So(ts.getHistory(), ShouldResemble, []int{6, 1}) 325 }) 326 327 Convey(`With an index of 1 and a maximum count of 1, fetches exactly 1 log.`, func() { 328 o.Index = 1 329 o.Count = 1 330 331 var cmd testSourceCommand 332 ts.send(cmd.logs(0, 1, 2, 3, 4, 5, 6).terminalIndex(6)) 333 334 f := newFetcher() 335 defer reap(f) 336 337 // First fetch will ask for exactly one log. 338 logs, err := loadLogs(f, 0) 339 So(err, ShouldEqual, io.EOF) 340 So(logs, ShouldResemble, []types.MessageIndex{1}) 341 342 So(ts.getHistory(), ShouldResemble, []int{1}) 343 }) 344 }) 345 }