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  }