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
5package data
6
7import (
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
25func 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
75type testFileDataLevel struct {
76	dirty    bool
77	children []testFileDataLevel
78	off      Int64Offset
79	size     int
80}
81
82type testFileDataHole struct {
83	start Int64Offset
84	end   Int64Offset
85}
86
87func 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
207func (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
249func 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
281func 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
311func 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
324func 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
334func 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
439func 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
485func 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
493func 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
503func 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
591func 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
619func 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
648func 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
689func 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
711func 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
742func 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
777func 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
799func 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.
857func 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}
882