Merge "Add proto for Test ownership metadata." into main
diff --git a/java/Android.bp b/java/Android.bp
index 4450c42..29c0957 100644
--- a/java/Android.bp
+++ b/java/Android.bp
@@ -15,6 +15,7 @@
         "soong-dexpreopt",
         "soong-genrule",
         "soong-java-config",
+        "soong-testing",
         "soong-provenance",
         "soong-python",
         "soong-remoteexec",
diff --git a/java/app.go b/java/app.go
index 2271378..41848ce 100755
--- a/java/app.go
+++ b/java/app.go
@@ -22,6 +22,7 @@
 	"path/filepath"
 	"strings"
 
+	"android/soong/testing"
 	"github.com/google/blueprint"
 	"github.com/google/blueprint/proptools"
 
@@ -1193,6 +1194,7 @@
 	a.testConfig = a.FixTestConfig(ctx, testConfig)
 	a.extraTestConfigs = android.PathsForModuleSrc(ctx, a.testProperties.Test_options.Extra_test_configs)
 	a.data = android.PathsForModuleSrc(ctx, a.testProperties.Data)
+	ctx.SetProvider(testing.TestModuleProviderKey, testing.TestModuleProviderData{})
 }
 
 func (a *AndroidTest) FixTestConfig(ctx android.ModuleContext, testConfig android.Path) android.Path {
diff --git a/java/bootclasspath_fragment.go b/java/bootclasspath_fragment.go
index e92e6e6..ea3fc9d 100644
--- a/java/bootclasspath_fragment.go
+++ b/java/bootclasspath_fragment.go
@@ -23,6 +23,7 @@
 
 	"android/soong/android"
 	"android/soong/dexpreopt"
+	"android/soong/testing"
 
 	"github.com/google/blueprint/proptools"
 
@@ -505,6 +506,7 @@
 	if ctx.Module() != ctx.FinalModule() {
 		b.HideFromMake()
 	}
+	ctx.SetProvider(testing.TestModuleProviderKey, testing.TestModuleProviderData{})
 }
 
 // getProfileProviderApex returns the name of the apex that provides a boot image profile, or an
diff --git a/java/java.go b/java/java.go
index dd04188..0d90efc 100644
--- a/java/java.go
+++ b/java/java.go
@@ -27,6 +27,7 @@
 	"android/soong/bazel"
 	"android/soong/bazel/cquery"
 	"android/soong/remoteexec"
+	"android/soong/testing"
 	"android/soong/ui/metrics/bp2build_metrics_proto"
 
 	"github.com/google/blueprint"
@@ -1228,10 +1229,12 @@
 	}
 
 	j.Test.generateAndroidBuildActionsWithConfig(ctx, configs)
+	ctx.SetProvider(testing.TestModuleProviderKey, testing.TestModuleProviderData{})
 }
 
 func (j *Test) GenerateAndroidBuildActions(ctx android.ModuleContext) {
 	j.generateAndroidBuildActionsWithConfig(ctx, nil)
+	ctx.SetProvider(testing.TestModuleProviderKey, testing.TestModuleProviderData{})
 }
 
 func (j *Test) generateAndroidBuildActionsWithConfig(ctx android.ModuleContext, configs []tradefed.Config) {
diff --git a/java/robolectric.go b/java/robolectric.go
index af56339..a8e6bfa 100644
--- a/java/robolectric.go
+++ b/java/robolectric.go
@@ -22,6 +22,7 @@
 
 	"android/soong/android"
 	"android/soong/java/config"
+	"android/soong/testing"
 	"android/soong/tradefed"
 
 	"github.com/google/blueprint/proptools"
@@ -253,6 +254,7 @@
 	}
 
 	r.installFile = ctx.InstallFile(installPath, ctx.ModuleName()+".jar", r.combinedJar, installDeps...)
+	ctx.SetProvider(testing.TestModuleProviderKey, testing.TestModuleProviderData{})
 }
 
 func generateRoboTestConfig(ctx android.ModuleContext, outputFile android.WritablePath,
diff --git a/python/Android.bp b/python/Android.bp
index 7578673..87810c9 100644
--- a/python/Android.bp
+++ b/python/Android.bp
@@ -10,6 +10,7 @@
         "soong-android",
         "soong-tradefed",
         "soong-cc",
+        "soong-testing",
     ],
     srcs: [
         "binary.go",
diff --git a/python/test.go b/python/test.go
index 6e23a44..cd7c73b 100644
--- a/python/test.go
+++ b/python/test.go
@@ -17,6 +17,7 @@
 import (
 	"fmt"
 
+	"android/soong/testing"
 	"github.com/google/blueprint/proptools"
 
 	"android/soong/android"
@@ -205,6 +206,7 @@
 			p.data = append(p.data, android.DataPath{SrcPath: javaDataSrcPath})
 		}
 	}
+	ctx.SetProvider(testing.TestModuleProviderKey, testing.TestModuleProviderData{})
 }
 
 func (p *PythonTestModule) AndroidMkEntries() []android.AndroidMkEntries {
diff --git a/sh/Android.bp b/sh/Android.bp
index 1deedc7..930fcf5 100644
--- a/sh/Android.bp
+++ b/sh/Android.bp
@@ -11,6 +11,7 @@
         "soong-android",
         "soong-cc",
         "soong-java",
+        "soong-testing",
         "soong-tradefed",
     ],
     srcs: [
diff --git a/sh/sh_binary.go b/sh/sh_binary.go
index 2e869f4..1bebc60 100644
--- a/sh/sh_binary.go
+++ b/sh/sh_binary.go
@@ -20,6 +20,7 @@
 	"sort"
 	"strings"
 
+	"android/soong/testing"
 	"github.com/google/blueprint"
 	"github.com/google/blueprint/proptools"
 
@@ -452,6 +453,7 @@
 			ctx.PropertyErrorf(property, "%q of type %q is not supported", dep.Name(), ctx.OtherModuleType(dep))
 		}
 	})
+	ctx.SetProvider(testing.TestModuleProviderKey, testing.TestModuleProviderData{})
 }
 
 func (s *ShTest) InstallInData() bool {
diff --git a/testing/Android.bp b/testing/Android.bp
new file mode 100644
index 0000000..26a7d93
--- /dev/null
+++ b/testing/Android.bp
@@ -0,0 +1,19 @@
+package {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+bootstrap_go_package {
+    name: "soong-testing",
+    pkgPath: "android/soong/testing",
+    deps: [
+        "blueprint",
+        "soong-android",
+        "soong-testing-test_spec_proto",
+
+    ],
+    srcs: [
+        "test_spec.go",
+        "init.go",
+    ],
+    pluginFor: ["soong_build"],
+}
diff --git a/testing/init.go b/testing/init.go
new file mode 100644
index 0000000..8820a60
--- /dev/null
+++ b/testing/init.go
@@ -0,0 +1,27 @@
+// Copyright 2022 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 testing
+
+import (
+	"android/soong/android"
+)
+
+func init() {
+	RegisterBuildComponents(android.InitRegistrationContext)
+}
+
+func RegisterBuildComponents(ctx android.RegistrationContext) {
+	ctx.RegisterModuleType("test_spec", TestSpecFactory)
+}
diff --git a/testing/test_spec.go b/testing/test_spec.go
new file mode 100644
index 0000000..1ad2768
--- /dev/null
+++ b/testing/test_spec.go
@@ -0,0 +1,127 @@
+// Copyright 2020 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 testing
+
+import (
+	"path/filepath"
+	"strconv"
+
+	"android/soong/android"
+	"android/soong/testing/test_spec_proto"
+	"github.com/google/blueprint"
+	"google.golang.org/protobuf/proto"
+)
+
+// ErrTestModuleDataNotFound is the error message for missing test module provider data.
+const ErrTestModuleDataNotFound = "The module '%s' does not provide test specification data. Hint: This issue could arise if either the module is not a valid testing module or if it lacks the required 'TestModuleProviderKey' provider.\n"
+
+func TestSpecFactory() android.Module {
+	module := &TestSpecModule{}
+
+	android.InitAndroidModule(module)
+	android.InitDefaultableModule(module)
+	module.AddProperties(&module.properties)
+
+	return module
+}
+
+type TestSpecModule struct {
+	android.ModuleBase
+	android.DefaultableModuleBase
+	android.BazelModuleBase
+
+	// Properties for "test_spec"
+	properties struct {
+		// Specifies the name of the test config.
+		Name string
+		// Specifies the team ID.
+		TeamId string
+		// Specifies the list of tests covered under this module.
+		Tests []string
+	}
+}
+
+type testsDepTagType struct {
+	blueprint.BaseDependencyTag
+}
+
+var testsDepTag = testsDepTagType{}
+
+func (module *TestSpecModule) DepsMutator(ctx android.BottomUpMutatorContext) {
+	// Validate Properties
+	if len(module.properties.TeamId) == 0 {
+		ctx.PropertyErrorf("TeamId", "Team Id not found in the test_spec module. Hint: Maybe the TeamId property hasn't been properly specified.")
+	}
+	if !isInt(module.properties.TeamId) {
+		ctx.PropertyErrorf("TeamId", "Invalid value for Team ID. The Team ID must be an integer.")
+	}
+	if len(module.properties.Tests) == 0 {
+		ctx.PropertyErrorf("Tests", "Expected to attribute some test but none found. Hint: Maybe the test property hasn't been properly specified.")
+	}
+	ctx.AddDependency(ctx.Module(), testsDepTag, module.properties.Tests...)
+}
+func isInt(s string) bool {
+	_, err := strconv.Atoi(s)
+	return err == nil
+}
+
+// Provider published by TestSpec
+type testSpecProviderData struct {
+	IntermediatePath android.WritablePath
+}
+
+var testSpecProviderKey = blueprint.NewProvider(testSpecProviderData{})
+
+type TestModuleProviderData struct {
+}
+
+var TestModuleProviderKey = blueprint.NewProvider(TestModuleProviderData{})
+
+func (module *TestSpecModule) GenerateAndroidBuildActions(ctx android.ModuleContext) {
+	for _, m := range ctx.GetDirectDepsWithTag(testsDepTag) {
+		if !ctx.OtherModuleHasProvider(m, TestModuleProviderKey) {
+			ctx.ModuleErrorf(ErrTestModuleDataNotFound, m.Name())
+		}
+	}
+	bpFilePath := filepath.Join(ctx.ModuleDir(), ctx.BlueprintsFile())
+	metadataList := make(
+		[]*test_spec_proto.TestSpec_OwnershipMetadata, 0,
+		len(module.properties.Tests),
+	)
+	for _, test := range module.properties.Tests {
+		targetName := test
+		metadata := test_spec_proto.TestSpec_OwnershipMetadata{
+			TrendyTeamId: &module.properties.TeamId,
+			TargetName:   &targetName,
+			Path:         &bpFilePath,
+		}
+		metadataList = append(metadataList, &metadata)
+	}
+	intermediatePath := android.PathForModuleOut(
+		ctx, "intermediateTestSpecMetadata.pb",
+	)
+	testSpecMetadata := test_spec_proto.TestSpec{OwnershipMetadataList: metadataList}
+	protoData, err := proto.Marshal(&testSpecMetadata)
+	if err != nil {
+		ctx.ModuleErrorf("Error: %s", err.Error())
+	}
+	android.WriteFileRule(ctx, intermediatePath, string(protoData))
+
+	ctx.SetProvider(
+		testSpecProviderKey, testSpecProviderData{
+			IntermediatePath: intermediatePath,
+		},
+	)
+}
diff --git a/testing/test_spec_proto/Android.bp b/testing/test_spec_proto/Android.bp
new file mode 100644
index 0000000..1cac492
--- /dev/null
+++ b/testing/test_spec_proto/Android.bp
@@ -0,0 +1,29 @@
+// Copyright 2022 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 {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+bootstrap_go_package {
+    name: "soong-testing-test_spec_proto",
+    pkgPath: "android/soong/testing/test_spec_proto",
+    deps: [
+            "golang-protobuf-reflect-protoreflect",
+            "golang-protobuf-runtime-protoimpl",
+        ],
+    srcs: [
+        "test_spec.pb.go",
+    ],
+}
diff --git a/testing/test_spec_proto/OWNERS b/testing/test_spec_proto/OWNERS
new file mode 100644
index 0000000..03bcdf1
--- /dev/null
+++ b/testing/test_spec_proto/OWNERS
@@ -0,0 +1,4 @@
+dariofreni@google.com
+joeo@google.com
+ronish@google.com
+caditya@google.com
diff --git a/testing/test_spec_proto/go.mod b/testing/test_spec_proto/go.mod
new file mode 100644
index 0000000..482cdbb
--- /dev/null
+++ b/testing/test_spec_proto/go.mod
@@ -0,0 +1,2 @@
+module test_spec_proto
+go 1.18
\ No newline at end of file
diff --git a/testing/test_spec_proto/regen.sh b/testing/test_spec_proto/regen.sh
new file mode 100644
index 0000000..2cf8203
--- /dev/null
+++ b/testing/test_spec_proto/regen.sh
@@ -0,0 +1,3 @@
+#!/bin/bash
+
+aprotoc --go_out=paths=source_relative:. test_spec.proto
diff --git a/testing/test_spec_proto/test_spec.pb.go b/testing/test_spec_proto/test_spec.pb.go
new file mode 100644
index 0000000..5cce600
--- /dev/null
+++ b/testing/test_spec_proto/test_spec.pb.go
@@ -0,0 +1,244 @@
+// 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.
+
+// Code generated by protoc-gen-go. DO NOT EDIT.
+// versions:
+// 	protoc-gen-go v1.30.0
+// 	protoc        v3.21.12
+// source: test_spec.proto
+
+package test_spec_proto
+
+import (
+	protoreflect "google.golang.org/protobuf/reflect/protoreflect"
+	protoimpl "google.golang.org/protobuf/runtime/protoimpl"
+	reflect "reflect"
+	sync "sync"
+)
+
+const (
+	// Verify that this generated code is sufficiently up-to-date.
+	_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
+	// Verify that runtime/protoimpl is sufficiently up-to-date.
+	_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
+)
+
+type TestSpec struct {
+	state         protoimpl.MessageState
+	sizeCache     protoimpl.SizeCache
+	unknownFields protoimpl.UnknownFields
+
+	// List of all test targets and their metadata.
+	OwnershipMetadataList []*TestSpec_OwnershipMetadata `protobuf:"bytes,1,rep,name=ownership_metadata_list,json=ownershipMetadataList" json:"ownership_metadata_list,omitempty"`
+}
+
+func (x *TestSpec) Reset() {
+	*x = TestSpec{}
+	if protoimpl.UnsafeEnabled {
+		mi := &file_test_spec_proto_msgTypes[0]
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		ms.StoreMessageInfo(mi)
+	}
+}
+
+func (x *TestSpec) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*TestSpec) ProtoMessage() {}
+
+func (x *TestSpec) ProtoReflect() protoreflect.Message {
+	mi := &file_test_spec_proto_msgTypes[0]
+	if protoimpl.UnsafeEnabled && x != nil {
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		if ms.LoadMessageInfo() == nil {
+			ms.StoreMessageInfo(mi)
+		}
+		return ms
+	}
+	return mi.MessageOf(x)
+}
+
+// Deprecated: Use TestSpec.ProtoReflect.Descriptor instead.
+func (*TestSpec) Descriptor() ([]byte, []int) {
+	return file_test_spec_proto_rawDescGZIP(), []int{0}
+}
+
+func (x *TestSpec) GetOwnershipMetadataList() []*TestSpec_OwnershipMetadata {
+	if x != nil {
+		return x.OwnershipMetadataList
+	}
+	return nil
+}
+
+type TestSpec_OwnershipMetadata struct {
+	state         protoimpl.MessageState
+	sizeCache     protoimpl.SizeCache
+	unknownFields protoimpl.UnknownFields
+
+	TargetName   *string `protobuf:"bytes,1,opt,name=target_name,json=targetName" json:"target_name,omitempty"`
+	Path         *string `protobuf:"bytes,2,opt,name=path" json:"path,omitempty"`
+	TrendyTeamId *string `protobuf:"bytes,3,opt,name=trendy_team_id,json=trendyTeamId" json:"trendy_team_id,omitempty"`
+}
+
+func (x *TestSpec_OwnershipMetadata) Reset() {
+	*x = TestSpec_OwnershipMetadata{}
+	if protoimpl.UnsafeEnabled {
+		mi := &file_test_spec_proto_msgTypes[1]
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		ms.StoreMessageInfo(mi)
+	}
+}
+
+func (x *TestSpec_OwnershipMetadata) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*TestSpec_OwnershipMetadata) ProtoMessage() {}
+
+func (x *TestSpec_OwnershipMetadata) ProtoReflect() protoreflect.Message {
+	mi := &file_test_spec_proto_msgTypes[1]
+	if protoimpl.UnsafeEnabled && x != nil {
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		if ms.LoadMessageInfo() == nil {
+			ms.StoreMessageInfo(mi)
+		}
+		return ms
+	}
+	return mi.MessageOf(x)
+}
+
+// Deprecated: Use TestSpec_OwnershipMetadata.ProtoReflect.Descriptor instead.
+func (*TestSpec_OwnershipMetadata) Descriptor() ([]byte, []int) {
+	return file_test_spec_proto_rawDescGZIP(), []int{0, 0}
+}
+
+func (x *TestSpec_OwnershipMetadata) GetTargetName() string {
+	if x != nil && x.TargetName != nil {
+		return *x.TargetName
+	}
+	return ""
+}
+
+func (x *TestSpec_OwnershipMetadata) GetPath() string {
+	if x != nil && x.Path != nil {
+		return *x.Path
+	}
+	return ""
+}
+
+func (x *TestSpec_OwnershipMetadata) GetTrendyTeamId() string {
+	if x != nil && x.TrendyTeamId != nil {
+		return *x.TrendyTeamId
+	}
+	return ""
+}
+
+var File_test_spec_proto protoreflect.FileDescriptor
+
+var file_test_spec_proto_rawDesc = []byte{
+	0x0a, 0x0f, 0x74, 0x65, 0x73, 0x74, 0x5f, 0x73, 0x70, 0x65, 0x63, 0x2e, 0x70, 0x72, 0x6f, 0x74,
+	0x6f, 0x12, 0x0f, 0x74, 0x65, 0x73, 0x74, 0x5f, 0x73, 0x70, 0x65, 0x63, 0x5f, 0x70, 0x72, 0x6f,
+	0x74, 0x6f, 0x22, 0xdf, 0x01, 0x0a, 0x08, 0x54, 0x65, 0x73, 0x74, 0x53, 0x70, 0x65, 0x63, 0x12,
+	0x63, 0x0a, 0x17, 0x6f, 0x77, 0x6e, 0x65, 0x72, 0x73, 0x68, 0x69, 0x70, 0x5f, 0x6d, 0x65, 0x74,
+	0x61, 0x64, 0x61, 0x74, 0x61, 0x5f, 0x6c, 0x69, 0x73, 0x74, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b,
+	0x32, 0x2b, 0x2e, 0x74, 0x65, 0x73, 0x74, 0x5f, 0x73, 0x70, 0x65, 0x63, 0x5f, 0x70, 0x72, 0x6f,
+	0x74, 0x6f, 0x2e, 0x54, 0x65, 0x73, 0x74, 0x53, 0x70, 0x65, 0x63, 0x2e, 0x4f, 0x77, 0x6e, 0x65,
+	0x72, 0x73, 0x68, 0x69, 0x70, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x52, 0x15, 0x6f,
+	0x77, 0x6e, 0x65, 0x72, 0x73, 0x68, 0x69, 0x70, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61,
+	0x4c, 0x69, 0x73, 0x74, 0x1a, 0x6e, 0x0a, 0x11, 0x4f, 0x77, 0x6e, 0x65, 0x72, 0x73, 0x68, 0x69,
+	0x70, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x12, 0x1f, 0x0a, 0x0b, 0x74, 0x61, 0x72,
+	0x67, 0x65, 0x74, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a,
+	0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x70, 0x61,
+	0x74, 0x68, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x70, 0x61, 0x74, 0x68, 0x12, 0x24,
+	0x0a, 0x0e, 0x74, 0x72, 0x65, 0x6e, 0x64, 0x79, 0x5f, 0x74, 0x65, 0x61, 0x6d, 0x5f, 0x69, 0x64,
+	0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x74, 0x72, 0x65, 0x6e, 0x64, 0x79, 0x54, 0x65,
+	0x61, 0x6d, 0x49, 0x64, 0x42, 0x27, 0x5a, 0x25, 0x61, 0x6e, 0x64, 0x72, 0x6f, 0x69, 0x64, 0x2f,
+	0x73, 0x6f, 0x6f, 0x6e, 0x67, 0x2f, 0x74, 0x65, 0x73, 0x74, 0x69, 0x6e, 0x67, 0x2f, 0x74, 0x65,
+	0x73, 0x74, 0x5f, 0x73, 0x70, 0x65, 0x63, 0x5f, 0x70, 0x72, 0x6f, 0x74, 0x6f,
+}
+
+var (
+	file_test_spec_proto_rawDescOnce sync.Once
+	file_test_spec_proto_rawDescData = file_test_spec_proto_rawDesc
+)
+
+func file_test_spec_proto_rawDescGZIP() []byte {
+	file_test_spec_proto_rawDescOnce.Do(func() {
+		file_test_spec_proto_rawDescData = protoimpl.X.CompressGZIP(file_test_spec_proto_rawDescData)
+	})
+	return file_test_spec_proto_rawDescData
+}
+
+var file_test_spec_proto_msgTypes = make([]protoimpl.MessageInfo, 2)
+var file_test_spec_proto_goTypes = []interface{}{
+	(*TestSpec)(nil),                   // 0: test_spec_proto.TestSpec
+	(*TestSpec_OwnershipMetadata)(nil), // 1: test_spec_proto.TestSpec.OwnershipMetadata
+}
+var file_test_spec_proto_depIdxs = []int32{
+	1, // 0: test_spec_proto.TestSpec.ownership_metadata_list:type_name -> test_spec_proto.TestSpec.OwnershipMetadata
+	1, // [1:1] is the sub-list for method output_type
+	1, // [1:1] is the sub-list for method input_type
+	1, // [1:1] is the sub-list for extension type_name
+	1, // [1:1] is the sub-list for extension extendee
+	0, // [0:1] is the sub-list for field type_name
+}
+
+func init() { file_test_spec_proto_init() }
+func file_test_spec_proto_init() {
+	if File_test_spec_proto != nil {
+		return
+	}
+	if !protoimpl.UnsafeEnabled {
+		file_test_spec_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} {
+			switch v := v.(*TestSpec); i {
+			case 0:
+				return &v.state
+			case 1:
+				return &v.sizeCache
+			case 2:
+				return &v.unknownFields
+			default:
+				return nil
+			}
+		}
+		file_test_spec_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} {
+			switch v := v.(*TestSpec_OwnershipMetadata); i {
+			case 0:
+				return &v.state
+			case 1:
+				return &v.sizeCache
+			case 2:
+				return &v.unknownFields
+			default:
+				return nil
+			}
+		}
+	}
+	type x struct{}
+	out := protoimpl.TypeBuilder{
+		File: protoimpl.DescBuilder{
+			GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
+			RawDescriptor: file_test_spec_proto_rawDesc,
+			NumEnums:      0,
+			NumMessages:   2,
+			NumExtensions: 0,
+			NumServices:   0,
+		},
+		GoTypes:           file_test_spec_proto_goTypes,
+		DependencyIndexes: file_test_spec_proto_depIdxs,
+		MessageInfos:      file_test_spec_proto_msgTypes,
+	}.Build()
+	File_test_spec_proto = out.File
+	file_test_spec_proto_rawDesc = nil
+	file_test_spec_proto_goTypes = nil
+	file_test_spec_proto_depIdxs = nil
+}
diff --git a/testing/test_spec_proto/test_spec.proto b/testing/test_spec_proto/test_spec.proto
new file mode 100644
index 0000000..86bc789
--- /dev/null
+++ b/testing/test_spec_proto/test_spec.proto
@@ -0,0 +1,33 @@
+// 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.
+
+syntax = "proto2";
+package test_spec_proto;
+option go_package = "android/soong/testing/test_spec_proto";
+
+message TestSpec {
+
+  message OwnershipMetadata {
+    // REQUIRED: Name of the build target
+    optional string target_name = 1;
+
+    // REQUIRED: Code location of the target.
+    // To be used to support legacy/backup systems that use OWNERS file and is
+    // also required for our dashboard to support per code location basis UI
+    optional string path = 2;
+
+    // REQUIRED: Team ID of the team that owns this target.
+    optional string trendy_team_id = 3;
+  }
+
+  // List of all test targets and their metadata.
+  repeated OwnershipMetadata ownership_metadata_list = 1;
+}