From 7d102a1f1671294f09d75fd33820959be3d5d376 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20Vautrain?= <35805113+Cedrok@users.noreply.github.com> Date: Thu, 11 Jul 2024 15:03:23 +0200 Subject: [PATCH] fix(cli) compute sizeU & height on modification (#493) * fix(cli) compute sizeU & height on modification * fix(cli) crash on device update other than sizeU & height attributes * fix(cli) crash on malformated sizeU/height update & delete unreachable code * fix(cli) broken tests * fix(cli) move RACKUNIT to "models" to fix U value approximations * fix(cli) better device un GetEntity & use it in update_test.go * fix(cli) tests for sizeU & height update * fix(cli) revert tags tests * fix(cli) better checks & value casts * fix(cli) reuse device maps in update_test * fix(cli) move sizeU/height related code to dedicated file + related tests * fix(api) check coherence between sizeU and height * fix(cli) reduce complexity * fix(api) sonacloud issue --- API/models/attributes.go | 40 ++++- API/models/update_object_test.go | 77 +++++++++ API/utils/util.go | 13 ++ CLI/controllers/create_test.go | 2 +- CLI/controllers/link_test.go | 2 +- CLI/controllers/template.go | 4 +- CLI/controllers/template_test.go | 3 +- CLI/controllers/update.go | 65 +------- CLI/controllers/update_test.go | 235 ++++++++++++--------------- CLI/controllers/utils.go | 1 - CLI/models/attributes.go | 19 ++- CLI/models/attributes_device.go | 55 +++++++ CLI/models/attributes_device_test.go | 112 +++++++++++++ CLI/models/attributes_test.go | 8 +- CLI/parser/ast.go | 9 +- CLI/parser/ast_test.go | 14 +- CLI/test/utils.go | 22 ++- 17 files changed, 444 insertions(+), 237 deletions(-) create mode 100644 CLI/models/attributes_device.go create mode 100644 CLI/models/attributes_device_test.go diff --git a/API/models/attributes.go b/API/models/attributes.go index fb333ceb4..087d417df 100644 --- a/API/models/attributes.go +++ b/API/models/attributes.go @@ -10,6 +10,8 @@ import ( "go.mongodb.org/mongo-driver/bson/primitive" ) +const RACKUNIT = 0.04445 //meter + func validateAttributes(entity int, data, parent map[string]any) *u.Error { attributes := data["attributes"].(map[string]any) switch entity { @@ -21,8 +23,11 @@ func validateAttributes(entity int, data, parent map[string]any) *u.Error { return err } case u.DEVICE: - var deviceSlots []string var err *u.Error + if err = checkSizeUAndHeight(attributes); err != nil { + return err + } + var deviceSlots []string if deviceSlots, err = slotToValidSlice(attributes); err != nil { return err } @@ -67,8 +72,6 @@ func validateDeviceSlots(deviceSlots []string, deviceName, deviceParentd string) Message: "Invalid slot: one or more requested slots are already in use"} } } - } else { - // fmt.Println(err) } } return nil @@ -152,3 +155,34 @@ func setCorridorColor(attributes map[string]any) { attributes["color"] = "000099" } } + +// Check if sizeU and height are coherents +func checkSizeUAndHeight(attributes map[string]any) *u.Error { + if attributes["sizeU"] == nil || attributes["height"] == nil { + return nil + } + + sizeU, err := u.GetFloat(attributes["sizeU"]) + if err != nil { + return &u.Error{ + Type: u.ErrBadFormat, + Message: err.Error(), + } + } + height := attributes["height"] + h := sizeU * RACKUNIT + switch heightUnit := attributes["heightUnit"]; heightUnit { + case "cm": + h *= 100 + case "mm": + h *= 1000 + } + if height == h { + return nil + } else { + return &u.Error{ + Type: u.ErrBadFormat, + Message: "sizeU and height are not consistent", + } + } +} diff --git a/API/models/update_object_test.go b/API/models/update_object_test.go index 34633556c..0b06e759d 100644 --- a/API/models/update_object_test.go +++ b/API/models/update_object_test.go @@ -9,6 +9,23 @@ import ( "github.com/stretchr/testify/assert" ) +var device map[string]any + +func init() { + siName := "site" + bdName := siName + ".building" + roName := bdName + ".room" + rkName := roName + ".rack" + + integration.RequireCreateSite(siName) + integration.RequireCreateBuilding(siName, "building") + integration.RequireCreateRoom(bdName, "room") + integration.RequireCreateRack(roName, "rack") + device = integration.RequireCreateDevice(rkName, "device") +} + +// region generic + func TestUpdateGenericWorks(t *testing.T) { generic := integration.RequireCreateGeneric("", "update-object-1") @@ -26,3 +43,63 @@ func TestUpdateGenericWorks(t *testing.T) { ) assert.Nil(t, err) } + +// endregion generic + +// region device's sizeU & height + +func TestUpdateDeviceSizeUAndHeightmm(t *testing.T) { + _, err := models.UpdateObject( + u.EntityToString(u.DEVICE), + device["id"].(string), + map[string]any{ + "attributes": map[string]any{ + "sizeU": 1, + "height": 44.45, + "heightUnit": "mm", + }, + }, + true, + integration.ManagerUserRoles, + false, + ) + assert.Nil(t, err) +} +func TestUpdateDeviceSizeUAndHeightcm(t *testing.T) { + _, err := models.UpdateObject( + u.EntityToString(u.DEVICE), + device["id"].(string), + map[string]any{ + "attributes": map[string]any{ + "sizeU": 1, + "height": 4.445, + "heightUnit": "cm", + }, + }, + true, + integration.ManagerUserRoles, + false, + ) + assert.Nil(t, err) +} + +func TestUpdateDeviceSizeUAndHeightmmError(t *testing.T) { + _, err := models.UpdateObject( + u.EntityToString(u.DEVICE), + device["id"].(string), + map[string]any{ + "attributes": map[string]any{ + "sizeU": 12, + "height": 44.45, + "heightUnit": "mm", + }, + }, + true, + integration.ManagerUserRoles, + false, + ) + assert.NotNil(t, err) + assert.ErrorContains(t, err, "sizeU and height are not consistent") +} + +// endregion device's sizeU & height diff --git a/API/utils/util.go b/API/utils/util.go index b0e5b1da5..11c93308f 100644 --- a/API/utils/util.go +++ b/API/utils/util.go @@ -8,6 +8,7 @@ import ( "context" "encoding/json" "flag" + "fmt" "log" "net/http" "net/url" @@ -507,3 +508,15 @@ func StrSliceContains(slice []string, elem string) bool { } return false } + +var floatType = reflect.TypeOf(float64(0)) + +func GetFloat(unk interface{}) (float64, error) { + v := reflect.ValueOf(unk) + v = reflect.Indirect(v) + if !v.Type().ConvertibleTo(floatType) { + return 0, fmt.Errorf("cannot convert %v to float64", v.Type()) + } + fv := v.Convert(floatType) + return fv.Float(), nil +} diff --git a/CLI/controllers/create_test.go b/CLI/controllers/create_test.go index f8c124496..d8913f2d5 100644 --- a/CLI/controllers/create_test.go +++ b/CLI/controllers/create_test.go @@ -461,7 +461,7 @@ func TestCreateDeviceWithSizeU(t *testing.T) { mockGetResponse := test_utils.GetEntity("rack", "A01", "BASIC.A.R1", "test-domain") sizeU := float64(2) - height := sizeU * 44.5 + height := sizeU * models.RACKUNIT * 1000 mockCreateResponse := map[string]any{ "category": "device", "id": "BASIC.A.R1.A01.D1", diff --git a/CLI/controllers/link_test.go b/CLI/controllers/link_test.go index 6a661f97f..c9dcbbf2e 100644 --- a/CLI/controllers/link_test.go +++ b/CLI/controllers/link_test.go @@ -55,7 +55,7 @@ func TestLinkObjectWithInvalidSlots(t *testing.T) { } err := controller.LinkObject(models.StrayPath+"chT", models.PhysicalPath+"BASIC/A/R1/A01", attributes, values, slots) assert.NotNil(t, err) - assert.Equal(t, "Invalid device syntax: .. can only be used in a single element vector", err.Error()) + assert.Equal(t, "invalid device syntax: .. can only be used in a single element vector", err.Error()) } func TestLinkObjectWithValidSlots(t *testing.T) { diff --git a/CLI/controllers/template.go b/CLI/controllers/template.go index a4a68ad4b..b51ce51d8 100644 --- a/CLI/controllers/template.go +++ b/CLI/controllers/template.go @@ -94,9 +94,9 @@ func (controller Controller) ApplyTemplate(attr, data map[string]interface{}, en if t == "chassis" || t == "server" { res := 0 if val, ok := sizeInf[2].(float64); ok { - res = int((val / 1000) / RACKUNIT) + res = int((val / 1000) / models.RACKUNIT) } else if val, ok := sizeInf[2].(int); ok { - res = int((float64(val) / 1000) / RACKUNIT) + res = int((float64(val) / 1000) / models.RACKUNIT) } else { return errors.New("invalid size vector on given template") } diff --git a/CLI/controllers/template_test.go b/CLI/controllers/template_test.go index 48accbe84..61b38c1cf 100644 --- a/CLI/controllers/template_test.go +++ b/CLI/controllers/template_test.go @@ -1,7 +1,6 @@ package controllers_test import ( - "cli/controllers" "cli/models" test_utils "cli/test" "maps" @@ -44,7 +43,7 @@ func TestApplyTemplateOfTypeDeviceWorks(t *testing.T) { test_utils.MockGetObjTemplate(mockAPI, template) - sizeU := int((float64(template["sizeWDHmm"].([]any)[2].(int)) / 1000) / controllers.RACKUNIT) + sizeU := int((float64(template["sizeWDHmm"].([]any)[2].(int)) / 1000) / models.RACKUNIT) err := controller.ApplyTemplate(attributes, device, models.DEVICE) assert.Nil(t, err) diff --git a/CLI/controllers/update.go b/CLI/controllers/update.go index 3aaa7f802..fcb2e85ad 100644 --- a/CLI/controllers/update.go +++ b/CLI/controllers/update.go @@ -16,6 +16,10 @@ func (controller Controller) UpdateObj(pathStr string, data map[string]any, with category = obj["category"].(string) } + if category == models.EntityToString(models.DEVICE) { + models.ComputeSizeUAndHeight(obj, data) + } + url, err := controller.ObjectUrl(pathStr, 0) if err != nil { return nil, err @@ -29,72 +33,13 @@ func (controller Controller) UpdateObj(pathStr string, data map[string]any, with return nil, err } - //Determine if Unity requires the message as - //Interact or Modify - entityType := models.EntityStrToInt(category) - if models.IsTag(pathStr) { - entityType = models.TAG - } else if models.IsLayer(pathStr) { + if models.IsLayer(pathStr) { // For layers, update the object to the hierarchy in order to be cached data := resp.Body["data"].(map[string]any) _, err = State.Hierarchy.AddObjectInPath(data, pathStr) if err != nil { return nil, err } - entityType = models.LAYER - } - - message := map[string]any{} - var key string - - if entityType == models.ROOM && (data["tilesName"] != nil || data["tilesColor"] != nil) { - println("Room modifier detected") - Disp(data) - - //Get the interactive key - key = determineStrKey(data, []string{"tilesName", "tilesColor"}) - - message["type"] = "interact" - message["data"] = map[string]any{ - "id": obj["id"], - "param": key, - "value": data[key], - } - } else if entityType == models.RACK && data["U"] != nil { - message["type"] = "interact" - message["data"] = map[string]any{ - "id": obj["id"], - "param": "U", - "value": data["U"], - } - } else if (entityType == models.DEVICE || entityType == models.RACK) && - (data["alpha"] != nil || data["slots"] != nil || data["localCS"] != nil) { - - //Get interactive key - key = determineStrKey(data, []string{"alpha", "U", "slots", "localCS"}) - - message["type"] = "interact" - message["data"] = map[string]any{ - "id": obj["id"], - "param": key, - "value": data[key], - } - } else if entityType == models.GROUP && data["content"] != nil { - message["type"] = "interact" - message["data"] = map[string]any{ - "id": obj["id"], - "param": "content", - "value": data["content"], - } - } else { - return resp.Body, nil - } - - if IsEntityTypeForOGrEE3D(entityType) { - err := controller.Ogree3D.InformOptional("UpdateObj", entityType, message) - if err != nil { - return nil, err - } } return resp.Body, nil diff --git a/CLI/controllers/update_test.go b/CLI/controllers/update_test.go index d63b54089..27a7fa068 100644 --- a/CLI/controllers/update_test.go +++ b/CLI/controllers/update_test.go @@ -1,15 +1,15 @@ package controllers_test import ( - "cli/controllers" "cli/models" test_utils "cli/test" - "strings" "testing" "github.com/stretchr/testify/assert" ) +// region tags + func TestUpdateTagColor(t *testing.T) { controller, mockAPI, _, _ := test_utils.NewControllerWithMocks(t) @@ -32,9 +32,6 @@ func TestUpdateTagColor(t *testing.T) { } test_utils.MockUpdateObject(mockAPI, dataUpdate, dataUpdated) - - controllers.State.ObjsForUnity = controllers.SetObjsForUnity([]string{"all"}) - _, err := controller.UpdateObj(path, dataUpdate, false) assert.Nil(t, err) } @@ -63,163 +60,131 @@ func TestUpdateTagSlug(t *testing.T) { } test_utils.MockUpdateObject(mockAPI, dataUpdate, dataUpdated) - - controllers.State.ObjsForUnity = controllers.SetObjsForUnity([]string{"all"}) - _, err := controller.UpdateObj(path, dataUpdate, false) assert.Nil(t, err) } -func TestUpdateRoomTiles(t *testing.T) { - tests := []struct { - name string - attributeKey string - oldValue string - newValue string - }{ - {"Color", "tilesColor", "aaaaaa", "aaaaab"}, - {"Name", "tilesName", "t1", "t2"}, - } +//endregion tags - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - controller, mockAPI, mockOgree3D, _ := test_utils.NewControllerWithMocks(t) - - room := test_utils.CopyMap(roomWithoutChildren) - room["attributes"] = map[string]any{ - tt.attributeKey: tt.oldValue, - } - updatedRoom := test_utils.CopyMap(room) - updatedRoom["attributes"].(map[string]any)[tt.attributeKey] = tt.newValue - dataUpdate := updatedRoom["attributes"].(map[string]any) - entity := models.ROOM - - path := "/Physical/" + strings.Replace(room["id"].(string), ".", "/", -1) - message := map[string]any{ - "type": "interact", - "data": map[string]any{ - "id": room["id"], - "param": tt.attributeKey, - "value": tt.newValue, - }, - } - - mockOgree3D.On("InformOptional", "UpdateObj", entity, message).Return(nil) - - test_utils.MockGetObject(mockAPI, room) - - dataUpdated := test_utils.CopyMap(room) - dataUpdated["attributes"].(map[string]any)[tt.attributeKey] = tt.newValue - - test_utils.MockUpdateObject(mockAPI, dataUpdate, dataUpdated) - - controllers.State.ObjsForUnity = controllers.SetObjsForUnity([]string{"all"}) - - result, err := controller.UpdateObj(path, dataUpdate, false) - assert.Nil(t, err) - assert.Equal(t, result["data"].(map[string]any)["attributes"].(map[string]any)[tt.attributeKey], tt.newValue) - mockOgree3D.AssertCalled(t, "InformOptional", "UpdateObj", entity, message) - }) - } -} +// region device's sizeU -func TestUpdateRackU(t *testing.T) { - controller, mockAPI, mockOgree3D, _ := test_utils.NewControllerWithMocks(t) - rack := test_utils.CopyMap(rack2) - rack["attributes"] = map[string]any{ - "U": true, - } - updatedRack := test_utils.CopyMap(rack) - updatedRack["attributes"].(map[string]any)["U"] = false - dataUpdate := updatedRack["attributes"].(map[string]any) - entity := models.RACK - - path := "/Physical/" + strings.Replace(rack["id"].(string), ".", "/", -1) - message := map[string]any{ - "type": "interact", - "data": map[string]any{ - "id": rack["id"], - "param": "U", - "value": false, - }, - } +// Test an update of a device's sizeU with heightUnit == mm +func TestUpdateDeviceSizeUmm(t *testing.T) { + controller, mockAPI, _, _ := test_utils.NewControllerWithMocks(t) - mockOgree3D.On("InformOptional", "UpdateObj", entity, message).Return(nil) + path := models.PhysicalIDToPath("BASIC.A.R1.A01.chU1") - test_utils.MockGetObject(mockAPI, rack) - test_utils.MockUpdateObject(mockAPI, dataUpdate, updatedRack) + device := test_utils.GetEntity("device", "chU1", "BASIC.A.R1.A01", "test") + test_utils.MockGetObject(mockAPI, device) - controllers.State.ObjsForUnity = controllers.SetObjsForUnity([]string{"all"}) + dataUpdate := map[string]any{ + "attributes": map[string]any{ + "sizeU": 1, + }, + } + mockDataUpdate := map[string]any{ + "attributes": map[string]any{ + "sizeU": float64(1), + "height": 44.45, + }, + } + device["attributes"].(map[string]any)["sizeU"] = 1 + device["attributes"].(map[string]any)["height"] = 44.45 - result, err := controller.UpdateObj(path, dataUpdate, false) + test_utils.MockUpdateObject(mockAPI, mockDataUpdate, device) + _, err := controller.UpdateObj(path, dataUpdate, false) assert.Nil(t, err) - assert.False(t, result["data"].(map[string]any)["attributes"].(map[string]any)["U"].(bool)) - mockOgree3D.AssertCalled(t, "InformOptional", "UpdateObj", entity, message) } -func TestUpdateDeviceAlpha(t *testing.T) { - controller, mockAPI, mockOgree3D, _ := test_utils.NewControllerWithMocks(t) - device := test_utils.CopyMap(chassis) - device["attributes"].(map[string]any)["alpha"] = true - updatedDevice := test_utils.CopyMap(device) - updatedDevice["attributes"].(map[string]any)["alpha"] = false - dataUpdate := updatedDevice["attributes"].(map[string]any) - entity := models.DEVICE - - path := "/Physical/" + strings.Replace(device["id"].(string), ".", "/", -1) - message := map[string]any{ - "type": "interact", - "data": map[string]any{ - "id": device["id"], - "param": "alpha", - "value": false, - }, - } +// Test an update of a device's sizeU with heightUnit == cm +func TestUpdateDeviceSizeUcm(t *testing.T) { + controller, mockAPI, _, _ := test_utils.NewControllerWithMocks(t) + + path := models.PhysicalIDToPath("BASIC.A.R1.A01.chU1") - mockOgree3D.On("InformOptional", "UpdateObj", entity, message).Return(nil) + device := test_utils.GetEntity("device", "chU1", "BASIC.A.R1.A01", "test") + device["attributes"].(map[string]any)["heightUnit"] = "cm" test_utils.MockGetObject(mockAPI, device) - test_utils.MockUpdateObject(mockAPI, dataUpdate, updatedDevice) - controllers.State.ObjsForUnity = controllers.SetObjsForUnity([]string{"all"}) + dataUpdate := map[string]any{ + "attributes": map[string]any{ + "sizeU": 1, + }, + } + mockDataUpdate := map[string]any{ + "attributes": map[string]any{ + "sizeU": float64(1), + "height": 4.445, + }, + } + device["attributes"].(map[string]any)["sizeU"] = 1 + device["attributes"].(map[string]any)["height"] = 4.445 - result, err := controller.UpdateObj(path, dataUpdate, false) + test_utils.MockUpdateObject(mockAPI, mockDataUpdate, device) + _, err := controller.UpdateObj(path, dataUpdate, false) assert.Nil(t, err) - assert.False(t, result["data"].(map[string]any)["attributes"].(map[string]any)["alpha"].(bool)) - mockOgree3D.AssertCalled(t, "InformOptional", "UpdateObj", entity, message) } -func TestUpdateGroupContent(t *testing.T) { - controller, mockAPI, mockOgree3D, _ := test_utils.NewControllerWithMocks(t) - group := test_utils.CopyMap(rackGroup) - group["attributes"] = map[string]any{ - "content": "A,B", +// endregion sizeU + +// region device's height + +// Test an update of a device's height with heightUnit == mm +func TestUpdateDeviceheightmm(t *testing.T) { + controller, mockAPI, _, _ := test_utils.NewControllerWithMocks(t) + + path := models.PhysicalIDToPath("BASIC.A.R1.A01.chU1") + + device := test_utils.GetEntity("device", "chU1", "BASIC.A.R1.A01", "test") + test_utils.MockGetObject(mockAPI, device) + + dataUpdate := map[string]any{ + "attributes": map[string]any{ + "height": 44.45, + }, } - newValue := "A,B,C" - updatedGroup := test_utils.CopyMap(group) - updatedGroup["attributes"].(map[string]any)["content"] = newValue - dataUpdate := updatedGroup["attributes"].(map[string]any) - entity := models.GROUP - - path := "/Physical/" + strings.Replace(group["id"].(string), ".", "/", -1) - message := map[string]any{ - "type": "interact", - "data": map[string]any{ - "id": group["id"], - "param": "content", - "value": newValue, + mockDataUpdate := map[string]any{ + "attributes": map[string]any{ + "sizeU": float64(1), + "height": 44.45, }, } + device["attributes"].(map[string]any)["sizeU"] = 1 + device["attributes"].(map[string]any)["height"] = 44.45 + + test_utils.MockUpdateObject(mockAPI, mockDataUpdate, device) + _, err := controller.UpdateObj(path, dataUpdate, false) + assert.Nil(t, err) +} - mockOgree3D.On("InformOptional", "UpdateObj", entity, message).Return(nil) +// Test an update of a device's height with heightUnit == cm +func TestUpdateDeviceheightcm(t *testing.T) { + controller, mockAPI, _, _ := test_utils.NewControllerWithMocks(t) - test_utils.MockGetObject(mockAPI, group) - test_utils.MockUpdateObject(mockAPI, dataUpdate, updatedGroup) + path := models.PhysicalIDToPath("BASIC.A.R1.A01.chU1") - controllers.State.ObjsForUnity = controllers.SetObjsForUnity([]string{"all"}) + device := test_utils.GetEntity("device", "chU1", "BASIC.A.R1.A01", "test") + device["attributes"].(map[string]any)["heightUnit"] = "cm" + test_utils.MockGetObject(mockAPI, device) - result, err := controller.UpdateObj(path, dataUpdate, false) + dataUpdate := map[string]any{ + "attributes": map[string]any{ + "height": 4.445, + }, + } + mockDataUpdate := map[string]any{ + "attributes": map[string]any{ + "sizeU": float64(1), + "height": 4.445, + }, + } + device["attributes"].(map[string]any)["sizeU"] = 1 + device["attributes"].(map[string]any)["height"] = 4.445 + + test_utils.MockUpdateObject(mockAPI, mockDataUpdate, device) + _, err := controller.UpdateObj(path, dataUpdate, false) assert.Nil(t, err) - assert.Equal(t, result["data"].(map[string]any)["attributes"].(map[string]any)["content"].(string), newValue) - mockOgree3D.AssertCalled(t, "InformOptional", "UpdateObj", entity, message) } + +// endregion diff --git a/CLI/controllers/utils.go b/CLI/controllers/utils.go index 134f24262..5d4881914 100644 --- a/CLI/controllers/utils.go +++ b/CLI/controllers/utils.go @@ -16,7 +16,6 @@ const ( DEBUG ) -const RACKUNIT = .04445 //meter const VIRTUALCONFIG = "virtual_config" // displays contents of maps diff --git a/CLI/models/attributes.go b/CLI/models/attributes.go index d5920d18f..17cfef80d 100644 --- a/CLI/models/attributes.go +++ b/CLI/models/attributes.go @@ -10,6 +10,8 @@ import ( "strings" ) +const RACKUNIT = .04445 //meter + type EntityAttributes map[string]any var BldgBaseAttrs = EntityAttributes{ @@ -126,10 +128,10 @@ func SetDeviceSizeUIfExists(attr EntityAttributes) { //And Set height if sizeUInt, ok := sizeU.(int); ok { attr["sizeU"] = sizeUInt - attr["height"] = float64(sizeUInt) * 44.5 + attr["height"] = float64(sizeUInt) * RACKUNIT * 1000 } else if sizeUFloat, ok := sizeU.(float64); ok { attr["sizeU"] = sizeUFloat - attr["height"] = sizeUFloat * 44.5 + attr["height"] = sizeUFloat * RACKUNIT * 1000 } } } @@ -158,7 +160,7 @@ func CheckExpandStrVector(slotVector []string) ([]string, error) { for _, slot := range slotVector { if strings.Contains(slot, "..") { if len(slotVector) != 1 { - return nil, fmt.Errorf("Invalid device syntax: .. can only be used in a single element vector") + return nil, fmt.Errorf("invalid device syntax: .. can only be used in a single element vector") } return expandStrToVector(slot) } else { @@ -170,7 +172,7 @@ func CheckExpandStrVector(slotVector []string) ([]string, error) { func expandStrToVector(slot string) ([]string, error) { slots := []string{} - errMsg := "Invalid device syntax: incorrect use of .. for slot" + errMsg := "invalid device syntax: incorrect use of .. for slot" parts := strings.Split(slot, "..") if len(parts) != 2 || (parts[0][:len(parts[0])-1] != parts[1][:len(parts[1])-1]) { @@ -191,3 +193,12 @@ func expandStrToVector(slot string) ([]string, error) { } } } + +func MapStringAny(value any) (map[string]any, error) { + m, ok := value.(map[string]any) + if ok { + return m, nil + } else { + return nil, fmt.Errorf("unable to convert given value to a map") + } +} diff --git a/CLI/models/attributes_device.go b/CLI/models/attributes_device.go new file mode 100644 index 000000000..7ccd9e72f --- /dev/null +++ b/CLI/models/attributes_device.go @@ -0,0 +1,55 @@ +package models + +import ( + "cli/utils" + "fmt" +) + +// Compute coherent sizeU or height according to given data +func ComputeSizeUAndHeight(obj, data map[string]any) error { + errMsg := "unknown heightUnit value" + if data["attributes"] == nil { + return nil + } + newAttrs, err := MapStringAny(data["attributes"]) + if err != nil { + return err + } + currentAttrs, err := MapStringAny(obj["attributes"]) + if err != nil { + return err + } + if newAttrs["sizeU"] != nil { + sizeU, err := utils.GetFloat(newAttrs["sizeU"]) + if err != nil { + return err + } + var height = sizeU * RACKUNIT + switch heightUnit := currentAttrs["heightUnit"]; heightUnit { + case "cm": + height *= 100 + case "mm": + height *= 1000 + default: + return fmt.Errorf(errMsg) + } + newAttrs["height"] = height + } + if newAttrs["height"] != nil { + height, err := utils.GetFloat(newAttrs["height"]) + if err != nil { + return err + } + var sizeU = height / RACKUNIT + switch heightUnit := currentAttrs["heightUnit"]; heightUnit { + case "cm": + sizeU /= 100 + case "mm": + sizeU /= 1000 + default: + return fmt.Errorf(errMsg) + } + newAttrs["sizeU"] = sizeU + } + return nil +} diff --git a/CLI/models/attributes_device_test.go b/CLI/models/attributes_device_test.go new file mode 100644 index 000000000..790166872 --- /dev/null +++ b/CLI/models/attributes_device_test.go @@ -0,0 +1,112 @@ +package models_test + +import ( + "cli/models" + "testing" + + "github.com/stretchr/testify/assert" +) + +// region sizeU + +func TestComputeFromSizeUmm(t *testing.T) { + device := map[string]any{ + "attributes": map[string]any{ + "heightUnit": "mm", + }, + } + input := map[string]any{ + "attributes": map[string]any{ + "sizeU": 1, + }, + } + err := models.ComputeSizeUAndHeight(device, input) + assert.Nil(t, err) + +} + +func TestComputeFromSizeUcm(t *testing.T) { + device := map[string]any{ + "attributes": map[string]any{ + "heightUnit": "cm", + }, + } + input := map[string]any{ + "attributes": map[string]any{ + "sizeU": 1, + }, + } + err := models.ComputeSizeUAndHeight(device, input) + assert.Nil(t, err) + +} + +func TestComputeFromSizeUFail(t *testing.T) { + device := map[string]any{ + "attributes": map[string]any{ + "heightUnit": "banana", + }, + } + input := map[string]any{ + "attributes": map[string]any{ + "sizeU": 1, + }, + } + err := models.ComputeSizeUAndHeight(device, input) + assert.NotNil(t, err) + assert.ErrorContains(t, err, "unknown heightUnit value") +} + +// endregion + +// region height + +func TestComputeFromHeightmm(t *testing.T) { + device := map[string]any{ + "attributes": map[string]any{ + "heightUnit": "mm", + }, + } + input := map[string]any{ + "attributes": map[string]any{ + "height": 44.45, + }, + } + err := models.ComputeSizeUAndHeight(device, input) + assert.Nil(t, err) + +} + +func TestComputeFromHeightcm(t *testing.T) { + device := map[string]any{ + "attributes": map[string]any{ + "heightUnit": "cm", + }, + } + input := map[string]any{ + "attributes": map[string]any{ + "height": 4.445, + }, + } + err := models.ComputeSizeUAndHeight(device, input) + assert.Nil(t, err) + +} + +func TestComputeFromHeightFail(t *testing.T) { + device := map[string]any{ + "attributes": map[string]any{ + "heightUnit": "banana", + }, + } + input := map[string]any{ + "attributes": map[string]any{ + "height": 1, + }, + } + err := models.ComputeSizeUAndHeight(device, input) + assert.NotNil(t, err) + assert.ErrorContains(t, err, "unknown heightUnit value") +} + +// endregion diff --git a/CLI/models/attributes_test.go b/CLI/models/attributes_test.go index 84829f110..51cc21254 100644 --- a/CLI/models/attributes_test.go +++ b/CLI/models/attributes_test.go @@ -16,22 +16,22 @@ func TestExpandSlotVector(t *testing.T) { slots, err := models.CheckExpandStrVector([]string{"slot1..slot3", "slot4"}) assert.Nil(t, slots) assert.NotNil(t, err) - assert.ErrorContains(t, err, "Invalid device syntax: .. can only be used in a single element vector") + assert.ErrorContains(t, err, "invalid device syntax: .. can only be used in a single element vector") slots, err = models.CheckExpandStrVector([]string{"slot1..slot3..slot7"}) assert.Nil(t, slots) assert.NotNil(t, err) - assert.ErrorContains(t, err, "Invalid device syntax: incorrect use of .. for slot") + assert.ErrorContains(t, err, "invalid device syntax: incorrect use of .. for slot") slots, err = models.CheckExpandStrVector([]string{"slot1..slots3"}) assert.Nil(t, slots) assert.NotNil(t, err) - assert.ErrorContains(t, err, "Invalid device syntax: incorrect use of .. for slot") + assert.ErrorContains(t, err, "invalid device syntax: incorrect use of .. for slot") slots, err = models.CheckExpandStrVector([]string{"slot1..slotE"}) assert.Nil(t, slots) assert.NotNil(t, err) - assert.ErrorContains(t, err, "Invalid device syntax: incorrect use of .. for slot") + assert.ErrorContains(t, err, "invalid device syntax: incorrect use of .. for slot") slots, err = models.CheckExpandStrVector([]string{"slot1..slot3"}) assert.Nil(t, err) diff --git a/CLI/parser/ast.go b/CLI/parser/ast.go index 7ccae2af0..d42b170b9 100644 --- a/CLI/parser/ast.go +++ b/CLI/parser/ast.go @@ -2,7 +2,6 @@ package parser import ( "cli/config" - "cli/controllers" cmd "cli/controllers" "cli/models" "cli/utils" @@ -897,12 +896,12 @@ func updateAttributes(path, attributeName string, values []any) (map[string]any, if len(values) > 1 { return nil, fmt.Errorf("attributes can only be assigned a single value") } - if vconfigAttr, found := strings.CutPrefix(attributeName, controllers.VIRTUALCONFIG+"."); found { + if vconfigAttr, found := strings.CutPrefix(attributeName, cmd.VIRTUALCONFIG+"."); found { if len(vconfigAttr) < 1 { return nil, fmt.Errorf("invalid attribute name") } vAttr := map[string]any{vconfigAttr: values[0]} - attributes = map[string]any{controllers.VIRTUALCONFIG: vAttr} + attributes = map[string]any{cmd.VIRTUALCONFIG: vAttr} } else { attributes = map[string]any{attributeName: values[0]} } @@ -1369,7 +1368,7 @@ func (n *createVirtualNode) execute() (interface{}, error) { if err != nil { return nil, err } - attributes := map[string]any{controllers.VIRTUALCONFIG: map[string]any{"type": vtype}} + attributes := map[string]any{cmd.VIRTUALCONFIG: map[string]any{"type": vtype}} if n.vlinks != nil { vlinks := []string{} @@ -1389,7 +1388,7 @@ func (n *createVirtualNode) execute() (interface{}, error) { if err != nil { return nil, err } - attributes[controllers.VIRTUALCONFIG].(map[string]any)["role"] = role + attributes[cmd.VIRTUALCONFIG].(map[string]any)["role"] = role } return nil, cmd.C.CreateObject(path, models.VIRTUALOBJ, diff --git a/CLI/parser/ast_test.go b/CLI/parser/ast_test.go index 85565e11a..0864a03cd 100644 --- a/CLI/parser/ast_test.go +++ b/CLI/parser/ast_test.go @@ -892,26 +892,20 @@ func TestCreateDeviceNodeExecution(t *testing.T) { _, mockAPI, _, _ := test_utils.SetMainEnvironmentMock(t) rack := test_utils.GetEntity("rack", "myRack", "mySite.myBuilding.myRoom", "") device := test_utils.GetEntity("device", "myDevice", "mySite.myBuilding.myRoom.myRack", "") - device["attributes"].(map[string]any)["posU/slot"] = []string{} - device["attributes"].(map[string]any)["sizeU"] = 10 - device["attributes"].(map[string]any)["sizeUnit"] = "mm" - device["attributes"].(map[string]any)["height"] = 445.0 - device["attributes"].(map[string]any)["heightUnit"] = "mm" - device["attributes"].(map[string]any)["orientation"] = "front" - device["attributes"].(map[string]any)["invertOffset"] = false - delete(device["attributes"].(map[string]any), "size") + delete(device["attributes"].(map[string]any), "size") delete(device, "id") delete(device, "children") + delete(device, "tags") test_utils.MockGetObject(mockAPI, rack) test_utils.MockCreateObject(mockAPI, "device", device) executionNode := createDeviceNode{ path: pathNode{path: &valueNode{"/Physical/mySite/myBuilding/myRoom/myRack/myDevice"}}, - posUOrSlot: []node{}, + posUOrSlot: []node{&valueNode{1}}, invertOffset: false, - sizeUOrTemplate: &valueNode{10}, + sizeUOrTemplate: &valueNode{2}, side: &valueNode{"front"}, } diff --git a/CLI/test/utils.go b/CLI/test/utils.go index 64a78d011..cf7d9be5e 100644 --- a/CLI/test/utils.go +++ b/CLI/test/utils.go @@ -176,19 +176,23 @@ func GetEntity(entityName string, name string, parentId string, domain string) m } case "device": return map[string]any{ + "attributes": map[string]any{ + "height": 88.9, + "heightUnit": "mm", + "invertOffset": false, + "orientation": "front", + "posU": 1, + "size": []float64{60, 120}, + "sizeU": 2, + "sizeUnit": "mm", + }, "category": "device", + "description": "", + "domain": domain, "id": id, "name": name, "parentId": parentId, - "domain": domain, - "description": "", - "attributes": map[string]any{ - "height": 47, - "heightUnit": "U", - "orientation": "front", - "size": []float64{1, 1}, - "sizeUnit": "cm", - }, + "tags": []any{}, } case "generic": return map[string]any{