[clang] Add resultdb support

This patch adds resultDB support for clang base bots.

Bug: 335328196
Change-Id: If55b6e1e5c26d3d0e375a89a89e7489c4c068902
Reviewed-on: https://fuchsia-review.googlesource.com/c/infra/recipes/+/1069010
Commit-Queue: Haowei Wu <haowei@google.com>
Reviewed-by: Paul Kirth <paulkirth@google.com>
diff --git a/recipes/contrib/clang.expected/ci_linux_x64.json b/recipes/contrib/clang.expected/ci_linux_x64.json
index bbef6cd..049ed4f 100644
--- a/recipes/contrib/clang.expected/ci_linux_x64.json
+++ b/recipes/contrib/clang.expected/ci_linux_x64.json
@@ -7982,6 +7982,7 @@
       "-DLLVM_OVERRIDE_MODEL_HEADER_INLINERSIZEMODEL=[START_DIR]/cipd/model/InlinerSizeModel.h",
       "-DLLVM_OVERRIDE_MODEL_OBJECT_INLINERSIZEMODEL=[START_DIR]/cipd/model/InlinerSizeModel.o",
       "-DLLVM_RAEVICT_MODEL_PATH=none",
+      "-DLLVM_LIT_ARGS=--resultdb-output=r.j -v",
       "-C",
       "RECIPE[fuchsia::contrib/clang].resources/Fuchsia-toolchain.cmake"
     ],
@@ -8396,6 +8397,93 @@
     ]
   },
   {
+    "cmd": [
+      "vpython3",
+      "-u",
+      "RECIPE_MODULE[recipe_engine::file]/resources/fileutil.py",
+      "--json-output",
+      "/path/to/tmp/json",
+      "glob",
+      "[START_DIR]/llvm_build",
+      "**/r.j"
+    ],
+    "env": {
+      "CLANG_MODULE_CACHE_PATH": "",
+      "GOMA_TMP_DIR": "[CLEANUP]/goma",
+      "GOMA_USE_LOCAL": "False"
+    },
+    "env_prefixes": {
+      "PATH": [
+        "[START_DIR]/cipd/bin"
+      ]
+    },
+    "infra_step": true,
+    "luci_context": {
+      "realm": {
+        "name": "fuchsia:ci"
+      },
+      "resultdb": {
+        "current_invocation": {
+          "name": "invocations/build:8945511751514863184",
+          "update_token": "token"
+        },
+        "hostname": "rdbhost"
+      }
+    },
+    "name": "clang.collect results.json",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@1@@@",
+      "@@@STEP_LOG_LINE@glob@[START_DIR]/llvm_build/[START_DIR]/llvm_build/r.j@@@",
+      "@@@STEP_LOG_LINE@glob@[START_DIR]/llvm_build/[START_DIR]/llvm_build/test/r.j@@@",
+      "@@@STEP_LOG_END@glob@@@"
+    ]
+  },
+  {
+    "cmd": [
+      "rdb",
+      "stream",
+      "-var",
+      "bucket:ci",
+      "-var",
+      "builder:builder",
+      "-new",
+      "-realm",
+      "fuchsia:ci",
+      "-include",
+      "--",
+      "vpython3",
+      "RECIPE[fuchsia::contrib/clang].resources/resultdb.py",
+      "--json=[START_DIR]/llvm_build/[START_DIR]/llvm_build/r.j",
+      "--json=[START_DIR]/llvm_build/[START_DIR]/llvm_build/test/r.j"
+    ],
+    "env": {
+      "CLANG_MODULE_CACHE_PATH": "",
+      "GOMA_TMP_DIR": "[CLEANUP]/goma",
+      "GOMA_USE_LOCAL": "False"
+    },
+    "env_prefixes": {
+      "PATH": [
+        "[START_DIR]/cipd/bin"
+      ]
+    },
+    "luci_context": {
+      "realm": {
+        "name": "fuchsia:ci"
+      },
+      "resultdb": {
+        "current_invocation": {
+          "name": "invocations/build:8945511751514863184",
+          "update_token": "token"
+        },
+        "hostname": "rdbhost"
+      }
+    },
+    "name": "clang.resultdb",
+    "~followup_annotations": [
+      "@@@STEP_NEST_LEVEL@1@@@"
+    ]
+  },
+  {
     "cmd": [],
     "name": "teardown goma"
   },
diff --git a/recipes/contrib/clang.py b/recipes/contrib/clang.py
index 8dd5f20..cf76320 100644
--- a/recipes/contrib/clang.py
+++ b/recipes/contrib/clang.py
@@ -3,6 +3,8 @@
 # found in the LICENSE file.
 """Recipe for building Clang toolchain without any runtimes."""
 
+import contextlib
+
 from PB.go.chromium.org.luci.common.proto.srcman.manifest import Manifest
 from PB.recipes.fuchsia.contrib.clang import InputProperties
 
@@ -25,6 +27,7 @@
     "recipe_engine/path",
     "recipe_engine/platform",
     "recipe_engine/properties",
+    "recipe_engine/resultdb",
     "recipe_engine/step",
 ]
 
@@ -32,6 +35,10 @@
 
 CIPD_SERVER_HOST = "chrome-infra-packages.appspot.com"
 
+# TODD(fxbug.dev/91157): Restore the file name once
+# path length issue is properly fixed.
+RESULTDB_JSON = "r.j"
+
 
 def RunSteps(api, props):
     # default values
@@ -163,6 +170,8 @@
                 ]
             )
 
+        options.append(f"-DLLVM_LIT_ARGS=--resultdb-output={RESULTDB_JSON} -v")
+
         with api.step.nest("clang"), api.context(env=env):
             api.cmake(
                 step_name="configure",
@@ -197,16 +206,51 @@
                     pkg_dir.joinpath("bin", hdrgen),
                 )
             api.cas_util.upload(pkg_dir, output_property="isolated")
-            api.ninja(
-                "tests",
-                [
-                    "check-clang",
-                    "check-llvm",
-                    "check-lld",
-                ],
-                ninja_jobs=ninja_jobs,
-                build_dir=build_dir,
-            )
+            with resultdb_context(api, build_dir):
+                api.ninja(
+                    "tests",
+                    [
+                        "check-clang",
+                        "check-llvm",
+                        "check-lld",
+                    ],
+                    ninja_jobs=ninja_jobs,
+                    build_dir=build_dir,
+                )
+
+
+@contextlib.contextmanager
+def resultdb_context(api, build_dir):
+    try:
+        yield
+    finally:
+        upload_to_resultdb(api, build_dir)
+
+
+def upload_to_resultdb(api, build_dir):
+    if not api.resultdb.enabled:
+        return  # pragma: no cover
+    results_paths = api.file.glob_paths(
+        "collect results.json",
+        build_dir,
+        "**/%s" % RESULTDB_JSON,
+        test_data=[
+            build_dir.joinpath(RESULTDB_JSON),
+            build_dir.joinpath("test", RESULTDB_JSON),
+        ],
+    )
+    if not results_paths:
+        return  # pragma: no cover
+    resultdb_base_variant = {
+        "bucket": api.buildbucket.build.builder.bucket,
+        "builder": api.buildbucket.build.builder.builder,
+    }
+    cmd = ["vpython3", api.resource("resultdb.py")]
+    cmd.extend(f"--json={p}" for p in results_paths)
+    api.step(
+        "resultdb",
+        api.resultdb.wrap(cmd, base_variant=resultdb_base_variant.copy(), include=True),
+    )
 
 
 def ensure_llvm_deps_prebuilts(
diff --git a/recipes/contrib/clang.resources/resultdb.py b/recipes/contrib/clang.resources/resultdb.py
new file mode 100755
index 0000000..9f38677
--- /dev/null
+++ b/recipes/contrib/clang.resources/resultdb.py
@@ -0,0 +1,94 @@
+#!/usr/bin/env vpython3
+
+# Copyright 2021 The Fuchsia Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+# [VPYTHON:BEGIN]
+# python_version: "3.8"
+# wheel: <
+#   name: "infra/python/wheels/idna-py2_py3"
+#   version: "version:2.10"
+# >
+# wheel: <
+#   name: "infra/python/wheels/urllib3-py2_py3"
+#   version: "version:1.26.4"
+# >
+# wheel: <
+#   name: "infra/python/wheels/certifi-py2_py3"
+#   version: "version:2020.12.5"
+# >
+# wheel: <
+#   name: "infra/python/wheels/chardet-py2_py3"
+#   version: "version:4.0.0"
+# >
+# wheel: <
+#   name: "infra/python/wheels/requests-py2_py3"
+#   version: "version:2.25.1"
+# >
+# [VPYTHON:END]
+
+"""Read JSON output from llvm-lit and upload the data to resultDB.
+
+Usage:
+  rdb [rdb flags] -- resultdb.py --json=JSON_FILE
+"""
+
+import json
+import argparse
+import os
+import sys
+import requests
+
+
+def read_jsons(json_files):
+    test_results = []
+    for json_file in json_files:
+        with open(json_file, encoding="utf-8") as f:
+            data = json.load(f)
+        test_results.extend(data["tests"])
+    return test_results
+
+
+def upload_results(test_results, url, auth_token):
+    res = requests.post(
+        url,
+        headers={
+            "Content-Type": "application/json",
+            "Accept": "application/json",
+            "Authorization": "ResultSink %s" % auth_token,
+        },
+        data=json.dumps({"testResults": test_results}),
+    )
+    res.raise_for_status()
+
+
+def main():
+    parser = argparse.ArgumentParser()
+    parser.add_argument("--json", action="append", help="json output from llvm-lit")
+    args = parser.parse_args()
+    if not args.json:
+        print("Please specify the lit json file(s).")
+        return 1
+    sink = None
+    if "LUCI_CONTEXT" in os.environ:
+        with open(os.environ["LUCI_CONTEXT"], encoding="utf-8") as f:
+            sink = json.load(f)["result_sink"]
+    if sink is None:
+        print("result_sink not defined")
+        return 1
+    test_results = read_jsons(args.json)
+    if not test_results:
+        print("Empty test results: skipping")
+        return 0
+    url = str.format(
+        "http://{}/prpc/luci.resultsink.v1.Sink/{}",
+        sink["address"],
+        "ReportTestResults",
+    )
+    upload_results(test_results, url, sink["auth_token"])
+    return 0
+
+
+if __name__ == "__main__":
+    sys.exit(main())