Skip to content

Commit

Permalink
APP-7497: Add Switch client, server, and fake model
Browse files Browse the repository at this point in the history
  • Loading branch information
ethanlookpotts committed Jan 24, 2025
1 parent 6b4f06f commit 904d410
Show file tree
Hide file tree
Showing 9 changed files with 645 additions and 0 deletions.
2 changes: 2 additions & 0 deletions components/register/all.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,11 @@ import (
_ "go.viam.com/rdk/components/input/register"
_ "go.viam.com/rdk/components/motor/register"
_ "go.viam.com/rdk/components/movementsensor/register"

// register APIs without implementations directly.
_ "go.viam.com/rdk/components/posetracker"
_ "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 switch_component contains a gRPC based switch client.
package switch_component

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{}) (int, 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 int(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)
}
146 changes: 146 additions & 0 deletions components/switch/client_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
package switch_component_test

import (
"context"
"net"
"testing"

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

switch_component "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.Switch{}
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
return 0, nil
}
injectSwitch.GetNumberOfPositionsFunc = func(ctx context.Context, extra map[string]interface{}) (int, error) {
extraOptions = extra
return 2, nil
}

injectSwitch2 := &inject.Switch{}
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) {
return 0, errCantGetPosition
}
injectSwitch2.GetNumberOfPositionsFunc = func(ctx context.Context, extra map[string]interface{}) (int, error) {
return 0, errCantGetNumberOfPositions
}

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

injectSwitch.DoFunc = testutils.EchoFunc

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 := switch_component.NewClientFromConn(context.Background(), conn, "", switch_component.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, "", switch_component.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)
})
}
108 changes: 108 additions & 0 deletions components/switch/fake/switch.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
// Package fake implements fake switches with different position counts.
package fake

import (
"context"
"fmt"
"sync"

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

var (
model2Way = resource.DefaultModelFamily.WithModel("fake-2way")
model3Way = resource.DefaultModelFamily.WithModel("fake-3way")
model10Way = resource.DefaultModelFamily.WithModel("fake-10way")
)

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

func init() {
// Register all three switch models
resource.RegisterComponent(switch_component.API, model2Way, resource.Registration[switch_component.Switch, *Config]{
Constructor: func(ctx context.Context, deps resource.Dependencies, conf resource.Config, logger logging.Logger) (switch_component.Switch, error) {
return NewSwitch(ctx, deps, conf, logger, 2)
},
})
resource.RegisterComponent(switch_component.API, model3Way, resource.Registration[switch_component.Switch, *Config]{
Constructor: func(ctx context.Context, deps resource.Dependencies, conf resource.Config, logger logging.Logger) (switch_component.Switch, error) {
return NewSwitch(ctx, deps, conf, logger, 3)
},
})
resource.RegisterComponent(switch_component.API, model10Way, resource.Registration[switch_component.Switch, *Config]{
Constructor: func(ctx context.Context, deps resource.Dependencies, conf resource.Config, logger logging.Logger) (switch_component.Switch, error) {
return NewSwitch(ctx, deps, conf, logger, 10)
},
})
}

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

// NewSwitch instantiates a new switch of the fake model type.
func NewSwitch(
ctx context.Context,
deps resource.Dependencies,
conf resource.Config,
logger logging.Logger,
positionCount int,
) (switch_component.Switch, error) {
s := &Switch{
Named: conf.ResourceName().AsNamed(),
logger: logger,
position: 0,
positionCount: positionCount,
}
if err := s.Reconfigure(ctx, deps, conf); err != nil {
return nil, err
}
return s, nil
}

// Reconfigure reconfigures the switch atomically and in place.
func (s *Switch) Reconfigure(_ context.Context, _ resource.Dependencies, conf resource.Config) error {
s.mu.Lock()
defer s.mu.Unlock()
return 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 >= uint32(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{}) (int, error) {
return s.positionCount, nil
}

// DoCommand executes a command on the switch.
func (s *Switch) DoCommand(ctx context.Context, cmd map[string]interface{}) (map[string]interface{}, error) {
return cmd, nil
}
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 904d410

Please sign in to comment.