From 392e425808808a4d80810130459ff138fcc7eb8c Mon Sep 17 00:00:00 2001 From: martha-johnston Date: Wed, 11 Dec 2024 15:46:28 -0500 Subject: [PATCH 1/7] add nil geometries check to grippper --- components/gripper/server.go | 9 +++++++++ components/gripper/server_test.go | 22 ++++++++++++++++++++++ 2 files changed, 31 insertions(+) diff --git a/components/gripper/server.go b/components/gripper/server.go index b067926f92f..0fa39d9c8bf 100644 --- a/components/gripper/server.go +++ b/components/gripper/server.go @@ -3,6 +3,7 @@ package gripper import ( "context" + "fmt" commonpb "go.viam.com/api/common/v1" pb "go.viam.com/api/component/gripper/v1" @@ -13,6 +14,11 @@ import ( "go.viam.com/rdk/spatialmath" ) +// ErrGeometriesNil is the returned error if gripper geometries are nil. +var ErrGeometriesNil = func(gripperName string) error { + return fmt.Errorf("gripper component %v Geometries should not return nil geometries", gripperName) +} + // serviceServer implements the GripperService from gripper.proto. type serviceServer struct { pb.UnimplementedGripperServiceServer @@ -90,5 +96,8 @@ func (s *serviceServer) GetGeometries(ctx context.Context, req *commonpb.GetGeom if err != nil { return nil, err } + if geometries == nil { + return nil, ErrGeometriesNil(req.GetName()) + } return &commonpb.GetGeometriesResponse{Geometries: spatialmath.NewGeometriesToProto(geometries)}, nil } diff --git a/components/gripper/server_test.go b/components/gripper/server_test.go index 248b22ac24e..6eae6fd5c57 100644 --- a/components/gripper/server_test.go +++ b/components/gripper/server_test.go @@ -5,12 +5,15 @@ import ( "errors" "testing" + "github.com/golang/geo/r3" + pbcommon "go.viam.com/api/common/v1" pb "go.viam.com/api/component/gripper/v1" "go.viam.com/test" "go.viam.com/utils/protoutils" "go.viam.com/rdk/components/gripper" "go.viam.com/rdk/resource" + "go.viam.com/rdk/spatialmath" "go.viam.com/rdk/testutils/inject" ) @@ -56,6 +59,14 @@ func TestServer(t *testing.T) { extraOptions = extra return nil } + injectGripper.GeometriesFunc = func(ctx context.Context) ([]spatialmath.Geometry, error) { + box, err := spatialmath.NewBox( + spatialmath.NewPose(r3.Vector{X: 0, Y: 0, Z: 0}, spatialmath.NewZeroPose().Orientation()), + r3.Vector{}, + testGripperName, + ) + return []spatialmath.Geometry{box}, err + } injectGripper2.OpenFunc = func(ctx context.Context, extra map[string]interface{}) error { gripperOpen = testGripperName2 @@ -67,6 +78,9 @@ func TestServer(t *testing.T) { injectGripper2.StopFunc = func(ctx context.Context, extra map[string]interface{}) error { return errStopUnimplemented } + injectGripper2.GeometriesFunc = func(ctx context.Context) ([]spatialmath.Geometry, error) { + return nil, nil + } t.Run("open", func(t *testing.T) { _, err := gripperServer.Open(context.Background(), &pb.OpenRequest{Name: missingGripperName}) @@ -122,4 +136,12 @@ func TestServer(t *testing.T) { test.That(t, err, test.ShouldNotBeNil) test.That(t, err, test.ShouldBeError, errStopUnimplemented) }) + + t.Run("geometries", func(t *testing.T) { + _, err = gripperServer.GetGeometries(context.Background(), &pbcommon.GetGeometriesRequest{Name: testGripperName}) + test.That(t, err, test.ShouldBeNil) + + _, err = gripperServer.GetGeometries(context.Background(), &pbcommon.GetGeometriesRequest{Name: testGripperName2}) + test.That(t, err, test.ShouldBeError, gripper.ErrGeometriesNil(testGripperName2)) + }) } From 517c7c3085bfc482502368a4ab3c3b2df31259c1 Mon Sep 17 00:00:00 2001 From: martha-johnston Date: Wed, 11 Dec 2024 15:50:03 -0500 Subject: [PATCH 2/7] add nil geometries test to client --- components/gripper/client_test.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/components/gripper/client_test.go b/components/gripper/client_test.go index b8b31777b9f..41da66eb9e6 100644 --- a/components/gripper/client_test.go +++ b/components/gripper/client_test.go @@ -59,6 +59,9 @@ func TestClient(t *testing.T) { injectGripper2.StopFunc = func(ctx context.Context, extra map[string]interface{}) error { return errStopUnimplemented } + injectGripper2.GeometriesFunc = func(ctx context.Context) ([]spatialmath.Geometry, error) { + return nil, nil + } gripperSvc, err := resource.NewAPIResourceCollection( gripper.API, @@ -145,6 +148,9 @@ func TestClient(t *testing.T) { test.That(t, err, test.ShouldNotBeNil) test.That(t, err.Error(), test.ShouldContainSubstring, errStopUnimplemented.Error()) + _, err = client2.Geometries(context.Background(), extra) + test.That(t, err.Error(), test.ShouldContainSubstring, gripper.ErrGeometriesNil(failGripperName).Error()) + test.That(t, client2.Close(context.Background()), test.ShouldBeNil) test.That(t, conn.Close(), test.ShouldBeNil) }) From 434ee1c7f8632ebd55b7aaa1cd46e79607cc85ed Mon Sep 17 00:00:00 2001 From: martha-johnston Date: Wed, 11 Dec 2024 16:17:52 -0500 Subject: [PATCH 3/7] add input controller nil checks --- components/input/client_test.go | 33 ++++++++++++++++++++++++++++-- components/input/server.go | 18 +++++++++++++++++ components/input/server_test.go | 36 +++++++++++++++++++++++++++------ 3 files changed, 79 insertions(+), 8 deletions(-) diff --git a/components/input/client_test.go b/components/input/client_test.go index dab4f65b2a8..ceda4327e70 100644 --- a/components/input/client_test.go +++ b/components/input/client_test.go @@ -67,9 +67,18 @@ func TestClient(t *testing.T) { return nil, errEventsFailed } + injectInputController3 := &inject.InputController{} + injectInputController3.ControlsFunc = func(ctx context.Context, extra map[string]interface{}) ([]input.Control, error) { + return nil, nil + } + injectInputController3.EventsFunc = func(ctx context.Context, extra map[string]interface{}) (map[input.Control]input.Event, error) { + return nil, nil + } + resources := map[resource.Name]input.Controller{ - input.Named(testInputControllerName): injectInputController, - input.Named(failInputControllerName): injectInputController2, + input.Named(testInputControllerName): injectInputController, + input.Named(failInputControllerName): injectInputController2, + input.Named(testInputControllerName4): injectInputController3, } inputControllerSvc, err := resource.NewAPIResourceCollection(input.API, resources) test.That(t, err, test.ShouldBeNil) @@ -270,6 +279,26 @@ func TestClient(t *testing.T) { test.That(t, err, test.ShouldNotBeNil) test.That(t, err.Error(), test.ShouldContainSubstring, "not of type Triggerable") }) + + t.Run("input controller client 3", func(t *testing.T) { + conn, err := viamgrpc.Dial(context.Background(), listener1.Addr().String(), logger) + test.That(t, err, test.ShouldBeNil) + client3, err := resourceAPI.RPCClient(context.Background(), conn, "", input.Named(failInputControllerName), logger) + test.That(t, err, test.ShouldBeNil) + + defer func() { + test.That(t, client3.Close(context.Background()), test.ShouldBeNil) + test.That(t, conn.Close(), test.ShouldBeNil) + }() + + _, err = client3.Controls(context.Background(), map[string]interface{}{}) + test.That(t, err, test.ShouldNotBeNil) + test.That(t, err.Error(), test.ShouldContainSubstring, input.ErrControlsNil(testInputControllerName4).Error()) + + _, err = client3.Events(context.Background(), map[string]interface{}{}) + test.That(t, err, test.ShouldNotBeNil) + test.That(t, err.Error(), test.ShouldContainSubstring, input.ErrEventsNil(testInputControllerName4).Error()) + }) } func TestClientRace(t *testing.T) { diff --git a/components/input/server.go b/components/input/server.go index 3bc81b7ee77..de1f14eae49 100644 --- a/components/input/server.go +++ b/components/input/server.go @@ -3,6 +3,7 @@ package input import ( "context" + "fmt" "github.com/pkg/errors" commonpb "go.viam.com/api/common/v1" @@ -13,6 +14,17 @@ import ( "go.viam.com/rdk/resource" ) +var ( + // ErrControlsNil is the returned error if input controller controls are nil. + ErrControlsNil = func(inputName string) error { + return fmt.Errorf("input controller component %v Controls should not return nil controls", inputName) + } + // ErrEventsNil is the returned error if input controller events are nil. + ErrEventsNil = func(inputName string) error { + return fmt.Errorf("input controller component %v Events should not return nil events", inputName) + } +) + // serviceServer implements the InputControllerService from proto. type serviceServer struct { pb.UnimplementedInputControllerServiceServer @@ -39,6 +51,9 @@ func (s *serviceServer) GetControls( if err != nil { return nil, err } + if controlList == nil { + return nil, ErrControlsNil(req.Controller) + } resp := &pb.GetControlsResponse{} @@ -62,6 +77,9 @@ func (s *serviceServer) GetEvents( if err != nil { return nil, err } + if eventsIn == nil { + return nil, ErrEventsNil(req.Controller) + } resp := &pb.GetEventsResponse{} diff --git a/components/input/server_test.go b/components/input/server_test.go index daad15d5afc..c150fe1eb6d 100644 --- a/components/input/server_test.go +++ b/components/input/server_test.go @@ -21,6 +21,7 @@ const ( testInputControllerName = "inputController1" failInputControllerName = "inputController2" missingInputControllerName = "inputController3" + testInputControllerName4 = "inputController4" ) var ( @@ -54,22 +55,24 @@ func (x *streamServer) Send(m *pb.StreamEventsResponse) error { return nil } -func newServer() (pb.InputControllerServiceServer, *inject.TriggerableInputController, *inject.InputController, error) { +func newServer() (pb.InputControllerServiceServer, *inject.TriggerableInputController, *inject.InputController, *inject.InputController, error) { injectInputController := &inject.TriggerableInputController{} injectInputController2 := &inject.InputController{} + injectInputController3 := &inject.InputController{} inputControllers := map[resource.Name]input.Controller{ - input.Named(testInputControllerName): injectInputController, - input.Named(failInputControllerName): injectInputController2, + input.Named(testInputControllerName): injectInputController, + input.Named(failInputControllerName): injectInputController2, + input.Named(testInputControllerName4): injectInputController3, } inputControllerSvc, err := resource.NewAPIResourceCollection(input.API, inputControllers) if err != nil { - return nil, nil, nil, err + return nil, nil, nil, nil, err } - return input.NewRPCServiceServer(inputControllerSvc).(pb.InputControllerServiceServer), injectInputController, injectInputController2, nil + return input.NewRPCServiceServer(inputControllerSvc).(pb.InputControllerServiceServer), injectInputController, injectInputController2, injectInputController3, nil } func TestServer(t *testing.T) { - inputControllerServer, injectInputController, injectInputController2, err := newServer() + inputControllerServer, injectInputController, injectInputController2, injectInputController3, err := newServer() test.That(t, err, test.ShouldBeNil) var extraOptions map[string]interface{} @@ -113,6 +116,13 @@ func TestServer(t *testing.T) { return errRegisterFailed } + injectInputController3.ControlsFunc = func(ctx context.Context, extra map[string]interface{}) ([]input.Control, error) { + return nil, nil + } + injectInputController2.EventsFunc = func(ctx context.Context, extra map[string]interface{}) (map[input.Control]input.Event, error) { + return nil, nil + } + t.Run("GetControls", func(t *testing.T) { _, err := inputControllerServer.GetControls( context.Background(), @@ -138,6 +148,13 @@ func TestServer(t *testing.T) { ) test.That(t, err, test.ShouldNotBeNil) test.That(t, err.Error(), test.ShouldContainSubstring, errControlsFailed.Error()) + + _, err = inputControllerServer.GetControls( + context.Background(), + &pb.GetControlsRequest{Controller: testInputControllerName4}, + ) + test.That(t, err, test.ShouldNotBeNil) + test.That(t, err.Error(), test.ShouldContainSubstring, input.ErrControlsNil(testInputControllerName4).Error()) }) t.Run("GetEvents", func(t *testing.T) { @@ -187,6 +204,13 @@ func TestServer(t *testing.T) { ) test.That(t, err, test.ShouldNotBeNil) test.That(t, err.Error(), test.ShouldContainSubstring, errEventsFailed.Error()) + + _, err = inputControllerServer.GetEvents( + context.Background(), + &pb.GetEventsRequest{Controller: testInputControllerName4}, + ) + test.That(t, err, test.ShouldNotBeNil) + test.That(t, err.Error(), test.ShouldContainSubstring, input.ErrEventsNil(testInputControllerName4).Error()) }) t.Run("StreamEvents", func(t *testing.T) { From 54829483ace91faac1ec28c4c593a96b53354672 Mon Sep 17 00:00:00 2001 From: martha-johnston Date: Wed, 11 Dec 2024 16:27:45 -0500 Subject: [PATCH 4/7] lint fix --- components/input/server_test.go | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/components/input/server_test.go b/components/input/server_test.go index c150fe1eb6d..634fedf936e 100644 --- a/components/input/server_test.go +++ b/components/input/server_test.go @@ -55,7 +55,13 @@ func (x *streamServer) Send(m *pb.StreamEventsResponse) error { return nil } -func newServer() (pb.InputControllerServiceServer, *inject.TriggerableInputController, *inject.InputController, *inject.InputController, error) { +func newServer() ( + pb.InputControllerServiceServer, + *inject.TriggerableInputController, + *inject.InputController, + *inject.InputController, + error, +) { injectInputController := &inject.TriggerableInputController{} injectInputController2 := &inject.InputController{} injectInputController3 := &inject.InputController{} @@ -68,7 +74,11 @@ func newServer() (pb.InputControllerServiceServer, *inject.TriggerableInputContr if err != nil { return nil, nil, nil, nil, err } - return input.NewRPCServiceServer(inputControllerSvc).(pb.InputControllerServiceServer), injectInputController, injectInputController2, injectInputController3, nil + return input.NewRPCServiceServer(inputControllerSvc).(pb.InputControllerServiceServer), + injectInputController, + injectInputController2, + injectInputController3, + nil } func TestServer(t *testing.T) { From eb8d23ee4359c5b205156db014c12dccadfd6fd4 Mon Sep 17 00:00:00 2001 From: martha-johnston Date: Wed, 11 Dec 2024 17:06:31 -0500 Subject: [PATCH 5/7] fix tests: --- components/input/client_test.go | 33 ++++--------------- components/input/server_test.go | 56 ++++++--------------------------- 2 files changed, 16 insertions(+), 73 deletions(-) diff --git a/components/input/client_test.go b/components/input/client_test.go index ceda4327e70..988c57805c8 100644 --- a/components/input/client_test.go +++ b/components/input/client_test.go @@ -61,10 +61,10 @@ func TestClient(t *testing.T) { injectInputController2 := &inject.InputController{} injectInputController2.ControlsFunc = func(ctx context.Context, extra map[string]interface{}) ([]input.Control, error) { - return nil, errControlsFailed + return nil, nil } injectInputController2.EventsFunc = func(ctx context.Context, extra map[string]interface{}) (map[input.Control]input.Event, error) { - return nil, errEventsFailed + return nil, nil } injectInputController3 := &inject.InputController{} @@ -76,9 +76,8 @@ func TestClient(t *testing.T) { } resources := map[resource.Name]input.Controller{ - input.Named(testInputControllerName): injectInputController, - input.Named(failInputControllerName): injectInputController2, - input.Named(testInputControllerName4): injectInputController3, + input.Named(testInputControllerName): injectInputController, + input.Named(failInputControllerName): injectInputController2, } inputControllerSvc, err := resource.NewAPIResourceCollection(input.API, resources) test.That(t, err, test.ShouldBeNil) @@ -261,11 +260,11 @@ func TestClient(t *testing.T) { _, err = client2.Controls(context.Background(), map[string]interface{}{}) test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, errControlsFailed.Error()) + test.That(t, err.Error(), test.ShouldContainSubstring, input.ErrControlsNil(failInputControllerName).Error()) _, err = client2.Events(context.Background(), map[string]interface{}{}) test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, errEventsFailed.Error()) + test.That(t, err.Error(), test.ShouldContainSubstring, input.ErrEventsNil(failInputControllerName).Error()) event1 := input.Event{ Time: time.Now().UTC(), @@ -279,26 +278,6 @@ func TestClient(t *testing.T) { test.That(t, err, test.ShouldNotBeNil) test.That(t, err.Error(), test.ShouldContainSubstring, "not of type Triggerable") }) - - t.Run("input controller client 3", func(t *testing.T) { - conn, err := viamgrpc.Dial(context.Background(), listener1.Addr().String(), logger) - test.That(t, err, test.ShouldBeNil) - client3, err := resourceAPI.RPCClient(context.Background(), conn, "", input.Named(failInputControllerName), logger) - test.That(t, err, test.ShouldBeNil) - - defer func() { - test.That(t, client3.Close(context.Background()), test.ShouldBeNil) - test.That(t, conn.Close(), test.ShouldBeNil) - }() - - _, err = client3.Controls(context.Background(), map[string]interface{}{}) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, input.ErrControlsNil(testInputControllerName4).Error()) - - _, err = client3.Events(context.Background(), map[string]interface{}{}) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, input.ErrEventsNil(testInputControllerName4).Error()) - }) } func TestClientRace(t *testing.T) { diff --git a/components/input/server_test.go b/components/input/server_test.go index 634fedf936e..851d0dc1e17 100644 --- a/components/input/server_test.go +++ b/components/input/server_test.go @@ -21,12 +21,9 @@ const ( testInputControllerName = "inputController1" failInputControllerName = "inputController2" missingInputControllerName = "inputController3" - testInputControllerName4 = "inputController4" ) var ( - errControlsFailed = errors.New("can't get controls") - errEventsFailed = errors.New("can't get last events") errTriggerEvent = errors.New("can't inject event") errSendFailed = errors.New("send fail") errRegisterFailed = errors.New("can't register callbacks") @@ -55,34 +52,22 @@ func (x *streamServer) Send(m *pb.StreamEventsResponse) error { return nil } -func newServer() ( - pb.InputControllerServiceServer, - *inject.TriggerableInputController, - *inject.InputController, - *inject.InputController, - error, -) { +func newServer() (pb.InputControllerServiceServer, *inject.TriggerableInputController, *inject.InputController, error) { injectInputController := &inject.TriggerableInputController{} injectInputController2 := &inject.InputController{} - injectInputController3 := &inject.InputController{} inputControllers := map[resource.Name]input.Controller{ - input.Named(testInputControllerName): injectInputController, - input.Named(failInputControllerName): injectInputController2, - input.Named(testInputControllerName4): injectInputController3, + input.Named(testInputControllerName): injectInputController, + input.Named(failInputControllerName): injectInputController2, } inputControllerSvc, err := resource.NewAPIResourceCollection(input.API, inputControllers) if err != nil { - return nil, nil, nil, nil, err + return nil, nil, nil, err } - return input.NewRPCServiceServer(inputControllerSvc).(pb.InputControllerServiceServer), - injectInputController, - injectInputController2, - injectInputController3, - nil + return input.NewRPCServiceServer(inputControllerSvc).(pb.InputControllerServiceServer), injectInputController, injectInputController2, nil } func TestServer(t *testing.T) { - inputControllerServer, injectInputController, injectInputController2, injectInputController3, err := newServer() + inputControllerServer, injectInputController, injectInputController2, err := newServer() test.That(t, err, test.ShouldBeNil) var extraOptions map[string]interface{} @@ -111,10 +96,10 @@ func TestServer(t *testing.T) { } injectInputController2.ControlsFunc = func(ctx context.Context, extra map[string]interface{}) ([]input.Control, error) { - return nil, errControlsFailed + return nil, nil } injectInputController2.EventsFunc = func(ctx context.Context, extra map[string]interface{}) (map[input.Control]input.Event, error) { - return nil, errEventsFailed + return nil, nil } injectInputController2.RegisterControlCallbackFunc = func( ctx context.Context, @@ -126,13 +111,6 @@ func TestServer(t *testing.T) { return errRegisterFailed } - injectInputController3.ControlsFunc = func(ctx context.Context, extra map[string]interface{}) ([]input.Control, error) { - return nil, nil - } - injectInputController2.EventsFunc = func(ctx context.Context, extra map[string]interface{}) (map[input.Control]input.Event, error) { - return nil, nil - } - t.Run("GetControls", func(t *testing.T) { _, err := inputControllerServer.GetControls( context.Background(), @@ -157,14 +135,7 @@ func TestServer(t *testing.T) { &pb.GetControlsRequest{Controller: failInputControllerName}, ) test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, errControlsFailed.Error()) - - _, err = inputControllerServer.GetControls( - context.Background(), - &pb.GetControlsRequest{Controller: testInputControllerName4}, - ) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, input.ErrControlsNil(testInputControllerName4).Error()) + test.That(t, err.Error(), test.ShouldContainSubstring, input.ErrControlsNil(failInputControllerName).Error()) }) t.Run("GetEvents", func(t *testing.T) { @@ -213,14 +184,7 @@ func TestServer(t *testing.T) { &pb.GetEventsRequest{Controller: failInputControllerName}, ) test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, errEventsFailed.Error()) - - _, err = inputControllerServer.GetEvents( - context.Background(), - &pb.GetEventsRequest{Controller: testInputControllerName4}, - ) - test.That(t, err, test.ShouldNotBeNil) - test.That(t, err.Error(), test.ShouldContainSubstring, input.ErrEventsNil(testInputControllerName4).Error()) + test.That(t, err.Error(), test.ShouldContainSubstring, input.ErrEventsNil(failInputControllerName).Error()) }) t.Run("StreamEvents", func(t *testing.T) { From e2962cf6f57dbe50c5c7618f1581f2edc79b7df9 Mon Sep 17 00:00:00 2001 From: martha-johnston Date: Thu, 12 Dec 2024 09:55:44 -0500 Subject: [PATCH 6/7] remove unused inject input controller --- components/input/client_test.go | 8 -------- 1 file changed, 8 deletions(-) diff --git a/components/input/client_test.go b/components/input/client_test.go index 988c57805c8..2dd24d6baf6 100644 --- a/components/input/client_test.go +++ b/components/input/client_test.go @@ -67,14 +67,6 @@ func TestClient(t *testing.T) { return nil, nil } - injectInputController3 := &inject.InputController{} - injectInputController3.ControlsFunc = func(ctx context.Context, extra map[string]interface{}) ([]input.Control, error) { - return nil, nil - } - injectInputController3.EventsFunc = func(ctx context.Context, extra map[string]interface{}) (map[input.Control]input.Event, error) { - return nil, nil - } - resources := map[resource.Name]input.Controller{ input.Named(testInputControllerName): injectInputController, input.Named(failInputControllerName): injectInputController2, From 6fcc06bf82bf921deec044ac0cb038f7d5e978d6 Mon Sep 17 00:00:00 2001 From: martha-johnston Date: Tue, 17 Dec 2024 14:48:36 -0500 Subject: [PATCH 7/7] add nil check to controls and events in client --- components/input/client.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/components/input/client.go b/components/input/client.go index f2b4db12237..acc008a9acc 100644 --- a/components/input/client.go +++ b/components/input/client.go @@ -76,6 +76,9 @@ func (c *client) Controls(ctx context.Context, extra map[string]interface{}) ([] if err != nil { return nil, err } + if resp.Controls == nil { + return nil, nil + } var controls []Control for _, control := range resp.Controls { controls = append(controls, Control(control)) @@ -95,6 +98,9 @@ func (c *client) Events(ctx context.Context, extra map[string]interface{}) (map[ if err != nil { return nil, err } + if resp.Events == nil { + return nil, nil + } eventsOut := make(map[Control]Event) for _, eventIn := range resp.Events {