github.com/keybase/client/go@v0.0.0-20240309051027-028f7c731f8b/kbfs/libkbfs/cr_chains_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 libkbfs
     6  
     7  import (
     8  	"reflect"
     9  	"testing"
    10  	"time"
    11  
    12  	"github.com/keybase/client/go/kbfs/data"
    13  	idutiltest "github.com/keybase/client/go/kbfs/idutil/test"
    14  	"github.com/keybase/client/go/kbfs/kbfsblock"
    15  	"github.com/keybase/client/go/kbfs/kbfscodec"
    16  	"github.com/keybase/client/go/kbfs/kbfscrypto"
    17  	"github.com/keybase/client/go/kbfs/kbfsmd"
    18  	"github.com/keybase/client/go/kbfs/tlf"
    19  	"github.com/keybase/client/go/kbfs/tlfhandle"
    20  	"github.com/keybase/client/go/logger"
    21  	"github.com/keybase/client/go/protocol/keybase1"
    22  	"github.com/stretchr/testify/require"
    23  	"golang.org/x/net/context"
    24  )
    25  
    26  func checkExpectedChains(t *testing.T, expected map[data.BlockPointer]data.BlockPointer,
    27  	expectedRenames map[data.BlockPointer]renameInfo, expectedRoot data.BlockPointer,
    28  	cc *crChains, checkTailPtr bool) {
    29  	if g, e := len(cc.byOriginal), len(expected); g != e {
    30  		t.Errorf("Wrong number of originals, %v vs %v", g, e)
    31  	}
    32  
    33  	if g, e := len(cc.byMostRecent), len(expected); g != e {
    34  		t.Errorf("Wrong number of most recents, %v vs %v", g, e)
    35  	}
    36  
    37  	if g, e := len(cc.renamedOriginals), len(expectedRenames); g != e {
    38  		t.Errorf("Wrong number of renames, %v vs %v", g, e)
    39  	}
    40  
    41  	if cc.originalRoot != expectedRoot {
    42  		t.Fatalf("Root pointer incorrect for multi RMDs, %v vs %v",
    43  			cc.originalRoot, expectedRoot)
    44  	}
    45  
    46  	for original, mostRecent := range expected {
    47  		chain, ok := cc.byOriginal[original]
    48  		if !ok {
    49  			t.Fatalf("No original for %v", original)
    50  		}
    51  
    52  		if checkTailPtr && chain.mostRecent != mostRecent {
    53  			t.Fatalf("Chain for %v does not end in %v", original, mostRecent)
    54  		}
    55  
    56  		mrChain, ok := cc.byMostRecent[mostRecent]
    57  		if !ok {
    58  			t.Fatalf("No most recent for %v", mostRecent)
    59  		}
    60  
    61  		if chain != mrChain {
    62  			t.Fatalf("Chain from %v does not end in most recent %v "+
    63  				"(%v) vs. (%v)", original, mostRecent, chain, mrChain)
    64  		}
    65  	}
    66  
    67  	if !reflect.DeepEqual(cc.renamedOriginals, expectedRenames) {
    68  		t.Errorf("Actual renames don't match the expected renames: %v vs %v",
    69  			cc.renamedOriginals, expectedRenames)
    70  	}
    71  }
    72  
    73  func testCRInitPtrs(n int) (currPtr byte, ptrs []data.BlockPointer,
    74  	revPtrs map[data.BlockPointer]data.BlockPointer) {
    75  	currPtr = byte(42)
    76  	revPtrs = make(map[data.BlockPointer]data.BlockPointer)
    77  	for i := 0; i < n; i++ {
    78  		ptr := data.BlockPointer{ID: kbfsblock.FakeID(currPtr)}
    79  		currPtr++
    80  		ptrs = append(ptrs, ptr)
    81  		revPtrs[ptr] = ptr
    82  	}
    83  	return currPtr, ptrs, revPtrs
    84  }
    85  
    86  func testCRFillOpPtrs(currPtr byte,
    87  	expected map[data.BlockPointer]data.BlockPointer,
    88  	revPtrs map[data.BlockPointer]data.BlockPointer,
    89  	affectedPtrs []data.BlockPointer, op op) (nextCurrPtr byte) {
    90  	for _, ptr := range affectedPtrs {
    91  		newPtr := data.BlockPointer{ID: kbfsblock.FakeID(currPtr)}
    92  		currPtr++
    93  		op.AddUpdate(ptr, newPtr)
    94  		expected[revPtrs[ptr]] = newPtr
    95  		revPtrs[newPtr] = revPtrs[ptr]
    96  	}
    97  	return currPtr
    98  }
    99  
   100  // If one of the ops is a rename, it doesn't check for exact equality
   101  func testCRCheckOps(t *testing.T, cc *crChains, original data.BlockPointer,
   102  	expectedOps []op) {
   103  	chain, ok := cc.byOriginal[original]
   104  	if !ok {
   105  		t.Fatalf("No chain at %v", original)
   106  	}
   107  
   108  	if g, e := len(chain.ops), len(expectedOps); g != e {
   109  		t.Fatalf("Wrong number of operations: %d vs %d: %v", g, e, chain.ops)
   110  	}
   111  
   112  	codec := kbfscodec.NewMsgpack()
   113  	for i, op := range chain.ops {
   114  		eOp := expectedOps[i]
   115  		// First check for rename create ops.
   116  		if co, ok := op.(*createOp); ok && co.renamed {
   117  			eCOp, ok := eOp.(*createOp)
   118  			if !ok {
   119  				t.Errorf("Expected op isn't a create for %v[%d]", original, i)
   120  			}
   121  
   122  			if co.NewName != eCOp.NewName || co.Dir.Unref != eCOp.Dir.Unref ||
   123  				!eCOp.renamed {
   124  				t.Errorf("Bad create op after rename: %v", co)
   125  			}
   126  		} else if ro, ok := op.(*rmOp); ok &&
   127  			// We can tell the rm half of a rename because the updates
   128  			// aren't initialized.
   129  			len(ro.Updates) == 0 {
   130  			eROp, ok := eOp.(*rmOp)
   131  			if !ok {
   132  				t.Errorf("Expected op isn't an rm for %v[%d]", original, i)
   133  			}
   134  
   135  			if ro.OldName != eROp.OldName || ro.Dir.Unref != eROp.Dir.Unref ||
   136  				eROp.Dir.Ref.IsInitialized() {
   137  				t.Errorf("Bad create op after rename: %v", ro)
   138  			}
   139  		} else {
   140  			ok, err := kbfscodec.Equal(codec, op, eOp)
   141  			if err != nil {
   142  				t.Fatalf("Couldn't compare ops: %v", err)
   143  			}
   144  			if !ok {
   145  				t.Errorf("Unexpected op %v at %v[%d]; expected %v", op,
   146  					original, i, eOp)
   147  			}
   148  		}
   149  
   150  	}
   151  }
   152  
   153  func newChainMDForTest(t *testing.T) rootMetadataWithKeyAndTimestamp {
   154  	tlfID := tlf.FakeID(1, tlf.Private)
   155  
   156  	uid := keybase1.MakeTestUID(1)
   157  	bh, err := tlf.MakeHandle(
   158  		[]keybase1.UserOrTeamID{uid.AsUserOrTeam()}, nil, nil, nil, nil)
   159  	require.NoError(t, err)
   160  
   161  	nug := idutiltest.NormalizedUsernameGetter{
   162  		uid.AsUserOrTeam(): "fake_user",
   163  	}
   164  
   165  	ctx := context.Background()
   166  	h, err := tlfhandle.MakeHandle(
   167  		ctx, bh, bh.Type(), nil, nug, nil, keybase1.OfflineAvailability_NONE)
   168  	require.NoError(t, err)
   169  
   170  	rmd, err := makeInitialRootMetadata(defaultClientMetadataVer, tlfID, h)
   171  	require.NoError(t, err)
   172  	rmd.SetLastModifyingWriter(uid)
   173  	key := kbfscrypto.MakeFakeVerifyingKeyOrBust("fake key")
   174  	return rootMetadataWithKeyAndTimestamp{
   175  		rmd, key, time.Unix(0, 0),
   176  	}
   177  }
   178  
   179  func makeChainCodec() kbfscodec.Codec {
   180  	codec := kbfscodec.NewMsgpack()
   181  	RegisterOps(codec)
   182  	return codec
   183  }
   184  
   185  func TestCRChainsSingleOp(t *testing.T) {
   186  	chainMD := newChainMDForTest(t)
   187  
   188  	currPtr, ptrs, revPtrs := testCRInitPtrs(3)
   189  	rootPtrUnref := ptrs[0]
   190  	dir1Unref := ptrs[1]
   191  	dir2Unref := ptrs[2]
   192  	expected := make(map[data.BlockPointer]data.BlockPointer)
   193  
   194  	co, err := newCreateOp("new", dir2Unref, data.File)
   195  	require.NoError(t, err)
   196  	_ = testCRFillOpPtrs(currPtr, expected, revPtrs,
   197  		[]data.BlockPointer{rootPtrUnref, dir1Unref, dir2Unref}, co)
   198  	chainMD.AddOp(co)
   199  	chainMD.data.Dir.BlockPointer = expected[rootPtrUnref]
   200  
   201  	chainMDs := []chainMetadata{chainMD}
   202  	cc, err := newCRChains(
   203  		context.Background(), makeChainCodec(), nil, chainMDs, nil, true)
   204  	if err != nil {
   205  		t.Fatalf("Error making chains: %v", err)
   206  	}
   207  	checkExpectedChains(t, expected, make(map[data.BlockPointer]renameInfo),
   208  		rootPtrUnref, cc, true)
   209  
   210  	// check for the create op
   211  	testCRCheckOps(t, cc, dir2Unref, []op{co})
   212  }
   213  
   214  func TestCRChainsRenameOp(t *testing.T) {
   215  	chainMD := newChainMDForTest(t)
   216  
   217  	currPtr, ptrs, revPtrs := testCRInitPtrs(3)
   218  	rootPtrUnref := ptrs[0]
   219  	dir1Unref := ptrs[1]
   220  	dir2Unref := ptrs[2]
   221  	filePtr := data.BlockPointer{ID: kbfsblock.FakeID(currPtr)}
   222  	currPtr++
   223  	expected := make(map[data.BlockPointer]data.BlockPointer)
   224  	expectedRenames := make(map[data.BlockPointer]renameInfo)
   225  
   226  	oldName, newName := "old", "new"
   227  	ro, err := newRenameOp(oldName, dir1Unref, newName, dir2Unref, filePtr, data.File)
   228  	require.NoError(t, err)
   229  	expectedRenames[filePtr] = renameInfo{dir1Unref, "old", dir2Unref, "new"}
   230  	_ = testCRFillOpPtrs(currPtr, expected, revPtrs,
   231  		[]data.BlockPointer{rootPtrUnref, dir1Unref, dir2Unref}, ro)
   232  	chainMD.AddOp(ro)
   233  	chainMD.data.Dir.BlockPointer = expected[rootPtrUnref]
   234  
   235  	chainMDs := []chainMetadata{chainMD}
   236  	cc, err := newCRChains(
   237  		context.Background(), makeChainCodec(), nil, chainMDs, nil, true)
   238  	if err != nil {
   239  		t.Fatalf("Error making chains: %v", err)
   240  	}
   241  
   242  	checkExpectedChains(t, expected, expectedRenames, rootPtrUnref, cc, true)
   243  
   244  	co, err := newCreateOp(newName, dir2Unref, data.File)
   245  	require.NoError(t, err)
   246  	co.renamed = true
   247  	testCRCheckOps(t, cc, dir2Unref, []op{co})
   248  	rmo, err := newRmOp(oldName, dir1Unref, data.Dir)
   249  	require.NoError(t, err)
   250  	testCRCheckOps(t, cc, dir1Unref, []op{rmo})
   251  }
   252  
   253  func testCRChainsMultiOps(t *testing.T) ([]chainMetadata, data.BlockPointer) {
   254  	// To start, we have: root/dir1/dir2/file1 and root/dir3/file2
   255  	// Sequence of operations:
   256  	// * setex root/dir3/file2
   257  	// * createfile root/dir1/file3
   258  	// * rename root/dir3/file2 root/dir1/file4
   259  	// * write root/dir1/file4
   260  	// * rm root/dir1/dir2/file1
   261  
   262  	f1 := "file1"
   263  	f2 := "file2"
   264  	f3 := "file3"
   265  	f4 := "file4"
   266  
   267  	currPtr, ptrs, revPtrs := testCRInitPtrs(5)
   268  	rootPtrUnref := ptrs[0]
   269  	dir1Unref := ptrs[1]
   270  	dir2Unref := ptrs[2]
   271  	dir3Unref := ptrs[3]
   272  	file4Unref := ptrs[4]
   273  	file2Ptr := data.BlockPointer{ID: kbfsblock.FakeID(currPtr)}
   274  	currPtr++
   275  	expected := make(map[data.BlockPointer]data.BlockPointer)
   276  	expectedRenames := make(map[data.BlockPointer]renameInfo)
   277  
   278  	bigChainMD := newChainMDForTest(t)
   279  	var multiChainMDs []chainMetadata
   280  
   281  	// setex root/dir3/file2
   282  	op1, err := newSetAttrOp(f2, dir3Unref, exAttr, file2Ptr)
   283  	require.NoError(t, err)
   284  	currPtr = testCRFillOpPtrs(currPtr, expected, revPtrs,
   285  		[]data.BlockPointer{rootPtrUnref, dir3Unref}, op1)
   286  	expected[file2Ptr] = file2Ptr // no update to the file ptr
   287  	bigChainMD.AddOp(op1)
   288  	newChainMD := newChainMDForTest(t)
   289  	newChainMD.AddOp(op1)
   290  	newChainMD.data.Dir.BlockPointer = expected[rootPtrUnref]
   291  	multiChainMDs = append(multiChainMDs, newChainMD)
   292  
   293  	// createfile root/dir1/file3
   294  	op2, err := newCreateOp(f3, dir1Unref, data.File)
   295  	require.NoError(t, err)
   296  	currPtr = testCRFillOpPtrs(currPtr, expected, revPtrs,
   297  		[]data.BlockPointer{expected[rootPtrUnref], dir1Unref}, op2)
   298  	bigChainMD.AddOp(op2)
   299  	newChainMD = newChainMDForTest(t)
   300  	newChainMD.AddOp(op2)
   301  	newChainMD.data.Dir.BlockPointer = expected[rootPtrUnref]
   302  	multiChainMDs = append(multiChainMDs, newChainMD)
   303  
   304  	// rename root/dir3/file2 root/dir1/file4
   305  	op3, err := newRenameOp(f2, expected[dir3Unref], f4,
   306  		expected[dir1Unref], file2Ptr, data.File)
   307  	require.NoError(t, err)
   308  	expectedRenames[file2Ptr] = renameInfo{dir3Unref, f2, dir1Unref, f4}
   309  	currPtr = testCRFillOpPtrs(currPtr, expected, revPtrs,
   310  		[]data.BlockPointer{expected[rootPtrUnref], expected[dir1Unref],
   311  			expected[dir3Unref]}, op3)
   312  	bigChainMD.AddOp(op3)
   313  	newChainMD = newChainMDForTest(t)
   314  	newChainMD.AddOp(op3)
   315  	newChainMD.data.Dir.BlockPointer = expected[rootPtrUnref]
   316  	multiChainMDs = append(multiChainMDs, newChainMD)
   317  
   318  	// write root/dir1/file4
   319  	op4, err := newSyncOp(file4Unref)
   320  	require.NoError(t, err)
   321  	currPtr = testCRFillOpPtrs(currPtr, expected, revPtrs,
   322  		[]data.BlockPointer{expected[rootPtrUnref], expected[dir1Unref], file4Unref},
   323  		op4)
   324  	bigChainMD.AddOp(op4)
   325  	newChainMD = newChainMDForTest(t)
   326  	newChainMD.AddOp(op4)
   327  	newChainMD.data.Dir.BlockPointer = expected[rootPtrUnref]
   328  	multiChainMDs = append(multiChainMDs, newChainMD)
   329  
   330  	// rm root/dir1/dir2/file1
   331  	op5, err := newRmOp(f1, dir2Unref, data.File)
   332  	require.NoError(t, err)
   333  	_ = testCRFillOpPtrs(currPtr, expected, revPtrs,
   334  		[]data.BlockPointer{expected[rootPtrUnref], expected[dir1Unref], dir2Unref},
   335  		op5)
   336  	bigChainMD.AddOp(op5)
   337  	newChainMD = newChainMDForTest(t)
   338  	newChainMD.AddOp(op5)
   339  	newChainMD.data.Dir.BlockPointer = expected[rootPtrUnref]
   340  	multiChainMDs = append(multiChainMDs, newChainMD)
   341  
   342  	bigChainMD.data.Dir.BlockPointer = expected[rootPtrUnref]
   343  	chainMDs := []chainMetadata{bigChainMD}
   344  	cc, err := newCRChains(
   345  		context.Background(), makeChainCodec(), nil, chainMDs, nil, true)
   346  	if err != nil {
   347  		t.Fatalf("Error making chains for big chainMD: %v", err)
   348  	}
   349  	checkExpectedChains(t, expected, expectedRenames, rootPtrUnref, cc, true)
   350  
   351  	// root should have no direct ops
   352  	testCRCheckOps(t, cc, rootPtrUnref, []op{})
   353  
   354  	// dir1 should have two creates (one of which is a rename)
   355  	co1, err := newCreateOp(f4, op3.NewDir.Unref, data.File)
   356  	require.NoError(t, err)
   357  	co1.renamed = true
   358  	testCRCheckOps(t, cc, dir1Unref, []op{op2, co1})
   359  
   360  	// dir2 should have one rm op
   361  	testCRCheckOps(t, cc, dir2Unref, []op{op5})
   362  
   363  	// dir3 should have the rm part of a rename
   364  	ro3, err := newRmOp(f2, op3.OldDir.Unref, data.File)
   365  	require.NoError(t, err)
   366  	testCRCheckOps(t, cc, dir3Unref, []op{ro3})
   367  
   368  	// file2 should have the setattr
   369  	testCRCheckOps(t, cc, file2Ptr, []op{op1})
   370  
   371  	// file4 should have one op
   372  	testCRCheckOps(t, cc, file4Unref, []op{op4})
   373  
   374  	// now make sure the chain of MDs gets the same answers
   375  	mcc, err := newCRChains(
   376  		context.Background(), makeChainCodec(), nil, multiChainMDs, nil, true)
   377  	if err != nil {
   378  		t.Fatalf("Error making chains for multi chainMDs: %v", err)
   379  	}
   380  	if !reflect.DeepEqual(cc.byOriginal, mcc.byOriginal) {
   381  		t.Fatalf("Heads for multi chainMDs does not match original for big chainMD: %v",
   382  			mcc.byOriginal)
   383  	}
   384  	if !reflect.DeepEqual(cc.byMostRecent, mcc.byMostRecent) {
   385  		t.Fatalf("Tails for multi chainMDs does not match most recents for "+
   386  			"big chainMD: %v", mcc.byMostRecent)
   387  	}
   388  	if mcc.originalRoot != rootPtrUnref {
   389  		t.Fatalf("Root pointer incorrect for multi chainMDs, %v vs %v",
   390  			mcc.originalRoot, rootPtrUnref)
   391  	}
   392  	return multiChainMDs, file4Unref
   393  }
   394  
   395  // Test multiple operations, both in one MD and across multiple MDs
   396  func TestCRChainsMultiOps(t *testing.T) {
   397  	testCRChainsMultiOps(t)
   398  }
   399  
   400  // Test that we collapse chains correctly
   401  func TestCRChainsCollapse(t *testing.T) {
   402  	// To start, we have: root/dir1/ and root/dir2/file1
   403  	// Sequence of operations:
   404  	// * createfile root/dir1/file2
   405  	// * setex root/dir2/file1
   406  	// * createfile root/dir1/file3
   407  	// * createfile root/dir1/file4
   408  	// * rm root/dir1/file2
   409  	// * rename root/dir2/file1 root/dir1/file3
   410  	// * rm root/dir1/file3
   411  	// * rename root/dir1/file4 root/dir1/file5
   412  	// * rename root/dir1/file5 root/dir1/file3
   413  
   414  	f1 := "file1"
   415  	f2 := "file2"
   416  	f3 := "file3"
   417  	f4 := "file4"
   418  	f5 := "file5"
   419  
   420  	currPtr, ptrs, revPtrs := testCRInitPtrs(3)
   421  	rootPtrUnref := ptrs[0]
   422  	dir1Unref := ptrs[1]
   423  	dir2Unref := ptrs[2]
   424  	file1Ptr := data.BlockPointer{ID: kbfsblock.FakeID(currPtr)}
   425  	currPtr++
   426  	file4Ptr := data.BlockPointer{ID: kbfsblock.FakeID(currPtr)}
   427  	currPtr++
   428  	expected := make(map[data.BlockPointer]data.BlockPointer)
   429  	expectedRenames := make(map[data.BlockPointer]renameInfo)
   430  
   431  	chainMD := newChainMDForTest(t)
   432  
   433  	// createfile root/dir1/file2
   434  	op1, err := newCreateOp(f2, dir1Unref, data.File)
   435  	require.NoError(t, err)
   436  	currPtr = testCRFillOpPtrs(currPtr, expected, revPtrs,
   437  		[]data.BlockPointer{rootPtrUnref, dir1Unref}, op1)
   438  	chainMD.AddOp(op1)
   439  
   440  	// setex root/dir2/file1
   441  	op2, err := newSetAttrOp(f1, dir2Unref, exAttr, file1Ptr)
   442  	require.NoError(t, err)
   443  	currPtr = testCRFillOpPtrs(currPtr, expected, revPtrs,
   444  		[]data.BlockPointer{expected[rootPtrUnref], dir2Unref}, op2)
   445  	expected[file1Ptr] = file1Ptr
   446  	chainMD.AddOp(op2)
   447  
   448  	// createfile root/dir1/file3
   449  	op3, err := newCreateOp(f3, expected[dir1Unref], data.File)
   450  	require.NoError(t, err)
   451  	currPtr = testCRFillOpPtrs(currPtr, expected, revPtrs,
   452  		[]data.BlockPointer{expected[rootPtrUnref], expected[dir1Unref]}, op3)
   453  	chainMD.AddOp(op3)
   454  
   455  	// createfile root/dir1/file4
   456  	op4, err := newCreateOp(f4, expected[dir1Unref], data.File)
   457  	require.NoError(t, err)
   458  	currPtr = testCRFillOpPtrs(currPtr, expected, revPtrs,
   459  		[]data.BlockPointer{expected[rootPtrUnref], expected[dir1Unref]}, op4)
   460  	chainMD.AddOp(op4)
   461  
   462  	// rm root/dir1/file2
   463  	op5, err := newRmOp(f2, expected[dir1Unref], data.File)
   464  	require.NoError(t, err)
   465  	currPtr = testCRFillOpPtrs(currPtr, expected, revPtrs,
   466  		[]data.BlockPointer{expected[rootPtrUnref], expected[dir1Unref]}, op5)
   467  	chainMD.AddOp(op5)
   468  
   469  	// rename root/dir2/file1 root/dir1/file3
   470  	op6, err := newRenameOp(f1, expected[dir2Unref], f3, expected[dir1Unref],
   471  		file1Ptr, data.File)
   472  	require.NoError(t, err)
   473  	expectedRenames[file1Ptr] = renameInfo{dir2Unref, f1, dir1Unref, f3}
   474  	currPtr = testCRFillOpPtrs(currPtr, expected, revPtrs,
   475  		[]data.BlockPointer{expected[rootPtrUnref], expected[dir1Unref],
   476  			expected[dir2Unref]}, op6)
   477  	chainMD.AddOp(op6)
   478  
   479  	// rm root/dir1/file3
   480  	op7, err := newRmOp(f3, expected[dir1Unref], data.File)
   481  	require.NoError(t, err)
   482  	currPtr = testCRFillOpPtrs(currPtr, expected, revPtrs,
   483  		[]data.BlockPointer{expected[rootPtrUnref], expected[dir1Unref]}, op7)
   484  	chainMD.AddOp(op7)
   485  
   486  	// rename root/dir1/file4 root/dir1/file5
   487  	op8, err := newRenameOp(f4, expected[dir1Unref], f5, expected[dir1Unref],
   488  		file4Ptr, data.File)
   489  	require.NoError(t, err)
   490  	currPtr = testCRFillOpPtrs(currPtr, expected, revPtrs,
   491  		[]data.BlockPointer{expected[rootPtrUnref], expected[dir1Unref]}, op8)
   492  	chainMD.AddOp(op8)
   493  
   494  	// rename root/dir1/file5 root/dir1/file3
   495  	op9, err := newRenameOp(f5, expected[dir1Unref], f3, expected[dir1Unref],
   496  		file4Ptr, data.File)
   497  	require.NoError(t, err)
   498  	// expected the previous old name, not the new one
   499  	expectedRenames[file4Ptr] = renameInfo{dir1Unref, f4, dir1Unref, f3}
   500  	_ = testCRFillOpPtrs(currPtr, expected, revPtrs,
   501  		[]data.BlockPointer{expected[rootPtrUnref], expected[dir1Unref]}, op9)
   502  	chainMD.AddOp(op9)
   503  
   504  	chainMD.data.Dir.BlockPointer = expected[rootPtrUnref]
   505  	chainMDs := []chainMetadata{chainMD}
   506  	cc, err := newCRChains(
   507  		context.Background(), makeChainCodec(), nil, chainMDs, nil, true)
   508  	if err != nil {
   509  		t.Fatalf("Error making chains: %v", err)
   510  	}
   511  	checkExpectedChains(t, expected, expectedRenames, rootPtrUnref, cc,
   512  		false /*tail ref pointer won't match due to collapsing*/)
   513  
   514  	// root should have no direct ops
   515  	testCRCheckOps(t, cc, rootPtrUnref, []op{})
   516  
   517  	// dir1 should only have one createOp (the final rename)
   518  	co1, err := newCreateOp(f3, op9.OldDir.Unref, data.File)
   519  	require.NoError(t, err)
   520  	co1.renamed = true
   521  	testCRCheckOps(t, cc, dir1Unref, []op{co1})
   522  
   523  	// dir2 should have the rm part of a rename
   524  	ro2, err := newRmOp(f1, op6.OldDir.Unref, data.File)
   525  	require.NoError(t, err)
   526  	testCRCheckOps(t, cc, dir2Unref, []op{ro2})
   527  
   528  	// file1 should have the setattr
   529  	testCRCheckOps(t, cc, file1Ptr, []op{op2})
   530  }
   531  
   532  func TestCRChainsRemove(t *testing.T) {
   533  	chainMDs, writtenFileUnref := testCRChainsMultiOps(t)
   534  
   535  	for i := range chainMDs {
   536  		chainMDs[i].(rootMetadataWithKeyAndTimestamp).RootMetadata.SetRevision(
   537  			kbfsmd.Revision(i))
   538  	}
   539  
   540  	ccs, err := newCRChains(
   541  		context.Background(), makeChainCodec(), nil, chainMDs, nil, true)
   542  	if err != nil {
   543  		t.Fatalf("Error making chains: %v", err)
   544  	}
   545  
   546  	// This should remove the write operation.
   547  	removedChains := ccs.remove(context.Background(),
   548  		logger.NewTestLogger(t), chainMDs[3].Revision())
   549  	require.Len(t, removedChains, 1)
   550  	require.Equal(t, removedChains[0].original, writtenFileUnref)
   551  	require.Len(t, removedChains[0].ops, 0)
   552  }
   553  
   554  func TestCRChainsCollapsedSyncOps(t *testing.T) {
   555  	chainMD := newChainMDForTest(t)
   556  
   557  	currPtr, ptrs, revPtrs := testCRInitPtrs(3)
   558  	rootPtrUnref := ptrs[0]
   559  	file1Unref := ptrs[1]
   560  	file2Unref := ptrs[2]
   561  	expected := make(map[data.BlockPointer]data.BlockPointer)
   562  
   563  	// Alternate contiguous writes between two files
   564  	currOff := uint64(0)
   565  	writeLen := uint64(10)
   566  	numWrites := uint64(3)
   567  	expected[rootPtrUnref] = rootPtrUnref
   568  	expected[file1Unref] = file1Unref
   569  	expected[file2Unref] = file2Unref
   570  
   571  	var so1, so2 *syncOp
   572  	var err error
   573  	for i := uint64(0); i < numWrites; i++ {
   574  		so1, err = newSyncOp(expected[file1Unref])
   575  		require.NoError(t, err)
   576  		so1.Writes = append(so1.Writes, WriteRange{Off: currOff, Len: writeLen})
   577  		currPtr = testCRFillOpPtrs(currPtr, expected, revPtrs,
   578  			[]data.BlockPointer{expected[rootPtrUnref], expected[file1Unref]}, so1)
   579  		chainMD.AddOp(so1)
   580  		chainMD.data.Dir.BlockPointer = expected[rootPtrUnref]
   581  
   582  		so2, err = newSyncOp(expected[file2Unref])
   583  		require.NoError(t, err)
   584  		so2.Writes = append(so2.Writes, WriteRange{Off: currOff, Len: writeLen})
   585  		currPtr = testCRFillOpPtrs(currPtr, expected, revPtrs,
   586  			[]data.BlockPointer{expected[rootPtrUnref], expected[file2Unref]}, so2)
   587  		chainMD.AddOp(so2)
   588  		chainMD.data.Dir.BlockPointer = expected[rootPtrUnref]
   589  
   590  		currOff += writeLen
   591  	}
   592  
   593  	chainMDs := []chainMetadata{chainMD}
   594  	cc, err := newCRChains(
   595  		context.Background(), makeChainCodec(), nil, chainMDs, nil, true)
   596  	if err != nil {
   597  		t.Fatalf("Error making chains: %v", err)
   598  	}
   599  	checkExpectedChains(t, expected, make(map[data.BlockPointer]renameInfo),
   600  		rootPtrUnref, cc, true)
   601  
   602  	// newCRChains copies the ops, so modifying them now is ok.
   603  	so1.Writes = []WriteRange{{Off: 0, Len: writeLen * numWrites}}
   604  	so2.Writes = []WriteRange{{Off: 0, Len: writeLen * numWrites}}
   605  
   606  	// Check for the collapsed syncOps.
   607  	testCRCheckOps(t, cc, file1Unref, []op{so1})
   608  	testCRCheckOps(t, cc, file2Unref, []op{so2})
   609  }