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 }