Skip to content

Commit

Permalink
APP-7497: Add Switch client, server, and fake models (#4741)
Browse files Browse the repository at this point in the history
  • Loading branch information
ethanlookpotts authored Jan 29, 2025
1 parent a891617 commit 85e76a9
Show file tree
Hide file tree
Showing 10 changed files with 771 additions and 0 deletions.
1 change: 1 addition & 0 deletions components/register/all.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,5 @@ import (
_ "go.viam.com/rdk/components/powersensor/register"
_ "go.viam.com/rdk/components/sensor/register"
_ "go.viam.com/rdk/components/servo/register"
_ "go.viam.com/rdk/components/switch/register"
)
88 changes: 88 additions & 0 deletions components/switch/client.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
// Package toggleswitch contains a gRPC based switch client.
package toggleswitch

import (
"context"

pb "go.viam.com/api/component/switch/v1"
"go.viam.com/utils/protoutils"
"go.viam.com/utils/rpc"

"go.viam.com/rdk/logging"
rprotoutils "go.viam.com/rdk/protoutils"
"go.viam.com/rdk/resource"
)

// client implements SwitchServiceClient.
type client struct {
resource.Named
resource.TriviallyReconfigurable
resource.TriviallyCloseable
name string
client pb.SwitchServiceClient
logger logging.Logger
}

// NewClientFromConn constructs a new Client from connection passed in.
func NewClientFromConn(
ctx context.Context,
conn rpc.ClientConn,
remoteName string,
name resource.Name,
logger logging.Logger,
) (Switch, error) {
c := pb.NewSwitchServiceClient(conn)
return &client{
Named: name.PrependRemote(remoteName).AsNamed(),
name: name.ShortName(),
client: c,
logger: logger,
}, nil
}

func (c *client) SetPosition(ctx context.Context, position uint32, extra map[string]interface{}) error {
ext, err := protoutils.StructToStructPb(extra)
if err != nil {
return err
}
_, err = c.client.SetPosition(ctx, &pb.SetPositionRequest{
Name: c.name,
Position: position,
Extra: ext,
})
return err
}

func (c *client) GetPosition(ctx context.Context, extra map[string]interface{}) (uint32, error) {
ext, err := protoutils.StructToStructPb(extra)
if err != nil {
return 0, err
}
resp, err := c.client.GetPosition(ctx, &pb.GetPositionRequest{
Name: c.name,
Extra: ext,
})
if err != nil {
return 0, err
}
return resp.Position, nil
}

func (c *client) GetNumberOfPositions(ctx context.Context, extra map[string]interface{}) (uint32, error) {
ext, err := protoutils.StructToStructPb(extra)
if err != nil {
return 0, err
}
resp, err := c.client.GetNumberOfPositions(ctx, &pb.GetNumberOfPositionsRequest{
Name: c.name,
Extra: ext,
})
if err != nil {
return 0, err
}
return resp.NumberOfPositions, nil
}

func (c *client) DoCommand(ctx context.Context, cmd map[string]interface{}) (map[string]interface{}, error) {
return rprotoutils.DoFromResourceClient(ctx, c.client, c.name, cmd)
}
150 changes: 150 additions & 0 deletions components/switch/client_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
package toggleswitch_test

import (
"context"
"net"
"testing"

"go.viam.com/test"
"go.viam.com/utils/rpc"

toggleswitch "go.viam.com/rdk/components/switch"
viamgrpc "go.viam.com/rdk/grpc"
"go.viam.com/rdk/logging"
"go.viam.com/rdk/resource"
"go.viam.com/rdk/testutils"
"go.viam.com/rdk/testutils/inject"
)

const (
testSwitchName = "switch1"
failSwitchName = "switch2"
missingSwitchName = "missing"
)

func TestClient(t *testing.T) {
logger := logging.NewTestLogger(t)
listener1, err := net.Listen("tcp", "localhost:0")
test.That(t, err, test.ShouldBeNil)
rpcServer, err := rpc.NewServer(logger, rpc.WithUnauthenticated())
test.That(t, err, test.ShouldBeNil)

var switchName string
var extraOptions map[string]interface{}

injectSwitch := inject.NewSwitch(testSwitchName)
injectSwitch.SetPositionFunc = func(ctx context.Context, position uint32, extra map[string]interface{}) error {
extraOptions = extra
switchName = testSwitchName
return nil
}
injectSwitch.GetPositionFunc = func(ctx context.Context, extra map[string]interface{}) (uint32, error) {
extraOptions = extra
switchName = testSwitchName
return 0, nil
}
injectSwitch.GetNumberOfPositionsFunc = func(ctx context.Context, extra map[string]interface{}) (uint32, error) {
extraOptions = extra
switchName = testSwitchName
return 2, nil
}
injectSwitch.DoFunc = testutils.EchoFunc

injectSwitch2 := inject.NewSwitch(failSwitchName)
injectSwitch2.SetPositionFunc = func(ctx context.Context, position uint32, extra map[string]interface{}) error {
switchName = failSwitchName
return errCantSetPosition
}
injectSwitch2.GetPositionFunc = func(ctx context.Context, extra map[string]interface{}) (uint32, error) {
switchName = failSwitchName
return 0, errCantGetPosition
}
injectSwitch2.GetNumberOfPositionsFunc = func(ctx context.Context, extra map[string]interface{}) (uint32, error) {
switchName = failSwitchName
return 0, errCantGetNumberOfPositions
}
injectSwitch2.DoFunc = testutils.EchoFunc

switchSvc, err := resource.NewAPIResourceCollection(
toggleswitch.API,
map[resource.Name]toggleswitch.Switch{
toggleswitch.Named(testSwitchName): injectSwitch,
toggleswitch.Named(failSwitchName): injectSwitch2,
})
test.That(t, err, test.ShouldBeNil)
resourceAPI, ok, err := resource.LookupAPIRegistration[toggleswitch.Switch](toggleswitch.API)
test.That(t, err, test.ShouldBeNil)
test.That(t, ok, test.ShouldBeTrue)
test.That(t, resourceAPI.RegisterRPCService(context.Background(), rpcServer, switchSvc), test.ShouldBeNil)

go rpcServer.Serve(listener1)
defer rpcServer.Stop()

// failing
t.Run("Failing client", func(t *testing.T) {
cancelCtx, cancel := context.WithCancel(context.Background())
cancel()
_, err := viamgrpc.Dial(cancelCtx, listener1.Addr().String(), logger)
test.That(t, err, test.ShouldNotBeNil)
test.That(t, err, test.ShouldBeError, context.Canceled)
})

// working
t.Run("switch client 1", func(t *testing.T) {
conn, err := viamgrpc.Dial(context.Background(), listener1.Addr().String(), logger)
test.That(t, err, test.ShouldBeNil)
client1, err := toggleswitch.NewClientFromConn(context.Background(), conn, "", toggleswitch.Named(testSwitchName), logger)
test.That(t, err, test.ShouldBeNil)

// DoCommand
resp, err := client1.DoCommand(context.Background(), testutils.TestCommand)
test.That(t, err, test.ShouldBeNil)
test.That(t, resp["command"], test.ShouldEqual, testutils.TestCommand["command"])
test.That(t, resp["data"], test.ShouldEqual, testutils.TestCommand["data"])

extra := map[string]interface{}{"foo": "SetPosition"}
err = client1.SetPosition(context.Background(), 0, extra)
test.That(t, err, test.ShouldBeNil)
test.That(t, extraOptions, test.ShouldResemble, extra)
test.That(t, switchName, test.ShouldEqual, testSwitchName)

extra = map[string]interface{}{"foo": "GetPosition"}
pos, err := client1.GetPosition(context.Background(), extra)
test.That(t, err, test.ShouldBeNil)
test.That(t, extraOptions, test.ShouldResemble, extra)
test.That(t, pos, test.ShouldEqual, 0)

extra = map[string]interface{}{"foo": "GetNumberOfPositions"}
count, err := client1.GetNumberOfPositions(context.Background(), extra)
test.That(t, err, test.ShouldBeNil)
test.That(t, extraOptions, test.ShouldResemble, extra)
test.That(t, count, test.ShouldEqual, 2)

test.That(t, client1.Close(context.Background()), test.ShouldBeNil)
test.That(t, conn.Close(), test.ShouldBeNil)
})

t.Run("switch client 2", func(t *testing.T) {
conn, err := viamgrpc.Dial(context.Background(), listener1.Addr().String(), logger)
test.That(t, err, test.ShouldBeNil)
client2, err := resourceAPI.RPCClient(context.Background(), conn, "", toggleswitch.Named(failSwitchName), logger)
test.That(t, err, test.ShouldBeNil)

extra := map[string]interface{}{}
err = client2.SetPosition(context.Background(), 0, extra)
test.That(t, err, test.ShouldNotBeNil)
test.That(t, err.Error(), test.ShouldContainSubstring, errCantSetPosition.Error())
test.That(t, switchName, test.ShouldEqual, failSwitchName)

_, err = client2.GetPosition(context.Background(), extra)
test.That(t, err, test.ShouldNotBeNil)
test.That(t, err.Error(), test.ShouldContainSubstring, errCantGetPosition.Error())

_, err = client2.GetNumberOfPositions(context.Background(), extra)
test.That(t, err, test.ShouldNotBeNil)
test.That(t, err.Error(), test.ShouldContainSubstring, errCantGetNumberOfPositions.Error())

test.That(t, client2.Close(context.Background()), test.ShouldBeNil)
test.That(t, conn.Close(), test.ShouldBeNil)
})
}
91 changes: 91 additions & 0 deletions components/switch/fake/switch.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
// Package fake implements fake switches with different position counts.
package fake

import (
"context"
"fmt"
"sync"

toggleswitch "go.viam.com/rdk/components/switch"
"go.viam.com/rdk/logging"
"go.viam.com/rdk/resource"
)

var model = resource.DefaultModelFamily.WithModel("fake")

// Config is the config for a fake switch.
type Config struct {
resource.TriviallyValidateConfig

// PositionCount is the number of positions that the switch can be in.
// If omitted, the switch will have two positions.
PositionCount *uint32 `json:"position_count"`
}

func init() {
// Register all three switch models
resource.RegisterComponent(toggleswitch.API, model, resource.Registration[toggleswitch.Switch, *Config]{
Constructor: NewSwitch,
})
}

// Switch is a fake switch that can be set to different positions.
type Switch struct {
resource.Named
resource.TriviallyCloseable
resource.AlwaysRebuild
mu sync.Mutex
logger logging.Logger
position uint32
positionCount uint32
}

// NewSwitch instantiates a new switch of the fake model type.
func NewSwitch(
ctx context.Context,
deps resource.Dependencies,
conf resource.Config,
logger logging.Logger,
) (toggleswitch.Switch, error) {
s := &Switch{
Named: conf.ResourceName().AsNamed(),
logger: logger,
position: 0,
positionCount: 2,
}

newConf, err := resource.NativeConfig[*Config](conf)
if err != nil {
return nil, err
}

if newConf.PositionCount != nil {
s.positionCount = *newConf.PositionCount
}

return s, nil
}

// SetPosition sets the switch to the specified position.
func (s *Switch) SetPosition(ctx context.Context, position uint32, extra map[string]interface{}) error {
s.mu.Lock()
defer s.mu.Unlock()

if position >= s.positionCount {
return fmt.Errorf("switch component %v position %d is invalid (valid range: 0-%d)", s.Name(), position, s.positionCount-1)
}
s.position = position
return nil
}

// GetPosition returns the current position of the switch.
func (s *Switch) GetPosition(ctx context.Context, extra map[string]interface{}) (uint32, error) {
s.mu.Lock()
defer s.mu.Unlock()
return s.position, nil
}

// GetNumberOfPositions returns the total number of valid positions for this switch.
func (s *Switch) GetNumberOfPositions(ctx context.Context, extra map[string]interface{}) (uint32, error) {
return s.positionCount, nil
}
31 changes: 31 additions & 0 deletions components/switch/fake/switch_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package fake_test

import (
"context"
"testing"

"go.viam.com/test"

toggleswitch "go.viam.com/rdk/components/switch"
"go.viam.com/rdk/components/switch/fake"
"go.viam.com/rdk/logging"
"go.viam.com/rdk/resource"
)

func TestSwitch(t *testing.T) {
logger := logging.NewTestLogger(t)
positionCount := uint32(3)
cfg := resource.Config{
Name: "fakeSwitch",
API: toggleswitch.API,
ConvertedAttributes: &fake.Config{
PositionCount: &positionCount,
},
}
s, err := fake.NewSwitch(context.Background(), nil, cfg, logger)
test.That(t, err, test.ShouldBeNil)

n, err := s.GetNumberOfPositions(context.Background(), nil)
test.That(t, err, test.ShouldBeNil)
test.That(t, n, test.ShouldEqual, positionCount)
}
7 changes: 7 additions & 0 deletions components/switch/register/register.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
// Package register registers all relevant switches and also API specific functions
package register

import (
// for switches.
_ "go.viam.com/rdk/components/switch/fake"
)
Loading

0 comments on commit 85e76a9

Please sign in to comment.