diff --git a/build/ci/.test.yml b/build/ci/.test.yml index 3379d59ff0eb2e968ffce59516037e94ca035ef4..234d563779bdde73cd50cd08cbdf7aa9f5cb9a3c 100644 --- a/build/ci/.test.yml +++ b/build/ci/.test.yml @@ -27,6 +27,7 @@ integration-test: allow_failure: true variables: GOSDN_LOG: "nolog" + GOSDN_CHANGE_TIMEOUT: "100ms" rules: - if: $CI_PIPELINE_SOURCE == "merge_request_event" && $CI_MERGE_REQUEST_TARGET_BRANCH_NAME == $CI_DEFAULT_BRANCH - if: $CI_NIGHTLY diff --git a/mocks/PrincipalNetworkDomain.go b/mocks/PrincipalNetworkDomain.go index 3645a6215e5e79a44adbae955facaa66fcb639f4..6c795cb7388b80a8446f5c2c8314ed98fa3eba5f 100644 --- a/mocks/PrincipalNetworkDomain.go +++ b/mocks/PrincipalNetworkDomain.go @@ -64,27 +64,48 @@ func (_m *PrincipalNetworkDomain) ChangeOND(_a0 uuid.UUID, operation interface{} return r0 } -// Committed provides a mock function with given fields: _a0 -func (_m *PrincipalNetworkDomain) Committed(_a0 uuid.UUID) (interface{}, error) { +// Commit provides a mock function with given fields: _a0 +func (_m *PrincipalNetworkDomain) Commit(_a0 uuid.UUID) error { ret := _m.Called(_a0) - var r0 interface{} - if rf, ok := ret.Get(0).(func(uuid.UUID) interface{}); ok { + var r0 error + if rf, ok := ret.Get(0).(func(uuid.UUID) error); ok { r0 = rf(_a0) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// Committed provides a mock function with given fields: +func (_m *PrincipalNetworkDomain) Committed() []uuid.UUID { + ret := _m.Called() + + var r0 []uuid.UUID + if rf, ok := ret.Get(0).(func() []uuid.UUID); ok { + r0 = rf() } else { if ret.Get(0) != nil { - r0 = ret.Get(0).(interface{}) + r0 = ret.Get(0).([]uuid.UUID) } } - var r1 error - if rf, ok := ret.Get(1).(func(uuid.UUID) error); ok { - r1 = rf(_a0) + return r0 +} + +// Confirm provides a mock function with given fields: _a0 +func (_m *PrincipalNetworkDomain) Confirm(_a0 uuid.UUID) error { + ret := _m.Called(_a0) + + var r0 error + if rf, ok := ret.Get(0).(func(uuid.UUID) error); ok { + r0 = rf(_a0) } else { - r1 = ret.Error(1) + r0 = ret.Error(0) } - return r0, r1 + return r0 } // ContainsDevice provides a mock function with given fields: _a0 @@ -115,6 +136,22 @@ func (_m *PrincipalNetworkDomain) Destroy() error { return r0 } +// Devices provides a mock function with given fields: +func (_m *PrincipalNetworkDomain) Devices() []uuid.UUID { + ret := _m.Called() + + var r0 []uuid.UUID + if rf, ok := ret.Get(0).(func() []uuid.UUID); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]uuid.UUID) + } + } + + return r0 +} + // GetDescription provides a mock function with given fields: func (_m *PrincipalNetworkDomain) GetDescription() string { ret := _m.Called() @@ -198,38 +235,6 @@ func (_m *PrincipalNetworkDomain) ID() uuid.UUID { return r0 } -// ListCommitted provides a mock function with given fields: -func (_m *PrincipalNetworkDomain) ListCommitted() []uuid.UUID { - ret := _m.Called() - - var r0 []uuid.UUID - if rf, ok := ret.Get(0).(func() []uuid.UUID); ok { - r0 = rf() - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).([]uuid.UUID) - } - } - - return r0 -} - -// ListPending provides a mock function with given fields: -func (_m *PrincipalNetworkDomain) ListPending() []uuid.UUID { - ret := _m.Called() - - var r0 []uuid.UUID - if rf, ok := ret.Get(0).(func() []uuid.UUID); ok { - r0 = rf() - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).([]uuid.UUID) - } - } - - return r0 -} - // MarshalDevice provides a mock function with given fields: _a0 func (_m *PrincipalNetworkDomain) MarshalDevice(_a0 uuid.UUID) (string, error) { ret := _m.Called(_a0) @@ -251,27 +256,20 @@ func (_m *PrincipalNetworkDomain) MarshalDevice(_a0 uuid.UUID) (string, error) { return r0, r1 } -// Pending provides a mock function with given fields: _a0 -func (_m *PrincipalNetworkDomain) Pending(_a0 uuid.UUID) (interface{}, error) { - ret := _m.Called(_a0) +// Pending provides a mock function with given fields: +func (_m *PrincipalNetworkDomain) Pending() []uuid.UUID { + ret := _m.Called() - var r0 interface{} - if rf, ok := ret.Get(0).(func(uuid.UUID) interface{}); ok { - r0 = rf(_a0) + var r0 []uuid.UUID + if rf, ok := ret.Get(0).(func() []uuid.UUID); ok { + r0 = rf() } else { if ret.Get(0) != nil { - r0 = ret.Get(0).(interface{}) + r0 = ret.Get(0).([]uuid.UUID) } } - var r1 error - if rf, ok := ret.Get(1).(func(uuid.UUID) error); ok { - r1 = rf(_a0) - } else { - r1 = ret.Error(1) - } - - return r0, r1 + return r0 } // RemoveDevice provides a mock function with given fields: _a0 diff --git a/nucleus/http.go b/nucleus/http.go index fe644c7a152dbe52de406d9cd64ccac7abeac493..887fa10175b02bfabf8a36e6bb4d844535adff6e 100644 --- a/nucleus/http.go +++ b/nucleus/http.go @@ -2,7 +2,6 @@ package nucleus import ( "code.fbi.h-da.de/cocsn/gosdn/forks/goarista/gnmi" - . "code.fbi.h-da.de/cocsn/gosdn/nucleus/pnd" "context" "fmt" "github.com/google/uuid" @@ -179,7 +178,7 @@ func httpHandler(writer http.ResponseWriter, request *http.Request) { handleServerError(writer, err) return } - writeIDs(writer, "Devices", p.(*pndImplementation).devices.UUIDs()) + writeIDs(writer, "Devices", p.Devices()) } case "init": writeIDs(writer, "PNDs", c.pndc.UUIDs()) @@ -197,10 +196,10 @@ func httpHandler(writer http.ResponseWriter, request *http.Request) { } writer.WriteHeader(http.StatusOK) case "change-list": - changes := pnd.ListCommitted() + changes := pnd.Committed() writeIDs(writer, "Tentative changes", changes) case "change-list-pending": - changes := pnd.ListPending() + changes := pnd.Pending() writeIDs(writer, "Pending changes", changes) case "change-commit": cuid, err := uuid.Parse(query.Get("cuid")) @@ -208,13 +207,7 @@ func httpHandler(writer http.ResponseWriter, request *http.Request) { handleServerError(writer, err) return } - change, err := pnd.Pending(cuid) - if err != nil { - handleServerError(writer, err) - return - } - err = change.(*Change).Commit() - if err != nil { + if err := pnd.Commit(cuid); err != nil { handleServerError(writer, err) return } @@ -225,13 +218,7 @@ func httpHandler(writer http.ResponseWriter, request *http.Request) { handleServerError(writer, err) return } - change, err := pnd.Committed(cuid) - if err != nil { - handleServerError(writer, err) - return - } - err = change.(*Change).Confirm() - if err != nil { + if err := pnd.Confirm(cuid); err != nil { handleServerError(writer, err) return } diff --git a/nucleus/http_test.go b/nucleus/http_test.go index f6b20983a2f18f914bd2a93a63d6e739c01af624..bd1eeaf40a28bbc4a7edc2b1fea6bfbc4562519f 100644 --- a/nucleus/http_test.go +++ b/nucleus/http_test.go @@ -150,19 +150,21 @@ func Test_httpApi(t *testing.T) { }, { name: "change commit", - request: apiEndpoint + "/api?q=change-commit" + args + "&cuid=" + uuid.New().String(), - want: &http.Response{StatusCode: http.StatusOK}, + request: apiEndpoint + "/api?q=change-commit" + args + "&cuid=" + cuid.String(), + // TODO: Mock Change for testing + want: &http.Response{StatusCode: http.StatusInternalServerError}, wantErr: false, }, { name: "change confirm", - request: apiEndpoint + "/api?q=change-confirm" + args + "&cuid=" + uuid.New().String(), - want: &http.Response{StatusCode: http.StatusOK}, + request: apiEndpoint + "/api?q=change-confirm" + args + "&cuid=" + cuid.String(), + // TODO: Mock Change for testing + want: &http.Response{StatusCode: http.StatusInternalServerError}, wantErr: false, }, { name: "bad request", - request: apiEndpoint + "/api?q=bad-request", + request: apiEndpoint + "/api?q=bad-request" + args, want: &http.Response{StatusCode: http.StatusBadRequest}, wantErr: false, }, diff --git a/nucleus/initialise_test.go b/nucleus/initialise_test.go index 7fb435337a602f4de461fae08f4b518b6a9f93fe..9bcd8d270f625d05445c9a818e4eedbd086edf6a 100644 --- a/nucleus/initialise_test.go +++ b/nucleus/initialise_test.go @@ -25,6 +25,7 @@ var defaultPndID uuid.UUID var ocUUID uuid.UUID var iid uuid.UUID var altIid uuid.UUID +var cuid uuid.UUID var sbi SouthboundInterface var pnd PrincipalNetworkDomain @@ -114,6 +115,7 @@ func readTestUUIDs() { ocUUID, err = uuid.Parse("5e252b70-38f2-4c99-a0bf-1b16af4d7e67") iid, err = uuid.Parse("8495a8ac-a1e8-418e-b787-10f5878b2690") altIid, err = uuid.Parse("edc5de93-2d15-4586-b2a7-fb1bc770986b") + cuid, err = uuid.Parse("3e8219b0-e926-400d-8660-217f2a25a7c6") if err != nil { log.Fatal(err) } @@ -139,5 +141,6 @@ func newPnd() pndImplementation { committedChanges: changeStore{store{}}, confirmedChanges: changeStore{store{}}, id: defaultPndID, + errChans: make(map[uuid.UUID]chan error), } } diff --git a/nucleus/pnd/change.go b/nucleus/pnd/change.go index 553e0368e7cdf2db16746ae8ba278690424cc204..bc5a34bd446ec93d77c33e0ec8f367072f4c7a51 100644 --- a/nucleus/pnd/change.go +++ b/nucleus/pnd/change.go @@ -14,23 +14,23 @@ import ( var changeTimeout time.Duration func init() { - timeout, err := time.ParseDuration(os.Getenv("GOSDN_CHANGE_TIMEOUT")) - if err != nil { - log.Fatal(err) - } - if timeout != time.Duration(0) { - changeTimeout = timeout + var err error + e := os.Getenv("GOSDN_CHANGE_TIMEOUT") + if e != "" { + changeTimeout, err = time.ParseDuration(e) + if err != nil { + log.Fatal(err) + } } else { - var err error changeTimeout, err = time.ParseDuration("10m") if err != nil { - log.Fatal() + log.Fatal(err) } } log.Debugf("change timeout set to %v", changeTimeout) } -func NewChange(device uuid.UUID, currentState ygot.GoStruct, change ygot.GoStruct, callback func(ygot.GoStruct, ygot.GoStruct) error) *Change { +func NewChange(device uuid.UUID, currentState ygot.GoStruct, change ygot.GoStruct, callback func(ygot.GoStruct, ygot.GoStruct) error, errChan chan error) *Change { return &Change{ cuid: uuid.New(), duid: device, @@ -40,6 +40,8 @@ func NewChange(device uuid.UUID, currentState ygot.GoStruct, change ygot.GoStruc committed: false, confirmed: false, callback: callback, + errChan: errChan, + Done: make(chan int), } } @@ -55,9 +57,13 @@ type Change struct { intendedState ygot.GoStruct committed bool confirmed bool + inconsistent bool callback func(ygot.GoStruct, ygot.GoStruct) error lock sync.RWMutex cancelFunc context.CancelFunc + errChan chan error + // TODO: Move nucleus.pndImplementation and Change to same package and unexport + Done chan int } func (c *Change) ID() uuid.UUID { @@ -65,10 +71,10 @@ func (c *Change) ID() uuid.UUID { } func (c *Change) Commit() error { - c.committed = true if err := c.callback(c.intendedState, c.previousState); err != nil { return err } + c.committed = true log.WithFields(log.Fields{ "change uuid": c.cuid, "device uuid": c.duid, @@ -87,14 +93,7 @@ func (c *Change) rollbackHandler(ctx context.Context) { c.lock.RLock() defer c.lock.RUnlock() if !c.confirmed { - err := c.callback(c.previousState, c.intendedState) - if err != nil { - log.WithFields(log.Fields{ - "change uuid": c.cuid, - "device uuid": c.duid, - "error": err, - }).Error("rollback error") - } + c.errChan <- c.callback(c.previousState, c.intendedState) log.WithFields(log.Fields{ "change uuid": c.cuid, "device uuid": c.duid, @@ -114,6 +113,9 @@ func (c *Change) Confirm() error { defer c.lock.Unlock() c.confirmed = true c.cancelFunc() + close(c.errChan) + c.Done <- 0 + close(c.Done) log.WithFields(log.Fields{ "change uuid": c.cuid, "device uuid": c.duid, diff --git a/nucleus/pnd/change_test.go b/nucleus/pnd/change_test.go index 67b97ee4e5b90ca668b8f8c8eab95bf5f4c9024c..7871a0db04a1527246df36a23b0ad83d264d10ef 100644 --- a/nucleus/pnd/change_test.go +++ b/nucleus/pnd/change_test.go @@ -2,10 +2,10 @@ package pnd import ( "context" + "errors" "github.com/google/uuid" "github.com/openconfig/ygot/exampleoc" "github.com/openconfig/ygot/ygot" - "os" "reflect" "sync" "testing" @@ -53,11 +53,7 @@ func TestChange_CommitRollback(t *testing.T) { if err := c.Commit(); (err != nil) != wantErr { t.Errorf("Commit() error = %v, wantErr %v", err, wantErr) } - timeout, err := time.ParseDuration(os.Getenv("GOSDN_CHANGE_TIMEOUT")) - if err != nil { - t.Error(err) - } - time.Sleep(timeout) + time.Sleep(changeTimeout) }() got := <-callback if !reflect.DeepEqual(got, want) { @@ -66,6 +62,66 @@ func TestChange_CommitRollback(t *testing.T) { close(callback) } +func TestChange_CommitRollbackError(t *testing.T) { + wantErr := false + want := errors.New("this is an expected error") + c := &Change{ + cuid: changeUUID, + duid: did, + timestamp: time.Now(), + previousState: rollbackDevice, + intendedState: commitDevice, + callback: func(first ygot.GoStruct, second ygot.GoStruct) error { + hostname := *first.(*exampleoc.Device).System.Hostname + t.Logf("hostname: %v", hostname) + switch hostname { + case rollback: + return errors.New("this is an expected error") + } + return nil + }, + lock: sync.RWMutex{}, + errChan: make(chan error), + } + go func() { + time.Sleep(time.Millisecond * 10) + if err := c.Commit(); (err != nil) != wantErr { + t.Errorf("Commit() error = %v, wantErr %v", err, wantErr) + } + time.Sleep(changeTimeout) + }() + got := <-c.errChan + if !reflect.DeepEqual(got, want) { + t.Errorf("Commit() = %v, want %v", got, want) + } + close(c.errChan) +} + +func TestChange_CommitError(t *testing.T) { + wantErr := true + c := &Change{ + cuid: changeUUID, + duid: did, + timestamp: time.Now(), + previousState: rollbackDevice, + intendedState: commitDevice, + callback: func(first ygot.GoStruct, second ygot.GoStruct) error { + return errors.New("this is an expected error") + }, + lock: sync.RWMutex{}, + } + go func() { + time.Sleep(time.Millisecond * 10) + if err := c.Commit(); (err != nil) != wantErr { + t.Errorf("Commit() error = %v, wantErr %v", err, wantErr) + } + }() + got := c.committed + if !reflect.DeepEqual(got, false) { + t.Errorf("Commit() = %v, want %v", got, false) + } +} + func TestChange_Commit(t *testing.T) { wantErr := false want := commit @@ -83,7 +139,9 @@ func TestChange_Commit(t *testing.T) { callback <- hostname return nil }, - lock: sync.RWMutex{}, + lock: sync.RWMutex{}, + errChan: make(chan error), + Done: make(chan int), } go func() { time.Sleep(time.Millisecond * 10) @@ -133,8 +191,7 @@ func TestChange_Confirm(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { c := &Change{ - cuid: tt.fields.cuid, - duid: tt.fields.duid, + committed: tt.fields.committed, timestamp: tt.fields.timestamp, previousState: &exampleoc.Device{ System: &exampleoc.System{ @@ -146,10 +203,10 @@ func TestChange_Confirm(t *testing.T) { Hostname: &commit, }, }, - callback: tt.fields.callback, - committed: tt.fields.committed, cancelFunc: cancel, lock: sync.RWMutex{}, + errChan: make(chan error), + Done: make(chan int, 1), } if err := c.Confirm(); (err != nil) != tt.wantErr { t.Errorf("Confirm() error = %v, wantErr %v", err, tt.wantErr) diff --git a/nucleus/principalNetworkDomain.go b/nucleus/principalNetworkDomain.go index df77cfa866b8c2a6afeadd47ced27d348edbd242..1e34096657abcd153f5e24589bc16ca0bf89e8e3 100644 --- a/nucleus/principalNetworkDomain.go +++ b/nucleus/principalNetworkDomain.go @@ -20,8 +20,9 @@ type PrincipalNetworkDomain interface { RemoveSbi(uuid.UUID) error AddDevice(interface{}) error GetDevice(uuid uuid.UUID) (ygot.GoStruct, error) - ChangeOND(uuid uuid.UUID, operation interface{}, path string, value ...string) error RemoveDevice(uuid.UUID) error + Devices() []uuid.UUID + ChangeOND(uuid uuid.UUID, operation interface{}, path string, value ...string) error Request(uuid.UUID, string) error RequestAll(string) error GetName() string @@ -30,21 +31,10 @@ type PrincipalNetworkDomain interface { ContainsDevice(uuid.UUID) bool GetSBIs() interface{} ID() uuid.UUID - ListPending() []uuid.UUID - Pending(uuid.UUID) (interface{}, error) - ListCommitted() []uuid.UUID - Committed(uuid.UUID) (interface{}, error) -} - -type pndImplementation struct { - name string - description string - sbic sbiStore - devices deviceStore - pendingChanges changeStore - committedChanges changeStore - confirmedChanges changeStore - id uuid.UUID + Pending() []uuid.UUID + Committed() []uuid.UUID + Commit(uuid.UUID) error + Confirm(uuid.UUID) error } // NewPND creates a Principle Network Domain @@ -58,6 +48,7 @@ func NewPND(name, description string, id uuid.UUID, sbi SouthboundInterface) (Pr committedChanges: changeStore{store{}}, confirmedChanges: changeStore{store{}}, id: id, + errChans: make(map[uuid.UUID]chan error), } if err := pnd.sbic.add(sbi); err != nil { return nil, &ErrAlreadyExists{item: sbi} @@ -65,26 +56,72 @@ func NewPND(name, description string, id uuid.UUID, sbi SouthboundInterface) (Pr return pnd, nil } -func (pnd *pndImplementation) ListPending() []uuid.UUID { +type pndImplementation struct { + name string + description string + sbic sbiStore + devices deviceStore + pendingChanges changeStore + committedChanges changeStore + confirmedChanges changeStore + id uuid.UUID + errChans map[uuid.UUID]chan error +} + +func (pnd *pndImplementation) Pending() []uuid.UUID { return pnd.pendingChanges.UUIDs() } -func (pnd *pndImplementation) ListCommitted() []uuid.UUID { +func (pnd *pndImplementation) Committed() []uuid.UUID { return pnd.committedChanges.UUIDs() } -func (pnd *pndImplementation)Pending(id uuid.UUID) (interface{}, error) { - return pnd.pendingChanges.get(id) +func (pnd *pndImplementation) Commit(u uuid.UUID) error { + change, err := pnd.pendingChanges.get(u) + if err != nil { + return err + } + if err := change.Commit(); err != nil { + return err + } + go func() { + for { + select { + case err := <-pnd.errChans[u]: + handleRollbackError(change.ID(), err) + case <-change.Done: + } + } + }() + if err := pnd.committedChanges.add(change); err != nil { + return err + } + return pnd.pendingChanges.delete(u) } -func (pnd *pndImplementation)Committed(id uuid.UUID) (interface{}, error) { - return pnd.committedChanges.get(id) +func (pnd *pndImplementation) Confirm(u uuid.UUID) error { + change, err := pnd.committedChanges.get(u) + if err != nil { + return err + } + if err := change.Confirm(); err != nil { + return err + } + if err := pnd.confirmedChanges.add(change); err != nil { + return err + } + close(pnd.errChans[u]) + return pnd.committedChanges.delete(u) } func (pnd *pndImplementation) ID() uuid.UUID { return pnd.id } +func (pnd *pndImplementation) Devices() []uuid.UUID { + return pnd.devices.UUIDs() +} + // GetName returns the name of the PND func (pnd *pndImplementation) GetName() string { return pnd.name @@ -243,6 +280,13 @@ func (pnd *pndImplementation) ChangeOND(uuid uuid.UUID, operation interface{}, p return err } + if len(value) > 1 { + return &ErrInvalidParameters{ + f: pnd.ChangeOND, + r: value, + } + } + switch operation { case TRANSPORT_UPDATE, TRANSPORT_REPLACE: typedValue := gnmi.TypedValue(value[0]) @@ -262,7 +306,14 @@ func (pnd *pndImplementation) ChangeOND(uuid uuid.UUID, operation interface{}, p return d.Transport.Set(ctx, state, change) } - change := NewChange(uuid, d.GoStruct, cpy, callback) + errChan := make(chan error) + change := NewChange(uuid, d.GoStruct, cpy, callback, errChan) + pnd.errChans[change.ID()] = errChan return pnd.pendingChanges.add(change) } + +func handleRollbackError(id uuid.UUID, err error) { + log.Error(err) + // TODO: Notion of invalid state needed. +} diff --git a/nucleus/principalNetworkDomain_test.go b/nucleus/principalNetworkDomain_test.go index ef4207edc9241670a4ebceb2ba3c56a31c2be3bf..b5e274353e535894ba131ab799042f5a07ba2add 100644 --- a/nucleus/principalNetworkDomain_test.go +++ b/nucleus/principalNetworkDomain_test.go @@ -524,16 +524,7 @@ func Test_pndImplementation_RequestAll(t *testing.T) { } func Test_pndImplementation_ChangeOND(t *testing.T) { - t.Fail() type fields struct { - name string - description string - sbic sbiStore - devices deviceStore - pendingChanges changeStore - committedChanges changeStore - confirmedChanges changeStore - id uuid.UUID } type args struct { uuid uuid.UUID @@ -547,23 +538,98 @@ func Test_pndImplementation_ChangeOND(t *testing.T) { args args wantErr bool }{ - // TODO: Add test cases. + { + name: "update", + fields: fields{}, + args: args{ + uuid: mdid, + operation: TRANSPORT_UPDATE, + path: "/system/config/hostname", + value: []string{"ceos3000"}, + }, + wantErr: false, + }, + { + name: "replace", + fields: fields{}, + args: args{ + uuid: mdid, + operation: TRANSPORT_REPLACE, + path: "/system/config/hostname", + value: []string{"ceos3000"}, + }, + wantErr: false, + }, + { + name: "delete", + fields: fields{}, + args: args{ + uuid: mdid, + operation: TRANSPORT_DELETE, + path: "/system/config/hostname", + }, + wantErr: false, + }, + { + name: "delete w/args", + fields: fields{}, + args: args{ + uuid: mdid, + operation: TRANSPORT_DELETE, + path: "/system/config/hostname", + value: []string{"ceos3000"}, + }, + wantErr: false, + }, + + // Negative test cases + { + name: "invalid operation", + fields: fields{}, + args: args{ + uuid: mdid, + operation: "INVALID", + }, + wantErr: true, + }, + { + name: "invalid arg count", + fields: fields{}, + args: args{ + uuid: mdid, + operation: TRANSPORT_UPDATE, + path: "/system/config/hostname", + value: []string{"ceos3000", "ceos3001"}, + }, + wantErr: true, + }, + { + name: "device not found", + fields: fields{}, + args: args{ + uuid: did, + operation: TRANSPORT_UPDATE, + }, + wantErr: true, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - pnd := &pndImplementation{ - name: tt.fields.name, - description: tt.fields.description, - sbic: tt.fields.sbic, - devices: tt.fields.devices, - pendingChanges: tt.fields.pendingChanges, - committedChanges: tt.fields.committedChanges, - confirmedChanges: tt.fields.confirmedChanges, - id: tt.fields.id, - } - if err := pnd.ChangeOND(tt.args.uuid, tt.args.operation, tt.args.path, tt.args.value...); (err != nil) != tt.wantErr { + p := newPnd() + d := mockDevice() + if err := p.AddDevice(&d); err != nil { + t.Error(err) + return + } + if err := p.ChangeOND(tt.args.uuid, tt.args.operation, tt.args.path, tt.args.value...); (err != nil) != tt.wantErr { t.Errorf("ChangeOND() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !tt.wantErr { + if len(p.pendingChanges.store) != 1 { + t.Errorf("ChangeOND() unexpected change count. got %v, want 1", len(p.pendingChanges.store)) + } } }) } -} \ No newline at end of file +}