tast: Check dependencies before building tests.

When run with -build (currently the default), "tast run ..."
rebuilds the test executable. To do this, it relies on the
executable's dependencies' source code having already been
checked out as a side effect of the dependencies having
previously been built by Portage. (/usr/lib/gopath is used
by default, although this change adds a -sysgopath flag that
can be used to specify a different workspace.)

This change makes tast use Portage's equery command to check
that the test package's dependencies are installed before
performing the build. If they aren't, a command that can be
used to install them is printed. Checking dependencies can
take close to a second; -checkdeps=false can be passed to
skip the check.

BUG=chromium:737628
TEST=unit tests pass; also manually verified that for both
     local and remote tests, an error is printed if
     dependencies are missing and that the check is skipped
     when -checkdeps=false is passed

Change-Id: I532821307e638f27b7dfe013e840b371c63b7547
Reviewed-on: https://chromium-review.googlesource.com/664379
Commit-Ready: Dan Erat <derat@chromium.org>
Tested-by: Dan Erat <derat@chromium.org>
Reviewed-by: Jason Clinton <jclinton@chromium.org>
diff --git a/src/chromiumos/tast/tast/build/build.go b/src/chromiumos/tast/tast/build/build.go
index 1d524c4..2b71a26 100644
--- a/src/chromiumos/tast/tast/build/build.go
+++ b/src/chromiumos/tast/tast/build/build.go
@@ -6,6 +6,7 @@
 package build
 
 import (
+	"bytes"
 	"context"
 	"fmt"
 	"os/exec"
@@ -15,10 +16,6 @@
 	"chromiumos/tast/tast/timing"
 )
 
-const (
-	sysGopath = "/usr/lib/gopath" // readonly Go workspace where source for system packages are stored
-)
-
 // GetLocalArch returns the local system's architecture as described by "uname -m".
 func GetLocalArch() (string, error) {
 	b, err := exec.Command("uname", "-m").Output()
@@ -51,11 +48,27 @@
 		return out, fmt.Errorf("unknown arch %q", cfg.Arch)
 	}
 
+	if cfg.PortagePkg != "" {
+		if missing, err := checkDeps(ctx, cfg.PortagePkg); err != nil {
+			return out, fmt.Errorf("failed checking deps for %s: %v", cfg.PortagePkg, err)
+		} else if len(missing) > 0 {
+			b := bytes.NewBufferString("To install missing dependencies, run:\n\n  sudo emerge -j 16 \\\n")
+			for i, dep := range missing {
+				suffix := ""
+				if i < len(missing)-1 {
+					suffix = " \\"
+				}
+				fmt.Fprintf(b, "    =%s%s\n", dep, suffix)
+			}
+			return b.Bytes(), fmt.Errorf("%s has missing dependencies", cfg.PortagePkg)
+		}
+	}
+
 	pkgDir := filepath.Join(cfg.OutDir, cfg.Arch)
 	cmd := exec.Command(comp, "build", "-i", "-ldflags=-s -w", "-pkgdir", pkgDir, "-o", path, pkg)
 	cmd.Env = []string{
 		"PATH=/usr/bin",
-		"GOPATH=" + strings.Join([]string{cfg.TestWorkspace, sysGopath}, ":"),
+		"GOPATH=" + strings.Join([]string{cfg.TestWorkspace, cfg.SysGopath}, ":"),
 	}
 	if out, err = cmd.CombinedOutput(); err != nil {
 		return out, err
diff --git a/src/chromiumos/tast/tast/build/build_test.go b/src/chromiumos/tast/tast/build/build_test.go
index bbf3d37..8c8842f 100644
--- a/src/chromiumos/tast/tast/build/build_test.go
+++ b/src/chromiumos/tast/tast/build/build_test.go
@@ -12,6 +12,8 @@
 	"os/exec"
 	"path/filepath"
 	"testing"
+
+	"chromiumos/tast/common/testutil"
 )
 
 func TestBuildTests(t *testing.T) {
@@ -21,21 +23,31 @@
 	}
 	defer os.RemoveAll(tempDir)
 
+	sysGopath := filepath.Join(tempDir, "gopath")
 	cfg := &Config{
 		TestWorkspace: tempDir,
+		SysGopath:     sysGopath,
 		OutDir:        filepath.Join(tempDir, "out"),
 	}
 	if cfg.Arch, err = GetLocalArch(); err != nil {
 		t.Fatal("Failed to get local arch: ", err)
 	}
 
-	srcDir := filepath.Join(tempDir, "src", "foo", "cmd")
-	if err = os.MkdirAll(srcDir, 0755); err != nil {
-		t.Fatal(err)
-	}
-	const exp = "success!"
-	code := fmt.Sprintf("package main\nfunc main() { print(\"%s\") }", exp)
-	if err := ioutil.WriteFile(filepath.Join(srcDir, "main.go"), []byte(code), 0644); err != nil {
+	// In order to test that the supplied system GOPATH is used, build a main
+	// package that prints a constant exported by a system package.
+	const (
+		pkgName   = "testpkg"  // system package's name (without chromiumos/tast prefix)
+		constName = "Msg"      // name of const exported by system package
+		constVal  = "success!" // value of const exported by system package
+	)
+	pkgCode := fmt.Sprintf("package %s\nconst %s = %q", pkgName, constName, constVal)
+	mainCode := fmt.Sprintf("package main\nimport %q\nfunc main() { print(%s.%s) }",
+		pkgName, pkgName, constName)
+
+	if err := testutil.WriteFiles(tempDir, map[string]string{
+		filepath.Join("src", pkgName, "lib.go"): pkgCode,
+		"src/foo/cmd/main.go":                   mainCode,
+	}); err != nil {
 		t.Fatal(err)
 	}
 
@@ -46,7 +58,7 @@
 
 	if out, err := exec.Command(bin).CombinedOutput(); err != nil {
 		t.Errorf("Failed to run %s: %v", bin, err)
-	} else if string(out) != exp {
-		t.Errorf("%s printed %q; want %q", bin, string(out), exp)
+	} else if string(out) != constVal {
+		t.Errorf("%s printed %q; want %q", bin, string(out), constVal)
 	}
 }
diff --git a/src/chromiumos/tast/tast/build/config.go b/src/chromiumos/tast/tast/build/config.go
index fe8cbba..a282cd7 100644
--- a/src/chromiumos/tast/tast/build/config.go
+++ b/src/chromiumos/tast/tast/build/config.go
@@ -19,10 +19,18 @@
 	// TestWorkspace is the path to the Go workspace where test source code is stored (i.e.
 	// containing a top-level directory named "src").
 	TestWorkspace string
+	// SysGopath is the path to the Go workspace containing source for test executables'
+	// emerged dependencies. This is typically /usr/lib/gopath.
+	SysGopath string
 	// Arch is the architecture to build for (as a machine name or processor given by "uname -m").
 	Arch string
 	// OutDir is the path to a directory where compiled code is stored (after appending arch).
 	OutDir string
+	// PortagePkg is the Portage package that contains the test executable (when tests are
+	// included in a system image rather than being compiled by the tast command).
+	// If non-empty, BuildTests checks that the package's direct dependencies are installed
+	// in the host sysroot before building tests.
+	PortagePkg string
 }
 
 // OutPath returns the path to a file named fn within cfg's architecture-specific output dir.
@@ -35,6 +43,8 @@
 func (c *Config) SetFlags(f *flag.FlagSet, trunkDir string) {
 	f.StringVar(&c.Arch, "arch", "", "target architecture (per \"uname -m\")")
 	f.StringVar(&c.OutDir, "outdir", defaultBuildOutDir, "directory storing build artifacts")
+	f.StringVar(&c.SysGopath, "sysgopath", "/usr/lib/gopath",
+		"Go workspace containing system package source code (if empty, chosen automatically)")
 	f.StringVar(&c.TestWorkspace, "testdir", filepath.Join(trunkDir, defaultTestDir),
 		"Go workspace containing test source code")
 }
diff --git a/src/chromiumos/tast/tast/build/portage.go b/src/chromiumos/tast/tast/build/portage.go
new file mode 100644
index 0000000..1aa7edb
--- /dev/null
+++ b/src/chromiumos/tast/tast/build/portage.go
@@ -0,0 +1,119 @@
+// Copyright 2017 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 build
+
+import (
+	"context"
+	"fmt"
+	"os/exec"
+	"regexp"
+	"strings"
+	"syscall"
+
+	"chromiumos/tast/tast/timing"
+)
+
+var equeryDepsRegexp *regexp.Regexp
+
+func init() {
+	// Matches lines containing first-level dependencies printed by an
+	// "equery -q -C g --depth=1 <pkg>" command, which produces output
+	// similar to the following (preceded by a blank line):
+	//
+	//	chromeos-base/tast-local-tests-9999:
+	//	 [  0]  chromeos-base/tast-local-tests-9999
+	//	 [  1]  chromeos-base/tast-common-9999
+	//	 [  1]  dev-go/cdp-0.9.1
+	//	 [  1]  dev-go/dbus-0.0.2-r5
+	//	 [  1]  dev-lang/go-1.8.3-r1
+	//	 [  1]  dev-vcs/git-2.12.2
+	equeryDepsRegexp = regexp.MustCompile("^\\s*\\[\\s*1\\]\\s+([\\S]+)")
+}
+
+// depInfo contains information about one of a package's dependencies.
+type depInfo struct {
+	pkg       string // dependency's package name
+	installed bool   // true if dependency is installed
+	err       error  // non-nil if error encountered while getting status
+}
+
+// checkDeps checks if all of portagePkg's direct dependencies are installed.
+// Missing dependencies are returned in badDeps, with package names in
+// the format "<category>/<package>-<version>" as keys and descriptive error
+// messages as values. err is set if a more-serious error is encountered while
+// trying to check dependencies.
+func checkDeps(ctx context.Context, portagePkg string) (missing []string, err error) {
+	if tl, ok := timing.FromContext(ctx); ok {
+		st := tl.Start("check_deps")
+		defer st.End()
+	}
+
+	cmd := exec.Command("equery", "-q", "-C", "g", "--depth=1", portagePkg)
+	out, err := cmd.Output()
+	if err != nil {
+		return nil, fmt.Errorf("%q failed: %v", strings.Join(cmd.Args, " "), err)
+	}
+
+	deps := parseEqueryDeps(out)
+	if len(deps) == 0 {
+		return nil, fmt.Errorf("no deps found in output from %q", strings.Join(cmd.Args, " "))
+	}
+
+	// "equery l" doesn't appear to accept multiple package names, so run queries in parallel.
+	ch := make(chan *depInfo, len(deps))
+	for _, dep := range deps {
+		go func(pkg string) {
+			info := &depInfo{pkg: pkg}
+			info.installed, info.err = portagePkgInstalled(pkg)
+			ch <- info
+		}(dep)
+	}
+
+	missing = make([]string, 0)
+	for range deps {
+		info := <-ch
+		if info.err != nil {
+			return missing, fmt.Errorf("failed getting status of %s: %v", info.pkg, info.err)
+		} else if !info.installed {
+			missing = append(missing, info.pkg)
+		}
+	}
+	return missing, nil
+}
+
+// parseEqueryDeps parses the output of checkDeps's "equery g" command and returns
+// the names (as "<category>/<package>-<version>") of first-level dependencies.
+func parseEqueryDeps(out []byte) []string {
+	deps := make([]string, 0)
+	for _, ln := range strings.Split(string(out), "\n") {
+		if matches := equeryDepsRegexp.FindStringSubmatch(ln); matches != nil {
+			deps = append(deps, matches[1])
+		}
+	}
+	return deps
+}
+
+// portagePkgInstalled runs "equery l" to check if pkg is installed.
+func portagePkgInstalled(pkg string) (bool, error) {
+	cmd := exec.Command("equery", "-q", "-C", "l", pkg)
+	out, err := cmd.Output()
+	if err != nil {
+		// equery (in "quiet mode") exits with 3 if the package isn't installed.
+		if exitErr, ok := err.(*exec.ExitError); ok {
+			if status, ok := exitErr.Sys().(syscall.WaitStatus); ok {
+				if status.ExitStatus() == 3 {
+					return false, nil
+				}
+			}
+		}
+		return false, fmt.Errorf("%q failed: %v", strings.Join(cmd.Args, " "), err)
+	}
+
+	// equery should print the package name.
+	if str := strings.TrimSpace(string(out)); str != pkg {
+		return false, fmt.Errorf("%q returned %q", strings.Join(cmd.Args, " "), str)
+	}
+	return true, nil
+}
diff --git a/src/chromiumos/tast/tast/build/portage_test.go b/src/chromiumos/tast/tast/build/portage_test.go
new file mode 100644
index 0000000..d6b5498
--- /dev/null
+++ b/src/chromiumos/tast/tast/build/portage_test.go
@@ -0,0 +1,35 @@
+// Copyright 2017 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 build
+
+import (
+	"reflect"
+	"testing"
+)
+
+func TestParseEqueryDeps(t *testing.T) {
+	// Copy-and-pasted output (including trailing whitespace) from
+	// "equery -q -C g --depth=1 chromeos-base/tast-local-tests-9999".
+	out := `
+chromeos-base/tast-local-tests-9999:
+ [  0]  chromeos-base/tast-local-tests-9999   
+ [  1]  chromeos-base/tast-common-9999   
+ [  1]  dev-go/cdp-0.9.1   
+ [  1]  dev-go/dbus-0.0.2-r5   
+ [  1]  dev-lang/go-1.8.3-r1   
+ [  1]  dev-vcs/git-2.12.2   
+`
+
+	exp := []string{
+		"chromeos-base/tast-common-9999",
+		"dev-go/cdp-0.9.1",
+		"dev-go/dbus-0.0.2-r5",
+		"dev-lang/go-1.8.3-r1",
+		"dev-vcs/git-2.12.2",
+	}
+	if act := parseEqueryDeps([]byte(out)); !reflect.DeepEqual(act, exp) {
+		t.Errorf("parseEqueryDeps(%q) = %v; want %v", out, act, exp)
+	}
+}
diff --git a/src/chromiumos/tast/tast/build_cmd.go b/src/chromiumos/tast/tast/build_cmd.go
index 1deb863..55e2965 100644
--- a/src/chromiumos/tast/tast/build_cmd.go
+++ b/src/chromiumos/tast/tast/build_cmd.go
@@ -38,13 +38,15 @@
 		fmt.Fprintf(os.Stderr, b.Usage())
 		return subcommands.ExitUsageError
 	}
+
 	if b.cfg.Arch == "" {
 		var err error
 		if b.cfg.Arch, err = build.GetLocalArch(); err != nil {
-			lg.Logf("Failed to get local arch: %v\n", err)
+			lg.Log("Failed to get local arch: ", err)
 			return subcommands.ExitFailure
 		}
 	}
+
 	if out, err := build.BuildTests(ctx, &b.cfg, f.Args()[0], f.Args()[1]); err != nil {
 		lg.Logf("Failed building tests: %v\n%s", err, string(out))
 		return subcommands.ExitFailure
diff --git a/src/chromiumos/tast/tast/run_cmd.go b/src/chromiumos/tast/tast/run_cmd.go
index 577ea0b..7a74a97 100644
--- a/src/chromiumos/tast/tast/run_cmd.go
+++ b/src/chromiumos/tast/tast/run_cmd.go
@@ -33,8 +33,9 @@
 
 // runCmd implements subcommands.Command to support running tests.
 type runCmd struct {
-	testType string     // type of tests to run (either "local" or "remote")
-	cfg      run.Config // shared config for running tests
+	testType  string     // type of tests to run (either "local" or "remote")
+	checkDeps bool       // true if test package's dependencies should be checked before building
+	cfg       run.Config // shared config for running tests
 }
 
 func (*runCmd) Name() string     { return "run" }
@@ -47,6 +48,7 @@
 
 func (r *runCmd) SetFlags(f *flag.FlagSet) {
 	f.StringVar(&r.testType, "testtype", "local", "type of tests to run (either \"local\" or \"remote\")")
+	f.BoolVar(&r.checkDeps, "checkdeps", true, "checks test package's dependencies before building")
 	r.cfg.SetFlags(f)
 	r.cfg.BuildCfg.SetFlags(f, getTrunkDir())
 }
@@ -111,8 +113,14 @@
 	lg.Log("Writing results to ", r.cfg.ResDir)
 	switch r.testType {
 	case localType:
+		if r.cfg.Build && r.checkDeps {
+			r.cfg.BuildCfg.PortagePkg = "chromeos-base/tast-local-tests-9999"
+		}
 		return run.Local(ctx, &r.cfg)
 	case remoteType:
+		if r.cfg.Build && r.checkDeps {
+			r.cfg.BuildCfg.PortagePkg = "chromeos-base/tast-remote-tests-9999"
+		}
 		return run.Remote(ctx, &r.cfg)
 	}
 	lg.Logf(fmt.Sprintf("Invalid test type %q\n\n%s", r.testType, r.Usage()))