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  }