From c04011f2762ec9437cb092c0c248e0814a8158ae Mon Sep 17 00:00:00 2001 From: Paolo Calao Date: Fri, 8 Oct 2021 19:38:32 +0200 Subject: [PATCH 01/20] Add dashboard create command --- cli/dashboard/create.go | 94 ++++++++++++++++++++++++++++++++++ cli/dashboard/dashboard.go | 1 + command/dashboard/create.go | 66 ++++++++++++++++++++++++ go.mod | 2 + go.sum | 4 ++ internal/iot/client.go | 10 ++++ internal/template/dashboard.go | 53 +++++++++++++++++++ internal/template/extract.go | 1 - internal/template/load.go | 93 ++++++++++++++++++++++++++++----- internal/template/vargetter.go | 62 ++++++++++++++++++++++ 10 files changed, 372 insertions(+), 14 deletions(-) create mode 100644 cli/dashboard/create.go create mode 100644 command/dashboard/create.go create mode 100644 internal/template/dashboard.go create mode 100644 internal/template/vargetter.go diff --git a/cli/dashboard/create.go b/cli/dashboard/create.go new file mode 100644 index 00000000..4a4cd527 --- /dev/null +++ b/cli/dashboard/create.go @@ -0,0 +1,94 @@ +// This file is part of arduino-cloud-cli. +// +// Copyright (C) 2021 ARDUINO SA (http://www.arduino.cc/) +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published +// by the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package dashboard + +import ( + "fmt" + "os" + "strings" + + "github.com/arduino/arduino-cli/cli/errorcodes" + "github.com/arduino/arduino-cli/cli/feedback" + "github.com/arduino/arduino-cloud-cli/command/dashboard" + "github.com/sirupsen/logrus" + "github.com/spf13/cobra" +) + +var createFlags struct { + name string + template string + override map[string]string +} + +func initCreateCommand() *cobra.Command { + createCommand := &cobra.Command{ + Use: "create", + Short: "Create a dashboard from a template", + Long: "Create a dashboard from a template for Arduino IoT Cloud", + Run: runCreateCommand, + } + createCommand.Flags().StringVarP(&createFlags.name, "name", "n", "", "Dashboard name") + createCommand.Flags().StringVarP(&createFlags.template, "template", "t", "", + "File containing a dashboard template, JSON and YAML format are supported", + ) + createCommand.Flags().StringToStringVarP(&createFlags.override, "override", "o", nil, + "Map stating the items to be overridden. Ex: 'thing-0=xxxxxxxx,thing-1=yyyyyyyy'") + + createCommand.MarkFlagRequired("template") + return createCommand +} + +func runCreateCommand(cmd *cobra.Command, args []string) { + logrus.Infof("Creating dashboard from template %s\n", createFlags.template) + + params := &dashboard.CreateParams{ + Template: createFlags.template, + Override: createFlags.override, + } + if createFlags.name != "" { + params.Name = &createFlags.name + } + + dashboard, err := dashboard.Create(params) + if err != nil { + feedback.Errorf("Error during dashboard create: %v", err) + os.Exit(errorcodes.ErrGeneric) + } + + feedback.PrintResult(createResult{dashboard}) +} + +type createResult struct { + dashboard *dashboard.DashboardInfo +} + +func (r createResult) Data() interface{} { + return r.dashboard +} + +func (r createResult) String() string { + return fmt.Sprintf( + "name: %s\nid: %s\nshared_by: %s\nshared_with: %s\nupdated_at: %s\nwidgets: %s", + r.dashboard.Name, + r.dashboard.ID, + r.dashboard.SharedBy, + strings.Join(r.dashboard.SharedWith, ", "), + r.dashboard.UpdatedAt, + strings.Join(r.dashboard.Widgets, ", "), + ) +} diff --git a/cli/dashboard/dashboard.go b/cli/dashboard/dashboard.go index ae39236f..793f1e8a 100644 --- a/cli/dashboard/dashboard.go +++ b/cli/dashboard/dashboard.go @@ -28,6 +28,7 @@ func NewCommand() *cobra.Command { Long: "Dashboard commands.", } + dashboardCommand.AddCommand(initCreateCommand()) dashboardCommand.AddCommand(initListCommand()) dashboardCommand.AddCommand(initDeleteCommand()) dashboardCommand.AddCommand(initExtractCommand()) diff --git a/command/dashboard/create.go b/command/dashboard/create.go new file mode 100644 index 00000000..b0c0ab56 --- /dev/null +++ b/command/dashboard/create.go @@ -0,0 +1,66 @@ +// This file is part of arduino-cloud-cli. +// +// Copyright (C) 2021 ARDUINO SA (http://www.arduino.cc/) +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published +// by the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package dashboard + +import ( + "errors" + + "github.com/arduino/arduino-cloud-cli/internal/config" + "github.com/arduino/arduino-cloud-cli/internal/iot" + "github.com/arduino/arduino-cloud-cli/internal/template" +) + +// CreateParams contains the parameters needed to create a new dashboard. +type CreateParams struct { + Name *string // Name of the new dashboard + Override map[string]string // Template parameters to be overridden + Template string // Path of the template file +} + +// Create allows to create a new dashboard +func Create(params *CreateParams) (*DashboardInfo, error) { + conf, err := config.Retrieve() + if err != nil { + return nil, err + } + iotClient, err := iot.NewClient(conf.Client, conf.Secret) + if err != nil { + return nil, err + } + + dashboard, err := template.LoadDashboard(params.Template, params.Override) + if err != nil { + return nil, err + } + + // Name passed as parameter has priority over name from template + if params.Name != nil { + dashboard.Name = *params.Name + } + // If name is not specified in the template, it should be passed as parameter + if dashboard.Name == "" { + return nil, errors.New("dashboard name not specified") + } + + newDashboard, err := iotClient.DashboardCreate(dashboard) + if err != nil { + return nil, err + } + + return getDashboardInfo(newDashboard), nil +} diff --git a/go.mod b/go.mod index 68e1a4f1..6ab281e9 100644 --- a/go.mod +++ b/go.mod @@ -7,6 +7,8 @@ require ( github.com/arduino/arduino-cli v0.0.0-20210607095659-16f41352eac3 github.com/arduino/go-paths-helper v1.6.1 github.com/arduino/iot-client-go v1.3.4-0.20210930122852-04551f4cb061 + github.com/gofrs/uuid v4.0.0+incompatible // indirect + github.com/google/go-cmp v0.5.6 // indirect github.com/howeyc/crc16 v0.0.0-20171223171357-2b2a61e366a6 github.com/juju/errors v0.0.0-20181118221551-089d3ea4e4d5 github.com/sirupsen/logrus v1.8.1 diff --git a/go.sum b/go.sum index 2fdc7a11..9d0693bb 100644 --- a/go.sum +++ b/go.sum @@ -137,6 +137,8 @@ github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9 github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/gofrs/uuid v3.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= +github.com/gofrs/uuid v4.0.0+incompatible h1:1SD/1F5pU8p29ybwgQSwpQk+mwdRrXCYuPhW6m+TnJw= +github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= @@ -179,6 +181,8 @@ github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ= +github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= diff --git a/internal/iot/client.go b/internal/iot/client.go index 54aac5b5..0b42326a 100644 --- a/internal/iot/client.go +++ b/internal/iot/client.go @@ -39,6 +39,7 @@ type Client interface { ThingDelete(id string) error ThingShow(id string) (*iotclient.ArduinoThing, error) ThingList(ids []string, device *string, props bool) ([]iotclient.ArduinoThing, error) + DashboardCreate(dashboard *iotclient.Dashboardv2) (*iotclient.ArduinoDashboardv2, error) DashboardShow(id string) (*iotclient.ArduinoDashboardv2, error) DashboardDelete(id string) error DashboardList() ([]iotclient.ArduinoDashboardv2, error) @@ -205,6 +206,15 @@ func (cl *client) ThingList(ids []string, device *string, props bool) ([]iotclie return things, nil } +// DashboardCreate adds a new dashboard on Arduino IoT Cloud. +func (cl *client) DashboardCreate(dashboard *iotclient.Dashboardv2) (*iotclient.ArduinoDashboardv2, error) { + newDashboard, _, err := cl.api.DashboardsV2Api.DashboardsV2Create(cl.ctx, *dashboard) + if err != nil { + return nil, fmt.Errorf("%s: %w", "adding new dashboard", errorDetail(err)) + } + return &newDashboard, nil +} + // DashboardShow allows to retrieve a specific dashboard, given its id, // from Arduino IoT Cloud. func (cl *client) DashboardShow(id string) (*iotclient.ArduinoDashboardv2, error) { diff --git a/internal/template/dashboard.go b/internal/template/dashboard.go new file mode 100644 index 00000000..cc238390 --- /dev/null +++ b/internal/template/dashboard.go @@ -0,0 +1,53 @@ +// This file is part of arduino-cloud-cli. +// +// Copyright (C) 2021 ARDUINO SA (http://www.arduino.cc/) +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published +// by the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package template + +import "encoding/json" + +type dashboardHelp struct { + Name string `json:"name,omitempty" yaml:"name,omitempty"` + Widgets []widgetHelp `json:"widgets,omitempty" yaml:"widgets,omitempty"` +} + +type widgetHelp struct { + Height int64 `json:"height" yaml:"height"` + HeightMobile int64 `json:"height_mobile,omitempty" yaml:"height_mobile,omitempty"` + Id string `json:"id" yaml:"id"` + Name string `json:"name,omitempty" yaml:"name,omitempty"` + Options map[string]interface{} `json:"options" yaml:"options"` + WidgetType string `json:"type" yaml:"type"` + Variables []variableHelp `json:"variables,omitempty" yaml:"variables,omitempty"` + Width int64 `json:"width" yaml:"width"` + WidthMobile int64 `json:"width_mobile,omitempty" yaml:"width_mobile,omitempty"` + X int64 `json:"x" yaml:"x"` + XMobile int64 `json:"x_mobile,omitempty" yaml:"x_mobile,omitempty"` + Y int64 `json:"y" yaml:"y"` + YMobile int64 `json:"y_mobile,omitempty" yaml:"y_mobile,omitempty"` +} + +type variableHelp struct { + ThingID string `json:"thing_id" yaml:"thing_id"` + VariableName string `json:"variable_id" yaml:"variable_id"` + VariableID string +} + +func (v *variableHelp) MarshalJSON() ([]byte, error) { + // Jsonize as a list of strings (variable uuids) + // in order to uniform to the other dashboard declaration (of iotclient) + return json.Marshal(v.VariableID) +} diff --git a/internal/template/extract.go b/internal/template/extract.go index 0da92c59..5ec6b02e 100644 --- a/internal/template/extract.go +++ b/internal/template/extract.go @@ -88,7 +88,6 @@ func FromDashboard(dashboard *iotclient.ArduinoDashboardv2) map[string]interface if len(w.Options) > 0 { widget["options"] = w.Options } - widgets = append(widgets, widget) } if len(widgets) > 0 { diff --git a/internal/template/load.go b/internal/template/load.go index b6a27df4..963d1db0 100644 --- a/internal/template/load.go +++ b/internal/template/load.go @@ -25,39 +25,47 @@ import ( "os" iotclient "github.com/arduino/iot-client-go" + "github.com/gofrs/uuid" "gopkg.in/yaml.v3" ) -// loadTemplate loads a template file and puts it into a generic template -// of type map[string]interface{}. -// The input template should be in json or yaml format. -func loadTemplate(file string) (map[string]interface{}, error) { +var ( + widgetOptWhitelist = map[string]struct{}{ + "showLabels": {}, + "min": {}, + "max": {}, + } +) + +// loadTemplate loads a template file and unmarshals it into whatever +// is pointed to by the template parameter. Note that template parameter should be a pointer. +// The input template file should be in json or yaml format. +func loadTemplate(file string, template interface{}) error { templateFile, err := os.Open(file) if err != nil { - return nil, err + return err } defer templateFile.Close() templateBytes, err := ioutil.ReadAll(templateFile) if err != nil { - return nil, err + return err } - template := make(map[string]interface{}) - // Extract template trying all the supported formats: json and yaml - if err = json.Unmarshal([]byte(templateBytes), &template); err != nil { - if err = yaml.Unmarshal([]byte(templateBytes), &template); err != nil { - return nil, errors.New("reading template file: template format is not valid") + if err = json.Unmarshal([]byte(templateBytes), template); err != nil { + if err = yaml.Unmarshal([]byte(templateBytes), template); err != nil { + return errors.New("reading template file: template format is not valid") } } - return template, nil + return nil } // LoadThing loads a thing from a thing template file. func LoadThing(file string) (*iotclient.Thing, error) { - template, err := loadTemplate(file) + var template map[string]interface{} + err := loadTemplate(file, &template) if err != nil { return nil, err } @@ -82,3 +90,62 @@ func LoadThing(file string) (*iotclient.Thing, error) { return thing, nil } + +// LoadDashboard loads a dashboard from a dashboard template file. +// It applies the thing overrides specified by the override parameter. +func LoadDashboard(file string, override map[string]string) (*iotclient.Dashboardv2, error) { + template := dashboardHelp{} + err := loadTemplate(file, &template) + if err != nil { + return nil, err + } + + // Adapt the template to the dashboard struct + for i, widget := range template.Widgets { + // Generate and set a uuid for each widget + id, err := uuid.NewV4() + if err != nil { + return nil, fmt.Errorf("cannot create a uuid for new widget: %w", err) + } + widget.Id = id.String() + filterWidgetOptions(widget.Options) + // Even if widget has no options, options field should exist + if widget.Options == nil { + widget.Options = make(map[string]interface{}) + } + // Set the correct variable id, given the thing id and the variable name + for j, variable := range widget.Variables { + // Check if thing name should be overridden + if id, ok := override[variable.ThingID]; ok { + variable.ThingID = id + } + variable.VariableID, err = vargetter.getVariableID(variable.ThingID, variable.VariableName) + if err != nil { + return nil, fmt.Errorf("thing with id %s doesn't have variable with name %s : %w", variable.ThingID, variable.VariableName, err) + } + widget.Variables[j] = variable + } + template.Widgets[i] = widget + } + + // Convert template into dashboard structure exploiting json marshalling/unmarshalling + dashboard := &iotclient.Dashboardv2{} + t, err := json.Marshal(template) + if err != nil { + return nil, fmt.Errorf("%s: %w", "extracting template", err) + } + err = json.Unmarshal(t, &dashboard) + if err != nil { + return nil, fmt.Errorf("%s: %w", "creating dashboard structure from template", err) + } + + return dashboard, nil +} + +func filterWidgetOptions(opts map[string]interface{}) { + for opt := range opts { + if _, ok := widgetOptWhitelist[opt]; !ok { + delete(opts, opt) + } + } +} diff --git a/internal/template/vargetter.go b/internal/template/vargetter.go new file mode 100644 index 00000000..917c24aa --- /dev/null +++ b/internal/template/vargetter.go @@ -0,0 +1,62 @@ +// This file is part of arduino-cloud-cli. +// +// Copyright (C) 2021 ARDUINO SA (http://www.arduino.cc/) +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published +// by the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package template + +import ( + "errors" + + "github.com/arduino/arduino-cloud-cli/internal/config" + "github.com/arduino/arduino-cloud-cli/internal/iot" +) + +// HACK: this global variable is used to mock getVariableID function during tests. +// This method is temporarily. +var vargetter = varGetter{ + getVariableID: getVariableID, +} + +type varGetter struct { + getVariableID func(thingID string, variableName string) (string, error) +} + +// inefficient: creates a new iot client for each variable name +// solutions: pass the client from the extern. this solves also test problems +// instantiate the client as a state of vargetter +func getVariableID(thingID string, variableName string) (string, error) { + conf, err := config.Retrieve() + if err != nil { + return "", err + } + iotClient, err := iot.NewClient(conf.Client, conf.Secret) + if err != nil { + return "", err + } + + thing, err := iotClient.ThingShow(thingID) + if err != nil { + return "", err + } + + for _, v := range thing.Properties { + if v.Name == variableName { + return v.Id, nil + } + } + + return "", errors.New("not found") +} From f88dfa22b9f96c354485ea3e1e2275703b77b228 Mon Sep 17 00:00:00 2001 From: Paolo Calao Date: Fri, 8 Oct 2021 19:41:44 +0200 Subject: [PATCH 02/20] Add tests --- internal/template/load_test.go | 219 ++++++++++++++++++ .../template/testdata/dashboard-detailed.yaml | 14 ++ .../testdata/dashboard-no-options.yaml | 12 + .../testdata/dashboard-two-widgets.yaml | 26 +++ .../testdata/dashboard-with-variable.yaml | 17 ++ .../testdata/dashboard-wrong-options.yaml | 18 ++ .../testdata/home-security-dashboard.json | 29 +++ .../testdata/home-security-dashboard.yaml | 15 ++ internal/template/testdata/prova.yaml | 39 ++++ 9 files changed, 389 insertions(+) create mode 100644 internal/template/load_test.go create mode 100644 internal/template/testdata/dashboard-detailed.yaml create mode 100644 internal/template/testdata/dashboard-no-options.yaml create mode 100644 internal/template/testdata/dashboard-two-widgets.yaml create mode 100644 internal/template/testdata/dashboard-with-variable.yaml create mode 100644 internal/template/testdata/dashboard-wrong-options.yaml create mode 100644 internal/template/testdata/home-security-dashboard.json create mode 100644 internal/template/testdata/home-security-dashboard.yaml create mode 100644 internal/template/testdata/prova.yaml diff --git a/internal/template/load_test.go b/internal/template/load_test.go new file mode 100644 index 00000000..1d0f6fd7 --- /dev/null +++ b/internal/template/load_test.go @@ -0,0 +1,219 @@ +// This file is part of arduino-cloud-cli. +// +// Copyright (C) 2021 ARDUINO SA (http://www.arduino.cc/) +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published +// by the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package template + +import ( + "testing" + + iotclient "github.com/arduino/iot-client-go" + "github.com/google/go-cmp/cmp" +) + +const ( + uuidv4Length = 36 +) + +var ( + dashboardTemplate = map[string]interface{}{ + "id": "home-security-alarm-dashboard", + "name": "Home Security Alarm", + "widgets": []interface{}{ + map[string]interface{}{ + "type": "Messenger", "name": "message_update", + "variables": []interface{}{map[string]interface{}{"thing_id": "home-security-alarm", "variable_id": "message_update"}}, + }, + map[string]interface{}{ + "type": "Switch", "name": "light_alarm", + "variables": []interface{}{map[string]interface{}{"thing_id": "home-security-alarm", "variable_id": "light_alarm"}}, + "options": map[string]interface{}{"showLabels": true}, + }, + }, + } + + dashboardDetailed = &iotclient.Dashboardv2{ + Name: "dashboard", + Widgets: []iotclient.Widget{ + {Name: "Switch-name", Height: 1, HeightMobile: 2, Width: 3, WidthMobile: 4, + X: 5, XMobile: 6, Y: 7, YMobile: 8, Options: map[string]interface{}{"showLabels": true}, + Type: "Switch", + }, + }, + } + + dashboardNoOptions = &iotclient.Dashboardv2{ + Name: "dashboard-no-options", + Widgets: []iotclient.Widget{ + {Name: "Switch-name", Height: 1, HeightMobile: 2, Width: 3, WidthMobile: 4, + X: 5, XMobile: 6, Y: 7, YMobile: 8, Options: map[string]interface{}{}, + Type: "Switch", + }, + }, + } + + dashboardWithVariable = &iotclient.Dashboardv2{ + Name: "dashboard-with-variable", + Widgets: []iotclient.Widget{ + {Name: "Switch-name", Height: 1, HeightMobile: 2, Width: 3, WidthMobile: 4, + X: 5, XMobile: 6, Y: 7, YMobile: 8, Options: map[string]interface{}{"showLabels": true}, + // in this test, the variable id is a concatenation of thing_id and variable_id + // this depends on the mocked function getVariableID + Type: "Switch", Variables: []string{"thing-variable"}, + }, + }, + } + + dashboardVariableOverride = &iotclient.Dashboardv2{ + Name: "dashboard-with-variable", + Widgets: []iotclient.Widget{ + {Name: "Switch-name", Height: 1, HeightMobile: 2, Width: 3, WidthMobile: 4, + X: 5, XMobile: 6, Y: 7, YMobile: 8, Options: map[string]interface{}{"showLabels": true}, + // in this test, the variable id is a concatenation of thing_id and variable_id + // this depends on the mocked function getVariableID + Type: "Switch", Variables: []string{"overridden-variable"}, + }, + }, + } + + dashboardTwoWidgets = &iotclient.Dashboardv2{ + Name: "dashboard-two-widgets", + Widgets: []iotclient.Widget{ + {Name: "blink_speed", Height: 7, Width: 8, + X: 7, Y: 5, Options: map[string]interface{}{"min": float64(0), "max": float64(5000)}, + Type: "Slider", Variables: []string{"remote-controlled-lights-blink_speed"}, + }, + {Name: "relay_2", Height: 5, Width: 5, + X: 5, Y: 0, Options: map[string]interface{}{"showLabels": true}, + Type: "Switch", Variables: []string{"remote-controlled-lights-relay_2"}, + }, + }, + } +) + +func TestLoadTemplate(t *testing.T) { + tests := []struct { + name string + file string + override map[string]string + want map[string]interface{} + }{ + + { + name: "yaml dashboard template", + file: "testdata/home-security-dashboard.yaml", + want: dashboardTemplate, + }, + + { + name: "json dashboard template", + file: "testdata/home-security-dashboard.json", + want: dashboardTemplate, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var got map[string]interface{} + err := loadTemplate(tt.file, &got) + if err != nil { + t.Errorf("%v", err) + } + if !cmp.Equal(got, tt.want) { + t.Errorf("Wrong template received, got=\n%s", cmp.Diff(tt.want, got)) + } + }) + } +} + +func TestLoadDashboard(t *testing.T) { + tests := []struct { + name string + file string + override map[string]string + want *iotclient.Dashboardv2 + }{ + { + name: "dashboard detailed", + file: "testdata/dashboard-detailed.yaml", + override: nil, + want: dashboardDetailed, + }, + + { + name: "dashboard with wrong options to be filtered out", + file: "testdata/dashboard-wrong-options.yaml", + override: nil, + want: dashboardDetailed, + }, + + { + name: "dashboard without options, should have a not nil map", + file: "testdata/dashboard-no-options.yaml", + override: nil, + want: dashboardNoOptions, + }, + + { + name: "dashboard with variable, mocked variable id is concatenation of thing_id and variable_id", + file: "testdata/dashboard-with-variable.yaml", + override: nil, + want: dashboardWithVariable, + }, + + { + name: "dashboard with variable, thing is overridden", + file: "testdata/dashboard-with-variable.yaml", + override: map[string]string{"thing": "overridden"}, + want: dashboardVariableOverride, + }, + + { + name: "dashboard with two widgets", + file: "testdata/dashboard-two-widgets.yaml", + override: nil, + want: dashboardTwoWidgets, + }, + } + + vargetter.getVariableID = mockGetVariableID + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := LoadDashboard(tt.file, tt.override) + if err != nil { + t.Errorf("%v", err) + } + + for i := range got.Widgets { + // check widget id generation + id := got.Widgets[i].Id + if len(id) != uuidv4Length { + t.Errorf("Widget ID is wrong: = %s", id) + } + got.Widgets[i].Id = "" + } + + if !cmp.Equal(got, tt.want) { + t.Errorf("Wrong template received, got=\n%s", cmp.Diff(tt.want, got)) + } + }) + } +} + +func mockGetVariableID(thingID string, variableName string) (string, error) { + return thingID + "-" + variableName, nil +} diff --git a/internal/template/testdata/dashboard-detailed.yaml b/internal/template/testdata/dashboard-detailed.yaml new file mode 100644 index 00000000..077122f3 --- /dev/null +++ b/internal/template/testdata/dashboard-detailed.yaml @@ -0,0 +1,14 @@ +name: dashboard +widgets: + - height: 1 + height_mobile: 2 + name: Switch-name + options: + showLabels: true + type: Switch + width: 3 + width_mobile: 4 + x: 5 + x_mobile: 6 + "y": 7 + y_mobile: 8 diff --git a/internal/template/testdata/dashboard-no-options.yaml b/internal/template/testdata/dashboard-no-options.yaml new file mode 100644 index 00000000..39d2f283 --- /dev/null +++ b/internal/template/testdata/dashboard-no-options.yaml @@ -0,0 +1,12 @@ +name: dashboard-no-options +widgets: + - height: 1 + height_mobile: 2 + name: Switch-name + type: Switch + width: 3 + width_mobile: 4 + x: 5 + x_mobile: 6 + "y": 7 + y_mobile: 8 diff --git a/internal/template/testdata/dashboard-two-widgets.yaml b/internal/template/testdata/dashboard-two-widgets.yaml new file mode 100644 index 00000000..6b6027f5 --- /dev/null +++ b/internal/template/testdata/dashboard-two-widgets.yaml @@ -0,0 +1,26 @@ +id: dashboard-two-widgets +name: dashboard-two-widgets +widgets: + - type: Slider + name: blink_speed + width: 8 + height: 7 + x: 7 + y: 5 + variables: + - thing_id: remote-controlled-lights + variable_id: blink_speed + options: + min: 0 + max: 5000 + - type: Switch + name: relay_2 + width: 5 + height: 5 + x: 5 + y: 0 + variables: + - thing_id: remote-controlled-lights + variable_id: relay_2 + options: + showLabels: true \ No newline at end of file diff --git a/internal/template/testdata/dashboard-with-variable.yaml b/internal/template/testdata/dashboard-with-variable.yaml new file mode 100644 index 00000000..d75a886e --- /dev/null +++ b/internal/template/testdata/dashboard-with-variable.yaml @@ -0,0 +1,17 @@ +name: dashboard-with-variable +widgets: + - height: 1 + height_mobile: 2 + name: Switch-name + options: + showLabels: true + variables: + - thing_id: thing + variable_id: variable + type: Switch + width: 3 + width_mobile: 4 + x: 5 + x_mobile: 6 + "y": 7 + y_mobile: 8 diff --git a/internal/template/testdata/dashboard-wrong-options.yaml b/internal/template/testdata/dashboard-wrong-options.yaml new file mode 100644 index 00000000..64eda7cf --- /dev/null +++ b/internal/template/testdata/dashboard-wrong-options.yaml @@ -0,0 +1,18 @@ +name: dashboard +widgets: + - height: 1 + height_mobile: 2 + name: Switch-name + options: + showLabels: true + mode: false + percentage: 100 + step: true + interpolation: true + type: Switch + width: 3 + width_mobile: 4 + x: 5 + x_mobile: 6 + "y": 7 + y_mobile: 8 diff --git a/internal/template/testdata/home-security-dashboard.json b/internal/template/testdata/home-security-dashboard.json new file mode 100644 index 00000000..7df2fcbe --- /dev/null +++ b/internal/template/testdata/home-security-dashboard.json @@ -0,0 +1,29 @@ +{ + "id": "home-security-alarm-dashboard", + "name": "Home Security Alarm", + "widgets": [ + { + "type": "Messenger", + "name": "message_update", + "variables": [ + { + "thing_id": "home-security-alarm", + "variable_id": "message_update" + } + ] + }, + { + "type": "Switch", + "name": "light_alarm", + "variables": [ + { + "thing_id": "home-security-alarm", + "variable_id": "light_alarm" + } + ], + "options": { + "showLabels": true + } + } + ] +} \ No newline at end of file diff --git a/internal/template/testdata/home-security-dashboard.yaml b/internal/template/testdata/home-security-dashboard.yaml new file mode 100644 index 00000000..ba38e851 --- /dev/null +++ b/internal/template/testdata/home-security-dashboard.yaml @@ -0,0 +1,15 @@ +id: home-security-alarm-dashboard +name: Home Security Alarm +widgets: + - type: Messenger + name: message_update + variables: + - thing_id: home-security-alarm + variable_id: message_update + - type: Switch + name: light_alarm + variables: + - thing_id: home-security-alarm + variable_id: light_alarm + options: + showLabels: true \ No newline at end of file diff --git a/internal/template/testdata/prova.yaml b/internal/template/testdata/prova.yaml new file mode 100644 index 00000000..310a4d95 --- /dev/null +++ b/internal/template/testdata/prova.yaml @@ -0,0 +1,39 @@ +name: prova +widgets: + - height: 4 + height_mobile: 4 + name: Switch + options: + icon: switch + readOnly: false + section: section-1 + showLabels: true + showThing: false + thingId: null + type: Switch + width: 4 + width_mobile: 8 + x: 0 + x_mobile: 0 + "y": 0 + y_mobile: 0 + - height: 5 + height_mobile: 5 + name: Slider + options: + icon: slider + max: 50 + min: 0 + readOnly: false + section: section-1 + thingId: 453fc065-51a9-40d9-95b4-4dbbf1cfc0cc + type: Slider + variables: + - thing_id: CrocodileDaEliminare + variable_id: prova + width: 6 + width_mobile: 8 + x: 4 + x_mobile: 0 + "y": 0 + y_mobile: 1 From 6ed7285b5c088198fcac6e1be0e470a8cad4984e Mon Sep 17 00:00:00 2001 From: Paolo Calao Date: Mon, 11 Oct 2021 11:45:17 +0200 Subject: [PATCH 03/20] Inject iotClient as dependency --- command/dashboard/create.go | 2 +- internal/template/load.go | 23 ++- internal/template/load_test.go | 53 +++-- internal/template/mocks/Client.go | 315 ++++++++++++++++++++++++++++++ internal/template/vargetter.go | 62 ------ 5 files changed, 371 insertions(+), 84 deletions(-) create mode 100644 internal/template/mocks/Client.go delete mode 100644 internal/template/vargetter.go diff --git a/command/dashboard/create.go b/command/dashboard/create.go index b0c0ab56..d92c5949 100644 --- a/command/dashboard/create.go +++ b/command/dashboard/create.go @@ -43,7 +43,7 @@ func Create(params *CreateParams) (*DashboardInfo, error) { return nil, err } - dashboard, err := template.LoadDashboard(params.Template, params.Override) + dashboard, err := template.LoadDashboard(params.Template, params.Override, iotClient) if err != nil { return nil, err } diff --git a/internal/template/load.go b/internal/template/load.go index 963d1db0..b2a4225e 100644 --- a/internal/template/load.go +++ b/internal/template/load.go @@ -24,6 +24,7 @@ import ( "io/ioutil" "os" + "github.com/arduino/arduino-cloud-cli/internal/iot" iotclient "github.com/arduino/iot-client-go" "github.com/gofrs/uuid" "gopkg.in/yaml.v3" @@ -93,7 +94,8 @@ func LoadThing(file string) (*iotclient.Thing, error) { // LoadDashboard loads a dashboard from a dashboard template file. // It applies the thing overrides specified by the override parameter. -func LoadDashboard(file string, override map[string]string) (*iotclient.Dashboardv2, error) { +// It requires an iot.Client parameter to retrieve the actual variable id. +func LoadDashboard(file string, override map[string]string, iotClient iot.Client) (*iotclient.Dashboardv2, error) { template := dashboardHelp{} err := loadTemplate(file, &template) if err != nil { @@ -119,7 +121,7 @@ func LoadDashboard(file string, override map[string]string) (*iotclient.Dashboar if id, ok := override[variable.ThingID]; ok { variable.ThingID = id } - variable.VariableID, err = vargetter.getVariableID(variable.ThingID, variable.VariableName) + variable.VariableID, err = getVariableID(variable.ThingID, variable.VariableName, iotClient) if err != nil { return nil, fmt.Errorf("thing with id %s doesn't have variable with name %s : %w", variable.ThingID, variable.VariableName, err) } @@ -149,3 +151,20 @@ func filterWidgetOptions(opts map[string]interface{}) { } } } + +// getVariableID returns the id of a variable, given its thing id and its variable name. +// If the variable is not found, an error is returned. +func getVariableID(thingID string, variableName string, iotClient iot.Client) (string, error) { + thing, err := iotClient.ThingShow(thingID) + if err != nil { + return "", err + } + + for _, v := range thing.Properties { + if v.Name == variableName { + return v.Id, nil + } + } + + return "", errors.New("not found") +} diff --git a/internal/template/load_test.go b/internal/template/load_test.go index 1d0f6fd7..f592b216 100644 --- a/internal/template/load_test.go +++ b/internal/template/load_test.go @@ -20,8 +20,10 @@ package template import ( "testing" + "github.com/arduino/arduino-cloud-cli/internal/template/mocks" iotclient "github.com/arduino/iot-client-go" "github.com/google/go-cmp/cmp" + "github.com/stretchr/testify/mock" ) const ( @@ -69,10 +71,9 @@ var ( Name: "dashboard-with-variable", Widgets: []iotclient.Widget{ {Name: "Switch-name", Height: 1, HeightMobile: 2, Width: 3, WidthMobile: 4, - X: 5, XMobile: 6, Y: 7, YMobile: 8, Options: map[string]interface{}{"showLabels": true}, - // in this test, the variable id is a concatenation of thing_id and variable_id - // this depends on the mocked function getVariableID - Type: "Switch", Variables: []string{"thing-variable"}, + X: 5, XMobile: 6, Y: 7, YMobile: 8, Options: map[string]interface{}{"showLabels": true}, Type: "Switch", + // variable id is set equal to the thing id by mockThingShow, in order to verify the thing override + Variables: []string{"thing"}, }, }, } @@ -81,24 +82,27 @@ var ( Name: "dashboard-with-variable", Widgets: []iotclient.Widget{ {Name: "Switch-name", Height: 1, HeightMobile: 2, Width: 3, WidthMobile: 4, - X: 5, XMobile: 6, Y: 7, YMobile: 8, Options: map[string]interface{}{"showLabels": true}, - // in this test, the variable id is a concatenation of thing_id and variable_id - // this depends on the mocked function getVariableID - Type: "Switch", Variables: []string{"overridden-variable"}, + X: 5, XMobile: 6, Y: 7, YMobile: 8, Options: map[string]interface{}{"showLabels": true}, Type: "Switch", + // variable id is set equal to the thing id by mockThingShow, in order to verify the thing override + Variables: []string{"overridden"}, }, }, } dashboardTwoWidgets = &iotclient.Dashboardv2{ Name: "dashboard-two-widgets", + // in this test, the variable id is a concatenation of thing_id and variable_id + // this depends on the mocked function getVariableID Widgets: []iotclient.Widget{ {Name: "blink_speed", Height: 7, Width: 8, - X: 7, Y: 5, Options: map[string]interface{}{"min": float64(0), "max": float64(5000)}, - Type: "Slider", Variables: []string{"remote-controlled-lights-blink_speed"}, + X: 7, Y: 5, Options: map[string]interface{}{"min": float64(0), "max": float64(5000)}, Type: "Slider", + // variable id is set equal to the thing id by mockThingShow, in order to verify the thing override + Variables: []string{"remote-controlled-lights"}, }, {Name: "relay_2", Height: 5, Width: 5, - X: 5, Y: 0, Options: map[string]interface{}{"showLabels": true}, - Type: "Switch", Variables: []string{"remote-controlled-lights-relay_2"}, + X: 5, Y: 0, Options: map[string]interface{}{"showLabels": true}, Type: "Switch", + // variable id is set equal to the thing id by mockThingShow, in order to verify the thing override + Variables: []string{"remote-controlled-lights"}, }, }, } @@ -140,6 +144,23 @@ func TestLoadTemplate(t *testing.T) { } func TestLoadDashboard(t *testing.T) { + + mockClient := &mocks.Client{} + mockThingShow := func(thingID string) *iotclient.ArduinoThing { + thing := &iotclient.ArduinoThing{ + Properties: []iotclient.ArduinoProperty{ + // variable id is set equal to the thing id in order to verify the thing override + // dashboard-with-variable variable + {Id: thingID, Name: "variable"}, + // dashboard-two-widgets variables + {Id: thingID, Name: "relay_2"}, + {Id: thingID, Name: "blink_speed"}, + }, + } + return thing + } + mockClient.On("ThingShow", mock.AnythingOfType("string")).Return(mockThingShow, nil) + tests := []struct { name string file string @@ -189,11 +210,9 @@ func TestLoadDashboard(t *testing.T) { }, } - vargetter.getVariableID = mockGetVariableID - for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got, err := LoadDashboard(tt.file, tt.override) + got, err := LoadDashboard(tt.file, tt.override, mockClient) if err != nil { t.Errorf("%v", err) } @@ -213,7 +232,3 @@ func TestLoadDashboard(t *testing.T) { }) } } - -func mockGetVariableID(thingID string, variableName string) (string, error) { - return thingID + "-" + variableName, nil -} diff --git a/internal/template/mocks/Client.go b/internal/template/mocks/Client.go new file mode 100644 index 00000000..09baa3a9 --- /dev/null +++ b/internal/template/mocks/Client.go @@ -0,0 +1,315 @@ +// Code generated by mockery v0.0.0-dev. DO NOT EDIT. + +package mocks + +import ( + iot "github.com/arduino/iot-client-go" + mock "github.com/stretchr/testify/mock" + + os "os" +) + +// Client is an autogenerated mock type for the Client type +type Client struct { + mock.Mock +} + +// CertificateCreate provides a mock function with given fields: id, csr +func (_m *Client) CertificateCreate(id string, csr string) (*iot.ArduinoCompressedv2, error) { + ret := _m.Called(id, csr) + + var r0 *iot.ArduinoCompressedv2 + if rf, ok := ret.Get(0).(func(string, string) *iot.ArduinoCompressedv2); ok { + r0 = rf(id, csr) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*iot.ArduinoCompressedv2) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(string, string) error); ok { + r1 = rf(id, csr) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// DashboardCreate provides a mock function with given fields: dashboard +func (_m *Client) DashboardCreate(dashboard *iot.Dashboardv2) (*iot.ArduinoDashboardv2, error) { + ret := _m.Called(dashboard) + + var r0 *iot.ArduinoDashboardv2 + if rf, ok := ret.Get(0).(func(*iot.Dashboardv2) *iot.ArduinoDashboardv2); ok { + r0 = rf(dashboard) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*iot.ArduinoDashboardv2) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(*iot.Dashboardv2) error); ok { + r1 = rf(dashboard) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// DashboardDelete provides a mock function with given fields: id +func (_m *Client) DashboardDelete(id string) error { + ret := _m.Called(id) + + var r0 error + if rf, ok := ret.Get(0).(func(string) error); ok { + r0 = rf(id) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// DashboardList provides a mock function with given fields: +func (_m *Client) DashboardList() ([]iot.ArduinoDashboardv2, error) { + ret := _m.Called() + + var r0 []iot.ArduinoDashboardv2 + if rf, ok := ret.Get(0).(func() []iot.ArduinoDashboardv2); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]iot.ArduinoDashboardv2) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func() error); ok { + r1 = rf() + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// DashboardShow provides a mock function with given fields: id +func (_m *Client) DashboardShow(id string) (*iot.ArduinoDashboardv2, error) { + ret := _m.Called(id) + + var r0 *iot.ArduinoDashboardv2 + if rf, ok := ret.Get(0).(func(string) *iot.ArduinoDashboardv2); ok { + r0 = rf(id) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*iot.ArduinoDashboardv2) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(string) error); ok { + r1 = rf(id) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// DeviceCreate provides a mock function with given fields: fqbn, name, serial, devType +func (_m *Client) DeviceCreate(fqbn string, name string, serial string, devType string) (*iot.ArduinoDevicev2, error) { + ret := _m.Called(fqbn, name, serial, devType) + + var r0 *iot.ArduinoDevicev2 + if rf, ok := ret.Get(0).(func(string, string, string, string) *iot.ArduinoDevicev2); ok { + r0 = rf(fqbn, name, serial, devType) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*iot.ArduinoDevicev2) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(string, string, string, string) error); ok { + r1 = rf(fqbn, name, serial, devType) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// DeviceDelete provides a mock function with given fields: id +func (_m *Client) DeviceDelete(id string) error { + ret := _m.Called(id) + + var r0 error + if rf, ok := ret.Get(0).(func(string) error); ok { + r0 = rf(id) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// DeviceList provides a mock function with given fields: +func (_m *Client) DeviceList() ([]iot.ArduinoDevicev2, error) { + ret := _m.Called() + + var r0 []iot.ArduinoDevicev2 + if rf, ok := ret.Get(0).(func() []iot.ArduinoDevicev2); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]iot.ArduinoDevicev2) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func() error); ok { + r1 = rf() + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// DeviceOTA provides a mock function with given fields: id, file, expireMins +func (_m *Client) DeviceOTA(id string, file *os.File, expireMins int) error { + ret := _m.Called(id, file, expireMins) + + var r0 error + if rf, ok := ret.Get(0).(func(string, *os.File, int) error); ok { + r0 = rf(id, file, expireMins) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// DeviceShow provides a mock function with given fields: id +func (_m *Client) DeviceShow(id string) (*iot.ArduinoDevicev2, error) { + ret := _m.Called(id) + + var r0 *iot.ArduinoDevicev2 + if rf, ok := ret.Get(0).(func(string) *iot.ArduinoDevicev2); ok { + r0 = rf(id) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*iot.ArduinoDevicev2) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(string) error); ok { + r1 = rf(id) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// ThingCreate provides a mock function with given fields: thing, force +func (_m *Client) ThingCreate(thing *iot.Thing, force bool) (*iot.ArduinoThing, error) { + ret := _m.Called(thing, force) + + var r0 *iot.ArduinoThing + if rf, ok := ret.Get(0).(func(*iot.Thing, bool) *iot.ArduinoThing); ok { + r0 = rf(thing, force) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*iot.ArduinoThing) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(*iot.Thing, bool) error); ok { + r1 = rf(thing, force) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// ThingDelete provides a mock function with given fields: id +func (_m *Client) ThingDelete(id string) error { + ret := _m.Called(id) + + var r0 error + if rf, ok := ret.Get(0).(func(string) error); ok { + r0 = rf(id) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// ThingList provides a mock function with given fields: ids, device, props +func (_m *Client) ThingList(ids []string, device *string, props bool) ([]iot.ArduinoThing, error) { + ret := _m.Called(ids, device, props) + + var r0 []iot.ArduinoThing + if rf, ok := ret.Get(0).(func([]string, *string, bool) []iot.ArduinoThing); ok { + r0 = rf(ids, device, props) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]iot.ArduinoThing) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func([]string, *string, bool) error); ok { + r1 = rf(ids, device, props) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// ThingShow provides a mock function with given fields: id +func (_m *Client) ThingShow(id string) (*iot.ArduinoThing, error) { + ret := _m.Called(id) + + var r0 *iot.ArduinoThing + if rf, ok := ret.Get(0).(func(string) *iot.ArduinoThing); ok { + r0 = rf(id) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*iot.ArduinoThing) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(string) error); ok { + r1 = rf(id) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// ThingUpdate provides a mock function with given fields: id, thing, force +func (_m *Client) ThingUpdate(id string, thing *iot.Thing, force bool) error { + ret := _m.Called(id, thing, force) + + var r0 error + if rf, ok := ret.Get(0).(func(string, *iot.Thing, bool) error); ok { + r0 = rf(id, thing, force) + } else { + r0 = ret.Error(0) + } + + return r0 +} diff --git a/internal/template/vargetter.go b/internal/template/vargetter.go deleted file mode 100644 index 917c24aa..00000000 --- a/internal/template/vargetter.go +++ /dev/null @@ -1,62 +0,0 @@ -// This file is part of arduino-cloud-cli. -// -// Copyright (C) 2021 ARDUINO SA (http://www.arduino.cc/) -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as published -// by the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . - -package template - -import ( - "errors" - - "github.com/arduino/arduino-cloud-cli/internal/config" - "github.com/arduino/arduino-cloud-cli/internal/iot" -) - -// HACK: this global variable is used to mock getVariableID function during tests. -// This method is temporarily. -var vargetter = varGetter{ - getVariableID: getVariableID, -} - -type varGetter struct { - getVariableID func(thingID string, variableName string) (string, error) -} - -// inefficient: creates a new iot client for each variable name -// solutions: pass the client from the extern. this solves also test problems -// instantiate the client as a state of vargetter -func getVariableID(thingID string, variableName string) (string, error) { - conf, err := config.Retrieve() - if err != nil { - return "", err - } - iotClient, err := iot.NewClient(conf.Client, conf.Secret) - if err != nil { - return "", err - } - - thing, err := iotClient.ThingShow(thingID) - if err != nil { - return "", err - } - - for _, v := range thing.Properties { - if v.Name == variableName { - return v.Id, nil - } - } - - return "", errors.New("not found") -} From 0becfc85b40ca79723c46b844a66f79c32b6507a Mon Sep 17 00:00:00 2001 From: Paolo Calao Date: Mon, 11 Oct 2021 12:06:43 +0200 Subject: [PATCH 04/20] Update widget options filter --- internal/template/load.go | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/internal/template/load.go b/internal/template/load.go index b2a4225e..fd997597 100644 --- a/internal/template/load.go +++ b/internal/template/load.go @@ -32,9 +32,16 @@ import ( var ( widgetOptWhitelist = map[string]struct{}{ - "showLabels": {}, - "min": {}, - "max": {}, + "showThing": {}, + "frameless": {}, + "interpolation": {}, + "max": {}, + "min": {}, + "mode": {}, + "percentage": {}, + "showLabels": {}, + "step": {}, + "vertical": {}, } ) From b06323c654371d37176111d87df7e03714139912 Mon Sep 17 00:00:00 2001 From: Paolo Calao Date: Mon, 11 Oct 2021 12:07:15 +0200 Subject: [PATCH 05/20] Update go.mod --- go.mod | 4 ++-- go.sum | 1 - 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 6ab281e9..5fa4a83b 100644 --- a/go.mod +++ b/go.mod @@ -7,8 +7,8 @@ require ( github.com/arduino/arduino-cli v0.0.0-20210607095659-16f41352eac3 github.com/arduino/go-paths-helper v1.6.1 github.com/arduino/iot-client-go v1.3.4-0.20210930122852-04551f4cb061 - github.com/gofrs/uuid v4.0.0+incompatible // indirect - github.com/google/go-cmp v0.5.6 // indirect + github.com/gofrs/uuid v4.0.0+incompatible + github.com/google/go-cmp v0.5.6 github.com/howeyc/crc16 v0.0.0-20171223171357-2b2a61e366a6 github.com/juju/errors v0.0.0-20181118221551-089d3ea4e4d5 github.com/sirupsen/logrus v1.8.1 diff --git a/go.sum b/go.sum index 9d0693bb..6a61a403 100644 --- a/go.sum +++ b/go.sum @@ -179,7 +179,6 @@ github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ= github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= From b72e27bde515c14c3b618143ec90f1d84ff306e7 Mon Sep 17 00:00:00 2001 From: Paolo Calao Date: Mon, 11 Oct 2021 14:09:53 +0200 Subject: [PATCH 06/20] Fix wrong widget options in tests --- internal/template/testdata/dashboard-wrong-options.yaml | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/internal/template/testdata/dashboard-wrong-options.yaml b/internal/template/testdata/dashboard-wrong-options.yaml index 64eda7cf..222d5b01 100644 --- a/internal/template/testdata/dashboard-wrong-options.yaml +++ b/internal/template/testdata/dashboard-wrong-options.yaml @@ -5,10 +5,9 @@ widgets: name: Switch-name options: showLabels: true - mode: false - percentage: 100 - step: true - interpolation: true + name: name-wrong + icon: wrong + section: section-wrong type: Switch width: 3 width_mobile: 4 From 1d996a4d1b069584e1048de4baed85bb315227e4 Mon Sep 17 00:00:00 2001 From: Paolo Calao Date: Tue, 12 Oct 2021 16:52:12 +0200 Subject: [PATCH 07/20] Remove share details --- cli/dashboard/create.go | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/cli/dashboard/create.go b/cli/dashboard/create.go index 4a4cd527..211dca70 100644 --- a/cli/dashboard/create.go +++ b/cli/dashboard/create.go @@ -83,11 +83,9 @@ func (r createResult) Data() interface{} { func (r createResult) String() string { return fmt.Sprintf( - "name: %s\nid: %s\nshared_by: %s\nshared_with: %s\nupdated_at: %s\nwidgets: %s", + "name: %s\nid: %s\nupdated_at: %s\nwidgets: %s", r.dashboard.Name, r.dashboard.ID, - r.dashboard.SharedBy, - strings.Join(r.dashboard.SharedWith, ", "), r.dashboard.UpdatedAt, strings.Join(r.dashboard.Widgets, ", "), ) From 46092d31e905ebb9c0683d23f218da6500fda372 Mon Sep 17 00:00:00 2001 From: Paolo Calao Date: Tue, 12 Oct 2021 16:57:37 +0200 Subject: [PATCH 08/20] Remove duplicated filter --- internal/template/load.go | 23 ----------------------- 1 file changed, 23 deletions(-) diff --git a/internal/template/load.go b/internal/template/load.go index fd997597..6654aed9 100644 --- a/internal/template/load.go +++ b/internal/template/load.go @@ -30,21 +30,6 @@ import ( "gopkg.in/yaml.v3" ) -var ( - widgetOptWhitelist = map[string]struct{}{ - "showThing": {}, - "frameless": {}, - "interpolation": {}, - "max": {}, - "min": {}, - "mode": {}, - "percentage": {}, - "showLabels": {}, - "step": {}, - "vertical": {}, - } -) - // loadTemplate loads a template file and unmarshals it into whatever // is pointed to by the template parameter. Note that template parameter should be a pointer. // The input template file should be in json or yaml format. @@ -151,14 +136,6 @@ func LoadDashboard(file string, override map[string]string, iotClient iot.Client return dashboard, nil } -func filterWidgetOptions(opts map[string]interface{}) { - for opt := range opts { - if _, ok := widgetOptWhitelist[opt]; !ok { - delete(opts, opt) - } - } -} - // getVariableID returns the id of a variable, given its thing id and its variable name. // If the variable is not found, an error is returned. func getVariableID(thingID string, variableName string, iotClient iot.Client) (string, error) { From 0d26146266e7e6c95b429c33ef71edbe62771572 Mon Sep 17 00:00:00 2001 From: Paolo Calao Date: Tue, 12 Oct 2021 17:00:02 +0200 Subject: [PATCH 09/20] Move getVariableID in dashboard file --- internal/template/dashboard.go | 24 +++++++++++++++++++++++- internal/template/load.go | 17 ----------------- 2 files changed, 23 insertions(+), 18 deletions(-) diff --git a/internal/template/dashboard.go b/internal/template/dashboard.go index cc238390..89a1f371 100644 --- a/internal/template/dashboard.go +++ b/internal/template/dashboard.go @@ -17,7 +17,12 @@ package template -import "encoding/json" +import ( + "encoding/json" + "errors" + + "github.com/arduino/arduino-cloud-cli/internal/iot" +) type dashboardHelp struct { Name string `json:"name,omitempty" yaml:"name,omitempty"` @@ -51,3 +56,20 @@ func (v *variableHelp) MarshalJSON() ([]byte, error) { // in order to uniform to the other dashboard declaration (of iotclient) return json.Marshal(v.VariableID) } + +// getVariableID returns the id of a variable, given its thing id and its variable name. +// If the variable is not found, an error is returned. +func getVariableID(thingID string, variableName string, iotClient iot.Client) (string, error) { + thing, err := iotClient.ThingShow(thingID) + if err != nil { + return "", err + } + + for _, v := range thing.Properties { + if v.Name == variableName { + return v.Id, nil + } + } + + return "", errors.New("not found") +} diff --git a/internal/template/load.go b/internal/template/load.go index 6654aed9..e46466b8 100644 --- a/internal/template/load.go +++ b/internal/template/load.go @@ -135,20 +135,3 @@ func LoadDashboard(file string, override map[string]string, iotClient iot.Client return dashboard, nil } - -// getVariableID returns the id of a variable, given its thing id and its variable name. -// If the variable is not found, an error is returned. -func getVariableID(thingID string, variableName string, iotClient iot.Client) (string, error) { - thing, err := iotClient.ThingShow(thingID) - if err != nil { - return "", err - } - - for _, v := range thing.Properties { - if v.Name == variableName { - return v.Id, nil - } - } - - return "", errors.New("not found") -} From e58bfc96242e735a1f76b7812d746a7f7227c7b3 Mon Sep 17 00:00:00 2001 From: Paolo Calao Date: Tue, 12 Oct 2021 19:12:00 +0200 Subject: [PATCH 10/20] Rename dashboard template structs --- internal/template/dashboard.go | 14 +++++++------- internal/template/load.go | 2 +- internal/template/load_test.go | 6 +++--- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/internal/template/dashboard.go b/internal/template/dashboard.go index 89a1f371..69e4be3e 100644 --- a/internal/template/dashboard.go +++ b/internal/template/dashboard.go @@ -24,19 +24,19 @@ import ( "github.com/arduino/arduino-cloud-cli/internal/iot" ) -type dashboardHelp struct { - Name string `json:"name,omitempty" yaml:"name,omitempty"` - Widgets []widgetHelp `json:"widgets,omitempty" yaml:"widgets,omitempty"` +type dashboardTemplate struct { + Name string `json:"name,omitempty" yaml:"name,omitempty"` + Widgets []widgetTemplate `json:"widgets,omitempty" yaml:"widgets,omitempty"` } -type widgetHelp struct { +type widgetTemplate struct { Height int64 `json:"height" yaml:"height"` HeightMobile int64 `json:"height_mobile,omitempty" yaml:"height_mobile,omitempty"` Id string `json:"id" yaml:"id"` Name string `json:"name,omitempty" yaml:"name,omitempty"` Options map[string]interface{} `json:"options" yaml:"options"` WidgetType string `json:"type" yaml:"type"` - Variables []variableHelp `json:"variables,omitempty" yaml:"variables,omitempty"` + Variables []variableTemplate `json:"variables,omitempty" yaml:"variables,omitempty"` Width int64 `json:"width" yaml:"width"` WidthMobile int64 `json:"width_mobile,omitempty" yaml:"width_mobile,omitempty"` X int64 `json:"x" yaml:"x"` @@ -45,13 +45,13 @@ type widgetHelp struct { YMobile int64 `json:"y_mobile,omitempty" yaml:"y_mobile,omitempty"` } -type variableHelp struct { +type variableTemplate struct { ThingID string `json:"thing_id" yaml:"thing_id"` VariableName string `json:"variable_id" yaml:"variable_id"` VariableID string } -func (v *variableHelp) MarshalJSON() ([]byte, error) { +func (v *variableTemplate) MarshalJSON() ([]byte, error) { // Jsonize as a list of strings (variable uuids) // in order to uniform to the other dashboard declaration (of iotclient) return json.Marshal(v.VariableID) diff --git a/internal/template/load.go b/internal/template/load.go index e46466b8..97397147 100644 --- a/internal/template/load.go +++ b/internal/template/load.go @@ -88,7 +88,7 @@ func LoadThing(file string) (*iotclient.Thing, error) { // It applies the thing overrides specified by the override parameter. // It requires an iot.Client parameter to retrieve the actual variable id. func LoadDashboard(file string, override map[string]string, iotClient iot.Client) (*iotclient.Dashboardv2, error) { - template := dashboardHelp{} + template := dashboardTemplate{} err := loadTemplate(file, &template) if err != nil { return nil, err diff --git a/internal/template/load_test.go b/internal/template/load_test.go index f592b216..762aa799 100644 --- a/internal/template/load_test.go +++ b/internal/template/load_test.go @@ -31,7 +31,7 @@ const ( ) var ( - dashboardTemplate = map[string]interface{}{ + dashboardTemplateTest = map[string]interface{}{ "id": "home-security-alarm-dashboard", "name": "Home Security Alarm", "widgets": []interface{}{ @@ -119,13 +119,13 @@ func TestLoadTemplate(t *testing.T) { { name: "yaml dashboard template", file: "testdata/home-security-dashboard.yaml", - want: dashboardTemplate, + want: dashboardTemplateTest, }, { name: "json dashboard template", file: "testdata/home-security-dashboard.json", - want: dashboardTemplate, + want: dashboardTemplateTest, }, } From cdf4888af33474b5f0735f52033890d3e49359f0 Mon Sep 17 00:00:00 2001 From: Paolo Calao Date: Tue, 12 Oct 2021 19:39:59 +0200 Subject: [PATCH 11/20] Update readme --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index 7273cd97..bf5c5d06 100644 --- a/README.md +++ b/README.md @@ -132,3 +132,7 @@ Delete a dashboard with the following command: Extract a template from an existing dashboard. The template can be saved in two formats: json or yaml. The default format is yaml: `$ arduino-cloud-cli dashboard extract --id --outfile --format ` + +Create a dashboard: dashboards can be created only starting from a template. Supported dashboard template formats are JSON and YAML. The name parameter is optional. If it is provided then it overrides the name retrieved from the template. The `override` flag can be used to override the template `thing_id` placeholder with the actual ID of the thing to be used. + +`$ arduino-cloud-cli dashboard create --name --template --override =,=` \ No newline at end of file From ded4984de59e6dc88838e37235177f5a63ed8307 Mon Sep 17 00:00:00 2001 From: Paolo Calao Date: Thu, 14 Oct 2021 15:08:54 +0200 Subject: [PATCH 12/20] Improve comments --- internal/template/dashboard.go | 2 +- internal/template/load.go | 7 ++++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/internal/template/dashboard.go b/internal/template/dashboard.go index 69e4be3e..ae702d0b 100644 --- a/internal/template/dashboard.go +++ b/internal/template/dashboard.go @@ -57,7 +57,7 @@ func (v *variableTemplate) MarshalJSON() ([]byte, error) { return json.Marshal(v.VariableID) } -// getVariableID returns the id of a variable, given its thing id and its variable name. +// getVariableID returns the id of a variable, given its name and its thing id. // If the variable is not found, an error is returned. func getVariableID(thingID string, variableName string, iotClient iot.Client) (string, error) { thing, err := iotClient.ThingShow(thingID) diff --git a/internal/template/load.go b/internal/template/load.go index 97397147..6de611a3 100644 --- a/internal/template/load.go +++ b/internal/template/load.go @@ -31,8 +31,9 @@ import ( ) // loadTemplate loads a template file and unmarshals it into whatever -// is pointed to by the template parameter. Note that template parameter should be a pointer. -// The input template file should be in json or yaml format. +// is pointed to by the template parameter. If template is nil or +// not a pointer, loadTemplate returns an error. +// file: path of a template file in json or yaml format. func loadTemplate(file string, template interface{}) error { templateFile, err := os.Open(file) if err != nil { @@ -103,7 +104,7 @@ func LoadDashboard(file string, override map[string]string, iotClient iot.Client } widget.Id = id.String() filterWidgetOptions(widget.Options) - // Even if widget has no options, options field should exist + // Even if the widget has no options, its field should exist if widget.Options == nil { widget.Options = make(map[string]interface{}) } From 60f38b377dd42809385bd7ff00bbc9c984f0e9bb Mon Sep 17 00:00:00 2001 From: Paolo Calao Date: Thu, 14 Oct 2021 15:16:02 +0200 Subject: [PATCH 13/20] Improve errors --- internal/template/dashboard.go | 6 +++--- internal/template/load.go | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/internal/template/dashboard.go b/internal/template/dashboard.go index ae702d0b..5a2c7277 100644 --- a/internal/template/dashboard.go +++ b/internal/template/dashboard.go @@ -19,7 +19,7 @@ package template import ( "encoding/json" - "errors" + "fmt" "github.com/arduino/arduino-cloud-cli/internal/iot" ) @@ -62,7 +62,7 @@ func (v *variableTemplate) MarshalJSON() ([]byte, error) { func getVariableID(thingID string, variableName string, iotClient iot.Client) (string, error) { thing, err := iotClient.ThingShow(thingID) if err != nil { - return "", err + return "", fmt.Errorf("getting variables of thing %s: %w", thingID, err) } for _, v := range thing.Properties { @@ -71,5 +71,5 @@ func getVariableID(thingID string, variableName string, iotClient iot.Client) (s } } - return "", errors.New("not found") + return "", fmt.Errorf("thing with id %s doesn't have variable with name %s : %w", thingID, variableName, err) } diff --git a/internal/template/load.go b/internal/template/load.go index 6de611a3..fd64a6d1 100644 --- a/internal/template/load.go +++ b/internal/template/load.go @@ -116,7 +116,7 @@ func LoadDashboard(file string, override map[string]string, iotClient iot.Client } variable.VariableID, err = getVariableID(variable.ThingID, variable.VariableName, iotClient) if err != nil { - return nil, fmt.Errorf("thing with id %s doesn't have variable with name %s : %w", variable.ThingID, variable.VariableName, err) + return nil, err } widget.Variables[j] = variable } From 328ceb55b80f303f50fb69505563ed1e40f37844 Mon Sep 17 00:00:00 2001 From: Paolo Calao Date: Thu, 14 Oct 2021 15:17:12 +0200 Subject: [PATCH 14/20] Fix typo --- internal/template/dashboard.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/template/dashboard.go b/internal/template/dashboard.go index 5a2c7277..086cdfb2 100644 --- a/internal/template/dashboard.go +++ b/internal/template/dashboard.go @@ -35,7 +35,7 @@ type widgetTemplate struct { Id string `json:"id" yaml:"id"` Name string `json:"name,omitempty" yaml:"name,omitempty"` Options map[string]interface{} `json:"options" yaml:"options"` - WidgetType string `json:"type" yaml:"type"` + Type string `json:"type" yaml:"type"` Variables []variableTemplate `json:"variables,omitempty" yaml:"variables,omitempty"` Width int64 `json:"width" yaml:"width"` WidthMobile int64 `json:"width_mobile,omitempty" yaml:"width_mobile,omitempty"` From d2661fb05e3814ffda952e4819f6cf913bfa6551 Mon Sep 17 00:00:00 2001 From: Paolo Calao Date: Thu, 14 Oct 2021 15:37:29 +0200 Subject: [PATCH 15/20] Comment dashboard template marshal method --- internal/template/dashboard.go | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/internal/template/dashboard.go b/internal/template/dashboard.go index 086cdfb2..4e6f36aa 100644 --- a/internal/template/dashboard.go +++ b/internal/template/dashboard.go @@ -51,9 +51,13 @@ type variableTemplate struct { VariableID string } +// MarshalJSON satisfies the Marshaler interface from json package. +// With this, when a variableTemplate is marshaled, it only marshals +// its VariableID. In this way, a widgetTemplate can be +// marshaled and then unmarshaled into a iot.Widget struct. +// In the same way, a dashboardTemplate can now be converted +// into a iot.DashboardV2 leveraging the JSON marshal/unmarshal. func (v *variableTemplate) MarshalJSON() ([]byte, error) { - // Jsonize as a list of strings (variable uuids) - // in order to uniform to the other dashboard declaration (of iotclient) return json.Marshal(v.VariableID) } From 45b3b4222d399f888021e0011099d331f51b4b7e Mon Sep 17 00:00:00 2001 From: Paolo Calao Date: Thu, 14 Oct 2021 15:43:17 +0200 Subject: [PATCH 16/20] remove unused testdata file --- internal/template/testdata/prova.yaml | 39 --------------------------- 1 file changed, 39 deletions(-) delete mode 100644 internal/template/testdata/prova.yaml diff --git a/internal/template/testdata/prova.yaml b/internal/template/testdata/prova.yaml deleted file mode 100644 index 310a4d95..00000000 --- a/internal/template/testdata/prova.yaml +++ /dev/null @@ -1,39 +0,0 @@ -name: prova -widgets: - - height: 4 - height_mobile: 4 - name: Switch - options: - icon: switch - readOnly: false - section: section-1 - showLabels: true - showThing: false - thingId: null - type: Switch - width: 4 - width_mobile: 8 - x: 0 - x_mobile: 0 - "y": 0 - y_mobile: 0 - - height: 5 - height_mobile: 5 - name: Slider - options: - icon: slider - max: 50 - min: 0 - readOnly: false - section: section-1 - thingId: 453fc065-51a9-40d9-95b4-4dbbf1cfc0cc - type: Slider - variables: - - thing_id: CrocodileDaEliminare - variable_id: prova - width: 6 - width_mobile: 8 - x: 4 - x_mobile: 0 - "y": 0 - y_mobile: 1 From 39b49e4649c1edded731de1ab4a9589005cdbc19 Mon Sep 17 00:00:00 2001 From: Paolo Calao Date: Thu, 14 Oct 2021 16:22:43 +0200 Subject: [PATCH 17/20] Improve uuid test --- internal/template/load_test.go | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/internal/template/load_test.go b/internal/template/load_test.go index 762aa799..8e0f1a6c 100644 --- a/internal/template/load_test.go +++ b/internal/template/load_test.go @@ -22,14 +22,11 @@ import ( "github.com/arduino/arduino-cloud-cli/internal/template/mocks" iotclient "github.com/arduino/iot-client-go" + "github.com/gofrs/uuid" "github.com/google/go-cmp/cmp" "github.com/stretchr/testify/mock" ) -const ( - uuidv4Length = 36 -) - var ( dashboardTemplateTest = map[string]interface{}{ "id": "home-security-alarm-dashboard", @@ -144,7 +141,6 @@ func TestLoadTemplate(t *testing.T) { } func TestLoadDashboard(t *testing.T) { - mockClient := &mocks.Client{} mockThingShow := func(thingID string) *iotclient.ArduinoThing { thing := &iotclient.ArduinoThing{ @@ -220,9 +216,11 @@ func TestLoadDashboard(t *testing.T) { for i := range got.Widgets { // check widget id generation id := got.Widgets[i].Id - if len(id) != uuidv4Length { - t.Errorf("Widget ID is wrong: = %s", id) + _, err := uuid.FromString(id) + if err != nil { + t.Errorf("Widget ID is not a valid UUID: %s", id) } + // Remove generated id to be able to compare the widget with the expected one got.Widgets[i].Id = "" } From 2b4dc1ddf5db2970c69efa2811307288020e01e7 Mon Sep 17 00:00:00 2001 From: Paolo Calao Date: Thu, 14 Oct 2021 16:25:02 +0200 Subject: [PATCH 18/20] Improve test messages --- internal/template/load_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/template/load_test.go b/internal/template/load_test.go index 8e0f1a6c..f256e496 100644 --- a/internal/template/load_test.go +++ b/internal/template/load_test.go @@ -134,7 +134,7 @@ func TestLoadTemplate(t *testing.T) { t.Errorf("%v", err) } if !cmp.Equal(got, tt.want) { - t.Errorf("Wrong template received, got=\n%s", cmp.Diff(tt.want, got)) + t.Errorf("Wrong template received, diff:\n%s", cmp.Diff(tt.want, got)) } }) } @@ -225,7 +225,7 @@ func TestLoadDashboard(t *testing.T) { } if !cmp.Equal(got, tt.want) { - t.Errorf("Wrong template received, got=\n%s", cmp.Diff(tt.want, got)) + t.Errorf("Wrong template received, diff:\n%s", cmp.Diff(tt.want, got)) } }) } From 8ae391420bd127c5265090c26ef8f3806154793d Mon Sep 17 00:00:00 2001 From: Paolo Calao Date: Thu, 14 Oct 2021 18:21:38 +0200 Subject: [PATCH 19/20] Improve tests --- internal/template/load_test.go | 46 +++++++++++-------- .../testdata/dashboard-with-variable.yaml | 2 +- 2 files changed, 28 insertions(+), 20 deletions(-) diff --git a/internal/template/load_test.go b/internal/template/load_test.go index f256e496..0847c6ce 100644 --- a/internal/template/load_test.go +++ b/internal/template/load_test.go @@ -27,6 +27,17 @@ import ( "github.com/stretchr/testify/mock" ) +const ( + // Real IDs will be UUIDs v4 like this: 9231a50b-8680-4489-a465-2b769fc310cb + // Here we use these text strings to improve test errors readability + switchyID = "switchy-id" + relayID = "relay-id" + blinkSpeedID = "blink_speed-id" + + thingOverriddenID = "thing-overridden-id" + switchyOverriddenID = "switchy-overridden-id" +) + var ( dashboardTemplateTest = map[string]interface{}{ "id": "home-security-alarm-dashboard", @@ -69,8 +80,7 @@ var ( Widgets: []iotclient.Widget{ {Name: "Switch-name", Height: 1, HeightMobile: 2, Width: 3, WidthMobile: 4, X: 5, XMobile: 6, Y: 7, YMobile: 8, Options: map[string]interface{}{"showLabels": true}, Type: "Switch", - // variable id is set equal to the thing id by mockThingShow, in order to verify the thing override - Variables: []string{"thing"}, + Variables: []string{switchyID}, }, }, } @@ -80,26 +90,21 @@ var ( Widgets: []iotclient.Widget{ {Name: "Switch-name", Height: 1, HeightMobile: 2, Width: 3, WidthMobile: 4, X: 5, XMobile: 6, Y: 7, YMobile: 8, Options: map[string]interface{}{"showLabels": true}, Type: "Switch", - // variable id is set equal to the thing id by mockThingShow, in order to verify the thing override - Variables: []string{"overridden"}, + Variables: []string{switchyOverriddenID}, }, }, } dashboardTwoWidgets = &iotclient.Dashboardv2{ Name: "dashboard-two-widgets", - // in this test, the variable id is a concatenation of thing_id and variable_id - // this depends on the mocked function getVariableID Widgets: []iotclient.Widget{ {Name: "blink_speed", Height: 7, Width: 8, X: 7, Y: 5, Options: map[string]interface{}{"min": float64(0), "max": float64(5000)}, Type: "Slider", - // variable id is set equal to the thing id by mockThingShow, in order to verify the thing override - Variables: []string{"remote-controlled-lights"}, + Variables: []string{blinkSpeedID}, }, {Name: "relay_2", Height: 5, Width: 5, X: 5, Y: 0, Options: map[string]interface{}{"showLabels": true}, Type: "Switch", - // variable id is set equal to the thing id by mockThingShow, in order to verify the thing override - Variables: []string{"remote-controlled-lights"}, + Variables: []string{relayID}, }, }, } @@ -143,17 +148,20 @@ func TestLoadTemplate(t *testing.T) { func TestLoadDashboard(t *testing.T) { mockClient := &mocks.Client{} mockThingShow := func(thingID string) *iotclient.ArduinoThing { - thing := &iotclient.ArduinoThing{ + if thingID == thingOverriddenID { + return &iotclient.ArduinoThing{ + Properties: []iotclient.ArduinoProperty{ + {Id: switchyOverriddenID, Name: "switchy"}, + }, + } + } + return &iotclient.ArduinoThing{ Properties: []iotclient.ArduinoProperty{ - // variable id is set equal to the thing id in order to verify the thing override - // dashboard-with-variable variable - {Id: thingID, Name: "variable"}, - // dashboard-two-widgets variables - {Id: thingID, Name: "relay_2"}, - {Id: thingID, Name: "blink_speed"}, + {Id: switchyID, Name: "switchy"}, + {Id: relayID, Name: "relay_2"}, + {Id: blinkSpeedID, Name: "blink_speed"}, }, } - return thing } mockClient.On("ThingShow", mock.AnythingOfType("string")).Return(mockThingShow, nil) @@ -194,7 +202,7 @@ func TestLoadDashboard(t *testing.T) { { name: "dashboard with variable, thing is overridden", file: "testdata/dashboard-with-variable.yaml", - override: map[string]string{"thing": "overridden"}, + override: map[string]string{"thing": thingOverriddenID}, want: dashboardVariableOverride, }, diff --git a/internal/template/testdata/dashboard-with-variable.yaml b/internal/template/testdata/dashboard-with-variable.yaml index d75a886e..262f04d3 100644 --- a/internal/template/testdata/dashboard-with-variable.yaml +++ b/internal/template/testdata/dashboard-with-variable.yaml @@ -7,7 +7,7 @@ widgets: showLabels: true variables: - thing_id: thing - variable_id: variable + variable_id: switchy type: Switch width: 3 width_mobile: 4 From b4473a7cda8f634bf10015b5b7282dd1e20a5746 Mon Sep 17 00:00:00 2001 From: Paolo Calao Date: Fri, 15 Oct 2021 10:56:15 +0200 Subject: [PATCH 20/20] Move iot Client mock --- internal/{template => iot}/mocks/Client.go | 0 internal/template/load_test.go | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename internal/{template => iot}/mocks/Client.go (100%) diff --git a/internal/template/mocks/Client.go b/internal/iot/mocks/Client.go similarity index 100% rename from internal/template/mocks/Client.go rename to internal/iot/mocks/Client.go diff --git a/internal/template/load_test.go b/internal/template/load_test.go index 0847c6ce..3790d09c 100644 --- a/internal/template/load_test.go +++ b/internal/template/load_test.go @@ -20,7 +20,7 @@ package template import ( "testing" - "github.com/arduino/arduino-cloud-cli/internal/template/mocks" + "github.com/arduino/arduino-cloud-cli/internal/iot/mocks" iotclient "github.com/arduino/iot-client-go" "github.com/gofrs/uuid" "github.com/google/go-cmp/cmp"