Skip to content

Commit

Permalink
APP-7497: Add Button 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 1c4d115 commit 6b4f06f
Show file tree
Hide file tree
Showing 11 changed files with 486 additions and 0 deletions.
50 changes: 50 additions & 0 deletions components/button/button.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
// Package button defines a button on your machine.
package button

import (
"context"

pb "go.viam.com/api/component/button/v1"

Check failure on line 7 in components/button/button.go

View workflow job for this annotation

GitHub Actions / macos / build

no required module provides package go.viam.com/api/component/button/v1; to add it:

"go.viam.com/rdk/resource"
"go.viam.com/rdk/robot"
)

func init() {
resource.RegisterAPI(API, resource.APIRegistration[Button]{
RPCServiceServerConstructor: NewRPCServiceServer,
RPCServiceHandler: pb.RegisterButtonServiceHandlerFromEndpoint,
RPCServiceDesc: &pb.ButtonService_ServiceDesc,
RPCClient: NewClientFromConn,
})
}

// SubtypeName is a constant that identifies the component resource API string.
const SubtypeName = "button"

// API is a variable that identifies the component resource API.
var API = resource.APINamespaceRDK.WithComponentType(SubtypeName)

// Named is a helper for getting the named grippers's typed resource name.
func Named(name string) resource.Name {
return resource.NewName(API, name)
}

// A Button represents a physical button.
type Button interface {
resource.Resource

// Push pushes the button.
// This will block until done or a new operation cancels this one.
Push(ctx context.Context, extra map[string]interface{}) error
}

// FromRobot is a helper for getting the named Button from the given Robot.
func FromRobot(r robot.Robot, name string) (Button, error) {
return robot.ResourceFromRobot[Button](r, Named(name))
}

// NamesFromRobot is a helper for getting all gripper names from the given Robot.
func NamesFromRobot(r robot.Robot) []string {
return robot.NamesByAPI(r, API)
}
31 changes: 31 additions & 0 deletions components/button/button_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package button_test

import (
"context"
"testing"

"go.viam.com/test"

"go.viam.com/rdk/components/button"
"go.viam.com/rdk/components/button/fake"
"go.viam.com/rdk/resource"
)

const (
testButtonName = "button1"
testButtonName2 = "button2"
failButtonName = "button3"
missingButtonName = "button4"
)

func TestPush(t *testing.T) {
cfg := resource.Config{
Name: "fakeButton",
API: button.API,
}
button, err := fake.NewButton(context.Background(), nil, cfg, nil)
test.That(t, err, test.ShouldBeNil)

err = button.Push(context.Background(), nil)
test.That(t, err, test.ShouldBeNil)
}
57 changes: 57 additions & 0 deletions components/button/client.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
// Package button contains a gRPC based button client.
package button

import (
"context"

pb "go.viam.com/api/component/button/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 GripperServiceClient.
type client struct {
resource.Named
resource.TriviallyReconfigurable
resource.TriviallyCloseable
name string
client pb.ButtonServiceClient
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,
) (Button, error) {
c := pb.NewButtonServiceClient(conn)
return &client{
Named: name.PrependRemote(remoteName).AsNamed(),
name: name.ShortName(),
client: c,
logger: logger,
}, nil
}

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

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

import (
"context"
"net"
"testing"

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

"go.viam.com/rdk/components/button"
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"
)

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 buttonPushed string
var extraOptions map[string]interface{}

injectButton := &inject.Button{}
injectButton.PushFunc = func(ctx context.Context, extra map[string]interface{}) error {
extraOptions = extra
buttonPushed = testButtonName
return nil
}

injectButton2 := &inject.Button{}
injectButton2.PushFunc = func(ctx context.Context, extra map[string]interface{}) error {
buttonPushed = failButtonName
return errCantPush
}

buttonSvc, err := resource.NewAPIResourceCollection(
button.API,
map[resource.Name]button.Button{button.Named(testButtonName): injectButton, button.Named(failButtonName): injectButton2})
test.That(t, err, test.ShouldBeNil)
resourceAPI, ok, err := resource.LookupAPIRegistration[button.Button](button.API)
test.That(t, err, test.ShouldBeNil)
test.That(t, ok, test.ShouldBeTrue)
test.That(t, resourceAPI.RegisterRPCService(context.Background(), rpcServer, buttonSvc), test.ShouldBeNil)

injectButton.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("button client 1", func(t *testing.T) {
conn, err := viamgrpc.Dial(context.Background(), listener1.Addr().String(), logger)
test.That(t, err, test.ShouldBeNil)
button1Client, err := button.NewClientFromConn(context.Background(), conn, "", button.Named(testButtonName), logger)
test.That(t, err, test.ShouldBeNil)

// DoCommand
resp, err := button1Client.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": "Push"}
err = button1Client.Push(context.Background(), extra)
test.That(t, err, test.ShouldBeNil)
test.That(t, extraOptions, test.ShouldResemble, extra)
test.That(t, buttonPushed, test.ShouldEqual, testButtonName)

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

t.Run("button 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, "", button.Named(failButtonName), logger)
test.That(t, err, test.ShouldBeNil)

extra := map[string]interface{}{}
err = client2.Push(context.Background(), extra)
test.That(t, err, test.ShouldNotBeNil)
test.That(t, err.Error(), test.ShouldContainSubstring, errCantPush.Error())
test.That(t, buttonPushed, test.ShouldEqual, failButtonName)

test.That(t, client2.Close(context.Background()), test.ShouldBeNil)
test.That(t, conn.Close(), test.ShouldBeNil)
})
}
58 changes: 58 additions & 0 deletions components/button/fake/button.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
// Package fake implements a fake button.
package fake

import (
"context"
"sync"

"go.viam.com/rdk/components/button"
"go.viam.com/rdk/logging"
"go.viam.com/rdk/resource"
)

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

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

func init() {
resource.RegisterComponent(button.API, model, resource.Registration[button.Button, *Config]{Constructor: NewButton})
}

// Button is a fake button that logs when it is pressed
type Button struct {
resource.Named
resource.TriviallyCloseable
mu sync.Mutex
logger logging.Logger
}

// NewButton instantiates a new button of the fake model type.
func NewButton(
ctx context.Context, deps resource.Dependencies, conf resource.Config, logger logging.Logger,
) (button.Button, error) {
b := &Button{
Named: conf.ResourceName().AsNamed(),
logger: logger,
}
if err := b.Reconfigure(ctx, deps, conf); err != nil {
return nil, err
}
return b, nil
}

// Reconfigure reconfigures the button atomically and in place.
func (b *Button) Reconfigure(_ context.Context, _ resource.Dependencies, conf resource.Config) error {
b.mu.Lock()
defer b.mu.Unlock()

return nil
}

// Push logs the push
func (b *Button) Push(ctx context.Context, extra map[string]interface{}) error {
b.logger.Info("pushed button")
return nil
}
7 changes: 7 additions & 0 deletions components/button/register/register.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
// Package register registers all relevant buttons and also API specific functions
package register

import (
// for buttons.
_ "go.viam.com/rdk/components/button/fake"
)
44 changes: 44 additions & 0 deletions components/button/server.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
// Package button contains a gRPC based button service server.
package button

import (
"context"

commonpb "go.viam.com/api/common/v1"
pb "go.viam.com/api/component/button/v1"

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

// serviceServer implements the ButtonService from button.proto.
type serviceServer struct {
pb.UnimplementedButtonServiceServer
coll resource.APIResourceCollection[Button]
}

// NewRPCServiceServer constructs an gripper gRPC service server.
// It is intentionally untyped to prevent use outside of tests.
func NewRPCServiceServer(coll resource.APIResourceCollection[Button]) interface{} {
return &serviceServer{coll: coll}
}

// Pushes a button
func (s *serviceServer) Push(ctx context.Context, req *pb.PushRequest) (*pb.PushResponse, error) {
button, err := s.coll.Resource(req.Name)
if err != nil {
return nil, err
}
return &pb.PushResponse{}, button.Push(ctx, req.Extra.AsMap())
}

// DoCommand receives arbitrary commands.
func (s *serviceServer) DoCommand(ctx context.Context,
req *commonpb.DoCommandRequest,
) (*commonpb.DoCommandResponse, error) {
gripper, err := s.coll.Resource(req.GetName())
if err != nil {
return nil, err
}
return protoutils.DoFromResourceServer(ctx, gripper, req)
}
Loading

0 comments on commit 6b4f06f

Please sign in to comment.