testservice: Create initial testservice executable

This CL will create an initial testservice executable.
This executable does not actual provision DUT or run tests. Those will
implement in future. For this CL, it will only start a TestService
server to listen to requests and log its status.

Run following command in chroot to build the executable
cd ~/trunk/src/platform/dev/test; ./fast_build.sh

Example:
 $ ./fast_build.sh
Start building: /home/seewaifu/go/pkg chromiumos/testservice/cmd/testservice /home/seewaifu/go/bin/testservice

$ ~/go/bin/testservice
2021/05/08 00:25:58 Starting testservice version <unknown>
2021/05/08 00:25:58 testservice listen to request at [::]:44273

$ ls /tmp/testservice/
20210507-171818  20210507-172003  20210507-172501  20210507-172558  log

$ cat /tmp/testservice/20210507-172558/log.txt
2021/05/08 00:25:58 Starting testservice version <unknown>
2021/05/08 00:25:58 testservice listen to request at [::]:44273

Unit Test Result
$ ./fast_build.sh -T
ok  	chromiumos/testservice/cmd/testservice	(cached)

BUG=b:187556761
TEST=./fast_build.sh -T; ./fast_build.sh; ~/go/bin/testservice

Change-Id: Ic8fe53dc125f6aced4fdbf5c846ee78b08eaec97
Reviewed-on: https://chromium-review.googlesource.com/c/chromiumos/platform/dev-util/+/2881276
Tested-by: Seewai Fu <seewaifu@google.com>
Commit-Queue: Seewai Fu <seewaifu@google.com>
Reviewed-by: Katherine Threlkeld <kathrelkeld@chromium.org>
Reviewed-by: C Shapiro <shapiroc@chromium.org>
diff --git a/test/fast_build.sh b/test/fast_build.sh
new file mode 100755
index 0000000..8fee600
--- /dev/null
+++ b/test/fast_build.sh
@@ -0,0 +1,172 @@
+#!/bin/bash -e
+
+# 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.
+
+# This script quickly builds the testservice executable or its unit tests within a
+# Chrome OS chroot.
+
+# Personal Go workspace used to cache compiled packages.
+readonly GOHOME="${HOME}/go"
+
+# Directory where compiled packages are cached.
+readonly PKGDIR="${GOHOME}/pkg"
+
+# Go workspaces containing the Test Service source.
+readonly SRCDIRS=(
+  "${HOME}/trunk/src/platform/dev/test"
+)
+
+# Package to build to produce testservice executables.
+readonly TESTSERVICE_PKG="chromiumos/testservice/cmd/testservice"
+
+# Output filename for testservice executable.
+readonly TESTSERVICE_OUT="${GOHOME}/bin/testservice"
+
+# Readonly Go workspaces containing source to build. Note that the packages
+# installed to /usr/lib/gopath (dev-go/crypto, dev-go/subcommand, etc.) need to
+# be emerged beforehand.
+export GOPATH="$(IFS=:; echo "${SRCDIRS[*]}"):"${HOME}"/trunk/src/platform/dev/lib:/usr/lib/gopath"
+
+# Disable cgo and PIE on building Test Service binaries. See:
+# https://crbug.com/976196
+# https://github.com/golang/go/issues/30986#issuecomment-475626018
+export CGO_ENABLED=0
+export GOPIE=0
+
+readonly CMD=$(basename "${0}")
+
+# Prints usage information and exits.
+usage() {
+  cat - <<EOF >&2
+Quickly builds the testservice executable or its unit tests.
+
+Usage: ${CMD}                             Builds testservice to ${TESTSERVICE_OUT}.
+       ${CMD} -b <pkg> -o <path>          Builds <pkg> to <path>.
+       ${CMD} [-v] -T                     Tests all packages.
+       ${CMD} [-v] [-r <regex>] -t <pkg>  Tests <pkg>.
+       ${CMD} -C                          Checks all code using "go vet".
+       ${CMD} -c <pkg>                    Checks <pkg>'s code.
+
+EOF
+  exit 1
+}
+
+# Prints all checkable packages.
+get_check_pkgs() {
+  local dir
+  for dir in "${SRCDIRS[@]}"; do
+    if [[ -d "${dir}/src" ]]; then
+      (cd "${dir}/src"
+       find . -name '*.go' | xargs dirname | sort | uniq | cut -b 3-)
+    fi
+  done
+}
+
+# Prints all testable packages.
+get_test_pkgs() {
+  local dir
+  for dir in "${SRCDIRS[@]}"; do
+    if [[ -d "${dir}/src" ]]; then
+      (cd "${dir}/src"
+       find . -name '*_test.go' | xargs dirname | sort | uniq | cut -b 3-)
+    fi
+  done
+}
+
+# Builds an executable package to a destination path.
+run_build() {
+  local pkg="${1}"
+  local dest="${2}"
+  go build -i -pkgdir "${PKGDIR}" -o "${dest}" "${pkg}"
+}
+
+# Checks one or more packages.
+run_vet() {
+  go vet -unusedresult.funcs=errors.New,errors.Wrap,errors.Wrapf,fmt.Errorf,\
+fmt.Sprint,fmt.Sprintf,sort.Reverse \
+    -printf.funcs=Log,Logf,Error,Errorf,Fatal,Fatalf,Wrap,Wrapf "${@}"
+}
+
+# Tests one or more packages.
+run_test() {
+  local args=("${@}" "${EXTRAARGS[@]}")
+  go test ${verbose_flag} -pkgdir "${PKGDIR}" \
+     ${test_regex:+"-run=${test_regex}"} "${args[@]}"
+}
+
+# Executable package to build.
+build_pkg=
+
+# Path to which executable package should be installed.
+build_out=
+
+# Package to check via "go vet".
+check_pkg=
+
+# Test package to build and run.
+test_pkg=
+
+# Verbose flag for testing.
+verbose_flag=
+
+# Test regex list for unit testing.
+test_regex=
+
+while getopts "CTb:c:ho:r:t:v-" opt; do
+  case "${opt}" in
+    C)
+      check_pkg=all
+      ;;
+    T)
+      test_pkg=all
+      ;;
+    b)
+      build_pkg="${OPTARG}"
+      ;;
+    c)
+      check_pkg="${OPTARG}"
+      ;;
+    o)
+      build_out="${OPTARG}"
+      ;;
+    r)
+      test_regex="${OPTARG}"
+      ;;
+    t)
+      test_pkg="${OPTARG}"
+      ;;
+    v)
+      verbose_flag="-v"
+      ;;
+    *)
+      usage
+      ;;
+  esac
+done
+
+shift $((OPTIND-1))
+EXTRAARGS=( "$@" )
+
+if [ -n "${build_pkg}" ]; then
+  if [ -z "${build_out}" ]; then
+    echo "Required output file missing: -o <path>" >&2
+    exit 1
+  fi
+  run_build "${build_pkg}" "${build_out}"
+elif [ -n "${test_pkg}" ]; then
+  if [ "${test_pkg}" = 'all' ]; then
+    run_test "$(get_test_pkgs)"
+  else
+    run_test "${test_pkg}"
+  fi
+elif [ -n "${check_pkg}" ]; then
+  if [ "${check_pkg}" = 'all' ]; then
+    run_vet "$(get_check_pkgs)"
+  else
+    run_vet "${check_pkg}"
+  fi
+else
+  run_build "${TESTSERVICE_PKG}" "${TESTSERVICE_OUT}"
+fi
diff --git a/test/src/chromiumos/testservice/cmd/testservice/main.go b/test/src/chromiumos/testservice/cmd/testservice/main.go
new file mode 100644
index 0000000..083122e
--- /dev/null
+++ b/test/src/chromiumos/testservice/cmd/testservice/main.go
@@ -0,0 +1,77 @@
+// 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 main implements the testservice used to run tests in RTD.
+package main
+
+import (
+	"flag"
+	"fmt"
+	"io"
+	"log"
+	"net"
+	"os"
+	"path/filepath"
+	"time"
+)
+
+// Version is the version info of this command. It is filled in during emerge.
+var Version = "<unknown>"
+
+// createLogFile creates a file and its parent directory for logging purpose.
+func createLogFile() (*os.File, error) {
+	t := time.Now()
+	fullPath := filepath.Join("/tmp/testservice/", t.Format("20060102-150405"))
+	if err := os.MkdirAll(fullPath, 0755); err != nil {
+		return nil, fmt.Errorf("failed to create directory %v: %v", fullPath, err)
+	}
+
+	logFullPathName := filepath.Join(fullPath, "log.txt")
+
+	// Log the full output of the command to disk.
+	logFile, err := os.Create(logFullPathName)
+	if err != nil {
+		return nil, fmt.Errorf("failed to create file %v: %v", fullPath, err)
+	}
+	return logFile, nil
+}
+
+// newLogger creates a logger. Using go default logger for now.
+func newLogger(logFile *os.File) *log.Logger {
+	mw := io.MultiWriter(logFile, os.Stderr)
+	return log.New(mw, "", log.LstdFlags|log.LUTC)
+}
+
+func main() {
+	os.Exit(func() int {
+		version := flag.Bool("version", false, "print version and exit")
+		flag.Parse()
+
+		if *version {
+			fmt.Println("testservice version ", Version)
+			return 0
+		}
+
+		logFile, err := createLogFile()
+		if err != nil {
+			log.Fatalln("Failed to create log file: ", err)
+		}
+		defer logFile.Close()
+
+		logger := newLogger(logFile)
+		logger.Println("Starting testservice version ", Version)
+		l, err := net.Listen("tcp", ":0")
+		if err != nil {
+			logger.Fatalln("Failed to create a net listener: ", err)
+			return 2
+		}
+		server, err := newTestServiceServer(l, logger)
+		if err != nil {
+			logger.Fatalln("Failed to start testservice server: ", err)
+		}
+
+		server.Serve(l)
+		return 0
+	}())
+}
diff --git a/test/src/chromiumos/testservice/cmd/testservice/testserviceserver.go b/test/src/chromiumos/testservice/cmd/testservice/testserviceserver.go
new file mode 100644
index 0000000..ba97d4d
--- /dev/null
+++ b/test/src/chromiumos/testservice/cmd/testservice/testserviceserver.go
@@ -0,0 +1,94 @@
+// 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 main implements the testservice server to listen to test and provision requests.
+package main
+
+import (
+	"context"
+	"log"
+	"net"
+
+	"chromiumos/lro"
+
+	"go.chromium.org/chromiumos/config/go/longrunning"
+	"go.chromium.org/chromiumos/config/go/test/api"
+	"google.golang.org/grpc"
+)
+
+// TestServiceServer implement a server that will listen to test and provision requests.
+type TestServiceServer struct {
+	Manager *lro.Manager
+	logger  *log.Logger
+}
+
+// newTestServiceServer creates a new test service server to listen to test requests.
+func newTestServiceServer(l net.Listener, logger *log.Logger) (*grpc.Server, error) {
+	s := &TestServiceServer{
+		Manager: lro.New(),
+		logger:  logger,
+	}
+	defer s.Manager.Close()
+	server := grpc.NewServer()
+	api.RegisterTestServiceServer(server, s)
+	longrunning.RegisterOperationsServer(server, s.Manager)
+	logger.Println("testservice listen to request at ", l.Addr().String())
+	return server, nil
+}
+
+// ProvisionDut installs a specified version of Chrome OS on the DUT, along
+// with any specified DLCs.
+//
+// If the DUT is already on the specified version of Chrome OS, the OS will
+// not be provisioned.
+//
+// If the DUT already has the specified list of DLCs, only the missing DLCs
+// will be provisioned.
+func (s *TestServiceServer) ProvisionDut(ctx context.Context, req *api.ProvisionDutRequest) (*longrunning.Operation, error) {
+	s.logger.Println("Received api.ProvisionDutRequest: ", *req)
+	op := s.Manager.NewOperation()
+	s.Manager.SetResult(op.Name, &api.ProvisionDutResponse{})
+	return op, nil
+}
+
+// ProvisionLacros installs a specified version of Lacros on the DUT.
+//
+// If the DUT already has the specified version of Lacros, Lacros will not be
+// provisioned.
+func (s *TestServiceServer) ProvisionLacros(ctx context.Context, req *api.ProvisionLacrosRequest) (*longrunning.Operation, error) {
+	s.logger.Println("Received api.ProvisionLacrosRequest: ", *req)
+	op := s.Manager.NewOperation()
+	s.Manager.SetResult(op.Name, &api.ProvisionLacrosResponse{})
+	return op, nil
+}
+
+// ProvisionAsh installs a specified version of ash-chrome on the DUT.
+//
+// This directly overwrites the version of ash-chrome on the current root
+// disk partition.
+func (s *TestServiceServer) ProvisionAsh(ctx context.Context, req *api.ProvisionAshRequest) (*longrunning.Operation, error) {
+	s.logger.Println("Received api.ProvisionAshRequest: ", *req)
+	op := s.Manager.NewOperation()
+	s.Manager.SetResult(op.Name, &api.ProvisionAshResponse{})
+	return op, nil
+}
+
+// ProvisionArc installs a specified version of ARC on the DUT.
+//
+// This directly overwrites the version of ARC on the current root
+// disk partition.
+func (s *TestServiceServer) ProvisionArc(ctx context.Context, req *api.ProvisionArcRequest) (*longrunning.Operation, error) {
+	s.logger.Println("Received api.ProvisionArcRequest: ", *req)
+	op := s.Manager.NewOperation()
+	s.Manager.SetResult(op.Name, &api.ProvisionArcResponse{})
+	return op, nil
+}
+
+// RunTests runs the requested tests.
+func (s *TestServiceServer) RunTests(ctx context.Context, req *api.RunTestsRequest) (*longrunning.Operation, error) {
+	s.logger.Println("Received api.RunTestsRequest: ", *req)
+	op := s.Manager.NewOperation()
+	s.Manager.SetResult(op.Name, &api.RunTestsResponse{})
+	return op, nil
+}
diff --git a/test/src/chromiumos/testservice/cmd/testservice/testserviceserver_test.go b/test/src/chromiumos/testservice/cmd/testservice/testserviceserver_test.go
new file mode 100644
index 0000000..fd5806a
--- /dev/null
+++ b/test/src/chromiumos/testservice/cmd/testservice/testserviceserver_test.go
@@ -0,0 +1,44 @@
+// 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 main
+
+import (
+	"bytes"
+	"context"
+	"log"
+	"net"
+	"testing"
+
+	"go.chromium.org/chromiumos/config/go/test/api"
+	"google.golang.org/grpc"
+)
+
+// TestTestServiceServer_Empty tests if TestServiceServer can handle emtpy requst without problem.
+func TestTestServiceServer_Empty(t *testing.T) {
+	var logBuf bytes.Buffer
+	l, err := net.Listen("tcp", ":0")
+	if err != nil {
+		t.Fatal("Failed to create a net listener: ", err)
+	}
+
+	ctx := context.Background()
+	srv, err := newTestServiceServer(l, log.New(&logBuf, "", log.LstdFlags|log.LUTC))
+	if err != nil {
+		t.Fatalf("Failed to start TestService server: %v", err)
+	}
+	go srv.Serve(l)
+	defer srv.Stop()
+
+	conn, err := grpc.Dial(l.Addr().String(), grpc.WithInsecure())
+	if err != nil {
+		t.Fatalf("Failed to dial: %v", err)
+	}
+	defer conn.Close()
+
+	cl := api.NewTestServiceClient(conn)
+	if _, err := cl.RunTests(ctx, &api.RunTestsRequest{}); err != nil {
+		t.Fatalf("Failed at api.RunTests: %v", err)
+	}
+}