Add "test-only" flag for cc modules

As part of aosp/3022586 where we added the idea of "test-only" modules
and top_level_test_targets, this CL implements that for cc_ modules.

We let users set "test-only" on cc_library, but not on other modules
where the module kind is implicitly test-only, like cc_test.
Here the implementation, not the user decides it is test-only.

% gqui from  "flatten(~/aosp-main-with-phones/out/soong/ownership/all_teams.pb, teams)" proto team.proto:AllTeams 'select teams.kind, count(*) where teams.top_level_target = true group by teams.kind'                    aosp_shiba[6:15:47]/0
+--------------+----------+
|  teams.kind  | count(*) |
+--------------+----------+
| art_cc_test  |       56 |
| cc_benchmark |       68 |
| cc_fuzz      |      515 |
| cc_test      |     3518 |
| cc_test_host |        6 |
+--------------+----------+

 % gqui from  "flatten(~/aosp-main-with-phones/out/soong/ownership/all_teams.pb, teams)" proto team.proto:AllTeams 'select teams.kind, count(*) where teams.test_only = true group by teams.kind'                           aosp_shiba[6:16:26]/0
+--------------------------+----------+
|        teams.kind        | count(*) |
+--------------------------+----------+
| art_cc_test              |       56 |
| art_cc_test_library      |       13 |
| cc_benchmark             |       68 |
| cc_fuzz                  |      515 |
| cc_test                  |     3518 |
| cc_test_host             |        6 |
| cc_test_library          |      484 |
+--------------------------+----------+

Bug: b/327280661

Test: m nothing --no-skip-soong-tests
Test: go test ./cc
Test: m all_teams
Change-Id: I344436c424a9dfbdcf27e10f42f5cebc3d2b1261
diff --git a/cc/Android.bp b/cc/Android.bp
index 8c86eb7..5ba9427 100644
--- a/cc/Android.bp
+++ b/cc/Android.bp
@@ -91,6 +91,7 @@
         "afdo_test.go",
         "binary_test.go",
         "cc_test.go",
+        "cc_test_only_property_test.go",
         "compiler_test.go",
         "gen_test.go",
         "genrule_test.go",
diff --git a/cc/cc.go b/cc/cc.go
index 69ecc78..6cbf458 100644
--- a/cc/cc.go
+++ b/cc/cc.go
@@ -845,6 +845,7 @@
 
 	VendorProperties VendorProperties
 	Properties       BaseProperties
+	sourceProperties android.SourceProperties
 
 	// initialize before calling Init
 	hod        android.HostOrDeviceSupported
@@ -1262,6 +1263,10 @@
 	for _, feature := range c.features {
 		c.AddProperties(feature.props()...)
 	}
+	// Allow test-only on libraries that are not cc_test_library
+	if c.library != nil && !c.testLibrary() {
+		c.AddProperties(&c.sourceProperties)
+	}
 
 	android.InitAndroidArchModule(c, c.hod, c.multilib)
 	android.InitApexModule(c)
@@ -2135,6 +2140,18 @@
 	if c.testModule {
 		android.SetProvider(ctx, testing.TestModuleProviderKey, testing.TestModuleProviderData{})
 	}
+
+	// If Test_only is set on a module in bp file, respect the setting, otherwise
+	// see if is a known test module type.
+	testOnly := c.testModule || c.testLibrary()
+	if c.sourceProperties.Test_only != nil {
+		testOnly = Bool(c.sourceProperties.Test_only)
+	}
+	android.SetProvider(ctx, android.TestOnlyProviderKey, android.TestModuleInformation{
+		TestOnly:       testOnly,
+		TopLevelTarget: c.testModule,
+	})
+
 	android.SetProvider(ctx, blueprint.SrcsFileProviderKey, blueprint.SrcsFileProviderData{SrcPaths: deps.GeneratedSources.Strings()})
 
 	android.CollectDependencyAconfigFiles(ctx, &c.mergedAconfigFiles)
diff --git a/cc/cc_test_only_property_test.go b/cc/cc_test_only_property_test.go
new file mode 100644
index 0000000..972e86b
--- /dev/null
+++ b/cc/cc_test_only_property_test.go
@@ -0,0 +1,187 @@
+// Copyright 2024 Google Inc. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package cc
+
+import (
+	"android/soong/android"
+	"android/soong/android/team_proto"
+	"log"
+	"strings"
+	"testing"
+
+	"github.com/google/blueprint"
+	"google.golang.org/protobuf/proto"
+)
+
+func TestTestOnlyProvider(t *testing.T) {
+	t.Parallel()
+	ctx := android.GroupFixturePreparers(
+		prepareForCcTest,
+		android.FixtureRegisterWithContext(func(ctx android.RegistrationContext) {
+			ctx.RegisterModuleType("cc_test_host", TestHostFactory)
+		}),
+	).RunTestWithBp(t, `
+                // These should be test-only
+                cc_fuzz { name: "cc-fuzz" }
+                cc_test { name: "cc-test", gtest:false }
+                cc_benchmark { name: "cc-benchmark" }
+                cc_library { name: "cc-library-forced",
+                             test_only: true }
+                cc_test_library {name: "cc-test-library", gtest: false}
+                cc_test_host {name: "cc-test-host", gtest: false}
+
+                // These should not be.
+                cc_genrule { name: "cc_genrule", cmd: "echo foo", out: ["out"] }
+                cc_library { name: "cc_library" }
+                cc_library { name: "cc_library_false", test_only: false }
+                cc_library_static { name: "cc_static" }
+                cc_library_shared { name: "cc_library_shared" }
+
+                cc_object { name: "cc-object" }
+	`)
+
+	// Visit all modules and ensure only the ones that should
+	// marked as test-only are marked as test-only.
+
+	actualTestOnly := []string{}
+	ctx.VisitAllModules(func(m blueprint.Module) {
+		if provider, ok := android.OtherModuleProvider(ctx.TestContext.OtherModuleProviderAdaptor(), m, android.TestOnlyProviderKey); ok {
+			if provider.TestOnly {
+				actualTestOnly = append(actualTestOnly, m.Name())
+			}
+		}
+	})
+	expectedTestOnlyModules := []string{
+		"cc-test",
+		"cc-library-forced",
+		"cc-fuzz",
+		"cc-benchmark",
+		"cc-test-library",
+		"cc-test-host",
+	}
+
+	notEqual, left, right := android.ListSetDifference(expectedTestOnlyModules, actualTestOnly)
+	if notEqual {
+		t.Errorf("test-only: Expected but not found: %v, Found but not expected: %v", left, right)
+	}
+}
+
+func TestTestOnlyInTeamsProto(t *testing.T) {
+	t.Parallel()
+	ctx := android.GroupFixturePreparers(
+		android.PrepareForTestWithTeamBuildComponents,
+		prepareForCcTest,
+		android.FixtureRegisterWithContext(func(ctx android.RegistrationContext) {
+			ctx.RegisterParallelSingletonType("all_teams", android.AllTeamsFactory)
+			ctx.RegisterModuleType("cc_test_host", TestHostFactory)
+
+		}),
+	).RunTestWithBp(t, `
+                package { default_team: "someteam"}
+
+                // These should be test-only
+                cc_fuzz { name: "cc-fuzz" }
+                cc_test { name: "cc-test", gtest:false }
+                cc_benchmark { name: "cc-benchmark" }
+                cc_library { name: "cc-library-forced",
+                             test_only: true }
+                cc_test_library {name: "cc-test-library", gtest: false}
+                cc_test_host {name: "cc-test-host", gtest: false}
+
+                // These should not be.
+                cc_genrule { name: "cc_genrule", cmd: "echo foo", out: ["out"] }
+                cc_library { name: "cc_library" }
+                cc_library_static { name: "cc_static" }
+                cc_library_shared { name: "cc_library_shared" }
+
+                cc_object { name: "cc-object" }
+		team {
+			name: "someteam",
+			trendy_team_id: "cool_team",
+		}
+	`)
+
+	var teams *team_proto.AllTeams
+	teams = getTeamProtoOutput(t, ctx)
+
+	// map of module name -> trendy team name.
+	actualTrueModules := []string{}
+	for _, teamProto := range teams.Teams {
+		if Bool(teamProto.TestOnly) {
+			actualTrueModules = append(actualTrueModules, teamProto.GetTargetName())
+		}
+	}
+	expectedTestOnlyModules := []string{
+		"cc-test",
+		"cc-library-forced",
+		"cc-fuzz",
+		"cc-benchmark",
+		"cc-test-library",
+		"cc-test-host",
+	}
+
+	notEqual, left, right := android.ListSetDifference(expectedTestOnlyModules, actualTrueModules)
+	if notEqual {
+		t.Errorf("test-only: Expected but not found: %v, Found but not expected: %v", left, right)
+	}
+}
+
+// Don't allow setting test-only on things that are always tests or never tests.
+func TestInvalidTestOnlyTargets(t *testing.T) {
+	testCases := []string{
+		` cc_test {  name: "cc-test", test_only: true, gtest: false, srcs: ["foo.cc"],  } `,
+		` cc_binary {  name: "cc-binary", test_only: true, srcs: ["foo.cc"],  } `,
+		` cc_test_library {name: "cc-test-library", test_only: true, gtest: false} `,
+		` cc_test_host {name: "cc-test-host", test_only: true, gtest: false} `,
+		` cc_defaults {name: "cc-defaults", test_only: true} `,
+	}
+
+	for i, bp := range testCases {
+		ctx := android.GroupFixturePreparers(
+			prepareForCcTest,
+			android.FixtureRegisterWithContext(func(ctx android.RegistrationContext) {
+				ctx.RegisterModuleType("cc_test_host", TestHostFactory)
+			})).
+			ExtendWithErrorHandler(android.FixtureIgnoreErrors).
+			RunTestWithBp(t, bp)
+		if len(ctx.Errs) == 0 {
+			t.Errorf("Expected err setting test_only in testcase #%d", i)
+		}
+		if len(ctx.Errs) > 1 {
+			t.Errorf("Too many errs: [%s] %v", bp, ctx.Errs)
+		}
+
+		if len(ctx.Errs) == 1 {
+			if !strings.Contains(ctx.Errs[0].Error(), "unrecognized property \"test_only\"") {
+				t.Errorf("ERR: %s bad bp: %s", ctx.Errs[0], bp)
+			}
+		}
+	}
+}
+
+func getTeamProtoOutput(t *testing.T, ctx *android.TestResult) *team_proto.AllTeams {
+	teams := new(team_proto.AllTeams)
+	config := ctx.SingletonForTests("all_teams")
+	allOutputs := config.AllOutputs()
+
+	protoPath := allOutputs[0]
+
+	out := config.MaybeOutput(protoPath)
+	outProto := []byte(android.ContentFromFileRuleForTests(t, ctx.TestContext, out))
+	if err := proto.Unmarshal(outProto, teams); err != nil {
+		log.Fatalln("Failed to parse teams proto:", err)
+	}
+	return teams
+}