github.com/cilium/statedb@v0.3.2/quick_test.go (about)

     1  package statedb
     2  
     3  import (
     4  	"cmp"
     5  	"iter"
     6  	"strings"
     7  	"testing"
     8  	"testing/quick"
     9  
    10  	"github.com/cilium/statedb/index"
    11  	"github.com/stretchr/testify/require"
    12  )
    13  
    14  var quickConfig = &quick.Config{
    15  	MaxCount: 5000,
    16  }
    17  
    18  // Use an object with strings for both primary and secondary
    19  // indexing. With non-unique indexes we'll create a composite
    20  // key out of them by concatenation and thus we need test that
    21  // this does not break iteration order etc.
    22  type quickObj struct {
    23  	A, B string
    24  }
    25  
    26  func (q quickObj) getA() string {
    27  	return q.A
    28  }
    29  func (q quickObj) getB() string {
    30  	return q.B
    31  }
    32  
    33  var (
    34  	aIndex = Index[quickObj, string]{
    35  		Name: "a",
    36  		FromObject: func(t quickObj) index.KeySet {
    37  			return index.NewKeySet(index.String(t.A))
    38  		},
    39  		FromKey:    index.String,
    40  		FromString: index.FromString,
    41  		Unique:     true,
    42  	}
    43  
    44  	bIndex = Index[quickObj, string]{
    45  		Name: "b",
    46  		FromObject: func(t quickObj) index.KeySet {
    47  			return index.NewKeySet(index.String(t.B))
    48  		},
    49  		FromKey:    index.String,
    50  		FromString: index.FromString,
    51  		Unique:     false,
    52  	}
    53  )
    54  
    55  func isOrdered[A cmp.Ordered, B any](t *testing.T, it iter.Seq2[A, B]) bool {
    56  	var prev A
    57  	for a := range it {
    58  		if cmp.Compare(a, prev) < 0 {
    59  			t.Logf("isOrdered: %#v < %#v!", a, prev)
    60  			return false
    61  		}
    62  		prev = a
    63  	}
    64  	return true
    65  }
    66  
    67  func seqLen[A, B any](it iter.Seq2[A, B]) int {
    68  	n := 0
    69  	for range it {
    70  		n++
    71  	}
    72  	return n
    73  }
    74  
    75  func TestDB_Quick(t *testing.T) {
    76  	table, err := NewTable("test", aIndex, bIndex)
    77  	require.NoError(t, err, "NewTable")
    78  	db := New()
    79  	require.NoError(t, db.RegisterTable(table), "RegisterTable")
    80  
    81  	anyTable := AnyTable{table}
    82  
    83  	numExpected := 0
    84  
    85  	check := func(a, b string) bool {
    86  		txn := db.WriteTxn(table)
    87  		_, hadOld, err := table.Insert(txn, quickObj{a, b})
    88  		require.NoError(t, err, "Insert")
    89  		if !hadOld {
    90  			numExpected++
    91  		}
    92  		if numExpected != table.NumObjects(txn) {
    93  			t.Logf("wrong object count")
    94  			return false
    95  		}
    96  
    97  		txn.Commit()
    98  		rtxn := db.ReadTxn()
    99  
   100  		if numExpected != table.NumObjects(rtxn) {
   101  			t.Logf("wrong object count")
   102  			return false
   103  		}
   104  
   105  		//
   106  		// Check queries against the primary index
   107  		//
   108  
   109  		if numExpected != seqLen(Map(table.All(rtxn), quickObj.getA)) {
   110  			t.Logf("All() via aIndex wrong length")
   111  			return false
   112  		}
   113  		if numExpected != seqLen(Map(table.Prefix(rtxn, aIndex.Query("")), quickObj.getA)) {
   114  			t.Logf("Prefix() via aIndex wrong length")
   115  			return false
   116  		}
   117  		if numExpected != seqLen(Map(table.LowerBound(rtxn, aIndex.Query("")), quickObj.getA)) {
   118  			t.Logf("LowerBound() via aIndex wrong length")
   119  			return false
   120  		}
   121  
   122  		obj, _, found := table.Get(rtxn, aIndex.Query(a))
   123  		if !found || obj.A != a {
   124  			t.Logf("Get() via aIndex")
   125  			return false
   126  		}
   127  		for obj := range table.Prefix(rtxn, aIndex.Query(a)) {
   128  			if !strings.HasPrefix(obj.A, a) {
   129  				t.Logf("Prefix() returned object with wrong prefix via aIndex")
   130  				return false
   131  			}
   132  		}
   133  		anyObjs, err := anyTable.Prefix(rtxn, "a", a)
   134  		require.NoError(t, err, "AnyTable.Prefix")
   135  		for anyObj := range anyObjs {
   136  			obj := anyObj.(quickObj)
   137  			if !strings.HasPrefix(obj.A, a) {
   138  				t.Logf("AnyTable.Prefix() returned object with wrong prefix via aIndex")
   139  				return false
   140  			}
   141  		}
   142  
   143  		for obj := range table.LowerBound(rtxn, aIndex.Query(a)) {
   144  			if cmp.Compare(obj.A, a) < 0 {
   145  				t.Logf("LowerBound() order wrong")
   146  				return false
   147  			}
   148  		}
   149  		anyObjs, err = anyTable.LowerBound(rtxn, "a", a)
   150  		require.NoError(t, err, "AnyTable.LowerBound")
   151  		for anyObj := range anyObjs {
   152  			obj := anyObj.(quickObj)
   153  			if cmp.Compare(obj.A, a) < 0 {
   154  				t.Logf("AnyTable.LowerBound() order wrong")
   155  				return false
   156  			}
   157  		}
   158  
   159  		if !isOrdered(t, Map(table.All(rtxn), quickObj.getA)) {
   160  			t.Logf("All() wrong order")
   161  			return false
   162  		}
   163  		if !isOrdered(t, Map(table.Prefix(rtxn, aIndex.Query("")), quickObj.getA)) {
   164  			t.Logf("Prefix() via aIndex wrong order")
   165  			return false
   166  		}
   167  		if !isOrdered(t, Map(table.LowerBound(rtxn, aIndex.Query("")), quickObj.getA)) {
   168  			t.Logf("LowerBound() via aIndex wrong order")
   169  			return false
   170  		}
   171  
   172  		//
   173  		// Check against the secondary (non-unique index)
   174  		//
   175  
   176  		// Non-unique indexes return the same number of objects as we've inserted.
   177  		if numExpected != seqLen(table.Prefix(rtxn, bIndex.Query(""))) {
   178  			t.Logf("Prefix() via bIndex wrong length")
   179  			return false
   180  		}
   181  		if numExpected != seqLen(table.LowerBound(rtxn, bIndex.Query(""))) {
   182  			t.Logf("LowerBound() via bIndex wrong length")
   183  			return false
   184  		}
   185  
   186  		// Get returns the first match, but since the index is non-unique, this might
   187  		// not be the one that we just inserted.
   188  		obj, _, found = table.Get(rtxn, bIndex.Query(b))
   189  		if !found || obj.B != b {
   190  			t.Logf("Get() via bIndex not found or wrong B")
   191  			return false
   192  		}
   193  
   194  		found = false
   195  		for obj := range table.List(rtxn, bIndex.Query(b)) {
   196  			if obj.B != b {
   197  				t.Logf("List() via bIndex wrong B")
   198  				return false
   199  			}
   200  			if obj.A == a {
   201  				found = true
   202  			}
   203  		}
   204  		if !found {
   205  			t.Logf("List() via bIndex, object not found")
   206  			return false
   207  		}
   208  
   209  		visited := map[string]struct{}{}
   210  		for obj := range table.Prefix(rtxn, bIndex.Query(b)) {
   211  			if !strings.HasPrefix(obj.B, b) {
   212  				t.Logf("Prefix() via bIndex has wrong prefix")
   213  				return false
   214  			}
   215  			if _, found := visited[obj.A]; found {
   216  				t.Logf("Prefix() visited object %q twice", obj.A)
   217  				return false
   218  			}
   219  			visited[obj.A] = struct{}{}
   220  		}
   221  
   222  		anyObjs, err = anyTable.Prefix(rtxn, "b", b)
   223  		require.NoError(t, err, "AnyTable.Prefix")
   224  		for anyObj := range anyObjs {
   225  			obj := anyObj.(quickObj)
   226  			if !strings.HasPrefix(obj.B, b) {
   227  				t.Logf("AnyTable.Prefix() via bIndex has wrong prefix")
   228  				return false
   229  			}
   230  		}
   231  
   232  		visited = map[string]struct{}{}
   233  		for obj := range table.LowerBound(rtxn, bIndex.Query(b)) {
   234  			if cmp.Compare(obj.B, b) < 0 {
   235  				t.Logf("LowerBound() via bIndex has wrong objects, expected %v >= %v", []byte(obj.B), []byte(b))
   236  				return false
   237  			}
   238  			if _, found := visited[obj.A]; found {
   239  				t.Logf("Prefix() visited object %q twice", obj.A)
   240  				return false
   241  			}
   242  			visited[obj.A] = struct{}{}
   243  		}
   244  
   245  		anyObjs, err = anyTable.LowerBound(rtxn, "b", b)
   246  		require.NoError(t, err, "AnyTable.LowerBound")
   247  		for anyObj := range anyObjs {
   248  			obj := anyObj.(quickObj)
   249  			if cmp.Compare(obj.B, b) < 0 {
   250  				t.Logf("AnyTable.LowerBound() via bIndex has wrong objects, expected %v >= %v", []byte(obj.B), []byte(b))
   251  				return false
   252  			}
   253  		}
   254  
   255  		// Iterating over the secondary index returns the objects in order
   256  		// defined by the "B" key.
   257  		if !isOrdered(t, Map(table.Prefix(rtxn, bIndex.Query("")), quickObj.getB)) {
   258  			t.Logf("Prefix() via bIndex has wrong order")
   259  			return false
   260  		}
   261  		if !isOrdered(t, Map(table.LowerBound(rtxn, bIndex.Query("")), quickObj.getB)) {
   262  			t.Logf("LowerBound() via bIndex has wrong order")
   263  			return false
   264  		}
   265  		return true
   266  	}
   267  
   268  	require.NoError(t, quick.Check(check, quickConfig))
   269  }