Allow test_suite to nest

This is useful for creating a suite of suites, or for creating a suite that includes all tests in a directory.

Bug: 372945132
Change-Id: Ifa21a69b3798f0dccf80eca0a31e5e8701a17e12
diff --git a/tradefed_modules/test_module_config.go b/tradefed_modules/test_module_config.go
index 7a04c19..5c13d64 100644
--- a/tradefed_modules/test_module_config.go
+++ b/tradefed_modules/test_module_config.go
@@ -383,6 +383,19 @@
 
 	// 4) Module.config / AndroidTest.xml
 	m.testConfig = m.fixTestConfig(ctx, m.provider.TestConfig)
+
+	// 5) We provide so we can be listed in test_suites.
+	android.SetProvider(ctx, tradefed.BaseTestProviderKey, tradefed.BaseTestProviderData{
+		InstalledFiles:          m.supportFiles.Paths(),
+		OutputFile:              baseApk,
+		TestConfig:              m.testConfig,
+		HostRequiredModuleNames: m.provider.HostRequiredModuleNames,
+		RequiredModuleNames:     m.provider.RequiredModuleNames,
+		TestSuites:              m.tradefedProperties.Test_suites,
+		IsHost:                  m.provider.IsHost,
+		LocalCertificate:        m.provider.LocalCertificate,
+		IsUnitTest:              m.provider.IsUnitTest,
+	})
 }
 
 var _ android.AndroidMkEntriesProvider = (*testModuleConfigHostModule)(nil)
diff --git a/tradefed_modules/test_suite.go b/tradefed_modules/test_suite.go
index cc35ce7..00585f5 100644
--- a/tradefed_modules/test_suite.go
+++ b/tradefed_modules/test_suite.go
@@ -24,6 +24,8 @@
 	"github.com/google/blueprint"
 )
 
+const testSuiteModuleType = "test_suite"
+
 type testSuiteTag struct{
 	blueprint.BaseDependencyTag
 }
@@ -38,7 +40,7 @@
 }
 
 func RegisterTestSuiteBuildComponents(ctx android.RegistrationContext) {
-	ctx.RegisterModuleType("test_suite", TestSuiteFactory)
+	ctx.RegisterModuleType(testSuiteModuleType, TestSuiteFactory)
 }
 
 var PrepareForTestWithTestSuiteBuildComponents = android.GroupFixturePreparers(
@@ -69,8 +71,15 @@
 }
 
 func (t *testSuiteModule) GenerateAndroidBuildActions(ctx android.ModuleContext) {
+	suiteName := ctx.ModuleName()
 	modulesByName := make(map[string]android.Module)
 	ctx.WalkDeps(func(child, parent android.Module) bool {
+		// Recurse into test_suite dependencies.
+		if ctx.OtherModuleType(child) == testSuiteModuleType {
+			ctx.Phony(suiteName, android.PathForPhony(ctx, child.Name()))
+			return true
+		}
+
 		// Only write out top level test suite dependencies here.
 		if _, ok := ctx.OtherModuleDependencyTag(child).(testSuiteTag); !ok {
 			return false
@@ -85,7 +94,6 @@
 		return false
 	})
 
-	suiteName := ctx.ModuleName()
 	var files []string
 	for name, module := range modulesByName {
 		// Get the test provider data from the child.
@@ -115,7 +123,7 @@
 	module := &testSuiteModule{}
 	module.AddProperties(&module.testSuiteProperties)
 
-	android.InitAndroidModule(module)
+	android.InitAndroidArchModule(module, android.HostAndDeviceSupported, android.MultilibCommon)
 	android.InitDefaultableModule(module)
 
 	return module
diff --git a/tradefed_modules/test_suite_test.go b/tradefed_modules/test_suite_test.go
index 57e760e..3c0a9eb 100644
--- a/tradefed_modules/test_suite_test.go
+++ b/tradefed_modules/test_suite_test.go
@@ -24,7 +24,6 @@
 func TestTestSuites(t *testing.T) {
 	t.Parallel()
 	ctx := android.GroupFixturePreparers(
-		android.PrepareForTestWithArchMutator,
 		java.PrepareForTestWithJavaDefaultModules,
 		android.FixtureRegisterWithContext(RegisterTestSuiteBuildComponents),
 	).RunTestWithBp(t, `
@@ -47,7 +46,7 @@
 			]
 		}
 	`)
-	manifestPath := ctx.ModuleForTests("my-suite", "").Output("out/soong/test_suites/my-suite/my-suite.json")
+	manifestPath := ctx.ModuleForTests("my-suite", "android_common").Output("out/soong/test_suites/my-suite/my-suite.json")
 	var actual testSuiteManifest
 	if err := json.Unmarshal([]byte(android.ContentFromFileRuleForTests(t, ctx.TestContext, manifestPath)), &actual); err != nil {
 		t.Errorf("failed to unmarshal manifest: %v", err)
@@ -67,10 +66,71 @@
 	android.AssertDeepEquals(t, "manifests differ", expected, actual)
 }
 
+func TestTestSuitesWithNested(t *testing.T) {
+	t.Parallel()
+	ctx := android.GroupFixturePreparers(
+		java.PrepareForTestWithJavaDefaultModules,
+		android.FixtureRegisterWithContext(RegisterTestSuiteBuildComponents),
+	).RunTestWithBp(t, `
+		android_test {
+			name: "TestModule1",
+			sdk_version: "current",
+		}
+
+		android_test {
+			name: "TestModule2",
+			sdk_version: "current",
+		}
+
+		android_test {
+			name: "TestModule3",
+			sdk_version: "current",
+		}
+
+		test_suite {
+			name: "my-child-suite",
+			description: "a child test suite",
+			tests: [
+				"TestModule1",
+				"TestModule2",
+			]
+		}
+
+		test_suite {
+			name: "my-all-tests-suite",
+			description: "a parent test suite",
+			tests: [
+				"TestModule1",
+				"TestModule3",
+				"my-child-suite",
+			]
+		}
+	`)
+	manifestPath := ctx.ModuleForTests("my-all-tests-suite", "android_common").Output("out/soong/test_suites/my-all-tests-suite/my-all-tests-suite.json")
+	var actual testSuiteManifest
+	if err := json.Unmarshal([]byte(android.ContentFromFileRuleForTests(t, ctx.TestContext, manifestPath)), &actual); err != nil {
+		t.Errorf("failed to unmarshal manifest: %v", err)
+	}
+	slices.Sort(actual.Files)
+
+	expected := testSuiteManifest{
+		Name: "my-all-tests-suite",
+		Files: []string{
+			"target/testcases/TestModule1/TestModule1.config",
+			"target/testcases/TestModule1/arm64/TestModule1.apk",
+			"target/testcases/TestModule2/TestModule2.config",
+			"target/testcases/TestModule2/arm64/TestModule2.apk",
+			"target/testcases/TestModule3/TestModule3.config",
+			"target/testcases/TestModule3/arm64/TestModule3.apk",
+		},
+	}
+
+	android.AssertDeepEquals(t, "manifests differ", expected, actual)
+}
+
 func TestTestSuitesNotInstalledInTestcases(t *testing.T) {
 	t.Parallel()
 	android.GroupFixturePreparers(
-		android.PrepareForTestWithArchMutator,
 		java.PrepareForTestWithJavaDefaultModules,
 		android.FixtureRegisterWithContext(RegisterTestSuiteBuildComponents),
 	).ExtendWithErrorHandler(android.FixtureExpectsAllErrorsToMatchAPattern([]string{