github.com/creachadair/ffs@v0.17.3/file/file_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 file_test 16 17 import ( 18 "fmt" 19 "io" 20 "io/fs" 21 "log" 22 "math/rand" 23 "sort" 24 "strings" 25 "sync" 26 "testing" 27 "time" 28 29 "github.com/creachadair/ffs/blob" 30 "github.com/creachadair/ffs/blob/memstore" 31 "github.com/creachadair/ffs/block" 32 "github.com/creachadair/ffs/file" 33 "github.com/creachadair/ffs/file/wiretype" 34 "github.com/google/go-cmp/cmp" 35 "github.com/google/go-cmp/cmp/cmpopts" 36 "google.golang.org/protobuf/encoding/prototext" 37 "google.golang.org/protobuf/proto" 38 ) 39 40 // Interface satisfaction checks. 41 var ( 42 _ fs.File = (*file.Cursor)(nil) 43 _ fs.ReadDirFile = (*file.Cursor)(nil) 44 _ fs.FileInfo = file.FileInfo{} 45 _ fs.DirEntry = file.DirEntry{} 46 ) 47 48 func TestRoundTrip(t *testing.T) { 49 cas := blob.CASFromKV(memstore.NewKV()) 50 51 // Construct a new file and write it to storage, then read it back and 52 // verify that the original state was correctly restored. 53 f := file.New(cas, &file.NewOptions{ 54 Stat: &file.Stat{Mode: 0640}, 55 PersistStat: true, 56 57 Split: &block.SplitConfig{Min: 17, Size: 84, Max: 500}, 58 }) 59 if n := f.Data().Size(); n != 0 { 60 t.Errorf("Size: got %d, want 0", n) 61 } 62 if key := f.Key(); key != "" { 63 t.Errorf("Key: got %q, want empty", key) 64 } 65 ctx := t.Context() 66 67 wantx := map[string]string{ 68 "fruit": "apple", 69 "nut": "hazelnut", 70 } 71 for k, v := range wantx { 72 f.XAttr().Set(k, v) 73 } 74 75 const testMessage = "Four fat fennel farmers fell feverishly for Felicia Frances" 76 fmt.Fprint(f.Cursor(ctx), testMessage) 77 if n := f.Data().Size(); n != int64(len(testMessage)) { 78 t.Errorf("Size: got %d, want %d", n, len(testMessage)) 79 } 80 fkey, err := f.Flush(ctx) 81 if err != nil { 82 t.Fatalf("Flushing failed: %v", err) 83 } 84 if key := f.Key(); key != fkey { 85 t.Errorf("Key: got %x, want %x", key, fkey) 86 } 87 88 g, err := file.Open(ctx, cas, fkey) 89 if err != nil { 90 t.Fatalf("Open %x: %v", fkey, err) 91 } 92 93 // Verify that file contents were preserved. 94 bits, err := io.ReadAll(g.Cursor(ctx)) 95 if err != nil { 96 t.Errorf("Reading %x: %v", fkey, err) 97 } 98 if got := string(bits); got != testMessage { 99 t.Errorf("Reading %x: got %q, want %q", fkey, got, testMessage) 100 } 101 102 // Verify that extended attributes were preserved. 103 gotx := make(map[string]string) 104 xa := g.XAttr() 105 for _, key := range xa.Names() { 106 gotx[key] = xa.Get(key) 107 } 108 if diff := cmp.Diff(wantx, gotx); diff != "" { 109 t.Errorf("XAttr (-want, +got)\n%s", diff) 110 } 111 112 // Verify that file stat was preserved. 113 ignoreUnexported := cmpopts.IgnoreUnexported(file.Stat{}) 114 if diff := cmp.Diff(f.Stat(), g.Stat(), ignoreUnexported); diff != "" { 115 t.Errorf("Stat (-want, +got)\n%s", diff) 116 } 117 if got, want := g.Stat().Persistent(), f.Stat().Persistent(); got != want { 118 t.Errorf("Stat persist: got %v, want %v", got, want) 119 } 120 121 // Verify that seek and truncation work. 122 if err := g.Truncate(ctx, 15); err != nil { 123 t.Errorf("Truncate(15): unexpected error: %v", err) 124 } else if pos, err := g.Cursor(ctx).Seek(0, io.SeekStart); err != nil { 125 t.Errorf("Seek(0): unexpected error: %v", err) 126 } else if pos != 0 { 127 t.Errorf("Pos after Seek(0): got %d, want 0", pos) 128 } else if bits, err := io.ReadAll(g.Cursor(ctx)); err != nil { 129 t.Errorf("Read failed: %v", err) 130 } else if got, want := string(bits), testMessage[:15]; got != want { 131 t.Errorf("Truncated message: got %q, want %q", got, want) 132 } 133 134 // Exercise the scanner. 135 if err := f.Scan(ctx, func(v file.ScanItem) bool { 136 if key := v.Key(); key != fkey { 137 t.Errorf("File key: got %x, want %x", key, fkey) 138 } 139 return true 140 }); err != nil { 141 t.Fatalf("Scan failed: %v", err) 142 } 143 } 144 145 func TestScan(t *testing.T) { 146 cas := blob.CASFromKV(memstore.NewKV()) 147 ctx := t.Context() 148 149 root := file.New(cas, nil) 150 setFile := func(ss ...string) { 151 cur := root 152 for _, s := range ss { 153 sub := root.New(nil) 154 cur.Child().Set(s, sub) 155 cur = sub 156 } 157 } 158 159 setFile("1", "4") 160 setFile("A", "B") 161 setFile("1", "2", "3") 162 setFile("9") 163 setFile("5", "6", "7", "8") 164 165 var got []string 166 if err := root.Scan(ctx, func(e file.ScanItem) bool { 167 e.File.XAttr().Set("name", e.Name) 168 got = append(got, e.Name) 169 return true 170 }); err != nil { 171 t.Fatalf("Scan failed: %v", err) 172 } 173 if !sort.StringsAreSorted(got) { 174 t.Errorf("Scan result: %q, should be sorted", got) 175 } 176 177 key, err := root.Flush(ctx) 178 if err != nil { 179 t.Fatalf("Flush failed: %v", err) 180 } 181 182 alt, err := file.Open(ctx, cas, key) 183 if err != nil { 184 t.Fatalf("Open %x failed: %v", key, err) 185 } 186 if err := alt.Scan(ctx, func(e file.ScanItem) bool { 187 if got := e.File.XAttr().Get("name"); got != e.Name { 188 t.Errorf("File %p name: got %q, want %q", e.File, got, e.Name) 189 } 190 return true 191 }); err != nil { 192 t.Errorf("Scan failed: %v", err) 193 } 194 } 195 196 func TestChild(t *testing.T) { 197 cas := blob.CASFromKV(memstore.NewKV()) 198 ctx := t.Context() 199 root := file.New(cas, nil) 200 201 names := []string{"all.txt", "your.go", "base.exe"} 202 for _, name := range names { 203 root.Child().Set(name, root.New(nil)) 204 } 205 206 // Names should come out in lexicographic order. 207 sort.Strings(names) 208 209 // Child names should be correct even without a flush. 210 if diff := cmp.Diff(names, root.Child().Names()); diff != "" { 211 t.Errorf("Wrong children (-want, +got):\n%s", diff) 212 } 213 214 // Flushing shouldn't disturb the names. 215 rkey, err := root.Flush(ctx) 216 if err != nil { 217 t.Fatalf("root.Flush failed: %v", err) 218 } 219 t.Logf("Flushed root to %x", rkey) 220 221 if diff := cmp.Diff(names, root.Child().Names()); diff != "" { 222 t.Errorf("Wrong children (-want, +got):\n%s", diff) 223 } 224 225 // Release should yield all the up-to-date children. 226 if n := root.Child().Release(); n != 3 { 227 t.Errorf("Release 1: got %d, want 3", n) 228 } 229 // Now there should be nothing to release. 230 if n := root.Child().Release(); n != 0 { 231 t.Errorf("Release 2: got %d, want 0", n) 232 } 233 } 234 235 func TestCycleCheck(t *testing.T) { 236 cas := blob.CASFromKV(memstore.NewKV()) 237 ctx := t.Context() 238 root := file.New(cas, nil) 239 240 kid := file.New(cas, nil) 241 root.Child().Set("harmless", kid) 242 kid.Child().Set("harmful", root) 243 244 key, err := root.Flush(ctx) 245 if err == nil { 246 t.Errorf("Cyclic flush: got %q, nil, want error", key) 247 } else { 248 t.Logf("Cyclic flush correctly failed: %v", err) 249 } 250 } 251 252 func TestSetData(t *testing.T) { 253 cas := blob.CASFromKV(memstore.NewKV()) 254 ctx := t.Context() 255 root := file.New(cas, &file.NewOptions{ 256 Split: &block.SplitConfig{ 257 Hasher: lineHash{}, 258 Min: 5, 259 Max: 100, 260 Size: 16, 261 }, 262 }) 263 264 // Flush out the block, so that we can check below that updating the content 265 // invalidates the key. 266 okey, err := root.Flush(ctx) 267 if err != nil { 268 t.Fatalf("Flush failed: %v", err) 269 } 270 t.Logf("Old root key: %x", okey) 271 272 const input = `My name is Ozymandias 273 King of Kings! 274 Look up on my works, ye mighty 275 and despair!` 276 if err := root.SetData(ctx, strings.NewReader(input)); err != nil { 277 t.Fatalf("SetData unexpectedly failed: %v", err) 278 } 279 key, err := root.Flush(ctx) 280 if err != nil { 281 t.Errorf("Flush failed: %v", err) 282 } 283 t.Logf("Root key: %x", key) 284 285 // Make sure we invalidated the file key by setting its data. 286 if okey == key { 287 t.Errorf("File data was not invalidated: key is %x", key) 288 } 289 290 // As a reality check, read the node back in and check that we got the right 291 // number of blocks. 292 data, err := cas.Get(ctx, key) 293 if err != nil { 294 t.Fatalf("Block fetch: %v", err) 295 } 296 var obj wiretype.Object 297 if err := proto.Unmarshal(data, &obj); err != nil { 298 t.Fatalf("Unmarshal object: %v", err) 299 } 300 pb, ok := obj.Value.(*wiretype.Object_Node) 301 if !ok { 302 t.Fatal("Object does not contain a node") 303 } 304 305 // Make sure we stored the right amount of data. 306 if got, want := pb.Node.Index.TotalBytes, uint64(len(input)); got != want { 307 t.Logf("Stored total bytes: got %d, want %d", got, want) 308 } 309 310 // Make sure we stored the expected number of blocks. 311 // The artificial hasher splits on newlines, so we can just count. 312 var gotBlocks int 313 for _, ext := range pb.Node.Index.Extents { 314 gotBlocks += len(ext.Blocks) 315 } 316 wantBlocks := len(strings.Split(input, "\n")) 317 if gotBlocks != wantBlocks { 318 t.Errorf("Stored blocks: got %d, want %d", gotBlocks, wantBlocks) 319 } 320 321 t.Logf("Encoded node:\n%s", prototext.Format(pb.Node)) 322 } 323 324 func TestConcurrentFile(t *testing.T) { 325 cas := blob.CASFromKV(memstore.NewKV()) 326 ctx := t.Context() 327 root := file.New(cas, nil) 328 root.Child().Set("foo", file.New(cas, nil)) 329 330 // Create a bunch of concurrent goroutines reading and writing data and 331 // metadata on the file, to expose data races. 332 var wg sync.WaitGroup 333 for i := 0; i < 200; i++ { 334 var buf [64]byte 335 wg.Add(1) 336 switch i % 9 { 337 case 0: 338 // Write a block of data. 339 go func() { 340 defer wg.Done() 341 c := rand.Intn(len(buf)) 342 nw, err := root.WriteAt(ctx, buf[:c], int64(rand.Intn(16384))) 343 if err != nil || nw != c { 344 t.Errorf("Write failed: got (%d, %v) want (%d, nil)", nw, err, c) 345 } 346 }() 347 case 1: 348 // Read a block of data. 349 go func() { 350 defer wg.Done() 351 c := rand.Intn(len(buf)) 352 if _, err := root.ReadAt(ctx, buf[:c], int64(rand.Intn(16384))); err != nil && err != io.EOF { 353 t.Errorf("ReadAt failed: unexpected error %v", err) 354 } 355 }() 356 case 2: 357 // Read stat metadata. 358 go func() { defer wg.Done(); _ = root.FileInfo() }() 359 case 3: 360 // Modify stat metadata. 361 go func() { defer wg.Done(); root.Stat().WithModTime(time.Now()).Update() }() 362 case 4: 363 // Read data stats. 364 go func() { defer wg.Done(); _ = root.Data().Size() }() 365 case 5: 366 // Scan reachable blocks. 367 go func() { defer wg.Done(); _ = root.Scan(ctx, func(file.ScanItem) bool { return true }) }() 368 case 6: 369 // Look up a child. 370 go func() { defer wg.Done(); _ = root.Child().Has("foo") }() 371 case 7: 372 // Delete a child. 373 go func() { defer wg.Done(); root.Child().Remove("bar") }() 374 case 8: 375 // Flush the root. 376 go func() { defer wg.Done(); root.Flush(ctx) }() 377 default: 378 log.Fatalf("Incorrect test, no handler for i=%d", i) 379 } 380 } 381 wg.Wait() 382 } 383 384 type lineHash struct{} 385 386 func (h lineHash) Hash() block.Hash { return h } 387 388 func (lineHash) Update(b byte) uint64 { 389 if b == '\n' { 390 return 1 391 } 392 return 2 393 }