From a6e918ca0cbf7670cd4e721649fd577c1835ec28 Mon Sep 17 00:00:00 2001
From: Manuel Kieweg <mail@manuelkieweg.de>
Date: Mon, 8 Mar 2021 16:30:59 +0000
Subject: [PATCH] tests for unmarshal and adjusted old tests

---
 cmd/gnmi/gnmi.go               |   2 +-
 nucleus/gnmi_transport.go      |   8 +-
 nucleus/gnmi_transport_test.go |  36 +++---
 nucleus/southbound.go          |  67 ++++++----
 nucleus/southbound_test.go     | 227 +++++++++++++++++++++++++++++++++
 5 files changed, 289 insertions(+), 51 deletions(-)

diff --git a/cmd/gnmi/gnmi.go b/cmd/gnmi/gnmi.go
index 43a1c1152..da537bf1c 100644
--- a/cmd/gnmi/gnmi.go
+++ b/cmd/gnmi/gnmi.go
@@ -46,7 +46,7 @@ func main() {
 
 	device.Transport = transport
 
-	p := []string{"/interfaces/interface"}
+	p := []string{"/interfaces"}
 	errors := 0
 	for _, path := range p {
 		err := pnd.RequestAll(path)
diff --git a/nucleus/gnmi_transport.go b/nucleus/gnmi_transport.go
index 13dfa1197..8a2c22f5d 100644
--- a/nucleus/gnmi_transport.go
+++ b/nucleus/gnmi_transport.go
@@ -19,7 +19,7 @@ var tapProto bool
 func init() {
 	// tapProto taps gpb.getResponse and gpb.Getrequests
 	// to binary file
-	// CAUTION only set true if you know what you do
+	// CAUTION only set true if you know what you're doing
 	tapProto = false
 }
 
@@ -94,10 +94,6 @@ func (g *Gnmi) ProcessResponse(resp interface{}, root interface{}, s *ytypes.Sch
 					return err
 				}
 			}
-			modelKey := extractModelKey(fullPath)
-			log.Debug(modelKey)
-			schema := models[modelKey]
-
 			val,ok := update.Val.Value.(*gpb.TypedValue_JsonIetfVal)
 			if ok {
 				opts := []ytypes.UnmarshalOpt{&ytypes.IgnoreExtraFields{}}
@@ -106,6 +102,8 @@ func (g *Gnmi) ProcessResponse(resp interface{}, root interface{}, s *ytypes.Sch
 				}
 				return nil
 			}
+			// TODO(mk): Evaluate hardcoded model key
+			schema := models["Device"]
 			opts := []ytypes.SetNodeOpt{&ytypes.InitMissingElements{}, &ytypes.TolerateJSONInconsistencies{}}
 			if err := g.SetNode(schema, root, update.Path, update.Val, opts...); err != nil {
 				return err
diff --git a/nucleus/gnmi_transport_test.go b/nucleus/gnmi_transport_test.go
index 3a88dd0a5..e54dff7b9 100644
--- a/nucleus/gnmi_transport_test.go
+++ b/nucleus/gnmi_transport_test.go
@@ -5,7 +5,6 @@ import (
 	"code.fbi.h-da.de/cocsn/gosdn/mocks"
 	"code.fbi.h-da.de/cocsn/gosdn/nucleus/util"
 	"code.fbi.h-da.de/cocsn/gosdn/test"
-	"code.fbi.h-da.de/cocsn/yang-models/generated/arista"
 	"code.fbi.h-da.de/cocsn/yang-models/generated/openconfig"
 	"context"
 	log "github.com/golang/glog"
@@ -24,6 +23,7 @@ func TestMain(m *testing.M) {
 	testSetupGnmi()
 	testSetupPnd()
 	testSetupStore()
+	testSetupSbi()
 	os.Exit(m.Run())
 }
 
@@ -76,9 +76,11 @@ func TestGnmi_Capabilities(t *testing.T) {
 
 	capabilityRequest := &gpb.CapabilityRequest{}
 
-	ctx := context.Background()
 	transport.client.(*mocks.GNMIClient).
-		On("Capabilities", ctx, capabilityRequest).
+		On("NewContext", mockContext, mock.Anything).
+		Return(mockContext)
+	transport.client.(*mocks.GNMIClient).
+		On("Capabilities", mockContext, capabilityRequest).
 		Return(capabilityResponse, nil)
 
 	type fields struct {
@@ -281,37 +283,28 @@ func TestGnmi_ProcessResponse(t *testing.T) {
 		wantErr bool
 	}{
 		{
-			name:   "Arista Full Node",
-			fields: fields{Sbi: &Arista{}},
+			name:   "Interfaces Interface",
+			fields: fields{Sbi: &OpenConfig{}},
 			args: args{
 				path: "../test/resp-full-node",
-				root: &arista.Device{},
-			},
-			wantErr: false,
-		},
-		{
-			name:   "Arista Interfaces Wildcard",
-			fields: fields{Sbi: &Arista{}},
-			args: args{
-				path: "../test/resp-interfaces-wildcard",
-				root: &arista.Device{},
+				root: &openconfig.Device{},
 			},
-			wantErr: false,
+			wantErr: true,
 		},
 		{
-			name:   "OC Full Node",
+			name:   "Interfaces Wildcard",
 			fields: fields{Sbi: &OpenConfig{}},
 			args: args{
-				path: "../test/resp-full-node",
+				path: "../test/resp-interfaces-wildcard",
 				root: &openconfig.Device{},
 			},
 			wantErr: false,
 		},
 		{
-			name:   "OC Interfaces Wildcard",
+			name:   "Root",
 			fields: fields{Sbi: &OpenConfig{}},
 			args: args{
-				path: "../test/resp-interfaces-wildcard",
+				path: "../test/resp-full-node-arista-ceos",
 				root: &openconfig.Device{},
 			},
 			wantErr: false,
@@ -321,6 +314,7 @@ func TestGnmi_ProcessResponse(t *testing.T) {
 		t.Run(tt.name, func(t *testing.T) {
 			g := &Gnmi{
 				SetNode: tt.fields.Sbi.SetNode(),
+				Unmarshal: tt.fields.Sbi.(*OpenConfig).Unmarshal(),
 			}
 			s := tt.fields.Sbi.Schema()
 			resp := &gpb.GetResponse{}
@@ -563,7 +557,7 @@ func Test_extractModelKey(t *testing.T) {
 	}
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
-			if got := extraxtPathElements(tt.args.path); got != tt.want {
+			if got := extractModelKey(tt.args.path); got != tt.want {
 				t.Errorf("extraxtPathElements() = %v, want %v", got, tt.want)
 			}
 		})
diff --git a/nucleus/southbound.go b/nucleus/southbound.go
index 150497df7..d1755dad2 100644
--- a/nucleus/southbound.go
+++ b/nucleus/southbound.go
@@ -73,33 +73,52 @@ func (oc *OpenConfig) SetNode() func(schema *yang.Entry, root interface{}, path
 // representation to the transport.
 // Needed for type assertion.
 func (oc *OpenConfig) Unmarshal() func([]byte, []string, interface{}, ...ytypes.UnmarshalOpt) error {
-	return func(bytes []byte, fields []string, goStruct interface{}, opt ...ytypes.UnmarshalOpt) error {
-		switch l := len(fields); l {
-		case 0:
-			return openconfig.Unmarshal(bytes, goStruct.(*openconfig.Device), opt...)
-		case 1:
-		default:
-			return &ErrUnsupportedPath{fields}
-		}
-		var c ygot.GoStruct
-		var field string
-
-		// Load SBI definition
-		d := openconfig.Device{}
-		c, field = iter(&d, fields)
-		if err := openconfig.Unmarshal(bytes, c, opt...); err != nil {
-			log.Error(err)
+	return unmarshal
+}
+
+// unmarshal parses a root or 1st level gNMI response to a go struct
+// Named return due to how recover works here
+func unmarshal(bytes []byte, fields []string, goStruct interface{}, opt ...ytypes.UnmarshalOpt) (err error) {
+	defer func() {
+		if r := recover(); r != nil {
+			err = r.(error)
 		}
-		reflect.ValueOf(goStruct.(*openconfig.Device)).Elem().FieldByName(field).Set(reflect.ValueOf(c))
-		return nil
+	}()
+	switch l := len(fields); l {
+	case 0:
+		return openconfig.Unmarshal(bytes, goStruct.(*openconfig.Device), opt...)
+	case 1:
+	default:
+		return &ErrUnsupportedPath{fields}
+	}
+	var c ygot.GoStruct
+	var field string
+
+	// Load SBI definition
+	d := openconfig.Device{}
+	c, field, err = iter(&d, fields)
+	if err != nil {
+		return
 	}
+	if err := openconfig.Unmarshal(bytes, c, opt...); err != nil {
+		log.Error(err)
+	}
+	reflect.ValueOf(goStruct.(*openconfig.Device)).Elem().FieldByName(field).Set(reflect.ValueOf(c))
+	return nil
 }
 
-func iter(a ygot.GoStruct, fields []string) (ygot.GoStruct, string) {
-	var b ygot.GoStruct
+// iter walks down the provided paths and initializes the ygot.GoStruct. It only works for
+// the root level. Watch out for named returns here
+// TODO(mk): Fix deeper layers
+func iter(a ygot.GoStruct, fields []string) (b ygot.GoStruct, f string, err error) {
+	defer func() {
+		if r := recover(); r != nil {
+			err = r.(error)
+		}
+	}()
 	var c ygot.GoStruct
 	var configStruct reflect.Value
-	f := fields[0]
+	f = fields[0]
 	s := reflect.ValueOf(a).Elem()
 	h := s.FieldByName(f)
 	configStruct = reflect.New(h.Type())
@@ -122,12 +141,12 @@ func iter(a ygot.GoStruct, fields []string) (ygot.GoStruct, string) {
 		b = configStruct.Elem().Interface().(ygot.GoStruct)
 	}
 	if len(fields) > 1 {
-		c, _ = iter(b, fields[1:])
+		c, _, _ = iter(b, fields[1:])
 	} else {
-		return b, f
+		return
 	}
 	reflect.ValueOf(b).Elem().FieldByName(f).Set(reflect.ValueOf(c))
-	return b, f
+	return
 }
 
 func (oc *OpenConfig) Id() uuid.UUID {
diff --git a/nucleus/southbound_test.go b/nucleus/southbound_test.go
index 0fa4ffa90..8063b8120 100644
--- a/nucleus/southbound_test.go
+++ b/nucleus/southbound_test.go
@@ -1 +1,228 @@
 package nucleus
+
+import (
+	"code.fbi.h-da.de/cocsn/gosdn/nucleus/util"
+	"code.fbi.h-da.de/cocsn/yang-models/generated/openconfig"
+	"github.com/google/uuid"
+	gpb "github.com/openconfig/gnmi/proto/gnmi"
+	"github.com/openconfig/ygot/ygot"
+	"github.com/openconfig/ygot/ytypes"
+	log "github.com/sirupsen/logrus"
+	"reflect"
+	"testing"
+)
+
+func testSetupSbi() {
+	var err error
+	aristaUUID, err = uuid.Parse("d3795249-579c-4be7-8818-29f113cb86ee")
+	if err != nil {
+		log.Fatal(err)
+	}
+
+	ocUUID, err = uuid.Parse("5e252b70-38f2-4c99-a0bf-1b16af4d7e67")
+	if err != nil {
+		log.Fatal(err)
+	}
+}
+
+var aristaUUID uuid.UUID
+var ocUUID uuid.UUID
+
+func TestOpenConfig_Id(t *testing.T) {
+	type fields struct {
+		transport Transport
+		schema    *ytypes.Schema
+		id        uuid.UUID
+	}
+	tests := []struct {
+		name   string
+		fields fields
+		want   uuid.UUID
+	}{
+		{
+			name: "default",
+			fields: fields{
+				transport: nil,
+				schema:    nil,
+				id:        ocUUID,
+			},
+			want: ocUUID,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			oc := &OpenConfig{
+				transport: tt.fields.transport,
+				schema:    tt.fields.schema,
+				id:        tt.fields.id,
+			}
+			if got := oc.Id(); !reflect.DeepEqual(got, tt.want) {
+				t.Errorf("Id() = %v, want %v", got, tt.want)
+			}
+		})
+	}
+}
+
+func TestOpenConfig_SbiIdentifier(t *testing.T) {
+	type fields struct {
+		transport Transport
+		schema    *ytypes.Schema
+		id        uuid.UUID
+	}
+	tests := []struct {
+		name   string
+		fields fields
+		want   string
+	}{
+		{name: "default", fields: fields{}, want: "openconfig"},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			oc := &OpenConfig{
+				transport: tt.fields.transport,
+				schema:    tt.fields.schema,
+				id:        tt.fields.id,
+			}
+			if got := oc.SbiIdentifier(); got != tt.want {
+				t.Errorf("SbiIdentifier() = %v, want %v", got, tt.want)
+			}
+		})
+	}
+}
+
+func TestOpenConfig_Schema(t *testing.T) {
+	schema, err := openconfig.Schema()
+	if err != nil {
+		t.Error(err)
+	}
+	type fields struct {
+		transport Transport
+		schema    *ytypes.Schema
+		id        uuid.UUID
+	}
+	tests := []struct {
+		name   string
+		fields fields
+		want   *ytypes.Schema
+	}{
+		{name: "default", fields: fields{}, want: schema},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			oc := &OpenConfig{
+				transport: tt.fields.transport,
+				schema:    tt.fields.schema,
+				id:        tt.fields.id,
+			}
+			if got := oc.Schema(); !reflect.DeepEqual(got, tt.want) {
+				t.Errorf("Schema() = %v, want %v", got, tt.want)
+			}
+		})
+	}
+}
+
+func Test_unmarshal(t *testing.T) {
+	type args struct {
+		path string
+		goStruct interface{}
+		opt      []ytypes.UnmarshalOpt
+	}
+	tests := []struct {
+		name    string
+		args    args
+		wantErr bool
+	}{
+		{
+			name: "fail",
+			args: args{
+				goStruct: &openconfig.Device{},
+				path: "../test/resp-interfaces-interface-arista-ceos",
+			},
+			wantErr: true,
+		},
+		{
+			name: "root w/opts",
+			args: args{
+				path:     "../test/resp-full-node-arista-ceos",
+				goStruct: &openconfig.Device{},
+				opt:      []ytypes.UnmarshalOpt{&ytypes.IgnoreExtraFields{}},
+			},
+			wantErr: false,
+		},
+		{
+			name: "root w/o opts",
+			args: args{
+				path:     "../test/resp-full-node-arista-ceos",
+				goStruct: &openconfig.Device{},
+				opt:      nil,
+			},
+			wantErr: true,
+		},
+		{
+			name: "interfaces w/opts",
+			args: args{
+				path:     "../test/resp-interfaces-arista-ceos",
+				goStruct: &openconfig.Device{},
+				opt:      []ytypes.UnmarshalOpt{&ytypes.IgnoreExtraFields{}},
+			},
+			wantErr: false,
+		},
+		{
+			name: "interfaces w/o opts",
+			args: args{
+				path:     "../test/resp-interfaces-arista-ceos",
+				goStruct: &openconfig.Device{},
+				opt:      nil,
+			},
+			wantErr: true,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			resp := &gpb.GetResponse{}
+			err := util.Read(tt.args.path, resp)
+			if err != nil {
+				t.Error(err)
+			}
+			fields := extraxtPathElements(resp.Notification[0].Update[0].Path)
+			bytes := resp.Notification[0].Update[0].Val.GetJsonIetfVal()
+			if err := unmarshal(bytes, fields, tt.args.goStruct, tt.args.opt...); (err != nil) != tt.wantErr {
+				t.Errorf("unmarshal() error = %v, wantErr %v", err, tt.wantErr)
+			}
+			if tt.args.goStruct.(*openconfig.Device).Interfaces == nil && tt.args.opt != nil{
+				t.Errorf("unmarshal() error: field Interfaces must not be nil")
+			}
+		})
+	}
+}
+
+func Test_iter(t *testing.T) {
+	type args struct {
+		a      ygot.GoStruct
+		fields []string
+	}
+	tests := []struct {
+		name      string
+		args      args
+		wantB     ygot.GoStruct
+		wantField string
+		wantErr   bool
+	}{
+		// TODO: Add test cases.
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			gotB, gotField, err := iter(tt.args.a, tt.args.fields)
+			if (err != nil) != tt.wantErr {
+				t.Errorf("iter() error = %v, wantErr %v", err, tt.wantErr)
+				return
+			}
+			if !reflect.DeepEqual(gotB, tt.wantB) {
+				t.Errorf("iter() gotB = %v, want %v", gotB, tt.wantB)
+			}
+			if gotField != tt.wantField {
+				t.Errorf("iter() gotField = %v, want %v", gotField, tt.wantField)
+			}
+		})
+	}
+}
-- 
GitLab