github.com/mgoltzsche/ctnr@v0.7.1-alpha/pkg/fs/tree/fsnode_test.go (about)

     1  package tree
     2  
     3  import (
     4  	"bytes"
     5  	"errors"
     6  	"fmt"
     7  	"net/url"
     8  	"os"
     9  	"path/filepath"
    10  	"regexp"
    11  	"sort"
    12  	"strings"
    13  	"testing"
    14  	"time"
    15  
    16  	"github.com/mgoltzsche/ctnr/pkg/fs"
    17  	"github.com/mgoltzsche/ctnr/pkg/fs/source"
    18  	"github.com/mgoltzsche/ctnr/pkg/fs/testutils"
    19  	"github.com/mgoltzsche/ctnr/pkg/idutils"
    20  	"github.com/stretchr/testify/assert"
    21  	"github.com/stretchr/testify/require"
    22  )
    23  
    24  var (
    25  	testErr error
    26  )
    27  
    28  func TestFsNode(t *testing.T) {
    29  	tt := fsNodeTester{t, newFsNodeTree(t, true)}
    30  
    31  	// Test node tree
    32  
    33  	mockWriter := testutils.NewWriterMock(t, fs.AttrsHash)
    34  	err := tt.node.Write(mockWriter)
    35  	require.NoError(t, err)
    36  	if !assert.Equal(t, expectedNodeOps(), mockWriter.Written, "node tree construction") {
    37  		t.FailNow()
    38  	}
    39  
    40  	// Test to/from string conversion
    41  
    42  	tt.node = newFsNodeTree(t, true)
    43  	if !assert.Equal(t, "/ type=dir usr=1:1 mode=750 mtime=1516669302", tt.node.String(), "String()") {
    44  		t.FailNow()
    45  	}
    46  
    47  	var buf bytes.Buffer
    48  	err = tt.FS().WriteTo(&buf, fs.AttrsAll)
    49  	require.NoError(t, err)
    50  	input := strings.TrimSpace(buf.String())
    51  	expectedLines := strings.Split(input, "\n")
    52  
    53  	parsed, err := ParseFsSpec([]byte(input))
    54  	if err != nil {
    55  		fmt.Println("INPUT:\n" + input)
    56  		t.Errorf("ParseFsSpec() returned error: %s (input may be wrong)", err)
    57  		t.FailNow()
    58  	}
    59  	require.NoError(t, err)
    60  	expectedNodes := testutils.MockWrites(t, tt.node).Written
    61  	actualNodes := testutils.MockWrites(t, parsed).Nodes
    62  	if !assert.Equal(t, expectedNodes, actualNodes, "parsed node structure") {
    63  		fmt.Println("EXPECTED:\n" + strings.Join(expectedNodes, "\n") + "\nINPUT:\n" + input)
    64  		t.FailNow()
    65  	}
    66  
    67  	// Assert String(Parse(s)) == s
    68  	buf.Reset()
    69  	err = parsed.WriteTo(&buf, fs.AttrsCompare)
    70  	require.NoError(t, err)
    71  	lines := strings.Split(strings.TrimSpace(buf.String()), "\n")
    72  	if !assert.Equal(t, expectedLines, lines, "String(ParseFsSpec(s)) != s") {
    73  		fmt.Println("INPUT:\n" + input + "\n\nOUTPUT:\n" + buf.String() + "\n")
    74  		t.FailNow()
    75  	}
    76  
    77  	// Assert mockWrites(t, parsed).written should be empty
    78  	parsed.(*FsNode).RemoveWhiteouts()
    79  	if !assert.Equal(t, []string{}, testutils.MockWrites(t, parsed).Written, "nodes written from parsed node structure") {
    80  		t.FailNow()
    81  	}
    82  
    83  	// Assert mockWrites(t, add(parsed, addFile)).written should only write changed files
    84  	mtime, err := time.Parse(time.RFC3339, "2018-01-23T01:01:42Z")
    85  	require.NoError(t, err)
    86  	times := fs.FileTimes{Mtime: mtime}
    87  	changedFile := testutils.NewSourceMock(fs.TypeFile, fs.FileAttrs{Mode: 0755, UserIds: idutils.UserIds{5000, 5000}, Size: 546868, FileTimes: times}, "sha256:newhex")
    88  	addNodes := func(f fs.FsNode) {
    89  		added, err := f.AddUpper("/etc/addedFile", changedFile)
    90  		require.NoError(t, err)
    91  		require.NotNil(t, added)
    92  		_, err = f.AddUpper("/etc/addedDir", source.NewSourceDir(fs.FileAttrs{Mode: os.ModeDir | 0754}))
    93  		require.NoError(t, err)
    94  		_, err = f.AddUpper("/etc/addedLink", changedFile)
    95  		require.NoError(t, err)
    96  		_, err = f.AddUpper("/etc/xnewdir/fifo", source.NewSourceFifo(fs.DeviceAttrs{fs.FileAttrs{Mode: 0644, UserIds: idutils.UserIds{0, 33}, FileTimes: times}, 0, 0}))
    97  		require.NoError(t, err)
    98  		_, err = f.AddWhiteout("/etc/symlink1/lfile1")
    99  		require.NoError(t, err)
   100  		existNode, err := f.Node("/etc/file1")
   101  		require.NoError(t, err)
   102  		existNode.Remove()
   103  		existNode, err = f.Node("/etc/dir2")
   104  		require.NoError(t, err)
   105  		existNode.Remove()
   106  		// Replace lower link
   107  		_, err = f.AddUpper("/etc/link3", source.NewSourceFifo(fs.DeviceAttrs{fs.FileAttrs{Mode: 0644, UserIds: idutils.UserIds{0, 33}, FileTimes: times}, 0, 0}))
   108  		require.NoError(t, err)
   109  	}
   110  	addNodes(parsed)
   111  	expectedOps := []string{
   112  		"/ type=dir usr=1:1 mode=750",
   113  		"/etc type=dir mode=755",
   114  		"/etc/addedDir type=dir mode=754",
   115  		"/etc/addedFile type=file usr=5000:5000 mode=755 size=546868 hash=sha256:newhex",
   116  		"/etc/addedLink hlink=/etc/addedFile",
   117  		"/etc/link3 type=fifo usr=0:33 mode=644",
   118  		"/etc/xdest type=dir usr=0:33 mode=755",
   119  		"/etc/xdest/lfile1 type=whiteout",
   120  		"/etc/xnewdir type=dir mode=755",
   121  		"/etc/xnewdir/fifo type=fifo usr=0:33 mode=644",
   122  	}
   123  	if !assert.Equal(t, expectedOps, testutils.MockWrites(t, parsed).Written, "nodes written after changes applied to parsed node structure") {
   124  		t.FailNow()
   125  	}
   126  
   127  	// Assert parsed.Diff(fs) is empty
   128  	node := newFsNodeTree(t, false)
   129  	node.RemoveWhiteouts()
   130  	buf.Reset()
   131  	err = node.WriteTo(&buf, fs.AttrsAll)
   132  	require.NoError(t, err)
   133  	nodeStr := buf.String()
   134  	expectedLines = strings.Split(nodeStr, "\n")
   135  	parsed, err = ParseFsSpec([]byte(nodeStr))
   136  	require.NoError(t, err)
   137  	diff, err := parsed.Diff(node)
   138  	require.NoError(t, err)
   139  	if !assert.Equal(t, []string{}, testutils.MockWrites(t, diff).Written, "a.Diff(a) should be empty") {
   140  		fmt.Println("##\n", nodeStr)
   141  		t.FailNow()
   142  	}
   143  	// Assert fs.Diff(fs) has empty string representation
   144  	buf.Reset()
   145  	err = diff.WriteTo(&buf, fs.AttrsCompare)
   146  	require.NoError(t, err)
   147  	if !assert.Equal(t, []string{". type=dir", ""}, strings.Split(buf.String(), "\n"), "string(fs.Diff(fs))") {
   148  		t.FailNow()
   149  	}
   150  	// Assert fs.Diff(fs).Empty() is true
   151  	if !assert.True(t, NewFS().Empty(), "NewFS().Empty()") {
   152  		t.FailNow()
   153  	}
   154  	if !assert.True(t, diff.Empty(), "fs.Diff(fs).Empty()") {
   155  		t.FailNow()
   156  	}
   157  	// Assert parsed.Diff(changedParsed) == changes
   158  	expectedOps = append(expectedOps,
   159  		// files that don't exist in file system b
   160  		"/etc/file1 type=whiteout",
   161  		"/etc/dir2 type=whiteout",
   162  	)
   163  	sort.Strings(expectedOps)
   164  	changedParsed, err := ParseFsSpec([]byte(nodeStr))
   165  	require.NoError(t, err)
   166  	addNodes(changedParsed)
   167  	changes, err := parsed.Diff(changedParsed)
   168  	require.NoError(t, err)
   169  	if !assert.Equal(t, expectedOps, testutils.MockWrites(t, changes).Written, "diff of nodes written after changes applied to parsed node structure") {
   170  		t.FailNow()
   171  	}
   172  	if !assert.False(t, changes.Empty(), "fs.Diff(changedFs).Empty()") {
   173  		t.FailNow()
   174  	}
   175  	// Assert parsed.Diff(otherFS) == change
   176  	addNodes(node)
   177  	// Node that equals existing should not be included in diff
   178  	_, err = node.AddUpper("/etc/dir1", source.NewSourceDir(fs.FileAttrs{Mode: os.ModeDir | 0755, UserIds: idutils.UserIds{0, 33}, FileTimes: times}))
   179  	require.NoError(t, err)
   180  	// Hardlink to unchanged file and to existing hardlink
   181  	oldFile := testutils.NewSourceMock(fs.TypeFile, fs.FileAttrs{Mode: 0644, UserIds: idutils.UserIds{1, 1}, Size: 689876, FileTimes: times}, "sha256:hex2")
   182  	oldFile2 := *oldFile
   183  	_, err = node.AddUpper("/etc/file2", &oldFile2)
   184  	require.NoError(t, err)
   185  	_, err = node.AddUpper("/etc/link1", oldFile)
   186  	require.NoError(t, err)
   187  	_, err = node.AddUpper("/etc/link2", oldFile)
   188  	require.NoError(t, err)
   189  	diff, err = parsed.Diff(node)
   190  	require.NoError(t, err)
   191  	if !assert.Equal(t, expectedOps, testutils.MockWrites(t, diff).Written, "parsed.Diff(otherFS)") {
   192  		t.FailNow()
   193  	}
   194  	_, err = node.AddUpper("/etc/xnewlinktooldfile", oldFile)
   195  	require.NoError(t, err)
   196  	diff, err = parsed.Diff(node)
   197  	require.NoError(t, err)
   198  	expectedOps = append(expectedOps,
   199  		// implicitly added lower file to layer to preserve hardlink in a compatible way
   200  		"/etc/link1 type=file usr=1:1 mode=644 size=689876 hash=sha256:hex2",
   201  		"/etc/link2 hlink=/etc/link1",
   202  		"/etc/xnewlinktooldfile hlink=/etc/link1",
   203  	)
   204  	sort.Strings(expectedOps)
   205  	if !assert.Equal(t, expectedOps, testutils.MockWrites(t, diff).Written, "parsed.Diff(otherFsWithLinkToUnchangedFiles)") {
   206  		t.FailNow()
   207  	}
   208  
   209  	// Test Hash()
   210  
   211  	// Hash() from new fs
   212  	hash1, err := tt.FS().Hash(fs.AttrsHash)
   213  	require.NoError(t, err)
   214  	tt.node = newFsNodeTree(t, true)
   215  	hash2, err := tt.FS().Hash(fs.AttrsHash)
   216  	require.NoError(t, err)
   217  	if hash1 != hash2 {
   218  		t.Errorf("Hash(): same content should result in same hash")
   219  		t.FailNow()
   220  	}
   221  	tt.add(changedFile, "/etc/file2")
   222  	hash2, err = tt.FS().Hash(fs.AttrsHash)
   223  	require.NoError(t, err)
   224  	if hash1 == hash2 {
   225  		t.Errorf("Hash(): should change when contents changed")
   226  		t.FailNow()
   227  	}
   228  	tt.node = newFsNodeTree(t, true)
   229  
   230  	// Hash() of two separate but equal file system diffs
   231  	parsed, err = ParseFsSpec([]byte(nodeStr))
   232  	require.NoError(t, err)
   233  	changedA, err := ParseFsSpec([]byte(nodeStr))
   234  	require.NoError(t, err)
   235  	changedB, err := ParseFsSpec([]byte(nodeStr))
   236  	require.NoError(t, err)
   237  	addNodes(changedA)
   238  	addNodes(changedB)
   239  	diffA, err := parsed.Diff(changedA)
   240  	require.NoError(t, err)
   241  	diffB, err := parsed.Diff(changedB)
   242  	require.NoError(t, err)
   243  	hashA, err := diffA.Hash(fs.AttrsHash)
   244  	require.NoError(t, err)
   245  	hashB, err := diffB.Hash(fs.AttrsHash)
   246  	require.NoError(t, err)
   247  	if hashA != hashB {
   248  		t.Errorf("diffA.Hash() != diffB.Hash()")
   249  		t.FailNow()
   250  	}
   251  
   252  	//
   253  	// TEST ERROR HANDLING
   254  	//
   255  
   256  	testErr = errors.New("expected error")
   257  	defer func() {
   258  		testErr = nil
   259  	}()
   260  
   261  	// Test WriteTo() returns error
   262  	src := testutils.NewSourceMock(fs.TypeDir, fs.FileAttrs{Mode: 0644}, "")
   263  	src.Err = testErr
   264  	tt.add(src, "addedbroken")
   265  	err = tt.FS().WriteTo(&buf, fs.AttrsAll)
   266  	require.Error(t, err)
   267  
   268  	// Test Hash() returns error
   269  	_, err = tt.FS().Hash(fs.AttrsAll)
   270  	require.Error(t, err)
   271  
   272  	// Test Write() returns error
   273  	err = tt.node.Write(mockWriter)
   274  	require.Error(t, err)
   275  }
   276  
   277  func newFsNodeTree(t *testing.T, withOverlay bool) *FsNode {
   278  	mtime, err := time.Parse(time.RFC3339, "2018-01-23T01:01:42Z")
   279  	require.NoError(t, err)
   280  	times := fs.FileTimes{Mtime: mtime}
   281  	usr1 := &idutils.UserIds{0, 33}
   282  	usr2 := &idutils.UserIds{1, 1}
   283  	srcDir1 := testutils.NewSourceMock(fs.TypeDir, fs.FileAttrs{Mode: os.ModeDir | 0755, UserIds: *usr1, FileTimes: times}, "")
   284  	newSrcFile1 := func() fs.Source {
   285  		return testutils.NewSourceMock(fs.TypeFile, fs.FileAttrs{Mode: 0755, UserIds: *usr1, Size: 12345, FileTimes: times}, "sha256:hex1")
   286  	}
   287  	srcDir2 := testutils.NewSourceMock(fs.TypeDir, fs.FileAttrs{Mode: os.ModeDir | 0750, UserIds: *usr2, FileTimes: times}, "")
   288  	srcDir3 := *srcDir2
   289  	srcDir3.Xattrs = map[string]string{"k": "v"}
   290  	newSrcFile2 := func() fs.Source {
   291  		return testutils.NewSourceMock(fs.TypeFile, fs.FileAttrs{Mode: 0644, UserIds: *usr2, Size: 689876, FileTimes: times}, "sha256:hex2")
   292  	}
   293  	srcSymlink1 := testutils.NewSourceMock(fs.TypeSymlink, fs.FileAttrs{Symlink: "xdest", UserIds: *usr1, FileTimes: times}, "")
   294  	srcSymlink2 := testutils.NewSourceMock(fs.TypeSymlink, fs.FileAttrs{Symlink: "../etc/xdest", UserIds: *usr2, FileTimes: times}, "")
   295  	srcSymlink3 := testutils.NewSourceMock(fs.TypeSymlink, fs.FileAttrs{Symlink: "/etc/xnewdest/newdir", UserIds: *usr1, FileTimes: times}, "")
   296  	srcLink := newSrcFile2()
   297  	srcArchive1 := &testutils.SourceOverlayMock{testutils.NewSourceMock(fs.TypeOverlay, fs.FileAttrs{UserIds: *usr1, Size: 98765, FileTimes: times}, "sha256:hex3")}
   298  	srcArchive2 := &testutils.SourceOverlayMock{testutils.NewSourceMock(fs.TypeOverlay, fs.FileAttrs{UserIds: *usr1, Size: 87658, FileTimes: times}, "sha256:hex4")}
   299  	tt := fsNodeTester{t, newFS()}
   300  	tt.add(srcDir1, "")
   301  	tt.add(srcDir1, "/emptydir")
   302  	tt.add(srcDir1, "/root/empty dir")
   303  	tt.add(srcDir2, "")
   304  	tt.add(srcDir2, ".")
   305  	tt.add(srcDir2, "/")
   306  	xdest := tt.add(srcDir1, "/etc/xdest")
   307  	tt.add(srcDir1, "/etc/xdest/overridewithfile")
   308  	tt.add(newSrcFile1(), "/etc/xdest/overridewithdir")
   309  	tt.add(newSrcFile2(), "/etc/file2")
   310  	tt.add(srcSymlink2, "/etc/symlink2")
   311  	tt.add(srcDir2, "/etc/dir1")
   312  	tt.add(srcDir1, "/etc/dir1").
   313  		add(&srcDir3, "../dir2/").
   314  		add(&srcDir3, "../../etc/dir2/")
   315  	srcFile1 := newSrcFile1()
   316  	tt.add(srcFile1, "/etc/file1")
   317  	srcFile2 := newSrcFile2()
   318  	tt.add(srcFile2, "/etc/dir2/x/y/filem")
   319  
   320  	// Test symlinks
   321  	tt.add(srcSymlink2, "/etc/symlink2")
   322  	tt.add(srcSymlink3, "/etc/symlink3")
   323  	symlink := tt.add(srcSymlink1, "/etc/symlink1")
   324  	tt.add(newSrcFile1(), "/etc/symlink1/lfile1")
   325  	tt.add(newSrcFile1(), "/etc/symlink1/overridewithfile")
   326  	tt.add(srcDir1, "/etc/symlink1/overridewithdir")
   327  	tt.add(srcDir1, "/etc/symlink1/ldir1")
   328  	srcFile1ResolvedParent := newSrcFile1()
   329  	tt.add(srcFile1ResolvedParent, "/etc/symlink2/lfile2")
   330  	tt.add(newSrcFile1(), "/etc/symlink3/lfile3")
   331  
   332  	// Test link
   333  	tt.add(srcLink, "/etc/link1")
   334  	tt.add(srcLink, "/etc/link2")
   335  	tt.add(srcLink, "/etc/link3")
   336  	tt.add(srcLink, "/etc/linkreplacewithparentdir")
   337  	tt.add(newSrcFile1(), "/etc/linkreplacewithparentdir/lfile3")
   338  	tt.add(srcLink, "/etc/linkreplacewithdir")
   339  	tt.add(srcDir1, "/etc/linkreplacewithdir")
   340  	tt.add(srcLink, "/etc/linkreplacewithfile")
   341  	tt.add(newSrcFile1(), "/etc/linkreplacewithfile")
   342  
   343  	// Test node overwrites
   344  	tt.add(newSrcFile1(), "/etc/fileoverwrite")
   345  	tt.add(newSrcFile2(), "/etc/fileoverwrite")
   346  	tt.add(newSrcFile1(), "/etc/fileoverwriteimplicit")
   347  	tt.add(newSrcFile2(), "/etc/fileoverwriteimplicit/filex")
   348  	tt.add(newSrcFile1(), "/etc/diroverwrite1/file")
   349  	tt.add(srcDir2, "/etc/diroverwrite1")
   350  	tt.add(newSrcFile1(), "/etc/diroverwrite2/file")
   351  	tt.add(newSrcFile2(), "/etc/diroverwrite2")
   352  	tt.add(srcSymlink1, "/etc/symlinkoverwritefile")
   353  	tt.add(newSrcFile1(), "/etc/symlinkoverwritefile")
   354  	tt.add(srcSymlink1, "/etc/symlinkoverwritedir")
   355  	tt.add(srcDir1, "/etc/symlinkoverwritedir")
   356  	// Test whiteout
   357  	tt.add(newSrcFile1(), "/etc/filetobeoverwrittenbywhiteout")
   358  	wh, err := tt.node.AddWhiteout("/etc/filetobeoverwrittenbywhiteout")
   359  	require.NoError(t, err)
   360  	assert.NotNil(t, wh)
   361  	tt.add(srcDir1, "/etc/dirtobeoverwrittenbywhiteout")
   362  	tt.add(newSrcFile1(), "/etc/dirtobeoverwrittenbywhiteout/nestedToBeDel")
   363  	wh, err = tt.node.AddWhiteout("/etc/dirtobeoverwrittenbywhiteout")
   364  	require.NoError(t, err)
   365  	assert.NotNil(t, wh)
   366  	tt.add(newSrcFile1(), "/etc/dircontainingwhiteout/whiteoutfile")
   367  	wh, err = tt.node.AddWhiteout("/etc/dircontainingwhiteout/whiteoutfile")
   368  	require.NoError(t, err)
   369  	assert.NotNil(t, wh)
   370  	// Test remove
   371  	rmDir := tt.add(srcDir1, "/etc/dirtoberemoved")
   372  	tt.add(newSrcFile1(), "/etc/dirtoberemoved/nestedfiletoberemoved")
   373  	rmFile := tt.add(newSrcFile1(), "/etc/filetoberemoved")
   374  	tt.add(srcDir1, "/etc/parentrmdir")
   375  	rmChild := tt.add(srcDir1, "/etc/parentrmdir/1stchildtoberemoved")
   376  	rmDir.node.Remove()
   377  	rmFile.node.Remove()
   378  	rmChild.node.Remove()
   379  
   380  	// Test overlay
   381  	if withOverlay {
   382  		tt.add(newSrcFile1(), "/overlay1/dir1/file1")
   383  		tt.add(srcArchive1, "/overlay1")
   384  		// /overlay1 dir permissions should be set after archive has been extracted
   385  		tt.add(srcDir2, "/overlay1")
   386  		tt.add(srcDir2, "/overlay1/dir2")
   387  		tt.add(newSrcFile1(), "/overlay3")
   388  		tt.add(srcArchive1, "/overlay3")
   389  		tt.add(srcArchive1, "/overlay4")
   390  		tt.add(srcArchive2, "/overlay4")
   391  		// /overlay2 dir should not be added as noop source with parent's attributes
   392  		tt.add(srcArchive1, "/overlay2")
   393  		tt.add(srcDir2, "/overlay2/dir2")
   394  		tt.add(srcArchive1, "/overlayx")
   395  		tt.add(srcDir1, "/overlayx")
   396  		overlay := tt.add(srcArchive1, "/overlayx/dirx/nestedoverlay")
   397  		tt.add(newSrcFile1(), "/overlayx/dirx/nestedoverlay/nestedoverlaychild")
   398  
   399  		// Test path resolution
   400  		xdest.add(newSrcFile1(), "../../etc/xadd-resolve-rel")
   401  		symlink.add(newSrcFile1(), "../../etc/xadd-resolve-rel-link")
   402  		xdest.add(newSrcFile1(), "/etc/xadd-resolve-abs")
   403  		xdest.add(newSrcFile2(), "../xadd-resolve-parent")
   404  
   405  		// Test path resolution
   406  		tt.assertResolve("/etc/file1", "/etc/file1", srcFile1, true)
   407  		tt.assertResolve("etc/file1", "/etc/file1", srcFile1, true)
   408  		etc := tt.assertResolve("etc", "/etc", nil, true)
   409  		_, ok := etc.node.source.(*source.SourceDir)
   410  		assert.True(t, ok, "/etc should be sourcedir")
   411  		tt.assertResolve("./etc", "/etc", nil, true)
   412  		etc.assertResolve("dir2/x/y/filem", "/etc/dir2/x/y/filem", srcFile2, true)
   413  		etc.assertResolve(".", "/etc", nil, true)
   414  		etc.assertResolve("../etc/file1", "/etc/file1", srcFile1, true)
   415  		etc.assertResolve("..", "/", srcDir2, true)
   416  		etc.assertResolve("/", "/", srcDir2, true)
   417  		tt.assertResolve("/", "/", srcDir2, true)
   418  		etc.assertResolve("../..", "/", nil, false)
   419  		tt.assertResolve("../etc", "/etc", nil, false)
   420  		tt.assertResolve("/etc/symlink2/lfile2", "/etc/xdest/lfile2", srcFile1ResolvedParent, true)
   421  		tt.assertResolve("/etc/symlink2/lfile2/nonexisting", "", nil, false)
   422  		tt.assertResolve("nonexisting", "", nil, false)
   423  		overlay1 := tt.assertResolve("/overlay1", "/overlay1", nil, true)
   424  		_, ok = overlay1.node.source.(*source.SourceDir)
   425  		assert.True(t, ok, "/overlay1 should be sourcedir")
   426  		// parent resolution
   427  		etc.assertResolve("..", "/", srcDir2, true)
   428  		// ...within overlay
   429  		overlay.assertResolve("..", "/overlayx/dirx", srcParentDir, true).
   430  			assertResolve("..", "/overlayx", nil, true).
   431  			assertResolve("..", "/", srcDir2, true)
   432  	}
   433  	return tt.node
   434  }
   435  
   436  func expectedNodeOps() []string {
   437  	expected := `
   438  		/ type=dir usr=1:1 mode=750
   439  		/emptydir type=dir usr=0:33 mode=755
   440  		/etc type=dir mode=755
   441  		/etc/dir1 type=dir usr=0:33 mode=755
   442  		/etc/dir2 type=dir usr=1:1 mode=750 xattr.k=v
   443  		/etc/dir2/x type=dir mode=755
   444  		/etc/dir2/x/y type=dir mode=755
   445  		/etc/dir2/x/y/filem type=file usr=1:1 mode=644 size=689876 hash=sha256:hex2
   446  		/etc/dircontainingwhiteout type=dir mode=755
   447  		/etc/dircontainingwhiteout/whiteoutfile type=whiteout
   448  		/etc/diroverwrite1 type=dir usr=1:1 mode=750
   449  		/etc/diroverwrite1/file type=file usr=0:33 mode=755 size=12345 hash=sha256:hex1
   450  		/etc/diroverwrite2 type=file usr=1:1 mode=644 size=689876 hash=sha256:hex2
   451  		/etc/dirtobeoverwrittenbywhiteout type=whiteout
   452  		/etc/file1 type=file usr=0:33 mode=755 size=12345 hash=sha256:hex1
   453  		/etc/file2 type=file usr=1:1 mode=644 size=689876 hash=sha256:hex2
   454  		/etc/fileoverwrite type=file usr=1:1 mode=644 size=689876 hash=sha256:hex2
   455  		/etc/fileoverwriteimplicit type=dir mode=755
   456  		/etc/fileoverwriteimplicit/filex type=file usr=1:1 mode=644 size=689876 hash=sha256:hex2
   457  		/etc/filetobeoverwrittenbywhiteout type=whiteout
   458  		/etc/link1 type=file usr=1:1 mode=644 size=689876 hash=sha256:hex2
   459  		/etc/link2 hlink=/etc/link1
   460  		/etc/link3 hlink=/etc/link1
   461  		/etc/linkreplacewithdir type=dir usr=0:33 mode=755
   462  		/etc/linkreplacewithfile type=file usr=0:33 mode=755 size=12345 hash=sha256:hex1
   463  		/etc/linkreplacewithparentdir type=dir mode=755
   464  		/etc/linkreplacewithparentdir/lfile3 type=file usr=0:33 mode=755 size=12345 hash=sha256:hex1
   465  		/etc/parentrmdir type=dir usr=0:33 mode=755
   466  		/etc/symlink1 type=symlink usr=0:33 link=xdest
   467  		/etc/symlink2 type=symlink usr=1:1 link=../etc/xdest
   468  		/etc/symlink3 type=symlink usr=0:33 link=/etc/xnewdest/newdir
   469  		/etc/symlinkoverwritedir type=dir usr=0:33 mode=755
   470  		/etc/symlinkoverwritefile type=file usr=0:33 mode=755 size=12345 hash=sha256:hex1
   471  		/etc/xadd-resolve-abs type=file usr=0:33 mode=755 size=12345 hash=sha256:hex1
   472  		/etc/xadd-resolve-parent type=file usr=1:1 mode=644 size=689876 hash=sha256:hex2
   473  		/etc/xadd-resolve-rel type=file usr=0:33 mode=755 size=12345 hash=sha256:hex1
   474  		/etc/xadd-resolve-rel-link type=file usr=0:33 mode=755 size=12345 hash=sha256:hex1
   475  		/etc/xdest type=dir usr=0:33 mode=755
   476  		/etc/xdest/ldir1 type=dir usr=0:33 mode=755
   477  		/etc/xdest/lfile1 type=file usr=0:33 mode=755 size=12345 hash=sha256:hex1
   478  		/etc/xdest/lfile2 type=file usr=0:33 mode=755 size=12345 hash=sha256:hex1
   479  		/etc/xdest/overridewithdir type=dir usr=0:33 mode=755
   480  		/etc/xdest/overridewithfile type=file usr=0:33 mode=755 size=12345 hash=sha256:hex1
   481  		/etc/xnewdest type=dir mode=755
   482  		/etc/xnewdest/newdir type=dir mode=755
   483  		/etc/xnewdest/newdir/lfile3 type=file usr=0:33 mode=755 size=12345 hash=sha256:hex1
   484  		/overlay1 type=dir mode=755
   485  		/overlay1/dir1 type=dir mode=755
   486  		/overlay1/dir1/file1 type=file usr=0:33 mode=755 size=12345 hash=sha256:hex1
   487  		/overlay1 type=overlay usr=0:33 size=98765 hash=sha256:hex3
   488  		/overlay1 type=dir usr=1:1 mode=750
   489  		/overlay1/dir2 type=dir usr=1:1 mode=750
   490  		/overlay2 type=dir mode=755
   491  		/overlay2 type=overlay usr=0:33 size=98765 hash=sha256:hex3
   492  		/overlay2/dir2 type=dir usr=1:1 mode=750
   493  		/overlay3 type=dir mode=755
   494  		/overlay3 type=overlay usr=0:33 size=98765 hash=sha256:hex3
   495  		/overlay4 type=dir mode=755
   496  		/overlay4 type=overlay usr=0:33 size=98765 hash=sha256:hex3
   497  		/overlay4 type=overlay usr=0:33 size=87658 hash=sha256:hex4
   498  		/overlayx type=dir mode=755
   499  		/overlayx type=overlay usr=0:33 size=98765 hash=sha256:hex3
   500  		/overlayx type=dir usr=0:33 mode=755
   501  		/overlayx/dirx/nestedoverlay type=dir mode=755
   502  		/overlayx/dirx/nestedoverlay type=overlay usr=0:33 size=98765 hash=sha256:hex3
   503  		/overlayx/dirx/nestedoverlay/nestedoverlaychild type=file usr=0:33 mode=755 size=12345 hash=sha256:hex1
   504  		/root type=dir mode=755
   505  		/root/empty%20dir type=dir usr=0:33 mode=755
   506  	`
   507  	expectedLines := strings.Split(strings.TrimSpace(expected), "\n")
   508  	for i, line := range expectedLines {
   509  		expectedLines[i] = strings.TrimSpace(line)
   510  	}
   511  	return expectedLines
   512  }
   513  
   514  func TestFsNodeEqual(t *testing.T) {
   515  	attrs1 := fs.NodeAttrs{fs.NodeInfo{fs.TypeFile, fs.FileAttrs{Mode: 0644}}, fs.DerivedAttrs{Hash: "hash"}}
   516  	attrs2 := attrs1
   517  	eq := func() bool {
   518  		node1, err := NewFS().AddUpper("/file", &attrs1)
   519  		require.NoError(t, err)
   520  		node2, err := NewFS().AddUpper("/file", &attrs2)
   521  		require.NoError(t, err)
   522  		eq, err := node1.(*FsNode).Equal(node2.(*FsNode))
   523  		require.NoError(t, err)
   524  		return eq
   525  	}
   526  	if !assert.True(t, eq(), "two files should equal") {
   527  		t.FailNow()
   528  	}
   529  	attrs2.NodeType = fs.TypeDir
   530  	if !assert.False(t, eq(), "two files should not equal when type changes") {
   531  		t.FailNow()
   532  	}
   533  	attrs2.NodeType = fs.TypeFile
   534  	attrs2.Hash = "changed"
   535  	if !assert.False(t, eq(), "two files should not equal when hash changes") {
   536  		t.FailNow()
   537  	}
   538  }
   539  
   540  func treePaths(node *FsNode, m map[string]bool) {
   541  	if node.NodeType != fs.TypeWhiteout {
   542  		m[node.Path()] = true
   543  	}
   544  	if node.child != nil {
   545  		treePaths(node.child, m)
   546  	}
   547  	if node.next != nil {
   548  		treePaths(node.next, m)
   549  	}
   550  }
   551  
   552  type fsNodeTester struct {
   553  	t    *testing.T
   554  	node *FsNode
   555  }
   556  
   557  func (s *fsNodeTester) FS() *FsNode {
   558  	return s.node
   559  }
   560  
   561  func (s *fsNodeTester) add(src fs.Source, dest string) *fsNodeTester {
   562  	f, err := s.node.addUpper(dest, src)
   563  	require.NoError(s.t, err)
   564  	require.NotNil(s.t, f)
   565  	return &fsNodeTester{s.t, f}
   566  }
   567  
   568  func (s *fsNodeTester) assertResolve(path string, expectedPath string, expectedSrc fs.Source, valid bool) *fsNodeTester {
   569  	node, err := s.node.node(path)
   570  	if err != nil {
   571  		if !valid {
   572  			return nil
   573  		}
   574  		s.t.Errorf("resolve path %s: %s", path, err)
   575  		s.t.FailNow()
   576  	} else if !valid {
   577  		s.t.Errorf("path %s should yield error but returned node %s", path, node.Path())
   578  		s.t.FailNow()
   579  	}
   580  	nPath := node.Path()
   581  	if nPath != expectedPath {
   582  		s.t.Errorf("node %s path %s should resolve to %s but was %q", node.Path(), path, expectedPath, nPath)
   583  		s.t.FailNow()
   584  	}
   585  	if expectedSrc != nil && !node.source.Attrs().Equal(expectedSrc.Attrs()) {
   586  		a := node.source.Attrs()
   587  		s.t.Errorf("unexpected source {%s} at %s", (&a).AttrString(fs.AttrsAll), nPath)
   588  		s.t.FailNow()
   589  	}
   590  	return &fsNodeTester{s.t, node}
   591  }
   592  
   593  func expectedWriteOps(t *testing.T) []string {
   594  	expectedOps := []string{}
   595  	typeRegex := regexp.MustCompile(" type=([^ ]+)")
   596  	exclAttrRegex := regexp.MustCompile(" hlink=[^ ]+| size=[^ ]+| hash=[^ ]+|\\.[^ =]+=[^ ]+")
   597  	for _, line := range expectedNodeOps() {
   598  		line := strings.TrimSpace(line)
   599  		path := line[:strings.Index(line, " type=")]
   600  		attrs := line[len(path):]
   601  		path, err := url.PathUnescape(path)
   602  		require.NoError(t, err)
   603  		var t fs.NodeType
   604  		if m := typeRegex.FindStringSubmatch(line); len(m) > 0 {
   605  			t = fs.NodeType(m[1])
   606  		}
   607  		if t == fs.TypeOverlay {
   608  			line = filepath.Join(path, "xtracted") + " type=file usr=0:33 mode=644"
   609  		} else if pos := strings.Index(line, " hlink="); pos != -1 {
   610  			line = path + " type=link" + line[pos:]
   611  		} else if t == fs.TypeWhiteout {
   612  			line = path + " type=whiteout"
   613  		} else {
   614  			line = path + string(exclAttrRegex.ReplaceAll([]byte(attrs), []byte("")))
   615  		}
   616  		expectedOps = append(expectedOps, line)
   617  	}
   618  	return expectedOps
   619  }