From c7600adee01f256db20ea94b8c5e3b3b9b778267 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jaime=20Caama=C3=B1o=20Ruiz?= Date: Thu, 21 Dec 2023 11:36:49 +0000 Subject: [PATCH] Add referential integrity transaction tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Provides coverage for the interaction of reference garbage collection with index constraint checks. Signed-off-by: Jaime CaamaƱo Ruiz --- database/inmemory/inmemory_test.go | 119 +++++++++++++++++++++++++++++ test/test_data.go | 32 +++++++- 2 files changed, 149 insertions(+), 2 deletions(-) diff --git a/database/inmemory/inmemory_test.go b/database/inmemory/inmemory_test.go index b26ec607..322c021f 100644 --- a/database/inmemory/inmemory_test.go +++ b/database/inmemory/inmemory_test.go @@ -924,3 +924,122 @@ func checkOperationResults(result []*ovsdb.OperationResult, ops ...ovsdb.Operati } return ovsdb.CheckOperationResults(r, ops) } + +func TestCheckIndexesWithReferentialIntegrity(t *testing.T) { + dbModel, err := GetModel() + require.NoError(t, err) + db := NewDatabase(map[string]model.ClientDBModel{"Open_vSwitch": dbModel.Client()}) + err = db.CreateDatabase("Open_vSwitch", dbModel.Schema) + require.NoError(t, err) + + ovsUUID := uuid.NewString() + managerUUID := uuid.NewString() + managerUUID2 := uuid.NewString() + ops := []ovsdb.Operation{ + { + Table: "Open_vSwitch", + Op: ovsdb.OperationInsert, + UUID: ovsUUID, + Row: ovsdb.Row{ + "manager_options": ovsdb.OvsSet{GoSet: []interface{}{ovsdb.UUID{GoUUID: managerUUID}}}, + }, + }, + { + Table: "Manager", + Op: ovsdb.OperationInsert, + UUID: managerUUID, + Row: ovsdb.Row{ + "target": "target", + }, + }, + } + + transaction := db.NewTransaction("Open_vSwitch") + results, updates := transaction.Transact(ops...) + require.Len(t, results, len(ops)) + for _, result := range results { + assert.Equal(t, "", result.Error) + } + err = db.Commit("Open_vSwitch", uuid.New(), updates) + require.NoError(t, err) + + tests := []struct { + desc string + ops func() []ovsdb.Operation + wantUpdates int + }{ + { + // As a row is deleted due to garbage collection, that row's index + // should be available for use by a different row + desc: "Replacing a strong reference should garbage collect and account for indexes", + ops: func() []ovsdb.Operation { + return []ovsdb.Operation{ + { + Table: "Open_vSwitch", + Op: ovsdb.OperationUpdate, + Row: ovsdb.Row{ + "manager_options": ovsdb.OvsSet{GoSet: []interface{}{ovsdb.UUID{GoUUID: managerUUID2}}}, + }, + Where: []ovsdb.Condition{ + ovsdb.NewCondition("_uuid", ovsdb.ConditionEqual, ovsdb.UUID{GoUUID: ovsUUID}), + }, + }, + { + Table: "Manager", + Op: ovsdb.OperationInsert, + UUID: managerUUID2, + Row: ovsdb.Row{ + "target": "target", + }, + }, + } + }, + // the update and insert above plus the delete from the garbage + // collection + wantUpdates: 3, + }, + { + desc: "A row that is not root and not strongly referenced should not cause index collisions", + ops: func() []ovsdb.Operation { + return []ovsdb.Operation{ + { + Table: "Manager", + Op: ovsdb.OperationInsert, + UUID: managerUUID2, + Row: ovsdb.Row{ + "target": "target", + }, + }, + } + }, + // no updates as the row is not strongly referenced + wantUpdates: 0, + }, + } + + for _, tt := range tests { + t.Run(tt.desc, func(t *testing.T) { + transaction := db.NewTransaction("Open_vSwitch") + ops := tt.ops() + results, update := transaction.Transact(ops...) + var err string + for _, result := range results { + if result.Error != "" { + err = result.Error + break + } + } + require.Empty(t, err, "got an unexpected error") + + tables := update.GetUpdatedTables() + var gotUpdates int + for _, table := range tables { + _ = update.ForEachRowUpdate(table, func(uuid string, row ovsdb.RowUpdate2) error { + gotUpdates++ + return nil + }) + } + assert.Equal(t, tt.wantUpdates, gotUpdates, "got a different number of updates than expected") + }) + } +} diff --git a/test/test_data.go b/test/test_data.go index 20686c17..562e6d92 100644 --- a/test/test_data.go +++ b/test/test_data.go @@ -17,6 +17,16 @@ const schema = ` "tables": { "Open_vSwitch": { "columns": { + "manager_options": { + "type": { + "key": { + "type": "uuid", + "refTable": "Manager" + }, + "min": 0, + "max": "unlimited" + } + }, "bridges": { "type": { "key": { @@ -27,6 +37,7 @@ const schema = ` } } }, + "isRoot": true, "maxRows": 1 }, "Bridge": { @@ -81,6 +92,7 @@ const schema = ` } } }, + "isRoot": true, "indexes": [ [ "name" @@ -118,12 +130,21 @@ const schema = ` } } }, + "isRoot": true, "indexes": [ [ "id", "bridge" ] ] + }, + "Manager": { + "columns": { + "target": { + "type": "string" + } + }, + "indexes": [["target"]] } } } @@ -143,8 +164,9 @@ type BridgeType struct { // OvsType is the simplified ORM model of the Bridge table type OvsType struct { - UUID string `ovsdb:"_uuid"` - Bridges []string `ovsdb:"bridges"` + UUID string `ovsdb:"_uuid"` + Bridges []string `ovsdb:"bridges"` + ManagerOptions []string `ovsdb:"manager_options"` } type FlowSampleCollectorSetType struct { @@ -155,6 +177,11 @@ type FlowSampleCollectorSetType struct { IPFIX *string // `ovsdb:"ipfix"` } +type Manager struct { + UUID string `ovsdb:"_uuid"` + Target string `ovsdb:"target"` +} + func GetModel() (model.DatabaseModel, error) { client, err := model.NewClientDBModel( "Open_vSwitch", @@ -162,6 +189,7 @@ func GetModel() (model.DatabaseModel, error) { "Open_vSwitch": &OvsType{}, "Bridge": &BridgeType{}, "Flow_Sample_Collector_Set": &FlowSampleCollectorSetType{}, + "Manager": &Manager{}, }, ) if err != nil {