github.com/m3db/m3@v1.5.1-0.20231129193456-75a402aa583b/src/dbnode/storage/series/reader_test.go (about) 1 // Copyright (c) 2017 Uber Technologies, Inc. 2 // 3 // Permission is hereby granted, free of charge, to any person obtaining a copy 4 // of this software and associated documentation files (the "Software"), to deal 5 // in the Software without restriction, including without limitation the rights 6 // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 // copies of the Software, and to permit persons to whom the Software is 8 // furnished to do so, subject to the following conditions: 9 // 10 // The above copyright notice and this permission notice shall be included in 11 // all copies or substantial portions of the Software. 12 // 13 // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 // THE SOFTWARE. 20 21 package series 22 23 import ( 24 "errors" 25 "strings" 26 "testing" 27 28 "github.com/golang/mock/gomock" 29 "github.com/stretchr/testify/assert" 30 "github.com/stretchr/testify/require" 31 32 "github.com/m3db/m3/src/dbnode/namespace" 33 "github.com/m3db/m3/src/dbnode/storage/block" 34 "github.com/m3db/m3/src/dbnode/ts" 35 "github.com/m3db/m3/src/dbnode/x/xio" 36 "github.com/m3db/m3/src/x/ident" 37 xtest "github.com/m3db/m3/src/x/test" 38 xtime "github.com/m3db/m3/src/x/time" 39 ) 40 41 func TestReaderUsingRetrieverReadEncoded(t *testing.T) { 42 ctrl := xtest.NewController(t) 43 defer ctrl.Finish() 44 45 opts := newSeriesTestOptions() 46 ropts := opts.RetentionOptions() 47 48 end := xtime.ToUnixNano(opts.ClockOptions().NowFn()().Truncate(ropts.BlockSize())) 49 start := end.Add(-2 * ropts.BlockSize()) 50 51 onRetrieveBlock := block.NewMockOnRetrieveBlock(ctrl) 52 53 retriever := NewMockQueryableBlockRetriever(ctrl) 54 retriever.EXPECT().IsBlockRetrievable(start).Return(true, nil) 55 retriever.EXPECT().IsBlockRetrievable(start.Add(ropts.BlockSize())).Return(true, nil) 56 57 var blockReaders []xio.BlockReader 58 curStart := start 59 for i := 0; i < 2; i++ { 60 reader := xio.NewMockSegmentReader(ctrl) 61 blockReaders = append(blockReaders, xio.BlockReader{ 62 SegmentReader: reader, 63 Start: curStart, 64 }) 65 curStart = curStart.Add(ropts.BlockSize()) 66 } 67 68 ctx := opts.ContextPool().Get() 69 defer ctx.Close() 70 71 retriever.EXPECT(). 72 Stream(ctx, ident.NewIDMatcher("foo"), 73 start, onRetrieveBlock, gomock.Any()). 74 Return(blockReaders[0], nil) 75 retriever.EXPECT(). 76 Stream(ctx, ident.NewIDMatcher("foo"), 77 start.Add(ropts.BlockSize()), onRetrieveBlock, gomock.Any()). 78 Return(blockReaders[1], nil) 79 80 reader := NewReaderUsingRetriever( 81 ident.StringID("foo"), retriever, onRetrieveBlock, nil, opts) 82 83 // Check reads as expected 84 iter, err := reader.ReadEncoded(ctx, start, end, namespace.Context{}) 85 require.NoError(t, err) 86 87 count := 0 88 for iter.Next(ctx) { 89 require.Equal(t, 1, len(iter.Current())) 90 assert.Equal(t, blockReaders[count], iter.Current()[0]) 91 count++ 92 } 93 require.NoError(t, iter.Err()) 94 require.Equal(t, 2, count) 95 } 96 97 type readTestCase struct { 98 title string 99 times []xtime.UnixNano 100 cachedBlocks map[xtime.UnixNano]streamResponse 101 diskBlocks map[xtime.UnixNano]streamResponse 102 bufferBlocks map[xtime.UnixNano]block.FetchBlockResult 103 expectedResults []block.FetchBlockResult 104 } 105 106 type streamResponse struct { 107 blockReader xio.BlockReader 108 err error 109 } 110 111 var ( 112 opts = newSeriesTestOptions() 113 ropts = opts.RetentionOptions() 114 blockSize = ropts.BlockSize() 115 // Subtract a few blocksizes to make sure the test cases don't try and query into 116 // the future. 117 start = xtime.ToUnixNano(opts.ClockOptions().NowFn()()). 118 Truncate(blockSize).Add(-5 * blockSize) 119 ) 120 121 var robustReaderTestCases = []readTestCase{ 122 { 123 // Should return an empty slice if there is no data. 124 title: "Handle no data", 125 times: []xtime.UnixNano{start}, 126 }, 127 { 128 // Read one block from disk which should return an error. 129 title: "Handles disk read errors", 130 times: []xtime.UnixNano{start}, 131 diskBlocks: map[xtime.UnixNano]streamResponse{ 132 start: { 133 err: errors.New("some-error"), 134 }, 135 }, 136 expectedResults: []block.FetchBlockResult{ 137 { 138 Start: start, 139 Err: errors.New("some-error"), 140 }, 141 }, 142 }, 143 { 144 // Read one block from the disk cache which should return an error. 145 title: "Handles disk cache read errors", 146 times: []xtime.UnixNano{start}, 147 cachedBlocks: map[xtime.UnixNano]streamResponse{ 148 start: { 149 err: errors.New("some-error"), 150 }, 151 }, 152 expectedResults: []block.FetchBlockResult{ 153 { 154 Start: start, 155 Err: errors.New("some-error"), 156 }, 157 }, 158 }, 159 { 160 // Read one block from the buffer which should return an error. 161 title: "Handles buffer read errors", 162 times: []xtime.UnixNano{start}, 163 bufferBlocks: map[xtime.UnixNano]block.FetchBlockResult{ 164 start: { 165 Start: start, 166 Err: errors.New("some-error"), 167 }, 168 }, 169 expectedResults: []block.FetchBlockResult{ 170 { 171 Start: start, 172 Err: errors.New("some-error"), 173 }, 174 }, 175 }, 176 { 177 // Read one block from the disk cache. 178 title: "Handles disk cache reads (should not query disk)", 179 times: []xtime.UnixNano{start}, 180 cachedBlocks: map[xtime.UnixNano]streamResponse{ 181 start: { 182 blockReader: xio.BlockReader{ 183 SegmentReader: xio.NewSegmentReader(ts.Segment{}), 184 Start: start, 185 BlockSize: blockSize, 186 }, 187 }, 188 }, 189 expectedResults: []block.FetchBlockResult{ 190 { 191 Start: start, 192 Blocks: []xio.BlockReader{{Start: start, BlockSize: blockSize}}, 193 }, 194 }, 195 }, 196 { 197 // Read two blocks, each of which should be returned from disk. 198 title: "Handles multiple disk reads", 199 times: []xtime.UnixNano{start, start.Add(blockSize)}, 200 diskBlocks: map[xtime.UnixNano]streamResponse{ 201 start: { 202 blockReader: xio.BlockReader{ 203 SegmentReader: xio.NewSegmentReader(ts.Segment{}), 204 Start: start, 205 BlockSize: blockSize, 206 }, 207 }, 208 start.Add(blockSize): { 209 blockReader: xio.BlockReader{ 210 SegmentReader: xio.NewSegmentReader(ts.Segment{}), 211 Start: start.Add(blockSize), 212 BlockSize: blockSize, 213 }, 214 }, 215 }, 216 expectedResults: []block.FetchBlockResult{ 217 { 218 Start: start, 219 Blocks: []xio.BlockReader{{Start: start, BlockSize: blockSize}}, 220 }, 221 { 222 Start: start.Add(blockSize), 223 Blocks: []xio.BlockReader{{Start: start.Add(blockSize), BlockSize: blockSize}}, 224 }, 225 }, 226 }, 227 { 228 // Read one block from the buffer. 229 title: "Handles buffer reads", 230 times: []xtime.UnixNano{start}, 231 bufferBlocks: map[xtime.UnixNano]block.FetchBlockResult{ 232 start: { 233 Start: start, 234 Blocks: []xio.BlockReader{{Start: start, BlockSize: blockSize}}, 235 }, 236 }, 237 expectedResults: []block.FetchBlockResult{ 238 { 239 Start: start, 240 Blocks: []xio.BlockReader{{Start: start, BlockSize: blockSize}}, 241 }, 242 }, 243 }, 244 { 245 title: "Combines data from disk cache and buffer for same blockstart", 246 times: []xtime.UnixNano{start}, 247 cachedBlocks: map[xtime.UnixNano]streamResponse{ 248 start: { 249 blockReader: xio.BlockReader{ 250 SegmentReader: xio.NewSegmentReader(ts.Segment{}), 251 Start: start, 252 BlockSize: blockSize, 253 }, 254 }, 255 }, 256 bufferBlocks: map[xtime.UnixNano]block.FetchBlockResult{ 257 start: { 258 Start: start, 259 Blocks: []xio.BlockReader{{Start: start, BlockSize: blockSize}}, 260 }, 261 }, 262 expectedResults: []block.FetchBlockResult{ 263 { 264 Start: start, 265 Blocks: []xio.BlockReader{ 266 // One from disk cache. 267 {Start: start, BlockSize: blockSize}, 268 // One from buffer. 269 {Start: start, BlockSize: blockSize}, 270 }, 271 }, 272 }, 273 }, 274 { 275 title: "Combines data from disk and buffer for same blockstart", 276 times: []xtime.UnixNano{start}, 277 diskBlocks: map[xtime.UnixNano]streamResponse{ 278 start: { 279 blockReader: xio.BlockReader{ 280 SegmentReader: xio.NewSegmentReader(ts.Segment{}), 281 Start: start, 282 BlockSize: blockSize, 283 }, 284 }, 285 }, 286 bufferBlocks: map[xtime.UnixNano]block.FetchBlockResult{ 287 start: { 288 Start: start, 289 Blocks: []xio.BlockReader{{Start: start, BlockSize: blockSize}}, 290 }, 291 }, 292 expectedResults: []block.FetchBlockResult{ 293 { 294 Start: start, 295 Blocks: []xio.BlockReader{ 296 // One from disk. 297 {Start: start, BlockSize: blockSize}, 298 // One from buffer. 299 {Start: start, BlockSize: blockSize}, 300 }, 301 }, 302 }, 303 }, 304 // Both disk cache and buffer have data for same blockstart but buffer has an 305 // error. The error should be propagated to the caller (not masked by the 306 // valid data from the disk cache). 307 { 308 title: "Handles buffer and disk cache merge with buffer error for same blockstart", 309 times: []xtime.UnixNano{start}, 310 cachedBlocks: map[xtime.UnixNano]streamResponse{ 311 start: { 312 blockReader: xio.BlockReader{ 313 SegmentReader: xio.NewSegmentReader(ts.Segment{}), 314 Start: start, 315 BlockSize: blockSize, 316 }, 317 }, 318 }, 319 bufferBlocks: map[xtime.UnixNano]block.FetchBlockResult{ 320 start: { 321 Start: start, 322 Err: errors.New("some-error"), 323 }, 324 }, 325 expectedResults: []block.FetchBlockResult{ 326 { 327 Start: start, 328 Err: errors.New("some-error"), 329 }, 330 }, 331 }, 332 // Both disk and buffer have data for same blockstart but buffer has an 333 // error. The error should be propagated to the caller (not masked by the 334 // valid data from the disk cache). 335 { 336 title: "Handles buffer and disk merge with buffer error for same blockstart", 337 times: []xtime.UnixNano{start}, 338 cachedBlocks: map[xtime.UnixNano]streamResponse{ 339 start: { 340 blockReader: xio.BlockReader{ 341 SegmentReader: xio.NewSegmentReader(ts.Segment{}), 342 Start: start, 343 BlockSize: blockSize, 344 }, 345 }, 346 }, 347 bufferBlocks: map[xtime.UnixNano]block.FetchBlockResult{ 348 start: { 349 Start: start, 350 Err: errors.New("some-error"), 351 }, 352 }, 353 expectedResults: []block.FetchBlockResult{ 354 { 355 Start: start, 356 Err: errors.New("some-error"), 357 }, 358 }, 359 }, 360 { 361 title: "Combines data from all sources for different block starts and same block starts", 362 times: []xtime.UnixNano{start, start.Add(blockSize), start.Add(2 * blockSize), start.Add(3 * blockSize)}, 363 // Block 1 and 3 from disk cache. 364 cachedBlocks: map[xtime.UnixNano]streamResponse{ 365 start: { 366 blockReader: xio.BlockReader{ 367 SegmentReader: xio.NewSegmentReader(ts.Segment{}), 368 Start: start, 369 BlockSize: blockSize, 370 }, 371 }, 372 start.Add(2 * blockSize): { 373 blockReader: xio.BlockReader{ 374 SegmentReader: xio.NewSegmentReader(ts.Segment{}), 375 Start: start.Add(2 * blockSize), 376 BlockSize: blockSize, 377 }, 378 }, 379 }, 380 // blocks 2 and 4 from disk. 381 diskBlocks: map[xtime.UnixNano]streamResponse{ 382 start.Add(blockSize): { 383 blockReader: xio.BlockReader{ 384 SegmentReader: xio.NewSegmentReader(ts.Segment{}), 385 Start: start.Add(blockSize), 386 BlockSize: blockSize, 387 }, 388 }, 389 start.Add(3 * blockSize): { 390 blockReader: xio.BlockReader{ 391 SegmentReader: xio.NewSegmentReader(ts.Segment{}), 392 Start: start.Add(3 * blockSize), 393 BlockSize: blockSize, 394 }, 395 }, 396 }, 397 // Blocks 1, 2, and 3 from buffer. 398 bufferBlocks: map[xtime.UnixNano]block.FetchBlockResult{ 399 start: { 400 Start: start, 401 Blocks: []xio.BlockReader{{Start: start, BlockSize: blockSize}}, 402 }, 403 start.Add(blockSize): { 404 Start: start.Add(blockSize), 405 Blocks: []xio.BlockReader{{Start: start.Add(blockSize), BlockSize: blockSize}}, 406 }, 407 start.Add(2 * blockSize): { 408 Start: start.Add(2 * blockSize), 409 Blocks: []xio.BlockReader{{Start: start.Add(2 * blockSize), BlockSize: blockSize}}, 410 }, 411 }, 412 expectedResults: []block.FetchBlockResult{ 413 { 414 Start: start, 415 Blocks: []xio.BlockReader{ 416 // One from disk cache. 417 {Start: start, BlockSize: blockSize}, 418 // One from buffer. 419 {Start: start, BlockSize: blockSize}, 420 }, 421 }, 422 { 423 Start: start.Add(blockSize), 424 Blocks: []xio.BlockReader{ 425 // One from disk. 426 {Start: start.Add(blockSize), BlockSize: blockSize}, 427 // One from buffer. 428 {Start: start.Add(blockSize), BlockSize: blockSize}, 429 }, 430 }, 431 { 432 Start: start.Add(2 * blockSize), 433 Blocks: []xio.BlockReader{ 434 // One from disk cache. 435 {Start: start.Add(2 * blockSize), BlockSize: blockSize}, 436 // One from buffer. 437 {Start: start.Add(2 * blockSize), BlockSize: blockSize}, 438 }, 439 }, 440 { 441 Start: start.Add(3 * blockSize), 442 Blocks: []xio.BlockReader{ 443 // One from disk. 444 {Start: start.Add(3 * blockSize), BlockSize: blockSize}, 445 }, 446 }, 447 }, 448 }, 449 } 450 451 func TestReaderFetchBlocksRobust(t *testing.T) { 452 for _, tc := range robustReaderTestCases { 453 tc := tc 454 t.Run(tc.title, func(t *testing.T) { 455 ctrl := xtest.NewController(t) 456 defer ctrl.Finish() 457 458 var ( 459 onRetrieveBlock = block.NewMockOnRetrieveBlock(ctrl) 460 retriever = NewMockQueryableBlockRetriever(ctrl) 461 diskCache = block.NewMockDatabaseSeriesBlocks(ctrl) 462 buffer = NewMockdatabaseBuffer(ctrl) 463 bufferReturn []block.FetchBlockResult 464 ) 465 466 ctx := opts.ContextPool().Get() 467 defer ctx.Close() 468 469 // Setup mocks. 470 for _, currTime := range tc.times { 471 cachedBlocks, wasInDiskCache := tc.cachedBlocks[currTime] 472 if wasInDiskCache { 473 // If the data was in the disk cache then expect a read from it but don't expect 474 // disk reads. 475 b := block.NewMockDatabaseBlock(ctrl) 476 if cachedBlocks.err != nil { 477 b.EXPECT().Stream(ctx).Return(xio.BlockReader{}, cachedBlocks.err) 478 } else { 479 b.EXPECT().Stream(ctx).Return(cachedBlocks.blockReader, nil) 480 } 481 diskCache.EXPECT().BlockAt(currTime).Return(b, true) 482 } else { 483 // If the data was not in the disk cache then expect that and setup a query 484 // for disk. 485 diskCache.EXPECT().BlockAt(currTime).Return(nil, false) 486 diskBlocks, ok := tc.diskBlocks[currTime] 487 if !ok { 488 retriever.EXPECT().IsBlockRetrievable(currTime).Return(false, nil) 489 } else { 490 retriever.EXPECT().IsBlockRetrievable(currTime).Return(true, nil) 491 if diskBlocks.err != nil { 492 retriever.EXPECT(). 493 Stream(ctx, ident.NewIDMatcher("foo"), currTime, nil, gomock.Any()). 494 Return(xio.BlockReader{}, diskBlocks.err) 495 } else { 496 retriever.EXPECT(). 497 Stream(ctx, ident.NewIDMatcher("foo"), currTime, nil, gomock.Any()). 498 Return(diskBlocks.blockReader, nil) 499 } 500 } 501 } 502 503 // Prepare buffer response one block at a time. 504 bufferBlocks, wasInBuffer := tc.bufferBlocks[currTime] 505 if wasInBuffer { 506 bufferReturn = append(bufferReturn, bufferBlocks) 507 } 508 } 509 510 // Expect final buffer result (batched function call). 511 if len(tc.bufferBlocks) == 0 { 512 buffer.EXPECT().IsEmpty().Return(true) 513 } else { 514 buffer.EXPECT().IsEmpty().Return(false) 515 buffer.EXPECT(). 516 FetchBlocks(ctx, tc.times, namespace.Context{}). 517 Return(bufferReturn) 518 } 519 520 reader := NewReaderUsingRetriever( 521 ident.StringID("foo"), retriever, onRetrieveBlock, nil, opts) 522 523 r, err := reader.fetchBlocksWithBlocksMapAndBuffer(ctx, tc.times, diskCache, buffer, namespace.Context{}) 524 require.NoError(t, err) 525 require.Equal(t, len(tc.expectedResults), len(r)) 526 527 for i, result := range r { 528 expectedResult := tc.expectedResults[i] 529 assert.Equal(t, expectedResult.Start, result.Start) 530 531 if expectedResult.Err != nil { 532 require.True(t, strings.Contains(result.Err.Error(), expectedResult.Err.Error())) 533 } else { 534 require.Equal(t, len(expectedResult.Blocks), len(result.Blocks)) 535 for _, block := range result.Blocks { 536 require.Equal(t, expectedResult.Start, block.Start) 537 } 538 } 539 } 540 }) 541 } 542 } 543 544 //nolint:scopelint 545 func TestReaderReadEncodedRobust(t *testing.T) { 546 for _, tc := range robustReaderTestCases { 547 tc := tc 548 t.Run(tc.title, func(t *testing.T) { 549 ctrl := xtest.NewController(t) 550 defer ctrl.Finish() 551 552 var ( 553 onRetrieveBlock = block.NewMockOnRetrieveBlock(ctrl) 554 retriever = NewMockQueryableBlockRetriever(ctrl) 555 diskCache = block.NewMockDatabaseSeriesBlocks(ctrl) 556 buffer = NewMockdatabaseBuffer(ctrl) 557 ) 558 559 ctx := opts.ContextPool().Get() 560 defer ctx.Close() 561 562 // Setup mocks. 563 for _, currTime := range tc.times { 564 cachedBlocks, wasInDiskCache := tc.cachedBlocks[currTime] 565 if wasInDiskCache { 566 // If the data was in the disk cache then expect a read from it but don't expect 567 // disk reads. 568 b := block.NewMockDatabaseBlock(ctrl) 569 b.EXPECT().SetLastReadTime(gomock.Any()).AnyTimes() 570 if cachedBlocks.err != nil { 571 b.EXPECT().Stream(ctx).Return(xio.BlockReader{}, cachedBlocks.err) 572 } else { 573 b.EXPECT().Stream(ctx).Return(cachedBlocks.blockReader, nil) 574 } 575 diskCache.EXPECT().BlockAt(currTime).Return(b, true) 576 if cachedBlocks.err != nil { 577 // Stop setting up mocks since the function will early return. 578 break 579 } 580 } else { 581 diskCache.EXPECT().BlockAt(currTime).Return(nil, false) 582 } 583 584 // Setup buffer mocks. 585 bufferBlocks, wasInBuffer := tc.bufferBlocks[currTime] 586 if wasInBuffer { 587 if bufferBlocks.Err != nil { 588 buffer.EXPECT(). 589 ReadEncoded(ctx, currTime, currTime.Add(blockSize), namespace.Context{}). 590 Return(nil, bufferBlocks.Err) 591 // Stop setting up mocks since the function will early return. 592 break 593 } else { 594 buffer.EXPECT(). 595 ReadEncoded(ctx, currTime, currTime.Add(blockSize), namespace.Context{}). 596 Return([][]xio.BlockReader{bufferBlocks.Blocks}, nil) 597 } 598 } else { 599 buffer.EXPECT(). 600 ReadEncoded(ctx, currTime, currTime.Add(blockSize), namespace.Context{}). 601 Return(nil, nil) 602 } 603 604 if !wasInDiskCache { 605 // If the data was not in the disk cache then setup a query for disk. 606 diskBlocks, ok := tc.diskBlocks[currTime] 607 if !ok { 608 retriever.EXPECT().IsBlockRetrievable(currTime).Return(false, nil) 609 } else { 610 retriever.EXPECT().IsBlockRetrievable(currTime).Return(true, nil) 611 if diskBlocks.err != nil { 612 retriever.EXPECT(). 613 Stream(ctx, ident.NewIDMatcher("foo"), currTime, onRetrieveBlock, gomock.Any()). 614 Return(xio.BlockReader{}, diskBlocks.err) 615 } else { 616 retriever.EXPECT(). 617 Stream(ctx, ident.NewIDMatcher("foo"), currTime, onRetrieveBlock, gomock.Any()). 618 Return(diskBlocks.blockReader, nil) 619 } 620 } 621 } 622 } 623 624 var ( 625 reader = NewReaderUsingRetriever( 626 ident.StringID("foo"), retriever, onRetrieveBlock, nil, opts) 627 start = tc.times[0] 628 // End is not inclusive so add blocksize to the last time. 629 end = tc.times[len(tc.times)-1].Add(blockSize) 630 ) 631 iter, err := reader.readersWithBlocksMapAndBuffer(ctx, start, end, diskCache, buffer, namespace.Context{}) 632 633 anyInMemErr := false 634 for _, sr := range tc.cachedBlocks { 635 if sr.err != nil { 636 anyInMemErr = true 637 break 638 } 639 } 640 641 for _, br := range tc.bufferBlocks { 642 if br.Err != nil { 643 anyInMemErr = true 644 break 645 } 646 } 647 648 if anyInMemErr { 649 require.Error(t, err) 650 return 651 } 652 653 r, err := iter.ToSlices(ctx) 654 655 for _, sr := range tc.diskBlocks { 656 if sr.err != nil { 657 require.Error(t, err) 658 return 659 } 660 } 661 require.NoError(t, err) 662 require.Equal(t, len(tc.expectedResults), len(r)) 663 664 for i, result := range r { 665 expectedResult := tc.expectedResults[i] 666 for _, br := range result { 667 assert.Equal(t, expectedResult.Start, br.Start) 668 } 669 require.Equal(t, len(expectedResult.Blocks), len(result)) 670 } 671 }) 672 } 673 }