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

     1  // SPDX-License-Identifier: Apache-2.0
     2  // Copyright Authors of Cilium
     3  
     4  package statedb
     5  
     6  import (
     7  	"testing"
     8  	"time"
     9  
    10  	"github.com/stretchr/testify/assert"
    11  	"github.com/stretchr/testify/require"
    12  
    13  	"github.com/cilium/statedb/index"
    14  )
    15  
    16  // Test_Regression_29324 tests that Get() on a index.String-based
    17  // unique index only returns exact matches.
    18  // https://github.com/cilium/cilium/issues/29324
    19  func Test_Regression_29324(t *testing.T) {
    20  	type object struct {
    21  		ID  string
    22  		Tag string
    23  	}
    24  	idIndex := Index[object, string]{
    25  		Name: "id",
    26  		FromObject: func(t object) index.KeySet {
    27  			return index.NewKeySet(index.String(t.ID))
    28  		},
    29  		FromKey: index.String,
    30  		Unique:  true,
    31  	}
    32  	tagIndex := Index[object, string]{
    33  		Name: "tag",
    34  		FromObject: func(t object) index.KeySet {
    35  			return index.NewKeySet(index.String(t.Tag))
    36  		},
    37  		FromKey: index.String,
    38  		Unique:  false,
    39  	}
    40  
    41  	db, _, _ := newTestDB(t)
    42  	table, err := NewTable("objects", idIndex, tagIndex)
    43  	require.NoError(t, err)
    44  	require.NoError(t, db.RegisterTable(table))
    45  
    46  	wtxn := db.WriteTxn(table)
    47  	table.Insert(wtxn, object{"foo", "aa"})
    48  	table.Insert(wtxn, object{"foobar", "aaa"})
    49  	table.Insert(wtxn, object{"baz", "aaaa"})
    50  	wtxn.Commit()
    51  
    52  	// Exact match should only return "foo"
    53  	txn := db.ReadTxn()
    54  	iter := table.List(txn, idIndex.Query("foo"))
    55  	items := Collect(iter)
    56  	if assert.Len(t, items, 1, "Get(\"foo\") should return one match") {
    57  		assert.EqualValues(t, "foo", items[0].ID)
    58  	}
    59  
    60  	// Partial match on prefix should not return anything
    61  	iter = table.List(txn, idIndex.Query("foob"))
    62  	items = Collect(iter)
    63  	assert.Len(t, items, 0, "Get(\"foob\") should return nothing")
    64  
    65  	// Query on non-unique index should only return exact match
    66  	iter = table.List(txn, tagIndex.Query("aa"))
    67  	items = Collect(iter)
    68  	if assert.Len(t, items, 1, "Get(\"aa\") on tags should return one match") {
    69  		assert.EqualValues(t, "foo", items[0].ID)
    70  	}
    71  
    72  	// Partial match on prefix should not return anything on non-unique index
    73  	iter = table.List(txn, idIndex.Query("a"))
    74  	items = Collect(iter)
    75  	assert.Len(t, items, 0, "Get(\"a\") should return nothing")
    76  }
    77  
    78  // The watch channel returned by Changes() must be a closed one if there
    79  // is anything left to iterate over. Otherwise on partial iteration we'll
    80  // wait on a watch channel that reflects the changes of a full iteration
    81  // and we might be stuck waiting even when there's unprocessed changes.
    82  func Test_Regression_Changes_Watch(t *testing.T) {
    83  	db, table, _ := newTestDB(t)
    84  
    85  	wtxn := db.WriteTxn(table)
    86  	changeIter, err := table.Changes(wtxn)
    87  	require.NoError(t, err, "Changes")
    88  	wtxn.Commit()
    89  
    90  	n := 0
    91  	changes, watch := changeIter.Next(db.ReadTxn())
    92  	for change := range changes {
    93  		t.Fatalf("did not expect changes, got: %v", change)
    94  	}
    95  
    96  	// The returned watch channel is closed on the first call to Next()
    97  	// as there may have been changes to iterate and we want it to be
    98  	// safe to either partially consume the changes or even block first
    99  	// on the watch channel and only then consume.
   100  	select {
   101  	case <-watch:
   102  	default:
   103  		t.Fatalf("Changes() watch channel not closed")
   104  	}
   105  
   106  	// Calling Next() again now will get a proper non-closed watch channel.
   107  	changes, watch = changeIter.Next(db.ReadTxn())
   108  	for change := range changes {
   109  		t.Fatalf("did not expect changes, got: %v", change)
   110  	}
   111  	select {
   112  	case <-watch:
   113  		t.Fatalf("Changes() watch channel unexpectedly closed")
   114  	default:
   115  	}
   116  
   117  	wtxn = db.WriteTxn(table)
   118  	table.Insert(wtxn, testObject{ID: 1})
   119  	table.Insert(wtxn, testObject{ID: 2})
   120  	table.Insert(wtxn, testObject{ID: 3})
   121  	wtxn.Commit()
   122  
   123  	// Observe the objects.
   124  	select {
   125  	case <-watch:
   126  	case <-time.After(time.Second):
   127  		t.Fatalf("Changes() watch channel not closed after inserts")
   128  	}
   129  
   130  	changes, watch = changeIter.Next(db.ReadTxn())
   131  	n = 0
   132  	for change := range changes {
   133  		require.False(t, change.Deleted, "not deleted")
   134  		n++
   135  	}
   136  	require.Equal(t, 3, n, "expected 3 objects")
   137  
   138  	// Delete the objects
   139  	wtxn = db.WriteTxn(table)
   140  	require.NoError(t, table.DeleteAll(wtxn), "DeleteAll")
   141  	wtxn.Commit()
   142  
   143  	// Partially observe the changes
   144  	<-watch
   145  	changes, watch = changeIter.Next(db.ReadTxn())
   146  	for change := range changes {
   147  		require.True(t, change.Deleted, "expected Deleted")
   148  		break
   149  	}
   150  
   151  	// Calling Next again after partially consuming the iterator
   152  	// should return a closed watch channel.
   153  	changes, watch = changeIter.Next(db.ReadTxn())
   154  	select {
   155  	case <-watch:
   156  	case <-time.After(time.Second):
   157  		t.Fatalf("Changes() watch channel not closed!")
   158  	}
   159  
   160  	// Consume the rest of the deletions.
   161  	n = 1
   162  	for change := range changes {
   163  		require.True(t, change.Deleted, "expected Deleted")
   164  		n++
   165  	}
   166  	require.Equal(t, 3, n, "expected 3 deletions")
   167  }
   168  
   169  // Prefix and LowerBound searches on non-unique indexes did not properly check
   170  // whether the object was a false positive due to matching on the primary key part
   171  // of the composite key (<secondary><primary><secondary length>). E.g. if the
   172  // composite keys were <a><aa><1> and <aa><b><2> then Prefix("aa") incorrectly
   173  // yielded the <a><aa><1> as it matched partially the primary key <aa>.
   174  //
   175  // Also another issue existed with the ordering of the results due to there being
   176  // no separator between <secondary> and <primary> parts of the composite key.
   177  // E.g. <a><z><1> and <aa><a><2> were yielded in the incorrect order
   178  // <aa><a><2> and <a><z><1>, which implied "aa" < "a"!
   179  func Test_Regression_Prefix_NonUnique(t *testing.T) {
   180  	type object struct {
   181  		ID  string
   182  		Tag string
   183  	}
   184  	idIndex := Index[object, string]{
   185  		Name: "id",
   186  		FromObject: func(t object) index.KeySet {
   187  			return index.NewKeySet(index.String(t.ID))
   188  		},
   189  		FromKey: index.String,
   190  		Unique:  true,
   191  	}
   192  	tagIndex := Index[object, string]{
   193  		Name: "tag",
   194  		FromObject: func(t object) index.KeySet {
   195  			return index.NewKeySet(index.String(t.Tag))
   196  		},
   197  		FromKey: index.String,
   198  		Unique:  false,
   199  	}
   200  
   201  	db, _, _ := newTestDB(t)
   202  	table, err := NewTable("objects", idIndex, tagIndex)
   203  	require.NoError(t, err)
   204  	require.NoError(t, db.RegisterTable(table))
   205  
   206  	wtxn := db.WriteTxn(table)
   207  	table.Insert(wtxn, object{"aa", "a"})
   208  	table.Insert(wtxn, object{"b", "bb"})
   209  	table.Insert(wtxn, object{"z", "b"})
   210  	wtxn.Commit()
   211  
   212  	// The tag index has one object with tag "a", prefix searching
   213  	// "aa" should return nothing.
   214  	txn := db.ReadTxn()
   215  	iter := table.Prefix(txn, tagIndex.Query("aa"))
   216  	items := Collect(iter)
   217  	assert.Len(t, items, 0, "Prefix(\"aa\") should return nothing")
   218  
   219  	iter = table.Prefix(txn, tagIndex.Query("a"))
   220  	items = Collect(iter)
   221  	if assert.Len(t, items, 1, "Prefix(\"a\") on tags should return one match") {
   222  		assert.EqualValues(t, "aa", items[0].ID)
   223  	}
   224  
   225  	// Check prefix search ordering: should be fully defined by the secondary key.
   226  	iter = table.Prefix(txn, tagIndex.Query("b"))
   227  	items = Collect(iter)
   228  	if assert.Len(t, items, 2, "Prefix(\"b\") on tags should return two matches") {
   229  		assert.EqualValues(t, "z", items[0].ID)
   230  		assert.EqualValues(t, "b", items[1].ID)
   231  	}
   232  
   233  	// With LowerBound search on "aa" we should see tags "b" and "bb" (in that order)
   234  	iter = table.LowerBound(txn, tagIndex.Query("aa"))
   235  	items = Collect(iter)
   236  	if assert.Len(t, items, 2, "LowerBound(\"aa\") on tags should return two matches") {
   237  		assert.EqualValues(t, "z", items[0].ID)
   238  		assert.EqualValues(t, "b", items[1].ID)
   239  	}
   240  }