Repository: ovn-org/libovsdb
Pull request #291 (2022-02-10 21:10:09)

adding Wait to client api
Adding client.api to support Wait

Example usage:

```
        lb := &nbdb.LoadBalancer{}
        condition := model.Condition{
            Field:    &lb.Name,
            Function: ovsdb.ConditionEqual,
            Value:    "lbName",
        }
        timeout0 := 0

        client.Where(lb, condition).Wait(
            ovsdb.WaitConditionNotEqual, // Until
            &timeout0, // Timeout
            &lb, // Row (and Table)
            &lb.Name, // Cols (aka fields)
        )
```
Fixes https://github.com/ovn-org/libovsdb/issues/284

coveralls: ## Pull Request Test Coverage Report for [Build 1850383040](https://coveralls.io/builds/46584606)

* **55** of **63**   **(87.3%)**  changed or added relevant lines in **1** file are covered.
* No unchanged relevant lines lost coverage.
* Overall coverage increased (+**0.2%**) to **73.609%**

---

|  Changes Missing Coverage | Covered Lines | Changed/Added Lines | % |
| :-----|--------------|--------|---: |
| [client/api.go](https://coveralls.io/builds/46584606/source?filename=client%2Fapi.go#L441) | 55 | 63 | 87.3%
<!-- | **Total:** | **55** | **63** | **87.3%** | -->


|  Totals | [![Coverage Status](https://coveralls.io/builds/46584606/badge)](https://coveralls.io/builds/46584606) |
| :-- | --: |
| Change from base [Build 1754146115](https://coveralls.io/builds/45987686): |  0.2% |
| Covered Lines: | 4471 |
| Relevant Lines: | 6074 |

---
##### 💛  - [Coveralls](https://coveralls.io)

flavio-fernandes: @amorenoz @dave-tucker PTAL

Patch:
diff --git a/client/api.go b/client/api.go
index 0297ff3..8caecad 100644
--- a/client/api.go
+++ b/client/api.go
@@ -69,6 +69,10 @@ type ConditionalAPI interface {
 
 	// Delete returns the Operations needed to delete the models selected via the condition
 	Delete() ([]ovsdb.Operation, error)
+
+	// Wait returns the operations needed to perform the wait specified
+	// by the until condition, timeout, row and columns based on provided parameters.
+	Wait(ovsdb.WaitCondition, *int, model.Model, ...interface{}) ([]ovsdb.Operation, error)
 }
 
 // ErrWrongType is used to report the user provided parameter has the wrong type
@@ -406,6 +410,80 @@ func (a api) Delete() ([]ovsdb.Operation, error) {
 	return operations, nil
 }
 
+func (a api) Wait(untilConFun ovsdb.WaitCondition, timeout *int, model model.Model, fields ...interface{}) ([]ovsdb.Operation, error) {
+	var operations []ovsdb.Operation
+
+	/*
+		    Ref: https://datatracker.ietf.org/doc/html/rfc7047.txt#section-5.2.6
+
+			lb := &nbdb.LoadBalancer{}
+			condition := model.Condition{
+				Field:    &lb.Name,
+				Function: ovsdb.ConditionEqual,
+				Value:    "lbName",
+			}
+			timeout0 := 0
+			client.Where(lb, condition).Wait(
+				ovsdb.WaitConditionNotEqual, // Until
+				&timeout0, // Timeout
+				&lb, // Row (and Table)
+				&lb.Name, // Cols (aka fields)
+			)
+	*/
+
+	conditions, err := a.cond.Generate()
+	if err != nil {
+		return nil, err
+	}
+
+	table, err := a.getTableFromModel(model)
+	if err != nil {
+		return nil, err
+	}
+
+	info, err := a.cache.DatabaseModel().NewModelInfo(model)
+	if err != nil {
+		return nil, err
+	}
+
+	var columnNames []string
+	if len(fields) > 0 {
+		columnNames = make([]string, 0, len(fields))
+		for _, f := range fields {
+			colName, err := info.ColumnByPtr(f)
+			if err != nil {
+				return nil, err
+			}
+			columnNames = append(columnNames, colName)
+		}
+	}
+
+	row, err := a.cache.Mapper().NewRow(info, fields...)
+	if err != nil {
+		return nil, err
+	}
+	rows := []ovsdb.Row{row}
+
+	for _, condition := range conditions {
+		operation := ovsdb.Operation{
+			Op:      ovsdb.OperationWait,
+			Table:   table,
+			Where:   condition,
+			Until:   string(untilConFun),
+			Columns: columnNames,
+			Rows:    rows,
+		}
+
+		if timeout != nil {
+			operation.Timeout = timeout
+		}
+
+		operations = append(operations, operation)
+	}
+
+	return operations, nil
+}
+
 // getTableFromModel returns the table name from a Model object after performing
 // type verifications on the model
 func (a api) getTableFromModel(m interface{}) (string, error) {
diff --git a/ovsdb/condition.go b/ovsdb/condition.go
index 1415521..783ac0f 100644
--- a/ovsdb/condition.go
+++ b/ovsdb/condition.go
@@ -7,6 +7,7 @@ import (
 )
 
 type ConditionFunction string
+type WaitCondition string
 
 const (
 	// ConditionLessThan is the less than condition
@@ -25,6 +26,11 @@ const (
 	ConditionIncludes ConditionFunction = "includes"
 	// ConditionExcludes is the excludes condition
 	ConditionExcludes ConditionFunction = "excludes"
+
+	// WaitConditionEqual is the equal condition
+	WaitConditionEqual WaitCondition = "=="
+	// WaitConditionNotEqual is the not equal condition
+	WaitConditionNotEqual WaitCondition = "!="
 )
 
 // Condition is described in RFC 7047: 5.1


Test patch:
diff --git a/client/api_test.go b/client/api_test.go
index 8c10beb..f549a4a 100644
--- a/client/api_test.go
+++ b/client/api_test.go
@@ -1292,3 +1292,171 @@ func BenchmarkAPIListPredicate(b *testing.B) {
 		})
 	}
 }
+
+func TestAPIWait(t *testing.T) {
+	tcache := apiTestCache(t, cache.Data{})
+	timeout0 := 0
+
+	test := []struct {
+		name      string
+		condition func(API) ConditionalAPI
+		prepare   func() (model.Model, []interface{})
+		until     ovsdb.WaitCondition
+		timeout   *int
+		result    []ovsdb.Operation
+		err       bool
+	}{
+		{
+			name: "timeout 0, no columns",
+			condition: func(a API) ConditionalAPI {
+				return a.Where(&testLogicalSwitchPort{
+					Name: "lsp0",
+				})
+			},
+			until:   "==",
+			timeout: &timeout0,
+			prepare: func() (model.Model, []interface{}) {
+				testLSP := testLogicalSwitchPort{
+					Name: "lsp0",
+				}
+				return &testLSP, nil
+			},
+			result: []ovsdb.Operation{
+				{
+					Op:      ovsdb.OperationWait,
+					Table:   "Logical_Switch_Port",
+					Timeout: &timeout0,
+					Where:   []ovsdb.Condition{{Column: "name", Function: ovsdb.ConditionEqual, Value: "lsp0"}},
+					Until:   string(ovsdb.WaitConditionEqual),
+					Columns: nil,
+					Rows:    []ovsdb.Row{{"name": "lsp0"}},
+				},
+			},
+			err: false,
+		},
+		{
+			name: "no timeout",
+			condition: func(a API) ConditionalAPI {
+				return a.Where(&testLogicalSwitchPort{
+					Name: "lsp0",
+				})
+			},
+			until: "!=",
+			prepare: func() (model.Model, []interface{}) {
+				testLSP := testLogicalSwitchPort{
+					Name: "lsp0",
+					Type: "someType",
+				}
+				return &testLSP, []interface{}{&testLSP.Name, &testLSP.Type}
+			},
+			result: []ovsdb.Operation{
+				{
+					Op:      ovsdb.OperationWait,
+					Timeout: nil,
+					Table:   "Logical_Switch_Port",
+					Where:   []ovsdb.Condition{{Column: "name", Function: ovsdb.ConditionEqual, Value: "lsp0"}},
+					Until:   string(ovsdb.WaitConditionNotEqual),
+					Columns: []string{"name", "type"},
+					Rows:    []ovsdb.Row{{"name": "lsp0", "type": "someType"}},
+				},
+			},
+			err: false,
+		},
+		{
+			name: "multiple conditions",
+			condition: func(a API) ConditionalAPI {
+				isUp := true
+				lsp := testLogicalSwitchPort{}
+				conditions := []model.Condition{
+					{
+						Field:    &lsp.Up,
+						Function: ovsdb.ConditionNotEqual,
+						Value:    &isUp,
+					},
+					{
+						Field:    &lsp.Name,
+						Function: ovsdb.ConditionEqual,
+						Value:    "lspNameCondition",
+					},
+				}
+				return a.Where(&lsp, conditions...)
+			},
+			until: "!=",
+			prepare: func() (model.Model, []interface{}) {
+				testLSP := testLogicalSwitchPort{
+					Name: "lsp0",
+					Type: "someType",
+				}
+				return &testLSP, []interface{}{&testLSP.Name, &testLSP.Type}
+			},
+			result: []ovsdb.Operation{
+				{
+					Op:      ovsdb.OperationWait,
+					Timeout: nil,
+					Table:   "Logical_Switch_Port",
+					Where: []ovsdb.Condition{
+						{
+							Column:   "up",
+							Function: ovsdb.ConditionNotEqual,
+							Value:    ovsdb.OvsSet{GoSet: []interface{}{true}},
+						},
+					},
+					Until:   string(ovsdb.WaitConditionNotEqual),
+					Columns: []string{"name", "type"},
+					Rows:    []ovsdb.Row{{"name": "lsp0", "type": "someType"}},
+				},
+				{
+					Op:      ovsdb.OperationWait,
+					Timeout: nil,
+					Table:   "Logical_Switch_Port",
+					Where:   []ovsdb.Condition{{Column: "name", Function: ovsdb.ConditionEqual, Value: "lspNameCondition"}},
+					Until:   string(ovsdb.WaitConditionNotEqual),
+					Columns: []string{"name", "type"},
+					Rows:    []ovsdb.Row{{"name": "lsp0", "type": "someType"}},
+				},
+			},
+			err: false,
+		},
+		{
+			name: "non-indexed condition error",
+			condition: func(a API) ConditionalAPI {
+				isUp := false
+				return a.Where(&testLogicalSwitchPort{Up: &isUp})
+			},
+			until: "==",
+			prepare: func() (model.Model, []interface{}) {
+				testLSP := testLogicalSwitchPort{Name: "lsp0"}
+				return &testLSP, nil
+			},
+			err: true,
+		},
+		{
+			name: "no operation",
+			condition: func(a API) ConditionalAPI {
+				return a.WhereCache(func(t *testLogicalSwitchPort) bool { return false })
+			},
+			until: "==",
+			prepare: func() (model.Model, []interface{}) {
+				testLSP := testLogicalSwitchPort{Name: "lsp0"}
+				return &testLSP, nil
+			},
+			result: []ovsdb.Operation{},
+			err:    false,
+		},
+	}
+
+	for _, tt := range test {
+		t.Run(fmt.Sprintf("ApiWait: %s", tt.name), func(t *testing.T) {
+			api := newAPI(tcache, &discardLogger)
+			cond := tt.condition(api)
+			model, fields := tt.prepare()
+			ops, err := cond.Wait(tt.until, tt.timeout, model, fields...)
+			if tt.err {
+				assert.NotNil(t, err)
+			} else {
+				assert.Nil(t, err)
+				assert.ElementsMatchf(t, tt.result, ops, "ovsdb.Operations should match")
+			}
+		})
+	}
+}