github.com/creachadair/ffs@v0.17.3/fpath/fpath_test.go (about)

     1  // Copyright 2019 Michael J. Fromberger. All Rights Reserved.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package fpath_test
    16  
    17  import (
    18  	"crypto/sha1"
    19  	"errors"
    20  	"flag"
    21  	"hash"
    22  	"io/fs"
    23  	"strconv"
    24  	"testing"
    25  
    26  	"github.com/creachadair/ffs/blob"
    27  	"github.com/creachadair/ffs/blob/memstore"
    28  	"github.com/creachadair/ffs/file"
    29  	"github.com/creachadair/ffs/fpath"
    30  	"github.com/creachadair/ffs/storage/filestore"
    31  	"github.com/google/go-cmp/cmp"
    32  )
    33  
    34  var (
    35  	saveStore = flag.String("save", "", "Save blobs to a filestore at this path")
    36  
    37  	// Interface satisfaction checks.
    38  	_ fs.FS        = fpath.FS{}
    39  	_ fs.StatFS    = fpath.FS{}
    40  	_ fs.SubFS     = fpath.FS{}
    41  	_ fs.ReadDirFS = fpath.FS{}
    42  )
    43  
    44  func mustNewCAS(t *testing.T, h func() hash.Hash) blob.CAS {
    45  	t.Helper()
    46  	if *saveStore == "" {
    47  		return blob.CASFromKV(memstore.NewKV())
    48  	}
    49  	fs, err := filestore.New(*saveStore)
    50  	if err != nil {
    51  		t.Fatalf("Opening filestore %q: %v", *saveStore, err)
    52  	}
    53  	ks, err := fs.KV(t.Context(), "")
    54  	if err != nil {
    55  		t.Fatalf("Opening keyspace: %v", err)
    56  	}
    57  	t.Logf("Saving test output to filestore %q", *saveStore)
    58  	return blob.CASFromKV(ks)
    59  }
    60  
    61  func TestPaths(t *testing.T) {
    62  	cas := mustNewCAS(t, sha1.New)
    63  
    64  	ctx := t.Context()
    65  	root := file.New(cas, nil)
    66  	setDir := func(s *file.Stat) { s.Mode = fs.ModeDir | 0755 }
    67  	openPath := func(path string, werr error) *file.File {
    68  		got, err := fpath.Open(ctx, root, path)
    69  		if !errorOK(err, werr) {
    70  			t.Errorf("OpenPath %q: got error %v, want %v", path, err, werr)
    71  		}
    72  		return got
    73  	}
    74  	createPath := func(path string, werr error) *file.File {
    75  		newf, err := fpath.Set(ctx, root, path, &fpath.SetOptions{
    76  			Create:  true,
    77  			SetStat: setDir,
    78  		})
    79  		if !errorOK(err, werr) {
    80  			t.Errorf("CreatePath %q: got error %v, want %v", path, err, werr)
    81  		}
    82  		return newf
    83  	}
    84  	removePath := func(path string, werr error) {
    85  		err := fpath.Remove(ctx, root, path)
    86  		if !errorOK(err, werr) {
    87  			t.Errorf("RemovePath %q: got error %v, want %v", path, err, werr)
    88  		}
    89  	}
    90  	setPath := func(path string, f *file.File, werr error) {
    91  		_, err := fpath.Set(ctx, root, path, &fpath.SetOptions{File: f})
    92  		if !errorOK(err, werr) {
    93  			t.Errorf("SetPath %q: got error %v, want %v", path, err, werr)
    94  		}
    95  	}
    96  
    97  	// Opening the empty path should return the root.
    98  	if got := openPath("", nil); got != root {
    99  		t.Errorf("Open empty path: got %p, want %p", got, root)
   100  	}
   101  
   102  	// Removing an empty path should quietly do nothing.
   103  	removePath("", nil)
   104  	removePath("/", nil)
   105  
   106  	// Setting a nil file without creation enabled should fail.
   107  	setPath("", nil, fpath.ErrNilFile)
   108  
   109  	// Setting on a non-existent path should fail, but the last element of the
   110  	// path may be missing.
   111  	setPath("/no/such/path", root.New(nil), file.ErrChildNotFound)
   112  	setPath("/okay", root.New(nil), nil)
   113  
   114  	// Removing non-existing non-empty paths should report an error,
   115  	removePath("nonesuch", file.ErrChildNotFound)
   116  	removePath("/no/such/path", file.ErrChildNotFound)
   117  
   118  	// Opening a non-existing path should report an error.
   119  	openPath("/a/lasting/peace", file.ErrChildNotFound)
   120  
   121  	// After creating a path, we should be able to open it and get back the same
   122  	// file value we created.
   123  	{
   124  		want := createPath("/a/lasting/peace", nil)
   125  		got := openPath("/a/lasting/peace", nil)
   126  		if got != want {
   127  			t.Errorf("Open returned the wrong file: got %+v, want %+v", got, want)
   128  		}
   129  	}
   130  
   131  	// Verify that the stat callback was properly invoked for path components
   132  	// that we created.
   133  	for _, path := range []string{"/a", "/a/lasting", "/a/lasting/peace"} {
   134  		got := openPath(path, nil).Stat().Mode
   135  		if want := fs.ModeDir | 0755; got != want {
   136  			t.Errorf("Wrong path mode for %q: got %v, want %v", path, got, want)
   137  		}
   138  	}
   139  
   140  	// Verify that the stat callback is not called for the final path element if
   141  	// we provided the file that is to be inserted.
   142  	{
   143  		const path = "/a/lasting/itch"
   144  		if newf, err := fpath.Set(ctx, root, path, &fpath.SetOptions{
   145  			Create:  true,
   146  			SetStat: setDir,
   147  			File:    root.New(nil),
   148  		}); err != nil {
   149  			t.Errorf("Create %q: got unexpected error %v", "/a/lasting/itch", err)
   150  		} else if got, want := newf.Stat().Mode, fs.FileMode(0); got != want {
   151  			t.Errorf("Wrong mode for %q: got %v, want %v", path, got, want)
   152  		}
   153  	}
   154  
   155  	// Prefixes of an existing path should exist.
   156  	openPath("/a", nil)
   157  	openPath("/a/lasting", nil)
   158  
   159  	// Non-existing siblings should report an error.
   160  	openPath("/a/lasting/war", file.ErrChildNotFound)
   161  
   162  	// Creating a sibling should work, and not disturb its neighbors.
   163  	createPath("/a/lasting/consequence", nil)
   164  	openPath("/a/lasting/peace", nil)
   165  	openPath("/a/lasting/consequence", nil)
   166  
   167  	// Removing a path should make it unreachable.
   168  	removePath("/a/lasting/peace", nil)
   169  	openPath("/a/lasting/peace", file.ErrChildNotFound)
   170  
   171  	createPath("/a/lasting/war/of/words", nil)
   172  	subtree := openPath("/a/lasting/war", nil)
   173  	openPath("/a/lasting/war/of", nil)
   174  	openPath("/a/lasting/war/of/words", nil)
   175  
   176  	// Removing an intermediate node drops the whole subtree, but not its ancestor.
   177  	removePath("/a/lasting/war", nil)
   178  	openPath("/a/lasting/war", file.ErrChildNotFound)
   179  	openPath("/a/lasting/war/of", file.ErrChildNotFound)
   180  	openPath("/a/lasting/war/of/words", file.ErrChildNotFound)
   181  
   182  	// A subtree can be spliced in, and preserve its structure.
   183  	createPath("/a/boring", nil)
   184  	setPath("/a/boring/sludge", subtree, nil)
   185  	openPath("/a/boring/sludge", nil)
   186  	openPath("/a/boring/sludge/of", nil)
   187  	openPath("/a/boring/sludge/of/words", nil)
   188  	createPath("/a/boring/song", nil)
   189  
   190  	setPath("", subtree, fpath.ErrEmptyPath)
   191  
   192  	// Verify that opening a path produces the right files.
   193  	if fp, err := fpath.OpenPath(ctx, root, "a/boring/sludge/of/words"); err != nil {
   194  		t.Errorf("OpenPath failed: %v", err)
   195  	} else {
   196  		want := []string{"a", "boring", "sludge", "of", "words"}
   197  		var got []string
   198  		for i, elt := range fp {
   199  			st := elt.Stat()
   200  			st.Mode = fs.ModeDir | 0750
   201  			st.Update()
   202  			elt.XAttr().Set("index", strconv.Itoa(i+1))
   203  			got = append(got, elt.Name())
   204  		}
   205  		if diff := cmp.Diff(want, got); diff != "" {
   206  			t.Errorf("Path names (-want, +got)\n%s", diff)
   207  		}
   208  	}
   209  
   210  	// Verify that walk is depth-first and respects its filter.
   211  	{
   212  		want := []string{
   213  			"", "a",
   214  			"a/boring", "a/boring/sludge", "a/boring/song",
   215  			"a/lasting", "a/lasting/consequence", "a/lasting/itch",
   216  			"okay",
   217  		}
   218  		var got []string
   219  		if err := fpath.Walk(ctx, root, func(e fpath.Entry) error {
   220  			got = append(got, e.Path)
   221  			if e.Err != nil {
   222  				return e.Err
   223  			} else if e.File == subtree {
   224  				return fpath.ErrSkipChildren
   225  			}
   226  			return nil
   227  		}); err != nil {
   228  			t.Errorf("Walk failed: %v", err)
   229  		}
   230  		if diff := cmp.Diff(want, got); diff != "" {
   231  			t.Errorf("Walk paths (-want, +got)\n%s", diff)
   232  		}
   233  	}
   234  
   235  	rk, err := root.Flush(ctx)
   236  	if err != nil {
   237  		t.Fatalf("Flush root: %v", err)
   238  	}
   239  	t.Logf("Root key: %x", rk)
   240  }
   241  
   242  func TestFS(t *testing.T) {
   243  	cas := mustNewCAS(t, sha1.New)
   244  	ctx := t.Context()
   245  
   246  	root := file.New(cas, &file.NewOptions{
   247  		Stat: &file.Stat{Mode: fs.ModeDir | 0755},
   248  	})
   249  	kid, err := fpath.Set(ctx, root, "kid", &fpath.SetOptions{
   250  		Create:  true,
   251  		SetStat: func(s *file.Stat) { s.Mode = 0644 },
   252  	})
   253  	if err != nil {
   254  		t.Fatalf("Create child: %v", err)
   255  	}
   256  
   257  	fp := fpath.NewFS(ctx, root)
   258  	t.Run("Open", func(t *testing.T) {
   259  		got, err := fp.Open("kid")
   260  		if err != nil {
   261  			t.Fatalf("Open kid: %v", err)
   262  		}
   263  		fi, err := got.Stat()
   264  		if err != nil {
   265  			t.Fatalf("Stat kid: %v", err)
   266  		}
   267  		if sys, ok := fi.Sys().(*file.File); !ok || sys != kid {
   268  			t.Fatalf("Stat sys: got %+v, want %+v", fi.Sys(), kid)
   269  		}
   270  	})
   271  
   272  	t.Run("Stat", func(t *testing.T) {
   273  		fi, err := fp.Stat("kid")
   274  		if err != nil {
   275  			t.Fatalf("Stat kid: %v", err)
   276  		}
   277  		if sys, ok := fi.Sys().(*file.File); !ok || sys != kid {
   278  			t.Fatalf("Stat sys: got %+v, want %+v", fi.Sys(), kid)
   279  		}
   280  	})
   281  
   282  	t.Run("ReadDir", func(t *testing.T) {
   283  		des, err := fp.ReadDir(".") // "." denotes the root, see fs.ValidPath
   284  		if err != nil {
   285  			t.Fatalf("ReadDir root: %v", err)
   286  		}
   287  		if len(des) != 1 {
   288  			t.Fatalf("Got %+v, wanted 1 entry", des)
   289  		}
   290  		if n := des[0].Name(); n != "kid" {
   291  			t.Errorf("Name: got %q, want %q", n, "kid")
   292  		}
   293  		if des[0].IsDir() {
   294  			t.Error("IsDir is true, want false")
   295  		}
   296  	})
   297  
   298  	rk, err := root.Flush(ctx)
   299  	if err != nil {
   300  		t.Fatalf("Flush root: %v", err)
   301  	}
   302  	t.Logf("Root key: %x", rk)
   303  }
   304  
   305  func errorOK(err, werr error) bool {
   306  	if werr == nil {
   307  		return err == nil
   308  	}
   309  	return errors.Is(err, werr)
   310  }