From 7edb29fc45e093b5253284b3a4829b57389d4480 Mon Sep 17 00:00:00 2001 From: angelapredolac Date: Wed, 10 Jun 2026 11:21:50 -0400 Subject: [PATCH 1/7] wired UploadDataFromPath through RobotService --- cmd/dumpcapture/main.go | 49 ++++++++++++++++++++++++++++++++++ go.mod | 2 ++ go.sum | 2 -- robot/client/client.go | 20 ++++++++++++++ robot/client/client_test.go | 49 ++++++++++++++++++++++++++++++++++ robot/impl/local_robot.go | 27 +++++++++++++++++++ robot/impl/local_robot_test.go | 30 +++++++++++++++++++++ robot/robot.go | 5 ++++ robot/server/server.go | 17 ++++++++++++ robot/server/server_test.go | 36 +++++++++++++++++++++++++ testutils/inject/robot.go | 18 +++++++++++++ 11 files changed, 253 insertions(+), 2 deletions(-) create mode 100644 cmd/dumpcapture/main.go diff --git a/cmd/dumpcapture/main.go b/cmd/dumpcapture/main.go new file mode 100644 index 00000000000..25bfdc53db6 --- /dev/null +++ b/cmd/dumpcapture/main.go @@ -0,0 +1,49 @@ +// Command dumpcapture prints the metadata and decoded SensorData payloads of a +// .capture/.prog file so you can see exactly what the data manager captured. +// +// Usage: +// +// go run ./cmd/dumpcapture /path/to/file.capture +package main + +import ( + "fmt" + "os" + + "google.golang.org/protobuf/encoding/protojson" + + "go.viam.com/rdk/data" +) + +func main() { + if len(os.Args) < 2 || os.Args[1] == "" { + fmt.Println("usage: go run ./cmd/dumpcapture ") + os.Exit(2) + } + + f, err := os.Open(os.Args[1]) + if err != nil { + panic(err) + } + defer f.Close() + + cf, err := data.ReadCaptureFile(f) + if err != nil { + panic(err) + } + + fmt.Println("=== METADATA ===") + fmt.Println(protojson.Format(cf.ReadMetadata())) + + fmt.Println("=== SENSOR DATA PAYLOADS ===") + i := 0 + for { + sd, err := cf.ReadNext() + if err != nil { + break // EOF + } + i++ + fmt.Printf("--- reading %d ---\n%s\n", i, protojson.Format(sd)) + } + fmt.Printf("total readings: %d\n", i) +} diff --git a/go.mod b/go.mod index 45b9c5d02f9..235654bab45 100644 --- a/go.mod +++ b/go.mod @@ -351,3 +351,5 @@ require ( github.com/ziutek/mymysql v1.5.4 // indirect golang.org/x/exp v0.0.0-20251113190631-e25ba8c21ef6 ) + +replace go.viam.com/api => ../api diff --git a/go.sum b/go.sum index 4a50ad7f762..eb8669b1c7d 100644 --- a/go.sum +++ b/go.sum @@ -1158,8 +1158,6 @@ go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM= go.uber.org/zap v1.18.1/go.mod h1:xg/QME4nWcxGxrpdeYfq7UvYrLh66cuVKdrbD1XF/NI= go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= -go.viam.com/api v0.1.555 h1:ncBlAwDpEk658aouQLoeq1RLDqdi3spWVFN0y6nsvXM= -go.viam.com/api v0.1.555/go.mod h1:nVe4WXrtc8aupJ8OWXSYx6KhCiOkr3VCbkwxD4D41xQ= go.viam.com/test v1.2.4 h1:JYgZhsuGAQ8sL9jWkziAXN9VJJiKbjoi9BsO33TW3ug= go.viam.com/test v1.2.4/go.mod h1:zI2xzosHdqXAJ/kFqcN+OIF78kQuTV2nIhGZ8EzvaJI= go.viam.com/utils v0.6.1 h1:xJhq+S2ADMTDj9538blmhI0otZCRAZUonRBkb+zHIHo= diff --git a/robot/client/client.go b/robot/client/client.go index b87837975da..608a250d702 100644 --- a/robot/client/client.go +++ b/robot/client/client.go @@ -24,6 +24,7 @@ import ( "go.uber.org/multierr" "go.uber.org/zap" "go.uber.org/zap/zapcore" + datasyncpb "go.viam.com/api/app/datasync/v1" commonpb "go.viam.com/api/common/v1" pb "go.viam.com/api/robot/v1" "go.viam.com/utils" @@ -1352,6 +1353,25 @@ func (rc *RobotClient) SendTraces(ctx context.Context, spans []*otlpv1.ResourceS return err } +// UploadDataFromPath uploads a file or directory from the robot to the cloud via the data manager. +func (rc *RobotClient) UploadDataFromPath(ctx context.Context, path string, md *datasyncpb.UploadMetadata) ( + uint64, uint64, uint64, uint64, []string, error, +) { + resp, err := rc.client.UploadDataFromPath(ctx, &pb.UploadDataFromPathRequest{ + Path: path, + UploadMetadata: md, + }) + if err != nil { + return 0, 0, 0, 0, nil, err + } + return resp.GetFilesUploaded(), + resp.GetFilesFailed(), + resp.GetBytesUploaded(), + resp.GetBytesTotal(), + resp.GetIds(), + nil +} + // Tunnel tunnels data to/from the read writer from/to the destination port on the server. This // function will close the connection passed in as part of cleanup. func (rc *RobotClient) Tunnel(ctx context.Context, conn io.ReadWriteCloser, dest int) error { diff --git a/robot/client/client_test.go b/robot/client/client_test.go index 46dff348647..a8e92a60e67 100644 --- a/robot/client/client_test.go +++ b/robot/client/client_test.go @@ -20,6 +20,7 @@ import ( "github.com/google/uuid" "github.com/jhump/protoreflect/grpcreflect" "go.uber.org/zap/zapcore" + datasyncpb "go.viam.com/api/app/datasync/v1" commonpb "go.viam.com/api/common/v1" armpb "go.viam.com/api/component/arm/v1" basepb "go.viam.com/api/component/base/v1" @@ -2363,3 +2364,51 @@ func TestListTunnels(t *testing.T) { test.That(t, err, test.ShouldBeNil) test.That(t, ttes, test.ShouldResemble, expectedTTEs) } + +func TestUploadDataFromPath(t *testing.T) { + logger := logging.NewTestLogger(t) + listener, err := net.Listen("tcp", "localhost:0") + test.That(t, err, test.ShouldBeNil) + gServer := grpc.NewServer() + + var capturedPath string + var capturedMD *datasyncpb.UploadMetadata + injectRobot := &inject.Robot{ + ResourceNamesFunc: func() []resource.Name { return nil }, + ResourceRPCAPIsFunc: func() []resource.RPCAPI { return nil }, + MachineStatusFunc: func(ctx context.Context) (robot.MachineStatus, error) { + return robot.MachineStatus{State: robot.StateRunning}, nil + }, + UploadDataFromPathFunc: func( + ctx context.Context, path string, md *datasyncpb.UploadMetadata, + ) (uint64, uint64, uint64, uint64, []string, error) { + capturedPath = path + capturedMD = md + return 2, 0, 512, 512, []string{"a", "b"}, nil + }, + } + + pb.RegisterRobotServiceServer(gServer, server.New(injectRobot)) + + go gServer.Serve(listener) + defer gServer.Stop() + + client, err := New(context.Background(), listener.Addr().String(), logger) + test.That(t, err, test.ShouldBeNil) + defer func() { + test.That(t, client.Close(context.Background()), test.ShouldBeNil) + }() + + md := &datasyncpb.UploadMetadata{Tags: []string{"tag1"}} + fu, ff, bu, bt, ids, err := client.UploadDataFromPath(context.Background(), "/data/foo", md) + test.That(t, err, test.ShouldBeNil) + test.That(t, fu, test.ShouldEqual, uint64(2)) + test.That(t, ff, test.ShouldEqual, uint64(0)) + test.That(t, bu, test.ShouldEqual, uint64(512)) + test.That(t, bt, test.ShouldEqual, uint64(512)) + test.That(t, ids, test.ShouldResemble, []string{"a", "b"}) + + // request fields actually crossed the wire + test.That(t, capturedPath, test.ShouldEqual, "/data/foo") + test.That(t, capturedMD.GetTags(), test.ShouldResemble, []string{"tag1"}) +} diff --git a/robot/impl/local_robot.go b/robot/impl/local_robot.go index 131dbdb5162..1518795d9c0 100644 --- a/robot/impl/local_robot.go +++ b/robot/impl/local_robot.go @@ -27,6 +27,7 @@ import ( semconv "go.opentelemetry.io/otel/semconv/v1.37.0" otlpv1 "go.opentelemetry.io/proto/otlp/trace/v1" "go.uber.org/multierr" + datasyncpb "go.viam.com/api/app/datasync/v1" packagespb "go.viam.com/api/app/packages/v1" goutils "go.viam.com/utils" "go.viam.com/utils/perf" @@ -58,6 +59,7 @@ import ( "go.viam.com/rdk/robot/packages" "go.viam.com/rdk/robot/web" weboptions "go.viam.com/rdk/robot/web/options" + "go.viam.com/rdk/services/datamanager" "go.viam.com/rdk/session" "go.viam.com/rdk/utils" ) @@ -171,6 +173,31 @@ func (r *localRobot) WriteTraceMessages(ctx context.Context, spans []*otlpv1.Res return err } +// dataFromPathUploader is the subset of the data manager service used by UploadDataFromPath. +type dataFromPathUploader interface { + UploadDataFromPath(ctx context.Context, path string, uploadMetadata *datasyncpb.UploadMetadata) ( + uint64, uint64, uint64, uint64, []string, error) +} + +// UploadDataFromPath uploads a file or directory to the cloud via the configured data manager service. +func (r *localRobot) UploadDataFromPath(ctx context.Context, path string, md *datasyncpb.UploadMetadata) ( + uint64, uint64, uint64, uint64, []string, error, +) { + names := datamanager.NamesFromRobot(r) + if len(names) == 0 { + return 0, 0, 0, 0, nil, errors.New("no data manager service configured") + } + svc, err := datamanager.FromProvider(r, names[0]) + if err != nil { + return 0, 0, 0, 0, nil, err + } + uploader, ok := svc.(dataFromPathUploader) + if !ok { + return 0, 0, 0, 0, nil, errors.New("data manager does not support UploadDataFromPath") + } + return uploader.UploadDataFromPath(ctx, path, md) +} + // FindBySimpleNameAndAPI finds a resource by its simple name and API. This is queried // through the resourceGetterForAPI for _all_ incoming gRPC requests related to a // resource. A nil resource and an error is returned in the case of no resource found, or diff --git a/robot/impl/local_robot_test.go b/robot/impl/local_robot_test.go index d21ff01fc7f..fe4707c163e 100644 --- a/robot/impl/local_robot_test.go +++ b/robot/impl/local_robot_test.go @@ -3251,6 +3251,36 @@ func TestCloudMetadata(t *testing.T) { }) } +func TestUploadDataFromPath(t *testing.T) { + logger := logging.NewTestLogger(t) + ctx := context.Background() + + t.Run("no data manager configured", func(t *testing.T) { + r := setupLocalRobot(t, ctx, &config.Config{}, logger) + _, _, _, _, _, err := r.UploadDataFromPath(ctx, "/tmp/whatever", nil) + test.That(t, err, test.ShouldNotBeNil) + test.That(t, err.Error(), test.ShouldContainSubstring, "no data manager") + }) + + t.Run("data manager does not support upload", func(t *testing.T) { + cfg := &config.Config{ + Services: []resource.Config{ + { + Name: "dm", + API: datamanager.API, + Model: resource.DefaultServiceModel, + ConvertedAttributes: &builtin.Config{}, + DependsOn: []string{internalcloud.InternalServiceName.String()}, + }, + }, + } + r := setupLocalRobot(t, ctx, cfg, logger) + _, _, _, _, _, err := r.UploadDataFromPath(ctx, "/tmp/whatever", nil) + test.That(t, err, test.ShouldNotBeNil) + test.That(t, err.Error(), test.ShouldContainSubstring, "does not support") + }) +} + func TestReconfigureOnModuleRename(t *testing.T) { ctx := context.Background() logger := logging.NewTestLogger(t) diff --git a/robot/robot.go b/robot/robot.go index ff26a5532d7..c87d88de7a3 100644 --- a/robot/robot.go +++ b/robot/robot.go @@ -12,6 +12,7 @@ import ( "github.com/jhump/protoreflect/dynamic" "github.com/pkg/errors" otlpv1 "go.opentelemetry.io/proto/otlp/trace/v1" + datasyncpb "go.viam.com/api/app/datasync/v1" "go.viam.com/rdk/cloud" "go.viam.com/rdk/config" @@ -196,6 +197,10 @@ type LocalRobot interface { // WriteTraceMessages writes trace spans to any configured exporters. WriteTraceMessages(context.Context, []*otlpv1.ResourceSpans) error + + // UploadDataFromPath uploads a file or directory at path to the cloud via the data manager. + UploadDataFromPath(ctx context.Context, path string, uploadMetadata *datasyncpb.UploadMetadata) ( + filesUploaded, filesFailed, bytesUploaded, bytesTotal uint64, ids []string, err error) } // A RemoteRobot is a Robot that was created through a connection. diff --git a/robot/server/server.go b/robot/server/server.go index 3a16a16087c..a516a224d36 100644 --- a/robot/server/server.go +++ b/robot/server/server.go @@ -70,6 +70,23 @@ func (s *Server) SendTraces(ctx context.Context, req *pb.SendTracesRequest) (*pb return nil, s.robot.WriteTraceMessages(ctx, req.ResourceSpans) } +// UploadDataFromPath uploads a file or directory from the robot to the cloud via the data manager. +func (s *Server) UploadDataFromPath(ctx context.Context, req *pb.UploadDataFromPathRequest) ( + *pb.UploadDataFromPathResponse, error, +) { + fu, ff, bu, bt, ids, err := s.robot.UploadDataFromPath(ctx, req.GetPath(), req.GetUploadMetadata()) + if err != nil { + return nil, err + } + return &pb.UploadDataFromPathResponse{ + FilesUploaded: fu, + FilesFailed: ff, + BytesUploaded: bu, + BytesTotal: bt, + Ids: ids, + }, nil +} + // Tunnel tunnels traffic to/from the client from/to a specified port on the server. func (s *Server) Tunnel(srv pb.RobotService_TunnelServer) error { req, err := srv.Recv() diff --git a/robot/server/server_test.go b/robot/server/server_test.go index 74a2aa4de67..0d8b99da1b7 100644 --- a/robot/server/server_test.go +++ b/robot/server/server_test.go @@ -14,6 +14,7 @@ import ( "github.com/google/uuid" "github.com/jhump/protoreflect/grpcreflect" "go.uber.org/zap/zapcore" + datasyncpb "go.viam.com/api/app/datasync/v1" commonpb "go.viam.com/api/common/v1" armpb "go.viam.com/api/component/arm/v1" pb "go.viam.com/api/robot/v1" @@ -640,6 +641,41 @@ func TestServer(t *testing.T) { test.That(t, resp.GetVersion(), test.ShouldEqual, "dev-unknown") test.That(t, resp.GetApiVersion(), test.ShouldEqual, "?") }) + + t.Run("UploadDataFromPath", func(t *testing.T) { + injectRobot := &inject.Robot{} + server := server.New(injectRobot) + + // success: request fields reach the robot, and counts/ids map back through. + injectRobot.UploadDataFromPathFunc = func( + ctx context.Context, path string, md *datasyncpb.UploadMetadata, + ) (uint64, uint64, uint64, uint64, []string, error) { + test.That(t, path, test.ShouldEqual, "/data/foo") + test.That(t, md.GetTags(), test.ShouldResemble, []string{"tag1"}) + return 3, 1, 1024, 2048, []string{"id1", "id2", "id3"}, nil + } + + resp, err := server.UploadDataFromPath(context.Background(), &pb.UploadDataFromPathRequest{ + Path: "/data/foo", + UploadMetadata: &datasyncpb.UploadMetadata{Tags: []string{"tag1"}}, + }) + test.That(t, err, test.ShouldBeNil) + test.That(t, resp.GetFilesUploaded(), test.ShouldEqual, uint64(3)) + test.That(t, resp.GetFilesFailed(), test.ShouldEqual, uint64(1)) + test.That(t, resp.GetBytesUploaded(), test.ShouldEqual, uint64(1024)) + test.That(t, resp.GetBytesTotal(), test.ShouldEqual, uint64(2048)) + test.That(t, resp.GetIds(), test.ShouldResemble, []string{"id1", "id2", "id3"}) + + // error is surfaced to the caller. + injectRobot.UploadDataFromPathFunc = func( + ctx context.Context, path string, md *datasyncpb.UploadMetadata, + ) (uint64, uint64, uint64, uint64, []string, error) { + return 0, 0, 0, 0, nil, errors.New("no data manager service configured") + } + _, err = server.UploadDataFromPath(context.Background(), &pb.UploadDataFromPathRequest{Path: "/data/foo"}) + test.That(t, err, test.ShouldNotBeNil) + test.That(t, err.Error(), test.ShouldContainSubstring, "no data manager") + }) } func TestModuleLogTimestamp(t *testing.T) { diff --git a/testutils/inject/robot.go b/testutils/inject/robot.go index 01548bea4b9..38476a2540e 100644 --- a/testutils/inject/robot.go +++ b/testutils/inject/robot.go @@ -8,6 +8,7 @@ import ( "time" "github.com/google/uuid" + datasyncpb "go.viam.com/api/app/datasync/v1" "go.viam.com/utils/pexec" "go.viam.com/rdk/cloud" @@ -59,6 +60,11 @@ type Robot struct { MachineStatusFunc func(ctx context.Context) (robot.MachineStatus, error) ShutdownFunc func(ctx context.Context) error ListTunnelsFunc func(ctx context.Context) ([]config.TrafficTunnelEndpoint, error) + UploadDataFromPathFunc func( + ctx context.Context, + path string, + uploadMetadata *datasyncpb.UploadMetadata, + ) (uint64, uint64, uint64, uint64, []string, error) ops *operation.Manager SessMgr session.Manager @@ -373,6 +379,18 @@ func (r *Robot) ListTunnels(ctx context.Context) ([]config.TrafficTunnelEndpoint return r.ListTunnelsFunc(ctx) } +// UploadDataFromPath calls the injected UploadDataFromPath or the real one. +func (r *Robot) UploadDataFromPath(ctx context.Context, path string, uploadMetadata *datasyncpb.UploadMetadata) ( + uint64, uint64, uint64, uint64, []string, error, +) { + r.Mu.RLock() + defer r.Mu.RUnlock() + if r.UploadDataFromPathFunc == nil { + return r.LocalRobot.UploadDataFromPath(ctx, path, uploadMetadata) + } + return r.UploadDataFromPathFunc(ctx, path, uploadMetadata) +} + type noopSessionManager struct{} func (m noopSessionManager) Start(ctx context.Context, ownerID string) (*session.Session, error) { From 86461c00b44c65d8018f5f5c3d52027a72c9e0ae Mon Sep 17 00:00:00 2001 From: angelapredolac Date: Wed, 10 Jun 2026 11:43:06 -0400 Subject: [PATCH 2/7] updated go.mod/go.sum --- go.mod | 4 +--- go.sum | 2 ++ 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 235654bab45..24c3d74c00b 100644 --- a/go.mod +++ b/go.mod @@ -101,7 +101,7 @@ require ( go.uber.org/goleak v1.3.0 go.uber.org/multierr v1.11.0 go.uber.org/zap v1.27.0 - go.viam.com/api v0.1.555 + go.viam.com/api v0.1.559-0.20260609195308-d8b5e1f67e37 go.viam.com/test v1.2.4 go.viam.com/utils v0.6.1 goji.io v2.0.2+incompatible @@ -351,5 +351,3 @@ require ( github.com/ziutek/mymysql v1.5.4 // indirect golang.org/x/exp v0.0.0-20251113190631-e25ba8c21ef6 ) - -replace go.viam.com/api => ../api diff --git a/go.sum b/go.sum index eb8669b1c7d..25243919249 100644 --- a/go.sum +++ b/go.sum @@ -1158,6 +1158,8 @@ go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM= go.uber.org/zap v1.18.1/go.mod h1:xg/QME4nWcxGxrpdeYfq7UvYrLh66cuVKdrbD1XF/NI= go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +go.viam.com/api v0.1.559-0.20260609195308-d8b5e1f67e37 h1:Pbxumk5u7yRfEjcWG8S9/HljfpQNqDBnnsyiT/lN/QQ= +go.viam.com/api v0.1.559-0.20260609195308-d8b5e1f67e37/go.mod h1:nVe4WXrtc8aupJ8OWXSYx6KhCiOkr3VCbkwxD4D41xQ= go.viam.com/test v1.2.4 h1:JYgZhsuGAQ8sL9jWkziAXN9VJJiKbjoi9BsO33TW3ug= go.viam.com/test v1.2.4/go.mod h1:zI2xzosHdqXAJ/kFqcN+OIF78kQuTV2nIhGZ8EzvaJI= go.viam.com/utils v0.6.1 h1:xJhq+S2ADMTDj9538blmhI0otZCRAZUonRBkb+zHIHo= From 25fc5911fec1290c9871b8719235a1e0833fbfa4 Mon Sep 17 00:00:00 2001 From: angelapredolac Date: Wed, 10 Jun 2026 11:44:35 -0400 Subject: [PATCH 3/7] remove unrelated dumpcapture scratch file --- cmd/dumpcapture/main.go | 49 ----------------------------------------- 1 file changed, 49 deletions(-) delete mode 100644 cmd/dumpcapture/main.go diff --git a/cmd/dumpcapture/main.go b/cmd/dumpcapture/main.go deleted file mode 100644 index 25bfdc53db6..00000000000 --- a/cmd/dumpcapture/main.go +++ /dev/null @@ -1,49 +0,0 @@ -// Command dumpcapture prints the metadata and decoded SensorData payloads of a -// .capture/.prog file so you can see exactly what the data manager captured. -// -// Usage: -// -// go run ./cmd/dumpcapture /path/to/file.capture -package main - -import ( - "fmt" - "os" - - "google.golang.org/protobuf/encoding/protojson" - - "go.viam.com/rdk/data" -) - -func main() { - if len(os.Args) < 2 || os.Args[1] == "" { - fmt.Println("usage: go run ./cmd/dumpcapture ") - os.Exit(2) - } - - f, err := os.Open(os.Args[1]) - if err != nil { - panic(err) - } - defer f.Close() - - cf, err := data.ReadCaptureFile(f) - if err != nil { - panic(err) - } - - fmt.Println("=== METADATA ===") - fmt.Println(protojson.Format(cf.ReadMetadata())) - - fmt.Println("=== SENSOR DATA PAYLOADS ===") - i := 0 - for { - sd, err := cf.ReadNext() - if err != nil { - break // EOF - } - i++ - fmt.Printf("--- reading %d ---\n%s\n", i, protojson.Format(sd)) - } - fmt.Printf("total readings: %d\n", i) -} From 25dfb0de1307eb0ce45870b85ac558b946fc8860 Mon Sep 17 00:00:00 2001 From: angelapredolac Date: Fri, 12 Jun 2026 13:45:24 -0400 Subject: [PATCH 4/7] thread extra through UploadDataFromPath --- robot/client/client.go | 7 ++++++- robot/client/client_test.go | 8 ++++++-- robot/impl/local_robot.go | 6 +++--- robot/impl/local_robot_test.go | 4 ++-- robot/robot.go | 2 +- robot/server/server.go | 2 +- robot/server/server_test.go | 11 ++++++++--- testutils/inject/robot.go | 9 +++++---- 8 files changed, 32 insertions(+), 17 deletions(-) diff --git a/robot/client/client.go b/robot/client/client.go index 608a250d702..c925932b758 100644 --- a/robot/client/client.go +++ b/robot/client/client.go @@ -1354,12 +1354,17 @@ func (rc *RobotClient) SendTraces(ctx context.Context, spans []*otlpv1.ResourceS } // UploadDataFromPath uploads a file or directory from the robot to the cloud via the data manager. -func (rc *RobotClient) UploadDataFromPath(ctx context.Context, path string, md *datasyncpb.UploadMetadata) ( +func (rc *RobotClient) UploadDataFromPath(ctx context.Context, path string, md *datasyncpb.UploadMetadata, extra map[string]interface{}) ( uint64, uint64, uint64, uint64, []string, error, ) { + ext, err := protoutils.StructToStructPb(extra) + if err != nil { + return 0, 0, 0, 0, nil, err + } resp, err := rc.client.UploadDataFromPath(ctx, &pb.UploadDataFromPathRequest{ Path: path, UploadMetadata: md, + Extra: ext, }) if err != nil { return 0, 0, 0, 0, nil, err diff --git a/robot/client/client_test.go b/robot/client/client_test.go index a8e92a60e67..f12d8f9e755 100644 --- a/robot/client/client_test.go +++ b/robot/client/client_test.go @@ -2373,6 +2373,7 @@ func TestUploadDataFromPath(t *testing.T) { var capturedPath string var capturedMD *datasyncpb.UploadMetadata + var capturedExtra map[string]interface{} injectRobot := &inject.Robot{ ResourceNamesFunc: func() []resource.Name { return nil }, ResourceRPCAPIsFunc: func() []resource.RPCAPI { return nil }, @@ -2380,10 +2381,11 @@ func TestUploadDataFromPath(t *testing.T) { return robot.MachineStatus{State: robot.StateRunning}, nil }, UploadDataFromPathFunc: func( - ctx context.Context, path string, md *datasyncpb.UploadMetadata, + ctx context.Context, path string, md *datasyncpb.UploadMetadata, extra map[string]interface{}, ) (uint64, uint64, uint64, uint64, []string, error) { capturedPath = path capturedMD = md + capturedExtra = extra return 2, 0, 512, 512, []string{"a", "b"}, nil }, } @@ -2400,7 +2402,8 @@ func TestUploadDataFromPath(t *testing.T) { }() md := &datasyncpb.UploadMetadata{Tags: []string{"tag1"}} - fu, ff, bu, bt, ids, err := client.UploadDataFromPath(context.Background(), "/data/foo", md) + extra := map[string]interface{}{"foo": "bar"} + fu, ff, bu, bt, ids, err := client.UploadDataFromPath(context.Background(), "/data/foo", md, extra) test.That(t, err, test.ShouldBeNil) test.That(t, fu, test.ShouldEqual, uint64(2)) test.That(t, ff, test.ShouldEqual, uint64(0)) @@ -2411,4 +2414,5 @@ func TestUploadDataFromPath(t *testing.T) { // request fields actually crossed the wire test.That(t, capturedPath, test.ShouldEqual, "/data/foo") test.That(t, capturedMD.GetTags(), test.ShouldResemble, []string{"tag1"}) + test.That(t, capturedExtra, test.ShouldResemble, map[string]interface{}{"foo": "bar"}) } diff --git a/robot/impl/local_robot.go b/robot/impl/local_robot.go index 1518795d9c0..66f6be46721 100644 --- a/robot/impl/local_robot.go +++ b/robot/impl/local_robot.go @@ -175,12 +175,12 @@ func (r *localRobot) WriteTraceMessages(ctx context.Context, spans []*otlpv1.Res // dataFromPathUploader is the subset of the data manager service used by UploadDataFromPath. type dataFromPathUploader interface { - UploadDataFromPath(ctx context.Context, path string, uploadMetadata *datasyncpb.UploadMetadata) ( + UploadDataFromPath(ctx context.Context, path string, uploadMetadata *datasyncpb.UploadMetadata, extra map[string]interface{}) ( uint64, uint64, uint64, uint64, []string, error) } // UploadDataFromPath uploads a file or directory to the cloud via the configured data manager service. -func (r *localRobot) UploadDataFromPath(ctx context.Context, path string, md *datasyncpb.UploadMetadata) ( +func (r *localRobot) UploadDataFromPath(ctx context.Context, path string, md *datasyncpb.UploadMetadata, extra map[string]interface{}) ( uint64, uint64, uint64, uint64, []string, error, ) { names := datamanager.NamesFromRobot(r) @@ -195,7 +195,7 @@ func (r *localRobot) UploadDataFromPath(ctx context.Context, path string, md *da if !ok { return 0, 0, 0, 0, nil, errors.New("data manager does not support UploadDataFromPath") } - return uploader.UploadDataFromPath(ctx, path, md) + return uploader.UploadDataFromPath(ctx, path, md, extra) } // FindBySimpleNameAndAPI finds a resource by its simple name and API. This is queried diff --git a/robot/impl/local_robot_test.go b/robot/impl/local_robot_test.go index fe4707c163e..d5b8e1be1c0 100644 --- a/robot/impl/local_robot_test.go +++ b/robot/impl/local_robot_test.go @@ -3257,7 +3257,7 @@ func TestUploadDataFromPath(t *testing.T) { t.Run("no data manager configured", func(t *testing.T) { r := setupLocalRobot(t, ctx, &config.Config{}, logger) - _, _, _, _, _, err := r.UploadDataFromPath(ctx, "/tmp/whatever", nil) + _, _, _, _, _, err := r.UploadDataFromPath(ctx, "/tmp/whatever", nil, nil) test.That(t, err, test.ShouldNotBeNil) test.That(t, err.Error(), test.ShouldContainSubstring, "no data manager") }) @@ -3275,7 +3275,7 @@ func TestUploadDataFromPath(t *testing.T) { }, } r := setupLocalRobot(t, ctx, cfg, logger) - _, _, _, _, _, err := r.UploadDataFromPath(ctx, "/tmp/whatever", nil) + _, _, _, _, _, err := r.UploadDataFromPath(ctx, "/tmp/whatever", nil, nil) test.That(t, err, test.ShouldNotBeNil) test.That(t, err.Error(), test.ShouldContainSubstring, "does not support") }) diff --git a/robot/robot.go b/robot/robot.go index c87d88de7a3..2797f589009 100644 --- a/robot/robot.go +++ b/robot/robot.go @@ -199,7 +199,7 @@ type LocalRobot interface { WriteTraceMessages(context.Context, []*otlpv1.ResourceSpans) error // UploadDataFromPath uploads a file or directory at path to the cloud via the data manager. - UploadDataFromPath(ctx context.Context, path string, uploadMetadata *datasyncpb.UploadMetadata) ( + UploadDataFromPath(ctx context.Context, path string, uploadMetadata *datasyncpb.UploadMetadata, extra map[string]interface{}) ( filesUploaded, filesFailed, bytesUploaded, bytesTotal uint64, ids []string, err error) } diff --git a/robot/server/server.go b/robot/server/server.go index a516a224d36..0b8979d05e1 100644 --- a/robot/server/server.go +++ b/robot/server/server.go @@ -74,7 +74,7 @@ func (s *Server) SendTraces(ctx context.Context, req *pb.SendTracesRequest) (*pb func (s *Server) UploadDataFromPath(ctx context.Context, req *pb.UploadDataFromPathRequest) ( *pb.UploadDataFromPathResponse, error, ) { - fu, ff, bu, bt, ids, err := s.robot.UploadDataFromPath(ctx, req.GetPath(), req.GetUploadMetadata()) + fu, ff, bu, bt, ids, err := s.robot.UploadDataFromPath(ctx, req.GetPath(), req.GetUploadMetadata(), req.Extra.AsMap()) if err != nil { return nil, err } diff --git a/robot/server/server_test.go b/robot/server/server_test.go index 0d8b99da1b7..c77c6182078 100644 --- a/robot/server/server_test.go +++ b/robot/server/server_test.go @@ -646,18 +646,23 @@ func TestServer(t *testing.T) { injectRobot := &inject.Robot{} server := server.New(injectRobot) - // success: request fields reach the robot, and counts/ids map back through. + ext, err := utilsproto.StructToStructPb(map[string]interface{}{"foo": "bar"}) + test.That(t, err, test.ShouldBeNil) + + // success: request fields (incl. extra) reach the robot, and counts/ids map back through. injectRobot.UploadDataFromPathFunc = func( - ctx context.Context, path string, md *datasyncpb.UploadMetadata, + ctx context.Context, path string, md *datasyncpb.UploadMetadata, extra map[string]interface{}, ) (uint64, uint64, uint64, uint64, []string, error) { test.That(t, path, test.ShouldEqual, "/data/foo") test.That(t, md.GetTags(), test.ShouldResemble, []string{"tag1"}) + test.That(t, extra, test.ShouldResemble, map[string]interface{}{"foo": "bar"}) return 3, 1, 1024, 2048, []string{"id1", "id2", "id3"}, nil } resp, err := server.UploadDataFromPath(context.Background(), &pb.UploadDataFromPathRequest{ Path: "/data/foo", UploadMetadata: &datasyncpb.UploadMetadata{Tags: []string{"tag1"}}, + Extra: ext, }) test.That(t, err, test.ShouldBeNil) test.That(t, resp.GetFilesUploaded(), test.ShouldEqual, uint64(3)) @@ -668,7 +673,7 @@ func TestServer(t *testing.T) { // error is surfaced to the caller. injectRobot.UploadDataFromPathFunc = func( - ctx context.Context, path string, md *datasyncpb.UploadMetadata, + ctx context.Context, path string, md *datasyncpb.UploadMetadata, extra map[string]interface{}, ) (uint64, uint64, uint64, uint64, []string, error) { return 0, 0, 0, 0, nil, errors.New("no data manager service configured") } diff --git a/testutils/inject/robot.go b/testutils/inject/robot.go index 38476a2540e..7114560e586 100644 --- a/testutils/inject/robot.go +++ b/testutils/inject/robot.go @@ -63,7 +63,7 @@ type Robot struct { UploadDataFromPathFunc func( ctx context.Context, path string, - uploadMetadata *datasyncpb.UploadMetadata, + uploadMetadata *datasyncpb.UploadMetadata, extra map[string]interface{}, ) (uint64, uint64, uint64, uint64, []string, error) ops *operation.Manager @@ -380,15 +380,16 @@ func (r *Robot) ListTunnels(ctx context.Context) ([]config.TrafficTunnelEndpoint } // UploadDataFromPath calls the injected UploadDataFromPath or the real one. -func (r *Robot) UploadDataFromPath(ctx context.Context, path string, uploadMetadata *datasyncpb.UploadMetadata) ( +func (r *Robot) UploadDataFromPath(ctx context.Context, path string, uploadMetadata *datasyncpb.UploadMetadata, + extra map[string]interface{}) ( uint64, uint64, uint64, uint64, []string, error, ) { r.Mu.RLock() defer r.Mu.RUnlock() if r.UploadDataFromPathFunc == nil { - return r.LocalRobot.UploadDataFromPath(ctx, path, uploadMetadata) + return r.LocalRobot.UploadDataFromPath(ctx, path, uploadMetadata, extra) } - return r.UploadDataFromPathFunc(ctx, path, uploadMetadata) + return r.UploadDataFromPathFunc(ctx, path, uploadMetadata, extra) } type noopSessionManager struct{} From d90b6b4d56576f1c4866e244b38298e054e527fc Mon Sep 17 00:00:00 2001 From: angelapredolac Date: Fri, 12 Jun 2026 13:48:46 -0400 Subject: [PATCH 5/7] clarified comment for dataFromPathUploader --- robot/impl/local_robot.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/robot/impl/local_robot.go b/robot/impl/local_robot.go index 66f6be46721..0a25feabbdb 100644 --- a/robot/impl/local_robot.go +++ b/robot/impl/local_robot.go @@ -173,7 +173,8 @@ func (r *localRobot) WriteTraceMessages(ctx context.Context, spans []*otlpv1.Res return err } -// dataFromPathUploader is the subset of the data manager service used by UploadDataFromPath. +// dataFromPathUploader is the capability interface localRobot type-asserts the configured +// data manager service for when UploadDataFromPath is called. type dataFromPathUploader interface { UploadDataFromPath(ctx context.Context, path string, uploadMetadata *datasyncpb.UploadMetadata, extra map[string]interface{}) ( uint64, uint64, uint64, uint64, []string, error) From 2162db7c8c7e35296c91c88dac77730ae353de60 Mon Sep 17 00:00:00 2001 From: angelapredolac Date: Fri, 12 Jun 2026 14:08:32 -0400 Subject: [PATCH 6/7] created UploadFromPathResult struct --- robot/client/client.go | 30 ++++++++++++++++++++---------- robot/client/client_test.go | 12 ++++++------ 2 files changed, 26 insertions(+), 16 deletions(-) diff --git a/robot/client/client.go b/robot/client/client.go index c925932b758..39d0d425362 100644 --- a/robot/client/client.go +++ b/robot/client/client.go @@ -1353,13 +1353,22 @@ func (rc *RobotClient) SendTraces(ctx context.Context, spans []*otlpv1.ResourceS return err } +// UploadFromPathResult is the aggregated result of an UploadDataFromPath call. +type UploadFromPathResult struct { + FilesUploaded uint64 + FilesFailed uint64 + BytesUploaded uint64 + BytesTotal uint64 + IDs []string +} + // UploadDataFromPath uploads a file or directory from the robot to the cloud via the data manager. func (rc *RobotClient) UploadDataFromPath(ctx context.Context, path string, md *datasyncpb.UploadMetadata, extra map[string]interface{}) ( - uint64, uint64, uint64, uint64, []string, error, + UploadFromPathResult, error, ) { ext, err := protoutils.StructToStructPb(extra) if err != nil { - return 0, 0, 0, 0, nil, err + return UploadFromPathResult{}, err } resp, err := rc.client.UploadDataFromPath(ctx, &pb.UploadDataFromPathRequest{ Path: path, @@ -1367,14 +1376,15 @@ func (rc *RobotClient) UploadDataFromPath(ctx context.Context, path string, md * Extra: ext, }) if err != nil { - return 0, 0, 0, 0, nil, err - } - return resp.GetFilesUploaded(), - resp.GetFilesFailed(), - resp.GetBytesUploaded(), - resp.GetBytesTotal(), - resp.GetIds(), - nil + return UploadFromPathResult{}, err + } + return UploadFromPathResult{ + FilesUploaded: resp.GetFilesUploaded(), + FilesFailed: resp.GetFilesFailed(), + BytesUploaded: resp.GetBytesUploaded(), + BytesTotal: resp.GetBytesTotal(), + IDs: resp.GetIds(), + }, nil } // Tunnel tunnels data to/from the read writer from/to the destination port on the server. This diff --git a/robot/client/client_test.go b/robot/client/client_test.go index f12d8f9e755..778ab21560b 100644 --- a/robot/client/client_test.go +++ b/robot/client/client_test.go @@ -2403,13 +2403,13 @@ func TestUploadDataFromPath(t *testing.T) { md := &datasyncpb.UploadMetadata{Tags: []string{"tag1"}} extra := map[string]interface{}{"foo": "bar"} - fu, ff, bu, bt, ids, err := client.UploadDataFromPath(context.Background(), "/data/foo", md, extra) + res, err := client.UploadDataFromPath(context.Background(), "/data/foo", md, extra) test.That(t, err, test.ShouldBeNil) - test.That(t, fu, test.ShouldEqual, uint64(2)) - test.That(t, ff, test.ShouldEqual, uint64(0)) - test.That(t, bu, test.ShouldEqual, uint64(512)) - test.That(t, bt, test.ShouldEqual, uint64(512)) - test.That(t, ids, test.ShouldResemble, []string{"a", "b"}) + test.That(t, res.FilesUploaded, test.ShouldEqual, uint64(2)) + test.That(t, res.FilesFailed, test.ShouldEqual, uint64(0)) + test.That(t, res.BytesUploaded, test.ShouldEqual, uint64(512)) + test.That(t, res.BytesTotal, test.ShouldEqual, uint64(512)) + test.That(t, res.IDs, test.ShouldResemble, []string{"a", "b"}) // request fields actually crossed the wire test.That(t, capturedPath, test.ShouldEqual, "/data/foo") From a53e3d3e3622422a7740d19cad58846a2bd1cb31 Mon Sep 17 00:00:00 2001 From: angelapredolac Date: Mon, 15 Jun 2026 16:13:49 -0400 Subject: [PATCH 7/7] return UploadDataFromPathResult across the chain --- robot/client/client.go | 17 ++++------------- robot/client/client_test.go | 10 ++++++++-- robot/impl/local_robot.go | 10 +++++----- robot/impl/local_robot_test.go | 4 ++-- robot/robot.go | 11 ++++++++++- robot/server/server.go | 12 ++++++------ robot/server/server_test.go | 24 +++++++++++++++--------- testutils/inject/robot.go | 4 ++-- 8 files changed, 52 insertions(+), 40 deletions(-) diff --git a/robot/client/client.go b/robot/client/client.go index 39d0d425362..8b81cf5b031 100644 --- a/robot/client/client.go +++ b/robot/client/client.go @@ -1353,22 +1353,13 @@ func (rc *RobotClient) SendTraces(ctx context.Context, spans []*otlpv1.ResourceS return err } -// UploadFromPathResult is the aggregated result of an UploadDataFromPath call. -type UploadFromPathResult struct { - FilesUploaded uint64 - FilesFailed uint64 - BytesUploaded uint64 - BytesTotal uint64 - IDs []string -} - // UploadDataFromPath uploads a file or directory from the robot to the cloud via the data manager. func (rc *RobotClient) UploadDataFromPath(ctx context.Context, path string, md *datasyncpb.UploadMetadata, extra map[string]interface{}) ( - UploadFromPathResult, error, + robot.UploadDataFromPathResult, error, ) { ext, err := protoutils.StructToStructPb(extra) if err != nil { - return UploadFromPathResult{}, err + return robot.UploadDataFromPathResult{}, err } resp, err := rc.client.UploadDataFromPath(ctx, &pb.UploadDataFromPathRequest{ Path: path, @@ -1376,9 +1367,9 @@ func (rc *RobotClient) UploadDataFromPath(ctx context.Context, path string, md * Extra: ext, }) if err != nil { - return UploadFromPathResult{}, err + return robot.UploadDataFromPathResult{}, err } - return UploadFromPathResult{ + return robot.UploadDataFromPathResult{ FilesUploaded: resp.GetFilesUploaded(), FilesFailed: resp.GetFilesFailed(), BytesUploaded: resp.GetBytesUploaded(), diff --git a/robot/client/client_test.go b/robot/client/client_test.go index 778ab21560b..e0f88ff4d9e 100644 --- a/robot/client/client_test.go +++ b/robot/client/client_test.go @@ -2382,11 +2382,17 @@ func TestUploadDataFromPath(t *testing.T) { }, UploadDataFromPathFunc: func( ctx context.Context, path string, md *datasyncpb.UploadMetadata, extra map[string]interface{}, - ) (uint64, uint64, uint64, uint64, []string, error) { + ) (robot.UploadDataFromPathResult, error) { capturedPath = path capturedMD = md capturedExtra = extra - return 2, 0, 512, 512, []string{"a", "b"}, nil + return robot.UploadDataFromPathResult{ + FilesUploaded: 2, + FilesFailed: 0, + BytesUploaded: 512, + BytesTotal: 512, + IDs: []string{"a", "b"}, + }, nil }, } diff --git a/robot/impl/local_robot.go b/robot/impl/local_robot.go index 0a25feabbdb..08158a74e35 100644 --- a/robot/impl/local_robot.go +++ b/robot/impl/local_robot.go @@ -177,24 +177,24 @@ func (r *localRobot) WriteTraceMessages(ctx context.Context, spans []*otlpv1.Res // data manager service for when UploadDataFromPath is called. type dataFromPathUploader interface { UploadDataFromPath(ctx context.Context, path string, uploadMetadata *datasyncpb.UploadMetadata, extra map[string]interface{}) ( - uint64, uint64, uint64, uint64, []string, error) + robot.UploadDataFromPathResult, error) } // UploadDataFromPath uploads a file or directory to the cloud via the configured data manager service. func (r *localRobot) UploadDataFromPath(ctx context.Context, path string, md *datasyncpb.UploadMetadata, extra map[string]interface{}) ( - uint64, uint64, uint64, uint64, []string, error, + robot.UploadDataFromPathResult, error, ) { names := datamanager.NamesFromRobot(r) if len(names) == 0 { - return 0, 0, 0, 0, nil, errors.New("no data manager service configured") + return robot.UploadDataFromPathResult{}, errors.New("no data manager service configured") } svc, err := datamanager.FromProvider(r, names[0]) if err != nil { - return 0, 0, 0, 0, nil, err + return robot.UploadDataFromPathResult{}, err } uploader, ok := svc.(dataFromPathUploader) if !ok { - return 0, 0, 0, 0, nil, errors.New("data manager does not support UploadDataFromPath") + return robot.UploadDataFromPathResult{}, errors.New("data manager does not support UploadDataFromPath") } return uploader.UploadDataFromPath(ctx, path, md, extra) } diff --git a/robot/impl/local_robot_test.go b/robot/impl/local_robot_test.go index d5b8e1be1c0..9777aafdbb4 100644 --- a/robot/impl/local_robot_test.go +++ b/robot/impl/local_robot_test.go @@ -3257,7 +3257,7 @@ func TestUploadDataFromPath(t *testing.T) { t.Run("no data manager configured", func(t *testing.T) { r := setupLocalRobot(t, ctx, &config.Config{}, logger) - _, _, _, _, _, err := r.UploadDataFromPath(ctx, "/tmp/whatever", nil, nil) + _, err := r.UploadDataFromPath(ctx, "/tmp/whatever", nil, nil) test.That(t, err, test.ShouldNotBeNil) test.That(t, err.Error(), test.ShouldContainSubstring, "no data manager") }) @@ -3275,7 +3275,7 @@ func TestUploadDataFromPath(t *testing.T) { }, } r := setupLocalRobot(t, ctx, cfg, logger) - _, _, _, _, _, err := r.UploadDataFromPath(ctx, "/tmp/whatever", nil, nil) + _, err := r.UploadDataFromPath(ctx, "/tmp/whatever", nil, nil) test.That(t, err, test.ShouldNotBeNil) test.That(t, err.Error(), test.ShouldContainSubstring, "does not support") }) diff --git a/robot/robot.go b/robot/robot.go index 2797f589009..fac37e1b046 100644 --- a/robot/robot.go +++ b/robot/robot.go @@ -44,6 +44,15 @@ func init() { } } +// UploadDataFromPathResult is the aggregated result of an UploadDataFromPath call. +type UploadDataFromPathResult struct { + FilesUploaded uint64 + FilesFailed uint64 + BytesUploaded uint64 + BytesTotal uint64 + IDs []string +} + // A Robot encompasses all functionality of some robot comprised // of parts, local and remote. // @@ -200,7 +209,7 @@ type LocalRobot interface { // UploadDataFromPath uploads a file or directory at path to the cloud via the data manager. UploadDataFromPath(ctx context.Context, path string, uploadMetadata *datasyncpb.UploadMetadata, extra map[string]interface{}) ( - filesUploaded, filesFailed, bytesUploaded, bytesTotal uint64, ids []string, err error) + UploadDataFromPathResult, error) } // A RemoteRobot is a Robot that was created through a connection. diff --git a/robot/server/server.go b/robot/server/server.go index 0b8979d05e1..001676be4d1 100644 --- a/robot/server/server.go +++ b/robot/server/server.go @@ -74,16 +74,16 @@ func (s *Server) SendTraces(ctx context.Context, req *pb.SendTracesRequest) (*pb func (s *Server) UploadDataFromPath(ctx context.Context, req *pb.UploadDataFromPathRequest) ( *pb.UploadDataFromPathResponse, error, ) { - fu, ff, bu, bt, ids, err := s.robot.UploadDataFromPath(ctx, req.GetPath(), req.GetUploadMetadata(), req.Extra.AsMap()) + res, err := s.robot.UploadDataFromPath(ctx, req.GetPath(), req.GetUploadMetadata(), req.Extra.AsMap()) if err != nil { return nil, err } return &pb.UploadDataFromPathResponse{ - FilesUploaded: fu, - FilesFailed: ff, - BytesUploaded: bu, - BytesTotal: bt, - Ids: ids, + FilesUploaded: res.FilesUploaded, + FilesFailed: res.FilesFailed, + BytesUploaded: res.BytesUploaded, + BytesTotal: res.BytesTotal, + Ids: res.IDs, }, nil } diff --git a/robot/server/server_test.go b/robot/server/server_test.go index c77c6182078..b7f5f5ffc7e 100644 --- a/robot/server/server_test.go +++ b/robot/server/server_test.go @@ -652,11 +652,17 @@ func TestServer(t *testing.T) { // success: request fields (incl. extra) reach the robot, and counts/ids map back through. injectRobot.UploadDataFromPathFunc = func( ctx context.Context, path string, md *datasyncpb.UploadMetadata, extra map[string]interface{}, - ) (uint64, uint64, uint64, uint64, []string, error) { + ) (robot.UploadDataFromPathResult, error) { test.That(t, path, test.ShouldEqual, "/data/foo") test.That(t, md.GetTags(), test.ShouldResemble, []string{"tag1"}) test.That(t, extra, test.ShouldResemble, map[string]interface{}{"foo": "bar"}) - return 3, 1, 1024, 2048, []string{"id1", "id2", "id3"}, nil + return robot.UploadDataFromPathResult{ + FilesUploaded: 2, + FilesFailed: 0, + BytesUploaded: 512, + BytesTotal: 512, + IDs: []string{"a", "b"}, + }, nil } resp, err := server.UploadDataFromPath(context.Background(), &pb.UploadDataFromPathRequest{ @@ -665,17 +671,17 @@ func TestServer(t *testing.T) { Extra: ext, }) test.That(t, err, test.ShouldBeNil) - test.That(t, resp.GetFilesUploaded(), test.ShouldEqual, uint64(3)) - test.That(t, resp.GetFilesFailed(), test.ShouldEqual, uint64(1)) - test.That(t, resp.GetBytesUploaded(), test.ShouldEqual, uint64(1024)) - test.That(t, resp.GetBytesTotal(), test.ShouldEqual, uint64(2048)) - test.That(t, resp.GetIds(), test.ShouldResemble, []string{"id1", "id2", "id3"}) + test.That(t, resp.GetFilesUploaded(), test.ShouldEqual, uint64(2)) + test.That(t, resp.GetFilesFailed(), test.ShouldEqual, uint64(0)) + test.That(t, resp.GetBytesUploaded(), test.ShouldEqual, uint64(512)) + test.That(t, resp.GetBytesTotal(), test.ShouldEqual, uint64(512)) + test.That(t, resp.GetIds(), test.ShouldResemble, []string{"a", "b"}) // error is surfaced to the caller. injectRobot.UploadDataFromPathFunc = func( ctx context.Context, path string, md *datasyncpb.UploadMetadata, extra map[string]interface{}, - ) (uint64, uint64, uint64, uint64, []string, error) { - return 0, 0, 0, 0, nil, errors.New("no data manager service configured") + ) (robot.UploadDataFromPathResult, error) { + return robot.UploadDataFromPathResult{}, errors.New("no data manager service configured") } _, err = server.UploadDataFromPath(context.Background(), &pb.UploadDataFromPathRequest{Path: "/data/foo"}) test.That(t, err, test.ShouldNotBeNil) diff --git a/testutils/inject/robot.go b/testutils/inject/robot.go index 7114560e586..ca929b6155f 100644 --- a/testutils/inject/robot.go +++ b/testutils/inject/robot.go @@ -64,7 +64,7 @@ type Robot struct { ctx context.Context, path string, uploadMetadata *datasyncpb.UploadMetadata, extra map[string]interface{}, - ) (uint64, uint64, uint64, uint64, []string, error) + ) (robot.UploadDataFromPathResult, error) ops *operation.Manager SessMgr session.Manager @@ -382,7 +382,7 @@ func (r *Robot) ListTunnels(ctx context.Context) ([]config.TrafficTunnelEndpoint // UploadDataFromPath calls the injected UploadDataFromPath or the real one. func (r *Robot) UploadDataFromPath(ctx context.Context, path string, uploadMetadata *datasyncpb.UploadMetadata, extra map[string]interface{}) ( - uint64, uint64, uint64, uint64, []string, error, + robot.UploadDataFromPathResult, error, ) { r.Mu.RLock() defer r.Mu.RUnlock()