Add tests for bazel_handler

Test: go test
Test: m nothing
Test: bp2build generate && sync; mixed build libc
Change-Id: Id4307a9e735b50221a8effb963325aaa57c3c7d0
diff --git a/android/bazel_handler.go b/android/bazel_handler.go
index 97eec30..9b44ae3 100644
--- a/android/bazel_handler.go
+++ b/android/bazel_handler.go
@@ -89,16 +89,24 @@
 	BuildStatementsToRegister() []bazel.BuildStatement
 }
 
-// A context object which tracks queued requests that need to be made to Bazel,
-// and their results after the requests have been made.
-type bazelContext struct {
+type bazelRunner interface {
+	issueBazelCommand(paths *bazelPaths, runName bazel.RunName, command bazelCommand, extraFlags ...string) (string, string, error)
+}
+
+type bazelPaths struct {
 	homeDir      string
 	bazelPath    string
 	outputBase   string
 	workspaceDir string
 	buildDir     string
 	metricsDir   string
+}
 
+// A context object which tracks queued requests that need to be made to Bazel,
+// and their results after the requests have been made.
+type bazelContext struct {
+	bazelRunner
+	paths        *bazelPaths
 	requests     map[cqueryKey]bool // cquery requests that have not yet been issued to Bazel
 	requestMutex sync.Mutex         // requests can be written in parallel
 
@@ -228,42 +236,56 @@
 		return noopBazelContext{}, nil
 	}
 
-	bazelCtx := bazelContext{buildDir: c.buildDir, requests: make(map[cqueryKey]bool)}
+	p, err := bazelPathsFromConfig(c)
+	if err != nil {
+		return nil, err
+	}
+	return &bazelContext{
+		bazelRunner: &builtinBazelRunner{},
+		paths:       p,
+		requests:    make(map[cqueryKey]bool),
+	}, nil
+}
+
+func bazelPathsFromConfig(c *config) (*bazelPaths, error) {
+	p := bazelPaths{
+		buildDir: c.buildDir,
+	}
 	missingEnvVars := []string{}
 	if len(c.Getenv("BAZEL_HOME")) > 1 {
-		bazelCtx.homeDir = c.Getenv("BAZEL_HOME")
+		p.homeDir = c.Getenv("BAZEL_HOME")
 	} else {
 		missingEnvVars = append(missingEnvVars, "BAZEL_HOME")
 	}
 	if len(c.Getenv("BAZEL_PATH")) > 1 {
-		bazelCtx.bazelPath = c.Getenv("BAZEL_PATH")
+		p.bazelPath = c.Getenv("BAZEL_PATH")
 	} else {
 		missingEnvVars = append(missingEnvVars, "BAZEL_PATH")
 	}
 	if len(c.Getenv("BAZEL_OUTPUT_BASE")) > 1 {
-		bazelCtx.outputBase = c.Getenv("BAZEL_OUTPUT_BASE")
+		p.outputBase = c.Getenv("BAZEL_OUTPUT_BASE")
 	} else {
 		missingEnvVars = append(missingEnvVars, "BAZEL_OUTPUT_BASE")
 	}
 	if len(c.Getenv("BAZEL_WORKSPACE")) > 1 {
-		bazelCtx.workspaceDir = c.Getenv("BAZEL_WORKSPACE")
+		p.workspaceDir = c.Getenv("BAZEL_WORKSPACE")
 	} else {
 		missingEnvVars = append(missingEnvVars, "BAZEL_WORKSPACE")
 	}
 	if len(c.Getenv("BAZEL_METRICS_DIR")) > 1 {
-		bazelCtx.metricsDir = c.Getenv("BAZEL_METRICS_DIR")
+		p.metricsDir = c.Getenv("BAZEL_METRICS_DIR")
 	} else {
 		missingEnvVars = append(missingEnvVars, "BAZEL_METRICS_DIR")
 	}
 	if len(missingEnvVars) > 0 {
 		return nil, errors.New(fmt.Sprintf("missing required env vars to use bazel: %s", missingEnvVars))
 	} else {
-		return &bazelCtx, nil
+		return &p, nil
 	}
 }
 
-func (context *bazelContext) BazelMetricsDir() string {
-	return context.metricsDir
+func (p *bazelPaths) BazelMetricsDir() string {
+	return p.metricsDir
 }
 
 func (context *bazelContext) BazelEnabled() bool {
@@ -296,17 +318,40 @@
 	return ""
 }
 
+type bazelCommand struct {
+	command string
+	// query or label
+	expression string
+}
+
+type mockBazelRunner struct {
+	bazelCommandResults map[bazelCommand]string
+	commands            []bazelCommand
+}
+
+func (r *mockBazelRunner) issueBazelCommand(paths *bazelPaths,
+	runName bazel.RunName,
+	command bazelCommand,
+	extraFlags ...string) (string, string, error) {
+	r.commands = append(r.commands, command)
+	if ret, ok := r.bazelCommandResults[command]; ok {
+		return ret, "", nil
+	}
+	return "", "", nil
+}
+
+type builtinBazelRunner struct{}
+
 // Issues the given bazel command with given build label and additional flags.
 // Returns (stdout, stderr, error). The first and second return values are strings
 // containing the stdout and stderr of the run command, and an error is returned if
 // the invocation returned an error code.
-func (context *bazelContext) issueBazelCommand(runName bazel.RunName, command string, labels []string,
+func (r *builtinBazelRunner) issueBazelCommand(paths *bazelPaths, runName bazel.RunName, command bazelCommand,
 	extraFlags ...string) (string, string, error) {
-
-	cmdFlags := []string{"--output_base=" + context.outputBase, command}
-	cmdFlags = append(cmdFlags, labels...)
-	cmdFlags = append(cmdFlags, "--package_path=%workspace%/"+context.intermediatesDir())
-	cmdFlags = append(cmdFlags, "--profile="+shared.BazelMetricsFilename(context, runName))
+	cmdFlags := []string{"--output_base=" + paths.outputBase, command.command}
+	cmdFlags = append(cmdFlags, command.expression)
+	cmdFlags = append(cmdFlags, "--package_path=%workspace%/"+paths.intermediatesDir())
+	cmdFlags = append(cmdFlags, "--profile="+shared.BazelMetricsFilename(paths, runName))
 
 	// Set default platforms to canonicalized values for mixed builds requests.
 	// If these are set in the bazelrc, they will have values that are
@@ -328,9 +373,9 @@
 	cmdFlags = append(cmdFlags, "--experimental_repository_disable_download")
 	cmdFlags = append(cmdFlags, extraFlags...)
 
-	bazelCmd := exec.Command(context.bazelPath, cmdFlags...)
-	bazelCmd.Dir = context.workspaceDir
-	bazelCmd.Env = append(os.Environ(), "HOME="+context.homeDir, pwdPrefix(),
+	bazelCmd := exec.Command(paths.bazelPath, cmdFlags...)
+	bazelCmd.Dir = paths.workspaceDir
+	bazelCmd.Env = append(os.Environ(), "HOME="+paths.homeDir, pwdPrefix(),
 		// Disables local host detection of gcc; toolchain information is defined
 		// explicitly in BUILD files.
 		"BAZEL_DO_NOT_DETECT_CPP_TOOLCHAIN=1")
@@ -367,7 +412,7 @@
     path = "%[1]s/build/bazel/bazel_skylib",
 )
 `
-	return []byte(fmt.Sprintf(formatString, context.workspaceDir))
+	return []byte(fmt.Sprintf(formatString, context.paths.workspaceDir))
 }
 
 func (context *bazelContext) mainBzlFileContents() []byte {
@@ -577,8 +622,8 @@
 
 // Returns a workspace-relative path containing build-related metadata required
 // for interfacing with Bazel. Example: out/soong/bazel.
-func (context *bazelContext) intermediatesDir() string {
-	return filepath.Join(context.buildDir, "bazel")
+func (p *bazelPaths) intermediatesDir() string {
+	return filepath.Join(p.buildDir, "bazel")
 }
 
 // Issues commands to Bazel to receive results for all cquery requests
@@ -590,7 +635,7 @@
 	var cqueryErr string
 	var err error
 
-	intermediatesDirPath := absolutePath(context.intermediatesDir())
+	intermediatesDirPath := absolutePath(context.paths.intermediatesDir())
 	if _, err := os.Stat(intermediatesDirPath); os.IsNotExist(err) {
 		err = os.Mkdir(intermediatesDirPath, 0777)
 	}
@@ -599,38 +644,38 @@
 		return err
 	}
 	err = ioutil.WriteFile(
-		absolutePath(filepath.Join(context.intermediatesDir(), "main.bzl")),
+		filepath.Join(intermediatesDirPath, "main.bzl"),
 		context.mainBzlFileContents(), 0666)
 	if err != nil {
 		return err
 	}
 	err = ioutil.WriteFile(
-		absolutePath(filepath.Join(context.intermediatesDir(), "BUILD.bazel")),
+		filepath.Join(intermediatesDirPath, "BUILD.bazel"),
 		context.mainBuildFileContents(), 0666)
 	if err != nil {
 		return err
 	}
-	cqueryFileRelpath := filepath.Join(context.intermediatesDir(), "buildroot.cquery")
+	cqueryFileRelpath := filepath.Join(context.paths.intermediatesDir(), "buildroot.cquery")
 	err = ioutil.WriteFile(
 		absolutePath(cqueryFileRelpath),
 		context.cqueryStarlarkFileContents(), 0666)
 	if err != nil {
 		return err
 	}
-	workspaceFileRelpath := filepath.Join(context.intermediatesDir(), "WORKSPACE.bazel")
 	err = ioutil.WriteFile(
-		absolutePath(workspaceFileRelpath),
+		filepath.Join(intermediatesDirPath, "WORKSPACE.bazel"),
 		context.workspaceFileContents(), 0666)
 	if err != nil {
 		return err
 	}
 	buildrootLabel := "//:buildroot"
-	cqueryOutput, cqueryErr, err = context.issueBazelCommand(bazel.CqueryBuildRootRunName, "cquery",
-		[]string{fmt.Sprintf("kind(rule, deps(%s))", buildrootLabel)},
+	cqueryOutput, cqueryErr, err = context.issueBazelCommand(
+		context.paths,
+		bazel.CqueryBuildRootRunName,
+		bazelCommand{"cquery", fmt.Sprintf("kind(rule, deps(%s))", buildrootLabel)},
 		"--output=starlark",
 		"--starlark:file="+cqueryFileRelpath)
-	err = ioutil.WriteFile(
-		absolutePath(filepath.Join(context.intermediatesDir(), "cquery.out")),
+	err = ioutil.WriteFile(filepath.Join(intermediatesDirPath, "cquery.out"),
 		[]byte(cqueryOutput), 0666)
 	if err != nil {
 		return err
@@ -661,11 +706,13 @@
 	//
 	// TODO(cparsons): Use --target_pattern_file to avoid command line limits.
 	var aqueryOutput string
-	aqueryOutput, _, err = context.issueBazelCommand(bazel.AqueryBuildRootRunName, "aquery",
-		[]string{fmt.Sprintf("deps(%s)", buildrootLabel),
-			// Use jsonproto instead of proto; actual proto parsing would require a dependency on Bazel's
-			// proto sources, which would add a number of unnecessary dependencies.
-			"--output=jsonproto"})
+	aqueryOutput, _, err = context.issueBazelCommand(
+		context.paths,
+		bazel.AqueryBuildRootRunName,
+		bazelCommand{"aquery", fmt.Sprintf("deps(%s)", buildrootLabel)},
+		// Use jsonproto instead of proto; actual proto parsing would require a dependency on Bazel's
+		// proto sources, which would add a number of unnecessary dependencies.
+		"--output=jsonproto")
 
 	if err != nil {
 		return err
@@ -679,8 +726,10 @@
 	// Issue a build command of the phony root to generate symlink forests for dependencies of the
 	// Bazel build. This is necessary because aquery invocations do not generate this symlink forest,
 	// but some of symlinks may be required to resolve source dependencies of the build.
-	_, _, err = context.issueBazelCommand(bazel.BazelBuildPhonyRootRunName, "build",
-		[]string{"//:phonyroot"})
+	_, _, err = context.issueBazelCommand(
+		context.paths,
+		bazel.BazelBuildPhonyRootRunName,
+		bazelCommand{"build", "//:phonyroot"})
 
 	if err != nil {
 		return err
@@ -696,7 +745,7 @@
 }
 
 func (context *bazelContext) OutputBase() string {
-	return context.outputBase
+	return context.paths.outputBase
 }
 
 // Singleton used for registering BUILD file ninja dependencies (needed
diff --git a/android/bazel_handler_test.go b/android/bazel_handler_test.go
new file mode 100644
index 0000000..85f701f
--- /dev/null
+++ b/android/bazel_handler_test.go
@@ -0,0 +1,118 @@
+package android
+
+import (
+	"os"
+	"path/filepath"
+	"reflect"
+	"testing"
+)
+
+func TestRequestResultsAfterInvokeBazel(t *testing.T) {
+	label := "//foo:bar"
+	arch := Arm64
+	bazelContext, _ := testBazelContext(t, map[bazelCommand]string{
+		bazelCommand{command: "cquery", expression: "kind(rule, deps(//:buildroot))"}: `@sourceroot//foo:bar|arm64>>out/foo/bar.txt`,
+	})
+	g, ok := bazelContext.GetOutputFiles(label, arch)
+	if ok {
+		t.Errorf("Did not expect cquery results prior to running InvokeBazel(), but got %s", g)
+	}
+	err := bazelContext.InvokeBazel()
+	if err != nil {
+		t.Fatalf("Did not expect error invoking Bazel, but got %s", err)
+	}
+	g, ok = bazelContext.GetOutputFiles(label, arch)
+	if !ok {
+		t.Errorf("Expected cquery results after running InvokeBazel(), but got none")
+	} else if w := []string{"out/foo/bar.txt"}; !reflect.DeepEqual(w, g) {
+		t.Errorf("Expected output %s, got %s", w, g)
+	}
+}
+
+func TestInvokeBazelWritesBazelFiles(t *testing.T) {
+	bazelContext, baseDir := testBazelContext(t, map[bazelCommand]string{})
+	err := bazelContext.InvokeBazel()
+	if err != nil {
+		t.Fatalf("Did not expect error invoking Bazel, but got %s", err)
+	}
+	if _, err := os.Stat(filepath.Join(baseDir, "bazel", "main.bzl")); os.IsNotExist(err) {
+		t.Errorf("Expected main.bzl to exist, but it does not")
+	} else if err != nil {
+		t.Errorf("Unexpected error stating main.bzl %s", err)
+	}
+
+	if _, err := os.Stat(filepath.Join(baseDir, "bazel", "BUILD.bazel")); os.IsNotExist(err) {
+		t.Errorf("Expected BUILD.bazel to exist, but it does not")
+	} else if err != nil {
+		t.Errorf("Unexpected error stating BUILD.bazel %s", err)
+	}
+
+	if _, err := os.Stat(filepath.Join(baseDir, "bazel", "WORKSPACE.bazel")); os.IsNotExist(err) {
+		t.Errorf("Expected WORKSPACE.bazel to exist, but it does not")
+	} else if err != nil {
+		t.Errorf("Unexpected error stating WORKSPACE.bazel %s", err)
+	}
+}
+
+func TestInvokeBazelPopulatesBuildStatements(t *testing.T) {
+	bazelContext, _ := testBazelContext(t, map[bazelCommand]string{
+		bazelCommand{command: "aquery", expression: "deps(//:buildroot)"}: `
+{
+  "artifacts": [{
+    "id": 1,
+    "pathFragmentId": 1
+  }, {
+    "id": 2,
+    "pathFragmentId": 2
+  }],
+  "actions": [{
+    "targetId": 1,
+    "actionKey": "x",
+    "mnemonic": "x",
+    "arguments": ["touch", "foo"],
+    "inputDepSetIds": [1],
+    "outputIds": [1],
+    "primaryOutputId": 1
+  }],
+  "depSetOfFiles": [{
+    "id": 1,
+    "directArtifactIds": [1, 2]
+  }],
+  "pathFragments": [{
+    "id": 1,
+    "label": "one"
+  }, {
+    "id": 2,
+    "label": "two"
+  }]
+}`,
+	})
+	err := bazelContext.InvokeBazel()
+	if err != nil {
+		t.Fatalf("Did not expect error invoking Bazel, but got %s", err)
+	}
+
+	got := bazelContext.BuildStatementsToRegister()
+	if want := 1; len(got) != want {
+		t.Errorf("Expected %d registered build statements, got %#v", want, got)
+	}
+}
+
+func testBazelContext(t *testing.T, bazelCommandResults map[bazelCommand]string) (*bazelContext, string) {
+	t.Helper()
+	p := bazelPaths{
+		buildDir:     t.TempDir(),
+		outputBase:   "outputbase",
+		workspaceDir: "workspace_dir",
+	}
+	aqueryCommand := bazelCommand{command: "aquery", expression: "deps(//:buildroot)"}
+	if _, exists := bazelCommandResults[aqueryCommand]; !exists {
+		bazelCommandResults[aqueryCommand] = "{}\n"
+	}
+	runner := &mockBazelRunner{bazelCommandResults: bazelCommandResults}
+	return &bazelContext{
+		bazelRunner: runner,
+		paths:       &p,
+		requests:    map[cqueryKey]bool{},
+	}, p.buildDir
+}