-
Notifications
You must be signed in to change notification settings - Fork 111
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
APP-7497: Add Button client, server, and fake model
- Loading branch information
1 parent
1c4d115
commit 6b4f06f
Showing
11 changed files
with
486 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" | ||
|
||
"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) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
}) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} |
Oops, something went wrong.