diff --git a/tools/metadata/Android.bp b/tools/metadata/Android.bp
new file mode 100644
index 0000000000..b2fabecb96
--- /dev/null
+++ b/tools/metadata/Android.bp
@@ -0,0 +1,14 @@
+package {
+ default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+blueprint_go_binary {
+ name: "metadata",
+ deps: [
+ "soong-testing-test_spec_proto",
+ "golang-protobuf-proto",
+ ],
+ srcs: [
+ "generator.go",
+ ]
+}
\ No newline at end of file
diff --git a/tools/metadata/generator.go b/tools/metadata/generator.go
new file mode 100644
index 0000000000..8e82f7fc32
--- /dev/null
+++ b/tools/metadata/generator.go
@@ -0,0 +1,169 @@
+package main
+
+import (
+ "flag"
+ "fmt"
+ "io"
+ "log"
+ "os"
+ "sort"
+ "strings"
+ "sync"
+
+ "android/soong/testing/test_spec_proto"
+ "google.golang.org/protobuf/proto"
+)
+
+type keyToLocksMap struct {
+ locks sync.Map
+}
+
+func (kl *keyToLocksMap) GetLockForKey(key string) *sync.Mutex {
+ mutex, _ := kl.locks.LoadOrStore(key, &sync.Mutex{})
+ return mutex.(*sync.Mutex)
+}
+
+func getSortedKeys(syncMap *sync.Map) []string {
+ var allKeys []string
+ syncMap.Range(
+ func(key, _ interface{}) bool {
+ allKeys = append(allKeys, key.(string))
+ return true
+ },
+ )
+
+ sort.Strings(allKeys)
+ return allKeys
+}
+
+func writeOutput(
+ outputFile string,
+ allMetadata []*test_spec_proto.TestSpec_OwnershipMetadata,
+) {
+ testSpec := &test_spec_proto.TestSpec{
+ OwnershipMetadataList: allMetadata,
+ }
+ data, err := proto.Marshal(testSpec)
+ if err != nil {
+ log.Fatal(err)
+ }
+ file, err := os.Create(outputFile)
+ if err != nil {
+ log.Fatal(err)
+ }
+ defer file.Close()
+
+ _, err = file.Write(data)
+ if err != nil {
+ log.Fatal(err)
+ }
+}
+
+func readFileToString(filePath string) string {
+ file, err := os.Open(filePath)
+ if err != nil {
+ log.Fatal(err)
+ }
+ defer file.Close()
+
+ data, err := io.ReadAll(file)
+ if err != nil {
+ log.Fatal(err)
+ }
+ return string(data)
+}
+
+func processProtobuf(
+ filePath string, ownershipMetadataMap *sync.Map, keyLocks *keyToLocksMap,
+ errCh chan error, wg *sync.WaitGroup,
+) {
+ defer wg.Done()
+
+ fileContent := strings.TrimRight(readFileToString(filePath), "\n")
+ testData := test_spec_proto.TestSpec{}
+ err := proto.Unmarshal([]byte(fileContent), &testData)
+ if err != nil {
+ errCh <- err
+ return
+ }
+
+ ownershipMetadata := testData.GetOwnershipMetadataList()
+ for _, metadata := range ownershipMetadata {
+ key := metadata.GetTargetName()
+ lock := keyLocks.GetLockForKey(key)
+ lock.Lock()
+
+ value, loaded := ownershipMetadataMap.LoadOrStore(
+ key, []*test_spec_proto.TestSpec_OwnershipMetadata{metadata},
+ )
+ if loaded {
+ existingMetadata := value.([]*test_spec_proto.TestSpec_OwnershipMetadata)
+ isDuplicate := false
+ for _, existing := range existingMetadata {
+ if metadata.GetTrendyTeamId() != existing.GetTrendyTeamId() {
+ errCh <- fmt.Errorf(
+ "Conflicting trendy team IDs found for %s at:\n%s with teamId"+
+ ": %s,\n%s with teamId: %s",
+ key,
+ metadata.GetPath(), metadata.GetTrendyTeamId(), existing.GetPath(),
+ existing.GetTrendyTeamId(),
+ )
+
+ lock.Unlock()
+ return
+ }
+ if metadata.GetTrendyTeamId() == existing.GetTrendyTeamId() && metadata.GetPath() == existing.GetPath() {
+ isDuplicate = true
+ break
+ }
+ }
+ if !isDuplicate {
+ existingMetadata = append(existingMetadata, metadata)
+ ownershipMetadataMap.Store(key, existingMetadata)
+ }
+ }
+
+ lock.Unlock()
+ }
+}
+
+func main() {
+ inputFile := flag.String("inputFile", "", "Input file path")
+ outputFile := flag.String("outputFile", "", "Output file path")
+ flag.Parse()
+
+ if *inputFile == "" || *outputFile == "" {
+ fmt.Println("Usage: metadata -inputFile -outputFile