github.com/keybase/client/go@v0.0.0-20240309051027-028f7c731f8b/kbfs/data/file_data_test.go (about) 1 // Copyright 2016 Keybase Inc. All rights reserved. 2 // Use of this source code is governed by a BSD 3 // license that can be found in the LICENSE file. 4 5 package data 6 7 import ( 8 "bytes" 9 "fmt" 10 "math" 11 "reflect" 12 "testing" 13 14 "github.com/keybase/client/go/kbfs/kbfsblock" 15 "github.com/keybase/client/go/kbfs/libkey" 16 libkeytest "github.com/keybase/client/go/kbfs/libkey/test" 17 "github.com/keybase/client/go/kbfs/tlf" 18 "github.com/keybase/client/go/libkb" 19 "github.com/keybase/client/go/logger" 20 "github.com/keybase/client/go/protocol/keybase1" 21 "github.com/stretchr/testify/require" 22 "golang.org/x/net/context" 23 ) 24 25 func setupFileDataTest(t *testing.T, maxBlockSize int64, 26 maxPtrsPerBlock int) (*FileData, BlockCache, DirtyBlockCache, *DirtyFile) { 27 // Make a fake file. 28 ptr := BlockPointer{ 29 ID: kbfsblock.FakeID(42), 30 DirectType: DirectBlock, 31 } 32 id := tlf.FakeID(1, tlf.Private) 33 file := Path{ 34 FolderBranch{Tlf: id}, 35 []PathNode{{ptr, NewPathPartString("file", nil)}}, 36 nil, 37 } 38 chargedTo := keybase1.MakeTestUID(1).AsUserOrTeam() 39 bsplit := &BlockSplitterSimple{maxBlockSize, maxPtrsPerBlock, 10, 0} 40 kmd := libkeytest.NewEmptyKeyMetadata(id, 1) 41 42 cleanCache := NewBlockCacheStandard(1<<10, 1<<20) 43 dirtyBcache := SimpleDirtyBlockCacheStandard() 44 getter := func(ctx context.Context, _ libkey.KeyMetadata, ptr BlockPointer, 45 _ Path, _ BlockReqType) (*FileBlock, bool, error) { 46 isDirty := true 47 block, err := dirtyBcache.Get(ctx, id, ptr, MasterBranch) 48 if err != nil { 49 // Check the clean cache. 50 block, err = cleanCache.Get(ptr) 51 if err != nil { 52 return nil, false, err 53 } 54 isDirty = false 55 } 56 fblock, ok := block.(*FileBlock) 57 if !ok { 58 return nil, false, 59 fmt.Errorf("Block for %s is not a file block", ptr) 60 } 61 return fblock, isDirty, nil 62 } 63 cacher := func(ctx context.Context, ptr BlockPointer, block Block) error { 64 return dirtyBcache.Put(ctx, id, ptr, MasterBranch, block) 65 } 66 67 log := logger.NewTestLogger(t) 68 fd := NewFileData( 69 file, chargedTo, bsplit, kmd, getter, cacher, log, 70 libkb.NewVDebugLog(log)) 71 df := NewDirtyFile(file, dirtyBcache) 72 return fd, cleanCache, dirtyBcache, df 73 } 74 75 type testFileDataLevel struct { 76 dirty bool 77 children []testFileDataLevel 78 off Int64Offset 79 size int 80 } 81 82 type testFileDataHole struct { 83 start Int64Offset 84 end Int64Offset 85 } 86 87 func testFileDataLevelFromData(t *testing.T, maxBlockSize Int64Offset, 88 maxPtrsPerBlock int, existingLevels int, fullDataLen Int64Offset, 89 holes []testFileDataHole, startWrite, endWrite, 90 holeShiftAfter Int64Offset, truncateExtend bool) testFileDataLevel { 91 // First fill in the leaf level. 92 var prevChildren []testFileDataLevel 93 var off Int64Offset 94 size := 0 95 nextHole := 0 96 for off < fullDataLen { 97 var nextOff Int64Offset 98 size = int(maxBlockSize) 99 makeFinalHole := false 100 switch { 101 case nextHole < len(holes) && off+maxBlockSize >= holes[nextHole].start: 102 size = int(holes[nextHole].start - off) 103 nextOff = holes[nextHole].end 104 nextHole++ 105 makeFinalHole = nextHole >= len(holes) && 106 nextOff >= fullDataLen 107 case off+maxBlockSize > fullDataLen: 108 size = int(fullDataLen - off) 109 nextOff = fullDataLen 110 default: 111 nextOff = off + maxBlockSize 112 } 113 dirty := off < endWrite && startWrite < off+Int64Offset(size) 114 // If this is a shrink, dirty the block containing startWrite. 115 if endWrite < 0 { 116 dirty = nextOff >= startWrite 117 } 118 newChild := testFileDataLevel{dirty, nil, off, size} 119 t.Logf("Expected leaf offset %d [dirty=%t]", off, dirty) 120 prevChildren = append(prevChildren, newChild) 121 122 if makeFinalHole { 123 // The final hole can only ever be dirty if there was a 124 // truncate to a new length. 125 newChild := testFileDataLevel{truncateExtend, nil, nextOff, 0} 126 t.Logf("Expected leaf offset %d (final)", nextOff) 127 prevChildren = append(prevChildren, newChild) 128 } 129 130 off = nextOff 131 } 132 if fullDataLen == 0 { 133 // Special case for a file that's been left empty. 134 newChild := testFileDataLevel{true, nil, 0, 0} 135 prevChildren = append(prevChildren, newChild) 136 } 137 138 // Now fill in any parents. If this is a shrink, force the new 139 // data to have the same number of levels (we never remove levels 140 // of indirection at the moment). 141 newLevels := 1 142 for len(prevChildren) != 1 || (endWrite < 0 && newLevels < existingLevels) { 143 prevChildIndex := 0 144 var level []testFileDataLevel 145 146 numAtLevel := int(math.Ceil(float64(len(prevChildren)) / 147 float64(maxPtrsPerBlock))) 148 for i := 0; i < numAtLevel; i++ { 149 // Split the previous children up (if any) into 150 // maxPtrsPerBlock chunks. 151 var children []testFileDataLevel 152 var off Int64Offset 153 newIndex := prevChildIndex + maxPtrsPerBlock 154 if newIndex > len(prevChildren) { 155 newIndex = len(prevChildren) 156 } 157 off = prevChildren[prevChildIndex].off 158 children = prevChildren[prevChildIndex:newIndex] 159 prevChildIndex = newIndex 160 dirty := false 161 for _, child := range children { 162 // A parent is dirty if it has a dirty child. 163 if child.dirty { 164 dirty = true 165 break 166 } 167 } 168 // Also if a new block was made in a hole, any indirect 169 // parent that comes after the end of the write will be 170 // dirty, due to hole shifting. 171 if holeShiftAfter > 0 && off >= holeShiftAfter { 172 dirty = true 173 // If this is the bottom-most parent block after a 174 // hole shift, its rightmost child will also be marked 175 // dirty. 176 if newLevels == 1 { 177 children[len(children)-1].dirty = true 178 } 179 } 180 newChild := testFileDataLevel{dirty, children, off, 0} 181 level = append(level, newChild) 182 } 183 prevChildren = level 184 newLevels++ 185 } 186 187 // Even in a shrink, the top block is always dirty. 188 currNode := &(prevChildren[0]) 189 if endWrite < 0 { 190 currNode.dirty = true 191 } 192 193 // If we added new levels, we can expect the old topmost block to 194 // be dirty, since we have to upload it with a new block ID. 195 for i := 0; i <= newLevels-existingLevels; i++ { 196 t.Logf("Dirtying level %d %d %d", i, newLevels, existingLevels) 197 currNode.dirty = true 198 if len(currNode.children) == 0 { 199 break 200 } 201 currNode = &(currNode.children[0]) 202 } 203 204 return prevChildren[0] 205 } 206 207 func (tfdl testFileDataLevel) check(t *testing.T, fd *FileData, 208 ptr BlockPointer, off Int64Offset, dirtyBcache DirtyBlockCache) ( 209 dirtyPtrs map[BlockPointer]bool) { 210 dirtyPtrs = make(map[BlockPointer]bool) 211 levelString := fmt.Sprintf("ptr=%s, off=%d", ptr, off) 212 t.Logf("Checking %s", levelString) 213 214 require.Equal(t, tfdl.off, off, levelString) 215 if tfdl.dirty { 216 dirtyPtrs[ptr] = true 217 require.True( 218 t, dirtyBcache.IsDirty(fd.tree.file.Tlf, ptr, MasterBranch), 219 levelString) 220 } 221 222 fblock, isDirty, err := fd.getter(nil, nil, ptr, Path{}, BlockRead) 223 require.NoError(t, err, levelString) 224 require.Equal(t, tfdl.dirty, isDirty, levelString) 225 require.NotNil(t, fblock, levelString) 226 227 // We expect this to be a leaf block. 228 if len(tfdl.children) == 0 { 229 require.False(t, fblock.IsInd, levelString) 230 require.Len(t, fblock.IPtrs, 0, levelString) 231 require.Len(t, fblock.Contents, tfdl.size, levelString) 232 return dirtyPtrs 233 } 234 235 // Otherwise it's indirect, so check all the children. 236 require.True(t, fblock.IsInd, levelString) 237 require.Len(t, fblock.IPtrs, len(tfdl.children), levelString) 238 require.Len(t, fblock.Contents, 0, levelString) 239 for i, iptr := range fblock.IPtrs { 240 childDirtyPtrs := tfdl.children[i].check( 241 t, fd, iptr.BlockPointer, iptr.Off, dirtyBcache) 242 for ptr := range childDirtyPtrs { 243 dirtyPtrs[ptr] = true 244 } 245 } 246 return dirtyPtrs 247 } 248 249 func testFileDataCheckWrite(t *testing.T, fd *FileData, 250 dirtyBcache DirtyBlockCache, df *DirtyFile, data []byte, off Int64Offset, 251 topBlock *FileBlock, oldDe DirEntry, expectedSize uint64, 252 expectedUnrefs []BlockInfo, expectedDirtiedBytes int64, 253 expectedBytesExtended int64, expectedTopLevel testFileDataLevel) { 254 // Do the write. 255 ctx := context.Background() 256 newDe, dirtyPtrs, unrefs, newlyDirtiedChildBytes, bytesExtended, err := 257 fd.Write(ctx, data, off, topBlock, oldDe, df) 258 require.NoError(t, err) 259 260 // Check the basics. 261 require.Equal(t, expectedSize, newDe.Size) 262 require.Equal(t, expectedDirtiedBytes, newlyDirtiedChildBytes) 263 require.Equal(t, expectedBytesExtended, bytesExtended) 264 265 // Go through each expected level and make sure we have the right 266 // set of dirty pointers and children. 267 expectedDirtyPtrs := expectedTopLevel.check( 268 t, fd, fd.rootBlockPointer(), 0, dirtyBcache) 269 dirtyPtrsMap := make(map[BlockPointer]bool) 270 for _, ptr := range dirtyPtrs { 271 dirtyPtrsMap[ptr] = true 272 } 273 require.True(t, reflect.DeepEqual(expectedDirtyPtrs, dirtyPtrsMap), 274 fmt.Sprintf("expected %v; got %v", expectedDirtyPtrs, dirtyPtrsMap)) 275 276 // TODO: set the EncodedSize of the existing blocks to something 277 // non-zero so that we get some unrefs. 278 require.Len(t, unrefs, 0) 279 } 280 281 func testFileDataWriteExtendEmptyFile(t *testing.T, maxBlockSize Int64Offset, 282 maxPtrsPerBlock int, fullDataLen Int64Offset) { 283 fd, cleanBcache, dirtyBcache, df := setupFileDataTest( 284 t, int64(maxBlockSize), maxPtrsPerBlock) 285 topBlock := NewFileBlock().(*FileBlock) 286 err := cleanBcache.Put( 287 fd.rootBlockPointer(), fd.tree.file.Tlf, topBlock, TransientEntry, 288 SkipCacheHash) 289 require.NoError(t, err) 290 de := DirEntry{} 291 data := make([]byte, fullDataLen) 292 for i := 0; i < int(fullDataLen); i++ { 293 data[i] = byte(i) 294 } 295 expectedTopLevel := testFileDataLevelFromData( 296 t, maxBlockSize, maxPtrsPerBlock, 0, fullDataLen, nil, 0, 297 fullDataLen, 0, false) 298 299 testFileDataCheckWrite( 300 t, fd, dirtyBcache, df, data, 0, topBlock, de, uint64(fullDataLen), 301 nil, int64(fullDataLen), int64(fullDataLen), expectedTopLevel) 302 303 // Make sure we can read back the complete data. 304 gotData := make([]byte, fullDataLen) 305 nRead, err := fd.Read(context.Background(), gotData, 0) 306 require.NoError(t, err) 307 require.Equal(t, Int64Offset(nRead), fullDataLen) 308 require.True(t, bytes.Equal(data, gotData)) 309 } 310 311 func testFileDataWriteNewLevel(t *testing.T, levels float64) { 312 capacity := math.Pow(2, levels) 313 halfCapacity := capacity/2 + 1 314 if levels == 1 { 315 halfCapacity = capacity - 1 316 } 317 // Fills half the leaf level. 318 testFileDataWriteExtendEmptyFile(t, 2, 2, Int64Offset(halfCapacity)) 319 // Fills whole leaf level. 320 testFileDataWriteExtendEmptyFile(t, 2, 2, Int64Offset(capacity)) 321 322 } 323 324 func TestFileDataWriteNewLevel(t *testing.T) { 325 for _, level := range []float64{1, 2, 3, 10} { 326 // capture range variable. 327 level := level 328 t.Run(fmt.Sprintf("%dLevels", int(level)), func(t *testing.T) { 329 testFileDataWriteNewLevel(t, level) 330 }) 331 } 332 } 333 334 func testFileDataLevelExistingBlocks(t *testing.T, fd *FileData, 335 maxBlockSize Int64Offset, maxPtrsPerBlock int, existingData []byte, 336 holes []testFileDataHole, cleanBcache BlockCache) (*FileBlock, int) { 337 // First fill in the leaf blocks. 338 var off Int64Offset 339 existingDataLen := Int64Offset(len(existingData)) 340 var prevChildren []*FileBlock 341 var leafOffs []Int64Offset 342 nextHole := 0 343 for off < existingDataLen { 344 endOff := off + maxBlockSize 345 var nextOff Int64Offset 346 makeFinalHole := false 347 switch { 348 case nextHole < len(holes) && endOff > holes[nextHole].start: 349 endOff = holes[nextHole].start 350 nextOff = holes[nextHole].end 351 nextHole++ 352 makeFinalHole = nextHole >= len(holes) && 353 nextOff >= existingDataLen 354 case endOff > existingDataLen: 355 endOff = existingDataLen 356 nextOff = existingDataLen 357 default: 358 nextOff = endOff 359 } 360 361 fblock := NewFileBlock().(*FileBlock) 362 fblock.Contents = existingData[off:endOff] 363 prevChildren = append(prevChildren, fblock) 364 t.Logf("Initial leaf offset %d, size %d", off, len(fblock.Contents)) 365 leafOffs = append(leafOffs, off) 366 367 if makeFinalHole { 368 fblock := NewFileBlock().(*FileBlock) 369 prevChildren = append(prevChildren, fblock) 370 t.Logf("Initial leaf offset %d (final)", nextOff) 371 leafOffs = append(leafOffs, nextOff) 372 } 373 374 off = nextOff 375 } 376 377 // Now fill in any parents. 378 numLevels := 1 379 for len(prevChildren) != 1 { 380 prevChildIndex := 0 381 var level []*FileBlock 382 numAtLevel := int(math.Ceil(float64(len(prevChildren)) / 383 float64(maxPtrsPerBlock))) 384 for i := 0; i < numAtLevel; i++ { 385 // Split the previous children up (if any) into maxPtrsPerBlock 386 // chunks. 387 var children []*FileBlock 388 newIndex := prevChildIndex + maxPtrsPerBlock 389 if newIndex > len(prevChildren) { 390 newIndex = len(prevChildren) 391 } 392 children = prevChildren[prevChildIndex:newIndex] 393 fblock := NewFileBlock().(*FileBlock) 394 fblock.IsInd = true 395 dt := DirectBlock 396 if numLevels > 1 { 397 dt = IndirectBlock 398 } 399 for j, child := range children { 400 id, err := kbfsblock.MakeTemporaryID() 401 require.NoError(t, err) 402 ptr := BlockPointer{ 403 ID: id, 404 DirectType: dt, 405 } 406 var off Int64Offset 407 if child.IsInd { 408 off = child.IPtrs[0].Off 409 } else { 410 off = leafOffs[prevChildIndex+j] 411 } 412 413 fblock.IPtrs = append(fblock.IPtrs, IndirectFilePtr{ 414 BlockInfo: BlockInfo{ptr, 0}, 415 Off: off, 416 }) 417 err = cleanBcache.Put( 418 ptr, fd.tree.file.Tlf, child, TransientEntry, SkipCacheHash) 419 require.NoError(t, err) 420 } 421 prevChildIndex = newIndex 422 level = append(level, fblock) 423 } 424 prevChildren = level 425 numLevels++ 426 } 427 428 if numLevels > 1 { 429 fd.tree.file.Path[len(fd.tree.file.Path)-1].DirectType = IndirectBlock 430 } 431 432 err := cleanBcache.Put( 433 fd.rootBlockPointer(), fd.tree.file.Tlf, prevChildren[0], 434 TransientEntry, SkipCacheHash) 435 require.NoError(t, err) 436 return prevChildren[0], numLevels 437 } 438 439 func testFileDataWriteExtendExistingFile(t *testing.T, maxBlockSize Int64Offset, 440 maxPtrsPerBlock int, existingLen, fullDataLen Int64Offset) { 441 fd, cleanBcache, dirtyBcache, df := setupFileDataTest( 442 t, int64(maxBlockSize), maxPtrsPerBlock) 443 data := make([]byte, fullDataLen) 444 for i := 0; i < int(fullDataLen); i++ { 445 data[i] = byte(i) 446 } 447 topBlock, levels := testFileDataLevelExistingBlocks( 448 t, fd, maxBlockSize, maxPtrsPerBlock, data[:existingLen], nil, 449 cleanBcache) 450 de := DirEntry{ 451 EntryInfo: EntryInfo{ 452 Size: uint64(existingLen), 453 }, 454 } 455 expectedTopLevel := testFileDataLevelFromData( 456 t, maxBlockSize, maxPtrsPerBlock, levels, fullDataLen, nil, existingLen, 457 fullDataLen, 0, false) 458 459 extendedBytes := fullDataLen - existingLen 460 // Round up to find out the number of dirty bytes. 461 remainder := extendedBytes % maxBlockSize 462 dirtiedBytes := extendedBytes 463 if remainder > 0 { 464 dirtiedBytes += (maxBlockSize - remainder) 465 } 466 // Add a block's worth of dirty bytes if we're extending past the 467 // first full level, because the original block still gets dirtied 468 // because it needs to be inserted under a new ID. 469 if existingLen == maxBlockSize { 470 dirtiedBytes += maxBlockSize 471 } 472 testFileDataCheckWrite( 473 t, fd, dirtyBcache, df, data[existingLen:], existingLen, 474 topBlock, de, uint64(fullDataLen), 475 nil, int64(dirtiedBytes), int64(extendedBytes), expectedTopLevel) 476 477 // Make sure we can read back the complete data. 478 gotData := make([]byte, fullDataLen) 479 nRead, err := fd.Read(context.Background(), gotData, 0) 480 require.NoError(t, err) 481 require.Equal(t, nRead, int64(fullDataLen)) 482 require.True(t, bytes.Equal(data, gotData)) 483 } 484 485 func testFileDataExtendExistingLevels(t *testing.T, levels float64) { 486 capacity := math.Pow(2, levels) 487 halfCapacity := capacity / 2 488 // Starts with one lower level and adds a level. 489 testFileDataWriteExtendExistingFile( 490 t, 2, 2, Int64Offset(halfCapacity), Int64Offset(capacity)) 491 } 492 493 func TestFileDataExtendExistingLevels(t *testing.T) { 494 for _, level := range []float64{1, 2, 3, 10} { 495 // capture range variable. 496 level := level 497 t.Run(fmt.Sprintf("%dLevels", int(level)), func(t *testing.T) { 498 testFileDataExtendExistingLevels(t, level) 499 }) 500 } 501 } 502 503 func testFileDataOverwriteExistingFile(t *testing.T, maxBlockSize Int64Offset, 504 maxPtrsPerBlock int, fullDataLen Int64Offset, holes []testFileDataHole, 505 startWrite, endWrite Int64Offset, finalHoles []testFileDataHole) { 506 fd, cleanBcache, dirtyBcache, df := setupFileDataTest( 507 t, int64(maxBlockSize), maxPtrsPerBlock) 508 data := make([]byte, fullDataLen) 509 for i := 0; i < int(fullDataLen); i++ { 510 data[i] = byte(i) 511 } 512 holeShiftAfter := Int64Offset(0) 513 effectiveStartWrite := startWrite 514 for _, hole := range holes { 515 for i := hole.start; i < hole.end; i++ { 516 data[i] = byte(0) 517 if holeShiftAfter == 0 && startWrite <= i && i < endWrite { 518 holeShiftAfter = i 519 // If we're writing in a hole, we might extend the 520 // block on its left edge to its block boundary, 521 // which means that's effectively the start of the 522 // write. 523 effectiveStartWrite = hole.start 524 } 525 } 526 } 527 topBlock, levels := testFileDataLevelExistingBlocks( 528 t, fd, maxBlockSize, maxPtrsPerBlock, data, holes, cleanBcache) 529 de := DirEntry{ 530 EntryInfo: EntryInfo{ 531 Size: uint64(fullDataLen), 532 }, 533 } 534 535 t.Logf("holeShiftAfter=%d", holeShiftAfter) 536 expectedTopLevel := testFileDataLevelFromData( 537 t, maxBlockSize, maxPtrsPerBlock, levels, fullDataLen, finalHoles, 538 effectiveStartWrite, endWrite, holeShiftAfter, false) 539 540 // Round up to find out the number of dirty bytes. 541 writtenBytes := endWrite - startWrite 542 remainder := writtenBytes % maxBlockSize 543 dirtiedBytes := writtenBytes 544 if remainder > 0 { 545 dirtiedBytes += (maxBlockSize - remainder) 546 } 547 // Capture extending an existing block to its block boundary when 548 // writing in a hole. 549 if effectiveStartWrite != startWrite && 550 effectiveStartWrite%maxBlockSize > 0 { 551 dirtiedBytes += maxBlockSize 552 } 553 554 // The extended bytes are the size of the new blocks that were 555 // added. This isn't exactly right, but for now just pick the 556 // start of the last hole and round it up to the next block. 557 extendedBytes := Int64Offset(0) 558 existingBytes := holes[len(holes)-1].start 559 remainder = existingBytes % maxBlockSize 560 if remainder > 0 { 561 existingBytes += (maxBlockSize - remainder) 562 } 563 if endWrite > existingBytes { 564 extendedBytes = endWrite - existingBytes 565 // Also ignore any bytes that are still in the hole. 566 if existingBytes < holeShiftAfter { 567 extendedBytes -= holeShiftAfter - existingBytes 568 } 569 } 570 571 newData := make([]byte, writtenBytes) 572 for i := startWrite; i < endWrite; i++ { 573 // The new data shifts each byte over by 1. 574 newData[i-startWrite] = byte(i + 1) 575 } 576 testFileDataCheckWrite( 577 t, fd, dirtyBcache, df, newData, startWrite, 578 topBlock, de, uint64(fullDataLen), 579 nil, int64(dirtiedBytes), int64(extendedBytes), expectedTopLevel) 580 581 copy(data[startWrite:endWrite], newData) 582 583 // Make sure we can read back the complete data. 584 gotData := make([]byte, fullDataLen) 585 nRead, err := fd.Read(context.Background(), gotData, 0) 586 require.NoError(t, err) 587 require.Equal(t, nRead, int64(fullDataLen)) 588 require.True(t, bytes.Equal(data, gotData)) 589 } 590 591 func TestFileDataWriteHole(t *testing.T) { 592 type test struct { 593 name string 594 start Int64Offset 595 end Int64Offset 596 final []testFileDataHole 597 } 598 599 tests := []test{ 600 {"Start", 5, 8, []testFileDataHole{{8, 10}}}, 601 // The first final hole starts at 6, instead of 5, because a write 602 // extends the existing block. 603 {"End", 8, 10, []testFileDataHole{{6, 8}, {10, 10}}}, 604 // The first final hole starts at 6, instead of 5, because a write 605 // extends the existing block. 606 {"Middle", 7, 9, []testFileDataHole{{6, 7}, {9, 10}}}, 607 } 608 609 for _, test := range tests { 610 // capture range variable. 611 test := test 612 t.Run(test.name, func(t *testing.T) { 613 testFileDataOverwriteExistingFile(t, 2, 2, 10, 614 []testFileDataHole{{5, 10}}, test.start, test.end, test.final) 615 }) 616 } 617 } 618 619 func testFileDataCheckTruncateExtend(t *testing.T, fd *FileData, 620 dirtyBcache DirtyBlockCache, df *DirtyFile, size uint64, 621 topBlock *FileBlock, oldDe DirEntry, expectedTopLevel testFileDataLevel) { 622 // Do the extending truncate. 623 ctx := context.Background() 624 625 _, parentBlocks, _, _, _, _, err := 626 fd.GetFileBlockAtOffset(ctx, topBlock, Int64Offset(size), BlockWrite) 627 require.NoError(t, err) 628 629 newDe, dirtyPtrs, err := fd.TruncateExtend( 630 ctx, size, topBlock, parentBlocks, oldDe, df) 631 require.NoError(t, err) 632 633 // Check the basics. 634 require.Equal(t, size, newDe.Size) 635 636 // Go through each expected level and make sure we have the right 637 // set of dirty pointers and children. 638 expectedDirtyPtrs := expectedTopLevel.check( 639 t, fd, fd.rootBlockPointer(), 0, dirtyBcache) 640 dirtyPtrsMap := make(map[BlockPointer]bool) 641 for _, ptr := range dirtyPtrs { 642 dirtyPtrsMap[ptr] = true 643 } 644 require.True(t, reflect.DeepEqual(expectedDirtyPtrs, dirtyPtrsMap), 645 fmt.Sprintf("expected %v; got %v", expectedDirtyPtrs, dirtyPtrsMap)) 646 } 647 648 func testFileDataTruncateExtendFile(t *testing.T, maxBlockSize Int64Offset, 649 maxPtrsPerBlock int, currDataLen Int64Offset, newSize uint64, 650 holes []testFileDataHole) { 651 fd, cleanBcache, dirtyBcache, df := setupFileDataTest( 652 t, int64(maxBlockSize), maxPtrsPerBlock) 653 data := make([]byte, currDataLen) 654 for i := 0; i < int(currDataLen); i++ { 655 data[i] = byte(i) 656 } 657 for _, hole := range holes { 658 for i := hole.start; i < hole.end; i++ { 659 data[i] = byte(0) 660 } 661 } 662 topBlock, levels := testFileDataLevelExistingBlocks( 663 t, fd, maxBlockSize, maxPtrsPerBlock, data, holes, cleanBcache) 664 de := DirEntry{ 665 EntryInfo: EntryInfo{ 666 Size: uint64(currDataLen), 667 }, 668 } 669 670 expectedTopLevel := testFileDataLevelFromData( 671 t, maxBlockSize, maxPtrsPerBlock, levels, currDataLen, 672 append(holes, testFileDataHole{currDataLen, Int64Offset(newSize)}), 673 currDataLen, Int64Offset(newSize), 0, true) 674 675 testFileDataCheckTruncateExtend( 676 t, fd, dirtyBcache, df, newSize, topBlock, de, expectedTopLevel) 677 678 newZeroes := make([]byte, Int64Offset(newSize)-currDataLen) 679 data = append(data, newZeroes...) 680 681 // Make sure we can read back the complete data. 682 gotData := make([]byte, newSize) 683 nRead, err := fd.Read(context.Background(), gotData, 0) 684 require.NoError(t, err) 685 require.Equal(t, nRead, int64(newSize)) 686 require.True(t, bytes.Equal(data, gotData)) 687 } 688 689 func TestFileDataTruncateExtendLevel(t *testing.T) { 690 type test struct { 691 name string 692 currLen Int64Offset 693 newSize uint64 694 } 695 696 tests := []test{ 697 {"Same", 5, 8}, 698 {"New", 3, 8}, 699 } 700 701 for _, test := range tests { 702 // capture range variable. 703 test := test 704 t.Run(test.name, func(t *testing.T) { 705 testFileDataTruncateExtendFile( 706 t, 2, 2, test.currLen, test.newSize, nil) 707 }) 708 } 709 } 710 711 func testFileDataCheckTruncateShrink(t *testing.T, fd *FileData, 712 dirtyBcache DirtyBlockCache, size uint64, 713 topBlock *FileBlock, oldDe DirEntry, expectedUnrefs []BlockInfo, 714 expectedDirtiedBytes int64, expectedTopLevel testFileDataLevel) { 715 // Do the extending truncate. 716 ctx := context.Background() 717 718 newDe, dirtyPtrs, unrefs, newlyDirtiedChildBytes, err := fd.TruncateShrink( 719 ctx, size, topBlock, oldDe) 720 require.NoError(t, err) 721 722 // Check the basics. 723 require.Equal(t, size, newDe.Size) 724 require.Equal(t, expectedDirtiedBytes, newlyDirtiedChildBytes) 725 726 // Go through each expected level and make sure we have the right 727 // set of dirty pointers and children. 728 expectedDirtyPtrs := expectedTopLevel.check( 729 t, fd, fd.rootBlockPointer(), 0, dirtyBcache) 730 dirtyPtrsMap := make(map[BlockPointer]bool) 731 for _, ptr := range dirtyPtrs { 732 dirtyPtrsMap[ptr] = true 733 } 734 require.True(t, reflect.DeepEqual(expectedDirtyPtrs, dirtyPtrsMap), 735 fmt.Sprintf("expected %v; got %v", expectedDirtyPtrs, dirtyPtrsMap)) 736 737 // TODO: set the EncodedSize of the existing blocks to something 738 // non-zero so that we get some unrefs. 739 require.Len(t, unrefs, 0) 740 } 741 742 func testFileDataShrinkExistingFile(t *testing.T, maxBlockSize Int64Offset, 743 maxPtrsPerBlock int, existingLen int64, newSize uint64) { 744 fd, cleanBcache, dirtyBcache, _ := setupFileDataTest( 745 t, int64(maxBlockSize), maxPtrsPerBlock) 746 data := make([]byte, existingLen) 747 for i := 0; i < int(existingLen); i++ { 748 data[i] = byte(i) 749 } 750 topBlock, levels := testFileDataLevelExistingBlocks( 751 t, fd, maxBlockSize, maxPtrsPerBlock, data, nil, cleanBcache) 752 de := DirEntry{ 753 EntryInfo: EntryInfo{ 754 Size: uint64(existingLen), 755 }, 756 } 757 expectedTopLevel := testFileDataLevelFromData( 758 t, maxBlockSize, maxPtrsPerBlock, levels, Int64Offset(newSize), nil, 759 Int64Offset(newSize), 760 Int64Offset(int64(newSize)-existingLen), /*negative*/ 761 0, false) 762 763 // Round up to find out the number of dirty bytes. 764 dirtiedBytes := int64(newSize) % int64(maxBlockSize) 765 testFileDataCheckTruncateShrink( 766 t, fd, dirtyBcache, newSize, topBlock, de, nil, dirtiedBytes, 767 expectedTopLevel) 768 769 // Make sure we can read back the complete data. 770 gotData := make([]byte, newSize) 771 nRead, err := fd.Read(context.Background(), gotData, 0) 772 require.NoError(t, err) 773 require.Equal(t, nRead, int64(newSize)) 774 require.True(t, bytes.Equal(data[:newSize], gotData)) 775 } 776 777 func TestFileDataTruncateShrink(t *testing.T) { 778 type test struct { 779 name string 780 currLen int64 781 newSize uint64 782 } 783 784 tests := []test{ 785 {"WithinBlock", 6, 5}, 786 {"WithinLevel", 8, 5}, 787 {"ToZero", 8, 0}, 788 } 789 790 for _, test := range tests { 791 // capture range variable. 792 test := test 793 t.Run(test.name, func(t *testing.T) { 794 testFileDataShrinkExistingFile(t, 2, 2, test.currLen, test.newSize) 795 }) 796 } 797 } 798 799 func testFileDataWriteExtendExistingFileWithGap(t *testing.T, 800 maxBlockSize Int64Offset, maxPtrsPerBlock int, existingLen, 801 fullDataLen, startWrite Int64Offset, finalHoles []testFileDataHole) { 802 fd, cleanBcache, dirtyBcache, df := setupFileDataTest( 803 t, int64(maxBlockSize), maxPtrsPerBlock) 804 data := make([]byte, fullDataLen) 805 for i := Int64Offset(0); i < fullDataLen; i++ { 806 if i < existingLen || i >= startWrite { 807 data[i] = byte(i) 808 } 809 } 810 topBlock, levels := testFileDataLevelExistingBlocks( 811 t, fd, maxBlockSize, maxPtrsPerBlock, data[:existingLen], nil, 812 cleanBcache) 813 de := DirEntry{ 814 EntryInfo: EntryInfo{ 815 Size: uint64(existingLen), 816 }, 817 } 818 // The write starts at `existingLen`, instead of `startWrite`, 819 // because we need to account for any bytes dirtied when extending 820 // the block to the left of the gap. 821 expectedTopLevel := testFileDataLevelFromData( 822 t, maxBlockSize, maxPtrsPerBlock, levels, fullDataLen, 823 finalHoles, existingLen, fullDataLen, 0, false) 824 825 extendedBytes := fullDataLen - existingLen 826 // Round up to find out the number of dirty bytes. 827 dirtiedBytes := fullDataLen - startWrite 828 remainder := dirtiedBytes % maxBlockSize 829 if remainder > 0 { 830 dirtiedBytes += (maxBlockSize - remainder) 831 } 832 // Dirty the current last block as well, if needed. 833 if existingLen%maxBlockSize > 0 { 834 dirtiedBytes += maxBlockSize 835 } 836 // Add a block's worth of dirty bytes if we're extending past the 837 // first full level, because the original block still gets dirtied 838 // because it needs to be inserted under a new ID. 839 if existingLen == maxBlockSize { 840 dirtiedBytes += maxBlockSize 841 } 842 testFileDataCheckWrite( 843 t, fd, dirtyBcache, df, data[startWrite:], startWrite, 844 topBlock, de, uint64(fullDataLen), 845 nil, int64(dirtiedBytes), int64(extendedBytes), expectedTopLevel) 846 847 // Make sure we can read back the complete data. 848 gotData := make([]byte, fullDataLen) 849 nRead, err := fd.Read(context.Background(), gotData, 0) 850 require.NoError(t, err) 851 require.Equal(t, nRead, int64(fullDataLen)) 852 require.True(t, bytes.Equal(data, gotData)) 853 } 854 855 // Test that we can write past the end of the last block of a file, 856 // leaving a gap. Regression tests for KBFS-1915. 857 func TestFileDataWriteExtendExistingFileWithGap(t *testing.T) { 858 type test struct { 859 name string 860 currLen Int64Offset 861 newSize Int64Offset 862 startWrite Int64Offset 863 finalHoles []testFileDataHole 864 } 865 866 tests := []test{ 867 {"SwitchToIndirect", 1, 16, 10, []testFileDataHole{{2, 10}}}, 868 {"FullExistingBlock", 6, 16, 10, []testFileDataHole{{6, 10}}}, 869 {"FillExistingBlock", 5, 16, 10, []testFileDataHole{{6, 10}}}, 870 } 871 872 for _, test := range tests { 873 // capture range variable. 874 test := test 875 t.Run(test.name, func(t *testing.T) { 876 testFileDataWriteExtendExistingFileWithGap( 877 t, 2, 2, test.currLen, test.newSize, test.startWrite, 878 test.finalHoles) 879 }) 880 } 881 }