tast: connect to Reports server from tast

Also adds a blank fake Reports server class for unit testing.

BUG=chromium:1166941
TEST=fast_build.sh -t chromiumos/tast/cmd/tast/internal/run
TEST=tast run $DUT example.Pass

Change-Id: I7e236061d9a91d9998b55b5078b6b0c240341673
Reviewed-on: https://chromium-review.googlesource.com/c/chromiumos/platform/tast/+/2635894
Tested-by: Tatsuhisa Yamaguchi <yamaguchi@chromium.org>
Reviewed-by: Seewai Fu <seewaifu@google.com>
Commit-Queue: Seewai Fu <seewaifu@google.com>
diff --git a/src/chromiumos/tast/cmd/tast/internal/run/config.go b/src/chromiumos/tast/cmd/tast/internal/run/config.go
index cf8e2e9..79164db 100644
--- a/src/chromiumos/tast/cmd/tast/internal/run/config.go
+++ b/src/chromiumos/tast/cmd/tast/internal/run/config.go
@@ -97,6 +97,7 @@
 	downloadPrivateBundles bool                 // whether to download private bundles if missing
 	downloadMode           planner.DownloadMode // strategy to download external data files
 	tlwServer              string               // address of the TLW server if available
+	reportsServer          string               // address of Reports server if available
 
 	localRunner    string // path to executable that runs local test bundles
 	localBundleDir string // dir where packaged local test bundles are installed
@@ -146,6 +147,7 @@
 	osVersion          string                     // Chrome OS Version
 	tlwConn            *grpc.ClientConn           // TLW gRPC service connection
 	tlwServerForDUT    string                     // TLW address accessible from DUT.
+	reportsConn        *grpc.ClientConn           // Reports gRPC service connection
 }
 
 // NewConfig returns a new configuration for executing test runners in the supplied mode.
@@ -195,6 +197,7 @@
 	f.BoolVar(&c.continueAfterFailure, "continueafterfailure", true, "try to run remaining tests after bundle/DUT crash or lost SSH connection")
 	f.IntVar(&c.sshRetries, "sshretries", 0, "number of SSH connect retries")
 	f.StringVar(&c.tlwServer, "tlwserver", "", "TLW server address")
+	f.StringVar(&c.reportsServer, "reports_server", "", "Reports server address")
 
 	f.IntVar(&c.totalShards, "totalshards", 1, "total number of shards to be used in a test run")
 	f.IntVar(&c.shardIndex, "shardindex", 0, "the index of shard to used in the current run")
@@ -268,6 +271,12 @@
 		}
 		c.tlwConn = nil
 	}
+	if c.reportsConn != nil {
+		if err := c.reportsConn.Close(); err != nil && firstErr == nil {
+			firstErr = err
+		}
+		c.reportsConn = nil
+	}
 	return firstErr
 }
 
diff --git a/src/chromiumos/tast/cmd/tast/internal/run/run.go b/src/chromiumos/tast/cmd/tast/internal/run/run.go
index e0ac06c..cc1032d 100644
--- a/src/chromiumos/tast/cmd/tast/internal/run/run.go
+++ b/src/chromiumos/tast/cmd/tast/internal/run/run.go
@@ -96,6 +96,10 @@
 		return errorStatusf(cfg, subcommands.ExitFailure, "Failed to connect to TLW server: %v", err), nil
 	}
 
+	if err := connectToReports(ctx, cfg); err != nil {
+		return errorStatusf(cfg, subcommands.ExitFailure, "Failed to connect to Reports server: %v", err), nil
+	}
+
 	if err := resolveTarget(ctx, cfg); err != nil {
 		return errorStatusf(cfg, subcommands.ExitFailure, "Failed to resolve target: %v", err), nil
 	}
@@ -166,6 +170,19 @@
 	return nil
 }
 
+// connectToReports connects to the Reports server.
+func connectToReports(ctx context.Context, cfg *Config) error {
+	if cfg.reportsServer == "" {
+		return nil
+	}
+	conn, err := grpc.DialContext(ctx, cfg.reportsServer, grpc.WithInsecure())
+	if err != nil {
+		return err
+	}
+	cfg.reportsConn = conn
+	return nil
+}
+
 // resolveTarget resolves cfg.Target using the TLW service if available.
 func resolveTarget(ctx context.Context, cfg *Config) error {
 	if cfg.tlwConn == nil {
diff --git a/src/chromiumos/tast/cmd/tast/internal/run/run_test.go b/src/chromiumos/tast/cmd/tast/internal/run/run_test.go
index f2a878f..5c79a9d 100644
--- a/src/chromiumos/tast/cmd/tast/internal/run/run_test.go
+++ b/src/chromiumos/tast/cmd/tast/internal/run/run_test.go
@@ -25,6 +25,7 @@
 
 	"chromiumos/tast/errors"
 	"chromiumos/tast/internal/control"
+	"chromiumos/tast/internal/fakereports"
 	"chromiumos/tast/internal/faketlw"
 	"chromiumos/tast/internal/runner"
 	"chromiumos/tast/internal/testing"
@@ -246,6 +247,35 @@
 	}
 }
 
+// TestRunWithReports tests Run() with fake Reports server.
+// TODO(crbug.com/1166951, crbug.com/1166955): Revise this test to check with RPC invocations.
+// Currently the main logic calls Dial() but does not invoke any RPC method yet.
+// Therefore no RPC connection to the fake server is actually tested yet.
+func TestRunWithReports(t *gotesting.T) {
+	stopFunc, addr := fakereports.Start(t)
+	defer stopFunc()
+
+	td := newLocalTestData(t)
+	defer td.close()
+	td.cfg.reportsServer = addr
+
+	td.cfg.runLocal = true
+	td.runFunc = func(args *runner.Args, stdout, stderr io.Writer) (status int) {
+		switch args.Mode {
+		case runner.RunTestsMode:
+			mw := control.NewMessageWriter(stdout)
+			mw.WriteMessage(&control.RunStart{Time: time.Unix(1, 0), NumTests: 0})
+			mw.WriteMessage(&control.RunEnd{Time: time.Unix(2, 0), OutDir: ""})
+		case runner.ListTestsMode:
+			json.NewEncoder(stdout).Encode([]testing.EntityWithRunnabilityInfo{})
+		}
+		return 0
+	}
+	if status, _ := Run(context.Background(), &td.cfg); status.ExitCode != subcommands.ExitSuccess {
+		t.Errorf("Run() = %v; want %v (%v)", status.ExitCode, subcommands.ExitSuccess, td.logbuf.String())
+	}
+}
+
 // TestRunWithSkippedTests makes sure that tests with unsupported dependency
 // would be skipped.
 func TestRunWithSkippedTests(t *gotesting.T) {
diff --git a/src/chromiumos/tast/internal/fakereports/fake_reports.go b/src/chromiumos/tast/internal/fakereports/fake_reports.go
new file mode 100644
index 0000000..a59a6ba
--- /dev/null
+++ b/src/chromiumos/tast/internal/fakereports/fake_reports.go
@@ -0,0 +1,48 @@
+// Copyright 2021 The Chromium OS Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+// Package fakereports provides a fake implementation of Reports service for unit testing.
+package fakereports
+
+import (
+	"context"
+	"net"
+	"testing"
+
+	"github.com/golang/protobuf/ptypes/empty"
+	"google.golang.org/grpc"
+	"google.golang.org/grpc/codes"
+	"google.golang.org/grpc/status"
+
+	"chromiumos/tast/internal/protocol"
+)
+
+type fakeReportsServer struct{}
+
+var _ protocol.ReportsServer = &fakeReportsServer{}
+
+// Start starts a gRPC server serving ReportsServer in the background for tests.
+// Callers are responsible for stopping the server by stopFunc().
+func Start(t *testing.T) (stopFunc func(), addr string) {
+	s := &fakeReportsServer{}
+	srv := grpc.NewServer()
+	protocol.RegisterReportsServer(srv, s)
+
+	lis, err := net.Listen("tcp", "127.0.0.1:0")
+	if err != nil {
+		t.Fatal("Failed to listen: ", err)
+	}
+	go srv.Serve(lis)
+	return srv.Stop, lis.Addr().String()
+}
+
+func (*fakeReportsServer) LogStream(srv protocol.Reports_LogStreamServer) error {
+	// TODO(crbug.com/1166951): Implement for unit tests.
+	return status.Errorf(codes.Unimplemented, "method LogStream not implemented")
+}
+
+func (*fakeReportsServer) ReportResult(ctx context.Context, req *protocol.ReportResultRequest) (*empty.Empty, error) {
+	// TODO(crbug.com/1166955): Implement for unit tests.
+	return nil, status.Errorf(codes.Unimplemented, "method ReportResult not implemented")
+}