Support cc code coverage for mixed build

Bug: 231322627
Test: Manual tests and unit tests
Change-Id: I786042af0d612192c54c3572f63a86a47174a242
diff --git a/android/bazel_handler.go b/android/bazel_handler.go
index 4d9423a..4dae6f6 100644
--- a/android/bazel_handler.go
+++ b/android/bazel_handler.go
@@ -111,7 +111,7 @@
 
 	// Issues commands to Bazel to receive results for all cquery requests
 	// queued in the BazelContext.
-	InvokeBazel() error
+	InvokeBazel(config Config) error
 
 	// Returns true if bazel is enabled for the given configuration.
 	BazelEnabled() bool
@@ -191,7 +191,7 @@
 	return result, nil
 }
 
-func (m MockBazelContext) InvokeBazel() error {
+func (m MockBazelContext) InvokeBazel(config Config) error {
 	panic("unimplemented")
 }
 
@@ -261,7 +261,7 @@
 	panic("unimplemented")
 }
 
-func (n noopBazelContext) InvokeBazel() error {
+func (n noopBazelContext) InvokeBazel(config Config) error {
 	panic("unimplemented")
 }
 
@@ -361,6 +361,7 @@
 type mockBazelRunner struct {
 	bazelCommandResults map[bazelCommand]string
 	commands            []bazelCommand
+	extraFlags          []string
 }
 
 func (r *mockBazelRunner) issueBazelCommand(paths *bazelPaths,
@@ -368,6 +369,7 @@
 	command bazelCommand,
 	extraFlags ...string) (string, string, error) {
 	r.commands = append(r.commands, command)
+	r.extraFlags = append(r.extraFlags, strings.Join(extraFlags, " "))
 	if ret, ok := r.bazelCommandResults[command]; ok {
 		return ret, "", nil
 	}
@@ -676,7 +678,7 @@
 
 // Issues commands to Bazel to receive results for all cquery requests
 // queued in the BazelContext.
-func (context *bazelContext) InvokeBazel() error {
+func (context *bazelContext) InvokeBazel(config Config) error {
 	context.results = make(map[cqueryKey]string)
 
 	var cqueryOutput string
@@ -759,15 +761,31 @@
 
 	// Issue an aquery command to retrieve action information about the bazel build tree.
 	//
-	// TODO(cparsons): Use --target_pattern_file to avoid command line limits.
 	var aqueryOutput string
+	var coverageFlags []string
+	if Bool(config.productVariables.ClangCoverage) {
+		coverageFlags = append(coverageFlags, "--collect_code_coverage")
+		if len(config.productVariables.NativeCoveragePaths) > 0 ||
+			len(config.productVariables.NativeCoverageExcludePaths) > 0 {
+			includePaths := JoinWithPrefixAndSeparator(config.productVariables.NativeCoveragePaths, "+", ",")
+			excludePaths := JoinWithPrefixAndSeparator(config.productVariables.NativeCoverageExcludePaths, "-", ",")
+			if len(includePaths) > 0 && len(excludePaths) > 0 {
+				includePaths += ","
+			}
+			coverageFlags = append(coverageFlags, fmt.Sprintf(`--instrumentation_filter=%s`,
+				includePaths+excludePaths))
+		}
+	}
+
+	extraFlags := append([]string{"--output=jsonproto"}, coverageFlags...)
+
 	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")
+		extraFlags...)
 
 	if err != nil {
 		return err
diff --git a/android/bazel_handler_test.go b/android/bazel_handler_test.go
index cfdccd7..dd9a7ed 100644
--- a/android/bazel_handler_test.go
+++ b/android/bazel_handler_test.go
@@ -4,11 +4,14 @@
 	"os"
 	"path/filepath"
 	"reflect"
+	"strings"
 	"testing"
 
 	"android/soong/bazel/cquery"
 )
 
+var testConfig = TestConfig("out", nil, "", nil)
+
 func TestRequestResultsAfterInvokeBazel(t *testing.T) {
 	label := "//foo:bar"
 	cfg := configKey{"arm64_armv8-a", Android}
@@ -16,7 +19,7 @@
 		bazelCommand{command: "cquery", expression: "deps(@soong_injection//mixed_builds:buildroot, 2)"}: `//foo:bar|arm64_armv8-a|android>>out/foo/bar.txt`,
 	})
 	bazelContext.QueueBazelRequest(label, cquery.GetOutputFiles, cfg)
-	err := bazelContext.InvokeBazel()
+	err := bazelContext.InvokeBazel(testConfig)
 	if err != nil {
 		t.Fatalf("Did not expect error invoking Bazel, but got %s", err)
 	}
@@ -30,7 +33,7 @@
 
 func TestInvokeBazelWritesBazelFiles(t *testing.T) {
 	bazelContext, baseDir := testBazelContext(t, map[bazelCommand]string{})
-	err := bazelContext.InvokeBazel()
+	err := bazelContext.InvokeBazel(testConfig)
 	if err != nil {
 		t.Fatalf("Did not expect error invoking Bazel, but got %s", err)
 	}
@@ -86,7 +89,7 @@
   }]
 }`,
 	})
-	err := bazelContext.InvokeBazel()
+	err := bazelContext.InvokeBazel(testConfig)
 	if err != nil {
 		t.Fatalf("Did not expect error invoking Bazel, but got %s", err)
 	}
@@ -97,6 +100,54 @@
 	}
 }
 
+func TestCoverageFlagsAfterInvokeBazel(t *testing.T) {
+	testConfig.productVariables.ClangCoverage = boolPtr(true)
+
+	testConfig.productVariables.NativeCoveragePaths = []string{"foo1", "foo2"}
+	testConfig.productVariables.NativeCoverageExcludePaths = []string{"bar1", "bar2"}
+	verifyExtraFlags(t, testConfig, `--collect_code_coverage --instrumentation_filter=+foo1,+foo2,-bar1,-bar2`)
+
+	testConfig.productVariables.NativeCoveragePaths = []string{"foo1"}
+	testConfig.productVariables.NativeCoverageExcludePaths = []string{"bar1"}
+	verifyExtraFlags(t, testConfig, `--collect_code_coverage --instrumentation_filter=+foo1,-bar1`)
+
+	testConfig.productVariables.NativeCoveragePaths = []string{"foo1"}
+	testConfig.productVariables.NativeCoverageExcludePaths = nil
+	verifyExtraFlags(t, testConfig, `--collect_code_coverage --instrumentation_filter=+foo1`)
+
+	testConfig.productVariables.NativeCoveragePaths = nil
+	testConfig.productVariables.NativeCoverageExcludePaths = []string{"bar1"}
+	verifyExtraFlags(t, testConfig, `--collect_code_coverage --instrumentation_filter=-bar1`)
+
+	testConfig.productVariables.ClangCoverage = boolPtr(false)
+	actual := verifyExtraFlags(t, testConfig, ``)
+	if strings.Contains(actual, "--collect_code_coverage") ||
+		strings.Contains(actual, "--instrumentation_filter=") {
+		t.Errorf("Expected code coverage disabled, but got %#v", actual)
+	}
+}
+
+func verifyExtraFlags(t *testing.T, config Config, expected string) string {
+	bazelContext, _ := testBazelContext(t, map[bazelCommand]string{})
+
+	err := bazelContext.InvokeBazel(config)
+	if err != nil {
+		t.Fatalf("Did not expect error invoking Bazel, but got %s", err)
+	}
+
+	flags := bazelContext.bazelRunner.(*mockBazelRunner).extraFlags
+	if expected := 3; len(flags) != expected {
+		t.Errorf("Expected %d extra flags got %#v", expected, flags)
+	}
+
+	actual := flags[1]
+	if !strings.Contains(actual, expected) {
+		t.Errorf("Expected %#v got %#v", expected, actual)
+	}
+
+	return actual
+}
+
 func testBazelContext(t *testing.T, bazelCommandResults map[bazelCommand]string) (*bazelContext, string) {
 	t.Helper()
 	p := bazelPaths{
diff --git a/android/util.go b/android/util.go
index 47c4583..a0f7160 100644
--- a/android/util.go
+++ b/android/util.go
@@ -32,6 +32,12 @@
 // JoinWithPrefix prepends the prefix to each string in the list and
 // returns them joined together with " " as separator.
 func JoinWithPrefix(strs []string, prefix string) string {
+	return JoinWithPrefixAndSeparator(strs, prefix, " ")
+}
+
+// JoinWithPrefixAndSeparator prepends the prefix to each string in the list and
+// returns them joined together with the given separator.
+func JoinWithPrefixAndSeparator(strs []string, prefix string, sep string) string {
 	if len(strs) == 0 {
 		return ""
 	}
@@ -40,7 +46,7 @@
 	buf.WriteString(prefix)
 	buf.WriteString(strs[0])
 	for i := 1; i < len(strs); i++ {
-		buf.WriteString(" ")
+		buf.WriteString(sep)
 		buf.WriteString(prefix)
 		buf.WriteString(strs[i])
 	}