github.com/creachadair/ffs@v0.17.3/blob/storetest/storetest.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 storetest provides correctness tests for implementations of the
    16  // [blob.KV] interface.
    17  package storetest
    18  
    19  import (
    20  	"context"
    21  	"errors"
    22  	"fmt"
    23  	"strconv"
    24  	"strings"
    25  	"sync"
    26  	"testing"
    27  
    28  	"github.com/creachadair/ffs/blob"
    29  	"github.com/creachadair/mds/mapset"
    30  	gocmp "github.com/google/go-cmp/cmp"
    31  )
    32  
    33  type op = func(context.Context, *testing.T, blob.KV)
    34  
    35  var script = []op{
    36  	// Verify that the store is initially empty.
    37  	opList(""),
    38  	opLen(0),
    39  
    40  	// Get for a non-existing key should report an error.
    41  	opGet("nonesuch", "", blob.ErrKeyNotFound),
    42  
    43  	// Put a value in and verify that it is recorded.
    44  	opPut("fruit", "apple", false, nil),
    45  	opGet("fruit", "apple", nil),
    46  
    47  	// Put for an existing key fails when replace is false.
    48  	opPut("fruit", "pear", false, blob.ErrKeyExists),
    49  
    50  	// Put for an existing key works when replace is true.
    51  	opPut("fruit", "pear", true, nil),
    52  	opGet("fruit", "pear", nil),
    53  
    54  	opList("", "fruit"),
    55  	opLen(1),
    56  
    57  	// Add some additional keys.
    58  	opPut("nut", "hazelnut", false, nil),
    59  	opPut("animal", "cat", false, nil),
    60  	opPut("beverage", "piƱa colada", false, nil),
    61  	opPut("animal", "badger", true, nil),
    62  
    63  	opList("", "animal", "beverage", "fruit", "nut"),
    64  	opLen(4),
    65  
    66  	opPut("0", "ahoy there", false, nil),
    67  	opLen(5),
    68  	opGet("0", "ahoy there", nil),
    69  	opList("", "0", "animal", "beverage", "fruit", "nut"),
    70  
    71  	// Verify that listing respects the stop condition without error.
    72  	opListRange("", "cola", "0", "animal", "beverage"),
    73  	opListRange("animal", "last", "animal", "beverage", "fruit"),
    74  	opListRange("baker", "crude", "beverage"),
    75  	opListRange("cut", "done"),
    76  
    77  	// A missing empty key must report the correct error.
    78  	opGet("", "", blob.ErrKeyNotFound),
    79  
    80  	// Check list starting points.
    81  	opList("a", "animal", "beverage", "fruit", "nut"),
    82  	opList("animal", "animal", "beverage", "fruit", "nut"),
    83  	opList("animated", "beverage", "fruit", "nut"),
    84  	opList("goofy", "nut"),
    85  	opList("nutty"),
    86  }
    87  
    88  var delScript = []op{
    89  	// Clean up.
    90  	opLen(5),
    91  	opDelete("0", nil),
    92  	opLen(4),
    93  	opDelete("animal", nil),
    94  	opLen(3),
    95  	opDelete("fruit", nil),
    96  	opLen(2),
    97  	opDelete("nut", nil),
    98  	opLen(1),
    99  	opDelete("beverage", nil),
   100  	opList(""),
   101  	opDelete("animal", blob.ErrKeyNotFound),
   102  }
   103  
   104  func opGet(key, want string, werr error) op {
   105  	return func(ctx context.Context, t *testing.T, s blob.KV) {
   106  		t.Helper()
   107  		got, err := s.Get(ctx, key)
   108  		if !errorOK(err, werr) {
   109  			t.Errorf("s.Get(%q): got error: %v, want: %v", key, err, werr)
   110  		} else if v := string(got); v != want {
   111  			t.Errorf("s.Get(%q): got %#q, want %#q", key, v, want)
   112  		}
   113  	}
   114  }
   115  
   116  func opPut(key, data string, replace bool, werr error) op {
   117  	return func(ctx context.Context, t *testing.T, s blob.KV) {
   118  		t.Helper()
   119  		err := s.Put(ctx, blob.PutOptions{
   120  			Key:     key,
   121  			Data:    []byte(data),
   122  			Replace: replace,
   123  		})
   124  		if !errorOK(err, werr) {
   125  			t.Errorf("s.Put(%q, %q, %v): got error: %v, want: %v", key, data, replace, err, werr)
   126  		}
   127  	}
   128  }
   129  
   130  func opDelete(key string, werr error) op {
   131  	return func(ctx context.Context, t *testing.T, s blob.KV) {
   132  		t.Helper()
   133  		err := s.Delete(ctx, key)
   134  		if !errorOK(err, werr) {
   135  			t.Errorf("s.Delete(%q): got error: %v, want: %v", key, err, werr)
   136  		}
   137  	}
   138  }
   139  
   140  func opList(from string, want ...string) op {
   141  	return opListRange(from, "", want...)
   142  }
   143  
   144  func opListRange(from, to string, want ...string) op {
   145  	return func(ctx context.Context, t *testing.T, s blob.KV) {
   146  		t.Helper()
   147  		var got []string
   148  		for key, err := range s.List(ctx, from) {
   149  			if err != nil {
   150  				t.Fatalf("s.List: unexpected error: %v", err)
   151  			}
   152  			if to != "" && key >= to {
   153  				break
   154  			}
   155  			got = append(got, key)
   156  		}
   157  		if diff := gocmp.Diff(got, want); diff != "" {
   158  			t.Errorf("s.List: wrong keys (-got, +want):\n%s", diff)
   159  		}
   160  	}
   161  }
   162  
   163  func opLen(want int64) op {
   164  	return func(ctx context.Context, t *testing.T, s blob.KV) {
   165  		t.Helper()
   166  		got, err := s.Len(ctx)
   167  		if err != nil {
   168  			t.Errorf("s.Len(): unexpected error: %v", err)
   169  		}
   170  		if got != want {
   171  			t.Errorf("s.Len(): got %d, want %d", got, want)
   172  		}
   173  	}
   174  }
   175  
   176  func errorOK(err, werr error) bool {
   177  	if werr == nil {
   178  		return err == nil
   179  	}
   180  	return errors.Is(err, werr)
   181  }
   182  
   183  // Run applies the test script to empty store s, then closes s.  Any errors are
   184  // reported to t.  After Run returns, the contents of s are garbage.
   185  func Run(t *testing.T, s blob.StoreCloser) {
   186  	k1, err := s.KV(t.Context(), "one")
   187  	if err != nil {
   188  		t.Fatalf("Create keyspace 1: %v", err)
   189  	}
   190  	k2, err := s.KV(t.Context(), "two")
   191  	if err != nil {
   192  		t.Fatalf("Create keyspace 2: %v", err)
   193  	}
   194  
   195  	// Run the test script on k1 and verify that k2 was not affected.
   196  	// Precondition: k1 and k2 are both initially empty.
   197  	runCheck := func(k1, k2 blob.KV) func(t *testing.T) {
   198  		return func(t *testing.T) {
   199  			for _, op := range script {
   200  				op(t.Context(), t, k1)
   201  			}
   202  
   203  			// Verify that the edits to k1 gave the expected result.
   204  			st, err := k1.Has(t.Context(), "fruit", "animal", "beverage", "nut", "nonesuch", "0")
   205  			if err != nil {
   206  				t.Errorf("KV 1 stat: unexpected error: %v", err)
   207  			} else if diff := gocmp.Diff(st, mapset.New("0", "animal", "fruit", "nut", "beverage")); diff != "" {
   208  				t.Errorf("KV 1 stat (-got, +want):\n%s", diff)
   209  			}
   210  
   211  			// Check that calling List inside List works.
   212  			var got []string
   213  			for key1, err := range k1.List(t.Context(), "fruit") {
   214  				if err != nil {
   215  					t.Errorf("List 1: unexpected error: %v", err)
   216  					break
   217  				}
   218  				got = append(got, strings.ToUpper(key1))
   219  				for key2, err := range k1.List(t.Context(), "beverage") {
   220  					if err != nil {
   221  						t.Errorf("List 2: unexpected error: %v", err)
   222  						break
   223  					}
   224  					got = append(got, key2)
   225  				}
   226  			}
   227  
   228  			// Check that calling Has and Get inside List works.
   229  			for _, err := range k1.List(t.Context(), "") {
   230  				if err != nil {
   231  					t.Fatalf("List: unexpected error: %v", err)
   232  				}
   233  				if got, err := k1.Get(t.Context(), "nut"); err != nil || string(got) != "hazelnut" {
   234  					t.Errorf("Get nut: got (%q, %v), want (hazelnut, nil)", got, err)
   235  				}
   236  
   237  				if hs, err := k1.Has(t.Context(), "fruit"); err != nil || !hs.Has("fruit") {
   238  					t.Errorf("Has fruit: got (%v, %v), want (fruit, nil)", hs, err)
   239  				}
   240  				break
   241  			}
   242  
   243  			if diff := gocmp.Diff(got, []string{
   244  				// Caps: outer list; Lowercase: inner list.
   245  				"FRUIT", "beverage", "fruit", "nut", "NUT", "beverage", "fruit", "nut",
   246  			}); diff != "" {
   247  				t.Errorf("List/List (-got, +want):\n%s", diff)
   248  			}
   249  
   250  			// Verify that the edits to k1 did not impart mass to k2.
   251  			if n, err := k2.Len(t.Context()); err != nil || n != 0 {
   252  				t.Errorf("KV 2 len: got (%v, %v), want (0, nil)", n, err)
   253  			}
   254  		}
   255  	}
   256  
   257  	// Run the deletion script on k and verify that k is empty afterward.
   258  	cleanup := func(k blob.KV) func(t *testing.T) {
   259  		return func(t *testing.T) {
   260  			for _, op := range delScript {
   261  				op(t.Context(), t, k)
   262  			}
   263  
   264  			// Verify that k is empty after cleanup.
   265  			if n, err := k.Len(t.Context()); err != nil || n != 0 {
   266  				t.Errorf("k1.Len: got (%v, %v), want (0, nil)", n, err)
   267  			}
   268  		}
   269  	}
   270  
   271  	casTest := func(s blob.Store) func(t *testing.T) {
   272  		return func(t *testing.T) {
   273  			cas, err := s.CAS(t.Context(), "testcas")
   274  			if err != nil {
   275  				t.Fatalf("Create CAS substore: %v", err)
   276  			}
   277  			const testData = "abcde"
   278  			key, err := cas.CASPut(t.Context(), []byte(testData))
   279  			if err != nil {
   280  				t.Errorf("CASPut %q: unexpected error: %v", testData, err)
   281  			} else if err := cas.Delete(t.Context(), key); err != nil {
   282  				t.Errorf("Delete(%x): unexpected error: %v", key, err)
   283  			}
   284  		}
   285  	}
   286  
   287  	t.Run("Root", func(t *testing.T) {
   288  		t.Run("Basic", runCheck(k1, k2))
   289  		t.Run("Cleanup", cleanup(k1))
   290  		t.Run("CAS", casTest(s))
   291  	})
   292  
   293  	t.Run("Sub", func(t *testing.T) {
   294  		sub, err := s.Sub(t.Context(), "testsub")
   295  		if err != nil {
   296  			t.Fatalf("Create test substore: %v", err)
   297  		}
   298  		k3, err := sub.KV(t.Context(), "three")
   299  		if err != nil {
   300  			t.Fatalf("Create keyspace 3: %v", err)
   301  		}
   302  		t.Run("Basic", runCheck(k3, k1))
   303  		t.Run("Cleanup", cleanup(k3))
   304  		t.Run("CAS", casTest(sub))
   305  	})
   306  
   307  	// Exercise concurrency.
   308  	const numWorkers = 16
   309  	const numKeys = 16
   310  
   311  	taskKey := func(task, key int) string {
   312  		return fmt.Sprintf("task-%d-key-%d", task, key)
   313  	}
   314  
   315  	t.Run("Concurrent", func(t *testing.T) {
   316  		var wg sync.WaitGroup
   317  		for i := range numWorkers {
   318  			wg.Add(1)
   319  			i := i
   320  			go func() {
   321  				defer wg.Done()
   322  
   323  				for k := range numKeys {
   324  					key := taskKey(i, k+1)
   325  					value := strconv.Itoa(k)
   326  					if err := k2.Put(t.Context(), blob.PutOptions{
   327  						Key:     key,
   328  						Data:    []byte(value),
   329  						Replace: true,
   330  					}); err != nil {
   331  						t.Errorf("Task %d: s.Put(%q=%q) failed: %v", i, key, value, err)
   332  					}
   333  				}
   334  
   335  				// List all the keys currently in the store, and pick out all those
   336  				// that belong to this task.
   337  				mine := fmt.Sprintf("task-%d-", i)
   338  				got := mapset.New[string]()
   339  				for key, err := range k2.List(t.Context(), "") {
   340  					if err != nil {
   341  						t.Errorf("Task %d: s.List failed: %v", i, err)
   342  						break
   343  					}
   344  					if strings.HasPrefix(key, mine) {
   345  						got.Add(key)
   346  					}
   347  				}
   348  
   349  				for k := range numKeys {
   350  					key := taskKey(i, k+1)
   351  					if val, err := k1.Get(t.Context(), key); err == nil {
   352  						t.Errorf("Task %d: k1.Get(%q) got %q, want error", i, key, val)
   353  					}
   354  					if _, err := k2.Get(t.Context(), key); err != nil {
   355  						t.Errorf("Task %d: k2.Get(%q) failed: %v", i, key, err)
   356  					}
   357  
   358  					// Verify that List did not miss any of this task's keys.
   359  					if !got.Has(key) {
   360  						t.Errorf("Task %d: k2.List missing key %q", i, key)
   361  					}
   362  				}
   363  
   364  				for k := range numKeys {
   365  					key := taskKey(i, k+1)
   366  					if err := k2.Delete(t.Context(), key); err != nil {
   367  						t.Errorf("Task %d: s.Delete(%q) failed: %v", i, key, err)
   368  					}
   369  				}
   370  			}()
   371  		}
   372  		wg.Wait()
   373  
   374  		// Verify that k2 is empty after the test settles.
   375  		if n, err := k2.Len(t.Context()); err != nil || n != 0 {
   376  			t.Errorf("k2.Len: got (%v, %v), want (0, nil)", n, err)
   377  		}
   378  	})
   379  
   380  	if err := s.Close(t.Context()); err != nil {
   381  		t.Errorf("Close failed: %v", err)
   382  	}
   383  }
   384  
   385  type nopStoreCloser struct {
   386  	blob.Store
   387  }
   388  
   389  func (nopStoreCloser) Close(context.Context) error { return nil }
   390  
   391  // NopCloser wraps a [blob.Store] with a no-op Close method to implement [blob.StoreCloser].
   392  func NopCloser(s blob.Store) blob.StoreCloser { return nopStoreCloser{Store: s} }
   393  
   394  // SubKV traverses a sequence of zero or more subspace names beginning at s,
   395  // and returns a KV for the last name in the sequence. Any error during
   396  // traversal logs a failure in t.
   397  func SubKV(t *testing.T, ctx context.Context, s blob.Store, names ...string) blob.KV {
   398  	return subWalk(t, ctx, s, names, func(s blob.Store, name string) (blob.KV, error) {
   399  		return s.KV(t.Context(), name)
   400  	})
   401  }
   402  
   403  // SubCAS traverses a sequence of zero or more subspace names beginning at s,
   404  // and returns a CAS for the last name in the sequence. Any error during
   405  // traversal logs a failure in t.
   406  func SubCAS(t *testing.T, ctx context.Context, s blob.Store, names ...string) blob.CAS {
   407  	return subWalk(t, ctx, s, names, func(s blob.Store, name string) (blob.CAS, error) {
   408  		return s.CAS(t.Context(), name)
   409  	})
   410  }
   411  
   412  func subWalk[T any](t *testing.T, ctx context.Context, s blob.Store, names []string, f func(blob.Store, string) (T, error)) T {
   413  	t.Helper()
   414  	if len(names) == 0 {
   415  		t.Fatal("No keyspace name provided")
   416  	}
   417  	cur := s
   418  	for _, name := range names[:len(names)-1] {
   419  		next, err := cur.Sub(t.Context(), name)
   420  		if err != nil {
   421  			t.Fatalf("Sub(%q) failed: %v", name, err)
   422  		}
   423  		cur = next
   424  	}
   425  	last := names[len(names)-1]
   426  	v, err := f(cur, last)
   427  	if err != nil {
   428  		t.Fatalf("Lookup(%q) failed: %v", last, err)
   429  	}
   430  	return v
   431  }