|
| 1 | +//go:build linux |
| 2 | + |
| 3 | +package instances |
| 4 | + |
| 5 | +import ( |
| 6 | + "io/fs" |
| 7 | + "os" |
| 8 | + "path/filepath" |
| 9 | + "testing" |
| 10 | + "unsafe" |
| 11 | + |
| 12 | + "github.com/stretchr/testify/require" |
| 13 | + "golang.org/x/sys/unix" |
| 14 | +) |
| 15 | + |
| 16 | +const ( |
| 17 | + fsIOCFiemap = 0xC020660B |
| 18 | + fiemapFlagSync = 0x1 |
| 19 | + fiemapMaxExtents = 64 |
| 20 | +) |
| 21 | + |
| 22 | +type fiemapHeader struct { |
| 23 | + Start uint64 |
| 24 | + Length uint64 |
| 25 | + Flags uint32 |
| 26 | + MappedExtents uint32 |
| 27 | + ExtentCount uint32 |
| 28 | + Reserved uint32 |
| 29 | +} |
| 30 | + |
| 31 | +type fiemapExtent struct { |
| 32 | + Logical uint64 |
| 33 | + Physical uint64 |
| 34 | + Length uint64 |
| 35 | + Reserved64 [2]uint64 |
| 36 | + Flags uint32 |
| 37 | + Reserved [3]uint32 |
| 38 | +} |
| 39 | + |
| 40 | +type fiemapRequest struct { |
| 41 | + Header fiemapHeader |
| 42 | + Extents [fiemapMaxExtents]fiemapExtent |
| 43 | +} |
| 44 | + |
| 45 | +func fileExtents(path string) ([]fiemapExtent, error) { |
| 46 | + f, err := os.Open(path) |
| 47 | + if err != nil { |
| 48 | + return nil, err |
| 49 | + } |
| 50 | + defer f.Close() |
| 51 | + |
| 52 | + var req fiemapRequest |
| 53 | + req.Header.Length = ^uint64(0) |
| 54 | + req.Header.Flags = fiemapFlagSync |
| 55 | + req.Header.ExtentCount = fiemapMaxExtents |
| 56 | + |
| 57 | + if _, _, errno := unix.Syscall(unix.SYS_IOCTL, f.Fd(), uintptr(fsIOCFiemap), uintptr(unsafe.Pointer(&req))); errno != 0 { |
| 58 | + return nil, errno |
| 59 | + } |
| 60 | + return req.Extents[:req.Header.MappedExtents], nil |
| 61 | +} |
| 62 | + |
| 63 | +// assertCopyReflinked walks srcDir and verifies that at least one regular |
| 64 | +// file shares a physical extent with its counterpart under dstDir. A |
| 65 | +// successful FICLONE leaves the destination pointing at the source's |
| 66 | +// extents, so FIEMAP will report identical fe_physical offsets. If every |
| 67 | +// inspected pair has disjoint extents, the FICLONE fast path silently |
| 68 | +// degraded to a full byte copy and we want to fail loudly. Requires a |
| 69 | +// reflink-capable filesystem under the test's scratch directory (XFS with |
| 70 | +// reflink=1 in CI). |
| 71 | +func assertCopyReflinked(t *testing.T, srcDir, dstDir string) { |
| 72 | + t.Helper() |
| 73 | + |
| 74 | + type candidate struct { |
| 75 | + rel string |
| 76 | + size int64 |
| 77 | + } |
| 78 | + var candidates []candidate |
| 79 | + require.NoError(t, filepath.WalkDir(srcDir, func(path string, d fs.DirEntry, err error) error { |
| 80 | + if err != nil { |
| 81 | + return err |
| 82 | + } |
| 83 | + if !d.Type().IsRegular() { |
| 84 | + return nil |
| 85 | + } |
| 86 | + info, err := d.Info() |
| 87 | + if err != nil { |
| 88 | + return err |
| 89 | + } |
| 90 | + if info.Size() == 0 { |
| 91 | + return nil |
| 92 | + } |
| 93 | + rel, err := filepath.Rel(srcDir, path) |
| 94 | + if err != nil { |
| 95 | + return err |
| 96 | + } |
| 97 | + candidates = append(candidates, candidate{rel: rel, size: info.Size()}) |
| 98 | + return nil |
| 99 | + })) |
| 100 | + require.NotEmpty(t, candidates, "no non-empty regular files under %s", srcDir) |
| 101 | + |
| 102 | + var inspected, shared int |
| 103 | + for _, c := range candidates { |
| 104 | + dstPath := filepath.Join(dstDir, c.rel) |
| 105 | + if _, err := os.Stat(dstPath); err != nil { |
| 106 | + continue |
| 107 | + } |
| 108 | + srcExtents, err := fileExtents(filepath.Join(srcDir, c.rel)) |
| 109 | + if err != nil { |
| 110 | + t.Logf("FIEMAP %s: %v", c.rel, err) |
| 111 | + continue |
| 112 | + } |
| 113 | + dstExtents, err := fileExtents(dstPath) |
| 114 | + if err != nil { |
| 115 | + t.Logf("FIEMAP %s: %v", dstPath, err) |
| 116 | + continue |
| 117 | + } |
| 118 | + inspected++ |
| 119 | + if extentsShareAny(srcExtents, dstExtents) { |
| 120 | + shared++ |
| 121 | + } |
| 122 | + } |
| 123 | + require.NotZero(t, inspected, "no files inspected for reflink sharing") |
| 124 | + require.NotZero(t, shared, |
| 125 | + "no files shared physical extents between %s and %s; FICLONE fast path produced full byte copies", |
| 126 | + srcDir, dstDir) |
| 127 | +} |
| 128 | + |
| 129 | +func extentsShareAny(a, b []fiemapExtent) bool { |
| 130 | + if len(a) == 0 || len(b) == 0 { |
| 131 | + return false |
| 132 | + } |
| 133 | + seen := make(map[uint64]struct{}, len(a)) |
| 134 | + for _, e := range a { |
| 135 | + seen[e.Physical] = struct{}{} |
| 136 | + } |
| 137 | + for _, e := range b { |
| 138 | + if _, ok := seen[e.Physical]; ok { |
| 139 | + return true |
| 140 | + } |
| 141 | + } |
| 142 | + return false |
| 143 | +} |
0 commit comments