mongo/evergreen/functions/code_coverage_data_process.py

195 lines
6.7 KiB
Python
Executable File

import glob
import os
import platform
import subprocess
import sys
import requests
import retry
from buildscripts.util.expansions import get_expansion
# This script is used to gather code coverage data from the build and post it to coveralls.
# It is run as part of the Evergreen build process.
# It is not intended to be run directly.
@retry.retry(tries=3, delay=5)
def retry_coveralls_report(args: list, env: dict[str, str]):
print("Running coveralls report...")
subprocess.run(args, env=env, check=True, encoding="utf-8")
@retry.retry(tries=3, delay=5)
def retry_coveralls_post(coveralls_report: str):
print("Posting to coveralls")
files = {"json_file": open(coveralls_report, "rb")}
response = requests.post("https://coveralls.io/api/v1/jobs", files=files)
print(response.text)
if not response.ok:
raise RuntimeError(f"Error while sending coveralls report: {response.status_code}")
def get_bazel_coverage_report_file() -> str:
BAZEL_BINARY = "bazel" # simplified since we already restrict which platforms can run coverage
proc = subprocess.run([BAZEL_BINARY, "info", "output_path"], check=True, capture_output=True)
bazel_output_location = proc.stdout.decode("utf-8").strip()
bazel_coverage_report_location = os.path.join(
bazel_output_location, "_coverage", "_coverage_report.dat"
)
return bazel_coverage_report_location
def main():
should_gather_code_coverage = get_expansion("gather_code_coverage_results", False)
if not should_gather_code_coverage:
print("Missing 'gather_code_coverage_results' expansion, skipping code coverage.")
return 0
gcov_binary = get_expansion("gcov_tool", None)
if not gcov_binary:
print(
"Missing 'gcov_tool' expansion, skipping code coverage because this is likely not a code coverage variant."
)
return 0
disallowed_arches = {"s390x", "s390", "ppc64le", "ppc64", "ppc", "ppcle"}
arch = platform.uname().machine.lower()
print(f"Detected arch: {arch}")
if arch in disallowed_arches:
raise RuntimeError(f"Code coverage not supported on architecture '{arch}'.")
if not os.path.exists(".git"):
raise RuntimeError(
"No git repo found in working directory. Code coverage needs git repo to function."
)
coveralls_token = get_expansion("coveralls_token")
assert coveralls_token is not None, "Coveralls token was not found"
github_pr_number = get_expansion("github_pr_number", "")
revision_order_id = get_expansion("revision_order_id")
# this keeps coverage reports consistent across evergreen tasks and merge queue maneuvers
github_commit = get_expansion("github_commit")
bazel_coverage_report_location = get_bazel_coverage_report_file()
if os.path.exists(bazel_coverage_report_location):
print("Found bazel coverage report.")
version_id = get_expansion("version_id")
task_id = get_expansion("task_id")
args = [
"coveralls",
"report",
bazel_coverage_report_location,
"--service-name=travis-ci",
f"--repo-token={coveralls_token}",
f"--job-id={revision_order_id}",
f"--build-url=https://spruce.mongodb.com/version/{version_id}/",
f"--job-url=https://spruce.mongodb.com/task/{task_id}/",
]
if github_pr_number:
args.append(
f"--pull-request={github_pr_number}",
)
my_env = os.environ.copy()
my_env["COVERALLS_GIT_COMMIT"] = github_commit
retry_coveralls_report(args, my_env)
# no gcda files are generated from bazel coverage so we can exit early here
return 0
print(f"No bazel coverage report found at {bazel_coverage_report_location}")
# because of bazel symlink shenanigans, the bazel gcda and gcno files are put in different
# directories when the GCOV_PREFIX and GCOV_PREFIX_STRIP env vars are used. We manually
# put the gcno files where the gcda files are generated to fix this.
has_bazel_gcno = False
workdir = get_expansion("workdir")
bazel_output_dir = os.path.join(workdir, "bazel-out")
for file in glob.iglob("./**/bazel-out/**/*.gcno", root_dir=workdir, recursive=True):
has_bazel_gcno = True
parts = file.split("bazel-out/")
assert len(parts) == 2, "Something went wrong, path was not split into 2 parts."
old_path = os.path.join(workdir, file)
new_path = os.path.join(bazel_output_dir, parts[1])
new_dir = os.path.dirname(new_path)
os.makedirs(new_dir, exist_ok=True)
os.rename(old_path, new_path)
if not has_bazel_gcno:
raise RuntimeError("Neither bazel coverage nor gcno files were found.")
my_env = os.environ.copy()
my_env["COVERALLS_REPO_TOKEN"] = coveralls_token
my_env["TRAVIS_PULL_REQUEST"] = github_pr_number
my_env["TRAVIS_JOB_ID"] = revision_order_id
my_env["TRAVIS_COMMIT"] = github_commit
coveralls_report = "gcovr-coveralls.json"
args = [
"python3",
"-m",
"gcovr",
"--output",
coveralls_report,
"--coveralls-pretty",
"--txt",
"gcovr-coveralls.txt",
"--print-summary",
"--exclude",
"build/debug/.*",
"--exclude",
".*bazel-out/.*",
"--exclude",
".*external/mongo_toolchain/.*",
"--exclude",
".*src/.*_gen\.(h|hpp|cpp)",
"--exclude",
".*src/mongo/db/cst/grammar\.yy",
"--exclude",
".*src/mongo/idl/.*",
"--exclude",
".*src/mongo/.*_test\.(h|hpp|cpp)",
"--exclude",
".*src/mongo/dbtests/.*",
"--exclude",
".*src/mongo/unittest/.*",
"--exclude",
".*/third_party/.*",
"--gcov-ignore-errors",
"source_not_found",
"--gcov-ignore-parse-errors",
"negative_hits.warn",
"--gcov-exclude-directories",
".*src/mongo/dbtests/.*",
"--gcov-exclude-directories",
".*src/mongo/idl/.*",
"--gcov-exclude-directories",
".*src/mongo/unittest/.*",
"--gcov-exclude-directories",
".*/third_party/.*",
"--gcov-executable",
gcov_binary,
bazel_output_dir,
]
print("Running gcovr command")
process = subprocess.run(
args, env=my_env, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, encoding="utf-8"
)
print(process.stdout)
if process.returncode != 0:
raise RuntimeError(f"gcovr failed with code: {process.returncode}")
if not os.path.exists(coveralls_report):
raise RuntimeError(f"Could not find coveralls json report at {coveralls_report}")
retry_coveralls_post(coveralls_report)
return 0
if __name__ == "__main__":
sys.exit(main())