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 }