Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

APP-7497: Add Button client, server, and fake model #4740

Merged
merged 5 commits into from
Jan 29, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 55 additions & 0 deletions components/button/button.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
// Package button defines a button on your machine.
package button

import (
"context"

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

"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))
}

// FromDependencies is a helper for getting the named button component from a collection of dependencies.
func FromDependencies(deps resource.Dependencies, name string) (Button, error) {
return resource.FromDependencies[Button](deps, 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)
}
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)
}
110 changes: 110 additions & 0 deletions components/button/client_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
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"
)

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

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.NewButton(testButtonName)
injectButton.PushFunc = func(ctx context.Context, extra map[string]interface{}) error {
extraOptions = extra
buttonPushed = testButtonName
return nil
}

injectButton2 := inject.NewButton(failButtonName)
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)
})
}
41 changes: 41 additions & 0 deletions components/button/fake/button.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
// Package fake implements a fake button.
package fake

import (
"context"

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

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

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

// Button is a fake button that logs when it is pressed.
type Button struct {
resource.Named
resource.TriviallyCloseable
resource.AlwaysRebuild
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,
}
return b, nil
}

// Push logs the push.
func (b *Button) Push(ctx context.Context, extra map[string]interface{}) error {
b.logger.Info("pushed button")
return nil
}
26 changes: 26 additions & 0 deletions components/button/fake/button_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package fake_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/logging"
"go.viam.com/rdk/resource"
)

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

err = button.Push(context.Background(), nil)
test.That(t, err, test.ShouldBeNil)
}
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) {
button, err := s.coll.Resource(req.GetName())
if err != nil {
return nil, err
}
return protoutils.DoFromResourceServer(ctx, button, req)
}
Loading
Loading