github.com/cockroachdb/pebble@v1.1.1-0.20240513155919-3622ade60459/vfs/vfs_test.go (about) 1 // Copyright 2012 The LevelDB-Go and Pebble Authors. All rights reserved. Use 2 // of this source code is governed by a BSD-style license that can be found in 3 // the LICENSE file. 4 5 package vfs 6 7 import ( 8 "bytes" 9 "fmt" 10 "io" 11 "os" 12 "runtime" 13 "sort" 14 "strings" 15 "testing" 16 17 "github.com/cockroachdb/datadriven" 18 "github.com/cockroachdb/errors" 19 "github.com/cockroachdb/errors/oserror" 20 "github.com/stretchr/testify/require" 21 ) 22 23 func normalizeError(err error) error { 24 // It is OS-specific which errors match IsExist, IsNotExist, and 25 // IsPermission, with OS-specific error messages. We normalize to the 26 // oserror.Err* errors which have standard error messages across 27 // platforms. 28 switch { 29 case oserror.IsExist(err): 30 return oserror.ErrExist 31 case oserror.IsNotExist(err): 32 return oserror.ErrNotExist 33 case oserror.IsPermission(err): 34 return oserror.ErrPermission 35 } 36 return err 37 } 38 39 // vfsTestFS is similar to loggingFS but is more specific to the vfs test. It 40 // logs more operations and logs return values and errors. 41 // It also supports injecting an error on Link. 42 type vfsTestFS struct { 43 FS 44 base string 45 w io.Writer 46 linkErr error 47 } 48 49 func (fs vfsTestFS) stripBase(path string) string { 50 if strings.HasPrefix(path, fs.base+"/") { 51 return path[len(fs.base)+1:] 52 } 53 return path 54 } 55 56 func (fs vfsTestFS) Create(name string) (File, error) { 57 f, err := fs.FS.Create(name) 58 fmt.Fprintf(fs.w, "create: %s [%v]\n", fs.stripBase(name), normalizeError(err)) 59 return vfsTestFSFile{f, fs.PathBase(name), fs.w}, err 60 } 61 62 func (fs vfsTestFS) Link(oldname, newname string) error { 63 err := fs.linkErr 64 if err == nil { 65 err = fs.FS.Link(oldname, newname) 66 } 67 fmt.Fprintf(fs.w, "link: %s -> %s [%v]\n", 68 fs.stripBase(oldname), fs.stripBase(newname), normalizeError(err)) 69 return err 70 } 71 72 func (fs vfsTestFS) ReuseForWrite(oldname, newname string) (File, error) { 73 f, err := fs.FS.ReuseForWrite(oldname, newname) 74 if err == nil { 75 f = vfsTestFSFile{f, fs.PathBase(newname), fs.w} 76 } 77 fmt.Fprintf(fs.w, "reuseForWrite: %s -> %s [%v]\n", 78 fs.stripBase(oldname), fs.stripBase(newname), normalizeError(err)) 79 return f, err 80 } 81 82 func (fs vfsTestFS) MkdirAll(dir string, perm os.FileMode) error { 83 err := fs.FS.MkdirAll(dir, perm) 84 fmt.Fprintf(fs.w, "mkdir: %s [%v]\n", fs.stripBase(dir), normalizeError(err)) 85 return err 86 } 87 88 func (fs vfsTestFS) Open(name string, opts ...OpenOption) (File, error) { 89 f, err := fs.FS.Open(name, opts...) 90 fmt.Fprintf(fs.w, "open: %s [%v]\n", fs.stripBase(name), normalizeError(err)) 91 return vfsTestFSFile{f, fs.stripBase(name), fs.w}, err 92 } 93 94 func (fs vfsTestFS) Remove(name string) error { 95 err := fs.FS.Remove(name) 96 fmt.Fprintf(fs.w, "remove: %s [%v]\n", fs.stripBase(name), normalizeError(err)) 97 return err 98 } 99 100 func (fs vfsTestFS) RemoveAll(name string) error { 101 err := fs.FS.RemoveAll(name) 102 fmt.Fprintf(fs.w, "remove-all: %s [%v]\n", fs.stripBase(name), normalizeError(err)) 103 return err 104 } 105 106 type vfsTestFSFile struct { 107 File 108 name string 109 w io.Writer 110 } 111 112 func (f vfsTestFSFile) Close() error { 113 err := f.File.Close() 114 fmt.Fprintf(f.w, "close: %s [%v]\n", f.name, err) 115 return err 116 } 117 118 func (f vfsTestFSFile) Preallocate(off, n int64) error { 119 err := f.File.Preallocate(off, n) 120 fmt.Fprintf(f.w, "preallocate(off=%d,n=%d): %s [%v]\n", off, n, f.name, err) 121 return err 122 } 123 124 func (f vfsTestFSFile) Sync() error { 125 err := f.File.Sync() 126 fmt.Fprintf(f.w, "sync: %s [%v]\n", f.name, err) 127 return err 128 } 129 130 func (f vfsTestFSFile) SyncData() error { 131 err := f.File.SyncData() 132 fmt.Fprintf(f.w, "sync-data: %s [%v]\n", f.name, err) 133 return err 134 } 135 136 func (f vfsTestFSFile) SyncTo(length int64) (fullSync bool, err error) { 137 fullSync, err = f.File.SyncTo(length) 138 fmt.Fprintf(f.w, "sync-to(%d): %s [%t,%v]\n", length, f.name, fullSync, err) 139 return fullSync, err 140 } 141 142 func runTestVFS(t *testing.T, baseFS FS, dir string) { 143 var buf bytes.Buffer 144 fs := vfsTestFS{FS: baseFS, base: dir, w: &buf} 145 146 datadriven.RunTest(t, "testdata/vfs", func(t *testing.T, td *datadriven.TestData) string { 147 switch td.Cmd { 148 case "define": 149 buf.Reset() 150 151 for _, arg := range td.CmdArgs { 152 switch arg.Key { 153 case "linkErr": 154 if len(arg.Vals) != 1 { 155 return fmt.Sprintf("%s: %s expected 1 value", td.Cmd, arg.Key) 156 } 157 switch arg.Vals[0] { 158 case "ErrExist": 159 fs.linkErr = oserror.ErrExist 160 case "ErrNotExist": 161 fs.linkErr = oserror.ErrNotExist 162 case "ErrPermission": 163 fs.linkErr = oserror.ErrPermission 164 default: 165 fs.linkErr = errors.New(arg.Vals[0]) 166 } 167 default: 168 return fmt.Sprintf("%s: unknown arg: %s", td.Cmd, arg.Key) 169 } 170 } 171 172 for _, line := range strings.Split(td.Input, "\n") { 173 parts := strings.Fields(line) 174 if len(parts) == 0 { 175 return "<op> [<args>]" 176 } 177 178 switch parts[0] { 179 case "clone": 180 if len(parts) < 3 { 181 return "clone <src> <dest> [disk|mem] [link] [sync]" 182 } 183 dstFS := fs 184 var opts []CloneOption 185 for _, p := range parts[3:] { 186 switch p { 187 case "disk": 188 dstFS = vfsTestFS{FS: Default, base: dir, w: &buf} 189 case "mem": 190 dstFS = vfsTestFS{FS: NewMem(), base: dir, w: &buf} 191 case "link": 192 opts = append(opts, CloneTryLink) 193 case "sync": 194 opts = append(opts, CloneSync) 195 default: 196 return fmt.Sprintf("unrecognized argument %q", p) 197 } 198 } 199 200 _, _ = Clone(fs, dstFS, fs.PathJoin(dir, parts[1]), fs.PathJoin(dir, parts[2]), opts...) 201 202 case "create": 203 if len(parts) != 2 { 204 return "create <name>" 205 } 206 f, _ := fs.Create(fs.PathJoin(dir, parts[1])) 207 f.Close() 208 209 case "link": 210 if len(parts) != 3 { 211 return "link <oldname> <newname>" 212 } 213 _ = fs.Link(fs.PathJoin(dir, parts[1]), fs.PathJoin(dir, parts[2])) 214 215 case "link-or-copy": 216 if len(parts) != 3 { 217 return "link-or-copy <oldname> <newname>" 218 } 219 _ = LinkOrCopy(fs, fs.PathJoin(dir, parts[1]), fs.PathJoin(dir, parts[2])) 220 221 case "reuseForWrite": 222 if len(parts) != 3 { 223 return "reuseForWrite <oldname> <newname>" 224 } 225 _, _ = fs.ReuseForWrite(fs.PathJoin(dir, parts[1]), fs.PathJoin(dir, parts[2])) 226 227 case "list": 228 if len(parts) != 2 { 229 return "list <dir>" 230 } 231 paths, _ := fs.List(fs.PathJoin(dir, parts[1])) 232 sort.Strings(paths) 233 for _, p := range paths { 234 fmt.Fprintln(&buf, p) 235 } 236 237 case "mkdir": 238 if len(parts) != 2 { 239 return "mkdir <dir>" 240 } 241 _ = fs.MkdirAll(fs.PathJoin(dir, parts[1]), 0755) 242 243 case "remove": 244 if len(parts) != 2 { 245 return "remove <name>" 246 } 247 _ = fs.Remove(fs.PathJoin(dir, parts[1])) 248 249 case "remove-all": 250 if len(parts) != 2 { 251 return "remove-all <name>" 252 } 253 _ = fs.RemoveAll(fs.PathJoin(dir, parts[1])) 254 } 255 } 256 257 return buf.String() 258 259 default: 260 return fmt.Sprintf("unknown command: %s", td.Cmd) 261 } 262 }) 263 } 264 265 func TestVFS(t *testing.T) { 266 t.Run("mem", func(t *testing.T) { 267 runTestVFS(t, NewMem(), "") 268 }) 269 if runtime.GOOS != "windows" { 270 t.Run("disk", func(t *testing.T) { 271 dir, err := os.MkdirTemp("", "test-vfs") 272 require.NoError(t, err) 273 defer func() { 274 _ = os.RemoveAll(dir) 275 }() 276 runTestVFS(t, Default, dir) 277 }) 278 } 279 } 280 281 func TestVFSGetDiskUsage(t *testing.T) { 282 dir, err := os.MkdirTemp("", "test-free-space") 283 require.NoError(t, err) 284 defer func() { 285 _ = os.RemoveAll(dir) 286 }() 287 _, err = Default.GetDiskUsage(dir) 288 require.Nil(t, err) 289 } 290 291 func TestVFSCreateLinkSemantics(t *testing.T) { 292 dir, err := os.MkdirTemp("", "test-create-link") 293 require.NoError(t, err) 294 defer func() { _ = os.RemoveAll(dir) }() 295 296 for _, fs := range []FS{Default, NewMem()} { 297 t.Run(fmt.Sprintf("%T", fs), func(t *testing.T) { 298 writeFile := func(path, contents string) { 299 path = fs.PathJoin(dir, path) 300 f, err := fs.Create(path) 301 require.NoError(t, err) 302 _, err = f.Write([]byte(contents)) 303 require.NoError(t, err) 304 require.NoError(t, f.Close()) 305 } 306 readFile := func(path string) string { 307 path = fs.PathJoin(dir, path) 308 f, err := fs.Open(path) 309 require.NoError(t, err) 310 b, err := io.ReadAll(f) 311 require.NoError(t, err) 312 require.NoError(t, f.Close()) 313 return string(b) 314 } 315 require.NoError(t, fs.MkdirAll(dir, 0755)) 316 317 // Write a file 'foo' and create a hardlink at 'bar'. 318 writeFile("foo", "foo") 319 require.NoError(t, fs.Link(fs.PathJoin(dir, "foo"), fs.PathJoin(dir, "bar"))) 320 321 // Both files should contain equal contents, because they're backed by 322 // the same inode. 323 require.Equal(t, "foo", readFile("foo")) 324 require.Equal(t, "foo", readFile("bar")) 325 326 // Calling Create on 'bar' must NOT truncate 'foo'. It should create a 327 // new file at path 'bar' with a new inode. 328 writeFile("bar", "bar") 329 330 require.Equal(t, "foo", readFile("foo")) 331 require.Equal(t, "bar", readFile("bar")) 332 }) 333 } 334 } 335 336 // TestVFSRootDirName ensures that opening the root directory on both the 337 // Default and MemFS works and returns a File which has the name of the 338 // path separator. 339 func TestVFSRootDirName(t *testing.T) { 340 for _, fs := range []FS{Default, NewMem()} { 341 sep := sep 342 if fs == Default { 343 sep = string(os.PathSeparator) 344 } 345 rootDir, err := fs.Open(sep) 346 require.NoError(t, err) 347 fi, err := rootDir.Stat() 348 require.NoError(t, err) 349 require.Equal(t, sep, fi.Name()) 350 } 351 } 352 353 // TestOpType is intended to catch operations that have been added without an 354 // associated string, which could result in a runtime panic. 355 func TestOpType(t *testing.T) { 356 for i := 0; i < int(opTypeMax); i++ { 357 require.NotPanics(t, func() { 358 _ = OpType(i).String() 359 }) 360 } 361 }